pcbjam/scripts/deploy/lib/cdn-store.mjs
Gergő Törcsvári 5248f3a464
fix(demo): default WRANGLER_CMD to npx + silence expected R2 not-found probes
The publish scripts shelled bare `wrangler` (ENOENT without a global install)
and the existence probes (registry.json / per-tool meta.json) printed wrangler's
'key does not exist' on a fresh bucket. Default to `npx wrangler@4` and capture
stderr on probes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:31:06 +02:00

142 lines
5.2 KiB
JavaScript

// Shared CDN publish primitives for the demo-deploy pipeline (publish-wasm,
// publish-content). A pluggable object store with two drivers — `local` (writes
// the exact bucket layout to a dir + a `_uploads.json` HTTP-metadata sidecar, so
// publishing is verifiable offline) and `r2` (shells `wrangler r2 object
// {get,put}`, needing only CLOUDFLARE_API_TOKEN). See docs/features/demo-deploy/.
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { execFileSync } from "node:child_process";
import { gzipSync, brotliCompressSync, constants as zc } from "node:zlib";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
export const IMMUTABLE = "public, max-age=31536000, immutable";
export const NO_STORE = "no-store";
export const sha256hex = (buf) => createHash("sha256").update(buf).digest("hex");
/** Compress bytes; returns { bytes, encoding }. mode: "gzip" | "br" | "none". */
export function compressBytes(bytes, mode, quality) {
if (mode === "none") return { bytes, encoding: null };
if (mode === "gzip")
return { bytes: gzipSync(bytes, { level: quality ?? 6 }), encoding: "gzip" };
// brotli — default quality 5 (good ratio, far faster than 11 on 100s of MB).
return {
bytes: brotliCompressSync(bytes, {
params: { [zc.BROTLI_PARAM_QUALITY]: quality ?? 5 },
}),
encoding: "br",
};
}
/** Content-Type from a file extension (best-effort; covers KiCad + web assets). */
export function contentTypeForPath(name) {
if (name.endsWith(".wasm")) return "application/wasm";
if (name.endsWith(".js") || name.endsWith(".mjs")) return "text/javascript";
if (name.endsWith(".json")) return "application/json";
if (name.endsWith(".html")) return "text/html";
if (name.endsWith(".svg")) return "image/svg+xml";
if (name.endsWith(".png")) return "image/png";
// KiCad project/board/schematic/lib files are s-expr / ini text.
if (/\.(kicad_\w+|net|csv|pos|drl|gbr|g[a-z0-9]+)$/i.test(name))
return "text/plain; charset=utf-8";
return "application/octet-stream";
}
// --- drivers ------------------------------------------------------------------
// Interface:
// getJSON(key) -> object | null (object stored uncompressed)
// put(key, bytes, { contentType, contentEncoding, cacheControl })
export function localDriver(outDir) {
const uploadsPath = join(outDir, "_uploads.json");
const uploads = existsSync(uploadsPath)
? JSON.parse(readFileSync(uploadsPath, "utf8"))
: {};
const abs = (key) => join(outDir, key);
return {
kind: "local",
getJSON(key) {
const p = abs(key);
return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null;
},
put(key, bytes, meta) {
const p = abs(key);
mkdirSync(dirname(p), { recursive: true });
writeFileSync(p, bytes);
uploads[key] = { ...meta, bytes: bytes.length };
mkdirSync(outDir, { recursive: true });
writeFileSync(uploadsPath, JSON.stringify(uploads, null, 2));
},
};
}
export function r2Driver({ bucket, remote }) {
// Default to `npx wrangler` so no global install is needed (override via env).
const wrangler = process.env.WRANGLER_CMD?.split(" ") || ["npx", "--yes", "wrangler@4"];
const flags = remote ? ["--remote"] : [];
// quiet: capture stderr instead of inheriting it — used for existence PROBES,
// where wrangler's "key does not exist" on a fresh bucket is expected + caught.
const run = (args, { quiet = false } = {}) =>
execFileSync(wrangler[0], [...wrangler.slice(1), ...args], {
stdio: ["pipe", "pipe", quiet ? "pipe" : "inherit"],
maxBuffer: 1024 * 1024 * 512,
});
const tmp = join(tmpdir(), `r2put-${process.pid}`);
return {
kind: "r2",
getJSON(key) {
const dest = `${tmp}-get`;
try {
// Probe: a missing object is the normal "not published yet" case.
run(["r2", "object", "get", `${bucket}/${key}`, "--file", dest, ...flags], {
quiet: true,
});
} catch {
return null; // not found / error → absent (expected on first publish)
}
try {
return JSON.parse(readFileSync(dest, "utf8"));
} finally {
rmSync(dest, { force: true });
}
},
put(key, bytes, meta) {
mkdirSync(dirname(tmp), { recursive: true });
writeFileSync(tmp, bytes);
const args = [
"r2", "object", "put", `${bucket}/${key}`,
"--file", tmp,
"--content-type", meta.contentType,
"--cache-control", meta.cacheControl,
...flags,
];
if (meta.contentEncoding) args.push("--content-encoding", meta.contentEncoding);
run(args);
rmSync(tmp, { force: true });
},
};
}
/** Construct the driver named by `driver` ("local" | "r2"). */
export function makeStore(driver, opts) {
if (driver === "local") return localDriver(opts.out);
if (driver === "r2") return r2Driver(opts);
throw new Error(`unknown driver: ${driver}`);
}
/** Put a JSON value (pretty-printed, uncompressed) with the given Cache-Control. */
export function putJSON(store, key, obj, cacheControl) {
store.put(key, Buffer.from(JSON.stringify(obj, null, 2)), {
contentType: "application/json",
contentEncoding: null,
cacheControl,
});
}