@tpitre/story-ui 1.2.0 → 1.3.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/dist/cli/index.js CHANGED
File without changes
@@ -6,6 +6,7 @@ import { buildClaudePrompt as buildFlexiblePrompt } from '../../story-generator/
6
6
  import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
7
7
  import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
8
8
  import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
9
+ import { extractAndValidateCodeBlock, createFallbackStory } from '../../story-generator/validateStory.js';
9
10
  const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
10
11
  const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
11
12
  // Legacy constants - now using dynamic discovery
@@ -185,17 +186,43 @@ export async function generateStoryFromPrompt(req, res) {
185
186
  console.log('Claude prompt:', fullPrompt);
186
187
  const aiText = await callClaude(fullPrompt);
187
188
  console.log('Claude raw response:', aiText);
188
- let fileContents = extractCodeBlock(aiText);
189
- if (!fileContents) {
190
- // Fallback: try to extract from first import statement onward
191
- const importIdx = aiText.indexOf('import');
192
- if (importIdx !== -1) {
193
- fileContents = aiText.slice(importIdx).trim();
189
+ // Use the new robust validation system
190
+ const validationResult = extractAndValidateCodeBlock(aiText);
191
+ let fileContents;
192
+ let hasValidationWarnings = false;
193
+ if (!validationResult.isValid) {
194
+ console.error('Generated code validation failed:', validationResult.errors);
195
+ // If we have fixedCode, use it
196
+ if (validationResult.fixedCode) {
197
+ fileContents = validationResult.fixedCode;
198
+ hasValidationWarnings = true;
199
+ console.log('Using auto-fixed code with warnings:', validationResult.warnings);
200
+ }
201
+ else {
202
+ // Create fallback story
203
+ console.log('Creating fallback story due to validation failure');
204
+ fileContents = createFallbackStory(prompt, config);
205
+ hasValidationWarnings = true;
206
+ }
207
+ }
208
+ else {
209
+ // Extract the validated code
210
+ const codeMatch = aiText.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
211
+ if (codeMatch) {
212
+ fileContents = codeMatch[1].trim();
213
+ }
214
+ else {
215
+ const importIdx = aiText.indexOf('import');
216
+ fileContents = importIdx !== -1 ? aiText.slice(importIdx).trim() : aiText.trim();
217
+ }
218
+ if (validationResult.warnings.length > 0) {
219
+ hasValidationWarnings = true;
220
+ console.log('Validation warnings:', validationResult.warnings);
194
221
  }
195
222
  }
196
- if (!fileContents || !fileContents.startsWith('import')) {
197
- console.error('No valid code block or import found in Claude response. Skipping file write.');
198
- return res.status(500).json({ error: 'Claude did not return a valid code block.' });
223
+ if (!fileContents) {
224
+ console.error('No valid code could be extracted or generated.');
225
+ return res.status(500).json({ error: 'Failed to generate valid TypeScript code.' });
199
226
  }
200
227
  // Generate title based on conversation context
201
228
  let aiTitle;
@@ -255,7 +282,12 @@ export async function generateStoryFromPrompt(req, res) {
255
282
  story: fileContents,
256
283
  environment: 'production',
257
284
  storage: 'in-memory',
258
- isUpdate
285
+ isUpdate,
286
+ validation: {
287
+ hasWarnings: hasValidationWarnings,
288
+ errors: validationResult.errors || [],
289
+ warnings: validationResult.warnings || []
290
+ }
259
291
  });
260
292
  }
261
293
  else {
@@ -270,7 +302,12 @@ export async function generateStoryFromPrompt(req, res) {
270
302
  story: fileContents,
271
303
  environment: 'development',
272
304
  storage: 'file-system',
273
- isUpdate
305
+ isUpdate,
306
+ validation: {
307
+ hasWarnings: hasValidationWarnings,
308
+ errors: validationResult.errors || [],
309
+ warnings: validationResult.warnings || []
310
+ }
274
311
  });
275
312
  }
276
313
  }
@@ -28,13 +28,18 @@ export function loadUserConfig() {
28
28
  console.log(`Loading Story UI config from: ${configPath}`);
29
29
  // Read and evaluate the config file
30
30
  const configContent = fs.readFileSync(configPath, 'utf-8');
31
- // Simple evaluation for CommonJS modules
32
- if (configContent.includes('module.exports')) {
31
+ // Handle both CommonJS and ES modules
32
+ if (configContent.includes('module.exports') || configContent.includes('export default')) {
33
33
  // Create a temporary module context
34
34
  const module = { exports: {} };
35
35
  const exports = module.exports;
36
+ // For ES modules, convert to CommonJS for evaluation
37
+ let evalContent = configContent;
38
+ if (configContent.includes('export default')) {
39
+ evalContent = configContent.replace(/export\s+default\s+/, 'module.exports = ');
40
+ }
36
41
  // Evaluate the config file content
37
- eval(configContent);
42
+ eval(evalContent);
38
43
  const userConfig = module.exports;
39
44
  const config = createStoryUIConfig(userConfig.default || userConfig);
40
45
  // Cache the loaded config
@@ -82,13 +87,14 @@ export function validateConfig(config) {
82
87
  if (!config.componentsPath && !config.componentsMetadataPath && (!config.components || config.components.length === 0)) {
83
88
  errors.push('Either componentsPath, componentsMetadataPath, or a components array must be specified');
84
89
  }
85
- if (config.componentsPath && !fs.existsSync(config.componentsPath)) {
90
+ // Only validate componentsPath if it's provided (not null/undefined)
91
+ if (config.componentsPath && config.componentsPath !== null && !fs.existsSync(config.componentsPath)) {
86
92
  errors.push(`Components path does not exist: ${config.componentsPath}`);
87
93
  }
88
94
  if (config.componentsMetadataPath && !fs.existsSync(config.componentsMetadataPath)) {
89
95
  errors.push(`Components metadata path does not exist: ${config.componentsMetadataPath}`);
90
96
  }
91
- // Check import path
97
+ // Check import path - but allow actual library names like 'antd'
92
98
  if (!config.importPath || config.importPath === 'your-component-library' || config.importPath.trim() === '') {
93
99
  errors.push('importPath must be configured to point to your component library');
94
100
  }
@@ -0,0 +1,262 @@
1
+ import * as ts from 'typescript';
2
+ /**
3
+ * Validates TypeScript code syntax and attempts to fix common issues
4
+ */
5
+ export function validateStoryCode(code, fileName = 'story.tsx') {
6
+ const result = {
7
+ isValid: true,
8
+ errors: [],
9
+ warnings: []
10
+ };
11
+ try {
12
+ // Create a TypeScript source file
13
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
14
+ // Check for syntax errors using the program API
15
+ const compilerOptions = {
16
+ jsx: ts.JsxEmit.ReactJSX,
17
+ target: ts.ScriptTarget.Latest,
18
+ module: ts.ModuleKind.ESNext,
19
+ allowJs: true,
20
+ skipLibCheck: true
21
+ };
22
+ // Create a program to get diagnostics
23
+ const program = ts.createProgram([fileName], compilerOptions, {
24
+ getSourceFile: (name) => name === fileName ? sourceFile : undefined,
25
+ writeFile: () => { },
26
+ getCurrentDirectory: () => '',
27
+ getDirectories: () => [],
28
+ fileExists: (name) => name === fileName,
29
+ readFile: () => '',
30
+ getCanonicalFileName: (name) => name,
31
+ useCaseSensitiveFileNames: () => true,
32
+ getNewLine: () => '\n',
33
+ getDefaultLibFileName: () => 'lib.d.ts'
34
+ });
35
+ const syntaxErrors = program.getSyntacticDiagnostics(sourceFile);
36
+ if (syntaxErrors.length > 0) {
37
+ result.isValid = false;
38
+ for (const diagnostic of syntaxErrors) {
39
+ if (diagnostic.file && diagnostic.start !== undefined) {
40
+ const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
41
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
42
+ result.errors.push(`Line ${position.line + 1}, Column ${position.character + 1}: ${message}`);
43
+ }
44
+ else {
45
+ result.errors.push(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
46
+ }
47
+ }
48
+ }
49
+ // Additional semantic checks
50
+ const semanticErrors = performSemanticChecks(sourceFile);
51
+ result.errors.push(...semanticErrors);
52
+ if (result.errors.length > 0) {
53
+ result.isValid = false;
54
+ // Attempt to fix common issues
55
+ const fixedCode = attemptAutoFix(code, result.errors);
56
+ if (fixedCode && fixedCode !== code) {
57
+ result.fixedCode = fixedCode;
58
+ // Re-validate the fixed code
59
+ const fixedValidation = validateStoryCode(fixedCode, fileName);
60
+ if (fixedValidation.isValid) {
61
+ result.isValid = true;
62
+ result.warnings.push('Code was automatically fixed for syntax errors');
63
+ }
64
+ }
65
+ }
66
+ }
67
+ catch (error) {
68
+ result.isValid = false;
69
+ result.errors.push(`Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
70
+ }
71
+ return result;
72
+ }
73
+ /**
74
+ * Performs additional semantic checks on the AST
75
+ */
76
+ function performSemanticChecks(sourceFile) {
77
+ const errors = [];
78
+ function visit(node) {
79
+ // Check for unclosed JSX elements
80
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
81
+ // Additional JSX-specific checks could go here
82
+ }
83
+ // Check for missing imports
84
+ if (ts.isIdentifier(node) && node.text && /^[A-Z]/.test(node.text)) {
85
+ // This is a potential component reference - would need more context to validate
86
+ }
87
+ ts.forEachChild(node, visit);
88
+ }
89
+ visit(sourceFile);
90
+ return errors;
91
+ }
92
+ /**
93
+ * Attempts to automatically fix common syntax errors
94
+ */
95
+ function attemptAutoFix(code, errors) {
96
+ let fixedCode = code;
97
+ // Fix common issues based on error patterns
98
+ for (const error of errors) {
99
+ if (error.includes('expected ","')) {
100
+ // Try to fix missing commas in object literals
101
+ fixedCode = fixMissingCommas(fixedCode);
102
+ }
103
+ if (error.includes('Unexpected token')) {
104
+ // Try to fix unexpected tokens
105
+ fixedCode = fixUnexpectedTokens(fixedCode);
106
+ }
107
+ if (error.includes('Unterminated string literal')) {
108
+ // Try to fix unterminated strings
109
+ fixedCode = fixUnterminatedStrings(fixedCode);
110
+ }
111
+ if (error.includes('JSX element') && error.includes('has no corresponding closing tag')) {
112
+ // Try to fix unclosed JSX elements
113
+ fixedCode = fixUnclosedJSX(fixedCode);
114
+ }
115
+ }
116
+ return fixedCode;
117
+ }
118
+ /**
119
+ * Fixes missing commas in object literals and function parameters
120
+ */
121
+ function fixMissingCommas(code) {
122
+ // Common patterns where commas might be missing
123
+ const fixes = [
124
+ // Fix object property definitions
125
+ { pattern: /(\w+):\s*([^,}\n]+)\s*\n\s*(\w+):/g, replacement: '$1: $2,\n $3:' },
126
+ // Fix array elements
127
+ { pattern: /(\w+)\s*\n\s*(\w+)/g, replacement: '$1,\n $2' },
128
+ // Fix function parameters
129
+ { pattern: /(\w+:\s*\w+)\s*\n\s*(\w+:)/g, replacement: '$1,\n $2' }
130
+ ];
131
+ let fixedCode = code;
132
+ for (const fix of fixes) {
133
+ fixedCode = fixedCode.replace(fix.pattern, fix.replacement);
134
+ }
135
+ return fixedCode;
136
+ }
137
+ /**
138
+ * Fixes unexpected token issues
139
+ */
140
+ function fixUnexpectedTokens(code) {
141
+ let fixedCode = code;
142
+ // Fix common unexpected token issues
143
+ const fixes = [
144
+ // Fix missing semicolons
145
+ { pattern: /^(\s*)(export\s+\w+.*[^;])\s*$/gm, replacement: '$1$2;' },
146
+ // Fix missing quotes around strings
147
+ { pattern: /:\s*([^"'\s,}]+)\s*(,|\})/g, replacement: ': "$1"$2' },
148
+ // Fix trailing commas in objects
149
+ { pattern: /,(\s*\})/g, replacement: '$1' }
150
+ ];
151
+ for (const fix of fixes) {
152
+ fixedCode = fixedCode.replace(fix.pattern, fix.replacement);
153
+ }
154
+ return fixedCode;
155
+ }
156
+ /**
157
+ * Fixes unterminated string literals
158
+ */
159
+ function fixUnterminatedStrings(code) {
160
+ let fixedCode = code;
161
+ // Find lines with unterminated strings and try to fix them
162
+ const lines = fixedCode.split('\n');
163
+ for (let i = 0; i < lines.length; i++) {
164
+ const line = lines[i];
165
+ // Check for unterminated quotes
166
+ const singleQuoteCount = (line.match(/'/g) || []).length;
167
+ const doubleQuoteCount = (line.match(/"/g) || []).length;
168
+ if (singleQuoteCount % 2 !== 0) {
169
+ lines[i] = line + "'";
170
+ }
171
+ else if (doubleQuoteCount % 2 !== 0) {
172
+ lines[i] = line + '"';
173
+ }
174
+ }
175
+ return lines.join('\n');
176
+ }
177
+ /**
178
+ * Fixes unclosed JSX elements
179
+ */
180
+ function fixUnclosedJSX(code) {
181
+ let fixedCode = code;
182
+ // Simple heuristic to fix common JSX issues
183
+ const jsxOpenTags = fixedCode.match(/<[A-Z]\w*[^>]*>/g) || [];
184
+ const jsxCloseTags = fixedCode.match(/<\/[A-Z]\w*>/g) || [];
185
+ // If we have more opening tags than closing tags, try to balance them
186
+ if (jsxOpenTags.length > jsxCloseTags.length) {
187
+ // This is a very basic fix - in practice, you'd need more sophisticated parsing
188
+ // For now, we'll just add a warning
189
+ }
190
+ return fixedCode;
191
+ }
192
+ /**
193
+ * Extracts and validates code blocks from AI responses
194
+ */
195
+ export function extractAndValidateCodeBlock(aiResponse) {
196
+ // Try multiple extraction methods
197
+ const extractionMethods = [
198
+ // Standard code blocks
199
+ (text) => {
200
+ const match = text.match(/```(?:tsx|jsx|typescript|ts|js|javascript)?\s*([\s\S]*?)\s*```/i);
201
+ return match ? match[1].trim() : null;
202
+ },
203
+ // Code starting with import
204
+ (text) => {
205
+ const importIndex = text.indexOf('import');
206
+ return importIndex !== -1 ? text.slice(importIndex).trim() : null;
207
+ },
208
+ // Code starting with export
209
+ (text) => {
210
+ const exportIndex = text.indexOf('export');
211
+ return exportIndex !== -1 ? text.slice(exportIndex).trim() : null;
212
+ }
213
+ ];
214
+ let extractedCode = null;
215
+ for (const method of extractionMethods) {
216
+ extractedCode = method(aiResponse);
217
+ if (extractedCode)
218
+ break;
219
+ }
220
+ if (!extractedCode) {
221
+ return {
222
+ isValid: false,
223
+ errors: ['No valid TypeScript code found in AI response'],
224
+ warnings: []
225
+ };
226
+ }
227
+ // Validate the extracted code
228
+ return validateStoryCode(extractedCode);
229
+ }
230
+ /**
231
+ * Creates a fallback story template when generation fails
232
+ */
233
+ export function createFallbackStory(prompt, config) {
234
+ const title = prompt.length > 50 ? prompt.substring(0, 50) + '...' : prompt;
235
+ const escapedTitle = title.replace(/"/g, '\\"');
236
+ return `import React from 'react';
237
+ import type { StoryObj } from '@storybook/react';
238
+
239
+ // Fallback story generated due to AI generation error
240
+ export default {
241
+ title: '${config.storyPrefix || 'Generated/'}${escapedTitle}',
242
+ component: () => (
243
+ <div style={{ padding: '2rem', textAlign: 'center', border: '2px dashed #ccc', borderRadius: '8px' }}>
244
+ <h2>Story Generation Error</h2>
245
+ <p>The AI-generated story contained syntax errors and could not be created.</p>
246
+ <p><strong>Original prompt:</strong> ${escapedTitle}</p>
247
+ <p>Please try rephrasing your request or contact support.</p>
248
+ </div>
249
+ ),
250
+ parameters: {
251
+ docs: {
252
+ description: {
253
+ story: 'This is a fallback story created when the AI generation failed due to syntax errors.'
254
+ }
255
+ }
256
+ }
257
+ };
258
+
259
+ export const Default: StoryObj = {
260
+ args: {}
261
+ };`;
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "AI-powered Storybook story generator for any React component library",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "license": "MIT",
42
42
  "repository": {
43
43
  "type": "git",
44
- "url": "https://github.com/southleft/story-ui.git"
44
+ "url": "git+https://github.com/southleft/story-ui.git"
45
45
  },
46
46
  "bugs": {
47
47
  "url": "https://github.com/southleft/story-ui/issues"
@@ -62,7 +62,8 @@
62
62
  "dotenv": "^16.3.1",
63
63
  "express": "^4.18.2",
64
64
  "inquirer": "^9.2.0",
65
- "node-fetch": "^2.6.7"
65
+ "node-fetch": "^2.6.7",
66
+ "typescript": "^5.8.3"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@commitlint/cli": "^19.6.1",
@@ -82,8 +83,7 @@
82
83
  "cz-conventional-changelog": "^3.3.0",
83
84
  "husky": "^9.1.7",
84
85
  "semantic-release": "^24.2.0",
85
- "ts-node": "^10.9.2",
86
- "typescript": "^5.8.3"
86
+ "ts-node": "^10.9.2"
87
87
  },
88
88
  "peerDependencies": {
89
89
  "@storybook/react": ">=6.0.0",
@@ -501,10 +501,45 @@ const StoryUIPanel: React.FC = () => {
501
501
 
502
502
  // Create user-friendly response message instead of showing raw markup
503
503
  let responseMessage: string;
504
+ let statusIcon = '✅';
505
+
506
+ // Check for validation issues
507
+ if (data.validation && data.validation.hasWarnings) {
508
+ statusIcon = '⚠️';
509
+ const warningCount = data.validation.warnings.length;
510
+ const errorCount = data.validation.errors.length;
511
+
512
+ if (errorCount > 0) {
513
+ statusIcon = '🔧';
514
+ }
515
+ }
516
+
504
517
  if (data.isUpdate) {
505
- responseMessage = `✅ Updated your story: "${data.title}"\n\nI've made the requested changes while keeping the same layout structure. You can view the updated component in Storybook.`;
518
+ responseMessage = `${statusIcon} Updated your story: "${data.title}"\n\nI've made the requested changes while keeping the same layout structure. You can view the updated component in Storybook.`;
506
519
  } else {
507
- responseMessage = `✅ Created new story: "${data.title}"\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup in the Docs tab.`;
520
+ responseMessage = `${statusIcon} Created new story: "${data.title}"\n\nI've generated the component with the requested features. You can view it in Storybook where you'll see both the rendered component and its markup in the Docs tab.`;
521
+ }
522
+
523
+ // Add validation information if there are issues
524
+ if (data.validation && data.validation.hasWarnings) {
525
+ responseMessage += '\n\n';
526
+
527
+ if (data.validation.errors.length > 0) {
528
+ responseMessage += `🔧 **Auto-fixed ${data.validation.errors.length} syntax error(s):**\n`;
529
+ responseMessage += data.validation.errors.slice(0, 3).map(error => ` • ${error}`).join('\n');
530
+ if (data.validation.errors.length > 3) {
531
+ responseMessage += `\n • ... and ${data.validation.errors.length - 3} more`;
532
+ }
533
+ responseMessage += '\n';
534
+ }
535
+
536
+ if (data.validation.warnings.length > 0) {
537
+ responseMessage += `⚠️ **Warnings:**\n`;
538
+ responseMessage += data.validation.warnings.slice(0, 2).map(warning => ` • ${warning}`).join('\n');
539
+ if (data.validation.warnings.length > 2) {
540
+ responseMessage += `\n • ... and ${data.validation.warnings.length - 2} more`;
541
+ }
542
+ }
508
543
  }
509
544
 
510
545
  const aiMsg = { role: 'ai' as const, content: responseMessage };
@@ -1,3 +0,0 @@
1
- export { Button } from './Button';
2
- export { Card } from './Card';
3
- export { Input } from './Input';
@@ -1,3 +0,0 @@
1
- export { Button } from './Button';
2
- export { Card } from './Card';
3
- export { Input } from './Input';
@@ -1 +0,0 @@
1
- {"root":["../index.ts","../story-ui.config.loader.ts","../story-ui.config.ts","../cli/index.ts","../cli/setup.ts","../mcp-server/index.ts","../mcp-server/routes/claude.ts","../mcp-server/routes/components.ts","../mcp-server/routes/generatestory.ts","../mcp-server/routes/memorystories.ts","../mcp-server/routes/storysync.ts","../story-generator/componentdiscovery.ts","../story-generator/configloader.ts","../story-generator/generatestory.ts","../story-generator/gitignoremanager.ts","../story-generator/inmemorystoryservice.ts","../story-generator/productiongitignoremanager.ts","../story-generator/promptgenerator.ts","../story-generator/storysync.ts"],"version":"5.8.3"}