From 2d1f6d4b703a1533ada2fb2f1b0b5cce6fb07a49 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Wed, 19 Aug 2026 21:22:37 -0400 Subject: [PATCH 1/4] feat(mcp): standardize project resolution context --- .changeset/steady-mcp-context.md | 8 + docs/mcp/configuration.md | 21 +- docs/mcp/figma-tools-setup.md | 62 - docs/mcp/index.md | 4 - docs/mcp/installation.md | 2 - docs/mcp/tools/cartridge-deploy.md | 20 +- docs/mcp/tools/diagnostics.md | 5 +- docs/mcp/tools/logs.md | 16 +- docs/mcp/tools/mrt-bundle-push.md | 5 +- docs/mcp/tools/scapi-custom-apis.md | 24 +- docs/mcp/tools/scapi-schemas-list.md | 19 +- .../sfnext-add-page-designer-decorator.md | 120 -- docs/mcp/tools/sfnext-analyze-component.md | 98 - docs/mcp/tools/sfnext-configure-theme.md | 131 -- docs/mcp/tools/sfnext-get-guidelines.md | 107 -- .../mcp/tools/sfnext-match-tokens-to-theme.md | 85 - docs/mcp/tools/sfnext-start-figma-workflow.md | 92 - docs/mcp/toolsets.md | 22 +- packages/b2c-dx-mcp/CONTRIBUTING.md | 31 +- packages/b2c-dx-mcp/README.md | 77 +- packages/b2c-dx-mcp/content/sfnext/auth.md | 62 - .../b2c-dx-mcp/content/sfnext/components.md | 123 -- packages/b2c-dx-mcp/content/sfnext/config.md | 180 -- .../content/sfnext/data-fetching.md | 323 ---- .../b2c-dx-mcp/content/sfnext/extensions.md | 80 - packages/b2c-dx-mcp/content/sfnext/i18n.md | 121 -- .../content/sfnext/page-designer.md | 78 - .../b2c-dx-mcp/content/sfnext/performance.md | 80 - .../b2c-dx-mcp/content/sfnext/pitfalls.md | 141 -- .../content/sfnext/quick-reference.md | 226 --- .../content/sfnext/state-management.md | 75 - packages/b2c-dx-mcp/content/sfnext/styling.md | 51 - packages/b2c-dx-mcp/content/sfnext/testing.md | 232 --- packages/b2c-dx-mcp/src/commands/mcp.ts | 61 +- packages/b2c-dx-mcp/src/registry.ts | 30 +- packages/b2c-dx-mcp/src/services.ts | 56 + packages/b2c-dx-mcp/src/tools/adapter.ts | 99 +- .../b2c-dx-mcp/src/tools/cartridges/index.ts | 20 +- .../src/tools/diagnostics/config-inspect.ts | 4 +- .../tools/diagnostics/debug-list-sessions.ts | 3 + .../tools/diagnostics/debug-start-session.ts | 15 +- .../tools/diagnostics/log-watch-registry.ts | 4 + .../src/tools/diagnostics/logs-watch-list.ts | 3 + .../src/tools/diagnostics/logs-watch-start.ts | 7 +- .../diagnostics/mrt-log-watch-registry.ts | Bin 10781 -> 10947 bytes .../tools/diagnostics/mrt-logs-watch-list.ts | 3 + .../tools/diagnostics/mrt-logs-watch-start.ts | 8 +- .../src/tools/diagnostics/session-registry.ts | 6 +- packages/b2c-dx-mcp/src/tools/index.ts | 1 - packages/b2c-dx-mcp/src/tools/mrt/index.ts | 4 + .../b2c-dx-mcp/src/tools/project-context.ts | 130 +- .../b2c-dx-mcp/src/tools/scapi/metrics-get.ts | 2 +- .../scapi-custom-api-generate-scaffold.ts | 43 +- .../scapi/scapi-custom-apis-get-status.ts | 2 +- .../src/tools/scapi/scapi-schemas-list.ts | 1 + .../src/tools/storefrontnext/README.md | 265 --- .../figma-to-component/figma-url-parser.ts | 71 - .../figma/figma-to-component/index.ts | 369 ---- .../figma/generate-component/decision.ts | 408 ----- .../figma/generate-component/formatter.ts | 109 -- .../figma/generate-component/index.ts | 163 -- .../figma/map-tokens/css-parser.ts | 337 ---- .../storefrontnext/figma/map-tokens/index.ts | 289 --- .../figma/map-tokens/token-matcher.ts | 366 ---- .../src/tools/storefrontnext/index.ts | 53 - .../page-designer-decorator/README.md | 262 --- .../page-designer-decorator/analyzer.ts | 672 ------- .../page-designer-decorator/index.ts | 740 -------- .../page-designer-decorator/rules.ts | 87 - .../rules/1-mode-selection.ts | 73 - .../rules/2a-auto-mode.ts | 104 -- .../rules/2b-0-interactive-overview.ts | 55 - .../rules/2b-1-interactive-analyze.ts | 144 -- .../rules/2b-2-interactive-select-props.ts | 86 - .../rules/2b-3-interactive-configure-attrs.ts | 101 - .../2b-4-interactive-configure-regions.ts | 69 - .../2b-5-interactive-confirm-generation.ts | 103 -- .../templates/decorator-generator.ts | 413 ----- .../sfnext-development-guidelines.ts | 176 -- .../storefrontnext/site-theming/README.md | 179 -- .../site-theming/color-contrast.ts | 236 --- .../site-theming/color-mapping.ts | 157 -- .../site-theming/guidance-merger.ts | 86 - .../storefrontnext/site-theming/index.ts | 158 -- .../site-theming/response-builder.ts | 351 ---- .../site-theming/theming-store.ts | 572 ------ .../storefrontnext/site-theming/types.ts | 48 - packages/b2c-dx-mcp/src/utils/constants.ts | 22 +- packages/b2c-dx-mcp/test/commands/mcp.test.ts | 39 + packages/b2c-dx-mcp/test/e2e/mcp-e2e.test.ts | 31 +- packages/b2c-dx-mcp/test/registry.test.ts | 125 +- .../b2c-dx-mcp/test/tools/adapter.test.ts | 68 +- .../tools/diagnostics/debug-tools.test.ts | 11 + .../test/tools/diagnostics/logs-tools.test.ts | 8 +- .../tools/diagnostics/mrt-logs-tools.test.ts | 9 +- .../scapi-custom-apis-get-status.test.ts | 1 + .../test/tools/storefrontnext/figma/README.md | 248 --- .../figma-url-parser.test.ts | 60 - .../figma/figma-to-component/index.test.ts | 206 --- .../generate-component/formatter.test.ts | 268 --- .../figma/generate-component/index.test.ts | 311 ---- .../figma/map-tokens/css-parser.test.ts | 381 ---- .../figma/map-tokens/index.test.ts | 412 ----- .../figma/map-tokens/token-matcher.test.ts | 565 ------ .../figma/test-fixtures/workflow-custom.md | 11 - .../test-fixtures/workflow-no-metadata.md | 7 - .../page-designer-decorator/README.md | 155 -- .../page-designer-decorator/index.test.ts | 1630 ----------------- .../sfnext-development-guidelines.test.ts | 651 ------- .../storefrontnext/site-theming/README.md | 178 -- .../site-theming/color-contrast.test.ts | 298 --- .../site-theming/color-mapping.test.ts | 140 -- .../site-theming/guidance-merger.test.ts | 97 - .../storefrontnext/site-theming/index.test.ts | 410 ----- .../site-theming/response-builder.test.ts | 319 ---- .../site-theming/theming-store.test.ts | 624 ------- .../b2c-tooling-sdk/data/tooling/index.json | 67 +- skills/b2c-cli/skills/b2c-config/SKILL.md | 9 +- skills/b2c-cli/skills/b2c-debug/SKILL.md | 2 +- .../references/PAGE-DESIGNER-SFN.md | 35 +- 120 files changed, 704 insertions(+), 17219 deletions(-) create mode 100644 .changeset/steady-mcp-context.md delete mode 100644 docs/mcp/figma-tools-setup.md delete mode 100644 docs/mcp/tools/sfnext-add-page-designer-decorator.md delete mode 100644 docs/mcp/tools/sfnext-analyze-component.md delete mode 100644 docs/mcp/tools/sfnext-configure-theme.md delete mode 100644 docs/mcp/tools/sfnext-get-guidelines.md delete mode 100644 docs/mcp/tools/sfnext-match-tokens-to-theme.md delete mode 100644 docs/mcp/tools/sfnext-start-figma-workflow.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/auth.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/components.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/config.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/data-fetching.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/extensions.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/i18n.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/page-designer.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/performance.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/pitfalls.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/quick-reference.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/state-management.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/styling.md delete mode 100644 packages/b2c-dx-mcp/content/sfnext/testing.md delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/README.md delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/figma-url-parser.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/index.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/decision.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/formatter.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/index.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/css-parser.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/index.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/token-matcher.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/README.md delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/analyzer.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/index.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/1-mode-selection.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2a-auto-mode.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-0-interactive-overview.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-1-interactive-analyze.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-2-interactive-select-props.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/templates/decorator-generator.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/sfnext-development-guidelines.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/README.md delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-contrast.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-mapping.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/guidance-merger.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/index.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/response-builder.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts delete mode 100644 packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/types.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/README.md delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/figma-url-parser.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/index.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/formatter.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/index.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/css-parser.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/index.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/token-matcher.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-custom.md delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-no-metadata.md delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/README.md delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/index.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/sfnext-development-guidelines.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/README.md delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-contrast.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-mapping.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/guidance-merger.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/index.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/response-builder.test.ts delete mode 100644 packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/theming-store.test.ts diff --git a/.changeset/steady-mcp-context.md b/.changeset/steady-mcp-context.md new file mode 100644 index 000000000..a23f08176 --- /dev/null +++ b/.changeset/steady-mcp-context.md @@ -0,0 +1,8 @@ +--- +'@salesforce/b2c-dx-mcp': minor +'@salesforce/b2c-dx-docs': patch +'@salesforce/b2c-agent-plugins': patch +'@salesforce/b2c-tooling-sdk': patch +--- + +Added per-call named-instance selection and consistent resolution provenance to project-aware MCP tools, including persisted context for debugger sessions and log watches. Removed the retired Storefront Next MCP toolset in favor of the current Storefront Next agent skills. diff --git a/docs/mcp/configuration.md b/docs/mcp/configuration.md index c4a8195b8..05c429569 100644 --- a/docs/mcp/configuration.md +++ b/docs/mcp/configuration.md @@ -84,12 +84,13 @@ If both `dw.json` and `~/.mobify` contain an API key, `dw.json` takes precedence ## Per-call Project Context {#project-directory} -Tools that resolve project files or B2C/MRT configuration expose two common per-call arguments: +Tools expose only the context they consume. Local project tools accept `projectDirectory`. Tools that resolve B2C/MRT configuration use the same three flat, optional arguments: -- `projectDirectory` selects the project root used for `.env`, `dw.json`, `package.json`, and relative project files. -- `configPath` explicitly selects a configuration file in `dw.json` format. Relative paths resolve from the effective `projectDirectory`. +- `projectDirectory` is an absolute project root for the call. It overrides the server-level project directory; the tool schema shows the exact server-level or `cwd` fallback it will use when omitted. +- `configPath` selects the primary configuration file in `dw.json` format. Relative paths resolve from `projectDirectory`. The shared default `dw.json` remains available for fallback and named-instance lookup. +- `instanceName` selects a named instance from the primary and shared default files without changing either file. The primary file is searched first. When omitted, the active/default instance is used. -Configuration-dependent tools receive these fields automatically. Pure documentation tools and follow-up calls that operate only on existing server-side state do not expose them. +Specialized roots such as `cartridgeDirectory`, `buildDirectory`, and `outputDirectory` remain separate and resolve from `projectDirectory` when relative. Pure documentation tools and follow-up calls that operate only on existing server-side state do not expose project or configuration fields. The server resolves the project directory in this order: @@ -99,7 +100,7 @@ The server resolves the project directory in this order: For reliable behavior, either set `--project-directory "${workspaceFolder}"` (or your client's project-path variable) in `mcp.json`, or let the agent pass `projectDirectory` per call. A per-call override controls both project-local configuration discovery (`.env`, `SFCC_CONFIG`, and `dw.json`) and relative filesystem paths. Use `configPath` when the desired `dw.json`-format file is not the project default. JSON-returning filesystem tools echo the resolved directory back in their output so you can confirm which path was used. -Each project-aware call selects its primary configuration path in this order, then adds the global `dw.json` to the available instances: +Each configuration-aware call selects its primary configuration path in this order, then adds the global `dw.json` to the available instances: 1. Per-call `configPath` 2. Server startup `--config` / `SFCC_CONFIG` @@ -112,13 +113,15 @@ This list selects the primary `dw.json`-format file. Individual configuration va The global `dw.json` is shared with the CLI and B2C DX VS Code extension. It is useful when an MCP client starts the server outside your project or when you want its instances available alongside project instances. -The primary and global `dw.json` files form one instance catalog. An instance named by `--instance` / `SFCC_INSTANCE` is searched in the primary file first and then the global file; same-name primary entries shadow global entries. The selected instance's fields are not merged across files. +The primary and global `dw.json` files form one instance catalog. An instance named by the MCP `instanceName` argument, CLI `--instance`, or `SFCC_INSTANCE` is searched in the primary file first and then the global file; same-name primary entries shadow global entries. The selected instance's fields are not merged across files. + +Configuration- and project-aware tools return a compact `resolution` block showing the effective project directory, selected configuration file, instance name, target hostname, and any specialized directories, together with the source of each choice. Session and watch start tools capture this block; their corresponding list tools return it so callers do not need to repeat context on follow-up calls. ::: tip Diagnosing configuration -Run the `config_inspect` tool (ask your agent to "inspect the B2C MCP configuration") to see the resolved configuration — instance, auth, SCAPI/MRT settings, and which source provided each value — along with the effective project directory and how it was resolved. Secrets are redacted by default. +Run the `config_inspect` tool (ask your agent to "inspect the B2C MCP configuration") to see the resolved configuration — instance, auth, SCAPI/MRT settings, the complete source graph, and the same compact `resolution` block returned by ordinary tools. Secrets are redacted by default. ::: -`config_inspect` uses the same SDK `loadConfig` resolver and globally registered CLI plugin configuration sources as `b2c setup inspect`. Given the same installed plugins, environment, `projectDirectory`, and `configPath`, its resolved values and source provenance follow the same pipeline; MCP adds the effective project-directory context to its response. +`config_inspect` uses the same SDK `loadConfig` resolver and globally registered CLI plugin configuration sources as `b2c setup inspect`. Given the same installed plugins, environment, `projectDirectory`, `configPath`, and `instanceName`, its resolved values and source provenance follow the same pipeline; MCP adds compact call-resolution context to its response. This is the [Agent Plugins](https://agent-plugins.org/plugin-authors/mcp-servers) `cwd` model: when a plugin declares an MCP server without an explicit `cwd`, the working directory defaults to the plugin root rather than your open project — which is exactly why the explicit outlets above matter. @@ -155,8 +158,6 @@ Override auto-discovery with `--toolsets` or `SFCC_TOOLSETS`: **Available toolsets:** `CARTRIDGES`, `MRT`, `PWAV3`, `SCAPI`, `STOREFRONTNEXT`, `all` -**Deprecated toolset:** `STOREFRONTNEXT_DEPRECATED` holds the legacy `sfnext_*` tools, which are not compatible with the Storefront Next 1.0 GA release and are superseded by the [`storefront-next`/`storefront-next-figma` agent-skills plugins](../guide/agent-skills). It is **never auto-enabled** and **not included in `all`** — request it explicitly with `--toolsets STOREFRONTNEXT_DEPRECATED --allow-non-ga-tools`. See [Toolsets](./toolsets#storefrontnext-deprecated). - With auto-discovery, the `SCAPI` toolset is always included. When using `--toolsets` or `--tools`, only the specified toolsets/tools are enabled. ### Individual Tool Selection diff --git a/docs/mcp/figma-tools-setup.md b/docs/mcp/figma-tools-setup.md deleted file mode 100644 index ad65a507b..000000000 --- a/docs/mcp/figma-tools-setup.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -description: Prerequisites and setup for Figma-to-component tools (workflow orchestrator, generate component, map tokens). ---- - -# Figma-to-Component Tools Setup - -::: warning DEPRECATED — use the agent-skills plugins instead -The Figma workflow MCP tools described on this page are **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. They have been superseded by the [`storefront-next` and `storefront-next-figma`](../guide/agent-skills) agent-skills plugins, which stay current with the GA release. They now live in the opt-in [`STOREFRONTNEXT_DEPRECATED`](./toolsets#storefrontnext-deprecated) toolset and **will be removed in a future release**. For Figma design-kit workflows, install the [`storefront-next-figma` plugin](../guide/agent-skills) instead. -::: - -Prerequisites and setup for using the Figma workflow tools: `sfnext_start_figma_workflow`, `sfnext_analyze_component`, and `sfnext_match_tokens_to_theme`. - -## Overview - -The Figma-to-component workflow requires an **external Figma MCP server** to fetch design data from Figma. The b2c-dx-mcp server handles workflow orchestration, component analysis, and token mapping, but it needs the Figma MCP server to access Figma designs. - -**Prerequisites:** - -- b2c-dx-mcp configured with `--allow-non-ga-tools` flag (Figma tools are preview) -- Storefront Next project -- `app.css` theme file (required for `sfnext_match_tokens_to_theme` tool; optional path can be provided) -- External Figma MCP server enabled in your MCP client - -See [Installation](./installation) for b2c-dx-mcp setup. - -## Figma MCP Setup - -The workflow requires a **separate Figma MCP server** to fetch design data from Figma. Enable it in your MCP client following the [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server). - -Figma provides two connection options: - -- **Desktop MCP (Local)** - Uses the Figma desktop app (no API token needed) -- **Remote MCP (Hosted)** - Requires a Figma Personal Access Token - -Check the [Figma MCP catalog](https://www.figma.com/mcp-catalog/) to see which option your MCP client supports, then follow the installation instructions in the Figma documentation. - -## Figma Design File - -You must have **view access** (or higher) to the Figma file. The workflow requires a Figma URL that includes the `node-id` query parameter to identify the specific frame or component to convert. - -**To get a URL with node-id:** - -1. Open the Figma file -2. Select the frame or component you want to convert -3. Right-click → **Copy link to selection** - -The workflow supports standard Figma URL formats with `node-id` parameter. No special Figma configuration (Dev Mode, Code Connect, plugins) is required. - -## Verification - -To confirm the Figma MCP server is working, list tools in your MCP client. You should see Figma MCP tools available (exact names may vary by provider). - -If the Figma MCP server is not enabled, the workflow tool will still return instructions and parsed parameters, but design data cannot be fetched from Figma. - -## Related Documentation - -- [sfnext_start_figma_workflow](./tools/sfnext-start-figma-workflow) - Workflow orchestrator (call first) -- [sfnext_analyze_component](./tools/sfnext-analyze-component) - REUSE/EXTEND/CREATE recommendation -- [sfnext_match_tokens_to_theme](./tools/sfnext-match-tokens-to-theme) - Token mapping -- [STOREFRONTNEXT Toolset](./toolsets#storefrontnext) - Overview of Storefront Next tools -- [`storefront-next-figma` plugin](/guide/agent-skills#available-plugins) - Agent skills for managing the Figma design kit (duplicate the kit, sync brand variables, publish Code Connect); also requires the [Figma MCP server](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server) -- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server) - Official Figma MCP setup diff --git a/docs/mcp/index.md b/docs/mcp/index.md index 167699d45..14bd4feb1 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -30,10 +30,6 @@ The **SCAPI** and **DIAGNOSTICS** toolsets are always enabled. On top of those, Every configuration also includes the always-on base toolsets (**SCAPI** + **DIAGNOSTICS**). Hybrid projects (e.g. cartridges + PWA Kit) get the union of the matching rows. You can also [manually select toolsets](./configuration#toolset-selection). -::: warning Storefront Next `sfnext_*` tools are deprecated -The legacy Storefront Next MCP tools (`sfnext_*`) are **not compatible with the Storefront Next 1.0 GA release** and have been superseded by the [`storefront-next` and `storefront-next-figma` agent-skills plugins](../guide/agent-skills). They no longer auto-enable for Storefront Next projects and have moved to the opt-in [`STOREFRONTNEXT_DEPRECATED`](./toolsets#storefrontnext-deprecated) toolset. Install the skills plugins instead — see the [Agent Skills guide](../guide/agent-skills). -::: - ## Plugins The MCP server uses the B2C CLI under the hood, so CLI plugins automatically extend MCP functionality. See the [CLI Plugin documentation](../guide/extending) for details. diff --git a/docs/mcp/installation.md b/docs/mcp/installation.md index 2c9d64d25..d1e85d8ff 100644 --- a/docs/mcp/installation.md +++ b/docs/mcp/installation.md @@ -12,8 +12,6 @@ This guide covers installing and configuring the B2C DX MCP Server for various M - A B2C Commerce project (for project-specific toolsets) - MCP client (Claude Code, Cursor, GitHub Copilot, or compatible client) -> **Note:** For Figma-to-component tools, you also need an external Figma MCP server enabled. See [Figma-to-Component Tools Setup](./figma-tools-setup) for details. - The MCP server is installed via `npx`, which downloads and runs the latest version on demand. For project type detection details, see [MCP Server Overview](./#project-type-detection). ## Claude Code diff --git a/docs/mcp/tools/cartridge-deploy.md b/docs/mcp/tools/cartridge-deploy.md index c7f3b3a71..7e44cde72 100644 --- a/docs/mcp/tools/cartridge-deploy.md +++ b/docs/mcp/tools/cartridge-deploy.md @@ -25,14 +25,16 @@ See [Configuration](../configuration) for complete credential setup details incl ### Parameters -| Parameter | Type | Required | Default | Description | -| ------------------ | -------- | -------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `projectDirectory` | string | No | Configured project directory | Project root used for configuration discovery and relative path resolution. Overrides the MCP process working directory. | -| `configPath` | string | No | Resolved from project context | Explicit `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`. | -| `directory` | string | No | Project directory (from `--project-directory` or auto-detected) | Path to directory to search for cartridges. The tool recursively searches for `.project` files to identify cartridges. | -| `cartridges` | string[] | No | All found cartridges | Array of cartridge names to include in the deployment. Use this to selectively deploy specific cartridges when you have multiple cartridges but only want to update some. If not specified, all cartridges found in the directory are deployed. | -| `exclude` | string[] | No | None | Array of cartridge names to exclude from the deployment. Use this to skip deploying certain cartridges, such as third-party or unchanged cartridges. Applied after the include filter. | -| `reload` | boolean | No | `false` | Whether to reload the code version after deployment. When `true`, the tool triggers a code version reload on the instance. | +| Parameter | Type | Required | Default | Description | +| -------------------- | -------- | -------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `projectDirectory` | string | No | Server project directory/cwd | Absolute project root used for configuration discovery and relative path resolution. The tool schema shows the exact fallback. | +| `configPath` | string | No | Resolved from project context | Primary `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`; the shared default remains available for instance lookup. | +| `instanceName` | string | No | Active/default instance | Named instance selected from the primary file first, then the shared default `dw.json`. | +| `cartridgeDirectory` | string | No | `projectDirectory` | Cartridge discovery root. Relative paths resolve from `projectDirectory`. | +| `directory` | string | No | — | Deprecated alias for `cartridgeDirectory`. | +| `cartridges` | string[] | No | All found cartridges | Array of cartridge names to include in the deployment. Use this to selectively deploy specific cartridges when you have multiple cartridges but only want to update some. If not specified, all cartridges found in the directory are deployed. | +| `exclude` | string[] | No | None | Array of cartridge names to exclude from the deployment. Use this to skip deploying certain cartridges, such as third-party or unchanged cartridges. Applied after the include filter. | +| `reload` | boolean | No | `false` | Whether to reload the code version after deployment. When `true`, the tool triggers a code version reload on the instance. | ### Usage @@ -48,7 +50,7 @@ Deploy specific cartridges and reload the code version: Deploy app_storefront_base and reload the code version. ``` -**Returns:** `{cartridges, codeVersion, reloaded, projectDirectory, resolvedDirectory}` — deployed cartridge mappings, code version, reload status, and the effective paths used. +**Returns:** deployed cartridge mappings, code version, reload status, and a `resolution` block identifying the selected project, configuration, instance, hostname, and cartridge directory. ## See Also diff --git a/docs/mcp/tools/diagnostics.md b/docs/mcp/tools/diagnostics.md index 712b1325a..fc669a587 100644 --- a/docs/mcp/tools/diagnostics.md +++ b/docs/mcp/tools/diagnostics.md @@ -41,10 +41,11 @@ Start a new script debugger session. Connects to the SDAPI, discovers cartridge | Parameter | Type | Required | Default | Description | | -------------------- | ------ | -------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `projectDirectory` | string | No | Configured project directory | Project root used to load `.env`/`dw.json` and resolve relative paths. Overrides the MCP process working directory. | -| `configPath` | string | No | Resolved from project context | Explicit `dw.json`-format configuration file. Overrides startup/project config selection; relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | Resolved from project context | Primary `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`; the shared default remains available. | +| `instanceName` | string | No | Active/default instance | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `cartridgeDirectory` | string | No | `projectDirectory` | Cartridge discovery and source-mapping root only. Use when cartridges are outside the project root; relative paths resolve from project root. | -**Returns:** `session_id`, `hostname`, discovered `cartridges`, resolved `projectDirectory`, resolved `cartridgeDirectory`, `session_cookie` (see [Server affinity](#server-affinity-hitting-breakpoints)), and `warnings`. +**Returns:** `session_id`, `hostname`, discovered `cartridges`, `resolution`, `session_cookie` (see [Server affinity](#server-affinity-hitting-breakpoints)), and `warnings`. The session retains its resolution context; `debug_list_sessions` returns it for later follow-up calls. ### debug_end_session diff --git a/docs/mcp/tools/logs.md b/docs/mcp/tools/logs.md index 17061bda2..9984f8048 100644 --- a/docs/mcp/tools/logs.md +++ b/docs/mcp/tools/logs.md @@ -39,7 +39,8 @@ List log files on the instance via WebDAV. | Parameter | Type | Required | Default | Description | | ------------------ | ---------------------------- | -------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | `projectDirectory` | string | No | configured project | Project root used for `.env` and default `dw.json` resolution. | -| `configPath` | string | No | project config | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | project config | Primary `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `instanceName` | string | No | active/default | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `prefixes` | string[] | No | all | Filter by log prefix (e.g., `["error", "customerror"]`). A path-like value such as `"internal/server"` lists logs in a subdirectory. | | `sort_by` | `"date" \| "name" \| "size"` | No | `date` | Sort field | | `sort_order` | `"asc" \| "desc"` | No | `desc` | Sort order | @@ -53,7 +54,8 @@ Fetch recent log entries in a single request/response. Filters (`since`, `level` | Parameter | Type | Required | Default | Description | | ------------------ | -------- | -------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | `projectDirectory` | string | No | configured project | Project root used for `.env` and default `dw.json` resolution. | -| `configPath` | string | No | project config | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | project config | Primary `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `instanceName` | string | No | active/default | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `prefixes` | string[] | No | `["error", "customerror"]` | Log prefixes to read. A path-like value such as `"internal/server"` reads logs from a subdirectory. | | `count` | number | No | `50` | Maximum entries to return | | `since` | string | No | | Relative time (`"5m"`, `"1h"`, `"2d"`) or ISO 8601 | @@ -71,7 +73,8 @@ Start a background log watch. Returns a `watch_id` immediately. Buffers entries | Parameter | Type | Required | Default | Description | | ------------------ | -------- | -------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `projectDirectory` | string | No | configured project | Project root used for `.env` and default `dw.json` resolution. | -| `configPath` | string | No | project config | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | project config | Primary `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `instanceName` | string | No | active/default | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `prefixes` | string[] | No | `["error", "customerror"]` | Log prefixes to watch. A path-like value such as `"internal/server"` watches logs in a subdirectory. | | `last_entries` | number | No | `0` | Pre-existing entries per file to emit on startup. `0` (default) captures only new entries; set >0 for recent context. | | `poll_interval_ms` | number | No | `3000` | How often the underlying tail polls WebDAV | @@ -115,7 +118,7 @@ No parameters. **Returns:** `{watches: [{watch_id, hostname, prefixes, buffered_entries, total_entries_seen, dropped_entries, files_discovered, stopped, created_at, last_activity_at}]}`. -> **Recovering orphaned watches:** watches live in the MCP server process and only one is allowed per hostname. Call `logs_watch_list` to find a lost watch, then `logs_watch_stop` with its `watch_id`. Idle watches are destroyed after 30 minutes; restarting the MCP server clears all watch state. +> **Recovering orphaned watches:** watches live in the MCP server process and only one is allowed per hostname. Call `logs_watch_list` to find a lost watch and its captured `resolution`, then `logs_watch_stop` with its `watch_id`. Idle watches are destroyed after 30 minutes; restarting the MCP server clears all watch state. --- @@ -152,7 +155,8 @@ Start a background tail of the configured MRT environment's logs. Returns a `wat | Parameter | Type | Required | Default | Description | | ------------------ | -------- | -------- | ------------------ | ---------------------------------------------------------------------------------------------------------- | | `projectDirectory` | string | No | configured project | Project root used for `.env` and default `dw.json` resolution. | -| `configPath` | string | No | project config | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | project config | Primary `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | +| `instanceName` | string | No | active/default | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `level` | string[] | No | | Drop entries not matching these levels (ERROR, WARN, INFO, DEBUG, ...) before buffering. Case-insensitive. | | `search` | string | No | | Drop entries not matching this case-insensitive substring (against message and raw) before buffering. | @@ -195,7 +199,7 @@ No parameters. **Returns:** `{watches: [{watch_id, project, environment, origin, buffered_entries, total_entries_seen, dropped_entries, stopped, created_at, last_activity_at}]}`. -> **Recovering orphaned watches:** watches live in the MCP server process and only one is allowed per project/environment/origin. Call `mrt_logs_watch_list` to find a lost watch, then `mrt_logs_watch_stop` with its `watch_id`. Idle watches are destroyed after 30 minutes; restarting the MCP server clears all watch state. +> **Recovering orphaned watches:** watches live in the MCP server process and only one is allowed per project/environment/origin. Call `mrt_logs_watch_list` to find a lost watch and its captured `resolution`, then `mrt_logs_watch_stop` with its `watch_id`. Idle watches are destroyed after 30 minutes; restarting the MCP server clears all watch state. --- diff --git a/docs/mcp/tools/mrt-bundle-push.md b/docs/mcp/tools/mrt-bundle-push.md index 88555f614..f990f6d82 100644 --- a/docs/mcp/tools/mrt-bundle-push.md +++ b/docs/mcp/tools/mrt-bundle-push.md @@ -32,7 +32,8 @@ Defaults for `buildDirectory`, `ssrOnly`, and `ssrShared` are chosen by detected | Parameter | Type | Required | Default | Description | | ------------------ | ------- | -------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `projectDirectory` | string | No | Configured project directory | Project root used for `.env`/`dw.json`, project-type detection, and relative build paths. | -| `configPath` | string | No | Resolved from project context | Explicit `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | Resolved from project context | Primary `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`; the shared default remains available. | +| `instanceName` | string | No | Active/default instance | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `buildDirectory` | string | No | `build` | Path to build directory containing the built project files. Can be absolute or relative to the project directory. | | `message` | string | No | None | Deployment message to include with the bundle push. Useful for tracking deployments. | | `ssrOnly` | string | No | Varies by project type | Glob patterns for server-only files (SSR), comma-separated or JSON array. These files are only included in the server bundle. | @@ -75,7 +76,7 @@ Push a bundle and deploy to staging with a deployment message: Build and push my Storefront Next bundle to staging with a deployment message. ``` -**Returns:** `{bundleId, projectSlug, target, deployed, message, projectDirectory, resolvedBuildDirectory}` +**Returns:** bundle details plus a `resolution` block identifying the project, selected configuration/instance, target hostname when available, and build directory. ## See also diff --git a/docs/mcp/tools/scapi-custom-apis.md b/docs/mcp/tools/scapi-custom-apis.md index 50f528d32..48a7a7b46 100644 --- a/docs/mcp/tools/scapi-custom-apis.md +++ b/docs/mcp/tools/scapi-custom-apis.md @@ -23,16 +23,17 @@ No authentication or instance required. This tool writes files locally into your ### Parameters -| Parameter | Type | Required | Description | -| ------------------ | ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `apiName` | string | Yes | API name in kebab-case (e.g. `my-products`). Must start with a lowercase letter; only letters, numbers, and hyphens. | -| `cartridgeName` | string | No | Cartridge that will contain the API. Omit to use the first cartridge found under the project (working directory or `projectRoot`). | -| `apiType` | `"admin"` \| `"shopper"` | No | **shopper** (siteId, customer-facing) or **admin** (no siteId). Default: `shopper`. | -| `apiDescription` | string | No | Short description of the API. | -| `projectDirectory` | string | No | Project root used for `.env`/`dw.json` discovery and relative path resolution. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`. | -| `projectRoot` | string | No | Project root for cartridge discovery. Default: MCP working directory (`--project-directory` / `SFCC_PROJECT_DIRECTORY`). | -| `outputDir` | string | No | Output directory override. Default: project root (scaffold writes under cartridge path). | +| Parameter | Type | Required | Description | +| -------------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | +| `apiName` | string | Yes | API name in kebab-case (e.g. `my-products`). Must start with a lowercase letter; only letters, numbers, and hyphens. | +| `cartridgeName` | string | No | Cartridge that will contain the API. Omit to use the first cartridge found under `cartridgeDirectory`. | +| `apiType` | `"admin"` \| `"shopper"` | No | **shopper** (siteId, customer-facing) or **admin** (no siteId). Default: `shopper`. | +| `apiDescription` | string | No | Short description of the API. | +| `projectDirectory` | string | No | Absolute project root used for project `.env` and relative path resolution. The schema shows the exact server/cwd fallback. | +| `cartridgeDirectory` | string | No | Cartridge discovery root. Relative paths resolve from `projectDirectory`. | +| `outputDirectory` | string | No | Output directory override. Relative paths resolve from the cartridge directory. | +| `projectRoot` | string | No | Deprecated alias for `cartridgeDirectory`. | +| `outputDir` | string | No | Deprecated alias for `outputDirectory`. | **Returns:** `{scaffold, outputDir, files: [{path, action}], postInstructions, projectDirectory, projectRoot}`. Errors if no cartridge is found. @@ -76,7 +77,8 @@ Requires OAuth credentials with the `sfcc.custom-apis` scope. See [Configuring S | Parameter | Type | Required | Description | | ------------------ | -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `projectDirectory` | string | No | Project root used for `.env`, default `dw.json`, and project configuration sources. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`. | +| `configPath` | string | No | Primary `dw.json`-format configuration file. Relative paths resolve from `projectDirectory`. | +| `instanceName` | string | No | Named instance selected from the primary configuration first, then the shared default `dw.json`. | | `status` | `"active"` \| `"not_registered"` | No | Filter by endpoint status. Omit to return all endpoints. | | `groupBy` | `"site"` \| `"type"` | No | Group output by `siteId` or `type` (Admin/Shopper). Omit for flat list. | | `columns` | string | No | Comma-separated field names to include. Omit for defaults (7 fields). Use all field names for complete data. | diff --git a/docs/mcp/tools/scapi-schemas-list.md b/docs/mcp/tools/scapi-schemas-list.md index a8a5042a8..496b5476d 100644 --- a/docs/mcp/tools/scapi-schemas-list.md +++ b/docs/mcp/tools/scapi-schemas-list.md @@ -18,14 +18,17 @@ Requires OAuth credentials with `sfcc.scapi-schemas` scope. See [B2C Credentials ### Parameters -| Parameter | Type | Required | Description | -| ---------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ | -| `apiFamily` | string | No | Filter by API family (e.g., `"shopper"`, `"product"`, `"checkout"`, `"custom"`). Custom APIs use `"custom"`. | -| `apiName` | string | No | Filter by API name (e.g., `"shopper-products"`, `"shopper-baskets"`). | -| `apiVersion` | string | No | Filter by API version (e.g., `"v1"`, `"v2"`). | -| `status` | `"current"` \| `"deprecated"` | No | Filter by schema status. Only works in list mode. | -| `includeSchemas` | boolean | No | Fetch full OpenAPI schema. Requires all three: `apiFamily`, `apiName`, and `apiVersion`. | -| `expandAll` | boolean | No | Return the full schema without collapsing. Requires `includeSchemas: true`. | +| Parameter | Type | Required | Description | +| ------------------ | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | +| `projectDirectory` | string | No | Absolute project root used for `.env`, `dw.json`, and package configuration; the schema shows its exact fallback. | +| `configPath` | string | No | Primary `dw.json`-format file; relative paths resolve from `projectDirectory`, with the shared default retained. | +| `instanceName` | string | No | Named instance selected from the primary configuration first, then the shared default `dw.json`. | +| `apiFamily` | string | No | Filter by API family (e.g., `"shopper"`, `"product"`, `"checkout"`, `"custom"`). Custom APIs use `"custom"`. | +| `apiName` | string | No | Filter by API name (e.g., `"shopper-products"`, `"shopper-baskets"`). | +| `apiVersion` | string | No | Filter by API version (e.g., `"v1"`, `"v2"`). | +| `status` | `"current"` \| `"deprecated"` | No | Filter by schema status. Only works in list mode. | +| `includeSchemas` | boolean | No | Fetch full OpenAPI schema. Requires all three: `apiFamily`, `apiName`, and `apiVersion`. | +| `expandAll` | boolean | No | Return the full schema without collapsing. Requires `includeSchemas: true`. | **List mode:** omit `includeSchemas` or any identifier to browse available schemas. **Fetch mode:** set `includeSchemas: true` and provide all three identifiers (`apiFamily`, `apiName`, `apiVersion`). diff --git a/docs/mcp/tools/sfnext-add-page-designer-decorator.md b/docs/mcp/tools/sfnext-add-page-designer-decorator.md deleted file mode 100644 index 0843e702d..000000000 --- a/docs/mcp/tools/sfnext-add-page-designer-decorator.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -description: Add Page Designer decorators to React components for Storefront Next to make them available in Page Designer. ---- - -# sfnext_add_page_designer_decorator - -::: warning DEPRECATED — use the agent-skills plugins instead -This tool is **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. It has been superseded by the [`storefront-next` and `storefront-next-figma`](../../guide/agent-skills) agent-skills plugins, which stay current with the GA release. It now lives in the opt-in [`STOREFRONTNEXT_DEPRECATED`](../toolsets#storefrontnext-deprecated) toolset (never auto-enabled, excluded from `--toolsets ALL`) and **will be removed in a future release**. Install the skills plugins instead — see the [Agent Skills guide](../../guide/agent-skills). -::: - -Adds Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefinition`) to React components to make them available in Page Designer for Storefront Next. - -## Overview - -The `sfnext_add_page_designer_decorator` tool analyzes React components and generates Page Designer decorators that enable components to be used in Page Designer. It supports two modes: - -1. **Auto Mode**: Quick setup with sensible defaults-automatically selects suitable props, infers types, and generates decorators immediately. -2. **Interactive Mode**: Multi-step workflow for fine-tuned control over decorator configuration. - -The tool uses component discovery to find components by name (e.g., "ProductItem", "ProductTile") without requiring exact file paths, making it easy to add Page Designer support to existing components. - -## Prerequisites - -- Storefront Next project with React components - -## Parameters - -| Parameter | Type | Required | Description | -| --------------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `projectDirectory` | string | No | Project root used for `.env`/`dw.json`, component discovery, and relative component/search paths. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | -| `component` | string | Yes | Component name (for example, `"ProductItem"`, `"ProductTile"`) or file path (for example, `"src/components/ProductItem.tsx"`). When a name is provided, the tool automatically searches common component directories. | -| `searchPaths` | string[] | No | Additional directories to search for components (for example, `["packages/retail/src", "app/features"]`). Only used when a component is specified by name (not path). | -| `autoMode` | boolean | No | Auto-generate all configurations with sensible defaults (skip interactive workflow). When enabled, automatically selects suitable props, infers types, and generates decorators without user confirmation. | -| `componentId` | string | No | Override component ID (default: auto-generated from component name). | -| `conversationContext` | object | No | Context for interactive mode workflow. See [Interactive Mode](#interactive-mode) for details. | - -### Conversation Context (Interactive Mode) - -When using interactive mode, provide `conversationContext` with the following structure: - -| Field | Type | Description | -| ------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| `step` | `"analyze"` \| `"select_props"` \| `"configure_attrs"` \| `"configure_regions"` \| `"confirm_generation"` | Current step in the conversation workflow | -| `componentInfo` | object | Cached component analysis from previous step | -| `selectedProps` | string[] | Props from component interface selected to expose in Page Designer | -| `newAttributes` | object[] | New attributes to add (not in existing props) | -| `attributeConfig` | object | Configuration for each attribute (explicit types, names, defaults, etc.) | -| `componentMetadata` | object | Component decorator configuration (id, name, description, group) | -| `regionConfig` | object | Region configuration (enabled, regions array) | - -## Operation Modes - -The tool supports two modes: - -1. **Auto Mode**: Quick setup with sensible defaults—automatically selects suitable props, infers types, and generates decorators immediately. -2. **Interactive Mode**: Multi-step workflow for fine-tuned control over decorator configuration. - -## Component Discovery - -The tool automatically searches for components in these locations (in order): - -1. `src/components/**` (PascalCase and kebab-case) -2. `app/components/**` -3. `components/**` -4. `src/**` (broader search) -5. Custom paths (if provided via `searchPaths`) - -**Examples:** - -- `"ProductItem"` → finds `src/components/product-item/index.tsx` or `ProductItem.tsx` -- `"ProductTile"` → finds `src/components/product-tile/ProductTile.tsx` or `product-tile/index.tsx` -- `"product-item"` → finds `src/components/product-item.tsx` or `product-item/index.tsx` - -**Tips:** - -- Use component name for portability -- Use path for unusual locations -- Add `searchPaths` for monorepos or non-standard structures - -## Usage Examples - -**Auto mode (quick setup):** - -``` -Use the MCP tool to add Page Designer decorators to my ProductItem component. -``` - -**Interactive mode (fine-tuned control):** - -``` -Use the MCP tool to add Page Designer decorators to my ProductTile component interactively. -``` - -**With custom search paths:** - -``` -Use the MCP tool to add Page Designer decorators to ProductItem, searching in packages/retail/src and app/features. -``` - -**Using component path:** - -``` -Use the MCP tool to add Page Designer decorators to src/components/ProductItem.tsx. -``` - -## Output - -The tool returns generated Page Designer decorator code that you can add to your component file. The decorators include component metadata, attribute definitions for props, and region definitions (if configured). - -## Related Tools - -- Part of the [STOREFRONTNEXT](../toolsets#storefrontnext) toolset -- Auto-enabled for Storefront Next projects - -## See Also - -- [STOREFRONTNEXT Toolset](../toolsets#storefrontnext) - Overview of Storefront Next development tools -- [Configuration](../configuration) - Configure project directory -- [Storefront Next Guide](../../guide/storefront-next) - Storefront Next development guide diff --git a/docs/mcp/tools/sfnext-analyze-component.md b/docs/mcp/tools/sfnext-analyze-component.md deleted file mode 100644 index 8587a2a2e..000000000 --- a/docs/mcp/tools/sfnext-analyze-component.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: Analyze design and discovered components to recommend REUSE, EXTEND, or CREATE strategy. ---- - -# sfnext_analyze_component - -::: warning DEPRECATED — use the agent-skills plugins instead -This tool is **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. It has been superseded by the [`storefront-next` and `storefront-next-figma`](../../guide/agent-skills) agent-skills plugins, which stay current with the GA release. It now lives in the opt-in [`STOREFRONTNEXT_DEPRECATED`](../toolsets#storefrontnext-deprecated) toolset (never auto-enabled, excluded from `--toolsets ALL`) and **will be removed in a future release**. Install the skills plugins instead — see the [Agent Skills guide](../../guide/agent-skills). -::: - -Analyzes design and discovered components to recommend a component generation strategy. Returns a REUSE, EXTEND, or CREATE action with confidence score, key differences, and suggested implementation approach. - -## Overview - -The `sfnext_analyze_component` tool compares design React code (e.g., from Figma, design handoff, or other sources) against existing components discovered in the codebase. It analyzes differences across styling, structure, behavior, and props, then recommends the best approach: - -- **REUSE**: Use existing component with props or minor styling adjustments -- **EXTEND**: Extend existing component via props, variant, or composition pattern -- **CREATE**: Create a new component (reference existing patterns if applicable) - -**Workflow position:** Call this tool **after** retrieving design data and discovering similar components. It is a required step in the Figma-to-component workflow. - -This tool is part of the STOREFRONTNEXT toolset. - -## Prerequisites - -- Design React code (from Figma MCP, design handoff, or other sources) -- Component discovery performed before calling -- Storefront Next project - -See [Figma-to-Component Tools Setup](../figma-tools-setup) for complete prerequisites and configuration. - -## Parameters - -| Parameter | Type | Required | Description | -| ---------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------ | -| `projectDirectory` | string | No | Project root used for project configuration and relative workspace paths. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | -| `figmaMetadata` | string | Yes | JSON string containing design metadata (from Figma MCP or empty). Can be empty string if metadata was not fetched. | -| `figmaCode` | string | Yes | React code from design (e.g., from Figma `mcp__figma__get_design_context`, or design handoff). | -| `componentName` | string | Yes | Suggested name for the component extracted from the design. | -| `discoveredComponents` | array | Yes | Array of similar components discovered using Glob/Grep/Read. Pass empty array if no similar components found. | -| `workspacePath` | string | No | Optional workspace path, resolved from `projectDirectory` when relative. | - -### Discovered Component Schema - -Each item in `discoveredComponents` must have: - -| Field | Type | Description | -| ------------ | ------ | ------------------------------------------ | -| `path` | string | Absolute file path to the component | -| `name` | string | Component name | -| `similarity` | number | Similarity score (0–100) | -| `matchType` | string | One of `'name'`, `'structure'`, `'visual'` | -| `code` | string | Full source code of the component | - -## Usage Examples - -### With Figma design URL - -``` -I have a Figma design at [URL]. Use the MCP tool to fetch the design code, search the codebase for similar components, then analyze and recommend whether to reuse, extend, or create a component. -``` - -### With design code already fetched - -``` -Use the MCP tool to analyze this design and recommend reuse, extend, or create. Design code: [paste React/JSX from Figma or design handoff]. Search the codebase for similar components first, then call the tool with the discovered components. -``` - -### Agent workflow note - -When the agent searches the codebase and finds no similar components, it should still call the tool with `discoveredComponents: []` to get a CREATE recommendation. The user does not need to specify this—the agent discovers it during the workflow. - -## Output - -Returns a formatted recommendation including: - -- **Decision**: REUSE, EXTEND, or CREATE -- **Confidence**: Percentage (0–100) -- **Matched Component**: Path, name, similarity (if applicable) -- **Key Differences**: List of difference descriptions -- **Suggested Approach**: Implementation guidance -- **Next Steps**: Action-specific instructions - -## Related Tools - -- [`sfnext_start_figma_workflow`](./sfnext-start-figma-workflow) - Call first to get workflow instructions and Figma parameters -- [`sfnext_match_tokens_to_theme`](./sfnext-match-tokens-to-theme) - Match design tokens to theme variables -- Part of the [STOREFRONTNEXT](../toolsets#storefrontnext) toolset -- Auto-enabled for Storefront Next projects - -## See Also - -- [Figma-to-Component Tools Setup](../figma-tools-setup) - Prerequisites and Figma MCP configuration -- [STOREFRONTNEXT Toolset](../toolsets#storefrontnext) - Overview of Storefront Next development tools -- [Configuration](../configuration) - Configure project directory -- [Storefront Next Guide](../../guide/storefront-next) - Storefront Next development guide diff --git a/docs/mcp/tools/sfnext-configure-theme.md b/docs/mcp/tools/sfnext-configure-theme.md deleted file mode 100644 index 169b6e65e..000000000 --- a/docs/mcp/tools/sfnext-configure-theme.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -description: Get theming guidelines, guided questions, and WCAG color contrast validation for Storefront Next. ---- - -# sfnext_configure_theme - -::: warning DEPRECATED — use the agent-skills plugins instead -This tool is **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. It has been superseded by the [`storefront-next` and `storefront-next-figma`](../../guide/agent-skills) agent-skills plugins, which stay current with the GA release. It now lives in the opt-in [`STOREFRONTNEXT_DEPRECATED`](../toolsets#storefrontnext-deprecated) toolset (never auto-enabled, excluded from `--toolsets ALL`) and **will be removed in a future release**. Install the skills plugins instead — see the [Agent Skills guide](../../guide/agent-skills). -::: - -Guides theming changes (colors, fonts, visual styling) for Storefront Next and validates color combinations for WCAG accessibility. - -## Overview - -The `sfnext_configure_theme` tool provides a structured workflow for applying theming to Storefront Next sites: - -1. **Guidelines** - Layout preservation rules, specification compliance, and accessibility requirements -2. **Guided Questions** - Collects user preferences (colors, fonts, mappings) one at a time -3. **WCAG Validation** - Automatically validates color contrast when `colorMapping` is provided - -The tool guides you through a structured workflow: answer questions about your design preferences → validate colors for accessibility → review findings → apply theme changes. - -## Prerequisites - -- Storefront Next project - -## Custom Theming Files - -Add custom theming guidance files by setting the `THEMING_FILES` environment variable. The value is a JSON array of `{key, path}` objects. Paths are resolved relative to the project directory (absolute paths also supported). Custom files are loaded when `sfnext_configure_theme` initializes for the project and are available via the `fileKeys` parameter. - -Use this only when you need project-specific guidance (for example, brand rules or design-system constraints). If the default files are sufficient, you can skip `THEMING_FILES`. - -**In `.env` file (recommended):** - -```bash -THEMING_FILES='[{"key":"brand-guidelines","path":"docs/brand-guidelines.md"}]' -``` - -**In MCP client `env` object:** - -```json -{ - "env": { - "THEMING_FILES": "[{\"key\":\"brand-guidelines\",\"path\":\"docs/brand-guidelines.md\"}]" - } -} -``` - -## Parameters - -| Parameter | Type | Required | Description | -| --------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `projectDirectory` | string | No | Project root used to load project `.env` values such as `THEMING_FILES` and resolve relative theming paths. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | -| `fileKeys` | string[] | No | File keys to add to the default set. Custom keys are merged with defaults: `theming-questions`, `theming-validation`, `theming-accessibility`. | -| `conversationContext` | object | No | Context from previous rounds. Omit to list available files. See [Conversation Context](#conversation-context) for details. | - -### Conversation Context - -When using the tool across multiple turns, provide `conversationContext` with the following structure: - -| Field | Type | Description | -| ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------ | -| `currentStep` | `"updating-information"` \| `"validation"` | Current step in the workflow | -| `collectedAnswers` | object | Previously collected answers. Include `colorMapping` to trigger automatic WCAG validation. | -| `questionsAsked` | string[] | List of question IDs already asked | - -**collectedAnswers** can include: - -| Field | Type | Description | -| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `colors` | object[] | Extracted colors with `hex` and optional `type` | -| `fonts` | object[] | Extracted fonts with `name` and optional `type` | -| `colorMapping` | object | Maps color keys to hex values (for example, `lightText`, `lightBackground`, `buttonText`, `buttonBackground`). **Providing this triggers automatic WCAG contrast validation.** | - -## Workflow - -The tool guides you through a structured workflow: - -1. **Information Gathering** - Answer questions about your brand colors, fonts, and design preferences -2. **Validation** - Automatic WCAG accessibility validation of color combinations -3. **Implementation** - Apply theme changes to your `app.css` file - -## Usage Examples - -Use natural language prompts to interact with the tool: - -**Start theming:** - -``` -I want to apply my brand colors to my Storefront Next site. Use the MCP tool to help me. -``` - -**Provide colors upfront:** - -``` -Use these colors: #635BFF (accent), #0A2540 (dark), #F6F9FC (brand), #FFFFFF (light). Use the MCP tool to guide me through theming. -``` - -**Specify fonts:** - -``` -I want to use Inter for body text and Playfair Display for headings. Use the MCP tool to help me theme my site. -``` - -**Validate colors for accessibility:** - -``` -I have a color scheme ready. Use the MCP tool to validate my colors for accessibility before I implement. -``` - -**Change existing theme:** - -``` -I want to change my site theme. Use the MCP tool to walk me through the process. -``` - -## Output - -The tool returns guidance and questions, or validation results when color mappings are provided. Validation includes contrast ratios, WCAG compliance status (AA/AAA), and recommendations for accessibility improvements. - -## Related Tools - -- Part of the [STOREFRONTNEXT](../toolsets#storefrontnext) toolset -- Auto-enabled for Storefront Next projects - -## See Also - -- [STOREFRONTNEXT Toolset](../toolsets#storefrontnext) - Overview of Storefront Next development tools -- [Storefront Next Guide](../../guide/storefront-next) - Storefront Next development guide -- [Configuration](../configuration) - Configure project directory diff --git a/docs/mcp/tools/sfnext-get-guidelines.md b/docs/mcp/tools/sfnext-get-guidelines.md deleted file mode 100644 index 28f7c129a..000000000 --- a/docs/mcp/tools/sfnext-get-guidelines.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -description: Get Storefront Next development guidelines and best practices for React Server Components, data loading, and framework constraints. ---- - -# sfnext_get_guidelines - -::: warning DEPRECATED — use the agent-skills plugins instead -This tool is **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. It has been superseded by the [`storefront-next` and `storefront-next-figma`](../../guide/agent-skills) agent-skills plugins, which stay current with the GA release. It now lives in the opt-in [`STOREFRONTNEXT_DEPRECATED`](../toolsets#storefrontnext-deprecated) toolset (never auto-enabled, excluded from `--toolsets ALL`) and **will be removed in a future release**. Install the skills plugins instead — see the [Agent Skills guide](../../guide/agent-skills). -::: - -Returns critical architecture rules, coding standards, and best practices for building Storefront Next applications with React Server Components. - -## Overview - -The `sfnext_get_guidelines` tool provides essential development guidance for Storefront Next. It: - -1. Returns comprehensive guidelines by default (quick-reference plus key sections). -2. Supports retrieving specific topic sections on demand. -3. Loads content from markdown files covering architecture, data fetching, components, testing, and more. - -**Important:** This tool is the **essential first step** for Storefront Next development. Use it before writing any code to understand non-negotiable patterns for React Server Components, data loading, and framework constraints. - -This tool is part of the STOREFRONTNEXT toolset and is auto-enabled for Storefront Next projects (detected by `@salesforce/storefront-next*` dependencies). - -## Parameters - -| Parameter | Type | Required | Default | Description | -| ---------- | -------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `sections` | string[] | No | `['quick-reference', 'data-fetching', 'components', 'testing']` | Optional array of specific sections to retrieve. If not specified, returns comprehensive guidelines. Pass an empty array to return an empty string. | - -### Available Sections - -| Section | Topics Covered | -| ------------------ | ------------------------------------------------------------------------------------------------------- | -| `quick-reference` | Critical rules, architecture principles, quick patterns | -| `data-fetching` | Server-only data loading (no client loaders), synchronous loaders for streaming, data fetching patterns | -| `state-management` | State management patterns | -| `auth` | Authentication and session management | -| `config` | Configuration system | -| `i18n` | i18n patterns and internationalization | -| `components` | Component best practices | -| `styling` | Tailwind CSS 4, Shadcn/ui, styling guidelines | -| `page-designer` | Page Designer integration | -| `performance` | Performance optimization | -| `testing` | Testing strategies | -| `extensions` | Framework extensions | -| `pitfalls` | Common pitfalls to avoid | - -## Usage Examples - -### Default (Comprehensive Guidelines) - -Get the default comprehensive set (quick-reference, data-fetching, components, testing): - -``` -Use the MCP tool to get Storefront Next development guidelines before I start coding. -``` - -### Single Section - -Retrieve a specific topic: - -``` -Use the MCP tool to get Storefront Next guidelines for data-fetching patterns. -``` - -### Multiple Related Sections - -Combine related sections in one call: - -``` -Use the MCP tool to get Storefront Next guidelines for data-fetching, components, and performance. -``` - -### All Sections - -Retrieve all available sections: - -``` -Use the MCP tool to get all Storefront Next development guidelines. -``` - -## Output - -Returns text content with guidelines for the requested section(s): - -- **Single section**: Returns content directly (no separators or instructions). -- **Multiple sections**: Returns content with `---` separators between sections, prefixed with instructions to display full content without summarization. - -The returned content includes: - -- Critical rules and best practices -- Code examples (correct ✅ and incorrect ❌ patterns) -- Quick reference snippets -- Framework-specific patterns for React Server Components - -## Related Tools - -- Part of the [STOREFRONTNEXT](../toolsets#storefrontnext) toolset -- Auto-enabled for Storefront Next projects -- [`sfnext_add_page_designer_decorator`](./sfnext-add-page-designer-decorator) - Add Page Designer decorators to components - -## See Also - -- [STOREFRONTNEXT Toolset](../toolsets#storefrontnext) - Overview of Storefront Next tools -- [Storefront Next Guide](../../guide/storefront-next) - User guide for Storefront Next -- [Configuration](../configuration) - Configure MCP server and toolset selection diff --git a/docs/mcp/tools/sfnext-match-tokens-to-theme.md b/docs/mcp/tools/sfnext-match-tokens-to-theme.md deleted file mode 100644 index a39d9b37d..000000000 --- a/docs/mcp/tools/sfnext-match-tokens-to-theme.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -description: Match Figma design tokens to existing theme tokens in app.css with confidence scores and suggestions. ---- - -# sfnext_match_tokens_to_theme - -::: warning DEPRECATED — use the agent-skills plugins instead -This tool is **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. It has been superseded by the [`storefront-next` and `storefront-next-figma`](../../guide/agent-skills) agent-skills plugins, which stay current with the GA release. It now lives in the opt-in [`STOREFRONTNEXT_DEPRECATED`](../toolsets#storefrontnext-deprecated) toolset (never auto-enabled, excluded from `--toolsets ALL`) and **will be removed in a future release**. Install the skills plugins instead — see the [Agent Skills guide](../../guide/agent-skills). -::: - -Matches Figma design tokens (colors, spacing, radius, etc.) to your Storefront Next theme tokens in `app.css`. Helps you identify which design tokens match existing theme variables and suggests new token names for values that don't have matches. - -## Overview - -The `sfnext_match_tokens_to_theme` tool helps you use theme tokens instead of hardcoded values in your components. After retrieving design tokens (from Figma, design handoff, or other sources), use this tool to match them against your Storefront Next theme. - -The tool reads your `app.css` theme file (or you can specify a custom path) and compares Figma design tokens against your existing theme variables. It returns a report with instructions showing which tokens match, which are similar, and which need new theme variables created. **The tool does not modify files**—it provides recommendations and instructions for you to apply. - -This tool is part of the STOREFRONTNEXT toolset. - -## Prerequisites - -- Storefront Next project with `app.css` (or provide `themeFilePath` explicitly) -- Design tokens extracted (from Figma, design system, or other sources) - -See [Figma-to-Component Tools Setup](../figma-tools-setup) for complete prerequisites and configuration. - -## Parameters - -| Parameter | Type | Required | Description | -| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------ | -| `projectDirectory` | string | No | Project root used for project configuration, theme discovery, and relative theme paths. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | -| `figmaTokens` | array | Yes | Array of design tokens (e.g., from Figma, design system, or style guide). | -| `themeFilePath` | string | No | Optional theme CSS path, resolved from `projectDirectory` when relative. If omitted, searches for `app.css`. | - -### Figma Token Schema - -Each token in `figmaTokens` must have: - -| Field | Type | Required | Description | -| ------------- | ------ | -------- | ----------------------------------------------------------------------------------- | -| `name` | string | Yes | Token name from Figma (e.g., `"Primary/Blue"`, `"Spacing/Large"`). | -| `value` | string | Yes | Token value (e.g., `"#2563eb"`, `"16px"`, `"0.5rem"`). | -| `type` | string | Yes | One of: `color`, `spacing`, `radius`, `opacity`, `fontSize`, `fontFamily`, `other`. | -| `description` | string | No | Optional description from Figma. | - -## Usage Examples - -**Match design tokens to your theme (default app.css):** - -``` -Use the MCP tool to match these design tokens to my theme: Primary/Blue #2563eb (color), Spacing/Large 16px (spacing). -``` - -**Match design tokens with custom theme file path:** - -``` -Use the MCP tool to match these design tokens to my theme at /path/to/app.css: -- Primary/Blue #2563eb (color) -- Spacing/Large 16px (spacing) -``` - -## Output - -Returns a report (does not modify files) showing: - -- Which Figma design tokens match existing theme variables (exact matches) -- Which tokens are similar to existing variables (suggested matches) -- Which tokens need new theme variables created (with suggested names) -- Instructions for using the matched tokens in components and adding new tokens to your theme file - -## Related Tools - -- [`sfnext_start_figma_workflow`](./sfnext-start-figma-workflow) - Workflow orchestrator; call first -- [`sfnext_analyze_component`](./sfnext-analyze-component) - REUSE/EXTEND/CREATE recommendation -- Part of the [STOREFRONTNEXT](../toolsets#storefrontnext) toolset -- Auto-enabled for Storefront Next projects - -## See Also - -- [Figma-to-Component Tools Setup](../figma-tools-setup) - Prerequisites and Figma MCP configuration -- [STOREFRONTNEXT Toolset](../toolsets#storefrontnext) - Overview of Storefront Next development tools -- [Configuration](../configuration) - Configure project directory -- [Storefront Next Guide](../../guide/storefront-next) - Storefront Next development guide diff --git a/docs/mcp/tools/sfnext-start-figma-workflow.md b/docs/mcp/tools/sfnext-start-figma-workflow.md deleted file mode 100644 index 9dbd487dd..000000000 --- a/docs/mcp/tools/sfnext-start-figma-workflow.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -description: Workflow orchestrator for Figma-to-component conversion. Parses your Figma URL and guides you through design-to-component conversion. ---- - -# sfnext_start_figma_workflow - -::: warning DEPRECATED — use the agent-skills plugins instead -This tool is **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. It has been superseded by the [`storefront-next` and `storefront-next-figma`](../../guide/agent-skills) agent-skills plugins, which stay current with the GA release. It now lives in the opt-in [`STOREFRONTNEXT_DEPRECATED`](../toolsets#storefrontnext-deprecated) toolset (never auto-enabled, excluded from `--toolsets ALL`) and **will be removed in a future release**. Install the skills plugins instead — see the [Agent Skills guide](../../guide/agent-skills). -::: - -Workflow orchestrator for converting Figma designs to Storefront Next components. Provide a Figma design URL to start the workflow, which extracts design data, analyzes your codebase, and produces component recommendations. - -## Overview - -When you provide a Figma design URL, the workflow will: - -- Fetch design context and screenshots from Figma -- Ask for your approval before exporting images (photos, logos, icons) -- Discover similar components in your project -- Recommend whether to REUSE, EXTEND, or CREATE a component -- Map Figma design tokens to your theme variables - -You receive a component recommendation with confidence score and a token mapping summary when the workflow completes. - -This tool is part of the STOREFRONTNEXT toolset. - -## Prerequisites - -- **B2C DX MCP** configured with `--allow-non-ga-tools` -- **Figma MCP server** (external) enabled in your MCP client -- **Valid Figma URL** with `node-id` query parameter (obtain by right-clicking a frame in Figma → Copy link to selection) - -See [Figma-to-Component Tools Setup](../figma-tools-setup) for complete prerequisites and configuration. - -## Parameters - -| Parameter | Type | Required | Description | -| ------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------- | -| `projectDirectory` | string | No | Project root used for project configuration and relative workflow paths. | -| `configPath` | string | No | Explicit `dw.json`-format configuration file; relative paths resolve from `projectDirectory`. | -| `figmaUrl` | string | Yes | The Figma design URL to convert. Must be a valid URL and include the `node-id` query parameter. | -| `workflowFilePath` | string | No | Optional path to a custom workflow `.md` file, resolved from `projectDirectory` when relative. | - -## Supported Figma URL Formats - -The parser supports these URL formats: - -- `https://figma.com/design/:fileKey/:fileName?node-id=1-2` -- `https://www.figma.com/design/:fileKey/:fileName?node-id=1-2` -- `https://figma.com/file/:fileKey/:fileName?node-id=1-2` - -The `node-id` parameter accepts hyphen format (`1-2`) or colon format (`1:2`). The parser converts hyphens to colons for Figma MCP compatibility. - -## Usage Examples - -### Basic Workflow Start - -``` -Use the MCP tool to convert this Figma design to a Storefront Next component: [Figma URL with node-id] -``` - -### Custom Workflow File - -``` -Use the MCP tool to start the Figma-to-component workflow with a custom workflow file at /path/to/custom-workflow.md -``` - -### Full Homepage Implementation - -Create a homepage from a Figma design, creating or updating components as needed: - -``` -Use the MCP tool to create this homepage from the Figma design: [Figma URL with node-id]. Create new components or update existing components using the MCP tool if necessary, then update the home page. The expected result should be that the homepage matches as closely as possible to the provided Figma design. -``` - -## Output - -The workflow returns a guide with extracted Figma parameters (`fileKey`, `nodeId`, and original URL). After the full workflow completes, you receive a component recommendation (REUSE/EXTEND/CREATE) with confidence score and a token mapping summary. - -## Related Tools - -- [`sfnext_analyze_component`](./sfnext-analyze-component) - Analyzes design and discovered components; recommends REUSE/EXTEND/CREATE -- [`sfnext_match_tokens_to_theme`](./sfnext-match-tokens-to-theme) - Matches design tokens to theme variables -- Part of the [STOREFRONTNEXT](../toolsets#storefrontnext) toolset -- Auto-enabled for Storefront Next projects - -## See Also - -- [Figma-to-Component Tools Setup](../figma-tools-setup) - Prerequisites and Figma MCP configuration -- [STOREFRONTNEXT Toolset](../toolsets#storefrontnext) - Overview of Storefront Next development tools -- [Configuration](../configuration) - Configure project directory -- [Storefront Next Guide](../../guide/storefront-next) - Storefront Next development guide diff --git a/docs/mcp/toolsets.md b/docs/mcp/toolsets.md index 5263a1396..f436b619c 100644 --- a/docs/mcp/toolsets.md +++ b/docs/mcp/toolsets.md @@ -51,33 +51,13 @@ Salesforce Commerce API discovery and exploration. **Always enabled.** ## STOREFRONTNEXT -Storefront Next development support. **Auto-enabled for** Storefront Next projects; enables the `MRT` and `CARTRIDGES` toolsets alongside the base `SCAPI` and `DIAGNOSTICS`. The legacy `sfnext_*` tools are deprecated — use the [`storefront-next` / `storefront-next-figma` agent-skills plugins](../guide/agent-skills) instead. +Storefront Next deployment and instance support. **Auto-enabled for** Storefront Next projects; enables the `MRT` and `CARTRIDGES` toolsets alongside the base `SCAPI` and `DIAGNOSTICS`. For coding guidance and project workflows, use the [`storefront-next` agent-skills plugin](../guide/agent-skills). - [`mrt_bundle_push`](./tools/mrt-bundle-push) — build and push a bundle - [MRT logs](./tools/logs#mrt-logs) — `mrt_logs_*` tools - [`scapi_schemas_list`](./tools/scapi-schemas-list) — list or fetch SCAPI schemas (standard and custom) - [Custom APIs](./tools/scapi-custom-apis) — scaffold custom endpoints and check their registration status -## STOREFRONTNEXT_DEPRECATED - -::: warning DEPRECATED — use the agent-skills plugins instead -The `sfnext_*` MCP tools are **deprecated** and **not compatible with the Storefront Next 1.0 GA release**. They have been superseded by the [`storefront-next`](../guide/agent-skills) and [`storefront-next-figma`](../guide/agent-skills) agent-skills plugins, which stay current with the GA release. **These tools will be removed in a future release.** - -This toolset is **never auto-enabled** and is **excluded from `--toolsets ALL`**; to use it you must request it explicitly _and_ pass `--allow-non-ga-tools`: - -```json -{"args": ["--toolsets", "STOREFRONTNEXT_DEPRECATED", "--allow-non-ga-tools"]} -``` - -::: - -- [`sfnext_get_guidelines`](./tools/sfnext-get-guidelines) -- [`sfnext_start_figma_workflow`](./tools/sfnext-start-figma-workflow) -- [`sfnext_analyze_component`](./tools/sfnext-analyze-component) -- [`sfnext_match_tokens_to_theme`](./tools/sfnext-match-tokens-to-theme) -- [`sfnext_add_page_designer_decorator`](./tools/sfnext-add-page-designer-decorator) -- [`sfnext_configure_theme`](./tools/sfnext-configure-theme) - ## Next Steps - [Configuration](./configuration) — credentials, environment variables, MCP flags, toolset selection, and logging diff --git a/packages/b2c-dx-mcp/CONTRIBUTING.md b/packages/b2c-dx-mcp/CONTRIBUTING.md index 77d607e60..95ec1073f 100644 --- a/packages/b2c-dx-mcp/CONTRIBUTING.md +++ b/packages/b2c-dx-mcp/CONTRIBUTING.md @@ -35,7 +35,12 @@ For local development or testing, use the development build directly: "mcpServers": { "b2c-dx-mcp": { "command": "node", - "args": ["/path/to/packages/b2c-dx-mcp/bin/dev.js", "--project-directory", "${workspaceFolder}", "--allow-non-ga-tools"] + "args": [ + "/path/to/packages/b2c-dx-mcp/bin/dev.js", + "--project-directory", + "${workspaceFolder}", + "--allow-non-ga-tools" + ] } } } @@ -67,11 +72,6 @@ npx mcp-inspector --cli node bin/dev.js --toolsets all --allow-non-ga-tools \ --method tools/call \ --tool-name cartridge_deploy -# Deprecated sfnext_* tools are excluded from `--toolsets all`; request the -# deprecated toolset explicitly to exercise them -npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT_DEPRECATED --allow-non-ga-tools \ - --method tools/call \ - --tool-name sfnext_add_page_designer_decorator ``` ### JSON-RPC via stdin @@ -95,11 +95,7 @@ Configure your IDE to use the local MCP server. Add this to your IDE's MCP confi "mcpServers": { "b2c-dx-local": { "command": "node", - "args": [ - "/full/path/to/packages/b2c-dx-mcp/bin/dev.js", - "--toolsets", "all", - "--allow-non-ga-tools" - ] + "args": ["/full/path/to/packages/b2c-dx-mcp/bin/dev.js", "--toolsets", "all", "--allow-non-ga-tools"] } } } @@ -118,6 +114,7 @@ When updating MCP documentation, you may need to create or update the "Add to Cu ### Link Format The deep link follows this format: + ``` cursor://anysphere.cursor-deeplink/mcp/install?name=b2c-dx-mcp&config= ``` @@ -127,16 +124,24 @@ cursor://anysphere.cursor-deeplink/mcp/install?name=b2c-dx-mcp&config= **⛔ `sfnext_*` tools are deprecated.** The Storefront Next MCP tools are **not compatible with the Storefront Next 1.0 GA release** and have been superseded by the [`storefront-next` and `storefront-next-figma` agent-skills plugins](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/agent-skills), which stay current with the GA release. They have moved to the `STOREFRONTNEXT_DEPRECATED` toolset, which is **never auto-enabled** and **excluded from `--toolsets all`**; to use them you must request the toolset explicitly (`--toolsets STOREFRONTNEXT_DEPRECATED --allow-non-ga-tools`). They will be removed in a future release. +| Toolset | Tools | Docs | +| -------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| CARTRIDGES | `cartridge_deploy` | [toolsets#cartridges](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets#cartridges) | +| MRT | `mrt_bundle_push` | [toolsets#mrt](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets#mrt) | +| SCAPI | `scapi_schemas_list`, `scapi_custom_apis_get_status`, `scapi_custom_api_generate_scaffold` | [toolsets#scapi](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets#scapi) | +| STOREFRONTNEXT | `mrt_bundle_push` + SCAPI tools (auto-enabled for Storefront Next projects) | [toolsets#storefrontnext](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets#storefrontnext) | +| PWAV3 | `pwakit_get_guidelines` + SCAPI tools | [toolsets#pwav3](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets#pwav3) | ### cartridge_deploy @@ -121,49 +118,6 @@ Generate new custom SCAPI endpoint in a cartridge. [Details](https://salesforcec - "Use the MCP tool to scaffold a new custom API named my-products." - "Use the MCP tool to create a custom admin API called customer-trips." -## Deprecated: Storefront Next (`sfnext_*`) tools - -> **⛔ Deprecated and not compatible with the Storefront Next 1.0 GA release.** The tools below have been superseded by the [`storefront-next` and `storefront-next-figma` agent-skills plugins](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/agent-skills) and will be removed in a future release. They no longer auto-enable for Storefront Next projects and are excluded from `--toolsets all`; to use them, request `--toolsets STOREFRONTNEXT_DEPRECATED --allow-non-ga-tools`. Migrate to the skills plugins. - -### sfnext_get_guidelines - -Get Storefront Next guidelines and best practices. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-get-guidelines) - -- "Use the MCP tool to show me critical Storefront Next rules." -- "Use the MCP tool to get data-fetching and component patterns." - -### sfnext_add_page_designer_decorator - -Add Page Designer decorators to components. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-add-page-designer-decorator) - -- "Use the MCP tool to add Page Designer decorators to my component." - -### sfnext_configure_theme - -Get theming guidelines, questions, and WCAG color validation for Storefront Next. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-configure-theme) - -- "Use the MCP tool to help me apply my brand colors to my Storefront Next site." -- "Use the MCP tool to validate my color combinations for accessibility." - -### sfnext_start_figma_workflow - -Workflow orchestrator for Figma-to-component conversion. Parses Figma URL, returns step-by-step instructions for subsequent tool calls. Requires external Figma MCP server. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-start-figma-workflow) — [Figma Setup](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/figma-tools-setup) - -- "Use the MCP tool to convert this Figma design to a Storefront Next component: [Figma URL with node-id]" -- "Use the MCP tool to create this homepage from the Figma design: [Figma URL with node-id]" - -### sfnext_analyze_component - -Analyze design and discovered components to recommend REUSE, EXTEND, or CREATE strategy. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-analyze-component) - -- "Use the MCP tool to analyze the Figma design and recommend whether to reuse, extend, or create a component." - -### sfnext_match_tokens_to_theme - -Map design tokens to existing theme tokens in app.css with confidence scores and suggestions. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-match-tokens-to-theme) - -- "Use the MCP tool to map these Figma design tokens to my theme." - ### pwakit_get_guidelines Get PWA Kit v3 guidelines. [Details](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/pwakit-get-guidelines) @@ -187,4 +141,3 @@ For MCP development, testing, and local setup, see [CONTRIBUTING.md](./CONTRIBUT ## License This project is licensed under the Apache License 2.0. See [LICENSE.txt](../../LICENSE.txt) for full details. - diff --git a/packages/b2c-dx-mcp/content/sfnext/auth.md b/packages/b2c-dx-mcp/content/sfnext/auth.md deleted file mode 100644 index b962df2f1..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/auth.md +++ /dev/null @@ -1,62 +0,0 @@ -# Authentication & Session Management - -## Architecture - -Split-cookie architecture with server/client contexts: - -- **Server middleware** (`auth.server.ts`): Manages SLAS tokens, writes cookies -- **Client middleware** (`auth.client.ts`): Reads cookies, maintains cache -- **React Context** (`AuthProvider`): Provides auth state to components - -## Cookie Design - -| Cookie Name | Purpose | User Type | Expiry | HttpOnly | -|-------------|---------|-----------|--------|----------| -| `cc-nx-g` | Guest refresh token | Guest | 30 days | No | -| `cc-nx` | Registered refresh token | Registered | 90 days | No | -| `cc-at` | Access token | Both | 30 min | No | -| `usid` | User session ID | Both | Matches refresh | No | -| `customerId` | Customer ID | Registered | Matches refresh | No | - -**Key Points**: - -- Only ONE refresh token exists (guest OR registered, never both) -- User type derived from which refresh token exists -- Cookies auto-namespaced with `siteId` -- Tokens auto-refresh when expired - -## Usage in Loaders/Actions - -```typescript -import { getAuth } from '@/middlewares/auth.server'; - -export function loader({ context }: LoaderFunctionArgs) { - const auth = getAuth(context); - - // Access auth properties - const accessToken = auth.access_token; - const customerId = auth.customer_id; - const isGuest = auth.userType === 'guest'; - const isRegistered = auth.userType === 'registered'; - - return { isGuest, customerId }; -} -``` - -## Usage in Components - -```typescript -import { useAuth } from '@/providers/auth'; - -export function MyComponent() { - const auth = useAuth(); - - if (auth?.userType === 'guest') { - return ; - } - - return
Welcome, customer {auth?.customer_id}
; -} -``` - -**Reference:** See README-AUTH.md for complete authentication documentation. diff --git a/packages/b2c-dx-mcp/content/sfnext/components.md b/packages/b2c-dx-mcp/content/sfnext/components.md deleted file mode 100644 index d2dfe1a59..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/components.md +++ /dev/null @@ -1,123 +0,0 @@ -# Component Patterns - -## Use the `createPage` HOC - -The `createPage` higher-order component standardizes page patterns with built-in Suspense and page key handling: - -```typescript -import { use } from 'react'; -import { createPage } from '@/components/create-page'; - -// Define your view component -function ProductView({ - product, - category -}: { - product: Promise; - category?: Promise -}) { - const productData = use(product); - const categoryData = category ? use(category) : null; - - return ( -
-

{productData.name}

- {categoryData &&

Category: {categoryData.name}

} -
- ); -} - -// Create page with fallback -const ProductPage = createPage({ - component: ProductView, - fallback: -}); - -export default ProductPage; -``` - -**Benefits:** - -- Eliminates repetitive Suspense/Await boilerplate -- Consistent loading states across pages -- Built-in page key management for navigation transitions -- Type-safe with full TypeScript support - -## shadcn/ui Components - -**RULES**: - -- ✅ Add via: `npx shadcn@latest add ` -- ❌ DO NOT modify `src/components/ui/` directly -- ✅ Create custom components elsewhere - -## Suspense Boundaries - -Use granular Suspense boundaries for better UX: - -```typescript -// ✅ RECOMMENDED - Multiple Suspense boundaries -export default function ProductPage({ loaderData: { product, reviews } }) { - return ( -
- }> - - {(data) => } - - - - }> - - {(data) => } - - -
- ); -} - -// ⚠️ OK - Single Suspense boundary (less granular) -export default createPage({ - component: ProductView, - fallback: -}); -``` - -## File Organization - -``` -src/components/product-tile/ -├── index.tsx # Component -├── index.test.tsx # Tests -└── stories/ - ├── index.stories.tsx # Storybook stories - └── __snapshots__/ # Storybook snapshots (optional) - └── product-tile-snapshot.tsx.snap - -# Skeleton components are separate components -src/components/product-skeleton/ -├── index.tsx -├── index.test.tsx -└── stories/ - └── index.stories.tsx -``` - -## Styling - -**Tailwind CSS 4** is the only styling approach allowed. Use utility classes directly in components. - -**Key rules:** - -- ✅ Use Tailwind utility classes -- ✅ Use `cn()` utility for conditional classes -- ❌ NO inline styles, NO CSS modules, NO separate CSS files - -**See `styling` section for:** Tailwind CSS 4, Shadcn/ui components, icons, responsive design, theme configuration, dark mode, best practices - -## Best Practices - -1. **Extract view components** - Separate data handling from presentation -2. **Type safety** - Define proper TypeScript interfaces -3. **Consistent fallbacks** - Reusable skeleton components -4. **Colocate tests** - Keep tests next to components -5. **Story coverage** - Create stories for all reusable components -6. **Tailwind utilities only** - Use Tailwind CSS classes, avoid inline styles or CSS modules diff --git a/packages/b2c-dx-mcp/content/sfnext/config.md b/packages/b2c-dx-mcp/content/sfnext/config.md deleted file mode 100644 index 5b45328fd..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/config.md +++ /dev/null @@ -1,180 +0,0 @@ -# Configuration Management - -## Overview - -All configuration is centralized in `config.server.ts` with environment variable overrides via `.env` files. The configuration system provides type-safe access to app settings with automatic parsing and validation. - -## Required Variables - -Copy `.env.default` to `.env` and set these required Commerce Cloud credentials: - -```bash -PUBLIC__app__commerce__api__clientId=your-client-id -PUBLIC__app__commerce__api__organizationId=your-org-id -PUBLIC__app__commerce__api__siteId=your-site-id -PUBLIC__app__commerce__api__shortCode=your-short-code -PUBLIC__app__defaultSiteId=your-site-id -PUBLIC__app__commerce__sites='[{"id":"your-site-id","defaultLocale":"en-US","defaultCurrency":"USD","supportedLocales":[{"id":"en-US","preferredCurrency":"USD"}],"supportedCurrencies":["USD"]}]' -``` - -**Note:** The `commerce.sites` array defines your site configuration including locales, currencies, and supported options. See `.env.default` for a complete example with multiple locales and currencies. - -## Adding Configuration - -1. **Define type in `src/config/schema.ts`**: - -```typescript -export type Config = { - app: { - myFeature: { - enabled: boolean; - maxItems: number; - }; - }; -}; -``` - -2. **Add defaults in `config.server.ts`**: - -```typescript -export default defineConfig({ - app: { - myFeature: { - enabled: false, - maxItems: 10, - }, - }, -}); -``` - -3. **Override via environment variables**: - -```bash -PUBLIC__app__myFeature__enabled=true -PUBLIC__app__myFeature__maxItems=20 -``` - -## Usage Patterns - -**In React Components**: - -```typescript -import { useConfig } from '@/config'; - -export function MyComponent() { - const config = useConfig(); - - if (config.myFeature.enabled) { - const maxItems = config.myFeature.maxItems; - // Your feature code - } -} -``` - -**In Server Loaders/Actions**: - -```typescript -import { getConfig } from '@/config'; - -export function loader({ context }: LoaderFunctionArgs) { - const config = getConfig(context); - - if (config.myFeature.enabled) { - // Your loader code - } -} -``` - -**In Client Loaders**: - -```typescript -import { getConfig } from '@/config'; - -export function clientLoader() { - const config = getConfig(); // No context needed - uses window.__APP_CONFIG__ - - if (config.myFeature.enabled) { - // Your loader code - } -} -``` - -**Note:** `getConfig()` and `useConfig()` return `AppConfig` which is the `app` section of the full `Config` type. So you access properties directly (e.g., `config.myFeature.enabled`) without the `app` prefix. - -## Environment Variable Rules - -Use the `PUBLIC__` prefix with double underscores (`__`) to set any config path: - -```bash -# Environment variable → Config path (in Config type) → Access via getConfig()/useConfig() -PUBLIC__app__commerce__sites='[...]' → config.app.commerce.sites → config.commerce.sites -PUBLIC__app__defaultSiteId=RefArchGlobal → config.app.defaultSiteId → config.defaultSiteId -PUBLIC__app__myFeature__enabled=true → config.app.myFeature.enabled → config.myFeature.enabled -``` - -**Multi-site Configuration Example:** - -```bash -PUBLIC__app__commerce__sites='[ - { - "id": "RefArchGlobal", - "defaultLocale": "en-US", - "defaultCurrency": "USD", - "supportedLocales": [ - {"id": "en-US", "preferredCurrency": "USD"}, - {"id": "de-DE", "preferredCurrency": "EUR"} - ], - "supportedCurrencies": ["USD", "EUR"] - } -]' -``` - -**Accessing Site Configuration:** - -```typescript -const config = getConfig(context); -const currentSite = config.commerce.sites[0]; // Get first site -const locale = currentSite.defaultLocale; // "en-US" -const currency = currentSite.defaultCurrency; // "USD" -``` - -Values are automatically parsed (numbers, booleans, JSON arrays/objects). - -Rules: -1. **`PUBLIC__` prefix**: Exposed to browser (client-safe values) -2. **No prefix**: Server-only (secrets, never exposed) -3. **`__` separator**: Navigate nested paths (`PUBLIC__app__commerce__sites`) -4. **Case-insensitive**: All casings work (normalized to match `config.server.ts`) -5. **Auto-parsing**: Strings, numbers, booleans, JSON arrays/objects -6. **Validation**: Paths must exist in `config.server.ts` (prevents typos) -7. **Depth limit**: Maximum 10 levels deep (use JSON values for deeper nesting) -8. **Path precedence**: More specific paths override less specific ones -9. **Protected paths**: `app__engagement` cannot be overridden via environment variables -10. **MRT limits**: Variable names max 512 characters, total PUBLIC__ values max 32KB - -**Note:** Site configuration (locales, currencies) is now managed via `PUBLIC__app__commerce__sites` array instead of individual `PUBLIC__app__site__locale` variables. This enables multi-site support. - -**Setting nested objects with JSON:** - -```bash -# Instead of multiple variables: -PUBLIC__app__myFeature__option1=value1 -PUBLIC__app__myFeature__option2=value2 - -# Use a single JSON value: -PUBLIC__app__myFeature='{"option1":"value1","option2":"value2","nested":{"enabled":true}}' -``` - -## Security - -```bash -# ✅ Safe for client (PUBLIC__ prefix) -PUBLIC__app__commerce__api__clientId=abc123 - -# ✅ Server-only (no prefix) -COMMERCE_API_SLAS_SECRET=your-secret -``` - -Read server-only secrets directly from `process.env` - never add to config. - -**Reference:** See src/config/README.md for complete configuration documentation. diff --git a/packages/b2c-dx-mcp/content/sfnext/data-fetching.md b/packages/b2c-dx-mcp/content/sfnext/data-fetching.md deleted file mode 100644 index 111e58893..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/data-fetching.md +++ /dev/null @@ -1,323 +0,0 @@ -# Data Fetching Patterns - -## Loader Functions - -**IMPORTANT**: This project **mandates server-only data loading**. Every UI route must only export a `loader` function. - -### Critical Rule: Synchronous Loaders for Streaming - -**IMPORTANT**: Loaders should be **synchronous functions that return objects containing promises**, NOT async functions. This enables non-blocking page transitions and streaming SSR. - -```typescript -// ✅ CORRECT - Synchronous loader returning promises -export function loader({ context }: LoaderFunctionArgs): ProductPageData { - const clients = createApiClients(context); - return { - product: clients.shopperProducts.getProduct({...}), // Promise - streams - reviews: clients.shopperProducts.getReviews({...}), // Promise - streams - }; -} - -// ❌ AVOID - Async loader blocks page transitions -export async function loader({ context }: LoaderFunctionArgs): Promise { - const product = await clients.shopperProducts.getProduct({...}); // Blocks! - return { product }; -} -``` - -**Why this matters:** -- Async loaders with `await` **block the entire page transition** until all data resolves -- Synchronous loaders returning promises allow React to **stream data progressively** -- Each promise resolves independently, enabling granular Suspense boundaries -- Users see content as it becomes available, not all at once - -**Behavior**: -- Initial load: Runs on server (SSR) -- Navigation: Runs on server (XHR/fetch to server) -- SCAPI requests always on MRT - -## Data Loading Strategies - -### Pattern 1: Awaited Data (Blocking) - -```typescript -// ⚠️ BLOCKS rendering until all data is ready -export async function loader({ params, context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - return { - product: await clients.shopperProducts.getProduct({ - params: { path: { id: params.productId } } - }).then(({ data }) => data) - }; -} -``` - -**Use when:** Critical data must be available before rendering (SEO, above-the-fold content) - -### Pattern 2: Deferred Data (Streaming) - -```typescript -// ✅ RECOMMENDED - Streams data progressively -export function loader({ params, context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - return { - // Return promises directly - they'll stream to client - product: clients.shopperProducts.getProduct({ - params: { path: { id: params.productId } } - }).then(({ data }) => data), - - reviews: clients.shopperProducts.getReviews({ - params: { path: { id: params.productId } } - }).then(({ data }) => data) - }; -} -``` - -**Use when:** Non-critical data can load after initial render - -### Pattern 3: Mixed Strategy - -```typescript -// ✅ BEST OF BOTH - Critical data awaited, rest streamed -export async function loader({ params, context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - - // Await critical data - const product = await clients.shopperProducts.getProduct({ - params: { path: { id: params.productId } } - }).then(({ data }) => data); - - return { - product, // Resolved - reviews: clients.shopperProducts.getReviews({ - params: { path: { id: params.productId } } - }).then(({ data }) => data), // Streamed - recommendations: clients.shopperProducts.getRecommendations({ - params: { path: { id: params.productId } } - }).then(({ data }) => data) // Streamed - }; -} -``` - -## Action Functions - -Handle mutations (form submissions, cart updates): - -```typescript -import {data, redirect} from 'react-router'; - -export async function action({request, context}: ActionFunctionArgs) { - const formData = await request.formData(); - const productId = formData.get('productId') as string; - - const clients = createApiClients(context); - - try { - await clients.shopperBasketsV2.addItemToBasket({ - params: { - path: {basketId}, - body: {productId, quantity: 1}, - }, - }); - - return data({success: true}); - } catch (error) { - return data({success: false, error: error.message}, {status: 400}); - } -} -``` - -## Interactive Data Fetching: useScapiFetcher - -For on-demand, user-triggered data fetching (after page load), use the `useScapiFetcher` hook instead of loaders. - -### `loader` vs `useScapiFetcher` - -| Aspect | `loader` | `useScapiFetcher` | -|--------|----------|-------------------| -| **When it runs** | Route navigation (page load) | On-demand (user interaction) | -| **Triggered by** | URL change | Component code (useEffect, button click) | -| **Data availability** | Before/during component render (streamed) | After component mounts | -| **Execution context** | Server (MRT) | Triggers server route | -| **Use case** | Initial page data | Dynamic, interactive fetching | - -### How `useScapiFetcher` Works - -```text -Component calls useScapiFetcher() - ↓ -Hook builds URL: /resource/api/client/{encoded-params} - ↓ -fetcher.load() or fetcher.submit() - ↓ -resource.api.client.$resource.ts loader/action runs ON SERVER - ↓ -createApiClients(context) makes SCAPI call (server-side) - ↓ -JSON response returned to component -``` - -**Important:** Even though you call `useScapiFetcher` from the browser, the actual SCAPI requests still happen **on the server** through the resource route, keeping credentials secure. - -### Example: Search Suggestions - -```typescript -import { useScapiFetcher } from '@/hooks/use-scapi-fetcher'; -import { useMemo, useCallback } from 'react'; - -export function useSearchSuggestions({ q, limit, currency }) { - // Prepare SCAPI parameters - const parameters = useMemo( - () => ({ - params: { - query: { q, limit, currency } - } - }), - [q, limit, currency] - ); - - // Hook automatically routes to server - const fetcher = useScapiFetcher( - 'shopperSearch', // SCAPI client - 'getSearchSuggestions', // Method name - parameters // Parameters - ); - - const refetch = useCallback(async () => { - await fetcher.load(); // Triggers server request - }, [fetcher]); - - return { - data: fetcher.data, - isLoading: fetcher.state === 'loading', - refetch - }; -} -``` - -### When to Use Each Approach - -| Scenario | Use | -|----------|-----| -| Load product data when visiting `/product/123` | `loader` | -| Load checkout data | `loader` | -| Search suggestions as user types | `useScapiFetcher` | -| Update customer profile in modal | `useScapiFetcher` | -| Load recommendations after page loads | `useScapiFetcher` | -| Fetch bonus products when modal opens | `useScapiFetcher` | -| Infinite scroll / Load more | `useScapiFetcher` | - -### Timeline Comparison - -```text -┌─────────────────────────────────────────────────────────────────┐ -│ loader (Server) │ -├─────────────────────────────────────────────────────────────────┤ -│ User clicks link → Server loader() → Stream data → Page render │ -│ │ -│ Timeline: [navigate] → [server fetch] → [stream to client] │ -│ │ -│ Data available: Streamed during render via Suspense │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ useScapiFetcher │ -├─────────────────────────────────────────────────────────────────┤ -│ Page loads → Component mounts → User types → fetcher.load() │ -│ │ -│ Timeline: [render] → [user action] → [fetch] → [re-render] │ -│ │ -│ Data available: AFTER user action, component re-renders │ -└─────────────────────────────────────────────────────────────────┘ -``` - -## API Client Usage - -Always use `createApiClients(context)`: - -```typescript -import { createApiClients } from '@/lib/api-clients'; - -export function loader({ context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - - // All SCAPI clients with full type safety: - clients.shopperProducts.getProduct({...}); - clients.shopperCustomers.getCustomer({...}); - clients.shopperBasketsV2.getBasket({...}); - clients.shopperSearch.productSearch({...}); - clients.shopperOrders.getOrder({...}); -} -``` - -## Parallel vs Sequential - -```typescript -// ✅ GOOD - Parallel requests -export function loader({ context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - - return { - product: clients.shopperProducts.getProduct({...}), - reviews: clients.shopperProducts.getReviews({...}), - recommendations: clients.shopperProducts.getRecommendations({...}) - }; - // All three requests start simultaneously -} - -// ❌ BAD - Sequential requests -export async function loader({ context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - - const product = await clients.shopperProducts.getProduct({...}); - const reviews = await clients.shopperProducts.getReviews({...}); - const recommendations = await clients.shopperProducts.getRecommendations({...}); - - return { product, reviews, recommendations }; - // Each request waits for the previous to complete -} -``` - -## Understanding Data Flow - -### Initial Page Load (SSR) - -```text -Browser → MRT Server - ↓ - loader() runs on server - ↓ - SCAPI requests on MRT - ↓ - HTML response → Browser -``` - -**Key characteristics:** -- The `loader()` runs on the server -- SCAPI requests happen server-side (direct in production, proxied in dev) -- Full HTML is returned to browser -- Client hydrates the HTML - -### Subsequent Navigation (SPA) - -All routes use server `loader` for both SSR and SPA navigation: - -```text -User clicks link → React Router intercepts - ↓ - Browser makes fetch() to server - ↓ - MRT Server receives request - ↓ - Same loader() runs on server - ↓ - SCAPI requests on MRT - ↓ - JSON response → Browser - ↓ - React updates DOM -``` - -**Key Point:** The loader function code is identical for both SSR and SPA navigation. The only difference is the response format (HTML vs JSON). This is why SCAPI credentials stay secure and MRT orchestration works consistently. - -**Reference:** See README-DATA.md for complete data fetching documentation. diff --git a/packages/b2c-dx-mcp/content/sfnext/extensions.md b/packages/b2c-dx-mcp/content/sfnext/extensions.md deleted file mode 100644 index f656d31c7..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/extensions.md +++ /dev/null @@ -1,80 +0,0 @@ -# Extension Development - -## Structure - -``` -src/extensions/my-extension/ -├── plugin-config.json # Plugin configuration -├── components/ # Extension components -├── routes/ # Extension routes -├── locales/ # Extension translations -└── providers/ # Extension providers -``` - -## Plugin Configuration - -**Insert component into plugin point**: - -```json -{ - "components": [ - { - "pluginId": "header.before.cart", - "path": "extensions/my-extension/components/badge.tsx", - "order": 0 - } - ], - "contextProviders": [ - { - "path": "extensions/my-extension/providers/my-provider.tsx", - "order": 0 - } - ] -} -``` - -## Extension Routes - -Files in `routes/` auto-register: - -```typescript -// src/extensions/my-extension/routes/my-route.tsx -export function loader() { - return { message: 'Hello' }; -} - -export default function MyRoute() { - const { message } = useLoaderData(); - return
{message}
; -} -``` - -## Extension Translations - -Auto-namespaced as `extPascalCase`: - -``` -src/extensions/my-extension/locales/ -├── en-US/translations.json -└── it-IT/translations.json -``` - -```typescript -const {t} = useTranslation('extMyExtension'); -t('welcome'); -``` - -## Integration Markers - -```typescript -// Single line -/** @sfdc-extension-line SFDC_EXT_MY_FEATURE */ -import myFeature from '@extensions/my-feature'; - -// Block -{/* @sfdc-extension-block-start SFDC_EXT_MY_FEATURE */} -My Feature -{/* @sfdc-extension-block-end SFDC_EXT_MY_FEATURE */} -``` - -For full documentation, read: src/extensions/README.md diff --git a/packages/b2c-dx-mcp/content/sfnext/i18n.md b/packages/b2c-dx-mcp/content/sfnext/i18n.md deleted file mode 100644 index ec0f42531..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/i18n.md +++ /dev/null @@ -1,121 +0,0 @@ -# Internationalization (i18n) - -## Overview - -- **Server instance**: Has access to all translations for all languages -- **Client instance**: Dynamically imports translations as JavaScript chunks -- **Dual API**: `useTranslation()` for components, `getTranslation()` for everything else - -## Adding Translations - -**In `src/locales/{language}/translations.json`**: - -```json -{ - "product": { - "title": "Product Details", - "addToCart": "Add to Cart", - "greeting": "Hello, {{name}}!", - "itemCount": { - "zero": "No items", - "one": "{{count}} item", - "other": "{{count}} items" - } - } -} -``` - -## Usage - -**2. Use in React components:** - -```typescript -import { useTranslation } from 'react-i18next'; - -export function ProductCard() { - const { t } = useTranslation('product'); - - return ( -
-

{t('title')}

- -

{t('greeting', { name: 'John' })}

-

{t('itemCount', { count: 5 })}

-
- ); -} -``` - -**3. Use in non-component code:** - -```typescript -import { getTranslation } from '@/lib/i18next'; - -// Client-side or utilities -const { t } = getTranslation(); -const message = t('product:addToCart'); - -// Server-side (loaders/actions) -export function loader(args: LoaderFunctionArgs) { - const { t } = getTranslation(args.context); - return { title: t('product:title') }; -} -``` - -## Validation Schemas with Translations - -**CRITICAL**: Use factory pattern for Zod schemas to avoid race conditions: - -```typescript -// ❌ WRONG - Module-level schema (race condition) -export const schema = z.object({ - email: z.string().email(t('validation:emailInvalid')) -}); - -// ✅ CORRECT - Factory function -import type { TFunction } from 'i18next'; - -export const createSchema = (t: TFunction) => { - return z.object({ - email: z.string().email(t('validation:emailInvalid')) - }); -}; - -// Usage in component -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -function MyForm() { - const { t } = useTranslation(); - const schema = useMemo(() => createSchema(t), [t]); - - const form = useForm({ resolver: zodResolver(schema) }); -} -``` - -## Language Switching - -```typescript -import LocaleSwitcher from '@/components/locale-switcher'; - -export function Footer() { - return
; -} -``` - -## Extension Translations - -Extensions use `extPascalCase` namespace: - -``` -src/extensions/my-extension/locales/ -├── en-US/translations.json -└── it-IT/translations.json -``` - -```typescript -const {t} = useTranslation('extMyExtension'); -t('welcome'); -``` - -**Reference:** See README-I18N.md for complete internationalization documentation. diff --git a/packages/b2c-dx-mcp/content/sfnext/page-designer.md b/packages/b2c-dx-mcp/content/sfnext/page-designer.md deleted file mode 100644 index 29a7b72ca..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/page-designer.md +++ /dev/null @@ -1,78 +0,0 @@ -# Page Designer Integration - -## Overview - -Page Designer is Commerce Cloud's visual editor in Business Manager: merchants build and edit storefront pages (home, category, etc.) without code. The app gets page structure (regions, components, attributes) from the **Shopper Experience API** and renders it via a **component registry** and ``. - -## Concepts - -| Concept | Role | -|--------|------| -| **Page** | Fetched in route loaders via `fetchPageFromLoader(args, { pageId })`. | -| **Region** | Named area on a page; rendered with ``. | -| **Component** | Content block (hero, grid, carousel, etc.) with a `typeId` and attributes; registered in `@/lib/registry`. | -| **Registry** | Static registry in `@/lib/static-registry.ts` is **auto-generated** by the staticRegistry Vite plugin — do not edit by hand. | - -## Which pages use Page Designer - -Only **content pages** that merchants edit in Business Manager use Page Designer; cart, checkout, account, and auth do not. - -| Uses Page Designer | Does not | -|--------------------|----------| -| Home (`pageId: 'homepage'`), Category/PLP (`plp`), Search (`search`), Product/PDP (`pdp`) | Cart, Checkout, Account, Order confirmation, Auth | - -To add a new content page: define a page type and ID in Commerce Cloud, then in your route use `fetchPageFromLoader(args, { pageId })` and `collectComponentDataPromises(args, pagePromise)`, and render `` for each region. - -## Getting started - -### Route (new Page Designer page) - -- **In the loader**: call `fetchPageFromLoader(args, { pageId: '...' })` and `collectComponentDataPromises(args, pagePromise)`; return `page` and `componentData` (keep loaders synchronous; return promises for streaming). -- **In the layout**: for each region, render `` with optional `fallbackElement` and `errorElement` for Suspense/error boundaries. -- **On the route module**: add `@PageType({ name, description, supportedAspectTypes })` and `@RegionDefinition([{ id, name, description, maxComponents }])` so Business Manager knows the page type and regions. Example routes: home (`_app._index.tsx`), category/PLP (`_app.category.$categoryId.tsx`). - -### Component (new Page Designer component) - -- **Add a metadata class** with `@Component('typeId', { name, description })` and `@AttributeDefinition()` (and optionally `@AttributeDefinition({ type: 'image' })`, `type: 'url'`, etc.) for each prop you want editable in Page Designer. Use `@RegionDefinition([...])` if the component has nested regions (e.g. a grid with slots). -- **Implement the React component** so it accepts those props (and strips Page Designer–only props like `component`, `page`, `componentData`, `designMetadata` before spreading to the DOM). If the component needs server data (e.g. products for a carousel), export a `loader({ componentData, context })` and optionally a `fallback` component; the registry calls the loader during `collectComponentDataPromises` and passes resolved data as the `data` prop. -- **Use the MCP tool `sfnext_add_page_designer_decorator`** to generate decorators instead of writing them by hand. Example components: `components/hero/index.tsx`, `components/content-card/index.tsx`, `components/product-carousel/index.tsx`. - -### After changes - -- **Rebuild the app** so the static registry (`lib/static-registry.ts`) is regenerated by the staticRegistry Vite plugin. Do not edit the static registry by hand. - -### Design mode - -- **Edit** and **Preview** mode are detected from the request via `isDesignModeActive(request)` and `isPreviewModeActive(request)` from `@salesforce/storefront-next-runtime/design/mode`. The root layout exposes `pageDesignerMode` in loader data (`'EDIT' | 'PREVIEW' | undefined`) so the tree can adapt (e.g. show outlines, disable interactions) when running inside Page Designer. - - -## MCP tools (recommended) - -Use the **B2C DX MCP server** for Page Designer work instead of hand-writing decorators and metadata. Configure the B2C DX MCP server in your IDE (e.g. in MCP settings) so these tools are available. - -### 1. `sfnext_add_page_designer_decorator` (STOREFRONTNEXT toolset) - -Adds Page Designer decorators to an existing React component so it can be used in Business Manager. The tool analyzes the component, picks suitable props, infers types (e.g. `*Url`/`*Link` → url, `*Image` → image, `is*`/`show*` → boolean), and generates `@Component('typeId', { name, description })`, `@AttributeDefinition()` on a metadata class, and optionally `@RegionDefinition([...])` for nested regions. It skips complex or UI-only props (e.g. className, style, callbacks). - -- **Auto mode** (fast): Ask in your IDE: *"Add Page Designer support to [ComponentName] with autoMode"*. The tool runs in one turn with no prompts. -- **Interactive mode** (control): Ask *"Add Page Designer support to [ComponentName]"* and answer questions about typeId, which props to expose, types, and optional nested regions. - -### 2. `cartridge_deploy` (CARTRIDGES toolset) - -Packages the cartridge, uploads it to Commerce Cloud via WebDAV, and unpacks it on the server. **Run `pnpm generate:cartridge` or `pnpm build` before `cartridge_deploy`.** Requires Commerce Cloud credentials (e.g. `dw.json` or explicit config). Use after generating metadata so the new/updated metadata is available in Business Manager. - -### Typical workflow - -1. **`sfnext_add_page_designer_decorator`** — Add decorators to the component (use autoMode for a quick first pass). -2. **Rebuild** — The static registry is auto-generated at build time by the staticRegistry Vite plugin. -3. **`cartridge_deploy`** — Deploy to Commerce Cloud so merchants can use the component in Business Manager. - -## Best Practices - -1. **Keep loaders synchronous**: Return promises for Page Designer pages to enable streaming -2. **Use registry for components**: Register all Page Designer components with proper `typeId` -3. **Handle design mode**: Adapt UI when `pageDesignerMode` is `'EDIT'` or `'PREVIEW'` -4. **Rebuild after registry changes**: Static registry is generated at build time -5. **Use MCP tools**: Leverage `sfnext_add_page_designer_decorator` for faster development - -**Reference:** See README.md for complete Page Designer documentation and MCP tool setup. diff --git a/packages/b2c-dx-mcp/content/sfnext/performance.md b/packages/b2c-dx-mcp/content/sfnext/performance.md deleted file mode 100644 index bc5e378dc..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/performance.md +++ /dev/null @@ -1,80 +0,0 @@ -# Performance Optimization - -## Bundle Size Limits - -The application enforces strict bundle size limits defined in `package.json` under the `bundlesize` configuration. Refer to `package.json` for the complete list of limits. - -**Check bundle size:** - -```bash -pnpm bundlesize:test # Verify limits -pnpm bundlesize:analyze # Analyze composition -``` - -## Built-in Metrics - -Enable in `config.server.ts`: - -```typescript -{ - performance: { - metrics: { - serverPerformanceMetricsEnabled: true, - clientPerformanceMetricsEnabled: true, - serverTimingHeaderEnabled: false // Debug only - } - } -} -``` - -Tracks: - -- SSR operations and rendering time -- SCAPI API calls with parallelization -- Authentication operations -- Client-side navigations - -## Best Practices - -### 1. Parallel Data Fetching - -**Key principle:** Return all promises simultaneously in loaders to enable parallel requests. Avoid sequential `await` calls. - -**Reference:** See `data-fetching` section for detailed parallel vs sequential patterns and code examples. - -### 2. Image Optimization - -Use the `DynamicImage` component with WebP format: - -```typescript -import { DynamicImage } from '@/components/dynamic-image'; - - -``` - -### 3. Progressive Streaming - -**Key principle:** Use synchronous loaders returning promises to enable progressive streaming. Await only critical data, stream the rest. - -**Reference:** See `data-fetching` section for detailed streaming patterns including mixed strategies (awaited + streamed). - -### 4. Lighthouse Audits - -Monitor performance metrics: - -- Preload critical CSS -- Use WebP images by default -- Lazy load below-the-fold content -- Optimize font loading - -```bash -pnpm lighthouse:ci # Run Lighthouse CI -``` - -**Reference:** See README-PERFORMANCE.md for complete performance optimization documentation. diff --git a/packages/b2c-dx-mcp/content/sfnext/pitfalls.md b/packages/b2c-dx-mcp/content/sfnext/pitfalls.md deleted file mode 100644 index 916a45bea..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/pitfalls.md +++ /dev/null @@ -1,141 +0,0 @@ -# Common Pitfalls - -## 1. Using Client Loaders/Actions - -```typescript -// ❌ NEVER USE - Client loaders are not permitted -export function clientLoader() { ... } - -// ❌ NEVER USE - Client actions are not permitted -export function clientAction() { ... } - -// ✅ REQUIRED - Server-only data loading -export function loader({ context }: LoaderFunctionArgs) { - const clients = createApiClients(context); - return { product: clients.shopperProducts.getProduct({...}) }; -} - -// ✅ REQUIRED - Server-only actions -export async function action({ request, context }: ActionFunctionArgs) { - const clients = createApiClients(context); - // Handle mutation on server -} -``` - -**Decision tree:** - -```text -Need data for page render? -└─ Use server `loader` - -Need to handle mutations (form submissions, cart updates)? -└─ Use server `action` - -Need on-demand fetching after page load? -└─ Use `useScapiFetcher` (search, modals, infinite scroll) -``` - -**Key Point:** ALL SCAPI requests happen on the server: -- `loader`: Runs on server, SCAPI direct (prod) or proxied (dev) -- `action`: Runs on server, handles mutations securely -- `useScapiFetcher`: Triggers server route that calls SCAPI - -## 2. Module-Level i18n in Schemas - -```typescript -// ❌ RACE CONDITION -const schema = z.object({ - email: z.string().email(t('error')), -}); - -// ✅ FACTORY PATTERN -export const createSchema = (t: TFunction) => { - return z.object({ - email: z.string().email(t('error')), - }); -}; -``` - -## 3. Using Async Loaders (Blocks Page Transitions) - -```typescript -// ❌ BLOCKS PAGE TRANSITIONS - Async loader with await -export async function loader({ context }: LoaderFunctionArgs) { - const product = await fetchProduct(); // Blocks! - const reviews = await fetchReviews(); // Blocks! - return { product, reviews }; -} - -// ✅ NON-BLOCKING - Synchronous loader returning promises -export function loader({ context }: LoaderFunctionArgs): PageData { - return { - product: fetchProduct(), // Streams progressively - reviews: fetchReviews(), // Streams progressively - }; -} -``` - -**Key insight:** Defining loaders as `async` and using `await` causes the entire page transition to block until all data resolves. Use synchronous loaders returning promises for streaming. - -**Reference:** See `data-fetching` section for comprehensive loader patterns including mixed strategies (awaited + streamed). - -## 4. Modifying shadcn/ui - -```typescript -// ❌ NEVER modify src/components/ui/ - -// ✅ Create wrapper -import { Button } from '@/components/ui/button'; -export function MyButton(props) { - return - - -// shadcn/ui: Add via npx shadcn@latest add -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; -``` - -**See `styling` section for:** Tailwind CSS 4 rules, Shadcn/ui components, dark mode, responsive design - ---- - -## 🔍 Get Detailed Guidelines - -Use the `sfnext_get_guidelines` MCP tool with specific sections: - -```json -{ - "sections": ["data-fetching", "components", "testing"] -} -``` - -**Available sections:** -- `data-fetching` - Loaders, actions, useScapiFetcher, data flow -- `components` - createPage HOC, Suspense, file organization -- `styling` - Tailwind CSS 4, Shadcn/ui, styling guidelines -- `testing` - Vitest, Storybook, coverage requirements -- `auth` - Authentication and session management -- `config` - Configuration system -- `i18n` - Internationalization patterns -- `state-management` - Client-side state with Zustand -- `page-designer` - Page Designer integration -- `performance` - Optimization techniques -- `extensions` - Extension development -- `pitfalls` - Common mistakes to avoid - ---- - -**When in doubt:** -1. Check existing code for similar examples -2. Use the MCP tool to get detailed section guidance -3. Follow architectural principles: server-only, streaming, TypeScript diff --git a/packages/b2c-dx-mcp/content/sfnext/state-management.md b/packages/b2c-dx-mcp/content/sfnext/state-management.md deleted file mode 100644 index ea6031a5d..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/state-management.md +++ /dev/null @@ -1,75 +0,0 @@ -# Client-Side State Management - -## Zustand Store Pattern - -Storefront Next uses Zustand for client-side state (basket, wishlist): - -```typescript -// src/middlewares/basket.client.ts -import {create} from 'zustand'; - -interface BasketStore { - basket: Basket | null; - setBasket: (basket: Basket | null) => void; - clearBasket: () => void; -} - -export const useBasketStore = create((set) => ({ - basket: null, - setBasket: (basket) => set({basket}), - clearBasket: () => set({basket: null}), -})); -``` - -## Context Integration - -Access Zustand state via context helpers: - -```typescript -import { getBasket, updateBasket } from '@/middlewares/basket.client'; - -// In clientLoader -export const clientLoader: ClientLoaderFunction = ({ context }) => { - const basket = getBasket(context); - return { basket, itemCount: basket?.productItems?.length ?? 0 }; -}; - -// In components -function CartIcon() { - const basket = getBasket(context); - return ; -} -``` - -## Update Pattern - -After mutations, update the store: - -```typescript -export async function clientAction({request, context}: ActionFunctionArgs) { - const formData = await request.formData(); - const productId = formData.get('productId') as string; - - const basket = getBasket(context); - const clients = createApiClients(context); - - const {data: updatedBasket} = await clients.shopperBasketsV2.addItemToBasket({ - params: {path: {basketId: basket.basketId}}, - body: [{productId, quantity: 1}], - }); - - // Update Zustand store - updateBasket(context, updatedBasket); - - return Response.json({success: true, basket: updatedBasket}); -} -``` - -## Best Practices - -1. **Use for ephemeral client state**: Shopping cart, UI state, temporary selections -2. **Don't duplicate server state**: Prefer React Router loaders for server data -3. **Keep stores focused**: Separate stores for basket, wishlist, etc. -4. **Sync with server**: Update store after successful mutations - -For full documentation on client-side state management patterns, see the Zustand documentation and React Router state management patterns. diff --git a/packages/b2c-dx-mcp/content/sfnext/styling.md b/packages/b2c-dx-mcp/content/sfnext/styling.md deleted file mode 100644 index 76968ce11..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/styling.md +++ /dev/null @@ -1,51 +0,0 @@ -# Styling Guidelines - -## Rules - -- ✅ **Use Tailwind utility classes** in component JSX -- ✅ **Use `cn()` utility** for conditional classes (`import { cn } from '@/lib/utils'`) -- ✅ **Follow mobile-first** responsive patterns (`md:`, `lg:` breakpoints) -- ❌ **NO inline styles** (`style={{...}}`) -- ❌ **NO CSS modules** (`.module.css` files) -- ❌ **NO separate CSS files** for component styles -- ✅ **Custom CSS** only in `src/app.css` for global styles and theme configuration - -## Shadcn/ui Components - -**Adding components:** - -```bash -npx shadcn@latest add -``` - -**Rules:** - -- ✅ **DO** customize components by editing files in `src/components/ui/` -- ❌ **DON'T** create custom components inside `src/components/ui/` -- ❌ **DON'T** manually copy components (use CLI instead) - -## Dark Mode - -Dark mode is supported via CSS variables and the `.dark` class. Theme variables automatically adapt: - -```typescript -
- -
-``` - -## Responsive Design - -Follow mobile-first responsive design: - -```typescript -
- {/* Content */} -
-``` - ---- - -**Reference:** See [README-UI-STYLING.md](docs/README-UI-STYLING.md) in your project for complete UI and styling documentation. diff --git a/packages/b2c-dx-mcp/content/sfnext/testing.md b/packages/b2c-dx-mcp/content/sfnext/testing.md deleted file mode 100644 index 5e01804ff..000000000 --- a/packages/b2c-dx-mcp/content/sfnext/testing.md +++ /dev/null @@ -1,232 +0,0 @@ -# Testing Strategy - -## Unit Tests (Vitest) - -This project uses **Vitest** for unit tests, running under Vite with jsdom as the default test environment. - -### Test File Organization - -Tests live alongside source files with `.test.ts` or `.test.tsx` extension: - -```typescript -// src/components/product-card/product-card.test.tsx -import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { ProductCard } from './product-card'; -import { mockProduct } from '@/test-utils/mocks'; - -describe('ProductCard', () => { - it('renders product name', () => { - render(); - expect(screen.getByText(mockProduct.productName)).toBeInTheDocument(); - }); -}); -``` - -### Test Utilities - -Test utilities are available in `src/test-utils/`: -- `config.ts` - Mock configuration objects and ConfigProvider wrappers -- `context-provider-utils.ts` - Context provider helpers for testing -- `context-provider.tsx` - Test context providers - -### Running Tests - -```bash -# Run all tests with coverage -pnpm test - -# Open Vitest UI (interactive test runner) -pnpm test:ui - -# Watch mode (re-run on file changes) -pnpm test:watch - -# Generate coverage report -pnpm test -# Coverage report outputs to console and coverage/ directory -``` - -### Coverage Requirements - -Coverage thresholds are enforced in `vitest.thresholds.ts`: -- Lines: 73% -- Statements: 73% -- Functions: 86% -- Branches: 87% - -These thresholds represent minimum values that must not be undershot. They should be raised regularly to reflect current status. - -### Testing Libraries - -- **@testing-library/react** - React component testing -- **@testing-library/jest-dom** - Custom Jest DOM matchers -- **@testing-library/user-event** - User interaction simulation -- **@vitest/coverage-v8** - Code coverage -- **@vitest/ui** - Interactive test UI - -## Storybook Testing - -Every reusable component should have a Storybook story file (`.stories.tsx`). - -### Story Structure - -```typescript -// src/components/product-card/product-card.stories.tsx -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { within, expect } from 'storybook/test'; -import { waitForStorybookReady } from '@storybook/test-utils'; -import { ProductCard } from './product-card'; -import { ConfigProvider } from '@/config/context'; -import { mockConfig } from '@/test-utils/config'; -import { mockProduct } from '@/test-utils/mocks'; - -const meta: Meta = { - title: 'Components/ProductCard', - component: ProductCard, - tags: ['autodocs', 'interaction'], - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - product: mockProduct, - }, - play: async ({ canvasElement }) => { - await waitForStorybookReady(canvasElement); - const canvas = within(canvasElement); - await expect(canvas.getByText(mockProduct.productName)).toBeInTheDocument(); - }, -}; -``` - -### Storybook Commands - -```bash -# Development server (port 6006) -pnpm storybook - -# Build static Storybook -pnpm build-storybook - -# Snapshot tests (visual regression) -pnpm test-storybook:snapshot -pnpm test-storybook:snapshot:update # Update snapshots - -# Interaction tests (play functions) -pnpm test-storybook:interaction -pnpm test-storybook:static:interaction # Against static build - -# Accessibility tests -pnpm test-storybook:a11y -pnpm test-storybook:static:a11y # Against static build - -# Generate story tests with coverage -pnpm generate:story-tests:coverage -``` - -### Storybook Features - -- **@storybook/addon-docs** - Automatic documentation generation -- **@storybook/addon-a11y** - Accessibility testing and validation -- **@storybook/addon-vitest** - Integration with Vitest -- **@storybook/test-runner** - Automated testing (interaction, a11y) -- **Viewport Toolbar** - Built-in toolbar for testing different screen sizes - -> **Important**: Use Storybook's built-in viewport toolbar instead of creating separate Mobile/Tablet/Desktop stories. Use the viewport selector in the Storybook toolbar to test components at different screen sizes. - -### Story Tags - -- `autodocs` - Enable automatic documentation -- `interaction` - Include in interaction test runs -- `skip-a11y` - Exclude from a11y tests (use sparingly) - -## Testing Best Practices - -### Component Testing - -1. **Colocate tests** - Keep test files next to source files -2. **Use test utilities** - Leverage `@/test-utils` for mocks and providers -3. **Mock external dependencies** - Use `vi.mock()` for API clients, context providers, etc. -4. **Test user interactions** - Use `@testing-library/user-event` for realistic interactions -5. **Test accessibility** - Use Storybook a11y addon and test-runner - -### Storybook Stories - -1. **Multiple variants** - Create stories for different states (Default, Loading, Error, etc.) -2. **Play functions** - Use `play` functions for interaction testing -3. **Decorators** - Wrap stories with necessary providers (ConfigProvider, etc.) -4. **Documentation** - Include component descriptions and prop documentation -5. **Viewport testing** - Use built-in viewport toolbar, not separate stories - -### Route Testing - -Route tests should mock: -- Loader functions and their return values -- Action functions -- React Router context -- API clients - -Example: - -```typescript -// src/routes/_app.product.$productId.test.tsx -import { describe, test, expect, vi } from 'vitest'; -import { render } from '@testing-library/react'; - -vi.mock('@/components/product-view', () => ({ - default: ({ product }: any) => ( -
-
{product?.name}
-
- ), -})); - -// Test route component... -``` - -## Testing Recommendations - -### SEO Crawler Emulation - -- Use **Googlebot user agent** in network conditions to emulate SEO crawler behavior -- This changes React Router's streaming strategy and shows what crawlers see for SSR -- Helps verify server-side rendering works correctly for search engines - -### Coverage Goals - -- Maintain coverage above thresholds defined in `vitest.thresholds.ts` -- Raise thresholds regularly as coverage improves -- Focus on testing critical paths and user-facing functionality - -### Test Organization - -``` -src/ -├── components/ -│ ├── product-card/ -│ │ ├── index.tsx # Component -│ │ ├── index.test.tsx # Unit tests -│ │ └── index.stories.tsx # Storybook stories -├── routes/ -│ ├── _app.product.$productId.tsx -│ └── _app.product.$productId.test.tsx -└── test-utils/ # Shared test utilities - ├── config.ts - └── context-provider-utils.ts -``` - -## References - -- **README-TESTS.md** - Complete testing documentation -- **.storybook/README-STORYBOOK.md** - Storybook setup and usage guide -- **vitest.thresholds.ts** - Coverage threshold definitions diff --git a/packages/b2c-dx-mcp/src/commands/mcp.ts b/packages/b2c-dx-mcp/src/commands/mcp.ts index e99daed5d..e90fe044e 100644 --- a/packages/b2c-dx-mcp/src/commands/mcp.ts +++ b/packages/b2c-dx-mcp/src/commands/mcp.ts @@ -149,11 +149,12 @@ import type {ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config'; import {EnvSource, readProjectEnvironment} from '@salesforce/b2c-tooling-sdk/config'; import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; import {B2CDxMcpServer} from '../server.js'; -import {Services} from '../services.js'; +import {Services, type ServicesResolutionInputs} from '../services.js'; import {ServerContext} from '../server-context.js'; import {registerToolsets} from '../registry.js'; import {TOOLSETS, type StartupFlags} from '../utils/index.js'; import type {ProjectContextInput} from '../tools/project-context.js'; +import type {ServicesLoader} from '../tools/adapter.js'; /** * oclif Command that starts the B2C DX MCP server. @@ -275,14 +276,17 @@ export default class McpServerCommand extends BaseCommand { const mrt = extractMrtFlags(this.flags as Record); - const baseOptions = this.getBaseConfigOptions(); - const effectiveProjectDirectory = projectContext?.projectDirectory ?? baseOptions.projectDirectory; + const baseOptions = this.flags ? this.getBaseConfigOptions() : {}; + const effectiveProjectDirectory = path.resolve( + projectContext?.projectDirectory ?? baseOptions.projectDirectory ?? process.cwd(), + ); const projectEnvironment = this.loadProjectEnvironment(effectiveProjectDirectory); const projectConfigPath = projectEnvironment?.SFCC_CONFIG; @@ -300,10 +304,9 @@ export default class McpServerCommand extends BaseCommand { const config = await this.loadConfiguration(projectContext); - const effectiveProjectDirectory = projectContext?.projectDirectory ?? this.flags?.['project-directory']; + const baseOptions = this.flags ? this.getBaseConfigOptions() : {}; + const configuredProjectDirectory = baseOptions.projectDirectory; + const effectiveProjectDirectory = path.resolve( + projectContext?.projectDirectory ?? configuredProjectDirectory ?? process.cwd(), + ); const projectEnvironment = this.loadProjectEnvironment(effectiveProjectDirectory); - return Services.fromResolvedConfig(config, projectEnvironment); + const projectConfigPath = projectEnvironment?.SFCC_CONFIG; + let primaryConfiguration: ServicesResolutionInputs['primaryConfiguration']; + if (projectContext?.configPath) { + primaryConfiguration = { + path: path.resolve(effectiveProjectDirectory, projectContext.configPath), + source: 'argument', + }; + } else if (baseOptions.configPath) { + primaryConfiguration = {path: path.resolve(baseOptions.configPath), source: 'server'}; + } else if (projectConfigPath) { + primaryConfiguration = { + path: path.isAbsolute(projectConfigPath) + ? projectConfigPath + : path.resolve(effectiveProjectDirectory, projectConfigPath), + source: 'projectEnvironment', + }; + } else { + primaryConfiguration = {path: path.join(effectiveProjectDirectory, 'dw.json'), source: 'projectDirectory'}; + } + + return Services.fromResolvedConfig(config, projectEnvironment, { + projectDirectory: { + path: effectiveProjectDirectory, + source: projectContext?.projectDirectory ? 'argument' : configuredProjectDirectory ? 'config' : 'cwd', + }, + primaryConfiguration, + }); } /** @@ -401,7 +434,15 @@ export default class McpServerCommand extends BaseCommand = { // (A SFRA workspace also matches the `cartridges` marker; the union dedupes.) sfra: ['CARTRIDGES'], 'pwa-kit-v3': ['PWAV3', 'MRT'], - // Note: STOREFRONTNEXT_DEPRECATED is intentionally NOT auto-activated. The - // legacy sfnext_* tools are superseded by the storefront-next agent-skills - // plugins and must be explicitly requested via --toolsets. 'storefront-next': ['STOREFRONTNEXT', 'MRT', 'CARTRIDGES'], }; @@ -97,7 +93,7 @@ export type ToolRegistry = Record; * @returns Complete tool registry */ export function createToolRegistry( - loadServices: (projectContext?: ProjectContextInput) => Promise | Services, + loadServices: ServicesLoader, serverContext?: ServerContext, detectedWorkspaces: readonly ProjectType[] = [], enabledDocCategories?: readonly DocCategory[], @@ -109,7 +105,6 @@ export function createToolRegistry( PWAV3: [], SCAPI: [], STOREFRONTNEXT: [], - STOREFRONTNEXT_DEPRECATED: [], }; // Collect all tools from all factories @@ -120,7 +115,6 @@ export function createToolRegistry( ...createMrtTools(loadServices), ...createPwav3Tools(loadServices), ...createScapiTools(loadServices), - ...createStorefrontNextTools(loadServices), ]; // Organize tools by their declared toolsets (supports multi-toolset) @@ -146,6 +140,7 @@ export function createToolRegistry( * without descending into unrelated deep trees. */ const DISCOVERY_MAX_DEPTH = 5; +const MCP_PACKAGE_DIRECTORY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); /** * Returns true when `dir` is a location that should never be recursively @@ -160,7 +155,7 @@ function isUnscannableRoot(dir: string): boolean { return true; } const home = os.homedir(); - return Boolean(home) && path.resolve(home) === resolved; + return (Boolean(home) && path.resolve(home) === resolved) || resolved === MCP_PACKAGE_DIRECTORY; } /** @@ -196,7 +191,7 @@ async function detectProjectTypes(flags: StartupFlags): Promise { if (isUnscannableRoot(projectDirectory)) { logger.warn( {projectDirectory}, - 'Project directory is a home or root directory; skipping workspace auto-discovery. ' + + 'Project directory is a home, filesystem root, or MCP installation directory; skipping workspace auto-discovery. ' + 'Set --project-directory or SFCC_PROJECT_DIRECTORY to the project path to enable it.', ); return []; @@ -237,7 +232,7 @@ const REGISTERED_SERVERS = new WeakSet(); export async function registerToolsets( flags: StartupFlags, server: B2CDxMcpServer, - loadServices: (projectContext?: ProjectContextInput) => Promise | Services, + loadServices: ServicesLoader, serverContext?: ServerContext, ): Promise { if (REGISTERED_SERVERS.has(server)) { @@ -299,14 +294,9 @@ export async function registerToolsets( ); } - // Determine which toolsets to enable. - // `ALL` expands to every toolset EXCEPT deprecated ones — deprecated toolsets - // must always be named explicitly. + // Determine which toolsets to enable. `ALL` expands to every toolset. const validToolsets = toolsets.filter((t): t is Toolset => TOOLSETS.includes(t as Toolset)); - const allNonDeprecatedToolsets = TOOLSETS.filter( - (t) => !DEPRECATED_TOOLSETS.includes(t as (typeof DEPRECATED_TOOLSETS)[number]), - ); - const toolsetsToEnable = new Set(toolsets.includes(ALL_TOOLSETS) ? allNonDeprecatedToolsets : validToolsets); + const toolsetsToEnable = new Set(toolsets.includes(ALL_TOOLSETS) ? TOOLSETS : validToolsets); // Auto-discovery: only when no valid toolsets AND no valid individual tools // were provided. This handles both (1) no flags provided, and (2) all diff --git a/packages/b2c-dx-mcp/src/services.ts b/packages/b2c-dx-mcp/src/services.ts index 81478b2a5..c95136bbc 100644 --- a/packages/b2c-dx-mcp/src/services.ts +++ b/packages/b2c-dx-mcp/src/services.ts @@ -49,6 +49,7 @@ import os from 'node:os'; import type {B2CInstance} from '@salesforce/b2c-tooling-sdk'; import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth'; import type {ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config'; +import type {ConfigurationResolutionSource, ProjectDirectoryInfo, ToolResolution} from './tools/project-context.js'; import { createCustomApisClient, createMetricsClient, @@ -87,6 +88,44 @@ export interface ServicesOptions { resolvedConfig: ResolvedB2CConfig; /** Project-scoped environment parsed from the effective project's .env file. */ projectEnvironment?: Readonly>; + /** Inputs needed to attribute project and primary-configuration selection. */ + resolution?: ServicesResolutionInputs; +} + +/** Resolution inputs captured centrally by the MCP command for one tool call. */ +export interface ServicesResolutionInputs { + /** Effective project directory and its selection source. */ + projectDirectory: ProjectDirectoryInfo; + /** Primary configuration candidate selected before the SDK resolver runs. */ + primaryConfiguration?: { + path: string; + source: Exclude; + }; +} + +function createToolResolution(config: ResolvedB2CConfig, inputs?: ServicesResolutionInputs): ToolResolution { + const configuredProjectDirectory = config.values.projectDirectory; + const projectDirectory = + inputs?.projectDirectory ?? + (configuredProjectDirectory + ? {path: configuredProjectDirectory, source: 'config' as const} + : {path: process.cwd(), source: 'cwd' as const}); + const dwJsonSource = config.sources.find((source) => source.name === 'DwJsonSource' && source.location); + const configurationSource: ConfigurationResolutionSource = dwJsonSource + ? dwJsonSource.scope === 'global' + ? 'globalDefault' + : (inputs?.primaryConfiguration?.source ?? 'projectDirectory') + : 'none'; + + return { + projectDirectory, + configuration: { + ...(dwJsonSource?.location ? {path: dwJsonSource.location} : {}), + source: configurationSource, + ...(config.values.instanceName ? {instanceName: config.values.instanceName} : {}), + ...(config.values.hostname ? {hostname: config.values.hostname} : {}), + }, + }; } /** @@ -126,6 +165,7 @@ export class Services { * @private */ private readonly projectEnvironment: Readonly>; + private readonly resolution: ToolResolution; private readonly resolvedConfig: ResolvedB2CConfig; public constructor(opts: ServicesOptions) { @@ -133,6 +173,7 @@ export class Services { this.mrtConfig = opts.mrtConfig ?? {}; this.resolvedConfig = opts.resolvedConfig; this.projectEnvironment = opts.projectEnvironment ?? {}; + this.resolution = createToolResolution(opts.resolvedConfig, opts.resolution); } /** @@ -150,6 +191,7 @@ export class Services { public static fromResolvedConfig( config: ResolvedB2CConfig, projectEnvironment?: Readonly>, + resolution?: ServicesResolutionInputs, ): Services { // Build MRT config using factory methods const mrtConfig: MrtConfig = { @@ -167,6 +209,7 @@ export class Services { mrtConfig, resolvedConfig: config, projectEnvironment, + resolution, }); } @@ -299,6 +342,19 @@ export class Services { return os.platform(); } + /** + * Get compact project and selected-configuration provenance for MCP results. + * Detailed source graphs remain available through {@link getResolvedConfig} + * for the config_inspect tool. + */ + public getResolution(): ToolResolution { + return { + projectDirectory: {...this.resolution.projectDirectory}, + ...(this.resolution.configuration ? {configuration: {...this.resolution.configuration}} : {}), + ...(this.resolution.directories ? {directories: {...this.resolution.directories}} : {}), + }; + } + /** * Get the resolved configuration (values, sources, warnings). * diff --git a/packages/b2c-dx-mcp/src/tools/adapter.ts b/packages/b2c-dx-mcp/src/tools/adapter.ts index c9e5075b2..bae3b6e77 100644 --- a/packages/b2c-dx-mcp/src/tools/adapter.ts +++ b/packages/b2c-dx-mcp/src/tools/adapter.ts @@ -76,7 +76,20 @@ import type {B2CInstance} from '@salesforce/b2c-tooling-sdk'; import type {McpTool, ToolResult, Toolset} from '../utils/index.js'; import type {Services, MrtConfig} from '../services.js'; import type {ServerContext} from '../server-context.js'; -import {projectContextInputSchema, type ProjectContextInput} from './project-context.js'; +import { + createProjectContextInputSchema, + type DirectoryResolutionInfo, + type ProjectContextDefaults, + type ProjectContextInput, + type ProjectContextKind, + type ToolResolution, +} from './project-context.js'; + +/** Services loader enriched with registration-time project fallback provenance. */ +export interface ServicesLoader { + (projectContext?: ProjectContextInput): Promise | Services; + projectContextDefaults?: ProjectContextDefaults; +} /** * Context provided to tool execute functions. @@ -107,6 +120,12 @@ export interface ToolExecutionContext { * Created once at server startup and shared across all tool invocations. */ serverContext?: ServerContext; + + /** Compact resolution provenance that will be attached to this tool result. */ + resolution: ToolResolution; + + /** Record a specialized directory resolved by the tool. */ + setResolvedDirectory: (name: string, value: DirectoryResolutionInfo) => void; } /** @@ -152,6 +171,13 @@ export interface ToolAdapterOptions { */ usesProjectContext?: boolean; + /** + * Whether this tool selects project configuration without requiring a + * B2CInstance or MRT auth object (for example config_inspect or SCAPI clients). + * Adds projectDirectory, configPath, and instanceName to the public schema. + */ + usesConfigurationContext?: boolean; + /** * Execute function that performs the tool's operation. * Receives validated input and a context with B2CInstance and/or auth based on requirements. @@ -218,6 +244,27 @@ export function jsonResult(data: unknown, indent = 2): ToolResult { }; } +/** Attach compact resolution provenance while preserving existing tool output. */ +export function attachResolution(result: ToolResult, resolution: ToolResolution): ToolResult { + let content = result.content; + let structuredContent: Record = {...result.structuredContent, resolution}; + + if (content.length === 1 && content[0]?.type === 'text') { + try { + const parsed = JSON.parse(content[0].text) as unknown; + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const output = {...(parsed as Record), resolution}; + content = [{...content[0], text: JSON.stringify(output, null, 2)}]; + structuredContent = {...output, ...result.structuredContent, resolution}; + } + } catch { + // Plain-text tools expose resolution through structuredContent only. + } + } + + return {...result, content, structuredContent}; +} + /** * Formats Zod validation errors into a human-readable string. * @@ -269,7 +316,7 @@ function formatZodErrors(error: z.ZodError): string { */ export function createToolAdapter( options: ToolAdapterOptions, - loadServices: (projectContext?: ProjectContextInput) => Promise | Services, + loadServices: ServicesLoader, serverContext?: ServerContext, ): McpTool { const { @@ -281,13 +328,22 @@ export function createToolAdapter( requiresInstance = false, requiresMrtAuth = false, usesProjectContext = false, + usesConfigurationContext = false, execute, formatOutput, } = options; - const effectiveUsesProjectContext = usesProjectContext || requiresInstance || requiresMrtAuth; - const effectiveInputSchema = effectiveUsesProjectContext - ? {...projectContextInputSchema, ...inputSchema} + const projectContextKind: ProjectContextKind | undefined = + requiresInstance || requiresMrtAuth || usesConfigurationContext + ? 'configuration' + : usesProjectContext + ? 'project' + : undefined; + const effectiveInputSchema = projectContextKind + ? { + ...createProjectContextInputSchema(projectContextKind, loadServices.projectContextDefaults), + ...inputSchema, + } : inputSchema; // Create Zod schema from inputSchema definition @@ -307,18 +363,27 @@ export function createToolAdapter( return errorResult(`Invalid input: ${formatZodErrors(parseResult.error)}`); } const args = parseResult.data as TInput; + let resolution: ToolResolution | undefined; try { // 2. Load Services to get fresh configuration (re-reads config files) - const projectContext = effectiveUsesProjectContext ? (args as ProjectContextInput) : undefined; + const projectContext = projectContextKind ? (args as ProjectContextInput) : undefined; const services = await loadServices(projectContext); + const executionResolution = services.getResolution(); + if (projectContextKind === 'project') { + delete executionResolution.configuration; + } + resolution = projectContextKind ? executionResolution : undefined; // 3. Get B2CInstance if required (loaded on each call) let b2cInstance: B2CInstance | undefined; if (requiresInstance) { if (!services.b2cInstance) { - return errorResult( - 'B2C instance error: Instance configuration required. Provide --server flag, set SFCC_SERVER environment variable, or configure dw.json', + return attachResolution( + errorResult( + 'B2C instance error: Instance configuration required. Provide --server flag, set SFCC_SERVER environment variable, or configure dw.json', + ), + executionResolution, ); } b2cInstance = services.b2cInstance; @@ -328,8 +393,11 @@ export function createToolAdapter( let mrtConfig: ToolExecutionContext['mrtConfig']; if (requiresMrtAuth) { if (!services.mrtConfig.auth) { - return errorResult( - 'MRT auth error: MRT API key required. Provide --api-key, set MRT_API_KEY environment variable, or configure ~/.mobify', + return attachResolution( + errorResult( + 'MRT auth error: MRT API key required. Provide --api-key, set MRT_API_KEY environment variable, or configure ~/.mobify', + ), + executionResolution, ); } mrtConfig = { @@ -346,15 +414,22 @@ export function createToolAdapter( mrtConfig, services, serverContext, + resolution: executionResolution, + setResolvedDirectory(name, value) { + executionResolution.directories ??= {}; + executionResolution.directories[name] = value; + }, }; const output = await execute(args, context); // 6. Format output - return formatOutput(output); + const result = formatOutput(output); + return resolution ? attachResolution(result, executionResolution) : result; } catch (error) { // Handle execution errors const message = error instanceof Error ? error.message : String(error); - return errorResult(`Execution error: ${message}`); + const result = errorResult(`Execution error: ${message}`); + return resolution ? attachResolution(result, resolution) : result; } }, }; diff --git a/packages/b2c-dx-mcp/src/tools/cartridges/index.ts b/packages/b2c-dx-mcp/src/tools/cartridges/index.ts index bccef04db..137347384 100644 --- a/packages/b2c-dx-mcp/src/tools/cartridges/index.ts +++ b/packages/b2c-dx-mcp/src/tools/cartridges/index.ts @@ -31,7 +31,9 @@ const CARTRIDGE_PATH_REMINDER = * Input type for cartridge_deploy tool. */ interface CartridgeDeployInput extends ProjectContextInput { - /** Path to directory containing cartridges (default: current directory) */ + /** Path to directory containing cartridges. */ + cartridgeDirectory?: string; + /** @deprecated Use cartridgeDirectory. */ directory?: string; /** Only deploy these cartridge names */ cartridges?: string[]; @@ -99,12 +101,17 @@ function createCartridgeDeployTool( requiresInstance: true, usesProjectContext: true, inputSchema: { + cartridgeDirectory: z + .string() + .optional() + .describe( + 'Optional cartridge discovery root. Relative paths resolve from projectDirectory. Defaults to projectDirectory.', + ), directory: z .string() .optional() .describe( - 'Path to directory to search for cartridges. Defaults to current project directory if not specified. ' + - 'The tool will recursively search this directory for .project files to identify cartridges.', + 'Deprecated alias for cartridgeDirectory. cartridgeDirectory takes precedence when both are supplied.', ), cartridges: z .array(z.string()) @@ -154,7 +161,12 @@ function createCartridgeDeployTool( // Resolve directory path: relative paths are resolved relative to project directory, absolute paths are used as-is const projectDirectory = context.services.resolveProjectDirectory(args.projectDirectory); - const directory = context.services.resolveWithProjectDirectory(args.directory, args.projectDirectory); + const directoryArgument = args.cartridgeDirectory ?? args.directory; + const directory = context.services.resolveWithProjectDirectory(directoryArgument, args.projectDirectory); + context.setResolvedDirectory('cartridgeDirectory', { + path: directory, + source: directoryArgument ? 'argument' : 'projectDirectory', + }); // Parse options const options: DeployOptions = { diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts index 6baa619c2..8e44a4afc 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/config-inspect.ts @@ -42,12 +42,12 @@ export function createConfigInspectTool(loadServices: () => Promise | description: 'Inspect the resolved B2C Commerce configuration the MCP server is using — instance hostname, auth, SCAPI, MRT, and other settings — along with which source (dw.json, environment variables, flags) provided each value. ' + 'Secrets (passwords, client secrets, API keys) are redacted by default. ' + - 'Pass projectDirectory and/or configPath to inspect the same project and dw.json-format file a CLI command would use. The output includes the effective projectDirectory and source provenance, which is useful for diagnosing why the server targets the wrong instance or cannot find a project. ' + + 'Pass projectDirectory, configPath, and/or instanceName to inspect the same project, configuration catalog, and named instance another tool would use. The detailed source graph is supplemented by the same compact resolution provenance returned by other configuration-aware tools. ' + 'Use this first when configuration seems wrong, auth is failing, or the server appears to be operating in the wrong directory.', toolsets: ['DIAGNOSTICS'], isGA: true, requiresInstance: false, - usesProjectContext: true, + usesConfigurationContext: true, inputSchema: { unmask: z .boolean() diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/debug-list-sessions.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/debug-list-sessions.ts index b79a136b4..667e29502 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/debug-list-sessions.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/debug-list-sessions.ts @@ -10,6 +10,7 @@ import type {ServerContext} from '../../server-context.js'; import {createToolAdapter, jsonResult} from '../adapter.js'; import {projectBreakpoint, type MappedBreakpoint} from '@salesforce/b2c-tooling-sdk/operations/debug'; import {getRegistry} from './session-registry.js'; +import type {ToolResolution} from '../project-context.js'; interface ListSessionsOutput { sessions: Array<{ @@ -20,6 +21,7 @@ interface ListSessionsOutput { session_cookie: null | {name: string; value: string}; created_at: string; last_activity_at: string; + resolution?: ToolResolution; }>; } @@ -52,6 +54,7 @@ export function createDebugListSessionsTool( session_cookie: dwsid ? {name: 'dwsid', value: dwsid} : null, created_at: new Date(entry.createdAt).toISOString(), last_activity_at: new Date(entry.lastActivityAt).toISOString(), + resolution: entry.resolution, }; }), }; diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/debug-start-session.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/debug-start-session.ts index 4fcfb2701..6492547d5 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/debug-start-session.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/debug-start-session.ts @@ -57,7 +57,7 @@ export function createDebugStartSessionTool( 'Optional cartridge discovery and debugger source-mapping root. Relative paths resolve from projectDirectory. Defaults to projectDirectory.', ), }, - usesProjectContext: true, + usesConfigurationContext: true, async execute(args, context) { const registry = getRegistry(context); @@ -76,6 +76,10 @@ export function createDebugStartSessionTool( args.cartridgeDirectory, args.projectDirectory, ); + context.setResolvedDirectory('cartridgeDirectory', { + path: cartridgeDir, + source: args.cartridgeDirectory ? 'argument' : 'projectDirectory', + }); const cartridges = findCartridges(cartridgeDir); const warnings: string[] = []; @@ -104,7 +108,14 @@ export function createDebugStartSessionTool( await manager.connect(); - const entry = registry.registerSession({hostname, clientId, manager, sourceMapper, cartridges}); + const entry = registry.registerSession({ + hostname, + clientId, + manager, + sourceMapper, + cartridges, + resolution: structuredClone(context.resolution), + }); const cartridgeMappings: Record = {}; for (const c of cartridges) cartridgeMappings[c.name] = c.src; diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/log-watch-registry.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/log-watch-registry.ts index ffbf70c80..b9b35f343 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/log-watch-registry.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/log-watch-registry.ts @@ -8,6 +8,7 @@ import {randomUUID} from 'node:crypto'; import {getLogger} from '@salesforce/b2c-tooling-sdk/logging'; import type {LogEntry, LogFile, TailLogsResult} from '@salesforce/b2c-tooling-sdk/operations/logs'; import type {ToolExecutionContext} from '../adapter.js'; +import type {ToolResolution} from '../project-context.js'; const IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes @@ -56,6 +57,7 @@ export interface LogWatchEntry { createdAt: number; lastActivityAt: number; stopped: boolean; + resolution?: ToolResolution; } export interface RegisterWatchOptions { @@ -64,6 +66,7 @@ export interface RegisterWatchOptions { tailResult: TailLogsResult; bufferCap?: number; bufferBytesCap?: number; + resolution?: ToolResolution; } /** Approximate in-memory byte cost of a buffered entry. */ @@ -273,6 +276,7 @@ export class LogWatchRegistry { stopped: false, totalEntriesSeen: 0, watchId, + resolution: opts.resolution, }; this.watches.set(watchId, entry); return entry; diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-list.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-list.ts index 1c5de1f0e..4ebc76ad5 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-list.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-list.ts @@ -10,6 +10,7 @@ import type {Services} from '../../services.js'; import type {ServerContext} from '../../server-context.js'; import {createToolAdapter, jsonResult} from '../adapter.js'; import {getLogWatchRegistry} from './log-watch-registry.js'; +import type {ToolResolution} from '../project-context.js'; interface ListWatchesOutput { watches: Array<{ @@ -23,6 +24,7 @@ interface ListWatchesOutput { stopped: boolean; total_entries_seen: number; watch_id: string; + resolution?: ToolResolution; }>; } @@ -53,6 +55,7 @@ export function createLogsWatchListTool( stopped: w.stopped, total_entries_seen: w.totalEntriesSeen, watch_id: w.watchId, + resolution: w.resolution, })), }; }, diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-start.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-start.ts index d129afdfd..0dd9aa568 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-start.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/logs-watch-start.ts @@ -142,7 +142,12 @@ export function createLogsWatchStartTool( // it isn't orphaned (it would otherwise poll WebDAV until process exit). let entry; try { - entry = registry.registerWatch({hostname, prefixes, tailResult}); + entry = registry.registerWatch({ + hostname, + prefixes, + tailResult, + resolution: structuredClone(context.resolution), + }); } catch (error) { await tailResult.stop().catch(() => {}); throw error; diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/mrt-log-watch-registry.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/mrt-log-watch-registry.ts index 5324a9d2037d52ba799969dc33a4ddb7b9b259f9..2de5a912f8822643b0632ce2f2283c7ebee428c4 100644 GIT binary patch delta 201 zcmbOmayWFuaYob3+=Bd~5`~h=f>ed-ko^3dpw#00oYIoa{JdI)w4(f61$8|={eq(W ztkmQZ-Q@hdlGKWl%?BBS*klwGir{MPtrSo+PZr=fAd62y-sW>0=}b~Fr3OHtQk|Mt mmRXdamz$bbQVS8=?8^O39K{GLh5Uk&Vm*YVn@yEpumJ!DzDr2} delta 48 zcmX>cIyYp)amLL|Og?Orb2;{I_T)@u5@1lNPR%RJEXvQzP0cH*W!QXz`; } @@ -52,6 +54,7 @@ export function createMrtLogsWatchListTool( stopped: w.stopped, total_entries_seen: w.totalEntriesSeen, watch_id: w.watchId, + resolution: w.resolution, })), }; }, diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/mrt-logs-watch-start.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/mrt-logs-watch-start.ts index 7b0fb4c54..cb033faff 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/mrt-logs-watch-start.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/mrt-logs-watch-start.ts @@ -159,7 +159,13 @@ export function createMrtLogsWatchStartTool( // exit). let entry; try { - entry = registry.registerWatch({project, environment, origin, tailResult}); + entry = registry.registerWatch({ + project, + environment, + origin, + tailResult, + resolution: structuredClone(context.resolution), + }); } catch (error) { tailResult.stop(); throw error; diff --git a/packages/b2c-dx-mcp/src/tools/diagnostics/session-registry.ts b/packages/b2c-dx-mcp/src/tools/diagnostics/session-registry.ts index eaab938ff..84a8abf84 100644 --- a/packages/b2c-dx-mcp/src/tools/diagnostics/session-registry.ts +++ b/packages/b2c-dx-mcp/src/tools/diagnostics/session-registry.ts @@ -14,6 +14,7 @@ import type { } from '@salesforce/b2c-tooling-sdk/operations/debug'; import type {CartridgeMapping} from '@salesforce/b2c-tooling-sdk/operations/code'; import type {ToolExecutionContext} from '../adapter.js'; +import type {ToolResolution} from '../project-context.js'; const IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes @@ -35,6 +36,7 @@ export interface DebugSessionEntry { haltWaiters: HaltWaiter[]; createdAt: number; lastActivityAt: number; + resolution?: ToolResolution; } export interface RegisterSessionOptions { @@ -43,6 +45,7 @@ export interface RegisterSessionOptions { manager: DebugSessionManager; sourceMapper: SourceMapper; cartridges: CartridgeMapping[]; + resolution?: ToolResolution; } export class DebugSessionRegistry { @@ -112,7 +115,7 @@ export class DebugSessionRegistry { } registerSession(opts: RegisterSessionOptions): DebugSessionEntry { - const {hostname, clientId, manager, sourceMapper, cartridges} = opts; + const {hostname, clientId, manager, sourceMapper, cartridges, resolution} = opts; const existing = this.findByHostAndClientId(hostname, clientId); if (existing) { throw new Error( @@ -135,6 +138,7 @@ export class DebugSessionRegistry { haltWaiters: [], createdAt: now, lastActivityAt: now, + resolution, }; this.sessions.set(sessionId, entry); return entry; diff --git a/packages/b2c-dx-mcp/src/tools/index.ts b/packages/b2c-dx-mcp/src/tools/index.ts index 0469c6cd2..37f0a1a48 100644 --- a/packages/b2c-dx-mcp/src/tools/index.ts +++ b/packages/b2c-dx-mcp/src/tools/index.ts @@ -24,4 +24,3 @@ export * from './docs/index.js'; export * from './mrt/index.js'; export * from './pwav3/index.js'; export * from './scapi/index.js'; -export * from './storefrontnext/index.js'; diff --git a/packages/b2c-dx-mcp/src/tools/mrt/index.ts b/packages/b2c-dx-mcp/src/tools/mrt/index.ts index 3d27fc53d..a271802fc 100644 --- a/packages/b2c-dx-mcp/src/tools/mrt/index.ts +++ b/packages/b2c-dx-mcp/src/tools/mrt/index.ts @@ -243,6 +243,10 @@ function createMrtBundlePushTool( args.buildDirectory ?? defaults.buildDirectory, args.projectDirectory, ); + context.setResolvedDirectory('buildDirectory', { + path: buildDirectory, + source: args.buildDirectory ? 'argument' : 'projectDirectory', + }); // Log all computed variables before pushing bundle const logger = getLogger(); diff --git a/packages/b2c-dx-mcp/src/tools/project-context.ts b/packages/b2c-dx-mcp/src/tools/project-context.ts index cce508f66..5c0de41e7 100644 --- a/packages/b2c-dx-mcp/src/tools/project-context.ts +++ b/packages/b2c-dx-mcp/src/tools/project-context.ts @@ -4,14 +4,17 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ -import {z} from 'zod'; +import path from 'node:path'; +import {z, type ZodRawShape} from 'zod'; /** Input shared by MCP tools that resolve files or configuration from a project. */ export interface ProjectContextInput { /** Per-call project directory override. */ projectDirectory?: string; - /** Per-call explicit dw.json-format configuration path. */ + /** Per-call explicit primary dw.json-format configuration path. */ configPath?: string; + /** Per-call named instance selection. */ + instanceName?: string; } /** Effective project directory and the source that selected it. */ @@ -20,24 +23,105 @@ export interface ProjectDirectoryInfo { source: 'argument' | 'config' | 'cwd'; } -/** Shared schema field injected into every project-aware MCP tool. */ -export const projectDirectoryInput = z - .string() - .optional() - .describe( - 'Absolute project directory for this call. Overrides --project-directory / SFCC_PROJECT_DIRECTORY and the MCP process working directory. Also controls project-local configuration discovery.', - ); - -/** Shared explicit dw.json path field injected into every project-aware MCP tool. */ -export const configPathInput = z - .string() - .optional() - .describe( - 'Explicit path to a dw.json-format configuration file for this call. Overrides startup --config / SFCC_CONFIG and project .env SFCC_CONFIG. Relative paths resolve from projectDirectory.', - ); - -/** Shared schema fields injected into every project-aware MCP tool. */ -export const projectContextInputSchema = { - projectDirectory: projectDirectoryInput, - configPath: configPathInput, -}; +/** How the selected dw.json file entered the configuration resolver. */ +export type ConfigurationResolutionSource = + | 'argument' + | 'globalDefault' + | 'none' + | 'projectDirectory' + | 'projectEnvironment' + | 'server'; + +/** Compact selected-configuration provenance returned by project-aware tools. */ +export interface ConfigurationResolutionInfo { + hostname?: string; + instanceName?: string; + path?: string; + source: ConfigurationResolutionSource; +} + +/** Provenance for a specialized project-relative directory. */ +export interface DirectoryResolutionInfo { + path: string; + source: 'argument' | 'projectDirectory'; +} + +/** Compact, common provenance block returned by project/config-aware tools. */ +export interface ToolResolution { + configuration?: ConfigurationResolutionInfo; + directories?: Record; + projectDirectory: ProjectDirectoryInfo; +} + +/** Defaults known when MCP tool schemas are registered. */ +export interface ProjectContextDefaults { + projectDirectory: ProjectDirectoryInfo; +} + +/** Whether a tool needs only a project root or full configuration selection. */ +export type ProjectContextKind = 'configuration' | 'project'; + +function defaultProjectContext(): ProjectContextDefaults { + return {projectDirectory: {path: process.cwd(), source: 'cwd'}}; +} + +/** Build the canonical project-directory field with the effective fallback embedded in its description. */ +export function createProjectDirectoryInput(defaults: ProjectContextDefaults = defaultProjectContext()) { + const fallback = defaults.projectDirectory; + const sourceDescription = + fallback.source === 'cwd' + ? 'the MCP process working directory' + : 'the server-level --project-directory / SFCC_PROJECT_DIRECTORY value'; + + return z + .string() + .refine((value) => path.isAbsolute(value), 'projectDirectory must be an absolute path') + .optional() + .describe( + `Optional absolute project root for this call. Overrides the server-level project directory. ` + + `When omitted, uses ${sourceDescription}: ${fallback.path}`, + ); +} + +/** Build the canonical explicit primary dw.json field. */ +export function createConfigPathInput() { + return z + .string() + .optional() + .describe( + 'Optional path to a dw.json-format configuration file. Relative paths resolve from projectDirectory. ' + + 'Selects the primary file ahead of server and project automatic selection; the shared default dw.json remains available as a fallback and for named-instance lookup.', + ); +} + +/** Build the canonical named-instance selection field. */ +export function createInstanceNameInput() { + return z + .string() + .min(1) + .optional() + .describe( + 'Optional named instance to select from the resolved primary and default dw.json files. The primary file is searched first. When omitted, the active/default instance is used.', + ); +} + +/** Build flat canonical schema fields for a local-project or configuration-aware tool. */ +export function createProjectContextInputSchema( + kind: ProjectContextKind, + defaults: ProjectContextDefaults = defaultProjectContext(), +): ZodRawShape { + const project = {projectDirectory: createProjectDirectoryInput(defaults)}; + if (kind === 'project') return project; + + return { + ...project, + configPath: createConfigPathInput(), + instanceName: createInstanceNameInput(), + }; +} + +/** Static field for schemas declared outside the shared adapter. */ +export const projectDirectoryInput = createProjectDirectoryInput(); + +/** Static configuration schema for legacy/manual tool definitions. */ +export const projectContextInputSchema = createProjectContextInputSchema('configuration'); diff --git a/packages/b2c-dx-mcp/src/tools/scapi/metrics-get.ts b/packages/b2c-dx-mcp/src/tools/scapi/metrics-get.ts index 71f87a824..b388ebf93 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/metrics-get.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/metrics-get.ts @@ -123,7 +123,7 @@ Retrieve observability metrics time-series for a B2C Commerce tenant. Returns me toolsets: ['SCAPI'], isGA: false, requiresInstance: false, // SCAPI uses OAuth directly - usesProjectContext: true, + usesConfigurationContext: true, inputSchema: { category: z .enum(['overall', 'sales', 'ecdn', 'third-party', 'scapi', 'scapi-hooks', 'mrt', 'controller', 'ocapi']) diff --git a/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-api-generate-scaffold.ts b/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-api-generate-scaffold.ts index 09f89cea6..af490b739 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-api-generate-scaffold.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-api-generate-scaffold.ts @@ -49,8 +49,12 @@ interface ScaffoldCustomApiInput extends ProjectContextInput { /** Short description of the API. Default: "A custom B2C Commerce API" */ apiDescription?: string; /** Project root for cartridge discovery and output. Default: MCP project directory */ + cartridgeDirectory?: string; + /** @deprecated Use cartridgeDirectory. */ projectRoot?: string; /** Output directory override. Default: scaffold default or project root */ + outputDirectory?: string; + /** @deprecated Use outputDirectory. */ outputDir?: string; } @@ -82,7 +86,10 @@ export async function executeScaffoldCustomApi( overrides?: ScaffoldCustomApiExecuteOverrides, ): Promise { const projectDirectory = services.resolveProjectDirectory(args.projectDirectory); - const projectRoot = services.resolveWithProjectDirectory(args.projectRoot, args.projectDirectory); + const projectRoot = services.resolveWithProjectDirectory( + args.cartridgeDirectory ?? args.projectRoot, + args.projectDirectory, + ); const getScaffold = overrides?.getScaffold ?? @@ -165,7 +172,7 @@ export async function executeScaffoldCustomApi( } const outputDir = resolveOutputDirectory({ - outputDir: args.outputDir, + outputDir: args.outputDirectory ?? args.outputDir, scaffold, projectRoot, }); @@ -224,7 +231,7 @@ export function createScaffoldCustomApiTool( name: 'scapi_custom_api_generate_scaffold', description: `Generate a new custom SCAPI endpoint (OAS 3.0 schema, api.json, script.js) in an existing cartridge. \ Required: apiName (kebab-case). Optional: cartridgeName (defaults to first cartridge found in project), apiType (shopper|admin) default to shopper, \ -apiDescription, projectRoot, outputDir.`, +apiDescription, cartridgeDirectory, outputDirectory.`, toolsets: ['PWAV3', 'SCAPI', 'STOREFRONTNEXT'], isGA: true, requiresInstance: false, @@ -248,16 +255,36 @@ apiDescription, projectRoot, outputDir.`, .optional() .describe('Admin (no siteId) or shopper (siteId, customer-facing). Default: shopper'), apiDescription: z.string().optional().describe('Short description of the API.'), - projectRoot: z + cartridgeDirectory: z .string() .nullish() .describe( - 'Optional cartridge discovery/output root, resolved relative to projectDirectory. Defaults to projectDirectory.', + 'Optional cartridge discovery root, resolved relative to projectDirectory. Defaults to projectDirectory.', ), - outputDir: z.string().optional().describe('Output directory override. Default: project root'), + projectRoot: z + .string() + .nullish() + .describe('Deprecated alias for cartridgeDirectory. cartridgeDirectory takes precedence.'), + outputDirectory: z + .string() + .optional() + .describe('Optional output directory. Relative paths resolve from cartridgeDirectory.'), + outputDir: z + .string() + .optional() + .describe('Deprecated alias for outputDirectory. outputDirectory takes precedence.'), }, - async execute(args, {services}) { - return executeScaffoldCustomApi(args, services, executeOverrides); + async execute(args, context) { + const output = await executeScaffoldCustomApi(args, context.services, executeOverrides); + context.setResolvedDirectory('cartridgeDirectory', { + path: output.projectRoot, + source: args.cartridgeDirectory || args.projectRoot ? 'argument' : 'projectDirectory', + }); + context.setResolvedDirectory('outputDirectory', { + path: output.outputDir, + source: args.outputDirectory || args.outputDir ? 'argument' : 'projectDirectory', + }); + return output; }, formatOutput(output) { if (output.error) { diff --git a/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-get-status.ts b/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-get-status.ts index d3a995592..a5cff0567 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-get-status.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-get-status.ts @@ -144,7 +144,7 @@ CLI: b2c scapi custom status`, toolsets: ['PWAV3', 'SCAPI', 'STOREFRONTNEXT'], isGA: true, requiresInstance: false, - usesProjectContext: true, + usesConfigurationContext: true, inputSchema: { status: z.enum(['active', 'not_registered']).optional().describe('Filter by status. Omit for all.'), groupBy: z.enum(['site', 'type']).optional().describe('Group by siteId or type (Admin/Shopper).'), diff --git a/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts b/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts index ca4129268..9ab676e21 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts @@ -308,6 +308,7 @@ export function createScapiSchemasListTool(loadServices: () => Promise toolsets: ['PWAV3', 'SCAPI', 'STOREFRONTNEXT'], isGA: true, requiresInstance: false, // SCAPI uses OAuth directly, doesn't need B2CInstance (hostname) + usesConfigurationContext: true, inputSchema: { apiFamily: z.string().optional().describe('API family (e.g., "checkout", "product", "custom").'), apiName: z.string().optional().describe('API name (e.g., "shopper-baskets", "shopper-products").'), diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md deleted file mode 100644 index d4a290e2f..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md +++ /dev/null @@ -1,265 +0,0 @@ -# Storefront Next Toolset - -MCP tools for Storefront Next development with React Server Components. - -## Tools - -### `sfnext_get_guidelines` - -**ESSENTIAL FIRST STEP** for Storefront Next development. Returns critical architecture rules, coding standards, and best practices. Use this tool FIRST before writing any Storefront Next code to understand non-negotiable patterns for React Server Components, data loading, and framework constraints. - -**Status**: ✅ Implemented - -**Use cases**: - -- Understand critical rules before writing code -- Learn recommended patterns and conventions -- Get guidance on architecture, data fetching, auth, i18n, components, performance, testing -- Troubleshoot issues and avoid common pitfalls -- Access comprehensive documentation on specific topics - -**Parameters**: - -- `sections` (optional, array): Specific guideline sections to retrieve - - **Default**: `['quick-reference', 'data-fetching', 'components', 'testing']` - Returns comprehensive guidelines covering the most critical topics - - **Single section**: Specify one section name to get focused content - - **Multiple sections**: Specify an array of section names to combine related documentation - - **Empty array**: Returns empty string - - Available section values: - - `quick-reference` - Critical rules, architecture principles, and quick patterns - - `data-fetching` - Data loading patterns with loaders, actions, and useScapiFetcher - - `state-management` - Client-side state management with Zustand - - `auth` - Authentication and session management - - `config` - Configuration system - - `i18n` - Internationalization patterns - - `components` - Component best practices - - `styling` - Tailwind CSS 4, Shadcn/ui, styling guidelines - - `page-designer` - Page Designer integration and component registry - - `performance` - Optimization techniques - - `testing` - Testing strategy - - `extensions` - Framework extensions - - `pitfalls` - Common mistakes to avoid - -**Returns**: Text content with guidelines for the requested section(s) - -**Output format**: - -- **Single section**: Returns content directly (no separators or instructions) -- **Multiple sections**: Returns content with `---` separators between sections, prefixed with instructions to display full content without summarization - -**Example usage**: - -```json -// Default - returns comprehensive guidelines (quick-reference + data-fetching + components + testing) -{ - "name": "sfnext_get_guidelines" -} - -// Single section -{ - "name": "sfnext_get_guidelines", - "arguments": { - "sections": ["data-fetching"] - } -} - -// Multiple related sections -{ - "name": "sfnext_get_guidelines", - "arguments": { - "sections": ["data-fetching", "components", "performance"] - } -} - -// All sections -{ - "name": "sfnext_get_guidelines", - "arguments": { - "sections": ["quick-reference", "data-fetching", "state-management", "auth", "config", "i18n", "components", "styling", "page-designer", "performance", "testing", "extensions", "pitfalls"] - } -} -``` - -### `sfnext_add_page_designer_decorator` - -Add Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefinition`) to existing React components for Storefront Next. - -**Status**: ✅ Implemented (non-GA - use `--allow-non-ga-tools` flag) - -**Use cases**: - -- Add Page Designer support to new components -- Convert existing components to be Page Designer-compatible -- Generate decorator code automatically or interactively -- Configure component attributes and regions for Page Designer - -**Parameters**: - -- `component` (required, string): Component name (e.g., "ProductCard") or file path -- `autoMode` (optional, boolean): Enable auto mode for quick setup -- `searchPaths` (optional, array): Additional directories to search for components -- `componentId` (optional, string): Override component ID -- `conversationContext` (optional, object): For interactive mode workflow steps - -**Returns**: Generated decorator code and instructions for adding to component file - -**Example usage**: - -```json -// Auto mode (quick setup) -{ - "name": "sfnext_add_page_designer_decorator", - "arguments": { - "component": "ProductCard", - "autoMode": true - } -} - -// Interactive mode (step-by-step) -{ - "name": "sfnext_add_page_designer_decorator", - "arguments": { - "component": "Hero", - "conversationContext": { - "step": "analyze" - } - } -} -``` - -### `sfnext_configure_theme` - -**MANDATORY** before implementing any theming changes. Provides theming guidelines, questions, and automatic color contrast validation. Call this tool FIRST when the user requests theming (even if colors/fonts are provided). Never implement without calling it first. - -**Status**: ✅ Implemented (non-GA - use `--allow-non-ga-tools` flag) - -**Use cases**: - -- Apply colors, fonts, or visual styling to a Storefront Next site -- Validate color combinations for WCAG accessibility before implementing -- Follow the theming workflow (questions → validation → confirmation → implement) - -**Parameters**: - -- `fileKeys` (optional, array): File keys to add to the default set. Defaults use `theming-questions`, `theming-validation`, `theming-accessibility` -- `conversationContext` (optional, object): Context from previous rounds - - `currentStep` (optional): Current step in the conversation - - `collectedAnswers` (optional): Previously collected answers; include `colorMapping` to trigger automatic validation (colorMapping alone is sufficient; colors array is not required) - - `questionsAsked` (optional): List of question IDs already asked - -**Returns**: Theming guidelines, questions to ask, and (when `colorMapping` provided, with or without colors array) automated WCAG contrast validation results - -**Example usage**: - -```json -// First call - get guidelines and questions -{ - "name": "sfnext_configure_theme", - "arguments": { - "conversationContext": { - "collectedAnswers": {"colors": [], "fonts": []} - } - } -} - -// Validation call - after constructing colorMapping (colorMapping alone triggers validation) -{ - "name": "sfnext_configure_theme", - "arguments": { - "conversationContext": { - "collectedAnswers": { - "colorMapping": { - "lightText": "#000000", - "lightBackground": "#FFFFFF", - "buttonText": "#FFFFFF", - "buttonBackground": "#0A2540" - } - } - } - } -} -``` - -## Implementation Details - -### Architecture - -#### `sfnext_get_guidelines` - -The tool loads content from markdown files in the `content/` directory: - -- **Content source**: Markdown files loaded at runtime from `packages/b2c-dx-mcp/content/*.md` -- **Quick Reference**: `quick-reference.md` - Critical rules and patterns -- **Section-Based**: Individual markdown files per topic (~100-200 lines each) -- **Default behavior**: Returns 4 sections by default for comprehensive coverage - -**Content Structure**: - -Each section markdown file includes: - -- Critical rules and best practices -- Code examples (correct ✅ and incorrect ❌ patterns) -- Quick reference snippets -- Framework-specific patterns for React Server Components - -**Behavior**: - -- **No sections specified**: Returns default comprehensive set (`quick-reference`, `data-fetching`, `components`, `testing`) -- **Single section**: Returns content directly without separators -- **Multiple sections**: Combines content with `---` separators and includes instructions for full content display -- **Empty array**: Returns empty string - -**Benefits**: - -✅ **Token Efficient**: Returns only relevant content (200-500 lines vs 20K+ full doc) -✅ **Modular**: Access specific sections as needed -✅ **Multi-Select**: Combine related sections in a single call for contextual learning -✅ **Always Current**: Content loaded from markdown files (easy to update) -✅ **Comprehensive Default**: Returns key sections by default for immediate value - -#### `sfnext_add_page_designer_decorator` - -The tool uses a rule-based architecture with TypeScript template literals for generating Page Designer decorators: - -- **Rule Rendering**: Pure TypeScript functions that return strings based on typed context -- **Type Safety**: Every rule has a strongly-typed context interface checked at compile time -- **Template Generation**: Code generation uses pure functions for decorator creation -- **Component Discovery**: Automatically searches common component directories (e.g., `src/components/**`, `app/components/**`) - -**Key Features**: - -- **Name-Based Lookup**: Find components by name (e.g., "ProductCard") without knowing paths -- **Auto-Discovery**: Searches common component directories automatically -- **Type-Safe**: Full TypeScript type inference for all contexts -- **Fast**: Direct function execution, no file I/O or compilation overhead -- **Flexible Input**: Supports component names or file paths - -**Modes**: - -- **Auto Mode**: Generates decorators immediately with sensible defaults -- **Interactive Mode**: Multi-step workflow with user confirmation at each stage - -**Component Discovery**: - -The tool automatically searches for components in these locations (in order): - -1. `src/components/**` (PascalCase and kebab-case) -2. `app/components/**` -3. `components/**` -4. `src/**` (broader search) -5. Custom paths (if provided via `searchPaths`) - -**Project Directory**: - -Component discovery uses the project directory resolved from `--project-directory` flag or `SFCC_PROJECT_DIRECTORY` environment variable (via Services). This ensures searches start from the correct project directory, especially when MCP clients spawn servers from the home directory. - -**See also**: [Detailed documentation](./page-designer-decorator/README.md) for complete usage guide, architecture details, and examples. - -#### `sfnext_configure_theme` - -The tool loads theming guidance from markdown files in `content/site-theming/` and runs automatic WCAG contrast validation when `colorMapping` is provided: - -- **Content source**: `theming-questions`, `theming-validation`, `theming-accessibility` (default); custom files via `fileKeys` or `THEMING_FILES` env -- **Workflow**: Call tool → Ask questions → Call with `colorMapping` (triggers validation) → Present findings → Wait for confirmation → Implement - -**See also**: [Detailed documentation](./site-theming/README.md) for complete usage guide, architecture details, and examples. diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/figma-url-parser.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/figma-url-parser.ts deleted file mode 100644 index 13cb1b77f..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/figma-url-parser.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Extracted parameters from a Figma design URL. - * - * @property {string} fileKey - Figma file identifier from URL path - * @property {string} nodeId - Node identifier (colon format for Figma MCP) - */ -export interface FigmaParams { - fileKey: string; - nodeId: string; -} - -/** - * Parses a Figma URL to extract fileKey and nodeId. - * - * Supported URL formats: - * - https://figma.com/design/:fileKey/:fileName?node-id=1-2 - * - https://www.figma.com/design/:fileKey/:fileName?node-id=1-2 - * - https://figma.com/file/:fileKey/:fileName?node-id=1-2 - * - * @param figmaUrl - The Figma URL to parse - * @returns Object with fileKey and nodeId - * @throws {Error} When URL is not from figma.com, fileKey cannot be extracted, or node-id is missing - * @throws {TypeError} When URL format is invalid - */ -export function parseFigmaUrl(figmaUrl: string): FigmaParams { - try { - const url = new URL(figmaUrl); - - // Validate it's a Figma URL - if (!url.hostname.includes('figma.com')) { - throw new Error('URL must be from figma.com'); - } - - // Extract fileKey from pathname - // Pattern: /design/:fileKey/:fileName or /file/:fileKey/:fileName - const pathMatch = url.pathname.match(/\/(design|file)\/([^/]+)/); - if (!pathMatch || !pathMatch[2]) { - throw new Error( - 'Could not extract fileKey from URL. Expected format: https://figma.com/design/:fileKey/:fileName', - ); - } - - const fileKey = pathMatch[2]; - - // Extract nodeId from query params - // Pattern: ?node-id=1-2 or ?node-id=1:2 - const nodeIdParam = url.searchParams.get('node-id'); - if (!nodeIdParam) { - throw new Error('Could not extract node-id from URL. Expected query parameter: ?node-id=1-2'); - } - - // Convert node-id format from "1-2" to "1:2" (Figma MCP expects colon format) - const nodeId = nodeIdParam.replaceAll('-', ':'); - - return { - fileKey, - nodeId, - }; - } catch (error) { - if (error instanceof TypeError) { - throw new TypeError(`Invalid URL format: ${figmaUrl}`); - } - throw error; - } -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/index.ts deleted file mode 100644 index a275a9427..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/figma-to-component/index.ts +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Figma-to-component workflow orchestrator tool. - * - * Parses Figma URLs, loads workflow instructions, and returns step-by-step guidance - * for converting Figma designs to Storefront Next components. - * - * @module tools/storefrontnext/figma/figma-to-component - */ - -import {z} from 'zod'; -import {readFileSync, existsSync} from 'node:fs'; -import type {McpTool} from '../../../../utils/index.js'; -import type {Services} from '../../../../services.js'; -import {createToolAdapter, textResult} from '../../../adapter.js'; -import {projectDirectoryInput} from '../../../project-context.js'; -import {parseFigmaUrl, type FigmaParams} from './figma-url-parser.js'; - -// prettier-ignore -const DEFAULT_WORKFLOW_CONTENT = `--- -description: Figma to StorefrontNext component conversion workflow -taskType: component ---- -# Figma to StorefrontNext Component Workflow - -IMPORTANT: The figma_to_component tool is a WORKFLOW ORCHESTRATOR that provides instructions only. It does NOT fetch design data or generate components. - -After calling figma_to_component, you MUST: -1. Call Figma MCP tools to fetch design data -2. Discover similar components using Glob/Grep/Read -3. Call generate-component tool for REUSE/EXTEND/CREATE recommendation -4. Call map-tokens tool for token mapping -5. Implement the recommended approach - -DO NOT STOP after receiving workflow instructions. Execute all steps to complete the conversion. - -## WORKFLOW_GUIDELINES - -### Overview -This workflow guides through converting a Figma design into a StorefrontNext-compliant component. - -### Key Principles -1. Review workflow plan and list out todos to user before fetching designs -2. ALWAYS fetch design context and visual reference from Figma MCP tools first. Do not attempt to generate code without retrieving at minimum the design context and screenshot. Metadata is optional -2a. **NEVER pass dirForAssetWrites on the initial get_design_context call.** Call it first WITHOUT that parameter to inspect the response. Only pass dirForAssetWrites after the user has explicitly approved image export. -2b. **MANDATORY GATE - Image export requires user approval:** Do NOT call get_design_context with dirForAssetWrites for ANY node until you have: (1) identified all image-containing nodes, (2) presented the list to the user, (3) asked "Should I export these N image assets now? (yes/no)", and (4) received an explicit "yes". Do NOT export images automatically. -2c. **Single prompt per batch:** Ask ONCE for the entire batch of image nodes. After the user says "yes", export all of them (via one or more get_design_context calls with dirForAssetWrites). Do NOT prompt again for each individual image. -3. Only call the Figma MCP tools listed in this workflow. If a tool is not available or not enabled, inform the user -4. ALWAYS discover similar components before creating new ones. Use Glob/Grep/Read to search the codebase -5. ALWAYS call generate-component tool with discovered components to get REUSE/EXTEND/CREATE recommendation -6. ALWAYS call map-tokens tool to map Figma tokens to existing theme variables rather than hardcoding values -7. Follow StorefrontNext patterns. All components must adhere to StorefrontNext architecture -8. Present a detailed plan to the user and wait for approval before implementing -9. Validate thoroughly. Always run validation checks before presenting the final component to the developer - -### Figma MCP Tools -When calling these tools, always include: clientLanguages="typescript", clientFrameworks="react" -- mcp__figma__get_design_context (REQUIRED): Generates UI code and returns asset URLs. **Initial call: do NOT pass dirForAssetWrites.** Call first without it to inspect the response. Only pass dirForAssetWrites after user has approved export (see Image and Asset Export below). -- mcp__figma__get_screenshot (REQUIRED): Provides visual reference of the design -- mcp__figma__get_metadata (REQUIRED when node is a section): Retrieves node hierarchy, layer types, names, positions, and sizes. Use when get_design_context returns sparse metadata (section nodes do not export assets) - -### Image and Asset Export (REQUIRED) -**MANDATORY GATE: Do not call get_design_context with dirForAssetWrites until the user has approved. You MUST ask ONCE for the entire batch—never prompt per image.** - -Section nodes return sparse metadata and do NOT export images. You MUST: - -1. **Initial probe (no export)**: Call get_design_context WITHOUT dirForAssetWrites first. Never pass dirForAssetWrites on the first call. -2. **Detect sparse response**: If get_design_context returns "sparse metadata" or "section node", call get_metadata with the same nodeId to retrieve the XML with child node IDs -3. **Identify image-containing nodes**: From the metadata XML (or from the initial response if it's a leaf with images), find nodes that contain images. Include: RECTANGLE with fills; nodes named with image-like names (e.g., photo, image, banner, hero); logos and brand assets (nodes named "logo", "Logo", "brand", "icon", "header", "footer", or similar); vector/component instances that represent logos or icons; frames that visually contain photos/illustrations; any node the screenshot suggests contains a logo or brand asset -4. **STOP and ask for approval (MANDATORY)**: Present the list of identified nodes (names and node IDs) to the user. Ask explicitly: "I found N image-containing nodes. Should I export these assets now? (yes/no)". STOP and wait for the user to respond. Do NOT call get_design_context with dirForAssetWrites for any node until the user confirms "yes". If you proceed without user confirmation, you have violated this workflow -5. **Download image nodes** (ONLY after user says "yes"): For each identified image-containing node, call get_design_context with: - - nodeId: the node ID from metadata or initial selection (e.g., "3351:1234") - - dirForAssetWrites: absolute path to the project's public images folder (e.g., \`{workspace}/packages/template-retail-rsc-app/public/images/figma-exports\`) -6. **Track downloaded assets**: Note which exported file path corresponds to which node/component (e.g., hero banner → hero-banner.webp, logo → nettle-logo.webp, category card 1 → infused-beverages.webp) -7. **Set image URLs in implementation**: When implementing the component, use the downloaded file paths for any img src or imageUrl props. Replace placeholder paths with the actual exported asset paths (e.g., \`/images/figma-exports/hero-banner.webp\`) - -### StorefrontNext MCP Tools (REQUIRED) -- generate-component: Analyzes Figma design and discovered components, recommends REUSE/EXTEND/CREATE strategy. MUST be called with discoveredComponents parameter -- map-tokens: Maps Figma design tokens to existing theme tokens. MUST be called to avoid hardcoded values -- validate_component: Validates component against StorefrontNext patterns (optional, not yet implemented) - -### AI-Driven Component Discovery (Before calling generate-component) -Before calling generate-component, you must discover similar components using your tools: - -**Discovery Strategy:** -1. **Name-Based Search (Primary):** - - Use Glob to find component files: \`**/components/**/*.tsx\`, \`**/src/**/*.tsx\` - - Exclude: \`**/node_modules/**\`, \`**/dist/**\`, \`**/*.test.tsx\`, \`**/*.stories.tsx\` - - Use Grep to search for component names similar to the Figma component name - - Look in: export statements, function names, interface names - -2. **Structure-Based Search (Secondary):** - - If name search yields poor results, search by code structure - - Look for similar hooks (useState, useEffect, etc.) - - Look for similar element patterns (buttons, forms, layouts) - - Search for 'use client' directive if Figma code is client-side - -3. **Read and Score Components:** - - Read each promising match - - Score similarity (0-100) based on: - * Name similarity: How close is the name? - * Purpose similarity: Does it serve the same function? - * Structure similarity: Similar JSX structure, hooks, props? - * Styling similarity: Similar Tailwind classes or theme usage? - - Assign match type: 'name', 'structure', or 'visual' - -4. **Select Top Matches:** - - Select top 1-3 matches with similarity >= 50% - - Sort by similarity score (highest first) - - If no matches found, pass empty array to generate-component - -**Discovery Tips:** -- Be semantic: "PrimaryButton" and "CallToAction" might serve the same purpose -- Consider component purpose and context, not just file names -- Check common directories first: components/ui/, components/shared/ -- Read component code to understand structure, props, and behavior -- Trust your judgment using React patterns knowledge - -### Component Requirements -- Use React Server Components (RSC) pattern by default -- Use Tailwind CSS classes with theme tokens, no inline styles or hardcoded values -- Follow TypeScript strict mode conventions -- Include proper accessibility attributes -- Follow existing file naming conventions -- Use absolute imports from '@/components', '@/lib', etc. - -## General Development Guidelines - -### Core Principles -- Thoroughly analyze requests and the existing project for successful implementation -- Promptly clarify ambiguous requirements - -### Development Workflow -- **Analyze Requirements** - Clearly define the objectives and functionalities required -- **Review Existing Code** - Examine the current codebase to identify similar solutions and potentially reusable components -- **Understand Existing Hooks and Utilities** - Familiarize with hooks and utility functions available within the project -- **Plan Implementation** - Design component structure before coding -- **Implement Incrementally** - Develop and test the service in small, manageable steps -- **Test Thoroughly** - Ensure comprehensive testing - -### After Generation -- Present the component code to the developer for review -- Provide file path suggestions based on component type -- Highlight any design tokens that don't have existing mappings -- List any validation warnings or suggestions - -## WORKFLOW_STEPS - -**Create and present to the user a task plan that reflects these steps while keeping the Workflow Guidelines in mind. Wait for approval before proceeding.** - -1. REQUIRED: Retrieve design context using mcp__figma__get_design_context with fileKey and nodeId. **Do NOT pass dirForAssetWrites on this initial call.** If the response is sparse (section node): call get_metadata, identify image-containing nodes, then STOP and ask the user "Should I export these N image assets now? (yes/no)". WAIT for user to respond. Only after user says "yes", call get_design_context for each image-containing node with dirForAssetWrites -2. REQUIRED: Retrieve visual reference using mcp__figma__get_screenshot with the provided fileKey and nodeId -3. REQUIRED when sparse: If get_design_context returns sparse metadata (section node), call mcp__figma__get_metadata to get child node IDs, identify image-containing nodes, then STOP and ask user for approval. Do NOT call get_design_context with dirForAssetWrites until user confirms "yes" -4. REQUIRED: Discover similar components in the codebase: - - Use Glob to find component files in common directories - - Use Grep to search for components with similar names or structure - - Use Read to examine promising matches - - Score similarity (0-100) and select top 1-3 matches - - Prepare discoveredComponents array for next step -5. REQUIRED: Analyze component generation strategy using generate-component tool with discovered components. This provides REUSE/EXTEND/CREATE recommendation. Wait for user approval of strategy before code changes -6. REQUIRED: Map Figma design tokens to existing StorefrontNext theme tokens using the map-tokens tool. Extract color, spacing, and other design tokens from Figma data and pass them to this tool for matching -7. OPTIONAL (not implemented): Validate the generated component against StorefrontNext patterns using the validate_component tool -8. REQUIRED: Implement the recommended approach and present the final component code to the developer for review`; - -export const figmaToComponentSchema = z - .object({ - figmaUrl: z - .string() - .url() - .describe('The Figma design URL to convert to a StorefrontNext component. Must include node-id parameter.'), - workflowFilePath: z - .string() - .optional() - .describe( - 'Optional path to a custom workflow .md file, resolved relative to projectDirectory when needed. If omitted, uses the default built-in workflow.', - ), - projectDirectory: projectDirectoryInput, - }) - .strict(); - -export type FigmaToComponentInput = z.infer; - -export interface WorkflowConfig { - /** YAML frontmatter key-value pairs. Parsed for future use (e.g., taskType-specific behavior). */ - metadata: Record; - content: string; -} - -function extractWorkflowContent(content: string): {metadata: Record; body: string} { - const metadata: Record = {}; - let body = content; - - const metadataMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/); - if (metadataMatch) { - const metadataText = metadataMatch[1]; - body = metadataMatch[2]; - - for (const line of metadataText.split('\n')) { - const match = line.match(/^(.+?):\s*(.+)$/); - if (match) { - metadata[match[1].trim()] = match[2].trim(); - } - } - } - - return {metadata, body: body.trim()}; -} - -function parseWorkflowFile(filePath?: string): WorkflowConfig { - let fileContent: string; - - if (filePath) { - if (!existsSync(filePath)) { - throw new Error(`Workflow file not found: ${filePath}`); - } - fileContent = readFileSync(filePath, 'utf8'); - } else { - fileContent = DEFAULT_WORKFLOW_CONTENT; - } - - const {metadata, body} = extractWorkflowContent(fileContent); - - return {metadata, content: body}; -} - -function formatFigmaParams(params: FigmaParams, originalUrl: string): string { - let section = '## Figma Design Parameters\n\n'; - section += '```json\n'; - section += JSON.stringify( - { - fileKey: params.fileKey, - nodeId: params.nodeId, - originalUrl, - }, - null, - 2, - ); - section += '\n```\n\n'; - section += - 'IMPORTANT: Use these exact parameters when calling Figma MCP tools. The `clientLanguages` parameter should be set to "typescript" and `clientFrameworks` should be set to "react".\n\n'; - return section; -} - -function formatWorkflowContent(content: string): string { - return `${content}\n\n`; -} - -function formatNextStepsReminder(): string { - return `--- -## CRITICAL: Next Steps Required - -This tool has provided workflow instructions only. You MUST now execute ALL steps below. - -**EXPECTED FINAL OUTPUT:** A recommendation with confidence score from sfnext_analyze_component tool AND a token mapping summary from sfnext_match_tokens_to_theme tool. - -### Step 1: Fetch Figma Design Data (Parallel Calls) -Call these Figma MCP tools with the parameters above: -- \`mcp__figma__get_design_context\` (REQUIRED) - **Do NOT pass dirForAssetWrites on the initial call.** Call first without it to inspect the response. If response is sparse (section node): call get_metadata to get child node IDs, identify image-containing nodes, then STOP and present the list to the user. Ask "Should I export these N image assets now? (yes/no)" and WAIT for user response. Only after user says "yes", call get_design_context per image-containing node with dirForAssetWrites -- \`mcp__figma__get_screenshot\` (REQUIRED) - Get visual reference -- \`mcp__figma__get_metadata\` (REQUIRED when sparse) - Use when get_design_context returns sparse metadata. After identifying image nodes: STOP, present list to user, wait for "yes" before exporting - -### Step 2: Discover Similar Components -Use your tools to find existing components: -- Use \`Glob\` to find component files: \`**/components/**/*.tsx\` -- Use \`Grep\` to search for similar names or patterns -- Use \`Read\` to examine promising matches -- Score each match (0-100) based on similarity - -### Step 3: Analyze Component Strategy (CRITICAL - DO NOT SKIP) -You MUST call \`sfnext_analyze_component\` tool with: -- figmaMetadata (from step 1, or empty string if not fetched) -- figmaCode (from step 1) -- componentName (extracted from Figma) -- discoveredComponents (from step 2) - -This tool returns the recommendation with confidence score that MUST be shown to the user. - -### Step 4: Map Design Tokens (CRITICAL - DO NOT SKIP) -You MUST call \`sfnext_match_tokens_to_theme\` tool with tokens extracted from Figma design. - -This tool returns the token mapping summary that MUST be shown to the user. - -### Step 5: Implement -After showing the recommendation and token mapping to the user, wait for approval then implement the code changes. Use the downloaded asset paths from Step 1 for any img src or imageUrl props—do not use placeholder paths. - -**DO NOT STOP until you have called sfnext_analyze_component AND sfnext_match_tokens_to_theme and shown their outputs to the user.** -`; -} - -function formatErrorResponse(details: string): string { - let response = `# Error: Invalid Figma URL\n\n${details}\n\n`; - response += 'Please provide a valid Figma URL in the format:\n'; - response += 'https://figma.com/design/:fileKey/:fileName?node-id=1-2\n\n'; - response += 'Example:\nhttps://figma.com/design/abc123/MyDesign?node-id=1-2\n'; - return response; -} - -/** - * Generates the workflow guide for Figma-to-component conversion. - * - * @param figmaUrl - Figma design URL with node-id query parameter - * @param workflowFilePath - Optional absolute path to custom workflow .md file; uses built-in default if omitted - * @returns Formatted workflow guide string with Figma parameters and step-by-step instructions, or error message if URL or workflow file is invalid - */ -export function generateWorkflowResponse(figmaUrl: string, workflowFilePath?: string): string { - let figmaParams: FigmaParams; - try { - figmaParams = parseFigmaUrl(figmaUrl); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return formatErrorResponse(errorMessage); - } - - let workflowConfig: WorkflowConfig; - try { - workflowConfig = parseWorkflowFile(workflowFilePath); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return `# Error: Workflow File Not Found\n\n${errorMessage}\n\nPlease provide a valid workflow file path or omit the parameter to use the default workflow.\n`; - } - - let response = '# Figma to StorefrontNext Workflow Guide\n\n'; - response += formatFigmaParams(figmaParams, figmaUrl); - response += formatWorkflowContent(workflowConfig.content); - response += formatNextStepsReminder(); - - return response; -} - -/** - * Creates the sfnext_start_figma_workflow MCP tool. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns MCP tool for workflow orchestration - */ -export function createFigmaToComponentTool(loadServices: () => Promise | Services): McpTool { - return createToolAdapter( - { - name: 'sfnext_start_figma_workflow', - description: - '[DEPRECATED] Superseded by the storefront-next and storefront-next-figma agent-skills plugins and NOT compatible with the Storefront Next 1.0 GA release. Will be removed in a future release. ' + - 'WORKFLOW ORCHESTRATOR: Call this tool FIRST when converting Figma designs. ' + - 'Parses Figma URL to extract fileKey and nodeId, returns step-by-step workflow instructions ' + - 'and parameters for subsequent tool calls. ' + - 'CRITICAL: This is only the FIRST step. After calling this tool, you MUST continue executing ' + - 'the complete workflow: 1) Call Figma MCP tools, 2) Discover similar components, ' + - '3) Call sfnext_analyze_component tool, 4) Call sfnext_match_tokens_to_theme tool, ' + - '5) Show both outputs to the user then implement the recommended approach.', - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - isGA: false, - requiresInstance: false, - usesProjectContext: true, - inputSchema: figmaToComponentSchema.shape, - async execute(args, context) { - const workflowFilePath = args.workflowFilePath - ? context.services.resolveWithProjectDirectory(args.workflowFilePath, args.projectDirectory) - : undefined; - return generateWorkflowResponse(args.figmaUrl, workflowFilePath); - }, - formatOutput: (output) => textResult(output), - }, - loadServices, - ); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/decision.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/decision.ts deleted file mode 100644 index de48ed5c7..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/decision.ts +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import type {SimilarComponent, ComponentAnalysisResult} from './index.js'; - -/** - * Categorized differences between a matched component and Figma design - * @property {DifferenceDetail[]} styling - Visual differences (Tailwind classes, inline styles, theme tokens) - * @property {DifferenceDetail[]} structural - JSX hierarchy differences (elements, nesting, root element changes) - * @property {DifferenceDetail[]} behavioral - Interaction differences (hooks, event handlers, client/server rendering) - * @property {DifferenceDetail[]} props - Interface/prop definition differences (new props, type changes) - */ -export interface ComponentDifferences { - styling: DifferenceDetail[]; - structural: DifferenceDetail[]; - behavioral: DifferenceDetail[]; - props: DifferenceDetail[]; -} - -/** - * Details about a specific difference between components - * @property {string} description - Explanation of the difference - * @property {'major'|'minor'|'moderate'} severity - Impact level: 'minor' (1pt), 'moderate' (3pts), 'major' (5pts) - * @property {boolean} isBackwardCompatible - Whether existing code using the component would still work after this change - */ -export interface DifferenceDetail { - description: string; - severity: 'major' | 'minor' | 'moderate'; - isBackwardCompatible: boolean; -} - -/** - * Analyzes differences between matched component and Figma design. - * - * @param matchedComponent - The existing component to compare against - * @param figmaCode - The Figma-generated React code - * @param _figmaMetadata - Reserved for future use (e.g., component hierarchy analysis). - * Currently unused but kept in the signature to avoid a breaking change once metadata - * analysis is implemented. - */ -export function analyzeComponentDifferences( - matchedComponent: SimilarComponent, - figmaCode: string, - _figmaMetadata: string, -): ComponentDifferences { - const differences: ComponentDifferences = { - styling: [], - structural: [], - behavioral: [], - props: [], - }; - - // Analyze styling differences - differences.styling = analyzeStylingDifferences(matchedComponent.code, figmaCode); - - // Analyze structural differences - differences.structural = analyzeStructuralDifferences(matchedComponent.code, figmaCode); - - // Analyze behavioral differences (hooks, state, effects) - differences.behavioral = analyzeBehavioralDifferences(matchedComponent.code, figmaCode); - - // Analyze prop differences - differences.props = analyzePropDifferences(matchedComponent.code, figmaCode); - - return differences; -} - -/** - * Analyzes styling differences (Tailwind classes, CSS, theme tokens) - */ -function analyzeStylingDifferences(existingCode: string, figmaCode: string): DifferenceDetail[] { - const differences: DifferenceDetail[] = []; - - // Extract className usage - const existingClasses = extractTailwindClasses(existingCode); - const figmaClasses = extractTailwindClasses(figmaCode); - - // Check for new classes in Figma design - const newClasses = figmaClasses.filter((c) => !existingClasses.includes(c)); - - if (newClasses.length > 0) { - differences.push({ - description: `New Tailwind classes: ${newClasses.slice(0, 5).join(', ')}${newClasses.length > 5 ? '...' : ''}`, - severity: newClasses.length > 10 ? 'major' : newClasses.length > 3 ? 'moderate' : 'minor', - isBackwardCompatible: true, - }); - } - - // Check for inline styles (anti-pattern) - if (figmaCode.includes('style={{') || figmaCode.includes('style="')) { - differences.push({ - description: 'Figma code contains inline styles (needs conversion to Tailwind)', - severity: 'moderate', - isBackwardCompatible: true, - }); - } - - return differences; -} - -/** - * Analyzes structural differences (JSX hierarchy, elements) - */ -function analyzeStructuralDifferences(existingCode: string, figmaCode: string): DifferenceDetail[] { - const differences: DifferenceDetail[] = []; - - // Extract JSX elements - const existingElements = extractJSXElements(existingCode); - const figmaElements = extractJSXElements(figmaCode); - - // Check for different root elements - if (existingElements[0] !== figmaElements[0]) { - differences.push({ - description: `Different root element: ${existingElements[0]} vs ${figmaElements[0]}`, - severity: 'moderate', - isBackwardCompatible: false, - }); - } - - // Check for additional nested elements - const newElements = figmaElements.filter((e) => !existingElements.includes(e)); - if (newElements.length > 0) { - differences.push({ - description: `New elements in Figma design: ${newElements.join(', ')}`, - severity: newElements.length > 3 ? 'major' : 'minor', - isBackwardCompatible: true, - }); - } - - return differences; -} - -/** - * Analyzes behavioral differences (hooks, state, effects, event handlers) - */ -function analyzeBehavioralDifferences(existingCode: string, figmaCode: string): DifferenceDetail[] { - const differences: DifferenceDetail[] = []; - - // Check for 'use client' directive - const existingIsClient = existingCode.includes("'use client'") || existingCode.includes('"use client"'); - const figmaIsClient = figmaCode.includes("'use client'") || figmaCode.includes('"use client"'); - - if (existingIsClient !== figmaIsClient) { - differences.push({ - description: figmaIsClient - ? 'Figma design requires client-side rendering' - : 'Existing component is client-side, Figma design could be RSC', - severity: 'major', - isBackwardCompatible: false, - }); - } - - // Check for new hooks - const existingHooks = extractHooks(existingCode); - const figmaHooks = extractHooks(figmaCode); - const newHooks = figmaHooks.filter((h) => !existingHooks.includes(h)); - - if (newHooks.length > 0) { - differences.push({ - description: `New React hooks needed: ${newHooks.join(', ')}`, - severity: 'moderate', - isBackwardCompatible: newHooks.every((h) => h.startsWith('use')), - }); - } - - // Check for event handlers - const existingHasHandlers = /on[A-Z]\w+=/g.test(existingCode); - const figmaHasHandlers = /on[A-Z]\w+=/g.test(figmaCode); - - if (!existingHasHandlers && figmaHasHandlers) { - differences.push({ - description: 'Figma design includes event handlers (onClick, onChange, etc.)', - severity: 'moderate', - isBackwardCompatible: true, - }); - } - - return differences; -} - -/** - * Analyzes prop differences - * parses TypeScript interfaces to compare prop definitions - */ -function analyzePropDifferences(existingCode: string, figmaCode: string): DifferenceDetail[] { - const differences: DifferenceDetail[] = []; - - const existingPropCount = (existingCode.match(/interface\s+\w+Props/g) || []).length; - const figmaPropCount = (figmaCode.match(/interface\s+\w+Props/g) || []).length; - - if (figmaPropCount > existingPropCount) { - differences.push({ - description: 'Figma design may require additional props', - severity: 'minor', - isBackwardCompatible: true, - }); - } - - return differences; -} - -/** - * Extracts Tailwind classes from code - */ -function extractTailwindClasses(code: string): string[] { - const classRegex = /className=["']([^"']+)["']/g; - const classes: Set = new Set(); - let match; - - while ((match = classRegex.exec(code)) !== null) { - const classList = match[1].split(/\s+/); - for (const c of classList) classes.add(c); - } - - return [...classes]; -} - -/** - * Extracts JSX elements from code (simplified) - */ -function extractJSXElements(code: string): string[] { - // Simple regex to find JSX opening tags - const elementRegex = /<([A-Z][a-zA-Z0-9]*|[a-z]+)[\s>]/g; - const elements: Set = new Set(); - let match; - - while ((match = elementRegex.exec(code)) !== null) { - elements.add(match[1]); - } - - return [...elements]; -} - -/** - * Extracts React hooks from code - */ -function extractHooks(code: string): string[] { - const hookRegex = /\b(use[A-Z]\w+)\(/g; - const hooks: Set = new Set(); - let match; - - while ((match = hookRegex.exec(code)) !== null) { - hooks.add(match[1]); - } - - return [...hooks]; -} - -/** - * Determines the appropriate action based on differences - * Uses type of difference + impact assessment - */ -export function determineAction( - matchedComponent: SimilarComponent, - differences: ComponentDifferences, -): ComponentAnalysisResult { - const allDifferences = [ - ...differences.styling, - ...differences.structural, - ...differences.behavioral, - ...differences.props, - ]; - - const severityScores = {minor: 1, moderate: 3, major: 5} as const; - let differenceScore = 0; - for (const diff of allDifferences) { - differenceScore += severityScores[diff.severity]; - } - - // Count breaking changes - const breakingChanges = allDifferences.filter((d) => !d.isBackwardCompatible).length; - - // Decision thresholds - const REUSE_THRESHOLD = 2; // Only minor styling differences - const EXTEND_THRESHOLD = 10; // Moderate differences that can be added - - // REUSE: Minimal differences, mostly styling - if (differenceScore <= REUSE_THRESHOLD && breakingChanges === 0) { - return { - action: 'REUSE', - confidence: Math.round(matchedComponent.similarity), - matchedComponent: { - path: matchedComponent.path, - name: matchedComponent.name, - similarity: matchedComponent.similarity, - }, - differences: allDifferences.map((d) => d.description), - recommendation: `The existing component "${matchedComponent.name}" can be reused with different props or minor styling adjustments.`, - suggestedApproach: `Use the existing component at ${matchedComponent.path} and customize it through props.`, - }; - } - - // CREATE: Major structural or behavioral differences, or many breaking changes - if (differenceScore > EXTEND_THRESHOLD || breakingChanges > 2) { - return { - action: 'CREATE', - confidence: 85, - matchedComponent: { - path: matchedComponent.path, - name: matchedComponent.name, - similarity: matchedComponent.similarity, - }, - differences: allDifferences.map((d) => d.description), - recommendation: `Differences are significant enough to warrant creating a new component.`, - suggestedApproach: `Create a new component. You may reference patterns from ${matchedComponent.path} but build a new component.`, - }; - } - - // EXTEND: Moderate differences that can be added - // Determine extend strategy: props / variant / composition - const extendStrategy = determineExtendStrategy(differences, allDifferences); - - return { - action: 'EXTEND', - confidence: Math.round((matchedComponent.similarity + 100 - differenceScore * 2) / 2), - matchedComponent: { - path: matchedComponent.path, - name: matchedComponent.name, - similarity: matchedComponent.similarity, - }, - differences: allDifferences.map((d) => d.description), - recommendation: `The existing component "${matchedComponent.name}" can be extended to support the Figma design.`, - suggestedApproach: generateExtendApproach(extendStrategy, matchedComponent, differences), - extendStrategy, - }; -} - -/** - * Determines the best extend strategy based on differences - * Context-dependent: checks type of difference then validates with impact - */ -function determineExtendStrategy( - differences: ComponentDifferences, - allDifferences: DifferenceDetail[], -): 'composition' | 'props' | 'variant' { - // Props extension: Only new optional behaviors (1-3 new props, backward compatible) - if ( - differences.props.length <= 3 && - differences.structural.length === 0 && - differences.behavioral.length <= 1 && - allDifferences.every((d) => d.isBackwardCompatible) - ) { - return 'props'; - } - - // Composition: Structural changes or new child components - if (differences.structural.length > 0 || differences.behavioral.some((d) => !d.isBackwardCompatible)) { - return 'composition'; - } - - // Variant pattern: Visual variations (styling focused, 4+ new classes) - if (differences.styling.some((d) => d.severity !== 'minor')) { - return 'variant'; - } - - // Default to props for small changes - return 'props'; -} - -/** - * Generates extend approach description based on strategy - */ -function generateExtendApproach( - strategy: 'composition' | 'props' | 'variant', - matchedComponent: SimilarComponent, - differences: ComponentDifferences, -): string { - const componentPath = matchedComponent.path; - const componentName = matchedComponent.name; - - switch (strategy) { - case 'composition': { - return `**Composition Pattern** -Create a new component that wraps/composes the existing one: -1. Create new component: ${componentName}Enhanced.tsx -2. Import and compose: <${componentName}>{/* new elements */} -3. Structural changes: ${differences.structural.map((d) => d.description).join(', ')} -4. This preserves the existing component while adding new behavior - -This approach works because there are structural changes that would break existing usage if added directly.`; - } - - case 'props': { - return `**Props Extension Pattern** -Extend the existing component by adding new optional props: -1. Modify ${componentPath} -2. Add new props to the interface (${differences.props.map((d) => d.description).join(', ')}) -3. Implement the new prop behavior while maintaining backward compatibility -4. Ensure existing usage is not affected - -This approach works because the changes are small and backward compatible.`; - } - - case 'variant': { - return `**Variant Pattern** -Add new visual variants to the existing component: -1. Modify ${componentPath} -2. Add variant definitions using your variant system (e.g., CVA) -3. New styling: ${differences.styling.map((d) => d.description).join(', ')} -4. Extend the component's visual options without breaking existing usage - -This approach works because the changes are primarily styling-focused.`; - } - } -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/formatter.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/formatter.ts deleted file mode 100644 index 6e2e158c2..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/formatter.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import type {ComponentAnalysisResult, GenerateComponentInput} from './index.js'; - -/** - * Formats the component generation recommendation for AI/developer. - * - * @param analysis - Component analysis result with REUSE/EXTEND/CREATE decision - * @param input - Original generate-component input (component name, etc.) - * @returns Formatted markdown string with recommendation, key differences, suggested approach, and next steps - */ -export function formatRecommendation(analysis: ComponentAnalysisResult, input: GenerateComponentInput): string { - let output = '# Component Generation Recommendation\n\n'; - - // Decision and confidence - output += `**Decision:** ${analysis.action}\n`; - output += `**Confidence:** ${analysis.confidence}%\n\n`; - - // Matched component (if any) - if (analysis.matchedComponent) { - output += `**Matched Component:**\n`; - output += `- \`${analysis.matchedComponent.name}\` at \`${analysis.matchedComponent.path}\`\n`; - output += `- Similarity: ${analysis.matchedComponent.similarity}%\n\n`; - } - - // Recommendation - output += `**Recommendation:** ${analysis.recommendation}\n\n`; - - // Key differences - if (analysis.differences && analysis.differences.length > 0) { - output += `**Key Differences:**\n`; - for (const [i, diff] of analysis.differences.entries()) { - output += `${i + 1}. ${diff}\n`; - } - output += '\n'; - } - - // Suggested approach - if (analysis.suggestedApproach) { - output += `## Suggested Approach\n\n${analysis.suggestedApproach}\n\n`; - } - - // Next steps - output += formatNextSteps(analysis, input); - - return output; -} - -/** - * Formats next steps based on action type - */ -function formatNextSteps(analysis: ComponentAnalysisResult, input: GenerateComponentInput): string { - let section = '## Next Steps\n\n'; - - switch (analysis.action) { - case 'CREATE': { - section += `Create new component: \`${input.componentName}\`\n\n`; - section += `**Implementation:**\n`; - section += `1. Create component file structure (index.tsx, types.ts if needed)\n`; - section += `2. Convert Figma code to StorefrontNext patterns:\n`; - section += ` - Use React Server Components by default (add 'use client' if needed)\n`; - section += ` - Replace inline styles with Tailwind classes\n`; - section += ` - Map colors/spacing to theme tokens\n`; - section += ` - Add proper TypeScript types and accessibility attributes\n`; - section += `3. Export component from index\n`; - - if (analysis.matchedComponent) { - section += `4. Reference patterns from \`${analysis.matchedComponent.path}\` for consistency\n`; - } - - section += '\n'; - break; - } - - case 'EXTEND': { - const strategy = analysis.extendStrategy || 'props'; - section += `**Strategy:** ${strategy === 'props' ? 'Add new props' : strategy === 'variant' ? 'Add variant' : 'Composition'}\n\n`; - section += `1. Modify \`${analysis.matchedComponent?.path}\`\n`; - - if (strategy === 'props') { - section += `2. Add new optional props to the interface\n`; - section += `3. Implement new prop behavior while maintaining backward compatibility\n`; - } else if (strategy === 'variant') { - section += `2. Add new variant to existing variant definitions\n`; - section += `3. Apply variant styling using theme tokens\n`; - } else { - section += `2. Create wrapper component that composes the base component\n`; - section += `3. Add additional elements/functionality in the wrapper\n`; - } - - section += `4. Validate: ensure existing usages still work\n\n`; - break; - } - - case 'REUSE': { - section += `Import and use \`${analysis.matchedComponent?.name}\` from \`${analysis.matchedComponent?.path}\`.\n`; - section += `Customize through props and Tailwind classes to match the Figma design.\n\n`; - break; - } - } - - section += '**Confirm before proceeding with implementation.**\n'; - - return section; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/index.ts deleted file mode 100644 index 39677ea37..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/generate-component/index.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Generate component tool for Figma-to-component workflow. - * - * Analyzes Figma design and discovered components to recommend REUSE, EXTEND, or CREATE strategy. - * - * @module tools/storefrontnext/figma/generate-component - */ - -import {z} from 'zod'; -import type {McpTool} from '../../../../utils/index.js'; -import type {Services} from '../../../../services.js'; -import {createToolAdapter, textResult} from '../../../adapter.js'; -import {projectDirectoryInput} from '../../../project-context.js'; -import {analyzeComponentDifferences, determineAction} from './decision.js'; -import {formatRecommendation} from './formatter.js'; - -/** - * A component discovered in the codebase that may match the Figma design. - * - * @property {string} path - Absolute file path to the component - * @property {string} name - Component name - * @property {number} similarity - Similarity score (0-100) - * @property {'name'|'structure'|'visual'} matchType - Type of match: 'name', 'structure', or 'visual' - * @property {string} code - Full source code of the component - */ -export interface SimilarComponent { - path: string; - name: string; - similarity: number; - matchType: 'name' | 'structure' | 'visual'; - code: string; -} - -const discoveredComponentSchema = z.object({ - path: z.string().describe('Absolute file path to the component'), - name: z.string().describe('Component name'), - similarity: z.number().min(0).max(100).describe('Similarity score (0-100)'), - matchType: z.enum(['name', 'structure', 'visual']).describe('Type of match found'), - code: z.string().describe('Full source code of the component'), -}); - -export const generateComponentSchema = z - .object({ - figmaMetadata: z.string().describe('JSON string containing Figma design metadata (from mcp__figma__get_metadata)'), - figmaCode: z.string().describe('Generated React code from Figma (from mcp__figma__get_design_context)'), - componentName: z.string().describe('Suggested name for the component extracted from Figma design'), - discoveredComponents: z - .array(discoveredComponentSchema) - .describe( - 'Array of similar components discovered using Glob/Grep/Read. Pass empty array if no similar components found.', - ), - workspacePath: z - .string() - .optional() - .describe('Optional workspace root path. Defaults to the MCP server project directory.'), - projectDirectory: projectDirectoryInput, - }) - .strict(); - -export type GenerateComponentInput = z.infer; - -/** - * Result of component analysis recommending REUSE, EXTEND, or CREATE. - * - * @property {'CREATE'|'EXTEND'|'REUSE'} action - Recommended action: 'CREATE', 'EXTEND', or 'REUSE' - * @property {number} confidence - Confidence score (0-100) - * @property {{path: string, name: string, similarity: number}} [matchedComponent] - Best-matching component (if action is REUSE or EXTEND) - * @property {string[]} [differences] - Key differences between Figma design and matched component - * @property {string} recommendation - Human-readable recommendation text - * @property {string} [suggestedApproach] - Implementation guidance - * @property {'composition'|'props'|'variant'} [extendStrategy] - Strategy for EXTEND: 'props', 'variant', or 'composition' - */ -export interface ComponentAnalysisResult { - action: 'CREATE' | 'EXTEND' | 'REUSE'; - confidence: number; - matchedComponent?: { - path: string; - name: string; - similarity: number; - }; - differences?: string[]; - recommendation: string; - suggestedApproach?: string; - extendStrategy?: 'composition' | 'props' | 'variant'; -} - -function analyzeComponent(input: GenerateComponentInput): ComponentAnalysisResult { - const similarComponents = input.discoveredComponents; - - if (similarComponents.length === 0) { - return { - action: 'CREATE', - confidence: 95, - recommendation: `No similar components found in the codebase. Will create new component: ${input.componentName}`, - suggestedApproach: 'Create a new component following StorefrontNext patterns.', - }; - } - - const topMatch = similarComponents[0]; - const differences = analyzeComponentDifferences(topMatch, input.figmaCode, input.figmaMetadata); - const decision = determineAction(topMatch, differences); - - return decision; -} - -/** - * Generates a component recommendation from Figma design and discovered components. - * - * @param input - Figma design data (metadata, code), component name, and discovered components - * @returns Formatted recommendation with REUSE/EXTEND/CREATE decision and implementation guidance, or error message on failure - */ -export function generateComponentRecommendation(input: GenerateComponentInput): string { - try { - const analysis = analyzeComponent(input); - const recommendation = formatRecommendation(analysis, input); - - return recommendation; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return `# Error: Component Generation Failed\n\n${errorMessage}\n\nPlease check the input parameters and try again.`; - } -} - -/** - * Creates the sfnext_analyze_component MCP tool. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns MCP tool for component analysis and recommendation - */ -export function createGenerateComponentTool(loadServices: () => Promise | Services): McpTool { - return createToolAdapter( - { - name: 'sfnext_analyze_component', - description: - '[DEPRECATED] Superseded by the storefront-next and storefront-next-figma agent-skills plugins and NOT compatible with the Storefront Next 1.0 GA release. Will be removed in a future release. ' + - 'Analyzes Figma design and discovered components to recommend component generation strategy. ' + - 'Workflow: 1) Discover similar components using Glob/Grep/Read tools, ' + - '2) Call this tool with the discoveredComponents parameter, ' + - '3) Tool analyzes differences and recommends REUSE/EXTEND/CREATE action, ' + - '4) Tool provides formatted recommendation with code examples and workflow steps. ' + - 'Call this tool AFTER retrieving Figma design data and discovering similar components.', - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - isGA: false, - requiresInstance: false, - usesProjectContext: true, - inputSchema: generateComponentSchema.shape, - async execute(args, context) { - return generateComponentRecommendation({ - ...args, - workspacePath: context.services.resolveWithProjectDirectory(args.workspacePath, args.projectDirectory), - }); - }, - formatOutput: (output) => textResult(output), - }, - loadServices, - ); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/css-parser.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/css-parser.ts deleted file mode 100644 index cbd3dcef9..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/css-parser.ts +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * PostCSS dependency: Used to parse theme CSS (app.css) into an AST for reliable extraction of - * design tokens. Required for: (1) walking @theme at-rules and inline blocks, (2) distinguishing - * light/dark/shared tokens via selectors (e.g. [data-theme="light"], :root), (3) handling nested - * rules and var() references. Regex-based parsing would be brittle for real-world theme files. - */ - -import {readFileSync, existsSync} from 'node:fs'; -import {join} from 'node:path'; -import postcss, {type AtRule, type Rule} from 'postcss'; - -/** - * Design token extracted from theme CSS (app.css). - * - * @property {string} name - CSS custom property name (e.g., "--color-primary") - * @property {string} value - Raw value (may be var() reference) - * @property {'dark'|'light'|'shared'} theme - Theme context: 'dark', 'light', or 'shared' - * @property {'color'|'fontFamily'|'fontSize'|'opacity'|'other'|'radius'|'spacing'} type - Token type for matching: color, spacing, radius, etc. - * @property {string} [resolvedValue] - Resolved value for var() references (actual hex/value) - */ -export interface ThemeToken { - name: string; - value: string; - theme: 'dark' | 'light' | 'shared'; - type: 'color' | 'fontFamily' | 'fontSize' | 'opacity' | 'other' | 'radius' | 'spacing'; - resolvedValue?: string; -} - -/** - * Parsed theme file with tokens organized by theme context. - * - * @property {ThemeToken[]} tokens - All resolved tokens (excludes unresolved var() references) - * @property {Map} lightTokens - Map of token name to ThemeToken for light theme - * @property {Map} darkTokens - Map of token name to ThemeToken for dark theme - * @property {Map} sharedTokens - Map of token name to ThemeToken for shared tokens - * @property {string[]} warnings - Warnings (e.g., unresolved var() references) - */ -export interface ParsedTheme { - tokens: ThemeToken[]; - lightTokens: Map; - darkTokens: Map; - sharedTokens: Map; - warnings: string[]; -} - -/** - * Determines the type of a CSS custom property based on its name and value - */ -function determineTokenType(name: string, value: string): ThemeToken['type'] { - const nameLower = name.toLowerCase(); - - if (nameLower.includes('color') || value.startsWith('#') || value.startsWith('rgb')) { - return 'color'; - } - if (nameLower.includes('radius')) { - return 'radius'; - } - if (nameLower.includes('opacity')) { - return 'opacity'; - } - if (nameLower.includes('font-size') || nameLower.includes('text-size')) { - return 'fontSize'; - } - if (nameLower.includes('font-family') || nameLower.includes('font-face')) { - return 'fontFamily'; - } - if ( - nameLower.includes('spacing') || - nameLower.includes('gap') || - nameLower.includes('padding') || - nameLower.includes('margin') - ) { - return 'spacing'; - } - - return 'other'; -} - -/** - * Extracts CSS custom property name from var() reference - * Example: "var(--primary)" -> "--primary" - */ -function extractVarName(value: string): null | string { - const match = value.match(/var\(([^)]+)\)/); - return match ? match[1].trim() : null; -} - -/** - * Resolves var() references to actual values - * Returns warnings for any unresolved references - */ -function resolveVarReferences(tokens: ThemeToken[]): string[] { - const warnings: string[] = []; - const tokenMap = new Map(); - for (const token of tokens) tokenMap.set(token.name, token); - - for (const token of tokens) { - if (token.value.includes('var(')) { - const varName = extractVarName(token.value); - if (varName) { - const referencedToken = tokenMap.get(varName); - if (referencedToken) { - // Recursively resolve if the referenced token also has a var() - token.resolvedValue = referencedToken.resolvedValue || referencedToken.value; - } else { - // Unresolved reference - log warning and skip this token - warnings.push( - `Warning: Token "${token.name}" references undefined variable "${varName}". This token will be excluded from matching.`, - ); - // Don't set resolvedValue - this token will be filtered out - } - } - } else { - token.resolvedValue = token.value; - } - } - - return warnings; -} - -/** - * Extracts custom properties from a PostCSS rule node - */ -function extractCustomPropertiesFromRule(rule: Rule, theme: 'dark' | 'light' | 'shared'): ThemeToken[] { - const tokens: ThemeToken[] = []; - - rule.walkDecls((decl) => { - if (decl.prop.startsWith('--')) { - tokens.push({ - name: decl.prop, - value: decl.value, - theme, - type: determineTokenType(decl.prop, decl.value), - }); - } - }); - - return tokens; -} - -/** - * Determines if a selector represents a dark theme context - */ -function isDarkThemeSelector(selector: string): boolean { - return ( - selector.includes('.dark') || - selector.includes('[data-theme="dark"]') || - selector.includes("[data-theme='dark']") || - (selector.includes('html:not(.dark)') && selector.includes('inverse')) - ); -} - -/** - * Determines if a selector represents a light theme context - */ -function isLightThemeSelector(selector: string): boolean { - return ( - selector === ':root' || - selector.includes('[data-theme="light"]') || - selector.includes("[data-theme='light']") || - (selector.includes('html.dark') && selector.includes('inverse')) - ); -} - -/** - * Parses CSS content to extract theme tokens from different sections using PostCSS - */ -function parseCSSContent(cssContent: string): ParsedTheme { - const allTokens: ThemeToken[] = []; - - // Parse CSS with PostCSS - const root = postcss.parse(cssContent); - - // Walk through all rules and at-rules - root.walkAtRules('theme', (atRule: AtRule) => { - // Extract @theme inline block (shared tokens) - if (atRule.params.includes('inline')) { - atRule.walkDecls((decl) => { - if (decl.prop.startsWith('--')) { - allTokens.push({ - name: decl.prop, - value: decl.value, - theme: 'shared', - type: determineTokenType(decl.prop, decl.value), - }); - } - }); - } - }); - - // Walk through all rules to find theme-specific tokens - root.walkRules((rule: Rule) => { - const selector = rule.selector; - - // Determine theme based on selector - let theme: 'dark' | 'light' | 'shared' | null = null; - - if (isDarkThemeSelector(selector)) { - theme = 'dark'; - } else if (isLightThemeSelector(selector)) { - theme = 'light'; - } - - // Extract tokens if we identified a theme - if (theme) { - const tokens = extractCustomPropertiesFromRule(rule, theme); - allTokens.push(...tokens); - } - }); - - // Resolve var() references and collect warnings - const warnings = resolveVarReferences(allTokens); - - // Filter out tokens with unresolved references - const resolvedTokens = allTokens.filter((token) => token.resolvedValue !== undefined); - - // Log warnings about skipped tokens - if (warnings.length > 0 && resolvedTokens.length < allTokens.length) { - const skippedCount = allTokens.length - resolvedTokens.length; - warnings.push( - `Skipped ${skippedCount} token(s) with unresolved var() references. These will not be available for matching.`, - ); - } - - // Organize tokens by theme - const lightTokens = new Map(); - const darkTokens = new Map(); - const sharedTokens = new Map(); - - for (const token of resolvedTokens) { - switch (token.theme) { - case 'dark': { - darkTokens.set(token.name, token); - break; - } - case 'light': { - lightTokens.set(token.name, token); - break; - } - case 'shared': { - sharedTokens.set(token.name, token); - break; - } - } - } - - return { - tokens: resolvedTokens, - lightTokens, - darkTokens, - sharedTokens, - warnings, - }; -} - -/** - * Finds the theme file path (app.css) in the workspace. - * - * @param workspaceRoot - Workspace root directory to search - * @returns Absolute path to app.css if found, or null - */ -export function findThemeFilePath(workspaceRoot?: string): null | string { - if (!workspaceRoot) { - return null; - } - - const possiblePaths = [join(workspaceRoot, 'src/app.css'), join(workspaceRoot, 'app.css')]; - - for (const path of possiblePaths) { - if (existsSync(path)) { - return path; - } - } - - return null; -} - -/** - * Parses theme file and extracts all CSS custom properties. - * - * When themeFilePath is not provided, searches for app.css in src/app.css or app.css relative to workspaceRoot. - * - * @param themeFilePath - Optional absolute path to theme CSS file - * @param workspaceRoot - Optional workspace root for theme file discovery when themeFilePath is omitted - * @returns Parsed theme with tokens organized by light/dark/shared - * @throws {Error} When theme file is not found - */ -export function parseThemeFile(themeFilePath?: string, workspaceRoot?: string): ParsedTheme { - const filePath = themeFilePath ?? findThemeFilePath(workspaceRoot); - - if (!filePath) { - throw new Error('Theme file (app.css) not found. Please provide the themeFilePath parameter.'); - } - - if (!existsSync(filePath)) { - throw new Error(`Theme file not found at: ${filePath}`); - } - - const cssContent = readFileSync(filePath, 'utf8'); - return parseCSSContent(cssContent); -} - -/** - * Gets all color tokens from parsed theme. - * - * @param parsedTheme - Parsed theme from parseThemeFile - * @returns Array of color-type tokens - */ -export function getColorTokens(parsedTheme: ParsedTheme): ThemeToken[] { - return parsedTheme.tokens.filter((token) => token.type === 'color'); -} - -/** - * Gets all spacing tokens from parsed theme. - * - * @param parsedTheme - Parsed theme from parseThemeFile - * @returns Array of spacing-type tokens - */ -export function getSpacingTokens(parsedTheme: ParsedTheme): ThemeToken[] { - return parsedTheme.tokens.filter((token) => token.type === 'spacing'); -} - -/** - * Gets all radius tokens from parsed theme. - * - * @param parsedTheme - Parsed theme from parseThemeFile - * @returns Array of radius-type tokens - */ -export function getRadiusTokens(parsedTheme: ParsedTheme): ThemeToken[] { - return parsedTheme.tokens.filter((token) => token.type === 'radius'); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/index.ts deleted file mode 100644 index 5d3e4048e..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/index.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Map tokens tool for Figma-to-component workflow. - * - * Maps Figma design tokens to Storefront Next theme tokens in app.css with exact/fuzzy matching. - * - * @module tools/storefrontnext/figma/map-tokens - */ - -import {z} from 'zod'; -import type {McpTool} from '../../../../utils/index.js'; -import type {Services} from '../../../../services.js'; -import {createToolAdapter, textResult} from '../../../adapter.js'; -import {projectDirectoryInput} from '../../../project-context.js'; -import {parseThemeFile} from './css-parser.js'; -import {matchTokens, type FigmaToken} from './token-matcher.js'; - -export const mapTokensToThemeSchema = z - .object({ - figmaTokens: z - .array( - z.object({ - name: z.string().describe('Token name from Figma (e.g., "Primary/Blue", "Spacing/Large")'), - value: z.string().describe('Token value (e.g., "#2563eb", "16px", "0.5rem")'), - type: z - .enum(['color', 'spacing', 'radius', 'opacity', 'fontSize', 'fontFamily', 'other']) - .describe('Type of the token'), - description: z.string().optional().describe('Optional description from Figma'), - }), - ) - .describe('Array of design tokens extracted from Figma'), - themeFilePath: z - .string() - .optional() - .describe( - 'Optional path to the theme CSS file, resolved relative to projectDirectory when needed. If omitted, searches for app.css in common locations.', - ), - projectDirectory: projectDirectoryInput, - }) - .strict(); - -export type MapTokensToThemeInput = z.infer; - -function formatTokenMatch(match: ReturnType[0]): string { - let output = `### ${match.figmaToken.name}\n\n`; - output += `- **Figma Value**: \`${match.figmaToken.value}\`\n`; - output += `- **Type**: ${match.figmaToken.type}\n`; - - if (match.figmaToken.description) { - output += `- **Description**: ${match.figmaToken.description}\n`; - } - - output += `\n#### Match Result\n\n`; - output += `- **Match Type**: ${match.matchType}\n`; - output += `- **Confidence**: ${match.confidence}%\n`; - - if (match.matchedToken) { - output += `- **Matched Token**: \`${match.matchedToken.name}\`\n`; - output += `- **Token Value**: \`${match.matchedToken.value}\`\n`; - output += `- **Resolved Value**: \`${match.matchedToken.resolvedValue || match.matchedToken.value}\`\n`; - output += `- **Theme**: ${match.matchedToken.theme}\n`; - } - - output += `- **Reason**: ${match.reason}\n\n`; - - if (match.suggestions && match.suggestions.length > 0) { - output += `#### Suggestions\n\n`; - for (const [index, suggestion] of match.suggestions.entries()) { - output += `${index + 1}. **${suggestion.tokenName}**\n`; - output += ` - Value: \`${suggestion.value}\`\n`; - output += ` - Theme: ${suggestion.theme}\n`; - output += ` - Reason: ${suggestion.reason}\n`; - if (suggestion.insertAfter) { - output += ` - Insert after: \`${suggestion.insertAfter}\`\n`; - } - output += `\n`; - } - } - - return output; -} - -function generateSummary(matches: ReturnType): string { - const exactMatches = matches.filter((m) => m.matchType === 'exact'); - const fuzzyMatches = matches.filter((m) => m.matchType === 'fuzzy'); - const noMatches = matches.filter((m) => m.matchType === 'none'); - const highConfidence = matches.filter((m) => m.confidence >= 70 && m.matchedToken); - const lowConfidence = matches.filter((m) => m.confidence < 70 && m.confidence > 0); - - let summary = `## Summary\n\n`; - summary += `- **Total Tokens**: ${matches.length}\n`; - summary += `- **Exact Matches**: ${exactMatches.length}\n`; - summary += `- **Fuzzy Matches**: ${fuzzyMatches.length}\n`; - summary += `- **No Matches**: ${noMatches.length}\n`; - summary += `- **High Confidence (≥70%)**: ${highConfidence.length}\n`; - summary += `- **Low Confidence (<70%)**: ${lowConfidence.length}\n\n`; - - if (exactMatches.length > 0) { - summary += `### ✅ Exact Matches (Use these tokens directly)\n\n`; - for (const match of exactMatches) { - if (match.matchedToken) { - summary += `- \`${match.figmaToken.name}\` → \`${match.matchedToken.name}\`\n`; - } - } - summary += `\n`; - } - - if (highConfidence.length > 0) { - summary += `### ⚠️ High Confidence Fuzzy Matches (Review and confirm)\n\n`; - for (const match of highConfidence) { - if (match.matchedToken) { - summary += `- \`${match.figmaToken.name}\` → \`${match.matchedToken.name}\` (${match.confidence}%)\n`; - } - } - summary += `\n`; - } - - if (lowConfidence.length > 0) { - summary += `### ⚠️ Low Confidence Matches (Verify before using)\n\n`; - for (const match of lowConfidence) { - if (match.matchedToken) { - summary += `- \`${match.figmaToken.name}\` → \`${match.matchedToken.name}\` (${match.confidence}%)\n`; - } - } - summary += `\n`; - } - - if (noMatches.length > 0) { - summary += `### ❌ No Matches (New tokens needed)\n\n`; - for (const match of noMatches) { - summary += `- \`${match.figmaToken.name}\`: ${match.figmaToken.value}\n`; - if (match.suggestions && match.suggestions.length > 0) { - summary += ` - Suggested: \`${match.suggestions[0].tokenName}\`\n`; - } - } - summary += `\n`; - } - - return summary; -} - -function generateRecommendations(matches: ReturnType): string { - const needsNewTokens = matches.filter((m) => m.matchType === 'none'); - const needsReview = matches.filter((m) => m.matchType === 'fuzzy' && m.confidence < 70); - - if (needsNewTokens.length === 0 && needsReview.length === 0) { - return `## ✅ Recommendations\n\nAll tokens have been matched with high confidence. You can proceed with using the matched tokens in your component.\n\n`; - } - - let recommendations = `## 📝 Recommendations\n\n`; - - if (needsNewTokens.length > 0) { - recommendations += `### Create New Tokens\n\n`; - recommendations += `The following tokens from Figma don't have matches in your theme. Consider adding them to your \`app.css\` file:\n\n`; - - for (const match of needsNewTokens) { - if (match.suggestions && match.suggestions.length > 0) { - const suggestion = match.suggestions[0]; - recommendations += `\`\`\`css\n`; - recommendations += `/* Add to ${suggestion.theme === 'both' ? ':root and .dark' : suggestion.theme === 'light' ? ':root' : '.dark'} section */\n`; - recommendations += `${suggestion.tokenName}: ${suggestion.value};\n`; - recommendations += `\`\`\`\n\n`; - } - } - } - - if (needsReview.length > 0) { - recommendations += `### Review Low Confidence Matches\n\n`; - recommendations += `The following matches have confidence below 70%. Please review and confirm they are correct before using:\n\n`; - - for (const match of needsReview) { - if (match.matchedToken) { - recommendations += `- **${match.figmaToken.name}** (${match.figmaToken.value})\n`; - recommendations += ` - Matched: \`${match.matchedToken.name}\` (${match.matchedToken.resolvedValue})\n`; - recommendations += ` - Confidence: ${match.confidence}%\n`; - recommendations += ` - Reason: ${match.reason}\n\n`; - } - } - } - - return recommendations; -} - -/** - * Maps Figma design tokens to existing theme tokens in app.css. - * - * @param args - Figma tokens array and optional theme file path - * @param workspaceRoot - Optional workspace root for theme file discovery; used when themeFilePath is not provided - * @returns Formatted mapping report with exact/fuzzy matches, confidence scores, and usage instructions, or error message on failure - */ -export function mapFigmaTokensToTheme(args: MapTokensToThemeInput, workspaceRoot?: string): string { - try { - const parsedTheme = parseThemeFile(args.themeFilePath, workspaceRoot); - - const figmaTokens: FigmaToken[] = args.figmaTokens.map((token) => ({ - name: token.name, - value: token.value, - type: token.type, - description: token.description, - })); - - const matches = matchTokens(figmaTokens, parsedTheme); - - let response = `# Figma Design Tokens → StorefrontNext Theme Mapping\n\n`; - - if (parsedTheme.warnings.length > 0) { - response += `## ⚠️ Warnings\n\n`; - for (const warning of parsedTheme.warnings) { - response += `- ${warning}\n`; - } - response += `\n`; - } - - response += generateSummary(matches); - - response += `## Detailed Mapping Results\n\n`; - for (const match of matches) { - response += formatTokenMatch(match); - } - - response += generateRecommendations(matches); - - response += `## 💡 Usage Instructions\n\n`; - response += `### Using Matched Tokens in Components\n\n`; - response += `For exact and high-confidence matches, use the token directly in your Tailwind classes:\n\n`; - response += `\`\`\`tsx\n`; - response += `// Instead of hardcoded colors\n`; - response += `
\n\n`; - response += `// Use theme tokens\n`; - response += `
\n`; - response += `\`\`\`\n\n`; - - response += `### Creating New Tokens\n\n`; - response += `If you need to add new tokens, add them to your \`app.css\` file in both light and dark theme sections:\n\n`; - response += `\`\`\`css\n`; - response += `:root {\n`; - response += ` --your-new-token: #value;\n`; - response += `}\n\n`; - response += `.dark {\n`; - response += ` --your-new-token: #dark-value;\n`; - response += `}\n`; - response += `\`\`\`\n\n`; - - return response; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return `# Error: Token Mapping Failed\n\n${errorMessage}\n\nPlease ensure the theme file path is correct and accessible.`; - } -} - -/** - * Creates the sfnext_match_tokens_to_theme MCP tool. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns MCP tool for token mapping - */ -export function createMapTokensToThemeTool(loadServices: () => Promise | Services): McpTool { - return createToolAdapter( - { - name: 'sfnext_match_tokens_to_theme', - description: - '[DEPRECATED] Superseded by the storefront-next and storefront-next-figma agent-skills plugins and NOT compatible with the Storefront Next 1.0 GA release. Will be removed in a future release. ' + - 'Maps Figma design tokens to existing StorefrontNext theme tokens in app.css. ' + - 'Analyzes Figma design tokens (colors, spacing, radius, etc.) and finds exact matches, ' + - 'provides fuzzy matches with confidence scores, suggests new token names for unmatched values, ' + - 'and recommends where to add new tokens in the CSS file. ' + - 'Use this tool after retrieving design variables from Figma MCP to ensure components use theme tokens instead of hardcoded values.', - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - isGA: false, - requiresInstance: false, - usesProjectContext: true, - inputSchema: mapTokensToThemeSchema.shape, - async execute(args, context) { - const workspaceRoot = context.services.resolveWithProjectDirectory(undefined, args.projectDirectory); - const themeFilePath = args.themeFilePath - ? context.services.resolveWithProjectDirectory(args.themeFilePath, args.projectDirectory) - : undefined; - return mapFigmaTokensToTheme({...args, themeFilePath}, workspaceRoot); - }, - formatOutput: (output) => textResult(output), - }, - loadServices, - ); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/token-matcher.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/token-matcher.ts deleted file mode 100644 index 4a0069bef..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/figma/map-tokens/token-matcher.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import type {ThemeToken, ParsedTheme} from './css-parser.js'; - -/** - * Design token extracted from Figma. - * - * @property {string} name - Token name from Figma (e.g., "Primary/Blue", "Spacing/Large") - * @property {string} value - Token value (e.g., "#2563eb", "16px", "0.5rem") - * @property {'color'|'fontFamily'|'fontSize'|'opacity'|'other'|'radius'|'spacing'} type - Token type for matching logic - * @property {string} [description] - Optional description from Figma - */ -export interface FigmaToken { - name: string; - value: string; - type: 'color' | 'fontFamily' | 'fontSize' | 'opacity' | 'other' | 'radius' | 'spacing'; - description?: string; -} - -/** - * Result of matching a Figma token to theme tokens. - * - * @property {FigmaToken} figmaToken - The Figma token that was matched - * @property {ThemeToken} [matchedToken] - Best-matching theme token (if found) - * @property {number} confidence - Match confidence (0-100) - * @property {'exact'|'fuzzy'|'none'} matchType - 'exact', 'fuzzy', or 'none' - * @property {string} reason - Human-readable explanation of the match - * @property {TokenSuggestion[]} [suggestions] - Suggested new tokens or alternatives (when no match or fuzzy match) - */ -export interface TokenMatch { - figmaToken: FigmaToken; - matchedToken?: ThemeToken; - confidence: number; - matchType: 'exact' | 'fuzzy' | 'none'; - reason: string; - suggestions?: TokenSuggestion[]; -} - -/** - * Suggestion for a new or alternative theme token. - * - * @property {string} tokenName - Suggested CSS custom property name - * @property {string} value - Token value - * @property {'both'|'dark'|'light'} theme - Which theme(s) to add to: 'both', 'dark', or 'light' - * @property {string} reason - Explanation for the suggestion - * @property {string} [insertAfter] - Optional token name to insert after in the theme file - */ -export interface TokenSuggestion { - tokenName: string; - value: string; - theme: 'both' | 'dark' | 'light'; - reason: string; - insertAfter?: string; -} - -/** - * Normalizes hex colors to lowercase 6-digit format - */ -function normalizeHexColor(hex: string): string { - let normalized = hex.toLowerCase().trim(); - - // Remove # if present - if (normalized.startsWith('#')) { - normalized = normalized.slice(1); - } - - // Expand 3-digit hex to 6-digit - if (normalized.length === 3) { - normalized = [...normalized].map((c) => c + c).join(''); - } - - return normalized; -} - -/** - * Calculates color distance between two hex colors (0-100, lower is closer) - */ -function calculateColorDistance(hex1: string, hex2: string): number { - const r1 = Number.parseInt(hex1.slice(0, 2), 16); - const g1 = Number.parseInt(hex1.slice(2, 4), 16); - const b1 = Number.parseInt(hex1.slice(4, 6), 16); - - const r2 = Number.parseInt(hex2.slice(0, 2), 16); - const g2 = Number.parseInt(hex2.slice(2, 4), 16); - const b2 = Number.parseInt(hex2.slice(4, 6), 16); - - // Euclidean distance normalized to 0-100 scale - const distance = Math.hypot(r1 - r2, g1 - g2, b1 - b2); - - // Max distance is sqrt(255^2 * 3) ≈ 441 - return (distance / 441) * 100; -} - -/** - * Calculates string similarity between two strings (0-100, higher is more similar) - * Uses Levenshtein distance algorithm - */ -function calculateStringSimilarity(str1: string, str2: string): number { - const s1 = str1.toLowerCase(); - const s2 = str2.toLowerCase(); - - // Exact match - if (s1 === s2) return 100; - - // Contains match bonus - if (s1.includes(s2) || s2.includes(s1)) { - return 80 + (Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length)) * 20; - } - - // Levenshtein distance - const matrix: number[][] = []; - const len1 = s1.length; - const len2 = s2.length; - - for (let i = 0; i <= len1; i++) { - matrix[i] = [i]; - } - - for (let j = 0; j <= len2; j++) { - matrix[0][j] = j; - } - - for (let i = 1; i <= len1; i++) { - for (let j = 1; j <= len2; j++) { - const cost = s1[i - 1] === s2[j - 1] ? 0 : 1; - matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost); - } - } - - const distance = matrix[len1][len2]; - const maxLen = Math.max(len1, len2); - return ((maxLen - distance) / maxLen) * 100; -} - -/** - * Extracts semantic meaning from token name - */ -function extractSemantics(name: string): string[] { - const parts = name.toLowerCase().replace(/^--/, '').split(/[-_]/); - - const semantics: string[] = []; - - // Color semantics - const colorKeywords = new Set([ - 'accent', - 'background', - 'border', - 'destructive', - 'error', - 'foreground', - 'info', - 'muted', - 'primary', - 'secondary', - 'success', - 'text', - 'warning', - ]); - const lightDark = new Set(['dark', 'light']); - const colorNames = new Set(['black', 'blue', 'gray', 'green', 'orange', 'purple', 'red', 'white', 'yellow']); - - for (const part of parts) { - if (colorKeywords.has(part)) { - semantics.push(`semantic:${part}`); - } - if (lightDark.has(part)) { - semantics.push(`theme:${part}`); - } - if (colorNames.has(part)) { - semantics.push(`color:${part}`); - } - } - - return semantics; -} - -/** - * Finds exact color match - */ -function findExactColorMatch(figmaValue: string, parsedTheme: ParsedTheme): null | ThemeToken { - const normalizedFigma = normalizeHexColor(figmaValue); - - for (const token of parsedTheme.tokens) { - if (token.type === 'color' && token.resolvedValue) { - const normalizedToken = normalizeHexColor(token.resolvedValue); - if (normalizedFigma === normalizedToken) { - return token; - } - } - } - - return null; -} - -/** - * Finds fuzzy matches based on color similarity and name similarity - */ -function findFuzzyMatches(figmaToken: FigmaToken, parsedTheme: ParsedTheme): Array<{token: ThemeToken; score: number}> { - const matches: Array<{token: ThemeToken; score: number}> = []; - - // Filter tokens by type - const relevantTokens = parsedTheme.tokens.filter((token) => token.type === figmaToken.type); - - const figmaSemantics = extractSemantics(figmaToken.name); - const normalizedFigmaValue = - figmaToken.type === 'color' && figmaToken.value.startsWith('#') - ? normalizeHexColor(figmaToken.value) - : figmaToken.value; - - for (const token of relevantTokens) { - let score = 0; - - // Name similarity (40% weight) - const nameSimilarity = calculateStringSimilarity(figmaToken.name, token.name); - score += nameSimilarity * 0.4; - - // Semantic similarity (30% weight) - const tokenSemantics = extractSemantics(token.name); - const semanticMatches = figmaSemantics.filter((s) => tokenSemantics.includes(s)).length; - const semanticScore = figmaSemantics.length > 0 ? (semanticMatches / figmaSemantics.length) * 100 : 0; - score += semanticScore * 0.3; - - // Value similarity (30% weight) - if (figmaToken.type === 'color' && token.resolvedValue) { - const normalizedTokenValue = normalizeHexColor(token.resolvedValue); - const colorDistance = calculateColorDistance(normalizedFigmaValue, normalizedTokenValue); - const colorSimilarity = Math.max(0, 100 - colorDistance); - score += colorSimilarity * 0.3; - } - - if (score > 20) { - // Only include matches with score > 20 - matches.push({token, score}); - } - } - - // Sort by score descending - return matches.sort((a, b) => b.score - a.score); -} - -/** - * Generates suggestions for new tokens if no good match found - */ -function generateTokenSuggestions(figmaToken: FigmaToken, parsedTheme: ParsedTheme): TokenSuggestion[] { - const suggestions: TokenSuggestion[] = []; - - // Analyze existing token naming patterns - const existingNames = parsedTheme.tokens.filter((t) => t.type === figmaToken.type).map((t) => t.name); - - // Extract common prefixes - const hasColorPrefix = existingNames.some((n) => n.startsWith('--color-')); - const hasRadiusPrefix = existingNames.some((n) => n.startsWith('--radius-')); - - // Generate token name based on Figma token name - let suggestedName = figmaToken.name.toLowerCase().replaceAll(/[^a-z0-9-]/g, '-'); - - // Add appropriate prefix if not present - if (figmaToken.type === 'color' && !suggestedName.startsWith('--color-') && hasColorPrefix) { - suggestedName = `--color-${suggestedName.replace(/^--/, '')}`; - } else if (figmaToken.type === 'radius' && !suggestedName.startsWith('--radius-') && hasRadiusPrefix) { - suggestedName = `--radius-${suggestedName.replace(/^--/, '')}`; - } else if (!suggestedName.startsWith('--')) { - suggestedName = `--${suggestedName}`; - } - - // Find a good place to insert - const similarTokens = existingNames.filter((name) => { - const similarity = calculateStringSimilarity(name, suggestedName); - return similarity > 30; - }); - - const insertAfter = similarTokens.length > 0 ? similarTokens[0] : undefined; - - // For colors, suggest both light and dark values - if (figmaToken.type === 'color') { - suggestions.push({ - tokenName: suggestedName, - value: figmaToken.value, - theme: 'both', - reason: `New token suggestion based on Figma token "${figmaToken.name}"`, - insertAfter, - }); - } else { - suggestions.push({ - tokenName: suggestedName, - value: figmaToken.value, - theme: 'light', - reason: `New token suggestion based on Figma token "${figmaToken.name}"`, - insertAfter, - }); - } - - return suggestions; -} - -/** - * Matches a single Figma token to existing theme tokens. - * - * @param figmaToken - Figma design token to match - * @param parsedTheme - Parsed theme from app.css - * @returns TokenMatch with exact, fuzzy, or no match and optional suggestions - */ -export function matchToken(figmaToken: FigmaToken, parsedTheme: ParsedTheme): TokenMatch { - // Try exact match first (only for colors with hex values) - if (figmaToken.type === 'color' && figmaToken.value.startsWith('#')) { - const exactMatch = findExactColorMatch(figmaToken.value, parsedTheme); - if (exactMatch) { - return { - figmaToken, - matchedToken: exactMatch, - confidence: 100, - matchType: 'exact', - reason: `Exact color match: ${figmaToken.value} matches ${exactMatch.name}`, - }; - } - } - - // Try fuzzy matching - const fuzzyMatches = findFuzzyMatches(figmaToken, parsedTheme); - - if (fuzzyMatches.length > 0 && fuzzyMatches[0].score >= 50) { - const bestMatch = fuzzyMatches[0]; - return { - figmaToken, - matchedToken: bestMatch.token, - confidence: Math.round(bestMatch.score), - matchType: 'fuzzy', - reason: `Fuzzy match based on name similarity and semantic meaning`, - suggestions: - fuzzyMatches.length > 1 - ? fuzzyMatches.slice(1, 4).map((m) => ({ - tokenName: m.token.name, - value: m.token.value, - theme: m.token.theme === 'shared' ? 'both' : m.token.theme, - reason: `Alternative match (confidence: ${Math.round(m.score)}%)`, - })) - : undefined, - }; - } - - // No good match found, generate suggestions - const suggestions = generateTokenSuggestions(figmaToken, parsedTheme); - - return { - figmaToken, - confidence: 0, - matchType: 'none', - reason: 'No matching token found. Consider creating a new token.', - suggestions, - }; -} - -/** - * Matches multiple Figma tokens to existing theme tokens. - * - * @param figmaTokens - Array of Figma design tokens to match - * @param parsedTheme - Parsed theme from app.css - * @returns Array of TokenMatch results, one per input token - */ -export function matchTokens(figmaTokens: FigmaToken[], parsedTheme: ParsedTheme): TokenMatch[] { - return figmaTokens.map((token) => matchToken(token, parsedTheme)); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts deleted file mode 100644 index 8abc61ca0..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Storefront Next (deprecated) toolset for B2C Commerce. - * - * **DEPRECATED:** These `sfnext_*` tools are superseded by the `storefront-next` - * and `storefront-next-figma` agent-skills plugins and are NOT compatible with - * the Storefront Next 1.0 GA release. They now live in the - * `STOREFRONTNEXT_DEPRECATED` toolset, which is never auto-activated by project - * detection and is excluded from `--toolsets ALL`. To use them you must request - * the toolset explicitly (`--toolsets STOREFRONTNEXT_DEPRECATED`). They will be - * removed in a future release. - * - * **Implemented Tools:** - * - `sfnext_get_guidelines` - Get development guidelines and best practices - * - `sfnext_add_page_designer_decorator` - Add Page Designer decorators to React components - * - `sfnext_configure_theme` - Get theming guidelines, questions, and validation - * - `sfnext_start_figma_workflow` - Convert Figma to components - * - `sfnext_analyze_component` - Analyze design and recommend REUSE/EXTEND/CREATE - * - `sfnext_match_tokens_to_theme` - Match design tokens to theme - * - * @module tools/storefrontnext - */ - -import type {McpTool} from '../../utils/index.js'; -import type {Services} from '../../services.js'; -import {createDeveloperGuidelinesTool} from './sfnext-development-guidelines.js'; -import {createPageDesignerDecoratorTool} from './page-designer-decorator/index.js'; -import {createSiteThemingTool} from './site-theming/index.js'; -import {createFigmaToComponentTool} from './figma/figma-to-component/index.js'; -import {createGenerateComponentTool} from './figma/generate-component/index.js'; -import {createMapTokensToThemeTool} from './figma/map-tokens/index.js'; - -/** - * Creates all tools for the deprecated STOREFRONTNEXT_DEPRECATED toolset. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns Array of MCP tools - */ -export function createStorefrontNextTools(loadServices: () => Promise | Services): McpTool[] { - return [ - createDeveloperGuidelinesTool(loadServices), - createPageDesignerDecoratorTool(loadServices), - createSiteThemingTool(loadServices), - createFigmaToComponentTool(loadServices), - createGenerateComponentTool(loadServices), - createMapTokensToThemeTool(loadServices), - ]; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/README.md b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/README.md deleted file mode 100644 index fddd981a6..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/README.md +++ /dev/null @@ -1,262 +0,0 @@ -# Page Designer Decorator Tool - -Tool for adding Page Designer decorators to React components using native TypeScript template literals. - -## 🎯 Overview - -This tool analyzes React components and generates Page Designer decorators (`@Component`, `@AttributeDefinition`, `@RegionDefinition`) to make components available in Page Designer for Storefront Next. - -## ✨ Key Features - -- **Name-Based Lookup**: Find components by name (e.g., "ProductItem", "ProductTile") without knowing paths -- **Auto-Discovery**: Automatically searches common component directories -- **Type-Safe**: Full TypeScript type inference for all contexts -- **Fast**: Direct function execution, no file I/O or compilation overhead -- **Flexible Input**: Supports component names or file paths -- **Two Modes**: Auto mode for quick setup, Interactive mode for fine-tuned control - -## 📁 File Structure - -``` -page-designer-decorator/ -├── analyzer.ts # Component parsing and analysis -├── rules.ts # Rule loader and exports -├── index.ts # Main tool implementation -├── rules/ -│ ├── 1-mode-selection.ts # Entry point -│ ├── 2a-auto-mode.ts # Auto mode workflow -│ ├── 2b-0-interactive-overview.ts # Interactive workflow overview -│ ├── 2b-1-interactive-analyze.ts # Step 1: Analysis -│ ├── 2b-2-interactive-select-props.ts # Step 2: Selection -│ ├── 2b-3-interactive-configure-attrs.ts # Step 3: Configuration -│ ├── 2b-4-interactive-configure-regions.ts # Step 4: Regions -│ └── 2b-5-interactive-confirm-generation.ts # Step 5: Generation -└── templates/ - └── decorator-generator.ts # Decorator code generation -``` - -## 🚀 Usage - -### Basic Usage (Name-Based - Recommended) - -```bash -# By component name (automatically finds the file) -sfnext_add_page_designer_decorator({ - component: "ProductItem", - autoMode: true -}) - -# Interactive mode -sfnext_add_page_designer_decorator({ - component: "ProductTile", - conversationContext: { step: "analyze" } -}) - -# With custom search paths (for unusual locations) -sfnext_add_page_designer_decorator({ - component: "ProductItem", - searchPaths: ["packages/retail/src", "app/features"], - autoMode: true -}) -``` - -### Path-Based Usage - -```bash -# If you prefer to specify the exact path -sfnext_add_page_designer_decorator({ - component: "src/components/ProductItem.tsx", - autoMode: true -}) -``` - -### Workflow - -1. **Component Discovery**: Provide name (e.g., "ProductItem") or path -2. **Mode Selection**: Choose Auto or Interactive mode -3. **Analysis** (Interactive only): Review component props -4. **Selection** (Interactive only): Select which props to expose -5. **Configuration** (Interactive only): Configure types and defaults -6. **Regions** (Interactive only): Configure nested content areas -7. **Generation**: Get decorator code - -### Component Discovery - -The tool automatically searches for components in these locations (in order): - -1. `src/components/**` (PascalCase and kebab-case) -2. `app/components/**` -3. `components/**` -4. `src/**` (broader search) -5. Custom paths (if provided via `searchPaths`) - -**Project Directory:** -Component discovery uses the project directory from `--project-directory` flag or `SFCC_PROJECT_DIRECTORY` environment variable (via Services). This ensures searches start from the correct project directory, especially when MCP clients spawn servers from the home directory. - -**Examples:** - -- `"ProductItem"` → finds `src/components/product-item/index.tsx` or `ProductItem.tsx` -- `"ProductTile"` → finds `src/components/product-tile/ProductTile.tsx` or `product-tile/index.tsx` -- `"product-item"` → finds `src/components/product-item.tsx` or `product-item/index.tsx` - -**Tips:** - -- Use component name for portability -- Use path for unusual locations -- Add `searchPaths` for monorepos or non-standard structures -- Ensure `--project-directory` flag or `SFCC_PROJECT_DIRECTORY` env var is set correctly - -## 🏗️ Architecture - -### Rule Rendering - -Rules are pure TypeScript functions that return strings: - -```typescript -${context.hasEditableProps - ? context.editableProps.map(prop => - `- \`${prop.name}\` (${prop.type})` - ).join('\n') - : '' -} -``` - -### Type Safety - -Every rule has a strongly-typed context interface: - -```typescript -export interface AnalyzeStepContext { - componentName: string; - file: string; - hasEditableProps: boolean; - editableProps: PropInfo[]; - // ... more fields -} - -export function renderAnalyzeStep(context: AnalyzeStepContext): string { - // TypeScript checks all variable access at compile time -} -``` - -### Template Generation - -Code generation uses pure functions: - -```typescript -export function generateDecoratorCode(context: MetadataContext): string { - const imports = generateImports(context); - const decorator = generateComponentDecorator(context); - const attributes = generateAttributes(context); - - return `${imports}${decorator}\nexport class ${context.metadataClassName} {\n${attributes}\n}`; -} -``` - -## 📦 Build Process - -All rules and templates are compiled into the JavaScript output: - -```json -{ - "scripts": { - "build": "tsc" - } -} -``` - -## 🎯 When to Use This Tool - -Use this tool when: - -- ✅ You need to add Page Designer support to React components -- ✅ You want automatic component discovery by name -- ✅ You prefer type-safe decorator generation -- ✅ You need both quick auto-mode and detailed interactive workflows - -## 🔧 Development - -### Adding a New Rule - -1. Create a new file in `rules/`: - -```typescript -// rules/my-new-rule.ts -export interface MyRuleContext { - message: string; -} - -export function renderMyRule(context: MyRuleContext): string { - return `# My Rule\n\n${context.message}`; -} -``` - -2. Export it from `rules.ts`: - -```typescript -import {renderMyRule, type MyRuleContext} from './rules/my-new-rule.js'; - -export const pageDesignerDecoratorRules = { - // ... existing rules - getMyRule(context: MyRuleContext): string { - return renderMyRule(context); - }, -}; -``` - -3. Use it in `index.ts`: - -```typescript -const instructions = pageDesignerDecoratorRules.getMyRule({ - message: 'Hello World', -}); -``` - -### Modifying Code Generation - -Edit `templates/decorator-generator.ts` directly. Changes require recompilation. - -## 📊 Performance - -The tool uses direct function execution with no file I/O or compilation overhead. Typical tool invocations complete in under 1ms. - -## ✅ Testing - -### Automated Tests - -```bash -pnpm build -pnpm test -``` - -Comprehensive test suite covers all workflow modes, component discovery, and error handling. - -### Running Tests - -Run the comprehensive Mocha test suite: - -```bash -cd packages/b2c-dx-mcp -pnpm run test:agent -- test/tools/storefrontnext/page-designer-decorator/index.test.ts -``` - -The test suite covers: - -- Component discovery (name-based, kebab-case, nested, path-based, custom paths, name collisions) -- Auto mode (basic, type inference, complex props exclusion, UI-only props exclusion, edge cases) -- Interactive mode (all steps: analyze, select_props, configure_attrs, configure_regions, confirm_generation) -- Error handling (invalid input, invalid step name, missing parameters) -- Edge cases (no props, only complex props, optional props, union types, already decorated components) -- Project directory resolution (from `--project-directory` flag or `SFCC_PROJECT_DIRECTORY` env var via Services) - -See [`test/tools/storefrontnext/page-designer-decorator/README.md`](../../../../test/tools/storefrontnext/page-designer-decorator/README.md) for detailed testing instructions. - -## 🎓 Learning Resources - -- [Template Literals (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) -- [MCP Tools Documentation](https://modelcontextprotocol.io/docs) - -## 📝 License - -Apache-2.0 - Copyright (c) 2025, Salesforce, Inc. diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/analyzer.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/analyzer.ts deleted file mode 100644 index 6dd1177df..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/analyzer.ts +++ /dev/null @@ -1,672 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {existsSync, readFileSync} from 'node:fs'; -import path from 'node:path'; -import {globSync} from 'glob'; -import {Project, InterfaceDeclaration, PropertySignature} from 'ts-morph'; - -/** - * Lazily-initialized, reusable ts-morph Project for component analysis. - * Creating a new Project (~40ms) on every analyzeComponent call is expensive; - * reusing one with an in-memory file system avoids repeated TypeScript compiler init. - */ -let cachedProject: Project | undefined; - -function getProject(): Project { - if (!cachedProject) { - cachedProject = new Project({ - useInMemoryFileSystem: true, - skipAddingFilesFromTsConfig: true, - }); - } - return cachedProject; -} - -// ============================================================================ -// TYPE DEFINITIONS -// ============================================================================ - -/** - * Component analysis result - */ -export interface ComponentInfo { - componentName: string; - interfaceName: null | string; - hasDecorators: boolean; - props: PropInfo[]; - exportType: 'default' | 'named'; - filePath: string; -} - -/** - * Property information extracted from component interface - */ -export interface PropInfo { - name: string; - type: string; - optional: boolean; - isComplex: boolean; // Can't be used directly in Page Designer - isUIOnly: boolean; // Styling/layout props not suitable for PD -} - -/** - * Type suggestion for attribute configuration - */ -export interface TypeSuggestion { - type: string; - reason: string; - priority: 'high' | 'low' | 'medium'; -} - -// ============================================================================ -// TYPE INFERENCE -// ============================================================================ - -/** - * Type mapping from TypeScript to SFCC Page Designer attribute types - */ -const TYPE_MAPPING: Record = { - String: 'string', - string: 'string', - Number: 'integer', - number: 'integer', - Boolean: 'boolean', - boolean: 'boolean', - Date: 'string', - URL: 'url', - CMSRecord: 'cms_record', -}; - -/** - * Valid SFCC Page Designer attribute types - */ -export const VALID_ATTRIBUTE_TYPES = [ - 'string', - 'text', - 'markup', - 'integer', - 'boolean', - 'product', - 'category', - 'file', - 'page', - 'image', - 'url', - 'enum', - 'custom', - 'cms_record', -] as const; - -/** - * Infer Page Designer attribute type from TypeScript type - */ -export function inferPageDesignerType(tsType: string): string { - if (TYPE_MAPPING[tsType]) { - return TYPE_MAPPING[tsType]; - } - - if (tsType.includes('|')) { - const firstType = tsType.split('|')[0].trim(); - return inferPageDesignerType(firstType); - } - - if (tsType.includes('[]') || tsType.includes('Array<')) { - return 'string'; - } - - return 'string'; -} - -/** - * Check if TypeScript type can be auto-inferred - */ -export function isAutoInferredType(tsType: string): boolean { - return Boolean(TYPE_MAPPING[tsType]); -} - -/** - * Check if type is too complex for Page Designer - */ -export function isComplexType(tsType: string): boolean { - return ( - tsType.includes('{') || - tsType.includes('<') || - tsType.includes('.') || - tsType.includes('=>') || - tsType.includes('React.') || - tsType.startsWith('(') - ); -} - -/** - * Check if property is UI-only - */ -export function isUIOnlyProp(propName: string): boolean { - const uiPatterns = [ - 'classname', - 'style', - 'theme', - 'variant', - 'size', - 'color', - 'loading', - 'disabled', - 'readonly', - 'onclick', - 'onchange', - 'onsubmit', - 'children', - 'key', - 'ref', - ]; - const nameLower = propName.toLowerCase(); - return uiPatterns.some((pattern) => nameLower.includes(pattern)); -} - -/** - * Generate Page Designer attribute type suggestions for a component prop - * - * **Inference Strategy:** - * Uses naming patterns and TypeScript types to suggest appropriate Page Designer types. - * This reduces manual configuration by auto-detecting common patterns. - * - * **Page Designer Types:** - * - `string`: Default text input - * - `url`: URL/link inputs (validates URL format) - * - `image`: Image asset picker - * - `html`: Rich text editor - * - `markup`: HTML/markdown editor - * - `enum`: Dropdown with predefined values - * - `boolean`: Checkbox - * - `number`: Numeric input - * - `product`: Product picker (SFCC-specific) - * - `category`: Category picker (SFCC-specific) - * - * **Heuristics (by priority):** - * 1. **High Priority**: Strong patterns (url, image, product) - * 2. **Medium Priority**: Contextual patterns (html, markup) - * 3. **Low Priority**: Weak signals (description → markup) - * - * Multiple suggestions allow developers to choose the best fit. - * - * @param propName - Property name from component interface - * @param tsType - TypeScript type string - * @returns Array of type suggestions with reasoning and priority - * - * @example - * // URL detection: - * generateTypeSuggestions('imageUrl', 'string') - * // => [{ type: 'url', reason: '...', priority: 'high' }] - * - * @example - * // Image detection: - * generateTypeSuggestions('heroImage', 'string') - * // => [{ type: 'image', reason: '...', priority: 'high' }] - * - * @example - * // Multiple suggestions: - * generateTypeSuggestions('description', 'string') - * // => [ - * // { type: 'markup', reason: '...', priority: 'low' }, - * // { type: 'html', reason: '...', priority: 'medium' } - * // ] - * - * @example - * // Product reference: - * generateTypeSuggestions('product', 'string') - * // => [{ type: 'product', reason: '...', priority: 'high' }] - * - * @public - */ -export function generateTypeSuggestions(propName: string, tsType: string): TypeSuggestion[] { - const suggestions: TypeSuggestion[] = []; - const nameLower = propName.toLowerCase(); - - // URL patterns - if (nameLower.includes('url') || nameLower.includes('link') || nameLower.includes('href')) { - suggestions.push({ - type: 'url', - reason: 'Property name suggests URL/link', - priority: 'high', - }); - } - - // Image patterns - if ( - nameLower.includes('image') || - nameLower.includes('img') || - nameLower.includes('picture') || - nameLower.includes('background') - ) { - suggestions.push({ - type: 'image', - reason: 'Property name suggests image asset', - priority: 'high', - }); - } - - // Rich text patterns - if ( - nameLower.includes('html') || - nameLower.includes('richtext') || - nameLower.includes('content') || - nameLower.includes('body') - ) { - suggestions.push({ - type: 'markup', - reason: 'Property name suggests rich content', - priority: 'medium', - }); - } - - // Multi-line text patterns - if (nameLower.includes('description') || nameLower.includes('bio') || nameLower.includes('message')) { - suggestions.push({ - type: 'text', - reason: 'Property name suggests multi-line text', - priority: 'medium', - }); - } - - // Array patterns - if (tsType.includes('[]') || tsType.includes('Array<')) { - suggestions.push({ - type: 'enum', - reason: 'Array types work best as enums for selection in Page Designer', - priority: 'high', - }); - } - - // Product/Category references - if (nameLower.includes('product') && !nameLower.includes('products')) { - suggestions.push({ - type: 'product', - reason: 'Property name suggests product reference', - priority: 'high', - }); - } - - if (nameLower.includes('category')) { - suggestions.push({ - type: 'category', - reason: 'Property name suggests category reference', - priority: 'high', - }); - } - - return suggestions; -} - -// ============================================================================ -// COMPONENT FILE PARSING -// ============================================================================ - -/** - * Extract component name from file content - * - * Priority order: - * 1. export default function X (inline default function) - * 2. export default X (default export of named identifier, e.g. export default ProductItem) - * 3. export function X (first named function export) - * 4. export const X = - * 5. fallback: 'Component' - * - * Note: (2) must be checked before (3) because files may have both "export function Foo" - * and "export default Bar" — the default export is the primary component. - */ -function extractComponentName(content: string): string { - const defaultFunctionMatch = content.match(/export\s+default\s+function\s+(\w+)/); - if (defaultFunctionMatch) { - return defaultFunctionMatch[1]; - } - - // export default X where X is a named identifier (not "function") - const defaultNamedMatch = content.match(/export\s+default\s+(?!function\s)(\w+)/); - if (defaultNamedMatch) { - return defaultNamedMatch[1]; - } - - const namedFunctionMatch = content.match(/export\s+function\s+(\w+)/); - if (namedFunctionMatch) { - return namedFunctionMatch[1]; - } - - const namedConstMatch = content.match(/export\s+const\s+(\w+)\s*=/); - if (namedConstMatch) { - return namedConstMatch[1]; - } - - return 'Component'; -} - -/** - * Detect export type - */ -function detectExportType(content: string): 'default' | 'named' { - return content.includes('export default') ? 'default' : 'named'; -} - -/** - * Parse component file and extract structure - */ -function parseComponentFile(filePath: string): ComponentInfo { - const content = readFileSync(filePath, 'utf8'); - - const hasDecorators = content.includes('@Component') || content.includes('@PageType'); - - if (hasDecorators) { - return { - componentName: extractComponentName(content), - interfaceName: null, - hasDecorators: true, - props: [], - exportType: detectExportType(content), - filePath, - }; - } - - const project = getProject(); - const sourceFile = project.createSourceFile(filePath, content, {overwrite: true}); - - try { - const interfaces = sourceFile.getInterfaces(); - const propsInterface = interfaces.find((i: InterfaceDeclaration) => i.getName().includes('Props')); - - if (!propsInterface) { - return { - componentName: extractComponentName(content), - interfaceName: null, - hasDecorators: false, - props: [], - exportType: detectExportType(content), - filePath, - }; - } - - const props: PropInfo[] = propsInterface.getProperties().map((prop: PropertySignature) => { - const name = prop.getName(); - const type = prop.getType().getText(); - const optional = prop.hasQuestionToken(); - - return { - name, - type, - optional, - isComplex: isComplexType(type), - isUIOnly: isUIOnlyProp(name), - }; - }); - - return { - componentName: extractComponentName(content), - interfaceName: propsInterface.getName(), - hasDecorators: false, - props, - exportType: detectExportType(content), - filePath, - }; - } finally { - project.removeSourceFile(sourceFile); - } -} - -// ============================================================================ -// COMPONENT ANALYZER -// ============================================================================ - -/** - * Component analyzer for Page Designer decorator generation - */ -class ComponentAnalyzer { - private cache: Map = new Map(); - - analyzeComponent(filePath: string): ComponentInfo { - const cached = this.cache.get(filePath); - if (cached) { - return cached; - } - - const analysis = parseComponentFile(filePath); - this.cache.set(filePath, analysis); - - return analysis; - } - - clearCache() { - this.cache.clear(); - } -} - -export const componentAnalyzer = new ComponentAnalyzer(); - -// ============================================================================ -// COMPONENT RESOLUTION (Name-Based Lookup) -// ============================================================================ - -/** - * Convert PascalCase or camelCase to kebab-case - * - * Used for finding components with different naming conventions. - * React components are typically PascalCase, but file names may be kebab-case. - * - * @param str - String to convert (e.g., "ProductCard", "myComponent") - * @returns Kebab-case string (e.g., "product-card", "my-component") - * - * @example - * toKebabCase('ProductCard') // => 'product-card' - * toKebabCase('MyButtonComponent') // => 'my-button-component' - * toKebabCase('heroSection') // => 'hero-section' - * - * @internal - */ -function toKebabCase(str: string): string { - return str - .replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2') - .replaceAll(/([A-Z])([A-Z][a-z])/g, '$1-$2') - .toLowerCase(); -} - -/** - * Search for component file by name using smart discovery patterns - * - * **Search Strategy (in priority order):** - * 1. Common component directories with exact name (PascalCase) - * 2. Kebab-case variants of the name - * 3. Index file patterns (for directory-based components) - * 4. Broader search in src/ - * 5. Custom search paths (if provided) - * - * **Why this order:** - * - Most projects follow conventions (src/components/) - * - PascalCase is React standard, checked first - * - Kebab-case is common for file names - * - Index files are common for complex components - * - Fallback to broader search if not in standard locations - * - * **Disambiguation:** - * If multiple files match, prefers the shortest path (closest to root). - * This typically selects the main component over similar named test/story files. - * - * @param componentName - Component name without extension (e.g., "ProductCard", "Hero") - * @param workspaceRoot - Absolute path to workspace root - * @param customPaths - Additional directories to search (e.g., ["packages/retail/src"]) - * @returns Absolute file path or null if not found - * - * @example - * // Finds: src/components/product-tile/ProductCard.tsx - * findComponentByName('ProductCard', '/workspace', undefined) - * - * @example - * // Finds: src/components/hero.tsx or src/components/hero/index.tsx - * findComponentByName('hero', '/workspace', undefined) - * - * @example - * // Searches in custom paths first - * findComponentByName('ProductCard', '/workspace', ['packages/retail/src']) - * - * @internal - */ -function findComponentByName(componentName: string, workspaceRoot: string, customPaths?: string[]): null | string { - // Normalize component name (remove file extensions) - const cleanName = componentName.replace(/\.(tsx?|jsx?)$/, ''); - const kebabName = toKebabCase(cleanName); - - // Search patterns (in order of priority) - const searchPatterns = [ - // Common component directories (PascalCase) - `src/components/**/${cleanName}.tsx`, - `src/components/**/${cleanName}.ts`, - `app/components/**/${cleanName}.tsx`, - `components/**/${cleanName}.tsx`, - - // Kebab-case variants - `src/components/**/${kebabName}.tsx`, - `app/components/**/${kebabName}.tsx`, - `components/**/${kebabName}.tsx`, - - // Index file patterns - `src/components/**/${kebabName}/index.tsx`, - `app/components/**/${kebabName}/index.tsx`, - - // Anywhere in src/ (broader search) - `src/**/${cleanName}.tsx`, - `src/**/${cleanName}.ts`, - `src/**/${kebabName}.tsx`, - - // Custom search paths (if provided) - ...(customPaths?.flatMap((path) => [ - `${path}/**/${cleanName}.tsx`, - `${path}/**/${cleanName}.ts`, - `${path}/**/${kebabName}.tsx`, - `${path}/**/${kebabName}/index.tsx`, - ]) || []), - ]; - - // Search with glob - for (const pattern of searchPatterns) { - try { - const matches = globSync(pattern, { - cwd: workspaceRoot, - absolute: true, - ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/out/**'], - }); - - if (matches.length > 0) { - // If multiple matches, prefer shortest path (closest to root) - const sorted = matches.sort((a, b) => a.length - b.length); - return sorted[0]; - } - } catch { - // Ignore glob errors and try next pattern - continue; - } - } - - return null; -} - -/** - * Resolve component input (name or path) to absolute file path - * - * **This is the main entry point for component discovery.** - * - * Supports two input modes: - * 1. **Name-based** (recommended): Just provide the component name - * 2. **Path-based** (backward compatible): Provide relative path from workspace - * - * **Name-based detection:** - * Input is treated as a name if it: - * - Does NOT contain path separators (/ or \) - * - Does NOT have a file extension (.tsx, .ts, etc.) - * - * **Path-based detection:** - * Input is treated as a path if it: - * - Contains / or \ - * - Has a file extension - * - * @param input - Component name or relative path - * @param workspaceRoot - Absolute path to workspace root - * @param searchPaths - Additional directories to search (only used for name-based) - * @returns Absolute file path to component - * @throws {Error} If component cannot be found, with detailed search information - * - * @example - * // Name-based (finds automatically): - * resolveComponent('ProductCard', '/workspace') - * // => '/workspace/src/components/product-tile/ProductCard.tsx' - * - * @example - * // Path-based (backward compatible): - * resolveComponent('src/components/ProductCard.tsx', '/workspace') - * // => '/workspace/src/components/ProductCard.tsx' - * - * @example - * // With custom search paths (for monorepos): - * resolveComponent('Hero', '/workspace', ['packages/retail/src', 'packages/shared']) - * // => '/workspace/packages/retail/src/components/Hero.tsx' - * - * @example - * // Error handling: - * try { - * resolveComponent('NonExistent', '/workspace') - * } catch (err) { - * // Error includes: - * // - List of searched locations - * // - Tried name variations - * // - Helpful tips for resolution - * } - * - * @public - */ -export function resolveComponent(input: string, workspaceRoot: string, searchPaths?: string[]): string { - // Check if input looks like a path (has / or \ or file extension) - const looksLikePath = input.includes('/') || input.includes('\\') || input.match(/\.(tsx?|jsx?|mjs|cjs|js)$/); - - if (looksLikePath) { - // Treat as path (backward compatible) - const fullPath = path.join(workspaceRoot, input); - if (existsSync(fullPath)) { - return fullPath; - } - throw new Error( - `Component file not found at path: ${input}\n\n` + - `Full path checked: ${fullPath}\n\n` + - `Tips:\n` + - ` 1. Use component name instead (e.g., "ProductCard") for automatic discovery\n` + - ` 2. If components are in a different repo, set --project-directory flag or SFCC_PROJECT_DIRECTORY env var`, - ); - } - - // Treat as component name - search for it - const found = findComponentByName(input, workspaceRoot, searchPaths); - - if (!found) { - const searchLocations = [ - 'src/components/**', - 'app/components/**', - 'components/**', - 'src/**', - ...(searchPaths || []), - ]; - - throw new Error( - `Component "${input}" not found.\n\n` + - `Searched in:\n${searchLocations.map((loc) => ` - ${loc}`).join('\n')}\n\n` + - `Tried variations:\n` + - ` - ${input}.tsx\n` + - ` - ${toKebabCase(input)}.tsx\n` + - ` - ${toKebabCase(input)}/index.tsx\n\n` + - `Tips:\n` + - ` 1. Provide full path: component: "src/components/ProductCard.tsx"\n` + - ` 2. Add custom search: searchPaths: ["packages/retail/src"]\n` + - ` 3. Check component name spelling and casing\n` + - ` 4. If components are in a different repo, set --project-directory flag or SFCC_PROJECT_DIRECTORY env var`, - ); - } - - return found; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/index.ts deleted file mode 100644 index 145d8073a..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/index.ts +++ /dev/null @@ -1,740 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {z, type ZodRawShape} from 'zod'; -import {componentAnalyzer, generateTypeSuggestions, resolveComponent, type TypeSuggestion} from './analyzer.js'; -import {generateDecoratorCode, type AttributeContext, type MetadataContext} from './templates/decorator-generator.js'; -import {pageDesignerDecoratorRules} from './rules.js'; -import type {McpTool} from '../../../utils/index.js'; -import type {Services} from '../../../services.js'; -import {projectContextInputSchema, type ProjectContextInput} from '../../project-context.js'; - -// ============================================================================ -// SCHEMA DEFINITION -// ============================================================================ - -export const pageDesignerDecoratorSchema = z - .object({ - component: z - .string() - .describe( - 'Component name (e.g., "ProductItem", "ProductTile") or file path (e.g., "src/components/ProductItem.tsx"). ' + - 'When a name is provided, the tool automatically searches common component directories. ' + - 'For backward compatibility, file paths are also supported.', - ), - - searchPaths: z - .array(z.string()) - .optional() - .describe( - 'Additional directories to search for components (e.g., ["packages/retail/src", "app/features"]). ' + - 'Only used when component is specified by name (not path).', - ), - - autoMode: z - .boolean() - .optional() - .describe( - 'Auto-generate all configurations with sensible defaults (skip interactive workflow). When enabled, automatically selects suitable props, infers types, and generates decorators without user confirmation.', - ), - - componentId: z.string().optional().describe('Override component ID (default: auto-generated from component name)'), - - conversationContext: z - .object({ - step: z - .enum(['analyze', 'select_props', 'configure_attrs', 'configure_regions', 'confirm_generation']) - .optional() - .describe('Current step in the conversation workflow'), - - componentInfo: z - .record(z.string(), z.any()) - .optional() - .describe('Cached component analysis from previous step'), - - selectedProps: z - .array(z.string()) - .optional() - .describe('Props from component interface selected to expose in Page Designer'), - - newAttributes: z - .array( - z.object({ - name: z.string(), - description: z.string().optional(), - required: z.boolean().optional(), - }), - ) - .optional() - .describe('New attributes to add (not in existing props)'), - - attributeConfig: z - .record( - z.string(), - z.object({ - type: z.string().optional(), - name: z.string().optional(), - defaultValue: z.any().optional(), - values: z.array(z.string()).optional(), - }), - ) - .optional() - .describe('Configuration for each attribute (explicit types, names, etc.)'), - - componentMetadata: z - .object({ - id: z.string(), - name: z.string(), - description: z.string(), - group: z.string().optional(), - }) - .optional() - .describe('Component decorator configuration'), - - regionConfig: z - .object({ - enabled: z.boolean().describe('Whether to include @RegionDefinition decorator'), - regions: z - .array( - z.object({ - id: z.string().describe('Region identifier (e.g., "main", "sidebar")'), - name: z.string().describe('Display name for the region'), - description: z.string().optional().describe('Description of the region purpose'), - maxComponents: z.number().optional().describe('Maximum number of components allowed in region'), - componentTypeInclusions: z - .array(z.string()) - .optional() - .describe('Allowed component types (whitelist)'), - componentTypeExclusions: z - .array(z.string()) - .optional() - .describe('Disallowed component types (blacklist)'), - }), - ) - .optional() - .describe('Array of region definitions'), - }) - .optional() - .describe('Region configuration for nested content areas'), - }) - .optional() - .describe('Conversation state for multi-turn interaction'), - - ...projectContextInputSchema, - }) - .strict(); - -export type PageDesignerDecoratorInput = z.infer; - -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -/** - * Convert component name to kebab-case for use as component ID - * - * Page Designer component IDs should be lowercase with hyphens. - * - * @param name - PascalCase or camelCase name - * @returns kebab-case identifier - * - * @example - * toKebabCase('ProductCard') // => 'product-card' - * toKebabCase('TwoColumnLayout') // => 'two-column-layout' - * - * @internal - */ -function toKebabCase(name: string): string { - return name - .replaceAll(/([a-z])([A-Z])/g, '$1-$2') - .replaceAll(/[\s_]+/g, '-') - .toLowerCase(); -} - -/** - * Convert camelCase prop name to human-readable display name - * - * Used for attribute names shown to merchants in Page Designer UI. - * - * @param fieldName - camelCase field name - * @returns Human-readable name with proper capitalization - * - * @example - * toHumanReadableName('imageUrl') // => 'Image Url' - * toHumanReadableName('ctaButtonText') // => 'Cta Button Text' - * - * @internal - */ -function toHumanReadableName(fieldName: string): string { - return fieldName - .replaceAll(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()) - .trim(); -} - -// ============================================================================ -// WORKFLOW STEP HANDLERS -// ============================================================================ - -/** - * Handle Interactive Mode - Step 1: Analyze - * - * Parses the component file and provides analysis to the LLM: - * - Component name and structure - * - All props with types - * - Categorization (editable, complex, UI-only) - * - Suggested component ID and name - * - * **LLM should then:** - * - Present findings to user - * - Ask which props to expose in Page Designer - * - Collect component metadata (ID, name, description, group) - * - Call next step with selectedProps and componentMetadata - * - * @internal - */ -function handleAnalyzeStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { - const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); - const componentInfo = componentAnalyzer.analyzeComponent(fullPath); - - const editableProps = componentInfo.props.filter((p) => !p.isComplex && !p.isUIOnly); - const complexProps = componentInfo.props.filter((p) => p.isComplex); - const uiProps = componentInfo.props.filter((p) => p.isUIOnly && !p.isComplex); - - const suggestedComponentId = args.componentId || toKebabCase(componentInfo.componentName); - const suggestedComponentName = toHumanReadableName(componentInfo.componentName); - - const instructions = pageDesignerDecoratorRules.getAnalyzeInstructions({ - componentName: componentInfo.componentName, - file: args.component, - hasDecorators: componentInfo.hasDecorators, - interfaceName: componentInfo.interfaceName || 'None found', - totalProps: componentInfo.props.length, - exportType: componentInfo.exportType, - hasEditableProps: editableProps.length > 0, - editableProps, - hasComplexProps: complexProps.length > 0, - complexProps, - hasUIProps: uiProps.length > 0, - uiProps, - suggestedComponentId, - suggestedComponentName, - }); - - return { - content: [ - { - type: 'text' as const, - text: instructions, - }, - ], - }; -} - -function handleSelectPropsStep(args: PageDesignerDecoratorInput, _workspaceRoot: string) { - const selectedProps = args.conversationContext?.selectedProps || []; - const newAttributes = args.conversationContext?.newAttributes || []; - const componentMetadata = args.conversationContext?.componentMetadata; - - if (!componentMetadata) { - return { - content: [ - { - type: 'text' as const, - text: '⚠️ Missing component metadata. Please provide component ID, name, description, and group from the analyze step.', - }, - ], - isError: true, - }; - } - - const confirmation = pageDesignerDecoratorRules.getSelectPropsConfirmation({ - componentMetadata: { - id: componentMetadata.id, - name: componentMetadata.name, - description: componentMetadata.description, - group: componentMetadata.group || 'odyssey_base', - }, - selectedProps, - newAttributes, - selectedPropsCount: selectedProps.length, - newAttributesCount: newAttributes.length, - totalAttributeCount: selectedProps.length + newAttributes.length, - hasSelectedProps: selectedProps.length > 0, - hasNewAttributes: newAttributes.length > 0, - }); - - return { - content: [ - { - type: 'text' as const, - text: confirmation, - }, - ], - }; -} - -function handleConfigureAttrsStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { - const selectedProps = args.conversationContext?.selectedProps || []; - const newAttributes = args.conversationContext?.newAttributes || []; - - const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); - const componentInfo = componentAnalyzer.analyzeComponent(fullPath); - - const attributeAnalysis: Array<{ - name: string; - source: 'existing' | 'new'; - tsType: string; - autoInferred: boolean; - suggestions: TypeSuggestion[]; - }> = []; - - for (const propName of selectedProps) { - const prop = componentInfo.props.find((p) => p.name === propName); - if (!prop) continue; - - const suggestions = generateTypeSuggestions(propName, prop.type); - - attributeAnalysis.push({ - name: propName, - source: 'existing', - tsType: prop.type, - autoInferred: suggestions.length === 0, - suggestions, - }); - } - - for (const attr of newAttributes) { - const suggestions = generateTypeSuggestions(attr.name, 'string'); - - attributeAnalysis.push({ - name: attr.name, - source: 'new', - tsType: 'string', - autoInferred: suggestions.length === 0, - suggestions, - }); - } - - const autoInferredAttrs = attributeAnalysis.filter((a) => a.autoInferred); - const needsConfigAttrs = attributeAnalysis.filter((a) => !a.autoInferred); - - const instructions = pageDesignerDecoratorRules.getConfigureAttrsInstructions({ - totalAttributes: attributeAnalysis.length, - autoInferredCount: autoInferredAttrs.length, - needsConfigCount: needsConfigAttrs.length, - hasAutoInferred: autoInferredAttrs.length > 0, - autoInferredAttrs: autoInferredAttrs.map((a) => ({name: a.name, tsType: a.tsType})), - hasNeedsConfig: needsConfigAttrs.length > 0, - needsConfigAttrs: needsConfigAttrs.map((attr) => ({ - name: attr.name, - tsType: attr.tsType, - source: attr.source === 'existing' ? 'Existing prop' : 'New attribute', - hasSuggestions: attr.suggestions.length > 0, - suggestions: attr.suggestions, - suggestedTypes: attr.suggestions.map((s) => s.type).join(', ') || 'string', - humanReadableName: toHumanReadableName(attr.name), - hasEnumSuggestion: attr.suggestions.some((s) => s.type === 'enum'), - })), - }); - - return { - content: [ - { - type: 'text' as const, - text: instructions, - }, - ], - }; -} - -function handleConfigureRegionsStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { - const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); - const componentInfo = componentAnalyzer.analyzeComponent(fullPath); - - const instructions = pageDesignerDecoratorRules.getConfigureRegionsInstructions({ - componentName: componentInfo.componentName, - }); - - return { - content: [ - { - type: 'text' as const, - text: instructions, - }, - ], - }; -} - -function hasNonEmptyConfig(config: Record | undefined): config is Record { - return config !== null && config !== undefined && Object.keys(config).length > 0; -} - -function buildAttributesFromProps( - selectedProps: string[], - props: {name: string; type: string; optional: boolean}[], - attributeConfig: Record>, -): AttributeContext[] { - return selectedProps.flatMap((propName) => { - const prop = props.find((p) => p.name === propName); - if (!prop) return []; - const config = attributeConfig[propName]; - return [ - { - name: propName, - tsType: prop.type, - optional: prop.optional, - hasConfig: hasNonEmptyConfig(config), - config, - }, - ]; - }); -} - -function buildAttributesFromNewAttrs( - newAttributes: {name: string; required?: boolean}[], - attributeConfig: Record>, -): AttributeContext[] { - return newAttributes.map((attr) => { - const config = attributeConfig[attr.name]; - return { - name: attr.name, - tsType: 'string', - optional: !attr.required, - hasConfig: hasNonEmptyConfig(config), - config, - }; - }); -} - -function resolveRegions(conversationContext: PageDesignerDecoratorInput['conversationContext']) { - const regionConfig = conversationContext?.regionConfig; - const enabled = Boolean(regionConfig?.enabled && regionConfig.regions?.length); - return { - hasRegions: enabled, - regions: enabled ? regionConfig!.regions! : [], - regionCount: enabled ? regionConfig!.regions!.length : 0, - }; -} - -function handleConfirmGenerationStep(args: PageDesignerDecoratorInput, workspaceRoot: string) { - const { - componentMetadata, - selectedProps = [], - newAttributes = [], - attributeConfig = {}, - } = args.conversationContext ?? {}; - - if (!componentMetadata) { - return { - content: [ - { - type: 'text' as const, - text: 'Error: Missing component metadata. Please start from the beginning.', - }, - ], - isError: true, - }; - } - - const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); - const componentInfo = componentAnalyzer.analyzeComponent(fullPath); - - const attributes = [ - ...buildAttributesFromProps(selectedProps, componentInfo.props, attributeConfig), - ...buildAttributesFromNewAttrs(newAttributes, attributeConfig), - ]; - - const {hasRegions, regions, regionCount} = resolveRegions(args.conversationContext); - const componentGroup = componentMetadata.group ?? 'odyssey_base'; - - const context: MetadataContext = { - needsImports: true, - componentId: componentMetadata.id, - componentName: componentMetadata.name, - componentDescription: componentMetadata.description, - componentGroup, - metadataClassName: `${componentInfo.componentName}Metadata`, - hasAttributes: attributes.length > 0, - hasRegions, - hasLoader: false, - regions, - attributes, - }; - - const decoratorCode = generateDecoratorCode(context); - - const userResponse = pageDesignerDecoratorRules.getConfirmGenerationInstructions({ - decoratorCode, - componentName: componentInfo.componentName, - componentId: componentMetadata.id, - componentGroup, - file: args.component, - attributeCount: attributes.length, - hasRegions, - regionCount, - }); - - return { - content: [ - { - type: 'text' as const, - text: userResponse, - }, - ], - }; -} - -/** - * Handle Auto Mode - Single-step decorator generation - * - * **Fully automated workflow:** - * 1. Analyzes component - * 2. Auto-selects suitable props (excludes complex and UI-only) - * 3. Auto-infers Page Designer types from naming patterns - * 4. Generates decorator code immediately - * 5. NO user interaction required - * - * **Selection criteria:** - * - ✅ Simple types (string, number, boolean) - * - ❌ Complex types (objects, functions, React nodes) - * - ❌ UI-only props (className, style, onClick, etc.) - * - * **Auto-configuration:** - * - High-confidence patterns get explicit types (url, image, enum) - * - Others use auto-inferred types - * - Human-readable names auto-generated - * - No regions configured (interactive mode for advanced features) - * - * **Use cases:** - * - Quick setup for standard components - * - Batch processing multiple components - * - Getting started quickly - * - * @internal - */ -function handleAutoMode(args: PageDesignerDecoratorInput, workspaceRoot: string) { - const fullPath = resolveComponent(args.component, workspaceRoot, args.searchPaths); - const componentInfo = componentAnalyzer.analyzeComponent(fullPath); - - if (componentInfo.hasDecorators) { - return { - content: [ - { - type: 'text' as const, - text: `# ⚠️ Component Already Decorated\n\nThe component \`${componentInfo.componentName}\` already has Page Designer decorators.\n\nWould you like to modify the existing decorators instead?`, - }, - ], - }; - } - - const selectedProps = componentInfo.props.filter((p) => !p.isComplex && !p.isUIOnly).map((p) => p.name); - - const attributeConfig: Record = {}; - const attributes: AttributeContext[] = []; - - for (const propName of selectedProps) { - const prop = componentInfo.props.find((p) => p.name === propName); - if (!prop) continue; - - const suggestions = generateTypeSuggestions(propName, prop.type); - const config: {name?: string; type?: string; values?: string[]; defaultValue?: unknown} = { - name: toHumanReadableName(propName), - }; - - const highPrioritySuggestion = suggestions.find((s) => s.priority === 'high'); - if (highPrioritySuggestion) { - config.type = highPrioritySuggestion.type; - - if (highPrioritySuggestion.type === 'enum') { - if (propName.toLowerCase().includes('size')) { - config.values = ['sm', 'default', 'lg']; - config.defaultValue = 'default'; - } else if (propName.toLowerCase().includes('variant')) { - config.values = ['default', 'primary', 'secondary']; - config.defaultValue = 'default'; - } - } - - if (highPrioritySuggestion.type === 'boolean') { - config.defaultValue = false; - } - } - - if (Object.keys(config).length > 1) { - attributeConfig[propName] = config; - } - - attributes.push({ - name: propName, - tsType: prop.type, - optional: prop.optional, - hasConfig: Object.keys(config).length > 1, - config: Object.keys(config).length > 1 ? config : undefined, - }); - } - - const componentId = args.componentId || toKebabCase(componentInfo.componentName); - const componentName = toHumanReadableName(componentInfo.componentName); - const componentDescription = `${componentName} component for Page Designer`; - - const context: MetadataContext = { - needsImports: true, - componentId, - componentName, - componentDescription, - componentGroup: 'odyssey_base', - metadataClassName: `${componentInfo.componentName}Metadata`, - hasAttributes: attributes.length > 0, - hasRegions: false, - hasLoader: false, - regions: [], - attributes, - }; - - const decoratorCode = generateDecoratorCode(context); - - const response = pageDesignerDecoratorRules.getAutoModeInstructions({ - componentName: componentInfo.componentName, - file: args.component, - componentId, - selectedPropCount: selectedProps.length, - autoConfigCount: Object.keys(attributeConfig).length, - autoInferredCount: selectedProps.length - Object.keys(attributeConfig).length, - hasNoSuitableProps: selectedProps.length === 0, - selectedProps: selectedProps.length > 0 ? selectedProps.map((p) => `\`${p}\``).join(', ') : 'None', - decoratorCode, - componentGroup: 'odyssey_base', - }); - - return { - content: [ - { - type: 'text' as const, - text: response, - }, - ], - }; -} - -// ============================================================================ -// TOOL EXPORT -// ============================================================================ - -/** - * Creates the Page Designer decorator tool for Storefront Next. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns The configured MCP tool - */ -export function createPageDesignerDecoratorTool( - loadServices: (projectContext?: ProjectContextInput) => Promise | Services, -): McpTool { - return { - name: 'sfnext_add_page_designer_decorator', - - description: - '[DEPRECATED] Superseded by the storefront-next and storefront-next-figma agent-skills plugins and NOT compatible with the Storefront Next 1.0 GA release. Will be removed in a future release. ' + - 'Adds Page Designer decorators (@Component, @AttributeDefinition, @RegionDefinition) to React components. ' + - 'Two modes: autoMode=true for quick setup with defaults, or interactive mode via conversationContext.step. ' + - 'Component discovery uses --project-directory flag or SFCC_PROJECT_DIRECTORY env var. ' + - 'Auto mode: selects suitable props, infers types, generates code immediately. ' + - 'Interactive mode: multi-step workflow (analyze → select_props → configure_attrs → configure_regions → confirm_generation).', - - inputSchema: pageDesignerDecoratorSchema.shape as ZodRawShape, - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - isGA: false, - - async handler(args: Record) { - try { - // Validate and parse input - const validatedArgs = pageDesignerDecoratorSchema.parse(args) as PageDesignerDecoratorInput; - // Use projectDirectory from services to ensure we search in the correct project directory - // This prevents searches in the home folder when MCP clients spawn servers from ~ - const services = await loadServices({ - projectDirectory: validatedArgs.projectDirectory, - configPath: validatedArgs.configPath, - }); - const workspaceRoot = services.resolveWithProjectDirectory(undefined, validatedArgs.projectDirectory); - - if (validatedArgs.autoMode === undefined && !validatedArgs.conversationContext) { - const fullPath = resolveComponent(validatedArgs.component, workspaceRoot, validatedArgs.searchPaths); - const componentInfo = componentAnalyzer.analyzeComponent(fullPath); - - const instructions = pageDesignerDecoratorRules.getModeSelectionInstructions({ - componentName: componentInfo.componentName, - file: validatedArgs.component, - }); - - return { - content: [ - { - type: 'text' as const, - text: instructions, - }, - ], - }; - } - - if (validatedArgs.autoMode) { - return handleAutoMode(validatedArgs, workspaceRoot); - } - - const step = validatedArgs.conversationContext?.step || 'analyze'; - - switch (step) { - case 'analyze': { - return handleAnalyzeStep(validatedArgs, workspaceRoot); - } - - case 'configure_attrs': { - return handleConfigureAttrsStep(validatedArgs, workspaceRoot); - } - - case 'configure_regions': { - return handleConfigureRegionsStep(validatedArgs, workspaceRoot); - } - - case 'confirm_generation': { - return handleConfirmGenerationStep(validatedArgs, workspaceRoot); - } - - case 'select_props': { - return handleSelectPropsStep(validatedArgs, workspaceRoot); - } - - default: { - const unknownStep: string = step; - throw new Error(`Unknown step: ${unknownStep}`); - } - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - // Check if it's a Zod validation error - if (error instanceof Error && error.name === 'ZodError') { - return { - content: [ - { - type: 'text' as const, - text: `# Error: Invalid Input\n\n${errorMessage}\n\nPlease check your input parameters and try again.`, - }, - ], - isError: true, - }; - } - return { - content: [ - { - type: 'text' as const, - text: `# Error Adding Page Designer Support\n\n${errorMessage}`, - }, - ], - isError: true, - }; - } - }, - }; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules.ts deleted file mode 100644 index 6ced5cdb1..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -// Import all rule renderers -import {renderModeSelection, type ModeSelectionContext} from './rules/1-mode-selection.js'; -import {renderInteractiveOverview} from './rules/2b-0-interactive-overview.js'; -import {renderAnalyzeStep, type AnalyzeStepContext} from './rules/2b-1-interactive-analyze.js'; -import {renderSelectPropsConfirmation, type SelectPropsContext} from './rules/2b-2-interactive-select-props.js'; -import {renderConfigureAttrs, type ConfigureAttrsContext} from './rules/2b-3-interactive-configure-attrs.js'; -import {renderConfigureRegions, type ConfigureRegionsContext} from './rules/2b-4-interactive-configure-regions.js'; -import {renderConfirmGeneration, type ConfirmGenerationContext} from './rules/2b-5-interactive-confirm-generation.js'; -import {renderAutoMode, type AutoModeContext} from './rules/2a-auto-mode.js'; - -/** - * Page Designer decorator rules - type-safe, zero dependencies - */ -export const pageDesignerDecoratorRules = { - /** - * Renders the mode selection prompt - */ - getModeSelectionInstructions(context: ModeSelectionContext): string { - return renderModeSelection(context); - }, - - /** - * Renders the interactive mode workflow overview - */ - getInteractiveOverview(): string { - return renderInteractiveOverview(); - }, - - /** - * Renders Interactive Analyze step instructions - */ - getAnalyzeInstructions(context: AnalyzeStepContext): string { - const workflow = this.getInteractiveOverview(); - const stepContent = renderAnalyzeStep(context); - return `${workflow}\n\n${stepContent}`; - }, - - /** - * Renders Interactive Select Props step confirmation - */ - getSelectPropsConfirmation(context: SelectPropsContext): string { - return renderSelectPropsConfirmation(context); - }, - - /** - * Renders Interactive Configure Attributes step instructions - */ - getConfigureAttrsInstructions(context: ConfigureAttrsContext): string { - return renderConfigureAttrs(context); - }, - - /** - * Renders Interactive Configure Regions step instructions - */ - getConfigureRegionsInstructions(context: ConfigureRegionsContext): string { - return renderConfigureRegions(context); - }, - - /** - * Renders Interactive Confirm Generation step (final code presentation) - */ - getConfirmGenerationInstructions(context: ConfirmGenerationContext): string { - return renderConfirmGeneration(context); - }, - - /** - * Renders Auto Mode instructions - */ - getAutoModeInstructions(context: AutoModeContext): string { - return renderAutoMode(context); - }, -}; - -// Re-export types for convenience -export type {ModeSelectionContext} from './rules/1-mode-selection.js'; -export type {AutoModeContext} from './rules/2a-auto-mode.js'; -export type {AnalyzeStepContext} from './rules/2b-1-interactive-analyze.js'; -export type {SelectPropsContext} from './rules/2b-2-interactive-select-props.js'; -export type {ConfigureAttrsContext} from './rules/2b-3-interactive-configure-attrs.js'; -export type {ConfigureRegionsContext} from './rules/2b-4-interactive-configure-regions.js'; -export type {ConfirmGenerationContext} from './rules/2b-5-interactive-confirm-generation.js'; diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/1-mode-selection.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/1-mode-selection.ts deleted file mode 100644 index 3f458ce8a..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/1-mode-selection.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Mode selection rule - Entry point for Page Designer decorator tool - */ -export interface ModeSelectionContext { - componentName: string; - file: string; -} - -export function renderModeSelection(context: ModeSelectionContext): string { - return `# 🎯 Choose Page Designer Setup Mode - -I need to know which mode you'd like to use for adding Page Designer support to **\`${context.componentName}\`**. - -## Available Modes - -### 🤖 Auto Mode (Quick & Automatic) -- **Best for**: Quick setup, standard components, batch processing -- **What happens**: - - Automatically analyzes the component - - Auto-selects suitable props (excludes complex types) - - Auto-infers types based on naming patterns - - Generates decorators immediately with sensible defaults - - **No confirmation needed** - code generated instantly -- **Time**: ~1 step -- **Control**: Low (uses smart defaults) - -### 👤 Interactive Mode (Step-by-Step) -- **Best for**: Complex components, custom requirements, learning the process -- **What happens**: - - Multi-step workflow with your input at each stage - - Review and approve prop selections - - Configure attribute types, names, and defaults - - Configure regions for nested content (optional) - - **Requires confirmation** before generating code -- **Time**: ~4-5 steps -- **Control**: High (you decide everything) - -## ⚡ How to Proceed - -**⚠️ IMPORTANT: WAIT for the user to choose a mode. DO NOT proceed automatically.** - -Please ask the user: **"Which mode would you like to use: Auto Mode or Interactive Mode?"** - -Once the user responds: - -**For Auto Mode**, call the tool again with: -\`\`\`json -{ - "file": "${context.file}", - "autoMode": true -} -\`\`\` - -**For Interactive Mode**, call the tool again with: -\`\`\`json -{ - "file": "${context.file}", - "conversationContext": { - "step": "analyze" - } -} -\`\`\` - ---- - -💡 **Tip**: If unsure, try **Auto Mode** first. You can always modify the generated decorators later or rerun in Interactive Mode for more control.`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2a-auto-mode.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2a-auto-mode.ts deleted file mode 100644 index 75962971f..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2a-auto-mode.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface AutoModeContext { - componentName: string; - file: string; - componentId: string; - selectedPropCount: number; - autoConfigCount: number; - autoInferredCount: number; - hasNoSuitableProps: boolean; - selectedProps: string; - decoratorCode: string; - componentGroup: string; -} - -export function renderAutoMode(context: AutoModeContext): string { - return `# Auto Mode - Page Designer Decorator Generation - -## LLM INSTRUCTIONS - -### Auto Mode Behavior - -**Single-Step Execution:** -- NO user interaction required -- NO questions to ask -- Analyze component automatically -- Auto-select all suitable props -- Auto-infer types based on naming patterns -- Generate code immediately - -**Auto-Selection Criteria:** -- ✅ Include: Simple props (string, number, boolean) -- ❌ Exclude: Complex types (objects, arrays, functions) -- ❌ Exclude: UI-only props (className, style, etc.) - -**Auto-Inference Patterns:** -- \`*Url\`, \`*Link\` → \`url\` type -- \`*Image\`, \`*Icon\` → \`image\` type -- \`is*\`, \`has*\`, \`enable*\`, \`show*\` → \`boolean\` type (default: false) -- \`*Size\` → \`enum\` type (values: ['default', 'primary', 'secondary']) -- \`*Variant\` → \`enum\` type (values: ['default', 'primary', 'secondary']) - -**Regions:** -- NOT configured in auto mode -- User must use interactive mode for regions - -### Component Analysis Summary - -**Component Name**: ${context.componentName} -**File**: ${context.file} -**Component ID**: ${context.componentId} -**Selected Props**: ${context.selectedPropCount} -**Auto-configured**: ${context.autoConfigCount} -**Auto-inferred**: ${context.autoInferredCount} - -${ - context.hasNoSuitableProps - ? `⚠️ **No suitable props found**. The component has only complex or UI-only props. -Consider adding new attributes manually or using interactive mode.` - : '' -} - ---- - -# USER-FACING RESPONSE - -# ✅ Page Designer Decorators Generated (Auto Mode) - -## Auto-Configuration Summary - -- **Component**: \`${context.componentName}\` -- **Component ID**: \`${context.componentId}\` -- **File**: \`${context.file}\` -- **Selected Props**: ${context.selectedProps} -- **Auto-configured**: ${context.autoConfigCount} -- **Auto-inferred**: ${context.autoInferredCount} - -${ - context.hasNoSuitableProps - ? `⚠️ **No suitable props found**. The component has only complex or UI-only props. -Consider adding new attributes manually or using interactive mode.` - : '' -} - -## Generated Code - -Add this metadata class to your component file: - -\`\`\`typescript -${context.decoratorCode} -\`\`\` - -## Next Steps - -1. **Add the code** to \`${context.file}\` (after imports, before component) -2. **Update component props** to make them optional and add type unions as needed -3. **Generate metadata**: Run \`sfnext generate-cartridge --project-directory .\` -4. **Deploy cartridge**: Run \`sfnext deploy-cartridge --project-directory .\` -5. **Verify in Business Manager**: Check Components > ${context.componentGroup} > ${context.componentName}`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-0-interactive-overview.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-0-interactive-overview.ts deleted file mode 100644 index e3b949fce..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-0-interactive-overview.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Interactive mode workflow overview - */ -export function renderInteractiveOverview(): string { - return `# ⚠️ MANDATORY: Adding Page Designer Support - -## 🚨 CRITICAL: Multi-Step Workflow - -**YOU MUST FOLLOW THIS WORKFLOW:** - -1. **analyze**: Present component analysis and ask configuration questions - - Component identity (ID, name, description, group) - - Which existing props to expose - - Whether to add new attributes - -2. **select_props**: Confirm user's selections - - Show what was selected - - Confirm component metadata - - Prepare for type configuration - -3. **configure_attrs**: Configure attribute types - - Show auto-inferred types - - Ask for explicit type configuration where needed - - Collect defaults and enum values - -4. **configure_regions**: Configure regions (optional) - - Ask if component needs nested content areas - - Configure region definitions if needed - -5. **confirm_generation**: Generate final decorator code - - Render decorators with all configurations - - Show code to user - -**VIOLATION OF THIS WORKFLOW IS A CRITICAL ERROR.** - -## Workflow Enforcement - -- Each step must complete before proceeding to the next -- User must confirm or provide input at each step -- Do not make assumptions about user preferences -- Do not skip steps, even if the answer seems obvious - -## Next Step Instructions - -After presenting analysis, you MUST: -1. Wait for user's answers to ALL questions -2. Call tool again with step: "select_props" and user's responses in conversationContext -3. NEVER proceed to code generation without completing all steps`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-1-interactive-analyze.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-1-interactive-analyze.ts deleted file mode 100644 index 4752206d1..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-1-interactive-analyze.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface PropInfo { - name: string; - type: string; - optional: boolean; -} - -export interface AnalyzeStepContext { - componentName: string; - file: string; - hasDecorators: boolean; - interfaceName?: string; - totalProps: number; - exportType: string; - hasEditableProps: boolean; - editableProps: PropInfo[]; - hasComplexProps: boolean; - complexProps: PropInfo[]; - hasUIProps: boolean; - uiProps: PropInfo[]; - suggestedComponentId: string; - suggestedComponentName: string; -} - -export function renderAnalyzeStep(context: AnalyzeStepContext): string { - return `# Step 1: Component Analysis - -## LLM INSTRUCTIONS - -### Component Analysis Results - -**Component Name**: ${context.componentName} -**File**: ${context.file} -**Has Decorators**: ${context.hasDecorators ? 'Yes (STOP - already decorated)' : 'No (proceed)'} -**Props Interface**: ${context.interfaceName || 'None found'} -**Total Props**: ${context.totalProps} - -${ - context.hasDecorators - ? `⚠️ **CRITICAL**: Component already has Page Designer decorators. -**ACTION**: Stop here and inform user. Do not proceed with generation.` - : '' -} - -### Next Actions (LLM) - -1. Present the analysis to the user -2. Ask all configuration questions (component identity, props selection, new properties) -3. Wait for user's complete response -4. THEN call tool again with step: "select_props" with collected answers - ---- - -# USER-FACING RESPONSE - -${ - context.hasDecorators - ? `# Analysis: ${context.componentName} - -✅ **This component already has Page Designer support.** - -The component has existing decorators (@Component, @AttributeDefinition, etc.). - -Would you like to modify the existing decorators instead?` - : `# Analysis: ${context.componentName} - -## Current State - -- **Component**: \`${context.componentName}\` -- **File**: \`${context.file}\` -- **Props Interface**: \`${context.interfaceName || 'None found'}\` -- **Export Type**: ${context.exportType} - -## Existing Properties Analysis - -${ - context.hasEditableProps - ? `### ✅ Suitable for Page Designer: - -${context.editableProps.map((prop) => `- \`${prop.name}\` (${prop.type})${prop.optional ? ' - optional' : ''}`).join('\n')}` - : '### ⚠️ No suitable properties found' -} - -${ - context.hasComplexProps - ? `### ⚠️ Complex (needs simplification): - -${context.complexProps.map((prop) => `- \`${prop.name}\` (${prop.type}) - Too complex for Page Designer`).join('\n')} - -These complex types cannot be used directly. Consider creating simpler alternatives.` - : '' -} - -${ - context.hasUIProps - ? `### 🎨 UI Props (typically not exposed): - -${context.uiProps.map((prop) => `- \`${prop.name}\` (${prop.type})`).join('\n')} - -These are styling/layout props, usually not exposed to Page Designer.` - : '' -} - -## Configuration Questions - -### 1️⃣ Component Identity - -I suggest: -- **ID**: \`${context.suggestedComponentId}\` -- **Name**: "${context.suggestedComponentName}" -- **Description**: *[Please provide a description]* -- **Group**: \`odyssey_base\` (default) or specify custom group - -✏️ Are these acceptable, or would you like to change them? - -### 2️⃣ Existing Properties - -${ - context.hasEditableProps - ? `**Which properties should be editable in Page Designer?** - -${context.editableProps.map((prop) => `- [ ] \`${prop.name}\` - ${prop.type}`).join('\n')}` - : '⚠️ No existing properties are suitable.' -} - -### 3️⃣ New Properties - -**Should I add any new properties** that don't exist in the component interface? - -Examples: -- Button text, labels, or headings -- Toggle flags (show/hide elements) -- Configuration options - ---- - -**Please answer these questions to proceed.**` -}`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-2-interactive-select-props.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-2-interactive-select-props.ts deleted file mode 100644 index 7f7f4e98b..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-2-interactive-select-props.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface NewAttribute { - name: string; - description?: string; - required?: boolean; -} - -export interface SelectPropsContext { - componentMetadata: { - id: string; - name: string; - description: string; - group?: string; - }; - selectedProps: string[]; - newAttributes: NewAttribute[]; - selectedPropsCount: number; - newAttributesCount: number; - totalAttributeCount: number; - hasSelectedProps: boolean; - hasNewAttributes: boolean; -} - -export function renderSelectPropsConfirmation(context: SelectPropsContext): string { - return `# Step 2: Selection Confirmation - -## LLM INSTRUCTIONS - -### Purpose -Present a clear confirmation of the user's selections from Step 1 (analyze). - -### Context Provided -- Component identity (id, name, description, group) -- Array of selected existing prop names -- Array of new attributes to add -- Counts and flags - -### Next Actions -After showing confirmation, instruct user to confirm proceeding to type configuration. - ---- - -# USER-FACING RESPONSE - -# ✅ Selection Confirmed - -## Component Configuration - -- **ID**: \`${context.componentMetadata.id}\` -- **Name**: "${context.componentMetadata.name}" -- **Description**: "${context.componentMetadata.description}" -- **Group**: \`${context.componentMetadata.group || 'odyssey_base'}\` - -${ - context.hasSelectedProps - ? `## 📋 Selected Existing Props (${context.selectedPropsCount}) - -${context.selectedProps.map((prop) => `- \`${prop}\``).join('\n')}` - : `## 📋 Selected Existing Props - -None selected.` -} - -${ - context.hasNewAttributes - ? `## ➕ New Attributes to Add (${context.newAttributesCount}) - -${context.newAttributes.map((attr) => `- \`${attr.name}\`${attr.description ? ` - ${attr.description}` : ''}${attr.required ? ' (required)' : ''}`).join('\n')}` - : `## ➕ New Attributes to Add - -None requested.` -} - ---- - -## 🎯 Next Step: Attribute Configuration - -Now I'll analyze the types for these ${context.totalAttributeCount} attribute(s) and help you configure them for Page Designer. - -**Please confirm**: Ready to proceed with type configuration? (Say "yes" or "proceed")`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts deleted file mode 100644 index 03a06d707..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-3-interactive-configure-attrs.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface TypeSuggestion { - type: string; - priority: string; - reason: string; -} - -export interface ConfigureAttrsContext { - totalAttributes: number; - autoInferredCount: number; - needsConfigCount: number; - hasAutoInferred: boolean; - autoInferredAttrs: Array<{name: string; tsType: string}>; - hasNeedsConfig: boolean; - needsConfigAttrs: Array<{ - name: string; - tsType: string; - source: string; - hasSuggestions: boolean; - suggestions: TypeSuggestion[]; - suggestedTypes: string; - humanReadableName: string; - hasEnumSuggestion: boolean; - }>; -} - -export function renderConfigureAttrs(context: ConfigureAttrsContext): string { - return `# Step 2: Attribute Configuration - -## LLM INSTRUCTIONS - -### Analysis Complete - -Total attributes to configure: ${context.totalAttributes} -Auto-inferred: ${context.autoInferredCount} -Need configuration: ${context.needsConfigCount} - -### Next Steps for LLM - -**WAIT for user to:** -1. Provide explicit type overrides (if desired) -2. Provide custom names/descriptions (if desired) -3. Provide enum values (if applicable) -4. Or confirm "use defaults" - -**THEN** call tool again with step: "configure_regions" - ---- - -# USER-FACING RESPONSE - -# Attribute Configuration - -${ - context.hasAutoInferred - ? `## ✅ Auto-Configured Attributes - -These attributes will use auto-inferred types (no explicit configuration needed): - -${context.autoInferredAttrs.map((attr) => `- **${attr.name}** (${attr.tsType}) → Auto-inferred as Page Designer type`).join('\n')}` - : '' -} - -${ - context.hasNeedsConfig - ? `## ⚙️ Attributes Needing Configuration - -${context.needsConfigAttrs - .map( - (attr, index) => `### ${index + 1}. \`${attr.name}\` - -- **TypeScript Type**: \`${attr.tsType}\` -- **Source**: ${attr.source} - -${ - attr.hasSuggestions - ? `**Recommendations**: - -${attr.suggestions.map((s) => `- **${s.type}** (${s.priority} priority): ${s.reason}`).join('\n')}` - : '' -} - -**Questions**: -- What Page Designer type should this be? (${attr.suggestedTypes}) -- Custom display name? (default: "${attr.humanReadableName}") -- Default value? -${attr.hasEnumSuggestion ? '- Enum values? (e.g., ["option1", "option2", "option3"])' : ''}`, - ) - .join('\n\n')}` - : '' -} - ---- - -**Please provide configuration for attributes that need it, or say "use defaults" to proceed.**`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts deleted file mode 100644 index 6e7025819..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-4-interactive-configure-regions.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface ConfigureRegionsContext { - componentName: string; -} - -export function renderConfigureRegions(context: ConfigureRegionsContext): string { - return `# Step 3: Region Configuration - -## LLM INSTRUCTIONS - -### 🚨 CRITICAL: Ask User About Regions - -**YOU MUST:** -1. Ask user if component needs regions for nested content -2. If YES, ask for region configurations: - - Region ID (e.g., "main", "sidebar", "footer") - - Region name (display name) - - Description (optional) - - Max components (optional) - - Component type filters (optional) -3. Wait for user response -4. THEN call tool again with step: "confirm_generation" and regionConfig filled - -### Region Context - -Regions allow business users to nest other components inside this component. -Examples: Hero with content slots, Layout containers, Section wrappers - ---- - -# USER-FACING RESPONSE - -# Step 3: Region Configuration - -**Component**: ${context.componentName} - -## About Regions - -Regions define areas where business users can insert other components in Page Designer. -Use regions for: -- Layout containers (e.g., grid, flex layouts) -- Content areas with multiple components -- Sections that need nested content - -**Does this component need regions for nested content?** - -Examples: -- ✅ **YES** for: Layout, Container, Section, Grid -- ❌ **NO** for: Button, Image, Text, ProductCarousel - ---- - -**Please answer:** -1. Does this component need regions? (yes/no) -2. If yes, provide region configuration(s) - -**Region Configuration Example:** -\`\`\` -- id: "main" - name: "Main Content" - description: "Primary content area" - maxComponents: 10 -\`\`\``; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts deleted file mode 100644 index 331af2738..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/rules/2b-5-interactive-confirm-generation.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface ConfirmGenerationContext { - decoratorCode: string; - componentName: string; - componentId: string; - componentGroup: string; - file: string; - attributeCount: number; - hasRegions: boolean; - regionCount?: number; -} - -export function renderConfirmGeneration(context: ConfirmGenerationContext): string { - return `# Step 5: Code Generation - -## LLM INSTRUCTIONS - -### Purpose -Present the generated Page Designer decorator code and next steps. - -### Context Provided -- Complete generated metadata class with decorators -- Component name, ID, group -- File path, attribute count, region info - -### Next Actions -1. Show the generated code -2. Provide clear next steps for deployment -3. Optionally offer to add code to the file - ---- - -# USER-FACING RESPONSE - -# ✅ Page Designer Decorators Generated - -## Generated Code - -Add this metadata class to \`${context.file}\`: - -\`\`\`typescript -${context.decoratorCode} -\`\`\` - -## Summary - -- **Component**: ${context.componentName} -- **Component ID**: \`${context.componentId}\` -- **Group**: \`${context.componentGroup}\` -- **Attributes**: ${context.attributeCount} -${context.hasRegions ? `- **Regions**: ${context.regionCount} configured` : ''} - -## Next Steps - -### 1. Add the Code - -Add the generated metadata class to \`${context.file}\`: -- Place it **after imports** -- Place it **before the component definition** - -### 2. Update Component Props (if needed) - -Make decorated props optional in your component interface: - -\`\`\`typescript -interface ${context.componentName}Props { - title?: string; // Add ? if attribute is not required - // ... other props -} -\`\`\` - -### 3. Generate Cartridge Metadata - -\`\`\`bash -cd packages/template-retail-rsc-app -pnpm sfnext generate-cartridge --project-directory . -\`\`\` - -This creates JSON files in \`cartridges/app_storefrontnext_base/cartridge/experience/components/\`. - -### 4. Deploy Cartridge - -\`\`\`bash -pnpm sfnext deploy-cartridge --project-directory . -\`\`\` - -### 5. Verify in Business Manager - -1. Log into Business Manager -2. Navigate to: **Merchant Tools > Site > Page Designer** -3. Find your component: **Components > ${context.componentGroup} > ${context.componentName}** -4. Verify all attributes appear correctly -5. Test editing a page with your component - ---- - -**Would you like me to add this code to your component file?**`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/templates/decorator-generator.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/templates/decorator-generator.ts deleted file mode 100644 index 1aca75a55..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/page-designer-decorator/templates/decorator-generator.ts +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface AttributeContext { - name: string; - tsType: string; - optional: boolean; - hasConfig: boolean; - config?: { - id?: string; - type?: string; - name?: string; - description?: string; - defaultValue?: unknown; - required?: boolean; - values?: string[]; - }; -} - -export interface RegionContext { - id: string; - name: string; - description?: string; - maxComponents?: number; - componentTypeInclusions?: string[]; - componentTypeExclusions?: string[]; -} - -export interface MetadataContext { - needsImports: boolean; - componentId: string; - componentName: string; - componentDescription: string; - componentGroup?: string; - metadataClassName: string; - hasAttributes: boolean; - hasRegions: boolean; - hasLoader: boolean; - regions: RegionContext[]; - attributes: AttributeContext[]; -} - -/** - * Generate a simple attribute decorator with auto-inferred type - * - * **Simple attributes** use default Page Designer behavior: - * - Type is inferred from TypeScript type - * - No custom configuration needed - * - Minimal decorator syntax - * - * **When to use:** - * - Basic string, number, or boolean props - * - No special validation or defaults needed - * - Standard field naming is acceptable - * - * @param attr - Attribute context - * @returns TypeScript code string for the attribute - * - * @example - * // Input: - * { name: 'title', tsType: 'string', optional: false, hasConfig: false } - * - * // Output: - * `@AttributeDefinition() - * title!: string;` - * - * @internal - */ -function generateSimpleAttribute(attr: AttributeContext): string { - return ` @AttributeDefinition() - ${attr.name}${attr.optional ? '?' : '!'}: ${attr.tsType};`; -} - -/** - * Generate a configured attribute decorator with explicit settings - * - * **Configured attributes** specify custom Page Designer behavior: - * - Explicit `type` (url, image, enum, etc.) - * - Custom `name` for display in Page Designer UI - * - `description` for merchant guidance - * - `defaultValue` for new instances - * - `required` flag for validation - * - `values` array for enum types - * - * **When to use:** - * - URL, image, or rich text fields (need specific editors) - * - Enum fields with predefined options - * - Fields with default values - * - Fields with merchant-friendly names - * - * @param attr - Attribute context with configuration - * @returns TypeScript code string for the configured attribute - * - * @example - * // Input (URL field): - * { - * name: 'ctaUrl', - * tsType: 'string', - * optional: false, - * hasConfig: true, - * config: { - * type: 'url', - * name: 'CTA Button URL', - * description: 'Destination URL for the call-to-action button' - * } - * } - * - * // Output: - * `@AttributeDefinition({ - * type: 'url', - * name: 'CTA Button URL', - * description: 'Destination URL for the call-to-action button', - * }) - * ctaUrl!: string;` - * - * @example - * // Input (Enum field): - * { - * name: 'variant', - * tsType: 'string', - * optional: false, - * hasConfig: true, - * config: { - * type: 'enum', - * name: 'Button Variant', - * values: ['primary', 'secondary', 'outline'], - * defaultValue: 'primary' - * } - * } - * - * // Output: - * `@AttributeDefinition({ - * type: 'enum', - * name: 'Button Variant', - * defaultValue: 'primary', - * values: ['primary', 'secondary', 'outline'], - * }) - * variant!: string;` - * - * @internal - */ -function generateConfiguredAttribute(attr: AttributeContext): string { - if (!attr.config) { - return generateSimpleAttribute(attr); - } - - const config = attr.config; - const configLines: string[] = []; - - if (config.id) { - configLines.push(` id: '${config.id}',`); - } - if (config.name) { - configLines.push(` name: '${config.name}',`); - } - if (config.type) { - configLines.push(` type: '${config.type}',`); - } - if (config.description) { - configLines.push(` description: '${config.description}',`); - } - if (config.defaultValue !== undefined) { - const valueStr = - typeof config.defaultValue === 'string' ? `'${config.defaultValue}'` : JSON.stringify(config.defaultValue); - configLines.push(` defaultValue: ${valueStr},`); - } - if (config.required !== undefined) { - configLines.push(` required: ${config.required},`); - } - if (config.values && config.values.length > 0) { - configLines.push(` values: [${config.values.map((v) => `'${v}'`).join(', ')}],`); - } - - return ` @AttributeDefinition({ -${configLines.join('\n')} - }) - ${attr.name}${attr.optional ? '?' : '!'}: ${attr.tsType};`; -} - -/** - * Generate import statements for Page Designer decorators - * - * **Decision logic:** - * - Always imports `Component` (required for all decorated components) - * - Conditionally imports `AttributeDefinition` (if component has editable props) - * - Conditionally imports `RegionDefinition` (if component has nested content areas) - * - * **Why conditional:** - * Avoids unused imports that would trigger linting warnings. - * - * @param context - Metadata context indicating what's needed - * @returns TypeScript import statements with trailing newlines - * - * @example - * // Component with attributes only: - * generateImports({ needsImports: true, hasAttributes: true, hasRegions: false }) - * // => `import { Component } from '@/lib/decorators/component'; - * // import { AttributeDefinition } from '@/lib/decorators/attribute-definition';\n\n` - * - * @example - * // Component with regions: - * generateImports({ needsImports: true, hasAttributes: true, hasRegions: true }) - * // => All three decorators imported - * - * @internal - */ -function generateImports(context: MetadataContext): string { - if (!context.needsImports) { - return ''; - } - - const imports: string[] = [`import { Component } from '@/lib/decorators/component';`]; - - if (context.hasAttributes) { - imports.push(`import { AttributeDefinition } from '@/lib/decorators/attribute-definition';`); - } - - if (context.hasRegions) { - imports.push(`import { RegionDefinition } from '@/lib/decorators';`); - } - - return `${imports.join('\n')}\n\n`; -} - -/** - * Generate @RegionDefinition decorator for nested content areas - * - * **Regions** define areas where merchants can add nested components in Page Designer. - * Common use cases: - * - Layout containers (grid cells, columns) - * - Content sections (header, body, footer) - * - Tab panels, accordion items - * - * **Configuration options:** - * - `id`: Unique identifier for the region - * - `name`: Display name in Page Designer - * - `description`: Merchant guidance - * - `maxComponents`: Limit number of nested components - * - `componentTypeInclusions`: Whitelist of allowed component types - * - `componentTypeExclusions`: Blacklist of disallowed component types - * - * @param context - Metadata context with region definitions - * @returns TypeScript code for @RegionDefinition decorator or empty string - * - * @example - * // Simple region: - * { - * hasRegions: true, - * regions: [{ - * id: 'main', - * name: 'Main Content Area', - * description: 'Add content components here' - * }] - * } - * // => `@RegionDefinition([ - * // { - * // id: 'main', - * // name: 'Main Content Area', - * // description: 'Add content components here', - * // } - * // ])\n` - * - * @example - * // Constrained region: - * { - * hasRegions: true, - * regions: [{ - * id: 'grid', - * name: 'Product Grid', - * maxComponents: 12, - * componentTypeInclusions: ['product-tile', 'product-card'] - * }] - * } - * - * @internal - */ -function generateRegionDefinition(context: MetadataContext): string { - if (!context.hasRegions || context.regions.length === 0) { - return ''; - } - - const regionsDef = context.regions - .map((region) => { - const lines: string[] = [` {`, ` id: '${region.id}',`, ` name: '${region.name}',`]; - - if (region.description) { - lines.push(` description: '${region.description}',`); - } - if (region.maxComponents !== undefined) { - lines.push(` maxComponents: ${region.maxComponents},`); - } - if (region.componentTypeInclusions && region.componentTypeInclusions.length > 0) { - lines.push( - ` componentTypeInclusions: [${region.componentTypeInclusions.map((t) => `'${t}'`).join(', ')}],`, - ); - } - if (region.componentTypeExclusions && region.componentTypeExclusions.length > 0) { - lines.push( - ` componentTypeExclusions: [${region.componentTypeExclusions.map((t) => `'${t}'`).join(', ')}],`, - ); - } - - lines.push(` }`); - return lines.join('\n'); - }) - .join(',\n'); - - return `@RegionDefinition([\n${regionsDef}\n])\n`; -} - -/** - * Generate complete Page Designer decorator code for a React component - * - * **This is the main code generation function.** - * - * Produces a TypeScript class with decorators that: - * 1. Registers the component in Page Designer - * 2. Defines editable attributes (props) - * 3. Optionally defines nested content regions - * - * **Output structure:** - * ```typescript - * import { Component } from '...'; - * import { AttributeDefinition } from '...'; - * - * @Component('component-id', { - * name: 'Component Name', - * description: '...', - * group: 'category' - * }) - * @RegionDefinition([...]) // Optional - * export class ComponentMetadata { - * @AttributeDefinition({ ... }) - * prop1!: string; - * - * @AttributeDefinition() - * prop2?: number; - * } - * ``` - * - * **Generated code must be:** - * - Added to the component file (after imports, before component) - * - Compiled with TypeScript - * - Used by the staticRegistry Vite plugin to generate the component registry - * - * @param context - Complete metadata context - * @returns TypeScript code string ready to paste into component file - * - * @example - * // Simple component with attributes: - * generateDecoratorCode({ - * needsImports: true, - * componentId: 'hero-banner', - * componentName: 'Hero Banner', - * componentDescription: 'Large hero section with image and CTA', - * componentGroup: 'content', - * metadataClassName: 'HeroBannerMetadata', - * hasAttributes: true, - * hasRegions: false, - * hasLoader: false, - * regions: [], - * attributes: [ - * { name: 'title', tsType: 'string', optional: false, hasConfig: false }, - * { name: 'imageUrl', tsType: 'string', optional: false, hasConfig: true, - * config: { type: 'image', name: 'Background Image' } } - * ] - * }) - * - * @example - * // Layout component with regions: - * generateDecoratorCode({ - * needsImports: true, - * componentId: 'two-column', - * componentName: 'Two Column Layout', - * componentDescription: 'Side-by-side content layout', - * componentGroup: 'layout', - * metadataClassName: 'TwoColumnMetadata', - * hasAttributes: false, - * hasRegions: true, - * hasLoader: false, - * regions: [ - * { id: 'left', name: 'Left Column' }, - * { id: 'right', name: 'Right Column' } - * ], - * attributes: [] - * }) - * - * @public - */ -export function generateDecoratorCode(context: MetadataContext): string { - const imports = generateImports(context); - - const componentDecorator = `@Component('${context.componentId}', { - name: '${context.componentName}', - description: '${context.componentDescription}',${context.componentGroup ? `\n group: '${context.componentGroup}',` : ''} -})`; - - const regionDefinition = generateRegionDefinition(context); - - const attributes = context.attributes - .map((attr) => { - return attr.hasConfig ? generateConfiguredAttribute(attr) : generateSimpleAttribute(attr); - }) - .join('\n\n'); - - return `${imports}${componentDecorator} -${regionDefinition}export class ${context.metadataClassName} { -${attributes} -}`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/sfnext-development-guidelines.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/sfnext-development-guidelines.ts deleted file mode 100644 index 88a855ae5..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/sfnext-development-guidelines.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Developer Guidelines tool for Storefront Next. - * - * Provides critical development guidelines and best practices for building - * Storefront Next applications with React Server Components. - * - * @module tools/storefrontnext/sfnext-development-guidelines - */ - -import {readFileSync} from 'node:fs'; -import {createRequire} from 'node:module'; -import path from 'node:path'; -import {z} from 'zod'; -import type {McpTool} from '../../utils/index.js'; -import type {Services} from '../../services.js'; -import {createToolAdapter, textResult} from '../adapter.js'; - -// Resolve the content directory from the package root -// Uses createRequire to find the package.json location, which is robust -// regardless of where this module is located in the build output -const require = createRequire(import.meta.url); -const packageRoot = path.dirname(require.resolve('@salesforce/b2c-dx-mcp/package.json')); -const CONTENT_DIR = path.join(packageRoot, 'content', 'sfnext'); - -/** - * Section metadata with key and optional description. - * Single source of truth for all available sections. - */ -const SECTIONS_METADATA = [ - {key: 'quick-reference', description: null}, // Meta-section, excluded from topics list - { - key: 'data-fetching', - description: - 'server-only data loading (no client loaders), synchronous loaders for streaming, data fetching patterns', - }, - {key: 'state-management', description: 'state management patterns'}, - {key: 'auth', description: 'authentication and session management'}, - {key: 'config', description: 'configuration'}, - {key: 'i18n', description: 'i18n patterns and internationalization'}, - {key: 'components', description: 'component best practices'}, - {key: 'styling', description: 'Tailwind CSS 4, Shadcn/ui, styling guidelines'}, - {key: 'page-designer', description: 'Page Designer integration'}, - {key: 'performance', description: 'performance optimization'}, - {key: 'testing', description: 'testing strategies'}, - {key: 'extensions', description: 'framework extensions'}, - {key: 'pitfalls', description: 'common pitfalls'}, -] as const; - -/** - * Derived: array of section keys for validation. - */ -const _SECTIONS = SECTIONS_METADATA.map((s) => s.key); - -type SectionKey = (typeof SECTIONS_METADATA)[number]['key']; - -/** - * Generates the topics list for the tool description. - * Excludes meta-sections (like quick-reference) that don't have descriptions. - * @returns Comma-separated list of topics - */ -function generateTopicsList(): string { - return SECTIONS_METADATA.filter((s) => s.description !== null) - .map((s) => s.description) - .join(', '); -} - -/** - * Input schema for the developer guidelines tool. - */ -interface DeveloperGuidelinesInput { - sections?: SectionKey[]; -} - -/** - * Detailed section content loaded from markdown files. - * Built dynamically from SECTIONS_METADATA to avoid duplication. - */ -const SECTION_CONTENT: Record = Object.fromEntries( - SECTIONS_METADATA.map((section) => { - const filename = `${section.key}.md`; - const filePath = path.join(CONTENT_DIR, filename); - const content = readFileSync(filePath, 'utf8'); - return [section.key, content]; - }), -) as Record; - -/** - * Default sections to return when no sections are specified. - * Includes quick-reference plus the most critical detailed sections - * to provide comprehensive guidelines by default. - */ -const DEFAULT_SECTIONS: SectionKey[] = ['quick-reference', 'data-fetching', 'components', 'testing']; - -/** - * Creates the developer guidelines tool for Storefront Next. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns The configured MCP tool - */ -export function createDeveloperGuidelinesTool(loadServices: () => Promise | Services): McpTool { - return createToolAdapter( - { - name: 'sfnext_get_guidelines', - description: - '[DEPRECATED] Superseded by the storefront-next and storefront-next-figma agent-skills plugins and NOT compatible with the Storefront Next 1.0 GA release. Will be removed in a future release. ' + - 'ESSENTIAL FIRST STEP for Storefront Next development. Returns critical architecture rules, coding standards, and best practices. ' + - 'Use this tool FIRST before writing any Storefront Next code to understand non-negotiable patterns for React Server Components, ' + - 'data loading, and framework constraints. Returns comprehensive guidelines by default (quick-reference + key sections); ' + - 'supports retrieving specific topic sections. ' + - 'CRITICAL INSTRUCTION: ALWAYS present ALL returned content in FULL - DO NOT SUMMARIZE, DO NOT ADD SUMMARIES, ' + - 'DO NOT ADD OVERVIEWS. The returned content IS the complete answer - display it exactly as provided.', - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - isGA: false, - requiresInstance: false, - inputSchema: { - sections: z - .array(z.enum([..._SECTIONS] as [string, ...string[]])) - .optional() - .describe( - 'Optional array of specific sections to retrieve. If not specified, returns comprehensive guidelines ' + - '(quick-reference, data-fetching, components, testing). ' + - 'CRITICAL: Present ALL returned content in FULL - DO NOT SUMMARIZE. ' + - 'Available sections: quick-reference, data-fetching, state-management, auth, config, i18n, ' + - 'components, page-designer, performance, testing, extensions, pitfalls. ' + - `Topics covered: ${generateTopicsList()}. ` + - 'Content is complete - present exactly as provided, no summaries.', - ), - }, - async execute(args) { - // Handle empty array case explicitly - if (args.sections && args.sections.length === 0) { - return ''; - } - - // Default to comprehensive set of key sections if no sections specified - const sections = args.sections || DEFAULT_SECTIONS; - - // Multiple sections: combine with separators - const combinedContent = sections.map((section) => SECTION_CONTENT[section]).join('\n\n---\n\n'); - - // Apply instructions for all multi-section responses to ensure full content display - // Single sections are typically smaller and don't need heavy instructions - const isMultiSection = sections.length > 1; - - // Prepend explicit instruction to present full content (not summarized) - // This helps AI assistants understand they should show the complete guidelines without summarization - const fullContentInstruction = isMultiSection - ? '⚠️ CRITICAL: Display the FULL content below. DO NOT summarize, condense, or add overviews.\n\n' + - '📋 STOREFRONT NEXT DEVELOPMENT GUIDELINES\n\n' + - '---\n\n' - : ''; - - // Add footer instruction to reinforce the message for multi-section responses - const footerInstruction = isMultiSection - ? '\n\n---\n\n⚠️ END OF CONTENT - Full content displayed above. Do not add summaries.\n' - : ''; - - // For single sections, return directly (backward compatible) - // For multiple sections, wrap with instructions - if (sections.length === 1) { - return SECTION_CONTENT[sections[0]]; - } - - return fullContentInstruction + combinedContent + footerInstruction; - }, - formatOutput: (output) => textResult(output), - }, - loadServices, - ); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/README.md b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/README.md deleted file mode 100644 index 4fa92febc..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# Site Theming Tool - -Tool for applying colors, fonts, and visual styling to Storefront Next sites with guided questions and automatic WCAG color contrast validation. - -## Overview - -This tool provides theming guidelines, collects user preferences through structured questions, and validates color combinations for accessibility before implementation. **Call this tool FIRST** when the user requests theming changes—even if they have already provided colors or fonts. - -## Key Features - -- **Mandatory workflow**: Ensures questions are asked and validation is performed before implementation -- **Automatic WCAG validation**: Validates color contrast when `colorMapping` is provided in `conversationContext.collectedAnswers` -- **Content-driven**: Loads guidance from markdown files in `content/site-theming/` -- **Merge support**: Combines multiple theming files via `fileKeys` -- **Custom content**: Add custom files via `THEMING_FILES` environment variable - -## File Structure - -``` -site-theming/ -├── index.ts # Tool factory and orchestration -├── types.ts # Shared types (ColorMapping, CollectedAnswers, etc.) -├── color-mapping.ts # Color combination derivation and WCAG validation -├── guidance-merger.ts # Merges multiple ThemingGuidance objects -├── response-builder.ts # Response construction from guidance and context -├── theming-store.ts # Content loading and parsing -└── color-contrast.ts # WCAG 2.1 contrast calculation and validation -``` - -Content files (in `packages/b2c-dx-mcp/content/site-theming/`): - -- `theming-questions.md` - Questions, critical rules, DO/DON'T guidelines -- `theming-validation.md` - Validation workflow, color/font validation rules -- `theming-accessibility.md` - Accessibility-specific guidance - -## Usage - -### Workflow - -1. **First call**: Call tool with `conversationContext.collectedAnswers` (can be empty `{colors: [], fonts: []}`) -2. **Ask questions**: Tool returns questions—ask user one at a time, collect answers -3. **Validation call**: Construct `colorMapping` from answers, call tool again with `collectedAnswers.colorMapping` -4. **Present findings**: Show validation results (contrast ratios, WCAG status) to user -5. **Wait for confirmation**: Do not implement until user confirms -6. **Implement**: Apply theme changes to `app.css` or project theme files - -### Basic Usage - -```json -// First call - get guidelines and questions -{ - "name": "sfnext_configure_theme", - "arguments": { - "conversationContext": { - "collectedAnswers": {"colors": [], "fonts": []} - } - } -} - -// Validation call - after constructing colorMapping -{ - "name": "sfnext_configure_theme", - "arguments": { - "conversationContext": { - "collectedAnswers": { - "colors": [{"hex": "#635BFF", "type": "primary"}], - "colorMapping": { - "lightText": "#000000", - "lightBackground": "#FFFFFF", - "buttonText": "#FFFFFF", - "buttonBackground": "#0A2540" - } - } - } - } -} -``` - -### List Available Files - -```json -{} -``` - -Returns list of loaded theming file keys. Use `fileKeys` to add custom files to the default set. - -### Custom Theming Files - -Set `THEMING_FILES` environment variable (JSON array of `{key, path}`): - -```bash -export THEMING_FILES='[{"key":"custom-theming","path":"path/to/custom-theming.md"}]' -``` - -Paths are relative to the project directory (from `--project-directory` or `SFCC_PROJECT_DIRECTORY`). - -Custom files can also be added via the `fileKeys` parameter when calling the tool. Files must follow the format below to be parsed correctly. - -#### Custom Theming File Format - -Custom theming files must be Markdown (`.md` or `.mdc`). The parser extracts content based on specific heading patterns. Use these headings to structure your file: - -| Heading pattern | Purpose | -| --------------------------- | ------------------------------------------------------------------------------- | -| `## 🔄 WORKFLOW` | Workflow steps and instructions. Numbered steps (`1. Step text`) are extracted. | -| `### 📝 EXTRACTION` | Instructions for extracting theming info from user input. | -| `### ✅ PRE-IMPLEMENTATION` | Pre-implementation checklist. | -| `## ✅ VALIDATION` | Validation rules. | -| `### A. Color` | Color validation rules. | -| `### B. Font` | Font validation rules. | -| `### C. General` | General validation rules. | -| `### IMPORTANT` | Validation requirements. | -| `## ⚠️ CRITICAL: Title` | Critical guidelines (layout preservation, wait-for-response, etc.). | -| `## 📋 Title` | Specification compliance rules. | -| `### What TO Change:` | DO rules. List items with `-` are extracted. | -| `### What NOT to Change:` | DON'T rules. List items with `-` are extracted. | - -**Questions**: Lines ending with `?` and length > 10 (from bullet or numbered lists) are extracted as questions. Keywords like "color", "font", "primary", "accent" determine category (colors, typography, general). - -**Optional frontmatter** (YAML at top of file): - -```yaml ---- -description: Brief description -alwaysApply: false ---- -``` - -Reference the built-in files (`theming-questions.md`, `theming-validation.md`, `theming-accessibility.md`) in `content/site-theming/` for examples. - -## Architecture - -### Content Loading - -The tool loads markdown files from `content/site-theming/` at initialization. It parses: - -- Workflow steps (## WORKFLOW) -- Validation rules (## VALIDATION) -- Critical guidelines (## CRITICAL) -- DO/DON'T rules (### What TO Change / What NOT to Change) -- Generated questions from guidelines - -### Color Contrast Validation - -When `colorMapping` is present in `collectedAnswers` (colorMapping alone is sufficient; the colors array is not required), the tool: - -1. Derives foreground/background combinations from the mapping -2. Calculates WCAG 2.1 contrast ratios -3. Determines AA/AAA compliance -4. Returns validation results with recommendations for failing combinations - -### Merging Guidance - -Multiple files are merged: questions are deduplicated by ID, guidelines and rules are concatenated, workflow and validation sections are combined. - -## When to Use This Tool - -Use this tool when: - -- User wants to apply brand colors, fonts, or visual styling to a Storefront Next site -- User has provided colors/fonts and needs validation before implementation -- You need to follow the theming workflow (questions, validation, confirmation) - -## Testing - -### Automated Tests - -```bash -cd packages/b2c-dx-mcp -pnpm run test:agent -- test/tools/storefrontnext/site-theming/ -``` - -The test suite covers tool metadata, behavior, color validation, file merging, edge cases, `color-contrast.ts`, and `theming-store.ts`. - -See [`test/tools/storefrontnext/site-theming/README.md`](../../../../test/tools/storefrontnext/site-theming/README.md) for detailed testing instructions and manual test scenarios. - -## License - -Apache-2.0 - Copyright (c) 2025, Salesforce, Inc. diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-contrast.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-contrast.ts deleted file mode 100644 index c8cec96d8..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-contrast.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * WCAG 2.1 color contrast utilities for accessibility validation. - * - * Provides luminance calculation, contrast ratio computation, and WCAG compliance - * checking for theming and color validation in Storefront Next. - * - * @module tools/storefrontnext/site-theming/color-contrast - */ - -/** - * WCAG 2.1 constants for contrast ratio calculation - * These values are specified in the WCAG 2.1 standard - */ -const WCAG_CONTRAST_OFFSET = 0.05; // Offset added to luminance values in contrast ratio formula - -// Linear RGB conversion constants (sRGB to linear RGB) -const LINEAR_RGB_THRESHOLD = 0.039_28; // Threshold for linear RGB conversion -const LINEAR_RGB_DIVISOR = 12.92; // Divisor for values below threshold -const GAMMA_CORRECTION_OFFSET = 0.055; // Offset for gamma correction -const GAMMA_CORRECTION_DIVISOR = 1.055; // Divisor for gamma correction -const GAMMA_EXPONENT = 2.4; // Gamma exponent for sRGB - -// Relative luminance weights (WCAG 2.1 standard) -const LUMINANCE_RED_WEIGHT = 0.2126; -const LUMINANCE_GREEN_WEIGHT = 0.7152; -const LUMINANCE_BLUE_WEIGHT = 0.0722; - -/** Valid 6-digit hex color pattern (with optional # prefix) */ -const HEX_PATTERN = /^#?[0-9A-Fa-f]{6}$/; - -/** - * Validates that a string is a valid 6-digit hex color. - * @param hex - Hex color string to validate - * @returns true if valid - */ -export function isValidHex(hex: string): boolean { - return typeof hex === 'string' && HEX_PATTERN.test(hex.trim()); -} - -/** - * Calculates the relative luminance of a color according to WCAG 2.1 - * @param hex - Hex color string (e.g., "#635BFF") - * @returns Relative luminance value between 0 and 1 - * @throws Error if hex format is invalid - */ -export function getLuminance(hex: string): number { - const trimmed = hex.trim(); - if (!HEX_PATTERN.test(trimmed)) { - throw new Error(`Invalid hex color: "${hex}". Expected 6-digit hex (e.g., #635BFF).`); - } - const cleanHex = trimmed.replace('#', ''); - - // Parse RGB values - const r = Number.parseInt(cleanHex.slice(0, 2), 16) / 255; - const g = Number.parseInt(cleanHex.slice(2, 4), 16) / 255; - const b = Number.parseInt(cleanHex.slice(4, 6), 16) / 255; - - // Convert to linear RGB - const [rs, gs, bs] = [r, g, b].map((c) => { - return c <= LINEAR_RGB_THRESHOLD - ? c / LINEAR_RGB_DIVISOR - : ((c + GAMMA_CORRECTION_OFFSET) / GAMMA_CORRECTION_DIVISOR) ** GAMMA_EXPONENT; - }); - - // Calculate relative luminance - return LUMINANCE_RED_WEIGHT * rs + LUMINANCE_GREEN_WEIGHT * gs + LUMINANCE_BLUE_WEIGHT * bs; -} - -/** - * Calculates the contrast ratio between two colors according to WCAG 2.1 - * @param color1 - First hex color string - * @param color2 - Second hex color string - * @returns Contrast ratio (1:1 to 21:1) - */ -export function getContrastRatio(color1: string, color2: string): number { - const l1 = getLuminance(color1); - const l2 = getLuminance(color2); - const lighter = Math.max(l1, l2); - const darker = Math.min(l1, l2); - return (lighter + WCAG_CONTRAST_OFFSET) / (darker + WCAG_CONTRAST_OFFSET); -} - -/** - * WCAG compliance levels - */ -export enum WCAGLevel { - AA = 'AA', // 4.5:1 for normal text - AA_LARGE = 'AA_LARGE', // 3:1 for large text - AAA = 'AAA', // 7:1 for normal text - AAA_LARGE = 'AAA_LARGE', // 4.5:1 for large text - FAIL = 'FAIL', -} - -/** - * Determines WCAG compliance level for a contrast ratio - * @param ratio - Contrast ratio - * @param isLargeText - Whether this is for large text (18pt+ or 14pt+ bold) - * @returns WCAG compliance level - */ -export function getWCAGLevel(ratio: number, isLargeText = false): WCAGLevel { - if (isLargeText) { - if (ratio >= 4.5) { - return WCAGLevel.AAA_LARGE; - } - if (ratio >= 3) { - return WCAGLevel.AA_LARGE; - } - return WCAGLevel.FAIL; - } - - if (ratio >= 7) { - return WCAGLevel.AAA; - } - if (ratio >= 4.5) { - return WCAGLevel.AA; - } - return WCAGLevel.FAIL; -} - -/** - * Result of color contrast validation for a single foreground/background pair. - * - * @property {string} color1 - First hex color (typically foreground) - * @property {string} color2 - Second hex color (typically background) - * @property {number} ratio - Contrast ratio (1:1 to 21:1) - * @property {WCAGLevel} wcagLevel - WCAG compliance level - * @property {boolean} passesAA - Whether the combination meets WCAG AA - * @property {boolean} passesAAA - Whether the combination meets WCAG AAA - * @property {boolean} isLargeText - Whether validation used large-text thresholds - * @property {string} visualAssessment - Readability assessment (excellent, good, acceptable, poor) - * @property {string} [recommendation] - Optional suggestion when contrast is suboptimal - */ -export interface ContrastValidationResult { - color1: string; - color2: string; - ratio: number; - wcagLevel: WCAGLevel; - passesAA: boolean; - passesAAA: boolean; - isLargeText: boolean; - visualAssessment: 'acceptable' | 'excellent' | 'good' | 'poor'; - recommendation?: string; -} - -/** - * Validates contrast between two colors - * @param color1 - First hex color string - * @param color2 - Second hex color string - * @param isLargeText - Whether this is for large text - * @returns Validation result with contrast ratio and compliance info - */ -export function validateContrast(color1: string, color2: string, isLargeText = false): ContrastValidationResult { - const ratio = getContrastRatio(color1, color2); - const wcagLevel = getWCAGLevel(ratio, isLargeText); - const passesAA = ratio >= (isLargeText ? 3 : 4.5); - const passesAAA = ratio >= (isLargeText ? 4.5 : 7); - - // Visual assessment based on ratio - let visualAssessment: 'acceptable' | 'excellent' | 'good' | 'poor'; - let recommendation: string | undefined; - - if (ratio >= 7) { - visualAssessment = 'excellent'; - } else if (ratio >= 5) { - visualAssessment = 'good'; - } else if (ratio >= 4.5) { - visualAssessment = 'acceptable'; - recommendation = - 'Meets minimum WCAG AA but may be difficult to read, especially for body text. Consider using a darker/lighter color for better readability.'; - } else { - visualAssessment = 'poor'; - recommendation = - 'Does not meet WCAG AA standards. Text will be difficult to read. Strongly recommend using a color with better contrast.'; - } - - return { - color1, - color2, - ratio, - wcagLevel, - passesAA, - passesAAA, - isLargeText, - visualAssessment, - recommendation, - }; -} - -/** - * Validates multiple color combinations for WCAG compliance. - * - * @param combinations - Array of foreground/background pairs with optional label and large-text flag - * @returns Array of validation results, each including the input label if provided - */ -export function validateColorCombinations( - combinations: Array<{ - foreground: string; - background: string; - isLargeText?: boolean; - label?: string; - }>, -): Array { - return combinations.map((combo) => ({ - ...validateContrast(combo.foreground, combo.background, combo.isLargeText ?? false), - label: combo.label, - })); -} - -/** - * Formats a validation result as a human-readable string for display to users. - * - * @param result - Validation result, optionally with a label for the color combination - * @returns Multi-line string with contrast ratio, WCAG status, and recommendation (if any) - */ -export function formatValidationResult(result: ContrastValidationResult & {label?: string}): string { - const label = result.label ? `${result.label}: ` : ''; - const textType = result.isLargeText ? 'large text' : 'normal text'; - const wcagStatus = result.passesAAA ? '✅ AAA' : result.passesAA ? '✅ AA' : '❌ FAIL'; - - let output = `${label}${result.color1} on ${result.color2}\n`; - output += ` Contrast Ratio: ${result.ratio.toFixed(2)}:1\n`; - output += ` WCAG ${textType}: ${wcagStatus}\n`; - output += ` Visual Assessment: ${result.visualAssessment.toUpperCase()}\n`; - - if (result.recommendation) { - output += ` ⚠️ ${result.recommendation}\n`; - } - - return output; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-mapping.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-mapping.ts deleted file mode 100644 index 4384d43e5..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/color-mapping.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Derives foreground/background color combinations from a color mapping and - * appends WCAG validation results to response text. - * - * @module tools/storefrontnext/site-theming/color-mapping - */ - -import {validateColorCombinations, formatValidationResult, isValidHex} from './color-contrast.js'; - -/** A foreground/background color pair for contrast validation */ -export type ColorCombination = { - foreground: string; - background: string; - label: string; - isLargeText?: boolean; -}; - -type ComboContext = {colorMapping: Record; lightBg: string; darkBg: string; buttonBg: string}; - -function tryTextCombo(key: string, color: string, keyLower: string, ctx: ComboContext): ColorCombination | null { - if (keyLower.includes('text') && keyLower.includes('light') && isValidHex(ctx.lightBg)) { - return {foreground: color, background: ctx.lightBg, label: `${key}: ${color} on light background (${ctx.lightBg})`}; - } - if (keyLower.includes('text') && keyLower.includes('dark') && isValidHex(ctx.darkBg)) { - return {foreground: color, background: ctx.darkBg, label: `${key}: ${color} on dark background (${ctx.darkBg})`}; - } - const isButtonText = keyLower === 'buttontext' || (keyLower.includes('button') && keyLower.includes('text')); - if (isButtonText && isValidHex(ctx.buttonBg)) { - return { - foreground: color, - background: ctx.buttonBg, - label: `${key}: ${color} on button background (${ctx.buttonBg})`, - }; - } - if (keyLower.includes('link') && isValidHex(ctx.lightBg)) { - return {foreground: color, background: ctx.lightBg, label: `${key}: ${color} on light background (${ctx.lightBg})`}; - } - return null; -} - -function tryBackgroundCombo(key: string, color: string, ctx: ComboContext): ColorCombination | null { - const foregroundKey = key.replace(/Background|Bg/i, 'Text') || key.replace(/Background|Bg/i, 'Foreground'); - const foreground = ctx.colorMapping[foregroundKey] || ctx.colorMapping[`${key.replace(/Background|Bg/i, '')}Text`]; - if (foreground?.startsWith('#') && isValidHex(foreground)) { - return {foreground, background: color, label: `${foregroundKey || 'text'} (${foreground}) on ${key} (${color})`}; - } - return null; -} - -function tryTextForegroundCombo( - key: string, - color: string, - keyLower: string, - ctx: ComboContext, -): ColorCombination | null { - const backgroundKey = key.replace(/Text|Foreground/i, 'Background') || key.replace(/Text|Foreground/i, 'Bg'); - let background = ctx.colorMapping[backgroundKey]; - let backgroundLabel = backgroundKey; - if (!background) { - background = keyLower.includes('button') ? ctx.buttonBg : keyLower.includes('dark') ? ctx.darkBg : ctx.lightBg; - backgroundLabel = keyLower.includes('button') - ? 'button background' - : keyLower.includes('dark') - ? 'dark background' - : 'light background'; - } - if (background?.startsWith('#') && isValidHex(background)) { - return {foreground: color, background, label: `${key} (${color}) on ${backgroundLabel} (${background})`}; - } - return null; -} - -function tryComboForEntry(key: string, color: string, ctx: ComboContext): ColorCombination | null { - const keyLower = key.toLowerCase(); - const textCombo = tryTextCombo(key, color, keyLower, ctx); - if (textCombo) return textCombo; - if (keyLower.includes('background') || keyLower.includes('bg')) return tryBackgroundCombo(key, color, ctx); - if (keyLower.includes('text') || keyLower.includes('foreground')) - return tryTextForegroundCombo(key, color, keyLower, ctx); - return null; -} - -/** - * Builds foreground/background color combinations from a semantic color mapping. - * Derives pairs for text-on-background, button text, links, etc. - */ -export function buildColorCombinations(colorMapping: Record): ColorCombination[] { - const ctx: ComboContext = { - colorMapping, - lightBg: colorMapping.lightBackground || colorMapping.background || '#FFFFFF', - darkBg: colorMapping.darkBackground || '#18181B', - buttonBg: colorMapping.buttonBackground || colorMapping.primary || '#0A2540', - }; - const combinations: ColorCombination[] = []; - - for (const [key, color] of Object.entries(colorMapping)) { - if (!color || !color.startsWith('#') || !isValidHex(color)) continue; - const combo = tryComboForEntry(key, color, ctx); - if (combo) combinations.push(combo); - } - - if (combinations.length === 0) { - const whiteBg = '#FFFFFF'; - const darkBgFallback = '#18181B'; - for (const [key, color] of Object.entries(colorMapping)) { - if (!color || !color.startsWith('#') || !isValidHex(color)) continue; - if (key.toLowerCase().includes('background') || key.toLowerCase().includes('bg')) continue; - combinations.push( - {foreground: color, background: whiteBg, label: `${key} (${color}) on white background`}, - {foreground: color, background: darkBgFallback, label: `${key} (${color}) on dark background`}, - ); - } - } - return combinations; -} - -/** - * Appends WCAG color contrast validation results to the given instructions string. - */ -export function appendValidationSection(internalInstructions: string, combinations: ColorCombination[]): string { - if (combinations.length === 0) { - return internalInstructions; - } - const results = validateColorCombinations(combinations); - let output = internalInstructions; - - for (const result of results) { - output += formatValidationResult(result); - output += '\n'; - } - - const hasIssues = results.some( - (r) => !r.passesAA || r.visualAssessment === 'poor' || r.visualAssessment === 'acceptable', - ); - if (hasIssues) { - output += '### ⚠️ VALIDATION SUMMARY\n\n'; - output += '**Issues found that should be addressed:**\n\n'; - for (const result of results.filter( - (r) => !r.passesAA || r.visualAssessment === 'poor' || r.visualAssessment === 'acceptable', - )) { - output += `- ${result.label || 'Color combination'}: ${result.recommendation || 'Needs improvement'}\n`; - } - output += '\n'; - output += - '**You MUST present these findings to the user BEFORE implementing and wait for their confirmation.**\n\n'; - } else { - output += '### ✅ VALIDATION SUMMARY\n\n'; - output += 'All color combinations meet WCAG AA standards and have good visual assessment.\n\n'; - } - return output; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/guidance-merger.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/guidance-merger.ts deleted file mode 100644 index 69420ddce..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/guidance-merger.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Merges multiple ThemingGuidance objects from different theming files. - * - * @module tools/storefrontnext/site-theming/guidance-merger - */ - -import type {ThemingGuidance} from './theming-store.js'; - -function mergeWorkflows(guidanceArray: ThemingGuidance[]): ThemingGuidance['workflow'] { - const workflows = guidanceArray.filter((g) => g.workflow); - if (workflows.length === 0) return undefined; - const merged: NonNullable = { - steps: [], - extractionInstructions: workflows[0].workflow?.extractionInstructions, - preImplementationChecklist: workflows[0].workflow?.preImplementationChecklist, - }; - for (const g of workflows) { - if (g.workflow?.steps) merged.steps.push(...g.workflow.steps); - if (!merged.extractionInstructions && g.workflow?.extractionInstructions) { - merged.extractionInstructions = g.workflow.extractionInstructions; - } - if (!merged.preImplementationChecklist && g.workflow?.preImplementationChecklist) { - merged.preImplementationChecklist = g.workflow.preImplementationChecklist; - } - } - return merged; -} - -function mergeValidations(guidanceArray: ThemingGuidance[]): ThemingGuidance['validation'] { - const validations = guidanceArray.filter((g) => g.validation); - if (validations.length === 0) return undefined; - const joinField = (field: keyof NonNullable) => - validations - .map((g) => g.validation?.[field]) - .filter((x): x is string => typeof x === 'string') - .join('\n\n'); - return { - colorValidation: joinField('colorValidation'), - fontValidation: joinField('fontValidation'), - generalValidation: joinField('generalValidation'), - requirements: joinField('requirements'), - }; -} - -function buildQuestionMap(guidanceArray: ThemingGuidance[]): Map { - const questionMap = new Map(); - for (const guidance of guidanceArray) { - for (const q of guidance.questions) { - if (!questionMap.has(q.id)) questionMap.set(q.id, q); - } - } - return questionMap; -} - -function buildMergedMetadata(guidanceArray: ThemingGuidance[]): ThemingGuidance['metadata'] { - return { - filePath: guidanceArray.map((g) => g.metadata.filePath).join(', '), - fileName: guidanceArray.map((g) => g.metadata.fileName).join(', '), - loadedAt: new Date(), - }; -} - -/** - * Merges multiple ThemingGuidance objects into one. - * Questions are deduplicated by ID; guidelines, rules, workflows, and validations are combined. - */ -export function mergeGuidance(guidanceArray: ThemingGuidance[]): ThemingGuidance { - if (guidanceArray.length === 0) throw new Error('Cannot merge empty guidance array'); - if (guidanceArray.length === 1) return guidanceArray[0]; - - const questionMap = buildQuestionMap(guidanceArray); - return { - questions: [...questionMap.values()], - guidelines: guidanceArray.flatMap((g) => g.guidelines), - rules: guidanceArray.flatMap((g) => g.rules), - metadata: buildMergedMetadata(guidanceArray), - workflow: mergeWorkflows(guidanceArray), - validation: mergeValidations(guidanceArray), - }; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/index.ts deleted file mode 100644 index 2647b4827..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/index.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Site Theming tool for Storefront Next. - * - * Provides theming guidelines, guided questions, and automatic WCAG color contrast - * validation. Call this tool first when users request brand colors or theme changes. - * - * @module tools/storefrontnext/site-theming - */ - -import {z} from 'zod'; -import type {McpTool} from '../../../utils/index.js'; -import type {Services} from '../../../services.js'; -import {createToolAdapter, textResult, errorResult, type ToolExecutionContext} from '../../adapter.js'; -import {projectDirectoryInput} from '../../project-context.js'; -import {siteThemingStore, type ThemingGuidance} from './theming-store.js'; -import {mergeGuidance} from './guidance-merger.js'; -import {generateResponse} from './response-builder.js'; -import type {SiteThemingInput} from './types.js'; - -export type { - ColorEntry, - ColorMapping, - CollectedAnswers, - ConversationContext, - FontEntry, - SiteThemingInput, -} from './types.js'; - -/** - * Creates the site theming MCP tool for Storefront Next. - * - * The tool guides theming changes (colors, fonts, visual styling) and validates color - * combinations for WCAG accessibility. It must be called before implementing any - * theming changes. - * - * @param loadServices - Function that loads configuration and returns Services instance - * @returns The configured MCP tool - */ -export function createSiteThemingTool(loadServices: () => Promise | Services): McpTool { - return createToolAdapter( - { - name: 'sfnext_configure_theme', - description: - '[DEPRECATED] Superseded by the storefront-next and storefront-next-figma agent-skills plugins and NOT compatible with the Storefront Next 1.0 GA release. Will be removed in a future release. ' + - '⚠️ MANDATORY: Call this tool FIRST before implementing any theming changes. ' + - 'Provides theming guidelines, questions, and automatic validation. ' + - 'CRITICAL RULES: Call immediately when user requests theming (even if colors/fonts provided). ' + - 'NEVER implement without calling this tool first. NEVER skip question-answer workflow. ' + - 'MUST ask questions and WAIT for responses. ' + - 'VALIDATION GATE: After collecting answers, call tool again with colorMapping to trigger validation. ' + - 'DEFAULT FILES: theming-questions, theming-validation, theming-accessibility. ' + - 'Use fileKeys to add custom files. ' + - 'WORKFLOW: Call tool → Ask questions → Call with colorMapping (validation) → Present findings → Wait confirmation → Implement', - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - isGA: false, - requiresInstance: false, - usesProjectContext: true, - inputSchema: { - projectDirectory: projectDirectoryInput, - fileKeys: z - .array(z.string()) - .optional() - .describe( - 'Array of file keys to add to the default set. If provided, guidance from all specified files will be merged with defaults: theming-questions, theming-validation, theming-accessibility. Available keys can be listed by calling the tool without parameters.', - ), - conversationContext: z - .object({ - currentStep: z - .string() - .optional() - .describe('Current step in the theming conversation (e.g., "collecting-colors", "collecting-fonts")'), - collectedAnswers: z - .record(z.string(), z.any()) - .optional() - .describe('Previously collected answers from the user'), - questionsAsked: z.array(z.string()).optional().describe('List of questions that have already been asked'), - }) - .optional() - .describe('Context from previous conversation rounds'), - }, - async execute(args: SiteThemingInput, context: ToolExecutionContext) { - siteThemingStore.initialize(context.services.resolveWithProjectDirectory(undefined, args.projectDirectory), { - themingFiles: context.services.getEnvironmentVariable('THEMING_FILES'), - }); - - const defaultFileKeys = ['theming-questions', 'theming-validation', 'theming-accessibility']; - let fileKeys: string[]; - - if (args.fileKeys && args.fileKeys.length > 0) { - const allKeys = [...defaultFileKeys, ...args.fileKeys]; - fileKeys = [...new Set(allKeys)]; - } else { - fileKeys = defaultFileKeys; - } - - const hasContext = - args.conversationContext && - (args.conversationContext.collectedAnswers || - args.conversationContext.questionsAsked || - args.conversationContext.currentStep); - - if (!args.fileKeys && !hasContext) { - const availableKeys = siteThemingStore.getKeys(); - if (availableKeys.length === 0) { - return { - text: 'No theming files have been loaded. Please ensure theming files are configured at server startup.', - isError: false, - }; - } - - return { - text: `Available theming files:\n\n${availableKeys.map((key) => `- ${key}`).join('\n')}\n\nDefault files (always used): theming-questions, theming-validation, theming-accessibility\n\nUse the \`fileKeys\` parameter to add additional files. User-provided files are merged with the defaults.`, - isError: false, - }; - } - - const guidanceArray: ThemingGuidance[] = []; - const missingKeys: string[] = []; - - for (const key of fileKeys) { - const guidance = siteThemingStore.get(key); - if (guidance) { - guidanceArray.push(guidance); - } else { - missingKeys.push(key); - } - } - - if (guidanceArray.length === 0 || missingKeys.length > 0) { - const availableKeys = siteThemingStore.getKeys(); - const keysList = fileKeys.length === 1 ? `key "${fileKeys[0]}"` : `keys: ${fileKeys.join(', ')}`; - const missingList = missingKeys.length === 1 ? `"${missingKeys[0]}"` : missingKeys.join(', '); - return { - text: `Theming file(s) with ${keysList} not found.\n\nMissing: ${missingList}\nAvailable keys: ${availableKeys.join(', ')}\n\nFiles are loaded at server startup. To add more files, configure them via the THEMING_FILES environment variable or update the server initialization.`, - isError: true, - }; - } - - const guidance = fileKeys.length > 1 ? mergeGuidance(guidanceArray) : guidanceArray[0]; - const response = generateResponse(guidance, args.conversationContext); - - return { - text: response, - isError: false, - }; - }, - formatOutput: (output: {text: string; isError?: boolean}) => - output.isError ? errorResult(output.text) : textResult(output.text), - }, - loadServices, - ); -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/response-builder.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/response-builder.ts deleted file mode 100644 index 73656e41f..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/response-builder.ts +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Builds the theming tool response from guidance and conversation context. - * - * @module tools/storefrontnext/site-theming/response-builder - */ - -import type {ThemingGuidance} from './theming-store.js'; -import type {CollectedAnswers, ColorEntry, ConversationContext, FontEntry} from './types.js'; -import {buildColorCombinations, appendValidationSection} from './color-mapping.js'; - -function isComponentScopeQuestion(question: ThemingGuidance['questions'][0]): boolean { - const questionLower = question.question.toLowerCase(); - return ( - questionLower.includes('which components') || - questionLower.includes('component scope') || - questionLower.includes('component group') - ); -} - -/** - * Returns questions relevant to the current conversation state, filtered and sorted. - */ -export function getRelevantQuestions( - guidance: ThemingGuidance, - context?: ConversationContext, -): ThemingGuidance['questions'] { - if (!context || !context.questionsAsked || context.questionsAsked.length === 0) { - return guidance.questions - .filter((q) => !isComponentScopeQuestion(q)) - .sort((a, b) => { - if (a.required !== b.required) { - return a.required ? -1 : 1; - } - const categoryOrder = {colors: 0, typography: 1, general: 2}; - return ( - (categoryOrder[a.category as keyof typeof categoryOrder] || 2) - - (categoryOrder[b.category as keyof typeof categoryOrder] || 2) - ); - }); - } - - const askedIds = new Set(context.questionsAsked); - const remaining = guidance.questions.filter((q) => !askedIds.has(q.id) && !isComponentScopeQuestion(q)); - - if (context.collectedAnswers) { - const followUps: ThemingGuidance['questions'] = []; - for (const q of remaining) { - if (q.followUpQuestions && context.collectedAnswers?.[q.id]) { - for (const [index, followUp] of q.followUpQuestions.entries()) { - followUps.push({ - id: `${q.id}-followup-${index}`, - question: followUp, - category: q.category, - required: false, - }); - } - } - } - return [...remaining, ...followUps]; - } - - return remaining; -} - -export function hasProvidedThemingInfo(context?: ConversationContext): boolean { - if (!context?.collectedAnswers) { - return false; - } - const collectedAnswers = context.collectedAnswers; - const hasColors = Boolean(collectedAnswers.colors && Array.isArray(collectedAnswers.colors)); - const hasFonts = Boolean(collectedAnswers.fonts && Array.isArray(collectedAnswers.fonts)); - return hasColors || hasFonts; -} - -function buildExtractionResponse(extractionInstructions: string): string { - const internal = - '# ⚠️ MANDATORY: Extract User-Provided Theming Information\n\n## 🚨 CRITICAL: Information Extraction Required\n\n' + - extractionInstructions; - const user = - "I need to extract the theming information from your input first.\n\nLet me review what you've shared and structure it properly, then I'll proceed with clarifying questions.\n\n"; - return `${internal}\n\n---\n\n# USER-FACING RESPONSE (What to say to the user):\n\n${user}`; -} - -function appendValidationInstructions(out: string, validation: NonNullable): string { - let s = - out + - '## ⚠️ MANDATORY: Input Validation\n\n**BEFORE implementing, you MUST validate ALL user-provided inputs:**\n\n'; - if (validation.colorValidation) - s += - '**A. Color Combination Validation (MANDATORY if colors provided):**\n\n' + validation.colorValidation + '\n\n'; - if (validation.fontValidation) - s += '**B. Font Validation (MANDATORY if fonts provided):**\n\n' + validation.fontValidation + '\n\n'; - if (validation.generalValidation) s += '**C. General Input Validation:**\n\n' + validation.generalValidation + '\n\n'; - if (validation.requirements) s += '**IMPORTANT:**\n\n' + validation.requirements + '\n\n'; - return s; -} - -function appendCriticalAndRules(out: string, guidance: ThemingGuidance): string { - let s = out; - const critical = guidance.guidelines.filter((g) => g.critical); - if (critical.length > 0) { - s += '## ⚠️ Critical Guidelines (INTERNAL - Follow these rules)\n\n'; - for (const g of critical) s += `### ${g.title}\n\n${g.content}\n\n`; - } - if (guidance.rules.length > 0) { - s += '## Rules to Follow (INTERNAL)\n\n'; - const doRules = guidance.rules.filter((r) => r.type === 'do'); - const dontRules = guidance.rules.filter((r) => r.type === 'dont'); - if (doRules.length > 0) { - s += '### ✅ What TO Do:\n\n'; - for (const r of doRules) s += `- ${r.description}\n`; - s += '\n'; - } - if (dontRules.length > 0) { - s += '### ❌ What NOT to Do:\n\n'; - for (const r of dontRules) s += `- ${r.description}\n`; - s += '\n'; - } - } - return s; -} - -function extractColorsFromArray(colors: unknown): string[] { - if (!colors || !Array.isArray(colors)) return []; - const out: string[] = []; - for (const color of colors as ColorEntry[]) { - if (color.hex && color.type) out.push(`${color.hex} (${color.type})`); - else if (color.hex) out.push(color.hex); - } - return out; -} - -function extractFontsFromArray(fonts: unknown): string[] { - if (!fonts || !Array.isArray(fonts)) return []; - const out: string[] = []; - for (const font of fonts as FontEntry[]) { - if (font.name) out.push(font.type ? `${font.name} (${font.type})` : font.name); - } - return out; -} - -function extractColorFromValue(value: unknown): null | string { - if (typeof value === 'string') return value; - if (typeof value === 'object' && value && 'hex' in value) { - const v = value as ColorEntry; - if (v.hex === undefined) return null; - return v.type ? `${v.hex} (${v.type})` : v.hex; - } - return null; -} - -function extractFontFromValue(value: unknown): null | string { - if (typeof value === 'string') return value; - if (typeof value === 'object' && value && 'name' in value) { - const v = value as FontEntry; - if (v.name === undefined) return null; - return v.type ? `${v.name} (${v.type})` : v.name; - } - return null; -} - -function shouldSkipKeyForOtherInfo(key: string, lowerKey: string): boolean { - if (key === 'colors' || key === 'fonts') return true; - if (lowerKey.includes('question') || lowerKey.includes('step')) return true; - if (lowerKey.includes('color') || lowerKey.includes('font')) return true; - return false; -} - -function extractOtherInfoFromEntries(collectedAnswers: Record): string[] { - const otherInfo: string[] = []; - for (const key of Object.keys(collectedAnswers)) { - const lowerKey = key.toLowerCase(); - if (shouldSkipKeyForOtherInfo(key, lowerKey)) continue; - const value = collectedAnswers[key]; - if (value === null || value === undefined) continue; - otherInfo.push(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`); - } - return otherInfo; -} - -function collectUserInfo(collectedAnswers: CollectedAnswers): { - colorsInfo: string[]; - fontsInfo: string[]; - otherInfo: string[]; -} { - const colorsInfo = [...extractColorsFromArray(collectedAnswers.colors)]; - const fontsInfo = [...extractFontsFromArray(collectedAnswers.fonts)]; - - for (const key of Object.keys(collectedAnswers)) { - const lowerKey = key.toLowerCase(); - const value = collectedAnswers[key]; - if (key === 'colors' || key === 'fonts' || lowerKey.includes('question') || lowerKey.includes('step')) continue; - if (lowerKey.includes('color') && !key.includes('colors')) { - const c = extractColorFromValue(value); - if (c) colorsInfo.push(c); - continue; - } - if (lowerKey.includes('font') && !key.includes('fonts')) { - const f = extractFontFromValue(value); - if (f) fontsInfo.push(f); - continue; - } - } - - const otherInfo = extractOtherInfoFromEntries(collectedAnswers); - return {colorsInfo, fontsInfo, otherInfo}; -} - -function buildUserInfoSection(info: {colorsInfo: string[]; fontsInfo: string[]; otherInfo: string[]}): string { - const {colorsInfo, fontsInfo, otherInfo} = info; - if (colorsInfo.length === 0 && fontsInfo.length === 0 && otherInfo.length === 0) { - return 'Following the theming workflow. I need a few clarifications before implementing.\n\n'; - } - let s = "## Information You've Provided\n\n"; - if (colorsInfo.length > 0) { - s += '### Colors:\n'; - for (const c of colorsInfo) s += `- ${c}\n`; - s += '\n'; - } - if (fontsInfo.length > 0) { - s += '### Fonts:\n'; - for (const f of fontsInfo) s += `- ${f}\n`; - s += '\n'; - } - if (otherInfo.length > 0) { - s += '### Other Information:\n'; - for (const o of otherInfo) s += `- ${o}\n`; - s += '\n'; - } - return ( - s + - "I've noted the information above. Before implementing, I need a few clarifications to ensure everything is set up correctly.\n\n" - ); -} - -function buildInternalInstructionsBase(guidance: ThemingGuidance, context?: ConversationContext): string { - let s = '# ⚠️ MANDATORY: Site Theming Guidelines and Questions\n\n## 🚨 CRITICAL: Read This First\n\n'; - if (guidance.workflow?.steps && guidance.workflow.steps.length > 0) { - s += '**YOU MUST FOLLOW THIS WORKFLOW - NO EXCEPTIONS:**\n\n'; - for (const [i, step] of guidance.workflow.steps.entries()) s += `${i + 1}. ${step}\n`; - s += '\n**VIOLATION OF THIS WORKFLOW IS A CRITICAL ERROR.**\n\n'; - } - if (guidance.validation) s = appendValidationInstructions(s, guidance.validation); - const colorMapping = context?.collectedAnswers?.colorMapping; - if (colorMapping && Object.keys(colorMapping).length > 0) { - s += - '## 🎨 AUTOMATED COLOR VALIDATION RESULTS\n\n**The following validation has been automatically performed using built-in contrast calculation:**\n\n'; - s = appendValidationSection(s, buildColorCombinations(colorMapping)); - } - return appendCriticalAndRules(s, guidance); -} - -function appendQuestionsToResponse( - internal: string, - user: string, - nextQuestions: ThemingGuidance['questions'], - relevantQuestions: ThemingGuidance['questions'], -): {internal: string; user: string} { - let userOut = user + '## Questions\n\n'; - const categories = [ - {category: 'colors', title: 'Color Questions'}, - {category: 'typography', title: 'Font Questions'}, - {category: 'general', title: 'General Questions'}, - ] as const; - for (const {category, title} of categories) { - const qs = nextQuestions.filter((q) => q.category === category); - if (qs.length > 0) { - userOut += `### ${title}\n\n`; - for (const [i, q] of qs.entries()) userOut += `**Question ${i + 1}**: ${q.question}\n\n`; - } - } - const remaining = relevantQuestions.length - nextQuestions.length; - if (remaining > 0) - userOut += `\n_Note: I have ${remaining} more question${remaining > 1 ? 's' : ''} to ask after you answer these._\n\n`; - userOut += 'Please answer these questions so I can proceed with the implementation.\n\n'; - - let internalOut = - internal + - "## Questions to Ask the User\n\n**IMPORTANT**: Ask these questions ONE AT A TIME and WAIT for the user's response before proceeding.\n\n**CRITICAL RULE**: NEVER implement changes after asking questions without waiting for the user's response.\n\n"; - internalOut += `**You have ${relevantQuestions.length} total questions to ask. Show ${nextQuestions.length} now, then continue with the rest after user responds.**\n\n`; - for (const [i, q] of nextQuestions.entries()) { - internalOut += `### Question ${i + 1} (${q.category}): ${q.id}\n\n${q.question}\n\n`; - if (q.required) internalOut += '**Required**: Yes\n\n'; - } - return {internal: internalOut, user: userOut}; -} - -function appendReadyOrWarningToResponse( - internal: string, - user: string, - guidance: ThemingGuidance, - context: NonNullable, -): {internal: string; user: string} { - const required = guidance.questions.filter((q) => q.required); - const answered = required.filter((q) => context.collectedAnswers?.[q.id] !== undefined); - if (answered.length < required.length) { - return { - internal: - internal + - '## ⚠️ WARNING: Not all required questions have been answered!\n\n**DO NOT implement yet. Continue asking questions.**\n\n', - user: user + 'I still need answers to some required questions before I can proceed.\n\n', - }; - } - let internalOut = internal; - if (guidance.workflow?.preImplementationChecklist) { - internalOut += - '## ⚠️ MANDATORY PRE-IMPLEMENTATION CHECKLIST\n\n' + guidance.workflow.preImplementationChecklist + '\n\n'; - } - const userOut = - user + - '## Ready to Implement\n\nI have collected all necessary information. Before implementing, I will validate all provided inputs (colors, fonts, etc.) for accessibility, availability, and best practices.\n\n'; - return {internal: internalOut, user: userOut}; -} - -/** - * Generates the full theming tool response from guidance and conversation context. - */ -export function generateResponse(guidance: ThemingGuidance, context?: ConversationContext): string { - const isFirstCall = !context || !context.questionsAsked || context.questionsAsked.length === 0; - if (isFirstCall && !hasProvidedThemingInfo(context) && guidance.workflow?.extractionInstructions) { - return buildExtractionResponse(guidance.workflow.extractionInstructions); - } - - const relevantQuestions = getRelevantQuestions(guidance, context); - const questionLimit = !context || context.questionsAsked?.length === 0 ? 5 : 3; - const nextQuestions = relevantQuestions.slice(0, questionLimit); - - let internalInstructions = buildInternalInstructionsBase(guidance, context); - const info = context?.collectedAnswers - ? collectUserInfo(context.collectedAnswers) - : {colorsInfo: [], fontsInfo: [], otherInfo: []}; - let userResponse = buildUserInfoSection(info); - - if (nextQuestions.length > 0) { - const appended = appendQuestionsToResponse(internalInstructions, userResponse, nextQuestions, relevantQuestions); - internalInstructions = appended.internal; - userResponse = appended.user; - } else if (context?.collectedAnswers && Object.keys(context.collectedAnswers).length > 0) { - const appended = appendReadyOrWarningToResponse(internalInstructions, userResponse, guidance, context); - internalInstructions = appended.internal; - userResponse = appended.user; - } - - return `${internalInstructions}\n\n---\n\n# USER-FACING RESPONSE (What to say to the user):\n\n${userResponse}`; -} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts deleted file mode 100644 index 4ef5d6a71..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts +++ /dev/null @@ -1,572 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {readFileSync, existsSync} from 'node:fs'; -import nodePath from 'node:path'; -const {join, dirname, basename} = nodePath; -import {createRequire} from 'node:module'; -import {getLogger} from '@salesforce/b2c-tooling-sdk/logging'; - -// Resolve the site-theming content directory from the package root -const require = createRequire(import.meta.url); -const packageRoot = dirname(require.resolve('@salesforce/b2c-dx-mcp/package.json')); -const SITE_THEMING_CONTENT_DIR = join(packageRoot, 'content', 'site-theming'); - -const logger = getLogger(); - -export interface ThemingGuidance { - questions: Array<{ - id: string; - question: string; - category: string; - required: boolean; - followUpQuestions?: string[]; - }>; - guidelines: Array<{ - category: string; - title: string; - content: string; - critical: boolean; - }>; - rules: Array<{ - type: 'do' | 'dont'; - description: string; - examples?: string[]; - }>; - workflow?: { - steps: string[]; - extractionInstructions?: string; - preImplementationChecklist?: string; - }; - validation?: { - colorValidation?: string; - fontValidation?: string; - generalValidation?: string; - requirements?: string; - }; - metadata: { - filePath: string; - fileName: string; - loadedAt: Date; - }; -} - -type ParsedQuestion = {id: string; question: string; category: string; required: boolean}; - -function parseWorkflowSection(content: string): ThemingGuidance['workflow'] { - const workflowMatch = content.match(/##\s*\u{1F504}\s*WORKFLOW[^#]*(?=##|$)/isu); - if (!workflowMatch) return undefined; - - const workflowContent = workflowMatch[0].replace(/##\s*\u{1F504}\s*WORKFLOW[^\n]*\n?/iu, '').trim(); - const stepMatches = workflowContent.match(/^\d+\.\s+(.+)$/gm); - const steps = stepMatches ? stepMatches.map((step) => step.replace(/^\d+\.\s+/, '').trim()) : []; - - const extractionMatch = workflowContent.match(/###\s*\u{1F4DD}\s*EXTRACTION[^#]*(?=###|$)/isu); - const extractionInstructions = extractionMatch - ? extractionMatch[0].replace(/###\s*\u{1F4DD}\s*EXTRACTION[^\n]*\n?/iu, '').trim() - : undefined; - - const checklistMatch = workflowContent.match(/###\s*\u2705\s*PRE-IMPLEMENTATION[^#]*(?=###|$)/is); - const preImplementationChecklist = checklistMatch - ? checklistMatch[0].replace(/###\s*\u2705\s*PRE-IMPLEMENTATION[^\n]*\n?/i, '').trim() - : undefined; - - if (steps.length > 0 || extractionInstructions || preImplementationChecklist) { - return {steps, extractionInstructions, preImplementationChecklist}; - } - return undefined; -} - -function parseValidationSection(content: string): ThemingGuidance['validation'] { - const validationMatch = content.match(/##\s*\u2705\s*VALIDATION[^#]*(?=##|$)/is); - if (!validationMatch) return undefined; - - const validationContent = validationMatch[0].replace(/##\s*\u2705\s*VALIDATION[^\n]*\n?/i, '').trim(); - - const colorValidationMatch = validationContent.match(/###\s*A\.\s*Color[^#]*(?=###|$)/is); - const colorValidation = colorValidationMatch - ? colorValidationMatch[0].replace(/###\s*A\.\s*Color[^\n]*\n?/i, '').trim() - : undefined; - - const fontValidationMatch = validationContent.match(/###\s*B\.\s*Font[^#]*(?=###|$)/is); - const fontValidation = fontValidationMatch - ? fontValidationMatch[0].replace(/###\s*B\.\s*Font[^\n]*\n?/i, '').trim() - : undefined; - - const generalValidationMatch = validationContent.match(/###\s*C\.\s*General[^#]*(?=###|$)/is); - const generalValidation = generalValidationMatch - ? generalValidationMatch[0].replace(/###\s*C\.\s*General[^\n]*\n?/i, '').trim() - : undefined; - - const requirementsMatch = validationContent.match(/###\s*IMPORTANT[^#]*(?=###|$)/is); - const requirements = requirementsMatch - ? requirementsMatch[0].replace(/###\s*IMPORTANT[^\n]*\n?/i, '').trim() - : undefined; - - if (colorValidation || fontValidation || generalValidation || requirements) { - return {colorValidation, fontValidation, generalValidation, requirements}; - } - return undefined; -} - -function extractRuleItems(content: string, pattern: RegExp, type: 'do' | 'dont'): ThemingGuidance['rules'] { - const rules: ThemingGuidance['rules'] = []; - let match; - while ((match = pattern.exec(content)) !== null) { - const items = match[1] - .split('\n') - .filter((line) => line.trim().startsWith('-')) - .map((line) => line.replace(/^-\s*/, '').trim()); - for (const item of items) { - rules.push({type, description: item}); - } - } - return rules; -} - -function generateColorQuestions( - guidance: ThemingGuidance, - content: string, - generateId: (cat: string) => string, -): ParsedQuestion[] { - const allGuidelines = guidance.guidelines; - const allRules = guidance.rules; - const colorGuidelines = allGuidelines.filter( - (g) => - g.content.toLowerCase().includes('color') || - g.content.toLowerCase().includes('hex') || - g.title.toLowerCase().includes('color'), - ); - const colorRules = allRules.filter( - (r) => - r.description.toLowerCase().includes('color') || - r.description.toLowerCase().includes('background-color') || - r.description.toLowerCase().includes('border-color'), - ); - if (colorGuidelines.length === 0 && colorRules.length === 0) return []; - - const questions: ParsedQuestion[] = []; - if ( - allGuidelines.some( - (g) => g.content.toLowerCase().includes('exact hex') || g.content.toLowerCase().includes('hex code'), - ) - ) { - questions.push({ - id: generateId('color'), - question: 'What are the exact hex color values you want to use? (Please provide hex codes, e.g., #635BFF)', - category: 'colors', - required: true, - }); - } - if ( - allGuidelines.some( - (g) => - g.content.toLowerCase().includes('color type mapping') || - g.content.toLowerCase().includes('color mapping') || - g.content.toLowerCase().includes('primary vs secondary') || - g.content.toLowerCase().includes('brand vs accent'), - ) - ) { - questions.push({ - id: generateId('color'), - question: - 'How should these colors be mapped? (e.g., which color should be primary vs secondary, brand vs accent)', - category: 'colors', - required: true, - }); - } - if ( - allGuidelines.some( - (g) => - g.content.toLowerCase().includes('color combinations') || g.content.toLowerCase().includes('propose color'), - ) - ) { - questions.push( - { - id: generateId('color'), - question: 'Which color should be used for primary actions vs secondary actions?', - category: 'colors', - required: false, - }, - { - id: generateId('color'), - question: 'What should be the hover state colors?', - category: 'colors', - required: false, - }, - ); - } - if (content.toLowerCase().includes('dark') && content.toLowerCase().includes('light')) { - questions.push({ - id: generateId('color'), - question: 'Do you want to support both light and dark themes? If yes, what colors should be used for each?', - category: 'colors', - required: false, - }); - } - return questions; -} - -function generateFontQuestions(guidance: ThemingGuidance, generateId: (cat: string) => string): ParsedQuestion[] { - const allGuidelines = guidance.guidelines; - const fontGuidelines = allGuidelines.filter( - (g) => - g.content.toLowerCase().includes('font') || - g.content.toLowerCase().includes('typography') || - g.title.toLowerCase().includes('font'), - ); - const fontRules = guidance.rules.filter( - (r) => - r.description.toLowerCase().includes('font') || - r.description.toLowerCase().includes('font-weight') || - r.description.toLowerCase().includes('font-size'), - ); - if (fontGuidelines.length === 0 && fontRules.length === 0) return []; - - const questions: ParsedQuestion[] = []; - if ( - allGuidelines.some( - (g) => g.content.toLowerCase().includes('exact font') || g.content.toLowerCase().includes('font name'), - ) - ) { - questions.push({ - id: generateId('font'), - question: 'What is the exact font family name you want to use? (e.g., "sohne-var")', - category: 'typography', - required: true, - }); - } - if ( - allGuidelines.some( - (g) => - g.content.toLowerCase().includes('font availability') || - g.content.toLowerCase().includes('custom font') || - g.content.toLowerCase().includes('google fonts'), - ) - ) { - questions.push({ - id: generateId('font'), - question: 'Is this a custom font that needs to be loaded, or should I use a Google Fonts equivalent?', - category: 'typography', - required: true, - }); - } - if ( - allGuidelines.some( - (g) => - g.content.toLowerCase().includes('headings and body') || - g.content.toLowerCase().includes('font apply') || - g.content.toLowerCase().includes('font usage'), - ) - ) { - questions.push({ - id: generateId('font'), - question: 'Should this font apply to both headings and body text, or just one?', - category: 'typography', - required: false, - }); - } - return questions; -} - -function generateLayoutQuestions( - guidance: ThemingGuidance, - content: string, - generateId: (cat: string) => string, -): ParsedQuestion[] { - const layoutGuidelines = guidance.guidelines.filter( - (g) => - g.content.toLowerCase().includes('layout') || - g.content.toLowerCase().includes('positioning') || - g.title.toLowerCase().includes('layout'), - ); - if (layoutGuidelines.length === 0) return []; - const allowsLayout = - content.toLowerCase().includes('layout changes') && content.toLowerCase().includes('explicitly requested'); - if (!allowsLayout) return []; - - return [ - { - id: generateId('general'), - question: 'Do you need any layout changes, or only visual styling (colors, fonts, etc.)?', - category: 'general', - required: false, - }, - ]; -} - -function generateQuestionsFromGuidelines(guidance: ThemingGuidance, content: string): ParsedQuestion[] { - let counter = 0; - const generateId = (cat: string) => `${cat}-${++counter}`; - return [ - ...generateColorQuestions(guidance, content, generateId), - ...generateFontQuestions(guidance, generateId), - ...generateLayoutQuestions(guidance, content, generateId), - ]; -} - -function extractQuestionLines(content: string): string[] { - const lines = content.split('\n').filter((line) => { - const t = line.trim(); - return t.endsWith('?') && t.length > 10; - }); - return lines - .map((line) => - line - .replace(/^[-*•]\s*/, '') - .replace(/^\d+\.\s*/, '') - .trim(), - ) - .filter((c) => c.length > 10 && c.endsWith('?')); -} - -function mergeQuestionsIntoGuidance( - guidance: ThemingGuidance, - content: string, - generated: ParsedQuestion[], - extracted: string[], -): void { - const colorQs = extracted.filter( - (q) => - q.toLowerCase().includes('color') || - q.toLowerCase().includes('primary') || - q.toLowerCase().includes('accent') || - q.toLowerCase().includes('brand') || - q.toLowerCase().includes('theme'), - ); - const fontQs = extracted.filter((q) => q.toLowerCase().includes('font') || q.toLowerCase().includes('typography')); - const generalQs = extracted.filter( - (q) => - !q.toLowerCase().includes('color') && - !q.toLowerCase().includes('primary') && - !q.toLowerCase().includes('accent') && - !q.toLowerCase().includes('brand') && - !q.toLowerCase().includes('theme') && - !q.toLowerCase().includes('font') && - !q.toLowerCase().includes('typography'), - ); - - let counter = generated.length; - const genId = (cat: string) => `${cat}-${++counter}`; - - guidance.questions.push(...generated); - for (const [i, q] of colorQs.entries()) { - guidance.questions.push({ - id: genId('color'), - question: q, - category: 'colors', - required: i === 0 && generated.filter((x) => x.category === 'colors').length === 0, - }); - } - for (const [i, q] of fontQs.entries()) { - guidance.questions.push({ - id: genId('font'), - question: q, - category: 'typography', - required: i === 0 && generated.filter((x) => x.category === 'typography').length === 0, - }); - } - for (const q of generalQs) { - guidance.questions.push({id: genId('general'), question: q, category: 'general', required: false}); - } - - if (guidance.questions.length === 0) { - const lower = content.toLowerCase(); - if (lower.includes('color')) { - guidance.questions.push({ - id: 'color-primary', - question: 'What colors should be used for theming? (Please provide hex codes)', - category: 'colors', - required: true, - }); - } - if (lower.includes('font') || lower.includes('typography')) { - guidance.questions.push({ - id: 'font-family', - question: 'What font family should be used? (Please provide exact font name)', - category: 'typography', - required: true, - }); - } - } -} - -/** - * Parses an .md/.mdc file and extracts theming questions and guidelines - */ -function parseThemingMDC(content: string, filePath: string): ThemingGuidance { - const guidance: ThemingGuidance = { - questions: [], - guidelines: [], - rules: [], - metadata: { - filePath, - fileName: basename(filePath), - loadedAt: new Date(), - }, - }; - - const workflow = parseWorkflowSection(content); - if (workflow) guidance.workflow = workflow; - - const validation = parseValidationSection(content); - if (validation) guidance.validation = validation; - - // Emoji prefixes use explicit code points (U+26A0 WARNING SIGN with optional - // U+FE0F VARIATION SELECTOR-16; U+1F4CB CLIPBOARD) so that regex literals do - // not depend on how the source file is decoded by the TS/Node runtime on - // different platforms. Also tolerate \r before newlines for CRLF checkouts. - const criticalSections = content.match(/##\s*\u26A0\uFE0F?\s*CRITICAL[^#]*/gi) || []; - const specificationSections = content.match(/##\s*\u{1F4CB}[^#]*/giu) || []; - - for (const section of criticalSections) { - const titleMatch = section.match(/##\s*\u26A0\uFE0F?\s*CRITICAL:\s*(.+?)\r?\n/); - const title = titleMatch ? titleMatch[1].trim() : 'Critical Rule'; - guidance.guidelines.push({ - category: 'critical', - title, - content: section.replace(/##\s*\u26A0\uFE0F?\s*CRITICAL[^\n]*\n/, '').trim(), - critical: true, - }); - } - - for (const section of specificationSections) { - const titleMatch = section.match(/##\s*\u{1F4CB}\s*(.+?)\r?\n/u); - const title = titleMatch ? titleMatch[1].trim() : 'Specification Rule'; - guidance.guidelines.push({ - category: 'specification', - title, - content: section.replace(/##\s*\u{1F4CB}[^\n]*\n/u, '').trim(), - critical: false, - }); - } - - const doRules = extractRuleItems(content, /###\s*What\s+TO\s+Change:([^#]*)/gi, 'do'); - const dontRules = extractRuleItems(content, /###\s*What\s+NOT\s+to\s+Change:([^#]*)/gi, 'dont'); - guidance.rules.push(...doRules, ...dontRules); - - const generatedQuestions = generateQuestionsFromGuidelines(guidance, content); - const extractedQuestions = extractQuestionLines(content); - mergeQuestionsIntoGuidance(guidance, content, generatedQuestions, extractedQuestions); - - return guidance; -} - -/** - * Theming Data Store - * Loads and caches theming guidance from .md/.mdc files - */ -export interface InitializeOptions { - /** Override content directory for default files (used in tests). */ - contentDirOverride?: string; - /** Project-scoped THEMING_FILES value. */ - themingFiles?: string; -} - -class ThemingStore { - private initializedForRoot: null | string = null; - private store: Map = new Map(); - - get(fileKey: string): ThemingGuidance | undefined { - return this.store.get(fileKey); - } - - getKeys(): string[] { - return [...this.store.keys()]; - } - - has(fileKey: string): boolean { - return this.store.has(fileKey); - } - - /** - * Initialize store with default files from content/site-theming. - * Uses workspaceRoot for THEMING_FILES env paths (relative to project). - * Skips re-loading when already initialized for the same root. - */ - initialize(workspaceRoot?: string, options?: InitializeOptions): void { - const root = workspaceRoot ?? process.cwd(); - if (this.initializedForRoot === root) { - return; - } - if (this.initializedForRoot !== null) { - this.store.clear(); - } - this.initializedForRoot = root; - - const contentDir = options?.contentDirOverride ?? SITE_THEMING_CONTENT_DIR; - const defaultFileKeys = ['theming-questions', 'theming-validation', 'theming-accessibility']; - const extensions = ['.md', '.mdc']; - - for (const key of defaultFileKeys) { - let filePath: null | string = null; - for (const ext of extensions) { - const candidate = join(contentDir, `${key}${ext}`); - if (existsSync(candidate)) { - filePath = candidate; - break; - } - } - if (filePath) { - try { - this.loadFile(key, filePath); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Could not load theming file ${filePath}: ${errorMessage}`); - } - } - } - - const themingFilesEnv = options?.themingFiles ?? process.env.THEMING_FILES; - if (themingFilesEnv) { - try { - this.loadThemingFilesFromEnv(themingFilesEnv, root); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Could not parse THEMING_FILES environment variable: ${errorMessage}`); - } - } - } - - loadFile(fileKey: string, filePath: string): void { - try { - if (!existsSync(filePath)) { - throw new Error(`File not found: ${filePath}`); - } - const content = readFileSync(filePath, 'utf8'); - const guidance = parseThemingMDC(content, filePath); - this.store.set(fileKey, guidance); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to load theming file ${filePath}: ${errorMessage}`); - } - } - - private loadThemingFilesFromEnv(envValue: string, root: string): void { - const files = JSON.parse(envValue) as Array<{key: string; path: string}>; - for (const {key, path: filePath} of files) { - // Use path.isAbsolute to detect absolute paths on both POSIX (/foo) and - // Windows (C:\foo); filePath.startsWith('/') misses Windows drive paths. - const fullPath = nodePath.isAbsolute(filePath) ? filePath : join(root, filePath); - this.tryLoadEnvFile(key, fullPath); - } - } - - private tryLoadEnvFile(key: string, fullPath: string): void { - if (!existsSync(fullPath)) { - logger.warn(`Theming file not found: ${fullPath} (key: ${key})`); - return; - } - try { - this.loadFile(key, fullPath); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Could not load theming file ${fullPath} (key: ${key}): ${errorMessage}`); - } - } -} - -export const siteThemingStore = new ThemingStore(); diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/types.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/types.ts deleted file mode 100644 index dca7019d8..000000000 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -/** - * Shared types for the site theming tool. - * - * @module tools/storefrontnext/site-theming/types - */ - -/** Mapping of semantic color roles (e.g. lightText, buttonBackground) to hex values */ -export type ColorMapping = Record; - -/** User-provided color with optional type label */ -export interface ColorEntry { - hex?: string; - type?: string; -} - -/** User-provided font with optional type label */ -export interface FontEntry { - name?: string; - type?: string; -} - -/** Collected answers from the theming conversation */ -export interface CollectedAnswers { - colors?: ColorEntry[]; - fonts?: FontEntry[]; - colorMapping?: ColorMapping; - [questionId: string]: unknown; -} - -/** Conversation context passed to the theming tool */ -export interface ConversationContext { - currentStep?: string; - collectedAnswers?: CollectedAnswers; - questionsAsked?: string[]; -} - -/** Input schema for the site theming tool */ -export interface SiteThemingInput { - fileKeys?: string[]; - conversationContext?: ConversationContext; - projectDirectory?: string; -} diff --git a/packages/b2c-dx-mcp/src/utils/constants.ts b/packages/b2c-dx-mcp/src/utils/constants.ts index 9c008a373..b71dbca5e 100644 --- a/packages/b2c-dx-mcp/src/utils/constants.ts +++ b/packages/b2c-dx-mcp/src/utils/constants.ts @@ -12,27 +12,7 @@ export const ALL_TOOLSETS = 'ALL'; /** * Available toolsets that can be enabled. */ -export const TOOLSETS = [ - 'CARTRIDGES', - 'DIAGNOSTICS', - 'MRT', - 'PWAV3', - 'SCAPI', - 'STOREFRONTNEXT', - 'STOREFRONTNEXT_DEPRECATED', -] as const; - -/** - * Deprecated toolsets. These can only be enabled by explicitly naming them via - * `--toolsets`; they are never auto-activated by project detection and are NOT - * included when `--toolsets ALL` is used. - * - * `STOREFRONTNEXT_DEPRECATED` holds the legacy `sfnext_*` MCP tools. They are - * superseded by the `storefront-next` and `storefront-next-figma` agent-skills - * plugins and are not compatible with the Storefront Next 1.0 GA release. They - * will be removed in a future release. - */ -export const DEPRECATED_TOOLSETS = ['STOREFRONTNEXT_DEPRECATED'] as const; +export const TOOLSETS = ['CARTRIDGES', 'DIAGNOSTICS', 'MRT', 'PWAV3', 'SCAPI', 'STOREFRONTNEXT'] as const; /** * Valid toolset names including the special "ALL" value. diff --git a/packages/b2c-dx-mcp/test/commands/mcp.test.ts b/packages/b2c-dx-mcp/test/commands/mcp.test.ts index 61c3867b1..2b7d50c7a 100644 --- a/packages/b2c-dx-mcp/test/commands/mcp.test.ts +++ b/packages/b2c-dx-mcp/test/commands/mcp.test.ts @@ -724,6 +724,45 @@ describe('McpServerCommand', () => { } }); + it('should select a named instance across the primary and shared default files', async () => { + const rootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-named-instance-')); + const projectDirectory = path.join(rootDirectory, 'project'); + const configPath = path.join(projectDirectory, 'dw.json'); + const defaultConfigPath = path.join(rootDirectory, 'shared.dw.json'); + fs.mkdirSync(projectDirectory); + fs.writeFileSync(configPath, JSON.stringify({configs: [{hostname: 'local.invalid', name: 'local'}]})); + fs.writeFileSync(defaultConfigPath, JSON.stringify({configs: [{hostname: 'shared.invalid', name: 'shared'}]})); + + try { + (command as unknown as {flags: Record}).flags = {}; + sandbox.stub(command as unknown as Record, 'getBaseConfigOptions').returns({ + defaultConfigPath, + }); + const load = (instanceName: string) => + ( + command as unknown as { + loadConfiguration(projectContext: { + instanceName: string; + projectDirectory: string; + }): Promise<{values: Record; sources: Array<{location?: string; scope?: string}>}>; + } + ).loadConfiguration({instanceName, projectDirectory}); + + const local = await load('local'); + expect(local.values.hostname).to.equal('local.invalid'); + expect(local.values.instanceName).to.equal('local'); + expect(local.sources.some((source) => source.location === configPath && source.scope !== 'global')).to.be.true; + + const shared = await load('shared'); + expect(shared.values.hostname).to.equal('shared.invalid'); + expect(shared.values.instanceName).to.equal('shared'); + expect(shared.sources.some((source) => source.location === defaultConfigPath && source.scope === 'global')).to + .be.true; + } finally { + fs.rmSync(rootDirectory, {recursive: true, force: true}); + } + }); + it('should keep registered CLI plugin sources in the per-call resolver pipeline', async () => { const projectDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-plugin-config-')); const configPath = path.join(projectDirectory, 'selected.dw.json'); diff --git a/packages/b2c-dx-mcp/test/e2e/mcp-e2e.test.ts b/packages/b2c-dx-mcp/test/e2e/mcp-e2e.test.ts index 237179a01..9039b293f 100644 --- a/packages/b2c-dx-mcp/test/e2e/mcp-e2e.test.ts +++ b/packages/b2c-dx-mcp/test/e2e/mcp-e2e.test.ts @@ -97,27 +97,6 @@ describe('MCP Server E2E', function () { await client.stop(); }); - it('excludes deprecated sfnext_* tools from --toolsets all', async () => { - const client = new McpE2EClient({args: ['--toolsets', 'all', '--allow-non-ga-tools']}); - await client.start(); - const result = (await client.call('tools/list')) as {tools: Array<{name: string}>}; - const names = result.tools.map((t) => t.name); - expect(names.some((n) => n.startsWith('sfnext_'))).to.be.false; - await client.stop(); - }); - - it('registers deprecated sfnext_* tools only when STOREFRONTNEXT_DEPRECATED is requested', async () => { - const client = new McpE2EClient({ - args: ['--toolsets', 'STOREFRONTNEXT_DEPRECATED', '--allow-non-ga-tools'], - }); - await client.start(); - const result = (await client.call('tools/list')) as {tools: Array<{name: string}>}; - const names = result.tools.map((t) => t.name); - expect(names).to.include('sfnext_get_guidelines'); - expect(names).to.include('sfnext_add_page_designer_decorator'); - await client.stop(); - }); - it('filters tools by individual tool name', async () => { const client = new McpE2EClient({ args: ['--tools', 'scapi_schemas_list,scapi_custom_apis_get_status', '--allow-non-ga-tools'], @@ -291,13 +270,13 @@ describe('MCP Server E2E', function () { it('returns proper error for invalid input when required param missing', async () => { const client = new McpE2EClient({ - args: ['--tools', 'sfnext_add_page_designer_decorator', '--allow-non-ga-tools'], + args: ['--tools', 'scapi_custom_api_generate_scaffold', '--allow-non-ga-tools'], }); await client.start(); try { await client.call('tools/call', { - name: 'sfnext_add_page_designer_decorator', - arguments: {}, // missing required componentName etc. + name: 'scapi_custom_api_generate_scaffold', + arguments: {}, // missing required apiName }); // May throw or return content with error } catch (error) { @@ -324,11 +303,9 @@ describe('MCP Server E2E', function () { await client.start(); const result = (await client.call('tools/list')) as {tools: Array<{name: string}>}; const names = result.tools.map((t) => t.name); - // Storefront Next auto-discovery enables the shared GA tools... + // Storefront Next auto-discovery enables the shared tools. expect(names).to.include('mrt_bundle_push'); expect(names.some((n) => n.startsWith('scapi_'))).to.be.true; - // ...but NOT the deprecated sfnext_* tools, which are opt-in only. - expect(names.some((n) => n.startsWith('sfnext_'))).to.be.false; await client.stop(); }); diff --git a/packages/b2c-dx-mcp/test/registry.test.ts b/packages/b2c-dx-mcp/test/registry.test.ts index 922b80d95..8fb327878 100644 --- a/packages/b2c-dx-mcp/test/registry.test.ts +++ b/packages/b2c-dx-mcp/test/registry.test.ts @@ -46,7 +46,6 @@ describe('registry', () => { expect(registry).to.have.property('PWAV3'); expect(registry).to.have.property('SCAPI'); expect(registry).to.have.property('STOREFRONTNEXT'); - expect(registry).to.have.property('STOREFRONTNEXT_DEPRECATED'); }); it('should create CARTRIDGES tools', () => { @@ -94,7 +93,7 @@ describe('registry', () => { expect(toolNames).to.include('scapi_custom_api_generate_scaffold'); }); - it('should create STOREFRONTNEXT tools (shared GA tools only; sfnext_* are deprecated)', () => { + it('should create STOREFRONTNEXT tools', () => { const loadServices = createMockLoadServicesWrapper(); const registry = createToolRegistry(loadServices); @@ -105,25 +104,6 @@ describe('registry', () => { // mrt_bundle_push and scapi tools appear in STOREFRONTNEXT (multi-toolset, GA) expect(toolNames).to.include('mrt_bundle_push'); expect(toolNames).to.include('scapi_schemas_list'); - // The legacy sfnext_* tools have moved to STOREFRONTNEXT_DEPRECATED - expect(toolNames).to.not.include('sfnext_get_guidelines'); - expect(toolNames).to.not.include('sfnext_add_page_designer_decorator'); - }); - - it('should create STOREFRONTNEXT_DEPRECATED tools (legacy sfnext_* tools)', () => { - const loadServices = createMockLoadServicesWrapper(); - const registry = createToolRegistry(loadServices); - - expect(registry.STOREFRONTNEXT_DEPRECATED).to.be.an('array'); - expect(registry.STOREFRONTNEXT_DEPRECATED.length).to.be.greaterThan(0); - - const toolNames = registry.STOREFRONTNEXT_DEPRECATED.map((t) => t.name); - expect(toolNames).to.include('sfnext_get_guidelines'); - expect(toolNames).to.include('sfnext_add_page_designer_decorator'); - expect(toolNames).to.include('sfnext_configure_theme'); - expect(toolNames).to.include('sfnext_start_figma_workflow'); - expect(toolNames).to.include('sfnext_analyze_component'); - expect(toolNames).to.include('sfnext_match_tokens_to_theme'); }); it('should assign correct toolsets to each tool', () => { @@ -146,19 +126,16 @@ describe('registry', () => { for (const tool of registry.STOREFRONTNEXT) { expect(tool.toolsets).to.include('STOREFRONTNEXT'); } - for (const tool of registry.STOREFRONTNEXT_DEPRECATED) { - expect(tool.toolsets).to.include('STOREFRONTNEXT_DEPRECATED'); - } }); - it('should expose projectDirectory and configPath on every project-aware tool', () => { + it('should expose standardized context fields by tool class', () => { const registry = createToolRegistry(createMockLoadServicesWrapper()); const toolsByName = new Map( Object.values(registry) .flat() .map((tool) => [tool.name, tool]), ); - const projectAwareTools = [ + const configurationAwareTools = [ 'cartridge_deploy', 'config_inspect', 'debug_start_session', @@ -168,20 +145,25 @@ describe('registry', () => { 'metrics_get', 'mrt_bundle_push', 'mrt_logs_watch_start', - 'scapi_custom_api_generate_scaffold', 'scapi_custom_apis_get_status', - 'sfnext_add_page_designer_decorator', - 'sfnext_analyze_component', - 'sfnext_configure_theme', - 'sfnext_match_tokens_to_theme', - 'sfnext_start_figma_workflow', + 'scapi_schemas_list', ]; + const localProjectTools = ['scapi_custom_api_generate_scaffold']; - for (const name of projectAwareTools) { + for (const name of configurationAwareTools) { const tool = toolsByName.get(name); expect(tool, `${name} should be registered`).to.not.be.undefined; expect(tool!.inputSchema, `${name} should accept projectDirectory`).to.have.property('projectDirectory'); expect(tool!.inputSchema, `${name} should accept configPath`).to.have.property('configPath'); + expect(tool!.inputSchema, `${name} should accept instanceName`).to.have.property('instanceName'); + } + + for (const name of localProjectTools) { + const tool = toolsByName.get(name); + expect(tool, `${name} should be registered`).to.not.be.undefined; + expect(tool!.inputSchema, `${name} should accept projectDirectory`).to.have.property('projectDirectory'); + expect(tool!.inputSchema, `${name} should not accept configPath`).not.to.have.property('configPath'); + expect(tool!.inputSchema, `${name} should not accept instanceName`).not.to.have.property('instanceName'); } expect(toolsByName.get('debug_start_session')!.inputSchema).to.have.property('cartridgeDirectory'); @@ -190,18 +172,18 @@ describe('registry', () => { it('registered tool handlers are invokable end-to-end', async () => { // Smoke test that verifies tools aren't just registered by name — the // handler can actually be invoked and produce a tool-call response. - // Uses sfnext_get_guidelines (pure, no network/services needed). + // Uses config_inspect (local configuration resolution, no network needed). const loadServices = createMockLoadServicesWrapper(); const registry = createToolRegistry(loadServices); - const tool = registry.STOREFRONTNEXT_DEPRECATED.find((t) => t.name === 'sfnext_get_guidelines'); - expect(tool, 'sfnext_get_guidelines must be registered in STOREFRONTNEXT_DEPRECATED').to.not.be.undefined; + const tool = registry.DIAGNOSTICS.find((t) => t.name === 'config_inspect'); + expect(tool, 'config_inspect must be registered in DIAGNOSTICS').to.not.be.undefined; - const result = await tool!.handler({sections: ['quick-reference']}); + const result = await tool!.handler({}); expect(result).to.have.property('content'); expect(result.content).to.be.an('array').and.to.have.lengthOf.greaterThan(0); expect(result.content[0]).to.have.property('type', 'text'); expect(result.content[0]).to.have.property('text').that.is.a('string').and.to.have.lengthOf.greaterThan(0); - expect(result.isError, 'guidelines tool should not error on a valid section').to.not.equal(true); + expect(result.isError, 'config_inspect should not error').to.not.equal(true); }); }); @@ -284,55 +266,10 @@ describe('registry', () => { const loadServices = createMockLoadServicesWrapper(); await registerToolsets(flags, server, loadServices); - // Should include tools from all non-deprecated toolsets (placeholder tools removed) + // Should include tools from all toolsets (placeholder tools removed) expect(server.registeredTools).to.include('cartridge_deploy'); expect(server.registeredTools).to.include('mrt_bundle_push'); expect(server.registeredTools).to.include('scapi_schemas_list'); - // ALL excludes the deprecated toolset — sfnext_* tools must NOT be registered - expect(server.registeredTools).to.not.include('sfnext_get_guidelines'); - expect(server.registeredTools).to.not.include('sfnext_add_page_designer_decorator'); - }); - - it('should NOT register deprecated toolset tools when ALL is specified', async () => { - const server = createMockServer(); - const flags: StartupFlags = { - toolsets: ['ALL'], - allowNonGaTools: true, - }; - - const loadServices = createMockLoadServicesWrapper(); - await registerToolsets(flags, server, loadServices); - - // The deprecated toolset is opt-in only and excluded from ALL. - for (const toolName of [ - 'sfnext_get_guidelines', - 'sfnext_add_page_designer_decorator', - 'sfnext_configure_theme', - 'sfnext_start_figma_workflow', - 'sfnext_analyze_component', - 'sfnext_match_tokens_to_theme', - ]) { - expect(server.registeredTools).to.not.include(toolName); - } - }); - - it('should register deprecated tools only when STOREFRONTNEXT_DEPRECATED is explicitly requested', async () => { - const server = createMockServer(); - const flags: StartupFlags = { - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - allowNonGaTools: true, - }; - - const loadServices = createMockLoadServicesWrapper(); - await registerToolsets(flags, server, loadServices); - - // All legacy sfnext_* tools should be registered when explicitly requested - expect(server.registeredTools).to.include('sfnext_get_guidelines'); - expect(server.registeredTools).to.include('sfnext_add_page_designer_decorator'); - expect(server.registeredTools).to.include('sfnext_configure_theme'); - expect(server.registeredTools).to.include('sfnext_start_figma_workflow'); - expect(server.registeredTools).to.include('sfnext_analyze_component'); - expect(server.registeredTools).to.include('sfnext_match_tokens_to_theme'); }); it('should register individual tools via --tools flag', async () => { @@ -451,24 +388,6 @@ describe('registry', () => { expect(server.registeredTools).to.include('scapi_custom_api_generate_scaffold'); }); - it('should skip non-GA tools when allowNonGaTools is false', async () => { - const server = createMockServer(); - const flags: StartupFlags = { - toolsets: ['STOREFRONTNEXT_DEPRECATED'], - allowNonGaTools: false, - }; - - const loadServices = createMockLoadServicesWrapper(); - await registerToolsets(flags, server, loadServices); - - // The deprecated sfnext_* tools are non-GA (isGA: false), so even when the - // deprecated toolset is explicitly requested they are skipped without --allow-non-ga-tools. - const sfnextOnlyTools = ['sfnext_get_guidelines', 'sfnext_add_page_designer_decorator']; - for (const toolName of sfnextOnlyTools) { - expect(server.registeredTools).to.not.include(toolName); - } - }); - it('should register GA tools even when allowNonGaTools is false', async () => { const server = createMockServer(); const flags: StartupFlags = { @@ -489,8 +408,6 @@ describe('registry', () => { // Non-GA tools should NOT be registered expect(server.registeredTools).to.not.include('metrics_get'); - expect(server.registeredTools).to.not.include('sfnext_get_guidelines'); - expect(server.registeredTools).to.not.include('sfnext_add_page_designer_decorator'); }); it('should register non-GA tools when allowNonGaTools is true', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/adapter.test.ts b/packages/b2c-dx-mcp/test/tools/adapter.test.ts index 793520b39..a69f75aa5 100644 --- a/packages/b2c-dx-mcp/test/tools/adapter.test.ts +++ b/packages/b2c-dx-mcp/test/tools/adapter.test.ts @@ -302,18 +302,22 @@ describe('tools/adapter', () => { it('should inject and pass per-call project context to the services loader', async () => { const services = createMockServices(); - let receivedProjectContext: undefined | {projectDirectory?: string; configPath?: string}; - const loadServices = (projectContext?: {projectDirectory?: string; configPath?: string}) => { + let receivedProjectContext: undefined | {projectDirectory?: string; configPath?: string; instanceName?: string}; + const loadServices = (projectContext?: { + projectDirectory?: string; + configPath?: string; + instanceName?: string; + }) => { receivedProjectContext = projectContext; return services; }; - const tool = createToolAdapter<{projectDirectory?: string; configPath?: string}, string>( + const tool = createToolAdapter<{projectDirectory?: string; configPath?: string; instanceName?: string}, string>( { name: 'project_tool', description: 'Uses project context', toolsets: ['CARTRIDGES'], - usesProjectContext: true, + usesConfigurationContext: true, inputSchema: {}, execute: async () => 'done', formatOutput: (output) => textResult(output), @@ -323,18 +327,74 @@ describe('tools/adapter', () => { expect(tool.inputSchema).to.have.property('projectDirectory'); expect(tool.inputSchema).to.have.property('configPath'); + expect(tool.inputSchema).to.have.property('instanceName'); const result = await tool.handler({ projectDirectory: '/workspace/storefront', configPath: '/workspace/config/dw.json', + instanceName: 'sandbox', }); expect(result.isError).to.be.undefined; expect(receivedProjectContext).to.deep.equal({ projectDirectory: '/workspace/storefront', configPath: '/workspace/config/dw.json', + instanceName: 'sandbox', }); }); + it('should describe the actual server fallback and attach compact resolution provenance', async () => { + const config = createMockResolvedConfig({ + hostname: 'sandbox.invalid', + instanceName: 'sandbox', + projectDirectory: '/server/project', + }); + config.sources = [ + { + fields: ['hostname', 'instanceName'], + location: '/shared/dw.json', + name: 'DwJsonSource', + scope: 'global', + }, + ]; + const services = new Services({ + resolvedConfig: config, + resolution: { + projectDirectory: {path: '/server/project', source: 'config'}, + primaryConfiguration: {path: '/server/project/dw.json', source: 'projectDirectory'}, + }, + }); + const loadServices = () => services; + loadServices.projectContextDefaults = { + projectDirectory: {path: '/server/project', source: 'config' as const}, + }; + const tool = createToolAdapter, {ok: boolean}>( + { + name: 'resolved_tool', + description: 'Resolved tool', + toolsets: ['DIAGNOSTICS'], + usesConfigurationContext: true, + inputSchema: {}, + execute: async () => ({ok: true}), + formatOutput: (output) => jsonResult(output), + }, + loadServices, + ); + + expect(tool.inputSchema.projectDirectory.description).to.include('/server/project'); + const result = await tool.handler({}); + const output = JSON.parse(getResultText(result)) as Record; + expect(output.resolution).to.deep.equal({ + configuration: { + hostname: 'sandbox.invalid', + instanceName: 'sandbox', + path: '/shared/dw.json', + source: 'globalDefault', + }, + projectDirectory: {path: '/server/project', source: 'config'}, + }); + expect(result.structuredContent?.resolution).to.deep.equal(output.resolution); + }); + it('should support tools that do not require instance', async () => { const loadServices = createMockLoadServices(); let contextReceived: ToolExecutionContext | undefined; diff --git a/packages/b2c-dx-mcp/test/tools/diagnostics/debug-tools.test.ts b/packages/b2c-dx-mcp/test/tools/diagnostics/debug-tools.test.ts index bc1053e77..4303d4190 100644 --- a/packages/b2c-dx-mcp/test/tools/diagnostics/debug-tools.test.ts +++ b/packages/b2c-dx-mcp/test/tools/diagnostics/debug-tools.test.ts @@ -146,6 +146,15 @@ describe('tools/diagnostics', () => { manager, sourceMapper, cartridges: [], + resolution: { + projectDirectory: {path: '/workspace/storefront', source: 'argument'}, + configuration: { + hostname: 'host.example.com', + instanceName: 'sandbox', + path: '/workspace/storefront/dw.json', + source: 'projectDirectory', + }, + }, }); entry.breakpoints = [{id: 1, line_number: 42, script_path: '/app_test/cartridge/controllers/Cart.js'}]; @@ -157,6 +166,7 @@ describe('tools/diagnostics', () => { halted_threads: number[]; breakpoints: unknown[]; session_cookie: null | {name: string; value: string}; + resolution?: {configuration?: {instanceName?: string}}; }>; }>(result); @@ -165,6 +175,7 @@ describe('tools/diagnostics', () => { expect(json.sessions[0].halted_threads).to.deep.equal([5]); expect(json.sessions[0].breakpoints).to.have.lengthOf(1); expect(json.sessions[0].session_cookie).to.deep.equal({name: 'dwsid', value: 'dwsid-value-123'}); + expect(json.sessions[0].resolution?.configuration?.instanceName).to.equal('sandbox'); }); it('should report session_cookie as null when no dwsid is set', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/diagnostics/logs-tools.test.ts b/packages/b2c-dx-mcp/test/tools/diagnostics/logs-tools.test.ts index 5e679c131..6a2561b62 100644 --- a/packages/b2c-dx-mcp/test/tools/diagnostics/logs-tools.test.ts +++ b/packages/b2c-dx-mcp/test/tools/diagnostics/logs-tools.test.ts @@ -380,11 +380,17 @@ describe('tools/diagnostics/logs', () => { const listTool = createLogsWatchListTool(loadServices, serverContext); const result = await listTool.handler({}); const json = getResultJson<{ - watches: Array<{hostname: string; prefixes: string[]; watch_id: string}>; + watches: Array<{ + hostname: string; + prefixes: string[]; + resolution?: {projectDirectory: {source: string}}; + watch_id: string; + }>; }>(result); expect(json.watches).to.have.lengthOf(1); expect(json.watches[0].hostname).to.equal('test.example.com'); expect(json.watches[0].prefixes).to.deep.equal(['error']); + expect(json.watches[0].resolution?.projectDirectory.source).to.equal('cwd'); }); it('errors when registry is missing', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/diagnostics/mrt-logs-tools.test.ts b/packages/b2c-dx-mcp/test/tools/diagnostics/mrt-logs-tools.test.ts index fdbcda065..1d4ca0958 100644 --- a/packages/b2c-dx-mcp/test/tools/diagnostics/mrt-logs-tools.test.ts +++ b/packages/b2c-dx-mcp/test/tools/diagnostics/mrt-logs-tools.test.ts @@ -438,12 +438,19 @@ describe('tools/diagnostics/mrt-logs', () => { const listTool = createMrtLogsWatchListTool(loadServices, serverContext); const json = getResultJson<{ - watches: Array<{environment: string; project: string; watch_id: string; stopped: boolean}>; + watches: Array<{ + environment: string; + project: string; + resolution?: {projectDirectory: {source: string}}; + watch_id: string; + stopped: boolean; + }>; }>(await listTool.handler({})); expect(json.watches).to.have.lengthOf(1); expect(json.watches[0].project).to.equal('my-storefront'); expect(json.watches[0].environment).to.equal('staging'); expect(json.watches[0].stopped).to.be.false; + expect(json.watches[0].resolution?.projectDirectory.source).to.equal('cwd'); }); it('lists distinct project/environment watches separately', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-get-status.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-get-status.test.ts index 00f5a72d6..7a43a6003 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-get-status.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-get-status.test.ts @@ -110,6 +110,7 @@ describe('tools/scapi/scapi-custom-apis-get-status', () => { expect(Object.keys(tool.inputSchema as object)).to.have.members([ 'projectDirectory', 'configPath', + 'instanceName', 'status', 'groupBy', 'columns', diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/README.md b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/README.md deleted file mode 100644 index 35172f2b2..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/README.md +++ /dev/null @@ -1,248 +0,0 @@ -# Testing Figma MCP Tools - -## Test Status - -The Figma tools have comprehensive unit tests covering: - -- **figma-url-parser**: Valid URLs (design/file), invalid host, missing node-id, malformed URL, unrecognized path pattern -- **figma-to-component**: Valid/invalid URLs, custom workflow files, no-metadata workflow, output structure, next steps reminder, error format, metadata parsing -- **generate-component**: CREATE/EXTEND/REUSE decision paths, strategy selection (props/variant/composition), difference detection (styling, structural, behavioral, props), RSC vs client directive -- **formatter**: All action types (CREATE/EXTEND/REUSE), all extend strategies, conditional sections -- **map-tokens (css-parser)**: Light/dark/shared themes, data-theme selectors, token type classification, var() resolution, findThemeFilePath, parseThemeFile workspace discovery -- **map-tokens (token-matcher)**: Exact color match, fuzzy matching, semantic matching, no-match suggestions, batch matching, theme:light/dark semantics - -All tests use the standard Mocha test framework. The Figma tools achieve ~99% statement coverage. Run with `pnpm test`. - -## Testing Approaches - -### 1. Unit Tests (Automated) - -Run the Figma test suite: - -```bash -cd packages/b2c-dx-mcp -pnpm run test:agent -- test/tools/storefrontnext/figma/ -``` - -### 2. MCP Inspector (Interactive Testing) - -Use the MCP Inspector to test the tools interactively: - -```bash -cd packages/b2c-dx-mcp -pnpm run inspect:dev -``` - -Or with STOREFRONTNEXT toolset only: - -```bash -mcp-inspector node --conditions development bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools -``` - -Then in the inspector: - -1. Click **Connect** -2. Click **List Tools** - you should see `sfnext_start_figma_workflow`, `sfnext_analyze_component`, `sfnext_match_tokens_to_theme` -3. Click on each tool to test with sample inputs - -### 3. CLI Testing - -Test via command line (run from `packages/b2c-dx-mcp`): - -```bash -cd packages/b2c-dx-mcp - -# List all tools (should include Figma tools) -npx mcp-inspector --cli node bin/run.js --toolsets STOREFRONTNEXT --allow-non-ga-tools --method tools/list - -# Call figma-to-component workflow -npx mcp-inspector --cli node bin/run.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ - --method tools/call \ - --tool-name sfnext_start_figma_workflow \ - --args '{"figmaUrl": "https://figma.com/design/abc123/MyDesign?node-id=1-2"}' - -# Call generate-component -npx mcp-inspector --cli node bin/run.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ - --method tools/call \ - --tool-name sfnext_analyze_component \ - --args '{"figmaMetadata": "{}", "figmaCode": "
Hello
", "componentName": "TestComponent", "discoveredComponents": []}' - -# Call map-tokens (requires --project-directory with Storefront Next project containing app.css) -npx mcp-inspector --cli node bin/run.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ - --project-directory /path/to/storefront-next \ - --method tools/call \ - --tool-name sfnext_match_tokens_to_theme \ - --args '{"figmaTokens": [{"name": "Primary", "value": "#2563eb", "type": "color"}]}' -``` - -### 4. Running Tests Against a Local Storefront Next Installation - -For `map-tokens` and `generate-component`, the tools use the project directory for theme file discovery and workspace context. Set `--project-directory` when starting the MCP server, or use `SFCC_WORKING_DIRECTORY` / `SFCC_PROJECT_DIRECTORY` if supported by your MCP client configuration. - -### 5. Manual Testing with Real Figma Design - -**Prerequisites:** - -- Figma MCP tools enabled (external; e.g., Figma's official MCP or compatible provider) for full end-to-end conversion -- Storefront Next project with `--project-directory` pointing to the project root -- Valid Figma design URL with `node-id` parameter (e.g., `https://figma.com/design/:fileKey/:fileName?node-id=1-2`) - -**Steps:** - -1. Configure the b2c-dx-mcp server with `--toolsets STOREFRONTNEXT --allow-non-ga-tools` and `--project-directory` set to your Storefront Next project -2. Use the tools via MCP Inspector or your IDE's MCP integration (e.g., Cursor Composer) -3. Follow the end-to-end workflow below - -### 6. Test Scenarios - -#### sfnext_start_figma_workflow - -**Valid Figma URL** - -```json -{ - "figmaUrl": "https://figma.com/design/abc123/MyDesign?node-id=1-2" -} -``` - -Expected: Returns workflow guide with fileKey, nodeId, step-by-step instructions, and next steps reminder. - -**Invalid URL** - -```json -{ - "figmaUrl": "not-a-valid-url" -} -``` - -Expected: Error message with URL format guidance. - -**Custom workflow file (optional)** - -```json -{ - "figmaUrl": "https://figma.com/design/abc123/MyDesign?node-id=1-2", - "workflowFilePath": "/path/to/custom-workflow.md" -} -``` - -Expected: Uses custom workflow content instead of default. - -#### sfnext_analyze_component - -**Empty discovered components (CREATE)** - -```json -{ - "figmaMetadata": "{}", - "figmaCode": "
Hello
", - "componentName": "HeroBanner", - "discoveredComponents": [] -} -``` - -Expected: CREATE recommendation with confidence ~95%. - -**With discovered components (REUSE/EXTEND)** - -```json -{ - "figmaMetadata": "{}", - "figmaCode": "", - "componentName": "PrimaryButton", - "discoveredComponents": [ - { - "path": "/src/components/ui/Button/index.tsx", - "name": "Button", - "similarity": 85, - "matchType": "name", - "code": "export default function Button({ children }) { return ; }" - } - ] -} -``` - -Expected: REUSE, EXTEND, or CREATE based on analyzed differences. - -#### sfnext_match_tokens_to_theme - -**Basic token mapping** - -```json -{ - "figmaTokens": [ - { "name": "Primary", "value": "#2563eb", "type": "color" }, - { "name": "Large Spacing", "value": "24px", "type": "spacing" }, - { "name": "Medium Radius", "value": "0.375rem", "type": "radius" } - ] -} -``` - -Expected: Summary with exact/fuzzy/no matches, confidence scores, and recommendations. Requires project directory with `app.css` or `src/app.css`. - -**With explicit theme file path** - -```json -{ - "figmaTokens": [{ "name": "Primary", "value": "#2563eb", "type": "color" }], - "themeFilePath": "/path/to/storefront-next/src/app.css" -} -``` - -Expected: Uses specified theme file instead of auto-discovery. - -## End-to-End Workflow (Manual) - -For a full Figma-to-component conversion, execute in order: - -1. **Call `sfnext_start_figma_workflow`** with the Figma URL. Receive fileKey, nodeId, and workflow instructions. - -2. **Call Figma MCP tools** (external) with the returned fileKey and nodeId: - - `mcp__figma__get_design_context` (REQUIRED) - - `mcp__figma__get_screenshot` (REQUIRED) - - `mcp__figma__get_metadata` (OPTIONAL) - -3. **Discover similar components** using Glob/Grep/Read to search the codebase for components similar to the Figma design. - -4. **Call `sfnext_analyze_component`** with figmaMetadata, figmaCode, componentName, and discoveredComponents. Receive REUSE/EXTEND/CREATE recommendation. - -5. **Call `sfnext_match_tokens_to_theme`** with design tokens extracted from Figma. Receive token mapping and suggestions. - -6. **Implement** the recommended approach and present the component to the developer for review. - -## Troubleshooting - -### Theme File Not Found - -If `sfnext_match_tokens_to_theme` returns "Theme file (app.css) not found": - -- Ensure `--project-directory` points to a Storefront Next project root -- Verify `app.css` or `src/app.css` exists in that directory -- Or pass `themeFilePath` explicitly with an absolute path - -### Invalid Figma URL - -If `figma_to_component_workflow` returns an error: - -- Use a URL from figma.com (design or file) -- Include the `node-id` query parameter (e.g., `?node-id=1-2`) -- Example: `https://figma.com/design/abc123/MyDesign?node-id=1-2` - -### Project Directory Resolution - -`generate_component` and `map_tokens` use the MCP server's project directory (from `--project-directory`). If tools cannot find files: - -- Confirm the MCP server was started with `--project-directory` set to your Storefront Next project -- Check that your MCP client (e.g., Cursor) passes the workspace folder correctly - -### Validation Errors - -For Zod validation errors: - -- **figma-to-component**: `figmaUrl` must be a valid URL string -- **generate-component**: All of figmaMetadata, figmaCode, componentName, discoveredComponents are required; discoveredComponents must be an array of objects with path, name, similarity, matchType, code -- **map-tokens**: figmaTokens must be an array of objects with name, value, type (one of: color, spacing, radius, opacity, fontSize, fontFamily, other) - -## See Also - -For user-facing setup, Figma MCP configuration, and prerequisites, see the [Figma-to-Component Tools Setup](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/figma-tools-setup) guide in the documentation. diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/figma-url-parser.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/figma-url-parser.test.ts deleted file mode 100644 index 05f8e0940..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/figma-url-parser.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {parseFigmaUrl} from '../../../../../src/tools/storefrontnext/figma/figma-to-component/figma-url-parser.js'; - -describe('parseFigmaUrl', () => { - describe('Valid URLs', () => { - it('parses Figma design URL with node-id', () => { - const url = 'https://figma.com/design/abc123/MyFile?node-id=1-2'; - const result = parseFigmaUrl(url); - - expect(result).to.deep.equal({ - fileKey: 'abc123', - nodeId: '1:2', - }); - }); - - it('parses Figma file URL with node-id', () => { - const url = 'https://figma.com/file/xyz789/AnotherFile?node-id=10-20'; - const result = parseFigmaUrl(url); - - expect(result).to.deep.equal({ - fileKey: 'xyz789', - nodeId: '10:20', - }); - }); - }); - - describe('Invalid URLs', () => { - it('throws error for non-Figma URL', () => { - const url = 'https://example.com/design/abc123/MyFile?node-id=1-2'; - - expect(() => parseFigmaUrl(url)).to.throw('URL must be from figma.com'); - }); - - it('throws error when node-id parameter is missing', () => { - const url = 'https://figma.com/design/abc123/MyFile'; - - expect(() => parseFigmaUrl(url)).to.throw( - 'Could not extract node-id from URL. Expected query parameter: ?node-id=1-2', - ); - }); - - it('throws error for malformed URL', () => { - const url = 'not-a-valid-url'; - - expect(() => parseFigmaUrl(url)).to.throw('Invalid URL format'); - }); - - it('throws error when URL path does not match /design/ or /file/ pattern', () => { - const url = 'https://figma.com/board/abc123/MyBoard?node-id=1-2'; - - expect(() => parseFigmaUrl(url)).to.throw('Could not extract fileKey from URL'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/index.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/index.test.ts deleted file mode 100644 index e5f455eaa..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/figma-to-component/index.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {join} from 'node:path'; -import {fileURLToPath} from 'node:url'; -import {generateWorkflowResponse} from '../../../../../src/tools/storefrontnext/figma/figma-to-component/index.js'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); - -describe('Figma To Component Workflow (figma/)', () => { - it('when provided valid Figma URL, it should return workflow guide with parsed parameters and workflow steps', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('Figma to StorefrontNext Workflow Guide'); - expect(response).to.include('Figma Design Parameters'); - expect(response).to.include('WORKFLOW_STEPS'); - expect(response).to.include('abc123'); - expect(response).to.include('1:2'); - expect(response).to.include('mcp__figma__get_metadata'); - expect(response).to.include('mcp__figma__get_design_context'); - expect(response).to.include('mcp__figma__get_screenshot'); - }); - - it('when provided valid Figma URL, response should include consolidated development guidelines', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('General Development Guidelines'); - expect(response).to.include('Analyze Requirements'); - }); - - it('when provided invalid Figma URL, it should return error message with guidance', () => { - const response = generateWorkflowResponse('not-a-valid-url'); - - expect(response).to.include('Error: Invalid Figma URL'); - expect(response).to.include('Please provide a valid Figma URL'); - }); - - it('when provided Figma URL without node-id parameter, it should return error message mentioning node-id', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign'); - - expect(response).to.include('Error: Invalid Figma URL'); - expect(response).to.include('node-id'); - }); - - it('when provided Figma URL with dash-formatted node-id, it should convert to colon format', () => { - const response = generateWorkflowResponse('https://figma.com/design/test123/Design?node-id=42-99'); - - expect(response).to.include('42:99'); - expect(response).to.include('test123'); - }); - - describe('Custom Workflow Files', () => { - it('when user provides valid custom workflow file, it should use that workflow content', () => { - const customPath = join(__dirname, '../test-fixtures/workflow-custom.md'); - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2', customPath); - - expect(response).to.include('Custom Test Workflow'); - expect(response).to.include('Custom step one'); - expect(response).to.include('Custom step two'); - }); - - it('when user provides nonexistent workflow file path, it should return error message', () => { - const response = generateWorkflowResponse( - 'https://figma.com/design/abc123/MyDesign?node-id=1-2', - '/nonexistent/path/workflow.md', - ); - - expect(response).to.include('Error'); - expect(response).to.include('Workflow file not found'); - }); - - it('when workflow file has no metadata section, it should still process workflow content', () => { - const noMetadataPath = join(__dirname, '../test-fixtures/workflow-no-metadata.md'); - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2', noMetadataPath); - - expect(response).to.include('Test Workflow Without Metadata'); - expect(response).to.include('Step one'); - }); - }); - - describe('Output Structure', () => { - it('when generating workflow guide, it should output sections in correct order', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - const titlePos = response.indexOf('# Figma to StorefrontNext Workflow Guide'); - const paramsPos = response.indexOf('## Figma Design Parameters'); - - expect(titlePos).to.be.greaterThan(-1); - expect(paramsPos).to.be.greaterThan(-1); - expect(titlePos).to.be.lessThan(paramsPos); - }); - - it('when generating workflow guide, it should include Figma MCP parameter hints', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('clientLanguages'); - expect(response).to.include('clientFrameworks'); - expect(response).to.include('typescript'); - expect(response).to.include('react'); - }); - }); - - describe('Next Steps Reminder', () => { - it('should include the critical next steps section', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('## CRITICAL: Next Steps Required'); - }); - - it('should include all required workflow steps', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('Step 1: Fetch Figma Design Data'); - expect(response).to.include('Step 2: Discover Similar Components'); - expect(response).to.include('Step 3: Analyze Component Strategy'); - expect(response).to.include('Step 4: Map Design Tokens'); - expect(response).to.include('Step 5: Implement'); - }); - - it('should reference required MCP tool names in the reminder', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('`mcp__figma__get_design_context`'); - expect(response).to.include('`mcp__figma__get_screenshot`'); - expect(response).to.include('`sfnext_analyze_component`'); - expect(response).to.include('`sfnext_match_tokens_to_theme`'); - }); - - it('should include the do-not-stop instruction', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include( - 'DO NOT STOP until you have called sfnext_analyze_component AND sfnext_match_tokens_to_theme', - ); - }); - - it('should include image export approval instruction (user must confirm before exporting)', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('wait for approval'); - expect(response).to.include('present the list to the user'); - expect(response).to.include('Should I export these'); - }); - - it('should include logo and brand asset detection in image identification criteria', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('logo'); - expect(response).to.include('brand'); - expect(response).to.include('icon'); - }); - - it('should instruct to NOT pass dirForAssetWrites on the initial get_design_context call', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('dirForAssetWrites'); - expect(response).to.include('Do NOT pass dirForAssetWrites on the initial call'); - }); - - it('should instruct single prompt per batch (ask once, not per image)', () => { - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - - expect(response).to.include('ask ONCE'); - expect(response).to.include('entire batch'); - }); - }); - - describe('Error Response Format', () => { - it('should include URL format guidance in error response', () => { - const response = generateWorkflowResponse('not-a-valid-url'); - - expect(response).to.include('https://figma.com/design/:fileKey/:fileName?node-id=1-2'); - }); - - it('should include an example URL in error response', () => { - const response = generateWorkflowResponse('not-a-valid-url'); - - expect(response).to.include('Example:'); - expect(response).to.include('https://figma.com/design/abc123/MyDesign?node-id=1-2'); - }); - }); - - describe('Metadata Parsing', () => { - it('when custom workflow has metadata with colon in value, it should parse body correctly', () => { - const customPath = join(__dirname, '../test-fixtures/workflow-custom.md'); - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2', customPath); - - expect(response).to.include('Custom Test Workflow'); - expect(response).to.not.include('description:'); - expect(response).to.not.include('taskType:'); - }); - - it('when workflow file has no metadata, it should use the entire content as body', () => { - const noMetadataPath = join(__dirname, '../test-fixtures/workflow-no-metadata.md'); - const response = generateWorkflowResponse('https://figma.com/design/abc123/MyDesign?node-id=1-2', noMetadataPath); - - expect(response).to.include('Test Workflow Without Metadata'); - expect(response).to.include('Step two'); - expect(response).to.include('Step three'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/formatter.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/formatter.test.ts deleted file mode 100644 index dcde8001c..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/formatter.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {formatRecommendation} from '../../../../../src/tools/storefrontnext/figma/generate-component/formatter.js'; -import type { - ComponentAnalysisResult, - GenerateComponentInput, -} from '../../../../../src/tools/storefrontnext/figma/generate-component/index.js'; - -const baseInput: GenerateComponentInput = { - figmaMetadata: '{}', - figmaCode: '
Hello
', - componentName: 'TestComponent', - discoveredComponents: [], -}; - -const matchedComponent = { - path: '/src/components/ui/Button/index.tsx', - name: 'Button', - similarity: 85, -}; - -describe('formatter', () => { - describe('formatRecommendation', () => { - it('includes decision and confidence in output', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 95, - recommendation: 'Create a new component.', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Decision:** CREATE'); - expect(result).to.include('**Confidence:** 95%'); - }); - - it('includes matched component details when present', () => { - const analysis: ComponentAnalysisResult = { - action: 'REUSE', - confidence: 90, - recommendation: 'Reuse existing component.', - matchedComponent, - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Matched Component:**'); - expect(result).to.include('`Button`'); - expect(result).to.include('`/src/components/ui/Button/index.tsx`'); - expect(result).to.include('Similarity: 85%'); - }); - - it('omits matched component section when absent', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 95, - recommendation: 'No similar components found.', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.not.include('**Matched Component:**'); - }); - - it('formats numbered differences list when present', () => { - const analysis: ComponentAnalysisResult = { - action: 'EXTEND', - confidence: 80, - recommendation: 'Extend the component.', - matchedComponent, - differences: ['New Tailwind classes', 'Different root element', 'New hook: useState'], - extendStrategy: 'composition', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Key Differences:**'); - expect(result).to.include('1. New Tailwind classes'); - expect(result).to.include('2. Different root element'); - expect(result).to.include('3. New hook: useState'); - }); - - it('omits differences section when array is empty', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 95, - recommendation: 'Create new.', - differences: [], - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.not.include('**Key Differences:**'); - }); - - it('includes suggested approach when present', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 95, - recommendation: 'Create new.', - suggestedApproach: 'Follow StorefrontNext patterns.', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('## Suggested Approach'); - expect(result).to.include('Follow StorefrontNext patterns.'); - }); - }); - - describe('formatNextSteps - CREATE', () => { - it('includes component name and file location', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 95, - recommendation: 'Create new component.', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('## Next Steps'); - expect(result).to.include('Create new component: `TestComponent`'); - expect(result).to.not.include('/src/components/ui/TestComponent/index.tsx'); - expect(result).to.include('Create component file structure'); - }); - - it('references matched component for patterns when present', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 70, - recommendation: 'Create new but reference existing.', - matchedComponent, - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('Reference patterns from `/src/components/ui/Button/index.tsx`'); - }); - - it('omits pattern reference when no matched component', () => { - const analysis: ComponentAnalysisResult = { - action: 'CREATE', - confidence: 95, - recommendation: 'Create new.', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.not.include('Reference patterns from'); - }); - }); - - describe('formatNextSteps - EXTEND', () => { - it('formats props strategy steps', () => { - const analysis: ComponentAnalysisResult = { - action: 'EXTEND', - confidence: 85, - recommendation: 'Extend with new props.', - matchedComponent, - extendStrategy: 'props', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Strategy:** Add new props'); - expect(result).to.include('Add new optional props to the interface'); - expect(result).to.include('backward compatibility'); - }); - - it('formats variant strategy steps', () => { - const analysis: ComponentAnalysisResult = { - action: 'EXTEND', - confidence: 80, - recommendation: 'Add variant.', - matchedComponent, - extendStrategy: 'variant', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Strategy:** Add variant'); - expect(result).to.include('Add new variant to existing variant definitions'); - expect(result).to.include('variant styling using theme tokens'); - }); - - it('formats composition strategy steps', () => { - const analysis: ComponentAnalysisResult = { - action: 'EXTEND', - confidence: 75, - recommendation: 'Compose wrapper.', - matchedComponent, - extendStrategy: 'composition', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Strategy:** Composition'); - expect(result).to.include('Create wrapper component that composes the base component'); - }); - - it('defaults to props strategy when extendStrategy is undefined', () => { - const analysis: ComponentAnalysisResult = { - action: 'EXTEND', - confidence: 80, - recommendation: 'Extend component.', - matchedComponent, - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Strategy:** Add new props'); - }); - - it('includes validate step for all strategies', () => { - const analysis: ComponentAnalysisResult = { - action: 'EXTEND', - confidence: 80, - recommendation: 'Extend.', - matchedComponent, - extendStrategy: 'variant', - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('Validate: ensure existing usages still work'); - }); - }); - - describe('formatNextSteps - REUSE', () => { - it('includes import instruction with component name and path', () => { - const analysis: ComponentAnalysisResult = { - action: 'REUSE', - confidence: 92, - recommendation: 'Reuse existing component directly.', - matchedComponent, - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('Import and use `Button`'); - expect(result).to.include('from `/src/components/ui/Button/index.tsx`'); - expect(result).to.include('Customize through props and Tailwind classes'); - }); - }); - - describe('common output', () => { - it('always includes confirmation prompt', () => { - for (const action of ['CREATE', 'EXTEND', 'REUSE'] as const) { - const analysis: ComponentAnalysisResult = { - action, - confidence: 80, - recommendation: 'Test.', - matchedComponent: action === 'CREATE' ? undefined : matchedComponent, - extendStrategy: action === 'EXTEND' ? 'props' : undefined, - }; - - const result = formatRecommendation(analysis, baseInput); - - expect(result).to.include('**Confirm before proceeding with implementation.**'); - } - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/index.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/index.test.ts deleted file mode 100644 index 0cffca784..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/generate-component/index.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - generateComponentRecommendation, - type GenerateComponentInput, -} from '../../../../../src/tools/storefrontnext/figma/generate-component/index.js'; -import { - analyzeComponentDifferences, - determineAction, -} from '../../../../../src/tools/storefrontnext/figma/generate-component/decision.js'; - -describe('generate-component', () => { - describe('GenerateComponentTool', () => { - it('returns CREATE when no similar components exist', () => { - const mockInput: GenerateComponentInput = { - figmaMetadata: JSON.stringify({name: 'UniqueComponent', type: 'COMPONENT', children: []}), - figmaCode: `export default function UniqueComponent() { return
Hello
; }`, - componentName: 'UniqueComponent', - discoveredComponents: [], - workspacePath: '/tmp/nonexistent', - }; - - const result = generateComponentRecommendation(mockInput); - - expect(result).to.include('CREATE'); - }); - - it('returns CREATE recommendation when given invalid JSON metadata', () => { - const mockInput: GenerateComponentInput = { - figmaMetadata: 'invalid json', - figmaCode: '', - componentName: '', - discoveredComponents: [], - }; - - const result = generateComponentRecommendation(mockInput); - - expect(result).to.be.a('string').and.to.have.lengthOf.greaterThan(0); - expect(result).to.include('CREATE'); - }); - }); - - describe('Decision Logic', () => { - const mockComponent = { - path: '/components/Button.tsx', - name: 'Button', - similarity: 85, - matchType: 'name' as const, - code: `export default function Button({ children }: { children: React.ReactNode }) { - return ; - }`, - }; - - it('returns REUSE when differences are minimal', () => { - const differences = { - styling: [{description: 'New class: px-4', severity: 'minor' as const, isBackwardCompatible: true}], - structural: [], - behavioral: [], - props: [], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('REUSE'); - expect(result.confidence).to.be.greaterThan(70); - }); - - it('returns EXTEND when differences are moderate', () => { - const differences = { - styling: [{description: 'New classes', severity: 'moderate' as const, isBackwardCompatible: true}], - structural: [], - behavioral: [{description: 'New hook: useState', severity: 'moderate' as const, isBackwardCompatible: true}], - props: [], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('EXTEND'); - expect(result.extendStrategy).to.not.be.undefined; - }); - - it('returns CREATE when differences exceed threshold', () => { - const differences = { - styling: [{description: 'Many new classes', severity: 'major' as const, isBackwardCompatible: true}], - structural: [{description: 'Different root', severity: 'major' as const, isBackwardCompatible: false}], - behavioral: [{description: 'Client vs server', severity: 'major' as const, isBackwardCompatible: false}], - props: [], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('CREATE'); - }); - - it('returns CREATE when breaking changes exceed limit', () => { - const differences = { - styling: [], - structural: [ - {description: 'Breaking 1', severity: 'moderate' as const, isBackwardCompatible: false}, - {description: 'Breaking 2', severity: 'moderate' as const, isBackwardCompatible: false}, - {description: 'Breaking 3', severity: 'moderate' as const, isBackwardCompatible: false}, - ], - behavioral: [], - props: [], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('CREATE'); - }); - }); - - describe('Difference Detection', () => { - it('detects new Tailwind classes as styling difference', () => { - const existing = `
Content
`; - const figma = `
Content
`; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - expect(differences.styling.length).to.be.greaterThan(0); - expect(differences.styling[0].description).to.include('Tailwind classes'); - }); - - it('detects inline styles as moderate severity', () => { - const existing = `
Content
`; - const figma = `
Content
`; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const inlineStylesDiff = differences.styling.find((d) => d.description.includes('inline styles')); - expect(inlineStylesDiff, 'expected an "inline styles" styling difference').to.not.be.undefined; - expect(inlineStylesDiff!.severity).to.equal('moderate'); - }); - - it('detects different root element as structural difference', () => { - const existing = `
Content
`; - const figma = ``; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const rootDiff = differences.structural.find((d) => d.description.includes('Different root element')); - expect(rootDiff, 'expected a "Different root element" structural difference').to.not.be.undefined; - expect(rootDiff!.isBackwardCompatible).to.be.false; - }); - - it('detects client directive change as behavioral difference', () => { - const existing = `export default function Component() { return
Hello
; }`; - const figma = `'use client'; export default function Component() { return
Hello
; }`; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const clientDiff = differences.behavioral.find((d) => d.description.includes('client-side rendering')); - expect(clientDiff, 'expected a "client-side rendering" behavioral difference').to.not.be.undefined; - expect(clientDiff!.severity).to.equal('major'); - }); - - it('detects new React hooks as behavioral difference', () => { - const existing = `export default function Component() { return
Hello
; }`; - const figma = `export default function Component() { const [state, setState] = useState(); return
Hello
; }`; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const useStateDiff = differences.behavioral.find((d) => d.description.includes('useState')); - expect(useStateDiff, 'expected a "useState" behavioral difference').to.not.be.undefined; - }); - - it('detects when existing component is client-side but Figma design could be RSC', () => { - const existing = `'use client'; export default function Component() { return
Hello
; }`; - const figma = `export default function Component() { return
Hello
; }`; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const rscDiff = differences.behavioral.find((d) => d.description.includes('could be RSC')); - expect(rscDiff, 'expected a "could be RSC" behavioral difference').to.not.be.undefined; - expect(rscDiff!.severity).to.equal('major'); - }); - - it('detects new event handlers as behavioral difference', () => { - const existing = `
Content
`; - const figma = ``; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const eventDiff = differences.behavioral.find((d) => d.description.includes('event handlers')); - expect(eventDiff, 'expected an "event handlers" behavioral difference').to.not.be.undefined; - expect(eventDiff!.severity).to.equal('moderate'); - }); - - it('detects when Figma design requires additional prop interfaces', () => { - const existing = `interface ButtonProps { label: string; }`; - const figma = `interface ButtonProps { label: string; } interface IconProps { name: string; }`; - - const differences = analyzeComponentDifferences( - {path: '', name: '', similarity: 100, matchType: 'name', code: existing}, - figma, - '{}', - ); - - const additionalPropsDiff = differences.props.find((d) => d.description.includes('additional props')); - expect(additionalPropsDiff, 'expected an "additional props" props difference').to.not.be.undefined; - expect(additionalPropsDiff!.isBackwardCompatible).to.be.true; - }); - }); - - describe('Strategy Selection', () => { - const mockComponent = { - path: '/components/Button.tsx', - name: 'Button', - similarity: 85, - matchType: 'name' as const, - code: `export default function Button() { return ; }`, - }; - - it('selects props strategy for backward compatible changes', () => { - const differences = { - styling: [], - structural: [], - behavioral: [{description: 'New hook', severity: 'minor' as const, isBackwardCompatible: true}], - props: [ - {description: 'New optional prop', severity: 'minor' as const, isBackwardCompatible: true}, - {description: 'Another prop', severity: 'minor' as const, isBackwardCompatible: true}, - ], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('EXTEND'); - expect(result.extendStrategy).to.equal('props'); - }); - - it('selects variant strategy for styling-focused changes', () => { - const differences = { - styling: [{description: 'Many new classes', severity: 'moderate' as const, isBackwardCompatible: true}], - structural: [], - behavioral: [], - props: [ - {description: 'Prop 1', severity: 'minor' as const, isBackwardCompatible: true}, - {description: 'Prop 2', severity: 'minor' as const, isBackwardCompatible: true}, - {description: 'Prop 3', severity: 'minor' as const, isBackwardCompatible: true}, - {description: 'Prop 4', severity: 'minor' as const, isBackwardCompatible: true}, - ], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('EXTEND'); - expect(result.extendStrategy).to.equal('variant'); - }); - - it('selects composition strategy for structural changes', () => { - const differences = { - styling: [], - structural: [{description: 'New nested elements', severity: 'moderate' as const, isBackwardCompatible: true}], - behavioral: [], - props: [], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('EXTEND'); - expect(result.extendStrategy).to.equal('composition'); - }); - - it('selects composition for non-backward compatible behavioral changes', () => { - const differences = { - styling: [], - structural: [], - behavioral: [{description: 'Breaking change', severity: 'moderate' as const, isBackwardCompatible: false}], - props: [], - }; - - const result = determineAction(mockComponent, differences); - - expect(result.action).to.equal('EXTEND'); - expect(result.extendStrategy).to.equal('composition'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/css-parser.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/css-parser.test.ts deleted file mode 100644 index 1820cb25e..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/css-parser.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - parseThemeFile, - findThemeFilePath, - getColorTokens, - getSpacingTokens, - getRadiusTokens, -} from '../../../../../src/tools/storefrontnext/figma/map-tokens/css-parser.js'; -import {writeFileSync, mkdirSync, rmSync} from 'node:fs'; -import {join} from 'node:path'; - -// Helper to create a temporary CSS file for testing -function createTestCSSFile(content: string): string { - const testDir = join(process.cwd(), 'test-temp'); - mkdirSync(testDir, {recursive: true}); - const testFile = join(testDir, 'test-app.css'); - writeFileSync(testFile, content, 'utf8'); - return testFile; -} - -// Cleanup temp files -function cleanupTestFiles() { - const testDir = join(process.cwd(), 'test-temp'); - try { - rmSync(testDir, {recursive: true, force: true}); - } catch { - // Ignore cleanup errors - } -} - -describe('css-parser', () => { - afterEach(() => { - cleanupTestFiles(); - }); - - describe('parseThemeFile', () => { - it('should parse light theme tokens from :root', () => { - const css = ` - :root { - --primary: #2563eb; - --secondary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.tokens.length).to.equal(2); - expect(result.lightTokens.size).to.equal(2); - expect(result.lightTokens.get('--primary')?.value).to.equal('#2563eb'); - expect(result.lightTokens.get('--primary')?.theme).to.equal('light'); - }); - - it('should parse dark theme tokens from .dark selector', () => { - const css = ` - .dark { - --primary: #1e40af; - --secondary: #475569; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.tokens.length).to.equal(2); - expect(result.darkTokens.size).to.equal(2); - expect(result.darkTokens.get('--primary')?.value).to.equal('#1e40af'); - expect(result.darkTokens.get('--primary')?.theme).to.equal('dark'); - }); - - it('should parse shared tokens from @theme inline', () => { - const css = ` - @theme inline { - --spacing-base: 16px; - --radius-md: 0.375rem; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.tokens.length).to.equal(2); - expect(result.sharedTokens.size).to.equal(2); - expect(result.sharedTokens.get('--spacing-base')?.theme).to.equal('shared'); - }); - - it('should correctly determine token types', () => { - const css = ` - :root { - --color-primary: #2563eb; - --spacing-large: 24px; - --radius-sm: 0.25rem; - --opacity-50: 0.5; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.lightTokens.get('--color-primary')?.type).to.equal('color'); - expect(result.lightTokens.get('--spacing-large')?.type).to.equal('spacing'); - expect(result.lightTokens.get('--radius-sm')?.type).to.equal('radius'); - expect(result.lightTokens.get('--opacity-50')?.type).to.equal('opacity'); - }); - - it('should classify font-size and font-family tokens', () => { - const css = ` - :root { - --font-size-base: 1rem; - --font-family-sans: 'Inter', sans-serif; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.lightTokens.get('--font-size-base')?.type).to.equal('fontSize'); - expect(result.lightTokens.get('--font-family-sans')?.type).to.equal('fontFamily'); - }); - - it('should resolve var() references', () => { - const css = ` - :root { - --blue-500: #2563eb; - --primary: var(--blue-500); - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - const primaryToken = result.lightTokens.get('--primary'); - expect(primaryToken?.value).to.equal('var(--blue-500)'); - expect(primaryToken?.resolvedValue).to.equal('#2563eb'); - }); - - it('should handle nested var() references', () => { - const css = ` - :root { - --base-color: #2563eb; - --blue-500: var(--base-color); - --primary: var(--blue-500); - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - const primaryToken = result.lightTokens.get('--primary'); - expect(primaryToken?.resolvedValue).to.equal('#2563eb'); - }); - - it('should warn about unresolved var() references', () => { - const css = ` - :root { - --primary: var(--nonexistent); - --secondary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.warnings.length).to.be.greaterThan(0); - expect(result.warnings[0]).to.include('--nonexistent'); - expect(result.warnings[0]).to.include('undefined variable'); - }); - - it('should skip tokens with unresolved references', () => { - const css = ` - :root { - --primary: var(--nonexistent); - --secondary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - // Only the resolved token should be present - expect(result.tokens.length).to.equal(1); - expect(result.lightTokens.has('--primary')).to.equal(false); - expect(result.lightTokens.has('--secondary')).to.equal(true); - }); - - it('should report count of skipped tokens', () => { - const css = ` - :root { - --primary: var(--missing1); - --secondary: var(--missing2); - --tertiary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.warnings.some((w) => w.includes('Skipped 2 token(s)'))).to.equal(true); - }); - - it('should handle mixed light and dark themes', () => { - const css = ` - :root { - --primary: #2563eb; - } - .dark { - --primary: #1e40af; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.tokens.length).to.equal(2); - expect(result.lightTokens.get('--primary')?.value).to.equal('#2563eb'); - expect(result.darkTokens.get('--primary')?.value).to.equal('#1e40af'); - }); - - it('should throw error for non-existent file', () => { - expect(() => parseThemeFile('/nonexistent/path/app.css')).to.throw('Theme file not found'); - }); - }); - - describe('token filter functions', () => { - it('should filter color tokens', () => { - const css = ` - :root { - --color-primary: #2563eb; - --spacing-large: 24px; - --radius-sm: 0.25rem; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - const colorTokens = getColorTokens(result); - - expect(colorTokens.length).to.equal(1); - expect(colorTokens[0].name).to.equal('--color-primary'); - }); - - it('should filter spacing tokens', () => { - const css = ` - :root { - --color-primary: #2563eb; - --spacing-large: 24px; - --gap-small: 8px; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - const spacingTokens = getSpacingTokens(result); - - expect(spacingTokens.length).to.equal(2); - }); - - it('should filter radius tokens', () => { - const css = ` - :root { - --color-primary: #2563eb; - --radius-sm: 0.25rem; - --radius-lg: 0.5rem; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - const radiusTokens = getRadiusTokens(result); - - expect(radiusTokens.length).to.equal(2); - }); - }); - - describe('data-theme selectors', () => { - it('should parse light tokens from [data-theme="light"] selector', () => { - const css = ` - [data-theme="light"] { - --primary: #2563eb; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.lightTokens.size).to.equal(1); - expect(result.lightTokens.get('--primary')?.theme).to.equal('light'); - }); - - it('should parse light tokens from single-quoted data-theme selector', () => { - const css = ` - [data-theme='light'] { - --primary: #2563eb; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.lightTokens.size).to.equal(1); - expect(result.lightTokens.get('--primary')?.theme).to.equal('light'); - }); - - it('should parse dark tokens from [data-theme="dark"] selector', () => { - const css = ` - [data-theme="dark"] { - --primary: #1e40af; - } - `; - const testFile = createTestCSSFile(css); - - const result = parseThemeFile(testFile); - - expect(result.darkTokens.size).to.equal(1); - expect(result.darkTokens.get('--primary')?.theme).to.equal('dark'); - }); - }); - - describe('findThemeFilePath', () => { - it('should return null when no workspaceRoot is provided', () => { - const result = findThemeFilePath(); - - expect(result).to.equal(null); - }); - - it('should return null when no app.css exists in workspace', () => { - const result = findThemeFilePath('/nonexistent/workspace'); - - expect(result).to.equal(null); - }); - - it('should find app.css in src/ directory', () => { - const testDir = join(process.cwd(), 'test-temp'); - const srcDir = join(testDir, 'src'); - mkdirSync(srcDir, {recursive: true}); - writeFileSync(join(srcDir, 'app.css'), ':root { --x: 1; }', 'utf8'); - - const result = findThemeFilePath(testDir); - - expect(result).to.equal(join(testDir, 'src/app.css')); - }); - - it('should find app.css in root directory when src/app.css does not exist', () => { - const testDir = join(process.cwd(), 'test-temp'); - mkdirSync(testDir, {recursive: true}); - writeFileSync(join(testDir, 'app.css'), ':root { --x: 1; }', 'utf8'); - - const result = findThemeFilePath(testDir); - - expect(result).to.equal(join(testDir, 'app.css')); - }); - }); - - describe('parseThemeFile workspace discovery', () => { - it('should throw when no themeFilePath and no workspaceRoot are provided', () => { - expect(() => parseThemeFile()).to.throw('Theme file (app.css) not found'); - }); - - it('should throw when workspaceRoot has no app.css', () => { - expect(() => parseThemeFile(undefined, '/nonexistent/workspace')).to.throw('Theme file (app.css) not found'); - }); - - it('should auto-discover and parse app.css from workspaceRoot', () => { - const testDir = join(process.cwd(), 'test-temp'); - const srcDir = join(testDir, 'src'); - mkdirSync(srcDir, {recursive: true}); - writeFileSync(join(srcDir, 'app.css'), ':root { --color-primary: #2563eb; }', 'utf8'); - - const result = parseThemeFile(undefined, testDir); - - expect(result.lightTokens.size).to.equal(1); - expect(result.lightTokens.get('--color-primary')?.value).to.equal('#2563eb'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/index.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/index.test.ts deleted file mode 100644 index ca638347a..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/index.test.ts +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - mapFigmaTokensToTheme, - type MapTokensToThemeInput, -} from '../../../../../src/tools/storefrontnext/figma/map-tokens/index.js'; -import {writeFileSync, mkdirSync, rmSync} from 'node:fs'; -import {join} from 'node:path'; - -function createTestCSSFile(content: string): string { - const testDir = join(process.cwd(), 'test-temp-integration'); - mkdirSync(testDir, {recursive: true}); - const testFile = join(testDir, 'app.css'); - writeFileSync(testFile, content, 'utf8'); - return testFile; -} - -function cleanupTestFiles() { - const testDir = join(process.cwd(), 'test-temp-integration'); - try { - rmSync(testDir, {recursive: true, force: true}); - } catch { - // Ignore cleanup errors - } -} - -describe('map-tokens-to-theme integration', () => { - afterEach(() => { - cleanupTestFiles(); - }); - - describe('tool execution', () => { - it('should execute the tool and return formatted results', () => { - const css = ` - :root { - --primary: #2563eb; - --secondary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - description: 'Primary brand color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Figma Design Tokens'); - expect(result).to.include('Primary'); - }); - - it('should show exact matches in summary', () => { - const css = ` - :root { - --primary: #2563eb; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('**Exact Matches**: 1'); - expect(result).to.include('✅ Exact Matches'); - }); - - it('should show fuzzy matches in summary', () => { - const css = ` - :root { - --primary: #2560e0; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary Button', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Fuzzy Matches'); - }); - - it('should show recommendations for new tokens', () => { - const css = ` - :root { - --primary: #2563eb; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Brand Purple', - value: '#9333ea', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Recommendations'); - expect(result).to.include('Create New Tokens'); - }); - - it('should display warnings for unresolved tokens', () => { - const css = ` - :root { - --primary: var(--nonexistent); - --secondary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Secondary', - value: '#64748b', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Warnings'); - expect(result).to.include('undefined variable'); - }); - - it('should handle multiple tokens with mixed results', () => { - const css = ` - :root { - --primary: #2563eb; - --secondary: #64748b; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - { - name: 'Secondary', - value: '#64748b', - type: 'color', - }, - { - name: 'Accent', - value: '#9333ea', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('**Total Tokens**: 3'); - expect(result).to.include('**Exact Matches**: 2'); - expect(result).to.include('**No Matches**: 1'); - }); - - it('should provide usage instructions', () => { - const css = ` - :root { - --primary: #2563eb; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Usage Instructions'); - expect(result).to.include('Using Matched Tokens in Components'); - }); - - it('should return error message when theme file not found', () => { - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: '/nonexistent/path/app.css', - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Error'); - expect(result).to.include('Theme file not found'); - }); - }); - - describe('detailed match output', () => { - it('should show token details including resolved values', () => { - const css = ` - :root { - --blue-500: #2563eb; - --primary: var(--blue-500); - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Matched Token'); - expect(result).to.include('Resolved Value'); - }); - - it('should show confidence scores', () => { - const css = ` - :root { - --primary: #2560e0; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary Button', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Confidence'); - expect(result).to.match(/\d+%/); - }); - - it('should show alternative suggestions', () => { - const css = ` - :root { - --primary: #2563eb; - --accent: #3b82f6; - --info: #0ea5e9; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Blue', - value: '#2563eb', - type: 'color', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('--primary'); - }); - }); - - describe('token type handling', () => { - it('should handle spacing tokens', () => { - const css = ` - :root { - --spacing-large: 24px; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Large Spacing', - value: '24px', - type: 'spacing', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Large Spacing'); - expect(result).to.include('spacing'); - }); - - it('should handle radius tokens', () => { - const css = ` - :root { - --radius-md: 0.375rem; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Medium Radius', - value: '0.375rem', - type: 'radius', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('Medium Radius'); - expect(result).to.include('radius'); - }); - - it('should handle mixed token types', () => { - const css = ` - :root { - --primary: #2563eb; - --spacing-large: 24px; - --radius-md: 0.375rem; - } - `; - const testFile = createTestCSSFile(css); - - const input: MapTokensToThemeInput = { - figmaTokens: [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - { - name: 'Large Spacing', - value: '24px', - type: 'spacing', - }, - { - name: 'Medium Radius', - value: '0.375rem', - type: 'radius', - }, - ], - themeFilePath: testFile, - }; - - const result = mapFigmaTokensToTheme(input); - - expect(result).to.include('**Total Tokens**: 3'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/token-matcher.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/token-matcher.test.ts deleted file mode 100644 index 431c65ca8..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/map-tokens/token-matcher.test.ts +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - matchToken, - matchTokens, - type FigmaToken, -} from '../../../../../src/tools/storefrontnext/figma/map-tokens/token-matcher.js'; -import type {ParsedTheme, ThemeToken} from '../../../../../src/tools/storefrontnext/figma/map-tokens/css-parser.js'; - -// Helper to create a mock ParsedTheme -function createMockTheme(tokens: ThemeToken[]): ParsedTheme { - const lightTokens = new Map(); - const darkTokens = new Map(); - const sharedTokens = new Map(); - - for (const token of tokens) { - switch (token.theme) { - case 'dark': { - darkTokens.set(token.name, token); - break; - } - case 'light': { - lightTokens.set(token.name, token); - break; - } - case 'shared': { - sharedTokens.set(token.name, token); - break; - } - } - } - - return { - tokens, - lightTokens, - darkTokens, - sharedTokens, - warnings: [], - }; -} - -describe('token-matcher', () => { - describe('matchToken - exact color matches', () => { - it('should find exact hex color match', () => { - const figmaToken: FigmaToken = { - name: 'Primary/Blue', - value: '#2563eb', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('exact'); - expect(match.confidence).to.equal(100); - expect(match.matchedToken?.name).to.equal('--primary'); - }); - - it('should match 3-digit hex to 6-digit hex', () => { - const figmaToken: FigmaToken = { - name: 'Red', - value: '#f00', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--error', - value: '#ff0000', - theme: 'light', - type: 'color', - resolvedValue: '#ff0000', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('exact'); - expect(match.confidence).to.equal(100); - }); - - it('should handle hex colors without # prefix', () => { - const figmaToken: FigmaToken = { - name: 'Green', - value: '#00ff00', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--success', - value: '00ff00', - theme: 'light', - type: 'color', - resolvedValue: '00ff00', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('exact'); - }); - }); - - describe('matchToken - fuzzy matching', () => { - it('should find fuzzy match based on name similarity', () => { - const figmaToken: FigmaToken = { - name: 'Primary', - value: '#2563eb', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary-button', - value: '#2560e0', - theme: 'light', - type: 'color', - resolvedValue: '#2560e0', - }, - { - name: '--secondary', - value: '#64748b', - theme: 'light', - type: 'color', - resolvedValue: '#64748b', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('fuzzy'); - expect(match.confidence).to.be.greaterThan(50); - expect(match.matchedToken?.name).to.equal('--primary-button'); - }); - - it('should match based on semantic meaning', () => { - const figmaToken: FigmaToken = { - name: 'Error/Red', - value: '#dc2626', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--destructive', - value: '#dc2626', - theme: 'light', - type: 'color', - resolvedValue: '#dc2626', - }, - { - name: '--primary', - value: '#dc2626', - theme: 'light', - type: 'color', - resolvedValue: '#dc2626', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - // Should match destructive due to error semantic - expect(match.matchType).to.equal('exact'); // Exact color match - expect(match.matchedToken?.name).to.equal('--destructive'); - }); - - it('should consider color similarity in fuzzy matching', () => { - const figmaToken: FigmaToken = { - name: 'primary-color', - value: '#2560e8', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - { - name: '--error', - value: '#dc2626', - theme: 'light', - type: 'color', - resolvedValue: '#dc2626', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('fuzzy'); - expect(match.matchedToken?.name).to.equal('--primary'); - }); - - it('should provide alternative suggestions for fuzzy matches', () => { - const figmaToken: FigmaToken = { - name: 'Blue Primary', - value: '#3b82f5', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary', - value: '#3b82f6', - theme: 'light', - type: 'color', - resolvedValue: '#3b82f6', - }, - { - name: '--accent', - value: '#3b80f0', - theme: 'light', - type: 'color', - resolvedValue: '#3b80f0', - }, - { - name: '--info', - value: '#3b85f8', - theme: 'light', - type: 'color', - resolvedValue: '#3b85f8', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('fuzzy'); - expect(match.suggestions).to.not.be.undefined; - expect(match.suggestions?.length).to.be.greaterThan(0); - }); - - it('should only match tokens of the same type', () => { - const figmaToken: FigmaToken = { - name: 'spacing', - value: '16px', - type: 'spacing', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--spacing-color', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - { - name: '--spacing-large', - value: '24px', - theme: 'light', - type: 'spacing', - resolvedValue: '24px', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - if (match.matchedToken) { - expect(match.matchedToken.type).to.equal('spacing'); - } - }); - }); - - describe('matchToken - no matches', () => { - it('should return no match when no similar tokens exist', () => { - const figmaToken: FigmaToken = { - name: 'Brand Purple', - value: '#9333ea', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('none'); - expect(match.confidence).to.equal(0); - }); - - it('should provide token name suggestions when no match found', () => { - const figmaToken: FigmaToken = { - name: 'Brand/Purple/500', - value: '#9333ea', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--color-primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.suggestions).to.not.be.undefined; - expect(match.suggestions?.length).to.be.greaterThan(0); - expect(match.suggestions?.[0].tokenName).to.include('--'); - }); - - it('should suggest appropriate prefix based on existing tokens', () => { - const figmaToken: FigmaToken = { - name: 'accent', - value: '#9333ea', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--color-primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - { - name: '--color-secondary', - value: '#64748b', - theme: 'light', - type: 'color', - resolvedValue: '#64748b', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.suggestions?.[0].tokenName).to.match(/^--color-/); - }); - }); - - describe('matchTokens - batch matching', () => { - it('should match multiple tokens at once', () => { - const figmaTokens: FigmaToken[] = [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - { - name: 'Secondary', - value: '#64748b', - type: 'color', - }, - ]; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - { - name: '--secondary', - value: '#64748b', - theme: 'light', - type: 'color', - resolvedValue: '#64748b', - }, - ]; - - const theme = createMockTheme(themeTokens); - const matches = matchTokens(figmaTokens, theme); - - expect(matches.length).to.equal(2); - expect(matches[0].matchType).to.equal('exact'); - expect(matches[1].matchType).to.equal('exact'); - }); - - it('should handle mixed match types', () => { - const figmaTokens: FigmaToken[] = [ - { - name: 'Primary', - value: '#2563eb', - type: 'color', - }, - { - name: 'Brand Purple', - value: '#9333ea', - type: 'color', - }, - ]; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - ]; - - const theme = createMockTheme(themeTokens); - const matches = matchTokens(figmaTokens, theme); - - expect(matches.length).to.equal(2); - expect(matches[0].matchType).to.equal('exact'); - expect(matches[1].matchType).to.equal('none'); - }); - }); - - describe('edge cases', () => { - it('should handle tokens with special characters', () => { - const figmaToken: FigmaToken = { - name: 'Primary/Button-Active', - value: '#2563eb', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--primary-button-active', - value: '#2563eb', - theme: 'light', - type: 'color', - resolvedValue: '#2563eb', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('exact'); - }); - - it('should handle empty token lists', () => { - const figmaTokens: FigmaToken[] = []; - const theme = createMockTheme([]); - - const matches = matchTokens(figmaTokens, theme); - - expect(matches.length).to.equal(0); - }); - - it('should handle non-color types with fuzzy matching', () => { - const figmaToken: FigmaToken = { - name: 'Large/Spacing', - value: '24px', - type: 'spacing', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--spacing-large', - value: '24px', - theme: 'light', - type: 'spacing', - resolvedValue: '24px', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - // Non-color types use fuzzy matching, may return 'none' if similarity is below threshold - // The key is they never use exact value matching like colors do - expect(['fuzzy', 'none']).to.include(match.matchType); - expect(match.suggestions).to.not.be.undefined; - }); - - it('should use theme:light and theme:dark semantics in fuzzy matching', () => { - const figmaToken: FigmaToken = { - name: 'light-background', - value: '#f8fafc', - type: 'color', - }; - - const themeTokens: ThemeToken[] = [ - { - name: '--light-bg', - value: '#f8fafc', - theme: 'light', - type: 'color', - resolvedValue: '#f8fafc', - }, - { - name: '--dark-bg', - value: '#0f172a', - theme: 'dark', - type: 'color', - resolvedValue: '#0f172a', - }, - ]; - - const theme = createMockTheme(themeTokens); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('exact'); - expect(match.matchedToken?.name).to.equal('--light-bg'); - }); - - it('should generate theme: "light" suggestion for non-color unmatched tokens', () => { - const figmaToken: FigmaToken = { - name: 'Large Radius', - value: '1rem', - type: 'radius', - }; - - const theme = createMockTheme([]); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('none'); - expect(match.suggestions).to.not.be.undefined; - expect(match.suggestions!.length).to.be.greaterThan(0); - expect(match.suggestions![0].theme).to.equal('light'); - }); - - it('should generate theme: "both" suggestion for unmatched color tokens', () => { - const figmaToken: FigmaToken = { - name: 'Brand Teal', - value: '#14b8a6', - type: 'color', - }; - - const theme = createMockTheme([]); - const match = matchToken(figmaToken, theme); - - expect(match.matchType).to.equal('none'); - expect(match.suggestions).to.not.be.undefined; - expect(match.suggestions![0].theme).to.equal('both'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-custom.md b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-custom.md deleted file mode 100644 index fb21a3e16..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-custom.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -description: Custom: Test Workflow -taskType: component ---- -# Custom Test Workflow - -## Steps - -1. Custom step one -2. Custom step two -3. Custom step three diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-no-metadata.md b/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-no-metadata.md deleted file mode 100644 index 6e8d06c2a..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/figma/test-fixtures/workflow-no-metadata.md +++ /dev/null @@ -1,7 +0,0 @@ -# Test Workflow Without Metadata - -## Steps - -1. Step one -2. Step two -3. Step three diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/README.md b/packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/README.md deleted file mode 100644 index cc0698909..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# Testing Page Designer Decorator Tool - -## Test Status - -The page-designer-decorator tool has comprehensive unit tests covering: -- ✅ Tool metadata (name, description, toolsets, isGA) -- ✅ Mode selection flow -- ✅ Auto mode (basic, type inference, complex/UI props exclusion, edge cases) -- ✅ Interactive mode (all steps: analyze, select_props, configure_attrs, configure_regions, confirm_generation) -- ✅ Component resolution (name-based, kebab-case, nested, path-based, custom searchPaths, name collisions) -- ✅ Error handling (invalid input, invalid step name, missing parameters) -- ✅ Input validation -- ✅ Edge cases (no props, only complex props, optional props, union types, already decorated components) -- ✅ Environment variables (SFCC_PROJECT_DIRECTORY) - -All tests use the standard Mocha test framework and run with `pnpm test`. - -## Testing Approaches - -### 1. Unit Tests (Automated) - -Run the test suite: - -```bash -cd packages/b2c-dx-mcp -pnpm run test:agent -- test/tools/storefrontnext/page-designer-decorator/index.test.ts -``` - -### 2. MCP Inspector (Interactive Testing) - -Use the MCP Inspector to test the tool interactively: - -```bash -cd packages/b2c-dx-mcp -pnpm run inspect:dev -``` - -Then in the inspector: -1. Click **Connect** -2. Click **List Tools** - you should see `sfnext_add_page_designer_decorator` -3. Click on the tool to test it with real inputs - -### 3. CLI Testing - -Test via command line: - -```bash -# List all tools (should include sfnext_add_page_designer_decorator) -npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools --method tools/list - -# Call the tool -npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ - --method tools/call \ - --tool-name sfnext_add_page_designer_decorator \ - --args '{"component": "MyComponent"}' -``` - -### 4. Running Tests Against a Local Storefront Next Installation - -The Mocha test suite supports testing against a real Storefront Next installation by setting `SFCC_PROJECT_DIRECTORY`: - -```bash -cd packages/b2c-dx-mcp -SFCC_PROJECT_DIRECTORY=/path/to/storefront-next \ - pnpm run test:agent -- test/tools/storefrontnext/page-designer-decorator/index.test.ts -``` - -Or set it as an environment variable: -```bash -export SFCC_PROJECT_DIRECTORY=/path/to/storefront-next -cd packages/b2c-dx-mcp -pnpm run test:agent -- test/tools/storefrontnext/page-designer-decorator/index.test.ts -``` - -**Important Notes for Real Project Mode**: -- Component discovery searches in your real Storefront Next project (`SFCC_PROJECT_DIRECTORY`) -- Tests create temporary directories for test components (not in your real project) -- Tests will **not** modify your real project files (read-only) -- Tests will use existing components from your real project if they exist -- The real project directory is preserved after testing -- To test with specific components, ensure they exist in your real project's `src/components/` directory - -**Alternative Testing Methods**: -- **MCP Inspector**: Interactive UI testing (see section 2 above) -- **CLI Testing**: Command-line testing (see section 3 above) -- **Manual Test Plan**: Full integration testing including Business Manager and Page Designer (see [manual test plan](../../../../../Documents/page-designer-decorator-manual-test-plan.md) for TC-7.x tests) - -### 5. Manual Testing with Real Components - -1. Set up a Storefront Next project (or use an existing one) -2. Create a test component: - -```tsx -// src/components/TestComponent.tsx -export interface TestComponentProps { - title: string; - description?: string; -} - -export default function TestComponent({title, description}: TestComponentProps) { - return
{title}
; -} -``` - -3. Set environment variable: -```bash -export SFCC_PROJECT_DIRECTORY=/path/to/storefront-next -``` - -4. Use the tool via MCP Inspector or your IDE's MCP integration - -### 6. Test Scenarios - -#### Mode Selection -```json -{ - "component": "TestComponent" -} -``` -Expected: Returns mode selection instructions - -#### Auto Mode -```json -{ - "component": "src/components/TestComponent.tsx", - "autoMode": true -} -``` -Expected: Generates decorators automatically - -#### Interactive Mode - Analyze Step -```json -{ - "component": "src/components/TestComponent.tsx", - "conversationContext": { - "step": "analyze" - } -} -``` -Expected: Returns component analysis - -## Troubleshooting - -### Component Not Found Errors - -If you get "Component not found" errors: -1. Verify `SFCC_PROJECT_DIRECTORY` is set correctly -2. Check that the component file exists at the expected path -3. Try using the full relative path: `"component": "src/components/MyComponent.tsx"` - -### Validation Errors - -If you get Zod validation errors: -- Check that all required fields are provided -- Verify field types match the schema (e.g., `component` must be a string) diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/index.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/index.test.ts deleted file mode 100644 index f44263bab..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/page-designer-decorator/index.test.ts +++ /dev/null @@ -1,1630 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {createPageDesignerDecoratorTool} from '../../../../src/tools/storefrontnext/page-designer-decorator/index.js'; -import {generateDecoratorCode} from '../../../../src/tools/storefrontnext/page-designer-decorator/templates/decorator-generator.js'; -import {Services} from '../../../../src/services.js'; -import type {ToolResult} from '../../../../src/utils/types.js'; -import {existsSync, mkdirSync, writeFileSync, rmSync} from 'node:fs'; -import path from 'node:path'; -import {tmpdir} from 'node:os'; -import {createMockResolvedConfig} from '../../../test-helpers.js'; - -/** - * Helper to extract text from a ToolResult. - * Throws if the first content item is not a text type. - * - * @param result - The ToolResult to extract text from - * @returns The text content from the first content item - * @throws {Error} If the first content item is not a text type - */ -function getResultText(result: ToolResult): string { - const content = result.content[0]; - if (content.type !== 'text') { - throw new Error(`Expected text content, got ${content.type}`); - } - return content.text; -} - -/** - * Create a mock services instance for testing. - * - * @param projectDirectory - Optional project directory (defaults to process.cwd()) - * @returns A new Services instance with empty configuration - */ -function createMockServices(projectDirectory?: string): Services { - const config = createMockResolvedConfig({projectDirectory}); - return new Services({resolvedConfig: config}); -} - -/** - * Create a temporary test component file. - * Creates components in the standard location that the tool searches for (`src/components/`). - * - * The component will have: - * - A Props interface with the specified props - * - A default export function component - * - Proper copyright header - * - * @param dir - The test directory root where the component should be created - * @param componentName - The name of the component (e.g., "TestComponent") - * @param props - Optional props string in the format "propName: type; propName2: type;" - * If not provided, defaults to "title: string;" - * @returns The absolute path to the created component file - * - * @example - * ```typescript - * const path = createTestComponent(testDir, 'MyComponent', 'title: string; count: number;'); - * // Creates: {testDir}/src/components/MyComponent.tsx - * ``` - */ -function createTestComponent(dir: string, componentName: string, props?: string): string { - // Create in src/components/ which is the standard search location - const componentPath = path.join(dir, 'src', 'components', `${componentName}.tsx`); - mkdirSync(path.dirname(componentPath), {recursive: true}); - - // Extract prop names for the component function - const propNames = props - ? props - .split(';') - .map((p) => p.trim()) - .filter((p) => p.length > 0) - .map((p) => p.split(':')[0].trim()) - .join(', ') - : 'title'; - - const componentContent = `/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -export interface ${componentName}Props { - ${props || 'title: string;'} -} - -export default function ${componentName}({${propNames}}: ${componentName}Props) { - return
{${propNames.split(',')[0].trim()}}
; -} -`; - - writeFileSync(componentPath, componentContent, 'utf8'); - return componentPath; -} - -/** - * Tests for the page-designer-decorator MCP tool. - * - * This test suite covers: - * - Tool metadata (name, description, toolsets, isGA) - * - Mode selection workflow - * - Auto mode decorator generation (including edge cases: no props, only complex props, optional props, union types, already decorated) - * - Interactive mode workflow (all steps) - * - Component resolution (by name, kebab-case, nested paths, path, custom searchPaths, name collisions) - * - Input validation - * - Error handling (invalid input, invalid step name, missing parameters) - * - Output format validation - * - * Tests use temporary directories and mock components to avoid dependencies - * on real project files. - */ -describe('tools/storefrontnext/page-designer-decorator', () => { - let services: Services; - let testDir: string; - let originalCwd: string; - const getServices = () => services; - - beforeEach(() => { - // Create a temporary directory for test components - testDir = path.join(tmpdir(), `b2c-mcp-test-${Date.now()}`); - mkdirSync(testDir, {recursive: true}); - originalCwd = process.cwd(); - process.chdir(testDir); - // Create services with projectDirectory set to test directory - services = createMockServices(testDir); - }); - - afterEach(() => { - process.chdir(originalCwd); - if (existsSync(testDir)) { - rmSync(testDir, {recursive: true, force: true}); - } - }); - - describe('tool metadata', () => { - it('should have correct tool name', () => { - const tool = createPageDesignerDecoratorTool(getServices); - expect(tool.name).to.equal('sfnext_add_page_designer_decorator'); - }); - - it('should have comprehensive description', () => { - const tool = createPageDesignerDecoratorTool(getServices); - const desc = tool.description; - - // Should mention Page Designer - expect(desc).to.include('Page Designer'); - expect(desc).to.include('decorator'); - - // Should mention modes - expect(desc).to.match(/AUTO MODE|auto mode/i); - expect(desc).to.match(/INTERACTIVE MODE|interactive mode/i); - - // Should mention key features - expect(desc).to.include('@Component'); - expect(desc).to.include('@AttributeDefinition'); - }); - - it('should be in STOREFRONTNEXT_DEPRECATED toolset', () => { - const tool = createPageDesignerDecoratorTool(getServices); - expect(tool.toolsets).to.include('STOREFRONTNEXT_DEPRECATED'); - expect(tool.toolsets).to.have.lengthOf(1); - }); - - it('should not be GA (generally available)', () => { - const tool = createPageDesignerDecoratorTool(getServices); - expect(tool.isGA).to.be.false; - }); - }); - - describe('mode selection', () => { - it('should show mode selection when called with only component name', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'TestComponent'); - - const result = await tool.handler({ - component: 'TestComponent', - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should present mode selection options - expect(text).to.match(/mode|Mode/i); - expect(text).to.match(/auto|Auto/i); - expect(text).to.match(/interactive|Interactive/i); - expect(text).to.include('TestComponent'); - }); - - it('should use projectDirectory from Services', async () => { - const customDir = path.join(tmpdir(), `b2c-mcp-test-custom-${Date.now()}`); - mkdirSync(customDir, {recursive: true}); - createTestComponent(customDir, 'CustomComponent'); - - // Create services with custom projectDirectory - const customServices = createMockServices(customDir); - const tool = createPageDesignerDecoratorTool(() => customServices); - - const result = await tool.handler({ - component: 'CustomComponent', - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.match(/mode|Mode/i); - - rmSync(customDir, {recursive: true, force: true}); - }); - }); - - describe('auto mode', () => { - it('should generate decorators in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'AutoComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'AutoComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should generate decorator code - expect(text).to.include('@Component'); - expect(text).to.include('@AttributeDefinition'); - expect(text).to.include('AutoComponent'); - expect(text).to.include('title'); - }); - - it('should handle component with multiple props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'MultiPropComponent', - `title: string; -description: string; -imageUrl: string;`, - ); - - const result = await tool.handler({ - component: 'MultiPropComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should include decorators for multiple props - expect(text).to.include('@Component'); - expect(text).to.include('title'); - expect(text).to.include('description'); - expect(text).to.include('imageUrl'); - }); - - it('should exclude complex props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'ComplexPropsComponent', - `title: string; -onClick: () => void; -config: { key: string; value: number };`, - ); - - const result = await tool.handler({ - component: 'ComplexPropsComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should include simple props in generated decorators - expect(text).to.include('title'); - // Complex props should not appear in @AttributeDefinition decorators - // (they might appear in instructions, but not in the actual decorator code) - const decoratorCodeMatch = text.match(/@AttributeDefinition[\s\S]*?\)/g); - if (decoratorCodeMatch) { - const decoratorCode = decoratorCodeMatch.join('\n'); - expect(decoratorCode).to.not.include('onClick'); - expect(decoratorCode).to.not.include('config'); - } - }); - - it('should exclude UI-only props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'UIPropsComponent', - `title: string; -className: string; -style: React.CSSProperties;`, - ); - - const result = await tool.handler({ - component: 'UIPropsComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should include content props in generated decorators - expect(text).to.include('title'); - // UI-only props should not appear in @AttributeDefinition decorators - // (they might appear in instructions, but not in the actual decorator code) - const decoratorCodeMatch = text.match(/@AttributeDefinition[\s\S]*?\)/g); - if (decoratorCodeMatch) { - const decoratorCode = decoratorCodeMatch.join('\n'); - expect(decoratorCode).to.not.include('className'); - expect(decoratorCode).to.not.include('style'); - } - }); - - it('should handle component already decorated in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const decoratedPath = path.join(testDir, 'src', 'components', 'DecoratedComponent.tsx'); - mkdirSync(path.dirname(decoratedPath), {recursive: true}); - writeFileSync( - decoratedPath, - `import {Component} from '@salesforce/retail-react-app/app/components/page-designer'; - -@Component({ - id: 'existing-component', - name: 'Existing Component', -}) -export class DecoratedComponentMetadata { - @AttributeDefinition() - title!: string; -} - -export interface DecoratedComponentProps { - title: string; -} - -export default function DecoratedComponent({title}: DecoratedComponentProps) { - return
{title}
; -}`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'DecoratedComponent', - autoMode: true, - }); - - // Auto mode must short-circuit and refuse to re-decorate, returning - // the deterministic "Already Decorated" notice that names the component - // and offers to modify the existing decorators. - const text = getResultText(result); - expect(text).to.include('Component Already Decorated'); - expect(text).to.include('DecoratedComponent'); - expect(text).to.include('already has Page Designer decorators'); - expect(text).to.include('modify the existing decorators'); - // It must NOT emit a fresh @Component/@AttributeDefinition decorator block. - expect(text).to.not.match(/@AttributeDefinition\s*\(/); - }); - - it('should handle component with no props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const emptyPath = path.join(testDir, 'src', 'components', 'EmptyProps.tsx'); - mkdirSync(path.dirname(emptyPath), {recursive: true}); - writeFileSync( - emptyPath, - `export interface EmptyPropsProps {} -export default function EmptyProps({}: EmptyPropsProps) { return
Empty
; }`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'EmptyProps', - autoMode: true, - }); - - // Should handle components with no props gracefully - expect(result).to.exist; - const text = getResultText(result); - // Should generate decorator code even with no props (just @Component, no @AttributeDefinition) - expect(text).to.include('@Component'); - expect(text).to.include('EmptyProps'); - }); - - it('should handle component with only complex props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'ComplexOnlyComponent', - `onClick: () => void; -config: { key: string }; -data: Array<{id: number}>;`, - ); - - const result = await tool.handler({ - component: 'ComplexOnlyComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should generate decorator code with just @Component (no @AttributeDefinition since all props are complex) - expect(text).to.include('@Component'); - expect(text).to.include('ComplexOnlyComponent'); - // Should not include complex props in decorators - const decoratorCodeMatch = text.match(/@AttributeDefinition[\s\S]*?\)/g); - if (decoratorCodeMatch) { - const decoratorCode = decoratorCodeMatch.join('\n'); - expect(decoratorCode).to.not.include('onClick'); - expect(decoratorCode).to.not.include('config'); - expect(decoratorCode).to.not.include('data'); - } - }); - - it('should handle component with optional props in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'OptionalPropsComponent', - `title?: string; -count?: number;`, - ); - - const result = await tool.handler({ - component: 'OptionalPropsComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should include optional props in generated decorators - expect(text).to.include('@Component'); - expect(text).to.include('OptionalPropsComponent'); - expect(text).to.include('title'); - expect(text).to.include('count'); - }); - - it('should handle component with union types in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'UnionTypesComponent', - `status: 'active' | 'inactive'; -value: string | number;`, - ); - - const result = await tool.handler({ - component: 'UnionTypesComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should handle union types appropriately - expect(text).to.include('@Component'); - expect(text).to.include('UnionTypesComponent'); - expect(text).to.include('status'); - expect(text).to.include('value'); - }); - - it('should auto-configure URL and image type props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'UrlImageComponent', - `heroImageUrl: string; -ctaUrl: string; -backgroundPicture: string;`, - ); - - const result = await tool.handler({ - component: 'UrlImageComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('@Component'); - expect(text).to.include('heroImageUrl'); - expect(text).to.include('ctaUrl'); - expect(text).to.include('backgroundPicture'); - }); - - it('should handle named export component in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const namedPath = path.join(testDir, 'src', 'components', 'NamedExportComponent.tsx'); - mkdirSync(path.dirname(namedPath), {recursive: true}); - writeFileSync( - namedPath, - `export interface NamedExportComponentProps { - title: string; -} -export function NamedExportComponent({title}: NamedExportComponentProps) { - return
{title}
; -}`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'NamedExportComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('NamedExportComponent'); - }); - - it('should handle const export component in auto mode', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const constPath = path.join(testDir, 'src', 'components', 'ConstExportComponent.tsx'); - mkdirSync(path.dirname(constPath), {recursive: true}); - writeFileSync( - constPath, - `export interface ConstExportComponentProps { - title: string; -} -export const ConstExportComponent = ({title}: ConstExportComponentProps) => { - return
{title}
; -};`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'ConstExportComponent', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('ConstExportComponent'); - }); - }); - - describe('interactive mode', () => { - describe('analyze step', () => { - it('should analyze component in analyze step', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'AnalyzeComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'AnalyzeComponent', - conversationContext: { - step: 'analyze', - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should show analysis results - expect(text).to.match(/component|Component/i); - expect(text).to.match(/prop|Prop|attribute|Attribute/i); - expect(text).to.include('AnalyzeComponent'); - expect(text).to.include('title'); - }); - - it('should categorize props correctly', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'CategorizedComponent', - `title: string; -onClick: () => void; -className: string;`, - ); - - const result = await tool.handler({ - component: 'CategorizedComponent', - conversationContext: { - step: 'analyze', - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should identify editable props - expect(text).to.include('title'); - // Should mention props analysis (may use different terminology) - expect(text).to.match(/prop|Prop|attribute|Attribute|editable|suitable/i); - // Should mention complex or UI props (may be described differently) - expect(text).to.match(/complex|Complex|UI|ui|exclude|skip/i); - }); - - it('should detect already-decorated component in analyze step', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const decoratedPath = path.join(testDir, 'src', 'components', 'AlreadyDecoratedAnalyze.tsx'); - mkdirSync(path.dirname(decoratedPath), {recursive: true}); - writeFileSync( - decoratedPath, - `import {Component} from '@/lib/decorators/component'; - -@Component('already-decorated', { name: 'Already Decorated' }) -export class AlreadyDecoratedAnalyzeMetadata {} - -export interface AlreadyDecoratedAnalyzeProps { title: string; } -export default function AlreadyDecoratedAnalyze({title}: AlreadyDecoratedAnalyzeProps) { - return
{title}
; -}`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'AlreadyDecoratedAnalyze', - conversationContext: {step: 'analyze'}, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.match(/already|decorated|existing/i); - }); - - it('should analyze component with no editable props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent( - testDir, - 'NoEditableComponent', - `onClick: () => void; -className: string;`, - ); - - const result = await tool.handler({ - component: 'NoEditableComponent', - conversationContext: {step: 'analyze'}, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.match(/No suitable|no suitable|⚠️/i); - }); - - it('should analyze component without a Props interface', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const noInterfacePath = path.join(testDir, 'src', 'components', 'NoInterfaceComponent.tsx'); - mkdirSync(path.dirname(noInterfacePath), {recursive: true}); - writeFileSync( - noInterfacePath, - `export default function NoInterfaceComponent() { - return
Hello
; -}`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'NoInterfaceComponent', - conversationContext: {step: 'analyze'}, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('None found'); - }); - }); - - describe('select_props step', () => { - it('should confirm selected props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'SelectPropsComponent', 'title: string; description: string;'); - - const result = await tool.handler({ - component: 'SelectPropsComponent', - conversationContext: { - step: 'select_props', - selectedProps: ['title', 'description'], - componentMetadata: { - id: 'select-props-component', - name: 'Select Props Component', - description: 'Test component', - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should confirm selections - expect(text).to.include('title'); - expect(text).to.include('description'); - expect(text).to.include('Select Props Component'); - }); - - it('should require component metadata', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'MissingMetadataComponent'); - - const result = await tool.handler({ - component: 'MissingMetadataComponent', - conversationContext: { - step: 'select_props', - selectedProps: ['title'], - // Missing componentMetadata - }, - }); - - // Should return error when metadata is missing - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.match(/metadata|Metadata/i); - }); - - it('should confirm with new attributes and no selected props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'NewAttrsSelectComponent'); - - const result = await tool.handler({ - component: 'NewAttrsSelectComponent', - conversationContext: { - step: 'select_props', - selectedProps: [], - newAttributes: [ - {name: 'ctaLabel', description: 'Call to action label', required: true}, - {name: 'subtitle'}, - ], - componentMetadata: { - id: 'new-attrs-select', - name: 'New Attrs Select', - description: 'Test with new attributes', - group: 'custom_group', - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('ctaLabel'); - expect(text).to.include('Call to action label'); - expect(text).to.include('None'); - }); - }); - - describe('configure_attrs step', () => { - it('should provide attribute configuration instructions', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'ConfigureAttrsComponent', 'imageUrl: string; description: string;'); - - const result = await tool.handler({ - component: 'ConfigureAttrsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['imageUrl', 'description'], - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should provide configuration guidance - expect(text).to.match(/attribute|Attribute|configure|Configure/i); - expect(text).to.include('imageUrl'); - expect(text).to.include('description'); - }); - - it('should suggest types for props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'TypeSuggestionsComponent', 'imageUrl: string; productId: string;'); - - const result = await tool.handler({ - component: 'TypeSuggestionsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['imageUrl', 'productId'], - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should suggest appropriate types - expect(text).to.match(/url|image|product/i); - }); - - it('should handle mix of auto-inferred and needs-config attrs', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'MixedAttrsComponent', 'count: number; heroImage: string;'); - - const result = await tool.handler({ - component: 'MixedAttrsComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['count', 'heroImage'], - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('count'); - expect(text).to.include('heroImage'); - expect(text).to.match(/auto|Auto|infer/i); - }); - - it('should handle new attributes in configure_attrs', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'NewAttrConfigComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'NewAttrConfigComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['title'], - newAttributes: [{name: 'categoryId', description: 'Category reference'}], - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('categoryId'); - expect(text).to.match(/category/i); - }); - - it('should handle enum suggestion for array-type props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'ArrayPropComponent', 'items: string[];'); - - const result = await tool.handler({ - component: 'ArrayPropComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['items'], - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('items'); - }); - - it('should handle non-enum suggestion for URL props', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'UrlOnlyComponent', 'pageUrl: string; title: string;'); - - const result = await tool.handler({ - component: 'UrlOnlyComponent', - conversationContext: { - step: 'configure_attrs', - selectedProps: ['pageUrl', 'title'], - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('pageUrl'); - expect(text).to.include('title'); - }); - }); - - describe('configure_regions step', () => { - it('should provide region configuration instructions', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'RegionsComponent'); - - const result = await tool.handler({ - component: 'RegionsComponent', - conversationContext: { - step: 'configure_regions', - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should provide region configuration guidance - expect(text).to.match(/region|Region/i); - }); - }); - - describe('confirm_generation step', () => { - it('should generate decorator code when all context provided', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'ConfirmComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'ConfirmComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['title'], - componentMetadata: { - id: 'confirm-component', - name: 'Confirm Component', - description: 'Test component', - }, - attributeConfig: { - title: { - type: 'string', - name: 'Title', - }, - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should generate complete decorator code - expect(text).to.include('@Component'); - expect(text).to.include('@AttributeDefinition'); - expect(text).to.include('ConfirmComponent'); - expect(text).to.include('title'); - }); - - it('should require component metadata', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'MissingMetadataConfirmComponent'); - - const result = await tool.handler({ - component: 'MissingMetadataConfirmComponent', - conversationContext: { - step: 'confirm_generation', - // Missing componentMetadata - }, - }); - - // Should return error when metadata is missing - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.match(/metadata|Metadata/i); - }); - - it('should generate code with regions', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'RegionGenComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'RegionGenComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['title'], - componentMetadata: { - id: 'region-gen-component', - name: 'Region Gen Component', - description: 'Test component with regions', - }, - regionConfig: { - enabled: true, - regions: [ - { - id: 'main', - name: 'Main Content', - description: 'Primary content area', - maxComponents: 5, - componentTypeInclusions: ['text-block', 'image-block'], - componentTypeExclusions: ['layout-grid'], - }, - { - id: 'sidebar', - name: 'Sidebar', - }, - ], - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('@Component'); - expect(text).to.include('RegionDefinition'); - expect(text).to.include('main'); - expect(text).to.include('Main Content'); - expect(text).to.include('Primary content area'); - expect(text).to.include('maxComponents'); - expect(text).to.include('componentTypeInclusions'); - expect(text).to.include('componentTypeExclusions'); - expect(text).to.include('sidebar'); - expect(text).to.include('Sidebar'); - expect(text).to.match(/region|Region/i); - }); - - it('should generate code with fully configured attributes', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'ConfiguredAttrComponent', 'heroImage: string; variant: string;'); - - const result = await tool.handler({ - component: 'ConfiguredAttrComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['heroImage', 'variant'], - componentMetadata: { - id: 'configured-attr', - name: 'Configured Attr', - description: 'Test with full config', - group: 'custom_group', - }, - attributeConfig: { - heroImage: { - type: 'image', - name: 'Hero Image', - }, - variant: { - type: 'enum', - name: 'Variant', - defaultValue: 'primary', - values: ['primary', 'secondary', 'outline'], - }, - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('@AttributeDefinition'); - expect(text).to.include("type: 'image'"); - expect(text).to.include('Hero Image'); - expect(text).to.include("type: 'enum'"); - expect(text).to.include("'primary'"); - expect(text).to.include("'secondary'"); - expect(text).to.include("'outline'"); - expect(text).to.include('defaultValue'); - expect(text).to.include('custom_group'); - }); - - it('should generate code with new attributes only', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'NewAttrsComponent'); - - const result = await tool.handler({ - component: 'NewAttrsComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: [], - newAttributes: [{name: 'ctaLabel', required: true}, {name: 'subtitle'}], - componentMetadata: { - id: 'new-attrs', - name: 'New Attrs', - description: 'Test with new attributes', - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('@Component'); - expect(text).to.include('ctaLabel'); - expect(text).to.include('subtitle'); - }); - - it('should use default group when group is omitted', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'NoGroupComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'NoGroupComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['title'], - componentMetadata: { - id: 'no-group', - name: 'No Group', - description: 'Test without group', - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('odyssey_base'); - }); - - it('should generate code with disabled region config', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'DisabledRegionComponent', 'title: string;'); - - const result = await tool.handler({ - component: 'DisabledRegionComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['title'], - componentMetadata: { - id: 'disabled-region', - name: 'Disabled Region', - description: 'Test with disabled regions', - }, - regionConfig: { - enabled: false, - regions: [{id: 'main', name: 'Main'}], - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('@Component'); - expect(text).to.not.include('RegionDefinition'); - }); - - it('should generate code with non-string defaultValue', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'DefaultValComponent', 'count: number;'); - - const result = await tool.handler({ - component: 'DefaultValComponent', - conversationContext: { - step: 'confirm_generation', - selectedProps: ['count'], - componentMetadata: { - id: 'default-val', - name: 'Default Val', - description: 'Test with non-string default', - }, - attributeConfig: { - count: { - type: 'integer', - name: 'Count', - defaultValue: 42, - }, - }, - }, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('defaultValue: 42'); - }); - }); - }); - - describe('error handling', () => { - it('should handle non-existent component gracefully', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - - const result = await tool.handler({ - component: 'NonExistentComponent', - }); - - // Should return an error result - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.match(/not found|error|Error/i); - }); - - it('should handle invalid input gracefully', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - - // Invalid input should be caught by zod validation - const result = await tool.handler({ - component: 123, // Invalid type - } as unknown as Record); - - // Should return an error result - expect(result.isError).to.be.true; - }); - - it('should handle invalid step name', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'TestComponent'); - - const result = await tool.handler({ - component: 'TestComponent', - conversationContext: {step: 'invalid_step'}, - } as unknown as Record); - - // Should return an error result for invalid step - expect(result.isError).to.be.true; - }); - - it('should handle missing required parameter', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - - const result = await tool.handler({} as unknown as Record); - - // Should return an error result - expect(result.isError).to.be.true; - }); - - it('should handle path-based component not found', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - - const result = await tool.handler({ - component: 'src/components/DoesNotExist.tsx', - }); - - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.match(/not found|Error/i); - }); - }); - - describe('component resolution', () => { - it('should find component by name in standard location', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'StandardLocationComponent'); - - const result = await tool.handler({ - component: 'StandardLocationComponent', - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('StandardLocationComponent'); - }); - - it('should find component by kebab-case name', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const kebabPath = path.join(testDir, 'src', 'components', 'product-card.tsx'); - mkdirSync(path.dirname(kebabPath), {recursive: true}); - writeFileSync( - kebabPath, - `export interface ProductCardProps { title: string; } -export default function ProductCard({title}: ProductCardProps) { return
{title}
; }`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'product-card', - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.match(/ProductCard|product-card/i); - }); - - it('should find nested component by name', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const nestedPath = path.join(testDir, 'src', 'components', 'hero', 'Hero.tsx'); - mkdirSync(path.dirname(nestedPath), {recursive: true}); - writeFileSync( - nestedPath, - `export interface HeroProps { title: string; } -export default function Hero({title}: HeroProps) { return
{title}
; }`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'Hero', - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('Hero'); - }); - - it('should find component by path', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const componentPath = createTestComponent(testDir, 'PathComponent'); - - const result = await tool.handler({ - component: path.relative(testDir, componentPath), - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('PathComponent'); - }); - - it('should use searchPaths when provided', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - // Create component in a custom location - const customDir = path.join(testDir, 'custom', 'components'); - mkdirSync(customDir, {recursive: true}); - const componentPath = path.join(customDir, 'CustomLocationComponent.tsx'); - writeFileSync( - componentPath, - `export interface CustomLocationComponentProps { - title: string; -} - -export default function CustomLocationComponent({title}: CustomLocationComponentProps) { - return
{title}
; -} -`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'CustomLocationComponent', - searchPaths: ['custom/components'], - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('CustomLocationComponent'); - }); - - it('should handle component name collision', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - // Create component in src/components/ - createTestComponent(testDir, 'CollisionComponent', 'title: string;'); - // Create component with same name in app/components/ - const appPath = path.join(testDir, 'app', 'components', 'CollisionComponent.tsx'); - mkdirSync(path.dirname(appPath), {recursive: true}); - writeFileSync( - appPath, - `export interface CollisionComponentProps { title: string; } -export default function CollisionComponent({title}: CollisionComponentProps) { return
{title}
; }`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'CollisionComponent', - }); - - // Should find one of the components (likely the first one found) - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('CollisionComponent'); - }); - - it('should pick default export over first named export (export default X pattern)', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - // Simulate product-item/index.tsx: has export function X (helper) and export default Y (main) - const productItemDir = path.join(testDir, 'src', 'components', 'product-item'); - mkdirSync(productItemDir, {recursive: true}); - const indexPath = path.join(productItemDir, 'index.tsx'); - writeFileSync( - indexPath, - `export interface ProductItemProps { productId: string; } - -export function ProductItemVariantImage() { return null; } -export function ProductItemVariantAttributes() { return null; } - -function ProductItem({ productId }: ProductItemProps) { - return
{productId}
; -} - -export default ProductItem; -`, - 'utf8', - ); - - const result = await tool.handler({ - component: 'ProductItem', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - // Must target ProductItem (default export), not ProductItemVariantImage (first named export) - expect(text).to.include('ProductItem'); - expect(text).not.to.include('ProductItemVariantImage'); - }); - }); - - describe('input validation', () => { - it('should accept valid component name', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'ValidComponent'); - - const result = await tool.handler({ - component: 'ValidComponent', - }); - - // Should not error on valid input - expect(result.isError).to.be.undefined; - }); - - it('should accept component path', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - const componentPath = createTestComponent(testDir, 'PathComponent'); - - const result = await tool.handler({ - component: path.relative(testDir, componentPath), - }); - - // Should not error on valid path - expect(result.isError).to.be.undefined; - }); - - it('should accept optional searchPaths', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'SearchComponent'); - - const result = await tool.handler({ - component: 'SearchComponent', - searchPaths: ['src/components'], - }); - - // Should not error with searchPaths - expect(result.isError).to.be.undefined; - }); - - it('should accept optional autoMode flag', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'AutoModeComponent'); - - const result = await tool.handler({ - component: 'AutoModeComponent', - autoMode: true, - }); - - // Should not error with autoMode - expect(result.isError).to.be.undefined; - }); - - it('should accept optional componentId', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'CustomIdComponent'); - - const result = await tool.handler({ - component: 'CustomIdComponent', - componentId: 'custom-component-id', - autoMode: true, - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.include('custom-component-id'); - }); - - it('should accept conversationContext with all steps', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'ConversationComponent'); - - const steps = ['analyze', 'select_props', 'configure_attrs', 'configure_regions', 'confirm_generation'] as const; - - const results = await Promise.all( - steps.map((step) => - tool.handler({ - component: 'ConversationComponent', - conversationContext: {step}, - }), - ), - ); - - const byStep = Object.fromEntries(steps.map((s, i) => [s, results[i]])) as Record< - (typeof steps)[number], - (typeof results)[number] - >; - - // analyze: deterministic header + component name in output - expect(byStep.analyze.isError).to.be.undefined; - expect(getResultText(byStep.analyze)).to.include('Step 1: Component Analysis'); - expect(getResultText(byStep.analyze)).to.include('ConversationComponent'); - - // select_props: requires componentMetadata; without it, errors with deterministic message - expect(byStep.select_props.isError).to.equal(true); - expect(getResultText(byStep.select_props)).to.include('Missing component metadata'); - - // configure_attrs: deterministic step header - expect(byStep.configure_attrs.isError).to.be.undefined; - expect(getResultText(byStep.configure_attrs)).to.include('Step 2: Attribute Configuration'); - - // configure_regions: deterministic step header + component name - expect(byStep.configure_regions.isError).to.be.undefined; - const regionsText = getResultText(byStep.configure_regions); - expect(regionsText).to.include('Step 3: Region Configuration'); - expect(regionsText).to.include('ConversationComponent'); - - // confirm_generation: requires componentMetadata; errors with deterministic message - expect(byStep.confirm_generation.isError).to.equal(true); - expect(getResultText(byStep.confirm_generation)).to.include('Missing component metadata'); - }); - }); - - describe('output format', () => { - it('should return text content in ToolResult format', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - createTestComponent(testDir, 'FormatComponent'); - - const result = await tool.handler({ - component: 'FormatComponent', - }); - - expect(result).to.have.property('content'); - expect(result.content).to.be.an('array'); - expect(result.content.length).to.be.greaterThan(0); - expect(result.content[0]).to.have.property('type', 'text'); - expect(result.content[0]).to.have.property('text'); - }); - - it('should return error format when component not found', async () => { - const tool = createPageDesignerDecoratorTool(getServices); - - const result = await tool.handler({ - component: 'NonExistentComponent', - }); - - expect(result.isError).to.be.true; - expect(result.content).to.be.an('array'); - expect(result.content[0]).to.have.property('type', 'text'); - }); - }); - - describe('generateDecoratorCode', () => { - it('should skip imports when needsImports is false', () => { - const code = generateDecoratorCode({ - needsImports: false, - componentId: 'test-comp', - componentName: 'Test Comp', - componentDescription: 'A test', - componentGroup: 'test_group', - metadataClassName: 'TestCompMetadata', - hasAttributes: false, - hasRegions: false, - hasLoader: false, - regions: [], - attributes: [], - }); - - expect(code).to.not.include('import'); - expect(code).to.include('@Component'); - expect(code).to.include('TestCompMetadata'); - }); - - it('should omit group line when componentGroup is empty', () => { - const code = generateDecoratorCode({ - needsImports: true, - componentId: 'no-group', - componentName: 'No Group', - componentDescription: 'Test', - componentGroup: '', - metadataClassName: 'NoGroupMetadata', - hasAttributes: false, - hasRegions: false, - hasLoader: false, - regions: [], - attributes: [], - }); - - expect(code).to.not.include('group:'); - }); - - it('should generate configured attributes with required field', () => { - const code = generateDecoratorCode({ - needsImports: true, - componentId: 'req-attr', - componentName: 'Req Attr', - componentDescription: 'Test', - metadataClassName: 'ReqAttrMetadata', - hasAttributes: true, - hasRegions: false, - hasLoader: false, - regions: [], - attributes: [ - { - name: 'title', - tsType: 'string', - optional: false, - hasConfig: true, - config: { - name: 'Title', - type: 'string', - required: true, - description: 'The main title', - id: 'title-field', - }, - }, - ], - }); - - expect(code).to.include('required: true'); - expect(code).to.include("id: 'title-field'"); - expect(code).to.include("description: 'The main title'"); - }); - - it('should generate regions with all optional fields', () => { - const code = generateDecoratorCode({ - needsImports: true, - componentId: 'full-region', - componentName: 'Full Region', - componentDescription: 'Test', - metadataClassName: 'FullRegionMetadata', - hasAttributes: false, - hasRegions: true, - hasLoader: false, - regions: [ - { - id: 'main', - name: 'Main', - description: 'Main content', - maxComponents: 10, - componentTypeInclusions: ['text-block'], - componentTypeExclusions: ['layout'], - }, - ], - attributes: [], - }); - - expect(code).to.include('RegionDefinition'); - expect(code).to.include("description: 'Main content'"); - expect(code).to.include('maxComponents: 10'); - expect(code).to.include("componentTypeInclusions: ['text-block']"); - expect(code).to.include("componentTypeExclusions: ['layout']"); - }); - - it('should fall back to simple attribute when hasConfig is true but config is undefined', () => { - const code = generateDecoratorCode({ - needsImports: false, - componentId: 'fallback', - componentName: 'Fallback', - componentDescription: 'Test', - metadataClassName: 'FallbackMetadata', - hasAttributes: true, - hasRegions: false, - hasLoader: false, - regions: [], - attributes: [ - { - name: 'title', - tsType: 'string', - optional: true, - hasConfig: true, - config: undefined, - }, - ], - }); - - expect(code).to.include('@AttributeDefinition()'); - expect(code).to.include('title?: string'); - }); - - it('should handle optional configured attribute', () => { - const code = generateDecoratorCode({ - needsImports: false, - componentId: 'opt-config', - componentName: 'Opt Config', - componentDescription: 'Test', - metadataClassName: 'OptConfigMetadata', - hasAttributes: true, - hasRegions: false, - hasLoader: false, - regions: [], - attributes: [ - { - name: 'subtitle', - tsType: 'string', - optional: true, - hasConfig: true, - config: {type: 'text', name: 'Subtitle'}, - }, - ], - }); - - expect(code).to.include('subtitle?: string'); - expect(code).to.include("type: 'text'"); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/sfnext-development-guidelines.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/sfnext-development-guidelines.test.ts deleted file mode 100644 index bb4aa2776..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/sfnext-development-guidelines.test.ts +++ /dev/null @@ -1,651 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {createDeveloperGuidelinesTool} from '../../../src/tools/storefrontnext/sfnext-development-guidelines.js'; -import {Services} from '../../../src/services.js'; -import type {ToolResult} from '../../../src/utils/types.js'; -import {createMockResolvedConfig} from '../../test-helpers.js'; - -/** - * Helper to extract text from a ToolResult. - * Throws if the first content item is not a text type. - */ -function getResultText(result: ToolResult): string { - const content = result.content[0]; - if (content.type !== 'text') { - throw new Error(`Expected text content, got ${content.type}`); - } - return content.text; -} - -/** - * Create a mock services instance for testing. - */ -function createMockServices(): Services { - return new Services({resolvedConfig: createMockResolvedConfig()}); -} - -describe('tools/storefrontnext/sfnext-development-guidelines', () => { - let services: Services; - - beforeEach(() => { - services = createMockServices(); - }); - - describe('tool metadata', () => { - it('should have correct tool name', () => { - const tool = createDeveloperGuidelinesTool(() => services); - expect(tool.name).to.equal('sfnext_get_guidelines'); - }); - - it('should have concise, action-oriented description', () => { - const tool = createDeveloperGuidelinesTool(() => services); - const desc = tool.description; - - // Should emphasize this is an essential first step (most important) - expect(desc).to.include('ESSENTIAL FIRST STEP'); - expect(desc).to.include('Use this tool FIRST'); - - // Should mention core purpose clearly - expect(desc).to.include('Storefront Next'); - expect(desc).to.include('architecture rules'); - expect(desc).to.include('coding standards'); - expect(desc).to.include('best practices'); - - // Should mention key architectural patterns - expect(desc).to.include('React Server Components'); - expect(desc).to.include('data loading'); - expect(desc).to.include('framework constraints'); - - // Should describe behavior concisely - expect(desc).to.match(/comprehensive|quick reference/i); - - // Should be reasonably short (optimized for LLM consumption) - // Note: Description includes critical instructions plus a deprecation - // notice prefix, so slightly longer than ideal. - expect(desc.length).to.be.lessThan(900); - }); - - it('should carry a deprecation notice', () => { - const tool = createDeveloperGuidelinesTool(() => services); - expect(tool.description).to.include('[DEPRECATED]'); - }); - - it('should list all sections in inputSchema description', () => { - const tool = createDeveloperGuidelinesTool(() => services); - - // The inputSchema should list all available sections for discoverability - // This is better UX than burying them in the main description - const allSections = [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ]; - - // Each section should be valid (tests that SECTIONS_METADATA is complete) - for (const section of allSections) { - const result = tool.handler({sections: [section]}); - expect(result).to.be.a('promise'); - } - }); - - it('should include detailed topics in inputSchema description', () => { - // Main description should be concise - // Detailed topics should be in inputSchema.sections.describe() - // This follows MCP best practices: main description = WHEN/WHY, inputSchema = HOW - - const tool = createDeveloperGuidelinesTool(() => services); - const desc = tool.description; - - // Main description should be concise, not list all topics - // Note: Description includes critical instructions plus a deprecation - // notice prefix, so slightly longer than ideal. - expect(desc.length).to.be.lessThan(900); - - // Main description focuses on WHEN and WHY - expect(desc).to.include('ESSENTIAL FIRST STEP'); - expect(desc).to.include('FIRST before writing'); - - // Detailed topics moved to inputSchema (verified by test above) - // This keeps main description scannable for LLMs while providing full detail where needed - }); - - it('should be in STOREFRONTNEXT_DEPRECATED toolset', () => { - const tool = createDeveloperGuidelinesTool(() => services); - expect(tool.toolsets).to.include('STOREFRONTNEXT_DEPRECATED'); - expect(tool.toolsets).to.have.lengthOf(1); - }); - - it('should be GA (generally available)', () => { - const tool = createDeveloperGuidelinesTool(() => services); - expect(tool.isGA).to.be.false; - }); - - it('should not require B2C instance', () => { - const tool = createDeveloperGuidelinesTool(() => services); - // Guidelines are static content, no instance needed - expect(tool).to.not.have.property('requiresInstance'); - }); - - it('should prevent section/description mismatch with single source of truth', () => { - // This test ensures that sections and descriptions are defined together - // in SECTIONS_METADATA, making it impossible to have mismatched arrays - - // Verify all 12 sections exist - const allSections = [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ]; - - // Create tool to verify derived _SECTIONS matches - const tool = createDeveloperGuidelinesTool(() => services); - - // Each section should be valid and retrievable - for (const section of allSections) { - const result = tool.handler({sections: [section]}); - expect(result).to.be.a('promise'); // Should not throw sync error - } - }); - }); - - describe('inputSchema behavior', () => { - it('should have sections parameter that is optional', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - - // Should work without providing sections parameter - const result = await tool.handler({}); - expect(result.isError).to.be.undefined; - expect(getResultText(result)).to.not.be.empty; - }); - - it('should accept array of valid section enums', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - - // All valid sections from _SECTIONS constant - const validSections = [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ]; - - for (const section of validSections) { - // eslint-disable-next-line no-await-in-loop - const result = await tool.handler({sections: [section]}); - expect(result.isError).to.be.undefined; - } - }); - }); - - describe('default behavior', () => { - it('should return comprehensive guidelines by default when no sections specified', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Verify it returns content (should be non-empty) - expect(text).to.not.be.empty; - - // Should contain quick-reference content - expect(text).to.match(/server|component|data|loading|TypeScript/i); - - // Should contain data-fetching section (comprehensive default) - expect(text).to.include('Data Fetching Patterns'); - - // Should contain components section - expect(text).to.include('Component Patterns'); - - // Should contain testing section - expect(text).to.include('Testing Strategy'); - }); - - it('should return empty string when sections array is explicitly empty', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: []}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - // Empty array returns empty string (edge case) - expect(text).to.be.empty; - }); - }); - - describe('single section retrieval', () => { - it('should support all 12 available sections as documented', () => { - // Verify the tool has exactly 12 sections available - // These are the sections mentioned in the inputSchema description - const expectedSections = [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ]; - - // This validates the contract stated in the inputSchema - expect(expectedSections).to.have.lengthOf(12); - }); - - it('should return quick-reference section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return data-fetching section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['data-fetching']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return state-management section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['state-management']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return auth section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['auth']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return config section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['config']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return i18n section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['i18n']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return components section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['components']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return page-designer section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['page-designer']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return performance section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['performance']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return testing section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['testing']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return extensions section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['extensions']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - - it('should return pitfalls section', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['pitfalls']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - expect(text).to.not.be.empty; - }); - }); - - describe('multiple section retrieval', () => { - it('should support contextual learning with multiple sections', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - - // Test related sections together (as mentioned in description) - const result = await tool.handler({ - sections: ['data-fetching', 'state-management'], - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should contain content from both sections - expect(text).to.not.be.empty; - - // Should contain the separator between sections - expect(text).to.include('\n\n---\n\n'); - - // Content should include topics from both sections - expect(text.toLowerCase()).to.match(/data|fetch|load/); - expect(text.toLowerCase()).to.match(/state|context/); - }); - - it('should combine three sections correctly', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({ - sections: ['auth', 'config', 'i18n'], - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should contain content - expect(text).to.not.be.empty; - - // Should have exactly 2 separators between the 3 content sections - // Plus 2 more from prefix and footer instructions = 4 total - const separators = text.match(/\n\n---\n\n/g); - expect(separators).to.have.lengthOf(4); - }); - - it('should maintain order of sections as requested', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - - // Request sections in specific order - const result = await tool.handler({ - sections: ['auth', 'config'], - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should contain the separator - expect(text).to.include('\n\n---\n\n'); - - // Split on separator - will include prefix and footer - const allParts = text.split('\n\n---\n\n'); - - // Filter out prefix (starts with warning emoji) and footer (contains "END OF CONTENT") - const contentSections = allParts.filter((part) => !part.includes('⚠️') && !part.includes('END OF CONTENT')); - - // Should have two content sections - expect(contentSections).to.have.lengthOf(2); - - // Verify content is from the expected sections in the correct order - expect(contentSections[0]).to.include('Authentication'); - expect(contentSections[1]).to.include('Configuration'); - }); - - it('should handle all sections at once', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({ - sections: [ - 'quick-reference', - 'data-fetching', - 'state-management', - 'auth', - 'config', - 'i18n', - 'components', - 'page-designer', - 'performance', - 'testing', - 'extensions', - 'pitfalls', - ], - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should contain content - expect(text).to.not.be.empty; - - // Should have at least 11 separators for 12 sections - // (may have more if markdown content contains similar patterns) - const separators = text.match(/\n\n---\n\n/g); - expect(separators).to.not.be.null; - expect(separators!.length).to.be.at.least(11); - - // Verify content from various sections is present - expect(text).to.include('Authentication'); - expect(text).to.include('Configuration'); - expect(text).to.include('Internationalization'); - }); - }); - - describe('input validation', () => { - it('should reject invalid section names', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['invalid-section']} as any); - - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.include('Invalid input'); - }); - - it('should reject empty strings in sections array', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: ['']} as any); - - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.include('Invalid input'); - }); - - it('should reject non-array sections parameter', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: 'quick-reference'} as any); - - expect(result.isError).to.be.true; - const text = getResultText(result); - expect(text).to.include('Invalid input'); - }); - }); - - describe('content verification', () => { - it('should load actual markdown content from files', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Verify it's actual markdown content (should have markdown formatting) - // Most markdown files have headers, lists, or code blocks - expect(text).to.match(/#|\*|-|```/); - }); - - it('should return different content for different sections', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - - const result1 = await tool.handler({sections: ['data-fetching']}); - const result2 = await tool.handler({sections: ['auth']}); - - expect(result1.isError).to.be.undefined; - expect(result2.isError).to.be.undefined; - - const text1 = getResultText(result1); - const text2 = getResultText(result2); - - // Different sections should have different content - expect(text1).to.not.equal(text2); - }); - - it('should cover critical topics mentioned in description', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - - // Test that key topics from the description are covered in relevant sections - const topicTests = [ - {section: 'data-fetching', keywords: ['server', 'data', 'load']}, - {section: 'auth', keywords: ['authentication', 'session']}, - {section: 'i18n', keywords: ['internationalization', 'locale', 'translation']}, - {section: 'performance', keywords: ['performance', 'optimization']}, - {section: 'testing', keywords: ['test']}, - {section: 'pitfalls', keywords: ['pitfall', 'common', 'avoid', 'mistake', 'error']}, - ]; - - for (const {section, keywords} of topicTests) { - // eslint-disable-next-line no-await-in-loop - const result = await tool.handler({sections: [section]}); - expect(result.isError).to.be.undefined; - - const text = getResultText(result).toLowerCase(); - - // At least one keyword should be present - const hasKeyword = keywords.some((keyword) => text.includes(keyword)); - expect(hasKeyword, `Section ${section} should contain one of: ${keywords.join(', ')}`).to.be.true; - } - }); - - it('should provide non-negotiable architecture rules in quick-reference', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // The description emphasizes "non-negotiable architecture rules" - // Quick reference should contain guidance about rules/patterns - const hasRulesOrPatterns = - text.toLowerCase().includes('rule') || - text.toLowerCase().includes('pattern') || - text.toLowerCase().includes('must') || - text.toLowerCase().includes('always') || - text.toLowerCase().includes('never'); - - expect(hasRulesOrPatterns, 'Quick reference should contain architecture rules/patterns').to.be.true; - }); - - it('should emphasize TypeScript-only approach', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: ['quick-reference']}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Description mentions "TypeScript-only" - expect(text.toLowerCase()).to.match(/typescript|\.tsx?|type/); - }); - }); - - describe('edge cases', () => { - it('should handle undefined sections parameter', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({sections: undefined}); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should default to comprehensive guidelines (quick-reference + key sections) - expect(text).to.not.be.empty; - expect(text).to.include('Data Fetching Patterns'); - }); - - it('should handle sections parameter explicitly set to null', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await tool.handler({sections: null} as any); - - // null is not a valid array, should error - expect(result.isError).to.be.true; - }); - - it('should handle duplicate sections in array', async () => { - const tool = createDeveloperGuidelinesTool(() => services); - const result = await tool.handler({ - sections: ['auth', 'auth'], - }); - - expect(result.isError).to.be.undefined; - const text = getResultText(result); - - // Should return content with one separator - expect(text).to.include('\n\n---\n\n'); - - // Split on separator - will include prefix and footer, so we need to filter - // The prefix ends with '---\n\n' and footer starts with '\n\n---\n\n' - // So we get: [prefix, section1, section2, footer] - const allParts = text.split('\n\n---\n\n'); - - // Filter out prefix (starts with warning emoji) and footer (contains "END OF CONTENT") - const contentSections = allParts.filter((part) => !part.includes('⚠️') && !part.includes('END OF CONTENT')); - - // Should have duplicated content (same section twice) - expect(contentSections).to.have.lengthOf(2); - // Content should be the same (both are the auth section) - expect(contentSections[0].trim()).to.equal(contentSections[1].trim()); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/README.md b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/README.md deleted file mode 100644 index 716b73e91..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# Testing Site Theming Tool - -## Test Status - -The site-theming tool has comprehensive unit tests covering: - -- **Tool metadata**: Name, description, toolsets, inputSchema -- **Tool behavior**: List files, retrieve guidance, error handling, question filtering -- **Color validation**: Automated WCAG contrast validation when `colorMapping` is provided -- **File merging**: `fileKeys` array, `fileKeys` with defaults, missing file errors -- **Edge cases**: Ready to Implement flow, validation summary for failing contrast -- **color-contrast.ts**: Luminance, contrast ratio, WCAG levels, validateContrast, formatValidationResult -- **theming-store.ts**: Initialize, loadFile, get/getKeys, THEMING_FILES env, workflow/validation parsing - -All tests use the standard Mocha test framework and run with `pnpm test`. - -## Testing Approaches - -### 1. Unit Tests (Automated) - -Run the test suite: - -```bash -cd packages/b2c-dx-mcp -pnpm run test:agent -- test/tools/storefrontnext/site-theming/ -``` - -### 2. MCP Inspector (Interactive Testing) - -Use the MCP Inspector to test the tool interactively: - -```bash -cd packages/b2c-dx-mcp -pnpm run inspect:dev -``` - -Then in the inspector: - -1. Click **Connect** -2. Click **List Tools** - you should see `sfnext_configure_theme` -3. Click on the tool to test it with real inputs - -### 3. CLI Testing - -Test via command line: - -```bash -# List all tools (should include sfnext_configure_theme) -npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools --method tools/list - -# Call the tool - list available files -npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ - --method tools/call \ - --tool-name sfnext_configure_theme \ - --args '{}' - -# Call with conversation context - get guidelines and questions -npx mcp-inspector --cli node bin/dev.js --toolsets STOREFRONTNEXT --allow-non-ga-tools \ - --method tools/call \ - --tool-name sfnext_configure_theme \ - --args '{"conversationContext":{"collectedAnswers":{"colors":[],"fonts":[]}}}' -``` - -### 4. Manual Test Scenarios - -#### List Available Files - -```json -{} -``` - -Expected: Returns list of available theming files (theming-questions, theming-validation, theming-accessibility) - -#### First Call - Get Guidelines and Questions - -```json -{ - "conversationContext": { - "collectedAnswers": { - "colors": [], - "fonts": [] - } - } -} -``` - -Expected: Returns theming guidelines, critical rules, and questions to ask the user - -#### With Collected Colors and Fonts - -```json -{ - "conversationContext": { - "questionsAsked": ["color-1"], - "collectedAnswers": { - "colors": [{"hex": "#635BFF", "type": "primary"}, {"hex": "#0A2540", "type": "secondary"}], - "fonts": [{"name": "sohne-var", "type": "body"}] - } - } -} -``` - -Expected: Returns "Information You've Provided" section with colors and fonts, plus next questions - -#### Validation Call - Trigger Color Contrast Check - -```json -{ - "conversationContext": { - "collectedAnswers": { - "colors": [{"hex": "#635BFF", "type": "primary"}], - "colorMapping": { - "lightText": "#000000", - "lightBackground": "#FFFFFF", - "darkText": "#FFFFFF", - "darkBackground": "#18181B", - "buttonText": "#FFFFFF", - "buttonBackground": "#0A2540" - } - } - } -} -``` - -Expected: Returns "AUTOMATED COLOR VALIDATION RESULTS" with contrast ratios and WCAG status - -#### Merge Multiple Files - -```json -{ - "fileKeys": ["theming-questions", "theming-validation"], - "conversationContext": { - "collectedAnswers": {"colors": [], "fonts": []} - } -} -``` - -Expected: Returns merged guidance from both files - -#### Non-Existent File Key - -```json -{ - "fileKeys": ["non-existent"], - "conversationContext": { - "collectedAnswers": {"colors": [], "fonts": []} - } -} -``` - -Expected: Returns error with "not found" and lists available keys - -### 5. Custom Theming Files (THEMING_FILES) - -To test with custom content, set the `THEMING_FILES` environment variable: - -```bash -export THEMING_FILES='[{"key":"custom-theming","path":"/path/to/custom-theming.md"}]' -``` - -The path is relative to the project directory (or absolute). The custom file is merged with the default files. - -## Troubleshooting - -### "No theming files have been loaded" - -- Ensure the MCP server was started from a directory where the package's `content/site-theming/` files are available -- Default files are loaded from the installed package at runtime - -### "File not found" for custom THEMING_FILES - -- Verify the path in `THEMING_FILES` is correct (relative to project dir or absolute) -- Ensure the JSON is valid: `[{"key":"my-key","path":"path/to/file.md"}]` - -### Validation Not Appearing - -- `colorMapping` must be present in `conversationContext.collectedAnswers` -- `collectedAnswers.colors` must be a non-empty array for validation to run diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-contrast.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-contrast.test.ts deleted file mode 100644 index 2ef125999..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-contrast.test.ts +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - getLuminance, - getContrastRatio, - getWCAGLevel, - validateContrast, - validateColorCombinations, - formatValidationResult, - isValidHex, - WCAGLevel, -} from '../../../../src/tools/storefrontnext/site-theming/color-contrast.js'; - -describe('tools/storefrontnext/site-theming/color-contrast', () => { - describe('getLuminance', () => { - it('should return 0 for pure black', () => { - expect(getLuminance('#000000')).to.equal(0); - expect(getLuminance('000000')).to.equal(0); - }); - - it('should return 1 for pure white', () => { - expect(getLuminance('#FFFFFF')).to.equal(1); - expect(getLuminance('FFFFFF')).to.equal(1); - }); - - it('should return correct luminance for mid-gray', () => { - const luminance = getLuminance('#808080'); - expect(luminance).to.be.greaterThan(0.2); - expect(luminance).to.be.lessThan(0.6); - }); - - it('should handle hex with # prefix', () => { - expect(getLuminance('#635BFF')).to.be.a('number'); - expect(getLuminance('#635BFF')).to.be.greaterThan(0); - expect(getLuminance('#635BFF')).to.be.lessThan(1); - }); - - it('should throw for invalid hex format', () => { - expect(() => getLuminance('#GG')).to.throw(/Invalid hex color/); - expect(() => getLuminance('xyz')).to.throw(/Invalid hex color/); - expect(() => getLuminance('#12345')).to.throw(/Invalid hex color/); - expect(() => getLuminance('#1234567')).to.throw(/Invalid hex color/); - }); - }); - - describe('isValidHex', () => { - it('should return true for valid 6-digit hex', () => { - expect(isValidHex('#635BFF')).to.be.true; - expect(isValidHex('635BFF')).to.be.true; - expect(isValidHex('#000000')).to.be.true; - expect(isValidHex('#FFFFFF')).to.be.true; - }); - - it('should return false for invalid hex', () => { - expect(isValidHex('#GG')).to.be.false; - expect(isValidHex('xyz')).to.be.false; - expect(isValidHex('#12345')).to.be.false; - expect(isValidHex('')).to.be.false; - }); - }); - - describe('getContrastRatio', () => { - it('should return 21 for black on white', () => { - const ratio = getContrastRatio('#000000', '#FFFFFF'); - expect(ratio).to.be.closeTo(21, 0.1); - }); - - it('should return 21 for white on black', () => { - const ratio = getContrastRatio('#FFFFFF', '#000000'); - expect(ratio).to.be.closeTo(21, 0.1); - }); - - it('should return 1 for same color', () => { - expect(getContrastRatio('#635BFF', '#635BFF')).to.equal(1); - }); - - it('should return same ratio regardless of order', () => { - const r1 = getContrastRatio('#000000', '#FFFFFF'); - const r2 = getContrastRatio('#FFFFFF', '#000000'); - expect(r1).to.equal(r2); - }); - }); - - describe('getWCAGLevel', () => { - describe('normal text', () => { - it('should return AAA for ratio >= 7', () => { - expect(getWCAGLevel(7)).to.equal(WCAGLevel.AAA); - expect(getWCAGLevel(10)).to.equal(WCAGLevel.AAA); - }); - - it('should return AA for ratio >= 4.5 and < 7', () => { - expect(getWCAGLevel(4.5)).to.equal(WCAGLevel.AA); - expect(getWCAGLevel(5.5)).to.equal(WCAGLevel.AA); - }); - - it('should return FAIL for ratio < 4.5', () => { - expect(getWCAGLevel(4.4)).to.equal(WCAGLevel.FAIL); - expect(getWCAGLevel(2)).to.equal(WCAGLevel.FAIL); - }); - }); - - describe('large text', () => { - it('should return AAA_LARGE for ratio >= 4.5', () => { - expect(getWCAGLevel(4.5, true)).to.equal(WCAGLevel.AAA_LARGE); - expect(getWCAGLevel(7, true)).to.equal(WCAGLevel.AAA_LARGE); - }); - - it('should return AA_LARGE for ratio >= 3 and < 4.5', () => { - expect(getWCAGLevel(3, true)).to.equal(WCAGLevel.AA_LARGE); - expect(getWCAGLevel(4, true)).to.equal(WCAGLevel.AA_LARGE); - }); - - it('should return FAIL for ratio < 3', () => { - expect(getWCAGLevel(2.9, true)).to.equal(WCAGLevel.FAIL); - }); - }); - }); - - describe('validateContrast', () => { - it('should return excellent for high contrast (black on white)', () => { - const result = validateContrast('#000000', '#FFFFFF'); - expect(result.passesAA).to.be.true; - expect(result.passesAAA).to.be.true; - expect(result.visualAssessment).to.equal('excellent'); - expect(result.ratio).to.be.closeTo(21, 0.1); - expect(result.wcagLevel).to.equal(WCAGLevel.AAA); - }); - - it('should return good for ratio between 5 and 7', () => { - // #6B6B6B on white gives ~5.3:1 (good range, 5-7) - const result = validateContrast('#6B6B6B', '#FFFFFF'); - expect(result.passesAA).to.be.true; - expect(result.visualAssessment).to.equal('good'); - }); - - it('should return acceptable for ratio at 4.5 threshold', () => { - const result = validateContrast('#767676', '#FFFFFF'); - expect(result.passesAA).to.be.true; - expect(result.visualAssessment).to.equal('acceptable'); - expect(result.recommendation).to.include('WCAG AA'); - }); - - it('should return poor with recommendation for failing contrast', () => { - const result = validateContrast('#CCCCCC', '#FFFFFF'); - expect(result.passesAA).to.be.false; - expect(result.visualAssessment).to.equal('poor'); - expect(result.recommendation).to.include('WCAG AA'); - }); - - it('should handle large text threshold', () => { - // #888888 on white gives ~3.9:1 - AA_LARGE (3:1) but not AAA_LARGE (4.5:1) - const result = validateContrast('#888888', '#FFFFFF', true); - expect(result.isLargeText).to.be.true; - expect(result.passesAA).to.be.true; - expect(result.wcagLevel).to.equal(WCAGLevel.AA_LARGE); - }); - - it('should throw for invalid hex', () => { - expect(() => validateContrast('#GG', '#FFFFFF')).to.throw(/Invalid hex color/); - expect(() => validateContrast('#000000', 'xyz')).to.throw(/Invalid hex color/); - }); - }); - - describe('validateColorCombinations', () => { - it('should validate multiple combinations', () => { - const results = validateColorCombinations([ - {foreground: '#000000', background: '#FFFFFF', label: 'Black on white'}, - {foreground: '#FFFFFF', background: '#000000', label: 'White on black'}, - ]); - - expect(results).to.have.lengthOf(2); - expect(results[0].label).to.equal('Black on white'); - expect(results[0].passesAA).to.be.true; - expect(results[1].label).to.equal('White on black'); - expect(results[1].passesAA).to.be.true; - }); - - it('should pass isLargeText to validateContrast', () => { - const results = validateColorCombinations([{foreground: '#767676', background: '#FFFFFF', isLargeText: true}]); - - expect(results[0].isLargeText).to.be.true; - expect(results[0].passesAA).to.be.true; - }); - - it('should throw when combination contains invalid hex', () => { - expect(() => - validateColorCombinations([ - {foreground: '#000000', background: '#FFFFFF'}, - {foreground: '#GG', background: '#FFFFFF'}, - ]), - ).to.throw(/Invalid hex color/); - }); - }); - - describe('formatValidationResult', () => { - it('should format passing result with label', () => { - const result = { - color1: '#000000', - color2: '#FFFFFF', - ratio: 21, - wcagLevel: WCAGLevel.AAA, - passesAA: true, - passesAAA: true, - isLargeText: false, - visualAssessment: 'excellent' as const, - label: 'Primary text', - }; - - const output = formatValidationResult(result); - - expect(output).to.include('Primary text:'); - expect(output).to.include('#000000 on #FFFFFF'); - expect(output).to.include('Contrast Ratio: 21.00:1'); - expect(output).to.include('WCAG normal text'); - expect(output).to.include('✅ AAA'); - expect(output).to.include('EXCELLENT'); - }); - - it('should format result with recommendation when failing', () => { - const result = { - color1: '#CCCCCC', - color2: '#FFFFFF', - ratio: 1.6, - wcagLevel: WCAGLevel.FAIL, - passesAA: false, - passesAAA: false, - isLargeText: false, - visualAssessment: 'poor' as const, - recommendation: 'Does not meet WCAG AA standards.', - }; - - const output = formatValidationResult(result); - - expect(output).to.include('❌ FAIL'); - expect(output).to.include('POOR'); - expect(output).to.include('Does not meet WCAG AA standards.'); - }); - - it('should format result without label when not provided', () => { - const result = { - color1: '#000000', - color2: '#FFFFFF', - ratio: 21, - wcagLevel: WCAGLevel.AAA, - passesAA: true, - passesAAA: true, - isLargeText: false, - visualAssessment: 'excellent' as const, - }; - - const output = formatValidationResult(result); - - expect(output).to.not.match(/^[^:]+: #/); - expect(output).to.include('#000000 on #FFFFFF'); - }); - - it('should format AA (not AAA) result with WCAG AA status', () => { - const result = { - color1: '#6B6B6B', - color2: '#FFFFFF', - ratio: 5.3, - wcagLevel: WCAGLevel.AA, - passesAA: true, - passesAAA: false, - isLargeText: false, - visualAssessment: 'good' as const, - }; - - const output = formatValidationResult(result); - - expect(output).to.include('✅ AA'); - expect(output).to.include('WCAG normal text'); - }); - - it('should format large text result', () => { - const result = { - color1: '#888888', - color2: '#FFFFFF', - ratio: 3.9, - wcagLevel: WCAGLevel.AA_LARGE, - passesAA: true, - passesAAA: false, - isLargeText: true, - visualAssessment: 'acceptable' as const, - }; - - const output = formatValidationResult(result); - - expect(output).to.include('WCAG large text'); - expect(output).to.include('✅ AA'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-mapping.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-mapping.test.ts deleted file mode 100644 index 8e08ca8ea..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/color-mapping.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - buildColorCombinations, - appendValidationSection, - type ColorCombination, -} from '../../../../src/tools/storefrontnext/site-theming/color-mapping.js'; - -describe('tools/storefrontnext/site-theming/color-mapping', () => { - describe('buildColorCombinations', () => { - it('should derive text-on-background combinations from semantic mapping', () => { - const mapping = { - lightText: '#000000', - lightBackground: '#FFFFFF', - buttonText: '#FFFFFF', - buttonBackground: '#0A2540', - }; - const combos = buildColorCombinations(mapping); - expect(combos).to.have.lengthOf.at.least(2); - expect(combos.some((c) => c.label.includes('light') && c.label.includes('light background'))).to.be.true; - expect(combos.some((c) => c.label.includes('button') && c.label.includes('button background'))).to.be.true; - }); - - it('should use fallback white/dark backgrounds when no text-background pairs found', () => { - const mapping = {accent: '#635BFF', primary: '#0A2540'}; - const combos = buildColorCombinations(mapping); - expect(combos).to.have.lengthOf(4); - expect(combos.filter((c) => c.background === '#FFFFFF')).to.have.lengthOf(2); - expect(combos.filter((c) => c.background === '#18181B')).to.have.lengthOf(2); - expect(combos.some((c) => c.label.includes('accent') && c.label.includes('white background'))).to.be.true; - expect(combos.some((c) => c.label.includes('primary') && c.label.includes('dark background'))).to.be.true; - }); - - it('should skip invalid hex values', () => { - const mapping = {lightText: '#000000', invalid: 'nothex', lightBackground: '#FFFFFF'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('invalid'))).to.be.false; - }); - - it('should skip non-hex values', () => { - const mapping = {lightText: 'rgb(0,0,0)', lightBackground: '#FFFFFF'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.foreground === 'rgb(0,0,0)')).to.be.false; - }); - - it('should derive link color on light background combination', () => { - const mapping = {linkColor: '#0A2540', lightBackground: '#FFFFFF'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('link') && c.label.includes('light background'))).to.be.true; - }); - - it('should derive background combo when foreground key exists in mapping', () => { - const mapping = { - lightText: '#000000', - lightBackground: '#FFFFFF', - darkText: '#FFFFFF', - darkBackground: '#18181B', - }; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('lightText') && c.background === '#FFFFFF')).to.be.true; - expect(combos.some((c) => c.label.includes('darkText') && c.background === '#18181B')).to.be.true; - }); - - it('should derive text-on-background using button/dark/light fallback when background key missing', () => { - const mapping = { - buttonText: '#FFFFFF', - buttonBackground: '#0A2540', - }; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('button') && c.label.includes('button background'))).to.be.true; - }); - - it('should use dark background fallback for darkForeground when darkBackground missing', () => { - const mapping = {darkForeground: '#FFFFFF'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('dark') && c.label.includes('dark background'))).to.be.true; - expect(combos.some((c) => c.background === '#18181B')).to.be.true; - }); - - it('should use button background fallback for buttonForeground when buttonBackground missing', () => { - const mapping = {buttonForeground: '#FFFFFF'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('button') && c.label.includes('button background'))).to.be.true; - }); - - it('should use light background fallback for primaryText when background key missing', () => { - const mapping = {primaryText: '#000000', lightBackground: '#FFFFFF'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('light background') && c.background === '#FFFFFF')).to.be.true; - }); - - it('should use fallback when background is invalid hex (tryTextForegroundCombo returns null)', () => { - const mapping = {primaryText: '#000000', lightBackground: 'rgb(255,255,255)'}; - const combos = buildColorCombinations(mapping); - expect(combos.some((c) => c.label.includes('white background'))).to.be.true; - }); - }); - - describe('appendValidationSection', () => { - it('should return instructions unchanged when combinations are empty', () => { - const instructions = '# Test\nSome content.'; - const result = appendValidationSection(instructions, []); - expect(result).to.equal(instructions); - }); - - it('should append validation results and summary when combinations provided', () => { - const combos: ColorCombination[] = [{foreground: '#000000', background: '#FFFFFF', label: 'Test combo'}]; - const result = appendValidationSection('# Base\n', combos); - expect(result).to.include('# Base'); - expect(result).to.include('VALIDATION SUMMARY'); - expect(result).to.include('Test combo'); - }); - - it('should append issues summary when contrast fails', () => { - const combos: ColorCombination[] = [{foreground: '#888888', background: '#999999', label: 'Low contrast'}]; - const result = appendValidationSection('', combos); - expect(result).to.include('Issues found that should be addressed'); - expect(result).to.include('Low contrast'); - }); - - it('should append issues summary when visual assessment is acceptable', () => { - // Ratio 4.5-5 produces "acceptable" (meets AA but borderline readability) - const combos: ColorCombination[] = [{foreground: '#737373', background: '#FFFFFF', label: 'Borderline'}]; - const result = appendValidationSection('', combos); - expect(result).to.include('Issues found that should be addressed'); - expect(result).to.include('Borderline'); - }); - - it('should append success summary when all pass', () => { - const combos: ColorCombination[] = [{foreground: '#000000', background: '#FFFFFF', label: 'Good contrast'}]; - const result = appendValidationSection('', combos); - expect(result).to.include('All color combinations meet WCAG AA'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/guidance-merger.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/guidance-merger.test.ts deleted file mode 100644 index 9954dc48e..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/guidance-merger.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {mergeGuidance} from '../../../../src/tools/storefrontnext/site-theming/guidance-merger.js'; -import type {ThemingGuidance} from '../../../../src/tools/storefrontnext/site-theming/theming-store.js'; - -function createGuidance(overrides: Partial = {}): ThemingGuidance { - return { - questions: [], - guidelines: [], - rules: [], - metadata: {filePath: '', fileName: '', loadedAt: new Date()}, - ...overrides, - }; -} - -describe('tools/storefrontnext/site-theming/guidance-merger', () => { - it('should throw when merging empty array', () => { - expect(() => mergeGuidance([])).to.throw('Cannot merge empty guidance array'); - }); - - it('should return single item unchanged', () => { - const g = createGuidance({questions: [{id: 'q1', question: 'Q?', category: 'colors', required: true}]}); - expect(mergeGuidance([g])).to.equal(g); - }); - - it('should merge workflows and use extractionInstructions from second when first lacks it', () => { - const g1 = createGuidance({ - workflow: {steps: ['Step 1'], extractionInstructions: undefined, preImplementationChecklist: undefined}, - }); - const g2 = createGuidance({ - workflow: { - steps: ['Step 2'], - extractionInstructions: 'Extract colors and fonts.', - preImplementationChecklist: 'Check all items.', - }, - }); - const merged = mergeGuidance([g1, g2]); - expect(merged.workflow?.extractionInstructions).to.equal('Extract colors and fonts.'); - expect(merged.workflow?.preImplementationChecklist).to.equal('Check all items.'); - expect(merged.workflow?.steps).to.deep.equal(['Step 1', 'Step 2']); - }); - - it('should merge validations from multiple guidance objects', () => { - const g1 = createGuidance({ - validation: { - colorValidation: 'Check colors.', - fontValidation: undefined, - generalValidation: undefined, - requirements: undefined, - }, - }); - const g2 = createGuidance({ - validation: { - colorValidation: undefined, - fontValidation: 'Check fonts.', - generalValidation: 'Check general.', - requirements: 'Important.', - }, - }); - const merged = mergeGuidance([g1, g2]); - expect(merged.validation?.colorValidation).to.include('Check colors.'); - expect(merged.validation?.fontValidation).to.include('Check fonts.'); - expect(merged.validation?.generalValidation).to.include('Check general.'); - expect(merged.validation?.requirements).to.include('Important.'); - }); - - it('should deduplicate questions by id', () => { - const g1 = createGuidance({ - questions: [{id: 'q1', question: 'First?', category: 'colors', required: true}], - }); - const g2 = createGuidance({ - questions: [{id: 'q1', question: 'Override?', category: 'colors', required: false}], - }); - const merged = mergeGuidance([g1, g2]); - expect(merged.questions).to.have.lengthOf(1); - expect(merged.questions[0].question).to.equal('First?'); - }); - - it('should concatenate guidelines and rules', () => { - const g1 = createGuidance({ - guidelines: [{category: 'c1', title: 'T1', content: 'C1', critical: true}], - rules: [{type: 'do', description: 'Do X'}], - }); - const g2 = createGuidance({ - guidelines: [{category: 'c2', title: 'T2', content: 'C2', critical: false}], - rules: [{type: 'dont', description: "Don't Y"}], - }); - const merged = mergeGuidance([g1, g2]); - expect(merged.guidelines).to.have.lengthOf(2); - expect(merged.rules).to.have.lengthOf(2); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/index.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/index.test.ts deleted file mode 100644 index 985e20379..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/index.test.ts +++ /dev/null @@ -1,410 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {createSiteThemingTool} from '../../../../src/tools/storefrontnext/site-theming/index.js'; -import {Services} from '../../../../src/services.js'; -import type {ToolResult} from '../../../../src/utils/types.js'; -import {createMockResolvedConfig, createMockLoadServices} from '../../../test-helpers.js'; - -/** - * Helper to extract text from a ToolResult. - * Throws if the first content item is not a text type. - */ -function getResultText(result: ToolResult): string { - const content = result.content[0]; - if (content.type !== 'text') { - throw new Error(`Expected text content, got ${content.type}`); - } - return content.text; -} - -/** - * Type guard for string (used to satisfy unicorn/prefer-native-coercion-functions). - */ -function isString(x: unknown): x is string { - return typeof x === 'string'; -} - -/** - * Create a mock services instance for testing. - */ -function createMockServices(): Services { - return new Services({resolvedConfig: createMockResolvedConfig()}); -} - -describe('tools/storefrontnext/site-theming', () => { - let services: Services; - - const defaultContext = { - collectedAnswers: { - colors: [] as Array<{hex?: string; type?: string}>, - fonts: [] as Array<{name?: string; type?: string}>, - }, - }; - - beforeEach(() => { - services = createMockServices(); - }); - - describe('tool metadata', () => { - it('should have correct structure', () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - expect(tool).to.have.property('name', 'sfnext_configure_theme'); - expect(tool.description).to.include('theming guidelines'); - expect(tool).to.have.property('inputSchema'); - expect(tool).to.have.property('handler'); - expect(tool.handler).to.be.a('function'); - }); - - it('should be in STOREFRONTNEXT_DEPRECATED toolset', () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - expect(tool.toolsets).to.include('STOREFRONTNEXT_DEPRECATED'); - }); - }); - - describe('tool behavior', () => { - it('should list available files when called without parameters', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({}); - - expect(result.content).to.exist; - const text = getResultText(result); - expect(text).to.include('Available theming files'); - expect(text).to.include('theming-questions'); - expect(text).to.include('theming-validation'); - expect(text).to.include('theming-accessibility'); - }); - - it('should retrieve and parse theming file from store', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: defaultContext, - }); - - expect(result.content).to.exist; - const text = getResultText(result); - expect(text).to.include('Layout Preservation'); - expect(text).to.include('Critical Guidelines'); - expect(text).to.include('Questions to Ask the User'); - }); - - it('should return error when file key does not exist', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['non-existent'], - }); - - expect(result.isError).to.equal(true); - expect(getResultText(result)).to.include('not found'); - expect(getResultText(result)).to.include('non-existent'); - }); - - it('should extract questions from content', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: defaultContext, - }); - - const text = getResultText(result); - expect(text).to.include('Questions to Ask the User'); - expect(text).to.match(/color|font/i); - }); - - it('should filter questions based on conversation context', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - - const firstResult = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: defaultContext, - }); - - const firstText = getResultText(firstResult); - expect(firstText).to.include('Questions to Ask the User'); - - // Extract a question id actually shown in the first response. The internal - // section formats each question as: ### Question N (category): id - const idMatch = firstText.match(/\((?:colors|typography|general)\):\s*([\w-]+)/); - expect(idMatch, 'expected first response to contain at least one question id').to.not.equal(null); - const askedId = idMatch![1]; - - const secondResult = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - questionsAsked: [askedId], - collectedAnswers: { - colors: [{hex: '#635BFF', type: 'primary'}], - fonts: [], - [askedId]: 'answered', - }, - }, - }); - - const secondText = getResultText(secondResult); - // The asked question id must be excluded from the next batch of questions. - // The internal section uses `### Question N (category): ` so we look - // specifically for that header pattern. - expect(secondText).to.not.match(new RegExp(`\\((?:colors|typography|general)\\):\\s*${askedId}\\b`)); - }); - - it('should include collected theming info in response', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - questionsAsked: ['color-1', 'color-2'], - collectedAnswers: { - colors: [ - {hex: '#635BFF', type: 'primary'}, - {hex: '#0A2540', type: 'secondary'}, - ], - fonts: [{name: 'sohne-var', type: 'body'}], - 'color-1': 'primary', - 'color-2': 'accent', - }, - }, - }); - - const text = getResultText(result); - expect(text).to.include("Information You've Provided"); - expect(text).to.include('#635BFF'); - expect(text).to.include('sohne-var'); - }); - - it('should include critical guidelines in response', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: defaultContext, - }); - - const text = getResultText(result); - expect(text).to.include('Critical Guidelines'); - expect(text).to.include('Layout Preservation'); - }); - - it("should include DO and DON'T rules", async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: defaultContext, - }); - - const text = getResultText(result); - expect(text).to.include('What TO Do'); - expect(text).to.include('What NOT to Do'); - expect(text).to.match(/position|color/); - }); - - it('should use default files when conversationContext provided without fileKeys', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - conversationContext: defaultContext, - }); - - expect(result.content).to.exist; - expect(result.isError).to.not.equal(true); - const text = getResultText(result); - expect(text).to.include('Questions to Ask the User'); - }); - - it('should run automated color validation when colorMapping is provided', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - questionsAsked: ['color-1'], - collectedAnswers: { - colors: [{hex: '#635BFF', type: 'primary'}], - fonts: [], - colorMapping: { - lightText: '#000000', - lightBackground: '#FFFFFF', - darkText: '#FFFFFF', - darkBackground: '#18181B', - buttonText: '#FFFFFF', - buttonBackground: '#0A2540', - }, - }, - }, - }); - - const text = getResultText(result); - expect(text).to.include('AUTOMATED COLOR VALIDATION RESULTS'); - expect(text).to.include('Contrast Ratio'); - expect(text).to.match(/WCAG|AAA|AA|FAIL/); - }); - - it('should run validation when only colorMapping is provided (no colors array)', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - collectedAnswers: { - colorMapping: { - lightText: '#000000', - lightBackground: '#FFFFFF', - buttonText: '#FFFFFF', - buttonBackground: '#0A2540', - }, - }, - }, - }); - - const text = getResultText(result); - expect(text).to.include('AUTOMATED COLOR VALIDATION RESULTS'); - expect(text).to.include('Contrast Ratio'); - expect(text).to.match(/WCAG|AAA|AA|FAIL/); - }); - - it('should merge guidance when fileKeys array is provided', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions', 'theming-validation'], - conversationContext: defaultContext, - }); - - expect(result.isError).to.not.equal(true); - const text = getResultText(result); - // Merged content should include from both files - expect(text).to.include('Questions to Ask the User'); - expect(text).to.satisfy( - (t: string) => - t.includes('Input Validation') || t.includes('VALIDATION') || t.includes('Color') || t.includes('contrast'), - ); - }); - - it('should combine fileKeys with default files', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-accessibility'], - conversationContext: defaultContext, - }); - - expect(result.isError).to.not.equal(true); - const text = getResultText(result); - expect(text).to.be.a('string'); - expect(text.length).to.be.greaterThan(0); - }); - - it('should return error when fileKeys contains non-existent key', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions', 'non-existent-key'], - conversationContext: defaultContext, - }); - - expect(result.isError).to.equal(true); - const text = getResultText(result); - expect(text).to.include('not found'); - expect(text).to.include('non-existent-key'); - }); - }); - - describe('edge cases', () => { - it('should show Ready to Implement when all required questions answered', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - - // First call to get initial questions - then simulate answering all - const firstResult = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: defaultContext, - }); - const firstText = getResultText(firstResult); - - // Extract question IDs from the response (they appear in "Question N (category): id" format) - const questionIdMatches = firstText.match(/\((\w+)\):\s*(color-\d+|font-\d+|general-\d+)/g); - const questionIds: string[] = questionIdMatches - ? [...new Set(questionIdMatches.map((m) => m.split(':').pop()?.trim()).filter((x) => isString(x)))] - : ['color-1', 'font-1', 'general-1']; - - const collectedAnswers: Record = { - colors: [{hex: '#635BFF', type: 'primary'}], - fonts: [{name: 'sohne-var', type: 'body'}], - }; - for (const id of questionIds) { - if (id) { - collectedAnswers[id] = 'answered'; - } - } - - const secondResult = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - questionsAsked: questionIds, - collectedAnswers, - }, - }); - - const secondText = getResultText(secondResult); - // Should show Ready to Implement, pre-implementation checklist, or continue with workflow - expect(secondText).to.satisfy( - (t: string) => - t.includes('Ready to Implement') || - t.includes('PRE-IMPLEMENTATION') || - t.includes('validate all provided inputs') || - t.includes('Questions to Ask') || - t.includes('MANDATORY'), - ); - }); - - it('should show validation summary when color combinations fail WCAG', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - questionsAsked: ['color-1'], - collectedAnswers: { - colors: [{hex: '#CCCCCC', type: 'primary'}], - fonts: [], - colorMapping: { - lightText: '#DDDDDD', - lightBackground: '#FFFFFF', - }, - }, - }, - }); - - const text = getResultText(result); - // Poor contrast should trigger validation summary - expect(text).to.satisfy( - (t: string) => - t.includes('VALIDATION SUMMARY') || - t.includes('Issues found') || - t.includes('WCAG') || - t.includes('contrast'), - ); - }); - - it('should skip invalid hex in colorMapping without error', async () => { - const tool = createSiteThemingTool(createMockLoadServices(services)); - const result = await tool.handler({ - fileKeys: ['theming-questions'], - conversationContext: { - questionsAsked: ['color-1'], - collectedAnswers: { - colors: [{hex: '#635BFF', type: 'primary'}], - fonts: [], - colorMapping: { - lightText: '#000000', - lightBackground: '#FFFFFF', - invalidKey: '#GG', - anotherInvalid: 'not-hex', - }, - }, - }, - }); - - expect(result.isError).to.not.equal(true); - const text = getResultText(result); - // Should still run validation for valid colors; invalid hex is filtered out - expect(text).to.include('AUTOMATED COLOR VALIDATION RESULTS'); - expect(text).to.include('Contrast Ratio'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/response-builder.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/response-builder.test.ts deleted file mode 100644 index 9cd816994..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/response-builder.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import { - generateResponse, - getRelevantQuestions, - hasProvidedThemingInfo, -} from '../../../../src/tools/storefrontnext/site-theming/response-builder.js'; -import type {ThemingGuidance} from '../../../../src/tools/storefrontnext/site-theming/theming-store.js'; - -function createGuidance(overrides: Partial = {}): ThemingGuidance { - return { - questions: [], - guidelines: [], - rules: [], - metadata: {filePath: '', fileName: '', loadedAt: new Date()}, - ...overrides, - }; -} - -describe('tools/storefrontnext/site-theming/response-builder', () => { - describe('hasProvidedThemingInfo', () => { - it('should return false when no context', () => { - expect(hasProvidedThemingInfo(undefined)).to.be.false; - }); - - it('should return true when colors array provided', () => { - expect(hasProvidedThemingInfo({collectedAnswers: {colors: [{hex: '#000'}]}})).to.be.true; - }); - - it('should return true when fonts array provided', () => { - expect(hasProvidedThemingInfo({collectedAnswers: {fonts: [{name: 'Arial'}]}})).to.be.true; - }); - }); - - describe('getRelevantQuestions', () => { - it('should filter component scope questions', () => { - const g = createGuidance({ - questions: [ - {id: 'q1', question: 'Which components?', category: 'general', required: false}, - {id: 'q2', question: 'What colors?', category: 'colors', required: true}, - ], - }); - const qs = getRelevantQuestions(g); - expect(qs).to.have.lengthOf(1); - expect(qs[0].question).to.equal('What colors?'); - }); - - it('should sort required before optional', () => { - const g = createGuidance({ - questions: [ - {id: 'q1', question: 'Optional?', category: 'general', required: false}, - {id: 'q2', question: 'Required?', category: 'colors', required: true}, - ], - }); - const qs = getRelevantQuestions(g); - expect(qs[0].required).to.be.true; - }); - - it('should exclude already-asked questions', () => { - const g = createGuidance({ - questions: [ - {id: 'q1', question: 'Q1?', category: 'colors', required: true}, - {id: 'q2', question: 'Q2?', category: 'colors', required: false}, - ], - }); - const qs = getRelevantQuestions(g, {questionsAsked: ['q1']}); - expect(qs).to.have.lengthOf(1); - expect(qs[0].id).to.equal('q2'); - }); - - it('should return remaining questions when questionsAsked but no collectedAnswers', () => { - const g = createGuidance({ - questions: [ - {id: 'q1', question: 'Q1?', category: 'colors', required: true}, - {id: 'q2', question: 'Q2?', category: 'general', required: false}, - ], - }); - const qs = getRelevantQuestions(g, {questionsAsked: ['q1']}); - expect(qs).to.have.lengthOf(1); - expect(qs[0].id).to.equal('q2'); - }); - - it('should add follow-up questions when answer provided', () => { - const g = createGuidance({ - questions: [ - { - id: 'q1', - question: 'Q1?', - category: 'colors', - required: false, - followUpQuestions: ['Follow-up 1?', 'Follow-up 2?'], - }, - {id: 'q2', question: 'Q2?', category: 'colors', required: true}, - ], - }); - // q2 asked first; q1 in remaining with proactive answer triggers follow-ups - const qs = getRelevantQuestions(g, { - questionsAsked: ['q2'], - collectedAnswers: {q1: 'yes'}, - }); - expect(qs.some((q) => q.question === 'Follow-up 1?')).to.be.true; - }); - }); - - describe('generateResponse', () => { - it('should return extraction response on first call with no theming info', () => { - const g = createGuidance({ - workflow: {steps: [], extractionInstructions: 'Extract colors and fonts from user input.'}, - }); - const result = generateResponse(g, {collectedAnswers: {}}); - expect(result).to.include('Extract User-Provided Theming Information'); - expect(result).to.include('Extract colors and fonts from user input.'); - expect(result).to.include('USER-FACING RESPONSE'); - }); - - it('should show ready to implement when all required questions answered', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: true}], - workflow: {steps: [], preImplementationChecklist: '- Item 1\n- Item 2'}, - }); - const result = generateResponse(g, { - collectedAnswers: {q1: '#000000', colors: [{hex: '#000000'}], colorMapping: {text: '#000000', bg: '#FFFFFF'}}, - questionsAsked: ['q1'], - }); - expect(result).to.include('Ready to Implement'); - expect(result).to.include('MANDATORY PRE-IMPLEMENTATION CHECKLIST'); - expect(result).to.include('Item 1'); - }); - - it('should show warning when required questions not all answered', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: true}], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [{hex: '#000000'}]}, - questionsAsked: ['q1'], - }); - expect(result).to.include('WARNING'); - expect(result).to.include('Not all required questions have been answered'); - expect(result).to.include('still need answers'); - }); - - it('should use empty info when context has no collectedAnswers', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - }); - const result = generateResponse(g, {questionsAsked: ['q1']}); - expect(result).to.include('USER-FACING RESPONSE'); - expect(result).not.to.include("Information You've Provided"); - }); - - it('should show empty workflow message when colors and fonts arrays are empty', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [], fonts: []}, - questionsAsked: [], - }); - expect(result).to.include('Following the theming workflow'); - expect(result).to.include('I need a few clarifications before implementing'); - }); - - it('should include otherInfo from non-color non-font keys', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [], spacing: {desktop: 8}, brand: 'acme'}, - questionsAsked: [], - }); - expect(result).to.include('Other Information'); - expect(result).to.include('spacing:'); - expect(result).to.include('brand: acme'); - }); - - it('should skip colorMapping and question keys in otherInfo', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: { - colors: [], - colorMapping: {lightText: '#000', lightBackground: '#FFF'}, - questionsAsked: ['q1'], - brand: 'acme', - }, - questionsAsked: [], - }); - expect(result).to.include('brand: acme'); - expect(result).not.to.include('colorMapping:'); - expect(result).not.to.include('questionsAsked:'); - }); - - it('should extract color from color-like keys (accentColor, primaryColor)', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [], accentColor: {hex: '#635BFF', type: 'accent'}}, - questionsAsked: [], - }); - expect(result).to.include("Information You've Provided"); - expect(result).to.include('#635BFF'); - }); - - it('should extract font from font-like keys (headingFont, bodyFont)', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Font?', category: 'typography', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {fonts: [], headingFont: {name: 'Sohne', type: 'title'}}, - questionsAsked: [], - }); - expect(result).to.include("Information You've Provided"); - expect(result).to.include('Sohne'); - }); - - it('should extract font without type from font-like keys', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Font?', category: 'typography', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {fonts: [], bodyFont: {name: 'Arial'}}, - questionsAsked: [], - }); - expect(result).to.include("Information You've Provided"); - expect(result).to.include('Arial'); - }); - - it('should show singular remaining when exactly one more question', () => { - const g = createGuidance({ - questions: [ - {id: 'q1', question: 'Colors?', category: 'colors', required: true}, - {id: 'q2', question: 'Font?', category: 'typography', required: false}, - {id: 'q3', question: 'Dark?', category: 'general', required: false}, - {id: 'q4', question: 'Spacing?', category: 'general', required: false}, - {id: 'q5', question: 'Radius?', category: 'general', required: false}, - ], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [{hex: '#000'}], q1: 'done'}, - questionsAsked: ['q1'], - }); - expect(result).to.match(/1 more question\b/); - expect(result).not.to.include('1 more questions'); - }); - - it('should show plural remaining when multiple more questions', () => { - const g = createGuidance({ - questions: [ - {id: 'q1', question: 'Colors?', category: 'colors', required: true}, - {id: 'q2', question: 'Font?', category: 'typography', required: false}, - {id: 'q3', question: 'Dark mode?', category: 'general', required: false}, - {id: 'q4', question: 'Spacing?', category: 'general', required: false}, - {id: 'q5', question: 'Radius?', category: 'general', required: false}, - {id: 'q6', question: 'Shadows?', category: 'general', required: false}, - ], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [{hex: '#000'}], q1: 'done'}, - questionsAsked: ['q1'], - }); - expect(result).to.match(/[2-9] more questions\b/); - }); - - it('should handle color-like keys with hex undefined without error', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {colors: [], accentColor: {hex: undefined, type: 'primary'}}, - questionsAsked: [], - }); - expect(result).to.include('USER-FACING RESPONSE'); - }); - - it('should handle font-like keys with name undefined without error', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Font?', category: 'typography', required: false}], - }); - const result = generateResponse(g, { - collectedAnswers: {fonts: [], headingFont: {name: undefined, type: 'title'}}, - questionsAsked: [], - }); - expect(result).to.include('USER-FACING RESPONSE'); - }); - - it('should include validation instructions when guidance has validation', () => { - const g = createGuidance({ - questions: [{id: 'q1', question: 'Colors?', category: 'colors', required: false}], - validation: { - colorValidation: 'Check contrast ratios.', - fontValidation: 'Verify font availability.', - generalValidation: 'Validate other inputs.', - requirements: 'Always validate before implementing.', - }, - }); - const result = generateResponse(g, { - collectedAnswers: {colors: []}, - questionsAsked: [], - }); - expect(result).to.include('MANDATORY: Input Validation'); - expect(result).to.include('Color Combination Validation'); - expect(result).to.include('Check contrast ratios'); - expect(result).to.include('Font Validation'); - expect(result).to.include('Verify font availability'); - expect(result).to.include('General Input Validation'); - expect(result).to.include('Validate other inputs'); - expect(result).to.include('IMPORTANT'); - expect(result).to.include('Always validate before implementing'); - }); - }); -}); diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/theming-store.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/theming-store.test.ts deleted file mode 100644 index e956d2f53..000000000 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/site-theming/theming-store.test.ts +++ /dev/null @@ -1,624 +0,0 @@ -/* - * Copyright (c) 2025, Salesforce, Inc. - * SPDX-License-Identifier: Apache-2 - * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 - */ - -import {expect} from 'chai'; -import {existsSync, mkdirSync, writeFileSync, rmSync, copyFileSync} from 'node:fs'; -import path from 'node:path'; -import {tmpdir} from 'node:os'; -import {createRequire} from 'node:module'; -import {siteThemingStore} from '../../../../src/tools/storefrontnext/site-theming/theming-store.js'; - -const require = createRequire(import.meta.url); -const packageRoot = path.dirname(require.resolve('@salesforce/b2c-dx-mcp/package.json')); -const defaultContentDir = path.join(packageRoot, 'content', 'site-theming'); - -describe('tools/storefrontnext/site-theming/theming-store', () => { - let testDir: string; - let originalThemingFiles: string | undefined; - - beforeEach(() => { - testDir = path.join(tmpdir(), `b2c-theming-store-test-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`); - mkdirSync(testDir, {recursive: true}); - originalThemingFiles = process.env.THEMING_FILES; - }); - - afterEach(() => { - process.env.THEMING_FILES = originalThemingFiles; - if (existsSync(testDir)) { - rmSync(testDir, {recursive: true, force: true}); - } - }); - - describe('initialize', () => { - it('should load default theming files from package content', () => { - siteThemingStore.initialize(testDir); - - const keys = siteThemingStore.getKeys(); - expect(keys).to.include('theming-questions'); - expect(keys).to.include('theming-validation'); - expect(keys).to.include('theming-accessibility'); - }); - - it('should load custom file via THEMING_FILES env', () => { - const customPath = path.join(testDir, 'custom-theming.md'); - writeFileSync( - customPath, - `# Custom Theming - -## ⚠️ CRITICAL: Test Rule -Test content for custom theming file. - -### What TO Change: -- custom-color - -### What NOT to Change: -- custom-layout - -What are the exact hex color values?`, - 'utf8', - ); - - process.env.THEMING_FILES = JSON.stringify([{key: 'custom-theming', path: customPath}]); - - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('custom-theming')).to.be.true; - const guidance = siteThemingStore.get('custom-theming'); - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.metadata.fileName).to.equal('custom-theming.md'); - expect(guidance!.guidelines.length).to.be.greaterThan(0); - expect(guidance!.rules.length).to.be.greaterThan(0); - }); - - it('should resolve relative paths from workspace root', () => { - const customPath = path.join(testDir, 'relative-theming.md'); - writeFileSync( - customPath, - `# Relative Theming -## ⚠️ CRITICAL: Relative -Test. -### What TO Change: -- color -### What NOT to Change: -- layout`, - 'utf8', - ); - - const relativePath = path.relative(testDir, customPath); - process.env.THEMING_FILES = JSON.stringify([{key: 'relative-theming', path: relativePath}]); - - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('relative-theming')).to.be.true; - }); - }); - - describe('get and getKeys', () => { - beforeEach(() => { - siteThemingStore.initialize(testDir); - }); - - it('should return guidance for existing key', () => { - const guidance = siteThemingStore.get('theming-questions'); - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.questions).to.be.an('array'); - expect(guidance!.guidelines).to.be.an('array'); - expect(guidance!.rules).to.be.an('array'); - expect(guidance!.metadata).to.have.property('filePath'); - expect(guidance!.metadata).to.have.property('fileName'); - }); - - it('should return undefined for non-existent key', () => { - expect(siteThemingStore.get('non-existent')).to.be.undefined; - }); - - it('should return all loaded keys from getKeys', () => { - const keys = siteThemingStore.getKeys(); - expect(keys).to.be.an('array'); - expect(keys.length).to.be.greaterThanOrEqual(3); - }); - }); - - describe('loadFile', () => { - it('should parse workflow section from markdown', () => { - const filePath = path.join(testDir, 'workflow-test.md'); - const content = [ - '# Workflow Test', - '', - '## 🔄 WORKFLOW', - '1. First step', - '2. Second step', - '', - '### 📝 EXTRACTION', - 'Extract color values from user input.', - '', - '### ✅ PRE-IMPLEMENTATION', - 'Verify all colors meet WCAG AA.', - ].join('\n'); - writeFileSync(filePath, content, 'utf8'); - - siteThemingStore.loadFile('workflow-test', filePath); - const guidance = siteThemingStore.get('workflow-test'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.metadata.fileName).to.equal('workflow-test.md'); - // Workflow is parsed when steps, extraction, or checklist exist - if (guidance!.workflow) { - expect(guidance!.workflow!.steps).to.be.an('array'); - if (guidance!.workflow!.steps!.length > 0) { - expect(guidance!.workflow!.steps).to.include('First step'); - } - if (guidance!.workflow!.extractionInstructions) { - expect(guidance!.workflow!.extractionInstructions).to.include('Extract color values'); - } - if (guidance!.workflow!.preImplementationChecklist) { - expect(guidance!.workflow!.preImplementationChecklist).to.include('WCAG AA'); - } - } - }); - - it('should not add validation when section has no sub-sections', () => { - const filePath = path.join(testDir, 'validation-empty.md'); - writeFileSync( - filePath, - `# Validation Empty -## ✅ VALIDATION -No validation sub-sections here. -### What TO Change: -- opacity -### What NOT to Change: -- display`, - 'utf8', - ); - - siteThemingStore.loadFile('validation-empty', filePath); - const guidance = siteThemingStore.get('validation-empty'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.validation).to.be.undefined; - }); - - it('should return undefined validation when section has no A/B/C/IMPORTANT sub-sections', () => { - const filePath = path.join(testDir, 'validation-no-subsections.md'); - writeFileSync( - filePath, - `# Validation No Subsections -## ✅ VALIDATION -Only plain text, no A. Color, B. Font, C. General, or IMPORTANT.`, - 'utf8', - ); - - siteThemingStore.loadFile('validation-no-subsections', filePath); - const guidance = siteThemingStore.get('validation-no-subsections'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.validation).to.be.undefined; - }); - - it('should generate color mapping question when content has brand vs accent', () => { - const filePath = path.join(testDir, 'color-mapping-q.md'); - writeFileSync( - filePath, - `# Color Mapping -## ⚠️ CRITICAL: Colors -Ask for clarification on color type mapping. Use exact hex. Primary vs secondary, brand vs accent. -### What TO Change: -- color -- background-color -### What NOT to Change: -- margin`, - 'utf8', - ); - - siteThemingStore.loadFile('color-mapping-q', filePath); - const guidance = siteThemingStore.get('color-mapping-q'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const mappingQ = guidance!.questions.find((q) => q.question.includes('primary vs secondary')); - expect(mappingQ, 'mappingQ must be defined').to.not.be.undefined; - }); - - it('should parse validation section from markdown', () => { - const filePath = path.join(testDir, 'validation-test.md'); - const content = [ - '# Validation Test', - '', - '## ✅ VALIDATION', - '', - '### A. Color Combination Validation', - 'Check contrast ratios for all color combinations.', - '', - '### B. Font Validation', - 'Verify font availability.', - '', - '### C. General Input Validation', - 'General validation rules.', - '', - '### IMPORTANT', - 'All validations must pass.', - ].join('\n'); - writeFileSync(filePath, content, 'utf8'); - - siteThemingStore.loadFile('validation-test', filePath); - const guidance = siteThemingStore.get('validation-test'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.metadata.fileName).to.equal('validation-test.md'); - // Validation is parsed when color, font, general, or requirements exist - if (guidance!.validation) { - if (guidance!.validation!.colorValidation) { - expect(guidance!.validation!.colorValidation).to.include('contrast ratios'); - } - if (guidance!.validation!.fontValidation) { - expect(guidance!.validation!.fontValidation).to.include('font availability'); - } - if (guidance!.validation!.generalValidation) { - expect(guidance!.validation!.generalValidation).to.include('General validation'); - } - if (guidance!.validation!.requirements) { - expect(guidance!.validation!.requirements).to.include('All validations'); - } - } - }); - - it('should throw when file does not exist', () => { - expect(() => siteThemingStore.loadFile('missing', path.join(testDir, 'does-not-exist.md'))).to.throw( - /File not found|Failed to load/, - ); - }); - - it('should parse file without workflow or validation sections', () => { - const filePath = path.join(testDir, 'minimal.md'); - writeFileSync( - filePath, - `# Minimal Theming -No workflow or validation sections. -## ⚠️ CRITICAL: Test -Some critical content. -### What TO Change: -- color -### What NOT to Change: -- layout`, - 'utf8', - ); - - siteThemingStore.loadFile('minimal', filePath); - const guidance = siteThemingStore.get('minimal'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.guidelines).to.have.lengthOf.at.least(1); - expect(guidance!.rules).to.have.lengthOf.at.least(1); - }); - - it('should generate fallback questions when no questions extracted', () => { - const filePath = path.join(testDir, 'color-only.md'); - writeFileSync( - filePath, - `# Color Only -Content about color theming. No explicit questions. -## ⚠️ CRITICAL: Colors -Use exact hex values. -### What TO Change: -- background-color -### What NOT to Change: -- margin`, - 'utf8', - ); - - siteThemingStore.loadFile('color-only', filePath); - const guidance = siteThemingStore.get('color-only'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.questions.length).to.be.greaterThan(0); - const colorQ = guidance!.questions.find((q) => q.category === 'colors'); - expect(colorQ, 'colorQ must be defined').to.not.be.undefined; - }); - - it('should generate font fallback question when content has font', () => { - const filePath = path.join(testDir, 'font-only.md'); - writeFileSync( - filePath, - `# Font Only -Typography and font styling. No workflow. -### What TO Change: -- font-size -### What NOT to Change: -- width`, - 'utf8', - ); - - siteThemingStore.loadFile('font-only', filePath); - const guidance = siteThemingStore.get('font-only'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const fontQ = guidance!.questions.find((q) => q.category === 'typography'); - expect(fontQ, 'fontQ must be defined').to.not.be.undefined; - }); - - it('should handle THEMING_FILES with non-existent path', () => { - process.env.THEMING_FILES = JSON.stringify([{key: 'missing-env', path: 'does-not-exist.md'}]); - - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('missing-env')).to.be.false; - }); - - it('should handle THEMING_FILES with invalid JSON', () => { - const customPath = path.join(testDir, 'valid.md'); - writeFileSync(customPath, '# Valid\n### What TO Change:\n- x\n### What NOT to Change:\n- y', 'utf8'); - process.env.THEMING_FILES = 'invalid-json'; - - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('valid')).to.be.false; - }); - - it('should resolve absolute paths in THEMING_FILES', () => { - const customPath = path.join(testDir, 'absolute-theming.md'); - writeFileSync( - customPath, - `# Absolute Path Test -## ⚠️ CRITICAL: Test -Content. -### What TO Change: -- color -### What NOT to Change: -- layout`, - 'utf8', - ); - - process.env.THEMING_FILES = JSON.stringify([{key: 'absolute-theming', path: customPath}]); - - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('absolute-theming')).to.be.true; - }); - - it('should extract and merge questions from content lines ending with ?', () => { - const filePath = path.join(testDir, 'questions-extracted.md'); - writeFileSync( - filePath, - `# Questions Test -## ⚠️ CRITICAL: Use exact hex -Use exact hex code values. -### What TO Change: -- color -- background-color -### What NOT to Change: -- margin - -- What are your primary brand colors? -- What font family for headings and body? -- Do you need dark mode support?`, - 'utf8', - ); - - siteThemingStore.loadFile('questions-extracted', filePath); - const guidance = siteThemingStore.get('questions-extracted'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - expect(guidance!.questions.length).to.be.greaterThan(0); - const hasColorQ = guidance!.questions.some((q) => q.question.includes('brand colors')); - const hasFontQ = guidance!.questions.some((q) => q.question.includes('font family')); - const hasGeneralQ = guidance!.questions.some((q) => q.question.includes('dark mode')); - expect(hasColorQ || hasFontQ || hasGeneralQ).to.be.true; - }); - - it('should handle THEMING_FILES path that exists but cannot be read', () => { - const subDir = path.join(testDir, 'subdir'); - mkdirSync(subDir, {recursive: true}); - process.env.THEMING_FILES = JSON.stringify([{key: 'dir-as-file', path: subDir}]); - - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('dir-as-file')).to.be.false; - }); - - it('should generate layout question when content allows layout changes', () => { - const filePath = path.join(testDir, 'layout-changes.md'); - writeFileSync( - filePath, - `# Layout Test -## ⚠️ CRITICAL: Layout -Layout changes are allowed when explicitly requested. -### What TO Change: -- color -### What NOT to Change: -- position - -When layout modifications are needed, they should be explicitly requested by the user.`, - 'utf8', - ); - - siteThemingStore.loadFile('layout-changes', filePath); - const guidance = siteThemingStore.get('layout-changes'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const layoutQ = guidance!.questions.find((q) => q.category === 'general' && q.question.includes('layout')); - expect(layoutQ, 'layoutQ must be defined').to.not.be.undefined; - }); - - it('should generate font question for headings and body when content has font usage', () => { - const filePath = path.join(testDir, 'font-usage.md'); - writeFileSync( - filePath, - `# Font Usage -## ⚠️ CRITICAL: Typography -Use exact font name. Font apply to headings and body. -### What TO Change: -- font-size -- font-weight -### What NOT to Change: -- width`, - 'utf8', - ); - - siteThemingStore.loadFile('font-usage', filePath); - const guidance = siteThemingStore.get('font-usage'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const fontQ = guidance!.questions.find( - (q) => q.category === 'typography' && q.question.includes('headings and body'), - ); - expect(fontQ, 'fontQ must be defined').to.not.be.undefined; - }); - - it('should generate color questions for color combinations and dark/light when content has both', () => { - const filePath = path.join(testDir, 'color-combos.md'); - writeFileSync( - filePath, - `# Color Combos -## ⚠️ CRITICAL: Colors -Propose color combinations. Use exact hex. Dark and light themes. -### What TO Change: -- color -- background-color -### What NOT to Change: -- margin`, - 'utf8', - ); - - siteThemingStore.loadFile('color-combos', filePath); - const guidance = siteThemingStore.get('color-combos'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const primaryQ = guidance!.questions.find((q) => q.question.includes('primary actions')); - const hoverQ = guidance!.questions.find((q) => q.question.includes('hover state')); - const darkLightQ = guidance!.questions.find((q) => q.question.includes('light and dark')); - expect(primaryQ || hoverQ || darkLightQ).to.exist; - }); - - it('should generate primary/hover questions when content has color combinations', () => { - const filePath = path.join(testDir, 'color-combos-only.md'); - writeFileSync( - filePath, - `# Color Combos Only -## ⚠️ CRITICAL: Colors -Propose color combinations for buttons and links. -### What TO Change: -- color -- background-color -### What NOT to Change: -- margin`, - 'utf8', - ); - - siteThemingStore.loadFile('color-combos-only', filePath); - const guidance = siteThemingStore.get('color-combos-only'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const primaryQ = guidance!.questions.find((q) => q.question.includes('primary actions')); - const hoverQ = guidance!.questions.find((q) => q.question.includes('hover state')); - expect(primaryQ, 'primaryQ must be defined').to.not.be.undefined; - expect(hoverQ, 'hoverQ must be defined').to.not.be.undefined; - }); - - it('should generate font question for Google Fonts when content has font availability', () => { - const filePath = path.join(testDir, 'font-availability.md'); - writeFileSync( - filePath, - `# Font Availability -## ⚠️ CRITICAL: Fonts -Use exact font name. Check font availability and Google Fonts. -### What TO Change: -- font-size -### What NOT to Change: -- width`, - 'utf8', - ); - - siteThemingStore.loadFile('font-availability', filePath); - const guidance = siteThemingStore.get('font-availability'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const fontQ = guidance!.questions.find((q) => q.question.includes('Google Fonts')); - expect(fontQ, 'fontQ must be defined').to.not.be.undefined; - }); - - it('should assign required to first extracted color/font question when no generated questions', () => { - const filePath = path.join(testDir, 'extracted-only.md'); - writeFileSync( - filePath, - `# Extracted Only -## 📋 Specification -Follow user specs exactly. -### What TO Change: -- opacity -### What NOT to Change: -- display - -- What are your primary brand colors? -- What font family for headings?`, - 'utf8', - ); - - siteThemingStore.loadFile('extracted-only', filePath); - const guidance = siteThemingStore.get('extracted-only'); - - expect(guidance, 'guidance must be defined').to.not.be.undefined; - const colorQ = guidance!.questions.find((q) => q.question.includes('brand colors')); - const fontQ = guidance!.questions.find((q) => q.question.includes('font family')); - expect(colorQ?.required).to.be.true; - expect(fontQ?.required).to.be.true; - }); - - it('should skip re-initialization when same root', () => { - siteThemingStore.initialize(testDir); - const keysFirst = siteThemingStore.getKeys(); - - siteThemingStore.initialize(testDir); - const keysSecond = siteThemingStore.getKeys(); - - expect(keysFirst).to.deep.equal(keysSecond); - }); - - it('should clear and re-load when root changes', () => { - delete process.env.THEMING_FILES; - const otherDir = path.join(tmpdir(), `b2c-theming-other-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`); - mkdirSync(otherDir, {recursive: true}); - try { - const customPath = path.join(testDir, 'first-root.md'); - writeFileSync(customPath, '# First\n### What TO Change:\n- x\n### What NOT to Change:\n- y', 'utf8'); - process.env.THEMING_FILES = JSON.stringify([{key: 'first-root', path: path.relative(testDir, customPath)}]); - siteThemingStore.initialize(testDir); - - expect(siteThemingStore.has('first-root')).to.be.true; - - const customPath2 = path.join(otherDir, 'second-root.md'); - writeFileSync(customPath2, '# Second\n### What TO Change:\n- x\n### What NOT to Change:\n- y', 'utf8'); - process.env.THEMING_FILES = JSON.stringify([{key: 'second-root', path: customPath2}]); - siteThemingStore.initialize(otherDir); - - expect(siteThemingStore.has('first-root')).to.be.false; - expect(siteThemingStore.has('second-root')).to.be.true; - } finally { - if (existsSync(otherDir)) { - rmSync(otherDir, {recursive: true, force: true}); - } - } - }); - - it('should log and continue when default file fails to load', () => { - const fakeContentDir = path.join(testDir, 'fake-content', 'site-theming'); - mkdirSync(fakeContentDir, {recursive: true}); - copyFileSync( - path.join(defaultContentDir, 'theming-validation.md'), - path.join(fakeContentDir, 'theming-validation.md'), - ); - copyFileSync( - path.join(defaultContentDir, 'theming-accessibility.md'), - path.join(fakeContentDir, 'theming-accessibility.md'), - ); - mkdirSync(path.join(fakeContentDir, 'theming-questions.md'), {recursive: true}); - - siteThemingStore.initialize(testDir, {contentDirOverride: fakeContentDir}); - - expect(siteThemingStore.has('theming-validation')).to.be.true; - expect(siteThemingStore.has('theming-accessibility')).to.be.true; - expect(siteThemingStore.has('theming-questions')).to.be.false; - }); - }); -}); diff --git a/packages/b2c-tooling-sdk/data/tooling/index.json b/packages/b2c-tooling-sdk/data/tooling/index.json index f71aeb2df..448855eeb 100644 --- a/packages/b2c-tooling-sdk/data/tooling/index.json +++ b/packages/b2c-tooling-sdk/data/tooling/index.json @@ -1,6 +1,6 @@ { "version": "2.0.0", - "generatedAt": "2026-08-20T00:18:13.543Z", + "generatedAt": "2026-08-20T01:15:39.329Z", "entries": [ { "id": "cli-account-manager", @@ -452,15 +452,6 @@ "headings": "Credentials • `dw.json` (Recommended) {#dw-json} • `.env` File {#env-file} • MRT Credentials (`~/.mobify`) {#mrt-credentials} • Per-call Project Context {#project-directory} • Configuration Priority • Toolset Selection • Auto-Discovery (Default) • Manual Selection • Individual Tool Selection • Logging • Telemetry • MCP Server Flags Reference {#mcp-server-flags} • Documentation Tools Restriction • Environment Variables Reference {#environment-variables-reference} • MCP Server Environment Variables {#mcp-server-environment-variables} • Next Steps", "preview": "Configure the B2C DX MCP Server with credentials, flags, environment variables, and toolset selection." }, - { - "id": "mcp-figma-tools-setup", - "title": "Figma-to-Component Tools Setup", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/figma-tools-setup.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/figma-tools-setup.md", - "headings": "Overview • Figma MCP Setup • Figma Design File • Verification • Related Documentation", - "preview": "Prerequisites and setup for Figma-to-component tools (workflow orchestrator, generate component, map tokens)." - }, { "id": "mcp-index", "title": "MCP Server", @@ -551,67 +542,13 @@ "headings": "scapi_schemas_list • Authentication • Parameters • Usage • See Also", "preview": "List or fetch SCAPI schema metadata and OpenAPI specs for standard and custom APIs." }, - { - "id": "mcp-tools-sfnext-add-page-designer-decorator", - "title": "sfnext_add_page_designer_decorator", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-add-page-designer-decorator.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-add-page-designer-decorator.md", - "headings": "Overview • Prerequisites • Parameters • Conversation Context (Interactive Mode) • Operation Modes • Component Discovery • Usage Examples • Output • Related Tools • See Also", - "preview": "Add Page Designer decorators to React components for Storefront Next to make them available in Page Designer." - }, - { - "id": "mcp-tools-sfnext-analyze-component", - "title": "sfnext_analyze_component", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-analyze-component.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-analyze-component.md", - "headings": "Overview • Prerequisites • Parameters • Discovered Component Schema • Usage Examples • With Figma design URL • With design code already fetched • Agent workflow note • Output • Related Tools • See Also", - "preview": "Analyze design and discovered components to recommend REUSE, EXTEND, or CREATE strategy." - }, - { - "id": "mcp-tools-sfnext-configure-theme", - "title": "sfnext_configure_theme", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-configure-theme.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-configure-theme.md", - "headings": "Overview • Prerequisites • Custom Theming Files • Parameters • Conversation Context • Workflow • Usage Examples • Output • Related Tools • See Also", - "preview": "Get theming guidelines, guided questions, and WCAG color contrast validation for Storefront Next." - }, - { - "id": "mcp-tools-sfnext-get-guidelines", - "title": "sfnext_get_guidelines", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-get-guidelines.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-get-guidelines.md", - "headings": "Overview • Parameters • Available Sections • Usage Examples • Default (Comprehensive Guidelines) • Single Section • Multiple Related Sections • All Sections • Output • Related Tools • See Also", - "preview": "Get Storefront Next development guidelines and best practices for React Server Components, data loading, and framework constraints." - }, - { - "id": "mcp-tools-sfnext-match-tokens-to-theme", - "title": "sfnext_match_tokens_to_theme", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-match-tokens-to-theme.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-match-tokens-to-theme.md", - "headings": "Overview • Prerequisites • Parameters • Figma Token Schema • Usage Examples • Output • Related Tools • See Also", - "preview": "Match Figma design tokens to existing theme tokens in app.css with confidence scores and suggestions." - }, - { - "id": "mcp-tools-sfnext-start-figma-workflow", - "title": "sfnext_start_figma_workflow", - "category": "tooling", - "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-start-figma-workflow.html", - "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/tools/sfnext-start-figma-workflow.md", - "headings": "Overview • Prerequisites • Parameters • Supported Figma URL Formats • Usage Examples • Basic Workflow Start • Custom Workflow File • Full Homepage Implementation • Output • Related Tools • See Also", - "preview": "Workflow orchestrator for Figma-to-component conversion. Parses your Figma URL and guides you through design-to-component conversion." - }, { "id": "mcp-toolsets", "title": "Toolsets & Tools", "category": "tooling", "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets.html", "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/toolsets.md", - "headings": "CARTRIDGES • DIAGNOSTICS • MRT • PWAV3 • SCAPI • STOREFRONTNEXT • STOREFRONTNEXT_DEPRECATED • Next Steps", + "headings": "CARTRIDGES • DIAGNOSTICS • MRT • PWAV3 • SCAPI • STOREFRONTNEXT • Next Steps", "preview": "Available toolsets and tools in the B2C DX MCP Server for SCAPI, CARTRIDGES, DIAGNOSTICS, MRT, PWAV3, and STOREFRONTNEXT development." }, { diff --git a/skills/b2c-cli/skills/b2c-config/SKILL.md b/skills/b2c-cli/skills/b2c-config/SKILL.md index 7c9e1cb24..76831707f 100644 --- a/skills/b2c-cli/skills/b2c-config/SKILL.md +++ b/skills/b2c-cli/skills/b2c-config/SKILL.md @@ -42,18 +42,19 @@ Without `-i`, an active primary instance wins. A root-level primary configuratio ### MCP Project Context -MCP tools that resolve project files or B2C/MRT configuration accept per-call `projectDirectory` and `configPath` arguments. This override is especially important for plugin installs, where the MCP process working directory may be the plugin directory rather than the open project. For each project-aware call, the MCP server: +Local MCP project tools accept `projectDirectory`. Tools that resolve B2C/MRT configuration accept the same flat `projectDirectory`, `configPath`, and `instanceName` arguments. This override is especially important for plugin installs, where the MCP process working directory may be the plugin directory rather than the open project. For each configuration-aware call, the MCP server: 1. Parses `.env` from `projectDirectory`. 2. Applies all supported B2C/MRT environment variables from that file. 3. Selects a `dw.json`-format configuration file in this order: per-call `configPath`; startup `--config` / `SFCC_CONFIG`; project `.env` `SFCC_CONFIG`; `${projectDirectory}/dw.json`; shared global default. 4. Resolves relative per-call `configPath` and project `.env` `SFCC_CONFIG` values from `projectDirectory`. -5. Continues through the normal CLI configuration sources, including registered plugin sources, MRT credentials, and `package.json`. -6. Resolves project-relative filesystem paths from the same root. +5. Selects `instanceName`, when supplied, from the primary file first and then the shared global `dw.json`, without changing either file. +6. Continues through the normal tooling configuration sources, including registered plugin sources, MRT credentials, and `package.json`. +7. Resolves specialized paths such as `cartridgeDirectory`, `buildDirectory`, and `outputDirectory` from the same root when relative. Project `.env` values are scoped to that MCP call so one project's environment does not leak into another. -The MCP `config_inspect` tool uses the same SDK resolver and registered CLI plugin configuration sources as `b2c setup inspect`. Use `projectDirectory` and/or `configPath` on the MCP call to make the comparison against the intended project and configuration file. +Each project/config-aware result includes compact `resolution` provenance. Session and watch start calls retain it, and their list tools expose it for later follow-up calls. The MCP `config_inspect` tool additionally returns the full source graph and uses the same SDK resolver and registered CLI plugin configuration sources as `b2c setup inspect`. Use `projectDirectory`, `configPath`, and/or `instanceName` to compare the intended project, file, and instance. ### `dw.json` Key Casing diff --git a/skills/b2c-cli/skills/b2c-debug/SKILL.md b/skills/b2c-cli/skills/b2c-debug/SKILL.md index ef3b95a5c..79a28a630 100644 --- a/skills/b2c-cli/skills/b2c-debug/SKILL.md +++ b/skills/b2c-cli/skills/b2c-debug/SKILL.md @@ -19,7 +19,7 @@ The CLI auto-discovers the target instance and credentials from `SFCC_*` environ Run `b2c setup inspect` to see the resolved configuration and which source provided each value (use `--json` for scripting, `--unmask` to reveal secrets). For precedence rules and troubleshooting, see the `b2c-cli:b2c-config` skill. -For MCP debugging, pass `projectDirectory` to `debug_start_session` whenever the MCP server may have been launched outside the project. The tool uses that root to load the project's `.env` and default `dw.json`; pass `configPath` to select a different `dw.json`-format file. Cartridge discovery and local/server source mapping default to `projectDirectory`; pass `cartridgeDirectory` only when the cartridges live under a different root. The MCP server controls its SDAPI client identity internally, so callers do not pass a debugger client ID. +For MCP debugging, pass `projectDirectory` to `debug_start_session` whenever the MCP server may have been launched outside the project. The tool uses that root to load the project's `.env` and default `dw.json`; pass `configPath` to select a different primary `dw.json`-format file and `instanceName` to select a named instance from the primary or shared default file. Cartridge discovery and local/server source mapping default to `projectDirectory`; pass `cartridgeDirectory` only when the cartridges live under a different root. The start call captures this information in `resolution`, which `debug_list_sessions` returns without requiring the caller to repeat it. The MCP server controls its SDAPI client identity internally, so callers do not pass a debugger client ID. ## Prerequisites diff --git a/skills/figma-to-sfnext-pagedesigner/skills/figma-to-sfnext-pagedesigner/references/PAGE-DESIGNER-SFN.md b/skills/figma-to-sfnext-pagedesigner/skills/figma-to-sfnext-pagedesigner/references/PAGE-DESIGNER-SFN.md index 9c17867e2..1d1c4af89 100644 --- a/skills/figma-to-sfnext-pagedesigner/skills/figma-to-sfnext-pagedesigner/references/PAGE-DESIGNER-SFN.md +++ b/skills/figma-to-sfnext-pagedesigner/skills/figma-to-sfnext-pagedesigner/references/PAGE-DESIGNER-SFN.md @@ -18,8 +18,6 @@ claude plugin install b2c-dx-mcp # MCP server (SCAPI schema discovery, MR claude plugin install storefront-next-figma # Figma design-kit sync workflows ``` -⚠️ **Do NOT use the deprecated `sfnext_*` MCP tools** (e.g. `sfnext_add_page_designer_decorator`). They predate Storefront Next 1.0 GA and are superseded by the `storefront-next` plugin above. - When uncertain about anything below, fetch the official docs (Section 7) — this feature area is young and moving. --- @@ -36,31 +34,38 @@ Page Designer (PD) is the visual editor in Business Manager where merchants buil 4. At runtime, the storefront renders PD pages via **prebuilt page manifests** served from the **MRT Data Store** (not assembled per shopper request), and/or the **`shopperExperience`** SCAPI client for page/content lookups. So there are two sync loops to keep straight: -- **Dev-time loop (you):** decorated components → generated metadata cartridge → deployed to B2C. Keeps the *palette of available components* in PD current. -- **Merchant-time loop (automatic):** merchant edits pages in PD → system job prebuilds manifests → pushed to MRT Data Store → storefront reads them. Keeps the *page content* current. + +- **Dev-time loop (you):** decorated components → generated metadata cartridge → deployed to B2C. Keeps the _palette of available components_ in PD current. +- **Merchant-time loop (automatic):** merchant edits pages in PD → system job prebuilds manifests → pushed to MRT Data Store → storefront reads them. Keeps the _page content_ current. --- ## 3. Developer workflow ### Author + Create/modify a React component, annotate with the PD decorators (`@Component` for component types, `@PageType` for page layouts with regions, `@Aspect` for dynamic page types like PDP/PLP templates). The `storefront-next` plugin skill has the current decorator API — follow it rather than guessing attribute schemas. ### Generate + deploy metadata + ```bash pnpm sfnext generate-cartridge # scans src/ decorators → JSON metadata in cartridge/cartridge/experience/ pnpm sfnext deploy-cartridge # uploads cartridge to B2C Commerce (reads dw.json / SDK config) ``` + Requires valid B2C credentials resolvable by the tooling SDK (`dw.json`, env vars, or CLI flags). ### Keep metadata in sync automatically (recommended once stable) + ```ts // config.ts export const GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH = true; // default: false ``` + With this on, every `pnpm sfnext push` (MRT deploy) first regenerates and redeploys the cartridge metadata — code on MRT and component palette in PD can't drift. ### Deploy the storefront itself + ```bash pnpm sfnext push -e -w ``` @@ -90,17 +95,17 @@ pnpm sfnext push -e -w ## 6. Vocabulary quick reference -| Term | Meaning | -|---|---| -| Page type | Layout definition with named regions merchants fill with components | -| Component type | Reusable building block with merchant-editable attributes | -| Region | Named slot in a page/component that accepts child components | -| Aspect / dynamic page | Template page driven by runtime attributes (e.g. PDP/PLP templates) | -| Decorators (`@Component`, `@PageType`, `@Aspect`) | TS annotations on React components that the CLI compiles into PD metadata | -| Generated cartridge | `cartridge/cartridge/experience/` JSON produced by `generate-cartridge` — never hand-edit; regenerate | -| Page manifest | Prebuilt PD page description stored in the MRT Data Store, read at render time | -| MRT Data Store | MRT-side storage syncing site preferences + PD manifests from B2C | -| `shopperExperience` | SCAPI client namespace for fetching PD pages/content in loaders | +| Term | Meaning | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Page type | Layout definition with named regions merchants fill with components | +| Component type | Reusable building block with merchant-editable attributes | +| Region | Named slot in a page/component that accepts child components | +| Aspect / dynamic page | Template page driven by runtime attributes (e.g. PDP/PLP templates) | +| Decorators (`@Component`, `@PageType`, `@Aspect`) | TS annotations on React components that the CLI compiles into PD metadata | +| Generated cartridge | `cartridge/cartridge/experience/` JSON produced by `generate-cartridge` — never hand-edit; regenerate | +| Page manifest | Prebuilt PD page description stored in the MRT Data Store, read at render time | +| MRT Data Store | MRT-side storage syncing site preferences + PD manifests from B2C | +| `shopperExperience` | SCAPI client namespace for fetching PD pages/content in loaders | --- From 178f439c66b7356d9114294ab754011dd0c427b4 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Wed, 19 Aug 2026 21:25:08 -0400 Subject: [PATCH 2/4] docs(mcp): remove retired Storefront Next sidebar --- docs/.vitepress/config.mts | 12 ------------ skills/b2c-cli/skills/b2c-docs/SKILL.md | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 854839287..387d1ccaa 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -212,18 +212,6 @@ const referenceSidebar = [ collapsed: true, items: [{text: 'Documentation Tools', link: '/mcp/tools/docs'}], }, - { - text: 'Storefront Next (deprecated)', - collapsed: true, - items: [ - {text: 'sfnext_get_guidelines', link: '/mcp/tools/sfnext-get-guidelines'}, - {text: 'sfnext_start_figma_workflow', link: '/mcp/tools/sfnext-start-figma-workflow'}, - {text: 'sfnext_analyze_component', link: '/mcp/tools/sfnext-analyze-component'}, - {text: 'sfnext_match_tokens_to_theme', link: '/mcp/tools/sfnext-match-tokens-to-theme'}, - {text: 'sfnext_add_page_designer_decorator', link: '/mcp/tools/sfnext-add-page-designer-decorator'}, - {text: 'sfnext_configure_theme', link: '/mcp/tools/sfnext-configure-theme'}, - ], - }, ], }, ]; diff --git a/skills/b2c-cli/skills/b2c-docs/SKILL.md b/skills/b2c-cli/skills/b2c-docs/SKILL.md index 393137cbf..863cdc81a 100644 --- a/skills/b2c-cli/skills/b2c-docs/SKILL.md +++ b/skills/b2c-cli/skills/b2c-docs/SKILL.md @@ -233,7 +233,7 @@ xmllint --schema "$(b2c docs schema catalog --path)" my-catalog.xml --noout | `commerce-api` | Commerce API (SCAPI/OCAPI) conceptual and how-to guides | `commerce-api/slas-passwordless-login-registration` | | `pwa-kit-managed-runtime` | PWA Kit and Managed Runtime (MRT) guides | `pwa-kit-managed-runtime/getting-started` | | `sfra` | Storefront Reference Architecture (SFRA) guides | `sfra/controllers-and-routes` | -| `sfnext` | Storefront Next (deprecated) guides | `sfnext/sfnext-get-started` | +| `sfnext` | Storefront Next guides | `sfnext/sfnext-get-started` | | `b2c-commerce` | General B2C Commerce platform guides | `b2c-commerce/business-manager-overview` | | `tooling` | B2C CLI reference, guides, MCP docs, SDK guidance, and VS Code extension docs | `guide-authentication`, `cli-jobs` | | `job-step` | Standard (system) job step catalog | `ImportCatalog`, `ExportCatalog`, `job-steps` | From 93f3111c53110635a45b729a5090e38e87286327 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Wed, 19 Aug 2026 21:39:18 -0400 Subject: [PATCH 3/4] test(mcp): align local checks with CI --- packages/b2c-dx-mcp/.c8rc.json | 2 +- packages/b2c-dx-mcp/package.json | 2 +- packages/b2c-dx-mcp/test/tools/adapter.test.ts | 17 ++++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/b2c-dx-mcp/.c8rc.json b/packages/b2c-dx-mcp/.c8rc.json index eb81059e5..46a26ec19 100644 --- a/packages/b2c-dx-mcp/.c8rc.json +++ b/packages/b2c-dx-mcp/.c8rc.json @@ -7,6 +7,6 @@ "check-coverage": true, "lines": 99, "functions": 96, - "branches": 94, + "branches": 90, "statements": 99 } diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index 2b9b92933..d1f409b57 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -87,7 +87,7 @@ "test": "c8 mocha --forbid-only --ignore \"test/e2e/**\" \"test/**/*.test.ts\"", "test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json --ignore \"test/e2e/**\" \"test/**/*.test.ts\"", "test:ci:win": "c8 --check-coverage=false mocha --forbid-only --reporter json --reporter-option output=test-results.json --ignore \"test/e2e/**\" \"test/**/*.test.ts\"", - "test:agent": "mocha --forbid-only --reporter min --ignore \"test/e2e/**\" \"test/**/*.test.ts\"", + "test:agent": "pnpm run pretest && mocha --forbid-only --reporter min --ignore \"test/e2e/**\" \"test/**/*.test.ts\"", "test:e2e": "mocha --forbid-only \"test/e2e/**/*.test.ts\"", "test:e2e:ci": "mocha --forbid-only --reporter json --reporter-option output=test-results-e2e.json \"test/e2e/**/*.test.ts\"", "coverage": "c8 report", diff --git a/packages/b2c-dx-mcp/test/tools/adapter.test.ts b/packages/b2c-dx-mcp/test/tools/adapter.test.ts index a69f75aa5..fdc952eae 100644 --- a/packages/b2c-dx-mcp/test/tools/adapter.test.ts +++ b/packages/b2c-dx-mcp/test/tools/adapter.test.ts @@ -11,7 +11,7 @@ import {Services} from '../../src/services.js'; import type {ToolExecutionContext} from '../../src/tools/adapter.js'; import type {ToolResult} from '../../src/utils/types.js'; import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth'; -import {resolveConfig} from '@salesforce/b2c-tooling-sdk/config'; +import {resolveConfig, type ConfigSourceInfo} from '@salesforce/b2c-tooling-sdk/config'; import {createMockResolvedConfig} from '../test-helpers.js'; // Create a mock services instance for testing @@ -343,12 +343,7 @@ describe('tools/adapter', () => { }); it('should describe the actual server fallback and attach compact resolution provenance', async () => { - const config = createMockResolvedConfig({ - hostname: 'sandbox.invalid', - instanceName: 'sandbox', - projectDirectory: '/server/project', - }); - config.sources = [ + const sources: ConfigSourceInfo[] = [ { fields: ['hostname', 'instanceName'], location: '/shared/dw.json', @@ -356,6 +351,14 @@ describe('tools/adapter', () => { scope: 'global', }, ]; + const config = { + ...createMockResolvedConfig({ + hostname: 'sandbox.invalid', + instanceName: 'sandbox', + projectDirectory: '/server/project', + }), + sources, + }; const services = new Services({ resolvedConfig: config, resolution: { From 441b4c352ff2b02446e27097302c24ce0d722ad2 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Wed, 19 Aug 2026 21:46:50 -0400 Subject: [PATCH 4/4] test(mcp): normalize project paths on Windows --- packages/b2c-dx-mcp/test/commands/mcp.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/b2c-dx-mcp/test/commands/mcp.test.ts b/packages/b2c-dx-mcp/test/commands/mcp.test.ts index 2b7d50c7a..660d3276d 100644 --- a/packages/b2c-dx-mcp/test/commands/mcp.test.ts +++ b/packages/b2c-dx-mcp/test/commands/mcp.test.ts @@ -533,8 +533,9 @@ describe('McpServerCommand', () => { } ).loadConfiguration({projectDirectory: '/per-call/project'}); - expect(config.values.projectDirectory).to.equal('/per-call/project'); - expect(config.values.workingDirectory).to.equal('/per-call/project'); + const expectedProjectDirectory = path.resolve('/per-call/project'); + expect(config.values.projectDirectory).to.equal(expectedProjectDirectory); + expect(config.values.workingDirectory).to.equal(expectedProjectDirectory); }); it('should load SFCC_CONFIG from the per-call project .env', async () => {