feat: introduce local-first SVG workbench

This commit is contained in:
2026-08-02 16:31:49 +02:00
commit 39d9802daa
97 changed files with 20702 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import {
access,
mkdtemp,
readFile,
rm,
stat,
writeFile,
} from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const workspace = path.dirname(root);
const portal = path.resolve(
process.env.TOOLBOX_PORTAL_DIR ?? path.join(workspace, "toolbox-portal"),
);
const appPackage = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"),
);
const portalPackage = JSON.parse(
await readFile(path.join(portal, "package.json"), "utf8"),
);
const artifact = path.join(
root,
"release",
`svg-tools-${appPackage.version}.zip`,
);
async function run(command, arguments_, cwd) {
await new Promise((resolve, reject) => {
const child = spawn(command, arguments_, {
cwd,
stdio: "inherit",
env: { ...process.env, CI: "1" },
});
child.once("error", reject);
child.once("exit", (code, signal) => {
if (code === 0) resolve();
else
reject(
new Error(
`${command} exited with ${code ?? `signal ${signal ?? "unknown"}`}`,
),
);
});
});
}
const details = await stat(artifact).catch(() => null);
if (!details?.isFile()) {
throw new Error(`Build the app release first: ${artifact}`);
}
const digest = createHash("sha256")
.update(await readFile(artifact))
.digest("hex");
const sidecar = await readFile(`${artifact}.sha256`, "utf8");
if (!sidecar.startsWith(`${digest} ${path.basename(artifact)}`)) {
throw new Error("The app release checksum sidecar does not match the ZIP");
}
await access(path.join(portal, "scripts", "assemble.mjs"));
await run(
process.platform === "win32" ? "npm.cmd" : "npm",
["run", "build"],
portal,
);
const temporary = await mkdtemp(
path.join(os.tmpdir(), "svg-tools-portal-smoke-"),
);
try {
const example = JSON.parse(
await readFile(
path.join(portal, "release", "toolbox.lock.example.json"),
"utf8",
),
);
const lock = {
...example,
releaseVersion: "0.10.0-smoke.0",
portalVersion: portalPackage.version,
apps: [
{
id: "de.add-ideas.svg-tools",
version: appPackage.version,
artifact: pathToFileURL(artifact).href,
sha256: digest,
target: "svg",
},
],
};
const lockFile = path.join(temporary, "toolbox.lock.json");
const output = path.join(temporary, "assembled", "toolbox");
const archive = path.join(temporary, "assembled", "toolbox.zip");
await writeFile(lockFile, `${JSON.stringify(lock, null, 2)}\n`, {
flag: "wx",
});
await run(
process.execPath,
[
path.join(portal, "scripts", "assemble.mjs"),
"--lock",
lockFile,
"--portal-dist",
path.join(portal, "dist"),
"--output",
output,
"--archive",
archive,
],
portal,
);
const catalogue = JSON.parse(
await readFile(path.join(output, "toolbox.catalog.json"), "utf8"),
);
const manifest = JSON.parse(
await readFile(
path.join(output, "apps", "svg", "toolbox-app.json"),
"utf8",
),
);
const appIndex = await readFile(
path.join(output, "apps", "svg", "index.html"),
"utf8",
);
if (
catalogue.apps.length !== 1 ||
catalogue.apps[0]?.manifest !== "./apps/svg/toolbox-app.json" ||
manifest.id !== "de.add-ideas.svg-tools" ||
manifest.version !== appPackage.version ||
!manifest.assets?.includes("./canvas-frame-controller.js") ||
/\b(?:src|href)=["']\//iu.test(appIndex)
) {
throw new Error(
"Assembled SVG Tools identity or relocatable assets changed",
);
}
for (const relative of [
"apps/svg/favicon.svg",
"apps/svg/canvas-frame-controller.js",
"apps/svg/LICENSE",
"apps/svg/SOURCE.md",
"toolbox.release.json",
]) {
const file = await stat(path.join(output, relative)).catch(() => null);
if (!file?.isFile()) throw new Error(`Assembly is missing ${relative}`);
}
process.stdout.write(
`Portal assembly smoke passed for SVG Tools ${appPackage.version} (${digest})\n`,
);
} finally {
await rm(temporary, { recursive: true, force: true });
}