vibe-design-system 2.9.0 → 2.9.2

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 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 (!require("fs").existsSync(tcp)) continue;
362
- const raw = JSON.parse(require("fs").readFileSync(tcp, "utf-8").replace(/\/\/[^\n]*/g, "").replace(/,(\s*[}\]])/g, "$1"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -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 comp = { file: rel, name, group, category, description, tokens, ...(isPageComponent ? { isPageComponent: true } : {}) };
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 (VARIANT, WRAPPER, CONFIGURED, SAFE) to prevent vertical stretching
2212
- if (profile !== "SECTION") lines.push(` parameters: { layout: "centered" },`);
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
- // Skip complex page-level components detected by scan (500+ lines, 4+ inline sub-components)
5204
- if (comp.isPageComponent) {
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,359 @@ 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
+ * Pass sourceContent to enable local interface/type parsing and nested property lookup.
5360
+ */
5361
+ function mockValueForType(type, localExports, sourceContent) {
5362
+ const t = type.trim();
5363
+ if (t === "boolean") return "false";
5364
+ if (t === "string") return '""';
5365
+ if (t === "number") return "0";
5366
+ if (t === "null" || t === "undefined") return "null";
5367
+ if (t.startsWith("() =>") || t === "VoidFunction") return "() => {}";
5368
+ if (t.startsWith("(") && t.includes("=>")) return "() => {}";
5369
+ // Inline object type → null (complex; user fills it in)
5370
+ if (t.startsWith("{")) return "null";
5371
+ // Union ending in null/undefined → null
5372
+ if (t.endsWith("| null") || t.endsWith("| undefined") || t.startsWith("null |")) return "null";
5373
+ // String literal union → pick first quoted value
5374
+ const firstLiteral = t.match(/["']([^"']+)["']/);
5375
+ if (firstLiteral && (t.startsWith('"') || t.startsWith("'"))) return `"${firstLiteral[1]}"`;
5376
+
5377
+ // Named PascalCase type — try multiple resolution strategies
5378
+ if (/^[A-Z][A-Za-z0-9]+$/.test(t)) {
5379
+ const typeLower = t.toLowerCase().replace(/def$|id$|key$|type$|interface$|kind$/i, "");
5380
+
5381
+ // Strategy 1: Direct top-level array match (RouteDef → ROUTES[0])
5382
+ if (typeLower.length >= 3) {
5383
+ for (const [expName, expKind] of Object.entries(localExports)) {
5384
+ if (expKind !== "array") continue;
5385
+ const expBase = expName.toLowerCase().replace(/_/g, "").replace(/s$/, "").replace(/data$/, "");
5386
+ if (expBase.slice(0, 4) === typeLower.slice(0, 4) || typeLower.slice(0, 4) === expBase.slice(0, 4)) {
5387
+ return `${expName}[0]`;
5388
+ }
5389
+ }
5390
+ }
5391
+
5392
+ // Strategy 2: Local interface/type alias → generate inline object
5393
+ // e.g. FilterState → { level1: null, level2: null }
5394
+ if (sourceContent) {
5395
+ const esc = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5396
+ const bodyRe = new RegExp(`(?:interface|type)\\s+${esc}\\s*(?:=\\s*)?\\{([^}]+)\\}`);
5397
+ const m = sourceContent.match(bodyRe);
5398
+ if (m) {
5399
+ const fields = m[1].split(";").map(s => s.trim()).filter(s => s.length > 0 && !s.startsWith("//"));
5400
+ const fieldPairs = fields.map(f => {
5401
+ const ci = f.indexOf(":");
5402
+ if (ci < 0) return null;
5403
+ const fname = f.slice(0, ci).trim().replace(/\?$/, "");
5404
+ const ftype = f.slice(ci + 1).trim();
5405
+ // Resolve field type (no recursion with sourceContent to prevent deep loops)
5406
+ const fmock = mockValueForType(ftype, localExports, null);
5407
+ return ` ${fname}: ${fmock}`;
5408
+ }).filter(Boolean);
5409
+ if (fieldPairs.length > 0) return `{\n${fieldPairs.join(",\n")}\n}`;
5410
+ }
5411
+ }
5412
+
5413
+ // Strategy 3: Nested array property lookup
5414
+ // SubStation → DATA[0]?.subStations?.[0] (camelCase plural of type name)
5415
+ const camel = t[0].toLowerCase() + t.slice(1);
5416
+ const plural = camel + "s";
5417
+ for (const [expName, expKind] of Object.entries(localExports)) {
5418
+ if (expKind !== "array") continue;
5419
+ return `${expName}[0]?.${plural}?.[0]`;
5420
+ }
5421
+ }
5422
+
5423
+ return "undefined as any";
5424
+ }
5425
+
5426
+ /**
5427
+ * Build the story file content for a single internal component.
5428
+ * Automatically uses render() format when props require complex/nested mock data.
5429
+ */
5430
+ function buildInternalComponentStoryContent(compName, parentImportPath, dataImportPath, dataSymbols, localExports, props, parentComp, sourceContent) {
5431
+ const lines = [];
5432
+ lines.push(`// @vds-regenerate — VDS auto-generated. Remove this line to prevent overwrite.`);
5433
+ lines.push(`import React from "react";`);
5434
+ lines.push(`import type { Meta, StoryObj } from "@storybook/react";`);
5435
+ lines.push(`import { ${compName} } from "${parentImportPath}";`);
5436
+ if (dataImportPath && dataSymbols.length > 0) {
5437
+ lines.push(`import { ${dataSymbols.join(", ")} } from "${dataImportPath}";`);
5438
+ }
5439
+ lines.push(``);
5440
+ lines.push(`/**`);
5441
+ lines.push(` * **${compName}** — sub-component of ${parentComp.name}.`);
5442
+ lines.push(` * Auto-exported by VDS for isolated Storybook documentation.`);
5443
+ lines.push(` */`);
5444
+ lines.push(`const meta: Meta<typeof ${compName}> = {`);
5445
+ lines.push(` title: "${parentComp.group || "Screens"}/${parentComp.name}/${compName}",`);
5446
+ lines.push(` component: ${compName},`);
5447
+ lines.push(` parameters: {`);
5448
+ lines.push(` layout: "centered",`);
5449
+ lines.push(` backgrounds: { default: "dark" },`);
5450
+ lines.push(` },`);
5451
+ lines.push(`};`);
5452
+ lines.push(`export default meta;`);
5453
+ lines.push(`type Story = StoryObj<typeof meta>;`);
5454
+ lines.push(``);
5455
+
5456
+ if (props.length === 0) {
5457
+ lines.push(`export const Default: Story = {};`);
5458
+ return lines.join("\n") + "\n";
5459
+ }
5460
+
5461
+ const mockedProps = props.map(p => ({
5462
+ name: p.name,
5463
+ mock: mockValueForType(p.type, localExports, sourceContent),
5464
+ }));
5465
+
5466
+ // Use render() format when any mock is complex: nested path (?.),
5467
+ // multiline inline object (\n), or unresolved (undefined as any)
5468
+ const needsRender = mockedProps.some(p =>
5469
+ p.mock === "undefined as any" ||
5470
+ p.mock.includes("?.") ||
5471
+ p.mock.includes("\n")
5472
+ );
5473
+
5474
+ if (!needsRender) {
5475
+ // Simple args format — all values are primitives or direct data refs
5476
+ const argLines = mockedProps.map(p => ` ${p.name}: ${p.mock},`).join("\n");
5477
+ lines.push(`export const Default: Story = {`);
5478
+ lines.push(` args: {`);
5479
+ lines.push(argLines);
5480
+ lines.push(` },`);
5481
+ lines.push(`};`);
5482
+ } else {
5483
+ // render() format — declare complex vars, guard against undefined, then render
5484
+ const varDecls = [];
5485
+ const propAttrs = [];
5486
+ const guardVarNames = [];
5487
+
5488
+ for (const { name, mock } of mockedProps) {
5489
+ if (mock.includes("?.") || mock === "undefined as any") {
5490
+ // Nested/unknown: declare variable + add to guard list
5491
+ const varName = `mock${name[0].toUpperCase() + name.slice(1)}`;
5492
+ varDecls.push(` const ${varName} = ${mock};`);
5493
+ guardVarNames.push(varName);
5494
+ propAttrs.push(` ${name}={${varName} as any}`);
5495
+ } else if (mock.includes("\n")) {
5496
+ // Multiline inline object (e.g. interface mock)
5497
+ const varName = `mock${name[0].toUpperCase() + name.slice(1)}`;
5498
+ const indented = mock.split("\n").map((l, i) => i === 0 ? l : " " + l).join("\n");
5499
+ varDecls.push(` const ${varName} = ${indented} as any;`);
5500
+ propAttrs.push(` ${name}={${varName}}`);
5501
+ } else {
5502
+ propAttrs.push(` ${name}={${mock}}`);
5503
+ }
5504
+ }
5505
+
5506
+ lines.push(`export const Default: Story = {`);
5507
+ lines.push(` render: () => {`);
5508
+ for (const v of varDecls) lines.push(v);
5509
+ if (guardVarNames.length > 0) {
5510
+ const check = guardVarNames.map(v => `!${v}`).join(" || ");
5511
+ lines.push(` if (${check}) return <div style={{ color: "#888", padding: "1rem" }}>⚠️ Mock data unavailable for <strong>${compName}</strong></div>;`);
5512
+ }
5513
+ lines.push(` return (`);
5514
+ lines.push(` <${compName}`);
5515
+ for (const attr of propAttrs) lines.push(attr);
5516
+ lines.push(` />`);
5517
+ lines.push(` );`);
5518
+ lines.push(` },`);
5519
+ lines.push(`};`);
5520
+ }
5521
+
5522
+ return lines.join("\n") + "\n";
5523
+ }
5524
+
5525
+ /**
5526
+ * Main orchestrator: auto-exports internal components + generates individual story files.
5527
+ * Called for each comp with isPageComponent: true.
5528
+ */
5529
+ function generateInternalComponentStories(comp) {
5530
+ const names = comp.internalComponentNames || [];
5531
+ if (names.length === 0) return;
5532
+
5533
+ // Locate the source file (COMPONENTS_REL_DIR is a module-level variable set in main())
5534
+ const sourceFile = path.join(PROJECT_ROOT, COMPONENTS_REL_DIR, comp.file);
5535
+ if (!fs.existsSync(sourceFile)) return;
5536
+
5537
+ const sourceContent = fs.readFileSync(sourceFile, "utf-8");
5538
+
5539
+ // Step 1: Auto-export internal components
5540
+ const newly = autoExportInternalComponents(sourceFile, names);
5541
+ if (newly.length > 0) {
5542
+ console.log(`[VDS] ${comp.name} → auto-exported ${newly.length} sub-components: ${newly.join(", ")}`);
5543
+ }
5544
+
5545
+ // Step 2: Find best local data import (most array-typed exports)
5546
+ const localImports = parseLocalDataImports(sourceContent);
5547
+ let bestImportPath = null;
5548
+ let bestSymbols = [];
5549
+ let bestExports = {};
5550
+ for (const li of localImports) {
5551
+ const resolved = path.resolve(path.dirname(sourceFile), li.importPath);
5552
+ const exts = ["", ".ts", ".tsx", ".js", ".jsx"];
5553
+ let found = null;
5554
+ for (const ext of exts) {
5555
+ const p = resolved + ext;
5556
+ if (fs.existsSync(p)) { found = p; break; }
5557
+ }
5558
+ if (!found) continue;
5559
+ const exports = parseDataFileExports(found);
5560
+ const arrayCount = Object.values(exports).filter(k => k === "array").length;
5561
+ if (arrayCount > Object.values(bestExports).filter(k => k === "array").length) {
5562
+ bestImportPath = li.importPath;
5563
+ bestSymbols = li.symbols;
5564
+ bestExports = exports;
5565
+ }
5566
+ }
5567
+
5568
+ // Step 3: Compute import paths relative to STORIES_DIR
5569
+ const storiesRelToSource = path.relative(path.dirname(sourceFile), path.join(PROJECT_ROOT, "src", "stories"));
5570
+ const sourceRelFromStories = path.relative(
5571
+ path.join(PROJECT_ROOT, "src", "stories"),
5572
+ sourceFile.replace(/\.(tsx?|jsx?)$/, "")
5573
+ ).replace(/\\/g, "/");
5574
+ const parentImportPath = sourceRelFromStories.startsWith(".") ? sourceRelFromStories : "./" + sourceRelFromStories;
5575
+
5576
+ let dataImportFromStories = null;
5577
+ if (bestImportPath) {
5578
+ const dataAbsolute = path.resolve(path.dirname(sourceFile), bestImportPath);
5579
+ dataImportFromStories = path.relative(
5580
+ path.join(PROJECT_ROOT, "src", "stories"),
5581
+ dataAbsolute
5582
+ ).replace(/\\/g, "/").replace(/\.(tsx?|jsx?)$/, "");
5583
+ if (!dataImportFromStories.startsWith(".")) dataImportFromStories = "./" + dataImportFromStories;
5584
+ }
5585
+
5586
+ // Step 4: Generate individual story files
5587
+ for (const name of names) {
5588
+ const storyFile = path.join(STORIES_DIR, `${name}.stories.tsx`);
5589
+ if (fs.existsSync(storyFile)) {
5590
+ // Preserve user-modified stories; overwrite only VDS-generated ones
5591
+ const existing = fs.readFileSync(storyFile, "utf-8");
5592
+ if (!existing.startsWith("// @vds-regenerate")) continue;
5593
+ }
5594
+ const props = extractComponentProps(sourceContent, name);
5595
+ const content = buildInternalComponentStoryContent(
5596
+ name, parentImportPath, dataImportFromStories, bestSymbols, bestExports, props, comp, sourceContent
5597
+ );
5598
+ fs.writeFileSync(storyFile, content, "utf-8");
5599
+ console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyFile)} (sub-component of ${comp.name})`);
5600
+ }
5601
+ }
5602
+
5243
5603
  main();
5244
5604