@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
|
@@ -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
|
|
40
|
+
// Step 2: Apply manual configurations as override/fallback
|
|
35
41
|
this.applyManualConfigurations();
|
|
36
|
-
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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(
|
|
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
|
}
|