Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266// Proves the package from a consumer's side: builds and packs the tarball,// asserts it holds exactly the files that ship, installs it into a scratch project,// type-checks a browser consumer and a Node-side one against its declarations with no// @types/node, type-checks a vite.config.ts that uses nib() from @nibling/kernel/vite,// resolves every export subpath through Node's own exports resolution, runs the// Node-side modules, and runs `vite build` on a small site against the installed copy// (the plugin finds the runtime from inside node_modules).// Usage: npm run pack:check [-- --keep] (--keep leaves the scratch dir for inspection)import { execFileSync } from "node:child_process";import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";import { createRequire } from "node:module";import { tmpdir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url";
const ROOT = fileURLToPath(new URL("..", import.meta.url));const TSC = createRequire(import.meta.url).resolve("typescript/bin/tsc");const KEEP = process.argv.includes("--keep");
// Everything the tarball may contain. Anything else (src/, demo/, tests/, sketches/,// dist/demo, dist/fixtures, source maps) is a packaging bug.const EXPECTED = [ "LICENSE", "LICENSE-node", "README.md", "dist/node/server.js", "dist/node/snapshot.js", "dist/node/vite.js", "dist/runtime/host.js", "dist/runtime/kernel.js", "dist/runtime/runtime.js", "dist/runtime/sw.js", "dist/runtime/swc.wasm", "dist/types/host/index.d.ts", "dist/types/node/server.d.ts", "dist/types/node/snapshot.d.ts", "dist/types/shared/public-types.d.ts", "dist/types/vite/index.d.ts", "package.json",];
const CONSUMER_TS = `import { boot, NibError, type BootOptions, type Kernel } from "@nibling/kernel";
const options: BootOptions = { base: new URL("/nib/", location.href), serviceWorker: "/nib-sw.js" };
export async function start(): Promise<Kernel | string> { try { const runtime: Kernel = await boot(options); const proc = runtime.exec("/app/server.mjs", { env: { NODE_ENV: "production" } }); const url: string = await runtime.waitForPort(3000); const code: number = await proc.exited; void url, code; return runtime; } catch (err) { if (err instanceof NibError) return err.code; throw err; }}`;
// A build plugin or dev server's use of the Node-side modules. Typed against the// shipped declarations only: the consumer has no @types/node, so a public type that// leaked Buffer or node:http's IncomingMessage would fail here.const NODE_CONSUMER_TS = `import { snapshot, encodeSnapshot, type Snapshot } from "@nibling/kernel/snapshot";import { contentSecurityPolicy, headersFor, nibHeaders, type Middleware } from "@nibling/kernel/server";
export function pack(dir: string): Uint8Array { const snap: Snapshot = snapshot(dir, "/app"); const first: string | undefined = snap.files[0]?.path; void first; return encodeSnapshot(snap);}
export const headers: Middleware = nibHeaders({ csp: (path) => path.endsWith(".html") && contentSecurityPolicy({ inlineScripts: true }), serviceWorkerAllowed: (path) => path === "/nib/sw.js",});const set: Record<string, string> = headersFor("/index.html");headers({ url: "/index.html" }, { setHeader(name: string, value: string) { set[name] = value; } }, (err?: unknown) => { void err; });`;
// A consumer's Vite config, checked with vite's own types (which need @types/node).const VITE_CONFIG_TS = `import { defineConfig } from "vite";import nib, { type NibOptions } from "@nibling/kernel/vite";
const options: NibOptions = { pages: ["/**"], host: "netlify" };export default defineConfig({ root: "site", build: { outDir: "../site-dist", emptyOutDir: true, target: "es2022" }, plugins: [nib(options)] });`;
// The site that config builds: a page that mounts a ?nib directory.const SITE = { "site/index.html": `<!doctype html><script type="module" src="./main.js"></script>\n`, "site/main.js": `import { getKernel } from "@nibling/kernel";import hello from "./hello?nib";const runtime = await getKernel();await runtime.mount(hello);runtime.exec(hello.root + "/" + hello.entry, { cwd: hello.root });`, "site/hello/package.json": `{ "name": "hello", "private": true }\n`, "site/hello/server.cjs": `require("node:http").createServer((req, res) => res.end("hi")).listen(3000);\n`,};
// What the build must hold, read back with plain fs.const VERIFY_BUILD = `import { existsSync, readdirSync, readFileSync } from "node:fs";const { version } = JSON.parse(readFileSync("node_modules/@nibling/kernel/package.json", "utf8"));const out = "site-dist";const assets = readdirSync(out + "/assets");const chunks = assets.filter((f) => f.endsWith(".js")).map((f) => readFileSync(out + "/assets/" + f, "utf8")).join("\\n");const checks = { ["__nib/" + version + "/{kernel,runtime,sw}.js, swc.wasm, byte for byte"]: ["kernel.js", "runtime.js", "sw.js", "swc.wasm"].every((f) => readFileSync(out + "/__nib/" + version + "/" + f).equals(readFileSync("node_modules/@nibling/kernel/dist/runtime/" + f))), "nib-sw.js at the root": readFileSync(out + "/nib-sw.js").equals(readFileSync("node_modules/@nibling/kernel/dist/runtime/sw.js")), "no host.js (bundled into the chunks)": !existsSync(out + "/__nib/" + version + "/host.js") && !chunks.includes('from"@nibling/kernel"'), "a hashed hello-*.snap": assets.some((f) => /^hello-[\\w-]{8}\\.snap$/.test(f)), "the wrapper configures the emitted runtime": chunks.includes('"../__nib/' + version + '/kernel.js",import.meta.url') && chunks.includes('"../nib-sw.js",import.meta.url'), "_headers": readFileSync(out + "/_headers", "utf8").includes("/__nib/*\\n Cache-Control: public, max-age=31536000, immutable\\n"),};for (const [what, ok] of Object.entries(checks)) console.log(" " + (ok ? "ok " : "BAD") + " " + what);if (!Object.values(checks).every(Boolean)) process.exit(1);`;
const step = (msg) => console.log(`• ${msg}`);class CheckFailed extends Error {}function fail(msg) { throw new CheckFailed(msg);}function run(cmd, args, cwd) { try { return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } catch (err) { fail(`${cmd} ${args.join(" ")} (in ${cwd})\n${err.stdout ?? ""}${err.stderr ?? ""}`); }}const kb = (n) => `${(n / 1024).toFixed(1)} kB`;
const scratch = mkdtempSync(join(tmpdir(), "nib-pack-check-"));try { step("build"); run(process.execPath, ["build.mjs"], ROOT); // The build just ran; --ignore-scripts skips prepack's rerun, whose output would // land in --json's stdout. step(`npm pack into ${scratch}`); const [pack] = JSON.parse(run("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", scratch], ROOT)); const files = pack.files.map((f) => f.path).sort(); for (const f of pack.files) console.log(` ${kb(f.size).padStart(10)} ${f.path}`); console.log(` ${pack.filename}: ${files.length} files, packed ${kb(pack.size)}, unpacked ${kb(pack.unpackedSize)}`);
const missing = EXPECTED.filter((f) => !files.includes(f)); const extra = files.filter((f) => !EXPECTED.includes(f)); if (missing.length || extra.length) { fail(`tarball file list differs from the expected set\n missing: ${missing.join(", ") || "-"}\n unexpected: ${extra.join(", ") || "-"}`); } step("tarball holds exactly the expected files");
const consumer = join(scratch, "consumer"); mkdirSync(join(consumer, "src"), { recursive: true }); writeFileSync(join(consumer, "package.json"), JSON.stringify({ name: "nib-consumer", private: true, type: "module" }, null, 2)); // vite (an optional peer) and @types/node at the versions this repo builds with, from // npm's cache. const devVersion = (name) => JSON.parse(readFileSync(join(ROOT, "node_modules", name, "package.json"), "utf8")).version; const deps = [`vite@${devVersion("vite")}`, `@types/node@${devVersion("@types/node")}`]; step(`npm install <tarball> ${deps.join(" ")} --offline into a scratch consumer`); run("npm", ["install", join(scratch, pack.filename), ...deps, "--no-audit", "--no-fund", "--offline"], consumer);
// A browser consumer: DOM + ES2022 only. types: [] keeps any @types/* found up the // tree out, and skipLibCheck: false checks the package's own declarations too. writeFileSync(join(consumer, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", lib: ["DOM", "ES2022"], types: [], strict: true, skipLibCheck: false, noEmit: true, }, include: ["src"], }, null, 2)); writeFileSync(join(consumer, "src/main.ts"), CONSUMER_TS); writeFileSync(join(consumer, "src/node-side.ts"), NODE_CONSUMER_TS); step(`type-check a consumer (moduleResolution bundler, lib dom+es2022, no @types/node) with ${TSC.replace(ROOT, "")}`); run(process.execPath, [TSC, "-p", "tsconfig.json"], consumer);
// A node16/nodenext consumer (a "type": "module" package under moduleResolution // nodenext): relative imports need explicit extensions there, so this catches a // shipped .d.ts that omits one even though the bundler pass above is clean. writeFileSync(join(consumer, "tsconfig.nodenext.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "nodenext", moduleResolution: "nodenext", lib: ["DOM", "ES2022"], types: [], strict: true, skipLibCheck: false, noEmit: true, }, include: ["src"], }, null, 2)); step(`type-check the same consumer (module/moduleResolution nodenext) with ${TSC.replace(ROOT, "")}`); run(process.execPath, [TSC, "-p", "tsconfig.nodenext.json"], consumer);
// vite.config.ts, as a consumer's tsconfig.node.json would check it: vite's types // need Node's. writeFileSync(join(consumer, "vite.config.ts"), VITE_CONFIG_TS); writeFileSync(join(consumer, "tsconfig.vite.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", lib: ["ES2022"], types: ["node"], strict: true, skipLibCheck: false, noEmit: true, }, include: ["vite.config.ts"], }, null, 2)); step("type-check vite.config.ts (nib() from @nibling/kernel/vite in defineConfig)"); run(process.execPath, [TSC, "-p", "tsconfig.vite.json"], consumer);
// Node's exports resolution, with no extra conditions. writeFileSync(join(consumer, "resolve.mjs"), `import { existsSync } from "node:fs";import { fileURLToPath } from "node:url";const want = { "@nibling/kernel": "dist/runtime/host.js", "@nibling/kernel/sw": "dist/runtime/sw.js", "@nibling/kernel/snapshot": "dist/node/snapshot.js", "@nibling/kernel/server": "dist/node/server.js", "@nibling/kernel/vite": "dist/node/vite.js", "@nibling/kernel/runtime/kernel.js": "dist/runtime/kernel.js", "@nibling/kernel/runtime/swc.wasm": "dist/runtime/swc.wasm", "@nibling/kernel/package.json": "package.json",};const bad = [];for (const [spec, rel] of Object.entries(want)) { const path = fileURLToPath(import.meta.resolve(spec)); const ok = path.endsWith("/node_modules/@nibling/kernel/" + rel) && existsSync(path); console.log(" " + (ok ? "ok " : "BAD") + " " + spec + " -> " + path.slice(path.indexOf("node_modules/"))); if (!ok) bad.push(spec);}let leaked = false;try { import.meta.resolve("@nibling/kernel/src/host/index.ts"); leaked = true; } catch {}console.log(" " + (leaked ? "BAD" : "ok ") + " @nibling/kernel/src/host/index.ts is not exported");
// The Node-side bundles run: snapshot this consumer's src/ and decode the header back.const { snapshot, encodeSnapshot } = await import("@nibling/kernel/snapshot");const { headersFor } = await import("@nibling/kernel/server");const snap = snapshot("src", "/app");const bytes = encodeSnapshot(snap);const headerLen = new DataView(bytes.buffer, bytes.byteOffset).getUint32(4, true);const header = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + headerLen)));const paths = header.files.map((f) => f[0]).join(" ");const ran = new TextDecoder().decode(bytes.subarray(0, 4)) === "NIBS" && paths === "/app/main.ts /app/node-side.ts" && headersFor("/")["Cross-Origin-Embedder-Policy"] === "require-corp";console.log(" " + (ran ? "ok " : "BAD") + " snapshot() + encodeSnapshot() + headersFor() run: " + paths + ", " + bytes.length + " bytes");if (bad.length || leaked || !ran) process.exit(1);`); step("resolve export subpaths with import.meta.resolve, and run the Node-side modules"); process.stdout.write(run(process.execPath, ["resolve.mjs"], consumer));
for (const [file, text] of Object.entries(SITE)) { mkdirSync(join(consumer, file, ".."), { recursive: true }); writeFileSync(join(consumer, file), text); } writeFileSync(join(consumer, "verify-build.mjs"), VERIFY_BUILD); step("vite build a site against the installed package, and check what it emitted"); run(process.execPath, ["node_modules/vite/bin/vite.js", "build", "--logLevel", "warn"], consumer); process.stdout.write(run(process.execPath, ["verify-build.mjs"], consumer));
const pkg = JSON.parse(readFileSync(join(consumer, "node_modules/@nibling/kernel/package.json"), "utf8")); console.log(`\n✓ pack:check passed: ${pkg.name}@${pkg.version}`);} catch (err) { if (!(err instanceof CheckFailed)) throw err; console.error(`\n✗ pack:check failed: ${err.message}`); process.exitCode = 1;} finally { if (KEEP) console.log(` scratch kept at ${scratch}`); else rmSync(scratch, { recursive: true, force: true });}