vibe-design-system 2.9.0 → 2.9.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/bin/init.js +42 -2
- package/package.json +1 -1
- package/vds-core-template/scan.mjs +26 -1
- package/vds-core-template/story-generator.mjs +272 -7
package/bin/init.js
CHANGED
|
@@ -24,6 +24,7 @@ const TEMPLATE_DIR = path.join(INSTALLER_ROOT, "vds-core-template");
|
|
|
24
24
|
const STORYBOOK_MAIN_TS = `import type { StorybookConfig } from "@storybook/react-vite";
|
|
25
25
|
import { mergeConfig } from "vite";
|
|
26
26
|
import path from "path";
|
|
27
|
+
import fs from "fs";
|
|
27
28
|
|
|
28
29
|
const config: StorybookConfig = {
|
|
29
30
|
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
@@ -33,10 +34,48 @@ const config: StorybookConfig = {
|
|
|
33
34
|
options: {},
|
|
34
35
|
},
|
|
35
36
|
async viteFinal(config) {
|
|
37
|
+
// Phase C2 — read tsconfig.json paths and inject as Vite aliases
|
|
38
|
+
const extraAliases = (() => {
|
|
39
|
+
try {
|
|
40
|
+
const tsConfigPaths = [
|
|
41
|
+
path.resolve(process.cwd(), "tsconfig.json"),
|
|
42
|
+
path.resolve(process.cwd(), "tsconfig.app.json"),
|
|
43
|
+
];
|
|
44
|
+
for (const tcp of tsConfigPaths) {
|
|
45
|
+
if (!fs.existsSync(tcp)) continue;
|
|
46
|
+
const raw = JSON.parse(fs.readFileSync(tcp, "utf-8").replace(/\\/\\/[^\\n]*/g, "").replace(/,(\\s*[}\\]])/g, "$1"));
|
|
47
|
+
const paths = raw?.compilerOptions?.paths || {};
|
|
48
|
+
const aliases: Record<string, string> = {};
|
|
49
|
+
for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
|
|
50
|
+
const cleanAlias = alias.replace(/\\/\\*$/, "");
|
|
51
|
+
const target = targets[0]?.replace(/\\/\\*$/, "") || "";
|
|
52
|
+
if (cleanAlias && target && cleanAlias !== "@") {
|
|
53
|
+
aliases[cleanAlias] = path.resolve(process.cwd(), target);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (Object.keys(aliases).length > 0) return aliases;
|
|
57
|
+
}
|
|
58
|
+
} catch (_) {}
|
|
59
|
+
return {};
|
|
60
|
+
})();
|
|
36
61
|
return mergeConfig(config, {
|
|
62
|
+
plugins: [
|
|
63
|
+
{
|
|
64
|
+
// Mock figma:asset/* imports — returns empty string so components render without crashing in Storybook
|
|
65
|
+
name: "vds-figma-asset-mock",
|
|
66
|
+
enforce: "pre" as const,
|
|
67
|
+
resolveId(id: string) {
|
|
68
|
+
if (id.startsWith("figma:asset")) return "\\0vds-figma-asset-mock";
|
|
69
|
+
},
|
|
70
|
+
load(id: string) {
|
|
71
|
+
if (id === "\\0vds-figma-asset-mock") return "export default '';";
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
37
75
|
resolve: {
|
|
38
76
|
alias: {
|
|
39
77
|
"@": path.resolve(process.cwd(), "src"),
|
|
78
|
+
...extraAliases,
|
|
40
79
|
},
|
|
41
80
|
},
|
|
42
81
|
});
|
|
@@ -338,6 +377,7 @@ export default config;
|
|
|
338
377
|
return `import type { StorybookConfig } from "@storybook/react-vite";
|
|
339
378
|
import { mergeConfig } from "vite";
|
|
340
379
|
import path from "path";
|
|
380
|
+
import fs from "fs";
|
|
341
381
|
|
|
342
382
|
const config: StorybookConfig = {
|
|
343
383
|
stories: [
|
|
@@ -358,8 +398,8 @@ const config: StorybookConfig = {
|
|
|
358
398
|
path.resolve(process.cwd(), "${srcPrefix}", "..", "tsconfig.json"),
|
|
359
399
|
];
|
|
360
400
|
for (const tcp of tsConfigPaths) {
|
|
361
|
-
if (!
|
|
362
|
-
const raw = JSON.parse(
|
|
401
|
+
if (!fs.existsSync(tcp)) continue;
|
|
402
|
+
const raw = JSON.parse(fs.readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
|
|
363
403
|
const paths = raw?.compilerOptions?.paths || {};
|
|
364
404
|
const aliases: Record<string, string> = {};
|
|
365
405
|
for (const [alias, targets] of Object.entries(paths) as [string, string[]][]) {
|
package/package.json
CHANGED
|
@@ -1145,6 +1145,8 @@ function extractTailwindTokens(content) {
|
|
|
1145
1145
|
/className\s*=\s*\{\s*["'`]([^"'`]+)["'`]/g,
|
|
1146
1146
|
/cn\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
|
1147
1147
|
/cva\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
|
1148
|
+
// CVA variant values: captures `default: "bg-x text-y"`, `destructive: "border-x..."` etc.
|
|
1149
|
+
/\b\w+\s*:\s*["'`]([a-zA-Z0-9_\-\/\s\[\]&>:%.]+)["'`]/g,
|
|
1148
1150
|
/["'`]([a-zA-Z0-9_\-\/\s\[\]&:%.]+(?:hover|focus|active|disabled|sm|md|lg|xl|2xl|dark:)[a-zA-Z0-9_\-\/\s\[\]&:%.]*)["'`]/g,
|
|
1149
1151
|
];
|
|
1150
1152
|
for (const re of patterns) {
|
|
@@ -2470,6 +2472,26 @@ function isComplexPageComponent(content) {
|
|
|
2470
2472
|
return true;
|
|
2471
2473
|
}
|
|
2472
2474
|
|
|
2475
|
+
/**
|
|
2476
|
+
* Extract top-level (no indentation) PascalCase component names from a source file.
|
|
2477
|
+
* Used for isPageComponent files to find internal sub-components that can be auto-exported.
|
|
2478
|
+
* Only matches declarations at column 0 (^) to exclude nested arrow functions.
|
|
2479
|
+
*/
|
|
2480
|
+
function extractTopLevelInternalComponentNames(content) {
|
|
2481
|
+
// Match only unindented `const ComponentName = (` or `const ComponentName: React.FC = (`
|
|
2482
|
+
const re = /^(?:export\s+)?const\s+([A-Z][A-Za-z0-9]+)\s*[:=]\s*(?:\(|\bReact\.)/gm;
|
|
2483
|
+
const names = [];
|
|
2484
|
+
const seen = new Set();
|
|
2485
|
+
let m;
|
|
2486
|
+
while ((m = re.exec(content)) !== null) {
|
|
2487
|
+
if (!seen.has(m[1])) {
|
|
2488
|
+
seen.add(m[1]);
|
|
2489
|
+
names.push(m[1]);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return names;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2473
2495
|
function scan() {
|
|
2474
2496
|
const relativeFiles = COMPONENTS_DIR ? getAllComponentFiles(COMPONENTS_DIR) : [];
|
|
2475
2497
|
if (!COMPONENTS_DIR) {
|
|
@@ -2495,7 +2517,10 @@ function scan() {
|
|
|
2495
2517
|
}
|
|
2496
2518
|
const tokens = extractTailwindTokens(content);
|
|
2497
2519
|
const isPageComponent = isComplexPageComponent(content);
|
|
2498
|
-
const
|
|
2520
|
+
const internalComponentNames = isPageComponent
|
|
2521
|
+
? extractTopLevelInternalComponentNames(content).filter(n => n !== name)
|
|
2522
|
+
: undefined;
|
|
2523
|
+
const comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true, internalComponentNames } : {}) };
|
|
2499
2524
|
// Phase D — CSS Module tokens
|
|
2500
2525
|
const cssModuleTokens = extractCssModuleTokens(COMPONENTS_DIR ? path.join(COMPONENTS_DIR, rel) : null);
|
|
2501
2526
|
if (cssModuleTokens) comp.cssModuleTokens = cssModuleTokens;
|
|
@@ -2208,8 +2208,11 @@ function buildStoryFileContent(comp) {
|
|
|
2208
2208
|
lines.push(` component: ComponentRef,`);
|
|
2209
2209
|
// SECTION: no props/args → autodocs tries to render React.lazy without Suspense → useRef crash
|
|
2210
2210
|
if (profile !== "SECTION") lines.push(` tags: ["autodocs"],`);
|
|
2211
|
-
// Center small components
|
|
2212
|
-
if (profile !== "SECTION")
|
|
2211
|
+
// Center small components; fullscreen for complex page-level components
|
|
2212
|
+
if (profile !== "SECTION") {
|
|
2213
|
+
const layout = comp.isPageComponent ? "fullscreen" : "centered";
|
|
2214
|
+
lines.push(` parameters: { layout: "${layout}" },`);
|
|
2215
|
+
}
|
|
2213
2216
|
// Wrap with QueryClientProvider for components that use @tanstack/react-query hooks
|
|
2214
2217
|
if (needsQueryClient) {
|
|
2215
2218
|
lines.push(` decorators: [(Story: any) => React.createElement(QueryClientProvider, { client: _queryClient }, React.createElement(Story))],`);
|
|
@@ -5200,11 +5203,8 @@ function main() {
|
|
|
5200
5203
|
const storyFileName = `${componentName}.stories.tsx`;
|
|
5201
5204
|
const storyPath = path.join(STORIES_DIR, storyFileName);
|
|
5202
5205
|
if (SKIP_LIST.includes(componentName)) continue;
|
|
5203
|
-
//
|
|
5204
|
-
|
|
5205
|
-
console.log(`[VDS] ${componentName} → skipped (complex page component — add to extraSkipList to suppress this message)`);
|
|
5206
|
-
continue;
|
|
5207
|
-
}
|
|
5206
|
+
// Complex page-level components (500+ lines, 4+ inline sub-components) — generate fullscreen story instead of skipping
|
|
5207
|
+
// (Previously skipped; now a simplified fullscreen story is generated so they appear in Storybook)
|
|
5208
5208
|
const requiredCount = Array.isArray(comp.props) ? comp.props.filter((p) => p.required === true).length : 0;
|
|
5209
5209
|
if (requiredCount > 3) {
|
|
5210
5210
|
console.log(`[VDS] ${componentName} → skipped (${requiredCount} required props — too complex to auto-generate)`);
|
|
@@ -5224,6 +5224,12 @@ function main() {
|
|
|
5224
5224
|
writtenCount++;
|
|
5225
5225
|
}
|
|
5226
5226
|
|
|
5227
|
+
// Phase I — Internal component sub-stories for isPageComponent files
|
|
5228
|
+
for (const comp of components) {
|
|
5229
|
+
if (!comp.isPageComponent || !Array.isArray(comp.internalComponentNames) || comp.internalComponentNames.length === 0) continue;
|
|
5230
|
+
generateInternalComponentStories(comp);
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5227
5233
|
// Summary
|
|
5228
5234
|
if (writtenCount === 0 && components.length > 0) {
|
|
5229
5235
|
const hasShadcnGroup = components.some(c => {
|
|
@@ -5240,5 +5246,264 @@ function main() {
|
|
|
5240
5246
|
}
|
|
5241
5247
|
}
|
|
5242
5248
|
|
|
5249
|
+
// ─── Phase I: Internal Component Sub-Story Engine ─────────────────────────────
|
|
5250
|
+
/**
|
|
5251
|
+
* Adds `export` to top-level internal component declarations in a source file.
|
|
5252
|
+
* Non-destructive: only adds `export` keyword, no logic changes.
|
|
5253
|
+
* Returns the list of names that were newly exported.
|
|
5254
|
+
*/
|
|
5255
|
+
function autoExportInternalComponents(sourceFilePath, names) {
|
|
5256
|
+
let src = fs.readFileSync(sourceFilePath, "utf-8");
|
|
5257
|
+
const newly = [];
|
|
5258
|
+
for (const n of names) {
|
|
5259
|
+
// Already exported? skip
|
|
5260
|
+
if (new RegExp(`^export\\s+const\\s+${n}\\b`, "m").test(src)) continue;
|
|
5261
|
+
// Exists as unexported top-level const?
|
|
5262
|
+
const re = new RegExp(`^(const\\s+${n}\\b)`, "m");
|
|
5263
|
+
if (!re.test(src)) continue;
|
|
5264
|
+
src = src.replace(re, "export $1");
|
|
5265
|
+
newly.push(n);
|
|
5266
|
+
}
|
|
5267
|
+
if (newly.length > 0) fs.writeFileSync(sourceFilePath, src, "utf-8");
|
|
5268
|
+
return newly;
|
|
5269
|
+
}
|
|
5270
|
+
|
|
5271
|
+
/**
|
|
5272
|
+
* Parse top-level relative import statements (local files only).
|
|
5273
|
+
* Returns [{importPath, symbols}]
|
|
5274
|
+
*/
|
|
5275
|
+
function parseLocalDataImports(content) {
|
|
5276
|
+
const re = /^import\s*\{([^}]+)\}\s*from\s*["'](\.[^"']+)["']/gm;
|
|
5277
|
+
const results = [];
|
|
5278
|
+
let m;
|
|
5279
|
+
while ((m = re.exec(content)) !== null) {
|
|
5280
|
+
const symbols = m[1].split(",").map(s => s.trim().replace(/\s+as\s+\w+$/, "").trim()).filter(Boolean);
|
|
5281
|
+
results.push({ importPath: m[2], symbols });
|
|
5282
|
+
}
|
|
5283
|
+
return results;
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
/**
|
|
5287
|
+
* Detect named exports from a data file: returns { exportName → 'array' | 'object' | 'scalar' }
|
|
5288
|
+
*/
|
|
5289
|
+
function parseDataFileExports(filePath) {
|
|
5290
|
+
if (!fs.existsSync(filePath)) return {};
|
|
5291
|
+
const src = fs.readFileSync(filePath, "utf-8");
|
|
5292
|
+
const result = {};
|
|
5293
|
+
const re = /^export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]/gm;
|
|
5294
|
+
let m;
|
|
5295
|
+
while ((m = re.exec(src)) !== null) {
|
|
5296
|
+
const name = m[1];
|
|
5297
|
+
const afterIdx = src.indexOf("=", m.index) + 1;
|
|
5298
|
+
const afterSlice = src.slice(afterIdx, afterIdx + 10).trimStart();
|
|
5299
|
+
result[name] = afterSlice.startsWith("[") ? "array" : afterSlice.startsWith("{") ? "object" : "scalar";
|
|
5300
|
+
}
|
|
5301
|
+
return result;
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
/**
|
|
5305
|
+
* Extract inline TypeScript prop types for a named component from source content.
|
|
5306
|
+
* Handles: `const Foo = ({ a, b }: { a: boolean; b: string }) =>`
|
|
5307
|
+
* Handles nested object types (e.g., `data: { subStation: SubStation; } | null`)
|
|
5308
|
+
* Returns [{name, type}]
|
|
5309
|
+
*/
|
|
5310
|
+
function extractComponentProps(content, componentName) {
|
|
5311
|
+
const esc = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5312
|
+
const declRe = new RegExp(`(?:export\\s+)?const\\s+${esc}\\s*[:=]\\s*\\(`, "g");
|
|
5313
|
+
const declMatch = declRe.exec(content);
|
|
5314
|
+
if (!declMatch) return [];
|
|
5315
|
+
const slice = content.slice(declMatch.index, declMatch.index + 5000);
|
|
5316
|
+
// Find `}: {` — start of inline type annotation
|
|
5317
|
+
const typeHeaderIdx = slice.search(/\}\s*:\s*\{/);
|
|
5318
|
+
if (typeHeaderIdx === -1) return [];
|
|
5319
|
+
const braceStart = slice.indexOf("{", typeHeaderIdx);
|
|
5320
|
+
// Walk forward tracking brace depth to find the matching closing brace
|
|
5321
|
+
let depth = 0, braceEnd = -1;
|
|
5322
|
+
for (let i = braceStart; i < slice.length; i++) {
|
|
5323
|
+
if (slice[i] === "{") depth++;
|
|
5324
|
+
else if (slice[i] === "}") { depth--; if (depth === 0) { braceEnd = i; break; } }
|
|
5325
|
+
}
|
|
5326
|
+
if (braceEnd === -1) return [];
|
|
5327
|
+
const typeBody = slice.slice(braceStart + 1, braceEnd);
|
|
5328
|
+
// Smart prop parser: tracks depth so `;` inside nested types is skipped
|
|
5329
|
+
const props = [];
|
|
5330
|
+
let i = 0;
|
|
5331
|
+
while (i < typeBody.length) {
|
|
5332
|
+
while (i < typeBody.length && /\s/.test(typeBody[i])) i++;
|
|
5333
|
+
if (i >= typeBody.length) break;
|
|
5334
|
+
const nameMatch = typeBody.slice(i).match(/^(\w+)\??(\s*:\s*)/);
|
|
5335
|
+
if (!nameMatch) { i++; continue; }
|
|
5336
|
+
const propName = nameMatch[1];
|
|
5337
|
+
i += nameMatch[0].length;
|
|
5338
|
+
let typeDepth = 0;
|
|
5339
|
+
const typeStartIdx = i;
|
|
5340
|
+
while (i < typeBody.length) {
|
|
5341
|
+
const ch = typeBody[i];
|
|
5342
|
+
if ("{([<".includes(ch)) typeDepth++;
|
|
5343
|
+
// `>` only closes angle brackets (generics), not `=>` arrow functions
|
|
5344
|
+
else if ("})]".includes(ch) || (ch === ">" && typeDepth > 0)) typeDepth--;
|
|
5345
|
+
else if (ch === ";" && typeDepth === 0) break;
|
|
5346
|
+
i++;
|
|
5347
|
+
}
|
|
5348
|
+
const type = typeBody.slice(typeStartIdx, i).trim();
|
|
5349
|
+
if (propName && type && !propName.startsWith("//") && propName !== "readonly") {
|
|
5350
|
+
props.push({ name: propName, type });
|
|
5351
|
+
}
|
|
5352
|
+
i++; // skip `;`
|
|
5353
|
+
}
|
|
5354
|
+
return props;
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
/**
|
|
5358
|
+
* Generate a mock value string for a TypeScript type, using available data exports.
|
|
5359
|
+
*/
|
|
5360
|
+
function mockValueForType(type, localExports) {
|
|
5361
|
+
const t = type.trim();
|
|
5362
|
+
if (t === "boolean") return "false";
|
|
5363
|
+
if (t === "string") return '""';
|
|
5364
|
+
if (t === "number") return "0";
|
|
5365
|
+
if (t === "null" || t === "undefined") return "null";
|
|
5366
|
+
if (t.startsWith("() =>") || t === "VoidFunction") return "() => {}";
|
|
5367
|
+
if (t.startsWith("(") && t.includes("=>")) return "() => {}";
|
|
5368
|
+
// Inline object type → null (complex; user fills it in)
|
|
5369
|
+
if (t.startsWith("{")) return "null";
|
|
5370
|
+
// Union ending in null/undefined → null
|
|
5371
|
+
if (t.endsWith("| null") || t.endsWith("| undefined") || t.startsWith("null |")) return "null";
|
|
5372
|
+
// String literal union → pick first quoted value
|
|
5373
|
+
const firstLiteral = t.match(/["']([^"']+)["']/);
|
|
5374
|
+
if (firstLiteral && (t.startsWith('"') || t.startsWith("'"))) return `"${firstLiteral[1]}"`;
|
|
5375
|
+
|
|
5376
|
+
// Try matching named type to available array exports
|
|
5377
|
+
// Strip common suffixes to get the "base" name (e.g., RouteDef→route, SubStation→substation)
|
|
5378
|
+
const typeLower = t.toLowerCase().replace(/def$|id$|key$|type$|interface$|kind$/i, "");
|
|
5379
|
+
if (typeLower.length >= 3) {
|
|
5380
|
+
for (const [expName, expKind] of Object.entries(localExports)) {
|
|
5381
|
+
if (expKind !== "array") continue;
|
|
5382
|
+
const expBase = expName.toLowerCase().replace(/_/g, "").replace(/s$/, "").replace(/data$/, "");
|
|
5383
|
+
// Direct match: RouteDef → ROUTES (routedef→rout vs routes→rout)
|
|
5384
|
+
if (expBase.slice(0, 4) === typeLower.slice(0, 4) || typeLower.slice(0, 4) === expBase.slice(0, 4)) {
|
|
5385
|
+
return `${expName}[0]`;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
return "undefined as any";
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
/**
|
|
5393
|
+
* Build the story file content for a single internal component.
|
|
5394
|
+
*/
|
|
5395
|
+
function buildInternalComponentStoryContent(compName, parentImportPath, dataImportPath, dataSymbols, localExports, props, parentComp) {
|
|
5396
|
+
const lines = [];
|
|
5397
|
+
lines.push(`// @vds-regenerate — VDS auto-generated. Remove this line to prevent overwrite.`);
|
|
5398
|
+
lines.push(`import React from "react";`);
|
|
5399
|
+
lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
|
|
5400
|
+
lines.push(`import { ${compName} } from "${parentImportPath}";`);
|
|
5401
|
+
if (dataImportPath && dataSymbols.length > 0) {
|
|
5402
|
+
lines.push(`import { ${dataSymbols.join(", ")} } from "${dataImportPath}";`);
|
|
5403
|
+
}
|
|
5404
|
+
lines.push(``);
|
|
5405
|
+
lines.push(`/**`);
|
|
5406
|
+
lines.push(` * **${compName}** — sub-component of ${parentComp.name}.`);
|
|
5407
|
+
lines.push(` * Auto-exported by VDS for isolated Storybook documentation.`);
|
|
5408
|
+
lines.push(` */`);
|
|
5409
|
+
lines.push(`const meta: Meta<typeof ${compName}> = {`);
|
|
5410
|
+
lines.push(` title: "${parentComp.group || "Screens"}/${parentComp.name}/${compName}",`);
|
|
5411
|
+
lines.push(` component: ${compName},`);
|
|
5412
|
+
lines.push(` parameters: {`);
|
|
5413
|
+
lines.push(` layout: "centered",`);
|
|
5414
|
+
lines.push(` backgrounds: { default: "dark" },`);
|
|
5415
|
+
lines.push(` },`);
|
|
5416
|
+
lines.push(`};`);
|
|
5417
|
+
lines.push(`export default meta;`);
|
|
5418
|
+
lines.push(`type Story = StoryObj<typeof meta>;`);
|
|
5419
|
+
lines.push(``);
|
|
5420
|
+
|
|
5421
|
+
if (props.length > 0) {
|
|
5422
|
+
const argLines = props.map(p => ` ${p.name}: ${mockValueForType(p.type, localExports)},`).join("\n");
|
|
5423
|
+
lines.push(`export const Default: Story = {`);
|
|
5424
|
+
lines.push(` args: {`);
|
|
5425
|
+
lines.push(argLines);
|
|
5426
|
+
lines.push(` },`);
|
|
5427
|
+
lines.push(`};`);
|
|
5428
|
+
} else {
|
|
5429
|
+
lines.push(`export const Default: Story = {};`);
|
|
5430
|
+
}
|
|
5431
|
+
return lines.join("\n") + "\n";
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5434
|
+
/**
|
|
5435
|
+
* Main orchestrator: auto-exports internal components + generates individual story files.
|
|
5436
|
+
* Called for each comp with isPageComponent: true.
|
|
5437
|
+
*/
|
|
5438
|
+
function generateInternalComponentStories(comp) {
|
|
5439
|
+
const names = comp.internalComponentNames || [];
|
|
5440
|
+
if (names.length === 0) return;
|
|
5441
|
+
|
|
5442
|
+
// Locate the source file (COMPONENTS_REL_DIR is a module-level variable set in main())
|
|
5443
|
+
const sourceFile = path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
|
|
5444
|
+
if (!fs.existsSync(sourceFile)) return;
|
|
5445
|
+
|
|
5446
|
+
const sourceContent = fs.readFileSync(sourceFile, "utf-8");
|
|
5447
|
+
|
|
5448
|
+
// Step 1: Auto-export internal components
|
|
5449
|
+
const newly = autoExportInternalComponents(sourceFile, names);
|
|
5450
|
+
if (newly.length > 0) {
|
|
5451
|
+
console.log(`[VDS] ${comp.name} → auto-exported ${newly.length} sub-components: ${newly.join(", ")}`);
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
// Step 2: Find best local data import (most array-typed exports)
|
|
5455
|
+
const localImports = parseLocalDataImports(sourceContent);
|
|
5456
|
+
let bestImportPath = null;
|
|
5457
|
+
let bestSymbols = [];
|
|
5458
|
+
let bestExports = {};
|
|
5459
|
+
for (const li of localImports) {
|
|
5460
|
+
const resolved = path.resolve(path.dirname(sourceFile), li.importPath);
|
|
5461
|
+
const exts = ["", ".ts", ".tsx", ".js", ".jsx"];
|
|
5462
|
+
let found = null;
|
|
5463
|
+
for (const ext of exts) {
|
|
5464
|
+
const p = resolved + ext;
|
|
5465
|
+
if (fs.existsSync(p)) { found = p; break; }
|
|
5466
|
+
}
|
|
5467
|
+
if (!found) continue;
|
|
5468
|
+
const exports = parseDataFileExports(found);
|
|
5469
|
+
const arrayCount = Object.values(exports).filter(k => k === "array").length;
|
|
5470
|
+
if (arrayCount > Object.values(bestExports).filter(k => k === "array").length) {
|
|
5471
|
+
bestImportPath = li.importPath;
|
|
5472
|
+
bestSymbols = li.symbols;
|
|
5473
|
+
bestExports = exports;
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
// Step 3: Compute import paths relative to STORIES_DIR
|
|
5478
|
+
const storiesRelToSource = path.relative(path.dirname(sourceFile), path.join(PROJECT_ROOT, "src", "stories"));
|
|
5479
|
+
const sourceRelFromStories = path.relative(
|
|
5480
|
+
path.join(PROJECT_ROOT, "src", "stories"),
|
|
5481
|
+
sourceFile.replace(/\.(tsx?|jsx?)$/, "")
|
|
5482
|
+
).replace(/\\/g, "/");
|
|
5483
|
+
const parentImportPath = sourceRelFromStories.startsWith(".") ? sourceRelFromStories : "./" + sourceRelFromStories;
|
|
5484
|
+
|
|
5485
|
+
let dataImportFromStories = null;
|
|
5486
|
+
if (bestImportPath) {
|
|
5487
|
+
const dataAbsolute = path.resolve(path.dirname(sourceFile), bestImportPath);
|
|
5488
|
+
dataImportFromStories = path.relative(
|
|
5489
|
+
path.join(PROJECT_ROOT, "src", "stories"),
|
|
5490
|
+
dataAbsolute
|
|
5491
|
+
).replace(/\\/g, "/").replace(/\.(tsx?|jsx?)$/, "");
|
|
5492
|
+
if (!dataImportFromStories.startsWith(".")) dataImportFromStories = "./" + dataImportFromStories;
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
// Step 4: Generate individual story files
|
|
5496
|
+
for (const name of names) {
|
|
5497
|
+
const storyFile = path.join(STORIES_DIR, `${name}.stories.tsx`);
|
|
5498
|
+
if (fs.existsSync(storyFile)) continue; // Never overwrite existing stories
|
|
5499
|
+
const props = extractComponentProps(sourceContent, name);
|
|
5500
|
+
const content = buildInternalComponentStoryContent(
|
|
5501
|
+
name, parentImportPath, dataImportFromStories, bestSymbols, bestExports, props, comp
|
|
5502
|
+
);
|
|
5503
|
+
fs.writeFileSync(storyFile, content, "utf-8");
|
|
5504
|
+
console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyFile)} (sub-component of ${comp.name})`);
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5243
5508
|
main();
|
|
5244
5509
|
|