vibe-design-system 2.9.1 → 2.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -5284,15 +5284,26 @@ function parseLocalDataImports(content) {
5284
5284
  }
5285
5285
 
5286
5286
  /**
5287
- * Detect named exports from a data file: returns { exportName → 'array' | 'object' | 'scalar' }
5287
+ * Detect named exports from a data file.
5288
+ * Returns { exportName → 'array' | 'object' | 'scalar' }
5289
+ * Also captures element type annotations: { 'EXPORTS_ELEMENT_TYPE' → 'TypeName' }
5290
+ * e.g. `export const METHODOLOGY_DATA: Stage[] = [...]` → { METHODOLOGY_DATA: 'array', METHODOLOGY_DATA_ELEMENT_TYPE: 'Stage' }
5288
5291
  */
5289
5292
  function parseDataFileExports(filePath) {
5290
5293
  if (!fs.existsSync(filePath)) return {};
5291
5294
  const src = fs.readFileSync(filePath, "utf-8");
5292
5295
  const result = {};
5293
- const re = /^export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]/gm;
5296
+ // Typed array exports: export const NAME: ElemType[] = [
5297
+ const typedRe = /^export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*([A-Za-z_$][A-Za-z0-9_$]*)\[\]/gm;
5294
5298
  let m;
5299
+ while ((m = typedRe.exec(src)) !== null) {
5300
+ result[m[1]] = "array";
5301
+ result[`${m[1]}_ELEMENT_TYPE`] = m[2]; // e.g. METHODOLOGY_DATA_ELEMENT_TYPE = "Stage"
5302
+ }
5303
+ // Untyped / other exports (fallback detection)
5304
+ const re = /^export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]/gm;
5295
5305
  while ((m = re.exec(src)) !== null) {
5306
+ if (result[m[1]]) continue; // already captured above
5296
5307
  const name = m[1];
5297
5308
  const afterIdx = src.indexOf("=", m.index) + 1;
5298
5309
  const afterSlice = src.slice(afterIdx, afterIdx + 10).trimStart();
@@ -5356,8 +5367,9 @@ function extractComponentProps(content, componentName) {
5356
5367
 
5357
5368
  /**
5358
5369
  * Generate a mock value string for a TypeScript type, using available data exports.
5370
+ * Pass sourceContent to enable local interface/type parsing and nested property lookup.
5359
5371
  */
5360
- function mockValueForType(type, localExports) {
5372
+ function mockValueForType(type, localExports, sourceContent) {
5361
5373
  const t = type.trim();
5362
5374
  if (t === "boolean") return "false";
5363
5375
  if (t === "string") return '""';
@@ -5373,26 +5385,80 @@ function mockValueForType(type, localExports) {
5373
5385
  const firstLiteral = t.match(/["']([^"']+)["']/);
5374
5386
  if (firstLiteral && (t.startsWith('"') || t.startsWith("'"))) return `"${firstLiteral[1]}"`;
5375
5387
 
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]`;
5388
+ // Named PascalCase type try multiple resolution strategies
5389
+ if (/^[A-Z][A-Za-z0-9]+$/.test(t)) {
5390
+ const typeLower = t.toLowerCase().replace(/def$|id$|key$|type$|interface$|kind$/i, "");
5391
+
5392
+ // Strategy 1: Direct top-level array match (RouteDef → ROUTES[0], Stage METHODOLOGY_DATA[0])
5393
+ if (typeLower.length >= 3) {
5394
+ // Sort by export name length descending — more-specific exports (METHODOLOGY_DATA) tried before simple ones (ROUTES)
5395
+ const sortedExports = Object.entries(localExports).sort((a, b) => b[0].length - a[0].length);
5396
+ // 1a: Exact element type annotation match (Stage METHODOLOGY_DATA_ELEMENT_TYPE=Stage → METHODOLOGY_DATA[0])
5397
+ for (const [expName, expKind] of sortedExports) {
5398
+ if (expKind !== "array") continue;
5399
+ if (localExports[`${expName}_ELEMENT_TYPE`] === t) return `${expName}[0]`;
5400
+ }
5401
+ // 1b: Name prefix match (RouteDef → ROUTES[0])
5402
+ for (const [expName, expKind] of sortedExports) {
5403
+ if (expKind !== "array") continue;
5404
+ const expBase = expName.toLowerCase().replace(/_/g, "").replace(/s$/, "").replace(/data$/, "");
5405
+ if (expBase.slice(0, 4) === typeLower.slice(0, 4) || typeLower.slice(0, 4) === expBase.slice(0, 4)) {
5406
+ return `${expName}[0]`;
5407
+ }
5408
+ }
5409
+ }
5410
+
5411
+ // Strategy 2: Local type alias resolution — e.g. Level1Filter = 'A' | 'B' | null → null
5412
+ if (sourceContent) {
5413
+ const esc = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5414
+ // 2a: Non-object type alias: type Foo = ...
5415
+ const aliasRe = new RegExp(`type\\s+${esc}\\s*=\\s*([^{;\\n]+)`);
5416
+ const aliasMatch = sourceContent.match(aliasRe);
5417
+ if (aliasMatch) {
5418
+ const aliasValue = aliasMatch[1].trim().replace(/;$/, "");
5419
+ // Recursively resolve the alias value (pass sourceContent for chained aliases)
5420
+ return mockValueForType(aliasValue, localExports, sourceContent);
5421
+ }
5422
+ // 2b: Interface or object type alias → generate inline default object
5423
+ // e.g. FilterState → { level1: null, level2: null }
5424
+ const bodyRe = new RegExp(`(?:interface|type)\\s+${esc}\\s*(?:=\\s*)?\\{([^}]+)\\}`);
5425
+ const m = sourceContent.match(bodyRe);
5426
+ if (m) {
5427
+ const fields = m[1].split(";").map(s => s.trim()).filter(s => s.length > 0 && !s.startsWith("//"));
5428
+ const fieldPairs = fields.map(f => {
5429
+ const ci = f.indexOf(":");
5430
+ if (ci < 0) return null;
5431
+ const fname = f.slice(0, ci).trim().replace(/\?$/, "");
5432
+ const ftype = f.slice(ci + 1).trim();
5433
+ // Resolve field type with sourceContent for alias chaining
5434
+ const fmock = mockValueForType(ftype, localExports, sourceContent);
5435
+ return ` ${fname}: ${fmock}`;
5436
+ }).filter(Boolean);
5437
+ if (fieldPairs.length > 0) return `{\n${fieldPairs.join(",\n")}\n}`;
5386
5438
  }
5387
5439
  }
5440
+
5441
+ // Strategy 3: Nested array property lookup
5442
+ // SubStation → METHODOLOGY_DATA[0]?.subStations?.[0] (prefer longer export names)
5443
+ const camel = t[0].toLowerCase() + t.slice(1);
5444
+ const plural = camel + "s";
5445
+ const sortedForNested = Object.entries(localExports)
5446
+ .filter(([, k]) => k === "array")
5447
+ .sort((a, b) => b[0].length - a[0].length); // longest name first
5448
+ if (sortedForNested.length > 0) {
5449
+ const [expName] = sortedForNested[0];
5450
+ return `${expName}[0]?.${plural}?.[0]`;
5451
+ }
5388
5452
  }
5453
+
5389
5454
  return "undefined as any";
5390
5455
  }
5391
5456
 
5392
5457
  /**
5393
5458
  * Build the story file content for a single internal component.
5459
+ * Automatically uses render() format when props require complex/nested mock data.
5394
5460
  */
5395
- function buildInternalComponentStoryContent(compName, parentImportPath, dataImportPath, dataSymbols, localExports, props, parentComp) {
5461
+ function buildInternalComponentStoryContent(compName, parentImportPath, dataImportPath, dataSymbols, localExports, props, parentComp, sourceContent) {
5396
5462
  const lines = [];
5397
5463
  lines.push(`// @vds-regenerate — VDS auto-generated. Remove this line to prevent overwrite.`);
5398
5464
  lines.push(`import React from "react";`);
@@ -5418,16 +5484,72 @@ function buildInternalComponentStoryContent(compName, parentImportPath, dataImpo
5418
5484
  lines.push(`type Story = StoryObj<typeof meta>;`);
5419
5485
  lines.push(``);
5420
5486
 
5421
- if (props.length > 0) {
5422
- const argLines = props.map(p => ` ${p.name}: ${mockValueForType(p.type, localExports)},`).join("\n");
5487
+ if (props.length === 0) {
5488
+ lines.push(`export const Default: Story = {};`);
5489
+ return lines.join("\n") + "\n";
5490
+ }
5491
+
5492
+ const mockedProps = props.map(p => ({
5493
+ name: p.name,
5494
+ mock: mockValueForType(p.type, localExports, sourceContent),
5495
+ }));
5496
+
5497
+ // Use render() format when any mock is complex: nested path (?.),
5498
+ // multiline inline object (\n), or unresolved (undefined as any)
5499
+ const needsRender = mockedProps.some(p =>
5500
+ p.mock === "undefined as any" ||
5501
+ p.mock.includes("?.") ||
5502
+ p.mock.includes("\n")
5503
+ );
5504
+
5505
+ if (!needsRender) {
5506
+ // Simple args format — all values are primitives or direct data refs
5507
+ const argLines = mockedProps.map(p => ` ${p.name}: ${p.mock},`).join("\n");
5423
5508
  lines.push(`export const Default: Story = {`);
5424
5509
  lines.push(` args: {`);
5425
5510
  lines.push(argLines);
5426
5511
  lines.push(` },`);
5427
5512
  lines.push(`};`);
5428
5513
  } else {
5429
- lines.push(`export const Default: Story = {};`);
5514
+ // render() format declare complex vars, guard against undefined, then render
5515
+ const varDecls = [];
5516
+ const propAttrs = [];
5517
+ const guardVarNames = [];
5518
+
5519
+ for (const { name, mock } of mockedProps) {
5520
+ if (mock.includes("?.") || mock === "undefined as any") {
5521
+ // Nested/unknown: declare variable + add to guard list
5522
+ const varName = `mock${name[0].toUpperCase() + name.slice(1)}`;
5523
+ varDecls.push(` const ${varName} = ${mock};`);
5524
+ guardVarNames.push(varName);
5525
+ propAttrs.push(` ${name}={${varName} as any}`);
5526
+ } else if (mock.includes("\n")) {
5527
+ // Multiline inline object (e.g. interface mock)
5528
+ const varName = `mock${name[0].toUpperCase() + name.slice(1)}`;
5529
+ const indented = mock.split("\n").map((l, i) => i === 0 ? l : " " + l).join("\n");
5530
+ varDecls.push(` const ${varName} = ${indented} as any;`);
5531
+ propAttrs.push(` ${name}={${varName}}`);
5532
+ } else {
5533
+ propAttrs.push(` ${name}={${mock}}`);
5534
+ }
5535
+ }
5536
+
5537
+ lines.push(`export const Default: Story = {`);
5538
+ lines.push(` render: () => {`);
5539
+ for (const v of varDecls) lines.push(v);
5540
+ if (guardVarNames.length > 0) {
5541
+ const check = guardVarNames.map(v => `!${v}`).join(" || ");
5542
+ lines.push(` if (${check}) return <div style={{ color: "#888", padding: "1rem" }}>⚠️ Mock data unavailable for <strong>${compName}</strong></div>;`);
5543
+ }
5544
+ lines.push(` return (`);
5545
+ lines.push(` <${compName}`);
5546
+ for (const attr of propAttrs) lines.push(attr);
5547
+ lines.push(` />`);
5548
+ lines.push(` );`);
5549
+ lines.push(` },`);
5550
+ lines.push(`};`);
5430
5551
  }
5552
+
5431
5553
  return lines.join("\n") + "\n";
5432
5554
  }
5433
5555
 
@@ -5495,10 +5617,14 @@ function generateInternalComponentStories(comp) {
5495
5617
  // Step 4: Generate individual story files
5496
5618
  for (const name of names) {
5497
5619
  const storyFile = path.join(STORIES_DIR, `${name}.stories.tsx`);
5498
- if (fs.existsSync(storyFile)) continue; // Never overwrite existing stories
5620
+ if (fs.existsSync(storyFile)) {
5621
+ // Preserve user-modified stories; overwrite only VDS-generated ones
5622
+ const existing = fs.readFileSync(storyFile, "utf-8");
5623
+ if (!existing.startsWith("// @vds-regenerate")) continue;
5624
+ }
5499
5625
  const props = extractComponentProps(sourceContent, name);
5500
5626
  const content = buildInternalComponentStoryContent(
5501
- name, parentImportPath, dataImportFromStories, bestSymbols, bestExports, props, comp
5627
+ name, parentImportPath, dataImportFromStories, bestSymbols, bestExports, props, comp, sourceContent
5502
5628
  );
5503
5629
  fs.writeFileSync(storyFile, content, "utf-8");
5504
5630
  console.log(`[VDS] Wrote ${path.relative(PROJECT_ROOT, storyFile)} (sub-component of ${comp.name})`);