feat: release design system engine, studio and component contracts

This commit is contained in:
Clintchiz committed 2026-10-19 13:09:03 +05:30
1 parent ff1065fb27
commit 3943b946d6
693 files changed
+175546 -3894

No files matched your search

+562
View File
@@ -0,0 +1,562 @@
/** Repeatable catalog hook/contract generation. Never rewrites behavior or existing attributes. */
import { readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parse } from "../packages/compiler/src/index.ts";
import { CORE_COMPONENT_CONTRACTS } from "../packages/design-system/src/core-contracts.ts";
import { CATALOG_PART_PROPERTIES } from "../packages/design-system/src/contract-properties.ts";
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
const apply = process.argv.includes("--write");
const directory = join(root, "packages/ui/components");
type ElementNode = {
type: "element";
tag: string;
attrs: Array<{ name: string; value: string; event?: boolean }>;
children: unknown[];
};
type Profile = keyof typeof CATALOG_PART_PROPERTIES;
const providerParts: Record<string, Record<string, string>> = {
Chart: {
canvas: "Chart.js draws canvas pixels; use the existing chart options/data API.",
accessible: "Runtime-generated accessible data table is owned by the chart adapter.",
},
DrawflowCanvas: {
viewport:
"Drawflow owns generated graph DOM and its styles. Use the existing provider options and CSS integration.",
},
Map: { embeddedFrame: "Embedded map document is cross-origin and owned by the map provider." },
};
const familyNames: Record<string, string[]> = {
forms: [
"AdvancedSelect",
"ColorPicker",
"ComboBox",
"DatePicker",
"FileInput",
"FileUploadProgress",
"InputGroup",
"InputNumber",
"PinInput",
"RangeSlider",
"SearchBox",
"StrongPassword",
"TimePicker",
"TogglePassword",
],
navigation: [
"BackToTop",
"Breadcrumb",
"Dropdown",
"Footer",
"Link",
"MegaMenu",
"Navbar",
"Pagination",
"PreferenceSwitcher",
"Scrollspy",
"Sidebar",
"TextLink",
],
layout: [
"AuthSplitLayout",
"CTASection",
"FeatureGrid",
"Grid",
"Hero",
"HeroActions",
"LayoutSplitter",
"Marquee",
"MarketingSectionHeader",
"MetricGrid",
"PublicPageShell",
"Section",
"SectionHeader",
"SplitHero",
],
typography: ["Typography", "Kbd", "Blockquote"],
media: ["Avatar", "AvatarGroup", "DeviceFrame", "Image"],
providers: ["Chart", "DrawflowCanvas", "Map"],
editors: ["FlowCanvas", "WysiwygEditor"],
feedback: ["Skeleton", "Spinner", "Tooltip", "Progress", "Toaster", "LegendIndicator", "Rating"],
};
const family = (name: string) =>
Object.entries(familyNames).find(([, names]) => names.includes(name))?.[0] ?? "collections";
const camel = (value: string) =>
value
.replace(/[-_]([a-z0-9])/g, (_, letter: string) => letter.toUpperCase())
.replace(/[^a-zA-Z0-9]/g, "");
const hooks = /^data-wrn-(?:owner|part|component|axis-[\w-]+)$/;
function stripHooks(value: unknown): unknown {
if (Array.isArray(value)) return value.map(stripHooks);
if (value && typeof value === "object")
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [
key,
key === "attrs" && Array.isArray(entry)
? entry.filter((attr) => !hooks.test(attr.name))
: stripHooks(entry),
]),
);
return value;
}
function elements(
value: unknown,
depth = 0,
output: Array<{ node: ElementNode; root: boolean }> = [],
): typeof output {
if (Array.isArray(value)) {
for (const item of value) elements(item, depth, output);
return output;
}
if (!value || typeof value !== "object") return output;
const record = value as Record<string, unknown>;
if (record.type === "element") {
const node = value as ElementNode;
output.push({ node, root: depth === 0 });
elements(node.children, depth + 1, output);
} else
for (const [key, entry] of Object.entries(record))
if (key !== "attrs") elements(entry, depth, output);
return output;
}
/** Scan tags without treating > inside quotes or brace expressions as an end marker. */
function openingTags(
source: string,
start: number,
end: number,
): Array<{ start: number; end: number; tag: string; raw: string }> {
const result = [];
for (let index = start; index < end; index++) {
if (source.startsWith("<!--", index)) {
index = source.indexOf("-->", index) + 2;
continue;
}
if (source[index] !== "<" || !/[A-Za-z]/.test(source[index + 1] ?? "")) continue;
const match = /^<([A-Za-z][\w.-]*)\b/.exec(source.slice(index));
if (!match) continue;
let quote = "",
braces = 0,
finish = index + match[0].length;
for (; finish < end; finish++) {
const char = source[finish]!;
if (quote) {
if (char === "\\") {
finish++;
continue;
}
if (char === quote) quote = "";
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "{") braces++;
else if (char === "}") braces--;
else if (char === ">" && braces === 0) break;
}
if (finish >= end) throw Error("Unclosed view tag");
result.push({
start: index,
end: finish + 1,
tag: match[1]!,
raw: source.slice(index, finish + 1),
});
index = finish;
}
return result;
}
const catalog: Record<string, unknown> = {};
const snapshots: Record<string, string> = {};
const pendingWrites: Array<{ path: string; source: string }> = [];
for (const file of readdirSync(directory)
.filter((file) => file.endsWith(".wrn"))
.sort()) {
const path = `packages/ui/components/${file}`,
current = readFileSync(join(root, path), "utf8");
const nameMatch = /\bcomponent\s+(\w+)/.exec(current);
if (nameMatch && Object.hasOwn(CORE_COMPONENT_CONTRACTS, nameMatch[1]!)) continue;
// Remove only this generator's attributes before regenerating; leave all original bytes intact.
const source = current.replace(
/ data-wrn-(?:owner|part|component|axis-[\w-]+)=(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g,
"",
);
const ast = parse(source);
const name = ast.name;
if (!name || Object.hasOwn(CORE_COMPONENT_CONTRACTS, name)) continue;
const view = /\bview\s*\{/.exec(source),
style = /^\s*style\s*\{/m.exec(source.slice((view?.index ?? 0) + (view?.[0].length ?? 0)));
if (!view || !style) throw Error(`${name}: missing view/style boundary`);
const start = view.index + view[0].length,
end = start + style.index;
const nodes = elements(ast.view),
tags = openingTags(source, start, end);
if (tags.length !== nodes.length || tags.some((tag, index) => tag.tag !== nodes[index]?.node.tag))
throw Error(`${name}: scanner/parser tag mismatch (${tags.length}/${nodes.length})`);
const props = ast.props ?? [];
const axes: Record<string, { default: string; values: string[] }> = {};
for (const prop of props)
if (
[
"variant",
"size",
"color",
"radius",
"shadow",
"pill",
"shape",
"rounded",
"bordered",
"flush",
"compact",
"dense",
"separated",
].includes(prop.name) &&
prop.default !== undefined
) {
let value: unknown;
try {
value = JSON.parse(prop.default);
} catch {
continue;
}
if (typeof value !== "string" && typeof value !== "boolean") continue;
const values = new Set([String(value)]);
for (const match of source.matchAll(
new RegExp(
`(?:wrn-component--${prop.name}-|wrn-[a-z-]+--${prop.name}-)([a-z0-9-]+)|data-${prop.name}=["']([a-z0-9-]+)["']`,
"g",
),
))
values.add(match[1] ?? match[2]!);
if (typeof value === "boolean") {
values.add("false");
values.add("true");
}
axes[prop.name] = { default: String(value), values: [...values].sort() };
}
const rootClass = nodes
.filter((item) => item.root)
.flatMap((item) =>
item.node.attrs.filter((attr) => attr.name === "class").map((attr) => attr.value),
)
.join(" ");
const baseClass =
(rootClass.match(/\bwrn-(?!component\b|visually\b)[\w-]+/g) ?? []).find(
(value) => !value.includes("--color-") && !value.includes("--size-"),
) ?? "";
const stem = baseClass.replace(/^wrn-next--|^wrn-/, "");
const parts: Record<
string,
{
elements: string[];
states: Record<string, string>;
profile: Profile;
ownership: "framework" | "provider" | "content" | "native";
role: string;
properties: readonly string[];
shapeBinding: boolean;
explicitAxes: Record<string, string>;
}
> = {};
const regions: Record<string, { ownership: string; parts: string[]; limitations: string }> = {};
const edits: Array<{ position: number; text: string }> = [];
for (let index = 0; index < nodes.length; index++) {
const { node, isRoot } = { node: nodes[index]!.node, isRoot: nodes[index]!.root };
const tag = tags[index]!;
if (
node.tag[0] !== node.tag[0]?.toLowerCase() ||
[
"slot",
"path",
"line",
"rect",
"circle",
"polyline",
"polygon",
"defs",
"stop",
"linearGradient",
"source",
"option",
"script",
"style",
].includes(node.tag)
)
continue;
const attributes = Object.fromEntries(
node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name, attr.value]),
);
const classes = (attributes.class ?? "").match(/\bwrn-[\w-]+/g) ?? [];
const bem = classes.find((value) => value.includes("__"));
const flat = classes.find(
(value) =>
baseClass && value.startsWith(baseClass + "-") && !value.startsWith(baseClass + "--"),
);
let suffix =
bem?.split("__")[1]?.replace(new RegExp(`^${stem}-`), "") ??
flat?.slice(baseClass.length + 1);
if (suffix?.includes("--")) suffix = suffix.split("--")[0];
let part = isRoot
? "root"
: suffix
? camel(suffix)
: node.tag === "canvas"
? "canvas"
: node.tag === "iframe"
? "embeddedFrame"
: node.tag === "svg"
? "vector"
: ["input", "textarea", "select"].includes(node.tag)
? "control"
: node.tag === "button"
? attributes["aria-expanded"] !== undefined
? "trigger"
: "button"
: node.tag === "a"
? "link"
: /^h[1-6]$/.test(node.tag)
? "title"
: node.tag === "label"
? "label"
: undefined;
if (Object.hasOwn(attributes, "data-drawflow-host")) part = "viewport";
if (!part) continue;
const interactive =
["button", "input", "select", "textarea", "a"].includes(node.tag) ||
Object.hasOwn(attributes, "tabindex");
let profile: Profile = isRoot
? family(name) === "layout"
? "layout"
: ["typography", "feedback"].includes(family(name))
? "text"
: "layout"
: /^h[1-6]$/.test(node.tag) ||
/^(title|label|description|text|copy|caption|heading|eyebrow|help|hint|error|summary|value|meta|name|breadcrumb)/i.test(
part,
)
? "text"
: interactive
? "control"
: /icon|spinner|indicator|dot|badgeMark|arrow|progressFill|track|gridPattern|decoration/i.test(
part,
)
? "decoration"
: /panel|surface|popover|menu|dialog|item|node$|card|tile|frame|content$|body$/i.test(
part,
)
? "surface"
: "layout";
if (["img", "picture", "video"].includes(node.tag)) profile = "media";
// Keep quantitative and provider-rendered geometry outside CSS recipes.
let ownership: "framework" | "provider" | "content" | "native" = "framework";
if (
["canvas", "iframe", "svg"].includes(node.tag) ||
attributes.type === "hidden" ||
/sr-only|visually-hidden/.test(attributes.class ?? "")
)
profile = "managed";
if (["canvas", "svg"].includes(node.tag))
regions[part] = {
ownership: "framework",
parts: [part],
limitations:
"Rendered pixels and vector geometry are owned by the existing renderer; recipes style surrounding parts only.",
};
if (providerParts[name]?.[part]) {
profile = "managed";
ownership = "provider";
regions[part] = { ownership, parts: [part], limitations: providerParts[name]![part]! };
}
if (node.tag === "iframe") {
profile = "managed";
ownership = "provider";
regions[part] = {
ownership,
parts: [part],
limitations: "Cross-origin embedded content cannot be styled through this contract.",
};
}
if (
["range", "color", "file", "date", "time"].includes(attributes.type ?? "") ||
node.tag === "select"
) {
ownership = "native";
profile = "text";
regions[part] = {
ownership,
parts: [part],
limitations:
"Only the outer native control is styled; picker dialogs, option popups, tracks and thumbs remain native or existing component CSS.",
};
}
if (Object.hasOwn(attributes, "contenteditable")) {
ownership = "content";
profile = "text";
regions[part] = {
ownership,
parts: [part],
limitations:
"Styles apply to the editor container, never rewrite the user-authored document or formatting.",
};
}
// A root frame is a surface only if its own base selector declares geometry.
if (isRoot && baseClass) {
const escaped = baseClass;
const cssBlock = new RegExp(`\\.${escaped}\\s*\\{([^}]*)\\}`, "g");
const ownCss = [...source.slice(end).matchAll(cssBlock)].map((match) => match[1]).join(" ");
if (
/border(?:-radius)?\s*:|box-shadow\s*:/.test(ownCss) &&
!["layout", "typography", "feedback"].includes(family(name))
)
profile = "surface";
if (/border-radius\s*:/.test(ownCss) && ["Skeleton", "Tooltip", "Progress"].includes(name))
profile = "surface";
}
if (
node.tag === "canvas" ||
node.tag === "iframe" ||
node.tag === "svg" ||
attributes.type === "hidden" ||
/sr-only|visually-hidden/.test(attributes.class ?? "")
)
profile = "managed";
const states: Record<string, string> = { hover: ":hover" };
if (interactive) Object.assign(states, { focusVisible: ":focus-visible", active: ":active" });
if (
["input", "button", "select", "textarea"].includes(node.tag) ||
Object.hasOwn(attributes, "aria-disabled")
)
states.disabled = ':is(:disabled,[aria-disabled="true"])';
if (Object.hasOwn(attributes, "aria-invalid") || Object.hasOwn(attributes, "data-invalid"))
states.invalid = ':is([aria-invalid="true"],[data-invalid="true"])';
if (Object.hasOwn(attributes, "readonly") || Object.hasOwn(attributes, "aria-readonly"))
states.readonly = ':is([readonly],[aria-readonly="true"])';
if (["checkbox", "radio"].includes(attributes.type ?? "")) {
states.checked = ":checked";
states.unchecked = ":not(:checked)";
if (attributes.type === "checkbox") states.indeterminate = ":indeterminate";
}
if (Object.hasOwn(attributes, "aria-expanded") || Object.hasOwn(attributes, "data-open"))
states.open = ':is([aria-expanded="true"],[data-open="true"])';
if (
Object.hasOwn(attributes, "aria-selected") ||
Object.hasOwn(attributes, "data-selected") ||
Object.hasOwn(attributes, "aria-current") ||
Object.hasOwn(attributes, "data-active")
)
states.selected =
':is([aria-selected="true"],[data-selected="true"],[aria-current="page"],[data-active="true"])';
const explicitAxes: Record<string, string> = {};
const axisAttrs = Object.keys(axes).map((axis) => {
const original = attributes[`data-${axis}`];
if (original && original !== `{${axis}}` && original.includes("{"))
explicitAxes[axis] = original;
const quote = original
? (new RegExp(`\\bdata-${axis}\\s*=\\s*(["'])`).exec(tag.raw)?.[1] ?? '"')
: '"';
return `data-wrn-axis-${axis}=${quote}${original ?? `{${axis}}`}${quote}`;
});
const previous = parts[part];
// Mixed structural roles sharing a part are conservatively limited to their intersection.
const properties = previous
? previous.properties.filter((property) =>
(CATALOG_PART_PROPERTIES[profile] as readonly string[]).includes(property),
)
: CATALOG_PART_PROPERTIES[profile];
parts[part] = {
elements: [...new Set([...(previous?.elements ?? []), node.tag])],
states: { ...previous?.states, ...states },
profile: previous?.profile === profile ? profile : previous ? "layout" : profile,
ownership: previous?.ownership ?? ownership,
role:
node.tag === "kbd" || node.tag === "code" || node.tag === "pre"
? "code"
: profile === "managed"
? "managed"
: profile,
properties,
shapeBinding:
previous?.shapeBinding || !!attributes["data-shape"] || !!attributes["data-rounded"],
explicitAxes: { ...previous?.explicitAxes, ...explicitAxes },
};
// Existing core hooks are never rewritten; generator is idempotent.
if (attributes["data-wrn-part"] !== undefined) {
if (attributes["data-wrn-part"] !== part || attributes["data-wrn-owner"] !== name)
throw Error(`${name}: conflicting existing hook`);
continue;
}
const attrs = ` data-wrn-owner="${name}" data-wrn-part="${part}"${isRoot ? ` data-wrn-component="${name}"` : ""}${axisAttrs.length ? " " + axisAttrs.join(" ") : ""}`;
edits.push({ position: tag.end - (tag.raw.endsWith("/>") ? 2 : 1), text: attrs });
}
let output = source;
for (const edit of edits.sort((a, b) => b.position - a.position))
output = output.slice(0, edit.position) + edit.text + output.slice(edit.position);
if (JSON.stringify(stripHooks(ast)) !== JSON.stringify(stripHooks(parse(output))))
throw Error(`${name}: non-hook AST changed`);
snapshots[path] = source;
catalog[name] = {
contractVersion: 1,
component: name,
source: path,
family: family(name),
support: "recipe-supported",
axes,
parts,
slots: [
...new Set(
nodes
.filter(({ node }) => node.tag === "slot")
.map(({ node }) => node.attrs.find((attr) => attr.name === "name")?.value ?? "default"),
),
],
tokens: [
...new Set([...source.matchAll(/var\(\s*--wrn-([\w-]+)/g)].map((match) => match[1])),
].sort(),
regions,
constraints: [
"Only declared outer parts are styled. Existing events, markup, slots and state transitions are unchanged.",
"Geometry, text scale and semantic color choices supplied by instance props retain precedence.",
],
fixtures: [
"baseline",
"square",
"soft",
"angular",
"light",
"dark",
"narrow",
"interaction-regression",
],
compatibility: { engine: 1, componentContract: 1 },
};
if (output !== current) pendingWrites.push({ path, source: output });
}
let serialized = JSON.stringify(
catalog,
(key, value) => {
if (key === "properties" && Array.isArray(value))
for (const [profile, properties] of Object.entries(CATALOG_PART_PROPERTIES))
if (JSON.stringify(value) === JSON.stringify(properties)) return `__PROFILE_${profile}__`;
return value;
},
2,
);
serialized = serialized.replace(/"__PROFILE_(\w+)__"/g, "CATALOG_PART_PROPERTIES.$1");
const generated = `// Generated by scripts/rollout-design-contracts.ts. Review contract boundaries when changing markup.\nimport { CATALOG_PART_PROPERTIES } from './contract-properties.ts';\nexport const CATALOG_COMPONENT_CONTRACTS = ${serialized} as const;\n`;
if (apply) {
mkdirSync(join(root, ".tmp-tests"), { recursive: true });
const snapshotPath = join(root, ".tmp-tests/design-phase-3-before.json");
// Keep the first pre-migration snapshot for independent regression verification.
try {
readFileSync(snapshotPath);
} catch {
writeFileSync(snapshotPath, JSON.stringify(snapshots));
}
for (const item of pendingWrites) writeFileSync(join(root, item.path), item.source);
writeFileSync(join(root, "packages/design-system/src/catalog-contracts.ts"), generated);
}
console.log(
`${apply ? "Wrote" : "Audited"} ${Object.keys(catalog).length} catalog contracts; every source passed non-hook AST preservation.`,
);