@tpitre/story-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.sample +17 -0
- package/LICENSE +21 -0
- package/README.md +531 -0
- package/dist/cli/index.js +250 -0
- package/dist/cli/setup.js +289 -0
- package/dist/index.js +12 -0
- package/dist/mcp-server/index.js +64 -0
- package/dist/mcp-server/routes/claude.js +30 -0
- package/dist/mcp-server/routes/components.js +26 -0
- package/dist/mcp-server/routes/generateStory.js +289 -0
- package/dist/mcp-server/routes/memoryStories.js +141 -0
- package/dist/mcp-server/routes/storySync.js +147 -0
- package/dist/story-generator/componentDiscovery.js +222 -0
- package/dist/story-generator/configLoader.js +482 -0
- package/dist/story-generator/generateStory.js +19 -0
- package/dist/story-generator/gitignoreManager.js +182 -0
- package/dist/story-generator/inMemoryStoryService.js +128 -0
- package/dist/story-generator/productionGitignoreManager.js +333 -0
- package/dist/story-generator/promptGenerator.js +201 -0
- package/dist/story-generator/storySync.js +201 -0
- package/dist/story-ui.config.js +114 -0
- package/dist/story-ui.config.loader.js +205 -0
- package/package.json +80 -0
- package/templates/README.md +32 -0
- package/templates/StoryUI/StoryUIPanel.stories.tsx +28 -0
- package/templates/StoryUI/StoryUIPanel.tsx +870 -0
- package/templates/StoryUI/index.tsx +2 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { DEFAULT_CONFIG, createStoryUIConfig } from '../story-ui.config.js';
|
|
4
|
+
// Config cache to prevent excessive loading
|
|
5
|
+
let cachedConfig = null;
|
|
6
|
+
let configLoadTime = 0;
|
|
7
|
+
const CONFIG_CACHE_TTL = 30000; // 30 seconds
|
|
8
|
+
/**
|
|
9
|
+
* Loads Story UI configuration from the user's project
|
|
10
|
+
* Looks for story-ui.config.js in the current working directory
|
|
11
|
+
* Uses caching to prevent excessive loading
|
|
12
|
+
*/
|
|
13
|
+
export function loadUserConfig() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
// Return cached config if still valid
|
|
16
|
+
if (cachedConfig && (now - configLoadTime) < CONFIG_CACHE_TTL) {
|
|
17
|
+
return cachedConfig;
|
|
18
|
+
}
|
|
19
|
+
const configPaths = [
|
|
20
|
+
path.join(process.cwd(), 'story-ui.config.js'),
|
|
21
|
+
path.join(process.cwd(), 'story-ui.config.ts'),
|
|
22
|
+
path.join(process.cwd(), '.storybook', 'story-ui.config.js'),
|
|
23
|
+
path.join(process.cwd(), '.storybook', 'story-ui.config.ts')
|
|
24
|
+
];
|
|
25
|
+
for (const configPath of configPaths) {
|
|
26
|
+
if (fs.existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
console.log(`Loading Story UI config from: ${configPath}`);
|
|
29
|
+
// Read and evaluate the config file
|
|
30
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
31
|
+
// Simple evaluation for CommonJS modules
|
|
32
|
+
if (configContent.includes('module.exports')) {
|
|
33
|
+
// Create a temporary module context
|
|
34
|
+
const module = { exports: {} };
|
|
35
|
+
const exports = module.exports;
|
|
36
|
+
// Evaluate the config file content
|
|
37
|
+
eval(configContent);
|
|
38
|
+
const userConfig = module.exports;
|
|
39
|
+
const config = createStoryUIConfig(userConfig.default || userConfig);
|
|
40
|
+
// Cache the loaded config
|
|
41
|
+
cachedConfig = config;
|
|
42
|
+
configLoadTime = now;
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.warn(`Failed to load config from ${configPath}:`, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Only log warnings once per cache period
|
|
52
|
+
if (!cachedConfig || (now - configLoadTime) >= CONFIG_CACHE_TTL) {
|
|
53
|
+
console.warn('No story-ui.config.js found. Using default configuration.');
|
|
54
|
+
console.warn('Please create a story-ui.config.js file in your project root to configure Story UI for your design system.');
|
|
55
|
+
}
|
|
56
|
+
// Cache the default config
|
|
57
|
+
cachedConfig = DEFAULT_CONFIG;
|
|
58
|
+
configLoadTime = now;
|
|
59
|
+
return DEFAULT_CONFIG;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validates that the configuration has the necessary paths and components
|
|
63
|
+
*/
|
|
64
|
+
export function validateConfig(config) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
// Check if generated stories path exists or can be created
|
|
67
|
+
if (!config.generatedStoriesPath) {
|
|
68
|
+
errors.push('generatedStoriesPath is required');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const dir = path.dirname(config.generatedStoriesPath);
|
|
72
|
+
if (!fs.existsSync(dir)) {
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
errors.push(`Cannot create generated stories directory: ${dir}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Check if components can be discovered
|
|
82
|
+
if (!config.componentsPath && !config.componentsMetadataPath) {
|
|
83
|
+
errors.push('Either componentsPath or componentsMetadataPath must be specified');
|
|
84
|
+
}
|
|
85
|
+
if (config.componentsPath && !fs.existsSync(config.componentsPath)) {
|
|
86
|
+
errors.push(`Components path does not exist: ${config.componentsPath}`);
|
|
87
|
+
}
|
|
88
|
+
if (config.componentsMetadataPath && !fs.existsSync(config.componentsMetadataPath)) {
|
|
89
|
+
errors.push(`Components metadata path does not exist: ${config.componentsMetadataPath}`);
|
|
90
|
+
}
|
|
91
|
+
// Check import path
|
|
92
|
+
if (!config.importPath || config.importPath === 'your-component-library' || config.importPath.trim() === '') {
|
|
93
|
+
errors.push('importPath must be configured to point to your component library');
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
isValid: errors.length === 0,
|
|
97
|
+
errors
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Analyzes existing Storybook files to detect design system patterns
|
|
102
|
+
*/
|
|
103
|
+
export function analyzeExistingStories(projectRoot = process.cwd()) {
|
|
104
|
+
const storyFiles = [];
|
|
105
|
+
const componentDirs = [];
|
|
106
|
+
const importPaths = [];
|
|
107
|
+
const componentPrefixes = [];
|
|
108
|
+
const layoutPatterns = [];
|
|
109
|
+
// Find all .stories.tsx/.stories.ts files
|
|
110
|
+
function findStoryFiles(dir, depth = 0) {
|
|
111
|
+
if (depth > 4)
|
|
112
|
+
return; // Limit recursion depth
|
|
113
|
+
try {
|
|
114
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
117
|
+
continue;
|
|
118
|
+
const fullPath = path.join(dir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
findStoryFiles(fullPath, depth + 1);
|
|
121
|
+
}
|
|
122
|
+
else if (entry.name.match(/\.stories\.(tsx?|jsx?)$/)) {
|
|
123
|
+
storyFiles.push(fullPath);
|
|
124
|
+
// Track component directory (parent of story file)
|
|
125
|
+
const componentDir = path.dirname(fullPath);
|
|
126
|
+
if (!componentDirs.includes(componentDir)) {
|
|
127
|
+
componentDirs.push(componentDir);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Skip directories we can't read
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
findStoryFiles(projectRoot);
|
|
137
|
+
// Analyze story files for patterns
|
|
138
|
+
for (const storyFile of storyFiles) {
|
|
139
|
+
try {
|
|
140
|
+
const content = fs.readFileSync(storyFile, 'utf-8');
|
|
141
|
+
// Extract import statements
|
|
142
|
+
const importMatches = content.match(/import\s+{[^}]+}\s+from\s+['"]([^'"]+)['"]/g);
|
|
143
|
+
if (importMatches) {
|
|
144
|
+
for (const importMatch of importMatches) {
|
|
145
|
+
const pathMatch = importMatch.match(/from\s+['"]([^'"]+)['"]/);
|
|
146
|
+
if (pathMatch) {
|
|
147
|
+
const importPath = pathMatch[1];
|
|
148
|
+
// Skip relative imports and focus on package imports
|
|
149
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
150
|
+
if (!importPaths.includes(importPath)) {
|
|
151
|
+
importPaths.push(importPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Extract component names to detect prefixes
|
|
158
|
+
const componentMatches = content.match(/<([A-Z][A-Za-z0-9]*)/g);
|
|
159
|
+
if (componentMatches) {
|
|
160
|
+
for (const match of componentMatches) {
|
|
161
|
+
const componentName = match.slice(1); // Remove '<'
|
|
162
|
+
// Detect common prefixes (2-3 characters)
|
|
163
|
+
const prefixMatch = componentName.match(/^([A-Z]{1,3})[A-Z]/);
|
|
164
|
+
if (prefixMatch) {
|
|
165
|
+
const prefix = prefixMatch[1];
|
|
166
|
+
if (!componentPrefixes.includes(prefix)) {
|
|
167
|
+
componentPrefixes.push(prefix);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Look for layout patterns
|
|
173
|
+
const layoutMatches = content.match(/<(Grid|Row|Col|Box|Stack|Flex|Layout|Container|Section)[^>]*>/g);
|
|
174
|
+
if (layoutMatches) {
|
|
175
|
+
for (const match of layoutMatches) {
|
|
176
|
+
if (!layoutPatterns.includes(match)) {
|
|
177
|
+
layoutPatterns.push(match);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// Skip files we can't read
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
storyFiles,
|
|
188
|
+
componentDirs,
|
|
189
|
+
importPaths,
|
|
190
|
+
componentPrefixes,
|
|
191
|
+
layoutPatterns
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Auto-detects design system configuration by analyzing the project structure
|
|
196
|
+
*/
|
|
197
|
+
export function autoDetectDesignSystem() {
|
|
198
|
+
const cwd = process.cwd();
|
|
199
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
200
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
205
|
+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
206
|
+
// First, try to detect known design systems from package.json
|
|
207
|
+
const knownSystems = detectKnownDesignSystems(dependencies);
|
|
208
|
+
if (knownSystems) {
|
|
209
|
+
console.log(`🎨 Detected known design system: ${knownSystems.importPath}`);
|
|
210
|
+
}
|
|
211
|
+
// Analyze existing Storybook files for patterns
|
|
212
|
+
const analysis = analyzeExistingStories(cwd);
|
|
213
|
+
console.log(`📊 Analysis found: ${analysis.storyFiles.length} story files, ${analysis.componentDirs.length} component directories`);
|
|
214
|
+
// Determine the most likely component directory
|
|
215
|
+
const componentPath = findMostLikelyComponentDirectory(analysis.componentDirs, cwd);
|
|
216
|
+
// Determine the most likely import path
|
|
217
|
+
const importPath = findMostLikelyImportPath(analysis.importPaths, packageJson.name);
|
|
218
|
+
// Determine component prefix
|
|
219
|
+
const componentPrefix = findMostLikelyPrefix(analysis.componentPrefixes);
|
|
220
|
+
// Determine layout patterns
|
|
221
|
+
const layoutRules = detectLayoutPatterns(analysis.layoutPatterns, componentPrefix);
|
|
222
|
+
// Build configuration
|
|
223
|
+
const config = {
|
|
224
|
+
generatedStoriesPath: path.join(cwd, 'src/stories/generated/'),
|
|
225
|
+
componentsPath: componentPath,
|
|
226
|
+
importPath: importPath,
|
|
227
|
+
componentPrefix: componentPrefix,
|
|
228
|
+
layoutRules: layoutRules
|
|
229
|
+
};
|
|
230
|
+
// Merge with known system config if available
|
|
231
|
+
if (knownSystems) {
|
|
232
|
+
return { ...knownSystems, ...config };
|
|
233
|
+
}
|
|
234
|
+
return config;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.warn('Failed to auto-detect design system:', error);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Detects known design systems from package.json dependencies
|
|
243
|
+
*/
|
|
244
|
+
function detectKnownDesignSystems(dependencies) {
|
|
245
|
+
// Material-UI detection
|
|
246
|
+
if (dependencies['@mui/material']) {
|
|
247
|
+
return {
|
|
248
|
+
importPath: '@mui/material',
|
|
249
|
+
componentPrefix: '',
|
|
250
|
+
layoutRules: {
|
|
251
|
+
multiColumnWrapper: 'Grid',
|
|
252
|
+
columnComponent: 'Grid',
|
|
253
|
+
containerComponent: 'Container',
|
|
254
|
+
layoutExamples: {
|
|
255
|
+
twoColumn: `<Grid container spacing={2}>
|
|
256
|
+
<Grid item xs={6}>
|
|
257
|
+
<Card>
|
|
258
|
+
<CardContent>
|
|
259
|
+
<Typography variant="h5">Left Card</Typography>
|
|
260
|
+
<Typography>Left content</Typography>
|
|
261
|
+
</CardContent>
|
|
262
|
+
</Card>
|
|
263
|
+
</Grid>
|
|
264
|
+
<Grid item xs={6}>
|
|
265
|
+
<Card>
|
|
266
|
+
<CardContent>
|
|
267
|
+
<Typography variant="h5">Right Card</Typography>
|
|
268
|
+
<Typography>Right content</Typography>
|
|
269
|
+
</CardContent>
|
|
270
|
+
</Card>
|
|
271
|
+
</Grid>
|
|
272
|
+
</Grid>`
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Chakra UI detection
|
|
278
|
+
if (dependencies['@chakra-ui/react']) {
|
|
279
|
+
return {
|
|
280
|
+
importPath: '@chakra-ui/react',
|
|
281
|
+
componentPrefix: '',
|
|
282
|
+
layoutRules: {
|
|
283
|
+
multiColumnWrapper: 'SimpleGrid',
|
|
284
|
+
columnComponent: 'Box',
|
|
285
|
+
containerComponent: 'Container'
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Ant Design detection
|
|
290
|
+
if (dependencies['antd']) {
|
|
291
|
+
return {
|
|
292
|
+
importPath: 'antd',
|
|
293
|
+
componentPrefix: '',
|
|
294
|
+
layoutRules: {
|
|
295
|
+
multiColumnWrapper: 'Row',
|
|
296
|
+
columnComponent: 'Col',
|
|
297
|
+
containerComponent: 'div'
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// Mantine detection
|
|
302
|
+
if (dependencies['@mantine/core']) {
|
|
303
|
+
return {
|
|
304
|
+
importPath: '@mantine/core',
|
|
305
|
+
componentPrefix: '',
|
|
306
|
+
layoutRules: {
|
|
307
|
+
multiColumnWrapper: 'SimpleGrid',
|
|
308
|
+
columnComponent: 'div',
|
|
309
|
+
containerComponent: 'Container'
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// ShadCN/UI detection
|
|
314
|
+
if (dependencies['@radix-ui/react-slot'] || dependencies['class-variance-authority']) {
|
|
315
|
+
return {
|
|
316
|
+
importPath: '@/components/ui',
|
|
317
|
+
componentPrefix: '',
|
|
318
|
+
layoutRules: {
|
|
319
|
+
multiColumnWrapper: 'div',
|
|
320
|
+
columnComponent: 'div',
|
|
321
|
+
containerComponent: 'div',
|
|
322
|
+
layoutExamples: {
|
|
323
|
+
twoColumn: `<div className="grid grid-cols-2 gap-4">
|
|
324
|
+
<div>
|
|
325
|
+
<Card>
|
|
326
|
+
<CardHeader>
|
|
327
|
+
<CardTitle>Left Card</CardTitle>
|
|
328
|
+
</CardHeader>
|
|
329
|
+
<CardContent>
|
|
330
|
+
<p>Left content</p>
|
|
331
|
+
</CardContent>
|
|
332
|
+
</Card>
|
|
333
|
+
</div>
|
|
334
|
+
<div>
|
|
335
|
+
<Card>
|
|
336
|
+
<CardHeader>
|
|
337
|
+
<CardTitle>Right Card</CardTitle>
|
|
338
|
+
</CardHeader>
|
|
339
|
+
<CardContent>
|
|
340
|
+
<p>Right content</p>
|
|
341
|
+
</CardContent>
|
|
342
|
+
</Card>
|
|
343
|
+
</div>
|
|
344
|
+
</div>`
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Finds the most likely component directory based on story file locations
|
|
353
|
+
*/
|
|
354
|
+
function findMostLikelyComponentDirectory(componentDirs, projectRoot) {
|
|
355
|
+
if (componentDirs.length === 0) {
|
|
356
|
+
// Fallback to common patterns
|
|
357
|
+
const commonPaths = [
|
|
358
|
+
path.join(projectRoot, 'src/components'),
|
|
359
|
+
path.join(projectRoot, 'components'),
|
|
360
|
+
path.join(projectRoot, 'lib/components'),
|
|
361
|
+
path.join(projectRoot, 'src/ui'),
|
|
362
|
+
path.join(projectRoot, 'ui')
|
|
363
|
+
];
|
|
364
|
+
for (const commonPath of commonPaths) {
|
|
365
|
+
if (fs.existsSync(commonPath)) {
|
|
366
|
+
return commonPath;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return path.join(projectRoot, 'src/components');
|
|
370
|
+
}
|
|
371
|
+
// Find the common parent directory of most story files
|
|
372
|
+
const dirCounts = {};
|
|
373
|
+
for (const dir of componentDirs) {
|
|
374
|
+
// Count occurrences of parent directories
|
|
375
|
+
let currentDir = dir;
|
|
376
|
+
while (currentDir !== projectRoot && currentDir !== path.dirname(currentDir)) {
|
|
377
|
+
dirCounts[currentDir] = (dirCounts[currentDir] || 0) + 1;
|
|
378
|
+
currentDir = path.dirname(currentDir);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Find the directory with the most story files
|
|
382
|
+
let maxCount = 0;
|
|
383
|
+
let bestDir = path.join(projectRoot, 'src/components');
|
|
384
|
+
for (const [dir, count] of Object.entries(dirCounts)) {
|
|
385
|
+
if (count > maxCount) {
|
|
386
|
+
maxCount = count;
|
|
387
|
+
bestDir = dir;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return bestDir;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Finds the most likely import path based on import analysis
|
|
394
|
+
*/
|
|
395
|
+
function findMostLikelyImportPath(importPaths, packageName) {
|
|
396
|
+
if (importPaths.length === 0) {
|
|
397
|
+
return packageName || 'your-component-library';
|
|
398
|
+
}
|
|
399
|
+
// Count frequency of import paths
|
|
400
|
+
const pathCounts = {};
|
|
401
|
+
for (const importPath of importPaths) {
|
|
402
|
+
// Skip common non-component libraries
|
|
403
|
+
if (importPath.includes('react') || importPath.includes('storybook') ||
|
|
404
|
+
importPath.includes('testing') || importPath.includes('jest')) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
pathCounts[importPath] = (pathCounts[importPath] || 0) + 1;
|
|
408
|
+
}
|
|
409
|
+
// Find the most common import path
|
|
410
|
+
let maxCount = 0;
|
|
411
|
+
let bestPath = packageName || 'your-component-library';
|
|
412
|
+
for (const [importPath, count] of Object.entries(pathCounts)) {
|
|
413
|
+
if (count > maxCount) {
|
|
414
|
+
maxCount = count;
|
|
415
|
+
bestPath = importPath;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return bestPath;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Finds the most likely component prefix
|
|
422
|
+
*/
|
|
423
|
+
function findMostLikelyPrefix(componentPrefixes) {
|
|
424
|
+
if (componentPrefixes.length === 0) {
|
|
425
|
+
return '';
|
|
426
|
+
}
|
|
427
|
+
// Count frequency of prefixes
|
|
428
|
+
const prefixCounts = {};
|
|
429
|
+
for (const prefix of componentPrefixes) {
|
|
430
|
+
prefixCounts[prefix] = (prefixCounts[prefix] || 0) + 1;
|
|
431
|
+
}
|
|
432
|
+
// Find the most common prefix
|
|
433
|
+
let maxCount = 0;
|
|
434
|
+
let bestPrefix = '';
|
|
435
|
+
for (const [prefix, count] of Object.entries(prefixCounts)) {
|
|
436
|
+
if (count > maxCount && count > 2) { // Only consider prefixes used multiple times
|
|
437
|
+
maxCount = count;
|
|
438
|
+
bestPrefix = prefix;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return bestPrefix;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Detects layout patterns from existing components
|
|
445
|
+
*/
|
|
446
|
+
function detectLayoutPatterns(layoutPatterns, componentPrefix) {
|
|
447
|
+
const rules = {
|
|
448
|
+
multiColumnWrapper: 'div',
|
|
449
|
+
columnComponent: 'div',
|
|
450
|
+
containerComponent: 'div',
|
|
451
|
+
layoutExamples: {
|
|
452
|
+
twoColumn: `<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}>
|
|
453
|
+
<div>Column 1 content</div>
|
|
454
|
+
<div>Column 2 content</div>
|
|
455
|
+
</div>`
|
|
456
|
+
},
|
|
457
|
+
prohibitedElements: []
|
|
458
|
+
};
|
|
459
|
+
// Analyze layout patterns to determine best components
|
|
460
|
+
for (const pattern of layoutPatterns) {
|
|
461
|
+
if (pattern.includes('Grid')) {
|
|
462
|
+
rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Grid` : 'Grid';
|
|
463
|
+
rules.columnComponent = componentPrefix ? `${componentPrefix}Grid` : 'Grid';
|
|
464
|
+
}
|
|
465
|
+
else if (pattern.includes('Row') && pattern.includes('Col')) {
|
|
466
|
+
rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Row` : 'Row';
|
|
467
|
+
rules.columnComponent = componentPrefix ? `${componentPrefix}Col` : 'Col';
|
|
468
|
+
}
|
|
469
|
+
else if (pattern.includes('Stack')) {
|
|
470
|
+
rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Stack` : 'Stack';
|
|
471
|
+
rules.columnComponent = componentPrefix ? `${componentPrefix}Box` : 'Box';
|
|
472
|
+
}
|
|
473
|
+
else if (pattern.includes('Layout')) {
|
|
474
|
+
rules.multiColumnWrapper = componentPrefix ? `${componentPrefix}Layout` : 'Layout';
|
|
475
|
+
rules.columnComponent = componentPrefix ? `${componentPrefix}LayoutSection` : 'LayoutSection';
|
|
476
|
+
}
|
|
477
|
+
if (pattern.includes('Container')) {
|
|
478
|
+
rules.containerComponent = componentPrefix ? `${componentPrefix}Container` : 'Container';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return rules;
|
|
482
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { STORY_UI_CONFIG } from '../story-ui.config.js';
|
|
4
|
+
function slugify(str) {
|
|
5
|
+
return str
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
8
|
+
.replace(/^-+|-+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
export function generateStory({ fileContents, fileName }) {
|
|
11
|
+
const outPath = path.join(STORY_UI_CONFIG.generatedStoriesPath, fileName);
|
|
12
|
+
fs.writeFileSync(outPath, fileContents, 'utf-8');
|
|
13
|
+
return outPath;
|
|
14
|
+
}
|
|
15
|
+
// Mock usage:
|
|
16
|
+
// generateStory({
|
|
17
|
+
// title: 'Login Form',
|
|
18
|
+
// jsx: '<al-input label="Email"></al-input>\n<al-input label="Password" type="password"></al-input>\n<al-button>Login</al-button>'
|
|
19
|
+
// });
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Manages .gitignore entries for Story UI generated content
|
|
5
|
+
*/
|
|
6
|
+
export class GitignoreManager {
|
|
7
|
+
constructor(config, projectRoot = process.cwd()) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.projectRoot = projectRoot;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Ensures the generated stories directory is added to .gitignore
|
|
13
|
+
*/
|
|
14
|
+
ensureGeneratedDirectoryIgnored() {
|
|
15
|
+
const gitignorePath = path.join(this.projectRoot, '.gitignore');
|
|
16
|
+
const generatedPath = this.getRelativeGeneratedPath();
|
|
17
|
+
if (!generatedPath) {
|
|
18
|
+
console.warn('Could not determine relative path for generated stories directory');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Create .gitignore if it doesn't exist
|
|
22
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
23
|
+
this.createGitignore(gitignorePath, generatedPath);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Check if the path is already ignored
|
|
27
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
28
|
+
if (this.isPathIgnored(gitignoreContent, generatedPath)) {
|
|
29
|
+
console.log(`✅ Generated stories directory already ignored: ${generatedPath}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Add the ignore rule
|
|
33
|
+
this.addIgnoreRule(gitignorePath, generatedPath);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Gets the relative path from project root to generated stories directory
|
|
37
|
+
*/
|
|
38
|
+
getRelativeGeneratedPath() {
|
|
39
|
+
try {
|
|
40
|
+
const absoluteGeneratedPath = path.resolve(this.config.generatedStoriesPath);
|
|
41
|
+
const absoluteProjectRoot = path.resolve(this.projectRoot);
|
|
42
|
+
// Get relative path from project root to generated directory
|
|
43
|
+
let relativePath = path.relative(absoluteProjectRoot, absoluteGeneratedPath);
|
|
44
|
+
// Normalize path separators for cross-platform compatibility
|
|
45
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
46
|
+
// Ensure it starts with ./ if it's a relative path
|
|
47
|
+
if (!relativePath.startsWith('../') && !relativePath.startsWith('/')) {
|
|
48
|
+
relativePath = './' + relativePath;
|
|
49
|
+
}
|
|
50
|
+
return relativePath;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('Error calculating relative path:', error);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new .gitignore file with Story UI section
|
|
59
|
+
*/
|
|
60
|
+
createGitignore(gitignorePath, generatedPath) {
|
|
61
|
+
const content = this.generateGitignoreSection(generatedPath);
|
|
62
|
+
fs.writeFileSync(gitignorePath, content);
|
|
63
|
+
console.log(`✅ Created .gitignore with Story UI generated directory: ${generatedPath}`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Checks if the generated path is already ignored
|
|
67
|
+
*/
|
|
68
|
+
isPathIgnored(gitignoreContent, generatedPath) {
|
|
69
|
+
const lines = gitignoreContent.split('\n').map(line => line.trim());
|
|
70
|
+
// Check for exact match or parent directory match
|
|
71
|
+
const pathVariations = [
|
|
72
|
+
generatedPath,
|
|
73
|
+
generatedPath.replace(/^\.\//, ''),
|
|
74
|
+
generatedPath + '/',
|
|
75
|
+
generatedPath.replace(/^\.\//, '') + '/',
|
|
76
|
+
generatedPath + '/**',
|
|
77
|
+
generatedPath.replace(/^\.\//, '') + '/**'
|
|
78
|
+
];
|
|
79
|
+
return pathVariations.some(variation => lines.includes(variation) ||
|
|
80
|
+
lines.includes(variation.replace(/\/$/, '')));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Adds ignore rule to existing .gitignore
|
|
84
|
+
*/
|
|
85
|
+
addIgnoreRule(gitignorePath, generatedPath) {
|
|
86
|
+
const existingContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
87
|
+
const newSection = this.generateGitignoreSection(generatedPath);
|
|
88
|
+
// Add with proper spacing
|
|
89
|
+
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
90
|
+
const updatedContent = existingContent + separator + newSection;
|
|
91
|
+
fs.writeFileSync(gitignorePath, updatedContent);
|
|
92
|
+
console.log(`✅ Added Story UI generated directory to .gitignore: ${generatedPath}`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generates the gitignore section for Story UI
|
|
96
|
+
*/
|
|
97
|
+
generateGitignoreSection(generatedPath) {
|
|
98
|
+
return `# Story UI - AI Generated Stories (ephemeral, not for version control)
|
|
99
|
+
# These are temporary stories for testing layouts and should not be committed
|
|
100
|
+
${generatedPath}/
|
|
101
|
+
${generatedPath}/**`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates the generated directory if it doesn't exist
|
|
105
|
+
*/
|
|
106
|
+
ensureGeneratedDirectoryExists() {
|
|
107
|
+
const generatedDir = this.config.generatedStoriesPath;
|
|
108
|
+
if (!fs.existsSync(generatedDir)) {
|
|
109
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
110
|
+
console.log(`✅ Created generated stories directory: ${generatedDir}`);
|
|
111
|
+
// Create a README to explain the purpose
|
|
112
|
+
this.createGeneratedDirectoryReadme(generatedDir);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Creates a README in the generated directory explaining its purpose
|
|
117
|
+
*/
|
|
118
|
+
createGeneratedDirectoryReadme(generatedDir) {
|
|
119
|
+
const readmePath = path.join(generatedDir, 'README.md');
|
|
120
|
+
const readmeContent = `# AI Generated Stories
|
|
121
|
+
|
|
122
|
+
This directory contains stories generated by Story UI for testing and iteration purposes.
|
|
123
|
+
|
|
124
|
+
## ⚠️ Important Notes
|
|
125
|
+
|
|
126
|
+
- **These stories are ephemeral** - they are meant for testing layouts and sharing with stakeholders
|
|
127
|
+
- **Do not commit these files** - they are automatically ignored by git
|
|
128
|
+
- **Stories are regenerated** - feel free to delete and regenerate as needed
|
|
129
|
+
|
|
130
|
+
## Purpose
|
|
131
|
+
|
|
132
|
+
These stories are designed for:
|
|
133
|
+
- 🎨 **Layout Testing** - Test different component arrangements
|
|
134
|
+
- 👥 **Stakeholder Review** - Share layouts with product owners, designers, and project managers
|
|
135
|
+
- 🔄 **Rapid Iteration** - Quickly generate and modify layouts
|
|
136
|
+
- 📱 **Design Validation** - Validate designs before implementation
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
Stories in this directory will appear in Storybook under the "${this.config.storyPrefix}" section.
|
|
141
|
+
|
|
142
|
+
Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Storybook story generator.
|
|
143
|
+
`;
|
|
144
|
+
fs.writeFileSync(readmePath, readmeContent);
|
|
145
|
+
console.log(`✅ Created README in generated directory`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Cleans up old generated stories (optional utility)
|
|
149
|
+
*/
|
|
150
|
+
cleanupOldStories(maxAge = 7 * 24 * 60 * 60 * 1000) {
|
|
151
|
+
const generatedDir = this.config.generatedStoriesPath;
|
|
152
|
+
if (!fs.existsSync(generatedDir)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const files = fs.readdirSync(generatedDir);
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
let cleanedCount = 0;
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
if (!file.endsWith('.stories.tsx')) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const filePath = path.join(generatedDir, file);
|
|
163
|
+
const stats = fs.statSync(filePath);
|
|
164
|
+
const age = now - stats.mtime.getTime();
|
|
165
|
+
if (age > maxAge) {
|
|
166
|
+
fs.unlinkSync(filePath);
|
|
167
|
+
cleanedCount++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (cleanedCount > 0) {
|
|
171
|
+
console.log(`🧹 Cleaned up ${cleanedCount} old generated stories`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Convenience function to set up gitignore for Story UI
|
|
177
|
+
*/
|
|
178
|
+
export function setupGitignoreForStoryUI(config, projectRoot) {
|
|
179
|
+
const manager = new GitignoreManager(config, projectRoot);
|
|
180
|
+
manager.ensureGeneratedDirectoryExists();
|
|
181
|
+
manager.ensureGeneratedDirectoryIgnored();
|
|
182
|
+
}
|