vibe-design-system 2.5.22 → 2.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ const cliT = (key, n) => CLI_LOCALES[CLI_LOCALE][key].replace("{n}", String(n));
|
|
|
29
29
|
const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
30
30
|
const COMPONENTS_DIR = path.join(PROJECT_ROOT, "src", "components");
|
|
31
31
|
const PAGES_DIR = path.join(PROJECT_ROOT, "src", "pages");
|
|
32
|
+
const APP_DIR = path.join(PROJECT_ROOT, "src", "app");
|
|
32
33
|
|
|
33
34
|
const IGNORE_DIRS = ["node_modules", "dist", ".next", "build", "vds-generated", ".storybook"];
|
|
34
35
|
const OUTPUT_FILE = path.join(PROJECT_ROOT, "vds-output.json");
|
|
@@ -436,6 +437,47 @@ function suggestNameFromPattern(pattern) {
|
|
|
436
437
|
return "Container";
|
|
437
438
|
}
|
|
438
439
|
|
|
440
|
+
/** Count direct child elements in JSX inner content (depth-1 opening tags). */
|
|
441
|
+
function countDirectChildren(innerContent) {
|
|
442
|
+
if (!innerContent || typeof innerContent !== "string") return 0;
|
|
443
|
+
let depth = 0;
|
|
444
|
+
let count = 0;
|
|
445
|
+
const s = innerContent;
|
|
446
|
+
let i = 0;
|
|
447
|
+
while (i < s.length) {
|
|
448
|
+
if (s.slice(i, i + 2) === "</") {
|
|
449
|
+
i += 2;
|
|
450
|
+
depth -= 1;
|
|
451
|
+
while (i < s.length && /[\w-]/.test(s[i])) i += 1;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (s[i] === "<" && /[\w]/.test(s[i + 1])) {
|
|
455
|
+
depth += 1;
|
|
456
|
+
if (depth === 1) count += 1;
|
|
457
|
+
i += 1;
|
|
458
|
+
while (i < s.length && /[\w-]/.test(s[i])) i += 1;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (s[i] === "{" || s[i] === '"' || s[i] === "'" || s[i] === "`") {
|
|
462
|
+
const open = s[i];
|
|
463
|
+
const close = open === "{" ? "}" : open;
|
|
464
|
+
i += 1;
|
|
465
|
+
let depthBrace = open === "{" ? 1 : 0;
|
|
466
|
+
while (i < s.length) {
|
|
467
|
+
if (depthBrace === 0 && (s[i] === close || (open === "{" && s[i] === "}"))) break;
|
|
468
|
+
if (s[i] === open) depthBrace += 1;
|
|
469
|
+
else if (s[i] === close) depthBrace -= 1;
|
|
470
|
+
else if ((open === '"' || open === "'") && s[i] === "\\") i += 1;
|
|
471
|
+
i += 1;
|
|
472
|
+
}
|
|
473
|
+
if (s[i] === close) i += 1;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
i += 1;
|
|
477
|
+
}
|
|
478
|
+
return count;
|
|
479
|
+
}
|
|
480
|
+
|
|
439
481
|
/** Extract full JSX element (opening tag to matching closing tag) and inner text. */
|
|
440
482
|
function extractFullElement(content, classNameMatchIndex, tagName) {
|
|
441
483
|
const tag = tagName;
|
|
@@ -475,17 +517,51 @@ function extractFullElement(content, classNameMatchIndex, tagName) {
|
|
|
475
517
|
return null;
|
|
476
518
|
}
|
|
477
519
|
|
|
478
|
-
/**
|
|
520
|
+
/** Return set of component names already in src/components (so we don't suggest them again). */
|
|
521
|
+
function getExistingComponentNames() {
|
|
522
|
+
const names = new Set();
|
|
523
|
+
if (!fs.existsSync(COMPONENTS_DIR)) return names;
|
|
524
|
+
const walk = (dir) => {
|
|
525
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
526
|
+
for (const e of entries) {
|
|
527
|
+
const full = path.join(dir, e.name);
|
|
528
|
+
if (e.isDirectory() && !IGNORE_DIRS.includes(e.name)) walk(full);
|
|
529
|
+
else if (e.isFile() && /\.(tsx|jsx)$/i.test(e.name)) {
|
|
530
|
+
const base = e.name.replace(/\.(tsx|jsx)$/i, "");
|
|
531
|
+
if (/^[A-Z]/.test(base)) names.add(base);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
walk(COMPONENTS_DIR);
|
|
536
|
+
return names;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Scan src/pages and src/app for repeated visual-section patterns (className + 2+ children); return component suggestions. */
|
|
479
540
|
function extractComponentSuggestions() {
|
|
480
|
-
|
|
481
|
-
|
|
541
|
+
const pageDirs = [
|
|
542
|
+
{ dir: PAGES_DIR, prefix: "src/pages/" },
|
|
543
|
+
{ dir: APP_DIR, prefix: "src/app/" },
|
|
544
|
+
];
|
|
545
|
+
const allPageFiles = [];
|
|
546
|
+
for (const { dir, prefix } of pageDirs) {
|
|
547
|
+
if (!fs.existsSync(dir)) continue;
|
|
548
|
+
const files = getAllComponentFiles(dir);
|
|
549
|
+
for (const rel of files) {
|
|
550
|
+
allPageFiles.push({ fullPath: path.join(dir, rel), srcRel: prefix + rel.replace(/\\/g, "/") });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (allPageFiles.length === 0) return [];
|
|
554
|
+
|
|
482
555
|
const byPattern = new Map();
|
|
483
556
|
const re = /className\s*=\s*["']([^"']+)["']|className\s*=\s*\{\s*["']([^"']+)["']/g;
|
|
484
557
|
|
|
485
|
-
for (const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
558
|
+
for (const { fullPath, srcRel } of allPageFiles) {
|
|
559
|
+
let content;
|
|
560
|
+
try {
|
|
561
|
+
content = fs.readFileSync(fullPath, "utf-8");
|
|
562
|
+
} catch (_) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
489
565
|
let m;
|
|
490
566
|
re.lastIndex = 0;
|
|
491
567
|
while ((m = re.exec(content)) !== null) {
|
|
@@ -500,11 +576,14 @@ function extractComponentSuggestions() {
|
|
|
500
576
|
const tagName = tagMatch ? tagMatch[1] : "div";
|
|
501
577
|
let fullJsx = null;
|
|
502
578
|
let innerText = "";
|
|
579
|
+
let innerContent = "";
|
|
503
580
|
const extracted = extractFullElement(content, m.index, tagName);
|
|
504
581
|
if (extracted) {
|
|
505
582
|
fullJsx = extracted.fullJsx;
|
|
506
583
|
innerText = extracted.innerText || "";
|
|
584
|
+
innerContent = extracted.innerContent || "";
|
|
507
585
|
}
|
|
586
|
+
const directChildren = countDirectChildren(innerContent);
|
|
508
587
|
byPattern.set(raw, {
|
|
509
588
|
count: 0,
|
|
510
589
|
files: new Set(),
|
|
@@ -512,6 +591,7 @@ function extractComponentSuggestions() {
|
|
|
512
591
|
snippet: `<${tagName} className="${raw}">...</${tagName}>`,
|
|
513
592
|
fullJsx,
|
|
514
593
|
innerText,
|
|
594
|
+
directChildren,
|
|
515
595
|
});
|
|
516
596
|
}
|
|
517
597
|
const entry = byPattern.get(raw);
|
|
@@ -520,19 +600,23 @@ function extractComponentSuggestions() {
|
|
|
520
600
|
}
|
|
521
601
|
}
|
|
522
602
|
|
|
603
|
+
const existingNames = getExistingComponentNames();
|
|
523
604
|
const suggestions = [];
|
|
524
|
-
for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText }] of byPattern.entries()) {
|
|
605
|
+
for (const [pattern, { count, files, snippet, tagName, fullJsx, innerText, directChildren }] of byPattern.entries()) {
|
|
525
606
|
if (count < 2) continue;
|
|
526
607
|
const suggestedName = suggestNameFromContent(innerText) || suggestNameFromPattern(pattern);
|
|
608
|
+
const name = sanitizeComponentName(suggestedName.replace(/\s+/g, ""));
|
|
609
|
+
if (existingNames.has(name)) continue;
|
|
610
|
+
if (directChildren < 2) continue;
|
|
527
611
|
suggestions.push({
|
|
528
|
-
suggestedName:
|
|
612
|
+
suggestedName: name,
|
|
529
613
|
tagName: tagName || "div",
|
|
530
614
|
pattern,
|
|
531
615
|
fullJsx,
|
|
532
616
|
occurrences: count,
|
|
533
617
|
foundIn: [...files].sort(),
|
|
534
618
|
snippet,
|
|
535
|
-
reason: `
|
|
619
|
+
reason: `Visual section candidate: same className cluster ${count}×, ${directChildren} child elements`,
|
|
536
620
|
});
|
|
537
621
|
}
|
|
538
622
|
return suggestions;
|
|
@@ -478,22 +478,36 @@ const REACTNODE_PLACEHOLDER_TEXT = {
|
|
|
478
478
|
children: "Example content",
|
|
479
479
|
};
|
|
480
480
|
|
|
481
|
-
/** Recursive list of .tsx file paths under dir (relative to dir). */
|
|
482
|
-
function
|
|
481
|
+
/** Recursive list of .tsx/.jsx file paths under dir (relative to dir). Index.tsx / index.tsx first for deterministic "first usage". */
|
|
482
|
+
function getAllTsxJsxUnderDir(dir) {
|
|
483
483
|
if (!fs.existsSync(dir)) return [];
|
|
484
484
|
const out = [];
|
|
485
485
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
486
486
|
for (const e of entries) {
|
|
487
487
|
const full = path.join(dir, e.name);
|
|
488
488
|
if (e.isDirectory()) {
|
|
489
|
-
out.push(...
|
|
490
|
-
} else if (e.isFile() && e.name.endsWith(".tsx")) {
|
|
489
|
+
out.push(...getAllTsxJsxUnderDir(full).map((r) => path.join(e.name, r)));
|
|
490
|
+
} else if (e.isFile() && (e.name.endsWith(".tsx") || e.name.endsWith(".jsx"))) {
|
|
491
491
|
out.push(e.name);
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
|
+
out.sort((a, b) => {
|
|
495
|
+
const baseA = path.basename(a, path.extname(a));
|
|
496
|
+
const baseB = path.basename(b, path.extname(b));
|
|
497
|
+
const isIndexA = baseA === "Index" || baseA === "index";
|
|
498
|
+
const isIndexB = baseB === "Index" || baseB === "index";
|
|
499
|
+
if (isIndexA && !isIndexB) return -1;
|
|
500
|
+
if (!isIndexA && isIndexB) return 1;
|
|
501
|
+
return a.localeCompare(b);
|
|
502
|
+
});
|
|
494
503
|
return out;
|
|
495
504
|
}
|
|
496
505
|
|
|
506
|
+
/** @deprecated Use getAllTsxJsxUnderDir. Kept for compatibility. */
|
|
507
|
+
function getAllTsxUnderDir(dir) {
|
|
508
|
+
return getAllTsxJsxUnderDir(dir);
|
|
509
|
+
}
|
|
510
|
+
|
|
497
511
|
/** Extract prop="value" and prop='value' and prop={"value"} from JSX tag content. Returns { propName: "value", ... }. */
|
|
498
512
|
function extractPropsFromJsxTagContent(tagContent) {
|
|
499
513
|
const props = {};
|
|
@@ -546,7 +560,7 @@ function buildMockArrayItems(componentSource, itemTypeName, propName) {
|
|
|
546
560
|
return items;
|
|
547
561
|
}
|
|
548
562
|
|
|
549
|
-
/** Find first real usage of component in src/pages or src/app; return { propName: "literalValue", ... } or null. */
|
|
563
|
+
/** Find first real usage of component in src/pages or src/app (Index.tsx preferred); return { propName: "literalValue", ... } or null. */
|
|
550
564
|
function findComponentUsageInPages(componentName, projectRoot) {
|
|
551
565
|
const srcDir = path.join(projectRoot, "src");
|
|
552
566
|
const pagesDir = path.join(srcDir, "pages");
|
|
@@ -557,7 +571,7 @@ function findComponentUsageInPages(componentName, projectRoot) {
|
|
|
557
571
|
const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
558
572
|
const tagOpenRe = new RegExp(`<${escapedName}\\s+([\\s\\S]*?)\\s*\\/?>`, "m");
|
|
559
573
|
for (const dir of dirs) {
|
|
560
|
-
const files =
|
|
574
|
+
const files = getAllTsxJsxUnderDir(dir);
|
|
561
575
|
for (const rel of files) {
|
|
562
576
|
const fullPath = path.join(dir, rel);
|
|
563
577
|
try {
|
|
@@ -596,7 +610,9 @@ function buildDefaultArgsForRequiredProps(props, usageFromPages = null, componen
|
|
|
596
610
|
const name = p.name;
|
|
597
611
|
const required = p.required === true;
|
|
598
612
|
if (/ReactNode|React\.ReactNode/.test(type)) {
|
|
599
|
-
const text =
|
|
613
|
+
const text = fromPages[name] != null && typeof fromPages[name] === "string"
|
|
614
|
+
? fromPages[name]
|
|
615
|
+
: (REACTNODE_PLACEHOLDER_TEXT[name] || componentPlaceholders[name] || "Content");
|
|
600
616
|
argLines.push(` ${name}: ${JSON.stringify(text)},`);
|
|
601
617
|
added.add(name);
|
|
602
618
|
continue;
|
|
@@ -1167,8 +1183,8 @@ function writeComponentSuggestionsStory(componentSuggestions) {
|
|
|
1167
1183
|
"export const Default: Story = {",
|
|
1168
1184
|
" render: () => (",
|
|
1169
1185
|
" <div style={{ padding: 24, fontFamily: \"system-ui, sans-serif\" }}>",
|
|
1170
|
-
" <h2 style={{ marginBottom: 16 }}>
|
|
1171
|
-
" <p style={{ color: \"#888\", marginBottom: 24 }}>
|
|
1186
|
+
" <h2 style={{ marginBottom: 16 }}>Visual section candidates (component suggestions)</h2>",
|
|
1187
|
+
" <p style={{ color: \"#888\", marginBottom: 24 }}>From src/pages and src/app: own className, 2+ child elements, pattern repeated 2+ times. Not yet in src/components. Consider extracting as components.</p>",
|
|
1172
1188
|
" <div style={{ display: \"flex\", flexDirection: \"column\", gap: 24 }}>",
|
|
1173
1189
|
" {suggestions.map((s, i) => (",
|
|
1174
1190
|
" <div key={i} style={{ border: \"1px solid #333\", borderRadius: 8, padding: 16, background: \"#111\" }}>",
|