@salesforce/b2c-dx-mcp 0.3.1 → 0.4.0
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 +47 -27
- package/content/page-designer.md +4 -4
- package/dist/commands/mcp.d.ts +13 -1
- package/dist/commands/mcp.js +16 -4
- package/dist/registry.d.ts +4 -4
- package/dist/registry.js +10 -10
- package/dist/services.d.ts +75 -1
- package/dist/services.js +124 -1
- package/dist/tools/adapter.d.ts +29 -21
- package/dist/tools/adapter.js +34 -24
- package/dist/tools/cartridges/index.d.ts +5 -3
- package/dist/tools/cartridges/index.js +55 -25
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/mrt/index.d.ts +2 -2
- package/dist/tools/mrt/index.js +29 -10
- package/dist/tools/page-designer-decorator/analyzer.d.ts +169 -0
- package/dist/tools/page-designer-decorator/analyzer.js +535 -0
- package/dist/tools/page-designer-decorator/index.d.ts +252 -0
- package/dist/tools/page-designer-decorator/index.js +597 -0
- package/dist/tools/page-designer-decorator/rules/1-mode-selection.d.ts +8 -0
- package/dist/tools/page-designer-decorator/rules/1-mode-selection.js +65 -0
- package/dist/tools/page-designer-decorator/rules/2a-auto-mode.d.ts +13 -0
- package/dist/tools/page-designer-decorator/rules/2a-auto-mode.js +87 -0
- package/dist/tools/page-designer-decorator/rules/2b-0-interactive-overview.d.ts +4 -0
- package/dist/tools/page-designer-decorator/rules/2b-0-interactive-overview.js +55 -0
- package/dist/tools/page-designer-decorator/rules/2b-1-interactive-analyze.d.ts +22 -0
- package/dist/tools/page-designer-decorator/rules/2b-1-interactive-analyze.js +109 -0
- package/dist/tools/page-designer-decorator/rules/2b-2-interactive-select-props.d.ts +21 -0
- package/dist/tools/page-designer-decorator/rules/2b-2-interactive-select-props.js +60 -0
- package/dist/tools/page-designer-decorator/rules/2b-3-interactive-configure-attrs.d.ts +27 -0
- package/dist/tools/page-designer-decorator/rules/2b-3-interactive-configure-attrs.js +68 -0
- package/dist/tools/page-designer-decorator/rules/2b-4-interactive-configure-regions.d.ts +4 -0
- package/dist/tools/page-designer-decorator/rules/2b-4-interactive-configure-regions.js +65 -0
- package/dist/tools/page-designer-decorator/rules/2b-5-interactive-confirm-generation.d.ts +11 -0
- package/dist/tools/page-designer-decorator/rules/2b-5-interactive-confirm-generation.js +92 -0
- package/dist/tools/page-designer-decorator/rules.d.ts +51 -0
- package/dist/tools/page-designer-decorator/rules.js +70 -0
- package/dist/tools/page-designer-decorator/templates/decorator-generator.d.ts +116 -0
- package/dist/tools/page-designer-decorator/templates/decorator-generator.js +350 -0
- package/dist/tools/pwav3/index.d.ts +2 -2
- package/dist/tools/pwav3/index.js +13 -13
- package/dist/tools/scapi/index.d.ts +10 -2
- package/dist/tools/scapi/index.js +5 -56
- package/dist/tools/scapi/scapi-custom-apis-status.d.ts +9 -0
- package/dist/tools/scapi/scapi-custom-apis-status.js +152 -0
- package/dist/tools/scapi/scapi-schemas-list.d.ts +12 -0
- package/dist/tools/scapi/scapi-schemas-list.js +248 -0
- package/dist/tools/storefrontnext/developer-guidelines.d.ts +2 -2
- package/dist/tools/storefrontnext/developer-guidelines.js +3 -3
- package/dist/tools/storefrontnext/index.d.ts +2 -2
- package/dist/tools/storefrontnext/index.js +13 -13
- package/oclif.manifest.json +15 -4
- package/package.json +10 -5
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component analysis result
|
|
3
|
+
*/
|
|
4
|
+
export interface ComponentInfo {
|
|
5
|
+
componentName: string;
|
|
6
|
+
interfaceName: null | string;
|
|
7
|
+
hasDecorators: boolean;
|
|
8
|
+
props: PropInfo[];
|
|
9
|
+
exportType: 'default' | 'named';
|
|
10
|
+
filePath: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Property information extracted from component interface
|
|
14
|
+
*/
|
|
15
|
+
export interface PropInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
type: string;
|
|
18
|
+
optional: boolean;
|
|
19
|
+
isComplex: boolean;
|
|
20
|
+
isUIOnly: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type suggestion for attribute configuration
|
|
24
|
+
*/
|
|
25
|
+
export interface TypeSuggestion {
|
|
26
|
+
type: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
priority: 'high' | 'low' | 'medium';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Valid SFCC Page Designer attribute types
|
|
32
|
+
*/
|
|
33
|
+
export declare const VALID_ATTRIBUTE_TYPES: readonly ["string", "text", "markup", "integer", "boolean", "product", "category", "file", "page", "image", "url", "enum", "custom", "cms_record"];
|
|
34
|
+
/**
|
|
35
|
+
* Infer Page Designer attribute type from TypeScript type
|
|
36
|
+
*/
|
|
37
|
+
export declare function inferPageDesignerType(tsType: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Check if TypeScript type can be auto-inferred
|
|
40
|
+
*/
|
|
41
|
+
export declare function isAutoInferredType(tsType: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Check if type is too complex for Page Designer
|
|
44
|
+
*/
|
|
45
|
+
export declare function isComplexType(tsType: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Check if property is UI-only
|
|
48
|
+
*/
|
|
49
|
+
export declare function isUIOnlyProp(propName: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Generate Page Designer attribute type suggestions for a component prop
|
|
52
|
+
*
|
|
53
|
+
* **Inference Strategy:**
|
|
54
|
+
* Uses naming patterns and TypeScript types to suggest appropriate Page Designer types.
|
|
55
|
+
* This reduces manual configuration by auto-detecting common patterns.
|
|
56
|
+
*
|
|
57
|
+
* **Page Designer Types:**
|
|
58
|
+
* - `string`: Default text input
|
|
59
|
+
* - `url`: URL/link inputs (validates URL format)
|
|
60
|
+
* - `image`: Image asset picker
|
|
61
|
+
* - `html`: Rich text editor
|
|
62
|
+
* - `markup`: HTML/markdown editor
|
|
63
|
+
* - `enum`: Dropdown with predefined values
|
|
64
|
+
* - `boolean`: Checkbox
|
|
65
|
+
* - `number`: Numeric input
|
|
66
|
+
* - `product`: Product picker (SFCC-specific)
|
|
67
|
+
* - `category`: Category picker (SFCC-specific)
|
|
68
|
+
*
|
|
69
|
+
* **Heuristics (by priority):**
|
|
70
|
+
* 1. **High Priority**: Strong patterns (url, image, product)
|
|
71
|
+
* 2. **Medium Priority**: Contextual patterns (html, markup)
|
|
72
|
+
* 3. **Low Priority**: Weak signals (description → markup)
|
|
73
|
+
*
|
|
74
|
+
* Multiple suggestions allow developers to choose the best fit.
|
|
75
|
+
*
|
|
76
|
+
* @param propName - Property name from component interface
|
|
77
|
+
* @param tsType - TypeScript type string
|
|
78
|
+
* @returns Array of type suggestions with reasoning and priority
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // URL detection:
|
|
82
|
+
* generateTypeSuggestions('imageUrl', 'string')
|
|
83
|
+
* // => [{ type: 'url', reason: '...', priority: 'high' }]
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Image detection:
|
|
87
|
+
* generateTypeSuggestions('heroImage', 'string')
|
|
88
|
+
* // => [{ type: 'image', reason: '...', priority: 'high' }]
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Multiple suggestions:
|
|
92
|
+
* generateTypeSuggestions('description', 'string')
|
|
93
|
+
* // => [
|
|
94
|
+
* // { type: 'markup', reason: '...', priority: 'low' },
|
|
95
|
+
* // { type: 'html', reason: '...', priority: 'medium' }
|
|
96
|
+
* // ]
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Product reference:
|
|
100
|
+
* generateTypeSuggestions('product', 'string')
|
|
101
|
+
* // => [{ type: 'product', reason: '...', priority: 'high' }]
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
export declare function generateTypeSuggestions(propName: string, tsType: string): TypeSuggestion[];
|
|
106
|
+
/**
|
|
107
|
+
* Component analyzer for Page Designer decorator generation
|
|
108
|
+
*/
|
|
109
|
+
declare class ComponentAnalyzer {
|
|
110
|
+
private cache;
|
|
111
|
+
analyzeComponent(filePath: string): ComponentInfo;
|
|
112
|
+
clearCache(): void;
|
|
113
|
+
}
|
|
114
|
+
export declare const componentAnalyzer: ComponentAnalyzer;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve component input (name or path) to absolute file path
|
|
117
|
+
*
|
|
118
|
+
* **This is the main entry point for component discovery.**
|
|
119
|
+
*
|
|
120
|
+
* Supports two input modes:
|
|
121
|
+
* 1. **Name-based** (recommended): Just provide the component name
|
|
122
|
+
* 2. **Path-based** (backward compatible): Provide relative path from workspace
|
|
123
|
+
*
|
|
124
|
+
* **Name-based detection:**
|
|
125
|
+
* Input is treated as a name if it:
|
|
126
|
+
* - Does NOT contain path separators (/ or \)
|
|
127
|
+
* - Does NOT have a file extension (.tsx, .ts, etc.)
|
|
128
|
+
*
|
|
129
|
+
* **Path-based detection:**
|
|
130
|
+
* Input is treated as a path if it:
|
|
131
|
+
* - Contains / or \
|
|
132
|
+
* - Has a file extension
|
|
133
|
+
*
|
|
134
|
+
* @param input - Component name or relative path
|
|
135
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
136
|
+
* @param searchPaths - Additional directories to search (only used for name-based)
|
|
137
|
+
* @returns Absolute file path to component
|
|
138
|
+
* @throws {Error} If component cannot be found, with detailed search information
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* // Name-based (finds automatically):
|
|
142
|
+
* resolveComponent('ProductCard', '/workspace')
|
|
143
|
+
* // => '/workspace/src/components/product-tile/ProductCard.tsx'
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* // Path-based (backward compatible):
|
|
147
|
+
* resolveComponent('src/components/ProductCard.tsx', '/workspace')
|
|
148
|
+
* // => '/workspace/src/components/ProductCard.tsx'
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* // With custom search paths (for monorepos):
|
|
152
|
+
* resolveComponent('Hero', '/workspace', ['packages/retail/src', 'packages/shared'])
|
|
153
|
+
* // => '/workspace/packages/retail/src/components/Hero.tsx'
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* // Error handling:
|
|
157
|
+
* try {
|
|
158
|
+
* resolveComponent('NonExistent', '/workspace')
|
|
159
|
+
* } catch (err) {
|
|
160
|
+
* // Error includes:
|
|
161
|
+
* // - List of searched locations
|
|
162
|
+
* // - Tried name variations
|
|
163
|
+
* // - Helpful tips for resolution
|
|
164
|
+
* }
|
|
165
|
+
*
|
|
166
|
+
* @public
|
|
167
|
+
*/
|
|
168
|
+
export declare function resolveComponent(input: string, workspaceRoot: string, searchPaths?: string[]): string;
|
|
169
|
+
export {};
|
|
@@ -0,0 +1,535 @@
|
|
|
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
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { globSync } from 'glob';
|
|
9
|
+
import { Project, InterfaceDeclaration, PropertySignature } from 'ts-morph';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// TYPE INFERENCE
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Type mapping from TypeScript to SFCC Page Designer attribute types
|
|
15
|
+
*/
|
|
16
|
+
const TYPE_MAPPING = {
|
|
17
|
+
String: 'string',
|
|
18
|
+
string: 'string',
|
|
19
|
+
Number: 'integer',
|
|
20
|
+
number: 'integer',
|
|
21
|
+
Boolean: 'boolean',
|
|
22
|
+
boolean: 'boolean',
|
|
23
|
+
Date: 'string',
|
|
24
|
+
URL: 'url',
|
|
25
|
+
CMSRecord: 'cms_record',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Valid SFCC Page Designer attribute types
|
|
29
|
+
*/
|
|
30
|
+
export const VALID_ATTRIBUTE_TYPES = [
|
|
31
|
+
'string',
|
|
32
|
+
'text',
|
|
33
|
+
'markup',
|
|
34
|
+
'integer',
|
|
35
|
+
'boolean',
|
|
36
|
+
'product',
|
|
37
|
+
'category',
|
|
38
|
+
'file',
|
|
39
|
+
'page',
|
|
40
|
+
'image',
|
|
41
|
+
'url',
|
|
42
|
+
'enum',
|
|
43
|
+
'custom',
|
|
44
|
+
'cms_record',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Infer Page Designer attribute type from TypeScript type
|
|
48
|
+
*/
|
|
49
|
+
export function inferPageDesignerType(tsType) {
|
|
50
|
+
if (TYPE_MAPPING[tsType]) {
|
|
51
|
+
return TYPE_MAPPING[tsType];
|
|
52
|
+
}
|
|
53
|
+
if (tsType.includes('|')) {
|
|
54
|
+
const firstType = tsType.split('|')[0].trim();
|
|
55
|
+
return inferPageDesignerType(firstType);
|
|
56
|
+
}
|
|
57
|
+
if (tsType.includes('[]') || tsType.includes('Array<')) {
|
|
58
|
+
return 'string';
|
|
59
|
+
}
|
|
60
|
+
return 'string';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if TypeScript type can be auto-inferred
|
|
64
|
+
*/
|
|
65
|
+
export function isAutoInferredType(tsType) {
|
|
66
|
+
return Boolean(TYPE_MAPPING[tsType]);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if type is too complex for Page Designer
|
|
70
|
+
*/
|
|
71
|
+
export function isComplexType(tsType) {
|
|
72
|
+
return (tsType.includes('{') ||
|
|
73
|
+
tsType.includes('<') ||
|
|
74
|
+
tsType.includes('.') ||
|
|
75
|
+
tsType.includes('=>') ||
|
|
76
|
+
tsType.includes('React.') ||
|
|
77
|
+
tsType.startsWith('('));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check if property is UI-only
|
|
81
|
+
*/
|
|
82
|
+
export function isUIOnlyProp(propName) {
|
|
83
|
+
const uiPatterns = [
|
|
84
|
+
'classname',
|
|
85
|
+
'style',
|
|
86
|
+
'theme',
|
|
87
|
+
'variant',
|
|
88
|
+
'size',
|
|
89
|
+
'color',
|
|
90
|
+
'loading',
|
|
91
|
+
'disabled',
|
|
92
|
+
'readonly',
|
|
93
|
+
'onclick',
|
|
94
|
+
'onchange',
|
|
95
|
+
'onsubmit',
|
|
96
|
+
'children',
|
|
97
|
+
'key',
|
|
98
|
+
'ref',
|
|
99
|
+
];
|
|
100
|
+
const nameLower = propName.toLowerCase();
|
|
101
|
+
return uiPatterns.some((pattern) => nameLower.includes(pattern));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generate Page Designer attribute type suggestions for a component prop
|
|
105
|
+
*
|
|
106
|
+
* **Inference Strategy:**
|
|
107
|
+
* Uses naming patterns and TypeScript types to suggest appropriate Page Designer types.
|
|
108
|
+
* This reduces manual configuration by auto-detecting common patterns.
|
|
109
|
+
*
|
|
110
|
+
* **Page Designer Types:**
|
|
111
|
+
* - `string`: Default text input
|
|
112
|
+
* - `url`: URL/link inputs (validates URL format)
|
|
113
|
+
* - `image`: Image asset picker
|
|
114
|
+
* - `html`: Rich text editor
|
|
115
|
+
* - `markup`: HTML/markdown editor
|
|
116
|
+
* - `enum`: Dropdown with predefined values
|
|
117
|
+
* - `boolean`: Checkbox
|
|
118
|
+
* - `number`: Numeric input
|
|
119
|
+
* - `product`: Product picker (SFCC-specific)
|
|
120
|
+
* - `category`: Category picker (SFCC-specific)
|
|
121
|
+
*
|
|
122
|
+
* **Heuristics (by priority):**
|
|
123
|
+
* 1. **High Priority**: Strong patterns (url, image, product)
|
|
124
|
+
* 2. **Medium Priority**: Contextual patterns (html, markup)
|
|
125
|
+
* 3. **Low Priority**: Weak signals (description → markup)
|
|
126
|
+
*
|
|
127
|
+
* Multiple suggestions allow developers to choose the best fit.
|
|
128
|
+
*
|
|
129
|
+
* @param propName - Property name from component interface
|
|
130
|
+
* @param tsType - TypeScript type string
|
|
131
|
+
* @returns Array of type suggestions with reasoning and priority
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* // URL detection:
|
|
135
|
+
* generateTypeSuggestions('imageUrl', 'string')
|
|
136
|
+
* // => [{ type: 'url', reason: '...', priority: 'high' }]
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // Image detection:
|
|
140
|
+
* generateTypeSuggestions('heroImage', 'string')
|
|
141
|
+
* // => [{ type: 'image', reason: '...', priority: 'high' }]
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* // Multiple suggestions:
|
|
145
|
+
* generateTypeSuggestions('description', 'string')
|
|
146
|
+
* // => [
|
|
147
|
+
* // { type: 'markup', reason: '...', priority: 'low' },
|
|
148
|
+
* // { type: 'html', reason: '...', priority: 'medium' }
|
|
149
|
+
* // ]
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* // Product reference:
|
|
153
|
+
* generateTypeSuggestions('product', 'string')
|
|
154
|
+
* // => [{ type: 'product', reason: '...', priority: 'high' }]
|
|
155
|
+
*
|
|
156
|
+
* @public
|
|
157
|
+
*/
|
|
158
|
+
export function generateTypeSuggestions(propName, tsType) {
|
|
159
|
+
const suggestions = [];
|
|
160
|
+
const nameLower = propName.toLowerCase();
|
|
161
|
+
// URL patterns
|
|
162
|
+
if (nameLower.includes('url') || nameLower.includes('link') || nameLower.includes('href')) {
|
|
163
|
+
suggestions.push({
|
|
164
|
+
type: 'url',
|
|
165
|
+
reason: 'Property name suggests URL/link',
|
|
166
|
+
priority: 'high',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Image patterns
|
|
170
|
+
if (nameLower.includes('image') ||
|
|
171
|
+
nameLower.includes('img') ||
|
|
172
|
+
nameLower.includes('picture') ||
|
|
173
|
+
nameLower.includes('background')) {
|
|
174
|
+
suggestions.push({
|
|
175
|
+
type: 'image',
|
|
176
|
+
reason: 'Property name suggests image asset',
|
|
177
|
+
priority: 'high',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Rich text patterns
|
|
181
|
+
if (nameLower.includes('html') ||
|
|
182
|
+
nameLower.includes('richtext') ||
|
|
183
|
+
nameLower.includes('content') ||
|
|
184
|
+
nameLower.includes('body')) {
|
|
185
|
+
suggestions.push({
|
|
186
|
+
type: 'markup',
|
|
187
|
+
reason: 'Property name suggests rich content',
|
|
188
|
+
priority: 'medium',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Multi-line text patterns
|
|
192
|
+
if (nameLower.includes('description') || nameLower.includes('bio') || nameLower.includes('message')) {
|
|
193
|
+
suggestions.push({
|
|
194
|
+
type: 'text',
|
|
195
|
+
reason: 'Property name suggests multi-line text',
|
|
196
|
+
priority: 'medium',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Array patterns
|
|
200
|
+
if (tsType.includes('[]') || tsType.includes('Array<')) {
|
|
201
|
+
suggestions.push({
|
|
202
|
+
type: 'enum',
|
|
203
|
+
reason: 'Array types work best as enums for selection in Page Designer',
|
|
204
|
+
priority: 'high',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Product/Category references
|
|
208
|
+
if (nameLower.includes('product') && !nameLower.includes('products')) {
|
|
209
|
+
suggestions.push({
|
|
210
|
+
type: 'product',
|
|
211
|
+
reason: 'Property name suggests product reference',
|
|
212
|
+
priority: 'high',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (nameLower.includes('category')) {
|
|
216
|
+
suggestions.push({
|
|
217
|
+
type: 'category',
|
|
218
|
+
reason: 'Property name suggests category reference',
|
|
219
|
+
priority: 'high',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return suggestions;
|
|
223
|
+
}
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// COMPONENT FILE PARSING
|
|
226
|
+
// ============================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Extract component name from file content
|
|
229
|
+
*/
|
|
230
|
+
function extractComponentName(content) {
|
|
231
|
+
const defaultFunctionMatch = content.match(/export\s+default\s+function\s+(\w+)/);
|
|
232
|
+
if (defaultFunctionMatch) {
|
|
233
|
+
return defaultFunctionMatch[1];
|
|
234
|
+
}
|
|
235
|
+
const namedFunctionMatch = content.match(/export\s+function\s+(\w+)/);
|
|
236
|
+
if (namedFunctionMatch) {
|
|
237
|
+
return namedFunctionMatch[1];
|
|
238
|
+
}
|
|
239
|
+
const namedConstMatch = content.match(/export\s+const\s+(\w+)\s*=/);
|
|
240
|
+
if (namedConstMatch) {
|
|
241
|
+
return namedConstMatch[1];
|
|
242
|
+
}
|
|
243
|
+
return 'Component';
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Detect export type
|
|
247
|
+
*/
|
|
248
|
+
function detectExportType(content) {
|
|
249
|
+
return content.includes('export default') ? 'default' : 'named';
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Parse component file and extract structure
|
|
253
|
+
*/
|
|
254
|
+
function parseComponentFile(filePath) {
|
|
255
|
+
const content = readFileSync(filePath, 'utf8');
|
|
256
|
+
const hasDecorators = content.includes('@Component') || content.includes('@PageType');
|
|
257
|
+
if (hasDecorators) {
|
|
258
|
+
return {
|
|
259
|
+
componentName: extractComponentName(content),
|
|
260
|
+
interfaceName: null,
|
|
261
|
+
hasDecorators: true,
|
|
262
|
+
props: [],
|
|
263
|
+
exportType: detectExportType(content),
|
|
264
|
+
filePath,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const project = new Project({
|
|
268
|
+
useInMemoryFileSystem: true,
|
|
269
|
+
skipAddingFilesFromTsConfig: true,
|
|
270
|
+
});
|
|
271
|
+
const sourceFile = project.createSourceFile(filePath, content);
|
|
272
|
+
const interfaces = sourceFile.getInterfaces();
|
|
273
|
+
const propsInterface = interfaces.find((i) => i.getName().includes('Props'));
|
|
274
|
+
if (!propsInterface) {
|
|
275
|
+
return {
|
|
276
|
+
componentName: extractComponentName(content),
|
|
277
|
+
interfaceName: null,
|
|
278
|
+
hasDecorators: false,
|
|
279
|
+
props: [],
|
|
280
|
+
exportType: detectExportType(content),
|
|
281
|
+
filePath,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const props = propsInterface.getProperties().map((prop) => {
|
|
285
|
+
const name = prop.getName();
|
|
286
|
+
const type = prop.getType().getText();
|
|
287
|
+
const optional = prop.hasQuestionToken();
|
|
288
|
+
return {
|
|
289
|
+
name,
|
|
290
|
+
type,
|
|
291
|
+
optional,
|
|
292
|
+
isComplex: isComplexType(type),
|
|
293
|
+
isUIOnly: isUIOnlyProp(name),
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
componentName: extractComponentName(content),
|
|
298
|
+
interfaceName: propsInterface.getName(),
|
|
299
|
+
hasDecorators: false,
|
|
300
|
+
props,
|
|
301
|
+
exportType: detectExportType(content),
|
|
302
|
+
filePath,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// COMPONENT ANALYZER
|
|
307
|
+
// ============================================================================
|
|
308
|
+
/**
|
|
309
|
+
* Component analyzer for Page Designer decorator generation
|
|
310
|
+
*/
|
|
311
|
+
class ComponentAnalyzer {
|
|
312
|
+
cache = new Map();
|
|
313
|
+
analyzeComponent(filePath) {
|
|
314
|
+
const cached = this.cache.get(filePath);
|
|
315
|
+
if (cached) {
|
|
316
|
+
return cached;
|
|
317
|
+
}
|
|
318
|
+
const analysis = parseComponentFile(filePath);
|
|
319
|
+
this.cache.set(filePath, analysis);
|
|
320
|
+
return analysis;
|
|
321
|
+
}
|
|
322
|
+
clearCache() {
|
|
323
|
+
this.cache.clear();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export const componentAnalyzer = new ComponentAnalyzer();
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// COMPONENT RESOLUTION (Name-Based Lookup)
|
|
329
|
+
// ============================================================================
|
|
330
|
+
/**
|
|
331
|
+
* Convert PascalCase or camelCase to kebab-case
|
|
332
|
+
*
|
|
333
|
+
* Used for finding components with different naming conventions.
|
|
334
|
+
* React components are typically PascalCase, but file names may be kebab-case.
|
|
335
|
+
*
|
|
336
|
+
* @param str - String to convert (e.g., "ProductCard", "myComponent")
|
|
337
|
+
* @returns Kebab-case string (e.g., "product-card", "my-component")
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* toKebabCase('ProductCard') // => 'product-card'
|
|
341
|
+
* toKebabCase('MyButtonComponent') // => 'my-button-component'
|
|
342
|
+
* toKebabCase('heroSection') // => 'hero-section'
|
|
343
|
+
*
|
|
344
|
+
* @internal
|
|
345
|
+
*/
|
|
346
|
+
function toKebabCase(str) {
|
|
347
|
+
return str
|
|
348
|
+
.replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
349
|
+
.replaceAll(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
350
|
+
.toLowerCase();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Search for component file by name using smart discovery patterns
|
|
354
|
+
*
|
|
355
|
+
* **Search Strategy (in priority order):**
|
|
356
|
+
* 1. Common component directories with exact name (PascalCase)
|
|
357
|
+
* 2. Kebab-case variants of the name
|
|
358
|
+
* 3. Index file patterns (for directory-based components)
|
|
359
|
+
* 4. Broader search in src/
|
|
360
|
+
* 5. Custom search paths (if provided)
|
|
361
|
+
*
|
|
362
|
+
* **Why this order:**
|
|
363
|
+
* - Most projects follow conventions (src/components/)
|
|
364
|
+
* - PascalCase is React standard, checked first
|
|
365
|
+
* - Kebab-case is common for file names
|
|
366
|
+
* - Index files are common for complex components
|
|
367
|
+
* - Fallback to broader search if not in standard locations
|
|
368
|
+
*
|
|
369
|
+
* **Disambiguation:**
|
|
370
|
+
* If multiple files match, prefers the shortest path (closest to root).
|
|
371
|
+
* This typically selects the main component over similar named test/story files.
|
|
372
|
+
*
|
|
373
|
+
* @param componentName - Component name without extension (e.g., "ProductCard", "Hero")
|
|
374
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
375
|
+
* @param customPaths - Additional directories to search (e.g., ["packages/retail/src"])
|
|
376
|
+
* @returns Absolute file path or null if not found
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* // Finds: src/components/product-tile/ProductCard.tsx
|
|
380
|
+
* findComponentByName('ProductCard', '/workspace', undefined)
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* // Finds: src/components/hero.tsx or src/components/hero/index.tsx
|
|
384
|
+
* findComponentByName('hero', '/workspace', undefined)
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* // Searches in custom paths first
|
|
388
|
+
* findComponentByName('ProductCard', '/workspace', ['packages/retail/src'])
|
|
389
|
+
*
|
|
390
|
+
* @internal
|
|
391
|
+
*/
|
|
392
|
+
function findComponentByName(componentName, workspaceRoot, customPaths) {
|
|
393
|
+
// Normalize component name (remove file extensions)
|
|
394
|
+
const cleanName = componentName.replace(/\.(tsx?|jsx?)$/, '');
|
|
395
|
+
const kebabName = toKebabCase(cleanName);
|
|
396
|
+
// Search patterns (in order of priority)
|
|
397
|
+
const searchPatterns = [
|
|
398
|
+
// Common component directories (PascalCase)
|
|
399
|
+
`src/components/**/${cleanName}.tsx`,
|
|
400
|
+
`src/components/**/${cleanName}.ts`,
|
|
401
|
+
`app/components/**/${cleanName}.tsx`,
|
|
402
|
+
`components/**/${cleanName}.tsx`,
|
|
403
|
+
// Kebab-case variants
|
|
404
|
+
`src/components/**/${kebabName}.tsx`,
|
|
405
|
+
`app/components/**/${kebabName}.tsx`,
|
|
406
|
+
`components/**/${kebabName}.tsx`,
|
|
407
|
+
// Index file patterns
|
|
408
|
+
`src/components/**/${kebabName}/index.tsx`,
|
|
409
|
+
`app/components/**/${kebabName}/index.tsx`,
|
|
410
|
+
// Anywhere in src/ (broader search)
|
|
411
|
+
`src/**/${cleanName}.tsx`,
|
|
412
|
+
`src/**/${cleanName}.ts`,
|
|
413
|
+
`src/**/${kebabName}.tsx`,
|
|
414
|
+
// Custom search paths (if provided)
|
|
415
|
+
...(customPaths?.flatMap((path) => [
|
|
416
|
+
`${path}/**/${cleanName}.tsx`,
|
|
417
|
+
`${path}/**/${cleanName}.ts`,
|
|
418
|
+
`${path}/**/${kebabName}.tsx`,
|
|
419
|
+
`${path}/**/${kebabName}/index.tsx`,
|
|
420
|
+
]) || []),
|
|
421
|
+
];
|
|
422
|
+
// Search with glob
|
|
423
|
+
for (const pattern of searchPatterns) {
|
|
424
|
+
try {
|
|
425
|
+
const matches = globSync(pattern, {
|
|
426
|
+
cwd: workspaceRoot,
|
|
427
|
+
absolute: true,
|
|
428
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/out/**'],
|
|
429
|
+
});
|
|
430
|
+
if (matches.length > 0) {
|
|
431
|
+
// If multiple matches, prefer shortest path (closest to root)
|
|
432
|
+
const sorted = matches.sort((a, b) => a.length - b.length);
|
|
433
|
+
return sorted[0];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Ignore glob errors and try next pattern
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Resolve component input (name or path) to absolute file path
|
|
445
|
+
*
|
|
446
|
+
* **This is the main entry point for component discovery.**
|
|
447
|
+
*
|
|
448
|
+
* Supports two input modes:
|
|
449
|
+
* 1. **Name-based** (recommended): Just provide the component name
|
|
450
|
+
* 2. **Path-based** (backward compatible): Provide relative path from workspace
|
|
451
|
+
*
|
|
452
|
+
* **Name-based detection:**
|
|
453
|
+
* Input is treated as a name if it:
|
|
454
|
+
* - Does NOT contain path separators (/ or \)
|
|
455
|
+
* - Does NOT have a file extension (.tsx, .ts, etc.)
|
|
456
|
+
*
|
|
457
|
+
* **Path-based detection:**
|
|
458
|
+
* Input is treated as a path if it:
|
|
459
|
+
* - Contains / or \
|
|
460
|
+
* - Has a file extension
|
|
461
|
+
*
|
|
462
|
+
* @param input - Component name or relative path
|
|
463
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
464
|
+
* @param searchPaths - Additional directories to search (only used for name-based)
|
|
465
|
+
* @returns Absolute file path to component
|
|
466
|
+
* @throws {Error} If component cannot be found, with detailed search information
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* // Name-based (finds automatically):
|
|
470
|
+
* resolveComponent('ProductCard', '/workspace')
|
|
471
|
+
* // => '/workspace/src/components/product-tile/ProductCard.tsx'
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* // Path-based (backward compatible):
|
|
475
|
+
* resolveComponent('src/components/ProductCard.tsx', '/workspace')
|
|
476
|
+
* // => '/workspace/src/components/ProductCard.tsx'
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* // With custom search paths (for monorepos):
|
|
480
|
+
* resolveComponent('Hero', '/workspace', ['packages/retail/src', 'packages/shared'])
|
|
481
|
+
* // => '/workspace/packages/retail/src/components/Hero.tsx'
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* // Error handling:
|
|
485
|
+
* try {
|
|
486
|
+
* resolveComponent('NonExistent', '/workspace')
|
|
487
|
+
* } catch (err) {
|
|
488
|
+
* // Error includes:
|
|
489
|
+
* // - List of searched locations
|
|
490
|
+
* // - Tried name variations
|
|
491
|
+
* // - Helpful tips for resolution
|
|
492
|
+
* }
|
|
493
|
+
*
|
|
494
|
+
* @public
|
|
495
|
+
*/
|
|
496
|
+
export function resolveComponent(input, workspaceRoot, searchPaths) {
|
|
497
|
+
// Check if input looks like a path (has / or \ or file extension)
|
|
498
|
+
const looksLikePath = input.includes('/') || input.includes('\\') || input.match(/\.(tsx?|jsx?|mjs|cjs|js)$/);
|
|
499
|
+
if (looksLikePath) {
|
|
500
|
+
// Treat as path (backward compatible)
|
|
501
|
+
const fullPath = path.join(workspaceRoot, input);
|
|
502
|
+
if (existsSync(fullPath)) {
|
|
503
|
+
return fullPath;
|
|
504
|
+
}
|
|
505
|
+
throw new Error(`Component file not found at path: ${input}\n\n` +
|
|
506
|
+
`Full path checked: ${fullPath}\n\n` +
|
|
507
|
+
`Tips:\n` +
|
|
508
|
+
` 1. Use component name instead (e.g., "ProductCard") for automatic discovery\n` +
|
|
509
|
+
` 2. If components are in a different repo, set --working-directory flag or SFCC_WORKING_DIRECTORY env var`);
|
|
510
|
+
}
|
|
511
|
+
// Treat as component name - search for it
|
|
512
|
+
const found = findComponentByName(input, workspaceRoot, searchPaths);
|
|
513
|
+
if (!found) {
|
|
514
|
+
const searchLocations = [
|
|
515
|
+
'src/components/**',
|
|
516
|
+
'app/components/**',
|
|
517
|
+
'components/**',
|
|
518
|
+
'src/**',
|
|
519
|
+
...(searchPaths || []),
|
|
520
|
+
];
|
|
521
|
+
throw new Error(`Component "${input}" not found.\n\n` +
|
|
522
|
+
`Searched in:\n${searchLocations.map((loc) => ` - ${loc}`).join('\n')}\n\n` +
|
|
523
|
+
`Tried variations:\n` +
|
|
524
|
+
` - ${input}.tsx\n` +
|
|
525
|
+
` - ${toKebabCase(input)}.tsx\n` +
|
|
526
|
+
` - ${toKebabCase(input)}/index.tsx\n\n` +
|
|
527
|
+
`Tips:\n` +
|
|
528
|
+
` 1. Provide full path: component: "src/components/ProductCard.tsx"\n` +
|
|
529
|
+
` 2. Add custom search: searchPaths: ["packages/retail/src"]\n` +
|
|
530
|
+
` 3. Check component name spelling and casing\n` +
|
|
531
|
+
` 4. If components are in a different repo, set --working-directory flag or SFCC_WORKING_DIRECTORY env var`);
|
|
532
|
+
}
|
|
533
|
+
return found;
|
|
534
|
+
}
|
|
535
|
+
//# sourceMappingURL=analyzer.js.map
|