Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912// runtime.install() writes node_modules from an app's package-lock.json. The tree it writes// is compared with dist/fixtures/<name>.snap, which is what `npm ci` wrote on the// machine that ran `npm run fixtures`, through the same file filter.// Its .bin links are compared with the links `npm ci` made in demo/apps/<name>.// Without a lockfile that satisfies package.json it resolves first, from registry// metadata page.route serves out of a recording, and writes package-lock.json, which// is compared with the one real npm wrote from that recording.// Tarballs come from registry.npmjs.org through tests/tarball-cache.ts: fetched once,// then served from disk.import { readdirSync, readFileSync, readlinkSync } from "node:fs";import { test, expect, type BrowserContext, type Page } from "@playwright/test";import type { InstallReport, Kernel } from "../src/host/index";// @ts-expect-error: an untyped .mjs scriptimport { fakePackument, loadRecording } from "../scripts/npm-oracle.mjs";import { cacheTarballs } from "./tarball-cache";
declare global { interface Window { __runtime?: Kernel }}
type Listing = Record<string, string>;
test.beforeEach(async ({ context }) => cacheTarballs(context));
/** Boot a runtime on `page`, mount the app's own files and install from its lockfile. */async function install(page: Page, app: string): Promise<{ report: InstallReport; files: Listing; bins: Listing }> { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const report = await page.evaluate(async (app) => { const runtime = window.__runtime!; runtime.mountSnapshot(await (await fetch(`/dist/fixtures/${app}.src.snap`)).arrayBuffer()); return await runtime.install(); }, app); return { report, ...await listModules(page) };}
/** Boot a runtime on `page`, write `lock` as /app/package-lock.json and install from it. */async function installLockfile(page: Page, lock: string, extra: Record<string, string> = {}): Promise<{ report: InstallReport; files: Listing; bins: Listing }> { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const report = await page.evaluate(async ({ lock, extra }) => { const runtime = window.__runtime!; runtime.writeTree({ ...extra, "/app/package-lock.json": lock }); return await runtime.install(); }, { lock, extra }); return { report, ...await listModules(page) };}
/** Every file under /app/node_modules with the SHA-256 of its bytes, and every link * with its target, keyed relative to /app as `bins` on disk is. */async function listModules(page: Page): Promise<{ files: Listing; bins: Listing }> { return await page.evaluate(async () => { const runtime = window.__runtime!; const files: Record<string, string> = {}; const bins: Record<string, string> = {}; const walk = async (dir: string): Promise<void> => { for (const entry of await runtime.fs.readdir(dir)) { const path = `${dir}/${entry.name}`; if (entry.kind === "dir") await walk(path); else if (entry.kind === "symlink") bins[path.slice("/app/".length)] = await runtime.fs.readlink(path); else files[path] = [...new Uint8Array(await crypto.subtle.digest("SHA-256", await runtime.fs.readFile(path) as Uint8Array<ArrayBuffer>))].map((b) => b.toString(16).padStart(2, "0")).join(""); } }; await walk("/app/node_modules"); return { files, bins }; });}
/** Every link `npm ci` made under the app's node_modules on disk (the .bin links), * read relative to the repo root, where Playwright runs. */function diskBins(app: string): Listing { const appDir = `demo/apps/${app}`; const out: Listing = {}; const walk = (dir: string): void => { for (const entry of readdirSync(`${appDir}/${dir}`, { withFileTypes: true })) { const path = `${dir}/${entry.name}`; if (entry.isSymbolicLink()) out[path] = readlinkSync(`${appDir}/${path}`); else if (entry.isDirectory()) walk(path); } }; walk("node_modules"); return out;}
const sha256 = async (bytes: Uint8Array): Promise<string> => [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes as Uint8Array<ArrayBuffer>))].map((b) => b.toString(16).padStart(2, "0")).join("");
/** The same listing, from the snapshot of what `npm ci` installed: a v3 .snap is "NIBS", * a u32le header length, a JSON header of [path, size, mtimeMs] and the bytes in order. */async function snapshotListing(page: Page, app: string): Promise<Listing> { const snap = new Uint8Array(await (await page.request.get(`/dist/fixtures/${app}.snap`)).body()); const headerLength = new DataView(snap.buffer, snap.byteOffset).getUint32(4, true); const header = JSON.parse(new TextDecoder().decode(snap.subarray(8, 8 + headerLength))) as { files: Array<[string, number, number]> }; const out: Listing = {}; let at = 8 + headerLength; for (const [path, size] of header.files) { if (path.startsWith("/app/node_modules/")) out[path] = await sha256(snap.subarray(at, at + size)); at += size; } return out;}
function diff(installed: Listing, snapshot: Listing) { const paths = [...new Set([...Object.keys(installed), ...Object.keys(snapshot)])].sort(); return { onlyInstalled: paths.filter((p) => !(p in snapshot)), onlySnapshot: paths.filter((p) => !(p in installed)), differ: paths.filter((p) => p in installed && p in snapshot && installed[p] !== snapshot[p]), };}
const SAME = { onlyInstalled: [], onlySnapshot: [], differ: [] };
// A lockfile that satisfies package.json resolves nothing, so no registry document is read.const NO_METADATA = { cached: 0, fetched: 0 };
// The fixtures were installed on darwin; the installer is a wasm32 linux. So vite's// install skips fsevents, which the snapshot has, and the linux lzma binding, as// `npm ci --os linux --cpu wasm32 --libc glibc` does, and npm's hidden lockfile, which// lists what was installed, differs.const CASES: Array<{ app: string; report: Omit<InstallReport, "ms">; diff: ReturnType<typeof diff> }> = [ { app: "clack", report: { bins: 0, packages: 6, files: 28, bytes: 165011, cached: 0, fetched: 6, skipped: [], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 6, removed: 0, changed: 0 } }, diff: SAME }, { app: "esbuild", report: { bins: 1, packages: 1, files: 13, bytes: 14531526, cached: 0, fetched: 1, skipped: [], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 1, removed: 0, changed: 0 } }, diff: SAME }, { app: "express", report: { bins: 0, packages: 65, files: 469, bytes: 1594932, cached: 0, fetched: 65, skipped: [], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 65, removed: 0, changed: 0 } }, diff: SAME }, { app: "hono", report: { bins: 0, packages: 2, files: 601, bytes: 1417765, cached: 0, fetched: 2, skipped: [], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 2, removed: 0, changed: 0 } }, diff: SAME }, { app: "ws", report: { bins: 0, packages: 1, files: 18, bytes: 136104, cached: 0, fetched: 1, skipped: [], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 1, removed: 0, changed: 0 } }, diff: SAME }, { app: "vite", report: { bins: 11, packages: 62, files: 1789, bytes: 33878866, cached: 0, fetched: 62, skipped: ["node_modules/@napi-rs/lzma-linux-x64-gnu", "node_modules/fsevents"], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 62, removed: 0, changed: 0 } }, diff: { onlyInstalled: [], onlySnapshot: ["/app/node_modules/fsevents/LICENSE", "/app/node_modules/fsevents/fsevents.d.ts", "/app/node_modules/fsevents/fsevents.js", "/app/node_modules/fsevents/package.json"], differ: ["/app/node_modules/.package-lock.json"], }, },];
for (const { app, report: expected, diff: expectedDiff } of CASES) { test(`${app}: the installed node_modules is the tree npm ci wrote, from the network and from the store`, async ({ page, context }) => { if (app === "vite") test.setTimeout(180_000); const snapshot = await snapshotListing(page, app); const cold = await install(page, app); const warm = await install(await context.newPage(), app); const { ms, ...report } = cold.report; expect(report).toEqual(expected); expect(ms).toBeGreaterThan(0); const { ms: _, ...warmReport } = warm.report; expect(warmReport).toEqual({ ...expected, cached: expected.fetched, fetched: 0 }); for (const { files, bins } of [cold, warm]) { expect(diff(files, snapshot)).toEqual(expectedDiff); expect(bins).toEqual(diskBins(app)); expect(Object.keys(bins).length).toBe(expected.bins); } });}
test("the .bin links are the ones npm ci made, by name and target", () => { expect([diskBins("esbuild"), diskBins("vite")]).toEqual([ { "node_modules/.bin/esbuild": "../esbuild-wasm/bin/esbuild" }, { "node_modules/.bin/baseline-browser-mapping": "../baseline-browser-mapping/dist/cli.cjs", "node_modules/.bin/browserslist": "../browserslist/cli.js", "node_modules/.bin/esbuild": "../esbuild/bin/esbuild", "node_modules/.bin/jsesc": "../jsesc/bin/jsesc", "node_modules/.bin/json5": "../json5/lib/cli.js", "node_modules/.bin/nanoid": "../nanoid/bin/nanoid.cjs", "node_modules/.bin/parser": "../@babel/parser/bin/babel-parser.js", "node_modules/.bin/rollup": "../rollup/dist/bin/rollup", "node_modules/.bin/semver": "../semver/bin/semver.js", "node_modules/.bin/update-browserslist-db": "../update-browserslist-db/cli.js", "node_modules/.bin/vite": "../vite/bin/vite.js", }, ]);});
const SEMVER = { name: "semver", version: "7.7.2", resolved: "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", integrity: "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",};
// Four aliases of one package claiming one bin name, with bins npm sanitizes. The// expected links are the ones npm 11.7 `npm ci` made from this lockfile: "a" collates// before "B" and claims "semver" though its file is missing, so neither "B" nor "z"// links it, and the root's own bin is not linked.const CONTESTED = JSON.stringify({ name: "contested", lockfileVersion: 3, requires: true, packages: { "": { name: "contested", dependencies: { a: "npm:semver@7.7.2", B: "npm:semver@7.7.2", z: "npm:semver@7.7.2" }, bin: { own: "own.js" } }, "node_modules/a": { ...SEMVER, bin: { semver: "missing.js", "../evil": "../../bin/semver.js" } }, "node_modules/B": { ...SEMVER, bin: { semver: "bin/semver.js", "nested/name": "./bin/semver.js" } }, "node_modules/z": { ...SEMVER, bin: { semver: "bin/semver.js", "..": "x.js" } }, "node_modules/B/node_modules/c": { ...SEMVER, bin: { semver: "bin/semver.js" } }, },});
test("a contested bin name goes to the first package in collation order, as npm ci links it", async ({ page }) => { const { report, bins } = await installLockfile(page, CONTESTED, { "/app/own.js": "#!/usr/bin/env node\n" }); expect(report.bins).toBe(3); expect(bins).toEqual({ "node_modules/.bin/evil": "../a/bin/semver.js", "node_modules/.bin/name": "../B/bin/semver.js", "node_modules/B/node_modules/.bin/semver": "../c/bin/semver.js", });});
const LZMA = (libc: "gnu" | "musl", integrity: string) => ({ version: "1.5.1", resolved: `https://registry.npmjs.org/@napi-rs/lzma-linux-x64-${libc}/-/lzma-linux-x64-${libc}-1.5.1.tgz`, integrity, cpu: ["x64"], license: "MIT", optional: true, os: ["linux"], libc: [libc === "gnu" ? "glibc" : "musl"], engines: { node: "^22.20 || ^24.12 || >=25" },});
// npm 11.7 writes this lockfile without the libc lines, because the registry's// abbreviated packument leaves libc out. `npm ci --os linux --cpu wasm32 --libc glibc`// from it installs neither binding: both want an x64 cpu.const LIBC_LOCK = JSON.stringify({ name: "libc", version: "1.0.0", lockfileVersion: 3, requires: true, packages: { "": { name: "libc", version: "1.0.0", optionalDependencies: { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@napi-rs/lzma-linux-x64-musl": "1.5.1" } }, "node_modules/@napi-rs/lzma-linux-x64-gnu": LZMA("gnu", "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="), "node_modules/@napi-rs/lzma-linux-x64-musl": LZMA("musl", "sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg=="), },});
test("the installer is a wasm32 linux: every x64 binding is skipped", async ({ page }) => { const { report: { ms: _, ...report }, files } = await installLockfile(page, LIBC_LOCK); expect(report).toEqual({ packages: 0, files: 0, bytes: 0, bins: 0, cached: 0, fetched: 0, skipped: ["node_modules/@napi-rs/lzma-linux-x64-gnu", "node_modules/@napi-rs/lzma-linux-x64-musl"], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 0, removed: 0, changed: 0 }, }); expect(Object.keys(files).sort()).toEqual(["/app/node_modules/.package-lock.json"]);});
// Resolved from package.json alone, the registry's abbreviated packument has no libc;// npm resolves from full packuments, so it skips a binding for the other libc. These// docs drop `cpu` so that libc alone decides; the version documents carry it.test("resolving from package.json, a -musl binding is skipped by the libc its version document names", async ({ page, context }) => { const docs = Object.fromEntries((["gnu", "musl"] as const).map((libc) => { const name = `@napi-rs/lzma-linux-x64-${libc}`; const { optional: _, cpu: ___, resolved, integrity, ...entry } = LZMA(libc, JSON.parse(LIBC_LOCK).packages[`node_modules/${name}`].integrity); const full = { name, ...entry, dist: { tarball: resolved, integrity } }; const { libc: __, ...abbreviated } = full; return [name, { full, packument: { name, "dist-tags": { latest: "1.5.1" }, versions: { "1.5.1": abbreviated } } }]; })); const log: string[] = []; await context.route("https://registry.npmjs.org/**", async (route) => { const path = decodeURIComponent(new URL(route.request().url()).pathname.slice(1)); if (path.includes("/-/")) return route.fallback(); log.push(`/${path}`); const [scope, name, version] = path.split("/"); const doc = docs[`${scope}/${name}`]; const body = version ? doc?.full : doc?.packument; const cors = { "access-control-allow-origin": "*" }; if (!body) return route.fulfill({ status: 404, headers: cors, contentType: "application/json", body: '{"error":"Not found"}' }); return route.fulfill({ headers: cors, contentType: "application/json", body: JSON.stringify(body) }); }); const { report, error } = await installFiles(page, { "/app/package.json": JSON.stringify(JSON.parse(LIBC_LOCK).packages[""]) }); expect(error).toBeNull(); expect([report!.lockfile, report!.packages, report!.skipped]).toEqual(["written", 1, ["node_modules/@napi-rs/lzma-linux-x64-musl"]]); expect(log.sort()).toEqual([ "/@napi-rs/lzma-linux-x64-gnu", "/@napi-rs/lzma-linux-x64-gnu/1.5.1", "/@napi-rs/lzma-linux-x64-musl", "/@napi-rs/lzma-linux-x64-musl/1.5.1", ]); expect(Object.keys((await listModules(page)).files).sort()).toEqual([ "/app/node_modules/.package-lock.json", "/app/node_modules/@napi-rs/lzma-linux-x64-gnu/package.json", ]);});
// tests/fixtures/wasm32-oxide is what `npm install @tailwindcss/oxide --os linux --cpu// wasm32 --libc glibc` (npm 11.7) wrote. It lists every platform's binding; npm installed// the wasm32 one, its bundled emnapi runtime inside it, and skipped the eleven native ones.test("a package's wasm32 binding installs in place of its native ones, as npm --cpu wasm32 installs it", async ({ page }) => { const lock = readFileSync("tests/fixtures/wasm32-oxide/package-lock.json", "utf8"); const natives = Object.entries(JSON.parse(lock).packages as Record<string, { cpu?: string[] }>) .filter(([, entry]) => entry.cpu && !entry.cpu.includes("wasm32")).map(([path]) => path).sort(); const { report, files } = await installLockfile(page, lock); expect(natives).toHaveLength(11); expect([report.lockfile, [...report.skipped].sort()]).toEqual(["kept", natives]); expect(Object.keys(files).filter((f) => f.endsWith("/package.json")).sort()).toEqual([ "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib/modules/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib/package.json", "/app/node_modules/@tailwindcss/oxide-wasm32-wasi/package.json", "/app/node_modules/@tailwindcss/oxide/package.json", ]);});
// fsevents 1.2.13 is darwin-only and depends on bindings and nan. The root depends on// bindings too, and on fsevents optionally. The lockfile is what npm 11.7 wrote for// this package.json; `npm ci --os linux --cpu x64` from it skips nan with fsevents,// since only fsevents needed it, and keeps bindings, which the root needs.const OPTIONAL_ONLY_PACKAGE = { name: "optional-only", version: "1.0.0", dependencies: { bindings: "1.5.0" }, optionalDependencies: { fsevents: "1.2.13" } };const OPTIONAL_ONLY_LOCK = `${JSON.stringify({ name: "optional-only", version: "1.0.0", lockfileVersion: 3, requires: true, packages: { "": { name: "optional-only", version: "1.0.0", dependencies: { bindings: "1.5.0", fsevents: "1.2.13" }, optionalDependencies: { fsevents: "1.2.13" } }, "node_modules/bindings": { version: "1.5.0", resolved: "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", integrity: "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", license: "MIT", dependencies: { "file-uri-to-path": "1.0.0" }, }, "node_modules/file-uri-to-path": { version: "1.0.0", resolved: "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", integrity: "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", license: "MIT", }, "node_modules/fsevents": { version: "1.2.13", resolved: "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", integrity: "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", deprecated: "Upgrade to fsevents v2 to mitigate potential security issues", hasInstallScript: true, license: "MIT", optional: true, os: ["darwin"], dependencies: { bindings: "^1.5.0", nan: "^2.12.1" }, engines: { node: ">= 4.0" }, }, "node_modules/nan": { version: "2.30.0", resolved: "https://registry.npmjs.org/nan/-/nan-2.30.0.tgz", integrity: "sha512-P5mokDkSvCGBz6mZDD/2l+w9QoICRXdCyZ9ONGmTePr40oihJg3Kpn0YU3twKd6PJnwSlLBWNQeOT6yWEV2c4A==", license: "MIT", optional: true, }, },}, null, 2)}\n`;
test("a package only a platform-skipped optional needs is skipped with it, as npm ci skips it", async ({ page }) => { const { report: { ms: _, ...report }, files } = await installLockfile(page, OPTIONAL_ONLY_LOCK, { "/app/package.json": JSON.stringify(OPTIONAL_ONLY_PACKAGE) }); expect(report).toEqual({ packages: 2, files: 10, bytes: 12208, bins: 0, cached: 0, fetched: 2, skipped: ["node_modules/fsevents", "node_modules/nan"], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 2, removed: 0, changed: 0 }, }); // SHA-256 of each file `npm ci --os linux --cpu x64` wrote that keepInPackage keeps. expect(files).toEqual({ "/app/node_modules/.package-lock.json": "f20003f45df0320f4c3d82e26fb6527a0766e6caa42be2292b6a6012240197f2", "/app/node_modules/bindings/bindings.js": "8e32a0d37f20bd6f7d5bdbf99d041aa27be47cbbe5172ac13ebf7380a10b3bf6", "/app/node_modules/bindings/package.json": "a87721fe406e1f1798fef44d697b46ea1efe346fda118010334713346ee4207c", "/app/node_modules/file-uri-to-path/.npmignore": "bfb16fc2dc3b7f24e20981aa586e31d66293c81eb7c2e5d0a9bbb252b13c6271", "/app/node_modules/file-uri-to-path/.travis.yml": "14cfedcd0d39a797fb56787bc06b46d09aa2786084bb0dec49467b9720fb2979", "/app/node_modules/file-uri-to-path/LICENSE": "75070206e4ee2a749be52444cecc05101ca8d314c9d6d335ff914046cbf086a0", "/app/node_modules/file-uri-to-path/index.d.ts": "46eb38b4f992126135e00159041a6e69e46402e56613bdc891d7b0d28fc31e37", "/app/node_modules/file-uri-to-path/index.js": "e62293e871bdd5a7449ff3c7956c9536ec1d2ea7369461de77322b5256bb93e7", "/app/node_modules/file-uri-to-path/package.json": "71eb1e24bb9694f89c613fa0aa307f977dd43f41d11794c7b48fabf6c55f66b0", "/app/node_modules/file-uri-to-path/test/test.js": "223c25dba6fefbb7cb491b026a32f070b1425475d3cd2d04627be5d71a07fd38", "/app/node_modules/file-uri-to-path/test/tests.json": "b48c50c1328ab8522fc0bf91923ea42fb59e63e847b967dac88123cb5e5b185d", });});
test("a non-optional package for another platform fails the install with EBADPLATFORM, as npm ci does", async ({ page }) => { const lock = JSON.parse(LIBC_LOCK); delete lock.packages["node_modules/@napi-rs/lzma-linux-x64-musl"].optional; await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const error = await page.evaluate(async (lock) => { const runtime = window.__runtime!; runtime.writeTree({ "/app/package-lock.json": lock }); return await runtime.install().then(() => null, (e: Error & { code?: string }) => ({ code: e.code, message: e.message })); }, JSON.stringify(lock)); expect(error).toEqual({ code: "EBADPLATFORM", message: 'Unsupported platform for @napi-rs/lzma-linux-x64-musl@1.5.1: wanted {"os":"linux","cpu":"x64","libc":"musl"} (current: {"os":"linux","cpu":"wasm32","libc":"glibc"})', });});
// @isaacs/testing-bundledeps 1.0.0 bundles @isaacs/testing-bundledeps-a, which brings// -b along; the lockfile is what npm 11.7 wrote for `npm install @isaacs/testing-bundledeps`.const BUNDLER = "node_modules/@isaacs/testing-bundledeps";const BUNDLED_PACKAGES = { "": { name: "bundled", version: "1.0.0", dependencies: { "@isaacs/testing-bundledeps": "^1.0.0" } }, [BUNDLER]: { version: "1.0.0", resolved: "https://registry.npmjs.org/@isaacs/testing-bundledeps/-/testing-bundledeps-1.0.0.tgz", integrity: "sha512-P8AF2FoTfHOPGY6W53FHVg9mza6ipzkppAwnbnNNkPaLQIEFTpx3U95ir1AKqmub7nTi115Qi6zHiqJzGe5Cqg==", bundleDependencies: ["@isaacs/testing-bundledeps-a"], dependencies: { "@isaacs/testing-bundledeps-a": "*", "@isaacs/testing-bundledeps-c": "*" }, }, "node_modules/@isaacs/testing-bundledeps-b": { version: "1.0.0", resolved: "https://registry.npmjs.org/@isaacs/testing-bundledeps-b/-/testing-bundledeps-b-1.0.0.tgz", integrity: "sha512-UDbCq7GHRDb743m4VBpnsui6hNeB3o08qe/FRnX9JFo0VHnLoXkdtvm/WurwABLxL/xw5wP/tfN2jF90EjQehQ==", }, "node_modules/@isaacs/testing-bundledeps-c": { version: "2.0.0", resolved: "https://registry.npmjs.org/@isaacs/testing-bundledeps-c/-/testing-bundledeps-c-2.0.0.tgz", integrity: "sha512-nwGzs5eUI+0/+CB2oF7ce3Xu070w38pB//NoV9I9RedeT/+Y4fiOcIbLXYzt/yVJkZEOmTYXa1lVsrTypPvtlA==", dependencies: { "@isaacs/testing-bundledeps-b": "*" }, }, [`${BUNDLER}/node_modules/@isaacs/testing-bundledeps-a`]: { version: "1.0.0", inBundle: true, dependencies: { "@isaacs/testing-bundledeps-b": "*" } }, [`${BUNDLER}/node_modules/@isaacs/testing-bundledeps-b`]: { version: "1.0.0", inBundle: true },};const bundledLock = (packages: Record<string, unknown>) => `${JSON.stringify({ name: "bundled", version: "1.0.0", lockfileVersion: 3, requires: true, packages }, null, 2)}\n`;
// SHA-256 of every file real `npm ci` wrote from each lockfile.const BUNDLER_FILES = { "/app/node_modules/@isaacs/testing-bundledeps/a/package.json": "54e6eedf21a49423b9a4f542fc93f5ca08092e95af6386b07f7df90629d2ad47", "/app/node_modules/@isaacs/testing-bundledeps/b/package.json": "4aeda5eac4e82d4398d0bf3dd180b5191fcbac1da05df6450b10527fe374408d", "/app/node_modules/@isaacs/testing-bundledeps/c/package.json": "91519e53e9922c98f0db3723cd62c73caf21a3b9ab5be31494f49199e363a407", "/app/node_modules/@isaacs/testing-bundledeps/node_modules/@isaacs/testing-bundledeps-a/package.json": "8c817c16e4739bfc1a0c328d0911e803d1908beeafab3d7ae2a8fdf5ad067df3", "/app/node_modules/@isaacs/testing-bundledeps/node_modules/@isaacs/testing-bundledeps-b/package.json": "dcb68ecd4d8808a0eb4e0846f25ff6fa29f2fb3f7b40617489e279254f6a7b84", "/app/node_modules/@isaacs/testing-bundledeps/package.json": "aadbefe0183914297dcd473068a4f1401ffb32b6d38d01bcbd21ad8acc2a6791",};
test("a bundled dependency arrives inside its bundler's tarball and is never fetched, as npm ci writes it", async ({ page }) => { const { report: { ms: _, ...report }, files } = await installLockfile(page, bundledLock(BUNDLED_PACKAGES)); expect(report).toEqual({ packages: 3, files: 8, bytes: 3284, bins: 0, cached: 0, fetched: 3, skipped: [], scripts: [], lockfile: "kept", metadata: NO_METADATA, warnings: [], diff: { added: 3, removed: 0, changed: 0 } }); expect(files).toEqual({ ...BUNDLER_FILES, "/app/node_modules/.package-lock.json": "e53419f113d66ed9e30c6c63263fe6a67d0f9b393668cf689223d4d183cf30c1", "/app/node_modules/@isaacs/testing-bundledeps-b/package.json": "4aeda5eac4e82d4398d0bf3dd180b5191fcbac1da05df6450b10527fe374408d", "/app/node_modules/@isaacs/testing-bundledeps-c/package.json": "917e3d5f043532c649a11a5a6becc1b0fefe54977b810f44f3422559cb97c412", });});
// The same lockfile, with -c moved inside the bundle, which does not hold it. npm ci// then fetches it by name@version (arborist reify.js marks it bundleMissing) and its// hidden lockfile records it without inBundle.test("a bundled dependency missing from its bundler's tarball is fetched from the registry, as npm ci does", async ({ page, context }) => { const { "node_modules/@isaacs/testing-bundledeps-b": _b, "node_modules/@isaacs/testing-bundledeps-c": _c, ...rest } = BUNDLED_PACKAGES; const lock = bundledLock({ ...rest, [`${BUNDLER}/node_modules/@isaacs/testing-bundledeps-c`]: { version: "2.0.0", inBundle: true, dependencies: { "@isaacs/testing-bundledeps-b": "*" } }, }); // The one packument read is -c's, for its tarball URL: the registry's document, cut to // the version the lock names. const c = BUNDLED_PACKAGES["node_modules/@isaacs/testing-bundledeps-c"]; const name = "@isaacs/testing-bundledeps-c"; await serveMetadata(context, (n) => n === name ? { name, "dist-tags": { latest: c.version }, versions: { [c.version]: { name, version: c.version, dependencies: c.dependencies, dist: { tarball: c.resolved, integrity: c.integrity } } }, } : undefined); const { report: { ms: _, ...report }, files } = await installLockfile(page, lock); expect(report).toEqual({ packages: 2, files: 7, bytes: 3178, bins: 0, cached: 0, fetched: 2, skipped: [], scripts: [], lockfile: "kept", metadata: { cached: 0, fetched: 1 }, warnings: [], diff: { added: 1, removed: 0, changed: 0 } }); expect(files).toEqual({ ...BUNDLER_FILES, "/app/node_modules/.package-lock.json": "ccf521bdac7bd31937eb24fe9fbf7d5896ef024e055449ec20155dcc898538eb", "/app/node_modules/@isaacs/testing-bundledeps/node_modules/@isaacs/testing-bundledeps-c/package.json": "917e3d5f043532c649a11a5a6becc1b0fefe54977b810f44f3422559cb97c412", });});
test("an installed bin runs through its .bin link in a child process, as under real Node", async ({ page }) => { await install(page, "esbuild"); const out = await page.evaluate(async () => { const runtime = window.__runtime!; runtime.writeTree({ "/app/version.cjs": ` const { spawn } = require("node:child_process"); const child = spawn("node", ["node_modules/.bin/esbuild", "--version"]); let out = ""; child.stdout.on("data", (c) => out += c); child.on("close", (code) => console.log(JSON.stringify({ code, out }))); `, }); const proc = runtime.exec("/app/version.cjs", { cwd: "/app" }); let stdout = ""; const dec = new TextDecoder(); const piped = proc.stdout.pipeTo(new WritableStream({ write(c) { stdout += dec.decode(c); } })); await proc.exited; await piped; return stdout; }); expect(out).toBe('{"code":0,"out":"0.28.2\\n"}\n');});
const HONO_INDEX = { status: 200, body: '<link rel="stylesheet" href="/static/style.css"><h1 id="hi">Hello from Hono</h1>' };
/** Run the hono app already in the page's FS and answer GET /. */async function honoIndex(page: Page): Promise<{ status: number; body: string }> { return await page.evaluate(async () => { const runtime = window.__runtime!; runtime.exec("/app/server.mjs"); const res = await fetch(await runtime.waitForPort(3000)); return { status: res.status, body: await res.text() }; });}
test("the installed hono app serves / as the snapshot app does", async ({ page }) => { await install(page, "hono"); expect(await honoIndex(page)).toEqual(HONO_INDEX);
await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); await page.evaluate(async () => window.__runtime!.mountSnapshot(await (await fetch("/dist/fixtures/hono.snap")).arrayBuffer())); expect(await honoIndex(page)).toEqual(HONO_INDEX);});
test("a second install, and an install on another page of the origin, take every tarball from the store and write the same tree", async ({ page, context }) => { const first = await install(page, "hono"); const again = await page.evaluate(async () => { const runtime = window.__runtime!; const changed = new Promise<boolean>((resolve) => { const off = runtime.onFsChanged(() => { off(); resolve(true); }); setTimeout(() => resolve(false), 5000); }); const report = await runtime.install(); return { cached: report.cached, fetched: report.fetched, fsChanged: await changed }; }); const reinstalled = (await listModules(page)).files; const other = await install(await context.newPage(), "hono"); expect([first.report.fetched, first.report.cached]).toEqual([2, 0]); expect(again).toEqual({ cached: 2, fetched: 0, fsChanged: true }); expect([other.report.fetched, other.report.cached]).toEqual([0, 2]); expect(reinstalled).toEqual(first.files); expect(other.files).toEqual(first.files);});
/** The package store as the page's origin holds it: every stored package with its size, * and the index's document as it parses (null when absent or unparseable). */async function storeState(page: Page): Promise<{ sizes: Record<string, number>; index: { budget?: number; entries: Record<string, { size: number; lastUse: number }> } | null }> { return await page.evaluate(async () => { const cache = await caches.open("nib-npm-packages-v2"); const sizes: Record<string, number> = {}; for (const req of await cache.keys()) sizes[req.url] = (await (await cache.match(req))!.blob()).size; const hit = await (await caches.open("nib-npm-index-v2")).match("https://nib.invalid/npm-store-index"); return { sizes, index: hit ? await hit.json().catch(() => null) : null }; });}
/** Write `text` as the index's document, or a budget into the one stored. */async function writeIndex(page: Page, edit: { budget: number } | { text: string }): Promise<void> { await page.evaluate(async (edit) => { const cache = await caches.open("nib-npm-index-v2"); const key = "https://nib.invalid/npm-store-index"; const text = "text" in edit ? edit.text : JSON.stringify({ ...await (await cache.match(key))!.json(), budget: edit.budget }); await cache.put(key, new Response(text)); }, edit);}
/** A package's key in the store: the sha512 its lockfile names. */const packageKey = (integrity: string): string => `https://nib.invalid/npm-package/sha512-${encodeURIComponent(integrity.slice("sha512-".length))}`;
/** The store key of each package an app's lockfile names. */function lockedPackages(app: string): string[] { const lock = JSON.parse(readFileSync(`demo/apps/${app}/package-lock.json`, "utf8")) as { packages: Record<string, { integrity?: string }> }; return Object.values(lock.packages).flatMap((p) => (p.integrity ? [packageKey(p.integrity)] : [])).sort();}
const sizesOf = (sizes: Record<string, number>, keys: string[]): number => keys.reduce((total, key) => total + sizes[key]!, 0);
test("over its budget the store deletes the least recently used packages, and never one the install used", async ({ page }) => { test.setTimeout(60_000); const [hono, clack, ws] = [lockedPackages("hono"), lockedPackages("clack"), lockedPackages("ws")];
await install(page, "hono"); await install(page, "clack"); await install(page, "ws"); const before = await storeState(page); expect(Object.keys(before.sizes).sort()).toEqual([...hono, ...clack, ...ws].sort()); expect(Object.keys(before.index!.entries).sort()).toEqual([...hono, ...clack, ...ws].sort()); expect(before.index!.entries[ws[0]!]!.lastUse).toBeGreaterThan(before.index!.entries[clack[0]!]!.lastUse);
// Room for ws and hono: hono is the least recently used but this install uses it, so // every clack package, the next least recently used, goes. await writeIndex(page, { budget: sizesOf(before.sizes, ws) + sizesOf(before.sizes, hono) }); const { report } = await install(page, "hono"); expect([report.fetched, report.cached]).toEqual([0, 2]); const after = await storeState(page); expect(Object.keys(after.sizes).sort()).toEqual([...hono, ...ws].sort()); expect(Object.fromEntries(Object.entries(after.index!.entries).map(([key, e]) => [key, e.size]))).toEqual(after.sizes);
// A budget of one byte keeps only what the install used. await writeIndex(page, { budget: 1 }); expect((await install(page, "ws")).report.cached).toBe(1); expect(Object.keys((await storeState(page)).sizes)).toEqual(ws);});
test("an index that does not parse is rebuilt from the store's packages at their real sizes", async ({ page }) => { await install(page, "hono"); await writeIndex(page, { text: '{"entries": {"https://nib.invalid/npm-package/x": {"size": "big"}}' }); expect((await storeState(page)).index).toBeNull(); await install(page, "ws"); const { sizes, index } = await storeState(page); expect(Object.keys(sizes).sort()).toEqual([...lockedPackages("hono"), ...lockedPackages("ws")].sort()); expect(Object.fromEntries(Object.entries(index!.entries).map(([key, e]) => [key, e.size]))).toEqual(sizes);});
test("the tarball cache an older store left is deleted by the next install", async ({ page }) => { await page.goto("/demo/fixture.html"); await page.evaluate(async () => (await caches.open("niw-npm-v1")).put("https://registry.npmjs.org/x/-/x-1.0.0.tgz", new Response("old"))); await install(page, "ws"); expect(await page.evaluate(() => caches.has("niw-npm-v1"))).toBe(false);});
// is-number is in no fixture's lockfile, so no other test's install has stored it.const IS_NUMBER = "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz";const IS_NUMBER_SRI = "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==";const WRONG_SRI = `sha512-${"A".repeat(86)}==`;
const lockfile = (integrity: string) => JSON.stringify({ name: "integrity", lockfileVersion: 3, requires: true, packages: { "": { name: "integrity", dependencies: { "is-number": "7.0.0" } }, "node_modules/is-number": { version: "7.0.0", resolved: IS_NUMBER, integrity }, },});
const EINTEGRITY = { code: "EINTEGRITY", message: "is-number@7.0.0 (node_modules/is-number): integrity checksum failed when using sha512: " + `wanted ${WRONG_SRI} but got ${IS_NUMBER_SRI}. (3730 bytes)`,};
test("a tarball that fails its integrity check rejects with EINTEGRITY, its package stored or not", async ({ page }) => { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const out = await page.evaluate(async ({ good, bad, key }) => { const runtime = window.__runtime!; const attempt = async (lock: string) => { runtime.writeTree({ "/app/package-lock.json": lock }); const result = await runtime.install().then( (r) => ({ fetched: r.fetched, cached: r.cached }), (e: Error & { code?: string }) => ({ code: e.code, message: e.message }), ); const tree = (await runtime.fs.readdir("/app/node_modules")).map((d) => d.name).sort(); return { result, stored: (await (await caches.open("nib-npm-packages-v2")).match(key)) !== undefined, tree }; }; runtime.writeTree({ "/app/node_modules/before.txt": "kept" }); return { cold: await attempt(bad), good: await attempt(good), warm: await attempt(bad), again: await attempt(good) }; }, { good: lockfile(IS_NUMBER_SRI), bad: lockfile(WRONG_SRI), key: packageKey(IS_NUMBER_SRI) }); expect(out).toEqual({ cold: { result: EINTEGRITY, stored: false, tree: ["before.txt"] }, good: { result: { fetched: 1, cached: 0 }, stored: true, tree: [".package-lock.json", "is-number"] }, warm: { result: EINTEGRITY, stored: true, tree: [".package-lock.json", "is-number"] }, again: { result: { fetched: 0, cached: 1 }, stored: true, tree: [".package-lock.json", "is-number"] }, });});
const registryFixture = (name: string, file: string): string => readFileSync(`tests/fixtures/registry/${name}/${file}`, "utf8");
type Documents = (name: string) => { versions: Record<string, unknown> } | undefined;
/** * Answer registry metadata from `docs`: `GET /<name>` with the packument and * `GET /<name>/<version>` with its `versions[version]`, 404 for anything else. Tarballs * fall through to the tarball cache. Each metadata request lands in the returned log by path. */async function serveMetadata(context: BrowserContext, docs: Documents): Promise<string[]> { const log: string[] = []; await context.route("https://registry.npmjs.org/**", async (route) => { const path = decodeURIComponent(new URL(route.request().url()).pathname.slice(1)); if (path.includes("/-/")) return route.fallback(); log.push(`/${path}`); const at = path.indexOf("/", path.startsWith("@") ? path.indexOf("/") + 1 : 0); const doc = docs(at === -1 ? path : path.slice(0, at)); const body = at === -1 ? doc : doc?.versions[path.slice(at + 1)]; const cors = { "access-control-allow-origin": "*" }; if (body === undefined) return route.fulfill({ status: 404, headers: cors, contentType: "application/json", body: '{"error":"Not found"}' }); return route.fulfill({ headers: cors, contentType: "application/json", body: JSON.stringify(body) }); }); return log;}
/** The packuments real npm read while recording `fixture`. */function recorded(fixture: string): Documents { const recording = loadRecording(fixture) as Map<string, { status: number; body: Uint8Array }>; return (name) => { const rec = recording.get(`/${name.replace("/", "%2f")}`); return rec?.status === 200 ? JSON.parse(new TextDecoder().decode(rec.body)) : undefined; };}
/** Boot a runtime on `page`, write `files` under /app and run runtime.install(). */async function installFiles(page: Page, files: Record<string, string>) { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); return await page.evaluate(async (files) => { const runtime = window.__runtime!; runtime.writeTree(files); return await runtime.install().then( (report) => ({ report, error: null }), (e: Error & { code?: string }) => ({ report: null, error: { code: e.code, message: e.message } }), ); }, files);}
async function readApp(page: Page, path: string): Promise<string | null> { return await page.evaluate(async (path) => { const bytes = await window.__runtime!.fs.readFile(path).catch(() => null); return bytes && new TextDecoder().decode(bytes as Uint8Array); }, path);}
for (const name of ["hono", "express"]) { test(`${name}: package.json alone resolves, installs and writes the package-lock.json npm wrote`, async ({ page, context }) => { const log = await serveMetadata(context, recorded(name)); const pkg = registryFixture(name, "package.json"); const lock = registryFixture(name, "package-lock.json"); const { report, error } = await installFiles(page, { "/app/package.json": pkg }); expect(error).toBeNull(); const packages = Object.keys(JSON.parse(lock).packages).length - 1; expect([report!.lockfile, report!.packages, report!.metadata]).toEqual(["written", packages, { cached: 0, fetched: log.length }]); expect(await readApp(page, "/app/package-lock.json")).toBe(lock); const resolved = await listModules(page);
const again = await page.evaluate(async () => { const runtime = window.__runtime!; const before = (await runtime.fs.stat("/app/package-lock.json")).mtimeMs; const report = await runtime.install(); return { report, touched: (await runtime.fs.stat("/app/package-lock.json")).mtimeMs !== before }; }); expect([again.report.lockfile, again.report.metadata, again.report.cached, again.touched]).toEqual(["kept", { cached: 0, fetched: 0 }, packages, false]); expect(await readApp(page, "/app/package-lock.json")).toBe(lock);
// The same lockfile installed as it is, on a page of its own. const other = await context.newPage(); const locked = await installFiles(other, { "/app/package.json": pkg, "/app/package-lock.json": lock }); expect(locked.report!.lockfile).toBe("kept"); expect(resolved).toEqual(await listModules(other)); });}
// tests/fixtures/registry/hono-stale is hono's lockfile under a package.json whose hono// range it no longer satisfies, recorded with `npm-oracle.mjs record`, which ran real// npm on that seed lockfile: npm read hono's packument and nothing else.test("a lockfile package.json outgrew is rewritten for that package alone, as npm rewrites it", async ({ page, context }) => { const log = await serveMetadata(context, recorded("hono-stale")); const { report, error } = await installFiles(page, { "/app/package.json": registryFixture("hono-stale", "package.json"), "/app/package-lock.json": registryFixture("hono-stale", "seed-lock.json"), }); expect(error).toBeNull(); expect(log).toEqual(["/hono", "/hono/4.12.34"]); expect([report!.lockfile, report!.metadata]).toEqual(["written", { cached: 0, fetched: 2 }]); expect(await readApp(page, "/app/package-lock.json")).toBe(registryFixture("hono-stale", "package-lock.json"));});
test("ERESOLVE rejects with npm's explanation and leaves node_modules and package-lock.json as they were", async ({ page, context }) => { const c = JSON.parse(readFileSync("tests/fixtures/fake/eresolve.json", "utf8")) as { package: unknown; registry: Record<string, Record<string, unknown>>; npm: { stderr: string } }; await serveMetadata(context, (name) => c.registry[name] && fakePackument(name, c.registry[name])); const lock = `${JSON.stringify({ name: "fake-eresolve", lockfileVersion: 3, requires: true, packages: {} }, null, 2)}\n`; const { error } = await installFiles(page, { "/app/package.json": JSON.stringify(c.package), "/app/package-lock.json": lock, "/app/node_modules/before.txt": "kept", }); expect(error!.code).toBe("ERESOLVE"); expect(`code ERESOLVE\nERESOLVE ${error!.message}`).toBe(c.npm.stderr); expect(await readApp(page, "/app/package-lock.json")).toBe(lock); expect(Object.keys((await listModules(page)).files)).toEqual(["/app/node_modules/before.txt"]);});
test("with neither package.json nor package-lock.json, install rejects as npm install does", async ({ page }) => { const { error } = await installFiles(page, {}); expect(error).toEqual({ code: "ENOENT", message: "Could not read package.json: Error: ENOENT: no such file or directory, open '/app/package.json'" });});
// tests/fixtures/wasm-substitutes: package-lock.json is what `npm install// --package-lock-only` (npm 11.7) wrote for its package.json, esbuild and rolldown, and// substitutes-lock.json what it wrote for esbuild-wasm and @rolldown/binding-wasm32-wasi.// The registry served here is their entries as packuments of one version each; every// spec pins that version or a range it satisfies, so npm would pick the same ones.const SUBSTITUTES_DIR = "tests/fixtures/wasm-substitutes";const SUBSTITUTES_PACKAGE = readFileSync(`${SUBSTITUTES_DIR}/package.json`, "utf8");const SUBSTITUTES_LOCK = readFileSync(`${SUBSTITUTES_DIR}/package-lock.json`, "utf8");const SUBSTITUTES_OWN_LOCK = readFileSync(`${SUBSTITUTES_DIR}/substitutes-lock.json`, "utf8");
type LockEntry = Record<string, unknown> & { version: string; resolved: string; integrity: string };
function lockDocuments(...locks: string[]): Documents { const docs = new Map<string, { name: string; "dist-tags": { latest: string }; versions: Record<string, unknown> }>(); for (const lock of locks) { for (const [location, entry] of Object.entries(JSON.parse(lock).packages as Record<string, LockEntry>)) { if (location === "") continue; const name = location.slice(location.lastIndexOf("node_modules/") + "node_modules/".length); const { resolved, integrity, optional: _, ...fields } = entry; docs.set(name, { name, "dist-tags": { latest: entry.version }, versions: { [entry.version]: { name, ...fields, dist: { tarball: resolved, integrity } } } }); } } return (name) => docs.get(name);}
const ROLLDOWN_NATIVES = Object.keys(JSON.parse(SUBSTITUTES_LOCK).packages).filter((l) => l.startsWith("node_modules/@rolldown/binding-")).sort();
const SUBSTITUTED_PACKAGES = [ "/app/node_modules/@emnapi/core/package.json", "/app/node_modules/@emnapi/runtime/package.json", "/app/node_modules/@emnapi/wasi-threads/package.json", "/app/node_modules/@napi-rs/wasm-runtime/package.json", "/app/node_modules/@oxc-project/types/package.json", "/app/node_modules/@rolldown/binding-wasm32-wasi/package.json", "/app/node_modules/@rolldown/pluginutils/package.json", "/app/node_modules/@tybys/wasm-util/package.json", "/app/node_modules/esbuild/package.json", "/app/node_modules/rolldown/package.json", "/app/node_modules/tslib/package.json",];
const packageFiles = (files: Listing): string[] => Object.keys(files).filter((f) => /\/node_modules\/(@[^/]+\/)?[^/@]+\/package\.json$/.test(f) && !f.includes("/dist/")).sort();
test("esbuild installs as esbuild-wasm and rolldown gets its wasm32-wasi binding, while package-lock.json stays the one npm wrote", async ({ page, context }) => { await serveMetadata(context, lockDocuments(SUBSTITUTES_LOCK, SUBSTITUTES_OWN_LOCK)); const { report, error } = await installFiles(page, { "/app/package.json": SUBSTITUTES_PACKAGE }); expect(error).toBeNull(); expect(await readApp(page, "/app/package-lock.json")).toBe(SUBSTITUTES_LOCK); expect(report!.substitutions).toEqual([ { kind: "alias", location: "node_modules/esbuild", package: "esbuild", substitute: "esbuild-wasm", version: "0.27.7" }, { kind: "binding", location: "node_modules/@rolldown/binding-wasm32-wasi", package: "@rolldown/binding-wasm32-wasi", version: "1.2.16", dependent: "node_modules/rolldown" }, ]); expect([report!.lockfile, report!.packages, [...report!.skipped].sort(), report!.scripts]).toEqual(["written", 11, ROLLDOWN_NATIVES, []]); const { files, bins } = await listModules(page); expect(packageFiles(files)).toEqual(SUBSTITUTED_PACKAGES); expect(bins).toEqual({ "node_modules/.bin/esbuild": "../esbuild/bin/esbuild", "node_modules/.bin/rolldown": "../rolldown/bin/cli.mjs" }); expect(JSON.parse((await readApp(page, "/app/node_modules/esbuild/package.json"))!).name).toBe("esbuild-wasm");
// The hidden lockfile records what is on disk: esbuild-wasm at node_modules/esbuild // as npm records an `npm:` alias, and the binding as one of rolldown's optional deps. const hidden = JSON.parse((await readApp(page, "/app/node_modules/.package-lock.json"))!).packages as Record<string, LockEntry>; const own = JSON.parse(SUBSTITUTES_OWN_LOCK).packages as Record<string, LockEntry>; const { "": _, "node_modules/esbuild-wasm": esbuildWasm, ...wasmDeps } = own; expect(hidden["node_modules/esbuild"]).toEqual({ name: "esbuild-wasm", ...esbuildWasm }); for (const [location, entry] of Object.entries(wasmDeps)) expect(hidden[location]).toEqual({ ...entry, optional: true }); expect(hidden["node_modules/rolldown"]!.optionalDependencies).toEqual({ ...(JSON.parse(SUBSTITUTES_LOCK).packages["node_modules/rolldown"].optionalDependencies), "@rolldown/binding-wasm32-wasi": "1.2.16", }); expect(Object.keys(hidden).filter((l) => l.startsWith("node_modules/@esbuild/"))).toEqual([]);});
test("npm ci from npm's lockfile installs the same substitutes and leaves the lockfile as it was", async ({ page, context }) => { await serveMetadata(context, lockDocuments(SUBSTITUTES_OWN_LOCK)); await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 }); const run = await page.evaluate(async (files) => { const runtime = window.__runtime!; runtime.writeTree(files); const proc = runtime.exec("/usr/local/bin/npm", { argv: ["ci"], cwd: "/app" }); const dec = new TextDecoder(); let stdout = ""; let stderr = ""; const out = proc.stdout.pipeTo(new WritableStream({ write(c) { stdout += dec.decode(c, { stream: true }); } })); const err = proc.stderr.pipeTo(new WritableStream({ write(c) { stderr += dec.decode(c, { stream: true }); } })); const status = await proc.exited; await Promise.all([out, err]); return { status, stdout, stderr }; }, { "/app/package.json": SUBSTITUTES_PACKAGE, "/app/package-lock.json": SUBSTITUTES_LOCK }); expect({ ...run, stdout: run.stdout.replace(/ in \d+(?:\.\d+)?(?:ms|s)$/m, " in {time}") }).toEqual({ status: 0, stdout: "\nadded 11 packages in {time}\n", stderr: "" }); expect(await readApp(page, "/app/package-lock.json")).toBe(SUBSTITUTES_LOCK); expect(packageFiles((await listModules(page)).files)).toEqual(SUBSTITUTED_PACKAGES);});
// demo/apps/vite overrides esbuild and rollup to their wasm builds itself; its lockfile// already holds them, so there is nothing to substitute and nothing to ask the registry.test("a project that already aliases esbuild and rollup to their wasm builds is not substituted again", async ({ page }) => { test.setTimeout(60_000); const { report } = await installFiles(page, { "/app/package.json": registryFixture("vite", "package.json"), "/app/package-lock.json": registryFixture("vite", "package-lock.json"), }); expect([report!.substitutions, report!.metadata, report!.lockfile]).toEqual([undefined, NO_METADATA, "kept"]); expect(JSON.parse((await readApp(page, "/app/node_modules/esbuild/package.json"))!)).toMatchObject({ name: "esbuild-wasm", version: "0.27.7" }); expect(JSON.parse((await readApp(page, "/app/node_modules/rollup/package.json"))!)).toMatchObject({ name: "@rollup/wasm-node", version: "4.65.1" });});
// ms here lists two napi-rs-style bindings and no wasm32-wasi one, which the registry// does not have. Its 404 is stored for its max-age, so the next install asks nothing.const NO_WASM_BINDING_LOCK = `${JSON.stringify({ name: "no-wasm-binding", lockfileVersion: 3, requires: true, packages: { "": { name: "no-wasm-binding", dependencies: { ms: "2.1.3" } }, "node_modules/ms": { version: "2.1.3", resolved: "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", integrity: "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", license: "MIT", optionalDependencies: { "ms-darwin-arm64": "2.1.3", "ms-linux-x64-gnu": "2.1.3" }, }, },}, null, 2)}\n`;
test("a wasm32-wasi binding the registry does not have changes nothing, and its 404 answers the next install from the store", async ({ page, context }) => { const log = await serveMetadata(context, () => undefined); const first = await installLockfile(page, NO_WASM_BINDING_LOCK); const again = await page.evaluate(() => window.__runtime!.install()); expect(log).toEqual(["/ms-wasm32-wasi"]); expect([first.report.substitutions, first.report.metadata, first.report.packages]).toEqual([undefined, { cached: 0, fetched: 1 }, 1]); expect([again.substitutions, again.metadata, again.diff]).toEqual([undefined, { cached: 1, fetched: 0 }, { added: 0, removed: 0, changed: 0 }]);});