feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -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");
|
||||
Reference in new issue
Block a user