@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
@@ -1,15 +1,21 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { DynamicPackageDiscovery } from './dynamicPackageDiscovery.js';
3
4
  export class EnhancedComponentDiscovery {
4
5
  constructor(config) {
5
6
  this.discoveredComponents = new Map();
7
+ this.validateAvailableComponents = new Set();
6
8
  this.config = config;
7
9
  }
8
10
  /**
9
11
  * Discover components from all available sources
12
+ * Priority: 1. Dynamic Discovery 2. Static Lists 3. Manual Config
10
13
  */
11
14
  async discoverAll() {
15
+ console.log('🔍 Starting comprehensive component discovery...');
16
+ // Step 1: Discover from all sources
12
17
  const sources = this.identifySources();
18
+ console.log(`📁 Found ${sources.length} discovery sources:`, sources.map(s => `${s.type}:${s.path}`));
13
19
  for (const source of sources) {
14
20
  try {
15
21
  switch (source.type) {
@@ -31,23 +37,129 @@ export class EnhancedComponentDiscovery {
31
37
  console.warn(`Failed to discover from ${source.type} at ${source.path}:`, error);
32
38
  }
33
39
  }
34
- // Apply any manual configurations
40
+ // Step 2: Apply manual configurations as override/fallback
35
41
  this.applyManualConfigurations();
36
- return Array.from(this.discoveredComponents.values());
42
+ // Step 3: Resolve component conflicts and apply prioritization
43
+ this.resolveComponentConflicts();
44
+ const finalComponents = Array.from(this.discoveredComponents.values());
45
+ console.log(`✅ Discovery complete: ${finalComponents.length} components found`);
46
+ // Log summary by source type
47
+ this.logDiscoverySummary(finalComponents);
48
+ return finalComponents;
49
+ }
50
+ /**
51
+ * Resolve naming conflicts between different sources
52
+ * Priority: Local > Manual Config > npm packages
53
+ */
54
+ resolveComponentConflicts() {
55
+ const conflicts = new Map();
56
+ // Group components by name to find conflicts
57
+ for (const component of this.discoveredComponents.values()) {
58
+ const name = component.name;
59
+ if (!conflicts.has(name)) {
60
+ conflicts.set(name, []);
61
+ }
62
+ conflicts.get(name).push(component);
63
+ }
64
+ // Resolve conflicts using priority system
65
+ for (const [name, componentList] of conflicts) {
66
+ if (componentList.length > 1) {
67
+ console.log(`⚠️ Resolving conflict for component "${name}" (${componentList.length} versions found)`);
68
+ // Priority order: local > manual config > npm
69
+ const prioritized = componentList.sort((a, b) => {
70
+ const getPriority = (comp) => {
71
+ if (comp.source.type === 'local')
72
+ return 1; // Highest priority
73
+ if (comp.source.type === 'npm')
74
+ return 2;
75
+ return 3; // Lowest priority for others
76
+ };
77
+ return getPriority(a) - getPriority(b);
78
+ });
79
+ // Keep highest priority, remove others
80
+ const winner = prioritized[0];
81
+ const losers = prioritized.slice(1);
82
+ for (const loser of losers) {
83
+ this.discoveredComponents.delete(loser.name);
84
+ }
85
+ console.log(`✅ Kept ${winner.source.type} version of "${name}" from ${winner.source.path}`);
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Log discovery summary for debugging
91
+ */
92
+ logDiscoverySummary(components) {
93
+ const summary = components.reduce((acc, comp) => {
94
+ const sourceType = comp.source.type;
95
+ if (!acc[sourceType])
96
+ acc[sourceType] = 0;
97
+ acc[sourceType]++;
98
+ return acc;
99
+ }, {});
100
+ console.log('📊 Component discovery summary:', summary);
101
+ }
102
+ /**
103
+ * Get the project root directory from the config
104
+ */
105
+ getProjectRoot() {
106
+ // If generatedStoriesPath exists, use it to determine project root
107
+ if (this.config.generatedStoriesPath) {
108
+ // Go up from src/stories/generated to find project root
109
+ let currentPath = path.resolve(this.config.generatedStoriesPath);
110
+ // Keep going up until we find a package.json
111
+ while (currentPath !== path.dirname(currentPath)) {
112
+ if (fs.existsSync(path.join(currentPath, 'package.json'))) {
113
+ return currentPath;
114
+ }
115
+ currentPath = path.dirname(currentPath);
116
+ }
117
+ }
118
+ // Fallback to current working directory
119
+ return process.cwd();
37
120
  }
38
121
  /**
39
122
  * Identify all potential component sources
40
123
  */
41
124
  identifySources() {
42
125
  const sources = [];
126
+ // Note: Auto-discovery removed - now using guided installation during init
43
127
  // Check for npm packages
128
+ // Always run dynamic discovery for design systems
44
129
  if (this.config.importPath && !this.config.importPath.startsWith('.')) {
45
130
  sources.push({
46
131
  type: 'npm',
47
132
  path: this.config.importPath
48
133
  });
49
134
  }
135
+ // Also discover from layout components if specified
136
+ if (this.config.layoutComponents && this.config.layoutComponents.length > 0) {
137
+ const layoutImportPaths = new Set();
138
+ for (const layoutComp of this.config.layoutComponents) {
139
+ if (layoutComp.importPath && !layoutComp.importPath.startsWith('.')) {
140
+ layoutImportPaths.add(layoutComp.importPath);
141
+ }
142
+ }
143
+ for (const layoutPath of layoutImportPaths) {
144
+ sources.push({
145
+ type: 'npm',
146
+ path: layoutPath
147
+ });
148
+ }
149
+ }
150
+ // Check for design system preferred components
151
+ if (this.config.designSystemGuidelines?.preferredComponents) {
152
+ for (const [category, packagePath] of Object.entries(this.config.designSystemGuidelines.preferredComponents)) {
153
+ if (typeof packagePath === 'string' && !packagePath.startsWith('.')) {
154
+ sources.push({
155
+ type: 'npm',
156
+ path: packagePath
157
+ });
158
+ }
159
+ }
160
+ }
50
161
  // Check for local component directories
162
+ // 1. Manually configured componentsPath (highest priority)
51
163
  if (this.config.componentsPath && fs.existsSync(this.config.componentsPath)) {
52
164
  sources.push({
53
165
  type: 'local',
@@ -55,8 +167,39 @@ export class EnhancedComponentDiscovery {
55
167
  patterns: ['*.tsx', '*.jsx', '*.ts', '*.js']
56
168
  });
57
169
  }
170
+ // 2. Auto-discover common React component directories from project root
171
+ const projectRoot = this.getProjectRoot();
172
+ const commonComponentDirs = [
173
+ 'src/components',
174
+ 'src/ui',
175
+ 'components',
176
+ 'ui',
177
+ 'src/lib/components',
178
+ 'lib/components',
179
+ 'src/shared/components',
180
+ 'shared/components'
181
+ ];
182
+ for (const dir of commonComponentDirs) {
183
+ const fullPath = path.join(projectRoot, dir);
184
+ if (fs.existsSync(fullPath) && fullPath !== this.config.componentsPath) {
185
+ sources.push({
186
+ type: 'local',
187
+ path: fullPath,
188
+ patterns: ['*.tsx', '*.jsx', '*.ts', '*.js']
189
+ });
190
+ }
191
+ }
192
+ // 3. Scan alongside stories in src/stories directory (co-located components)
193
+ const storiesDir = path.join(projectRoot, 'src/stories');
194
+ if (fs.existsSync(storiesDir)) {
195
+ sources.push({
196
+ type: 'local',
197
+ path: storiesDir,
198
+ patterns: ['*.tsx', '*.jsx'] // Only component files, not story files
199
+ });
200
+ }
58
201
  // Check for TypeScript definitions
59
- const nodeModulesPath = path.join(process.cwd(), 'node_modules');
202
+ const nodeModulesPath = path.join(projectRoot, 'node_modules');
60
203
  if (this.config.importPath && fs.existsSync(nodeModulesPath)) {
61
204
  const packagePath = path.join(nodeModulesPath, this.config.importPath);
62
205
  const typesPath = path.join(nodeModulesPath, '@types', this.config.importPath.replace(/^@/, '').replace('/', '__'));
@@ -76,29 +219,57 @@ export class EnhancedComponentDiscovery {
76
219
  return sources;
77
220
  }
78
221
  /**
79
- * Discover components from npm packages
222
+ * Auto-discovery removed - now handled by guided installation during init
223
+ * This function is kept for backward compatibility but does nothing
224
+ */
225
+ addDesignSystemPackages(sources) {
226
+ // Functionality moved to guided installation process
227
+ }
228
+ /**
229
+ * Check if a package is likely to contain React components (not utilities, types, etc.)
80
230
  */
231
+ isLikelyComponentPackage(packageName) {
232
+ const name = packageName.toLowerCase();
233
+ // Skip obvious utility packages
234
+ const utilityPatterns = [
235
+ 'types',
236
+ 'utils', 'util', 'utilities',
237
+ 'helpers', 'constants', 'config',
238
+ 'analytics', 'tracking', 'metrics',
239
+ 'tokens', 'theme', 'styles', 'css',
240
+ 'icons', 'icon', // Icons are usually too numerous and specific
241
+ 'editor-', // Editor plugins are usually too specific
242
+ 'smart-card', // Requires SmartCardProvider wrapper - too complex for simple stories
243
+ '-types', '-utils', '-constants',
244
+ 'babel-', 'webpack-', 'rollup-', 'eslint-',
245
+ 'test', 'mock', 'fixture', 'storybook',
246
+ 'codemod', 'migration',
247
+ 'build', 'dev', 'cli'
248
+ ];
249
+ // Skip if contains utility patterns
250
+ if (utilityPatterns.some(pattern => name.includes(pattern))) {
251
+ return false;
252
+ }
253
+ return true;
254
+ }
255
+ /**
256
+ * Discover components from npm packages using dynamic runtime discovery
257
+ */
81
258
  async discoverFromNpmPackage(source) {
82
- const packagePath = path.join(process.cwd(), 'node_modules', source.path);
259
+ // Determine the project root from the generated stories path
260
+ const projectRoot = this.getProjectRoot();
261
+ const packagePath = path.join(projectRoot, 'node_modules', source.path);
83
262
  if (!fs.existsSync(packagePath)) {
263
+ console.warn(`Package ${source.path} not found in node_modules at ${packagePath}`);
84
264
  return;
85
265
  }
86
- // Read package.json to find entry points
87
- const packageJsonPath = path.join(packagePath, 'package.json');
88
- if (fs.existsSync(packageJsonPath)) {
89
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
90
- // Try to find component exports
91
- const possiblePaths = [
92
- packageJson.main,
93
- packageJson.module,
94
- packageJson.exports?.['.'],
95
- 'index.js',
96
- 'index.ts',
97
- 'lib/index.js',
98
- 'dist/index.js',
99
- 'es/index.js'
100
- ].filter(Boolean);
101
- // For known design systems, use predefined component lists
266
+ console.log(`🔍 Dynamically discovering components from ${source.path}...`);
267
+ // Use dynamic discovery to get real exports
268
+ const dynamicDiscovery = new DynamicPackageDiscovery(source.path, projectRoot);
269
+ const packageExports = await dynamicDiscovery.getRealPackageExports();
270
+ if (!packageExports) {
271
+ console.log(`📋 ${source.path}: Using static component list (design system detected)`);
272
+ // Fallback to predefined components if dynamic discovery fails
102
273
  const knownComponents = this.getKnownDesignSystemComponents(source.path);
103
274
  if (knownComponents.length > 0) {
104
275
  for (const comp of knownComponents) {
@@ -109,110 +280,41 @@ export class EnhancedComponentDiscovery {
109
280
  category: comp.category || this.categorizeComponent(comp.name || '', comp.description || '')
110
281
  });
111
282
  }
112
- return;
113
- }
114
- // Try dynamic import for modern packages
115
- for (const entryPath of possiblePaths) {
116
- const fullPath = path.join(packagePath, entryPath);
117
- if (fs.existsSync(fullPath)) {
118
- try {
119
- // This is where we'd ideally do dynamic import, but for safety
120
- // we'll rely on known patterns and TypeScript definitions
121
- console.log(`Found entry point: ${fullPath}`);
122
- }
123
- catch (error) {
124
- // Silent fail - will try other methods
125
- }
126
- }
127
283
  }
284
+ return;
285
+ }
286
+ // Process the real components found in the package
287
+ const realComponents = packageExports.components.filter(comp => comp.isComponent);
288
+ console.log(`✅ Found ${realComponents.length} real components in ${source.path} v${packageExports.packageVersion}`);
289
+ console.log(`📦 Available components: ${realComponents.map(c => c.name).join(', ')}`);
290
+ for (const realComp of realComponents) {
291
+ // Get enhanced metadata from predefined list if available
292
+ const knownComponents = this.getKnownDesignSystemComponents(source.path);
293
+ const knownComp = knownComponents.find(k => k.name === realComp.name);
294
+ this.discoveredComponents.set(realComp.name, {
295
+ name: realComp.name,
296
+ source,
297
+ filePath: '',
298
+ // Use known metadata if available, otherwise generate basic metadata
299
+ description: knownComp?.description || `${realComp.name} component`,
300
+ category: knownComp?.category || this.categorizeComponent(realComp.name, ''),
301
+ props: knownComp?.props || [],
302
+ slots: knownComp?.slots || [],
303
+ examples: knownComp?.examples || [],
304
+ __componentPath: realComp.__componentPath
305
+ });
128
306
  }
307
+ // Store the component names for validation
308
+ this.validateAvailableComponents = new Set(realComponents.map(c => c.name));
129
309
  }
130
310
  /**
131
311
  * Get known components for popular design systems
312
+ * Returns empty array to rely on dynamic discovery
132
313
  */
133
314
  getKnownDesignSystemComponents(packageName) {
134
- const components = [];
135
- switch (packageName) {
136
- case 'antd':
137
- case 'ant-design':
138
- return [
139
- // Layout
140
- { name: 'Layout', category: 'layout', description: 'Main layout wrapper' },
141
- { name: 'Row', category: 'layout', description: 'Grid row for layouts' },
142
- { name: 'Col', category: 'layout', description: 'Grid column for layouts' },
143
- { name: 'Grid', category: 'layout', description: 'Grid layout component' },
144
- { name: 'Space', category: 'layout', description: 'Spacing component' },
145
- { name: 'Divider', category: 'layout', description: 'Divider line' },
146
- // Data Display
147
- { name: 'Table', category: 'content', description: 'Data table', props: ['dataSource', 'columns', 'pagination', 'loading'] },
148
- { name: 'Card', category: 'content', description: 'Card container', props: ['title', 'extra', 'loading', 'bordered'] },
149
- { name: 'Statistic', category: 'content', description: 'Statistical display', props: ['title', 'value', 'prefix', 'suffix'] },
150
- { name: 'List', category: 'content', description: 'List display', props: ['dataSource', 'renderItem', 'loading'] },
151
- { name: 'Badge', category: 'content', description: 'Badge for status', props: ['count', 'dot', 'status'] },
152
- { name: 'Tag', category: 'content', description: 'Tag label', props: ['color', 'closable', 'icon'] },
153
- { name: 'Avatar', category: 'content', description: 'User avatar', props: ['src', 'size', 'shape', 'icon'] },
154
- { name: 'Progress', category: 'content', description: 'Progress bar', props: ['percent', 'status', 'type'] },
155
- // Form
156
- { name: 'Form', category: 'form', description: 'Form container', props: ['layout', 'onFinish', 'initialValues'] },
157
- { name: 'Input', category: 'form', description: 'Text input', props: ['placeholder', 'value', 'onChange', 'size'] },
158
- { name: 'Select', category: 'form', description: 'Select dropdown', props: ['options', 'value', 'onChange', 'placeholder'] },
159
- { name: 'Button', category: 'form', description: 'Button', props: ['type', 'size', 'loading', 'icon', 'onClick'] },
160
- { name: 'Switch', category: 'form', description: 'Toggle switch', props: ['checked', 'onChange', 'size'] },
161
- { name: 'DatePicker', category: 'form', description: 'Date picker', props: ['value', 'onChange', 'format'] },
162
- // Feedback
163
- { name: 'Alert', category: 'feedback', description: 'Alert message', props: ['message', 'type', 'showIcon', 'closable'] },
164
- { name: 'Modal', category: 'feedback', description: 'Modal dialog', props: ['title', 'visible', 'onOk', 'onCancel'] },
165
- { name: 'Tooltip', category: 'feedback', description: 'Tooltip', props: ['title', 'placement'] },
166
- { name: 'Dropdown', category: 'feedback', description: 'Dropdown menu', props: ['menu', 'placement', 'trigger'] },
167
- // Navigation
168
- { name: 'Menu', category: 'navigation', description: 'Navigation menu', props: ['items', 'mode', 'selectedKeys'] },
169
- { name: 'Tabs', category: 'navigation', description: 'Tabbed navigation', props: ['items', 'activeKey', 'onChange'] },
170
- { name: 'Breadcrumb', category: 'navigation', description: 'Breadcrumb navigation', props: ['items'] },
171
- { name: 'Pagination', category: 'navigation', description: 'Pagination', props: ['current', 'total', 'pageSize', 'onChange'] }
172
- ];
173
- case '@mui/material':
174
- return [
175
- // Layout
176
- { name: 'Box', category: 'layout', description: 'Basic layout box' },
177
- { name: 'Container', category: 'layout', description: 'Responsive container' },
178
- { name: 'Grid', category: 'layout', description: 'Grid layout', props: ['container', 'item', 'xs', 'sm', 'md', 'lg', 'xl'] },
179
- { name: 'Stack', category: 'layout', description: 'Stack layout' },
180
- // Surfaces
181
- { name: 'Card', category: 'content', description: 'Card surface' },
182
- { name: 'CardContent', category: 'content', description: 'Card content area' },
183
- { name: 'Paper', category: 'content', description: 'Paper surface' },
184
- // Data Display
185
- { name: 'Typography', category: 'content', description: 'Text typography', props: ['variant', 'component'] },
186
- { name: 'Table', category: 'content', description: 'Data table' },
187
- { name: 'Chip', category: 'content', description: 'Chip component' },
188
- // Inputs
189
- { name: 'Button', category: 'form', description: 'Button', props: ['variant', 'color', 'size'] },
190
- { name: 'TextField', category: 'form', description: 'Text input', props: ['label', 'variant', 'value', 'onChange'] },
191
- { name: 'Select', category: 'form', description: 'Select dropdown' },
192
- { name: 'Switch', category: 'form', description: 'Toggle switch' }
193
- ];
194
- case '@chakra-ui/react':
195
- return [
196
- // Layout
197
- { name: 'Box', category: 'layout', description: 'Basic layout box' },
198
- { name: 'Flex', category: 'layout', description: 'Flexbox layout' },
199
- { name: 'Grid', category: 'layout', description: 'CSS Grid layout' },
200
- { name: 'SimpleGrid', category: 'layout', description: 'Simple grid layout', props: ['columns', 'spacing'] },
201
- { name: 'Stack', category: 'layout', description: 'Stack layout', props: ['direction', 'spacing'] },
202
- { name: 'HStack', category: 'layout', description: 'Horizontal stack' },
203
- { name: 'VStack', category: 'layout', description: 'Vertical stack' },
204
- // Content
205
- { name: 'Card', category: 'content', description: 'Card container' },
206
- { name: 'Text', category: 'content', description: 'Text component' },
207
- { name: 'Heading', category: 'content', description: 'Heading text' },
208
- { name: 'Badge', category: 'content', description: 'Badge component' },
209
- // Form
210
- { name: 'Button', category: 'form', description: 'Button', props: ['colorScheme', 'size', 'variant'] },
211
- { name: 'Input', category: 'form', description: 'Text input' },
212
- { name: 'Select', category: 'form', description: 'Select dropdown' }
213
- ];
214
- }
215
- return components;
315
+ // Return empty array to rely purely on dynamic component discovery
316
+ // This ensures we test the actual package scanning capabilities
317
+ return [];
216
318
  }
217
319
  /**
218
320
  * Discover components from local files
@@ -223,9 +325,17 @@ export class EnhancedComponentDiscovery {
223
325
  }
224
326
  const files = this.findComponentFiles(source.path, source.patterns || ['*.tsx', '*.jsx']);
225
327
  for (const file of files) {
328
+ // Skip story files, test files, and other non-component files
329
+ if (this.isNonComponentFile(file)) {
330
+ continue;
331
+ }
226
332
  const content = fs.readFileSync(file, 'utf-8');
227
333
  const componentName = this.extractComponentName(file, content);
228
334
  if (componentName && !this.discoveredComponents.has(componentName)) {
335
+ // Skip Story UI components and other internal components
336
+ if (this.shouldSkipComponent(componentName, content)) {
337
+ continue;
338
+ }
229
339
  const props = this.extractPropsFromFile(content);
230
340
  this.discoveredComponents.set(componentName, {
231
341
  name: componentName,
@@ -240,6 +350,40 @@ export class EnhancedComponentDiscovery {
240
350
  }
241
351
  }
242
352
  }
353
+ /**
354
+ * Check if a file should be skipped (stories, tests, etc.)
355
+ */
356
+ isNonComponentFile(filePath) {
357
+ const fileName = path.basename(filePath);
358
+ const skipPatterns = [
359
+ /\.stories?\.(tsx?|jsx?)$/i, // Story files
360
+ /\.test\.(tsx?|jsx?)$/i, // Test files
361
+ /\.spec\.(tsx?|jsx?)$/i, // Spec files
362
+ /\.d\.ts$/i, // Type definition files
363
+ /index\.(tsx?|jsx?)$/i, // Index files (usually just exports)
364
+ /\.config\.(tsx?|jsx?)$/i, // Config files
365
+ /\.mock\.(tsx?|jsx?)$/i, // Mock files
366
+ ];
367
+ return skipPatterns.some(pattern => pattern.test(fileName));
368
+ }
369
+ /**
370
+ * Check if a component should be skipped based on name or content
371
+ */
372
+ shouldSkipComponent(componentName, content) {
373
+ // Skip Story UI components
374
+ if (componentName === 'StoryUIPanel' || componentName.startsWith('StoryUI')) {
375
+ return true;
376
+ }
377
+ // Skip components that look like story exports
378
+ if (componentName.endsWith('Story') || componentName.endsWith('Example') || componentName.endsWith('Demo')) {
379
+ return true;
380
+ }
381
+ // Skip if content indicates it's not a proper component (e.g., just exports)
382
+ if (content.includes('export default meta') || content.includes('satisfies Meta')) {
383
+ return true;
384
+ }
385
+ return false;
386
+ }
243
387
  /**
244
388
  * Find component files recursively
245
389
  */
@@ -329,6 +473,9 @@ export class EnhancedComponentDiscovery {
329
473
  * Categorize component based on name and content
330
474
  */
331
475
  categorizeComponent(name, content) {
476
+ if (!name || typeof name !== 'string') {
477
+ return 'other';
478
+ }
332
479
  const nameLower = name.toLowerCase();
333
480
  // Layout components
334
481
  if (/^(layout|grid|row|col|column|container|box|flex|stack|section|wrapper|panel)/.test(nameLower)) {
@@ -413,6 +560,7 @@ export class EnhancedComponentDiscovery {
413
560
  * Apply manual component configurations
414
561
  */
415
562
  applyManualConfigurations() {
563
+ // Add main components from config
416
564
  if (this.config.components && Array.isArray(this.config.components)) {
417
565
  for (const comp of this.config.components) {
418
566
  const existing = this.discoveredComponents.get(comp.name);
@@ -431,5 +579,108 @@ export class EnhancedComponentDiscovery {
431
579
  });
432
580
  }
433
581
  }
582
+ // Add layout components from config
583
+ if (this.config.layoutComponents && Array.isArray(this.config.layoutComponents)) {
584
+ for (const comp of this.config.layoutComponents) {
585
+ const existing = this.discoveredComponents.get(comp.name);
586
+ this.discoveredComponents.set(comp.name, {
587
+ name: comp.name,
588
+ filePath: '',
589
+ props: comp.props || existing?.props || [],
590
+ source: {
591
+ type: 'custom-elements',
592
+ path: 'manual-config'
593
+ },
594
+ description: comp.description || existing?.description || `${comp.name} component`,
595
+ category: comp.category || existing?.category || this.categorizeComponent(comp.name, ''),
596
+ slots: comp.slots || existing?.slots || [],
597
+ examples: comp.examples || existing?.examples || []
598
+ });
599
+ }
600
+ }
601
+ }
602
+ /**
603
+ * Validate that component names actually exist in the discovered package
604
+ */
605
+ async validateComponentNames(componentNames) {
606
+ // If we have real component validation data, use it
607
+ if (this.validateAvailableComponents.size > 0) {
608
+ const valid = [];
609
+ const invalid = [];
610
+ const suggestions = new Map();
611
+ for (const componentName of componentNames) {
612
+ if (this.validateAvailableComponents.has(componentName)) {
613
+ valid.push(componentName);
614
+ }
615
+ else {
616
+ invalid.push(componentName);
617
+ // Find a similar component
618
+ const suggestion = this.findSimilarComponent(componentName, Array.from(this.validateAvailableComponents));
619
+ if (suggestion) {
620
+ suggestions.set(componentName, suggestion);
621
+ }
622
+ }
623
+ }
624
+ return { valid, invalid, suggestions };
625
+ }
626
+ // Fallback to discovered components if no validation set
627
+ const discovered = Array.from(this.discoveredComponents.keys());
628
+ const valid = componentNames.filter(name => this.discoveredComponents.has(name));
629
+ const invalid = componentNames.filter(name => !this.discoveredComponents.has(name));
630
+ const suggestions = new Map();
631
+ for (const invalidName of invalid) {
632
+ const suggestion = this.findSimilarComponent(invalidName, discovered);
633
+ if (suggestion) {
634
+ suggestions.set(invalidName, suggestion);
635
+ }
636
+ }
637
+ return { valid, invalid, suggestions };
638
+ }
639
+ /**
640
+ * Find a similar component name
641
+ */
642
+ findSimilarComponent(targetName, availableComponents) {
643
+ if (!targetName || typeof targetName !== 'string') {
644
+ return null;
645
+ }
646
+ const targetLower = targetName.toLowerCase();
647
+ // Direct substring matches
648
+ for (const available of availableComponents) {
649
+ if (!available || typeof available !== 'string') {
650
+ continue;
651
+ }
652
+ const availableLower = available.toLowerCase();
653
+ if (availableLower.includes(targetLower) || targetLower.includes(availableLower)) {
654
+ return available;
655
+ }
656
+ }
657
+ // Special case mappings for common mistakes
658
+ const commonMappings = {
659
+ 'stack': ['BlockStack', 'InlineStack', 'LegacyStack'],
660
+ 'layout': ['Layout', 'Box'],
661
+ 'container': ['Box', 'Layout'],
662
+ 'grid': ['Grid', 'InlineGrid'],
663
+ 'text': ['Text'],
664
+ 'button': ['Button'],
665
+ 'card': ['Card', 'LegacyCard']
666
+ };
667
+ const mapping = commonMappings[targetLower];
668
+ if (mapping) {
669
+ for (const suggestion of mapping) {
670
+ if (availableComponents.includes(suggestion)) {
671
+ return suggestion;
672
+ }
673
+ }
674
+ }
675
+ return null;
676
+ }
677
+ /**
678
+ * Get the available component names for validation
679
+ */
680
+ getAvailableComponentNames() {
681
+ if (this.validateAvailableComponents.size > 0) {
682
+ return Array.from(this.validateAvailableComponents).sort();
683
+ }
684
+ return Array.from(this.discoveredComponents.keys()).sort();
434
685
  }
435
686
  }
@@ -1,14 +1,18 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { STORY_UI_CONFIG } from '../story-ui.config.js';
4
3
  function slugify(str) {
5
4
  return str
6
5
  .toLowerCase()
7
6
  .replace(/[^a-z0-9]+/g, '-')
8
7
  .replace(/^-+|-+$/g, '');
9
8
  }
10
- export function generateStory({ fileContents, fileName }) {
11
- const outPath = path.join(STORY_UI_CONFIG.generatedStoriesPath, fileName);
9
+ export function generateStory({ fileContents, fileName, config }) {
10
+ const outPath = path.join(config.generatedStoriesPath, fileName);
11
+ // Ensure the directory exists
12
+ const dir = path.dirname(outPath);
13
+ if (!fs.existsSync(dir)) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
12
16
  fs.writeFileSync(outPath, fileContents, 'utf-8');
13
17
  return outPath;
14
18
  }