chore: add pnpm workspace configuration with package definitions and overrides

This commit is contained in:
snoozescript
2026-08-14 23:35:45 +05:30
parent ae2cb896a6
commit bcb302ac7d
389 changed files with 65206 additions and 0 deletions
+524
View File
@@ -0,0 +1,524 @@
#!/usr/bin/env node
/**
* find-duplicates — component duplication scanner for the web-app source.
*
* Parses every TSX file under `src` with the TypeScript compiler API and
* computes four things:
*
* 1. Layout idiom census — exact VStack/HStack/Card/Section/Grid prop
* configurations repeated across the codebase (e.g. `VStack(gap=0)`).
* 2. Structural fingerprints — normalized JSX subtree shapes (text and
* non-layout prop values stripped) that recur in ≥ N places. These are
* the raw material for extracting shared components.
* 3. Known idioms — hand-tuned recognizers for patterns that keep showing
* up by hand: label+description pairs, stat cards, section headers,
* icon+text rows, form action footers, wizard progress steps.
* 4. File similarity — pairwise Jaccard over each file's structural
* fingerprints, surfacing pages that are near-copies of each other.
*
* Usage:
* node scripts/find-duplicates.mjs # run from apps/web-app
* node scripts/find-duplicates.mjs --root src --min-count 3
* node scripts/find-duplicates.mjs --json > report.json
* node scripts/find-duplicates.mjs --limit 8 # rows per section
*
* Zero runtime dependencies: only `typescript` (already a devDependency).
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative, resolve } from "node:path";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const ts = require("typescript");
// ---------------------------------------------------------------------------
// CLI args
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
const flag = (name, fallback) => {
const i = args.indexOf(name);
return i === -1 ? fallback : args[i + 1];
};
const has = (name) => args.includes(name);
const ROOT = flag("--root", "src");
const MIN_COUNT = Number(flag("--min-count", "3"));
const LIMIT = Number(flag("--limit", "10"));
const AS_JSON = has("--json");
const LAYOUT_TAGS = new Set([
"VStack",
"HStack",
"StackItem",
"Section",
"Card",
"Grid",
"Center",
"Layout",
"LayoutHeader",
"LayoutContent",
"LayoutFooter",
"LayoutPanel",
]);
// ---------------------------------------------------------------------------
// Source collection
// ---------------------------------------------------------------------------
const SRC_DIR = resolve(process.cwd(), ROOT);
if (!statSync(SRC_DIR, { throwIfNoParent: false })?.isDirectory?.()) {
console.error(`Cannot find source directory: ${SRC_DIR}`);
process.exit(1);
}
/** Collect all .tsx files under a directory, skipping build/scratch dirs. */
function collectTsx(dir, out = []) {
for (const entry of readdirSync(dir)) {
if (entry === "node_modules" || entry === "dist" || entry === ".astryx-tmp") continue;
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) collectTsx(full, out);
else if (full.endsWith(".tsx") || full.endsWith(".ts")) out.push(full);
}
return out;
}
const files = collectTsx(SRC_DIR).sort();
const parsed = [];
for (const file of files) {
const text = readFileSync(file, "utf8");
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
parsed.push({ file, text, sf });
}
// ---------------------------------------------------------------------------
// AST helpers
// ---------------------------------------------------------------------------
/** Normalized attributes of a JSX element: [{key, value}] with literals resolved. */
function attrsOf(node) {
const out = [];
for (const prop of node.attributes.properties) {
if (!ts.isJsxAttribute(prop)) {
out.push({ key: "...", value: "..." }); // spread
continue;
}
const key = prop.name.getText();
const init = prop.initializer;
if (!init) {
out.push({ key, value: "true" }); // boolean shorthand
} else if (ts.isStringLiteral(init)) {
out.push({ key, value: init.text });
} else if (ts.isJsxExpression(init)) {
const e = init.expression;
if (!e) out.push({ key, value: "expr" });
else if (ts.isStringLiteral(e) || ts.isNumericLiteral(e)) out.push({ key, value: e.text });
else if (e.kind === ts.SyntaxKind.TrueKeyword) out.push({ key, value: "true" });
else if (e.kind === ts.SyntaxKind.FalseKeyword) out.push({ key, value: "false" });
else out.push({ key, value: "expr" });
} else {
out.push({ key, value: "?" });
}
}
return out;
}
/**
* Flatten a TSX subtree into element nodes:
* { tag, attrs, elemChildren, textCount, line, parent }
*/
function flattenJsx(node, sf, parent = null, out = []) {
if (ts.isJsxElement(node)) {
const el = {
tag: node.openingElement.tagName.getText(),
attrs: attrsOf(node.openingElement),
elemChildren: [],
textCount: 0,
line: node.getStart(sf).then ? 0 : sf.getLineAndCharacterOfPosition(node.getStart()).line + 1,
parent,
};
for (const child of node.children) {
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
el.elemChildren.push(flattenJsx(child, sf, el, out));
} else if (ts.isJsxText(child) && child.getText().trim().length > 0) {
el.textCount += 1;
} else if (ts.isJsxExpression(child)) {
el.textCount += 1; // {expression} counts as dynamic text
}
}
out.push(el);
return el;
}
if (ts.isJsxSelfClosingElement(node)) {
const el = {
tag: node.tagName.getText(),
attrs: attrsOf(node),
elemChildren: [],
textCount: 0,
line: sf.getLineAndCharacterOfPosition(node.getStart()).line + 1,
parent,
};
out.push(el);
return el;
}
return null;
}
for (const p of parsed) {
p.elements = [];
const visit = (node) => {
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
flattenJsx(node, p.sf, null, p.elements);
ts.forEachChild(node, visit);
};
visit(p.sf);
}
// ---------------------------------------------------------------------------
// 1. Layout idiom census — exact prop configs for layout primitives
// ---------------------------------------------------------------------------
const layoutCounts = new Map(); // sig -> {count, files:Set, examples:[{file,line}]}
for (const p of parsed) {
for (const el of p.elements) {
if (!LAYOUT_TAGS.has(el.tag)) continue;
const props = el.attrs
.filter((a) => a.key !== "...")
.map((a) => `${a.key}=${a.value}`)
.sort()
.join(",");
const sig = `${el.tag}(${props})`;
const entry = layoutCounts.get(sig) ?? { count: 0, files: new Set(), examples: [] };
entry.count += 1;
entry.files.add(p.file);
if (entry.examples.length < 3) entry.examples.push({ file: p.file, line: el.line });
layoutCounts.set(sig, entry);
}
}
// ---------------------------------------------------------------------------
// 2. Structural fingerprints — shape only, text/prop values stripped
// ---------------------------------------------------------------------------
const MAX_DEPTH = 4;
function shapeSig(el, depth) {
const attrs = el.attrs
.map((a) => `${a.key}:${typeof a.value === "string" ? "s" : a.value}`)
.sort()
.join(",");
const kids = depth > 0 ? el.elemChildren.map((c) => shapeSig(c, depth - 1)).join(";") : "";
const text = el.textCount > 0 ? `#t${el.textCount}` : "";
return `${el.tag}(${attrs})[${kids}${text}]`;
}
const shapeCounts = new Map(); // sig -> {count, files:Set, examples:[{file,line}]}
for (const p of parsed) {
for (const el of p.elements) {
const sig = shapeSig(el, MAX_DEPTH);
const entry = shapeCounts.get(sig) ?? { count: 0, files: new Set(), examples: [] };
entry.count += 1;
entry.files.add(p.file);
if (entry.examples.length < 3) entry.examples.push({ file: p.file, line: el.line });
shapeCounts.set(sig, entry);
}
}
// ---------------------------------------------------------------------------
// 3. Known idioms
// ---------------------------------------------------------------------------
const getAttr = (el, key) => el.attrs.find((a) => a.key === key)?.value;
/** Text element with supporting typography + secondary color (the "subtitle"). */
const isSupporting = (el) =>
el.tag === "Text" &&
(getAttr(el, "type") === "supporting" || getAttr(el, "type") === undefined) &&
(getAttr(el, "color") === "secondary" || getAttr(el, "color") === undefined);
function detectIdioms() {
const results = {
labelPair: [], // VStack gap=0: [Text(label), Text(supporting/secondary)]
sectionHeader: [], // VStack gap=0: [Heading, Text(supporting/secondary)]
statCard: [], // Card padding>=3: VStack gap=1: [supporting, Heading, supporting]
iconTextRow: [], // HStack gap 1-3 vAlign=center: [Icon, VStack]
formActions: [], // HStack gap=2: [Button, Button, ...]
wizardProgress: [], // HStack wrap=wrap: several [Badge, Text] HStacks
};
const push = (key, p, el, label) =>
results[key].push({ file: p.file, line: el.line, detail: label });
for (const p of parsed) {
for (const el of p.elements) {
const kids = el.elemChildren;
// labelPair / sectionHeader
if (el.tag === "VStack" && getAttr(el, "gap") === "0" && kids.length === 2) {
const [a, b] = kids;
if (isSupporting(b) && a.tag === "Text") push("labelPair", p, el, "label+description");
if (isSupporting(b) && a.tag === "Heading")
push("sectionHeader", p, el, "heading+description");
}
// statCard
if (el.tag === "Card" && kids.length === 1) {
const inner = kids[0];
const pad = Number(getAttr(el, "padding") ?? "0");
if (inner.tag === "VStack" && getAttr(inner, "gap") === "1" && pad >= 3) {
const [t1, h, t2] = inner.elemChildren;
if (
t1?.tag === "Text" &&
isSupporting(t1) &&
h?.tag === "Heading" &&
t2?.tag === "Text" &&
isSupporting(t2)
) {
push("statCard", p, el, `Card padding=${pad} + VStack gap=1`);
}
}
}
// iconTextRow
if (el.tag === "HStack" && getAttr(el, "vAlign") === "center") {
const gap = Number(getAttr(el, "gap") ?? "0");
if (gap >= 1 && gap <= 3 && kids[0]?.tag === "Icon" && kids[1]?.tag === "VStack") {
push("iconTextRow", p, el, `Icon + VStack, gap=${gap}`);
}
}
// formActions
if (
el.tag === "HStack" &&
getAttr(el, "gap") === "2" &&
kids.length >= 2 &&
kids.every((k) => k.tag === "Button")
) {
push("formActions", p, el, `${kids.length} buttons`);
}
// wizardProgress
if (el.tag === "HStack" && getAttr(el, "wrap") === "wrap" && kids.length >= 2) {
const stepy = kids.every(
(k) =>
k.tag === "HStack" &&
k.elemChildren.some((c) => c.tag === "Badge") &&
k.elemChildren.some((c) => c.tag === "Text"),
);
if (stepy) push("wizardProgress", p, el, `${kids.length} steps`);
}
}
}
return results;
}
const idioms = detectIdioms();
// ---------------------------------------------------------------------------
// 4. File similarity — Jaccard over shape fingerprints (depth 2..3)
// ---------------------------------------------------------------------------
function fileSigSet(p) {
const set = new Set();
for (const el of p.elements) {
set.add(shapeSig(el, 3));
}
return set;
}
const sigSets = new Map(parsed.map((p) => [p.file, fileSigSet(p)]));
const pairs = [];
const fileList = [...sigSets.keys()];
for (let i = 0; i < fileList.length; i++) {
const a = sigSets.get(fileList[i]);
for (let j = i + 1; j < fileList.length; j++) {
const b = sigSets.get(fileList[j]);
if (a.size < 15 || b.size < 15) continue;
let inter = 0;
for (const s of a) if (b.has(s)) inter += 1;
const union = a.size + b.size - inter;
const jac = inter / union;
if (jac >= 0.3 && inter >= 8) {
pairs.push({
a: fileList[i],
b: fileList[j],
jaccard: jac,
shared: inter,
});
}
}
}
pairs.sort((x, y) => y.jaccard - x.jaccard || y.shared - x.shared);
// ---------------------------------------------------------------------------
// 5. Import cluster census
// ---------------------------------------------------------------------------
const importModuleCount = new Map(); // module -> files
const coImportCount = new Map(); // "mod1|mod2" -> files
for (const p of parsed) {
const mods = new Set();
const visit = (node) => {
if (
ts.isImportDeclaration(node) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
const mod = node.moduleSpecifier.text;
if (mod.startsWith("@astryxdesign/core")) mods.add(mod);
}
ts.forEachChild(node, visit);
};
visit(p.sf);
const list = [...mods].sort();
for (const m of list) {
const e = importModuleCount.get(m) ?? { count: 0, files: new Set() };
e.count += 1;
e.files.add(p.file);
importModuleCount.set(m, e);
}
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
const key = `${list[i]}|${list[j]}`;
const e = coImportCount.get(key) ?? { count: 0, files: new Set() };
e.count += 1;
e.files.add(p.file);
coImportCount.set(key, e);
}
}
}
// ---------------------------------------------------------------------------
// Reporting
// ---------------------------------------------------------------------------
const rel = (f) => relative(process.cwd(), f);
const fmtList = (s) => {
const arr = [...s].map(rel).sort();
if (arr.length <= 5) return arr.join(", ");
return `${arr.slice(0, 5).join(", ")} … (+${arr.length - 5} more)`;
};
const sections = [];
// --- layout census ---
const layoutRows = [...layoutCounts.entries()]
.filter(([, e]) => e.count >= MIN_COUNT && e.files.size >= 2)
.sort((a, b) => b[1].count - a[1].count);
sections.push({
title: `Layout idiom census — exact VStack/HStack/Card/Grid/Section configs (≥ ${MIN_COUNT} uses, ≥ 2 files)`,
rows: layoutRows.slice(0, LIMIT).map(([sig, e]) => ({
signature: sig,
count: e.count,
files: e.files.size,
filesList: fmtList(e.files),
example: `${rel(e.examples[0].file)}:${e.examples[0].line}`,
})),
total: layoutRows.length,
});
// --- shape fingerprints ---
const shapeRows = [...shapeCounts.entries()]
.filter(([, e]) => e.count >= MIN_COUNT && e.files.size >= 2)
.sort((a, b) => b[1].count - a[1].count);
sections.push({
title: `Repeated JSX structure fingerprints (≥ ${MIN_COUNT} occurrences, ≥ 2 files)`,
rows: shapeRows.slice(0, LIMIT).map(([sig, e]) => ({
signature: sig.length > 220 ? `${sig.slice(0, 220)}…` : sig,
count: e.count,
files: e.files.size,
filesList: fmtList(e.files),
example: `${rel(e.examples[0].file)}:${e.examples[0].line}`,
})),
total: shapeRows.length,
});
// --- idioms ---
const idiomNames = {
labelPair: "Label+description pair (VStack gap=0: Text + supporting Text)",
sectionHeader: "Section header (VStack gap=0: Heading + supporting Text)",
statCard: "Stat card (Card padding>=3 > VStack gap=1: 2 supporting Texts + Heading)",
iconTextRow: "Icon + text row (HStack vAlign=center: Icon + VStack)",
formActions: "Form action row (HStack gap=2: 2+ Buttons)",
wizardProgress: "Wizard progress (HStack wrap=wrap of Badge+Text steps)",
};
const idiomRows = Object.entries(idioms).map(([key, hits]) => {
const byFile = new Map();
for (const h of hits) {
if (!byFile.has(h.file)) byFile.set(h.file, h.line);
if (byFile.size >= 3) break;
}
return {
idiom: idiomNames[key],
key,
count: hits.length,
files: new Set(hits.map((h) => h.file)).size,
examples: [...byFile.entries()].map(([f, l]) => `${rel(f)}:${l}`),
};
});
sections.push({
title: "Known hand-rolled idioms — extraction candidates",
rows: idiomRows,
total: idiomRows.length,
});
// --- file similarity ---
sections.push({
title: "Most similar page pairs (Jaccard over structure fingerprints)",
rows: pairs.slice(0, LIMIT).map((p) => ({
pair: `${rel(p.a)} ↔ ${rel(p.b)}`,
jaccard: p.jaccard.toFixed(2),
shared: p.shared,
})),
total: pairs.length,
});
// --- import clusters ---
const coRows = [...coImportCount.entries()]
.filter(([, e]) => e.count >= 5)
.sort((a, b) => b[1].count - a[1].count);
sections.push({
title: "Most co-imported @astryxdesign/core modules (signals composite components to extract)",
rows: coRows.slice(0, LIMIT).map(([key, e]) => ({
modules: key.replace("@astryxdesign/core", ""),
count: e.count,
files: e.files.size,
})),
total: coRows.length,
});
// ---------------------------------------------------------------------------
// Output
// ---------------------------------------------------------------------------
const summary = {
filesScanned: files.length,
elementsParsed: parsed.reduce((n, p) => n + p.elements.length, 0),
vstackUses: [...layoutCounts.entries()]
.filter(([s]) => s.startsWith("VStack"))
.reduce((n, [, e]) => n + e.count, 0),
hstackUses: [...layoutCounts.entries()]
.filter(([s]) => s.startsWith("HStack"))
.reduce((n, [, e]) => n + e.count, 0),
};
if (AS_JSON) {
console.log(JSON.stringify({ summary, sections }, null, 2));
} else {
console.log(`\n=== Component duplication scanner ===`);
console.log(
`Scanned ${summary.filesScanned} files, ${summary.elementsParsed} JSX elements ` +
`(${summary.vstackUses} VStack, ${summary.hstackUses} HStack uses).\n`,
);
for (const s of sections) {
console.log(`\n## ${s.title} (${s.total} matches)\n`);
for (const r of s.rows) {
if (r.signature !== undefined) {
console.log(` ${r.count.toString().padStart(4)}× ${r.signature}`);
console.log(` in ${r.files} files · e.g. ${r.example}`);
} else if (r.idiom !== undefined) {
console.log(` ${r.count.toString().padStart(4)}× ${r.idiom}`);
console.log(` in ${r.files} files · e.g. ${r.examples.join(", ")}`);
} else if (r.pair !== undefined) {
console.log(` ${(r.jaccard * 100).toString().padStart(2)}% ${r.pair}`);
console.log(` ${r.shared} shared structure fingerprints`);
} else if (r.modules !== undefined) {
console.log(` ${r.count.toString().padStart(4)}× ${r.modules}`);
console.log(` in ${r.files} files`);
}
}
}
console.log("\n");
}