import { execFileSync } from "node:child_process"; import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { join, extname } from "node:path"; import { hash as blake3 } from "blake3-wasm"; import { fileURLToPath } from "node:url"; import { createHash } from "node:crypto"; import { parse } from "jsonc-parser"; const root = fileURLToPath(new URL("../", import.meta.url)); const output = join(root, "dist/release"); const readJson = async (path) => JSON.parse(await readFile(join(root, path), "utf8")); const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); // Only package the allowlisted build outputs; never read local secrets or state. await rm(output, { recursive: true, force: true }); await mkdir(output, { recursive: true }); execFileSync( "pnpm", [ "exec", "wrangler", "deploy", "--dry-run", "--outdir", "dist/release/worker", ], { cwd: root, stdio: "inherit", }, ); await rm(join(output, "worker/README.md"), { force: true }); // Debug maps remain in the normal build, outside the deployable inventory. for (const file of await readdir(join(output, "worker"))) if (file.endsWith(".map")) await rm(join(output, "worker", file)); await cp(join(root, "dist/client"), join(output, "assets"), { recursive: true, }); const configErrors = []; const config = parse( await readFile(join(root, "wrangler.jsonc"), "utf8"), configErrors, { allowTrailingComma: true }, ); if (configErrors.length) throw new Error("Invalid wrangler.jsonc"); const contract = await readJson("deployment/manifest.json"); const pkg = await readJson("package.json"); // Account-specific fields are supplied only when installing this artifact. const platform = Object.fromEntries( [ "compatibility_date", "compatibility_flags", "assets", "durable_objects", "exports", "containers", "ai", "browser", "worker_loaders", "observability", "keep_vars", ] .filter((key) => key in config) .map((key) => [key, config[key]]), ); platform.assets = { ...platform.assets, directory: "./assets" }; // `remote` controls local development, not the installed AI binding contract. platform.ai = { binding: platform.ai.binding }; const deployment = { ...platform, main: "./worker/index.js", no_bundle: true }; await writeFile( join(output, "deployment.json"), JSON.stringify(deployment, null, 2) + "\n", ); async function inventory(directory, prefix = "") { const files = []; for (const entry of (await readdir(directory, { withFileTypes: true })).sort( (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0), )) { const path = prefix + entry.name; if (entry.isDirectory()) files.push(...(await inventory(join(directory, entry.name), path + "/"))); else { if (!entry.isFile()) throw new Error(`Unexpected artifact entry: ${path}`); const bytes = await readFile(join(directory, entry.name)); const extension = extname(path).slice(1); const mime = { js: "application/javascript", css: "text/css", html: "text/html", svg: "image/svg+xml", png: "image/png", ico: "image/x-icon", woff2: "font/woff2", txt: "text/plain", json: "application/json", }[extension]; if (!mime) throw new Error(`Unsupported artifact MIME: ${path}`); files.push({ path, size: bytes.length, sha256: sha256(bytes), mime, ...(path.startsWith("assets/") ? { assetHash: blake3(bytes.toString("base64") + extension) .toString("hex") .slice(0, 32), } : {}), }); } } return files; } const manifest = { ...contract, release: pkg.version, sourceRevision: execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", }).trim(), sourceDirty: execFileSync("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: root, encoding: "utf8", }).trim().length > 0, lockfileSha256: sha256(await readFile(join(root, "pnpm-lock.yaml"))), deployment: "deployment.json", files: await inventory(output), }; delete manifest.wranglerConfig; const bytes = JSON.stringify(manifest, null, 2) + "\n"; await writeFile(join(output, "manifest.json"), bytes); await writeFile(join(output, "manifest.sha256"), sha256(bytes) + "\n"); console.log(`Customer release ${manifest.release}: ${sha256(bytes)}`);