vibe-design-system 2.8.39 → 2.8.41

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
@@ -95,12 +95,14 @@ Storybook stilleri sayfalardan farklıysa: .storybook/preview.ts içinde uygulam
95
95
  `;
96
96
 
97
97
  const WATCH_MJS = `#!/usr/bin/env node
98
- import { watch } from 'fs';
98
+ import { watch, existsSync } from 'fs';
99
99
  import { execSync } from 'child_process';
100
100
  import { resolve } from 'path';
101
101
 
102
102
  const ROOT = resolve(process.cwd());
103
- const SRC = resolve(ROOT, 'src');
103
+ // Auto-detect frontend src dir (supports fullstack projects)
104
+ const SRC_CANDIDATES = ['src', 'client/src', 'frontend/src', 'web/src'];
105
+ const SRC = resolve(ROOT, SRC_CANDIDATES.find((p) => existsSync(resolve(ROOT, p, 'components'))) || SRC_CANDIDATES.find((p) => existsSync(resolve(ROOT, p))) || 'src');
104
106
  let timeout = null;
105
107
 
106
108
  function run() {
@@ -145,6 +147,25 @@ function getProjectRoot() {
145
147
  return process.cwd();
146
148
  }
147
149
 
150
+ /**
151
+ * Fullstack proje desteği: client/src, frontend/src, web/src varsa onu döndür.
152
+ * Yoksa standart "src" döndür.
153
+ */
154
+ function detectFrontendSrcDir(projectRoot) {
155
+ // Check fullstack prefixes FIRST — they take priority over bare src/
156
+ // e.g. crypto project has empty src/ (only stories/) but real code in client/src/
157
+ for (const prefix of ["client/src", "frontend/src", "web/src"]) {
158
+ const full = path.join(projectRoot, prefix);
159
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
160
+ const hasComponents = fs.existsSync(path.join(full, "components"));
161
+ const hasAppFiles = fs.readdirSync(full).some((f) => /\.(tsx|jsx)$/i.test(f));
162
+ if (hasComponents || hasAppFiles) return prefix;
163
+ }
164
+ }
165
+ // Fallback: standard src/
166
+ return "src";
167
+ }
168
+
148
169
  function readPackageJson(projectRoot) {
149
170
  const pkgPath = path.join(projectRoot, "package.json");
150
171
  if (!fs.existsSync(pkgPath)) return null;
@@ -223,51 +244,20 @@ function detectFramework(pkg) {
223
244
  return "vite";
224
245
  }
225
246
 
226
- /** Vite 7+ kullanılıyorsa Storybook 8 peer dep çakışır.
227
- * .npmrc'ye legacy-peer-deps=true ekleyerek tüm npm komutlarını bağışık hale getirir. */
228
- function ensureLegacyPeerDepsIfNeeded(projectRoot, pkg) {
229
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
230
- const viteVersion = allDeps["vite"] || "";
231
- // "^7.x", "~7.x", "7.x" gibi tüm Vite 7+ versiyonlarını yakala
232
- const isVite7Plus = /^\^?~?[7-9]\d*\./.test(viteVersion) || /^\^?~?[1-9]\d+\./.test(viteVersion);
233
- if (!isVite7Plus) return;
234
- const npmrcPath = path.join(projectRoot, ".npmrc");
235
- let content = fs.existsSync(npmrcPath) ? fs.readFileSync(npmrcPath, "utf-8") : "";
236
- if (content.includes("legacy-peer-deps")) return; // idempotent
237
- content = content ? content.trimEnd() + "\nlegacy-peer-deps=true\n" : "legacy-peer-deps=true\n";
238
- fs.writeFileSync(npmrcPath, content, "utf-8");
239
- console.log("📝 .npmrc: legacy-peer-deps=true eklendi (Vite 7+ / Storybook 8 uyumluluk).");
240
- }
241
-
242
247
  /** Framework'e göre Storybook framework paket adı */
243
248
  function storybookFrameworkPackage(framework) {
244
249
  if (framework === "nextjs") return "@storybook/nextjs";
245
250
  return "@storybook/react-vite"; // vite & remix
246
251
  }
247
252
 
248
- /** Projenin gerçek frontend src dizinini tespit eder.
249
- * Fullstack projelerde (client/, frontend/, web/) doğru alias için gerekli. */
250
- function detectFrontendSrcDir(projectRoot) {
251
- // Fullstack / monorepo: frontend ayrı bir klasörde olabilir
252
- const candidates = [
253
- "client/src",
254
- "frontend/src",
255
- "web/src",
256
- ];
257
- for (const c of candidates) {
258
- if (fs.existsSync(path.join(projectRoot, c))) return c;
259
- }
260
- return "src"; // varsayılan
261
- }
262
-
263
253
  /** Framework'e göre .storybook/main.ts içeriği */
264
- function buildStorybookMainTs(framework, projectRoot) {
254
+ function buildStorybookMainTs(framework, srcPrefix) {
265
255
  if (framework === "nextjs") {
266
256
  return `import type { StorybookConfig } from "@storybook/nextjs";
267
257
 
268
258
  const config: StorybookConfig = {
269
259
  stories: [
270
- "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
260
+ "../${srcPrefix}/**/*.stories.@(js|jsx|mjs|ts|tsx)",
271
261
  "../app/**/*.stories.@(js|jsx|mjs|ts|tsx)",
272
262
  ],
273
263
  addons: ["@storybook/addon-essentials", "@storybook/addon-a11y"],
@@ -281,13 +271,12 @@ export default config;
281
271
  `;
282
272
  }
283
273
  // vite (default) & remix
284
- const srcDir = detectFrontendSrcDir(projectRoot || process.cwd());
285
274
  return `import type { StorybookConfig } from "@storybook/react-vite";
286
275
  import { mergeConfig } from "vite";
287
276
  import path from "path";
288
277
 
289
278
  const config: StorybookConfig = {
290
- stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
279
+ stories: ["../${srcPrefix}/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
291
280
  addons: ["@storybook/addon-essentials", "@storybook/addon-a11y"],
292
281
  framework: {
293
282
  name: "@storybook/react-vite",
@@ -297,7 +286,7 @@ const config: StorybookConfig = {
297
286
  return mergeConfig(config, {
298
287
  resolve: {
299
288
  alias: {
300
- "@": path.resolve(process.cwd(), "${srcDir}"),
289
+ "@": path.resolve(process.cwd(), "${srcPrefix}"),
301
290
  },
302
291
  },
303
292
  });
@@ -310,11 +299,13 @@ export default config;
310
299
 
311
300
  /** Framework'e göre CSS import path'i */
312
301
  function buildStorybookPreviewTs(framework, projectRoot) {
313
- // CSS dosyasını bul
302
+ // CSS dosyasını bul — fullstack projeler (client/src, frontend/src, web/src) dahil
303
+ const frontendPrefixes = ["src", "client/src", "frontend/src", "web/src"];
314
304
  const cssCandidates = [
315
- ["src/index.css", "../src/index.css"],
316
- ["src/globals.css", "../src/globals.css"],
317
- ["src/styles/globals.css", "../src/styles/globals.css"],
305
+ ...frontendPrefixes.map((p) => [p + "/index.css", "../" + p + "/index.css"]),
306
+ ...frontendPrefixes.map((p) => [p + "/globals.css", "../" + p + "/globals.css"]),
307
+ ...frontendPrefixes.map((p) => [p + "/styles/globals.css", "../" + p + "/styles/globals.css"]),
308
+ ...frontendPrefixes.map((p) => [p + "/App.css", "../" + p + "/App.css"]),
318
309
  ["app/globals.css", "../app/globals.css"],
319
310
  ];
320
311
  let cssImport = '// CSS bulunamadı — projenizin global CSS dosyasını buraya ekleyin';
@@ -355,10 +346,10 @@ export default preview;
355
346
  // ADIM 1 — Bağımlılık kontrolü
356
347
  function needsStorybook(projectRoot, framework) {
357
348
  const pkg = readPackageJson(projectRoot);
358
- if (!pkg) return true; // package.json yok → yükle (main guard zaten önce hata verecek)
359
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
349
+ if (!pkg) return false;
350
+ const dev = pkg.devDependencies || {};
360
351
  const fwPkg = storybookFrameworkPackage(framework);
361
- return !(allDeps.storybook && allDeps[fwPkg]);
352
+ return !(dev.storybook && dev[fwPkg]);
362
353
  }
363
354
 
364
355
  // Storybook 8.6.x — aynı minor sürümde tutarak addon uyumluluk uyarısını önler
@@ -367,41 +358,62 @@ const STORYBOOK_VERSION = "8.6.17";
367
358
  function installStorybook(projectRoot, framework) {
368
359
  const fwPkg = storybookFrameworkPackage(framework);
369
360
  console.log(`📚 Storybook v8 (${fwPkg}) kuruluyor...`);
370
- const r = spawnSync(
371
- "npm",
372
- [
373
- "install",
374
- "--save-dev",
375
- "--save-exact",
376
- "--legacy-peer-deps",
377
- `storybook@${STORYBOOK_VERSION}`,
378
- `${fwPkg}@${STORYBOOK_VERSION}`,
379
- `@storybook/react@${STORYBOOK_VERSION}`,
380
- `@storybook/addon-essentials@${STORYBOOK_VERSION}`,
381
- `@storybook/addon-a11y@${STORYBOOK_VERSION}`,
382
- `@storybook/blocks@${STORYBOOK_VERSION}`,
383
- ],
384
- { cwd: projectRoot, stdio: "inherit", shell: true }
385
- );
361
+ // No extra deps — @tailwindcss/vite removed (causes ERESOLVE in non-Tailwind projects)
362
+
363
+ // Vite 7+ peer dependency conflict: Storybook 8 requires vite ^4/5/6 but project has vite 7+
364
+ // Detect Vite version and add --legacy-peer-deps if needed
365
+ const pkg = readPackageJson(projectRoot);
366
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
367
+ const viteVer = deps?.vite || "";
368
+ const viteIsMajor7Plus = /^[\^~]?[7-9]/.test(viteVer) || /^\d+/.test(viteVer) && parseInt(viteVer) >= 7;
369
+ const needsLegacyPeers = viteIsMajor7Plus;
370
+
371
+ if (needsLegacyPeers) {
372
+ console.log("⚠️ Vite 7+ algılandı — --legacy-peer-deps ile kurulum yapılıyor...");
373
+ // Ensure .npmrc has legacy-peer-deps for future installs too
374
+ const npmrcPath = path.join(projectRoot, ".npmrc");
375
+ const npmrcContent = fs.existsSync(npmrcPath) ? fs.readFileSync(npmrcPath, "utf-8") : "";
376
+ if (!npmrcContent.includes("legacy-peer-deps")) {
377
+ fs.appendFileSync(npmrcPath, "\nlegacy-peer-deps=true\n", "utf-8");
378
+ }
379
+ }
380
+
381
+ const installArgs = [
382
+ "install",
383
+ "--save-dev",
384
+ "--save-exact",
385
+ ...(needsLegacyPeers ? ["--legacy-peer-deps"] : []),
386
+ `storybook@${STORYBOOK_VERSION}`,
387
+ `${fwPkg}@${STORYBOOK_VERSION}`,
388
+ `@storybook/react@${STORYBOOK_VERSION}`,
389
+ `@storybook/addon-essentials@${STORYBOOK_VERSION}`,
390
+ `@storybook/addon-a11y@${STORYBOOK_VERSION}`,
391
+ `@storybook/blocks@${STORYBOOK_VERSION}`,
392
+ ];
393
+
394
+ const r = spawnSync("npm", installArgs, {
395
+ cwd: projectRoot,
396
+ stdio: "inherit",
397
+ shell: true,
398
+ });
386
399
  if (r.status !== 0) {
387
- console.error("❌ npm install storybook başarısız oldu. VDS kurulumu durduruluyor.");
400
+ console.error("❌ npm install storybook başarısız oldu.");
388
401
  process.exit(1);
389
402
  }
390
403
  }
391
404
 
392
405
  // ADIM 2 — .storybook/
393
- function ensureStorybook(projectRoot, framework) {
406
+ function ensureStorybook(projectRoot, framework, srcPrefix) {
394
407
  const storybookDir = path.join(projectRoot, ".storybook");
395
408
  if (!fs.existsSync(storybookDir)) {
396
409
  fs.mkdirSync(storybookDir, { recursive: true });
397
410
  console.log("📁 .storybook/ oluşturuldu.");
398
411
  }
399
412
 
400
- const mainTs = buildStorybookMainTs(framework, projectRoot);
413
+ const mainTs = buildStorybookMainTs(framework, srcPrefix);
401
414
  const mainPath = path.join(storybookDir, "main.ts");
402
415
  fs.writeFileSync(mainPath, mainTs, "utf-8");
403
- const detectedSrc = detectFrontendSrcDir(projectRoot);
404
- console.log(`📝 .storybook/main.ts yazıldı (framework: ${framework}, @ alias: ${detectedSrc}).`);
416
+ console.log(`📝 .storybook/main.ts yazıldı (framework: ${framework}).`);
405
417
 
406
418
  const previewTs = buildStorybookPreviewTs(framework, projectRoot);
407
419
 
@@ -519,17 +531,15 @@ function ensureCursorrules(projectRoot) {
519
531
  console.log("📄 .cursorrules yazıldı.");
520
532
  }
521
533
 
522
- // ADIM 6 — src/stories/ oluştur (scan artık klasörü kendisi buluyor; zorla yaratmaya gerek yok)
523
- function ensureStoriesDir(projectRoot) {
524
- const srcDir = path.join(projectRoot, "src");
534
+ // ADIM 6 — stories/ oluştur (scan artık klasörü kendisi buluyor; zorla yaratmaya gerek yok)
535
+ function ensureStoriesDir(projectRoot, srcPrefix) {
536
+ const srcDir = path.join(projectRoot, srcPrefix);
525
537
  if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
526
538
  const storiesDir = path.join(srcDir, "stories");
527
539
  if (!fs.existsSync(storiesDir)) {
528
540
  fs.mkdirSync(storiesDir, { recursive: true });
529
- console.log("📁 src/stories/ oluşturuldu.");
541
+ console.log(`📁 ${srcPrefix}/stories/ oluşturuldu.`);
530
542
  }
531
- // scan.mjs artık klasörü kendisi buluyor (src/components → components → app/components → ...)
532
- // Klasör yoksa oluşturma — projeye ait mevcut yapıyı koruyalım.
533
543
  }
534
544
 
535
545
  // ADIM 7 — İlk tarama
@@ -601,15 +611,15 @@ module.exports = {
601
611
  }
602
612
 
603
613
  // ADIM 9 — Storybook örnek dosyalarını sil
604
- function removeStorybookExamples(projectRoot) {
605
- const storiesDir = path.join(projectRoot, "src", "stories");
614
+ function removeStorybookExamples(projectRoot, srcPrefix) {
615
+ const storiesDir = path.join(projectRoot, srcPrefix, "stories");
606
616
  if (!fs.existsSync(storiesDir)) return;
607
617
  for (const name of STORYBOOK_EXAMPLE_FILES) {
608
618
  const filePath = path.join(storiesDir, name);
609
619
  if (fs.existsSync(filePath)) {
610
620
  try {
611
621
  fs.unlinkSync(filePath);
612
- console.log("🗑️ Silindi: src/stories/" + name);
622
+ console.log(`🗑️ Silindi: ${srcPrefix}/stories/${name}`);
613
623
  } catch (_) {}
614
624
  }
615
625
  }
@@ -643,10 +653,10 @@ if (monorepoPackages.length > 0 && pkg.workspaces) {
643
653
  }
644
654
 
645
655
  const framework = detectFramework(pkg);
646
- console.log(`🔍 Framework tespit edildi: ${framework}\n`);
647
-
648
- // Vite 7+ projelerinde Storybook 8 peer dep çakışmasını önle
649
- ensureLegacyPeerDepsIfNeeded(projectRoot, pkg);
656
+ const srcPrefix = detectFrontendSrcDir(projectRoot);
657
+ console.log(`🔍 Framework tespit edildi: ${framework}`);
658
+ if (srcPrefix !== "src") console.log(`📂 Fullstack proje algılandı: kaynak dizin → ${srcPrefix}`);
659
+ console.log("");
650
660
 
651
661
  // ADIM 1
652
662
  if (needsStorybook(projectRoot, framework)) {
@@ -656,7 +666,7 @@ if (needsStorybook(projectRoot, framework)) {
656
666
  }
657
667
 
658
668
  // ADIM 2
659
- ensureStorybook(projectRoot, framework);
669
+ ensureStorybook(projectRoot, framework, srcPrefix);
660
670
 
661
671
  // ADIM 3
662
672
  ensureVdsCore(projectRoot);
@@ -668,10 +678,10 @@ addScripts(projectRoot);
668
678
  ensureCursorrules(projectRoot);
669
679
 
670
680
  // ADIM 6
671
- ensureStoriesDir(projectRoot);
681
+ ensureStoriesDir(projectRoot, srcPrefix);
672
682
 
673
683
  // ADIM 9 (örnek dosyaları taramadan önce silebiliriz; scan src/components ve src/pages tarar, src/stories değil)
674
- removeStorybookExamples(projectRoot);
684
+ removeStorybookExamples(projectRoot, srcPrefix);
675
685
 
676
686
  // ADIM 9b
677
687
  ensureVdsConfig(projectRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-design-system",
3
- "version": "2.8.39",
3
+ "version": "2.8.41",
4
4
  "description": "Auto-generate design systems for vibe coding projects",
5
5
  "homepage": "https://vibedesign.tech",
6
6
  "repository": {
@@ -40,41 +40,59 @@ function resolveDir(envKey, candidates) {
40
40
  return null;
41
41
  }
42
42
 
43
- const SRC_DIR = resolveDir("VDS_SRC_DIR", [
44
- path.join(PROJECT_ROOT, "src"),
45
- path.join(PROJECT_ROOT, "app"),
46
- PROJECT_ROOT,
47
- ]) || path.join(PROJECT_ROOT, "src");
43
+ // Fullstack project detection: prefer client/src > frontend/src > web/src if they contain
44
+ // components/ or .tsx files (indicating the real frontend source dir).
45
+ function detectBestSrcDir() {
46
+ if (process.env.VDS_SRC_DIR) {
47
+ const d = path.resolve(PROJECT_ROOT, process.env.VDS_SRC_DIR);
48
+ if (fs.existsSync(d)) return d;
49
+ }
50
+ // Check fullstack prefixes FIRST — they take priority if they look like a real frontend dir
51
+ for (const prefix of ["client/src", "frontend/src", "web/src"]) {
52
+ const d = path.join(PROJECT_ROOT, prefix);
53
+ if (fs.existsSync(d) && fs.statSync(d).isDirectory()) {
54
+ const hasComponents = fs.existsSync(path.join(d, "components"));
55
+ const hasAppFiles = fs.readdirSync(d).some((f) => /\.(tsx|jsx)$/i.test(f));
56
+ if (hasComponents || hasAppFiles) return d;
57
+ }
58
+ }
59
+ // Fallback: standard src/
60
+ for (const d of [
61
+ path.join(PROJECT_ROOT, "src"),
62
+ path.join(PROJECT_ROOT, "app"),
63
+ PROJECT_ROOT,
64
+ ]) {
65
+ if (fs.existsSync(d)) return d;
66
+ }
67
+ return path.join(PROJECT_ROOT, "src");
68
+ }
69
+ const SRC_DIR = detectBestSrcDir();
48
70
 
49
71
  const COMPONENTS_DIR = resolveDir("VDS_COMPONENTS_DIR", [
72
+ // Fullstack patterns first — if client/src/components exists, prefer it over src/components
73
+ path.join(PROJECT_ROOT, "client", "src", "components"),
74
+ path.join(PROJECT_ROOT, "frontend", "src", "components"),
75
+ path.join(PROJECT_ROOT, "web", "src", "components"),
50
76
  path.join(PROJECT_ROOT, "src", "components"),
51
77
  path.join(PROJECT_ROOT, "components"),
52
78
  path.join(PROJECT_ROOT, "app", "components"),
53
79
  path.join(PROJECT_ROOT, "src", "views"),
54
80
  path.join(PROJECT_ROOT, "src", "modules"),
55
- // Fullstack / monorepo patterns (client/, frontend/, web/)
56
- path.join(PROJECT_ROOT, "client", "src", "components"),
57
- path.join(PROJECT_ROOT, "frontend", "src", "components"),
58
- path.join(PROJECT_ROOT, "web", "src", "components"),
59
- path.join(PROJECT_ROOT, "client", "components"),
60
- path.join(PROJECT_ROOT, "frontend", "components"),
61
81
  ]);
62
82
 
63
83
  const PAGES_DIR = resolveDir("VDS_PAGES_DIR", [
64
- path.join(PROJECT_ROOT, "src", "pages"),
65
- path.join(PROJECT_ROOT, "pages"),
66
- // Fullstack / monorepo patterns
67
84
  path.join(PROJECT_ROOT, "client", "src", "pages"),
68
85
  path.join(PROJECT_ROOT, "frontend", "src", "pages"),
69
86
  path.join(PROJECT_ROOT, "web", "src", "pages"),
87
+ path.join(PROJECT_ROOT, "src", "pages"),
88
+ path.join(PROJECT_ROOT, "pages"),
70
89
  ]);
71
90
 
72
91
  const APP_DIR = resolveDir("VDS_APP_DIR", [
73
- path.join(PROJECT_ROOT, "src", "app"),
74
- path.join(PROJECT_ROOT, "app"),
75
- // Fullstack / monorepo patterns
76
92
  path.join(PROJECT_ROOT, "client", "src", "app"),
77
93
  path.join(PROJECT_ROOT, "frontend", "src", "app"),
94
+ path.join(PROJECT_ROOT, "src", "app"),
95
+ path.join(PROJECT_ROOT, "app"),
78
96
  ]);
79
97
 
80
98
  // ── Monorepo detection ────────────────────────────────────────────────────────
@@ -382,311 +400,10 @@ function compareComponents(prevList, newResults) {
382
400
  return { added, removed, modified };
383
401
  }
384
402
 
385
- /**
386
- * Path-based classification: derive Storybook group from the first folder segment.
387
- * Examples:
388
- * ui/button.tsx → group: "UI"
389
- * circles/CircleCard.tsx → group: "Circles"
390
- * time/TimeDashboard.tsx → group: "Time"
391
- * time-resources/planning/… → group: "Time Resources"
392
- * settings/UserProfile.tsx → group: "Settings"
393
- */
403
+ /** Path-based classification: ui/ → shadcn, components root → Components, else Uncategorized. */
394
404
  function classifyByPath(rel) {
395
- const normalized = rel.replace(/\\/g, "/");
396
- const firstSegment = normalized.split("/")[0] || "";
397
-
398
- // ui/ → always "UI" (shadcn primitives)
399
- if (firstSegment === "ui") return { group: "UI", category: null };
400
-
401
- // Root-level file (no subdirectory): firstSegment is a filename like "NavLink.tsx"
402
- // Use "Components" instead of the filename as the group
403
- if (firstSegment.includes(".")) return { group: "Components", category: null };
404
-
405
- // Convert kebab-case folder to Title Case (time-resources → Time Resources)
406
- const group = firstSegment
407
- .split("-")
408
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
409
- .join(" ") || "Components";
410
-
411
- return { group, category: null };
412
- }
413
-
414
- /**
415
- * Infer the reusability tier of a component from path + content signals.
416
- * primitive → src/components/ui/ (shadcn atoms)
417
- * component → small, focused domain component (< 200 lines, ≤ 6 local imports)
418
- * feature → medium, contains state / multiple sub-parts
419
- * page → full page/view — reference only, not for direct reuse
420
- */
421
- // Semantic filename keywords that indicate a view-level (feature) component
422
- // even if the file is small (data fetched via context/hooks, not passed as props)
423
- const FEATURE_FILENAME_KEYWORDS = /Dashboard|Profile|Hub|Overview|Planning|Timesheet|Pipeline|Workload|Schedule|Portfolio|Chart|Board|Reports?|Analytics|Settings$/i;
424
-
425
- function inferTier(rel, content) {
426
- const normalized = rel.replace(/\\/g, "/");
427
- if (normalized.startsWith("ui/") || normalized.includes("/ui/")) return "primitive";
428
-
429
- const filename = normalized.split("/").pop()?.replace(/\.(tsx|jsx|ts|js)$/, "") || "";
430
- const lines = content.split("\n").length;
431
- // Count relative imports (../ ./) AND path-alias imports (@/components/) as local dependencies
432
- const localImports = (content.match(/from\s+['"](?:\.\.?\/?|@\/components\/)/g) || []).length;
433
-
434
- if (lines >= 400 || localImports >= 12) return "page";
435
- // Section/Layout/View/Panel/Screen named components with few imports are view-level feature tier.
436
- // e.g. BusinessModelSection, HeroSection, CheckoutView, AppLayout, CartPanel — regardless of line count.
437
- // Threshold localImports <= 3: genuine sections import very little; aggregators already hit lines/imports above.
438
- const SECTION_FILENAME_PATTERNS = /Section$|Layout$|View$|Panel$|Screen$/;
439
- if (SECTION_FILENAME_PATTERNS.test(filename) && localImports <= 3) return "feature";
440
- // Semantic keyword: treat as feature even if small (fetches data via context/hooks)
441
- if (lines >= 200 || localImports >= 7 || FEATURE_FILENAME_KEYWORDS.test(filename)) return "feature";
442
- return "component";
443
- }
444
-
445
- // Tailwind pseudo-class modifiers that appear inside class strings — never valid cva option names
446
- const CVA_SKIP = new Set([
447
- "hover","focus","active","disabled","dark","group","peer","placeholder",
448
- "before","after","visited","checked","required","invalid","valid","not",
449
- "open","closed","empty","enabled","first","last","odd","even",
450
- ]);
451
-
452
- /**
453
- * Extract cva() variant options from a file, e.g.:
454
- * variants: { variant: { default, destructive, ghost }, size: { sm, md, lg } }
455
- * Returns { variant: ["default","destructive",...], size: [...] } or null.
456
- * Filters out Tailwind pseudo-class modifiers that appear inside class strings.
457
- */
458
- function extractCvaVariants(content) {
459
- const block = content.match(/variants\s*:\s*\{([\s\S]*?)\}\s*,?\s*defaultVariants/);
460
- if (!block) return null;
461
- const result = {};
462
- // Each top-level key is a variant dimension (variant, size, intent…)
463
- // Match: variantName: { optionA: "...", optionB: "..." }
464
- const keyRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
465
- let m;
466
- while ((m = keyRe.exec(block[1])) !== null) {
467
- const dimName = m[1];
468
- if (CVA_SKIP.has(dimName)) continue; // skip pseudo-class keys at outer level
469
- // Extract option names: only words at the start of a line (key: "value")
470
- const optRe = /^\s{6,}(\w[\w-]*)\s*:/gm;
471
- let om;
472
- const options = [];
473
- while ((om = optRe.exec(m[2])) !== null) {
474
- if (!CVA_SKIP.has(om[1])) options.push(om[1]);
475
- }
476
- if (options.length > 0) result[dimName] = options;
477
- }
478
- return Object.keys(result).length > 0 ? result : null;
479
- }
480
-
481
- // Common HTML/React props that are noise for the agent — skip them in cursor rules
482
- const PROPS_SKIP = new Set([
483
- "className","style","id","key","ref","children","asChild","slot",
484
- "tabIndex","role","aria-label","aria-describedby","aria-hidden","aria-labelledby",
485
- "data-testid","htmlFor","draggable","title","lang","dir","onClick","onChange",
486
- "onSubmit","onBlur","onFocus","onKeyDown","onKeyUp","onMouseEnter","onMouseLeave",
487
- ]);
488
-
489
- /**
490
- * Extract TypeScript prop types from a component file.
491
- * Looks for interface *Props* { ... } or type *Props* = { ... }
492
- * Returns Array<{ name, type, required }> or null.
493
- */
494
- function extractTSProps(content) {
495
- // Find the start of a props interface or type alias
496
- const startRe = /(?:interface|type)\s+\w*[Pp]rops\w*[^{]*\{/;
497
- const sm = startRe.exec(content);
498
- if (!sm) return null;
499
-
500
- // Walk forward counting braces to find the matching closing }
501
- const blockStart = sm.index + sm[0].length;
502
- let depth = 1;
503
- let i = blockStart;
504
- while (i < content.length && depth > 0) {
505
- if (content[i] === "{") depth++;
506
- if (content[i] === "}") depth--;
507
- i++;
508
- }
509
- const propsBlock = content.slice(blockStart, i - 1);
510
-
511
- const props = [];
512
- // Match top-level props: optional leading whitespace (1-6 chars), prop name, optional ?, colon, type
513
- const propRe = /^[ \t]{1,6}([\w]+)(\?)?:\s*(.+?)[ \t]*;?[ \t]*$/gm;
514
- let pm;
515
- while ((pm = propRe.exec(propsBlock)) !== null) {
516
- const propName = pm[1].trim();
517
- if (PROPS_SKIP.has(propName)) continue;
518
- // Skip comment lines that accidentally match (e.g. "// foo:")
519
- if (propsBlock.slice(pm.index).match(/^[ \t]*\/\//)) continue;
520
-
521
- const optional = pm[2] === "?";
522
- let type = pm[3]
523
- .replace(/\s*\/\/.*$/, "") // strip inline // comments (with any space before //)
524
- .trim() // remove surrounding whitespace
525
- .replace(/;$/, "") // strip trailing semicolon (now truly at the end)
526
- .trim();
527
-
528
- // Skip multi-line types (unbalanced braces — the rest is on the next line)
529
- const openBraces = (type.match(/\{/g) || []).length;
530
- const closeBraces = (type.match(/\}/g) || []).length;
531
- if (openBraces !== closeBraces) continue;
532
-
533
- // Simplify verbose React type names
534
- type = type.replace(/React\.ReactNode/g, "ReactNode");
535
- type = type.replace(/React\.FC[^;,\n]*/g, "FC");
536
- type = type.replace(/React\.CSSProperties/g, "CSSProperties");
537
- type = type.replace(/React\.RefObject<[^>]+>/g, "RefObject");
538
- type = type.replace(/React\.MouseEvent[^;,\n]*/g, "MouseEvent");
539
- type = type.replace(/React\.ChangeEvent[^;,\n]*/g, "ChangeEvent");
540
- type = type.replace(/React\.KeyboardEvent[^;,\n]*/g, "KeyboardEvent");
541
-
542
- // Truncate very long types
543
- if (type.length > 50) type = type.slice(0, 47) + "...";
544
-
545
- props.push({ name: propName, type, required: !optional });
546
- }
547
- return props.length > 0 ? props : null;
548
- }
549
-
550
- /**
551
- * Scan the entire src/ directory for non-Props TypeScript interface and type alias
552
- * declarations and build a type registry used by story-generator for realistic mock defaults.
553
- *
554
- * Returns { TypeName: Array<{ name, type, required }> } or null.
555
- * Only includes types with ≥2 fields; skips Props/State/Context/Config/Options/Ref types.
556
- */
557
- function extractProjectTypes() {
558
- const registry = {};
559
- const seen = new Set();
560
-
561
- function scanDir(dir) {
562
- if (!fs.existsSync(dir)) return;
563
- let entries;
564
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
565
- for (const e of entries) {
566
- if (e.isDirectory()) {
567
- if (IGNORE_DIRS.includes(e.name) || e.name === "__tests__") continue;
568
- scanDir(path.join(dir, e.name));
569
- } else if (/\.(ts|tsx)$/.test(e.name) &&
570
- !e.name.endsWith(".stories.tsx") &&
571
- !e.name.endsWith(".test.ts") &&
572
- !e.name.endsWith(".spec.ts")) {
573
- const fullPath = path.join(dir, e.name);
574
- let content;
575
- try { content = fs.readFileSync(fullPath, "utf-8"); } catch { continue; }
576
-
577
- // Find all export interface / type declarations whose names start with uppercase
578
- const declRe = /(?:export\s+)?(?:interface|type)\s+([A-Z][A-Za-z0-9_]*)(?:<[^>]*>)?(?:\s+extends\s+[^{]+)?\s*(?:=\s*(?![\|&]))?(?=\s*\{)/g;
579
- let dm;
580
- while ((dm = declRe.exec(content)) !== null) {
581
- const typeName = dm[1];
582
- // Skip non-data types
583
- if (/Props$|State$|Context$|Config$|Options$|Params$|Ref$|Handle$|Return$|Result$|Theme$/.test(typeName)) continue;
584
- if (/^(FC|React|JSX|HTML|Event|Mouse|Key|Touch|Change|Focus|Blur|Scroll|Input|Submit|SVG)/.test(typeName)) continue;
585
- if (seen.has(typeName)) continue; // first declaration wins
586
-
587
- // Locate the opening brace of the body
588
- const braceIdx = content.indexOf("{", dm.index + dm[0].length - 1);
589
- if (braceIdx === -1) continue;
590
-
591
- // Walk forward counting braces to find matching closing brace
592
- let depth = 1, i = braceIdx + 1;
593
- while (i < content.length && depth > 0) {
594
- if (content[i] === "{") depth++;
595
- if (content[i] === "}") depth--;
596
- i++;
597
- }
598
- const block = content.slice(braceIdx + 1, i - 1);
599
-
600
- // Parse fields — for data types we only skip React/HTML-specific attrs, NOT id/title/name etc.
601
- const fields = [];
602
- const fieldRe = /^[ \t]{1,6}(?:readonly\s+)?(\w+)(\?)?:\s*(.+?)[ \t]*;?[ \t]*$/gm;
603
- let fm;
604
- while ((fm = fieldRe.exec(block)) !== null) {
605
- const fieldName = fm[1].trim();
606
- // Only skip HTML/React presentation attrs — keep data fields like id, title, name, status
607
- if (/^(className|style|ref|key|htmlFor|tabIndex|role|draggable|aria-|data-)/.test(fieldName)) continue;
608
- if (block.slice(fm.index).match(/^[ \t]*\/\//)) continue;
609
- const optional = fm[2] === "?";
610
- let type = (fm[3] || "")
611
- .replace(/\s*\/\/.*$/, "")
612
- .trim()
613
- .replace(/;$/, "")
614
- .trim();
615
- const openB = (type.match(/\{/g) || []).length;
616
- const closeB = (type.match(/\}/g) || []).length;
617
- if (openB !== closeB) continue;
618
- type = type.replace(/React\.ReactNode/g, "ReactNode");
619
- if (type.length > 60) type = type.slice(0, 57) + "...";
620
- fields.push({ name: fieldName, type, required: !optional });
621
- }
622
-
623
- // Only register types with at least 2 fields (otherwise not useful for mock generation)
624
- if (fields.length >= 2) {
625
- registry[typeName] = fields;
626
- seen.add(typeName);
627
- }
628
- }
629
- }
630
- }
631
- }
632
-
633
- scanDir(SRC_DIR);
634
- return Object.keys(registry).length > 0 ? registry : null;
635
- }
636
-
637
- /**
638
- * Extract named UI patterns from a feature-tier file.
639
- *
640
- * Signal 1 — JSX block comments: {/* SECTION TITLE *\/}
641
- * e.g. {/* CLUSTER 1: REVENUE & BILLING *\/} → label "CLUSTER 1: REVENUE & BILLING"
642
- *
643
- * Signal 2 — Nearest <h3>/<h2> text (fallback when no comments found)
644
- * e.g. <h3>Unbilled WIP</h3> → label "Unbilled WIP"
645
- *
646
- * Returns Array<{ label: string, line: number }> or null.
647
- * Safe on any JSX/TSX file — never throws.
648
- */
649
- function extractInlinePatterns(content) {
650
- const patterns = [];
651
- const seen = new Set();
652
-
653
- // Signal 1: JSX block comments — {/* ... */}
654
- const commentRe = /\{\/\*\s*([\s\S]{3,120}?)\s*\*\/\}/g;
655
- let cm;
656
- while ((cm = commentRe.exec(content)) !== null) {
657
- const raw = cm[1].replace(/\s+/g, " ").trim();
658
- // Skip too-short, pure annotation, or code-style comments
659
- if (!raw || raw.length < 5) continue;
660
- if (/^(TODO|FIXME|HACK|NOTE|@|eslint|prettier|ts-ignore|type-)/i.test(raw)) continue;
661
- // Skip comments that look like code (contain JSX/HTML tags)
662
- if (/<[A-Za-z]/.test(raw)) continue;
663
- const lineNum = content.slice(0, cm.index).split("\n").length;
664
- const label = raw.slice(0, 80);
665
- const key = label.toLowerCase().replace(/\s+/g, " ");
666
- if (!seen.has(key)) {
667
- seen.add(key);
668
- patterns.push({ label, line: lineNum });
669
- }
670
- }
671
-
672
- // Signal 2: <h3>/<h2> text content (only if no JSX comments found)
673
- if (patterns.length === 0) {
674
- const headingRe = /<h[23][^>]*>\s*([^<{]{4,60}?)\s*<\/h[23]>/g;
675
- let hm;
676
- while ((hm = headingRe.exec(content)) !== null) {
677
- const raw = hm[1].replace(/\s+/g, " ").trim();
678
- if (!raw || raw.length < 4) continue;
679
- const lineNum = content.slice(0, hm.index).split("\n").length;
680
- const label = raw.slice(0, 80);
681
- const key = label.toLowerCase();
682
- if (!seen.has(key)) {
683
- seen.add(key);
684
- patterns.push({ label, line: lineNum });
685
- }
686
- }
687
- }
688
-
689
- return patterns.length > 0 ? patterns : null;
405
+ if (rel.startsWith("ui/")) return { group: "shadcn", category: "UI" };
406
+ return { group: "Components", category: "Components" };
690
407
  }
691
408
 
692
409
  function getAllComponentFiles(dir, baseDir = dir) {
@@ -734,6 +451,10 @@ function extractBrandAssets() {
734
451
  path.join(PROJECT_ROOT, "public"),
735
452
  path.join(PROJECT_ROOT, "src", "assets"),
736
453
  path.join(PROJECT_ROOT, "src", "images"),
454
+ path.join(PROJECT_ROOT, "client", "src", "assets"),
455
+ path.join(PROJECT_ROOT, "client", "src", "images"),
456
+ path.join(PROJECT_ROOT, "frontend", "src", "assets"),
457
+ path.join(PROJECT_ROOT, "web", "src", "assets"),
737
458
  ];
738
459
  for (const dir of dirs) {
739
460
  const relDir = path.relative(PROJECT_ROOT, dir).replace(/\\/g, "/");
@@ -745,15 +466,16 @@ function extractBrandAssets() {
745
466
  assets.push({ path: filePath, name: baseName, type });
746
467
  }
747
468
  }
748
- // Fallback: if no branded assets found by keyword, include all image files from src/assets
469
+ // Fallback: if no branded assets found by keyword, include all image files from SRC_DIR/assets
749
470
  if (assets.length === 0 || assets.every((r) => r.type === "asset")) {
750
471
  const assetsDir = path.join(SRC_DIR, "assets");
751
472
  if (fs.existsSync(assetsDir)) {
752
473
  const imgExtRe = /\.(png|jpg|jpeg|svg|gif|webp|ico)$/i;
753
474
  const allImages = fs.readdirSync(assetsDir).filter((f) => imgExtRe.test(f));
475
+ const relAssetsDir = path.relative(PROJECT_ROOT, assetsDir).replace(/\\/g, "/");
754
476
  for (const img of allImages) {
755
- if (!assets.some((r) => r.path === "src/assets/" + img)) {
756
- assets.push({ type: "asset", path: "src/assets/" + img, name: img });
477
+ if (!assets.some((r) => r.path === relAssetsDir + "/" + img)) {
478
+ assets.push({ type: "asset", path: relAssetsDir + "/" + img, name: img });
757
479
  }
758
480
  }
759
481
  }
@@ -761,163 +483,30 @@ function extractBrandAssets() {
761
483
  return assets;
762
484
  }
763
485
 
764
- /** Extract Lucide icons used in app code with per-component JSX usage counts.
765
- * Returns { name, total, topFiles }[] sorted by total desc.
766
- * Handles aliased imports: `import { ArrowRight as Arrow }` — counted under original name. */
486
+ /** Extract icon names from `import { A, B, C } from "lucide-react"` in app code only (exclude stories so generated Star/defaults don't pollute). */
767
487
  function extractLucideIconsUsed(srcDir) {
768
488
  const allFiles = getAllTsxJsxInDir(srcDir);
769
489
  const files = allFiles.filter((rel) => !/^stories[/\\]/.test(rel.replace(/\\/g, "/")));
770
-
771
- // originalName → { total: number, topFiles: Map<componentName, count> }
772
- const iconData = new Map();
490
+ const names = new Set();
773
491
  const importRe = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
774
-
775
492
  for (const rel of files) {
776
493
  const fullPath = path.join(srcDir, rel);
777
- let content;
778
- try { content = fs.readFileSync(fullPath, "utf-8"); } catch (_) { continue; }
779
- const componentName = path.basename(rel).replace(/\.(tsx|jsx|ts|js)$/, "");
780
-
781
- // Pass 1 — collect local-name → original-name for this file
782
- importRe.lastIndex = 0;
783
- let m;
784
- const localToOriginal = new Map();
785
- while ((m = importRe.exec(content)) !== null) {
786
- m[1].split(",").forEach((part) => {
787
- const trimmed = part.trim();
788
- if (!trimmed) return;
789
- const asMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/);
790
- if (asMatch) {
791
- const orig = asMatch[1];
792
- const local = asMatch[2];
793
- if (/^[A-Z][a-zA-Z0-9]*$/.test(orig)) {
794
- localToOriginal.set(local, orig);
795
- if (!iconData.has(orig)) iconData.set(orig, { total: 0, topFiles: new Map() });
796
- }
797
- } else {
798
- const name = trimmed.split(/\s+/)[0];
799
- if (name && /^[A-Z][a-zA-Z0-9]*$/.test(name)) {
800
- localToOriginal.set(name, name);
801
- if (!iconData.has(name)) iconData.set(name, { total: 0, topFiles: new Map() });
802
- }
803
- }
804
- });
805
- }
806
-
807
- // Pass 2 — count JSX usages `<LocalName ` / `<LocalName/` / `<LocalName>`
808
- for (const [localName, originalName] of localToOriginal.entries()) {
809
- const jsxRe = new RegExp(`<${localName}[\\s/>]`, "g");
810
- let count = 0;
811
- while (jsxRe.exec(content) !== null) count++;
812
- if (count > 0) {
813
- const data = iconData.get(originalName);
814
- data.total += count;
815
- data.topFiles.set(componentName, (data.topFiles.get(componentName) || 0) + count);
494
+ try {
495
+ const content = fs.readFileSync(fullPath, "utf-8");
496
+ let m;
497
+ while ((m = importRe.exec(content)) !== null) {
498
+ const block = m[1];
499
+ block.split(",").forEach((part) => {
500
+ const trimmed = part.trim();
501
+ const asMatch = trimmed.match(/^(\w+)\s+as\s+/);
502
+ const name = asMatch ? asMatch[1] : trimmed.split(/\s+/)[0];
503
+ if (name && /^[A-Z][a-zA-Z0-9]*$/.test(name)) names.add(name);
504
+ });
816
505
  }
817
- }
506
+ } catch (_) {}
507
+ importRe.lastIndex = 0;
818
508
  }
819
-
820
- return [...iconData.entries()]
821
- .map(([name, data]) => ({
822
- name,
823
- total: data.total,
824
- topFiles: [...data.topFiles.entries()]
825
- .sort((a, b) => b[1] - a[1])
826
- .slice(0, 5)
827
- .map(([n]) => n),
828
- }))
829
- .sort((a, b) => b.total - a.total || a.name.localeCompare(b.name));
830
- }
831
-
832
- /** Scan app source for responsive breakpoints, grid column patterns, gaps and max-width usage.
833
- * Returns { breakpoints, gridCols, gaps, maxWidths, containerCount } or null if nothing found. */
834
- function extractGridSystem(srcDir) {
835
- if (!fs.existsSync(srcDir)) return null;
836
- const allFiles = getAllTsxJsxInDir(srcDir);
837
- const files = allFiles.filter((rel) => !/^stories[/\\]/.test(rel.replace(/\\/g, "/")));
838
- if (files.length === 0) return null;
839
-
840
- const BP_NAMES = ["sm", "md", "lg", "xl", "2xl"];
841
- const bpData = {};
842
- for (const bp of BP_NAMES) bpData[bp] = { count: 0, topFiles: new Map() };
843
-
844
- const colData = {}; // colValue → { count, topFiles: Map }
845
- const gapCounts = {};
846
- const maxWCounts = {};
847
- let containerCount = 0;
848
-
849
- for (const rel of files) {
850
- let content;
851
- try { content = fs.readFileSync(path.join(srcDir, rel), "utf-8"); } catch (_) { continue; }
852
- const componentName = path.basename(rel).replace(/\.(tsx|jsx|ts|js)$/, "");
853
-
854
- // Breakpoints: sm: md: lg: xl: 2xl:
855
- const bpRe = /\b(2xl|xl|lg|md|sm):/g;
856
- let m;
857
- while ((m = bpRe.exec(content)) !== null) {
858
- const name = m[1];
859
- bpData[name].count++;
860
- bpData[name].topFiles.set(componentName, (bpData[name].topFiles.get(componentName) || 0) + 1);
861
- }
862
-
863
- // grid-cols-{value}
864
- const gcRe = /\bgrid-cols-((?:\[[\w\s\-.,/()*+]+\]|\w+))/g;
865
- while ((m = gcRe.exec(content)) !== null) {
866
- const val = m[1];
867
- if (!colData[val]) colData[val] = { count: 0, topFiles: new Map() };
868
- colData[val].count++;
869
- colData[val].topFiles.set(componentName, (colData[val].topFiles.get(componentName) || 0) + 1);
870
- }
871
-
872
- // gap-{n}, gap-x-{n}, gap-y-{n}
873
- const gapRe = /\bgap(?:-[xy])?-((?:\[[\w\s./]+\]|\d[\w.]*|px))\b/g;
874
- while ((m = gapRe.exec(content)) !== null) {
875
- const val = m[1];
876
- gapCounts[val] = (gapCounts[val] || 0) + 1;
877
- }
878
-
879
- // max-w-{value}
880
- const maxWRe = /\bmax-w-((?:\[[\w\s./]+\]|[\w-]+))/g;
881
- while ((m = maxWRe.exec(content)) !== null) {
882
- const val = m[1];
883
- maxWCounts[val] = (maxWCounts[val] || 0) + 1;
884
- }
885
-
886
- // container class
887
- let ctrM;
888
- const ctrRe = /\bcontainer\b/g;
889
- while ((ctrM = ctrRe.exec(content)) !== null) containerCount++;
890
- }
891
-
892
- const breakpoints = {};
893
- for (const bp of BP_NAMES) {
894
- if (bpData[bp].count > 0) {
895
- breakpoints[bp] = {
896
- count: bpData[bp].count,
897
- topFiles: [...bpData[bp].topFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([n]) => n),
898
- };
899
- }
900
- }
901
-
902
- const gridCols = {};
903
- for (const [val, data] of Object.entries(colData).sort((a, b) => b[1].count - a[1].count)) {
904
- gridCols[val] = {
905
- count: data.count,
906
- topFiles: [...data.topFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([n]) => n),
907
- };
908
- }
909
-
910
- const gaps = Object.fromEntries(
911
- Object.entries(gapCounts).sort((a, b) => b[1] - a[1]).slice(0, 12)
912
- );
913
-
914
- const maxWidths = Object.fromEntries(
915
- Object.entries(maxWCounts).sort((a, b) => b[1] - a[1]).slice(0, 10)
916
- );
917
-
918
- if (Object.keys(breakpoints).length === 0 && Object.keys(gridCols).length === 0) return null;
919
-
920
- return { breakpoints, gridCols, gaps, maxWidths, containerCount };
509
+ return [...names].sort();
921
510
  }
922
511
 
923
512
  function extractVdsTags(content) {
@@ -1660,11 +1249,13 @@ function extractFoundations() {
1660
1249
  const borderRadiusScale = {};
1661
1250
 
1662
1251
  // Read ALL existing CSS files and combine them (covers Tailwind v4 split configs)
1252
+ // Detect frontend source dir for fullstack projects (client/src, frontend/src, web/src)
1253
+ const frontendPrefixes = ["src", "client/src", "frontend/src", "web/src"];
1663
1254
  const allCssCandidates = [
1664
- path.join(PROJECT_ROOT, "src", "index.css"),
1665
- path.join(PROJECT_ROOT, "src", "globals.css"),
1666
- path.join(PROJECT_ROOT, "src", "styles", "globals.css"),
1667
- path.join(PROJECT_ROOT, "src", "App.css"),
1255
+ ...frontendPrefixes.map((p) => path.join(PROJECT_ROOT, p, "index.css")),
1256
+ ...frontendPrefixes.map((p) => path.join(PROJECT_ROOT, p, "globals.css")),
1257
+ ...frontendPrefixes.map((p) => path.join(PROJECT_ROOT, p, "styles", "globals.css")),
1258
+ ...frontendPrefixes.map((p) => path.join(PROJECT_ROOT, p, "App.css")),
1668
1259
  path.join(PROJECT_ROOT, "app", "globals.css"),
1669
1260
  ];
1670
1261
  const cssChunks = [];
@@ -1908,7 +1499,10 @@ function extractFoundations() {
1908
1499
 
1909
1500
  // Fallback: if no typography tokens from config, extract from Google Fonts @import
1910
1501
  if (Object.keys(typography).length === 0 || (!typography.body && !typography.bodyFontFamily)) {
1911
- for (const cssFile of ["src/index.css", "src/App.css", "src/globals.css", "src/styles/globals.css"]) {
1502
+ const typoCssCandidates = frontendPrefixes.flatMap((p) => [
1503
+ p + "/index.css", p + "/App.css", p + "/globals.css", p + "/styles/globals.css",
1504
+ ]);
1505
+ for (const cssFile of typoCssCandidates) {
1912
1506
  const cssPath = path.join(PROJECT_ROOT, cssFile);
1913
1507
  if (!fs.existsSync(cssPath)) continue;
1914
1508
  try {
@@ -1934,11 +1528,15 @@ function extractFoundations() {
1934
1528
  try {
1935
1529
  const { globSync } = projectRequire("glob");
1936
1530
  const ROOT = PROJECT_ROOT;
1937
- allTsxFiles = globSync("src/**/*.{tsx,jsx,ts,js}", {
1938
- cwd: ROOT,
1939
- absolute: true,
1940
- ignore: ["**/*.stories.*", "**/node_modules/**"],
1941
- });
1531
+ // Use multiple globs to cover standard + fullstack project structures
1532
+ const srcGlobs = frontendPrefixes.map((p) => p + "/**/*.{tsx,jsx,ts,js}");
1533
+ for (const g of srcGlobs) {
1534
+ allTsxFiles.push(...globSync(g, {
1535
+ cwd: ROOT,
1536
+ absolute: true,
1537
+ ignore: ["**/*.stories.*", "**/node_modules/**"],
1538
+ }));
1539
+ }
1942
1540
  } catch (_) {
1943
1541
  function walkDir(dir, list) {
1944
1542
  if (!fs.existsSync(dir)) return;
@@ -2278,111 +1876,6 @@ function extractTokenUsage() {
2278
1876
  };
2279
1877
  }
2280
1878
 
2281
- function extractColorUsage(colorNames) {
2282
- if (!fs.existsSync(SRC_DIR)) return {};
2283
- if (!Array.isArray(colorNames) || colorNames.length === 0) return {};
2284
- const files = getAllTsxJsxInDir(SRC_DIR);
2285
- if (!Array.isArray(files) || files.length === 0) return {};
2286
-
2287
- const nameSet = new Set(colorNames);
2288
- const usage = {};
2289
- for (const name of colorNames) {
2290
- usage[name] = { bg: 0, text: 0, border: 0, other: 0, topFiles: new Map() };
2291
- }
2292
-
2293
- // Build reverse lookup: lowercase hex → color name (for arbitrary-* and inline-* tokens)
2294
- // e.g. "arbitrary-4f46e5" → hex key "4f46e5"
2295
- const hexToName = new Map();
2296
- for (const name of colorNames) {
2297
- if (name.startsWith("arbitrary-") || name.startsWith("inline-")) {
2298
- const hexKey = name.replace(/^(arbitrary|inline)-/, "").toLowerCase();
2299
- hexToName.set(hexKey, name);
2300
- }
2301
- }
2302
-
2303
- // Single-pass regex: matches bg-primary, text-card-foreground, border-chart-1, etc.
2304
- const colorUtilRe = /\b(bg|text|border|fill|stroke|ring|from|to|via|outline)-([\w][\w-]*)/g;
2305
- // Bracket-notation: bg-[#4F46E5], text-[#4f46e5/80], etc.
2306
- const bracketHexRe = /\b(bg|text|border|fill|stroke|ring|from|to|via|outline)-\[#([0-9a-fA-F]{3,8})(?:\/[\d.]+)?\]/g;
2307
- // Inline style hex colors: color:"#4F46E5", backgroundColor:'#4f46e5', etc.
2308
- const inlineHexRe = /(?:color|(?:background|border|fill|stroke|ring)(?:Color)?)\s*[:=]\s*['"]#([0-9a-fA-F]{3,8})['"]/g;
2309
-
2310
- for (const rel of files) {
2311
- if (rel.includes("stories")) continue;
2312
- let content;
2313
- try {
2314
- content = fs.readFileSync(path.join(SRC_DIR, rel), "utf-8");
2315
- } catch (_) { continue; }
2316
- const componentName = path.basename(rel).replace(/\.(tsx|jsx|ts|js)$/, "");
2317
-
2318
- let m;
2319
- // ── Token-based utilities (bg-primary, text-muted-foreground, …) ──
2320
- const reCopy = new RegExp(colorUtilRe.source, "g");
2321
- while ((m = reCopy.exec(content)) !== null) {
2322
- const prefix = m[1];
2323
- const token = m[2];
2324
- if (!nameSet.has(token)) continue;
2325
- if (prefix === "bg") usage[token].bg++;
2326
- else if (prefix === "text") usage[token].text++;
2327
- else if (prefix === "border") usage[token].border++;
2328
- else usage[token].other++;
2329
- usage[token].topFiles.set(componentName, (usage[token].topFiles.get(componentName) || 0) + 1);
2330
- }
2331
-
2332
- // ── Bracket-notation hex utilities (bg-[#4F46E5], …) ──
2333
- if (hexToName.size > 0) {
2334
- const brCopy = new RegExp(bracketHexRe.source, "gi");
2335
- while ((m = brCopy.exec(content)) !== null) {
2336
- const prefix = m[1].toLowerCase();
2337
- const hexKey = m[2].toLowerCase();
2338
- // Normalise 3-char hex → 6-char for matching
2339
- const normalKey = hexKey.length === 3
2340
- ? hexKey[0] + hexKey[0] + hexKey[1] + hexKey[1] + hexKey[2] + hexKey[2]
2341
- : hexKey;
2342
- const name = hexToName.get(normalKey) || hexToName.get(hexKey);
2343
- if (!name) continue;
2344
- if (prefix === "bg") usage[name].bg++;
2345
- else if (prefix === "text") usage[name].text++;
2346
- else if (prefix === "border") usage[name].border++;
2347
- else usage[name].other++;
2348
- usage[name].topFiles.set(componentName, (usage[name].topFiles.get(componentName) || 0) + 1);
2349
- }
2350
-
2351
- // ── Inline style hex colors (color: "#4F46E5") ──
2352
- const inCopy = new RegExp(inlineHexRe.source, "gi");
2353
- while ((m = inCopy.exec(content)) !== null) {
2354
- const hexKey = m[1].toLowerCase();
2355
- const normalKey = hexKey.length === 3
2356
- ? hexKey[0] + hexKey[0] + hexKey[1] + hexKey[1] + hexKey[2] + hexKey[2]
2357
- : hexKey;
2358
- const name = hexToName.get(normalKey) || hexToName.get(hexKey);
2359
- if (!name) continue;
2360
- usage[name].other++;
2361
- usage[name].topFiles.set(componentName, (usage[name].topFiles.get(componentName) || 0) + 1);
2362
- }
2363
- }
2364
- }
2365
-
2366
- const result = {};
2367
- for (const name of colorNames) {
2368
- const data = usage[name];
2369
- const total = data.bg + data.text + data.border + data.other;
2370
- if (total > 0) {
2371
- result[name] = {
2372
- bg: data.bg,
2373
- text: data.text,
2374
- border: data.border,
2375
- total,
2376
- topFiles: [...data.topFiles.entries()]
2377
- .sort((a, b) => b[1] - a[1])
2378
- .slice(0, 5)
2379
- .map(([n]) => n),
2380
- };
2381
- }
2382
- }
2383
- return result;
2384
- }
2385
-
2386
1879
  function extractButtonUsage() {
2387
1880
  if (!fs.existsSync(SRC_DIR)) return null;
2388
1881
  const files = getAllTsxJsxInDir(SRC_DIR);
@@ -2465,13 +1958,7 @@ function scan() {
2465
1958
  description = "";
2466
1959
  }
2467
1960
  const tokens = extractTailwindTokens(content);
2468
- const tier = inferTier(rel, content);
2469
- const variants = (tier === "primitive") ? extractCvaVariants(content) : null;
2470
- const props = (tier === "component" || tier === "feature") ? extractTSProps(content) : null;
2471
- const patterns = (tier === "feature") ? extractInlinePatterns(content) : null;
2472
- const lineCount = content.split("\n").length;
2473
- const localImportCount = (content.match(/from\s+['"]\.\.\?\//g) || []).length;
2474
- results.push({ file: rel, name, group, category, description, tokens, tier, lines: lineCount, localImports: localImportCount, ...(variants ? { variants } : {}), ...(props ? { props } : {}), ...(patterns ? { patterns } : {}) });
1961
+ results.push({ file: rel, name, group, category, description, tokens });
2475
1962
  }
2476
1963
  if (PAGES_DIR && fs.existsSync(PAGES_DIR)) {
2477
1964
  const pageFiles = getAllComponentFiles(PAGES_DIR);
@@ -2480,7 +1967,6 @@ function scan() {
2480
1967
  const content = fs.readFileSync(fullPath, "utf-8");
2481
1968
  const name = humanizeName(rel);
2482
1969
  const tokens = extractTailwindTokens(content);
2483
- const lineCount = content.split("\n").length;
2484
1970
  results.push({
2485
1971
  file: path.relative(PROJECT_ROOT, PAGES_DIR).replace(/\\/g, "/") + "/" + rel,
2486
1972
  name,
@@ -2488,9 +1974,6 @@ function scan() {
2488
1974
  category: "Pages",
2489
1975
  description: "",
2490
1976
  tokens,
2491
- tier: "page",
2492
- lines: lineCount,
2493
- localImports: 0,
2494
1977
  });
2495
1978
  }
2496
1979
  }
@@ -2505,13 +1988,6 @@ function scan() {
2505
1988
  if (tokenUsage) {
2506
1989
  foundations.tokenUsage = tokenUsage;
2507
1990
  }
2508
- const colorNames = Object.keys(foundations.colors || {}).filter((k) => k !== "_dark");
2509
- const colorUsage = extractColorUsage(colorNames);
2510
- if (Object.keys(colorUsage).length > 0) foundations.colorUsage = colorUsage;
2511
- const gridSystem = extractGridSystem(SRC_DIR);
2512
- if (gridSystem) foundations.gridSystem = gridSystem;
2513
- const projectTypes = extractProjectTypes();
2514
- if (projectTypes) foundations.types = projectTypes;
2515
1991
  const componentSuggestions = extractComponentSuggestions();
2516
1992
  const unreleasedSectionCandidates = extractUnreleasedSectionCandidates();
2517
1993
  const output = {
@@ -14,17 +14,22 @@ import { fileURLToPath } from "url";
14
14
 
15
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
16
  const PROJECT_ROOT = path.join(__dirname, "..");
17
- const STORYBOOK_DIR = path.join(PROJECT_ROOT, ".storybook");
18
17
 
19
- /** Projenin gerçek frontend src dizinini tespit eder (fullstack/monorepo desteği). */
18
+ // Auto-detect frontend src dir for fullstack projects (client/src, frontend/src, web/src)
20
19
  function detectSrcDir(root) {
21
- for (const candidate of ["client/src", "frontend/src", "web/src"]) {
22
- if (fs.existsSync(path.join(root, candidate))) return path.join(root, candidate);
20
+ for (const prefix of ["src", "client/src", "frontend/src", "web/src"]) {
21
+ const full = path.join(root, prefix);
22
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
23
+ const hasComponents = fs.existsSync(path.join(full, "components"));
24
+ const hasAppFiles = fs.readdirSync(full).some((f) => /\.(tsx|jsx)$/i.test(f));
25
+ if (hasComponents || hasAppFiles) return full;
26
+ }
23
27
  }
24
28
  return path.join(root, "src");
25
29
  }
26
30
 
27
31
  const SRC_DIR = detectSrcDir(PROJECT_ROOT);
32
+ const STORYBOOK_DIR = path.join(PROJECT_ROOT, ".storybook");
28
33
 
29
34
  // ── vds.config.js loader ──────────────────────────────────────────────────────
30
35
  function loadVdsConfig() {
@@ -128,7 +133,7 @@ function detectHooksInFile(content) {
128
133
 
129
134
  /** Map: relative path (src/...) -> Set of hook names */
130
135
  function detectHooksUsedInProject(projectRoot) {
131
- const srcDir = detectSrcDir(projectRoot);
136
+ const srcDir = SRC_DIR;
132
137
  const srcRel = path.relative(projectRoot, srcDir).replace(/\\/g, "/");
133
138
  const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(srcRel, r));
134
139
  const byFile = new Map();
@@ -145,7 +150,7 @@ function detectHooksUsedInProject(projectRoot) {
145
150
 
146
151
  /** Find file that exports providerName (e.g. TimerProvider). If hookName is given, prefer a file that also contains that hook (e.g. context/SidebarContext.tsx for useSidebar over ui/sidebar). */
147
152
  function findProviderExportPath(projectRoot, providerName, hookName) {
148
- const srcDir = detectSrcDir(projectRoot);
153
+ const srcDir = SRC_DIR;
149
154
  const srcRel = path.relative(projectRoot, srcDir).replace(/\\/g, "/");
150
155
  const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(srcRel, r));
151
156
  const exportRe = new RegExp(
@@ -157,8 +162,8 @@ function findProviderExportPath(projectRoot, providerName, hookName) {
157
162
  try {
158
163
  const content = fs.readFileSync(full, "utf-8");
159
164
  if (exportRe.test(content)) {
160
- // Strip the src prefix (could be "src", "client/src", "frontend/src", etc.)
161
- const withoutExt = rel.replace(/\.(tsx?|jsx?)$/i, "").replace(new RegExp("^" + srcRel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\/?"), "");
165
+ const srcRelEscaped = srcRel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ const withoutExt = rel.replace(/\.(tsx?|jsx?)$/i, "").replace(new RegExp("^" + srcRelEscaped + "/?"), "");
162
167
  const pathForImport = "@/" + withoutExt;
163
168
  const hasHook = hookName && new RegExp("\\b" + hookName + "\\b").test(content);
164
169
  candidates.push({ pathForImport, hasHook });
@@ -202,7 +207,7 @@ function detectRouterPackage(projectRoot) {
202
207
  }
203
208
 
204
209
  function projectUsesRouterHooks(projectRoot) {
205
- const srcDir = detectSrcDir(projectRoot);
210
+ const srcDir = SRC_DIR;
206
211
  if (!fs.existsSync(srcDir)) return false;
207
212
  const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(srcDir, r));
208
213
  for (const file of files) {
@@ -218,7 +223,7 @@ function projectUsesRouterHooks(projectRoot) {
218
223
 
219
224
  /** Detect if project uses react-dnd (useDrag, useDrop, or import from react-dnd). */
220
225
  function detectReactDnd(projectRoot) {
221
- const srcDir = detectSrcDir(projectRoot);
226
+ const srcDir = SRC_DIR;
222
227
  const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(srcDir, r));
223
228
  for (const full of files) {
224
229
  try {
@@ -412,7 +417,7 @@ function injectProviderDecorators(projectRoot) {
412
417
  // 7. Handle hooks without provider: add warning comment to affected story files
413
418
  if (hooksWithoutProvider.size > 0) {
414
419
  const componentNameFromPath = (rel) => path.basename(rel, path.extname(rel));
415
- const storiesDir = path.join(projectRoot, "src", "stories");
420
+ const storiesDir = path.join(SRC_DIR, "stories");
416
421
  const storyRels = fs.existsSync(storiesDir) ? getAllSourceFiles(storiesDir, storiesDir) : [];
417
422
  const storyByComponent = new Map();
418
423
  for (const r of storyRels) {
@@ -12,7 +12,21 @@ import { fileURLToPath } from "url";
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const PROJECT_ROOT = path.join(__dirname, "..");
15
- const SRC_DIR = path.join(PROJECT_ROOT, "src");
15
+
16
+ // Auto-detect frontend src dir for fullstack projects (client/src, frontend/src, web/src)
17
+ function detectSrcDir(root) {
18
+ for (const prefix of ["src", "client/src", "frontend/src", "web/src"]) {
19
+ const full = path.join(root, prefix);
20
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory()) {
21
+ const hasComponents = fs.existsSync(path.join(full, "components"));
22
+ const hasAppFiles = fs.readdirSync(full).some((f) => /\.(tsx|jsx)$/i.test(f));
23
+ if (hasComponents || hasAppFiles) return full;
24
+ }
25
+ }
26
+ return path.join(root, "src");
27
+ }
28
+
29
+ const SRC_DIR = detectSrcDir(PROJECT_ROOT);
16
30
  const STORYBOOK_DIR = path.join(PROJECT_ROOT, ".storybook");
17
31
  const MOCKS_DIR = path.join(STORYBOOK_DIR, "mocks");
18
32
 
@@ -70,9 +84,9 @@ function isProblematic(spec) {
70
84
  }
71
85
 
72
86
  function collectProblematicImports(projectRoot) {
73
- const srcDir = path.join(projectRoot, "src");
87
+ const srcDir = SRC_DIR;
74
88
  if (!fs.existsSync(srcDir)) return new Set();
75
- const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(projectRoot, "src", r));
89
+ const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(srcDir, r));
76
90
  const found = new Set();
77
91
  for (const file of files) {
78
92
  try {
@@ -87,9 +101,9 @@ function collectProblematicImports(projectRoot) {
87
101
 
88
102
  /** Check relative imports resolve to a file; report unresolved. */
89
103
  function reportUnresolvedImports(projectRoot) {
90
- const srcDir = path.join(projectRoot, "src");
104
+ const srcDir = detectSrcDir(projectRoot);
91
105
  if (!fs.existsSync(srcDir)) return;
92
- const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(projectRoot, "src", r));
106
+ const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(srcDir, r));
93
107
  for (const file of files) {
94
108
  try {
95
109
  const content = fs.readFileSync(file, "utf-8");
@@ -204,33 +218,12 @@ function injectAliases(projectRoot, specifiers) {
204
218
  }
205
219
  }
206
220
 
207
- /** Inject resolve.dedupe into .storybook/main.* viteFinal to prevent multiple React instances.
208
- * Multiple React instances cause "Cannot read properties of null (reading 'useRef')" at MemoryRouter. */
209
- function injectDedupe(projectRoot) {
210
- const mainPath = getMainPath(projectRoot);
211
- if (!mainPath) return;
212
- let content = fs.readFileSync(mainPath, "utf-8");
213
- if (content.includes("dedupe")) return; // idempotent
214
- // Match: alias block + its trailing comma + whitespace/newline + resolve closing }
215
- // e.g. alias: { "@": path.resolve(...) },\n },
216
- // [^{}]* ensures we skip injection if alias has nested object values (safe fallback)
217
- const pattern = /(\balias\s*:\s*\{[^{}]*\}),(\s*\})/;
218
- if (!pattern.test(content)) return;
219
- content = content.replace(pattern, (_, aliasBlock, resolveClose) =>
220
- `${aliasBlock},\n dedupe: ["react", "react-dom", "react-router-dom"],${resolveClose}`
221
- );
222
- fs.writeFileSync(mainPath, content, "utf-8");
223
- console.log("[VDS] Storybook adapt: injected React dedupe into viteFinal");
224
- }
225
-
226
221
  function main() {
227
222
  const projectRoot = PROJECT_ROOT;
228
223
  if (!fs.existsSync(path.join(projectRoot, ".storybook"))) {
229
224
  console.log("[VDS] .storybook not found; skip storybook-adapt.");
230
225
  return;
231
226
  }
232
- // Always inject dedupe — prevents "useRef null" crashes from multiple React instances
233
- injectDedupe(projectRoot);
234
227
  reportUnresolvedImports(projectRoot);
235
228
  const problematic = collectProblematicImports(projectRoot);
236
229
  if (problematic.size === 0) return;