@salesforce/b2c-dx-mcp 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -370
- package/content/pwav3/components.md +400 -0
- package/content/pwav3/config.md +124 -0
- package/content/pwav3/data-fetching.md +213 -0
- package/content/pwav3/extensibility.md +167 -0
- package/content/pwav3/i18n.md +214 -0
- package/content/pwav3/quick-reference.md +169 -0
- package/content/pwav3/routing.md +107 -0
- package/content/pwav3/state-management.md +193 -0
- package/content/pwav3/styling.md +248 -0
- package/content/pwav3/testing.md +124 -0
- package/content/site-theming/theming-accessibility.md +126 -0
- package/content/site-theming/theming-questions.md +208 -0
- package/content/site-theming/theming-validation.md +174 -0
- package/dist/commands/mcp.d.ts +3 -3
- package/dist/commands/mcp.js +7 -7
- package/dist/registry.js +1 -1
- package/dist/services.d.ts +15 -15
- package/dist/services.js +21 -14
- package/dist/tools/adapter.d.ts +2 -2
- package/dist/tools/adapter.js +2 -2
- package/dist/tools/cartridges/index.js +1 -6
- package/dist/tools/index.d.ts +1 -4
- package/dist/tools/index.js +1 -4
- package/dist/tools/mrt/index.js +4 -9
- package/dist/tools/pwav3/index.d.ts +12 -3
- package/dist/tools/pwav3/index.js +5 -63
- package/dist/tools/pwav3/pwa-kit-development-guidelines.d.ts +9 -0
- package/dist/tools/pwav3/pwa-kit-development-guidelines.js +151 -0
- package/dist/tools/scapi/index.d.ts +1 -1
- package/dist/tools/scapi/index.js +6 -1
- package/dist/tools/scapi/scapi-custom-api-scaffold.d.ts +60 -0
- package/dist/tools/scapi/scapi-custom-api-scaffold.js +175 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/figma-url-parser.d.ts +24 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/figma-url-parser.js +53 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/index.d.ts +42 -0
- package/dist/tools/storefrontnext/figma/figma-to-component/index.js +325 -0
- package/dist/tools/storefrontnext/figma/generate-component/decision.d.ts +40 -0
- package/dist/tools/storefrontnext/figma/generate-component/decision.js +312 -0
- package/dist/tools/storefrontnext/figma/generate-component/formatter.d.ts +9 -0
- package/dist/tools/storefrontnext/figma/generate-component/formatter.js +92 -0
- package/dist/tools/storefrontnext/figma/generate-component/index.d.ts +114 -0
- package/dist/tools/storefrontnext/figma/generate-component/index.js +98 -0
- package/dist/tools/storefrontnext/figma/map-tokens/css-parser.d.ts +71 -0
- package/dist/tools/storefrontnext/figma/map-tokens/css-parser.js +260 -0
- package/dist/tools/storefrontnext/figma/map-tokens/index.d.ts +61 -0
- package/dist/tools/storefrontnext/figma/map-tokens/index.js +234 -0
- package/dist/tools/storefrontnext/figma/map-tokens/token-matcher.d.ts +65 -0
- package/dist/tools/storefrontnext/figma/map-tokens/token-matcher.js +268 -0
- package/dist/tools/storefrontnext/index.d.ts +17 -0
- package/dist/tools/storefrontnext/index.js +10 -60
- package/dist/tools/storefrontnext/page-designer-decorator/analyzer.js +15 -0
- package/dist/tools/storefrontnext/page-designer-decorator/index.js +3 -3
- package/dist/tools/storefrontnext/{developer-guidelines.js → sfnext-development-guidelines.js} +3 -3
- package/dist/tools/storefrontnext/site-theming/color-contrast.d.ts +92 -0
- package/dist/tools/storefrontnext/site-theming/color-contrast.js +186 -0
- package/dist/tools/storefrontnext/site-theming/color-mapping.d.ts +16 -0
- package/dist/tools/storefrontnext/site-theming/color-mapping.js +131 -0
- package/dist/tools/storefrontnext/site-theming/guidance-merger.d.ts +11 -0
- package/dist/tools/storefrontnext/site-theming/guidance-merger.js +78 -0
- package/dist/tools/storefrontnext/site-theming/index.d.ts +14 -0
- package/dist/tools/storefrontnext/site-theming/index.js +122 -0
- package/dist/tools/storefrontnext/site-theming/response-builder.d.ts +16 -0
- package/dist/tools/storefrontnext/site-theming/response-builder.js +316 -0
- package/dist/tools/storefrontnext/site-theming/theming-store.d.ts +62 -0
- package/dist/tools/storefrontnext/site-theming/theming-store.js +410 -0
- package/dist/tools/storefrontnext/site-theming/types.d.ts +35 -0
- package/dist/tools/storefrontnext/site-theming/types.js +7 -0
- package/oclif.manifest.json +8 -5
- package/package.json +9 -6
- /package/content/{auth.md → sfnext/auth.md} +0 -0
- /package/content/{components.md → sfnext/components.md} +0 -0
- /package/content/{config.md → sfnext/config.md} +0 -0
- /package/content/{data-fetching.md → sfnext/data-fetching.md} +0 -0
- /package/content/{extensions.md → sfnext/extensions.md} +0 -0
- /package/content/{i18n.md → sfnext/i18n.md} +0 -0
- /package/content/{page-designer.md → sfnext/page-designer.md} +0 -0
- /package/content/{performance.md → sfnext/performance.md} +0 -0
- /package/content/{pitfalls.md → sfnext/pitfalls.md} +0 -0
- /package/content/{quick-reference.md → sfnext/quick-reference.md} +0 -0
- /package/content/{state-management.md → sfnext/state-management.md} +0 -0
- /package/content/{styling.md → sfnext/styling.md} +0 -0
- /package/content/{testing.md → sfnext/testing.md} +0 -0
- /package/dist/tools/storefrontnext/{developer-guidelines.d.ts → sfnext-development-guidelines.d.ts} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComponentAnalysisResult, GenerateComponentInput } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Formats the component generation recommendation for AI/developer.
|
|
4
|
+
*
|
|
5
|
+
* @param analysis - Component analysis result with REUSE/EXTEND/CREATE decision
|
|
6
|
+
* @param input - Original generate-component input (component name, etc.)
|
|
7
|
+
* @returns Formatted markdown string with recommendation, key differences, suggested approach, and next steps
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatRecommendation(analysis: ComponentAnalysisResult, input: GenerateComponentInput): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Formats the component generation recommendation for AI/developer.
|
|
8
|
+
*
|
|
9
|
+
* @param analysis - Component analysis result with REUSE/EXTEND/CREATE decision
|
|
10
|
+
* @param input - Original generate-component input (component name, etc.)
|
|
11
|
+
* @returns Formatted markdown string with recommendation, key differences, suggested approach, and next steps
|
|
12
|
+
*/
|
|
13
|
+
export function formatRecommendation(analysis, input) {
|
|
14
|
+
let output = '# Component Generation Recommendation\n\n';
|
|
15
|
+
// Decision and confidence
|
|
16
|
+
output += `**Decision:** ${analysis.action}\n`;
|
|
17
|
+
output += `**Confidence:** ${analysis.confidence}%\n\n`;
|
|
18
|
+
// Matched component (if any)
|
|
19
|
+
if (analysis.matchedComponent) {
|
|
20
|
+
output += `**Matched Component:**\n`;
|
|
21
|
+
output += `- \`${analysis.matchedComponent.name}\` at \`${analysis.matchedComponent.path}\`\n`;
|
|
22
|
+
output += `- Similarity: ${analysis.matchedComponent.similarity}%\n\n`;
|
|
23
|
+
}
|
|
24
|
+
// Recommendation
|
|
25
|
+
output += `**Recommendation:** ${analysis.recommendation}\n\n`;
|
|
26
|
+
// Key differences
|
|
27
|
+
if (analysis.differences && analysis.differences.length > 0) {
|
|
28
|
+
output += `**Key Differences:**\n`;
|
|
29
|
+
for (const [i, diff] of analysis.differences.entries()) {
|
|
30
|
+
output += `${i + 1}. ${diff}\n`;
|
|
31
|
+
}
|
|
32
|
+
output += '\n';
|
|
33
|
+
}
|
|
34
|
+
// Suggested approach
|
|
35
|
+
if (analysis.suggestedApproach) {
|
|
36
|
+
output += `## Suggested Approach\n\n${analysis.suggestedApproach}\n\n`;
|
|
37
|
+
}
|
|
38
|
+
// Next steps
|
|
39
|
+
output += formatNextSteps(analysis, input);
|
|
40
|
+
return output;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Formats next steps based on action type
|
|
44
|
+
*/
|
|
45
|
+
function formatNextSteps(analysis, input) {
|
|
46
|
+
let section = '## Next Steps\n\n';
|
|
47
|
+
switch (analysis.action) {
|
|
48
|
+
case 'CREATE': {
|
|
49
|
+
section += `Create new component: \`${input.componentName}\`\n\n`;
|
|
50
|
+
section += `**Implementation:**\n`;
|
|
51
|
+
section += `1. Create component file structure (index.tsx, types.ts if needed)\n`;
|
|
52
|
+
section += `2. Convert Figma code to StorefrontNext patterns:\n`;
|
|
53
|
+
section += ` - Use React Server Components by default (add 'use client' if needed)\n`;
|
|
54
|
+
section += ` - Replace inline styles with Tailwind classes\n`;
|
|
55
|
+
section += ` - Map colors/spacing to theme tokens\n`;
|
|
56
|
+
section += ` - Add proper TypeScript types and accessibility attributes\n`;
|
|
57
|
+
section += `3. Export component from index\n`;
|
|
58
|
+
if (analysis.matchedComponent) {
|
|
59
|
+
section += `4. Reference patterns from \`${analysis.matchedComponent.path}\` for consistency\n`;
|
|
60
|
+
}
|
|
61
|
+
section += '\n';
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'EXTEND': {
|
|
65
|
+
const strategy = analysis.extendStrategy || 'props';
|
|
66
|
+
section += `**Strategy:** ${strategy === 'props' ? 'Add new props' : strategy === 'variant' ? 'Add variant' : 'Composition'}\n\n`;
|
|
67
|
+
section += `1. Modify \`${analysis.matchedComponent?.path}\`\n`;
|
|
68
|
+
if (strategy === 'props') {
|
|
69
|
+
section += `2. Add new optional props to the interface\n`;
|
|
70
|
+
section += `3. Implement new prop behavior while maintaining backward compatibility\n`;
|
|
71
|
+
}
|
|
72
|
+
else if (strategy === 'variant') {
|
|
73
|
+
section += `2. Add new variant to existing variant definitions\n`;
|
|
74
|
+
section += `3. Apply variant styling using theme tokens\n`;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
section += `2. Create wrapper component that composes the base component\n`;
|
|
78
|
+
section += `3. Add additional elements/functionality in the wrapper\n`;
|
|
79
|
+
}
|
|
80
|
+
section += `4. Validate: ensure existing usages still work\n\n`;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'REUSE': {
|
|
84
|
+
section += `Import and use \`${analysis.matchedComponent?.name}\` from \`${analysis.matchedComponent?.path}\`.\n`;
|
|
85
|
+
section += `Customize through props and Tailwind classes to match the Figma design.\n\n`;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
section += '**Confirm before proceeding with implementation.**\n';
|
|
90
|
+
return section;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate component tool for Figma-to-component workflow.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes Figma design and discovered components to recommend REUSE, EXTEND, or CREATE strategy.
|
|
5
|
+
*
|
|
6
|
+
* @module tools/storefrontnext/figma/generate-component
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { McpTool } from '../../../../utils/index.js';
|
|
10
|
+
import type { Services } from '../../../../services.js';
|
|
11
|
+
/**
|
|
12
|
+
* A component discovered in the codebase that may match the Figma design.
|
|
13
|
+
*
|
|
14
|
+
* @property {string} path - Absolute file path to the component
|
|
15
|
+
* @property {string} name - Component name
|
|
16
|
+
* @property {number} similarity - Similarity score (0-100)
|
|
17
|
+
* @property {'name'|'structure'|'visual'} matchType - Type of match: 'name', 'structure', or 'visual'
|
|
18
|
+
* @property {string} code - Full source code of the component
|
|
19
|
+
*/
|
|
20
|
+
export interface SimilarComponent {
|
|
21
|
+
path: string;
|
|
22
|
+
name: string;
|
|
23
|
+
similarity: number;
|
|
24
|
+
matchType: 'name' | 'structure' | 'visual';
|
|
25
|
+
code: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const generateComponentSchema: z.ZodObject<{
|
|
28
|
+
figmaMetadata: z.ZodString;
|
|
29
|
+
figmaCode: z.ZodString;
|
|
30
|
+
componentName: z.ZodString;
|
|
31
|
+
discoveredComponents: z.ZodArray<z.ZodObject<{
|
|
32
|
+
path: z.ZodString;
|
|
33
|
+
name: z.ZodString;
|
|
34
|
+
similarity: z.ZodNumber;
|
|
35
|
+
matchType: z.ZodEnum<["name", "structure", "visual"]>;
|
|
36
|
+
code: z.ZodString;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
name: string;
|
|
39
|
+
code: string;
|
|
40
|
+
path: string;
|
|
41
|
+
similarity: number;
|
|
42
|
+
matchType: "name" | "structure" | "visual";
|
|
43
|
+
}, {
|
|
44
|
+
name: string;
|
|
45
|
+
code: string;
|
|
46
|
+
path: string;
|
|
47
|
+
similarity: number;
|
|
48
|
+
matchType: "name" | "structure" | "visual";
|
|
49
|
+
}>, "many">;
|
|
50
|
+
workspacePath: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, "strict", z.ZodTypeAny, {
|
|
52
|
+
figmaMetadata: string;
|
|
53
|
+
figmaCode: string;
|
|
54
|
+
componentName: string;
|
|
55
|
+
discoveredComponents: {
|
|
56
|
+
name: string;
|
|
57
|
+
code: string;
|
|
58
|
+
path: string;
|
|
59
|
+
similarity: number;
|
|
60
|
+
matchType: "name" | "structure" | "visual";
|
|
61
|
+
}[];
|
|
62
|
+
workspacePath?: string | undefined;
|
|
63
|
+
}, {
|
|
64
|
+
figmaMetadata: string;
|
|
65
|
+
figmaCode: string;
|
|
66
|
+
componentName: string;
|
|
67
|
+
discoveredComponents: {
|
|
68
|
+
name: string;
|
|
69
|
+
code: string;
|
|
70
|
+
path: string;
|
|
71
|
+
similarity: number;
|
|
72
|
+
matchType: "name" | "structure" | "visual";
|
|
73
|
+
}[];
|
|
74
|
+
workspacePath?: string | undefined;
|
|
75
|
+
}>;
|
|
76
|
+
export type GenerateComponentInput = z.infer<typeof generateComponentSchema>;
|
|
77
|
+
/**
|
|
78
|
+
* Result of component analysis recommending REUSE, EXTEND, or CREATE.
|
|
79
|
+
*
|
|
80
|
+
* @property {'CREATE'|'EXTEND'|'REUSE'} action - Recommended action: 'CREATE', 'EXTEND', or 'REUSE'
|
|
81
|
+
* @property {number} confidence - Confidence score (0-100)
|
|
82
|
+
* @property {{path: string, name: string, similarity: number}} [matchedComponent] - Best-matching component (if action is REUSE or EXTEND)
|
|
83
|
+
* @property {string[]} [differences] - Key differences between Figma design and matched component
|
|
84
|
+
* @property {string} recommendation - Human-readable recommendation text
|
|
85
|
+
* @property {string} [suggestedApproach] - Implementation guidance
|
|
86
|
+
* @property {'composition'|'props'|'variant'} [extendStrategy] - Strategy for EXTEND: 'props', 'variant', or 'composition'
|
|
87
|
+
*/
|
|
88
|
+
export interface ComponentAnalysisResult {
|
|
89
|
+
action: 'CREATE' | 'EXTEND' | 'REUSE';
|
|
90
|
+
confidence: number;
|
|
91
|
+
matchedComponent?: {
|
|
92
|
+
path: string;
|
|
93
|
+
name: string;
|
|
94
|
+
similarity: number;
|
|
95
|
+
};
|
|
96
|
+
differences?: string[];
|
|
97
|
+
recommendation: string;
|
|
98
|
+
suggestedApproach?: string;
|
|
99
|
+
extendStrategy?: 'composition' | 'props' | 'variant';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generates a component recommendation from Figma design and discovered components.
|
|
103
|
+
*
|
|
104
|
+
* @param input - Figma design data (metadata, code), component name, and discovered components
|
|
105
|
+
* @returns Formatted recommendation with REUSE/EXTEND/CREATE decision and implementation guidance, or error message on failure
|
|
106
|
+
*/
|
|
107
|
+
export declare function generateComponentRecommendation(input: GenerateComponentInput): string;
|
|
108
|
+
/**
|
|
109
|
+
* Creates the storefront_next_generate_component MCP tool.
|
|
110
|
+
*
|
|
111
|
+
* @param loadServices - Function that loads configuration and returns Services instance
|
|
112
|
+
* @returns MCP tool for component analysis and recommendation
|
|
113
|
+
*/
|
|
114
|
+
export declare function createGenerateComponentTool(loadServices: () => Services): McpTool;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Generate component tool for Figma-to-component workflow.
|
|
8
|
+
*
|
|
9
|
+
* Analyzes Figma design and discovered components to recommend REUSE, EXTEND, or CREATE strategy.
|
|
10
|
+
*
|
|
11
|
+
* @module tools/storefrontnext/figma/generate-component
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { createToolAdapter, textResult } from '../../../adapter.js';
|
|
15
|
+
import { analyzeComponentDifferences, determineAction } from './decision.js';
|
|
16
|
+
import { formatRecommendation } from './formatter.js';
|
|
17
|
+
const discoveredComponentSchema = z.object({
|
|
18
|
+
path: z.string().describe('Absolute file path to the component'),
|
|
19
|
+
name: z.string().describe('Component name'),
|
|
20
|
+
similarity: z.number().min(0).max(100).describe('Similarity score (0-100)'),
|
|
21
|
+
matchType: z.enum(['name', 'structure', 'visual']).describe('Type of match found'),
|
|
22
|
+
code: z.string().describe('Full source code of the component'),
|
|
23
|
+
});
|
|
24
|
+
export const generateComponentSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
figmaMetadata: z.string().describe('JSON string containing Figma design metadata (from mcp__figma__get_metadata)'),
|
|
27
|
+
figmaCode: z.string().describe('Generated React code from Figma (from mcp__figma__get_design_context)'),
|
|
28
|
+
componentName: z.string().describe('Suggested name for the component extracted from Figma design'),
|
|
29
|
+
discoveredComponents: z
|
|
30
|
+
.array(discoveredComponentSchema)
|
|
31
|
+
.describe('Array of similar components discovered using Glob/Grep/Read. Pass empty array if no similar components found.'),
|
|
32
|
+
workspacePath: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Optional workspace root path. Defaults to the MCP server project directory.'),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
function analyzeComponent(input) {
|
|
39
|
+
const similarComponents = input.discoveredComponents;
|
|
40
|
+
if (similarComponents.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
action: 'CREATE',
|
|
43
|
+
confidence: 95,
|
|
44
|
+
recommendation: `No similar components found in the codebase. Will create new component: ${input.componentName}`,
|
|
45
|
+
suggestedApproach: 'Create a new component following StorefrontNext patterns.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const topMatch = similarComponents[0];
|
|
49
|
+
const differences = analyzeComponentDifferences(topMatch, input.figmaCode, input.figmaMetadata);
|
|
50
|
+
const decision = determineAction(topMatch, differences);
|
|
51
|
+
return decision;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generates a component recommendation from Figma design and discovered components.
|
|
55
|
+
*
|
|
56
|
+
* @param input - Figma design data (metadata, code), component name, and discovered components
|
|
57
|
+
* @returns Formatted recommendation with REUSE/EXTEND/CREATE decision and implementation guidance, or error message on failure
|
|
58
|
+
*/
|
|
59
|
+
export function generateComponentRecommendation(input) {
|
|
60
|
+
try {
|
|
61
|
+
const analysis = analyzeComponent(input);
|
|
62
|
+
const recommendation = formatRecommendation(analysis, input);
|
|
63
|
+
return recommendation;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
67
|
+
return `# Error: Component Generation Failed\n\n${errorMessage}\n\nPlease check the input parameters and try again.`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates the storefront_next_generate_component MCP tool.
|
|
72
|
+
*
|
|
73
|
+
* @param loadServices - Function that loads configuration and returns Services instance
|
|
74
|
+
* @returns MCP tool for component analysis and recommendation
|
|
75
|
+
*/
|
|
76
|
+
export function createGenerateComponentTool(loadServices) {
|
|
77
|
+
return createToolAdapter({
|
|
78
|
+
name: 'storefront_next_generate_component',
|
|
79
|
+
description: 'Analyzes Figma design and discovered components to recommend component generation strategy. ' +
|
|
80
|
+
'Workflow: 1) Discover similar components using Glob/Grep/Read tools, ' +
|
|
81
|
+
'2) Call this tool with the discoveredComponents parameter, ' +
|
|
82
|
+
'3) Tool analyzes differences and recommends REUSE/EXTEND/CREATE action, ' +
|
|
83
|
+
'4) Tool provides formatted recommendation with code examples and workflow steps. ' +
|
|
84
|
+
'Call this tool AFTER retrieving Figma design data and discovering similar components.',
|
|
85
|
+
toolsets: ['STOREFRONTNEXT'],
|
|
86
|
+
isGA: false,
|
|
87
|
+
requiresInstance: false,
|
|
88
|
+
inputSchema: generateComponentSchema.shape,
|
|
89
|
+
async execute(args, context) {
|
|
90
|
+
return generateComponentRecommendation({
|
|
91
|
+
...args,
|
|
92
|
+
workspacePath: args.workspacePath ?? context.services.resolveWithProjectDirectory(),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
formatOutput: (output) => textResult(output),
|
|
96
|
+
}, loadServices);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design token extracted from theme CSS (app.css).
|
|
3
|
+
*
|
|
4
|
+
* @property {string} name - CSS custom property name (e.g., "--color-primary")
|
|
5
|
+
* @property {string} value - Raw value (may be var() reference)
|
|
6
|
+
* @property {'dark'|'light'|'shared'} theme - Theme context: 'dark', 'light', or 'shared'
|
|
7
|
+
* @property {'color'|'fontFamily'|'fontSize'|'opacity'|'other'|'radius'|'spacing'} type - Token type for matching: color, spacing, radius, etc.
|
|
8
|
+
* @property {string} [resolvedValue] - Resolved value for var() references (actual hex/value)
|
|
9
|
+
*/
|
|
10
|
+
export interface ThemeToken {
|
|
11
|
+
name: string;
|
|
12
|
+
value: string;
|
|
13
|
+
theme: 'dark' | 'light' | 'shared';
|
|
14
|
+
type: 'color' | 'fontFamily' | 'fontSize' | 'opacity' | 'other' | 'radius' | 'spacing';
|
|
15
|
+
resolvedValue?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parsed theme file with tokens organized by theme context.
|
|
19
|
+
*
|
|
20
|
+
* @property {ThemeToken[]} tokens - All resolved tokens (excludes unresolved var() references)
|
|
21
|
+
* @property {Map<string,ThemeToken>} lightTokens - Map of token name to ThemeToken for light theme
|
|
22
|
+
* @property {Map<string,ThemeToken>} darkTokens - Map of token name to ThemeToken for dark theme
|
|
23
|
+
* @property {Map<string,ThemeToken>} sharedTokens - Map of token name to ThemeToken for shared tokens
|
|
24
|
+
* @property {string[]} warnings - Warnings (e.g., unresolved var() references)
|
|
25
|
+
*/
|
|
26
|
+
export interface ParsedTheme {
|
|
27
|
+
tokens: ThemeToken[];
|
|
28
|
+
lightTokens: Map<string, ThemeToken>;
|
|
29
|
+
darkTokens: Map<string, ThemeToken>;
|
|
30
|
+
sharedTokens: Map<string, ThemeToken>;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Finds the theme file path (app.css) in the workspace.
|
|
35
|
+
*
|
|
36
|
+
* @param workspaceRoot - Workspace root directory to search
|
|
37
|
+
* @returns Absolute path to app.css if found, or null
|
|
38
|
+
*/
|
|
39
|
+
export declare function findThemeFilePath(workspaceRoot?: string): null | string;
|
|
40
|
+
/**
|
|
41
|
+
* Parses theme file and extracts all CSS custom properties.
|
|
42
|
+
*
|
|
43
|
+
* When themeFilePath is not provided, searches for app.css in src/app.css or app.css relative to workspaceRoot.
|
|
44
|
+
*
|
|
45
|
+
* @param themeFilePath - Optional absolute path to theme CSS file
|
|
46
|
+
* @param workspaceRoot - Optional workspace root for theme file discovery when themeFilePath is omitted
|
|
47
|
+
* @returns Parsed theme with tokens organized by light/dark/shared
|
|
48
|
+
* @throws {Error} When theme file is not found
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseThemeFile(themeFilePath?: string, workspaceRoot?: string): ParsedTheme;
|
|
51
|
+
/**
|
|
52
|
+
* Gets all color tokens from parsed theme.
|
|
53
|
+
*
|
|
54
|
+
* @param parsedTheme - Parsed theme from parseThemeFile
|
|
55
|
+
* @returns Array of color-type tokens
|
|
56
|
+
*/
|
|
57
|
+
export declare function getColorTokens(parsedTheme: ParsedTheme): ThemeToken[];
|
|
58
|
+
/**
|
|
59
|
+
* Gets all spacing tokens from parsed theme.
|
|
60
|
+
*
|
|
61
|
+
* @param parsedTheme - Parsed theme from parseThemeFile
|
|
62
|
+
* @returns Array of spacing-type tokens
|
|
63
|
+
*/
|
|
64
|
+
export declare function getSpacingTokens(parsedTheme: ParsedTheme): ThemeToken[];
|
|
65
|
+
/**
|
|
66
|
+
* Gets all radius tokens from parsed theme.
|
|
67
|
+
*
|
|
68
|
+
* @param parsedTheme - Parsed theme from parseThemeFile
|
|
69
|
+
* @returns Array of radius-type tokens
|
|
70
|
+
*/
|
|
71
|
+
export declare function getRadiusTokens(parsedTheme: ParsedTheme): ThemeToken[];
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2
|
|
4
|
+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* PostCSS dependency: Used to parse theme CSS (app.css) into an AST for reliable extraction of
|
|
8
|
+
* design tokens. Required for: (1) walking @theme at-rules and inline blocks, (2) distinguishing
|
|
9
|
+
* light/dark/shared tokens via selectors (e.g. [data-theme="light"], :root), (3) handling nested
|
|
10
|
+
* rules and var() references. Regex-based parsing would be brittle for real-world theme files.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import postcss, {} from 'postcss';
|
|
15
|
+
/**
|
|
16
|
+
* Determines the type of a CSS custom property based on its name and value
|
|
17
|
+
*/
|
|
18
|
+
function determineTokenType(name, value) {
|
|
19
|
+
const nameLower = name.toLowerCase();
|
|
20
|
+
if (nameLower.includes('color') || value.startsWith('#') || value.startsWith('rgb')) {
|
|
21
|
+
return 'color';
|
|
22
|
+
}
|
|
23
|
+
if (nameLower.includes('radius')) {
|
|
24
|
+
return 'radius';
|
|
25
|
+
}
|
|
26
|
+
if (nameLower.includes('opacity')) {
|
|
27
|
+
return 'opacity';
|
|
28
|
+
}
|
|
29
|
+
if (nameLower.includes('font-size') || nameLower.includes('text-size')) {
|
|
30
|
+
return 'fontSize';
|
|
31
|
+
}
|
|
32
|
+
if (nameLower.includes('font-family') || nameLower.includes('font-face')) {
|
|
33
|
+
return 'fontFamily';
|
|
34
|
+
}
|
|
35
|
+
if (nameLower.includes('spacing') ||
|
|
36
|
+
nameLower.includes('gap') ||
|
|
37
|
+
nameLower.includes('padding') ||
|
|
38
|
+
nameLower.includes('margin')) {
|
|
39
|
+
return 'spacing';
|
|
40
|
+
}
|
|
41
|
+
return 'other';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extracts CSS custom property name from var() reference
|
|
45
|
+
* Example: "var(--primary)" -> "--primary"
|
|
46
|
+
*/
|
|
47
|
+
function extractVarName(value) {
|
|
48
|
+
const match = value.match(/var\(([^)]+)\)/);
|
|
49
|
+
return match ? match[1].trim() : null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolves var() references to actual values
|
|
53
|
+
* Returns warnings for any unresolved references
|
|
54
|
+
*/
|
|
55
|
+
function resolveVarReferences(tokens) {
|
|
56
|
+
const warnings = [];
|
|
57
|
+
const tokenMap = new Map();
|
|
58
|
+
for (const token of tokens)
|
|
59
|
+
tokenMap.set(token.name, token);
|
|
60
|
+
for (const token of tokens) {
|
|
61
|
+
if (token.value.includes('var(')) {
|
|
62
|
+
const varName = extractVarName(token.value);
|
|
63
|
+
if (varName) {
|
|
64
|
+
const referencedToken = tokenMap.get(varName);
|
|
65
|
+
if (referencedToken) {
|
|
66
|
+
// Recursively resolve if the referenced token also has a var()
|
|
67
|
+
token.resolvedValue = referencedToken.resolvedValue || referencedToken.value;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Unresolved reference - log warning and skip this token
|
|
71
|
+
warnings.push(`Warning: Token "${token.name}" references undefined variable "${varName}". This token will be excluded from matching.`);
|
|
72
|
+
// Don't set resolvedValue - this token will be filtered out
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
token.resolvedValue = token.value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return warnings;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extracts custom properties from a PostCSS rule node
|
|
84
|
+
*/
|
|
85
|
+
function extractCustomPropertiesFromRule(rule, theme) {
|
|
86
|
+
const tokens = [];
|
|
87
|
+
rule.walkDecls((decl) => {
|
|
88
|
+
if (decl.prop.startsWith('--')) {
|
|
89
|
+
tokens.push({
|
|
90
|
+
name: decl.prop,
|
|
91
|
+
value: decl.value,
|
|
92
|
+
theme,
|
|
93
|
+
type: determineTokenType(decl.prop, decl.value),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return tokens;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Determines if a selector represents a dark theme context
|
|
101
|
+
*/
|
|
102
|
+
function isDarkThemeSelector(selector) {
|
|
103
|
+
return (selector.includes('.dark') ||
|
|
104
|
+
selector.includes('[data-theme="dark"]') ||
|
|
105
|
+
selector.includes("[data-theme='dark']") ||
|
|
106
|
+
(selector.includes('html:not(.dark)') && selector.includes('inverse')));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Determines if a selector represents a light theme context
|
|
110
|
+
*/
|
|
111
|
+
function isLightThemeSelector(selector) {
|
|
112
|
+
return (selector === ':root' ||
|
|
113
|
+
selector.includes('[data-theme="light"]') ||
|
|
114
|
+
selector.includes("[data-theme='light']") ||
|
|
115
|
+
(selector.includes('html.dark') && selector.includes('inverse')));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parses CSS content to extract theme tokens from different sections using PostCSS
|
|
119
|
+
*/
|
|
120
|
+
function parseCSSContent(cssContent) {
|
|
121
|
+
const allTokens = [];
|
|
122
|
+
// Parse CSS with PostCSS
|
|
123
|
+
const root = postcss.parse(cssContent);
|
|
124
|
+
// Walk through all rules and at-rules
|
|
125
|
+
root.walkAtRules('theme', (atRule) => {
|
|
126
|
+
// Extract @theme inline block (shared tokens)
|
|
127
|
+
if (atRule.params.includes('inline')) {
|
|
128
|
+
atRule.walkDecls((decl) => {
|
|
129
|
+
if (decl.prop.startsWith('--')) {
|
|
130
|
+
allTokens.push({
|
|
131
|
+
name: decl.prop,
|
|
132
|
+
value: decl.value,
|
|
133
|
+
theme: 'shared',
|
|
134
|
+
type: determineTokenType(decl.prop, decl.value),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Walk through all rules to find theme-specific tokens
|
|
141
|
+
root.walkRules((rule) => {
|
|
142
|
+
const selector = rule.selector;
|
|
143
|
+
// Determine theme based on selector
|
|
144
|
+
let theme = null;
|
|
145
|
+
if (isDarkThemeSelector(selector)) {
|
|
146
|
+
theme = 'dark';
|
|
147
|
+
}
|
|
148
|
+
else if (isLightThemeSelector(selector)) {
|
|
149
|
+
theme = 'light';
|
|
150
|
+
}
|
|
151
|
+
// Extract tokens if we identified a theme
|
|
152
|
+
if (theme) {
|
|
153
|
+
const tokens = extractCustomPropertiesFromRule(rule, theme);
|
|
154
|
+
allTokens.push(...tokens);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Resolve var() references and collect warnings
|
|
158
|
+
const warnings = resolveVarReferences(allTokens);
|
|
159
|
+
// Filter out tokens with unresolved references
|
|
160
|
+
const resolvedTokens = allTokens.filter((token) => token.resolvedValue !== undefined);
|
|
161
|
+
// Log warnings about skipped tokens
|
|
162
|
+
if (warnings.length > 0 && resolvedTokens.length < allTokens.length) {
|
|
163
|
+
const skippedCount = allTokens.length - resolvedTokens.length;
|
|
164
|
+
warnings.push(`Skipped ${skippedCount} token(s) with unresolved var() references. These will not be available for matching.`);
|
|
165
|
+
}
|
|
166
|
+
// Organize tokens by theme
|
|
167
|
+
const lightTokens = new Map();
|
|
168
|
+
const darkTokens = new Map();
|
|
169
|
+
const sharedTokens = new Map();
|
|
170
|
+
for (const token of resolvedTokens) {
|
|
171
|
+
switch (token.theme) {
|
|
172
|
+
case 'dark': {
|
|
173
|
+
darkTokens.set(token.name, token);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'light': {
|
|
177
|
+
lightTokens.set(token.name, token);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 'shared': {
|
|
181
|
+
sharedTokens.set(token.name, token);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
tokens: resolvedTokens,
|
|
188
|
+
lightTokens,
|
|
189
|
+
darkTokens,
|
|
190
|
+
sharedTokens,
|
|
191
|
+
warnings,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Finds the theme file path (app.css) in the workspace.
|
|
196
|
+
*
|
|
197
|
+
* @param workspaceRoot - Workspace root directory to search
|
|
198
|
+
* @returns Absolute path to app.css if found, or null
|
|
199
|
+
*/
|
|
200
|
+
export function findThemeFilePath(workspaceRoot) {
|
|
201
|
+
if (!workspaceRoot) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const possiblePaths = [join(workspaceRoot, 'src/app.css'), join(workspaceRoot, 'app.css')];
|
|
205
|
+
for (const path of possiblePaths) {
|
|
206
|
+
if (existsSync(path)) {
|
|
207
|
+
return path;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Parses theme file and extracts all CSS custom properties.
|
|
214
|
+
*
|
|
215
|
+
* When themeFilePath is not provided, searches for app.css in src/app.css or app.css relative to workspaceRoot.
|
|
216
|
+
*
|
|
217
|
+
* @param themeFilePath - Optional absolute path to theme CSS file
|
|
218
|
+
* @param workspaceRoot - Optional workspace root for theme file discovery when themeFilePath is omitted
|
|
219
|
+
* @returns Parsed theme with tokens organized by light/dark/shared
|
|
220
|
+
* @throws {Error} When theme file is not found
|
|
221
|
+
*/
|
|
222
|
+
export function parseThemeFile(themeFilePath, workspaceRoot) {
|
|
223
|
+
const filePath = themeFilePath ?? findThemeFilePath(workspaceRoot);
|
|
224
|
+
if (!filePath) {
|
|
225
|
+
throw new Error('Theme file (app.css) not found. Please provide the themeFilePath parameter.');
|
|
226
|
+
}
|
|
227
|
+
if (!existsSync(filePath)) {
|
|
228
|
+
throw new Error(`Theme file not found at: ${filePath}`);
|
|
229
|
+
}
|
|
230
|
+
const cssContent = readFileSync(filePath, 'utf8');
|
|
231
|
+
return parseCSSContent(cssContent);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Gets all color tokens from parsed theme.
|
|
235
|
+
*
|
|
236
|
+
* @param parsedTheme - Parsed theme from parseThemeFile
|
|
237
|
+
* @returns Array of color-type tokens
|
|
238
|
+
*/
|
|
239
|
+
export function getColorTokens(parsedTheme) {
|
|
240
|
+
return parsedTheme.tokens.filter((token) => token.type === 'color');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Gets all spacing tokens from parsed theme.
|
|
244
|
+
*
|
|
245
|
+
* @param parsedTheme - Parsed theme from parseThemeFile
|
|
246
|
+
* @returns Array of spacing-type tokens
|
|
247
|
+
*/
|
|
248
|
+
export function getSpacingTokens(parsedTheme) {
|
|
249
|
+
return parsedTheme.tokens.filter((token) => token.type === 'spacing');
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Gets all radius tokens from parsed theme.
|
|
253
|
+
*
|
|
254
|
+
* @param parsedTheme - Parsed theme from parseThemeFile
|
|
255
|
+
* @returns Array of radius-type tokens
|
|
256
|
+
*/
|
|
257
|
+
export function getRadiusTokens(parsedTheme) {
|
|
258
|
+
return parsedTheme.tokens.filter((token) => token.type === 'radius');
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=css-parser.js.map
|