vds-test-empty 1.0.0
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/.cursorrules +12 -0
- package/.storybook/main.ts +10 -0
- package/.storybook/preview.ts +4 -0
- package/package.json +23 -0
- package/public/vds-output.json +26 -0
- package/src/stories/foundations/Brand.stories.tsx +22 -0
- package/src/stories/foundations/Colors.stories.tsx +21 -0
- package/vds-core/README.md +52 -0
- package/vds-core/VdsPreview.tsx +72 -0
- package/vds-core/dashboard-server.mjs +237 -0
- package/vds-core/dist/dashboard/assets/index-DIRjrdDY.js +43 -0
- package/vds-core/dist/dashboard/index.html +68 -0
- package/vds-core/scan.mjs +916 -0
- package/vds-core/story-generator.mjs +880 -0
- package/vds-core/watch.mjs +11 -0
- package/vds-output.json +26 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VDS Core — Ghost scan. Run from project root: node vds-core/scan.mjs
|
|
4
|
+
* Paths are relative to the host project root (parent of vds-core).
|
|
5
|
+
* Set VDS_LOCALE=en|tr for CLI message language (default: en).
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PROJECT_ROOT = path.join(__dirname, "..");
|
|
15
|
+
const projectRequire = createRequire(path.join(PROJECT_ROOT, "package.json"));
|
|
16
|
+
|
|
17
|
+
const CLI_LOCALES = {
|
|
18
|
+
en: {
|
|
19
|
+
componentsNotFound: "src/components not found. VDS scan skipped.",
|
|
20
|
+
scanComplete: "VDS: {n} components → vds-output.json & public/vds-output.json",
|
|
21
|
+
},
|
|
22
|
+
tr: {
|
|
23
|
+
componentsNotFound: "src/components bulunamadı. VDS taraması atlandı.",
|
|
24
|
+
scanComplete: "VDS: {n} bileşen → vds-output.json ve public/vds-output.json",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const CLI_LOCALE = (process.env.VDS_LOCALE === "tr" ? "tr" : "en");
|
|
28
|
+
const cliT = (key, n) => CLI_LOCALES[CLI_LOCALE][key].replace("{n}", String(n));
|
|
29
|
+
const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
30
|
+
const COMPONENTS_DIR = path.join(PROJECT_ROOT, "src", "components");
|
|
31
|
+
const PAGES_DIR = path.join(PROJECT_ROOT, "src", "pages");
|
|
32
|
+
const OUTPUT_FILE = path.join(PROJECT_ROOT, "vds-output.json");
|
|
33
|
+
const PUBLIC_MANIFEST = path.join(PROJECT_ROOT, "public", "vds-output.json");
|
|
34
|
+
const HISTORY_FILE = path.join(PROJECT_ROOT, "vds-history.json");
|
|
35
|
+
|
|
36
|
+
const DEFAULT_VITE_PORT = 5173;
|
|
37
|
+
|
|
38
|
+
function getVitePort() {
|
|
39
|
+
for (const name of ["vite.config.ts", "vite.config.js"]) {
|
|
40
|
+
const p = path.join(PROJECT_ROOT, name);
|
|
41
|
+
if (!fs.existsSync(p)) continue;
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
44
|
+
const m = raw.match(/port\s*:\s*(\d+)/);
|
|
45
|
+
if (m) return parseInt(m[1], 10);
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
}
|
|
48
|
+
return DEFAULT_VITE_PORT;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function vdsDashboardUrl() {
|
|
52
|
+
const port = getVitePort();
|
|
53
|
+
return `http://localhost:${port}/vds`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatClickableLink(url) {
|
|
57
|
+
return `\u001b]8;;${url}\u0007${url}\u001b]8;;\u0007`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getGitBranch() {
|
|
61
|
+
try {
|
|
62
|
+
return execSync("git branch --show-current", { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim() || null;
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getGitEngineer() {
|
|
69
|
+
try {
|
|
70
|
+
return execSync("git config user.name", { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim() || null;
|
|
71
|
+
} catch (_) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function incPatch(version) {
|
|
77
|
+
const parts = String(version || "1.0.0").split(".");
|
|
78
|
+
const major = parseInt(parts[0], 10) || 1;
|
|
79
|
+
const minor = parseInt(parts[1], 10) || 0;
|
|
80
|
+
const patch = (parseInt(parts[2], 10) || 0) + 1;
|
|
81
|
+
return `${major}.${minor}.${patch}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readPreviousOutput() {
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(OUTPUT_FILE)) {
|
|
87
|
+
const raw = fs.readFileSync(OUTPUT_FILE, "utf-8");
|
|
88
|
+
const data = JSON.parse(raw);
|
|
89
|
+
return data.components || [];
|
|
90
|
+
}
|
|
91
|
+
} catch (_) {}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readLastVersion() {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(HISTORY_FILE)) {
|
|
98
|
+
const raw = fs.readFileSync(HISTORY_FILE, "utf-8");
|
|
99
|
+
const data = JSON.parse(raw);
|
|
100
|
+
const entries = Array.isArray(data) ? data : data.entries || [];
|
|
101
|
+
const last = entries[entries.length - 1];
|
|
102
|
+
return last ? last.version : "1.0.0";
|
|
103
|
+
}
|
|
104
|
+
} catch (_) {}
|
|
105
|
+
return "1.0.0";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readHistoryEntries() {
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(HISTORY_FILE)) {
|
|
111
|
+
const raw = fs.readFileSync(HISTORY_FILE, "utf-8");
|
|
112
|
+
const data = JSON.parse(raw);
|
|
113
|
+
return Array.isArray(data) ? data : data.entries || [];
|
|
114
|
+
}
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function compareComponents(prevList, newResults) {
|
|
120
|
+
const prevByFile = new Map((prevList || []).map((c) => [c.file, c]));
|
|
121
|
+
const newByFile = new Map(newResults.map((c) => [c.file, c]));
|
|
122
|
+
const added = [];
|
|
123
|
+
const removed = [];
|
|
124
|
+
const modified = [];
|
|
125
|
+
for (const [file, c] of newByFile) {
|
|
126
|
+
const p = prevByFile.get(file);
|
|
127
|
+
if (!p) added.push({ type: "added", name: c.name, group: c.group, category: c.category });
|
|
128
|
+
else if (p.name !== c.name || p.group !== c.group || p.category !== c.category) {
|
|
129
|
+
modified.push({ type: "modified", name: c.name, group: c.group, category: c.category });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const [file, c] of prevByFile) {
|
|
133
|
+
if (!newByFile.has(file)) removed.push({ type: "removed", name: c.name, group: c.group, category: c.category });
|
|
134
|
+
}
|
|
135
|
+
return { added, removed, modified };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const CLASSIFICATION_RULES = [
|
|
139
|
+
{ keywords: ["analysisdashboard", "componentlibrary"], group: "Sections", category: "Dashboard" },
|
|
140
|
+
{
|
|
141
|
+
keywords: [
|
|
142
|
+
"enterprisepushpanel",
|
|
143
|
+
"figmalibrarygenerator",
|
|
144
|
+
"integrationguide",
|
|
145
|
+
"projectdropzone",
|
|
146
|
+
"repoconnect",
|
|
147
|
+
"tokensstudioguide",
|
|
148
|
+
],
|
|
149
|
+
group: "Sections",
|
|
150
|
+
category: "Features",
|
|
151
|
+
},
|
|
152
|
+
{ keywords: ["hover-card"], group: "Feedback", category: "Feedback" },
|
|
153
|
+
{
|
|
154
|
+
keywords: [
|
|
155
|
+
"card",
|
|
156
|
+
"table",
|
|
157
|
+
"list",
|
|
158
|
+
"badge",
|
|
159
|
+
"avatar",
|
|
160
|
+
"chart",
|
|
161
|
+
"accordion",
|
|
162
|
+
"tabs",
|
|
163
|
+
"collapsible",
|
|
164
|
+
"toggle-group",
|
|
165
|
+
"carousel",
|
|
166
|
+
"scroll-area",
|
|
167
|
+
],
|
|
168
|
+
group: "Data Display",
|
|
169
|
+
category: "Data Display",
|
|
170
|
+
},
|
|
171
|
+
{ keywords: ["toggle"], group: "Actions", category: "Toggle" },
|
|
172
|
+
{ keywords: ["button", "cta"], group: "Actions", category: "Button" },
|
|
173
|
+
{ keywords: ["drawer", "sheet"], group: "Feedback", category: "Overlay" },
|
|
174
|
+
{
|
|
175
|
+
keywords: [
|
|
176
|
+
"modal",
|
|
177
|
+
"dialog",
|
|
178
|
+
"alert",
|
|
179
|
+
"toast",
|
|
180
|
+
"toaster",
|
|
181
|
+
"sonner",
|
|
182
|
+
"tooltip",
|
|
183
|
+
"popover",
|
|
184
|
+
"hover-card",
|
|
185
|
+
"progress",
|
|
186
|
+
"skeleton",
|
|
187
|
+
],
|
|
188
|
+
group: "Feedback",
|
|
189
|
+
category: "Feedback",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
keywords: [
|
|
193
|
+
"input",
|
|
194
|
+
"form",
|
|
195
|
+
"select",
|
|
196
|
+
"checkbox",
|
|
197
|
+
"textarea",
|
|
198
|
+
"label",
|
|
199
|
+
"switch",
|
|
200
|
+
"radio",
|
|
201
|
+
"slider",
|
|
202
|
+
"calendar",
|
|
203
|
+
"input-otp",
|
|
204
|
+
"command",
|
|
205
|
+
],
|
|
206
|
+
group: "Forms",
|
|
207
|
+
category: "Forms",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
keywords: [
|
|
211
|
+
"nav",
|
|
212
|
+
"header",
|
|
213
|
+
"sidebar",
|
|
214
|
+
"menu",
|
|
215
|
+
"menubar",
|
|
216
|
+
"dropdown-menu",
|
|
217
|
+
"navigation-menu",
|
|
218
|
+
"context-menu",
|
|
219
|
+
"navlink",
|
|
220
|
+
"breadcrumb",
|
|
221
|
+
"pagination",
|
|
222
|
+
],
|
|
223
|
+
group: "Navigation",
|
|
224
|
+
category: "Navigation",
|
|
225
|
+
},
|
|
226
|
+
{ keywords: ["separator", "aspect-ratio", "resizable"], group: "Layout", category: "Layout" },
|
|
227
|
+
{
|
|
228
|
+
keywords: ["hero", "banner", "section", "pricing", "feature", "featuresgrid", "interactivedemo", "modernhero"],
|
|
229
|
+
group: "Sections",
|
|
230
|
+
category: "Sections",
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
function getAllComponentFiles(dir, baseDir = dir) {
|
|
235
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
236
|
+
const files = [];
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const fullPath = path.join(dir, entry.name);
|
|
239
|
+
if (entry.isDirectory()) {
|
|
240
|
+
files.push(...getAllComponentFiles(fullPath, baseDir));
|
|
241
|
+
} else if (entry.isFile() && /\.(tsx|jsx)$/i.test(entry.name)) {
|
|
242
|
+
files.push(path.relative(baseDir, fullPath).replace(/\\/g, "/"));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return files;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getAllTsxJsxInDir(dir) {
|
|
249
|
+
if (!fs.existsSync(dir)) return [];
|
|
250
|
+
return getAllComponentFiles(dir, dir);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const BRAND_KEYWORDS = /logo|brand|icon|favicon|emblem|mark/i;
|
|
254
|
+
const BRAND_EXTENSIONS = /\.(svg|png|ico)$/i;
|
|
255
|
+
|
|
256
|
+
function getFilesByExtension(dir, extRe, baseDir = dir) {
|
|
257
|
+
if (!fs.existsSync(dir)) return [];
|
|
258
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
259
|
+
const files = [];
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
const fullPath = path.join(dir, entry.name);
|
|
262
|
+
if (entry.isDirectory()) {
|
|
263
|
+
files.push(...getFilesByExtension(fullPath, extRe, baseDir));
|
|
264
|
+
} else if (entry.isFile() && extRe.test(entry.name)) {
|
|
265
|
+
files.push(path.relative(baseDir, fullPath).replace(/\\/g, "/"));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Scan public/, src/assets/, src/images/ for .svg, .png, .ico; filter by brand keywords. */
|
|
272
|
+
function extractBrandAssets() {
|
|
273
|
+
const assets = [];
|
|
274
|
+
const dirs = [
|
|
275
|
+
path.join(PROJECT_ROOT, "public"),
|
|
276
|
+
path.join(PROJECT_ROOT, "src", "assets"),
|
|
277
|
+
path.join(PROJECT_ROOT, "src", "images"),
|
|
278
|
+
];
|
|
279
|
+
for (const dir of dirs) {
|
|
280
|
+
const relDir = path.relative(PROJECT_ROOT, dir).replace(/\\/g, "/");
|
|
281
|
+
const files = getFilesByExtension(dir, BRAND_EXTENSIONS, PROJECT_ROOT);
|
|
282
|
+
for (const filePath of files) {
|
|
283
|
+
const baseName = path.basename(filePath);
|
|
284
|
+
if (!BRAND_KEYWORDS.test(baseName)) continue;
|
|
285
|
+
const type = /favicon|\.ico$/i.test(baseName) ? "favicon" : /logo|brand|emblem|mark/i.test(baseName) ? "logo" : "icon";
|
|
286
|
+
assets.push({ path: filePath, name: baseName, type });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Fallback: if no branded assets found by keyword, include all image files from src/assets
|
|
290
|
+
if (assets.length === 0 || assets.every((r) => r.type === "asset")) {
|
|
291
|
+
const assetsDir = path.join(SRC_DIR, "assets");
|
|
292
|
+
if (fs.existsSync(assetsDir)) {
|
|
293
|
+
const imgExtRe = /\.(png|jpg|jpeg|svg|gif|webp|ico)$/i;
|
|
294
|
+
const allImages = fs.readdirSync(assetsDir).filter((f) => imgExtRe.test(f));
|
|
295
|
+
for (const img of allImages) {
|
|
296
|
+
if (!assets.some((r) => r.path === "src/assets/" + img)) {
|
|
297
|
+
assets.push({ type: "asset", path: "src/assets/" + img, name: img });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return assets;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Extract icon names from `import { A, B, C } from "lucide-react"` (or 'lucide-react'). Returns unique list. */
|
|
306
|
+
function extractLucideIconsUsed(srcDir) {
|
|
307
|
+
const files = getAllTsxJsxInDir(srcDir);
|
|
308
|
+
const names = new Set();
|
|
309
|
+
const importRe = /import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/g;
|
|
310
|
+
for (const rel of files) {
|
|
311
|
+
const fullPath = path.join(srcDir, rel);
|
|
312
|
+
try {
|
|
313
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
314
|
+
let m;
|
|
315
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
316
|
+
const block = m[1];
|
|
317
|
+
block.split(",").forEach((part) => {
|
|
318
|
+
const trimmed = part.trim();
|
|
319
|
+
const asMatch = trimmed.match(/^(\w+)\s+as\s+/);
|
|
320
|
+
const name = asMatch ? asMatch[1] : trimmed.split(/\s+/)[0];
|
|
321
|
+
if (name && /^[A-Z][a-zA-Z0-9]*$/.test(name)) names.add(name);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
} catch (_) {}
|
|
325
|
+
importRe.lastIndex = 0;
|
|
326
|
+
}
|
|
327
|
+
return [...names].sort();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function extractVdsTags(content) {
|
|
331
|
+
const tags = {};
|
|
332
|
+
const tagNames = ["vds-group", "vds-category", "vds-name", "vds-description"];
|
|
333
|
+
for (const tag of tagNames) {
|
|
334
|
+
const re = new RegExp(`@${tag}\\s+([^\n*]+)`, "g");
|
|
335
|
+
const m = re.exec(content);
|
|
336
|
+
if (m) tags[tag.replace("vds-", "")] = m[1].trim();
|
|
337
|
+
}
|
|
338
|
+
return Object.keys(tags).length ? tags : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function classifyByFileName(filePath) {
|
|
342
|
+
const baseName = path.basename(filePath, path.extname(filePath)).toLowerCase();
|
|
343
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
344
|
+
const found = rule.keywords.some((kw) => baseName.includes(kw));
|
|
345
|
+
if (found) return { group: rule.group, category: rule.category };
|
|
346
|
+
}
|
|
347
|
+
return { group: "Uncategorized", category: "Uncategorized" };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function humanizeName(filePath) {
|
|
351
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
352
|
+
return base.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const COMPONENT_SUGGESTION_KEYWORDS = [
|
|
356
|
+
{ re: /\bcard\b/i, name: "Card" },
|
|
357
|
+
{ re: /\bsection\b/i, name: "Section" },
|
|
358
|
+
{ re: /\bitem\b/i, name: "Item" },
|
|
359
|
+
{ re: /\bbadge\b/i, name: "Badge" },
|
|
360
|
+
{ re: /\brow\b/i, name: "Row" },
|
|
361
|
+
{ re: /\bgrid\b/i, name: "Grid" },
|
|
362
|
+
{ re: /\bhero\b/i, name: "Hero" },
|
|
363
|
+
{ re: /\bcta\b/i, name: "Cta" },
|
|
364
|
+
{ re: /\bstat\b/i, name: "Stat" },
|
|
365
|
+
{ re: /\bprice\b/i, name: "Pricing" },
|
|
366
|
+
{ re: /\bfeature\b/i, name: "Feature" },
|
|
367
|
+
{ re: /\bbutton\b/i, name: "Button" },
|
|
368
|
+
{ re: /\btag\b/i, name: "Tag" },
|
|
369
|
+
{ re: /\bchip\b/i, name: "Chip" },
|
|
370
|
+
{ re: /\btile\b/i, name: "Tile" },
|
|
371
|
+
{ re: /\bbanner\b/i, name: "Banner" },
|
|
372
|
+
{ re: /\bmodal\b/i, name: "Modal" },
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
function suggestNameFromPattern(pattern, index) {
|
|
376
|
+
const parts = [];
|
|
377
|
+
for (const { re, name } of COMPONENT_SUGGESTION_KEYWORDS) {
|
|
378
|
+
if (re.test(pattern)) parts.push(name);
|
|
379
|
+
}
|
|
380
|
+
if (parts.length > 0) return parts.join("");
|
|
381
|
+
return "Block" + (index + 1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Scan src/pages/*.tsx for repeated className clusters; return component suggestions. */
|
|
385
|
+
function extractComponentSuggestions() {
|
|
386
|
+
if (!fs.existsSync(PAGES_DIR)) return [];
|
|
387
|
+
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
388
|
+
const byPattern = new Map();
|
|
389
|
+
const re = /className\s*=\s*["']([^"']+)["']|className\s*=\s*\{\s*["']([^"']+)["']/g;
|
|
390
|
+
|
|
391
|
+
for (const rel of pageFiles) {
|
|
392
|
+
const fullPath = path.join(PAGES_DIR, rel);
|
|
393
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
394
|
+
const srcRel = "src/pages/" + rel.replace(/\\/g, "/");
|
|
395
|
+
let m;
|
|
396
|
+
re.lastIndex = 0;
|
|
397
|
+
while ((m = re.exec(content)) !== null) {
|
|
398
|
+
const raw = (m[1] ?? m[2] ?? "").trim().replace(/\s+/g, " ").trim();
|
|
399
|
+
if (!raw) continue;
|
|
400
|
+
const classCount = raw.split(/\s+/).filter(Boolean).length;
|
|
401
|
+
if (classCount < 3) continue;
|
|
402
|
+
if (!byPattern.has(raw)) {
|
|
403
|
+
const before = content.substring(0, m.index);
|
|
404
|
+
const lastOpen = before.lastIndexOf("<");
|
|
405
|
+
const tagMatch = lastOpen >= 0 ? content.slice(lastOpen).match(/<(\w+)/) : null;
|
|
406
|
+
const tagName = tagMatch ? tagMatch[1] : "div";
|
|
407
|
+
byPattern.set(raw, {
|
|
408
|
+
count: 0,
|
|
409
|
+
files: new Set(),
|
|
410
|
+
snippet: `<${tagName} className="${raw}">...</${tagName}>`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const entry = byPattern.get(raw);
|
|
414
|
+
entry.count += 1;
|
|
415
|
+
entry.files.add(srcRel);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const suggestions = [];
|
|
420
|
+
let blockIndex = 0;
|
|
421
|
+
for (const [pattern, { count, files, snippet }] of byPattern.entries()) {
|
|
422
|
+
if (count < 2) continue;
|
|
423
|
+
const suggestedName = suggestNameFromPattern(pattern, blockIndex);
|
|
424
|
+
if (suggestedName.startsWith("Block")) blockIndex += 1;
|
|
425
|
+
suggestions.push({
|
|
426
|
+
suggestedName,
|
|
427
|
+
occurrences: count,
|
|
428
|
+
foundIn: [...files].sort(),
|
|
429
|
+
pattern,
|
|
430
|
+
snippet,
|
|
431
|
+
reason: `Same className cluster appears ${count} times`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return suggestions;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function extractTailwindTokens(content) {
|
|
438
|
+
const tokens = new Set();
|
|
439
|
+
const patterns = [
|
|
440
|
+
/className\s*=\s*["'`]([^"'`]+)["'`]/g,
|
|
441
|
+
/className\s*=\s*\{\s*["'`]([^"'`]+)["'`]/g,
|
|
442
|
+
/cn\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
|
443
|
+
/cva\s*\(\s*["'`]([^"'`]+)["'`]/g,
|
|
444
|
+
/["'`]([a-zA-Z0-9_\-\/\s\[\]&:%.]+(?:hover|focus|active|disabled|sm|md|lg|xl|2xl|dark:)[a-zA-Z0-9_\-\/\s\[\]&:%.]*)["'`]/g,
|
|
445
|
+
];
|
|
446
|
+
for (const re of patterns) {
|
|
447
|
+
let m;
|
|
448
|
+
while ((m = re.exec(content)) !== null) {
|
|
449
|
+
const part = m[1];
|
|
450
|
+
part
|
|
451
|
+
.replace(/\$\{[^}]*\}/g, " ")
|
|
452
|
+
.split(/\s+/)
|
|
453
|
+
.map((s) => s.trim())
|
|
454
|
+
.filter((s) => s.length > 0 && /^[a-zA-Z0-9_\-\[\]\/:&%.]+$/.test(s))
|
|
455
|
+
.forEach((s) => tokens.add(s));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return [...tokens].sort();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Parse HSL string "0 0% 0%" or "hsl(0 0% 0%)" and return hex. */
|
|
462
|
+
function hslToHex(hslStr) {
|
|
463
|
+
const match = hslStr.match(/hsl\s*\(\s*([\d.]+)\s*[, ]\s*([\d.]+)%\s*[, ]\s*([\d.]+)%\s*\)/) ||
|
|
464
|
+
hslStr.match(/^([\d.]+)\s+([\d.]+)%\s+([\d.]+)%$/);
|
|
465
|
+
if (!match) return null;
|
|
466
|
+
const h = Number(match[1]) / 360;
|
|
467
|
+
const s = Number(match[2]) / 100;
|
|
468
|
+
const l = Number(match[3]) / 100;
|
|
469
|
+
let r, g, b;
|
|
470
|
+
if (s === 0) {
|
|
471
|
+
r = g = b = l;
|
|
472
|
+
} else {
|
|
473
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
474
|
+
const p = 2 * l - q;
|
|
475
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
476
|
+
g = hue2rgb(p, q, h);
|
|
477
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
478
|
+
}
|
|
479
|
+
const toHex = (x) => {
|
|
480
|
+
const n = Math.round(Math.max(0, Math.min(255, x * 255)));
|
|
481
|
+
return n.toString(16).padStart(2, "0");
|
|
482
|
+
};
|
|
483
|
+
return "#" + toHex(r) + toHex(g) + toHex(b);
|
|
484
|
+
}
|
|
485
|
+
function hue2rgb(p, q, t) {
|
|
486
|
+
if (t < 0) t += 1;
|
|
487
|
+
if (t > 1) t -= 1;
|
|
488
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
489
|
+
if (t < 1 / 2) return q;
|
|
490
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
491
|
+
return p;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function parseCssVarBlock(block) {
|
|
495
|
+
const out = {};
|
|
496
|
+
const varRe = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
497
|
+
let m;
|
|
498
|
+
while ((m = varRe.exec(block)) !== null) {
|
|
499
|
+
const name = m[1];
|
|
500
|
+
const raw = m[2].trim();
|
|
501
|
+
out[name] = raw;
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Resolve Tailwind theme for boxShadow, spacing, screens, zIndex, motion. Uses resolveConfig when available. */
|
|
507
|
+
function getTailwindTheme() {
|
|
508
|
+
const empty = { shadows: {}, spacing: {}, breakpoints: {}, zIndex: {}, transitionDuration: {}, transitionTimingFunction: {}, animation: {} };
|
|
509
|
+
try {
|
|
510
|
+
const resolveConfig = projectRequire("tailwindcss/resolveConfig");
|
|
511
|
+
let config = { content: [{ raw: "", extension: "html" }], theme: {} };
|
|
512
|
+
const twPath = path.join(PROJECT_ROOT, "tailwind.config.js");
|
|
513
|
+
const twPathMjs = path.join(PROJECT_ROOT, "tailwind.config.mjs");
|
|
514
|
+
if (fs.existsSync(twPath)) {
|
|
515
|
+
config = projectRequire(twPath);
|
|
516
|
+
if (config && typeof config === "object" && config.default) config = config.default;
|
|
517
|
+
} else if (fs.existsSync(twPathMjs)) {
|
|
518
|
+
config = projectRequire(twPathMjs);
|
|
519
|
+
if (config && typeof config === "object" && config.default) config = config.default;
|
|
520
|
+
}
|
|
521
|
+
const resolved = resolveConfig(config);
|
|
522
|
+
const theme = resolved.theme || {};
|
|
523
|
+
const boxShadow = theme.boxShadow;
|
|
524
|
+
const spacing = theme.spacing;
|
|
525
|
+
const screens = theme.screens;
|
|
526
|
+
const zIndex = theme.zIndex;
|
|
527
|
+
const transitionDuration = theme.transitionDuration;
|
|
528
|
+
const transitionTimingFunction = theme.transitionTimingFunction;
|
|
529
|
+
const animation = theme.animation;
|
|
530
|
+
const toObj = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
|
|
531
|
+
return {
|
|
532
|
+
shadows: toObj(boxShadow),
|
|
533
|
+
spacing: toObj(spacing),
|
|
534
|
+
breakpoints: toObj(screens),
|
|
535
|
+
zIndex: toObj(zIndex),
|
|
536
|
+
transitionDuration: toObj(transitionDuration),
|
|
537
|
+
transitionTimingFunction: toObj(transitionTimingFunction),
|
|
538
|
+
animation: toObj(animation),
|
|
539
|
+
};
|
|
540
|
+
} catch (_) {
|
|
541
|
+
return empty;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function extractFoundations() {
|
|
546
|
+
const colors = {};
|
|
547
|
+
const colorsDark = {};
|
|
548
|
+
const typography = {};
|
|
549
|
+
const cssRadiusVars = {};
|
|
550
|
+
const borderRadiusScale = {};
|
|
551
|
+
const cssPath = path.join(PROJECT_ROOT, "src", "index.css");
|
|
552
|
+
const globalsCss = path.join(PROJECT_ROOT, "src", "globals.css");
|
|
553
|
+
const appGlobals = path.join(PROJECT_ROOT, "app", "globals.css");
|
|
554
|
+
const cssToRead = fs.existsSync(cssPath) ? cssPath : fs.existsSync(globalsCss) ? globalsCss : appGlobals;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
if (fs.existsSync(cssToRead)) {
|
|
558
|
+
const css = fs.readFileSync(cssToRead, "utf-8");
|
|
559
|
+
|
|
560
|
+
const rootMatch = css.match(/:root\s*\{([\s\S]*?)\}/);
|
|
561
|
+
if (rootMatch) {
|
|
562
|
+
const rootVars = parseCssVarBlock(rootMatch[1]);
|
|
563
|
+
for (const [name, value] of Object.entries(rootVars)) {
|
|
564
|
+
if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
|
|
565
|
+
const hsl = `hsl(${value})`;
|
|
566
|
+
colors[name] = { value: hsl, hex: hslToHex(hsl) || hsl };
|
|
567
|
+
} else if (/rem|px/.test(value)) {
|
|
568
|
+
cssRadiusVars[name] = value;
|
|
569
|
+
} else {
|
|
570
|
+
colors[name] = { value, hex: value };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const darkMatch = css.match(/\.dark\s*\{([\s\S]*?)\}/) ||
|
|
576
|
+
css.match(/\[data-theme\s*=\s*["']dark["']\]\s*\{([\s\S]*?)\}/);
|
|
577
|
+
if (darkMatch) {
|
|
578
|
+
const darkVars = parseCssVarBlock(darkMatch[1]);
|
|
579
|
+
for (const [name, value] of Object.entries(darkVars)) {
|
|
580
|
+
if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
|
|
581
|
+
const hsl = `hsl(${value})`;
|
|
582
|
+
colorsDark[name] = { value: hsl, hex: hslToHex(hsl) || hsl };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const bodyMatch = css.match(/body\s*\{[^}]*font-family:\s*([^;]+);/);
|
|
588
|
+
if (bodyMatch) typography.body = bodyMatch[1].trim();
|
|
589
|
+
const monoMatch = css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/);
|
|
590
|
+
if (monoMatch) typography.mono = monoMatch[1].trim();
|
|
591
|
+
}
|
|
592
|
+
} catch (_) {}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
const twPath = path.join(PROJECT_ROOT, "tailwind.config.ts");
|
|
596
|
+
const twPathJs = path.join(PROJECT_ROOT, "tailwind.config.js");
|
|
597
|
+
const twFile = fs.existsSync(twPath) ? twPath : twPathJs;
|
|
598
|
+
if (fs.existsSync(twFile)) {
|
|
599
|
+
const tw = fs.readFileSync(twFile, "utf-8");
|
|
600
|
+
const sansMatch = tw.match(/sans:\s*\[([^\]]+)\]/);
|
|
601
|
+
if (sansMatch) {
|
|
602
|
+
typography.tailwindSans = sansMatch[1]
|
|
603
|
+
.split(",")
|
|
604
|
+
.map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
|
|
605
|
+
.filter(Boolean);
|
|
606
|
+
}
|
|
607
|
+
const monoMatch2 = tw.match(/mono:\s*\[([^\]]+)\]/);
|
|
608
|
+
if (monoMatch2) {
|
|
609
|
+
typography.tailwindMono = monoMatch2[1]
|
|
610
|
+
.split(",")
|
|
611
|
+
.map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
|
|
612
|
+
.filter(Boolean);
|
|
613
|
+
}
|
|
614
|
+
const brMatch = tw.match(/borderRadius:\s*\{([\s\S]*?)\}/);
|
|
615
|
+
if (brMatch) {
|
|
616
|
+
const body = brMatch[1];
|
|
617
|
+
const brRe = /(\w+):\s*"([^"]+)"/g;
|
|
618
|
+
let m2;
|
|
619
|
+
while ((m2 = brRe.exec(body)) !== null) borderRadiusScale[m2[1]] = m2[2];
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch (_) {}
|
|
623
|
+
|
|
624
|
+
// Fallback: if no CSS variable colors found, extract hardcoded colors from component files
|
|
625
|
+
if (Object.keys(colors).length === 0) {
|
|
626
|
+
const hardcodedColors = new Map();
|
|
627
|
+
const srcFiles = getAllTsxJsxInDir(SRC_DIR);
|
|
628
|
+
for (const f of srcFiles) {
|
|
629
|
+
try {
|
|
630
|
+
const fullPath = path.join(SRC_DIR, f);
|
|
631
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
632
|
+
const matches = content.matchAll(/(bg|text|border|from|to|via|ring|fill|stroke|shadow|outline|accent|divide|decoration|placeholder)-\[#([0-9a-fA-F]{3,8})\]/g);
|
|
633
|
+
for (const m of matches) {
|
|
634
|
+
const hex = "#" + m[2].toLowerCase();
|
|
635
|
+
if (!hardcodedColors.has(hex)) {
|
|
636
|
+
hardcodedColors.set(hex, { usages: new Set(), count: 0 });
|
|
637
|
+
}
|
|
638
|
+
hardcodedColors.get(hex).usages.add(m[1]);
|
|
639
|
+
hardcodedColors.get(hex).count++;
|
|
640
|
+
}
|
|
641
|
+
} catch (_) {}
|
|
642
|
+
}
|
|
643
|
+
const sorted = [...hardcodedColors.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
644
|
+
for (const [hex, info] of sorted) {
|
|
645
|
+
const primaryUsage = [...info.usages][0];
|
|
646
|
+
const name = `color-${primaryUsage}-${hex.slice(1)}`;
|
|
647
|
+
colors[name] = { value: hex, hex };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const radius = {};
|
|
652
|
+
if (cssRadiusVars.radius) radius.base = cssRadiusVars.radius;
|
|
653
|
+
if (Object.keys(borderRadiusScale).length > 0) radius.borderRadius = borderRadiusScale;
|
|
654
|
+
const foundationsColors = { ...colors };
|
|
655
|
+
if (Object.keys(colorsDark).length > 0) foundationsColors._dark = colorsDark;
|
|
656
|
+
|
|
657
|
+
// Fallback: if no typography tokens from config, extract from Google Fonts @import
|
|
658
|
+
if (Object.keys(typography).length === 0 || (!typography.body && !typography.bodyFontFamily)) {
|
|
659
|
+
for (const cssFile of ["src/index.css", "src/App.css", "src/globals.css", "src/styles/globals.css"]) {
|
|
660
|
+
const cssPath = path.join(PROJECT_ROOT, cssFile);
|
|
661
|
+
if (!fs.existsSync(cssPath)) continue;
|
|
662
|
+
try {
|
|
663
|
+
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
664
|
+
const fontImports = cssContent.matchAll(/@import\s+url\(['"]?https:\/\/fonts\.googleapis\.com\/css2\?family=([^&'"]+)/g);
|
|
665
|
+
const families = [];
|
|
666
|
+
for (const m of fontImports) {
|
|
667
|
+
const family = decodeURIComponent(m[1]).replace(/\+/g, " ").split(":")[0];
|
|
668
|
+
families.push(family);
|
|
669
|
+
}
|
|
670
|
+
if (families.length > 0) {
|
|
671
|
+
typography.body = families[0] + ", sans-serif";
|
|
672
|
+
typography.fontFamilies = families;
|
|
673
|
+
if (families.length > 1) typography.headingFont = families.find((f) => f !== families[0]) || families[1];
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
} catch (_) {}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const twTheme = getTailwindTheme();
|
|
681
|
+
const normalizeThemeObj = (obj) => {
|
|
682
|
+
if (!obj || typeof obj !== "object") return {};
|
|
683
|
+
const out = {};
|
|
684
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
685
|
+
if (v !== undefined && v !== null) out[k] = typeof v === "string" ? v : String(v);
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
};
|
|
689
|
+
return {
|
|
690
|
+
colors: foundationsColors,
|
|
691
|
+
typography,
|
|
692
|
+
radius,
|
|
693
|
+
shadows: normalizeThemeObj(twTheme.shadows),
|
|
694
|
+
spacing: normalizeThemeObj(twTheme.spacing),
|
|
695
|
+
breakpoints: normalizeThemeObj(twTheme.breakpoints),
|
|
696
|
+
zIndex: normalizeThemeObj(twTheme.zIndex),
|
|
697
|
+
motion: {
|
|
698
|
+
transitionDuration: normalizeThemeObj(twTheme.transitionDuration),
|
|
699
|
+
transitionTimingFunction: normalizeThemeObj(twTheme.transitionTimingFunction),
|
|
700
|
+
animation: normalizeThemeObj(twTheme.animation),
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function scan() {
|
|
706
|
+
if (!fs.existsSync(COMPONENTS_DIR)) {
|
|
707
|
+
console.error(CLI_LOCALES[CLI_LOCALE].componentsNotFound);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const relativeFiles = getAllComponentFiles(COMPONENTS_DIR);
|
|
711
|
+
const results = [];
|
|
712
|
+
for (const rel of relativeFiles) {
|
|
713
|
+
const fullPath = path.join(COMPONENTS_DIR, rel);
|
|
714
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
715
|
+
const vdsTags = extractVdsTags(content);
|
|
716
|
+
let group, category, name, description;
|
|
717
|
+
if (vdsTags) {
|
|
718
|
+
group = vdsTags.group ?? "Uncategorized";
|
|
719
|
+
category = vdsTags.category ?? "Uncategorized";
|
|
720
|
+
name = vdsTags.name ?? humanizeName(rel);
|
|
721
|
+
description = vdsTags.description ?? "";
|
|
722
|
+
} else {
|
|
723
|
+
const classified = classifyByFileName(rel);
|
|
724
|
+
group = classified.group;
|
|
725
|
+
category = classified.category;
|
|
726
|
+
name = humanizeName(rel);
|
|
727
|
+
description = "";
|
|
728
|
+
}
|
|
729
|
+
const tokens = extractTailwindTokens(content);
|
|
730
|
+
results.push({ file: rel, name, group, category, description, tokens });
|
|
731
|
+
}
|
|
732
|
+
if (fs.existsSync(PAGES_DIR)) {
|
|
733
|
+
const pageFiles = getAllComponentFiles(PAGES_DIR);
|
|
734
|
+
for (const rel of pageFiles) {
|
|
735
|
+
const fullPath = path.join(PAGES_DIR, rel);
|
|
736
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
737
|
+
const name = humanizeName(rel);
|
|
738
|
+
const tokens = extractTailwindTokens(content);
|
|
739
|
+
results.push({
|
|
740
|
+
file: "pages/" + rel,
|
|
741
|
+
name,
|
|
742
|
+
group: "Sections",
|
|
743
|
+
category: "Pages",
|
|
744
|
+
description: "",
|
|
745
|
+
tokens,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const foundations = extractFoundations();
|
|
750
|
+
foundations.icons = extractLucideIconsUsed(SRC_DIR);
|
|
751
|
+
foundations.brand = { assets: extractBrandAssets() };
|
|
752
|
+
const componentSuggestions = extractComponentSuggestions();
|
|
753
|
+
const output = {
|
|
754
|
+
branch: getGitBranch(),
|
|
755
|
+
engineer: getGitEngineer(),
|
|
756
|
+
scannedAt: new Date().toISOString(),
|
|
757
|
+
totalComponents: results.length,
|
|
758
|
+
components: results,
|
|
759
|
+
foundations,
|
|
760
|
+
componentSuggestions,
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const prevComponents = readPreviousOutput();
|
|
764
|
+
const { added, removed, modified } = compareComponents(prevComponents, results);
|
|
765
|
+
const hasChanges = added.length > 0 || removed.length > 0 || modified.length > 0;
|
|
766
|
+
|
|
767
|
+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(output, null, 2), "utf-8");
|
|
768
|
+
const publicDir = path.join(PROJECT_ROOT, "public");
|
|
769
|
+
if (!fs.existsSync(publicDir)) fs.mkdirSync(publicDir, { recursive: true });
|
|
770
|
+
fs.writeFileSync(PUBLIC_MANIFEST, JSON.stringify(output, null, 2), "utf-8");
|
|
771
|
+
|
|
772
|
+
if (hasChanges) {
|
|
773
|
+
const lastVersion = readLastVersion();
|
|
774
|
+
const nextVersion = incPatch(lastVersion);
|
|
775
|
+
const changes = [...added, ...removed, ...modified];
|
|
776
|
+
const historyEntry = {
|
|
777
|
+
version: nextVersion,
|
|
778
|
+
timestamp: new Date().toISOString(),
|
|
779
|
+
branch: getGitBranch(),
|
|
780
|
+
engineer: getGitEngineer(),
|
|
781
|
+
changes,
|
|
782
|
+
};
|
|
783
|
+
const entries = readHistoryEntries();
|
|
784
|
+
entries.push(historyEntry);
|
|
785
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(entries, null, 2), "utf-8");
|
|
786
|
+
console.log(
|
|
787
|
+
`[VDS] v${nextVersion}: ${added.length} added, ${removed.length} removed, ${modified.length} modified`
|
|
788
|
+
);
|
|
789
|
+
console.log(cliT("scanComplete", results.length));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function loadVdsOutputFromBranch(branch) {
|
|
794
|
+
try {
|
|
795
|
+
const raw = execSync(`git show ${branch}:vds-output.json`, { cwd: PROJECT_ROOT, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
796
|
+
return JSON.parse(raw);
|
|
797
|
+
} catch (_) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function componentSignature(c) {
|
|
803
|
+
return JSON.stringify({ name: c.name, group: c.group, category: c.category, tokens: (c.tokens || []).slice().sort() });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function runCompare() {
|
|
807
|
+
const argv = process.argv.slice(2);
|
|
808
|
+
const idx = argv.indexOf("--compare");
|
|
809
|
+
const branch1 = idx >= 0 ? argv[idx + 1] : null;
|
|
810
|
+
const branch2 = idx >= 0 ? argv[idx + 2] : null;
|
|
811
|
+
if (!branch1 || !branch2) {
|
|
812
|
+
console.error("Kullanım: node vds-core/scan.mjs --compare <branch1> <branch2>");
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
const data1 = loadVdsOutputFromBranch(branch1);
|
|
816
|
+
const data2 = loadVdsOutputFromBranch(branch2);
|
|
817
|
+
if (!data1 || !data1.components) {
|
|
818
|
+
console.error(`vds-output.json bulunamadı veya geçersiz: ${branch1}. Önce o branch'te 'npm run vds' çalıştırın.`);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
if (!data2 || !data2.components) {
|
|
822
|
+
console.error(`vds-output.json bulunamadı veya geçersiz: ${branch2}. Önce o branch'te 'npm run vds' çalıştırın.`);
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
const byFile1 = new Map(data1.components.map((c) => [c.file, c]));
|
|
826
|
+
const byFile2 = new Map(data2.components.map((c) => [c.file, c]));
|
|
827
|
+
const files1 = new Set(byFile1.keys());
|
|
828
|
+
const files2 = new Set(byFile2.keys());
|
|
829
|
+
const commonFiles = [...files1].filter((f) => files2.has(f));
|
|
830
|
+
const only1 = [...files1].filter((f) => !files2.has(f));
|
|
831
|
+
const only2 = [...files2].filter((f) => !files1.has(f));
|
|
832
|
+
const different = commonFiles.filter((f) => componentSignature(byFile1.get(f)) !== componentSignature(byFile2.get(f)));
|
|
833
|
+
const commonCount = commonFiles.length - different.length;
|
|
834
|
+
|
|
835
|
+
const label1 = branch1.length > 20 ? branch1.slice(0, 17) + "…" : branch1;
|
|
836
|
+
const label2 = branch2.length > 20 ? branch2.slice(0, 17) + "…" : branch2;
|
|
837
|
+
console.log(`\n📊 Branch karşılaştırma: ${label1} vs ${label2}\n`);
|
|
838
|
+
console.log(`✅ Ortak: ${commonCount} component`);
|
|
839
|
+
if (different.length > 0) {
|
|
840
|
+
different.forEach((file) => {
|
|
841
|
+
const c = byFile1.get(file);
|
|
842
|
+
console.log(`🟡 Farklılaşan: ${c.name} (token veya meta farkı)`);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
if (only1.length > 0) {
|
|
846
|
+
only1.forEach((file) => {
|
|
847
|
+
const c = byFile1.get(file);
|
|
848
|
+
const shortBranch = branch1.length > 15 ? branch1.replace(/^[^/]+\//, "").slice(0, 12) : branch1;
|
|
849
|
+
console.log(`🔴 Sadece ${shortBranch}'ta: ${c.name}`);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (only2.length > 0) {
|
|
853
|
+
only2.forEach((file) => {
|
|
854
|
+
const c = byFile2.get(file);
|
|
855
|
+
const shortBranch = branch2.length > 15 ? branch2.replace(/^[^/]+\//, "").slice(0, 12) : branch2;
|
|
856
|
+
console.log(`🔴 Sadece ${shortBranch}'ta: ${c.name}`);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
console.log("");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const isWatch = process.argv.includes("--watch");
|
|
863
|
+
const isCompare = process.argv.includes("--compare");
|
|
864
|
+
|
|
865
|
+
if (isCompare) {
|
|
866
|
+
runCompare();
|
|
867
|
+
} else if (isWatch) {
|
|
868
|
+
if (!fs.existsSync(COMPONENTS_DIR)) {
|
|
869
|
+
console.error(CLI_LOCALES[CLI_LOCALE].componentsNotFound);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
let debounceTimer = null;
|
|
873
|
+
let lastChangedFiles = new Set();
|
|
874
|
+
|
|
875
|
+
function runScan() {
|
|
876
|
+
const files = [...lastChangedFiles];
|
|
877
|
+
lastChangedFiles.clear();
|
|
878
|
+
if (files.length) {
|
|
879
|
+
const rel = path.relative(PROJECT_ROOT, files[0]);
|
|
880
|
+
if (files.length === 1) {
|
|
881
|
+
console.log(`[VDS] ${rel}`);
|
|
882
|
+
} else {
|
|
883
|
+
console.log(`[VDS] ${files.length} files changed`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
scan();
|
|
887
|
+
console.log("[VDS] Watch: src/components (.tsx, .jsx). Scan complete.");
|
|
888
|
+
const url = vdsDashboardUrl();
|
|
889
|
+
console.log("[VDS] Dashboard: " + formatClickableLink(url) + "\n");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function scheduleScan(filename) {
|
|
893
|
+
if (!filename || !/\.(tsx|jsx)$/i.test(filename)) return;
|
|
894
|
+
lastChangedFiles.add(path.join(COMPONENTS_DIR, filename));
|
|
895
|
+
clearTimeout(debounceTimer);
|
|
896
|
+
debounceTimer = setTimeout(runScan, 300);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
console.log("[VDS] Watching src/components for .tsx / .jsx changes…\n");
|
|
900
|
+
runScan();
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
const watcher = fs.watch(COMPONENTS_DIR, { recursive: true }, (eventType, filename) => {
|
|
904
|
+
if (filename) scheduleScan(filename);
|
|
905
|
+
});
|
|
906
|
+
process.on("SIGINT", () => {
|
|
907
|
+
watcher.close();
|
|
908
|
+
process.exit(0);
|
|
909
|
+
});
|
|
910
|
+
} catch (err) {
|
|
911
|
+
console.error("[VDS] Watch failed:", err.message);
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
scan();
|
|
916
|
+
}
|