#!/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 }); }