Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427// The Vite plugin's dev server, from a consumer's side: tests/consumers/vite is a minimal// Vite app whose config is `plugins: [nib()]` and whose code only calls getKernel(). The// plugin must isolate the page, serve the runtime at /__nib/<version>/ and the Service// Worker at /nib-sw.js, and configure the host with those URLs. With ?mode=nib the page// mounts examples/hello through a ?nib import, and edits on disk reach the running guest// over HMR without reloading the page. `vite build` emits the same layout into outDir,// plus _headers, and `vite preview` serves that build with the same outcomes.import { test, expect, type Page } from "@playwright/test";import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";import { createServer as createNetServer } from "node:net";import { tmpdir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url";import { build, createLogger, createServer, preview, type InlineConfig, type PreviewServer, type ViteDevServer } from "vite";import nib from "../src/vite/index";
const FIXTURE = fileURLToPath(new URL("./consumers/vite/", import.meta.url));const VERSION = (JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }).version;const RUNTIME = `/__nib/${VERSION}/`;
let server: ViteDevServer;let origin: string;
function freePort(): Promise<number> { return new Promise((resolve, reject) => { const probe = createNetServer().once("error", reject).listen(0, "localhost", () => { const { port } = probe.address() as { port: number }; probe.close(() => resolve(port)); }); });}
test.beforeAll(async () => { const port = await freePort(); server = await createServer({ configFile: `${FIXTURE}vite.config.ts`, logLevel: "warn", clearScreen: false, server: { port, strictPort: true, host: "localhost" }, }); await server.listen(); origin = `http://localhost:${port}`;});
test.afterAll(async () => { await server?.close();});
/** Record the path of every request the page, its workers and its SW make. */function recordPaths(page: Page): string[] { const paths: string[] = []; page.context().on("request", (req) => { paths.push(new URL(req.url()).pathname); }); return paths;}
test("a nib() dev server isolates the page, serves the runtime and wires @nibling/kernel", async ({ page }) => { const paths = recordPaths(page); const res = await page.goto(`${origin}/`); expect(res?.headers()["cross-origin-embedder-policy"]).toBe("require-corp"); await expect(page.frameLocator("#app").locator("#msg")).toHaveText("hello from a Vite consumer", { timeout: 20_000 }); await expect(page.locator("#err")).toHaveText(""); expect(await page.evaluate(() => self.crossOriginIsolated)).toBe(true); expect(await page.evaluate(() => new URL(navigator.serviceWorker.controller!.scriptURL).pathname)).toBe("/nib-sw.js"); // swc.wasm loads only for TypeScript; the second test fetches it. for (const file of ["kernel.js", "runtime.js"]) expect(paths).toContain(RUNTIME + file);});
test("runtime files carry their content types and the isolation headers", async ({ request }) => { const wasm = await request.get(`${origin}${RUNTIME}swc.wasm`); expect(wasm.headers()["content-type"]).toBe("application/wasm"); const kernel = await request.get(`${origin}${RUNTIME}kernel.js`); expect(kernel.headers()["content-type"]).toMatch(/^text\/javascript/); expect(kernel.headers()["cross-origin-embedder-policy"]).toBe("require-corp"); expect(kernel.headers()["cross-origin-resource-policy"]).toBe("same-origin"); const sw = await request.get(`${origin}/nib-sw.js`); expect(sw.headers()["content-type"]).toMatch(/^text\/javascript/); expect(sw.headers()["service-worker-allowed"]).toBeUndefined(); expect((await request.get(`${origin}/__nib/0.0.0-other/kernel.js`)).status()).toBe(404);});
test("Vite's base prefixes every runtime URL, and pages narrows COOP/COEP", async ({ request }) => { const port = await freePort(); const sub = await createServer({ configFile: false, root: FIXTURE, cacheDir: fileURLToPath(new URL("../node_modules/.vite-consumer", import.meta.url)), logLevel: "warn", clearScreen: false, base: "/sub/", plugins: [nib({ pages: ["/sub/play/**"] })], server: { port, strictPort: true, host: "localhost" }, }); await sub.listen(); try { const at = `http://localhost:${port}`; expect((await request.get(`${at}/sub${RUNTIME}kernel.js`)).status()).toBe(200); // Below the origin root, scope "/" needs the header. expect((await request.get(`${at}/sub/nib-sw.js`)).headers()["service-worker-allowed"]).toBe("/"); const page = await request.get(`${at}/sub/`); expect(page.headers()["cross-origin-embedder-policy"]).toBeUndefined(); expect(page.headers()["cross-origin-resource-policy"]).toBe("same-origin"); expect((await request.get(`${at}/sub/play/`)).headers()["cross-origin-embedder-policy"]).toBe("require-corp"); const host = await (await request.get(`${at}/sub/@id/__x00__@nibling/kernel`)).text(); expect(host).toContain(`base: "/sub${RUNTIME}"`); expect(host).toContain(`serviceWorker: "/sub/nib-sw.js"`); } finally { await sub.close(); }});
test("resourcePolicy and embedders reach the headers and the Service Worker's guest responses", async ({ page, request }) => { const port = await freePort(); const sandbox = await createServer({ configFile: false, root: FIXTURE, cacheDir: fileURLToPath(new URL("../node_modules/.vite-consumer", import.meta.url)), logLevel: "warn", clearScreen: false, plugins: [nib({ resourcePolicy: "cross-origin", embedders: ["https://app.example"] })], server: { port, strictPort: true, host: "localhost" }, }); await sandbox.listen(); try { const at = `http://localhost:${port}`; const doc = await request.get(`${at}/`); expect(doc.headers()["cross-origin-resource-policy"], "the page's CORP").toBe("cross-origin"); expect(doc.headers()["content-security-policy"], "the page names its embedders").toBe("frame-ancestors https://app.example"); const kernel = await request.get(`${at}${RUNTIME}kernel.js`); expect(kernel.headers()["cross-origin-resource-policy"], "a runtime file's CORP").toBe("cross-origin"); const host = await (await request.get(`${at}/@id/__x00__@nibling/kernel`)).text(); expect(host).toContain(`resourcePolicy: "cross-origin"`);
await page.goto(`${at}/`); await expect(page.frameLocator("#app").locator("#msg")).toHaveText("hello from a Vite consumer", { timeout: 20_000 }); expect(await page.evaluate(() => new URL(navigator.serviceWorker.controller!.scriptURL).search)).toBe("?resource-policy=cross-origin"); const guest = await page.evaluate(async () => { const res = await fetch(document.querySelector<HTMLIFrameElement>("#app")!.src); return [res.headers.get("cross-origin-resource-policy"), res.headers.get("cross-origin-embedder-policy")]; }); expect(guest, "the guest server's response, through the Service Worker").toEqual(["cross-origin", "require-corp"]); } finally { await sandbox.close(); }});
test("runtime: false sends pages their headers and serves no runtime", async ({ request }) => { const port = await freePort(); const coordinator = await createServer({ configFile: false, root: FIXTURE, cacheDir: fileURLToPath(new URL("../node_modules/.vite-consumer", import.meta.url)), logLevel: "warn", clearScreen: false, plugins: [nib({ pages: ["/edit"], runtime: false })], server: { port, strictPort: true, host: "localhost" }, }); await coordinator.listen(); try { const at = `http://localhost:${port}`; const edit = await request.get(`${at}/edit`); expect(edit.headers()["cross-origin-embedder-policy"]).toBe("require-corp"); expect(edit.headers()["cross-origin-opener-policy"]).toBe("same-origin"); expect((await request.get(`${at}/`)).headers()["cross-origin-embedder-policy"]).toBeUndefined(); const kernel = await request.get(`${at}${RUNTIME}kernel.js`); expect(kernel.headers()["content-type"], "no runtime file, only the SPA fallback").toMatch(/^text\/html/); expect(kernel.headers()["cross-origin-embedder-policy"]).toBeUndefined(); expect((await request.get(`${at}/nib-sw.js`)).headers()["content-type"]).toMatch(/^text\/html/); } finally { await coordinator.close(); }});
const HELLO = `${FIXTURE}examples/hello/`;
/** Run `body`, then put each file back as it was. */async function restoring(files: string[], body: () => Promise<void>): Promise<void> { const saved = files.map((file) => [file, readFileSync(file)] as const); try { await body(); } finally { for (const [file, bytes] of saved) writeFileSync(file, bytes); }}
async function bootHello(page: Page): Promise<void> { await page.goto(`${origin}/?mode=nib`); await expect(page.frameLocator("#app").locator("#msg")).toHaveText("hello from examples/hello", { timeout: 20_000 }); await expect(page.locator("#err")).toHaveText(""); await page.evaluate(() => { (window as unknown as { __marker: number }).__marker = 1; });}
const marker = (page: Page): Promise<unknown> => page.evaluate(() => (window as unknown as { __marker?: number }).__marker);
test("an edit under a ?nib dir reaches the running guest without a page reload", async ({ page }) => { await restoring([`${HELLO}message.txt`], async () => { await bootHello(page); const frame = page.frameLocator("#app"); writeFileSync(`${HELLO}message.txt`, "edited on disk\n"); await expect.poll(async () => { await page.locator("#app").evaluate((iframe: HTMLIFrameElement) => iframe.contentWindow!.location.reload()); return frame.locator("#msg").textContent({ timeout: 2_000 }).catch(() => null); }, { timeout: 10_000 }).toBe("edited on disk"); expect(await marker(page)).toBe(1); });});
test("a package.json change remounts the snapshot, still without a page reload", async ({ page }) => { await restoring([`${HELLO}package.json`], async () => { await bootHello(page); const snapRequests: string[] = []; page.on("request", (req) => { if (new URL(req.url()).pathname.startsWith("/@nib/snap/")) snapRequests.push(req.url()); }); const pkg = JSON.parse(readFileSync(`${HELLO}package.json`, "utf8")) as Record<string, unknown>; writeFileSync(`${HELLO}package.json`, JSON.stringify({ ...pkg, version: "1.0.1" }, null, 2) + "\n"); await expect.poll(() => snapRequests.some((url) => new URL(url).searchParams.has("t")), { timeout: 10_000 }).toBe(true); expect(await marker(page)).toBe(1); });});
test("a ?nib module carries metadata only, and a lockfile without node_modules fails the import", async () => { const resolved = await server.pluginContainer.resolveId("./examples/hello?nib", `${FIXTURE}main.ts`); const loaded = await server.pluginContainer.load(resolved!.id); const code = typeof loaded === "string" ? loaded : loaded!.code!; const app = JSON.parse(/^const app = (.*);$/m.exec(code)![1]!) as Record<string, unknown>; expect(app).toMatchObject({ root: "/examples/hello", entry: "server.cjs", kind: "server" }); expect(app.src).toMatch(/^\/@nib\/snap\/hello-[0-9a-f]+\.snap$/); expect(app.files).toBe(3); expect(code).not.toContain("hello from examples/hello"); const snap = await fetch(`${origin}${app.src as string}`); expect(snap.headers.get("cross-origin-resource-policy")).toBe("same-origin"); // The v3 format's magic and header. (Not app.bytes: the previous test's package.json // restore may land in the watcher between the load and this fetch.) const bytes = new Uint8Array(await snap.arrayBuffer()); expect(new TextDecoder().decode(bytes.subarray(0, 4))).toBe("NIBS"); const headerLength = new DataView(bytes.buffer).getUint32(4, true); const header = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + headerLength))) as { files: [string, number, number][] }; expect(header.files.map(([path]) => path)).toEqual(["/examples/hello/message.txt", "/examples/hello/package.json", "/examples/hello/server.cjs"]);
const locked = mkdtempSync(join(tmpdir(), "nib-locked-")); try { writeFileSync(join(locked, "package.json"), "{}"); writeFileSync(join(locked, "package-lock.json"), "{}"); const id = (await server.pluginContainer.resolveId(`${locked}?nib`))!.id; await expect(server.pluginContainer.load(id)).rejects.toThrow(/has a lockfile but no node_modules: run "npm ci" in /); } finally { rmSync(locked, { recursive: true, force: true }); }});
test.describe("vite build and vite preview", () => { let outDir: string; const scratch: string[] = []; const temp = (prefix: string): string => { const dir = mkdtempSync(join(tmpdir(), prefix)); scratch.push(dir); return dir; };
/** Build the fixture (its own config unless `config` replaces it) into a temp outDir. */ async function buildFixture(config: InlineConfig = {}): Promise<string> { const out = temp("nib-vite-build-"); await build({ configFile: `${FIXTURE}vite.config.ts`, logLevel: "warn", ...config, build: { outDir: out, emptyOutDir: true, ...config.build }, }); return out; }
async function previewing(out: string, config: InlineConfig, body: (at: string) => Promise<void>): Promise<void> { const port = await freePort(); const server: PreviewServer = await preview({ configFile: `${FIXTURE}vite.config.ts`, logLevel: "warn", ...config, build: { outDir: out }, preview: { port, strictPort: true, host: "localhost" }, }); try { await body(`http://localhost:${port}`); } finally { await server.close(); } }
test.beforeAll(async () => { outDir = await buildFixture(); });
test.afterAll(() => { for (const dir of scratch) rmSync(dir, { recursive: true, force: true }); });
test("a build emits the runtime unhashed, each snapshot hashed, nib-sw.js at the root and _headers", () => { for (const file of ["kernel.js", "runtime.js", "sw.js", "swc.wasm"]) expect(existsSync(join(outDir, `__nib/${VERSION}`, file))).toBe(true); // host.js is bundled into the consumer's chunks through the wrapper module. expect(existsSync(join(outDir, `__nib/${VERSION}/host.js`))).toBe(false); expect(readFileSync(join(outDir, "nib-sw.js"))).toEqual(readFileSync(new URL("../dist/runtime/sw.js", import.meta.url))); const snaps = readdirSync(join(outDir, "assets")).filter((name) => name.endsWith(".snap")); expect(snaps).toEqual([expect.stringMatching(/^hello-[\w-]{8}\.snap$/)]); expect(new TextDecoder().decode(readFileSync(join(outDir, "assets", snaps[0]!)).subarray(0, 4))).toBe("NIBS"); expect(readFileSync(join(outDir, "_headers"), "utf8")).toBe([ "# @nibling/kernel: the runtime's headers, written by nib() (vite build)", "/*", " Cross-Origin-Resource-Policy: same-origin", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", "/__nib/*", " Cache-Control: public, max-age=31536000, immutable", "/nib-sw.js", " Cache-Control: no-cache", "", ].join("\n")); });
test("vite preview serves the build isolated, and both modes boot and run", async ({ page }) => { await previewing(outDir, {}, async (at) => { const res = await page.goto(`${at}/`); expect(res?.headers()["cross-origin-embedder-policy"]).toBe("require-corp"); await expect(page.frameLocator("#app").locator("#msg")).toHaveText("hello from a Vite consumer", { timeout: 20_000 }); await expect(page.locator("#err")).toHaveText(""); expect(await page.evaluate(() => self.crossOriginIsolated)).toBe(true); expect(await page.evaluate(() => new URL(navigator.serviceWorker.controller!.scriptURL).pathname)).toBe("/nib-sw.js"); expect((await page.request.get(`${at}${RUNTIME}kernel.js`)).headers()["cross-origin-embedder-policy"]).toBe("require-corp"); expect((await page.request.get(`${at}/__nib/0.0.0-other/kernel.js`)).status()).toBe(404);
await page.goto(`${at}/?mode=nib`); await expect(page.frameLocator("#app").locator("#msg")).toHaveText("hello from examples/hello", { timeout: 20_000 }); await expect(page.locator("#err")).toHaveText(""); }); });
test("a relative base (\"./\") resolves every runtime and snapshot URL against its chunk", async ({ page }) => { const out = await buildFixture({ base: "./" }); const chunks = readdirSync(join(out, "assets")).filter((name) => name.endsWith(".js")).map((name) => readFileSync(join(out, "assets", name), "utf8")).join("\n"); expect(chunks).toContain(`"../__nib/${VERSION}/kernel.js",import.meta.url`); expect(chunks).toContain(`"../nib-sw.js",import.meta.url`); expect(chunks).not.toContain(`"/__nib/`); await previewing(out, { base: "./" }, async (at) => { await page.goto(`${at}/?mode=nib`); await expect(page.frameLocator("#app").locator("#msg")).toHaveText("hello from examples/hello", { timeout: 20_000 }); await expect(page.locator("#err")).toHaveText(""); }); });
test("_headers follows base and pages and keeps a publicDir _headers; host \"none\" prints them instead", async () => { const publicDir = temp("nib-vite-public-"); writeFileSync(join(publicDir, "_headers"), "/api/*\n X-Own: 1\n"); const sub: InlineConfig = { configFile: false, root: FIXTURE, base: "/sub/", publicDir }; const out = await buildFixture({ ...sub, plugins: [nib({ pages: ["/sub/play/**"] })] }); expect(readFileSync(join(out, "_headers"), "utf8")).toBe([ "/api/*", " X-Own: 1", "", "# @nibling/kernel: the runtime's headers, written by nib() (vite build)", "/sub/*", " Cross-Origin-Resource-Policy: same-origin", "/sub/play/*", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", "/sub/__nib/*", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", " Cache-Control: public, max-age=31536000, immutable", "/sub/nib-sw.js", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", " Cache-Control: no-cache", " Service-Worker-Allowed: /", "", ].join("\n"));
const logged: string[] = []; const logger = createLogger("info"); logger.info = (msg) => { logged.push(msg); }; const none = await buildFixture({ configFile: false, root: FIXTURE, publicDir: false, customLogger: logger, logLevel: "info", plugins: [nib({ host: "none" })] }); expect(existsSync(join(none, "_headers"))).toBe(false); const note = logged.find((msg) => msg.includes("nib: no host config was written")); expect(note).toContain(" /__nib/*\n Cache-Control: public, max-age=31536000, immutable\n"); expect(note).toContain(" /nib-sw.js\n Cache-Control: no-cache\n"); });
test("_headers carries resourcePolicy and embedders; runtime: false writes only the page rules", async () => { const sandbox = await buildFixture({ configFile: false, root: FIXTURE, publicDir: false, plugins: [nib({ resourcePolicy: "cross-origin", embedders: ["https://app.example", "http://127.0.0.1:5173"] })] }); expect(readFileSync(join(sandbox, "_headers"), "utf8")).toBe([ "# @nibling/kernel: the runtime's headers, written by nib() (vite build)", "/*", " Cross-Origin-Resource-Policy: cross-origin", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", " Content-Security-Policy: frame-ancestors https://app.example http://127.0.0.1:5173", "/__nib/*", " Cache-Control: public, max-age=31536000, immutable", "/nib-sw.js", " Cache-Control: no-cache", "", ].join("\n")); const chunks = readdirSync(join(sandbox, "assets")).filter((name) => name.endsWith(".js")).map((name) => readFileSync(join(sandbox, "assets", name), "utf8")).join("\n"); expect(chunks).toContain(`resourcePolicy:"cross-origin"`);
const coordinator = await buildFixture({ configFile: false, root: FIXTURE, publicDir: false, plugins: [nib({ pages: ["/new", "/*/*/edit"], runtime: false })] }); expect(existsSync(join(coordinator, "__nib")), "no runtime directory").toBe(false); expect(existsSync(join(coordinator, "nib-sw.js")), "no Service Worker").toBe(false); expect(readFileSync(join(coordinator, "_headers"), "utf8")).toBe([ "# @nibling/kernel: the runtime's headers, written by nib() (vite build)", "/*", " Cross-Origin-Resource-Policy: same-origin", "/new", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", "/:p1/:p2/edit", " Cross-Origin-Opener-Policy: same-origin", " Cross-Origin-Embedder-Policy: require-corp", "", ].join("\n")); });
test("_headers names each whole-segment * in pages, since a host rule allows one wildcard", async () => { const out = await buildFixture({ configFile: false, root: FIXTURE, publicDir: false, plugins: [nib({ pages: ["/*/*/edit", "/repo/*/*/**"] })] }); const headers = readFileSync(join(out, "_headers"), "utf8"); expect(headers).toContain("\n/:p1/:p2/edit\n Cross-Origin-Opener-Policy: same-origin\n"); expect(headers).toContain("\n/repo/:p1/:p2/*\n Cross-Origin-Opener-Policy: same-origin\n"); await expect(buildFixture({ configFile: false, root: FIXTURE, publicDir: false, plugins: [nib({ pages: ["/a*/b*"] })] })) .rejects.toThrow('allows one wildcard per path, so pages can\'t hold "/a*/b*"'); });});