Files
WRNexusJS/scripts/audit-design-system.mjs

87 lines
4.8 KiB
JavaScript

import { readFileSync, readdirSync, mkdirSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import console from "node:console";
import { COMPONENT_CONTRACTS } from "../packages/design-system/src/contracts.ts";
// Source inventory only: these observations do not certify rendered behavior.
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const read = (path) => readFileSync(resolve(root, path), "utf8");
const names = (source, expression) =>
[...new Set([...source.matchAll(expression)].map((m) => m[1]))].sort();
const reference = JSON.parse(read("packages/ui/component-reference.json"));
const catalog = JSON.parse(read("packages/ui/component-catalog.json"));
const components = readdirSync(resolve(root, "packages/ui/components"))
.filter((f) => f.endsWith(".wrn"))
.sort()
.map((file) => {
const path = `packages/ui/components/${file}`;
const source = read(path);
const name = /^\s*component\s+(\w+)/m.exec(source)?.[1];
if (!name) throw new Error(`Missing component declaration: ${path}`);
const entry = reference.components.find((c) => c.name === name);
return {
name,
source: path,
sha256: createHash("sha256").update(source).digest("hex"),
category: catalog.components.find((c) => c.name === name)?.category ?? "uncatalogued",
inReference: Boolean(entry),
inCatalog: catalog.components.some((c) => c.name === name),
props: entry?.props?.map((p) => p.name) ?? [],
tokenReferences: names(source, /var\(\s*--wrn-([\w-]+)/g),
dataAttributes: names(source, /\b(data-[\w-]+)\s*=/g),
slots: names(source, /<slot\b[^>]*\bname="([^"]+)"/g),
hasDefaultSlot: /<slot(?:\s|>)/.test(source),
hasLocalStyle: /\bstyle\s*\{/.test(source),
hasContractPartHooks: /\bdata-wrn-part\s*=/.test(source),
literalGeometryDeclarations: (
source.match(
/\b(?:border-radius|padding(?:-inline|-block)?|box-shadow|transform)\s*:\s*(?!var\()[^;{}]+;/g,
) ?? []
).length,
status:
COMPONENT_CONTRACTS[name]?.source === path ? "recipe-supported" : "legacy-uncontracted",
contractVersion: COMPONENT_CONTRACTS[name]?.contractVersion ?? null,
regions: COMPONENT_CONTRACTS[name]?.regions ?? {},
};
});
const actual = new Set(components.map((c) => c.name));
const report = {
schemaVersion: 1,
methodology:
"Static observations from current working tree, including uncommitted files. Props copied from generated reference; not a parser or visual/behavior certification. Literal declaration count is a heuristic and includes legitimate structural values.",
componentCount: components.length,
referenceCount: reference.components.length,
catalogCount: catalog.components.length,
referenceOnly: reference.components.filter((c) => !actual.has(c.name)).map((c) => c.name),
catalogOnly: catalog.components.filter((c) => !actual.has(c.name)).map((c) => c.name),
components,
};
const directory = resolve(root, "docs/design-system");
mkdirSync(directory, { recursive: true });
writeFileSync(
resolve(directory, "component-coverage.json"),
`${JSON.stringify(report, null, 2)}\n`,
);
const rows = components.map(
(c) =>
`| ${c.name} | ${c.category} | ${c.tokenReferences.length} | ${c.hasLocalStyle ? "yes" : "no"} | ${c.hasContractPartHooks ? "yes" : "no"} | ${c.inReference && c.inCatalog ? "matched" : "gap"} | ${c.status} |`,
);
writeFileSync(
resolve(directory, "component-coverage.md"),
`# Design-system source coverage\n\nRegenerate with \`node scripts/audit-design-system.mjs\`. This inventories the working tree, including uncommitted components. It does not certify visual, accessibility or runtime behavior.\n\nSources: **${components.length}**; reference entries: **${report.referenceCount}**; catalog entries: **${report.catalogCount}**. Versioned contracts cover all 104 components. Provider, native and content-owned regions retain explicit styling limits in the JSON report. Status reports hook/recipe support, not accessibility certification. Token counts are source-local references, not inherited/global coverage. Part hooks refer specifically to the versioned \`data-wrn-part\` convention.\n\n| Component | Category | Tokens | Local style | Part hooks | Metadata | Status |\n| --- | --- | ---: | --- | --- | --- | --- |\n${rows.join("\n")}\n`,
);
console.log(
JSON.stringify({
components: report.componentCount,
reference: report.referenceCount,
catalog: report.catalogCount,
metadataGaps: components.filter((c) => !c.inReference || !c.inCatalog).map((c) => c.name),
localStyles: components.filter((c) => c.hasLocalStyle).length,
partHooks: components.filter((c) => c.hasContractPartHooks).length,
referenceOnly: report.referenceOnly,
catalogOnly: report.catalogOnly,
}),
);