@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.
Files changed (35) hide show
  1. package/.env.sample +3 -1
  2. package/README.md +160 -606
  3. package/dist/cli/index.js +23 -24
  4. package/dist/cli/setup.js +295 -36
  5. package/dist/mcp-server/index.js +67 -0
  6. package/dist/mcp-server/routes/generateStory.js +323 -56
  7. package/dist/story-generator/componentBlacklist.js +181 -0
  8. package/dist/story-generator/componentDiscovery.js +9 -2
  9. package/dist/story-generator/configLoader.js +109 -39
  10. package/dist/story-generator/considerationsLoader.js +204 -0
  11. package/dist/story-generator/documentation-sources.js +36 -0
  12. package/dist/story-generator/documentationLoader.js +214 -0
  13. package/dist/story-generator/dynamicPackageDiscovery.js +527 -0
  14. package/dist/story-generator/enhancedComponentDiscovery.js +369 -118
  15. package/dist/story-generator/generateStory.js +7 -3
  16. package/dist/story-generator/postProcessStory.js +71 -0
  17. package/dist/story-generator/promptGenerator.js +286 -37
  18. package/dist/story-generator/storyHistory.js +118 -0
  19. package/dist/story-generator/storyTracker.js +33 -18
  20. package/dist/story-generator/storyValidator.js +39 -0
  21. package/dist/story-generator/universalDesignSystemAdapter.js +209 -0
  22. package/dist/story-generator/validateStory.js +82 -7
  23. package/dist/story-ui.config.js +12 -5
  24. package/package.json +11 -6
  25. package/templates/StoryUI/StoryUIPanel.stories.tsx +29 -13
  26. package/templates/StoryUI/StoryUIPanel.tsx +489 -359
  27. package/templates/react-import-rule.json +36 -0
  28. package/templates/story-generation-rules.json +29 -0
  29. package/templates/story-ui-considerations.json +156 -0
  30. package/templates/story-ui-considerations.md +109 -0
  31. package/templates/story-ui-docs-README.md +55 -0
  32. package/dist/scripts/test-validation.js +0 -81
  33. package/dist/test-storybooks/chakra-test/src/components/index.js +0 -3
  34. package/dist/test-storybooks/custom-design-test/src/components/index.js +0 -3
  35. 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
+ }