// Install each demo/apps/ fixture from its lockfile, pin its public/ mtimes // (scripts/fixed-mtime.mjs) and snapshot it to dist/fixtures/.snap, and, for an // app with a lockfile, its own files alone to .src.snap. `npm ci` is skipped // when node_modules already matches the lock (npm's hidden lockfile records what is // installed), and for an app with no lockfile, which has no dependencies. // dist/fixtures/index.json catalogs the apps for the slopbox shell, with what it // downloads for each and how many packages it installs. import { execFileSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { pinMtimes } from "./fixed-mtime.mjs"; import { platformAllows } from "../src/kernel/npm/lockfile.ts"; import { snapshot, encodeSnapshot } from "../src/node/snapshot.ts"; const ROOT = resolve(fileURLToPath(new URL("..", import.meta.url))); const APPS = join(ROOT, "demo/apps"); const OUT = join(ROOT, "dist/fixtures"); // npm-install-checks' libc family for this host: none off linux. const HOST_LIBC = process.platform !== "linux" ? undefined : process.report.getReport().header.glibcVersionRuntime ? "glibc" : "musl"; function installed(appDir) { const lock = join(appDir, "package-lock.json"); const hidden = join(appDir, "node_modules/.package-lock.json"); if (!existsSync(hidden)) return false; const wantLock = JSON.parse(readFileSync(lock, "utf8")); const haveLock = JSON.parse(readFileSync(hidden, "utf8")); // The hidden lockfile names the app too, and the snapshot carries it: a renamed app // needs a fresh `npm ci` even when every package matches. if (haveLock.name !== wantLock.name) return false; const want = wantLock.packages; const have = haveLock.packages; const same = (k) => want[k] && have[k].version === want[k].version && have[k].integrity === want[k].integrity; // npm leaves an optional package its os/cpu excluded out of the hidden lockfile. const skipped = (k) => want[k].optional === true && !platformAllows(want[k], process.platform, process.arch, HOST_LIBC); return Object.keys(have).every(same) && Object.keys(want).every((k) => k === "" || k in have || skipped(k)); } const ENTRY_CANDIDATES = ["server.mjs", "server.cjs", "server.js"]; function entryOf(appDir, pkg) { if (pkg.main) return pkg.main; const found = ENTRY_CANDIDATES.find((file) => existsSync(join(appDir, file))); if (!found) throw new Error(`${appDir}: no package.json main and none of ${ENTRY_CANDIDATES.join(", ")}`); return found; } mkdirSync(OUT, { recursive: true }); const catalog = []; for (const name of readdirSync(APPS).sort()) { const appDir = join(APPS, name); if (!existsSync(join(appDir, "package.json"))) continue; const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf8")); const locked = existsSync(join(appDir, "package-lock.json")); if (locked && !installed(appDir)) { execFileSync("npm", ["ci", "--prefix", appDir, "--no-audit", "--no-fund"], { stdio: "inherit" }); } pinMtimes(join(appDir, "public")); const full = snapshot(appDir, "/app"); writeFileSync(join(OUT, `${name}.snap`), encodeSnapshot(full)); // slopbox downloads an app's own files and installs the rest. An app without a // lockfile has no dependencies, so its .snap is already its own files. const own = locked ? snapshot(appDir, "/app", { modules: false }) : full; const shipped = encodeSnapshot(own); if (locked) writeFileSync(join(OUT, `${name}.src.snap`), shipped); const files = own.files.length; const bytes = shipped.length; catalog.push({ name, title: pkg.slopbox?.title ?? pkg.name, description: pkg.description ?? "", kind: pkg.slopbox?.kind ?? "server", // Apps run as production servers unless they say otherwise (a dev server does). env: pkg.slopbox?.env ?? { NODE_ENV: "production" }, entry: entryOf(appDir, pkg), install: locked, // Directories, relative to the app, that slopbox keeps across visits (runtime.persist). persist: pkg.slopbox?.persist ?? [], bytes, files, ...(locked && { packages: Object.keys(JSON.parse(readFileSync(join(appDir, "package-lock.json"), "utf8")).packages).length - 1 }), }); console.log(`fixture ${name}: ${files} files, ${bytes} bytes${locked ? " before install" : ""}`); } writeFileSync(join(OUT, "index.json"), JSON.stringify(catalog, null, 2));