feat: introduce local-first SVG workbench
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user