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

+86
View File
@@ -0,0 +1,86 @@
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,
}),
);
+2 -1
View File
@@ -1,3 +1,4 @@
import { isIndependentReleaseVersion } from "./lib/package-version.mjs";
import console from "node:console";
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
@@ -112,7 +113,7 @@ for (const directory of packageDirectories) {
if (hasHelperKit) helperPackageCount++;
componentCount += components.length;
if (!/^0\.8\.\d+$/.test(manifest.version))
if (!isIndependentReleaseVersion(manifest.version))
failures.push(`${manifest.name}: invalid independent version ${manifest.version}`);
if (!existsSync(entry)) failures.push(`${manifest.name}: missing public entry ${main}`);
if (!existsSync(join(packageRoot, "README.md")))
+4 -1
View File
@@ -1,4 +1,4 @@
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { generateApplicationTypesWithPlugins } from "../packages/cli/src/types.ts";
@@ -20,6 +20,9 @@ try {
return !/(?:^|\/)(?:dist|node_modules|\.wrnexus(?:-[^/]*)?)(?:\/|$)/.test(relative);
},
});
// The scratch config must resolve the same declared dependencies as the example.
if (existsSync(join(source, "node_modules")))
symlinkSync(join(source, "node_modules"), join(scratch, "node_modules"), "junction");
await generateApplicationTypesWithPlugins(scratch);
const stale = generated.filter((relative) => {
+221
View File
@@ -0,0 +1,221 @@
/** Copied into an isolated tarball consumer by prepare-design-release.ts. Package imports only. */
import assert from "node:assert/strict";
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { CORE_DESIGN_SYSTEMS, compileDesignSystem } from "@wrnexus/design-system";
import { createDesignPackage, readDesignPackage } from "@wrnexus/design-system/package";
import { StudioSession } from "@wrnexus/design-studio";
import { planDesignUpgrade, applyDesignUpgrade } from "@wrnexus/design-studio/upgrades";
import { createStudioHandler } from "@wrnexus/design-studio/server";
import { createFrameworkMcpServer } from "@wrnexus/mcp";
import { uiComponentNames } from "@wrnexus/ui/registry";
import { resolveThemeConfig } from "@wrnexus/styles/theme";
import schema from "@wrnexus/design-system/schema.json";
assert.equal(uiComponentNames().length, 104);
assert.ok(schema);
const system = structuredClone(CORE_DESIGN_SYSTEMS.square);
const original = await createDesignPackage(system);
const next = structuredClone(system);
next.version = "1.1.0";
next.tokens["color.primary"]!.value = "#abcdef";
const upgraded = await createDesignPackage(next);
const source = JSON.stringify(upgraded);
const sessions: StudioSession[] = [];
const applications = [];
for (const name of ["application-one", "application-two"]) {
const root = resolve(name);
mkdirSync(root, { recursive: true });
writeFileSync(join(root, "wrn.config.ts"), "export default { existing: true };");
const storage = {
value: "",
getItem() {
return this.value || null;
},
setItem(_key: string, value: string) {
this.value = value;
},
};
const session = new StudioSession(system, storage);
sessions.push(session);
session.setLocks(["/tokens/shape.control"]);
const review = await planDesignUpgrade(session, source);
assert.equal(review.canApply, true);
await applyDesignUpgrade(session, source, review.baseRevision);
assert.equal(session.valid.version, "1.1.0");
session.undo();
assert.equal(session.valid.version, "1.0.0");
session.redo();
session.edit("{unfinished");
const restored = new StudioSession(undefined, storage);
assert.equal(restored.restore(), true);
assert.equal(restored.source, "{unfinished");
restored.undo();
assert.equal(restored.valid.version, "1.1.0");
const server = createFrameworkMcpServer(root, { design: true, designWrite: true });
let draft = (await server.call("wrnexus_design_create", {
baseRevision: "none",
source: JSON.stringify(system),
})) as { revision: string };
const migration = (await server.call("wrnexus_design_upgrade_plan", { source })) as {
canApply: boolean;
};
assert.equal(migration.canApply, true);
draft = (await server.call("wrnexus_design_upgrade", {
baseRevision: draft.revision,
source,
})) as typeof draft;
const page = {
id: "account",
title: "Account settings",
components: [
{ component: "Input", props: { id: "name", label: "Display name" } },
{ component: "Button", props: { label: "Save profile", variant: "default" } },
],
};
draft = (await server.call("wrnexus_design_page", {
baseRevision: draft.revision,
page,
})) as typeof draft;
const preview = (await server.call("wrnexus_design_preview", { pageId: "account" })) as {
html: string;
};
assert.ok(preview.html.includes('data-wrn-component="Button"'));
assert.ok(preview.html.includes("Save profile"));
const pinned = (await server.call("wrnexus_design_package")) as { package: typeof upgraded };
assert.equal(pinned.package.integrity.digest, upgraded.integrity.digest);
await server.call("wrnexus_design_apply", {
baseRevision: draft.revision,
baseApplicationRevision: "none",
});
const state = (await server.call("wrnexus_design_inspect")) as {
application: { revision: string };
};
assert.notEqual(state.application.revision, "none");
assert.equal(
readFileSync(join(root, "wrn.config.ts"), "utf8"),
"export default { existing: true };",
);
applications.push({
name,
digest: pinned.package.integrity.digest,
applied: state.application.revision,
});
}
assert.equal(applications[0]!.digest, applications[1]!.digest);
assert.equal(applications[0]!.applied, applications[1]!.applied);
assert.equal(
(await readDesignPackage(JSON.stringify(original))).package.integrity.digest,
original.integrity.digest,
);
for (const system of Object.values(CORE_DESIGN_SYSTEMS)) {
assert.ok(compileDesignSystem(system).css);
assert.ok(resolveThemeConfig({ designSystem: { system } }).recipeCss);
}
const handler = await createStudioHandler({ applicationRoot: resolve("application-one") });
assert.equal((await handler(new Request("http://127.0.0.1:4320/"))).status, 200);
const client = await handler(new Request("http://127.0.0.1:4320/studio.js"));
assert.equal(client.status, 200);
assert.ok((await client.text()).length > 1000);
const req = await handler(
new Request("http://127.0.0.1:4320/api/preview", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ system: CORE_DESIGN_SYSTEMS.angular, mode: "dark" }),
}),
);
assert.equal(req.status, 200);
const result = await req.json();
assert.ok(
(await (await handler(new Request("http://127.0.0.1:4320" + result.url))).text()).includes(
'data-wrn-component="Button"',
),
);
assert.equal(existsSync(resolve("node_modules/@wrnexus/design-studio/src")), false);
assert.equal(
existsSync(resolve("node_modules/@wrnexus/design-studio/assets/studio-client.js")),
true,
);
writeFileSync(
"browser-probe.ts",
'import {StudioSession} from "@wrnexus/design-studio"; import {createDesignPackage} from "@wrnexus/design-system/package"; import {planDesignUpgrade} from "@wrnexus/design-studio/upgrades"; console.log(new StudioSession(),createDesignPackage,planDesignUpgrade);',
);
const browser = await Bun.build({ entrypoints: [resolve("browser-probe.ts")], target: "browser" });
assert.equal(browser.success, true, browser.logs.join("\n"));
assert.ok(browser.outputs.length);
const defaultServer = createFrameworkMcpServer(resolve("application-one"));
assert.equal(defaultServer.tools().length, 8);
const stdio = resolve("node_modules/@wrnexus/mcp/dist/stdio.js");
const child = Bun.spawn(
[process.execPath, stdio, "--root=" + resolve("application-one"), "--design"],
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
);
child.stdin.write(
JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "wrnexus_design_context", arguments: { components: ["Button"] } },
}) + "\n",
);
child.stdin.end();
const protocol = JSON.parse((await new Response(child.stdout).text()).trim());
assert.equal(await child.exited, 0);
assert.ok(protocol.result.structuredContent.value.components.Button);
const cli = Bun.spawn(
[
process.execPath,
resolve("node_modules/@wrnexus/design-studio/dist/cli.js"),
"--app-root",
resolve("application-one"),
],
{ stdout: "pipe", stderr: "pipe", env: { ...process.env, DESIGN_STUDIO_PORT: "0" } },
);
let startupTimer: ReturnType<typeof setTimeout> | undefined;
try {
const startup = await Promise.race([
cli.stdout.getReader().read(),
new Promise<never>((_resolve, reject) => {
startupTimer = setTimeout(
() => reject(Error("Installed Studio CLI startup timed out.")),
10000,
);
}),
]);
const line = new TextDecoder().decode(startup.value);
const address = /http:\/\/127\.0\.0\.1:\d+\//.exec(line)?.[0];
assert.ok(address, line);
assert.equal((await fetch(address)).status, 200);
} finally {
if (startupTimer) clearTimeout(startupTimer);
cli.kill();
await cli.exited;
}
writeFileSync(
process.env.WRNEXUS_DESIGN_REPORT!,
JSON.stringify(
{
packagingVerified: false,
published: false,
applications,
checks: [
"104 shipped component sources",
"JSON schema asset",
"browser-safe root and upgrades bundle",
"prebuilt Studio client without src files",
"real composed and agent SSR previews",
"shared pin and CSS consistency across two applications",
"invalid draft reload and undo/redo",
"reviewed package upgrades and MCP operations",
"unchanged default MCP tools and installed stdio protocol",
"existing application configuration preserved",
"installed Studio CLI starts without source files",
],
},
null,
2,
),
);
console.log(
"Installed tarball consumers passed for two applications, Studio browser/server assets and MCP.",
);
+2
View File
@@ -0,0 +1,2 @@
import { runDesignStudio } from "../packages/design-studio/src/cli.ts";
await runDesignStudio();
+113
View File
@@ -0,0 +1,113 @@
/** Reproducible Phase 6 local agent evaluation. No provider calls, routes or deployments. */
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve, join } from "node:path";
import { createFrameworkMcpServer } from "../packages/mcp/src/index.ts";
import { CORE_DESIGN_SYSTEMS } from "../packages/design-system/src/index.ts";
import { compileWrnFile } from "../packages/compiler/src/index.ts";
import { checkDesignConformance } from "../packages/design-studio/src/conformance.ts";
import type { AgentPage } from "../packages/design-studio/src/agent-pages.ts";
const output = resolve("output/design-system-phase-6");
mkdirSync(output, { recursive: true });
const report: Array<Record<string, unknown>> = [];
for (const [name, system] of Object.entries(CORE_DESIGN_SYSTEMS)) {
const directory = join(output, name);
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, "package.json"), "{}");
const server = createFrameworkMcpServer(directory, { design: true, designWrite: true });
let draft = (await server.call("wrnexus_design_inspect")) as { revision: string };
draft = (await server.call("wrnexus_design_create", {
baseRevision: draft.revision,
source: JSON.stringify(system),
})) as typeof draft;
const context = await server.call("wrnexus_design_context", {
components: ["PageHeader", "Input", "Button", "Alert"],
});
writeFileSync(join(directory, "agent-context.json"), JSON.stringify(context, null, 2));
for (const id of ["account", "support"]) {
const page: AgentPage = {
id,
title: id === "account" ? "Account settings" : "Contact support",
components: [
{
component: "PageHeader",
props: { title: id === "account" ? "Your profile" : "How can we help?" },
},
{
component: "Input",
props: { id: id + "-name", label: "Your name", placeholder: "Enter your name" },
},
{
component: "Input",
props: {
id: id + "-email",
label: "Email",
type: "email",
placeholder: "you@example.com",
},
},
{
component: "Alert",
props: {
title: "Design preview",
description: "This page uses your selected design system.",
},
},
{
component: "Button",
props: { label: id === "account" ? "Save profile" : "Send request", variant: "default" },
},
],
};
draft = (await server.call("wrnexus_design_page", {
baseRevision: draft.revision,
page,
})) as typeof draft;
for (const mode of ["light", "dark"]) {
const preview = (await server.call("wrnexus_design_preview", { pageId: id, mode })) as {
html: string;
};
writeFileSync(join(directory, `${id}-${mode}.html`), preview.html);
}
}
const exported = (await server.call("wrnexus_design_export")) as {
pages: Record<string, string>;
artifacts: Record<string, string>;
};
for (const [filename, source] of Object.entries(exported.pages)) {
writeFileSync(join(directory, filename), source);
const conformance = checkDesignConformance(source);
const compiled = compileWrnFile(source);
if (!conformance.ok || !compiled) throw Error("Agent evaluation failed.");
report.push({
system: name,
page: filename,
compiled: true,
conformance,
modes: ["light", "dark"],
preview: "Trusted shipped SSR components; static CSP-protected HTML.",
});
}
for (const [name, body] of Object.entries(exported.artifacts))
writeFileSync(join(directory, name + ".txt"), body);
}
writeFileSync(
join(output, "report.json"),
JSON.stringify(
{
passed: true,
pages: report.length,
previews: report.length * 2,
evaluations: report,
limits: [
"No LLM call. This exercises the agent tool protocol deterministically.",
"Static checks and SSR previews do not certify all accessibility, dynamic behavior or visual quality.",
"Generated pages and design artifacts require explicit application integration.",
],
},
null,
2,
),
);
console.log(
`Agent evaluation passed: ${report.length} pages and ${report.length * 2} previews. ${output}`,
);
+3
View File
@@ -81,6 +81,9 @@ export function validateStagedManifest(stage: string, manifest: Record<string, u
...manifestTargets(manifest.types),
...manifestTargets(manifest.bin),
...manifestTargets(manifest.exports),
...((manifest.wrnexus as any)?.clientRuntimes ?? []).flatMap((runtime: { entry?: string }) =>
manifestTargets(runtime.entry),
),
];
for (const target of new Set(targets)) {
if (!target.startsWith("./")) throw new Error(`${name} has unsafe package target ${target}.`);
+6
View File
@@ -0,0 +1,6 @@
/** Package versions are independent of the framework release line. Release candidates use stable SemVer triples. */
export function isIndependentReleaseVersion(value) {
return (
typeof value === "string" && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(value)
);
}
+227
View File
@@ -0,0 +1,227 @@
/** Local release preparation only. Never authenticates to or publishes to a registry. */
import {
existsSync,
readdirSync,
readFileSync,
writeFileSync,
mkdirSync,
mkdtempSync,
rmSync,
} from "node:fs";
import { basename, dirname, join, resolve, sep } from "node:path";
import { tmpdir } from "node:os";
import { spawnSync } from "node:child_process";
import { validateAndHashStage } from "./lib/package-integrity.ts";
const root = resolve(import.meta.dir, "..");
const output = join(root, "output", "design-system-phase-7");
const stage = join(root, ".publish-design");
const mode = process.argv[2] ?? "plan";
const all = new Map(
readdirSync(join(root, "packages")).flatMap((name) => {
const file = join(root, "packages", name, "package.json");
if (!existsSync(file)) return [];
const manifest = JSON.parse(readFileSync(file, "utf8"));
return [[manifest.name, { directory: name, ...manifest }]];
}),
);
const names = new Set<string>();
function visit(name: string) {
if (names.has(name)) return;
const pkg = all.get(name);
if (!pkg) throw Error("Missing workspace package " + name);
names.add(name);
for (const dependency of Object.keys({
...pkg.dependencies,
...pkg.optionalDependencies,
...pkg.peerDependencies,
}))
if (dependency.startsWith("@wrnexus/")) visit(dependency);
}
for (const name of ["@wrnexus/design-system", "@wrnexus/design-studio", "@wrnexus/mcp"])
visit(name);
mkdirSync(output, { recursive: true });
const plan = {
formatVersion: 1,
stageDirectory: stage,
packages: [...names].map((name) => ({ name, version: all.get(name).version })),
schemaVersion: 1,
contractVersion: 1,
designPackageVersion: 1,
registryAvailability: "not-checked",
published: false,
notes: [
"Source versions remain unchanged. Confirm unused release versions before publication.",
"Use the existing version/release process for production; this command only stages and packs locally.",
"Production-wide API/visual/type gates must pass separately before publication.",
],
};
writeFileSync(join(output, "release-plan.json"), JSON.stringify(plan, null, 2));
function run(command: string, args: string[], cwd = root) {
const result = spawnSync(command, args, {
cwd,
encoding: "utf8",
maxBuffer: 20 * 1024 * 1024,
env: { ...process.env, WRNEXUS_DESIGN_REPORT: join(output, "consumer-report.json") },
});
if (result.status !== 0)
throw Error(
`${basename(command)} failed: ${result.error?.message ?? result.stderr ?? result.stdout}`,
);
return result.stdout;
}
if (mode === "plan") {
console.log(JSON.stringify(plan, null, 2));
} else if (mode === "stage") {
console.log(
run(process.execPath, [
"run",
"scripts/publish-packages.ts",
"design-system",
"design-studio",
"mcp",
"--with-dependencies",
"--stage-root=.publish-design",
]),
);
} else if (mode === "check") {
writeFileSync(
join(output, "consumer-report.json"),
JSON.stringify({ packagingVerified: false, published: false, status: "running" }, null, 2),
);
const integrity = JSON.parse(readFileSync(join(stage, "PACKAGE-INTEGRITY.json"), "utf8"));
if (
integrity.packages.length !== names.size ||
integrity.packages.some(
(pkg: { name: string; version: string }) =>
!names.has(pkg.name) || pkg.version !== all.get(pkg.name)?.version,
)
)
throw Error("Staged closure differs from release plan. Restage all design packages.");
const node = Bun.which("node");
if (!node) throw Error("Node.js with npm is required to verify tarballs.");
const npm = join(dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
if (!existsSync(npm)) throw Error("Unable to locate npm CLI.");
const tarballs = join(output, "tarballs");
mkdirSync(tarballs, { recursive: true });
const dependencies: Record<string, string> = {};
const packed = [];
for (const pkg of integrity.packages) {
const folder = join(stage, pkg.name.slice("@wrnexus/".length));
const manifest = JSON.parse(readFileSync(join(folder, "package.json"), "utf8"));
const actual = validateAndHashStage(folder, manifest);
if (JSON.stringify(actual) !== JSON.stringify(pkg))
throw Error("Staged integrity mismatch for " + pkg.name);
const files = JSON.parse(
run(node, [
npm,
"pack",
folder,
"--pack-destination",
tarballs,
"--json",
"--ignore-scripts",
]),
);
dependencies[pkg.name] = "file:" + join(tarballs, files[0].filename).replace(/\\/g, "/");
packed.push({
name: pkg.name,
version: manifest.version,
filename: files[0].filename,
integrity: files[0].integrity,
});
}
writeFileSync(join(output, "tarballs.json"), JSON.stringify(packed, null, 2));
const consumer = mkdtempSync(join(tmpdir(), "wrnexus-design-release-"));
try {
writeFileSync(
join(consumer, "package.json"),
JSON.stringify({
name: "design-release-consumer",
private: true,
type: "module",
dependencies,
}),
);
console.log(
run(node, [npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], consumer),
);
writeFileSync(
join(consumer, "probe.ts"),
readFileSync(join(root, "scripts", "design-release-consumer.ts"), "utf8"),
);
console.log(run(process.execPath, ["probe.ts"], consumer));
writeFileSync(
join(consumer, "types.ts"),
'import {CORE_DESIGN_SYSTEMS} from "@wrnexus/design-system"; import {createDesignPackage} from "@wrnexus/design-system/package"; import {StudioSession} from "@wrnexus/design-studio"; import {planDesignUpgrade} from "@wrnexus/design-studio/upgrades"; import {createFrameworkMcpServer} from "@wrnexus/mcp"; const session = new StudioSession(CORE_DESIGN_SYSTEMS.square); const artifact = await createDesignPackage(session.valid); await planDesignUpgrade(session, JSON.stringify(artifact)); createFrameworkMcpServer(".", {design:true,designWrite:true}); const recovery: string | undefined = session.recoverySource; console.log(recovery);',
);
writeFileSync(
join(consumer, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
noEmit: true,
module: "ESNext",
moduleResolution: "bundler",
target: "ESNext",
skipLibCheck: true,
types: [],
lib: ["ESNext", "DOM"],
},
files: ["types.ts"],
}),
);
run(
node,
[join(root, "node_modules", "typescript", "bin", "tsc"), "--project", "tsconfig.json"],
consumer,
);
console.log("Installed package declarations passed consumer type checking.");
} finally {
// The path comes exclusively from mkdtemp; verify its workspace-independent boundary before removal.
if (
!resolve(consumer).startsWith(resolve(tmpdir()) + sep) ||
!basename(consumer).startsWith("wrnexus-design-release-")
)
console.error("Skipped cleanup: invalid consumer boundary.");
else if (process.argv.includes("--keep-consumer")) {
writeFileSync(
join(output, "consumer-path.json"),
JSON.stringify({ directory: consumer }, null, 2),
);
console.log("Retained installed consumer for local QA: " + consumer);
} else rmSync(consumer, { recursive: true, force: true });
}
const solo = mkdtempSync(join(tmpdir(), "wrnexus-design-release-"));
try {
writeFileSync(
join(solo, "package.json"),
JSON.stringify({
name: "mcp-only-release-consumer",
private: true,
type: "module",
dependencies: { "@wrnexus/mcp": dependencies["@wrnexus/mcp"] },
}),
);
run(node, [npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], solo);
writeFileSync(
join(solo, "probe.ts"),
'import assert from "node:assert/strict"; import {createFrameworkMcpServer} from "@wrnexus/mcp"; import {existsSync} from "node:fs"; assert.equal(existsSync("node_modules/@wrnexus/design-studio"),false); assert.equal(createFrameworkMcpServer(".").tools().length,8); await assert.rejects(createFrameworkMcpServer(".",{design:true}).call("wrnexus_design_context"),/WRN-MCP-DESIGN-INSTALL/); console.log("MCP-only install keeps default APIs without requiring Studio.");',
);
console.log(run(process.execPath, ["probe.ts"], solo));
} finally {
if (
resolve(solo).startsWith(resolve(tmpdir()) + sep) &&
basename(solo).startsWith("wrnexus-design-release-")
)
rmSync(solo, { recursive: true, force: true });
}
const reportPath = join(output, "consumer-report.json");
const report = JSON.parse(readFileSync(reportPath, "utf8"));
report.packagingVerified = true;
report.checkedAt = new Date().toISOString();
report.checks.push(
"installed declaration consumer type check",
"MCP-only installation without optional Studio peer",
);
writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log("Local tarballs verified. No registry publication performed.");
} else throw Error("Usage: bun scripts/prepare-design-release.ts plan | stage | check");
+46
View File
@@ -0,0 +1,46 @@
import { catalogFixtureDocument } from "../packages/ui/test/design-system-catalog-fixture.ts";
import { CORE_DESIGN_SYSTEMS } from "../packages/design-system/src/index.ts";
import { resolveThemeConfig, renderThemeCss } from "../packages/styles/src/theme.ts";
import { designFixtureDocument } from "../packages/ui/test/design-system-fixture.ts";
import { getComponentControllerRuntime } from "../packages/csr/src/index.ts";
const server = Bun.serve({
hostname: "127.0.0.1",
port: Number(process.env.DESIGN_PREVIEW_PORT ?? 4318),
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/__wrnexus/controllers.js")
return new Response(getComponentControllerRuntime(), {
headers: { "content-type": "text/javascript" },
});
const name = url.searchParams.get("system") ?? "square";
const system = Object.hasOwn(CORE_DESIGN_SYSTEMS, name)
? CORE_DESIGN_SYSTEMS[name as keyof typeof CORE_DESIGN_SYSTEMS]
: undefined;
if (!system) return new Response("Unknown design system", { status: 404 });
if (url.pathname === "/theme.css")
return new Response(renderThemeCss(resolveThemeConfig({ designSystem: { system } })), {
headers: { "content-type": "text/css" },
});
if (url.pathname === "/catalog")
return new Response(
await catalogFixtureDocument(
system,
url.searchParams.get("mode") === "dark" ? "dark" : "light",
Math.max(0, Math.min(6, Number(url.searchParams.get("page")) || 0)),
),
{ headers: { "content-type": "text/html; charset=utf-8" } },
);
if (url.pathname !== "/") return new Response("Not found", { status: 404 });
const mode = url.searchParams.get("mode") === "dark" ? "dark" : "light";
return new Response(
await designFixtureDocument(
system,
mode,
url.searchParams.get("delivery") === "linked" ? "linked" : "inline",
),
{ headers: { "content-type": "text/html; charset=utf-8" } },
);
},
});
console.log(`Design-system preview: ${server.url}`);
+61 -8
View File
@@ -24,14 +24,23 @@ import {
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import process from "node:process";
import { validateAndHashStage, type StagedPackageIntegrity } from "./lib/package-integrity.ts";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const packagesDir = join(repoRoot, "packages");
const stageRoot = join(repoRoot, ".publish");
const stageArgument =
process.argv.find((arg) => arg.startsWith("--stage-root="))?.slice(13) ?? ".publish";
const stageRoot = resolve(repoRoot, stageArgument);
if (
dirname(stageRoot) !== resolve(repoRoot) ||
!/^\.publish(?:-[a-z0-9-]+)?$/.test(basename(stageRoot))
)
throw Error(
"Stage root must be a .publish or .publish-NAME directory directly inside this workspace.",
);
const REGISTRY = process.env.WRNEXUS_NPM_REGISTRY ?? "https://registry.npmjs.org/";
// First release is published PRIVATE ("restricted") so we can test before the
// world sees it; flip to public with `npm access public @wrnexus/<name>` (or set
@@ -44,6 +53,7 @@ const HOMEPAGE = "https://wrnexusjs.dev";
/** Extra (non-code) files each package must ship, relative to the package root. */
const ASSETS: Record<string, string[]> = {
"@wrnexus/design-studio": ["assets"],
"@wrnexus/ui": [
"components",
"styles",
@@ -97,7 +107,11 @@ function topoSort(pkgs: Pkg[]): Pkg[] {
const visit = (p: Pkg) => {
if (seen.has(p.name)) return;
seen.add(p.name);
for (const dep of Object.keys(p.manifest.dependencies ?? {})) {
for (const dep of Object.keys({
...p.manifest.dependencies,
...p.manifest.optionalDependencies,
...p.manifest.peerDependencies,
})) {
const d = byName.get(dep);
if (d) visit(d);
}
@@ -121,6 +135,7 @@ function entriesOf(dir: string, m: Record<string, any>): Record<string, string>
map[rel] = join(dir, v.replace(/^\.\//, "")).replace(/\\/g, "/");
};
add(m.main);
for (const runtime of m.wrnexus?.clientRuntimes ?? []) add(runtime.entry);
if (m.bin) Object.values(m.bin).forEach(add);
if (m.exports) for (const v of Object.values(m.exports)) add(v);
if (m.wrnexus?.plugin) {
@@ -205,6 +220,8 @@ function publishManifest(
out.exports = { ".": { types: out.types, import: out.main } };
}
if (m.peerDependenciesMeta) out.peerDependenciesMeta = m.peerDependenciesMeta;
if (m.bin) {
out.bin = {};
for (const [k, v] of Object.entries(m.bin)) out.bin[k] = js(v as string);
@@ -223,6 +240,11 @@ function publishManifest(
if (m.wrnexus) {
out.wrnexus = structuredClone(m.wrnexus);
for (const runtime of out.wrnexus.clientRuntimes ?? []) {
if (typeof runtime.entry === "string" && runtime.entry.endsWith(".ts")) {
runtime.entry = js(runtime.entry);
}
}
const plugin = out.wrnexus?.plugin?.plugin;
if (typeof plugin === "string" && plugin.endsWith(".ts")) {
out.wrnexus.plugin.plugin = js(plugin);
@@ -237,7 +259,9 @@ function publishManifest(
}
async function main() {
const filter = process.argv.slice(2);
const filter = process.argv
.slice(2)
.filter((arg) => !arg.startsWith("--stage-root=") && arg !== "--with-dependencies");
const allPackages = readPackages([]);
const workspaceVersions = new Map(
allPackages.map((pkg) => [pkg.name, String(pkg.manifest.version)]),
@@ -247,6 +271,22 @@ async function main() {
(pkg) => filter.includes(pkg.dir.split(/[\\/]/).pop()!) || filter.includes(pkg.name),
)
: allPackages;
if (process.argv.includes("--with-dependencies")) {
const visited = new Set(selected.map((pkg) => pkg.name));
for (let index = 0; index < selected.length; index++) {
for (const name of Object.keys({
...selected[index]!.manifest.dependencies,
...selected[index]!.manifest.optionalDependencies,
...selected[index]!.manifest.peerDependencies,
})) {
if (!name.startsWith("@wrnexus/") || visited.has(name)) continue;
const dependency = allPackages.find((pkg) => pkg.name === name);
if (!dependency) throw Error(`Missing workspace dependency ${name}.`);
visited.add(name);
selected.push(dependency);
}
}
}
const pkgs = topoSort(selected);
if (!pkgs.length) {
console.error("No @wrnexus packages matched.");
@@ -265,7 +305,7 @@ async function main() {
entry: entries,
outDir: join(p.dir, "dist"),
format: ["esm"],
dts: true,
dts: { compilerOptions: { ignoreDeprecations: "6.0" } },
external: [
/^@wrnexus\//,
/^bun:/,
@@ -307,9 +347,20 @@ async function main() {
for (const a of packageAssets(p.manifest)) {
if (existsSync(join(p.dir, a))) cpSync(join(p.dir, a), join(stage, a), { recursive: true });
}
if (p.name === "@wrnexus/design-studio") {
const browser = await Bun.build({
entrypoints: [join(p.dir, "src", "client.ts")],
target: "browser",
minify: true,
});
if (!browser.success || browser.outputs.length !== 1)
throw Error("Studio browser asset build failed.");
mkdirSync(join(stage, "assets"), { recursive: true });
writeFileSync(join(stage, "assets", "studio-client.js"), await browser.outputs[0]!.text());
}
const manifest = publishManifest(p.manifest, p.manifest.version, workspaceVersions);
writeFileSync(join(stage, "package.json"), JSON.stringify(manifest, null, 2) + "\n");
if (p.manifest.wrnexus && !manifest.wrnexus?.plugin?.plugin) {
if (p.manifest.wrnexus?.plugin && !manifest.wrnexus?.plugin?.plugin) {
throw new Error(`${p.name} lost its wrnexus.plugin metadata while staging.`);
}
for (const asset of packageAssets(p.manifest)) {
@@ -347,11 +398,13 @@ async function main() {
);
console.log(
`\n✓ Staged and verified ${order.length} package(s) under .publish/ (access: ${ACCESS})`,
`\n✓ Staged and verified ${order.length} package(s) under ${basename(stageRoot)}/ (access: ${ACCESS})`,
);
console.log("Publish order (deps first):");
for (const n of order) {
console.log(` npm publish .publish/${n.replace("@wrnexus/", "")} --access ${ACCESS}`);
console.log(
` npm publish ${basename(stageRoot)}/${n.replace("@wrnexus/", "")} --access ${ACCESS}`,
);
}
if (ACCESS === "restricted") {
console.log("\nAfter testing, flip every package to public with:");
+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.`,
);
+3 -2
View File
@@ -1,3 +1,4 @@
import { isIndependentReleaseVersion } from "./lib/package-version.mjs";
import console from "node:console";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs";
@@ -79,8 +80,8 @@ const manifests = trackedFiles
.filter(({ value }) => value.name?.startsWith("@wrnexus/"));
addCheck(
"SEC-PACKAGE-VERSION",
manifests.every(({ value }) => /^0\.8\.\d+$/.test(value.version)),
"Every @wrnexus package must use a valid independent 0.8 version.",
manifests.every(({ value }) => isIndependentReleaseVersion(value.version)),
"Every @wrnexus package must use a valid independent release version.",
);
addCheck(
"SEC-PRIVATE-PUBLISH",
+14
View File
@@ -32,6 +32,20 @@ try {
throw new Error("Safe stage did not produce deterministic SHA-256 records.");
}
const runtimeManifest = {
...manifest,
wrnexus: { clientRuntimes: [{ id: "chartjs", entry: "./dist/chart-runtime.js" }] },
};
let missingRuntimeRejected = false;
try {
validateAndHashStage(safe, runtimeManifest);
} catch (error) {
missingRuntimeRejected = String(error).includes("chart-runtime.js");
}
if (!missingRuntimeRejected) throw new Error("Missing client runtime was not rejected.");
writeFileSync(join(safe, "dist", "chart-runtime.js"), "export {};\n");
validateAndHashStage(safe, runtimeManifest);
const forbidden = join(root, "forbidden");
mkdirSync(forbidden);
writeFileSync(join(forbidden, ".env"), "TOKEN=secret\n");
+17
View File
@@ -0,0 +1,17 @@
import console from "node:console";
import assert from "node:assert/strict";
import { isIndependentReleaseVersion } from "./lib/package-version.mjs";
for (const version of ["0.1.0", "0.1.51", "0.8.117", "1.0.0"])
assert.equal(isIndependentReleaseVersion(version), true, version);
for (const version of [
"0.01.0",
"01.0.0",
"0.8",
"1.2.3.4",
"1.2.3-beta",
"workspace:*",
"garbage",
null,
])
assert.equal(isIndependentReleaseVersion(version), false, String(version));
console.log("Independent release version policy probes passed.");
+31
View File
@@ -86,6 +86,13 @@ try {
dependencies[name] = viewer.devDependencies[name];
}
}
// The all-package probe imports the optional React adapter explicitly.
// Ordinary framework consumers still do not need React installed.
if (stagedPackageNames.has("@wrnexus/react")) {
const adapter = JSON.parse(readFileSync(join(root, "packages/react/package.json"), "utf8"));
dependencies.react = adapter.peerDependencies.react;
dependencies["react-dom"] = adapter.peerDependencies["react-dom"];
}
writeFileSync(
join(consumer, "package.json"),
JSON.stringify(
@@ -130,6 +137,30 @@ try {
throw new Error(`Staged package import probe failed:\n${probe.stderr || probe.stdout}`);
if (probe.stdout) process.stdout.write(probe.stdout);
if (stagedPackageNames.has("@wrnexus/ui")) {
writeFileSync(
join(consumer, "ui-runtime-probe.mjs"),
`import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
const root = resolve("node_modules/@wrnexus/ui");
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
for (const runtime of manifest.wrnexus?.clientRuntimes ?? []) {
const entry = resolve(root, runtime.entry);
if (!existsSync(entry) || entry.endsWith(".ts")) throw new Error("Missing compiled UI runtime: " + runtime.id);
if (runtime.bundle === false) continue;
const result = await Bun.build({ entrypoints: [entry], target: "browser" });
if (!result.success) throw new AggregateError(result.logs, "UI browser runtime failed: " + runtime.id);
}
if (!existsSync(resolve(root, "components/Chart.wrn"))) throw new Error("Missing Chart component");
console.log("Published UI component and browser runtimes passed.");
`,
);
const ui = spawnSync(bun, ["ui-runtime-probe.mjs"], { cwd: consumer, encoding: "utf8" });
if (ui.status !== 0)
throw new Error(`Staged UI runtime probe failed:\n${ui.stderr || ui.stdout}`);
if (ui.stdout) process.stdout.write(ui.stdout);
}
if (stagedPackageNames.has("@wrnexus/viewer")) {
writeFileSync(
join(consumer, "viewer-probe.mjs"),
+3 -2
View File
@@ -1,3 +1,4 @@
import { isIndependentReleaseVersion } from "./lib/package-version.mjs";
import console from "node:console";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
@@ -59,8 +60,8 @@ check(
);
check("root version is a 0.8 release", /^0\.8\.\d+$/.test(releaseVersion));
check(
"all framework packages use valid independent 0.8 versions",
packageManifests.every((manifest) => /^0\.8\.\d+$/.test(manifest.version)),
"all framework packages use valid independent release versions",
packageManifests.every((manifest) => isIndependentReleaseVersion(manifest.version)),
);
const bunLockPackages = [
...text("bun.lock").matchAll(/^ {4}"packages\/[^"\r\n]+": \{\r?\n([\s\S]*?)^ {4}\},?$/gm),