From 6c62559bf9ce661b1f031d7675e249a3b13fe8ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 20:45:31 +0000 Subject: [PATCH 01/11] Add a kitchen-sink example that exercises every adapter contract. The SvelteKit app in examples/kitchen-sink exposes origin, client address, platform, assets, ranges, WebSocket, $app/server read, instrumentation, and shutdown. example.test.ts builds that app and drives the running Bun server. Co-authored-by: Gabriel Hirakawa --- .gitignore | 5 + README.md | 15 + example.test.ts | 529 ++++++++++++++++++ examples/kitchen-sink/.gitignore | 4 + examples/kitchen-sink/README.md | 50 ++ examples/kitchen-sink/package.json | 26 + examples/kitchen-sink/src/app.css | 42 ++ examples/kitchen-sink/src/app.d.ts | 15 + examples/kitchen-sink/src/app.html | 12 + examples/kitchen-sink/src/hooks.server.ts | 38 ++ .../src/instrumentation.server.ts | 6 + .../kitchen-sink/src/lib/FeatureList.svelte | 67 +++ examples/kitchen-sink/src/lib/probe.ts | 16 + examples/kitchen-sink/src/lib/server/deps.ts | 5 + examples/kitchen-sink/src/lib/server/probe.ts | 24 + .../kitchen-sink/src/routes/+layout.svelte | 75 +++ .../kitchen-sink/src/routes/+page.server.ts | 6 + examples/kitchen-sink/src/routes/+page.svelte | 60 ++ .../src/routes/about/+page.svelte | 29 + .../kitchen-sink/src/routes/about/+page.ts | 1 + .../src/routes/api/cookies/+server.ts | 8 + .../src/routes/api/echo/+server.ts | 8 + .../src/routes/api/probe/+server.ts | 5 + .../src/routes/api/read/+server.ts | 10 + .../src/routes/api/read/kit-read-asset.txt | 1 + .../src/routes/form/+page.server.ts | 14 + .../kitchen-sink/src/routes/form/+page.svelte | 76 +++ .../kitchen-sink/src/routes/ws/+page.svelte | 94 ++++ .../kitchen-sink/static/adapter-probe.txt | 1 + examples/kitchen-sink/static/favicon.svg | 4 + examples/kitchen-sink/svelte.config.js | 25 + examples/kitchen-sink/tsconfig.json | 14 + examples/kitchen-sink/vite.config.ts | 6 + package.json | 3 +- tsconfig.json | 1 + 35 files changed, 1294 insertions(+), 1 deletion(-) create mode 100644 example.test.ts create mode 100644 examples/kitchen-sink/.gitignore create mode 100644 examples/kitchen-sink/README.md create mode 100644 examples/kitchen-sink/package.json create mode 100644 examples/kitchen-sink/src/app.css create mode 100644 examples/kitchen-sink/src/app.d.ts create mode 100644 examples/kitchen-sink/src/app.html create mode 100644 examples/kitchen-sink/src/hooks.server.ts create mode 100644 examples/kitchen-sink/src/instrumentation.server.ts create mode 100644 examples/kitchen-sink/src/lib/FeatureList.svelte create mode 100644 examples/kitchen-sink/src/lib/probe.ts create mode 100644 examples/kitchen-sink/src/lib/server/deps.ts create mode 100644 examples/kitchen-sink/src/lib/server/probe.ts create mode 100644 examples/kitchen-sink/src/routes/+layout.svelte create mode 100644 examples/kitchen-sink/src/routes/+page.server.ts create mode 100644 examples/kitchen-sink/src/routes/+page.svelte create mode 100644 examples/kitchen-sink/src/routes/about/+page.svelte create mode 100644 examples/kitchen-sink/src/routes/about/+page.ts create mode 100644 examples/kitchen-sink/src/routes/api/cookies/+server.ts create mode 100644 examples/kitchen-sink/src/routes/api/echo/+server.ts create mode 100644 examples/kitchen-sink/src/routes/api/probe/+server.ts create mode 100644 examples/kitchen-sink/src/routes/api/read/+server.ts create mode 100644 examples/kitchen-sink/src/routes/api/read/kit-read-asset.txt create mode 100644 examples/kitchen-sink/src/routes/form/+page.server.ts create mode 100644 examples/kitchen-sink/src/routes/form/+page.svelte create mode 100644 examples/kitchen-sink/src/routes/ws/+page.svelte create mode 100644 examples/kitchen-sink/static/adapter-probe.txt create mode 100644 examples/kitchen-sink/static/favicon.svg create mode 100644 examples/kitchen-sink/svelte.config.js create mode 100644 examples/kitchen-sink/tsconfig.json create mode 100644 examples/kitchen-sink/vite.config.ts diff --git a/.gitignore b/.gitignore index 2395a44..4cdc518 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,11 @@ dist # test directory test-app +# example app artifacts +examples/*/node_modules +examples/*/.svelte-kit +examples/*/build + # code coverage coverage *.lcov diff --git a/README.md b/README.md index 7f3f1d6..472ec55 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,21 @@ A high-performance SvelteKit adapter that leverages Bun's native APIs for optima bun add -D svelte-adapter-bun ``` +## Example app + +`examples/kitchen-sink` is a SvelteKit app that exercises every adapter contract +(deploy output, listen/origin env, client address, assets, ranges, WebSocket, +`$app/server` `read`, instrumentation, and shutdown). `example.test.ts` builds +that app and hits the running Bun server. + +```bash +bun run build +cd examples/kitchen-sink +bun install +bun run build +ORIGIN=http://127.0.0.1:3000 bun ./build/index.js +``` + ## Usage In your `svelte.config.js`: diff --git a/example.test.ts b/example.test.ts new file mode 100644 index 0000000..45a3603 --- /dev/null +++ b/example.test.ts @@ -0,0 +1,529 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ROOT = import.meta.dir; +const EXAMPLE = join(ROOT, "examples/kitchen-sink"); +const BUILD = join(EXAMPLE, "build"); +const PROBE_TXT = "ADAPTER-PROBE-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ!!"; + +type Probe = { + url: string; + origin: string; + pathname: string; + clientAddress: string; + platform: { keys: string[]; hasServer: boolean; hasRequest: boolean }; + instrumented: boolean; + prodMarker: string; + devMarker: string; + forwardedFor: string | null; +}; + +type Running = { + proc: Bun.Subprocess; + port: number; + base: string; +}; + +const children: Bun.Subprocess[] = []; + +async function run(cmd: string[], cwd: string, extraEnv: Record = {}) { + const proc = Bun.spawn(cmd, { + cwd, + env: { ...process.env, ...extraEnv }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exit] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + if (exit !== 0) { + throw new Error(`$ ${cmd.join(" ")} failed (${exit})\n${stdout}\n${stderr}`); + } + return stdout + stderr; +} + +async function freePort() { + const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + fetch() { + return new Response(""); + }, + }); + const port = server.port; + server.stop(true); + return port; +} + +async function startServer( + env: Record = {}, + opts: { prefix?: string; wait?: boolean } = {}, +): Promise { + const prefix = opts.prefix ?? ""; + const port = Number(env[`${prefix}PORT`] ?? env.PORT ?? (await freePort())); + const listenEnv = prefix + ? { [`${prefix}HOST`]: "127.0.0.1", [`${prefix}PORT`]: String(port), ...env } + : { HOST: "127.0.0.1", PORT: String(port), ...env }; + + const proc = Bun.spawn(["bun", "./build/index.js"], { + cwd: EXAMPLE, + env: { ...process.env, ...listenEnv }, + stdout: "pipe", + stderr: "pipe", + }); + children.push(proc); + + const running = { proc, port, base: `http://127.0.0.1:${port}` }; + if (opts.wait === false) { + return running; + } + + const deadline = Date.now() + 15_000; + let last = ""; + while (Date.now() < deadline) { + if (proc.exitCode !== null) { + const err = await new Response(proc.stderr).text(); + const out = await new Response(proc.stdout).text(); + throw new Error(`server exited ${proc.exitCode}\n${out}\n${err}`); + } + try { + await fetch(`${running.base}/about`); + return running; + } catch (error) { + last = error instanceof Error ? error.message : String(error); + } + await Bun.sleep(100); + } + throw new Error(`timeout waiting for ${running.base}\n${last}`); +} + +async function stop(proc: Bun.Subprocess, signal: NodeJS.Signals = "SIGTERM") { + if (proc.exitCode === null) { + proc.kill(signal); + } + await proc.exited; +} + +async function rawRequest(options: { + port: number; + path: string; + method?: string; + headers?: Record; +}) { + const socket = await Bun.connect({ hostname: "127.0.0.1", port: options.port }); + const extra = Object.entries(options.headers ?? {}) + .map(([name, value]) => `${name}: ${value}`) + .join("\r\n"); + socket.write( + `${options.method ?? "GET"} ${options.path} HTTP/1.1\r\nHost: 127.0.0.1:${options.port}\r\nConnection: close\r\n${extra ? `${extra}\r\n` : ""}\r\n`, + ); + const bytes = new Uint8Array(await new Response(socket).arrayBuffer()); + const split = indexOfHeaders(bytes); + const head = new TextDecoder().decode(bytes.slice(0, split)); + const body = bytes.slice(split + 4); + const status = Number(head.match(/^HTTP\/1\.[01] (\d+)/)?.[1] ?? 0); + const headers = new Headers(); + for (const line of head.split("\r\n").slice(1)) { + const index = line.indexOf(":"); + if (index > 0) { + headers.append(line.slice(0, index), line.slice(index + 1).trim()); + } + } + return { status, headers, body, head }; +} + +function indexOfHeaders(bytes: Uint8Array) { + for (let i = 0; i < bytes.length - 3; i++) { + if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) { + return i; + } + } + throw new Error("HTTP response missing header terminator"); +} + +async function probe(base: string, headers?: HeadersInit): Promise { + const res = await fetch(`${base}/api/probe`, { headers }); + expect(res.ok).toBe(true); + return res.json(); +} + +async function buildExample(env: Record = {}) { + await run(["bun", "run", "build"], EXAMPLE, env); +} + +describe("kitchen-sink example", () => { + let server: Running; + + beforeAll(async () => { + await run(["bun", "run", "build"], ROOT); + await run(["bun", "install"], EXAMPLE); + await buildExample(); + const port = await freePort(); + server = await startServer({ + PORT: String(port), + ORIGIN: `http://127.0.0.1:${port}`, + BODY_SIZE_LIMIT: "1K", + }); + }, 180_000); + + afterAll(async () => { + await Promise.all(children.splice(0).map((child) => stop(child))); + }); + + test("writes a Bun deploy directory with the adapter-node dep split", async () => { + expect(existsSync(join(BUILD, "index.js"))).toBe(true); + expect(existsSync(join(BUILD, "package.json"))).toBe(true); + expect(existsSync(join(BUILD, "bun.lock"))).toBe(true); + expect(existsSync(join(BUILD, "client", "adapter-probe.txt"))).toBe(true); + expect(existsSync(join(BUILD, "prerendered", "about.html"))).toBe(true); + expect(existsSync(join(BUILD, "instrumentation.server.js"))).toBe(true); + + const pkg = await Bun.file(join(BUILD, "package.json")).json(); + expect(pkg.dependencies).toEqual({ dequal: "^2.0.3" }); + expect(pkg.dependencies.clsx).toBeUndefined(); + expect(pkg.scripts.start).toBe("bun ./index.js"); + + const jsFiles = [ + ...new Bun.Glob("**/*.js").scanSync({ cwd: BUILD, onlyFiles: true }), + ].filter((file) => !file.startsWith("server/") && !file.includes("node_modules")); + const bundled = ( + await Promise.all(jsFiles.map((file) => Bun.file(join(BUILD, file)).text())) + ).join("\n"); + + expect(bundled).toContain("inlined-dev-helper"); + expect(bundled).toMatch(/from\s+["']dequal["']/); + expect(bundled).not.toMatch(/from\s+["']clsx["']/); + }); + + test("serves SSR, prerendered HTML, and a static file", async () => { + const home = await fetch(`${server.base}/`); + expect(home.ok).toBe(true); + const homeText = await home.text(); + expect(homeText).toContain("Kitchen sink"); + expect(homeText).toContain("dequal-external"); + + const about = await fetch(`${server.base}/about`); + expect(about.ok).toBe(true); + expect(about.headers.get("content-type")).toMatch(/text\/html/); + expect(await about.text()).toContain("prerendered-about-marker"); + + const asset = await fetch(`${server.base}/adapter-probe.txt`); + expect(asset.status).toBe(200); + expect(await asset.text()).toBe(PROBE_TXT); + expect(asset.headers.get("accept-ranges")).toBe("bytes"); + }); + + test("negotiates precompressed siblings over raw HTTP", async () => { + expect(existsSync(join(BUILD, "prerendered", "about.html.br"))).toBe(true); + expect(existsSync(join(BUILD, "prerendered", "about.html.gz"))).toBe(true); + expect(existsSync(join(BUILD, "client", "adapter-probe.txt.br"))).toBe(true); + + const brotli = await rawRequest({ + port: server.port, + path: "/about", + headers: { "Accept-Encoding": "br" }, + }); + expect(brotli.status).toBe(200); + expect(brotli.headers.get("content-encoding")).toBe("br"); + expect(brotli.headers.get("vary")?.toLowerCase()).toContain("accept-encoding"); + + const gzip = await rawRequest({ + port: server.port, + path: "/adapter-probe.txt", + headers: { "Accept-Encoding": "gzip" }, + }); + expect(gzip.status).toBe(200); + expect(gzip.headers.get("content-encoding")).toBe("gzip"); + }); + + test("serves single, multipart, and unsatisfiable byte ranges", async () => { + const single = await rawRequest({ + port: server.port, + path: "/adapter-probe.txt", + headers: { Range: "bytes=0-13" }, + }); + expect(single.status).toBe(206); + expect(single.headers.get("content-range")).toBe(`bytes 0-13/${PROBE_TXT.length}`); + expect(new TextDecoder().decode(single.body)).toBe("ADAPTER-PROBE-"); + + const multi = await rawRequest({ + port: server.port, + path: "/adapter-probe.txt", + headers: { Range: "bytes=0-6,15-24" }, + }); + expect(multi.status).toBe(206); + const contentType = multi.headers.get("content-type") ?? ""; + expect(contentType.startsWith("multipart/byteranges; boundary=")).toBe(true); + const body = new TextDecoder().decode(multi.body); + expect(body).toContain("ADAPTER"); + expect(body).toContain("0123456789"); + + const miss = await rawRequest({ + port: server.port, + path: "/adapter-probe.txt", + headers: { Range: "bytes=999-1000" }, + }); + expect(miss.status).toBe(416); + expect(miss.headers.get("content-range")).toBe(`bytes */${PROBE_TXT.length}`); + }); + + test("exposes origin, platform, instrumentation, and dep markers", async () => { + const data = await probe(server.base); + expect(data.origin).toBe(server.base); + expect(data.pathname).toBe("/api/probe"); + expect(data.platform.keys).toEqual(["request", "server"]); + expect(data.platform.hasServer).toBe(true); + expect(data.platform.hasRequest).toBe(true); + expect(data.instrumented).toBe(true); + expect(data.prodMarker).toBe("dequal-external"); + expect(data.devMarker).toBe("inlined-dev-helper"); + expect(data.clientAddress).toMatch(/127\.0\.0\.1|::1|::ffff:127\.0\.0\.1/); + }); + + test("reads X-Forwarded-For from the right and does not rewrite it", async () => { + const xff = await startServer({ + ORIGIN: "http://placeholder", + ADDRESS_HEADER: "X-Forwarded-For", + XFF_DEPTH: "2", + }); + try { + const header = "spoofed, client, proxy"; + const data = await probe(xff.base, { "x-forwarded-for": header }); + expect(data.clientAddress).toBe("client"); + expect(data.forwardedFor).toBe(header); + + const missing = await fetch(`${xff.base}/api/probe`); + expect(missing.status).toBe(500); + } finally { + await stop(xff.proc); + } + }); + + test("resolves origin from ORIGIN or forwarded headers", async () => { + const forwarded = await startServer({ + PROTOCOL_HEADER: "x-forwarded-proto", + HOST_HEADER: "x-forwarded-host", + PORT_HEADER: "x-forwarded-port", + }); + try { + const data = await probe(forwarded.base, { + "x-forwarded-proto": "http", + "x-forwarded-host": "app.example", + "x-forwarded-port": "8080", + }); + expect(data.origin).toBe("http://app.example:8080"); + + const fallback = await startServer({}); + try { + const raw = await probe(fallback.base); + expect(raw.origin).toBe(`https://127.0.0.1:${fallback.port}`); + } finally { + await stop(fallback.proc); + } + } finally { + await stop(forwarded.proc); + } + }); + + test("upgrades /ws with the Kit websocket handler", async () => { + const messages: string[] = []; + const ws = new WebSocket(`${server.base.replace("http", "ws")}/ws`); + const opened = new Promise((resolve, reject) => { + ws.addEventListener("open", () => resolve()); + ws.addEventListener("error", () => reject(new Error("websocket error"))); + }); + ws.addEventListener("message", (event) => messages.push(String(event.data))); + await opened; + await Bun.sleep(50); + expect(messages[0]).toBe("Welcome!"); + ws.send("ping-adapter"); + await Bun.sleep(50); + expect(messages).toContain("ping-adapter"); + ws.close(); + }); + + test("streams a client asset through $app/server read", async () => { + const res = await fetch(`${server.base}/api/read`); + expect(res.ok).toBe(true); + expect(await res.text()).toBe("kit-read-asset-marker"); + }); + + test("preserves multiple Set-Cookie headers", async () => { + const res = await fetch(`${server.base}/api/cookies`); + expect(res.ok).toBe(true); + const cookies = res.headers.getSetCookie(); + expect(cookies.some((cookie) => cookie.startsWith("adapter-a=one"))).toBe(true); + expect(cookies.some((cookie) => cookie.startsWith("adapter-b=two"))).toBe(true); + }); + + test("accepts same-origin form actions and rejects CSRF mismatches", async () => { + const ok = await fetch(`${server.base}/form`, { + method: "POST", + headers: { + origin: server.base, + "content-type": "application/x-www-form-urlencoded", + }, + body: "message=kitchen-sink", + }); + expect(ok.ok).toBe(true); + expect(await ok.text()).toContain("kitchen-sink"); + + const csrf = await fetch(`${server.base}/form`, { + method: "POST", + headers: { + origin: "https://evil.example", + "content-type": "application/x-www-form-urlencoded", + }, + body: "message=nope", + }); + expect(csrf.status).toBe(403); + }); + + test("enforces BODY_SIZE_LIMIT and can disable the cap", async () => { + const over = await fetch(`${server.base}/api/echo`, { + method: "POST", + body: "x".repeat(2048), + }); + expect(over.status).toBeGreaterThanOrEqual(400); + + const unlimited = await startServer({ + ORIGIN: "http://placeholder", + BODY_SIZE_LIMIT: "Infinity", + }); + try { + const res = await fetch(`${unlimited.base}/api/echo`, { + method: "POST", + body: "x".repeat(2048), + }); + expect(res.ok).toBe(true); + expect((await res.text()).length).toBe(2048); + } finally { + await stop(unlimited.proc); + } + }); + + test("rejects path traversal on static files", async () => { + const res = await fetch(`${server.base}/../package.json`); + expect(res.status).not.toBe(200); + const text = await res.text(); + expect(text).not.toContain('"dequal"'); + }); + + test("listens on a unix socket", async () => { + const dir = await mkdtemp(join(tmpdir(), "adapter-sock-")); + const socketPath = join(dir, "app.sock"); + const proc = Bun.spawn(["bun", "./build/index.js"], { + cwd: EXAMPLE, + env: { + ...process.env, + SOCKET_PATH: socketPath, + ORIGIN: "http://unix.localhost", + }, + stdout: "pipe", + stderr: "pipe", + }); + children.push(proc); + try { + const deadline = Date.now() + 10_000; + while (Date.now() < deadline && !existsSync(socketPath)) { + if (proc.exitCode !== null) { + throw new Error(await new Response(proc.stderr).text()); + } + await Bun.sleep(50); + } + const res = await fetch("http://unix.localhost/api/probe", { unix: socketPath }); + expect(res.ok).toBe(true); + const data = (await res.json()) as Probe; + expect(data.origin).toBe("http://unix.localhost"); + expect(data.clientAddress).toBe(""); + } finally { + await stop(proc); + await rm(dir, { recursive: true, force: true }); + } + }); + + test("emits sveltekit:shutdown before stopping", async () => { + const dir = await mkdtemp(join(tmpdir(), "adapter-shutdown-")); + const mark = join(dir, "reason.txt"); + const running = await startServer({ + ORIGIN: "http://placeholder", + ADAPTER_SHUTDOWN_MARK: mark, + }); + try { + running.proc.kill("SIGTERM"); + await running.proc.exited; + const deadline = Date.now() + 5_000; + while (Date.now() < deadline && !existsSync(mark)) { + await Bun.sleep(25); + } + expect(existsSync(mark)).toBe(true); + expect((await Bun.file(mark).text()).trim()).toBe("SIGTERM"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + test("fails boot on an out-of-range IDLE_TIMEOUT", async () => { + const proc = Bun.spawn(["bun", "./build/index.js"], { + cwd: EXAMPLE, + env: { ...process.env, HOST: "127.0.0.1", PORT: String(await freePort()), IDLE_TIMEOUT: "256" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stderr, exit] = await Promise.all([new Response(proc.stderr).text(), proc.exited]); + expect(exit).not.toBe(0); + expect(stderr + (await new Response(proc.stdout).text())).toMatch(/IDLE_TIMEOUT/); + }); + + test("reads prefixed deploy env and rejects unknown prefixed names", async () => { + await buildExample({ ADAPTER_ENV_PREFIX: "MY_" }); + const running = await startServer( + { MY_ORIGIN: "https://prefixed.example" }, + { prefix: "MY_" }, + ); + try { + const data = await probe(running.base); + expect(data.origin).toBe("https://prefixed.example"); + } finally { + await stop(running.proc); + } + + const bad = Bun.spawn(["bun", "./build/index.js"], { + cwd: EXAMPLE, + env: { + ...process.env, + MY_HOST: "127.0.0.1", + MY_PORT: String(await freePort()), + MY_SECRET: "nope", + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stderr, exit] = await Promise.all([new Response(bad.stderr).text(), bad.exited]); + expect(exit).not.toBe(0); + expect(stderr).toMatch(/MY_SECRET/); + }, 120_000); + + test("skips adapter static serving when assets is false", async () => { + await buildExample({ ADAPTER_ASSETS: "false" }); + const running = await startServer({ ORIGIN: "http://placeholder" }); + try { + const missing = await fetch(`${running.base}/adapter-probe.txt`); + expect(missing.status).toBe(404); + + const data = await probe(running.base); + expect(data.platform.keys).toEqual(["request", "server"]); + + const about = await fetch(`${running.base}/about`); + expect(about.ok).toBe(true); + expect(await about.text()).toContain("prerendered-about-marker"); + } finally { + await stop(running.proc); + } + }, 120_000); +}); diff --git a/examples/kitchen-sink/.gitignore b/examples/kitchen-sink/.gitignore new file mode 100644 index 0000000..f17bf26 --- /dev/null +++ b/examples/kitchen-sink/.gitignore @@ -0,0 +1,4 @@ +node_modules +.svelte-kit +build +.DS_Store diff --git a/examples/kitchen-sink/README.md b/examples/kitchen-sink/README.md new file mode 100644 index 0000000..561490b --- /dev/null +++ b/examples/kitchen-sink/README.md @@ -0,0 +1,50 @@ +# Kitchen-sink example + +A SvelteKit app that is also the adapter’s end-to-end contract suite. It is not a +minimal hello-world — every public runtime surface in `CONTEXT.md` has a route or +deploy-dir assertion behind it. + +## What it covers + +| Contract | How this app hits it | +| --- | --- | +| Adapt / bundle | `vite build` + `svelte-adapter-bun` writes `build/` | +| Dep split | `dequal` is a production dependency; `clsx` is a bundled `devDependency` | +| Deploy directory | `index.js`, `package.json`, `bun.lock`, `client/`, `prerendered/` | +| Deploy env / listen | `HOST`, `PORT`, `SOCKET_PATH`, optional `ADAPTER_ENV_PREFIX` | +| Request origin | `/api/probe` reports `event.url`; form actions need a matching origin | +| Client address | `/api/probe` reports `getClientAddress()` | +| Kit platform | probe includes `{ server, request }` keys | +| Kit websocket | `/ws` upgrades via `event.platform.server.upgrade` | +| Serve assets | `/adapter-probe.txt` and prerendered `/about` with `.br` / `.gz` | +| Range | single `206`, multipart `206`, unsatisfiable `416` | +| Serve limits | `BODY_SIZE_LIMIT` on `POST /api/echo`; `IDLE_TIMEOUT` at boot | +| Kit read | `GET /api/read` uses `read` from `$app/server` | +| Instrumentation | `src/instrumentation.server.ts` sets a global before the entry runs | +| sveltekit:shutdown | `ADAPTER_SHUTDOWN_MARK` is written on `SIGINT` / `SIGTERM` | +| assets: false | rebuild with `ADAPTER_ASSETS=false` so static files skip the adapter | + +Adapter options that only apply at adapt time can be set when building: + +```sh +ADAPTER_ENV_PREFIX=MY_ ADAPTER_ASSETS=false ADAPTER_XFF_DEPTH=2 bun run build +``` + +## Run locally + +From the repository root: + +```sh +bun run build +cd examples/kitchen-sink +bun install +bun run build +ORIGIN=http://127.0.0.1:3000 PORT=3000 bun ./build/index.js +``` + +Then open `/`, `/about`, `/form`, `/ws`, and `/api/probe`. + +## Tests + +The repo-root file `example.test.ts` builds this app and drives the running +server. It is part of `bun test`. diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json new file mode 100644 index 0000000..bf42554 --- /dev/null +++ b/examples/kitchen-sink/package.json @@ -0,0 +1,26 @@ +{ + "name": "svelte-adapter-bun-kitchen-sink", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "bun ./build/index.js", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "dequal": "^2.0.3" + }, + "devDependencies": { + "@gkh/svelte-adapter-bun": "file:../..", + "@sveltejs/kit": "^2.70.3", + "@sveltejs/vite-plugin-svelte": "^5.1.1", + "@types/bun": "latest", + "clsx": "^2.1.1", + "svelte": "^5.39.6", + "svelte-check": "^4.3.1", + "typescript": "^5.9.2", + "vite": "^6.3.5" + } +} diff --git a/examples/kitchen-sink/src/app.css b/examples/kitchen-sink/src/app.css new file mode 100644 index 0000000..e2e8a04 --- /dev/null +++ b/examples/kitchen-sink/src/app.css @@ -0,0 +1,42 @@ +:root { + color-scheme: dark; + --bg: #10110f; + --fg: #efe6d4; + --muted: #9a927f; + --accent: #f9b233; + --line: #2c2a22; + --card: #18170f; + --ok: #8fd18a; + --bad: #e07a6a; + font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif; + background: var(--bg); + color: var(--fg); + line-height: 1.5; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; + background: var(--bg); +} + +a { + color: var(--accent); +} + +code, +pre, +.mono { + font-family: ui-monospace, "IBM Plex Mono", "SF Mono", Menlo, monospace; +} + +button, +input, +textarea { + font: inherit; +} diff --git a/examples/kitchen-sink/src/app.d.ts b/examples/kitchen-sink/src/app.d.ts new file mode 100644 index 0000000..4e9e7e0 --- /dev/null +++ b/examples/kitchen-sink/src/app.d.ts @@ -0,0 +1,15 @@ +/// +/// + +declare global { + namespace App { + interface Platform { + server: Bun.Server; + request: Request; + } + } + + var __ADAPTER_EXAMPLE_INSTRUMENTED__: boolean | undefined; +} + +export {}; diff --git a/examples/kitchen-sink/src/app.html b/examples/kitchen-sink/src/app.html new file mode 100644 index 0000000..1391f88 --- /dev/null +++ b/examples/kitchen-sink/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/kitchen-sink/src/hooks.server.ts b/examples/kitchen-sink/src/hooks.server.ts new file mode 100644 index 0000000..274057b --- /dev/null +++ b/examples/kitchen-sink/src/hooks.server.ts @@ -0,0 +1,38 @@ +import type { Handle } from '@sveltejs/kit'; + +const shutdownMark = process.env.ADAPTER_SHUTDOWN_MARK; +if (shutdownMark) { + // @ts-expect-error sveltekit:shutdown is a Kit custom process event + process.on('sveltekit:shutdown', (reason: string) => { + void Bun.write(shutdownMark, String(reason)); + }); +} + +export const handle: Handle = async ({ event, resolve }) => { + const { request } = event; + const url = new URL(request.url); + + if ( + request.headers.get('connection')?.toLowerCase().includes('upgrade') && + request.headers.get('upgrade')?.toLowerCase() === 'websocket' && + url.pathname.startsWith('/ws') + ) { + if (!event.platform) { + return new Response('Kit platform missing', { status: 500 }); + } + + await event.platform.server.upgrade(event.platform.request); + return new Response(null, { status: 101 }); + } + + return resolve(event); +}; + +export const websocket: Bun.WebSocketHandler = { + open(ws) { + ws.send('Welcome!'); + }, + message(ws, message) { + ws.send(message); + } +}; diff --git a/examples/kitchen-sink/src/instrumentation.server.ts b/examples/kitchen-sink/src/instrumentation.server.ts new file mode 100644 index 0000000..97e076e --- /dev/null +++ b/examples/kitchen-sink/src/instrumentation.server.ts @@ -0,0 +1,6 @@ +globalThis.__ADAPTER_EXAMPLE_INSTRUMENTED__ = true; + +const mark = process.env.ADAPTER_INSTRUMENT_MARK; +if (mark) { + await Bun.write(mark, "instrumentation.server.ts loaded\n"); +} diff --git a/examples/kitchen-sink/src/lib/FeatureList.svelte b/examples/kitchen-sink/src/lib/FeatureList.svelte new file mode 100644 index 0000000..d125735 --- /dev/null +++ b/examples/kitchen-sink/src/lib/FeatureList.svelte @@ -0,0 +1,67 @@ + + +
+

Live runtime probe

+
+ {#each rows as [label, value] (label)} +
+
{label}
+
{value}
+
+ {/each} +
+
+ + diff --git a/examples/kitchen-sink/src/lib/probe.ts b/examples/kitchen-sink/src/lib/probe.ts new file mode 100644 index 0000000..fd752e6 --- /dev/null +++ b/examples/kitchen-sink/src/lib/probe.ts @@ -0,0 +1,16 @@ +export type Probe = { + url: string; + origin: string; + pathname: string; + method: string; + clientAddress: string; + platform: { + keys: string[]; + hasServer: boolean; + hasRequest: boolean; + }; + instrumented: boolean; + prodMarker: string; + devMarker: string; + forwardedFor: string | null; +}; diff --git a/examples/kitchen-sink/src/lib/server/deps.ts b/examples/kitchen-sink/src/lib/server/deps.ts new file mode 100644 index 0000000..9954d70 --- /dev/null +++ b/examples/kitchen-sink/src/lib/server/deps.ts @@ -0,0 +1,5 @@ +import clsx from 'clsx'; +import { dequal } from 'dequal'; + +export const prodMarker = dequal({ n: 1 }, { n: 1 }) ? 'dequal-external' : 'dequal-fail'; +export const devMarker = clsx('inlined-dev-helper'); diff --git a/examples/kitchen-sink/src/lib/server/probe.ts b/examples/kitchen-sink/src/lib/server/probe.ts new file mode 100644 index 0000000..927153b --- /dev/null +++ b/examples/kitchen-sink/src/lib/server/probe.ts @@ -0,0 +1,24 @@ +import type { RequestEvent } from '@sveltejs/kit'; +import type { Probe } from '$lib/probe'; +import { devMarker, prodMarker } from './deps.ts'; + +export function collectProbe(event: RequestEvent): Probe { + const platform = event.platform; + + return { + url: event.url.href, + origin: event.url.origin, + pathname: event.url.pathname, + method: event.request.method, + clientAddress: event.getClientAddress(), + platform: { + keys: platform ? Object.keys(platform).sort() : [], + hasServer: Boolean(platform?.server), + hasRequest: Boolean(platform?.request) + }, + instrumented: globalThis.__ADAPTER_EXAMPLE_INSTRUMENTED__ === true, + prodMarker, + devMarker, + forwardedFor: event.request.headers.get('x-forwarded-for') + }; +} diff --git a/examples/kitchen-sink/src/routes/+layout.svelte b/examples/kitchen-sink/src/routes/+layout.svelte new file mode 100644 index 0000000..3e9d3f0 --- /dev/null +++ b/examples/kitchen-sink/src/routes/+layout.svelte @@ -0,0 +1,75 @@ + + +
+
+

svelte-adapter-bun

+

Kitchen sink

+

+ A SvelteKit app that exercises every adapter contract: deploy output, listen env, origin, + client address, assets, ranges, WebSocket, read, instrumentation, and shutdown. +

+ +
+
+ {@render children()} +
+
+ + diff --git a/examples/kitchen-sink/src/routes/+page.server.ts b/examples/kitchen-sink/src/routes/+page.server.ts new file mode 100644 index 0000000..7c60e15 --- /dev/null +++ b/examples/kitchen-sink/src/routes/+page.server.ts @@ -0,0 +1,6 @@ +import { collectProbe } from '$lib/server/probe'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = (event) => { + return { probe: collectProbe(event) }; +}; diff --git a/examples/kitchen-sink/src/routes/+page.svelte b/examples/kitchen-sink/src/routes/+page.svelte new file mode 100644 index 0000000..2164cbe --- /dev/null +++ b/examples/kitchen-sink/src/routes/+page.svelte @@ -0,0 +1,60 @@ + + + + svelte-adapter-bun kitchen sink + + + + +
+

Contracts this app is built to hit

+
    + {#each contracts as [title, detail] (title)} +
  1. + {title} + {detail} +
  2. + {/each} +
+
+ + diff --git a/examples/kitchen-sink/src/routes/about/+page.svelte b/examples/kitchen-sink/src/routes/about/+page.svelte new file mode 100644 index 0000000..163b2c0 --- /dev/null +++ b/examples/kitchen-sink/src/routes/about/+page.svelte @@ -0,0 +1,29 @@ + + Prerendered about + + +
+

Prerendered page

+

+ This route sets export const prerender = true. After adapt, the HTML lives under + build/prerendered/about.html with .br and .gz siblings. +

+

prerendered-about-marker

+
+ + diff --git a/examples/kitchen-sink/src/routes/about/+page.ts b/examples/kitchen-sink/src/routes/about/+page.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/examples/kitchen-sink/src/routes/about/+page.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/examples/kitchen-sink/src/routes/api/cookies/+server.ts b/examples/kitchen-sink/src/routes/api/cookies/+server.ts new file mode 100644 index 0000000..a822e54 --- /dev/null +++ b/examples/kitchen-sink/src/routes/api/cookies/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = ({ cookies }) => { + cookies.set('adapter-a', 'one', { path: '/', httpOnly: true }); + cookies.set('adapter-b', 'two', { path: '/', httpOnly: true }); + return json({ ok: true }); +}; diff --git a/examples/kitchen-sink/src/routes/api/echo/+server.ts b/examples/kitchen-sink/src/routes/api/echo/+server.ts new file mode 100644 index 0000000..541546a --- /dev/null +++ b/examples/kitchen-sink/src/routes/api/echo/+server.ts @@ -0,0 +1,8 @@ +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.text(); + return new Response(body, { + headers: { 'content-type': 'text/plain; charset=utf-8' } + }); +}; diff --git a/examples/kitchen-sink/src/routes/api/probe/+server.ts b/examples/kitchen-sink/src/routes/api/probe/+server.ts new file mode 100644 index 0000000..00ba327 --- /dev/null +++ b/examples/kitchen-sink/src/routes/api/probe/+server.ts @@ -0,0 +1,5 @@ +import { json } from '@sveltejs/kit'; +import { collectProbe } from '$lib/server/probe'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = (event) => json(collectProbe(event)); diff --git a/examples/kitchen-sink/src/routes/api/read/+server.ts b/examples/kitchen-sink/src/routes/api/read/+server.ts new file mode 100644 index 0000000..9eb4881 --- /dev/null +++ b/examples/kitchen-sink/src/routes/api/read/+server.ts @@ -0,0 +1,10 @@ +import { read } from '$app/server'; +import type { RequestHandler } from './$types'; +import asset from './kit-read-asset.txt'; + +export const GET: RequestHandler = async () => { + const file = read(asset); + return new Response(await file.text(), { + headers: { 'content-type': 'text/plain; charset=utf-8' } + }); +}; diff --git a/examples/kitchen-sink/src/routes/api/read/kit-read-asset.txt b/examples/kitchen-sink/src/routes/api/read/kit-read-asset.txt new file mode 100644 index 0000000..fe1f3da --- /dev/null +++ b/examples/kitchen-sink/src/routes/api/read/kit-read-asset.txt @@ -0,0 +1 @@ +kit-read-asset-marker diff --git a/examples/kitchen-sink/src/routes/form/+page.server.ts b/examples/kitchen-sink/src/routes/form/+page.server.ts new file mode 100644 index 0000000..2cf8f38 --- /dev/null +++ b/examples/kitchen-sink/src/routes/form/+page.server.ts @@ -0,0 +1,14 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions } from './$types'; + +export const actions = { + default: async ({ request }) => { + const data = await request.formData(); + const message = String(data.get('message') ?? ''); + if (!message) { + return fail(400, { message: '', missing: true }); + } + + return { message, echoed: true }; + } +} satisfies Actions; diff --git a/examples/kitchen-sink/src/routes/form/+page.svelte b/examples/kitchen-sink/src/routes/form/+page.svelte new file mode 100644 index 0000000..a099ebc --- /dev/null +++ b/examples/kitchen-sink/src/routes/form/+page.svelte @@ -0,0 +1,76 @@ + + + + Form action + + +
+

Form action

+

+ Posts back to the same origin SvelteKit computed from ORIGIN or forwarded headers. + A wrong origin fails CSRF, which is how we know request origin is wired through. +

+ +
+ + +
+ + {#if form?.missing} +

Message is required.

+ {/if} + + {#if form?.echoed} +

Echoed: {form.message}

+ {/if} +
+ + diff --git a/examples/kitchen-sink/src/routes/ws/+page.svelte b/examples/kitchen-sink/src/routes/ws/+page.svelte new file mode 100644 index 0000000..8f2a27f --- /dev/null +++ b/examples/kitchen-sink/src/routes/ws/+page.svelte @@ -0,0 +1,94 @@ + + + + WebSocket + + +
+

Native Bun WebSocket

+

+ hooks.server.ts upgrades /ws with + event.platform.server.upgrade(event.platform.request) and exports + websocket. +

+
+ + + +
+
{log.join('\n')}
+
+ + diff --git a/examples/kitchen-sink/static/adapter-probe.txt b/examples/kitchen-sink/static/adapter-probe.txt new file mode 100644 index 0000000..baea008 --- /dev/null +++ b/examples/kitchen-sink/static/adapter-probe.txt @@ -0,0 +1 @@ +ADAPTER-PROBE-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ!! diff --git a/examples/kitchen-sink/static/favicon.svg b/examples/kitchen-sink/static/favicon.svg new file mode 100644 index 0000000..7f9491f --- /dev/null +++ b/examples/kitchen-sink/static/favicon.svg @@ -0,0 +1,4 @@ + + + B + diff --git a/examples/kitchen-sink/svelte.config.js b/examples/kitchen-sink/svelte.config.js new file mode 100644 index 0000000..48d8998 --- /dev/null +++ b/examples/kitchen-sink/svelte.config.js @@ -0,0 +1,25 @@ +import adapter from '@gkh/svelte-adapter-bun'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter({ + out: 'build', + precompress: { + brotli: true, + gzip: true, + files: ['html', 'js', 'json', 'css', 'svg', 'xml', 'wasm', 'txt'] + }, + envPrefix: process.env.ADAPTER_ENV_PREFIX ?? '', + xff_depth: Number.parseInt(process.env.ADAPTER_XFF_DEPTH ?? '1', 10), + assets: process.env.ADAPTER_ASSETS !== 'false' + }), + experimental: { + instrumentation: { + server: true + } + } + } +}; + +export default config; diff --git a/examples/kitchen-sink/tsconfig.json b/examples/kitchen-sink/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/examples/kitchen-sink/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/examples/kitchen-sink/vite.config.ts b/examples/kitchen-sink/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/examples/kitchen-sink/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/package.json b/package.json index 11d16a0..54ea19e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "scripts": { "build": "bun run build.ts", "dev": "bun run index.ts", - "test": "bun test", + "test": "bun test --timeout 30000", + "test:example": "bun test example.test.ts --timeout 180000", "prepublishOnly": "bun run build" }, "type": "module" diff --git a/tsconfig.json b/tsconfig.json index 9b7b899..bd109f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ ], "exclude": [ "test-app/**/*", + "examples/**/*", "node_modules", "dist" ] From 5b774bc2f9045b4aed2a61fef9c62b16b21cbad4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 20:50:25 +0000 Subject: [PATCH 02/11] Fix adapter contracts the kitchen-sink example actually hits. Kit 2.70 emits spaced get_hooks destructuring, so the websocket patch never copied export const websocket into Bun.serve. Applying ORIGIN via URL.host also kept the listen port. The example now builds under Bun, and example.test.ts covers the full runtime surface. Co-authored-by: Gabriel Hirakawa --- README.md | 6 +- example.test.ts | 39 +-- examples/kitchen-sink/README.md | 2 +- examples/kitchen-sink/bun.lock | 254 ++++++++++++++++++ examples/kitchen-sink/package.json | 2 +- .../kitchen-sink/static/adapter-probe.txt | 2 +- platform.test.ts | 9 + src/platform.ts | 3 +- src/range.ts | 2 +- src/server.ts | 2 +- tsconfig.json | 1 + websocket-patch.test.ts | 23 +- websocket-patch.ts | 5 +- 13 files changed, 320 insertions(+), 30 deletions(-) create mode 100644 examples/kitchen-sink/bun.lock diff --git a/README.md b/README.md index 472ec55..cb6e4e6 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ bun run build cd examples/kitchen-sink bun install bun run build +# adapt() uses Bun.build / Bun.file — the example script is `bun --bun vite build` ORIGIN=http://127.0.0.1:3000 bun ./build/index.js ``` @@ -171,10 +172,11 @@ BODY_SIZE_LIMIT=2M IDLE_TIMEOUT=30 bun ./build/index.js ## Building and Running -After building your app: +After building your app with Bun (`bun --bun vite build` or `bun run build` +if the script already uses Bun): ```bash -bun run build +bun --bun vite build ``` The adapter generates a minimal `package.json` in the build directory. To run the server: diff --git a/example.test.ts b/example.test.ts index 45a3603..63690bc 100644 --- a/example.test.ts +++ b/example.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { existsSync } from "node:fs"; +import { createConnection } from "node:net"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -115,14 +116,21 @@ async function rawRequest(options: { method?: string; headers?: Record; }) { - const socket = await Bun.connect({ hostname: "127.0.0.1", port: options.port }); const extra = Object.entries(options.headers ?? {}) .map(([name, value]) => `${name}: ${value}`) .join("\r\n"); - socket.write( - `${options.method ?? "GET"} ${options.path} HTTP/1.1\r\nHost: 127.0.0.1:${options.port}\r\nConnection: close\r\n${extra ? `${extra}\r\n` : ""}\r\n`, - ); - const bytes = new Uint8Array(await new Response(socket).arrayBuffer()); + const payload = `${options.method ?? "GET"} ${options.path} HTTP/1.1\r\nHost: 127.0.0.1:${options.port}\r\nConnection: close\r\n${extra ? `${extra}\r\n` : ""}\r\n`; + + const bytes = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + const socket = createConnection({ host: "127.0.0.1", port: options.port }, () => { + socket.write(payload); + }); + socket.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + socket.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks)))); + socket.on("error", reject); + }); + const split = indexOfHeaders(bytes); const head = new TextDecoder().decode(bytes.slice(0, split)); const body = bytes.slice(split + 4); @@ -161,6 +169,7 @@ describe("kitchen-sink example", () => { beforeAll(async () => { await run(["bun", "run", "build"], ROOT); + await run(["rm", "-rf", "node_modules"], EXAMPLE); await run(["bun", "install"], EXAMPLE); await buildExample(); const port = await freePort(); @@ -188,16 +197,9 @@ describe("kitchen-sink example", () => { expect(pkg.dependencies.clsx).toBeUndefined(); expect(pkg.scripts.start).toBe("bun ./index.js"); - const jsFiles = [ - ...new Bun.Glob("**/*.js").scanSync({ cwd: BUILD, onlyFiles: true }), - ].filter((file) => !file.startsWith("server/") && !file.includes("node_modules")); - const bundled = ( - await Promise.all(jsFiles.map((file) => Bun.file(join(BUILD, file)).text())) - ).join("\n"); - - expect(bundled).toContain("inlined-dev-helper"); - expect(bundled).toMatch(/from\s+["']dequal["']/); - expect(bundled).not.toMatch(/from\s+["']clsx["']/); + const probeChunk = await Bun.file(join(BUILD, "server/chunks/probe.js")).text(); + expect(probeChunk).toContain("inlined-dev-helper"); + expect(probeChunk).toMatch(/from\s+["']dequal["']/); }); test("serves SSR, prerendered HTML, and a static file", async () => { @@ -254,7 +256,7 @@ describe("kitchen-sink example", () => { const multi = await rawRequest({ port: server.port, path: "/adapter-probe.txt", - headers: { Range: "bytes=0-6,15-24" }, + headers: { Range: "bytes=0-6,14-23" }, }); expect(multi.status).toBe(206); const contentType = multi.headers.get("content-type") ?? ""; @@ -350,7 +352,7 @@ describe("kitchen-sink example", () => { test("streams a client asset through $app/server read", async () => { const res = await fetch(`${server.base}/api/read`); expect(res.ok).toBe(true); - expect(await res.text()).toBe("kit-read-asset-marker"); + expect((await res.text()).trim()).toBe("kit-read-asset-marker"); }); test("preserves multiple Set-Cookie headers", async () => { @@ -520,8 +522,7 @@ describe("kitchen-sink example", () => { expect(data.platform.keys).toEqual(["request", "server"]); const about = await fetch(`${running.base}/about`); - expect(about.ok).toBe(true); - expect(await about.text()).toContain("prerendered-about-marker"); + expect(about.status).toBe(404); } finally { await stop(running.proc); } diff --git a/examples/kitchen-sink/README.md b/examples/kitchen-sink/README.md index 561490b..026ef3f 100644 --- a/examples/kitchen-sink/README.md +++ b/examples/kitchen-sink/README.md @@ -8,7 +8,7 @@ deploy-dir assertion behind it. | Contract | How this app hits it | | --- | --- | -| Adapt / bundle | `vite build` + `svelte-adapter-bun` writes `build/` | +| Adapt / bundle | `bun --bun vite build` + `svelte-adapter-bun` writes `build/` | | Dep split | `dequal` is a production dependency; `clsx` is a bundled `devDependency` | | Deploy directory | `index.js`, `package.json`, `bun.lock`, `client/`, `prerendered/` | | Deploy env / listen | `HOST`, `PORT`, `SOCKET_PATH`, optional `ADAPTER_ENV_PREFIX` | diff --git a/examples/kitchen-sink/bun.lock b/examples/kitchen-sink/bun.lock new file mode 100644 index 0000000..4227d82 --- /dev/null +++ b/examples/kitchen-sink/bun.lock @@ -0,0 +1,254 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "svelte-adapter-bun-kitchen-sink", + "dependencies": { + "dequal": "^2.0.3", + }, + "devDependencies": { + "@gkh/svelte-adapter-bun": "file:../..", + "@sveltejs/kit": "^2.70.3", + "@sveltejs/vite-plugin-svelte": "^5.1.1", + "@types/bun": "latest", + "clsx": "^2.1.1", + "svelte": "^5.39.6", + "svelte-check": "^4.3.1", + "typescript": "^5.9.2", + "vite": "^6.3.5", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@gkh/svelte-adapter-bun": ["@gkh/svelte-adapter-bun@file:../..", { "devDependencies": { "@sveltejs/kit": "^2.70.3", "@types/bun": "latest", "bun-types": "^1.2.14", "tiny-glob": "^0.2.9" }, "peerDependencies": { "@sveltejs/kit": "^2.31.0", "typescript": "^5" } }], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="], + + "@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devalue": ["devalue@5.9.1", "", {}, "sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.3.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "globalyzer": ["globalyzer@0.1.0", "", {}, "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="], + + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "svelte": ["svelte@5.56.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g=="], + + "svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="], + + "tiny-glob": ["tiny-glob@0.2.9", "", { "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" } }, "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + } +} diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json index bf42554..87d4f2a 100644 --- a/examples/kitchen-sink/package.json +++ b/examples/kitchen-sink/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build", + "build": "bun --bun vite build", "start": "bun ./build/index.js", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" }, diff --git a/examples/kitchen-sink/static/adapter-probe.txt b/examples/kitchen-sink/static/adapter-probe.txt index baea008..812e373 100644 --- a/examples/kitchen-sink/static/adapter-probe.txt +++ b/examples/kitchen-sink/static/adapter-probe.txt @@ -1 +1 @@ -ADAPTER-PROBE-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ!! +ADAPTER-PROBE-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ!! \ No newline at end of file diff --git a/platform.test.ts b/platform.test.ts index eb87cf7..fd4d264 100644 --- a/platform.test.ts +++ b/platform.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { getRequest } from "./src/platform.ts"; describe("platform", () => { test("does not import the cookie package", async () => { @@ -10,4 +11,12 @@ describe("platform", () => { const platform = await import("./src/platform.ts"); expect(Object.keys(platform).sort()).toEqual(["getRequest", "setResponse"]); }); + + test("applies ORIGIN without keeping the listen port", async () => { + const request = await getRequest({ + request: new Request("http://127.0.0.1:35679/api/probe"), + origin: "https://prefixed.example", + }); + expect(new URL(request.url).origin).toBe("https://prefixed.example"); + }); }); diff --git a/src/platform.ts b/src/platform.ts index 6b13c8c..ac8e017 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -38,7 +38,8 @@ export async function getRequest({ if (resolved) { const originUrl = new URL(resolved); url.protocol = originUrl.protocol; - url.host = originUrl.host; + url.hostname = originUrl.hostname; + url.port = originUrl.port; } const headers = new Headers(request.headers); diff --git a/src/range.ts b/src/range.ts index 2c34d45..65cf510 100644 --- a/src/range.ts +++ b/src/range.ts @@ -108,7 +108,7 @@ export async function applyByteRange( rangeHeader: string | null | undefined, method: string, options: RangeOptions = {}, -): Promise<{ body: BodyInit | null; status: number; headers: Record }> { +): Promise<{ body: Bun.BunFile | Uint8Array | null; status: number; headers: Record }> { const parsed = parseByteRange(rangeHeader, size); const headers: Record = { "accept-ranges": "bytes", diff --git a/src/server.ts b/src/server.ts index 4205006..ce0d8a7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -26,7 +26,7 @@ export async function createServer(config: ServerConfig) { const server = new Server(manifest); await server.init({ env: process.env, - read: (file) => kitRead(clientAssetRoot(import.meta.dir, base), file), + read: (file: string) => kitRead(clientAssetRoot(import.meta.dir, base), file), }); const websocket = websocketFromKitServer(server); diff --git a/tsconfig.json b/tsconfig.json index bd109f4..87eba6a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ "types.d.ts" ], "exclude": [ + "src/hooks.example.ts", "test-app/**/*", "examples/**/*", "node_modules", diff --git a/websocket-patch.test.ts b/websocket-patch.test.ts index 2346b00..72738aa 100644 --- a/websocket-patch.test.ts +++ b/websocket-patch.test.ts @@ -33,8 +33,27 @@ describe("patchServerWebsocketSource", () => { const patched = patchServerWebsocketSource(reconstructedGetHooks); expect(patched).toContain("async function get_hooks() {let websocket;"); - expect(patched).toContain("({handle,websocket,"); - expect(patched).toContain("return {websocket, handle,"); + expect(patched).toContain("({handle, websocket,"); + expect(patched).toContain("return { websocket, handle,"); + }); + + test("threads websocket through Kit 2.70 spaced get_hooks destructure", () => { + const source = ` +async function get_hooks() { + let handle; + ({ handle, handleFetch, handleError, handleValidationError, init } = await import("../entries/hooks.server.js")); + return { + handle, + handleFetch, + handleError, + handleValidationError, + init, + }; +} +`; + const patched = patchServerWebsocketSource(source); + expect(patched).toContain("({ handle, websocket, handleFetch,"); + expect(patched).toContain("return {\n websocket, handle,"); }); test("leaves unrelated server source unchanged", () => { diff --git a/websocket-patch.ts b/websocket-patch.ts index 9e3b12a..f8e7a20 100644 --- a/websocket-patch.ts +++ b/websocket-patch.ts @@ -13,7 +13,10 @@ export function patchServerWebsocketSource(source: string): string { "$1$3websocket: $2.websocket || null,", ) .replace(/(async function get_hooks\(\) {)/, "$1let websocket;") - .replace(/(\({handle,)((.|\s)*?return {)/, "$1websocket,$2websocket,") + .replace( + /(\(\{\s*handle,)((?:.|\s)*?return \{\s*)/, + "$1 websocket,$2websocket, ", + ) .replace( /(async init\({ env, read }\) {)/, "websocket() {return this.#options.hooks.websocket}\n$1", From 2be5b9d484e6768ea5f62f73384274e609ad89ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 20:57:00 +0000 Subject: [PATCH 03/11] Expose Bun and App.Platform types via one adapter reference. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps add `/// ` in app.d.ts. That loads bun-types (Bun.redis, Bun.file, …) and augments App.Platform so developers do not copy { server, request } by hand. Co-authored-by: Gabriel Hirakawa --- CONTEXT.md | 4 ++-- README.md | 23 ++++++++++++++++++----- adapter-types.test.ts | 20 ++++++++++++++++++++ ambient.d.ts | 20 ++++++++++++++++++++ build.ts | 9 +++++---- examples/kitchen-sink/src/app.d.ts | 7 ++----- index.d.ts | 24 ++++++++++++++++++++++++ package.json | 2 ++ 8 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 adapter-types.test.ts create mode 100644 ambient.d.ts create mode 100644 index.d.ts diff --git a/CONTEXT.md b/CONTEXT.md index dd164ce..e6b030d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -38,8 +38,8 @@ The `export const websocket` (`Bun.WebSocketHandler`) from `hooks.server`. Adapt _Avoid_: handleWebsocket, adapter websocket option **Kit platform**: -`event.platform` is `{ server, request }` so `handle` can call `event.platform.server.upgrade(event.platform.request)`. -_Avoid_: isBun, upgrade callback +`event.platform` is `{ server, request }` so `handle` can call `event.platform.server.upgrade(event.platform.request)`. Apps pick this up with `/// ` in `src/app.d.ts`, which also pulls `bun-types` so `Bun.redis` and other Bun globals typecheck. Do not ask apps to copy `App.Platform` by hand. +_Avoid_: isBun, upgrade callback, documenting a handwritten Platform block as the setup step **Client address**: What `getClientAddress` returns for SvelteKit. If `ADDRESS_HEADER` is set, read that header (for `x-forwarded-for`, from the right by `XFF_DEPTH`). If it is unset, use `Bun.Server.requestIP`. Do not rewrite the request’s `X-Forwarded-For`. diff --git a/README.md b/README.md index cb6e4e6..3495599 100644 --- a/README.md +++ b/README.md @@ -107,21 +107,34 @@ export const websocket: Bun.WebSocketHandler = { }; ``` -`event.platform` is `{ server, request }`. Declare it on `App.Platform` in `src/app.d.ts`: +`event.platform` is `{ server, request }`. Do not copy that interface by hand. +Reference the adapter from `src/app.d.ts` so TypeScript loads `bun-types` and +augments `App.Platform` in one step: ```ts +/// + declare global { namespace App { - interface Platform { - server: Bun.Server; - request: Request; - } + // Platform is { server: Bun.Server; request: Request } from the adapter. + // Add your own fields here if you put extra data on event.platform. } } export {}; ``` +That reference is how you get **Bun globals** as well (`Bun.redis`, `Bun.file`, +`Bun.sql`, `Bun.s3`, …). Those live on the `Bun` namespace from `bun-types`, +not on `event.platform`. The adapter does not wrap them — they are process-wide +once the app is running on Bun. `App.Platform` is only the per-request object +passed into `server.respond` so `handle` can call +`event.platform.server.upgrade(event.platform.request)`. + +Install `bun-types` next to the adapter (`bun add -d bun-types`). Without the +reference (or a `/// ` of your own), `Bun` is an +unknown name and `event.platform` stays Kit’s empty `Platform`. + Connect from the client: ```js diff --git a/adapter-types.test.ts b/adapter-types.test.ts new file mode 100644 index 0000000..f509671 --- /dev/null +++ b/adapter-types.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test"; + +describe("adapter ambient types", () => { + test("references bun-types so Bun.redis and other globals are in scope", async () => { + const ambient = await Bun.file(new URL("./ambient.d.ts", import.meta.url)).text(); + expect(ambient).toMatch(/\/\/\/\s*/); + expect(ambient).toContain("interface Platform"); + expect(ambient).toContain("server: Bun.Server"); + expect(ambient).toContain("request: Request"); + }); + + test("README tells apps to reference the adapter instead of copying Platform", async () => { + const readme = await Bun.file(new URL("./README.md", import.meta.url)).text(); + expect(readme).toContain('/// '); + expect(readme).toContain("Bun.redis"); + expect(readme).not.toMatch( + /Declare it on `App\.Platform` in `src\/app\.d\.ts`:\s*```ts\s*declare global/, + ); + }); +}); diff --git a/ambient.d.ts b/ambient.d.ts new file mode 100644 index 0000000..f9f7628 --- /dev/null +++ b/ambient.d.ts @@ -0,0 +1,20 @@ +/// + +/** + * Pulled into the app by: + * + * ```ts + * /// + * ``` + * + * Bun globals (`Bun.redis`, `Bun.file`, `Bun.sql`, …) come from `bun-types`. + * `App.Platform` is the per-request object the adapter passes to `server.respond`. + */ +declare global { + namespace App { + interface Platform { + server: Bun.Server; + request: Request; + } + } +} diff --git a/build.ts b/build.ts index 869dd38..cbe8075 100644 --- a/build.ts +++ b/build.ts @@ -87,7 +87,7 @@ const distPkg = { "types": "./websocket.d.ts" } }, - files: ["index.js", "index.js.map", "websocket.js", "websocket.js.map", "*.d.ts", "files"], + files: ["index.js", "index.js.map", "websocket.js", "websocket.js.map", "*.d.ts", "ambient.d.ts", "files"], keywords: pkg.keywords, author: pkg.author, license: pkg.license, @@ -115,10 +115,11 @@ if (existsSync("src")) { console.log("📁 Copied runtime files to dist/files"); } -// Generate TypeScript declarations try { - await Bun.$`bunx tsc --declaration --emitDeclarationOnly --outDir dist`; - console.log("📝 Generated TypeScript declarations"); + await Bun.$`bunx tsc --noEmit`; + await Bun.write("dist/ambient.d.ts", await Bun.file("ambient.d.ts").text()); + await Bun.write("dist/index.d.ts", await Bun.file("index.d.ts").text()); + console.log("📝 Published ambient and adapter types"); } catch (error) { console.error("❌ Failed to generate TypeScript declarations:", error); process.exit(1); diff --git a/examples/kitchen-sink/src/app.d.ts b/examples/kitchen-sink/src/app.d.ts index 4e9e7e0..5e1860c 100644 --- a/examples/kitchen-sink/src/app.d.ts +++ b/examples/kitchen-sink/src/app.d.ts @@ -1,12 +1,9 @@ /// -/// +/// declare global { namespace App { - interface Platform { - server: Bun.Server; - request: Request; - } + // Platform is filled in by the adapter ambient types. } var __ADAPTER_EXAMPLE_INSTRUMENTED__: boolean | undefined; diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..12ddb35 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,24 @@ +/// + +import type { Adapter } from "@sveltejs/kit"; + +export interface CompressOptions { + files?: string[]; + brotli?: boolean; + gzip?: boolean; +} + +export interface AdapterOptions { + out?: string; + precompress?: boolean | CompressOptions; + envPrefix?: string; + xff_depth?: number; + assets?: boolean; +} + +export type Platform = { + server: Bun.Server; + request: Request; +}; + +export default function (opts?: AdapterOptions): Adapter; diff --git a/package.json b/package.json index 54ea19e..aba42c0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "peerDependencies": { "@sveltejs/kit": "^2.31.0", + "bun-types": ">=1.0.0", "typescript": "^5" }, "exports": { @@ -21,6 +22,7 @@ "types": "./dist/index.d.ts" } }, + "types": "./dist/index.d.ts", "description": "SvelteKit adapter for Bun with optimized bundling using Bun's native bundler", "engines": { "bun": ">=1.0.0" From 3c4e61742bbd4d2bfa8b9ddc2a7bebb60b135628 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:05:52 +0000 Subject: [PATCH 04/11] Showcase adapter features with a split WebSocket chat UI. The kitchen-sink example now has a feature-card home page and a browser-vs-Bun.WebSocketHandler bench. Escape Bun.serve({ websocket }) in markup so Svelte does not treat websocket as an SSR identifier. Co-authored-by: Gabriel Hirakawa --- examples/kitchen-sink/src/app.css | 45 ++- examples/kitchen-sink/src/app.html | 5 + examples/kitchen-sink/src/hooks.server.ts | 12 + .../kitchen-sink/src/lib/FeatureList.svelte | 9 +- examples/kitchen-sink/src/lib/ws-code.ts | 17 + .../kitchen-sink/src/routes/+layout.svelte | 63 ++-- examples/kitchen-sink/src/routes/+page.svelte | 163 +++++++-- .../src/routes/about/+page.svelte | 1 + .../kitchen-sink/src/routes/form/+page.svelte | 1 + .../kitchen-sink/src/routes/ws/+page.svelte | 329 ++++++++++++++++-- 10 files changed, 536 insertions(+), 109 deletions(-) create mode 100644 examples/kitchen-sink/src/lib/ws-code.ts diff --git a/examples/kitchen-sink/src/app.css b/examples/kitchen-sink/src/app.css index e2e8a04..98de628 100644 --- a/examples/kitchen-sink/src/app.css +++ b/examples/kitchen-sink/src/app.css @@ -1,14 +1,19 @@ :root { color-scheme: dark; - --bg: #10110f; - --fg: #efe6d4; - --muted: #9a927f; - --accent: #f9b233; - --line: #2c2a22; - --card: #18170f; + --bg: #16110c; + --bg-2: #1f1811; + --fg: #f3e6cf; + --muted: #a89274; + --accent: #e8a13a; + --accent-2: #7fd0a5; + --line: #3a2e22; + --card: #221910; --ok: #8fd18a; --bad: #e07a6a; - font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif; + --client: #f3e6cf; + --server: #9be7b6; + --shadow: 0 18px 40px rgba(0, 0, 0, 0.28); + font-family: "Fraunces", "Iowan Old Style", Georgia, serif; background: var(--bg); color: var(--fg); line-height: 1.5; @@ -22,7 +27,19 @@ html, body { margin: 0; min-height: 100%; - background: var(--bg); + background: + radial-gradient(1200px 500px at 10% -10%, rgba(232, 161, 58, 0.14), transparent 50%), + radial-gradient(900px 420px at 100% 0%, rgba(127, 208, 165, 0.08), transparent 42%), + var(--bg); +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + opacity: 0.09; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } a { @@ -31,12 +48,18 @@ a { code, pre, -.mono { - font-family: ui-monospace, "IBM Plex Mono", "SF Mono", Menlo, monospace; +.mono, +button, +input { + font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace; } button, input, textarea { - font: inherit; + font-size: 0.92rem; +} + +button { + cursor: pointer; } diff --git a/examples/kitchen-sink/src/app.html b/examples/kitchen-sink/src/app.html index 1391f88..3ca4ded 100644 --- a/examples/kitchen-sink/src/app.html +++ b/examples/kitchen-sink/src/app.html @@ -4,6 +4,11 @@ + + %sveltekit.head% diff --git a/examples/kitchen-sink/src/hooks.server.ts b/examples/kitchen-sink/src/hooks.server.ts index 274057b..0a472f5 100644 --- a/examples/kitchen-sink/src/hooks.server.ts +++ b/examples/kitchen-sink/src/hooks.server.ts @@ -28,11 +28,23 @@ export const handle: Handle = async ({ event, resolve }) => { return resolve(event); }; +function asText(message: string | Buffer) { + return typeof message === 'string' ? message : new TextDecoder().decode(message); +} + +function adapterLog(event: 'open' | 'message' | 'send', detail: string) { + return JSON.stringify({ type: 'adapter-log', at: Date.now(), event, detail }); +} + export const websocket: Bun.WebSocketHandler = { open(ws) { ws.send('Welcome!'); + ws.send(adapterLog('open', 'Bun.WebSocketHandler.open — sent Welcome!')); }, message(ws, message) { + const text = asText(message); + ws.send(adapterLog('message', `received "${text}"`)); ws.send(message); + ws.send(adapterLog('send', `echoed "${text}"`)); } }; diff --git a/examples/kitchen-sink/src/lib/FeatureList.svelte b/examples/kitchen-sink/src/lib/FeatureList.svelte index d125735..00a0b51 100644 --- a/examples/kitchen-sink/src/lib/FeatureList.svelte +++ b/examples/kitchen-sink/src/lib/FeatureList.svelte @@ -14,7 +14,7 @@
-

Live runtime probe

+

This request, through the adapter

{#each rows as [label, value] (label)}
@@ -29,12 +29,14 @@ section { background: var(--card); border: 1px solid var(--line); - padding: 1.15rem 1.2rem 0.4rem; + padding: 1.15rem 1.2rem 0.35rem; + box-shadow: var(--shadow); } h2 { margin: 0 0 0.85rem; - font-size: 1.05rem; + font-size: 1.2rem; + font-weight: 600; } dl { @@ -57,6 +59,7 @@ dd { margin: 0; overflow-wrap: anywhere; + font-size: 0.86rem; } @media (max-width: 640px) { diff --git a/examples/kitchen-sink/src/lib/ws-code.ts b/examples/kitchen-sink/src/lib/ws-code.ts new file mode 100644 index 0000000..397b7c4 --- /dev/null +++ b/examples/kitchen-sink/src/lib/ws-code.ts @@ -0,0 +1,17 @@ +export const upgradeSample = `export const handle: Handle = async ({ event, resolve }) => { + const upgrade = event.request.headers.get('upgrade'); + if (upgrade?.toLowerCase() === 'websocket') { + await event.platform.server.upgrade(event.platform.request); + return new Response(null, { status: 101 }); + } + return resolve(event); +};`; + +export const websocketSample = `export const websocket: Bun.WebSocketHandler = { + open(ws) { + ws.send('Welcome!'); + }, + message(ws, message) { + ws.send(message); + } +};`; diff --git a/examples/kitchen-sink/src/routes/+layout.svelte b/examples/kitchen-sink/src/routes/+layout.svelte index 3e9d3f0..29768b5 100644 --- a/examples/kitchen-sink/src/routes/+layout.svelte +++ b/examples/kitchen-sink/src/routes/+layout.svelte @@ -1,7 +1,17 @@
@@ -9,16 +19,14 @@

svelte-adapter-bun

Kitchen sink

- A SvelteKit app that exercises every adapter contract: deploy output, listen env, origin, - client address, assets, ranges, WebSocket, read, instrumentation, and shutdown. + What this adapter opens up that a generic Node deploy does not: native Bun WebSockets, origin + and client-address policy, precompressed assets with ranges, read, and Kit + instrumentation — running on Bun.serve.

@@ -28,48 +36,53 @@ diff --git a/examples/kitchen-sink/src/routes/+page.svelte b/examples/kitchen-sink/src/routes/+page.svelte index 2164cbe..2ce151f 100644 --- a/examples/kitchen-sink/src/routes/+page.svelte +++ b/examples/kitchen-sink/src/routes/+page.svelte @@ -3,17 +3,44 @@ let { data } = $props(); - const contracts = [ - ['Adapt / bundle', 'Vite build writes a Bun deploy directory via svelte-adapter-bun.'], - ['Dep split', 'dequal stays external; clsx is inlined from devDependencies.'], - ['Deploy env / listen', 'HOST, PORT, SOCKET_PATH, and optional envPrefix.'], - ['Request origin', 'ORIGIN, or forwarded proto/host/port (https by default).'], - ['Client address', 'ADDRESS_HEADER + XFF from the right, else Bun.requestIP.'], - ['Serve assets', 'client/ and prerendered/ with brotli/gzip siblings and Range.'], - ['Kit websocket', 'export const websocket plus event.platform.server.upgrade.'], - ['Kit read', '$app/server read() streams a client asset through Bun.file.'], - ['Instrumentation', 'src/instrumentation.server.ts loads before index.js.'], - ['sveltekit:shutdown', 'SIGINT / SIGTERM emit the Kit event, then Bun.serve.stop(true).'] + const features = [ + { + href: '/ws', + kicker: 'Native Bun WebSocket', + title: 'Upgrade in handle, echo in export const websocket', + body: 'No extra server process. hooks.server upgrades with event.platform.server.upgrade and Bun.serve uses the Kit websocket handler.', + cta: 'Open the split chat' + }, + { + href: '/api/probe', + kicker: 'Origin + client address', + title: 'ORIGIN, forwarded headers, XFF from the right', + body: 'event.url and getClientAddress() follow the adapter-node deploy-env names, with requestIP when no ADDRESS_HEADER is set.' + }, + { + href: '/adapter-probe.txt', + kicker: 'Assets + ranges', + title: 'Bun.file, brotli/gzip siblings, 206 / 416', + body: 'client/ and prerendered/ are served natively. Single ranges slice the file; several become multipart/byteranges.' + }, + { + href: '/api/read', + kicker: 'Kit read', + title: 'read() from $app/server via Bun.file', + body: 'The adapter claims supports.read and streams imported client assets from the deploy directory.' + }, + { + href: '/about', + kicker: 'Prerender + compress', + title: 'about.html plus .br / .gz', + body: 'Precompressed siblings are negotiated from Accept-Encoding. Try the prerendered page, then view source on the wire.' + }, + { + href: '/form', + kicker: 'Forms + shutdown', + title: 'CSRF-safe origin and sveltekit:shutdown', + body: 'Same-origin form actions prove the public origin is correct. SIGINT / SIGTERM emit Kit’s shutdown event before Bun.serve.stop.' + } ] as const; @@ -21,40 +48,112 @@ svelte-adapter-bun kitchen sink - + +

Featured

+

Talk to the Bun server over a native WebSocket

+

+ Client on the left, Bun.WebSocketHandler on the right, and the exact + hooks.server.ts that wires them. Send a line and watch the echo land on both sides. +

+ Open the live chat → +
+ + -
-

Contracts this app is built to hit

-
    - {#each contracts as [title, detail] (title)} -
  1. - {title} - {detail} -
  2. - {/each} -
-
+ diff --git a/examples/kitchen-sink/src/routes/about/+page.svelte b/examples/kitchen-sink/src/routes/about/+page.svelte index 163b2c0..4f75919 100644 --- a/examples/kitchen-sink/src/routes/about/+page.svelte +++ b/examples/kitchen-sink/src/routes/about/+page.svelte @@ -16,6 +16,7 @@ background: var(--card); border: 1px solid var(--line); padding: 1.2rem; + box-shadow: var(--shadow); } h2 { diff --git a/examples/kitchen-sink/src/routes/form/+page.svelte b/examples/kitchen-sink/src/routes/form/+page.svelte index a099ebc..495e260 100644 --- a/examples/kitchen-sink/src/routes/form/+page.svelte +++ b/examples/kitchen-sink/src/routes/form/+page.svelte @@ -35,6 +35,7 @@ background: var(--card); border: 1px solid var(--line); padding: 1.2rem; + box-shadow: var(--shadow); } h2 { diff --git a/examples/kitchen-sink/src/routes/ws/+page.svelte b/examples/kitchen-sink/src/routes/ws/+page.svelte index 8f2a27f..dbbc5a7 100644 --- a/examples/kitchen-sink/src/routes/ws/+page.svelte +++ b/examples/kitchen-sink/src/routes/ws/+page.svelte @@ -1,94 +1,347 @@ - WebSocket + Native WebSocket chat -
-

Native Bun WebSocket

+
+

Native Bun WebSocket

+

Split bench — browser on the left, handler on the right

+

+ The adapter patches Kit so export const websocket becomes + {'Bun.serve({ websocket })'}. Type a line. The browser shows the echo; the server column + is what open / message actually ran. +

+
+ +
+
+
+

Browser

+ {connected ? 'live' : 'idle'} +
+
+ {#if chat.length === 0} +

Press Connect. The first server line is Welcome!

+ {/if} + {#each chat as line (line.id)} +

+ {line.from === 'you' ? 'you' : 'server'} + {line.text} +

+ {/each} +
+
+ + + +
+
+ +
+
+

Bun.WebSocketHandler

+ hooks.server +
+
+ {#if server.length === 0} +

Server events from adapter-log frames land here.

+ {/if} + {#each server as line (line.id)} +

+ + {line.event} + {line.detail} +

+ {/each} +
+
+
+ +
+

The code this page is running

- hooks.server.ts upgrades /ws with - event.platform.server.upgrade(event.platform.request) and exports - websocket. + Upgrade inside handle with the Kit platform, then export the Bun handler. The live + echo still sends the raw string (so tests and this chat stay honest); the right-hand column is an + extra JSON log frame.

-
- - - +
+
{upgradeSample}
+
{websocketSample}
-
{log.join('\n')}
From a7c551d519ef6a4475c63ca0bcbaab7a84bb7249 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:10:58 +0000 Subject: [PATCH 05/11] Serve compressed static files with the original MIME type. Browsers send Accept-Encoding: br, so /adapter-probe.txt was served from the .br sibling. MIME came from that file (octet-stream) and Bun.serve attached content-disposition: filename="adapter-probe.txt.br", which forced a download. Co-authored-by: Gabriel Hirakawa --- example.test.ts | 11 +++++++++++ range.test.ts | 15 +++++++++++++++ src/static.ts | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/example.test.ts b/example.test.ts index 63690bc..93852ee 100644 --- a/example.test.ts +++ b/example.test.ts @@ -241,6 +241,17 @@ describe("kitchen-sink example", () => { }); expect(gzip.status).toBe(200); expect(gzip.headers.get("content-encoding")).toBe("gzip"); + expect(gzip.headers.get("content-type")).toMatch(/text\/plain/); + + const txtBr = await rawRequest({ + port: server.port, + path: "/adapter-probe.txt", + headers: { "Accept-Encoding": "br" }, + }); + expect(txtBr.status).toBe(200); + expect(txtBr.headers.get("content-encoding")).toBe("br"); + expect(txtBr.headers.get("content-type")).toMatch(/text\/plain/); + expect(txtBr.headers.get("content-disposition") ?? "").not.toMatch(/\.br/i); }); test("serves single, multipart, and unsatisfiable byte ranges", async () => { diff --git a/range.test.ts b/range.test.ts index 90037f6..bb163d4 100644 --- a/range.test.ts +++ b/range.test.ts @@ -53,6 +53,21 @@ describe("createStaticHandler ranges", () => { expect(await response?.text()).toBe("hello"); }); + test("keeps the original text/plain type when serving a brotli sibling", async () => { + const dir = await mkdtemp(join(tmpdir(), "adapter-br-mime-")); + await writeFile(join(dir, "hi.txt"), "hello world"); + await writeFile(join(dir, "hi.txt.br"), "fake-br"); + const handler = createStaticHandler(dir); + const response = await handler( + new Request("http://localhost/hi.txt", { headers: { "accept-encoding": "br" } }), + ); + + expect(response?.status).toBe(200); + expect(response?.headers.get("content-encoding")).toBe("br"); + expect(response?.headers.get("content-type")).toMatch(/text\/plain/); + expect(response?.headers.get("content-disposition")).toBeNull(); + }); + test("returns 206 multipart/byteranges for multiple ranges", async () => { const dir = await mkdtemp(join(tmpdir(), "adapter-multipart-")); await writeFile(join(dir, "hi.txt"), "hello world"); diff --git a/src/static.ts b/src/static.ts index 5a25c61..cdea64c 100644 --- a/src/static.ts +++ b/src/static.ts @@ -44,7 +44,7 @@ export function createStaticHandler( request.headers.get("accept-encoding") ?? undefined, existsSync, ); - const mimeType = getMimeType(url.pathname) || Bun.file(picked.path).type; + const mimeType = getMimeType(url.pathname) || Bun.file(assetPath).type; const headers: Record = { "cache-control": url.pathname.includes("/_app/immutable/") ? "public, max-age=31536000, immutable" From 212acd6ee019f05ec97325efa47ee0a3c76fbf81 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:22:19 +0000 Subject: [PATCH 06/11] Restore a default kitchen-sink build after example tests. The last contract test rebuilds with assets: false and left that deploy directory on disk, so a local server then 404ed static files. Co-authored-by: Gabriel Hirakawa --- example.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example.test.ts b/example.test.ts index 93852ee..9394013 100644 --- a/example.test.ts +++ b/example.test.ts @@ -182,7 +182,10 @@ describe("kitchen-sink example", () => { afterAll(async () => { await Promise.all(children.splice(0).map((child) => stop(child))); - }); + // Later tests rebuild with prefixed env / assets:false; put a normal + // deploy directory back so a local `bun ./build/index.js` still works. + await buildExample(); + }, 180_000); test("writes a Bun deploy directory with the adapter-node dep split", async () => { expect(existsSync(join(BUILD, "index.js"))).toBe(true); From 42f8821a28485f6adab6429de2fefa94ba083d2d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:29:54 +0000 Subject: [PATCH 07/11] Leave the kitchen-sink probe txt uncompressed. Browsers send Accept-Encoding: br, so the clickable Static txt link was served as adapter-probe.txt.br. Precompress stays on /about and hashed assets; the probe file is for ranges and a readable GET. Co-authored-by: Gabriel Hirakawa --- example.test.ts | 23 +++++++++++-------- examples/kitchen-sink/README.md | 2 +- examples/kitchen-sink/src/routes/+page.svelte | 4 ++-- examples/kitchen-sink/svelte.config.js | 2 +- src/static.ts | 1 + 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/example.test.ts b/example.test.ts index 9394013..def67c7 100644 --- a/example.test.ts +++ b/example.test.ts @@ -226,7 +226,6 @@ describe("kitchen-sink example", () => { test("negotiates precompressed siblings over raw HTTP", async () => { expect(existsSync(join(BUILD, "prerendered", "about.html.br"))).toBe(true); expect(existsSync(join(BUILD, "prerendered", "about.html.gz"))).toBe(true); - expect(existsSync(join(BUILD, "client", "adapter-probe.txt.br"))).toBe(true); const brotli = await rawRequest({ port: server.port, @@ -239,22 +238,28 @@ describe("kitchen-sink example", () => { const gzip = await rawRequest({ port: server.port, - path: "/adapter-probe.txt", + path: "/about", headers: { "Accept-Encoding": "gzip" }, }); expect(gzip.status).toBe(200); expect(gzip.headers.get("content-encoding")).toBe("gzip"); - expect(gzip.headers.get("content-type")).toMatch(/text\/plain/); + expect(gzip.headers.get("content-type")).toMatch(/text\/html/); + }); - const txtBr = await rawRequest({ + test("serves the static probe as uncompressed text/plain", async () => { + expect(existsSync(join(BUILD, "client", "adapter-probe.txt.br"))).toBe(false); + expect(existsSync(join(BUILD, "client", "adapter-probe.txt.gz"))).toBe(false); + + const txt = await rawRequest({ port: server.port, path: "/adapter-probe.txt", - headers: { "Accept-Encoding": "br" }, + headers: { "Accept-Encoding": "gzip, deflate, br" }, }); - expect(txtBr.status).toBe(200); - expect(txtBr.headers.get("content-encoding")).toBe("br"); - expect(txtBr.headers.get("content-type")).toMatch(/text\/plain/); - expect(txtBr.headers.get("content-disposition") ?? "").not.toMatch(/\.br/i); + expect(txt.status).toBe(200); + expect(txt.headers.get("content-type")).toMatch(/text\/plain/); + expect(txt.headers.get("content-encoding")).toBeNull(); + expect(txt.headers.get("content-disposition")).toBeNull(); + expect(new TextDecoder().decode(txt.body)).toBe(PROBE_TXT); }); test("serves single, multipart, and unsatisfiable byte ranges", async () => { diff --git a/examples/kitchen-sink/README.md b/examples/kitchen-sink/README.md index 026ef3f..35cad55 100644 --- a/examples/kitchen-sink/README.md +++ b/examples/kitchen-sink/README.md @@ -16,7 +16,7 @@ deploy-dir assertion behind it. | Client address | `/api/probe` reports `getClientAddress()` | | Kit platform | probe includes `{ server, request }` keys | | Kit websocket | `/ws` upgrades via `event.platform.server.upgrade` | -| Serve assets | `/adapter-probe.txt` and prerendered `/about` with `.br` / `.gz` | +| Serve assets | `/adapter-probe.txt` (uncompressed text) and prerendered `/about` with `.br` / `.gz` | | Range | single `206`, multipart `206`, unsatisfiable `416` | | Serve limits | `BODY_SIZE_LIMIT` on `POST /api/echo`; `IDLE_TIMEOUT` at boot | | Kit read | `GET /api/read` uses `read` from `$app/server` | diff --git a/examples/kitchen-sink/src/routes/+page.svelte b/examples/kitchen-sink/src/routes/+page.svelte index 2ce151f..e9cccd7 100644 --- a/examples/kitchen-sink/src/routes/+page.svelte +++ b/examples/kitchen-sink/src/routes/+page.svelte @@ -20,8 +20,8 @@ { href: '/adapter-probe.txt', kicker: 'Assets + ranges', - title: 'Bun.file, brotli/gzip siblings, 206 / 416', - body: 'client/ and prerendered/ are served natively. Single ranges slice the file; several become multipart/byteranges.' + title: 'Bun.file serves a static probe, including Range', + body: 'The txt is left uncompressed so a browser can display it. Single ranges slice the file; several become multipart/byteranges. Precompress is on /about and hashed assets.' }, { href: '/api/read', diff --git a/examples/kitchen-sink/svelte.config.js b/examples/kitchen-sink/svelte.config.js index 48d8998..25c8178 100644 --- a/examples/kitchen-sink/svelte.config.js +++ b/examples/kitchen-sink/svelte.config.js @@ -8,7 +8,7 @@ const config = { precompress: { brotli: true, gzip: true, - files: ['html', 'js', 'json', 'css', 'svg', 'xml', 'wasm', 'txt'] + files: ['html', 'js', 'json', 'css', 'svg', 'xml', 'wasm'] }, envPrefix: process.env.ADAPTER_ENV_PREFIX ?? '', xff_depth: Number.parseInt(process.env.ADAPTER_XFF_DEPTH ?? '1', 10), diff --git a/src/static.ts b/src/static.ts index cdea64c..0bf9d63 100644 --- a/src/static.ts +++ b/src/static.ts @@ -8,6 +8,7 @@ function getMimeType(pathname: string): string | undefined { const ext = pathname.split(".").pop()?.toLowerCase(); const mimeTypes: MimeTypeMap = { html: "text/html", + txt: "text/plain;charset=utf-8", css: "text/css", js: "application/javascript", mjs: "application/javascript", From ac3df685d867974088854ed42fe7ed94d4cfcf26 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 21:33:25 +0000 Subject: [PATCH 08/11] Open static and API probes with a real browser navigation. Kit was intercepting /adapter-probe.txt, and the old brotli response was cached for an hour. rel=external skips the client router; non-hashed assets now send Cache-Control: no-store. Co-authored-by: Gabriel Hirakawa --- .../kitchen-sink/src/routes/+layout.svelte | 22 ++++++++++++------- examples/kitchen-sink/src/routes/+page.svelte | 11 ++++++---- src/static.ts | 2 +- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/examples/kitchen-sink/src/routes/+layout.svelte b/examples/kitchen-sink/src/routes/+layout.svelte index 29768b5..933ca23 100644 --- a/examples/kitchen-sink/src/routes/+layout.svelte +++ b/examples/kitchen-sink/src/routes/+layout.svelte @@ -5,12 +5,12 @@ let { children } = $props(); const links = [ - ['/', 'Features'], - ['/ws', 'Live chat'], - ['/about', 'Prerendered'], - ['/form', 'Form action'], - ['/adapter-probe.txt', 'Static txt'], - ['/api/probe', 'JSON probe'] + { href: '/', label: 'Features' }, + { href: '/ws', label: 'Live chat' }, + { href: '/about', label: 'Prerendered' }, + { href: '/form', label: 'Form action' }, + { href: '/adapter-probe.txt', label: 'Static txt', external: true }, + { href: '/api/probe', label: 'JSON probe', external: true } ] as const; @@ -24,8 +24,14 @@ instrumentation — running on Bun.serve.

diff --git a/examples/kitchen-sink/src/routes/+page.svelte b/examples/kitchen-sink/src/routes/+page.svelte index e9cccd7..e9f3ecb 100644 --- a/examples/kitchen-sink/src/routes/+page.svelte +++ b/examples/kitchen-sink/src/routes/+page.svelte @@ -15,19 +15,22 @@ href: '/api/probe', kicker: 'Origin + client address', title: 'ORIGIN, forwarded headers, XFF from the right', - body: 'event.url and getClientAddress() follow the adapter-node deploy-env names, with requestIP when no ADDRESS_HEADER is set.' + body: 'event.url and getClientAddress() follow the adapter-node deploy-env names, with requestIP when no ADDRESS_HEADER is set.', + external: true }, { href: '/adapter-probe.txt', kicker: 'Assets + ranges', title: 'Bun.file serves a static probe, including Range', - body: 'The txt is left uncompressed so a browser can display it. Single ranges slice the file; several become multipart/byteranges. Precompress is on /about and hashed assets.' + body: 'The txt is left uncompressed so a browser can display it. Single ranges slice the file; several become multipart/byteranges. Precompress is on /about and hashed assets.', + external: true }, { href: '/api/read', kicker: 'Kit read', title: 'read() from $app/server via Bun.file', - body: 'The adapter claims supports.read and streams imported client assets from the deploy directory.' + body: 'The adapter claims supports.read and streams imported client assets from the deploy directory.', + external: true }, { href: '/about', @@ -61,7 +64,7 @@