@tpitre/story-ui 1.7.1 → 2.0.1
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/.env.sample +3 -1
- package/README.md +160 -606
- package/dist/cli/index.js +23 -24
- package/dist/cli/setup.js +295 -36
- package/dist/mcp-server/index.js +67 -0
- package/dist/mcp-server/routes/generateStory.js +323 -56
- package/dist/story-generator/componentBlacklist.js +181 -0
- package/dist/story-generator/componentDiscovery.js +9 -2
- package/dist/story-generator/configLoader.js +109 -39
- package/dist/story-generator/considerationsLoader.js +204 -0
- package/dist/story-generator/documentation-sources.js +36 -0
- package/dist/story-generator/documentationLoader.js +214 -0
- package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
- package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
- package/dist/story-generator/generateStory.js +7 -3
- package/dist/story-generator/postProcessStory.js +71 -0
- package/dist/story-generator/promptGenerator.js +286 -37
- package/dist/story-generator/storyHistory.js +118 -0
- package/dist/story-generator/storyTracker.js +33 -18
- package/dist/story-generator/storyValidator.js +39 -0
- package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
- package/dist/story-generator/validateStory.js +82 -7
- package/dist/story-ui.config.js +12 -5
- package/package.json +11 -6
- package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
- package/templates/StoryUI/StoryUIPanel.tsx +489 -359
- package/templates/react-import-rule.json +36 -0
- package/templates/story-generation-rules.json +29 -0
- package/templates/story-ui-considerations.json +156 -0
- package/templates/story-ui-considerations.md +109 -0
- package/templates/story-ui-docs-README.md +55 -0
- package/dist/scripts/test-validation.js +0 -81
- package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
- package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
/**
|
|
5
|
+
* Dynamically discovers what components are actually available in an installed package
|
|
6
|
+
*/
|
|
7
|
+
export class DynamicPackageDiscovery {
|
|
8
|
+
constructor(packageName, projectRoot = process.cwd()) {
|
|
9
|
+
this.packageName = packageName;
|
|
10
|
+
this.projectRoot = projectRoot;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get the real exports from the installed package
|
|
14
|
+
*/
|
|
15
|
+
async getRealPackageExports() {
|
|
16
|
+
try {
|
|
17
|
+
const packagePath = path.join(this.projectRoot, 'node_modules', this.packageName);
|
|
18
|
+
if (!fs.existsSync(packagePath)) {
|
|
19
|
+
console.warn(`Package ${this.packageName} not found in node_modules`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
// Get package version
|
|
23
|
+
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
24
|
+
let packageVersion = 'unknown';
|
|
25
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
26
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
27
|
+
packageVersion = packageJson.version || 'unknown';
|
|
28
|
+
}
|
|
29
|
+
// Try to require the package and inspect its exports
|
|
30
|
+
const packageExports = await this.requirePackage(this.packageName);
|
|
31
|
+
if (!packageExports) {
|
|
32
|
+
console.log(`🔄 Could not directly import ${this.packageName}, falling back to structure analysis`);
|
|
33
|
+
// Don't return null here - fall back to structure discovery
|
|
34
|
+
}
|
|
35
|
+
const components = [];
|
|
36
|
+
let allExports = [];
|
|
37
|
+
if (packageExports) {
|
|
38
|
+
// Successfully imported package - analyze exports
|
|
39
|
+
allExports = Object.keys(packageExports);
|
|
40
|
+
for (const exportName of allExports) {
|
|
41
|
+
const exportValue = packageExports[exportName];
|
|
42
|
+
const component = {
|
|
43
|
+
name: exportName,
|
|
44
|
+
isComponent: this.isLikelyComponent(exportName, exportValue),
|
|
45
|
+
type: this.getExportType(exportValue),
|
|
46
|
+
__componentPath: exportValue?.__componentPath
|
|
47
|
+
};
|
|
48
|
+
components.push(component);
|
|
49
|
+
}
|
|
50
|
+
// Check if we found any actual components
|
|
51
|
+
const componentCount = components.filter(c => c.isComponent).length;
|
|
52
|
+
console.log(`📋 Found ${componentCount} components in main ${this.packageName} export`);
|
|
53
|
+
// If no components found in main export, fall back to structure analysis
|
|
54
|
+
if (componentCount === 0) {
|
|
55
|
+
console.log(`🔄 No components in main export, falling back to structure analysis for ${this.packageName}...`);
|
|
56
|
+
const structureExports = this.discoverFromPackageStructure();
|
|
57
|
+
if (structureExports) {
|
|
58
|
+
const structureComponentNames = Object.keys(structureExports);
|
|
59
|
+
console.log(`📁 Structure analysis found ${structureComponentNames.length} components`);
|
|
60
|
+
// Replace with structure-discovered components
|
|
61
|
+
allExports = structureComponentNames;
|
|
62
|
+
components.length = 0; // Clear the array
|
|
63
|
+
for (const exportName of structureComponentNames) {
|
|
64
|
+
const structureExport = structureExports[exportName];
|
|
65
|
+
const component = {
|
|
66
|
+
name: exportName,
|
|
67
|
+
isComponent: true, // Assume true since we filtered in structure discovery
|
|
68
|
+
type: 'function',
|
|
69
|
+
__componentPath: structureExport?.__componentPath
|
|
70
|
+
};
|
|
71
|
+
components.push(component);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Failed to import - fall back to structure analysis
|
|
78
|
+
console.log(`📁 Import failed, analyzing package structure for ${this.packageName}...`);
|
|
79
|
+
const structureExports = this.discoverFromPackageStructure();
|
|
80
|
+
if (structureExports) {
|
|
81
|
+
allExports = Object.keys(structureExports);
|
|
82
|
+
for (const exportName of allExports) {
|
|
83
|
+
const structureExport = structureExports[exportName];
|
|
84
|
+
const component = {
|
|
85
|
+
name: exportName,
|
|
86
|
+
isComponent: true, // Assume true since we filtered in structure discovery
|
|
87
|
+
type: 'function',
|
|
88
|
+
__componentPath: structureExport?.__componentPath
|
|
89
|
+
};
|
|
90
|
+
components.push(component);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
console.log(`✅ Discovered ${components.filter(c => c.isComponent).length} components from ${this.packageName} v${packageVersion}`);
|
|
95
|
+
return {
|
|
96
|
+
components,
|
|
97
|
+
allExports,
|
|
98
|
+
packageVersion
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(`Failed to discover exports from ${this.packageName}:`, error);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Require the package safely
|
|
108
|
+
*/
|
|
109
|
+
async requirePackage(packageName) {
|
|
110
|
+
try {
|
|
111
|
+
// First try dynamic import (for ES modules)
|
|
112
|
+
try {
|
|
113
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
114
|
+
const module = await dynamicImport(packageName);
|
|
115
|
+
return module;
|
|
116
|
+
}
|
|
117
|
+
catch (importError) {
|
|
118
|
+
// Check if this is a CSS import error (common with compiled design systems)
|
|
119
|
+
const errorMessage = importError?.message || String(importError);
|
|
120
|
+
if (errorMessage.includes('.css:') || errorMessage.includes('Unexpected token')) {
|
|
121
|
+
console.log(`🔄 ${packageName}: CSS detected, using static analysis (normal for design systems)`);
|
|
122
|
+
return this.discoverFromPackageStructure();
|
|
123
|
+
}
|
|
124
|
+
// Fall back to require (for CommonJS)
|
|
125
|
+
// Create require from the project root's package.json to ensure correct module resolution
|
|
126
|
+
const projectPackageJson = path.join(this.projectRoot, 'package.json');
|
|
127
|
+
const require = createRequire(projectPackageJson);
|
|
128
|
+
return require(packageName);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Check if this is a CSS import error
|
|
133
|
+
const errorMessage = error?.message || String(error);
|
|
134
|
+
if (errorMessage.includes('.css:') || errorMessage.includes('Unexpected token')) {
|
|
135
|
+
console.log(`🔄 ${packageName}: CSS detected, using static analysis (normal for design systems)`);
|
|
136
|
+
return this.discoverFromPackageStructure();
|
|
137
|
+
}
|
|
138
|
+
if (errorMessage.includes('window is not defined')) {
|
|
139
|
+
console.log(`🔄 ${packageName}: Browser-only component, using static analysis`);
|
|
140
|
+
return this.discoverFromPackageStructure();
|
|
141
|
+
}
|
|
142
|
+
console.log(`📋 ${packageName}: Dynamic import failed, using static analysis`);
|
|
143
|
+
return this.discoverFromPackageStructure();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Determine if an export is likely a React component
|
|
148
|
+
*/
|
|
149
|
+
isLikelyComponent(name, value) {
|
|
150
|
+
// Skip obvious non-components
|
|
151
|
+
if (this.isUtilityExport(name)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// React components typically start with uppercase
|
|
155
|
+
if (!/^[A-Z]/.test(name)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// Check if it's a function or class (likely React component)
|
|
159
|
+
const type = typeof value;
|
|
160
|
+
if (type === 'function') {
|
|
161
|
+
// Additional checks for React components
|
|
162
|
+
return this.looksLikeReactComponent(name, value);
|
|
163
|
+
}
|
|
164
|
+
// Some components might be wrapped in objects
|
|
165
|
+
if (type === 'object' && value !== null) {
|
|
166
|
+
// Check if object has component-like properties
|
|
167
|
+
return this.hasComponentLikeProperties(value);
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if a name indicates a utility export (not a component)
|
|
173
|
+
*/
|
|
174
|
+
isUtilityExport(name) {
|
|
175
|
+
const utilityPatterns = [
|
|
176
|
+
/^use[A-Z]/, // hooks
|
|
177
|
+
/^create[A-Z]/, // factory functions
|
|
178
|
+
/^get[A-Z]/, // getter functions
|
|
179
|
+
/^set[A-Z]/, // setter functions
|
|
180
|
+
/^handle[A-Z]/, // handlers
|
|
181
|
+
/^on[A-Z]/, // event handlers
|
|
182
|
+
/Config$/,
|
|
183
|
+
/Provider$/,
|
|
184
|
+
/Context$/,
|
|
185
|
+
/^default$/,
|
|
186
|
+
/^DEFAULT_/,
|
|
187
|
+
/^SUPPORTED_/,
|
|
188
|
+
/^Key$/,
|
|
189
|
+
/^DATA_/,
|
|
190
|
+
/String$/,
|
|
191
|
+
/ToHex$/,
|
|
192
|
+
/ToRgb$/,
|
|
193
|
+
/ToHsl$/,
|
|
194
|
+
/ToHsb$/,
|
|
195
|
+
/_SECRET_/,
|
|
196
|
+
/Value$/
|
|
197
|
+
];
|
|
198
|
+
return utilityPatterns.some(pattern => pattern.test(name));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if a function looks like a React component
|
|
202
|
+
*/
|
|
203
|
+
looksLikeReactComponent(name, fn) {
|
|
204
|
+
// Must start with uppercase
|
|
205
|
+
if (!/^[A-Z]/.test(name)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
// Skip known utility functions
|
|
209
|
+
if (this.isUtilityExport(name)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
// Check function signature - React components typically take props
|
|
213
|
+
const fnString = fn.toString();
|
|
214
|
+
// Skip if it looks like a utility function
|
|
215
|
+
if (fnString.includes('function create') ||
|
|
216
|
+
fnString.includes('function get') ||
|
|
217
|
+
fnString.includes('function set') ||
|
|
218
|
+
fnString.includes('function use')) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Check if an object has component-like properties
|
|
225
|
+
*/
|
|
226
|
+
hasComponentLikeProperties(obj) {
|
|
227
|
+
// Some components are wrapped in objects with render methods or similar
|
|
228
|
+
return (typeof obj.render === 'function' ||
|
|
229
|
+
typeof obj.component === 'function' ||
|
|
230
|
+
typeof obj.Component === 'function');
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get the type of an export
|
|
234
|
+
*/
|
|
235
|
+
getExportType(value) {
|
|
236
|
+
const type = typeof value;
|
|
237
|
+
if (type === 'function') {
|
|
238
|
+
// Try to distinguish between function and class
|
|
239
|
+
const fnString = value.toString();
|
|
240
|
+
if (fnString.startsWith('class ') || /^function [A-Z]/.test(fnString)) {
|
|
241
|
+
return 'class';
|
|
242
|
+
}
|
|
243
|
+
return 'function';
|
|
244
|
+
}
|
|
245
|
+
if (type === 'object' && value !== null) {
|
|
246
|
+
return 'object';
|
|
247
|
+
}
|
|
248
|
+
return 'unknown';
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get only the component names that should be used for story generation
|
|
252
|
+
*/
|
|
253
|
+
async getAvailableComponentNames() {
|
|
254
|
+
const exports = await this.getRealPackageExports();
|
|
255
|
+
if (!exports) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
return exports.components
|
|
259
|
+
.filter(comp => comp.isComponent)
|
|
260
|
+
.map(comp => comp.name)
|
|
261
|
+
.sort();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Validate that a list of component names are actually available
|
|
265
|
+
*/
|
|
266
|
+
async validateComponentNames(componentNames) {
|
|
267
|
+
const availableComponents = await this.getAvailableComponentNames();
|
|
268
|
+
const availableSet = new Set(availableComponents);
|
|
269
|
+
const valid = [];
|
|
270
|
+
const invalid = [];
|
|
271
|
+
const suggestions = new Map();
|
|
272
|
+
for (const componentName of componentNames) {
|
|
273
|
+
if (availableSet.has(componentName)) {
|
|
274
|
+
valid.push(componentName);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
invalid.push(componentName);
|
|
278
|
+
// Try to find a similar component
|
|
279
|
+
const suggestion = this.findSimilarComponent(componentName, availableComponents);
|
|
280
|
+
if (suggestion) {
|
|
281
|
+
suggestions.set(componentName, suggestion);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { valid, invalid, suggestions };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Find a similar component name
|
|
289
|
+
*/
|
|
290
|
+
findSimilarComponent(targetName, availableComponents) {
|
|
291
|
+
const targetLower = targetName.toLowerCase();
|
|
292
|
+
// Direct substring matches
|
|
293
|
+
for (const available of availableComponents) {
|
|
294
|
+
const availableLower = available.toLowerCase();
|
|
295
|
+
if (availableLower.includes(targetLower) || targetLower.includes(availableLower)) {
|
|
296
|
+
return available;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Special case mappings for common mistakes
|
|
300
|
+
const commonMappings = {
|
|
301
|
+
'stack': ['BlockStack', 'InlineStack', 'LegacyStack'],
|
|
302
|
+
'layout': ['Layout', 'Box'],
|
|
303
|
+
'container': ['Box', 'Layout'],
|
|
304
|
+
'grid': ['Grid', 'InlineGrid'],
|
|
305
|
+
'text': ['Text'],
|
|
306
|
+
'button': ['Button'],
|
|
307
|
+
'card': ['Card', 'LegacyCard']
|
|
308
|
+
};
|
|
309
|
+
const mapping = commonMappings[targetLower];
|
|
310
|
+
if (mapping) {
|
|
311
|
+
for (const suggestion of mapping) {
|
|
312
|
+
if (availableComponents.includes(suggestion)) {
|
|
313
|
+
return suggestion;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Alternative discovery method when package imports fail due to CSS
|
|
321
|
+
* Analyzes package.json exports and TypeScript definitions
|
|
322
|
+
*/
|
|
323
|
+
discoverFromPackageStructure() {
|
|
324
|
+
try {
|
|
325
|
+
const packagePath = path.join(this.projectRoot, 'node_modules', this.packageName);
|
|
326
|
+
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
327
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
328
|
+
console.log(`📦 No package.json found for ${this.packageName}`);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
332
|
+
const exports = {};
|
|
333
|
+
// Method 1: Analyze package.json exports field
|
|
334
|
+
if (packageJson.exports) {
|
|
335
|
+
console.log(`📋 Analyzing exports field in ${this.packageName}/package.json`);
|
|
336
|
+
this.extractExportsFromPackageJson(packageJson.exports, exports);
|
|
337
|
+
}
|
|
338
|
+
// Method 2: Look for index.d.ts or main TypeScript declarations
|
|
339
|
+
const typingsPath = packageJson.types || packageJson.typings || './dist/types/index.d.ts';
|
|
340
|
+
const fullTypingsPath = path.join(packagePath, typingsPath);
|
|
341
|
+
if (fs.existsSync(fullTypingsPath)) {
|
|
342
|
+
console.log(`📋 Analyzing TypeScript declarations for ${this.packageName}`);
|
|
343
|
+
this.extractExportsFromTypeDefinitions(fullTypingsPath, exports);
|
|
344
|
+
}
|
|
345
|
+
// Method 3: Scan for component subdirectories (for packages like Base Web)
|
|
346
|
+
if (Object.keys(exports).length === 0) {
|
|
347
|
+
console.log(`📁 Scanning subdirectories for ${this.packageName} components...`);
|
|
348
|
+
this.scanComponentSubdirectories(packagePath, exports);
|
|
349
|
+
}
|
|
350
|
+
return Object.keys(exports).length > 0 ? exports : null;
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
console.warn(`Alternative discovery failed for ${this.packageName}:`, error);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Scan package subdirectories for components (e.g., antd/button, chakra-ui/input)
|
|
359
|
+
*/
|
|
360
|
+
scanComponentSubdirectories(packagePath, result) {
|
|
361
|
+
try {
|
|
362
|
+
console.log(`🔍 Scanning ${packagePath} for component subdirectories...`);
|
|
363
|
+
const entries = fs.readdirSync(packagePath, { withFileTypes: true });
|
|
364
|
+
console.log(`📁 Found ${entries.length} entries in ${packagePath}`);
|
|
365
|
+
let componentDirsFound = 0;
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
if (!entry.isDirectory())
|
|
368
|
+
continue;
|
|
369
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
370
|
+
continue;
|
|
371
|
+
componentDirsFound++;
|
|
372
|
+
const subdirPath = path.join(packagePath, entry.name);
|
|
373
|
+
const indexTypingsPath = path.join(subdirPath, 'index.d.ts');
|
|
374
|
+
// Check if this subdirectory has an index.d.ts (likely a component)
|
|
375
|
+
if (fs.existsSync(indexTypingsPath)) {
|
|
376
|
+
try {
|
|
377
|
+
const typingsContent = fs.readFileSync(indexTypingsPath, 'utf-8');
|
|
378
|
+
// Look for component exports (functions/classes starting with uppercase)
|
|
379
|
+
const componentExports = this.extractComponentsFromTypings(typingsContent);
|
|
380
|
+
if (componentExports.length > 0) {
|
|
381
|
+
console.log(`📦 Found ${componentExports.length} components in ${entry.name}/`);
|
|
382
|
+
// Add each component to the result
|
|
383
|
+
for (const componentName of componentExports) {
|
|
384
|
+
// Create a mock export function for this component
|
|
385
|
+
result[componentName] = () => { };
|
|
386
|
+
result[componentName].displayName = componentName;
|
|
387
|
+
result[componentName].__componentPath = `${this.packageName}/${entry.name}`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
// Skip this subdirectory if we can't read its typings
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
console.log(`✅ Scanned ${componentDirsFound} component directories for ${this.packageName}`);
|
|
398
|
+
console.log(`📦 Total components found in subdirectories: ${Object.keys(result).length}`);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.warn(`Failed to scan subdirectories for ${this.packageName}:`, error);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Extract component names from TypeScript declaration content
|
|
406
|
+
*/
|
|
407
|
+
extractComponentsFromTypings(content) {
|
|
408
|
+
const components = [];
|
|
409
|
+
// Look for export statements with component-like names
|
|
410
|
+
const exportRegex = /export\s+{\s*([^}]+)\s*}/g;
|
|
411
|
+
const defaultExportRegex = /export\s+{\s*default\s+as\s+(\w+)\s*}/g;
|
|
412
|
+
const namedExportRegex = /export\s+.*?\s+(\w+)\s*(?:,|$)/g;
|
|
413
|
+
let match;
|
|
414
|
+
// Extract from export { ... } statements
|
|
415
|
+
while ((match = exportRegex.exec(content)) !== null) {
|
|
416
|
+
const exportsList = match[1];
|
|
417
|
+
const exports = exportsList.split(',').map(e => e.trim());
|
|
418
|
+
for (const exp of exports) {
|
|
419
|
+
// Handle "default as ComponentName" pattern
|
|
420
|
+
const defaultAsMatch = exp.match(/default\s+as\s+(\w+)/);
|
|
421
|
+
if (defaultAsMatch) {
|
|
422
|
+
const componentName = defaultAsMatch[1];
|
|
423
|
+
if (this.isComponentName(componentName)) {
|
|
424
|
+
components.push(componentName);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// Handle regular export names
|
|
429
|
+
const cleanName = exp.replace(/\s+as\s+\w+/, '').trim();
|
|
430
|
+
if (this.isComponentName(cleanName)) {
|
|
431
|
+
components.push(cleanName);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return [...new Set(components)]; // Remove duplicates
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Check if a name looks like a React component
|
|
440
|
+
*/
|
|
441
|
+
isComponentName(name) {
|
|
442
|
+
// Must start with uppercase letter
|
|
443
|
+
if (!/^[A-Z]/.test(name))
|
|
444
|
+
return false;
|
|
445
|
+
// Skip constants and utilities
|
|
446
|
+
if (name.toUpperCase() === name)
|
|
447
|
+
return false; // ALL_CAPS constants
|
|
448
|
+
if (name.startsWith('Styled'))
|
|
449
|
+
return false; // Styled components (usually internal)
|
|
450
|
+
if (name.endsWith('Provider'))
|
|
451
|
+
return false; // Context providers
|
|
452
|
+
if (name.endsWith('Context'))
|
|
453
|
+
return false; // React contexts
|
|
454
|
+
if (name.endsWith('Type') || name.endsWith('Types'))
|
|
455
|
+
return false; // Type definitions
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Extract component exports from package.json exports field
|
|
460
|
+
*/
|
|
461
|
+
extractExportsFromPackageJson(exportsField, result) {
|
|
462
|
+
if (typeof exportsField === 'string') {
|
|
463
|
+
// Simple export like "./dist/index.js"
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (typeof exportsField === 'object') {
|
|
467
|
+
for (const [key, value] of Object.entries(exportsField)) {
|
|
468
|
+
if (key === '.' || key === './index') {
|
|
469
|
+
// Main export - we'll analyze this elsewhere
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (key.startsWith('./') && !key.includes('*')) {
|
|
473
|
+
// Named export like "./Button" or "./components/Button"
|
|
474
|
+
const componentName = key.replace('./', '').split('/').pop();
|
|
475
|
+
if (componentName && /^[A-Z]/.test(componentName)) {
|
|
476
|
+
result[componentName] = `Component_${componentName}`;
|
|
477
|
+
console.log(`📍 Found component export: ${componentName}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Extract component declarations from TypeScript definition files
|
|
485
|
+
*/
|
|
486
|
+
extractExportsFromTypeDefinitions(typingsPath, result) {
|
|
487
|
+
try {
|
|
488
|
+
const content = fs.readFileSync(typingsPath, 'utf-8');
|
|
489
|
+
// Look for export declarations like:
|
|
490
|
+
// export declare const Button: ...
|
|
491
|
+
// export default Button
|
|
492
|
+
// export { Button }
|
|
493
|
+
const exportPatterns = [
|
|
494
|
+
/export\s+declare\s+const\s+([A-Z][a-zA-Z0-9]+)/g,
|
|
495
|
+
/export\s+declare\s+function\s+([A-Z][a-zA-Z0-9]+)/g,
|
|
496
|
+
/export\s+\{\s*([A-Z][a-zA-Z0-9, ]+)\s*\}/g,
|
|
497
|
+
/export\s+default\s+([A-Z][a-zA-Z0-9]+)/g,
|
|
498
|
+
];
|
|
499
|
+
for (const pattern of exportPatterns) {
|
|
500
|
+
let match;
|
|
501
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
502
|
+
const componentName = match[1];
|
|
503
|
+
if (componentName && /^[A-Z]/.test(componentName)) {
|
|
504
|
+
result[componentName] = `Component_${componentName}`;
|
|
505
|
+
console.log(`📍 Found component in .d.ts: ${componentName}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
console.warn(`Could not read TypeScript definitions: ${error}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Create a dynamic discovery instance for a package
|
|
517
|
+
*/
|
|
518
|
+
export function createDynamicDiscovery(packageName, projectRoot) {
|
|
519
|
+
return new DynamicPackageDiscovery(packageName, projectRoot);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Quick function to get available components for a package
|
|
523
|
+
*/
|
|
524
|
+
export async function getPackageComponents(packageName, projectRoot) {
|
|
525
|
+
const discovery = createDynamicDiscovery(packageName, projectRoot);
|
|
526
|
+
return await discovery.getAvailableComponentNames();
|
|
527
|
+
}
|