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
|
-
|
|
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,
|
|
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
|
-
"
|
|
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: ["
|
|
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(), "${
|
|
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
|
-
["
|
|
316
|
-
["
|
|
317
|
-
["
|
|
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
|
|
359
|
-
const
|
|
349
|
+
if (!pkg) return false;
|
|
350
|
+
const dev = pkg.devDependencies || {};
|
|
360
351
|
const fwPkg = storybookFrameworkPackage(framework);
|
|
361
|
-
return !(
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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.
|
|
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,
|
|
413
|
+
const mainTs = buildStorybookMainTs(framework, srcPrefix);
|
|
401
414
|
const mainPath = path.join(storybookDir, "main.ts");
|
|
402
415
|
fs.writeFileSync(mainPath, mainTs, "utf-8");
|
|
403
|
-
|
|
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 —
|
|
523
|
-
function ensureStoriesDir(projectRoot) {
|
|
524
|
-
const srcDir = path.join(projectRoot,
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
@@ -40,41 +40,59 @@ function resolveDir(envKey, candidates) {
|
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
|
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 === "
|
|
756
|
-
assets.push({ type: "asset", path: "
|
|
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
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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,
|
|
1665
|
-
path.join(PROJECT_ROOT,
|
|
1666
|
-
path.join(PROJECT_ROOT,
|
|
1667
|
-
path.join(PROJECT_ROOT,
|
|
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
|
-
|
|
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
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
// Auto-detect frontend src dir for fullstack projects (client/src, frontend/src, web/src)
|
|
20
19
|
function detectSrcDir(root) {
|
|
21
|
-
for (const
|
|
22
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
161
|
-
const withoutExt = rel.replace(/\.(tsx?|jsx?)$/i, "").replace(new RegExp("^" +
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
87
|
+
const srcDir = SRC_DIR;
|
|
74
88
|
if (!fs.existsSync(srcDir)) return new Set();
|
|
75
|
-
const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(
|
|
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 =
|
|
104
|
+
const srcDir = detectSrcDir(projectRoot);
|
|
91
105
|
if (!fs.existsSync(srcDir)) return;
|
|
92
|
-
const files = getAllSourceFiles(srcDir, srcDir).map((r) => path.join(
|
|
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;
|