@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
|
@@ -14,7 +14,9 @@ export class StoryTracker {
|
|
|
14
14
|
if (fs.existsSync(this.mappingFile)) {
|
|
15
15
|
const data = JSON.parse(fs.readFileSync(this.mappingFile, 'utf-8'));
|
|
16
16
|
for (const mapping of data) {
|
|
17
|
-
|
|
17
|
+
if (mapping && mapping.title && typeof mapping.title === 'string') {
|
|
18
|
+
this.mappings.set(mapping.title.toLowerCase(), mapping);
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -39,47 +41,56 @@ export class StoryTracker {
|
|
|
39
41
|
* Find an existing story by title
|
|
40
42
|
*/
|
|
41
43
|
findByTitle(title) {
|
|
44
|
+
if (!title || typeof title !== 'string') {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
42
47
|
// Normalize the title for comparison
|
|
43
48
|
const normalizedTitle = title.toLowerCase().trim();
|
|
44
49
|
// Try exact match first
|
|
45
50
|
let mapping = this.mappings.get(normalizedTitle);
|
|
46
51
|
if (mapping)
|
|
47
52
|
return mapping;
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (key.includes(normalizedTitle) || normalizedTitle.includes(key)) {
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
// Check for very similar titles (e.g., "dashboard" vs "inventory dashboard")
|
|
55
|
-
const keywords = normalizedTitle.split(/\s+/);
|
|
56
|
-
const keyKeywords = key.split(/\s+/);
|
|
57
|
-
// If all keywords from the shorter title are in the longer one
|
|
58
|
-
const shortKeywords = keywords.length < keyKeywords.length ? keywords : keyKeywords;
|
|
59
|
-
const longKeywords = keywords.length < keyKeywords.length ? keyKeywords : keywords;
|
|
60
|
-
if (shortKeywords.every(word => longKeywords.includes(word))) {
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
53
|
+
// Don't do fuzzy matching for titles - only exact matches
|
|
54
|
+
// This prevents "Card" from matching "Card Layouts" or "Profile Card"
|
|
55
|
+
// Users expect new stories when they request variations
|
|
64
56
|
return undefined;
|
|
65
57
|
}
|
|
66
58
|
/**
|
|
67
59
|
* Find an existing story by prompt similarity
|
|
68
60
|
*/
|
|
69
61
|
findByPrompt(prompt) {
|
|
62
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
70
65
|
const normalizedPrompt = prompt.toLowerCase().trim();
|
|
71
66
|
// Remove common prefixes like "generate a", "create a", etc.
|
|
72
67
|
const cleanPrompt = normalizedPrompt
|
|
73
68
|
.replace(/^(generate|create|build|make|design|show|write|produce|construct|draft|compose|implement|add|render|display)\s+(a|an|the)?\s*/i, '')
|
|
74
69
|
.trim();
|
|
70
|
+
// Extract key terms from the prompt
|
|
71
|
+
const promptKeywords = cleanPrompt.split(/\s+/).filter(word => word.length > 2);
|
|
75
72
|
// Try to find by similar prompts
|
|
76
73
|
for (const mapping of this.mappings.values()) {
|
|
74
|
+
if (!mapping || !mapping.prompt || typeof mapping.prompt !== 'string') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
77
|
const mappingPrompt = mapping.prompt.toLowerCase()
|
|
78
78
|
.replace(/^(generate|create|build|make|design|show|write|produce|construct|draft|compose|implement|add|render|display)\s+(a|an|the)?\s*/i, '')
|
|
79
79
|
.trim();
|
|
80
|
+
// Exact match
|
|
80
81
|
if (mappingPrompt === cleanPrompt) {
|
|
81
82
|
return mapping;
|
|
82
83
|
}
|
|
84
|
+
// Fuzzy matching based on shared keywords
|
|
85
|
+
const mappingKeywords = mappingPrompt.split(/\s+/).filter(word => word.length > 2);
|
|
86
|
+
const sharedKeywords = promptKeywords.filter(word => mappingKeywords.includes(word));
|
|
87
|
+
// Only consider it similar if 90% or more keywords match AND at least 4 keywords
|
|
88
|
+
// This prevents false positives like "card" matching "card layouts" vs "card animations"
|
|
89
|
+
const similarityThreshold = Math.max(4, Math.floor(promptKeywords.length * 0.9));
|
|
90
|
+
if (sharedKeywords.length >= similarityThreshold && promptKeywords.length >= 4) {
|
|
91
|
+
console.log(`🔄 Found similar story: "${mapping.title}" (${sharedKeywords.length}/${promptKeywords.length} keywords match)`);
|
|
92
|
+
return mapping;
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
return undefined;
|
|
85
96
|
}
|
|
@@ -87,6 +98,10 @@ export class StoryTracker {
|
|
|
87
98
|
* Register a new or updated story
|
|
88
99
|
*/
|
|
89
100
|
registerStory(mapping) {
|
|
101
|
+
if (!mapping || !mapping.title || typeof mapping.title !== 'string') {
|
|
102
|
+
console.warn('Invalid mapping provided to registerStory:', mapping);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
90
105
|
const normalizedTitle = mapping.title.toLowerCase();
|
|
91
106
|
// Check if we're updating an existing story
|
|
92
107
|
const existing = this.findByTitle(mapping.title);
|
|
@@ -109,7 +124,7 @@ export class StoryTracker {
|
|
|
109
124
|
removeStory(titleOrFileName) {
|
|
110
125
|
// Try to find by title first
|
|
111
126
|
const byTitle = this.findByTitle(titleOrFileName);
|
|
112
|
-
if (byTitle) {
|
|
127
|
+
if (byTitle && byTitle.title && typeof byTitle.title === 'string') {
|
|
113
128
|
this.mappings.delete(byTitle.title.toLowerCase());
|
|
114
129
|
this.saveMappings();
|
|
115
130
|
return true;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function validateStory(storyContent) {
|
|
2
|
+
const errors = [];
|
|
3
|
+
const lines = storyContent.split('\n');
|
|
4
|
+
// These are warnings/suggestions rather than strict forbidden patterns
|
|
5
|
+
// Only flag truly problematic patterns that would break the story
|
|
6
|
+
const forbiddenPatterns = [
|
|
7
|
+
{ pattern: /UNSAFE_style\s*=\s*\{/i, message: 'The `UNSAFE_style` prop is strictly forbidden. Do not use it for any reason.' },
|
|
8
|
+
{ pattern: /UNSAFE_className\s*=\s*['"]/i, message: 'The `UNSAFE_className` prop is forbidden.' },
|
|
9
|
+
{ pattern: /<Text\s+as\s*=\s*["']h[1-6]["']/i, message: 'Text component does not support heading elements (h1-h6) in the "as" prop. Use Heading component instead.' },
|
|
10
|
+
// Remove overly strict rules - divs, imgs, and inline styles are fine in moderation
|
|
11
|
+
// Only check for actual syntax errors or patterns that would break Storybook
|
|
12
|
+
];
|
|
13
|
+
lines.forEach((line, index) => {
|
|
14
|
+
for (const { pattern, message } of forbiddenPatterns) {
|
|
15
|
+
if (pattern.test(line)) {
|
|
16
|
+
errors.push({
|
|
17
|
+
message,
|
|
18
|
+
line: index + 1,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
// Check for truncated story (multiple closing tags on a single line followed by };)
|
|
24
|
+
const lastFewLines = lines.slice(-5).join('\n');
|
|
25
|
+
if (lastFewLines.match(/<\/\w+><\/\w+><\/\w+><\/\w+>.*\n\s*\};/)) {
|
|
26
|
+
errors.push({
|
|
27
|
+
message: 'Story appears to be truncated. Multiple closing tags found on a single line followed by abrupt ending.',
|
|
28
|
+
line: lines.length - 1,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Check for proper story structure
|
|
32
|
+
if (!storyContent.includes('export default meta')) {
|
|
33
|
+
errors.push({
|
|
34
|
+
message: 'Story is missing required "export default meta" statement.',
|
|
35
|
+
line: 1,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return errors;
|
|
39
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Universal adapter to make Story UI work with any React design system
|
|
5
|
+
*/
|
|
6
|
+
export class UniversalDesignSystemAdapter {
|
|
7
|
+
constructor(projectRoot = process.cwd()) {
|
|
8
|
+
this.detectedSystems = [];
|
|
9
|
+
this.projectRoot = projectRoot;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Auto-detect which design systems are available in the project
|
|
13
|
+
*/
|
|
14
|
+
async detectDesignSystems() {
|
|
15
|
+
const packageJsonPath = path.join(this.projectRoot, 'package.json');
|
|
16
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
17
|
+
console.log('📦 No package.json found for design system detection');
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
21
|
+
const allDeps = {
|
|
22
|
+
...packageJson.dependencies,
|
|
23
|
+
...packageJson.devDependencies,
|
|
24
|
+
...packageJson.peerDependencies
|
|
25
|
+
};
|
|
26
|
+
this.detectedSystems = [];
|
|
27
|
+
// Check for known design systems
|
|
28
|
+
this.checkForChakraUI(allDeps);
|
|
29
|
+
this.checkForAntDesign(allDeps);
|
|
30
|
+
this.checkForMantine(allDeps);
|
|
31
|
+
this.checkForGenericReactComponents(allDeps);
|
|
32
|
+
console.log(`🎨 Detected ${this.detectedSystems.length} design systems:`, this.detectedSystems.map(ds => ds.name));
|
|
33
|
+
return this.detectedSystems;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate optimal Story UI config for detected design systems
|
|
37
|
+
*/
|
|
38
|
+
generateOptimalConfig() {
|
|
39
|
+
const primarySystem = this.getPrimaryDesignSystem();
|
|
40
|
+
if (!primarySystem) {
|
|
41
|
+
return this.getGenericReactConfig();
|
|
42
|
+
}
|
|
43
|
+
switch (primarySystem.type) {
|
|
44
|
+
case 'chakra-ui':
|
|
45
|
+
return this.getChakraUIConfig();
|
|
46
|
+
case 'antd':
|
|
47
|
+
return this.getAntDesignConfig();
|
|
48
|
+
case 'mantine':
|
|
49
|
+
return this.getMantineConfig();
|
|
50
|
+
default:
|
|
51
|
+
return this.getGenericReactConfig();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Design system detection methods
|
|
55
|
+
checkForChakraUI(deps) {
|
|
56
|
+
if (deps['@chakra-ui/react']) {
|
|
57
|
+
this.detectedSystems.push({
|
|
58
|
+
name: 'Chakra UI',
|
|
59
|
+
type: 'chakra-ui',
|
|
60
|
+
scope: '@chakra-ui',
|
|
61
|
+
primaryPackage: '@chakra-ui/react',
|
|
62
|
+
commonComponents: ['Box', 'Flex', 'Grid', 'Stack', 'Text', 'Heading', 'Button', 'Input'],
|
|
63
|
+
layoutComponents: ['Box', 'Flex', 'Grid', 'Stack', 'HStack', 'VStack'],
|
|
64
|
+
formComponents: ['Input', 'Button', 'Select', 'Checkbox', 'Radio'],
|
|
65
|
+
importPatterns: {
|
|
66
|
+
default: [],
|
|
67
|
+
named: ['Box', 'Flex', 'Grid', 'Stack', 'Text', 'Heading', 'Button', 'Input']
|
|
68
|
+
},
|
|
69
|
+
designTokens: {
|
|
70
|
+
spacing: 'spacing.*',
|
|
71
|
+
colors: 'colors.*'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
checkForAntDesign(deps) {
|
|
77
|
+
if (deps['antd']) {
|
|
78
|
+
this.detectedSystems.push({
|
|
79
|
+
name: 'Ant Design',
|
|
80
|
+
type: 'antd',
|
|
81
|
+
primaryPackage: 'antd',
|
|
82
|
+
commonComponents: ['Layout', 'Row', 'Col', 'Space', 'Typography', 'Button', 'Input'],
|
|
83
|
+
layoutComponents: ['Layout', 'Row', 'Col', 'Space'],
|
|
84
|
+
formComponents: ['Input', 'Button', 'Select', 'Checkbox', 'Radio'],
|
|
85
|
+
importPatterns: {
|
|
86
|
+
default: [],
|
|
87
|
+
named: ['Layout', 'Row', 'Col', 'Space', 'Typography', 'Button', 'Input']
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
checkForMantine(deps) {
|
|
93
|
+
if (deps['@mantine/core']) {
|
|
94
|
+
this.detectedSystems.push({
|
|
95
|
+
name: 'Mantine',
|
|
96
|
+
type: 'mantine',
|
|
97
|
+
scope: '@mantine',
|
|
98
|
+
primaryPackage: '@mantine/core',
|
|
99
|
+
commonComponents: ['Box', 'Flex', 'Grid', 'Stack', 'Text', 'Title', 'Button', 'TextInput'],
|
|
100
|
+
layoutComponents: ['Box', 'Flex', 'Grid', 'Stack'],
|
|
101
|
+
formComponents: ['TextInput', 'Button', 'Select', 'Checkbox', 'Radio'],
|
|
102
|
+
importPatterns: {
|
|
103
|
+
default: [],
|
|
104
|
+
named: ['Box', 'Flex', 'Grid', 'Stack', 'Text', 'Title', 'Button', 'TextInput']
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
checkForGenericReactComponents(deps) {
|
|
110
|
+
// Check for generic React component libraries
|
|
111
|
+
const genericLibraries = [
|
|
112
|
+
'react-bootstrap',
|
|
113
|
+
'semantic-ui-react',
|
|
114
|
+
'rebass',
|
|
115
|
+
'theme-ui',
|
|
116
|
+
'styled-components',
|
|
117
|
+
'@emotion/react'
|
|
118
|
+
];
|
|
119
|
+
const foundGeneric = genericLibraries.filter(lib => deps[lib]);
|
|
120
|
+
if (foundGeneric.length > 0) {
|
|
121
|
+
this.detectedSystems.push({
|
|
122
|
+
name: 'Generic React Components',
|
|
123
|
+
type: 'generic',
|
|
124
|
+
primaryPackage: foundGeneric[0],
|
|
125
|
+
commonComponents: ['div', 'span', 'button', 'input', 'form'],
|
|
126
|
+
layoutComponents: ['div', 'section', 'main'],
|
|
127
|
+
formComponents: ['input', 'button', 'select', 'textarea'],
|
|
128
|
+
importPatterns: {
|
|
129
|
+
default: [],
|
|
130
|
+
named: []
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Configuration generators
|
|
136
|
+
getPrimaryDesignSystem() {
|
|
137
|
+
// Prioritize based on completeness and popularity
|
|
138
|
+
const priorities = ['chakra-ui', 'antd', 'mantine', 'generic'];
|
|
139
|
+
for (const priority of priorities) {
|
|
140
|
+
const system = this.detectedSystems.find(ds => ds.type === priority);
|
|
141
|
+
if (system)
|
|
142
|
+
return system;
|
|
143
|
+
}
|
|
144
|
+
return this.detectedSystems[0] || null;
|
|
145
|
+
}
|
|
146
|
+
getChakraUIConfig() {
|
|
147
|
+
return {
|
|
148
|
+
designSystemGuidelines: {
|
|
149
|
+
name: "Chakra UI",
|
|
150
|
+
preferredComponents: {
|
|
151
|
+
layout: "@chakra-ui/react",
|
|
152
|
+
buttons: "@chakra-ui/react",
|
|
153
|
+
forms: "@chakra-ui/react"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
layoutRules: {
|
|
157
|
+
multiColumnWrapper: "Grid",
|
|
158
|
+
columnComponent: "Box",
|
|
159
|
+
containerComponent: "Container"
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
getAntDesignConfig() {
|
|
164
|
+
return {
|
|
165
|
+
designSystemGuidelines: {
|
|
166
|
+
name: "Ant Design",
|
|
167
|
+
preferredComponents: {
|
|
168
|
+
layout: "antd",
|
|
169
|
+
buttons: "antd",
|
|
170
|
+
forms: "antd"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
layoutRules: {
|
|
174
|
+
multiColumnWrapper: "Row",
|
|
175
|
+
columnComponent: "Col",
|
|
176
|
+
containerComponent: "Layout"
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
getMantineConfig() {
|
|
181
|
+
return {
|
|
182
|
+
designSystemGuidelines: {
|
|
183
|
+
name: "Mantine",
|
|
184
|
+
preferredComponents: {
|
|
185
|
+
layout: "@mantine/core",
|
|
186
|
+
buttons: "@mantine/core",
|
|
187
|
+
forms: "@mantine/core"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
getGenericReactConfig() {
|
|
193
|
+
return {
|
|
194
|
+
designSystemGuidelines: {
|
|
195
|
+
name: "Generic React",
|
|
196
|
+
preferredComponents: {
|
|
197
|
+
layout: "react",
|
|
198
|
+
buttons: "react",
|
|
199
|
+
forms: "react"
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
layoutRules: {
|
|
203
|
+
multiColumnWrapper: "div",
|
|
204
|
+
columnComponent: "div",
|
|
205
|
+
containerComponent: "div"
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { isBlacklistedComponent, validateImports } from './componentBlacklist.js';
|
|
2
3
|
/**
|
|
3
4
|
* Validates TypeScript code syntax and attempts to fix common issues
|
|
4
5
|
*/
|
|
5
|
-
export function validateStoryCode(code, fileName = 'story.tsx') {
|
|
6
|
+
export function validateStoryCode(code, fileName = 'story.tsx', config) {
|
|
6
7
|
const result = {
|
|
7
8
|
isValid: true,
|
|
8
9
|
errors: [],
|
|
@@ -47,8 +48,15 @@ export function validateStoryCode(code, fileName = 'story.tsx') {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
// Additional semantic checks
|
|
50
|
-
const semanticErrors = performSemanticChecks(sourceFile);
|
|
51
|
+
const semanticErrors = performSemanticChecks(sourceFile, config);
|
|
51
52
|
result.errors.push(...semanticErrors);
|
|
53
|
+
// Check for React import
|
|
54
|
+
const hasJSX = code.includes('<') || code.includes('/>');
|
|
55
|
+
const hasReactImport = code.includes('import React from \'react\';');
|
|
56
|
+
if (hasJSX && !hasReactImport) {
|
|
57
|
+
result.errors.push('Missing React import - add "import React from \'react\';" at the top of the file');
|
|
58
|
+
result.isValid = false;
|
|
59
|
+
}
|
|
52
60
|
if (result.errors.length > 0) {
|
|
53
61
|
result.isValid = false;
|
|
54
62
|
// Attempt to fix common issues
|
|
@@ -56,7 +64,7 @@ export function validateStoryCode(code, fileName = 'story.tsx') {
|
|
|
56
64
|
if (fixedCode && fixedCode !== code) {
|
|
57
65
|
result.fixedCode = fixedCode;
|
|
58
66
|
// Re-validate the fixed code
|
|
59
|
-
const fixedValidation = validateStoryCode(fixedCode, fileName);
|
|
67
|
+
const fixedValidation = validateStoryCode(fixedCode, fileName, config);
|
|
60
68
|
if (fixedValidation.isValid) {
|
|
61
69
|
result.isValid = true;
|
|
62
70
|
result.warnings.push('Code was automatically fixed for syntax errors');
|
|
@@ -73,9 +81,50 @@ export function validateStoryCode(code, fileName = 'story.tsx') {
|
|
|
73
81
|
/**
|
|
74
82
|
* Performs additional semantic checks on the AST
|
|
75
83
|
*/
|
|
76
|
-
function performSemanticChecks(sourceFile) {
|
|
84
|
+
function performSemanticChecks(sourceFile, config) {
|
|
77
85
|
const errors = [];
|
|
86
|
+
const availableComponents = new Set();
|
|
87
|
+
const importedComponents = new Set();
|
|
88
|
+
// If config is provided, collect available components
|
|
89
|
+
if (config && config.componentsToImport) {
|
|
90
|
+
config.componentsToImport.forEach((comp) => availableComponents.add(comp));
|
|
91
|
+
}
|
|
78
92
|
function visit(node) {
|
|
93
|
+
// Check import statements for invalid components
|
|
94
|
+
if (ts.isImportDeclaration(node) && node.importClause && node.importClause.namedBindings) {
|
|
95
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
96
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
97
|
+
const importPath = moduleSpecifier.text;
|
|
98
|
+
// Check if this is the main component library import
|
|
99
|
+
if (config && config.importPath && importPath === config.importPath) {
|
|
100
|
+
if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
101
|
+
node.importClause.namedBindings.elements.forEach(element => {
|
|
102
|
+
const componentName = element.name.text;
|
|
103
|
+
importedComponents.add(componentName);
|
|
104
|
+
// Check if component exists in available components
|
|
105
|
+
if (availableComponents.size > 0) {
|
|
106
|
+
if (isBlacklistedComponent(componentName, availableComponents)) {
|
|
107
|
+
// This is a blacklisted component
|
|
108
|
+
const validation = validateImports([componentName], availableComponents);
|
|
109
|
+
const suggestions = validation.suggestions.get(componentName);
|
|
110
|
+
let errorMsg = `Import error: "${componentName}" is not a valid component from ${importPath}. This appears to be a story export name or made-up component.`;
|
|
111
|
+
if (suggestions && suggestions.length > 0) {
|
|
112
|
+
errorMsg += ` Use these components instead: ${suggestions.join(', ')}.`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
errorMsg += ` Use basic components like Box, Stack, Text, Button instead.`;
|
|
116
|
+
}
|
|
117
|
+
errors.push(errorMsg);
|
|
118
|
+
}
|
|
119
|
+
else if (!availableComponents.has(componentName)) {
|
|
120
|
+
errors.push(`Import error: "${componentName}" is not available from ${importPath}. Available components include: ${Array.from(availableComponents).slice(0, 10).join(', ')}...`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
79
128
|
// Check for unclosed JSX elements
|
|
80
129
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
81
130
|
// Additional JSX-specific checks could go here
|
|
@@ -94,6 +143,10 @@ function performSemanticChecks(sourceFile) {
|
|
|
94
143
|
*/
|
|
95
144
|
function attemptAutoFix(code, errors) {
|
|
96
145
|
let fixedCode = code;
|
|
146
|
+
// CRITICAL: Fix missing React import first (most important for JSX)
|
|
147
|
+
if (errors.some(e => e.includes('Missing React import'))) {
|
|
148
|
+
fixedCode = fixMissingReactImport(fixedCode);
|
|
149
|
+
}
|
|
97
150
|
// First, check if the code appears to be truncated
|
|
98
151
|
if (isCodeTruncated(fixedCode)) {
|
|
99
152
|
fixedCode = fixTruncatedCode(fixedCode);
|
|
@@ -229,6 +282,27 @@ function fixUnterminatedStrings(code) {
|
|
|
229
282
|
}
|
|
230
283
|
return lines.join('\n');
|
|
231
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Fixes missing React import for JSX
|
|
287
|
+
*/
|
|
288
|
+
function fixMissingReactImport(code) {
|
|
289
|
+
// Check if code has JSX but no React import
|
|
290
|
+
const hasJSX = code.includes('<') || code.includes('/>');
|
|
291
|
+
const hasReactImport = code.includes('import React') || code.includes('* as React');
|
|
292
|
+
if (hasJSX && !hasReactImport) {
|
|
293
|
+
// Find the first import statement or the beginning of the file
|
|
294
|
+
const firstImportIndex = code.indexOf('import');
|
|
295
|
+
if (firstImportIndex !== -1) {
|
|
296
|
+
// Insert React import before the first import
|
|
297
|
+
return code.slice(0, firstImportIndex) + "import React from 'react';\n" + code.slice(firstImportIndex);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// No imports, add at the beginning
|
|
301
|
+
return "import React from 'react';\n" + code;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return code;
|
|
305
|
+
}
|
|
232
306
|
/**
|
|
233
307
|
* Fixes unclosed JSX elements
|
|
234
308
|
*/
|
|
@@ -333,7 +407,7 @@ function findInsertPosition(code) {
|
|
|
333
407
|
/**
|
|
334
408
|
* Extracts and validates code blocks from AI responses
|
|
335
409
|
*/
|
|
336
|
-
export function extractAndValidateCodeBlock(aiResponse) {
|
|
410
|
+
export function extractAndValidateCodeBlock(aiResponse, config) {
|
|
337
411
|
// Try multiple extraction methods
|
|
338
412
|
const extractionMethods = [
|
|
339
413
|
// Standard code blocks
|
|
@@ -366,7 +440,7 @@ export function extractAndValidateCodeBlock(aiResponse) {
|
|
|
366
440
|
};
|
|
367
441
|
}
|
|
368
442
|
// Validate the extracted code
|
|
369
|
-
return validateStoryCode(extractedCode);
|
|
443
|
+
return validateStoryCode(extractedCode, 'story.tsx', config);
|
|
370
444
|
}
|
|
371
445
|
/**
|
|
372
446
|
* Creates a fallback story template when generation fails
|
|
@@ -374,8 +448,9 @@ export function extractAndValidateCodeBlock(aiResponse) {
|
|
|
374
448
|
export function createFallbackStory(prompt, config) {
|
|
375
449
|
const title = prompt.length > 50 ? prompt.substring(0, 50) + '...' : prompt;
|
|
376
450
|
const escapedTitle = title.replace(/"/g, '\\"');
|
|
451
|
+
const storybookFramework = config.storybookFramework || '@storybook/react';
|
|
377
452
|
return `import React from 'react';
|
|
378
|
-
import type { StoryObj } from '
|
|
453
|
+
import type { StoryObj } from '${storybookFramework}';
|
|
379
454
|
|
|
380
455
|
// Fallback story generated due to AI generation error
|
|
381
456
|
export default {
|
package/dist/story-ui.config.js
CHANGED
|
@@ -54,15 +54,22 @@ export const DEFAULT_CONFIG = {
|
|
|
54
54
|
},
|
|
55
55
|
prohibitedElements: []
|
|
56
56
|
},
|
|
57
|
-
sampleStory: `import type { StoryObj } from '@storybook/react
|
|
57
|
+
sampleStory: `import type { Meta, StoryObj } from '@storybook/react';
|
|
58
|
+
import React from 'react';
|
|
58
59
|
import { Card } from 'your-component-library';
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
title: '
|
|
61
|
+
const meta = {
|
|
62
|
+
title: 'Generated/Sample Layout',
|
|
62
63
|
component: Card,
|
|
63
|
-
|
|
64
|
+
parameters: {
|
|
65
|
+
layout: 'centered',
|
|
66
|
+
},
|
|
67
|
+
} satisfies Meta<typeof Card>;
|
|
68
|
+
|
|
69
|
+
export default meta;
|
|
70
|
+
type Story = StoryObj<typeof meta>;
|
|
64
71
|
|
|
65
|
-
export const Default:
|
|
72
|
+
export const Default: Story = {
|
|
66
73
|
args: {
|
|
67
74
|
children: (
|
|
68
75
|
<Card>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpitre/story-ui",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI-powered Storybook story generator
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "AI-powered Storybook story generator with dynamic component discovery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"release": "semantic-release",
|
|
25
25
|
"release:dry-run": "semantic-release --dry-run",
|
|
26
26
|
"commit": "cz",
|
|
27
|
-
"prepare": "husky"
|
|
28
|
-
"push": "./scripts/push.sh"
|
|
27
|
+
"prepare": "husky"
|
|
29
28
|
},
|
|
30
29
|
"keywords": [
|
|
31
30
|
"storybook",
|
|
@@ -36,7 +35,12 @@
|
|
|
36
35
|
"design-system",
|
|
37
36
|
"claude",
|
|
38
37
|
"mcp",
|
|
39
|
-
"story-generation"
|
|
38
|
+
"story-generation",
|
|
39
|
+
"documentation",
|
|
40
|
+
"real-time",
|
|
41
|
+
"component-library",
|
|
42
|
+
"iteration",
|
|
43
|
+
"version-history"
|
|
40
44
|
],
|
|
41
45
|
"author": "Story UI Contributors",
|
|
42
46
|
"license": "MIT",
|
|
@@ -58,10 +62,10 @@
|
|
|
58
62
|
"dependencies": {
|
|
59
63
|
"chalk": "^5.3.0",
|
|
60
64
|
"commander": "^11.0.0",
|
|
61
|
-
"concurrently": "^8.2.0",
|
|
62
65
|
"cors": "^2.8.5",
|
|
63
66
|
"dotenv": "^16.3.1",
|
|
64
67
|
"express": "^4.18.2",
|
|
68
|
+
"glob": "^11.0.3",
|
|
65
69
|
"inquirer": "^9.2.0",
|
|
66
70
|
"node-fetch": "^2.6.7",
|
|
67
71
|
"typescript": "^5.8.3"
|
|
@@ -77,6 +81,7 @@
|
|
|
77
81
|
"@semantic-release/release-notes-generator": "^14.0.1",
|
|
78
82
|
"@types/cors": "^2.8.17",
|
|
79
83
|
"@types/express": "^4.17.21",
|
|
84
|
+
"@types/glob": "^8.1.0",
|
|
80
85
|
"@types/inquirer": "^9.0.0",
|
|
81
86
|
"@types/node": "^20.4.2",
|
|
82
87
|
"@types/node-fetch": "^2.6.12",
|
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import StoryUIPanel from './StoryUIPanel';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { StoryUIPanel } from './StoryUIPanel';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const meta = {
|
|
6
6
|
title: 'Story UI/Story Generator',
|
|
7
7
|
component: StoryUIPanel,
|
|
8
8
|
parameters: {
|
|
9
9
|
layout: 'fullscreen',
|
|
10
10
|
docs: {
|
|
11
11
|
description: {
|
|
12
|
-
component:
|
|
12
|
+
component: `
|
|
13
|
+
Story UI Panel connects to the MCP server running on your configured port.
|
|
14
|
+
The port is determined by:
|
|
15
|
+
1. VITE_STORY_UI_PORT environment variable (recommended)
|
|
16
|
+
2. URL parameter: ?mcp-port=XXXX
|
|
17
|
+
3. Default port: 4001
|
|
18
|
+
|
|
19
|
+
This design system agnostic approach works with any component library.
|
|
20
|
+
`
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
23
|
+
},
|
|
24
|
+
} satisfies Meta<typeof StoryUIPanel>;
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
19
28
|
|
|
20
|
-
export const Default =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
render: () => {
|
|
31
|
+
// Check for URL parameter override first
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
34
|
+
const mcpPortParam = urlParams.get('mcp-port');
|
|
35
|
+
|
|
36
|
+
if (mcpPortParam) {
|
|
37
|
+
// Set the global variable that the panel will use
|
|
38
|
+
(window as any).STORY_UI_MCP_PORT = mcpPortParam;
|
|
39
|
+
}
|
|
26
40
|
}
|
|
41
|
+
|
|
42
|
+
return <StoryUIPanel />;
|
|
27
43
|
}
|
|
28
44
|
};
|