From a849d1d01cf746d754923b9392991111ab1fe411 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Tue, 4 Aug 2026 13:48:03 +0300
Subject: [PATCH 01/94] feat(adapter-bun): implement environment variable
handling and server configuration
- Add env.js for managing environment variables with validation and fallback options.
- Introduce handler.js to manage the Bun-native SvelteKit request handling.
- Create index.js to configure server options based on environment variables.
- Implement static.js for serving static files and handling prerendered paths.
- Add utils.js for utility functions related to byte parsing and header management.
- Create tests for environment variable functions and utility functions.
- Set up a basic SvelteKit application with routes, static files, and event streams.
- Configure Playwright for end-to-end testing of the application.
---
.changeset/new-bun-adapter.md | 5 +
.github/workflows/platform-tests-all.yml | 7 +-
.github/workflows/platform-tests-bun.yml | 54 ++++
.../docs/25-build-and-deploy/20-adapters.md | 1 +
.../25-build-and-deploy/45-adapter-bun.md | 207 +++++++++++++
packages/adapter-bun/.gitignore | 5 +
packages/adapter-bun/CHANGELOG.md | 7 +
packages/adapter-bun/LICENSE | 22 ++
packages/adapter-bun/README.md | 15 +
packages/adapter-bun/ambient.d.ts | 14 +
packages/adapter-bun/index.d.ts | 66 ++++
packages/adapter-bun/index.js | 284 ++++++++++++++++++
packages/adapter-bun/internal.d.ts | 18 ++
packages/adapter-bun/package.json | 58 ++++
packages/adapter-bun/rolldown.config.js | 64 ++++
packages/adapter-bun/src/dir.js | 4 +
packages/adapter-bun/src/env.js | 102 +++++++
packages/adapter-bun/src/env.spec.ts | 50 +++
packages/adapter-bun/src/handler.js | 136 +++++++++
packages/adapter-bun/src/index.js | 185 ++++++++++++
packages/adapter-bun/src/static.js | 194 ++++++++++++
packages/adapter-bun/src/utils.js | 56 ++++
packages/adapter-bun/src/utils.spec.ts | 30 ++
.../adapter-bun/test/apps/basic/.gitignore | 5 +
.../adapter-bun/test/apps/basic/package.json | 20 ++
.../test/apps/basic/playwright.config.js | 1 +
.../adapter-bun/test/apps/basic/src/app.html | 11 +
.../test/apps/basic/src/routes/+page.svelte | 6 +
.../basic/src/routes/event-stream/+server.js | 7 +
.../apps/basic/src/routes/platform/+server.js | 10 +
.../basic/src/routes/prerendered/+page.js | 2 +
.../basic/src/routes/prerendered/+page.svelte | 1 +
.../test/apps/basic/static/data.json | 1 +
.../adapter-bun/test/apps/basic/test/test.js | 73 +++++
.../adapter-bun/test/apps/basic/tsconfig.json | 15 +
.../test/apps/basic/vite.config.js | 17 ++
packages/adapter-bun/test/utils.js | 29 ++
packages/adapter-bun/tsconfig.json | 26 ++
packages/adapter-bun/vitest.config.js | 5 +
pnpm-lock.yaml | 50 +++
pnpm-workspace.yaml | 1 +
41 files changed, 1863 insertions(+), 1 deletion(-)
create mode 100644 .changeset/new-bun-adapter.md
create mode 100644 .github/workflows/platform-tests-bun.yml
create mode 100644 documentation/docs/25-build-and-deploy/45-adapter-bun.md
create mode 100644 packages/adapter-bun/.gitignore
create mode 100644 packages/adapter-bun/CHANGELOG.md
create mode 100644 packages/adapter-bun/LICENSE
create mode 100644 packages/adapter-bun/README.md
create mode 100644 packages/adapter-bun/ambient.d.ts
create mode 100644 packages/adapter-bun/index.d.ts
create mode 100644 packages/adapter-bun/index.js
create mode 100644 packages/adapter-bun/internal.d.ts
create mode 100644 packages/adapter-bun/package.json
create mode 100644 packages/adapter-bun/rolldown.config.js
create mode 100644 packages/adapter-bun/src/dir.js
create mode 100644 packages/adapter-bun/src/env.js
create mode 100644 packages/adapter-bun/src/env.spec.ts
create mode 100644 packages/adapter-bun/src/handler.js
create mode 100644 packages/adapter-bun/src/index.js
create mode 100644 packages/adapter-bun/src/static.js
create mode 100644 packages/adapter-bun/src/utils.js
create mode 100644 packages/adapter-bun/src/utils.spec.ts
create mode 100644 packages/adapter-bun/test/apps/basic/.gitignore
create mode 100644 packages/adapter-bun/test/apps/basic/package.json
create mode 100644 packages/adapter-bun/test/apps/basic/playwright.config.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/app.html
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/+page.svelte
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte
create mode 100644 packages/adapter-bun/test/apps/basic/static/data.json
create mode 100644 packages/adapter-bun/test/apps/basic/test/test.js
create mode 100644 packages/adapter-bun/test/apps/basic/tsconfig.json
create mode 100644 packages/adapter-bun/test/apps/basic/vite.config.js
create mode 100644 packages/adapter-bun/test/utils.js
create mode 100644 packages/adapter-bun/tsconfig.json
create mode 100644 packages/adapter-bun/vitest.config.js
diff --git a/.changeset/new-bun-adapter.md b/.changeset/new-bun-adapter.md
new file mode 100644
index 000000000000..c68d9ce16589
--- /dev/null
+++ b/.changeset/new-bun-adapter.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-bun': minor
+---
+
+feat: add a Bun-native adapter with static file serving and single-executable support
diff --git a/.github/workflows/platform-tests-all.yml b/.github/workflows/platform-tests-all.yml
index ce9f494cc2a2..5384c733695e 100644
--- a/.github/workflows/platform-tests-all.yml
+++ b/.github/workflows/platform-tests-all.yml
@@ -29,9 +29,14 @@ jobs:
with:
sha: ${{ inputs.sha }}
+ bun:
+ uses: ./.github/workflows/platform-tests-bun.yml
+ with:
+ sha: ${{ inputs.sha }}
+
report-status:
if: always()
- needs: [vercel, netlify, node]
+ needs: [vercel, netlify, node, bun]
permissions:
contents: write # to create the repository_dispatch event
runs-on: ubuntu-latest
diff --git a/.github/workflows/platform-tests-bun.yml b/.github/workflows/platform-tests-bun.yml
new file mode 100644
index 000000000000..1ef4a4984184
--- /dev/null
+++ b/.github/workflows/platform-tests-bun.yml
@@ -0,0 +1,54 @@
+name: Platform Tests (Bun)
+
+on:
+ workflow_dispatch:
+ inputs:
+ sha:
+ description: 'Commit SHA to test'
+ required: false
+ default: ''
+ workflow_call:
+ inputs:
+ sha:
+ description: 'Commit SHA to test'
+ required: false
+ type: string
+ default: ''
+
+permissions:
+ contents: read
+
+env:
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
+
+jobs:
+ test-basic:
+ if: github.repository == 'sveltejs/kit'
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ inputs.sha || github.sha }}
+ persist-credentials: false
+
+ - uses: ./.github/actions/node-setup
+
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
+ with:
+ bun-version: 1.3.14
+
+ - name: Build adapter-bun files
+ working-directory: packages/adapter-bun
+ run: pnpm prepublishOnly
+
+ - uses: ./.github/actions/platform-test
+ with:
+ test-app-dir: packages/adapter-bun/test/apps/basic
+ os: ubuntu-latest
+
+ - name: Compile executable
+ working-directory: packages/adapter-bun/test/apps/basic
+ env:
+ COMPILE: 'true'
+ run: pnpm build
diff --git a/documentation/docs/25-build-and-deploy/20-adapters.md b/documentation/docs/25-build-and-deploy/20-adapters.md
index ae9999044ccb..9d53dbf146b8 100644
--- a/documentation/docs/25-build-and-deploy/20-adapters.md
+++ b/documentation/docs/25-build-and-deploy/20-adapters.md
@@ -9,6 +9,7 @@ Official adapters exist for a variety of platforms — these are documented on t
- [`@sveltejs/adapter-cloudflare`](adapter-cloudflare) for Cloudflare Workers and Cloudflare Pages
- [`@sveltejs/adapter-netlify`](adapter-netlify) for Netlify
- [`@sveltejs/adapter-node`](adapter-node) for Node servers
+- [`@sveltejs/adapter-bun`](adapter-bun) for Bun servers
- [`@sveltejs/adapter-static`](adapter-static) for static site generation (SSG)
- [`@sveltejs/adapter-vercel`](adapter-vercel) for Vercel
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
new file mode 100644
index 000000000000..13f3789e8908
--- /dev/null
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -0,0 +1,207 @@
+---
+title: Bun servers
+---
+
+To generate a standalone [Bun](https://bun.com/) server, use [`adapter-bun`](https://github.com/sveltejs/kit/tree/main/packages/adapter-bun). The generated server uses [`Bun.serve`](https://bun.com/docs/runtime/http/server) and `Bun.file` directly.
+
+## Usage
+
+Install with `bun add -D @sveltejs/adapter-bun`, then add the adapter to your `vite.config.js`:
+
+```js
+// @errors: 2307 2554
+/// file: vite.config.js
+import adapter from '@sveltejs/adapter-bun';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ sveltekit({
+ adapter: adapter()
+ })
+ ]
+});
+```
+
+Build your app with `bun run build`, then start it with:
+
+```sh
+bun ./build
+```
+
+The default output directory is `build`. Production dependencies are externalised in the same way as with [`adapter-node`](adapter-node): packages in `dependencies` must be installed alongside the build, while packages in `devDependencies` are bundled into it.
+
+Client assets and prerendered pages are served with Bun-native file responses. This includes streaming and range requests, conditional requests using `ETag` and `Last-Modified`, correct MIME types, immutable caching for hashed SvelteKit assets, and optional precompressed Brotli and gzip files.
+
+## Options
+
+The adapter accepts these options:
+
+```js
+// @errors: 2307 2554
+/// file: vite.config.js
+import adapter from '@sveltejs/adapter-bun';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ sveltekit({
+ adapter: adapter({
+ out: 'build',
+ precompress: true,
+ envPrefix: '',
+ serverOptions: {
+ idleTimeout: 30
+ },
+ compile: false
+ })
+ })
+ ]
+});
+```
+
+### out
+
+The directory to build the server to. It defaults to `build`.
+
+### precompress
+
+Precompresses assets and prerendered pages with gzip and Brotli. It defaults to `true`. The server selects the best supported representation from the request's `Accept-Encoding` header.
+
+### envPrefix
+
+Adds a prefix to all environment variables read by the production server. For example, with `envPrefix: 'MY_'`, configure the server with `MY_HOST`, `MY_PORT`, and `MY_REUSE_PORT`.
+
+### serverOptions
+
+Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings such as `hostname`, `port`, `reusePort`, `ipv6Only`, `idleTimeout`, `development`, `maxRequestBodySize`, `tls`, `http3`, and `http1`. Environment variables override these defaults.
+
+`fetch`, `routes`, `websocket`, and `error` handlers cannot be serialized. To use those APIs, create a [custom server](#Custom-server).
+
+### compile
+
+Set `compile: true` to additionally generate `build/app`, a single executable containing the Bun runtime, your server code, client assets, and prerendered pages:
+
+```js
+adapter({ compile: true });
+```
+
+Only the executable is required at runtime. It is specific to the platform on which it was built. Advanced options can select another Bun target and enable minification, bytecode, or source maps:
+
+```js
+adapter({
+ compile: {
+ outfile: 'build/my-app',
+ target: 'bun-linux-x64',
+ minify: true,
+ bytecode: true,
+ sourcemap: true
+ }
+});
+```
+
+The `bun` executable must be available while the SvelteKit build runs. Native dependencies and cross-compilation have the same constraints as [`bun build --compile`](https://bun.com/docs/bundler/executables).
+
+## Environment variables
+
+In production, Bun automatically reads `.env` files. All of the following variables can be prefixed using `envPrefix`.
+
+### `PORT`, `HOST`, and `SOCKET_PATH`
+
+The server listens on `0.0.0.0:3000` by default. Configure a TCP listener with `HOST` and `PORT`, or set `SOCKET_PATH` to use a Unix domain socket instead:
+
+```sh
+HOST=127.0.0.1 PORT=4000 bun ./build
+SOCKET_PATH=/tmp/sveltekit.sock bun ./build
+```
+
+On Linux, `SOCKET_PATH` may begin with a null byte to use an abstract namespace socket.
+
+### `REUSE_PORT` and `IPV6_ONLY`
+
+Set `REUSE_PORT=true` to let multiple Bun processes bind the same port. The operating system load balances requests between them. `SO_REUSEPORT` is supported on Linux; macOS and Windows ignore it.
+
+Set `IPV6_ONLY=true` to enable `IPV6_V6ONLY` on an IPv6 listener.
+
+### `BODY_SIZE_LIMIT`
+
+The maximum request body size in bytes. It supports `K`, `M`, and `G` suffixes and defaults to `512K`.
+
+### `IDLE_TIMEOUT` and `SHUTDOWN_TIMEOUT`
+
+`IDLE_TIMEOUT` sets Bun's connection inactivity timeout in seconds. It must be between `0` and `255`; `0` disables the timeout. The adapter automatically disables the timeout for server-sent event responses.
+
+On `SIGINT` or `SIGTERM`, the server stops accepting connections and waits for in-flight requests. `SHUTDOWN_TIMEOUT` controls how many seconds it waits before forcefully closing active connections and defaults to `30`.
+
+### `DEVELOPMENT`
+
+Set `DEVELOPMENT=true` to enable Bun's contextual server error pages. It defaults to `false` in the generated production server.
+
+### TLS and HTTP/3
+
+Use `TLS_CERT` and `TLS_KEY` to provide certificate and private-key file paths. Each value may instead be a JSON array of file paths. The following additional variables are available:
+
+- `TLS_CA`
+- `TLS_PASSPHRASE`
+- `TLS_SERVER_NAME`
+- `TLS_DH_PARAMS_FILE`
+- `TLS_LOW_MEMORY_MODE`
+- `TLS_SECURE_OPTIONS`
+
+Set `HTTP3=true` to enable Bun's experimental HTTP/3 support. This requires TLS and cannot be combined with `SOCKET_PATH`. Set `HTTP1=false` together with `HTTP3=true` for an HTTP/3-only listener.
+
+### Proxy headers
+
+When [`paths.origin`](configuration#paths) is not configured, the adapter derives the request origin from Bun's request URL and the `host` header. Set `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` when a trusted reverse proxy exposes the public origin through other headers:
+
+```sh
+PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host bun ./build
+```
+
+Set `ADDRESS_HEADER` to the trusted proxy header containing the client address. If it is `x-forwarded-for`, set `XFF_DEPTH` to the number of trusted proxies and the adapter will select the address from the right-hand side of the list.
+
+Only use these variables behind a trusted proxy because clients can spoof forwarded headers.
+
+## Platform-specific context
+
+The `platform` property contains the original `Request` and Bun `Server`:
+
+```js
+/** @type {import('./$types').RequestHandler} */
+export function GET({ platform }) {
+ const address = platform.server.requestIP(platform.request);
+ return Response.json(address);
+}
+```
+
+## Custom server
+
+The build contains `index.js`, which starts the default server, and `handler.js`, which exports the Bun-native SvelteKit request handler. Import the handler when you need Bun routes, WebSockets, custom error handling, or other `Bun.serve` options that cannot be represented as JSON:
+
+```js
+/// file: server.js
+import { handler } from './build/handler.js';
+
+const server = Bun.serve({
+ port: 3000,
+ routes: {
+ '/health': new Response('ok')
+ },
+ websocket: {
+ message(socket, message) {
+ socket.send(message);
+ }
+ },
+ fetch: handler,
+ error(error) {
+ console.error(error);
+ return new Response('Internal Server Error', { status: 500 });
+ }
+});
+
+console.log(`Listening on ${server.url}`);
+```
+
+When using a custom server, implement lifecycle behavior such as signal handling yourself. The handler still serves static and prerendered files and reads the proxy-header environment variables described above.
diff --git a/packages/adapter-bun/.gitignore b/packages/adapter-bun/.gitignore
new file mode 100644
index 000000000000..7b1c9a0ade28
--- /dev/null
+++ b/packages/adapter-bun/.gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+node_modules
+/files
+coverage/
+
diff --git a/packages/adapter-bun/CHANGELOG.md b/packages/adapter-bun/CHANGELOG.md
new file mode 100644
index 000000000000..de4bffe01c21
--- /dev/null
+++ b/packages/adapter-bun/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @sveltejs/adapter-bun
+
+## 1.0.0-next.0
+
+### Patch Changes
+
+- Initial release
diff --git a/packages/adapter-bun/LICENSE b/packages/adapter-bun/LICENSE
new file mode 100644
index 000000000000..b0306577d9da
--- /dev/null
+++ b/packages/adapter-bun/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2026 Svelte contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/packages/adapter-bun/README.md b/packages/adapter-bun/README.md
new file mode 100644
index 000000000000..f2b3b06bb449
--- /dev/null
+++ b/packages/adapter-bun/README.md
@@ -0,0 +1,15 @@
+# @sveltejs/adapter-bun
+
+[Adapter](https://svelte.dev/docs/kit/adapters) for SvelteKit apps that generates a standalone Bun server.
+
+## Docs
+
+[Docs](https://svelte.dev/docs/kit/adapter-bun)
+
+## Changelog
+
+[The Changelog for this package is available on GitHub](https://github.com/sveltejs/kit/blob/main/packages/adapter-bun/CHANGELOG.md).
+
+## License
+
+[MIT](LICENSE)
diff --git a/packages/adapter-bun/ambient.d.ts b/packages/adapter-bun/ambient.d.ts
new file mode 100644
index 000000000000..7cc997913082
--- /dev/null
+++ b/packages/adapter-bun/ambient.d.ts
@@ -0,0 +1,14 @@
+///
+
+import type { Server } from 'bun';
+
+declare global {
+ namespace App {
+ export interface Platform {
+ /** The original Web API request received by `Bun.serve`. */
+ request: Request;
+ /** The Bun HTTP server handling the request. */
+ server: Server;
+ }
+ }
+}
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
new file mode 100644
index 000000000000..e9f52df02c35
--- /dev/null
+++ b/packages/adapter-bun/index.d.ts
@@ -0,0 +1,66 @@
+import type { Adapter } from '@sveltejs/kit';
+import type { Serve, TLSOptions } from 'bun';
+import './ambient.js';
+
+declare global {
+ const ENV_PREFIX: string;
+ const PRECOMPRESS: boolean;
+ const ORIGIN: string | undefined;
+}
+
+type ServerOptions = Omit<
+ Serve.BaseServeOptions & Serve.HostnamePortServeOptions,
+ 'fetch' | 'routes' | 'websocket' | 'error'
+> & {
+ unix?: string;
+ tls?: TLSOptions | TLSOptions[];
+};
+
+interface CompileOptions {
+ /**
+ * The executable path, relative to the project root.
+ * @default `${out}/app`
+ */
+ outfile?: string;
+ /**
+ * A Bun executable target such as `bun-linux-x64` or `bun-windows-x64-baseline`.
+ * By default, Bun compiles for the current platform.
+ */
+ target?: string;
+ /** Include Bun bytecode to improve startup time. */
+ bytecode?: boolean;
+ /** Minify the server bundle before compiling it. */
+ minify?: boolean;
+ /** Generate a source map alongside the executable. */
+ sourcemap?: boolean;
+}
+
+interface AdapterOptions {
+ /**
+ * The directory to build the server to.
+ * @default 'build'
+ */
+ out?: string;
+ /**
+ * Enables precompressing assets and prerendered pages with gzip and brotli.
+ * @default true
+ */
+ precompress?: boolean;
+ /**
+ * A prefix for the environment variables used to configure the production server.
+ */
+ envPrefix?: string;
+ /**
+ * Default options passed to `Bun.serve`. Environment variables take precedence.
+ * The options must be JSON-serializable. Use `build/handler.js` with a custom
+ * `Bun.serve` call for routes, WebSockets, or custom error handling.
+ */
+ serverOptions?: ServerOptions;
+ /**
+ * Compile the build into a single executable containing the server and static assets.
+ * @default false
+ */
+ compile?: boolean | CompileOptions;
+}
+
+export default function plugin(options?: AdapterOptions): Adapter;
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
new file mode 100644
index 000000000000..86c1b07b3b5e
--- /dev/null
+++ b/packages/adapter-bun/index.js
@@ -0,0 +1,284 @@
+import { readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
+import { spawn } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { rolldown } from 'rolldown';
+
+const files = fileURLToPath(new URL('./files', import.meta.url).href);
+
+/** @param {string} str */
+function escape_regex(str) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+/** @type {import('./index.js').default} */
+export default function (opts = {}) {
+ const {
+ out = 'build',
+ precompress = true,
+ envPrefix = '',
+ serverOptions = {},
+ compile = false
+ } = opts;
+
+ return {
+ name: '@sveltejs/adapter-bun',
+ async adapt(builder) {
+ const tmp = builder.getBuildDirectory('adapter-bun');
+ const base = builder.config.kit.paths.base;
+
+ builder.rimraf(out);
+ builder.rimraf(tmp);
+ builder.mkdirp(tmp);
+ builder.mkdirp(`${out}/client${base}`);
+ builder.mkdirp(`${out}/prerendered${base}`);
+
+ builder.log.minor('Copying assets');
+ const client_files = with_base(builder.writeClient(`${out}/client${base}`), base);
+ const prerendered_files = with_base(
+ builder.writePrerendered(`${out}/prerendered${base}`),
+ base
+ );
+
+ /** @type {string[]} */
+ let client_compressed = [];
+ /** @type {string[]} */
+ let prerendered_compressed = [];
+ if (precompress) {
+ builder.log.minor('Compressing assets');
+ [client_compressed, prerendered_compressed] = await Promise.all([
+ builder.compress(`${out}/client`),
+ builder.compress(`${out}/prerendered`)
+ ]);
+ }
+ const compressed_files = [...client_compressed, ...prerendered_compressed];
+
+ builder.log.minor('Building server');
+
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
+ const server = builder.getServerDirectory();
+ const entries = posixify(`${tmp}/entries`);
+ builder.copy(files, entries);
+
+ const dir_id = `${entries}/dir.js`;
+ const server_options_file = `${server}/adapter-bun-options.js`;
+
+ writeFileSync(
+ `${server}/manifest.js`,
+ [
+ `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
+ `export const client_files = new Set(${JSON.stringify(client_files)});`,
+ `export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
+ `export const compressed_files = new Set(${JSON.stringify(compressed_files)});`,
+ `export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
+ ].join('\n\n')
+ );
+ writeFileSync(server_options_file, `export default ${serialize(serverOptions)};\n`);
+
+ /** @type {Record} */
+ const input = {
+ index: `${entries}/index.js`,
+ handler: `${entries}/handler.js`
+ };
+
+ if (builder.hasServerInstrumentationFile()) {
+ input['instrumentation.server'] = `${server}/instrumentation.server.js`;
+ }
+
+ const bundle = await rolldown({
+ input,
+ external: [
+ 'bun',
+ /^bun:/,
+ // dependencies could have deep exports, so we need a regex
+ ...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\\/.*)?$`))
+ ],
+ platform: 'node',
+ resolve: {
+ conditionNames: ['bun', 'node']
+ },
+ experimental: {
+ nativeMagicString: true
+ },
+ plugins: [
+ {
+ name: 'adapter-bun-resolve-app',
+ resolveId(id) {
+ if (id === 'SERVER') return `${server}/index.js`;
+ if (id === 'MANIFEST') return `${server}/manifest.js`;
+ if (id === 'SERVER_OPTIONS') return server_options_file;
+ }
+ },
+ {
+ name: 'adapter-bun-replace-constants',
+ transform: {
+ filter: { id: new RegExp(escape_regex(entries)) },
+ handler(_code, _id, { magicString }) {
+ if (!magicString) {
+ throw new Error('experimental.nativeMagicString is not enabled');
+ }
+ magicString
+ .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
+ .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
+ .replace(
+ /\bORIGIN\b/g,
+ JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
+ );
+ return {
+ code: magicString,
+ map: magicString.generateMap().toString()
+ };
+ }
+ }
+ }
+ ]
+ });
+
+ await bundle.write({
+ dir: out,
+ format: 'esm',
+ sourcemap: true,
+ codeSplitting: {
+ groups: [
+ {
+ name: 'dir',
+ test: dir_id
+ }
+ ]
+ },
+ chunkFileNames(chunk) {
+ if (chunk.name === 'dir') return '[name].js';
+ return 'server/chunks/[name]-[hash].js';
+ }
+ });
+
+ if (builder.hasServerInstrumentationFile()) {
+ builder.instrument({
+ entrypoint: `${out}/index.js`,
+ instrumentation: `${out}/instrumentation.server.js`,
+ module: {
+ exports: ['hostname', 'port', 'server', 'unix']
+ }
+ });
+ }
+
+ if (compile) {
+ builder.log.minor('Compiling executable');
+ await compile_executable(out, compile === true ? {} : compile, [
+ ...client_files.map((file) => `client/${file}`),
+ ...client_compressed.flatMap((file) => [
+ `client/${posixify(file)}.br`,
+ `client/${posixify(file)}.gz`
+ ]),
+ ...prerendered_files.map((file) => `prerendered/${file}`),
+ ...prerendered_compressed.flatMap((file) => [
+ `prerendered/${posixify(file)}.br`,
+ `prerendered/${posixify(file)}.gz`
+ ])
+ ]);
+ }
+ },
+
+ supports: {
+ read: () => true,
+ instrumentation: () => true
+ }
+ };
+}
+
+/**
+ * @param {string[]} files
+ * @param {string} base
+ * @returns {string[]}
+ */
+function with_base(files, base) {
+ const prefix = base.slice(1);
+ return files.map((file) => posixify(prefix ? `${prefix}/${file}` : file));
+}
+
+/**
+ * @param {unknown} value
+ * @returns {string}
+ */
+function serialize(value) {
+ try {
+ const serialized = JSON.stringify(value, (_key, item) => {
+ if (typeof item === 'function' || typeof item === 'symbol' || typeof item === 'bigint') {
+ throw new TypeError(`serverOptions must be JSON-serializable, received ${typeof item}`);
+ }
+ if (typeof item === 'number' && !Number.isFinite(item)) {
+ throw new TypeError('serverOptions must contain only finite numbers');
+ }
+ return item;
+ });
+ if (serialized === undefined) {
+ throw new TypeError('serverOptions must be a JSON-serializable object');
+ }
+ return serialized;
+ } catch (error) {
+ throw new Error('Could not serialize adapter-bun serverOptions', { cause: error });
+ }
+}
+
+/**
+ * @param {string} out
+ * @param {NonNullable>} options
+ * @param {string[]} assets
+ * @returns {Promise}
+ */
+function compile_executable(out, options, assets) {
+ const outfile = options.outfile ?? `${out}/app`;
+ const entrypoint = `${out}/adapter-bun-compile.js`;
+ const args = ['build', '--compile'];
+
+ if (options.target) args.push(`--target=${options.target}`);
+ if (options.bytecode) args.push('--bytecode');
+ if (options.minify) args.push('--minify');
+ if (options.sourcemap) args.push('--sourcemap=linked');
+
+ const unique_assets = [...new Set(assets)];
+ const imports = unique_assets.map(
+ (file, index) =>
+ `import asset_${index} from ${JSON.stringify(`./${file}`)} with { type: 'file' };`
+ );
+ const entries = unique_assets.map((file, index) => [
+ file,
+ `{ path: asset_${index}, lastModified: ${Math.trunc(statSync(`${out}/${file}`).mtimeMs)} }`
+ ]);
+ writeFileSync(
+ entrypoint,
+ [
+ ...imports,
+ `globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
+ .map(([file, identifier]) => `[${JSON.stringify(file)}, ${identifier}]`)
+ .join(',')}]);`,
+ `await import('./index.js');`
+ ].join('\n')
+ );
+
+ args.push(entrypoint, `--outfile=${outfile}`);
+
+ return new Promise((resolve, reject) => {
+ const child = spawn('bun', args, { stdio: 'inherit' });
+ child.on('error', (error) => {
+ rmSync(entrypoint, { force: true });
+ reject(new Error('Could not run Bun to compile the server executable', { cause: error }));
+ });
+ child.on('exit', (code, signal) => {
+ rmSync(entrypoint, { force: true });
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(
+ new Error(
+ `Bun executable compilation failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}`
+ )
+ );
+ }
+ });
+ });
+}
+
+/** @param {string} str */
+function posixify(str) {
+ return str.replace(/\\/g, '/');
+}
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
new file mode 100644
index 000000000000..239e6b614761
--- /dev/null
+++ b/packages/adapter-bun/internal.d.ts
@@ -0,0 +1,18 @@
+declare module 'MANIFEST' {
+ import type { SSRManifest } from '@sveltejs/kit';
+
+ export const client_files: Set;
+ export const compressed_files: Set;
+ export const manifest: SSRManifest;
+ export const prerendered_files: Set;
+ export const prerendered_paths: Set;
+}
+
+declare module 'SERVER' {
+ export { Server } from '@sveltejs/kit';
+}
+
+declare module 'SERVER_OPTIONS' {
+ const options: Record;
+ export default options;
+}
diff --git a/packages/adapter-bun/package.json b/packages/adapter-bun/package.json
new file mode 100644
index 000000000000..a837be148a36
--- /dev/null
+++ b/packages/adapter-bun/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "@sveltejs/adapter-bun",
+ "version": "1.0.0-next.0",
+ "description": "Adapter for SvelteKit apps that generates a standalone Bun server",
+ "keywords": [
+ "adapter",
+ "bun",
+ "deploy",
+ "hosting",
+ "svelte",
+ "sveltekit"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/sveltejs/kit.git",
+ "directory": "packages/adapter-bun"
+ },
+ "license": "MIT",
+ "homepage": "https://svelte.dev/docs/kit/adapter-bun",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./index.d.ts",
+ "import": "./index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "types": "index.d.ts",
+ "files": [
+ "files",
+ "index.js",
+ "index.d.ts",
+ "ambient.d.ts"
+ ],
+ "scripts": {
+ "dev": "rolldown -cw",
+ "build": "rolldown -c",
+ "test": "vitest run",
+ "check": "tsc",
+ "lint": "prettier --check .",
+ "format": "pnpm lint --write",
+ "prepublishOnly": "pnpm build"
+ },
+ "devDependencies": {
+ "@playwright/test": "catalog:",
+ "@sveltejs/kit": "workspace:^",
+ "@types/node": "catalog:",
+ "typescript": "catalog:",
+ "vitest": "catalog:"
+ },
+ "dependencies": {
+ "bun-types": "^1.3.14",
+ "rolldown": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@sveltejs/kit": "^3.0.0-next.0"
+ }
+}
diff --git a/packages/adapter-bun/rolldown.config.js b/packages/adapter-bun/rolldown.config.js
new file mode 100644
index 000000000000..c8f06672853f
--- /dev/null
+++ b/packages/adapter-bun/rolldown.config.js
@@ -0,0 +1,64 @@
+/** @import { Plugin, RolldownOptions } from 'rolldown' */
+import { builtinModules } from 'node:module';
+import { rmSync } from 'node:fs';
+import { join } from 'node:path';
+
+/**
+ * @param {string} filepath
+ * @returns {Plugin}
+ */
+function clearOutput(filepath) {
+ return {
+ name: 'clear-output',
+ buildStart: {
+ order: 'pre',
+ sequential: true,
+ handler() {
+ rmSync(filepath, { recursive: true, force: true });
+ }
+ }
+ };
+}
+
+/** @returns {Plugin} */
+function prefixBuiltinModules() {
+ return {
+ name: 'prefix-built-in-modules',
+ resolveId(source) {
+ if (builtinModules.includes(source)) {
+ return { id: 'node:' + source, external: true };
+ }
+ }
+ };
+}
+
+const dir_id = join(import.meta.dirname, 'src', 'dir.js');
+
+/** @type {RolldownOptions} */
+export default {
+ input: {
+ index: 'src/index.js',
+ handler: 'src/handler.js'
+ },
+ output: {
+ dir: 'files',
+ format: 'esm',
+ hoistTransitiveImports: false,
+ chunkFileNames(chunk) {
+ if (chunk.name === 'dir') return '[name].js';
+ return 'chunks/[name].js';
+ },
+ codeSplitting: {
+ groups: [
+ {
+ name: 'dir',
+ test: dir_id
+ }
+ ]
+ }
+ },
+ plugins: [clearOutput('files'), prefixBuiltinModules()],
+ // resolved at adapt time
+ external: ['MANIFEST', 'SERVER', 'SERVER_OPTIONS'],
+ platform: 'node'
+};
diff --git a/packages/adapter-bun/src/dir.js b/packages/adapter-bun/src/dir.js
new file mode 100644
index 000000000000..0d659cb20e31
--- /dev/null
+++ b/packages/adapter-bun/src/dir.js
@@ -0,0 +1,4 @@
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const dir = dirname(fileURLToPath(import.meta.url));
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
new file mode 100644
index 000000000000..10d91f1e190a
--- /dev/null
+++ b/packages/adapter-bun/src/env.js
@@ -0,0 +1,102 @@
+import process from 'node:process';
+
+const expected = new Set([
+ 'SOCKET_PATH',
+ 'HOST',
+ 'PORT',
+ 'REUSE_PORT',
+ 'IPV6_ONLY',
+ 'IDLE_TIMEOUT',
+ 'BODY_SIZE_LIMIT',
+ 'DEVELOPMENT',
+ 'HTTP3',
+ 'HTTP1',
+ 'TLS_CERT',
+ 'TLS_KEY',
+ 'TLS_CA',
+ 'TLS_PASSPHRASE',
+ 'TLS_SERVER_NAME',
+ 'TLS_DH_PARAMS_FILE',
+ 'TLS_LOW_MEMORY_MODE',
+ 'TLS_SECURE_OPTIONS',
+ 'XFF_DEPTH',
+ 'ADDRESS_HEADER',
+ 'PROTOCOL_HEADER',
+ 'HOST_HEADER',
+ 'PORT_HEADER',
+ 'SHUTDOWN_TIMEOUT'
+]);
+
+export const env_prefix = ENV_PREFIX;
+
+if (env_prefix) {
+ for (const name in process.env) {
+ if (name.startsWith(env_prefix)) {
+ const unprefixed = name.slice(env_prefix.length);
+ if (!expected.has(unprefixed)) {
+ throw new Error(
+ `You should change envPrefix (${env_prefix}) to avoid conflicts with existing environment variables — unexpectedly saw ${name}`
+ );
+ }
+ }
+ }
+}
+
+/**
+ * @param {string} name
+ * @param {string | undefined} [fallback]
+ * @returns {string | undefined}
+ */
+export function env(name, fallback) {
+ const prefixed = env_prefix + name;
+ return prefixed in process.env ? process.env[prefixed] : fallback;
+}
+
+/**
+ * @param {string} name
+ * @param {boolean | undefined} [fallback]
+ * @returns {boolean | undefined}
+ */
+export function boolean_env(name, fallback) {
+ const value = env(name);
+ if (value === undefined) return fallback;
+ if (/^(?:1|true|yes|on)$/i.test(value)) return true;
+ if (/^(?:0|false|no|off)$/i.test(value)) return false;
+
+ throw new Error(
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a boolean)`
+ );
+}
+
+/**
+ * @param {string} name
+ * @param {number | undefined} [fallback]
+ * @param {{ min?: number; max?: number }} [limits]
+ * @returns {number | undefined}
+ */
+export function number_env(name, fallback, limits = {}) {
+ const value = env(name);
+ if (value === undefined) return fallback;
+ if (!/^\d+$/.test(value)) {
+ throw new Error(
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative integer)`
+ );
+ }
+
+ const number = Number(value);
+ if (
+ !Number.isSafeInteger(number) ||
+ number < (limits.min ?? 0) ||
+ number > (limits.max ?? Infinity)
+ ) {
+ const range =
+ limits.max === undefined
+ ? `at least ${limits.min ?? 0}`
+ : `between ${limits.min ?? 0} and ${limits.max}`;
+ throw new Error(
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected an integer ${range})`
+ );
+ }
+
+ return number;
+}
diff --git a/packages/adapter-bun/src/env.spec.ts b/packages/adapter-bun/src/env.spec.ts
new file mode 100644
index 000000000000..abeb41c86186
--- /dev/null
+++ b/packages/adapter-bun/src/env.spec.ts
@@ -0,0 +1,50 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import { boolean_env, number_env } from './env.js';
+
+vi.hoisted(() => {
+ vi.stubGlobal('ENV_PREFIX', '');
+});
+
+describe('boolean_env', () => {
+ afterEach(() => vi.unstubAllEnvs());
+
+ test.each(['1', 'true', 'YES', 'on'])('parses %s as true', (value) => {
+ vi.stubEnv('OPTION', value);
+ expect(boolean_env('OPTION')).toBe(true);
+ });
+
+ test.each(['0', 'false', 'NO', 'off'])('parses %s as false', (value) => {
+ vi.stubEnv('OPTION', value);
+ expect(boolean_env('OPTION')).toBe(false);
+ });
+
+ test('uses the fallback when the variable is not set', () => {
+ expect(boolean_env('OPTION', true)).toBe(true);
+ });
+
+ test('rejects other values', () => {
+ vi.stubEnv('OPTION', 'maybe');
+ expect(() => boolean_env('OPTION')).toThrow('expected a boolean');
+ });
+});
+
+describe('number_env', () => {
+ afterEach(() => vi.unstubAllEnvs());
+
+ test('parses non-negative integers', () => {
+ vi.stubEnv('OPTION', '0');
+ expect(number_env('OPTION')).toBe(0);
+ });
+
+ test('enforces limits', () => {
+ vi.stubEnv('OPTION', '256');
+ expect(() => number_env('OPTION', undefined, { max: 255 })).toThrow(
+ 'expected an integer between 0 and 255'
+ );
+ });
+
+ test('rejects non-integers', () => {
+ vi.stubEnv('OPTION', '1.5');
+ expect(() => number_env('OPTION')).toThrow('expected a non-negative integer');
+ });
+});
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
new file mode 100644
index 000000000000..1ae1728b1da1
--- /dev/null
+++ b/packages/adapter-bun/src/handler.js
@@ -0,0 +1,136 @@
+import { Server } from 'SERVER';
+import { manifest } from 'MANIFEST';
+import { env, env_prefix, number_env } from './env.js';
+import { asset_path, serve_static } from './static.js';
+
+const server = new Server(manifest);
+const origin = ORIGIN;
+
+const address_header = env('ADDRESS_HEADER', '')?.toLowerCase();
+const protocol_header = env('PROTOCOL_HEADER', '')?.toLowerCase();
+const host_header = env('HOST_HEADER', '')?.toLowerCase();
+const port_header = env('PORT_HEADER', '')?.toLowerCase();
+const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 }) ?? 1;
+
+await server.init({
+ env: Bun.env,
+ read: (file) => Bun.file(asset_path('client', file)).stream()
+});
+
+/**
+ * The Bun-native SvelteKit request handler. Import it from `build/handler.js`
+ * when an application needs to construct `Bun.serve` itself.
+ * @param {Request} request
+ * @param {import('bun').Server} bun_server
+ * @returns {Promise}
+ */
+export async function handler(request, bun_server) {
+ const url = new URL(request.url);
+
+ let pathname;
+ try {
+ pathname = decodeURIComponent(url.pathname);
+ } catch {
+ return new Response('Bad Request', { status: 400 });
+ }
+
+ const static_response = await serve_static(request, pathname);
+ if (static_response) return static_response;
+
+ let request_origin = origin;
+ try {
+ request_origin ||= get_origin(request, url);
+ } catch (error) {
+ console.error(
+ `Could not determine request origin: ${error instanceof Error ? error.message : String(error)}`
+ );
+ return new Response('Bad Request', { status: 400 });
+ }
+
+ let normalized_request = request;
+ if (request_origin !== url.origin) {
+ const normalized_url = new URL(url.pathname + url.search, request_origin);
+ normalized_request = new Request(normalized_url, request);
+ }
+
+ const response = await server.respond(normalized_request, {
+ platform: {
+ request,
+ server: bun_server
+ },
+ getClientAddress: () => get_client_address(request, bun_server)
+ });
+
+ if (response.headers.get('content-type')?.startsWith('text/event-stream')) {
+ bun_server.timeout(request, 0);
+ response.headers.set('x-accel-buffering', 'no');
+ }
+
+ return response;
+}
+
+/**
+ * @param {Request} request
+ * @param {URL} url
+ * @returns {string}
+ */
+function get_origin(request, url) {
+ const protocol = decodeURIComponent(
+ (protocol_header ? request.headers.get(protocol_header) : null) || url.protocol.slice(0, -1)
+ );
+ if (protocol.includes(':')) {
+ throw new Error(
+ `The ${protocol_header} header specified ${protocol}, which is invalid because it includes \`:\``
+ );
+ }
+
+ const host =
+ (host_header ? request.headers.get(host_header) : null) ||
+ request.headers.get('host') ||
+ url.host;
+ if (!host) {
+ throw new Error(
+ `The request must include a ${host_header ? `${host_header} or host` : 'host'} header`
+ );
+ }
+
+ const port = port_header ? request.headers.get(port_header) : null;
+ if (port && !/^\d+$/.test(port)) {
+ throw new Error(`The ${port_header} header specified an invalid port: ${port}`);
+ }
+
+ const value = port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
+ return new URL(value).origin;
+}
+
+/**
+ * @param {Request} request
+ * @param {import('bun').Server} bun_server
+ * @returns {string}
+ */
+function get_client_address(request, bun_server) {
+ if (address_header) {
+ const value = request.headers.get(address_header);
+ if (value === null) {
+ throw new Error(
+ `Address header was specified with ${env_prefix + 'ADDRESS_HEADER'}=${address_header} but is absent from request`
+ );
+ }
+
+ if (address_header === 'x-forwarded-for') {
+ const addresses = value.split(',');
+ if (xff_depth > addresses.length) {
+ throw new Error(
+ `${env_prefix + 'XFF_DEPTH'} is ${xff_depth}, but only found ${addresses.length} addresses`
+ );
+ }
+ return addresses[addresses.length - xff_depth].trim();
+ }
+
+ return value;
+ }
+
+ const address = bun_server.requestIP(request)?.address;
+ if (!address) throw new Error('Could not determine client address');
+ return address;
+}
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
new file mode 100644
index 000000000000..b9fd667ae3a1
--- /dev/null
+++ b/packages/adapter-bun/src/index.js
@@ -0,0 +1,185 @@
+import process from 'node:process';
+import server_options from 'SERVER_OPTIONS';
+import { handler } from './handler.js';
+import { boolean_env, env, number_env } from './env.js';
+import { parse_as_bytes } from './utils.js';
+
+const options = { ...server_options };
+delete options.fetch;
+
+export const unix = env('SOCKET_PATH', /** @type {string | undefined} */ (options.unix));
+export const hostname = env(
+ 'HOST',
+ /** @type {string | undefined} */ (options.hostname) ?? '0.0.0.0'
+);
+export const port = env('PORT', options.port === undefined ? '3000' : String(options.port));
+
+if (unix) {
+ options.unix = unix;
+ delete options.hostname;
+ delete options.port;
+ delete options.reusePort;
+ delete options.ipv6Only;
+} else {
+ delete options.unix;
+ options.hostname = hostname;
+ options.port = port;
+ options.reusePort = boolean_env(
+ 'REUSE_PORT',
+ /** @type {boolean | undefined} */ (options.reusePort)
+ );
+ options.ipv6Only = boolean_env(
+ 'IPV6_ONLY',
+ /** @type {boolean | undefined} */ (options.ipv6Only)
+ );
+}
+
+options.idleTimeout = number_env(
+ 'IDLE_TIMEOUT',
+ /** @type {number | undefined} */ (options.idleTimeout),
+ { max: 255 }
+);
+const development = boolean_env('DEVELOPMENT');
+if (development !== undefined) {
+ options.development = development;
+} else if (options.development === undefined) {
+ options.development = false;
+}
+
+const body_size_limit = parse_as_bytes(
+ env('BODY_SIZE_LIMIT', String(options.maxRequestBodySize ?? '512K')) || ''
+);
+if (!Number.isSafeInteger(body_size_limit) || body_size_limit < 0) {
+ throw new Error(
+ `Invalid BODY_SIZE_LIMIT: ${JSON.stringify(env('BODY_SIZE_LIMIT'))}. Please provide a non-negative integer with an optional K, M, or G suffix.`
+ );
+}
+options.maxRequestBodySize = body_size_limit;
+
+const http3 = boolean_env('HTTP3', /** @type {boolean | undefined} */ (options.http3));
+const http1 = boolean_env('HTTP1', /** @type {boolean | undefined} */ (options.http1));
+if (http3 !== undefined) options.http3 = http3;
+if (http1 !== undefined) options.http1 = http1;
+
+const tls = get_tls_options(options.tls);
+if (tls) options.tls = tls;
+
+if (options.http3 && !options.tls) {
+ throw new Error('HTTP3 requires TLS_CERT and TLS_KEY or TLS server options');
+}
+if (options.http1 === false && !options.http3) {
+ throw new Error('HTTP1=false requires HTTP3=true');
+}
+if (unix && options.http3) {
+ throw new Error('HTTP3 cannot be used with SOCKET_PATH');
+}
+
+options.fetch = handler;
+
+export const server = Bun.serve(
+ /** @type {import('bun').Serve.Options} */ (/** @type {unknown} */ (options))
+);
+
+console.log(unix ? `Listening on ${unix}` : `Listening on ${server.url}`);
+
+const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT', 30) ?? 30;
+let shutting_down = false;
+
+/** @param {'SIGINT' | 'SIGTERM'} reason */
+async function graceful_shutdown(reason) {
+ if (shutting_down) return;
+ shutting_down = true;
+
+ let forced = false;
+ const timeout = setTimeout(() => {
+ forced = true;
+ void server.stop(true);
+ }, shutdown_timeout * 1000);
+
+ await server.stop(false);
+ clearTimeout(timeout);
+ // @ts-expect-error custom events cannot be typed
+ process.emit('sveltekit:shutdown', reason);
+
+ if (forced) console.warn(`Forced shutdown after ${shutdown_timeout} seconds`);
+}
+
+process.on('SIGTERM', () => void graceful_shutdown('SIGTERM'));
+process.on('SIGINT', () => void graceful_shutdown('SIGINT'));
+
+/**
+ * @param {unknown} configured
+ * @returns {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined}
+ */
+function get_tls_options(configured) {
+ const cert = env('TLS_CERT');
+ const key = env('TLS_KEY');
+ const ca = env('TLS_CA');
+ const passphrase = env('TLS_PASSPHRASE');
+ const server_name = env('TLS_SERVER_NAME');
+ const dh_params_file = env('TLS_DH_PARAMS_FILE');
+ const low_memory_mode = boolean_env('TLS_LOW_MEMORY_MODE');
+ const secure_options = number_env('TLS_SECURE_OPTIONS');
+
+ if (
+ cert === undefined &&
+ key === undefined &&
+ ca === undefined &&
+ passphrase === undefined &&
+ server_name === undefined &&
+ dh_params_file === undefined &&
+ low_memory_mode === undefined &&
+ secure_options === undefined
+ ) {
+ return /** @type {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined} */ (
+ configured
+ );
+ }
+
+ if (Array.isArray(configured)) {
+ throw new Error(
+ 'TLS environment variables cannot be merged with an SNI array from serverOptions'
+ );
+ }
+
+ const tls = /** @type {import('bun').TLSOptions} */ ({
+ ...(configured && typeof configured === 'object' ? configured : {})
+ });
+ if (cert !== undefined) tls.cert = tls_files(cert);
+ if (key !== undefined) tls.key = tls_files(key);
+ if (ca !== undefined) tls.ca = tls_files(ca);
+ if (passphrase !== undefined) tls.passphrase = passphrase;
+ if (server_name !== undefined) tls.serverName = server_name;
+ if (dh_params_file !== undefined) tls.dhParamsFile = dh_params_file;
+ if (low_memory_mode !== undefined) tls.lowMemoryMode = low_memory_mode;
+ if (secure_options !== undefined) tls.secureOptions = secure_options;
+
+ if (!tls.cert || !tls.key) {
+ throw new Error('TLS requires both TLS_CERT and TLS_KEY');
+ }
+
+ return tls;
+}
+
+/**
+ * @param {string} value
+ * @returns {import('bun').BunFile | import('bun').BunFile[]}
+ */
+function tls_files(value) {
+ /** @type {unknown} */
+ let paths;
+ try {
+ paths = value.startsWith('[') ? JSON.parse(value) : value;
+ } catch (error) {
+ throw new Error('TLS file lists must be JSON arrays of paths', { cause: error });
+ }
+
+ if (Array.isArray(paths)) {
+ if (!paths.every((path) => typeof path === 'string')) {
+ throw new Error('TLS file paths must be strings');
+ }
+ return paths.map((path) => Bun.file(path));
+ }
+ if (typeof paths !== 'string') throw new Error('TLS file paths must be strings');
+ return Bun.file(paths);
+}
diff --git a/packages/adapter-bun/src/static.js b/packages/adapter-bun/src/static.js
new file mode 100644
index 000000000000..eee9116a6fe2
--- /dev/null
+++ b/packages/adapter-bun/src/static.js
@@ -0,0 +1,194 @@
+import { extname, join } from 'node:path';
+import {
+ client_files,
+ compressed_files,
+ manifest,
+ prerendered_files,
+ prerendered_paths
+} from 'MANIFEST';
+import { dir } from './dir.js';
+import { accepts_encoding, append_vary } from './utils.js';
+
+const embedded_files =
+ /** @type {Map | undefined} */ (
+ /** @type {any} */ (globalThis)[Symbol.for('sveltekit.adapter-bun.assets')]
+ );
+
+/**
+ * @param {'client' | 'prerendered'} directory
+ * @param {string} relative
+ * @returns {{ path: string; lastModified?: number }}
+ */
+function asset(directory, relative) {
+ return (
+ embedded_files?.get(`${directory}/${relative}`) ?? {
+ path: join(dir, directory, relative)
+ }
+ );
+}
+
+/**
+ * @param {'client' | 'prerendered'} directory
+ * @param {string} relative
+ * @returns {string}
+ */
+export function asset_path(directory, relative) {
+ return asset(directory, relative).path;
+}
+
+/**
+ * @param {string} pathname
+ * @returns {string | undefined}
+ */
+function find_prerendered_file(pathname) {
+ const relative = pathname.slice(1);
+ return (
+ relative.endsWith('/')
+ ? [`${relative}index.html`]
+ : [relative, `${relative}.html`, `${relative}/index.html`]
+ ).find((candidate) => prerendered_files.has(candidate));
+}
+
+/**
+ * Relative reference from `from` to `to`, which must differ only by a trailing slash.
+ * Keep in sync with the copy in `packages/kit/src/utils/url.js`.
+ * @param {string} from
+ * @param {string} to
+ * @returns {string}
+ */
+function relative_pathname(from, to) {
+ const segment = to.replace(/\/$/, '').split('/').at(-1);
+ return from.endsWith('/') ? `../${segment}` : `${segment}/`;
+}
+
+/**
+ * @param {Request} request
+ * @param {string} pathname
+ * @returns {Promise}
+ */
+export async function serve_static(request, pathname) {
+ if (request.method !== 'GET' && request.method !== 'HEAD') return;
+
+ const client_file = pathname.slice(1);
+ if (client_files.has(client_file)) {
+ return serve_file(request, client_file, true);
+ }
+
+ if (prerendered_paths.has(pathname)) {
+ const file = find_prerendered_file(pathname);
+ if (file) return serve_file(request, file, false);
+ }
+
+ const inverted = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
+ if (prerendered_paths.has(inverted)) {
+ const url = new URL(request.url);
+ return new Response(null, {
+ status: 308,
+ headers: { location: relative_pathname(pathname, inverted) + url.search }
+ });
+ }
+}
+
+/**
+ * @param {Request} request
+ * @param {string} relative
+ * @param {boolean} client
+ * @returns {Promise}
+ */
+async function serve_file(request, relative, client) {
+ const directory = client ? 'client' : 'prerendered';
+ const original = asset(directory, relative);
+ const can_precompress =
+ PRECOMPRESS && compressed_files.has(relative) && !request.headers.has('range');
+
+ /** @type {'br' | 'gzip' | undefined} */
+ let encoding;
+ if (can_precompress && accepts_encoding(request.headers.get('accept-encoding'), 'br')) {
+ encoding = 'br';
+ } else if (can_precompress && accepts_encoding(request.headers.get('accept-encoding'), 'gzip')) {
+ encoding = 'gzip';
+ }
+
+ const selected =
+ encoding === 'br'
+ ? asset(directory, `${relative}.br`)
+ : encoding === 'gzip'
+ ? asset(directory, `${relative}.gz`)
+ : original;
+ const file = Bun.file(selected.path);
+ if (!(await file.exists())) return;
+
+ const size = file.size;
+ const last_modified = Math.trunc((selected.lastModified ?? file.lastModified) / 1000) * 1000;
+ const etag = `W/"${last_modified.toString(16)}-${size.toString(16)}${encoding ? `-${encoding}` : ''}"`;
+ const headers = new Headers();
+
+ let type = manifest.mimeTypes[extname(relative)] || Bun.file(original.path).type;
+ if (type === 'text/html') type += ';charset=utf-8';
+ if (type) headers.set('content-type', type);
+
+ headers.set('accept-ranges', 'bytes');
+ headers.set('etag', etag);
+ if (last_modified > 0) {
+ headers.set('last-modified', new Date(last_modified).toUTCString());
+ }
+ if (client && relative.startsWith(`${manifest.appPath}/immutable/`)) {
+ headers.set('cache-control', 'public,max-age=31536000,immutable');
+ }
+ if (PRECOMPRESS && compressed_files.has(relative)) {
+ append_vary(headers, 'Accept-Encoding');
+ }
+ if (encoding) headers.set('content-encoding', encoding);
+
+ const if_none_match = request.headers.get('if-none-match');
+ if (
+ if_none_match === '*' ||
+ if_none_match?.split(',').some((value) => value.trim() === etag) ||
+ (!if_none_match &&
+ last_modified > 0 &&
+ new Date(request.headers.get('if-modified-since') || 0).getTime() >= last_modified)
+ ) {
+ return new Response(null, { status: 304, headers });
+ }
+
+ let start = 0;
+ let end = size - 1;
+ let status = 200;
+ const range = request.headers.get('range');
+ const if_range = request.headers.get('if-range');
+ if (
+ range &&
+ (!if_range ||
+ if_range === etag ||
+ (last_modified > 0 && new Date(if_range).getTime() >= last_modified))
+ ) {
+ const match = /^bytes=(\d*)-(\d*)$/.exec(range);
+ if (!match || (!match[1] && !match[2])) {
+ headers.set('content-range', `bytes */${size}`);
+ return new Response(null, { status: 416, headers });
+ }
+
+ if (!match[1]) {
+ const suffix = Number(match[2]);
+ start = Math.max(0, size - suffix);
+ } else {
+ start = Number(match[1]);
+ if (match[2]) end = Number(match[2]);
+ }
+
+ if (start >= size || end < start) {
+ headers.set('content-range', `bytes */${size}`);
+ return new Response(null, { status: 416, headers });
+ }
+
+ end = Math.min(end, size - 1);
+ status = 206;
+ headers.set('content-range', `bytes ${start}-${end}/${size}`);
+ }
+
+ const content_length = end - start + 1;
+ headers.set('content-length', String(content_length));
+ const body = request.method === 'HEAD' ? null : file.slice(start, end + 1);
+
+ return new Response(body, { status, headers });
+}
diff --git a/packages/adapter-bun/src/utils.js b/packages/adapter-bun/src/utils.js
new file mode 100644
index 000000000000..4053cbea3b75
--- /dev/null
+++ b/packages/adapter-bun/src/utils.js
@@ -0,0 +1,56 @@
+/**
+ * @param {string} value
+ * @returns {number}
+ */
+export function parse_as_bytes(value) {
+ const multiplier =
+ {
+ K: 1024,
+ M: 1024 * 1024,
+ G: 1024 * 1024 * 1024
+ }[value[value.length - 1]?.toUpperCase()] ?? 1;
+
+ return Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier;
+}
+
+/**
+ * @param {Headers} headers
+ * @param {string} value
+ */
+export function append_vary(headers, value) {
+ const current = headers.get('vary');
+ if (!current) {
+ headers.set('vary', value);
+ return;
+ }
+
+ const values = current.split(',').map((part) => part.trim().toLowerCase());
+ if (!values.includes(value.toLowerCase()) && !values.includes('*')) {
+ headers.set('vary', `${current}, ${value}`);
+ }
+}
+
+/**
+ * @param {string | null} header
+ * @param {string} encoding
+ * @returns {boolean}
+ */
+export function accepts_encoding(header, encoding) {
+ if (!header) return false;
+
+ /** @type {boolean | undefined} */
+ let wildcard;
+ for (const item of header.split(',')) {
+ const [name, ...parameters] = item.trim().toLowerCase().split(';');
+ let quality = 1;
+ for (const parameter of parameters) {
+ const match = /^q\s*=\s*(0(?:\.\d+)?|1(?:\.0+)?)$/.exec(parameter.trim());
+ if (match) quality = Number(match[1]);
+ }
+
+ if (name === encoding) return quality > 0;
+ if (name === '*') wildcard = quality > 0;
+ }
+
+ return wildcard ?? false;
+}
diff --git a/packages/adapter-bun/src/utils.spec.ts b/packages/adapter-bun/src/utils.spec.ts
new file mode 100644
index 000000000000..e963bfc83c88
--- /dev/null
+++ b/packages/adapter-bun/src/utils.spec.ts
@@ -0,0 +1,30 @@
+import { describe, expect, test } from 'vitest';
+import { accepts_encoding, append_vary, parse_as_bytes } from './utils.js';
+
+describe('parse_as_bytes', () => {
+ test.each([
+ ['200', 200],
+ ['512K', 512 * 1024],
+ ['200M', 200 * 1024 * 1024],
+ ['1G', 1024 * 1024 * 1024],
+ ['asdf', NaN]
+ ] as const)('parses %s', (input, expected) => {
+ expect(parse_as_bytes(input)).toBe(expected);
+ });
+});
+
+describe('accepts_encoding', () => {
+ test('honors quality values and wildcards', () => {
+ expect(accepts_encoding('gzip;q=0, *;q=1', 'gzip')).toBe(false);
+ expect(accepts_encoding('gzip;q=0, *;q=1', 'br')).toBe(true);
+ expect(accepts_encoding('br; q=0.5', 'br')).toBe(true);
+ });
+});
+
+describe('append_vary', () => {
+ test('does not add duplicate values', () => {
+ const headers = new Headers({ vary: 'Origin, Accept-Encoding' });
+ append_vary(headers, 'accept-encoding');
+ expect(headers.get('vary')).toBe('Origin, Accept-Encoding');
+ });
+});
diff --git a/packages/adapter-bun/test/apps/basic/.gitignore b/packages/adapter-bun/test/apps/basic/.gitignore
new file mode 100644
index 000000000000..00d16428cb9c
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/.gitignore
@@ -0,0 +1,5 @@
+.svelte-kit
+build
+node_modules
+test-results
+
diff --git a/packages/adapter-bun/test/apps/basic/package.json b/packages/adapter-bun/test/apps/basic/package.json
new file mode 100644
index 000000000000..d5c7aec8c807
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "test-bun-basic",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "MY_CUSTOM_PORT=4174 vite build",
+ "preview": "MY_CUSTOM_PORT=4174 bun ./build",
+ "prepare": "svelte-kit sync || echo ''",
+ "test:platform": "playwright test"
+ },
+ "devDependencies": {
+ "@playwright/test": "catalog:",
+ "@sveltejs/kit": "workspace:^",
+ "@sveltejs/vite-plugin-svelte": "catalog:",
+ "svelte": "catalog:",
+ "vite": "catalog:"
+ },
+ "type": "module"
+}
diff --git a/packages/adapter-bun/test/apps/basic/playwright.config.js b/packages/adapter-bun/test/apps/basic/playwright.config.js
new file mode 100644
index 000000000000..33d36b651014
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/playwright.config.js
@@ -0,0 +1 @@
+export { config as default } from '../../utils.js';
diff --git a/packages/adapter-bun/test/apps/basic/src/app.html b/packages/adapter-bun/test/apps/basic/src/app.html
new file mode 100644
index 000000000000..f273cc58f7eb
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/app.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/+page.svelte b/packages/adapter-bun/test/apps/basic/src/routes/+page.svelte
new file mode 100644
index 000000000000..f1ef75ba7a5d
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/+page.svelte
@@ -0,0 +1,6 @@
+
+
+Hello from Bun!
+
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js
new file mode 100644
index 000000000000..cc152e0033ba
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js
@@ -0,0 +1,7 @@
+export function GET() {
+ return new Response('data: hello\n\n', {
+ headers: {
+ 'content-type': 'text/event-stream'
+ }
+ });
+}
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js
new file mode 100644
index 000000000000..456ea012cec7
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js
@@ -0,0 +1,10 @@
+import { json } from '@sveltejs/kit';
+
+/** @type {import('./$types').RequestHandler} */
+export function GET({ getClientAddress, platform }) {
+ return json({
+ address: getClientAddress(),
+ request: platform?.request instanceof Request,
+ server: typeof platform?.server?.requestIP === 'function'
+ });
+}
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js
new file mode 100644
index 000000000000..ba58d86071e4
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js
@@ -0,0 +1,2 @@
+export const prerender = true;
+export const trailingSlash = 'always';
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte
new file mode 100644
index 000000000000..a994afef288b
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte
@@ -0,0 +1 @@
+Prerendered
diff --git a/packages/adapter-bun/test/apps/basic/static/data.json b/packages/adapter-bun/test/apps/basic/static/data.json
new file mode 100644
index 000000000000..e3adbd4e3a31
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/static/data.json
@@ -0,0 +1 @@
+{ "message": "hello from a static file" }
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
new file mode 100644
index 000000000000..45d7b412462f
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -0,0 +1,73 @@
+import { expect, test } from '@playwright/test';
+
+test('renders and hydrates the app', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('h1')).toHaveText('Hello from Bun!');
+ await expect(page.locator('button')).toHaveText('Toggle: false');
+ await page.locator('button').click();
+ await expect(page.locator('button')).toHaveText('Toggle: true');
+});
+
+test('provides Bun request context', async ({ request }) => {
+ const response = await request.get('/platform');
+ const body = await response.json();
+ expect(body.address).toBeTruthy();
+ expect(body.request).toBe(true);
+ expect(body.server).toBe(true);
+});
+
+test('serves static files with Bun file responses', async ({ request }) => {
+ const response = await request.get('/data.json', {
+ headers: { 'accept-encoding': 'identity' }
+ });
+ expect(response.status()).toBe(200);
+ expect(response.headers()['content-type']).toBe('application/json');
+ expect(response.headers()['accept-ranges']).toBe('bytes');
+ expect(response.headers()['vary']).toBe('Accept-Encoding');
+ expect(await response.json()).toEqual({ message: 'hello from a static file' });
+});
+
+test('supports ranges and conditional requests for static files', async ({ request }) => {
+ const initial = await request.get('/data.json', {
+ headers: { 'accept-encoding': 'identity' }
+ });
+ const etag = initial.headers()['etag'];
+ const last_modified = initial.headers()['last-modified'];
+ const body = await initial.text();
+ expect(etag).toBeTruthy();
+ expect(last_modified).toBeTruthy();
+
+ const not_modified = await request.get('/data.json', {
+ headers: { 'accept-encoding': 'identity', 'if-none-match': etag }
+ });
+ expect(not_modified.status()).toBe(304);
+ const not_modified_since = await request.get('/data.json', {
+ headers: { 'accept-encoding': 'identity', 'if-modified-since': last_modified }
+ });
+ expect(not_modified_since.status()).toBe(304);
+
+ const range = await request.get('/data.json', {
+ headers: { 'accept-encoding': 'identity', range: 'bytes=0-3' }
+ });
+ expect(range.status()).toBe(206);
+ expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`);
+ expect(await range.text()).toBe(body.slice(0, 4));
+});
+
+test('does not serve static files for non-GET requests', async ({ request }) => {
+ const response = await request.post('/data.json');
+ expect(response.status()).not.toBe(200);
+ expect(await response.text()).not.toContain('hello from a static file');
+});
+
+test('redirects prerendered paths to their canonical trailing slash', async ({ request }) => {
+ const response = await request.get('/prerendered?value=1', { maxRedirects: 0 });
+ expect(response.status()).toBe(308);
+ expect(response.headers()['location']).toBe('prerendered/?value=1');
+});
+
+test('configures long-lived event streams', async ({ request }) => {
+ const response = await request.get('/event-stream');
+ expect(response.headers()['content-type']).toContain('text/event-stream');
+ expect(response.headers()['x-accel-buffering']).toBe('no');
+});
diff --git a/packages/adapter-bun/test/apps/basic/tsconfig.json b/packages/adapter-bun/test/apps/basic/tsconfig.json
new file mode 100644
index 000000000000..030becfe8111
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "$app/tsconfig",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/packages/adapter-bun/test/apps/basic/vite.config.js b/packages/adapter-bun/test/apps/basic/vite.config.js
new file mode 100644
index 000000000000..fe9700dff051
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/vite.config.js
@@ -0,0 +1,17 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+import adapter from '../../../index.js';
+
+export default defineConfig({
+ build: {
+ minify: false
+ },
+ plugins: [
+ sveltekit({
+ adapter: adapter({
+ envPrefix: 'MY_CUSTOM_',
+ compile: process.env.COMPILE === 'true'
+ })
+ })
+ ]
+});
diff --git a/packages/adapter-bun/test/utils.js b/packages/adapter-bun/test/utils.js
new file mode 100644
index 000000000000..4294b9cf9352
--- /dev/null
+++ b/packages/adapter-bun/test/utils.js
@@ -0,0 +1,29 @@
+import { devices } from '@playwright/test';
+import process from 'node:process';
+import { number_from_env } from '../../../test-utils/index.js';
+
+/** @type {import('@playwright/test').PlaywrightTestConfig} */
+export const config = {
+ forbidOnly: !!process.env.CI,
+ timeout: process.env.CI ? 45000 : 15000,
+ webServer: {
+ command: 'pnpm build && pnpm preview',
+ port: 4174
+ },
+ retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0),
+ projects: [
+ {
+ name: 'chromium'
+ }
+ ],
+ use: {
+ ...devices['Desktop Chrome'],
+ screenshot: 'only-on-failure',
+ trace: 'retain-on-failure',
+ channel: 'chromium'
+ },
+ workers: process.env.CI ? 2 : number_from_env('KIT_E2E_WORKERS', undefined),
+ reporter: 'list',
+ testDir: 'test',
+ testMatch: /(.+\.)?(test|spec)\.[jt]s/
+};
diff --git a/packages/adapter-bun/tsconfig.json b/packages/adapter-bun/tsconfig.json
new file mode 100644
index 000000000000..b8d20398e770
--- /dev/null
+++ b/packages/adapter-bun/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "target": "es2022",
+ "module": "node16",
+ "moduleResolution": "node16",
+ "skipLibCheck": true,
+ "paths": {
+ "@sveltejs/kit": ["../kit/types/index"]
+ },
+ "types": ["bun-types", "node"]
+ },
+ "include": [
+ "index.js",
+ "rolldown.config.js",
+ "vitest.config.js",
+ "src/**/*.js",
+ "src/**/*.ts",
+ "test/utils.js",
+ "tests/**/*.js",
+ "tests/**/*.ts",
+ "./internal.d.ts"
+ ]
+}
diff --git a/packages/adapter-bun/vitest.config.js b/packages/adapter-bun/vitest.config.js
new file mode 100644
index 000000000000..a15b3a470a6d
--- /dev/null
+++ b/packages/adapter-bun/vitest.config.js
@@ -0,0 +1,5 @@
+// we need this file to prevent Vitest from resolving a Vitest config from another directory
+
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ef91f2258c1b..f92dae4a38a2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -144,6 +144,49 @@ importers:
specifier: 'catalog:'
version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))
+ packages/adapter-bun:
+ dependencies:
+ bun-types:
+ specifier: ^1.3.14
+ version: 1.3.14
+ rolldown:
+ specifier: ^1.2.0
+ version: 1.2.0
+ devDependencies:
+ '@playwright/test':
+ specifier: 'catalog:'
+ version: 1.61.1
+ '@sveltejs/kit':
+ specifier: workspace:^
+ version: link:../kit
+ '@types/node':
+ specifier: 'catalog:'
+ version: 22.19.19
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.3
+ vitest:
+ specifier: 'catalog:'
+ version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))
+
+ packages/adapter-bun/test/apps/basic:
+ devDependencies:
+ '@playwright/test':
+ specifier: 'catalog:'
+ version: 1.61.1
+ '@sveltejs/kit':
+ specifier: workspace:^
+ version: link:../../../../kit
+ '@sveltejs/vite-plugin-svelte':
+ specifier: 'catalog:'
+ version: 7.0.0(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))
+ svelte:
+ specifier: 'catalog:'
+ version: 5.56.3(@typescript-eslint/types@8.61.1)
+ vite:
+ specifier: 'catalog:'
+ version: 8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0)
+
packages/adapter-cloudflare:
dependencies:
'@cloudflare/workers-types':
@@ -3024,6 +3067,9 @@ packages:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
engines: {node: 20 || >=22}
+ bun-types@1.3.14:
+ resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==}
+
cac@7.0.0:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
@@ -5663,6 +5709,10 @@ snapshots:
dependencies:
balanced-match: 4.0.4
+ bun-types@1.3.14:
+ dependencies:
+ '@types/node': 22.19.19
+
cac@7.0.0: {}
chai@6.2.2: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ed9d61be95fc..1e8307dd00b6 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -16,6 +16,7 @@ engineStrict: true
packages:
- packages/*
+ - packages/adapter-bun/test/apps/*
- packages/adapter-cloudflare/test/apps/*
- packages/adapter-netlify/test/apps/*
- packages/adapter-node/test/apps/*
From ec55d7d335154984f0c3c4e73e6b9829df607caf Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Tue, 4 Aug 2026 13:48:28 +0300
Subject: [PATCH 02/94] feat(adapter-bun): update build process to use Bun's
JavaScript API and enhance type definitions
---
.github/workflows/platform-tests-bun.yml | 2 +-
.../25-build-and-deploy/45-adapter-bun.md | 10 +++-
packages/adapter-bun/index.d.ts | 9 +---
packages/adapter-bun/index.js | 50 ++++++++-----------
4 files changed, 33 insertions(+), 38 deletions(-)
diff --git a/.github/workflows/platform-tests-bun.yml b/.github/workflows/platform-tests-bun.yml
index 1ef4a4984184..be085a21a37a 100644
--- a/.github/workflows/platform-tests-bun.yml
+++ b/.github/workflows/platform-tests-bun.yml
@@ -51,4 +51,4 @@ jobs:
working-directory: packages/adapter-bun/test/apps/basic
env:
COMPILE: 'true'
- run: pnpm build
+ run: bun run --bun build
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 13f3789e8908..873e78d93ba5 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -88,7 +88,13 @@ Set `compile: true` to additionally generate `build/app`, a single executable co
adapter({ compile: true });
```
-Only the executable is required at runtime. It is specific to the platform on which it was built. Advanced options can select another Bun target and enable minification, bytecode, or source maps:
+The adapter uses the [`Bun.build`](https://bun.com/reference/bun/build) JavaScript API directly. Because Vite normally respects its Node.js shebang, run the build with Bun's `--bun` flag when compilation is enabled:
+
+```sh
+bun run --bun build
+```
+
+Only the executable is required at runtime. It is specific to the platform on which it was built. Advanced options can select another Bun target and enable minification, bytecode, source maps, or other [`compile` options](https://bun.com/reference/bun/CompileBuildOptions):
```js
adapter({
@@ -102,7 +108,7 @@ adapter({
});
```
-The `bun` executable must be available while the SvelteKit build runs. Native dependencies and cross-compilation have the same constraints as [`bun build --compile`](https://bun.com/docs/bundler/executables).
+Native dependencies and cross-compilation have the same constraints as [Bun's single-file executables](https://bun.com/docs/bundler/executables).
## Environment variables
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index e9f52df02c35..30eb05c51c0f 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -1,5 +1,5 @@
import type { Adapter } from '@sveltejs/kit';
-import type { Serve, TLSOptions } from 'bun';
+import type { CompileBuildOptions, Serve, TLSOptions } from 'bun';
import './ambient.js';
declare global {
@@ -16,17 +16,12 @@ type ServerOptions = Omit<
tls?: TLSOptions | TLSOptions[];
};
-interface CompileOptions {
+interface CompileOptions extends Omit {
/**
* The executable path, relative to the project root.
* @default `${out}/app`
*/
outfile?: string;
- /**
- * A Bun executable target such as `bun-linux-x64` or `bun-windows-x64-baseline`.
- * By default, Bun compiles for the current platform.
- */
- target?: string;
/** Include Bun bytecode to improve startup time. */
bytecode?: boolean;
/** Minify the server bundle before compiling it. */
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 86c1b07b3b5e..2cf71c217e7f 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,5 +1,4 @@
import { readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
-import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { rolldown } from 'rolldown';
@@ -225,15 +224,16 @@ function serialize(value) {
* @param {string[]} assets
* @returns {Promise}
*/
-function compile_executable(out, options, assets) {
+async function compile_executable(out, options, assets) {
+ if (typeof Bun === 'undefined') {
+ throw new Error(
+ 'Compiling an executable requires running the SvelteKit build with Bun. Use `bun run --bun build`.'
+ );
+ }
+
const outfile = options.outfile ?? `${out}/app`;
const entrypoint = `${out}/adapter-bun-compile.js`;
- const args = ['build', '--compile'];
-
- if (options.target) args.push(`--target=${options.target}`);
- if (options.bytecode) args.push('--bytecode');
- if (options.minify) args.push('--minify');
- if (options.sourcemap) args.push('--sourcemap=linked');
+ const { bytecode, minify, sourcemap, outfile: _, ...compile } = options;
const unique_assets = [...new Set(assets)];
const imports = unique_assets.map(
@@ -255,27 +255,21 @@ function compile_executable(out, options, assets) {
].join('\n')
);
- args.push(entrypoint, `--outfile=${outfile}`);
-
- return new Promise((resolve, reject) => {
- const child = spawn('bun', args, { stdio: 'inherit' });
- child.on('error', (error) => {
- rmSync(entrypoint, { force: true });
- reject(new Error('Could not run Bun to compile the server executable', { cause: error }));
- });
- child.on('exit', (code, signal) => {
- rmSync(entrypoint, { force: true });
- if (code === 0) {
- resolve();
- } else {
- reject(
- new Error(
- `Bun executable compilation failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}`
- )
- );
- }
+ try {
+ const result = await Bun.build({
+ entrypoints: [entrypoint],
+ compile: { ...compile, outfile },
+ bytecode,
+ minify,
+ sourcemap: sourcemap ? 'linked' : undefined
});
- });
+
+ if (!result.success) {
+ throw new AggregateError(result.logs, 'Bun executable compilation failed');
+ }
+ } finally {
+ rmSync(entrypoint, { force: true });
+ }
}
/** @param {string} str */
From a443fc019a52df2d278269400f4eee3fa20428a1 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Wed, 5 Aug 2026 22:57:00 +0300
Subject: [PATCH 03/94] feat(adapter-bun): enhance compile options and update
executable handling in adapter
---
.../25-build-and-deploy/45-adapter-bun.md | 10 +++--
packages/adapter-bun/index.d.ts | 19 +++-------
packages/adapter-bun/index.js | 37 +++++++++----------
3 files changed, 29 insertions(+), 37 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 873e78d93ba5..8c95459f65de 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -94,16 +94,18 @@ The adapter uses the [`Bun.build`](https://bun.com/reference/bun/build) JavaScri
bun run --bun build
```
-Only the executable is required at runtime. It is specific to the platform on which it was built. Advanced options can select another Bun target and enable minification, bytecode, source maps, or other [`compile` options](https://bun.com/reference/bun/CompileBuildOptions):
+With the default options, only the executable is required at runtime. It is specific to the platform on which it was built. For advanced configuration, pass [`Bun.BuildConfig`](https://bun.com/reference/bun/BuildConfig) options directly. The adapter supplies the generated `entrypoints`, so that property is not configurable. Options such as code splitting may emit additional runtime files:
```js
adapter({
compile: {
- outfile: 'build/my-app',
- target: 'bun-linux-x64',
+ compile: {
+ outfile: 'build/my-app',
+ target: 'bun-linux-x64'
+ },
minify: true,
bytecode: true,
- sourcemap: true
+ sourcemap: 'linked'
}
});
```
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 30eb05c51c0f..4966b8a46393 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -1,5 +1,5 @@
import type { Adapter } from '@sveltejs/kit';
-import type { CompileBuildOptions, Serve, TLSOptions } from 'bun';
+import type { BuildConfig, Serve, TLSOptions } from 'bun';
import './ambient.js';
declare global {
@@ -16,19 +16,9 @@ type ServerOptions = Omit<
tls?: TLSOptions | TLSOptions[];
};
-interface CompileOptions extends Omit {
- /**
- * The executable path, relative to the project root.
- * @default `${out}/app`
- */
- outfile?: string;
- /** Include Bun bytecode to improve startup time. */
- bytecode?: boolean;
- /** Minify the server bundle before compiling it. */
- minify?: boolean;
- /** Generate a source map alongside the executable. */
- sourcemap?: boolean;
-}
+type CompileOptions = Omit & {
+ compile: NonNullable;
+};
interface AdapterOptions {
/**
@@ -53,6 +43,7 @@ interface AdapterOptions {
serverOptions?: ServerOptions;
/**
* Compile the build into a single executable containing the server and static assets.
+ * Pass Bun build options directly for advanced configuration. The generated entrypoint is reserved.
* @default false
*/
compile?: boolean | CompileOptions;
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 2cf71c217e7f..741898c8fe6b 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -162,18 +162,22 @@ export default function (opts = {}) {
if (compile) {
builder.log.minor('Compiling executable');
- await compile_executable(out, compile === true ? {} : compile, [
- ...client_files.map((file) => `client/${file}`),
- ...client_compressed.flatMap((file) => [
- `client/${posixify(file)}.br`,
- `client/${posixify(file)}.gz`
- ]),
- ...prerendered_files.map((file) => `prerendered/${file}`),
- ...prerendered_compressed.flatMap((file) => [
- `prerendered/${posixify(file)}.br`,
- `prerendered/${posixify(file)}.gz`
- ])
- ]);
+ await compile_executable(
+ out,
+ compile === true ? { compile: { outfile: `${out}/app` } } : compile,
+ [
+ ...client_files.map((file) => `client/${file}`),
+ ...client_compressed.flatMap((file) => [
+ `client/${posixify(file)}.br`,
+ `client/${posixify(file)}.gz`
+ ]),
+ ...prerendered_files.map((file) => `prerendered/${file}`),
+ ...prerendered_compressed.flatMap((file) => [
+ `prerendered/${posixify(file)}.br`,
+ `prerendered/${posixify(file)}.gz`
+ ])
+ ]
+ );
}
},
@@ -231,9 +235,7 @@ async function compile_executable(out, options, assets) {
);
}
- const outfile = options.outfile ?? `${out}/app`;
const entrypoint = `${out}/adapter-bun-compile.js`;
- const { bytecode, minify, sourcemap, outfile: _, ...compile } = options;
const unique_assets = [...new Set(assets)];
const imports = unique_assets.map(
@@ -257,11 +259,8 @@ async function compile_executable(out, options, assets) {
try {
const result = await Bun.build({
- entrypoints: [entrypoint],
- compile: { ...compile, outfile },
- bytecode,
- minify,
- sourcemap: sourcemap ? 'linked' : undefined
+ ...options,
+ entrypoints: [entrypoint]
});
if (!result.success) {
From 7cde708b73c739f0ef79355e92c2db6855e9f15f Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Wed, 5 Aug 2026 23:31:46 +0300
Subject: [PATCH 04/94] feat(adapter-bun): update build commands to require Bun
and enhance error handling
---
.../25-build-and-deploy/45-adapter-bun.md | 6 +-
packages/adapter-bun/index.js | 114 ++++++++----------
packages/adapter-bun/package.json | 4 +-
packages/adapter-bun/test/utils.js | 2 +-
pnpm-lock.yaml | 6 +-
5 files changed, 57 insertions(+), 75 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 8c95459f65de..7b4f6eb3a144 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -24,7 +24,8 @@ export default defineConfig({
});
```
-Build your app with `bun run build`, then start it with:
+The adapter uses Bun's bundler and must run inside Bun. Build your app with `bun run --bun build`,
+then start it with:
```sh
bun ./build
@@ -88,7 +89,8 @@ Set `compile: true` to additionally generate `build/app`, a single executable co
adapter({ compile: true });
```
-The adapter uses the [`Bun.build`](https://bun.com/reference/bun/build) JavaScript API directly. Because Vite normally respects its Node.js shebang, run the build with Bun's `--bun` flag when compilation is enabled:
+The adapter uses the [`Bun.build`](https://bun.com/reference/bun/build) JavaScript API directly. The
+`--bun` flag is required because Vite normally respects its Node.js shebang:
```sh
bun run --bun build
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 741898c8fe6b..aa26078d2683 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,14 +1,8 @@
import { readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
-import { rolldown } from 'rolldown';
const files = fileURLToPath(new URL('./files', import.meta.url).href);
-/** @param {string} str */
-function escape_regex(str) {
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
/** @type {import('./index.js').default} */
export default function (opts = {}) {
const {
@@ -22,6 +16,12 @@ export default function (opts = {}) {
return {
name: '@sveltejs/adapter-bun',
async adapt(builder) {
+ if (typeof Bun === 'undefined') {
+ throw new Error(
+ 'adapter-bun requires running the SvelteKit build with Bun. Use `bun run --bun build`.'
+ );
+ }
+
const tmp = builder.getBuildDirectory('adapter-bun');
const base = builder.config.kit.paths.base;
@@ -73,82 +73,68 @@ export default function (opts = {}) {
);
writeFileSync(server_options_file, `export default ${serialize(serverOptions)};\n`);
- /** @type {Record} */
- const input = {
- index: `${entries}/index.js`,
- handler: `${entries}/handler.js`
- };
+ const entrypoints = [`${entries}/index.js`, `${entries}/handler.js`, dir_id];
if (builder.hasServerInstrumentationFile()) {
- input['instrumentation.server'] = `${server}/instrumentation.server.js`;
+ entrypoints.push(`${server}/instrumentation.server.js`);
}
- const bundle = await rolldown({
- input,
+ const result = await Bun.build({
+ entrypoints,
+ outdir: out,
+ target: 'bun',
+ format: 'esm',
+ conditions: ['bun', 'node'],
+ sourcemap: 'linked',
+ splitting: true,
+ naming: {
+ entry: '[name].[ext]',
+ chunk: 'server/chunks/[name]-[hash].[ext]'
+ },
external: [
'bun',
- /^bun:/,
- // dependencies could have deep exports, so we need a regex
- ...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\\/.*)?$`))
+ 'bun:*',
+ ...Object.keys(pkg.dependencies || {}).flatMap((dependency) => [
+ dependency,
+ `${dependency}/*`
+ ])
],
- platform: 'node',
- resolve: {
- conditionNames: ['bun', 'node']
- },
- experimental: {
- nativeMagicString: true
- },
plugins: [
{
- name: 'adapter-bun-resolve-app',
- resolveId(id) {
- if (id === 'SERVER') return `${server}/index.js`;
- if (id === 'MANIFEST') return `${server}/manifest.js`;
- if (id === 'SERVER_OPTIONS') return server_options_file;
- }
- },
- {
- name: 'adapter-bun-replace-constants',
- transform: {
- filter: { id: new RegExp(escape_regex(entries)) },
- handler(_code, _id, { magicString }) {
- if (!magicString) {
- throw new Error('experimental.nativeMagicString is not enabled');
+ name: 'adapter-bun',
+ setup(build) {
+ build.onResolve({ filter: /^(SERVER|MANIFEST|SERVER_OPTIONS)$/ }, ({ path }) => {
+ if (path === 'SERVER') return { path: `${server}/index.js` };
+ if (path === 'MANIFEST') return { path: `${server}/manifest.js` };
+ return { path: server_options_file };
+ });
+
+ build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, ({ path }) => {
+ let contents = readFileSync(path, 'utf8');
+ if (posixify(path) === dir_id) {
+ // Bun places shared modules two levels below the output directory
+ contents = contents.replace(
+ 'dirname(fileURLToPath(import.meta.url))',
+ "fileURLToPath(new URL('../../', import.meta.url))"
+ );
}
- magicString
+ contents = contents
.replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
.replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
.replace(
/\bORIGIN\b/g,
JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
);
- return {
- code: magicString,
- map: magicString.generateMap().toString()
- };
- }
+ return { contents, loader: 'js' };
+ });
}
}
]
});
- await bundle.write({
- dir: out,
- format: 'esm',
- sourcemap: true,
- codeSplitting: {
- groups: [
- {
- name: 'dir',
- test: dir_id
- }
- ]
- },
- chunkFileNames(chunk) {
- if (chunk.name === 'dir') return '[name].js';
- return 'server/chunks/[name]-[hash].js';
- }
- });
+ if (!result.success) {
+ throw new AggregateError(result.logs, 'Bun server build failed');
+ }
if (builder.hasServerInstrumentationFile()) {
builder.instrument({
@@ -229,12 +215,6 @@ function serialize(value) {
* @returns {Promise}
*/
async function compile_executable(out, options, assets) {
- if (typeof Bun === 'undefined') {
- throw new Error(
- 'Compiling an executable requires running the SvelteKit build with Bun. Use `bun run --bun build`.'
- );
- }
-
const entrypoint = `${out}/adapter-bun-compile.js`;
const unique_assets = [...new Set(assets)];
diff --git a/packages/adapter-bun/package.json b/packages/adapter-bun/package.json
index a837be148a36..66c26f8d7aa0 100644
--- a/packages/adapter-bun/package.json
+++ b/packages/adapter-bun/package.json
@@ -45,12 +45,12 @@
"@playwright/test": "catalog:",
"@sveltejs/kit": "workspace:^",
"@types/node": "catalog:",
+ "rolldown": "^1.2.0",
"typescript": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
- "bun-types": "^1.3.14",
- "rolldown": "^1.2.0"
+ "bun-types": "^1.3.14"
},
"peerDependencies": {
"@sveltejs/kit": "^3.0.0-next.0"
diff --git a/packages/adapter-bun/test/utils.js b/packages/adapter-bun/test/utils.js
index 4294b9cf9352..80ccd39dfa21 100644
--- a/packages/adapter-bun/test/utils.js
+++ b/packages/adapter-bun/test/utils.js
@@ -7,7 +7,7 @@ export const config = {
forbidOnly: !!process.env.CI,
timeout: process.env.CI ? 45000 : 15000,
webServer: {
- command: 'pnpm build && pnpm preview',
+ command: 'bun run --bun build && bun run preview',
port: 4174
},
retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f92dae4a38a2..2a148600e8f6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -149,9 +149,6 @@ importers:
bun-types:
specifier: ^1.3.14
version: 1.3.14
- rolldown:
- specifier: ^1.2.0
- version: 1.2.0
devDependencies:
'@playwright/test':
specifier: 'catalog:'
@@ -162,6 +159,9 @@ importers:
'@types/node':
specifier: 'catalog:'
version: 22.19.19
+ rolldown:
+ specifier: ^1.2.0
+ version: 1.2.0
typescript:
specifier: 'catalog:'
version: 6.0.3
From ed7ac5a53219b865802be6d795d2e75ed5ce9b85 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Wed, 5 Aug 2026 23:39:40 +0300
Subject: [PATCH 05/94] feat(adapter-bun): migrate build process to Bun and
remove Rolldown configuration
---
packages/adapter-bun/build.js | 21 ++++++++
packages/adapter-bun/index.js | 2 +-
packages/adapter-bun/package.json | 5 +-
packages/adapter-bun/rolldown.config.js | 64 -------------------------
packages/adapter-bun/tsconfig.json | 2 +-
pnpm-lock.yaml | 3 --
6 files changed, 25 insertions(+), 72 deletions(-)
create mode 100644 packages/adapter-bun/build.js
delete mode 100644 packages/adapter-bun/rolldown.config.js
diff --git a/packages/adapter-bun/build.js b/packages/adapter-bun/build.js
new file mode 100644
index 000000000000..3213ca5a8f40
--- /dev/null
+++ b/packages/adapter-bun/build.js
@@ -0,0 +1,21 @@
+import { rmSync } from 'node:fs';
+
+rmSync('files', { recursive: true, force: true });
+
+const result = await Bun.build({
+ entrypoints: ['src/index.js', 'src/handler.js', 'src/dir.js'],
+ outdir: 'files',
+ target: 'bun',
+ format: 'esm',
+ splitting: true,
+ naming: {
+ entry: '[name].[ext]',
+ chunk: 'chunks/[name]-[hash].[ext]'
+ },
+ // resolved at adapt time
+ external: ['MANIFEST', 'SERVER', 'SERVER_OPTIONS']
+});
+
+if (!result.success) {
+ throw new AggregateError(result.logs, 'Could not build adapter-bun');
+}
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index aa26078d2683..7db356555486 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -111,7 +111,7 @@ export default function (opts = {}) {
build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, ({ path }) => {
let contents = readFileSync(path, 'utf8');
- if (posixify(path) === dir_id) {
+ if (contents.includes('dirname(fileURLToPath(import.meta.url))')) {
// Bun places shared modules two levels below the output directory
contents = contents.replace(
'dirname(fileURLToPath(import.meta.url))',
diff --git a/packages/adapter-bun/package.json b/packages/adapter-bun/package.json
index 66c26f8d7aa0..5abaeb389c70 100644
--- a/packages/adapter-bun/package.json
+++ b/packages/adapter-bun/package.json
@@ -33,8 +33,8 @@
"ambient.d.ts"
],
"scripts": {
- "dev": "rolldown -cw",
- "build": "rolldown -c",
+ "dev": "bun --watch build.js",
+ "build": "bun build.js",
"test": "vitest run",
"check": "tsc",
"lint": "prettier --check .",
@@ -45,7 +45,6 @@
"@playwright/test": "catalog:",
"@sveltejs/kit": "workspace:^",
"@types/node": "catalog:",
- "rolldown": "^1.2.0",
"typescript": "catalog:",
"vitest": "catalog:"
},
diff --git a/packages/adapter-bun/rolldown.config.js b/packages/adapter-bun/rolldown.config.js
deleted file mode 100644
index c8f06672853f..000000000000
--- a/packages/adapter-bun/rolldown.config.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/** @import { Plugin, RolldownOptions } from 'rolldown' */
-import { builtinModules } from 'node:module';
-import { rmSync } from 'node:fs';
-import { join } from 'node:path';
-
-/**
- * @param {string} filepath
- * @returns {Plugin}
- */
-function clearOutput(filepath) {
- return {
- name: 'clear-output',
- buildStart: {
- order: 'pre',
- sequential: true,
- handler() {
- rmSync(filepath, { recursive: true, force: true });
- }
- }
- };
-}
-
-/** @returns {Plugin} */
-function prefixBuiltinModules() {
- return {
- name: 'prefix-built-in-modules',
- resolveId(source) {
- if (builtinModules.includes(source)) {
- return { id: 'node:' + source, external: true };
- }
- }
- };
-}
-
-const dir_id = join(import.meta.dirname, 'src', 'dir.js');
-
-/** @type {RolldownOptions} */
-export default {
- input: {
- index: 'src/index.js',
- handler: 'src/handler.js'
- },
- output: {
- dir: 'files',
- format: 'esm',
- hoistTransitiveImports: false,
- chunkFileNames(chunk) {
- if (chunk.name === 'dir') return '[name].js';
- return 'chunks/[name].js';
- },
- codeSplitting: {
- groups: [
- {
- name: 'dir',
- test: dir_id
- }
- ]
- }
- },
- plugins: [clearOutput('files'), prefixBuiltinModules()],
- // resolved at adapt time
- external: ['MANIFEST', 'SERVER', 'SERVER_OPTIONS'],
- platform: 'node'
-};
diff --git a/packages/adapter-bun/tsconfig.json b/packages/adapter-bun/tsconfig.json
index b8d20398e770..946e9388ada8 100644
--- a/packages/adapter-bun/tsconfig.json
+++ b/packages/adapter-bun/tsconfig.json
@@ -13,8 +13,8 @@
"types": ["bun-types", "node"]
},
"include": [
+ "build.js",
"index.js",
- "rolldown.config.js",
"vitest.config.js",
"src/**/*.js",
"src/**/*.ts",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2a148600e8f6..bc61fa675c90 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -159,9 +159,6 @@ importers:
'@types/node':
specifier: 'catalog:'
version: 22.19.19
- rolldown:
- specifier: ^1.2.0
- version: 1.2.0
typescript:
specifier: 'catalog:'
version: 6.0.3
From 49f91562ef30046f64a96ca9c345b7027e0a9a1d Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Wed, 5 Aug 2026 23:49:39 +0300
Subject: [PATCH 06/94] feat(adapter-bun): refactor manifest and server options
handling in Bun build process
---
packages/adapter-bun/index.js | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 7db356555486..ce9b44c0e4d7 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -59,20 +59,9 @@ export default function (opts = {}) {
builder.copy(files, entries);
const dir_id = `${entries}/dir.js`;
+ const manifest_file = `${server}/adapter-bun-manifest.js`;
const server_options_file = `${server}/adapter-bun-options.js`;
- writeFileSync(
- `${server}/manifest.js`,
- [
- `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
- `export const client_files = new Set(${JSON.stringify(client_files)});`,
- `export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
- `export const compressed_files = new Set(${JSON.stringify(compressed_files)});`,
- `export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
- ].join('\n\n')
- );
- writeFileSync(server_options_file, `export default ${serialize(serverOptions)};\n`);
-
const entrypoints = [`${entries}/index.js`, `${entries}/handler.js`, dir_id];
if (builder.hasServerInstrumentationFile()) {
@@ -81,6 +70,16 @@ export default function (opts = {}) {
const result = await Bun.build({
entrypoints,
+ files: {
+ [manifest_file]: [
+ `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
+ `export const client_files = new Set(${JSON.stringify(client_files)});`,
+ `export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
+ `export const compressed_files = new Set(${JSON.stringify(compressed_files)});`,
+ `export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
+ ].join('\n\n'),
+ [server_options_file]: `export default ${serialize(serverOptions)};\n`
+ },
outdir: out,
target: 'bun',
format: 'esm',
@@ -105,8 +104,8 @@ export default function (opts = {}) {
setup(build) {
build.onResolve({ filter: /^(SERVER|MANIFEST|SERVER_OPTIONS)$/ }, ({ path }) => {
if (path === 'SERVER') return { path: `${server}/index.js` };
- if (path === 'MANIFEST') return { path: `${server}/manifest.js` };
- return { path: server_options_file };
+ if (path === 'MANIFEST') return { path: manifest_file };
+ if (path === 'SERVER_OPTIONS') return { path: server_options_file };
});
build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, ({ path }) => {
From eaa9f303d20ff41b80a349e7ab8839c0e08251cb Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Wed, 5 Aug 2026 23:51:44 +0300
Subject: [PATCH 07/94] feat(adapter-bun): switch to async file reading and
enhance onLoad handling in Bun build process
---
packages/adapter-bun/index.js | 42 +++++++++++++++++++----------------
1 file changed, 23 insertions(+), 19 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index ce9b44c0e4d7..934649985b40 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,4 +1,5 @@
-import { readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
+import { rmSync, statSync, writeFileSync } from 'node:fs';
+import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const files = fileURLToPath(new URL('./files', import.meta.url).href);
@@ -53,7 +54,7 @@ export default function (opts = {}) {
builder.log.minor('Building server');
- const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
+ const pkg = JSON.parse(await readFile('package.json', 'utf8'));
const server = builder.getServerDirectory();
const entries = posixify(`${tmp}/entries`);
builder.copy(files, entries);
@@ -108,24 +109,27 @@ export default function (opts = {}) {
if (path === 'SERVER_OPTIONS') return { path: server_options_file };
});
- build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, ({ path }) => {
- let contents = readFileSync(path, 'utf8');
- if (contents.includes('dirname(fileURLToPath(import.meta.url))')) {
- // Bun places shared modules two levels below the output directory
- contents = contents.replace(
- 'dirname(fileURLToPath(import.meta.url))',
- "fileURLToPath(new URL('../../', import.meta.url))"
- );
+ build.onLoad(
+ { filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ },
+ async ({ path }) => {
+ let contents = await readFile(path, 'utf8');
+ if (contents.includes('dirname(fileURLToPath(import.meta.url))')) {
+ // Bun places shared modules two levels below the output directory
+ contents = contents.replace(
+ 'dirname(fileURLToPath(import.meta.url))',
+ "fileURLToPath(new URL('../../', import.meta.url))"
+ );
+ }
+ contents = contents
+ .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
+ .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
+ .replace(
+ /\bORIGIN\b/g,
+ JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
+ );
+ return { contents, loader: 'js' };
}
- contents = contents
- .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
- .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
- .replace(
- /\bORIGIN\b/g,
- JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
- );
- return { contents, loader: 'js' };
- });
+ );
}
}
]
From 60c065195d0c5c307fdd546cd789a373d54a0d36 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 00:04:48 +0300
Subject: [PATCH 08/94] feat(adapter-bun): enhance compile options and update
build process for executable generation
---
.../25-build-and-deploy/45-adapter-bun.md | 4 +-
packages/adapter-bun/index.js | 207 +++++++++---------
2 files changed, 106 insertions(+), 105 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 7b4f6eb3a144..2da11743911a 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -83,7 +83,9 @@ Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings
### compile
-Set `compile: true` to additionally generate `build/app`, a single executable containing the Bun runtime, your server code, client assets, and prerendered pages:
+Set `compile: true` to generate `build/app`, a single executable containing the Bun runtime, your
+server code, client assets, and prerendered pages. In this mode, the adapter builds the executable
+directly instead of generating the JavaScript server files:
```js
adapter({ compile: true });
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 934649985b40..fb8a3ac54b65 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,5 +1,6 @@
-import { rmSync, statSync, writeFileSync } from 'node:fs';
+import { statSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const files = fileURLToPath(new URL('./files', import.meta.url).href);
@@ -52,7 +53,7 @@ export default function (opts = {}) {
}
const compressed_files = [...client_compressed, ...prerendered_compressed];
- builder.log.minor('Building server');
+ builder.log.minor(compile ? 'Compiling executable' : 'Building server');
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
const server = builder.getServerDirectory();
@@ -62,29 +63,25 @@ export default function (opts = {}) {
const dir_id = `${entries}/dir.js`;
const manifest_file = `${server}/adapter-bun-manifest.js`;
const server_options_file = `${server}/adapter-bun-options.js`;
+ const instrumentation = builder.hasServerInstrumentationFile()
+ ? `${server}/instrumentation.server.js`
+ : undefined;
+ const virtual_files = {
+ [manifest_file]: [
+ `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
+ `export const client_files = new Set(${JSON.stringify(client_files)});`,
+ `export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
+ `export const compressed_files = new Set(${JSON.stringify(compressed_files)});`,
+ `export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
+ ].join('\n\n'),
+ [server_options_file]: `export default ${serialize(serverOptions)};\n`
+ };
+ const compile_options = compile === true ? { compile: { outfile: `${out}/app` } } : compile;
const entrypoints = [`${entries}/index.js`, `${entries}/handler.js`, dir_id];
-
- if (builder.hasServerInstrumentationFile()) {
- entrypoints.push(`${server}/instrumentation.server.js`);
- }
-
- const result = await Bun.build({
- entrypoints,
- files: {
- [manifest_file]: [
- `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
- `export const client_files = new Set(${JSON.stringify(client_files)});`,
- `export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
- `export const compressed_files = new Set(${JSON.stringify(compressed_files)});`,
- `export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
- ].join('\n\n'),
- [server_options_file]: `export default ${serialize(serverOptions)};\n`
- },
+ /** @type {Omit} */
+ let build_options = {
outdir: out,
- target: 'bun',
- format: 'esm',
- conditions: ['bun', 'node'],
sourcemap: 'linked',
splitting: true,
naming: {
@@ -98,48 +95,84 @@ export default function (opts = {}) {
dependency,
`${dependency}/*`
])
- ],
- plugins: [
- {
- name: 'adapter-bun',
- setup(build) {
- build.onResolve({ filter: /^(SERVER|MANIFEST|SERVER_OPTIONS)$/ }, ({ path }) => {
- if (path === 'SERVER') return { path: `${server}/index.js` };
- if (path === 'MANIFEST') return { path: manifest_file };
- if (path === 'SERVER_OPTIONS') return { path: server_options_file };
- });
+ ]
+ };
- build.onLoad(
- { filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ },
- async ({ path }) => {
- let contents = await readFile(path, 'utf8');
- if (contents.includes('dirname(fileURLToPath(import.meta.url))')) {
- // Bun places shared modules two levels below the output directory
- contents = contents.replace(
- 'dirname(fileURLToPath(import.meta.url))',
- "fileURLToPath(new URL('../../', import.meta.url))"
- );
- }
- contents = contents
- .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
- .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
- .replace(
- /\bORIGIN\b/g,
- JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
- );
- return { contents, loader: 'js' };
- }
+ if (compile_options) {
+ const assets = [
+ ...client_files.map((file) => `client/${file}`),
+ ...client_compressed.flatMap((file) => [
+ `client/${posixify(file)}.br`,
+ `client/${posixify(file)}.gz`
+ ]),
+ ...prerendered_files.map((file) => `prerendered/${file}`),
+ ...prerendered_compressed.flatMap((file) => [
+ `prerendered/${posixify(file)}.br`,
+ `prerendered/${posixify(file)}.gz`
+ ])
+ ];
+ const compile_file = `${out}/adapter-bun-compile.js`;
+ virtual_files[compile_file] = create_compile_entrypoint(
+ out,
+ assets,
+ `${entries}/index.js`,
+ instrumentation
+ );
+ entrypoints.splice(0, entrypoints.length, compile_file);
+ build_options = compile_options;
+ } else if (instrumentation) {
+ entrypoints.push(instrumentation);
+ }
+
+ const adapter_plugin = {
+ name: 'adapter-bun',
+ /** @param {import('bun').PluginBuilder} build */
+ setup(build) {
+ build.onResolve({ filter: /^(SERVER|MANIFEST|SERVER_OPTIONS)$/ }, ({ path }) => {
+ if (path === 'SERVER') return { path: `${server}/index.js` };
+ if (path === 'MANIFEST') return { path: manifest_file };
+ if (path === 'SERVER_OPTIONS') return { path: server_options_file };
+ });
+
+ build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, async ({ path }) => {
+ let contents = await readFile(path, 'utf8');
+ if (contents.includes('dirname(fileURLToPath(import.meta.url))')) {
+ // Bun places shared modules two levels below the output directory
+ contents = contents.replace(
+ 'dirname(fileURLToPath(import.meta.url))',
+ "fileURLToPath(new URL('../../', import.meta.url))"
);
}
- }
- ]
+ contents = contents
+ .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
+ .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
+ .replace(
+ /\bORIGIN\b/g,
+ JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
+ );
+ return { contents, loader: 'js' };
+ });
+ }
+ };
+
+ const result = await Bun.build({
+ target: 'bun',
+ format: 'esm',
+ conditions: ['bun', 'node'],
+ ...build_options,
+ entrypoints,
+ files: {
+ ...build_options.files,
+ ...virtual_files
+ },
+ plugins: [...(build_options.plugins ?? []), adapter_plugin]
});
if (!result.success) {
throw new AggregateError(result.logs, 'Bun server build failed');
}
- if (builder.hasServerInstrumentationFile()) {
+ if (instrumentation && !compile) {
builder.instrument({
entrypoint: `${out}/index.js`,
instrumentation: `${out}/instrumentation.server.js`,
@@ -148,26 +181,6 @@ export default function (opts = {}) {
}
});
}
-
- if (compile) {
- builder.log.minor('Compiling executable');
- await compile_executable(
- out,
- compile === true ? { compile: { outfile: `${out}/app` } } : compile,
- [
- ...client_files.map((file) => `client/${file}`),
- ...client_compressed.flatMap((file) => [
- `client/${posixify(file)}.br`,
- `client/${posixify(file)}.gz`
- ]),
- ...prerendered_files.map((file) => `prerendered/${file}`),
- ...prerendered_compressed.flatMap((file) => [
- `prerendered/${posixify(file)}.br`,
- `prerendered/${posixify(file)}.gz`
- ])
- ]
- );
- }
},
supports: {
@@ -213,45 +226,31 @@ function serialize(value) {
/**
* @param {string} out
- * @param {NonNullable>} options
* @param {string[]} assets
- * @returns {Promise}
+ * @param {string} entrypoint
+ * @param {string | undefined} instrumentation
+ * @returns {string}
*/
-async function compile_executable(out, options, assets) {
- const entrypoint = `${out}/adapter-bun-compile.js`;
-
+function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
const unique_assets = [...new Set(assets)];
const imports = unique_assets.map(
(file, index) =>
- `import asset_${index} from ${JSON.stringify(`./${file}`)} with { type: 'file' };`
+ `import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
);
const entries = unique_assets.map((file, index) => [
file,
`{ path: asset_${index}, lastModified: ${Math.trunc(statSync(`${out}/${file}`).mtimeMs)} }`
]);
- writeFileSync(
- entrypoint,
- [
- ...imports,
- `globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
- .map(([file, identifier]) => `[${JSON.stringify(file)}, ${identifier}]`)
- .join(',')}]);`,
- `await import('./index.js');`
- ].join('\n')
- );
-
- try {
- const result = await Bun.build({
- ...options,
- entrypoints: [entrypoint]
- });
-
- if (!result.success) {
- throw new AggregateError(result.logs, 'Bun executable compilation failed');
- }
- } finally {
- rmSync(entrypoint, { force: true });
- }
+ return [
+ ...imports,
+ `globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
+ .map(([file, identifier]) => `[${JSON.stringify(file)}, ${identifier}]`)
+ .join(',')}]);`,
+ instrumentation && `await import(${JSON.stringify(instrumentation)});`,
+ `await import(${JSON.stringify(entrypoint)});`
+ ]
+ .filter(Boolean)
+ .join('\n');
}
/** @param {string} str */
From 53401f3609d9ba1dc479b3d944f560316a6f7688 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 00:59:40 +0300
Subject: [PATCH 09/94] feat(adapter-bun): streamline asset handling and
improve static file serving with URL encoding support
---
.../25-build-and-deploy/45-adapter-bun.md | 9 +-
packages/adapter-bun/index.d.ts | 6 -
packages/adapter-bun/index.js | 39 +---
packages/adapter-bun/internal.d.ts | 1 -
packages/adapter-bun/src/assets.js | 15 ++
packages/adapter-bun/src/handler.js | 12 +-
packages/adapter-bun/src/index.js | 2 +
packages/adapter-bun/src/static.js | 185 ++++--------------
packages/adapter-bun/src/utils.js | 42 ----
packages/adapter-bun/src/utils.spec.ts | 18 +-
.../test/apps/basic/static/encoded name.txt | 1 +
.../adapter-bun/test/apps/basic/test/test.js | 43 ++--
12 files changed, 89 insertions(+), 284 deletions(-)
create mode 100644 packages/adapter-bun/src/assets.js
create mode 100644 packages/adapter-bun/test/apps/basic/static/encoded name.txt
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 2da11743911a..dc5120c688b6 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -33,7 +33,7 @@ bun ./build
The default output directory is `build`. Production dependencies are externalised in the same way as with [`adapter-node`](adapter-node): packages in `dependencies` must be installed alongside the build, while packages in `devDependencies` are bundled into it.
-Client assets and prerendered pages are served with Bun-native file responses. This includes streaming and range requests, conditional requests using `ETag` and `Last-Modified`, correct MIME types, immutable caching for hashed SvelteKit assets, and optional precompressed Brotli and gzip files.
+Client assets and prerendered pages are served through Bun's native `routes` and file responses. This includes streaming and range requests, conditional requests using `Last-Modified`, correct MIME types, and immutable caching for hashed SvelteKit assets.
## Options
@@ -51,7 +51,6 @@ export default defineConfig({
sveltekit({
adapter: adapter({
out: 'build',
- precompress: true,
envPrefix: '',
serverOptions: {
idleTimeout: 30
@@ -67,10 +66,6 @@ export default defineConfig({
The directory to build the server to. It defaults to `build`.
-### precompress
-
-Precompresses assets and prerendered pages with gzip and Brotli. It defaults to `true`. The server selects the best supported representation from the request's `Accept-Encoding` header.
-
### envPrefix
Adds a prefix to all environment variables read by the production server. For example, with `envPrefix: 'MY_'`, configure the server with `MY_HOST`, `MY_PORT`, and `MY_REUSE_PORT`.
@@ -216,4 +211,4 @@ const server = Bun.serve({
console.log(`Listening on ${server.url}`);
```
-When using a custom server, implement lifecycle behavior such as signal handling yourself. The handler still serves static and prerendered files and reads the proxy-header environment variables described above.
+When using a custom server, implement lifecycle behavior such as signal handling and static file serving yourself. The handler only serves dynamic SvelteKit requests and reads the proxy-header environment variables described above.
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 4966b8a46393..f2fd3f752f7a 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -4,7 +4,6 @@ import './ambient.js';
declare global {
const ENV_PREFIX: string;
- const PRECOMPRESS: boolean;
const ORIGIN: string | undefined;
}
@@ -26,11 +25,6 @@ interface AdapterOptions {
* @default 'build'
*/
out?: string;
- /**
- * Enables precompressing assets and prerendered pages with gzip and brotli.
- * @default true
- */
- precompress?: boolean;
/**
* A prefix for the environment variables used to configure the production server.
*/
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index fb8a3ac54b65..09b89971fe8d 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,4 +1,3 @@
-import { statSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -7,13 +6,7 @@ const files = fileURLToPath(new URL('./files', import.meta.url).href);
/** @type {import('./index.js').default} */
export default function (opts = {}) {
- const {
- out = 'build',
- precompress = true,
- envPrefix = '',
- serverOptions = {},
- compile = false
- } = opts;
+ const { out = 'build', envPrefix = '', serverOptions = {}, compile = false } = opts;
return {
name: '@sveltejs/adapter-bun',
@@ -40,19 +33,6 @@ export default function (opts = {}) {
base
);
- /** @type {string[]} */
- let client_compressed = [];
- /** @type {string[]} */
- let prerendered_compressed = [];
- if (precompress) {
- builder.log.minor('Compressing assets');
- [client_compressed, prerendered_compressed] = await Promise.all([
- builder.compress(`${out}/client`),
- builder.compress(`${out}/prerendered`)
- ]);
- }
- const compressed_files = [...client_compressed, ...prerendered_compressed];
-
builder.log.minor(compile ? 'Compiling executable' : 'Building server');
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
@@ -71,7 +51,6 @@ export default function (opts = {}) {
`export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
`export const client_files = new Set(${JSON.stringify(client_files)});`,
`export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
- `export const compressed_files = new Set(${JSON.stringify(compressed_files)});`,
`export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
].join('\n\n'),
[server_options_file]: `export default ${serialize(serverOptions)};\n`
@@ -101,15 +80,7 @@ export default function (opts = {}) {
if (compile_options) {
const assets = [
...client_files.map((file) => `client/${file}`),
- ...client_compressed.flatMap((file) => [
- `client/${posixify(file)}.br`,
- `client/${posixify(file)}.gz`
- ]),
- ...prerendered_files.map((file) => `prerendered/${file}`),
- ...prerendered_compressed.flatMap((file) => [
- `prerendered/${posixify(file)}.br`,
- `prerendered/${posixify(file)}.gz`
- ])
+ ...prerendered_files.map((file) => `prerendered/${file}`)
];
const compile_file = `${out}/adapter-bun-compile.js`;
virtual_files[compile_file] = create_compile_entrypoint(
@@ -145,7 +116,6 @@ export default function (opts = {}) {
}
contents = contents
.replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
- .replace(/\bPRECOMPRESS\b/g, JSON.stringify(precompress))
.replace(
/\bORIGIN\b/g,
JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
@@ -237,10 +207,7 @@ function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
(file, index) =>
`import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
);
- const entries = unique_assets.map((file, index) => [
- file,
- `{ path: asset_${index}, lastModified: ${Math.trunc(statSync(`${out}/${file}`).mtimeMs)} }`
- ]);
+ const entries = unique_assets.map((file, index) => [file, `asset_${index}`]);
return [
...imports,
`globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 239e6b614761..3745485373b7 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -2,7 +2,6 @@ declare module 'MANIFEST' {
import type { SSRManifest } from '@sveltejs/kit';
export const client_files: Set;
- export const compressed_files: Set;
export const manifest: SSRManifest;
export const prerendered_files: Set;
export const prerendered_paths: Set;
diff --git a/packages/adapter-bun/src/assets.js b/packages/adapter-bun/src/assets.js
new file mode 100644
index 000000000000..07eb05133c25
--- /dev/null
+++ b/packages/adapter-bun/src/assets.js
@@ -0,0 +1,15 @@
+import { join } from 'node:path';
+import { dir } from './dir.js';
+
+const embedded_files = /** @type {Map | undefined} */ (
+ /** @type {any} */ (globalThis)[Symbol.for('sveltekit.adapter-bun.assets')]
+);
+
+/**
+ * @param {'client' | 'prerendered'} directory
+ * @param {string} relative
+ * @returns {string}
+ */
+export function asset_path(directory, relative) {
+ return embedded_files?.get(`${directory}/${relative}`) ?? join(dir, directory, relative);
+}
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 1ae1728b1da1..db4a68c002b4 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -1,7 +1,7 @@
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';
+import { asset_path } from './assets.js';
import { env, env_prefix, number_env } from './env.js';
-import { asset_path, serve_static } from './static.js';
const server = new Server(manifest);
const origin = ORIGIN;
@@ -27,16 +27,6 @@ await server.init({
export async function handler(request, bun_server) {
const url = new URL(request.url);
- let pathname;
- try {
- pathname = decodeURIComponent(url.pathname);
- } catch {
- return new Response('Bad Request', { status: 400 });
- }
-
- const static_response = await serve_static(request, pathname);
- if (static_response) return static_response;
-
let request_origin = origin;
try {
request_origin ||= get_origin(request, url);
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index b9fd667ae3a1..f4b25137894f 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -2,6 +2,7 @@ import process from 'node:process';
import server_options from 'SERVER_OPTIONS';
import { handler } from './handler.js';
import { boolean_env, env, number_env } from './env.js';
+import { routes } from './static.js';
import { parse_as_bytes } from './utils.js';
const options = { ...server_options };
@@ -75,6 +76,7 @@ if (unix && options.http3) {
}
options.fetch = handler;
+options.routes = routes;
export const server = Bun.serve(
/** @type {import('bun').Serve.Options} */ (/** @type {unknown} */ (options))
diff --git a/packages/adapter-bun/src/static.js b/packages/adapter-bun/src/static.js
index eee9116a6fe2..a66dc1124932 100644
--- a/packages/adapter-bun/src/static.js
+++ b/packages/adapter-bun/src/static.js
@@ -1,40 +1,5 @@
-import { extname, join } from 'node:path';
-import {
- client_files,
- compressed_files,
- manifest,
- prerendered_files,
- prerendered_paths
-} from 'MANIFEST';
-import { dir } from './dir.js';
-import { accepts_encoding, append_vary } from './utils.js';
-
-const embedded_files =
- /** @type {Map | undefined} */ (
- /** @type {any} */ (globalThis)[Symbol.for('sveltekit.adapter-bun.assets')]
- );
-
-/**
- * @param {'client' | 'prerendered'} directory
- * @param {string} relative
- * @returns {{ path: string; lastModified?: number }}
- */
-function asset(directory, relative) {
- return (
- embedded_files?.get(`${directory}/${relative}`) ?? {
- path: join(dir, directory, relative)
- }
- );
-}
-
-/**
- * @param {'client' | 'prerendered'} directory
- * @param {string} relative
- * @returns {string}
- */
-export function asset_path(directory, relative) {
- return asset(directory, relative).path;
-}
+import { client_files, manifest, prerendered_files, prerendered_paths } from 'MANIFEST';
+import { asset_path } from './assets.js';
/**
* @param {string} pathname
@@ -62,133 +27,59 @@ function relative_pathname(from, to) {
}
/**
- * @param {Request} request
* @param {string} pathname
- * @returns {Promise}
+ * @returns {string}
*/
-export async function serve_static(request, pathname) {
- if (request.method !== 'GET' && request.method !== 'HEAD') return;
-
- const client_file = pathname.slice(1);
- if (client_files.has(client_file)) {
- return serve_file(request, client_file, true);
- }
-
- if (prerendered_paths.has(pathname)) {
- const file = find_prerendered_file(pathname);
- if (file) return serve_file(request, file, false);
- }
-
- const inverted = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
- if (prerendered_paths.has(inverted)) {
- const url = new URL(request.url);
- return new Response(null, {
- status: 308,
- headers: { location: relative_pathname(pathname, inverted) + url.search }
- });
- }
+function encode_pathname(pathname) {
+ return pathname.split('/').map(encodeURIComponent).join('/');
}
/**
- * @param {Request} request
+ * @param {'client' | 'prerendered'} directory
* @param {string} relative
* @param {boolean} client
- * @returns {Promise}
+ * @returns {Partial>}
*/
-async function serve_file(request, relative, client) {
- const directory = client ? 'client' : 'prerendered';
- const original = asset(directory, relative);
- const can_precompress =
- PRECOMPRESS && compressed_files.has(relative) && !request.headers.has('range');
-
- /** @type {'br' | 'gzip' | undefined} */
- let encoding;
- if (can_precompress && accepts_encoding(request.headers.get('accept-encoding'), 'br')) {
- encoding = 'br';
- } else if (can_precompress && accepts_encoding(request.headers.get('accept-encoding'), 'gzip')) {
- encoding = 'gzip';
- }
-
- const selected =
- encoding === 'br'
- ? asset(directory, `${relative}.br`)
- : encoding === 'gzip'
- ? asset(directory, `${relative}.gz`)
- : original;
- const file = Bun.file(selected.path);
- if (!(await file.exists())) return;
-
- const size = file.size;
- const last_modified = Math.trunc((selected.lastModified ?? file.lastModified) / 1000) * 1000;
- const etag = `W/"${last_modified.toString(16)}-${size.toString(16)}${encoding ? `-${encoding}` : ''}"`;
+function file_route(directory, relative, client) {
const headers = new Headers();
-
- let type = manifest.mimeTypes[extname(relative)] || Bun.file(original.path).type;
- if (type === 'text/html') type += ';charset=utf-8';
- if (type) headers.set('content-type', type);
-
- headers.set('accept-ranges', 'bytes');
- headers.set('etag', etag);
- if (last_modified > 0) {
- headers.set('last-modified', new Date(last_modified).toUTCString());
- }
if (client && relative.startsWith(`${manifest.appPath}/immutable/`)) {
headers.set('cache-control', 'public,max-age=31536000,immutable');
}
- if (PRECOMPRESS && compressed_files.has(relative)) {
- append_vary(headers, 'Accept-Encoding');
- }
- if (encoding) headers.set('content-encoding', encoding);
-
- const if_none_match = request.headers.get('if-none-match');
- if (
- if_none_match === '*' ||
- if_none_match?.split(',').some((value) => value.trim() === etag) ||
- (!if_none_match &&
- last_modified > 0 &&
- new Date(request.headers.get('if-modified-since') || 0).getTime() >= last_modified)
- ) {
- return new Response(null, { status: 304, headers });
- }
- let start = 0;
- let end = size - 1;
- let status = 200;
- const range = request.headers.get('range');
- const if_range = request.headers.get('if-range');
- if (
- range &&
- (!if_range ||
- if_range === etag ||
- (last_modified > 0 && new Date(if_range).getTime() >= last_modified))
- ) {
- const match = /^bytes=(\d*)-(\d*)$/.exec(range);
- if (!match || (!match[1] && !match[2])) {
- headers.set('content-range', `bytes */${size}`);
- return new Response(null, { status: 416, headers });
- }
+ const path = asset_path(directory, relative);
+ return {
+ GET: new Response(Bun.file(path), { headers }),
+ HEAD: new Response(Bun.file(path), { headers })
+ };
+}
- if (!match[1]) {
- const suffix = Number(match[2]);
- start = Math.max(0, size - suffix);
- } else {
- start = Number(match[1]);
- if (match[2]) end = Number(match[2]);
- }
+/** @type {import('bun').Serve.Routes} */
+export const routes = {};
- if (start >= size || end < start) {
- headers.set('content-range', `bytes */${size}`);
- return new Response(null, { status: 416, headers });
- }
+for (const file of client_files) {
+ routes[encode_pathname(`/${file}`)] = file_route('client', file, true);
+}
- end = Math.min(end, size - 1);
- status = 206;
- headers.set('content-range', `bytes ${start}-${end}/${size}`);
+for (const pathname of prerendered_paths) {
+ const file = find_prerendered_file(pathname);
+ const route = encode_pathname(pathname);
+ if (file && routes[route] === undefined) {
+ routes[route] = file_route('prerendered', file, false);
}
+}
+
+for (const pathname of prerendered_paths) {
+ const inverted = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
+ if (!inverted) continue;
- const content_length = end - start + 1;
- headers.set('content-length', String(content_length));
- const body = request.method === 'HEAD' ? null : file.slice(start, end + 1);
+ const route = encode_pathname(inverted);
+ if (routes[route] !== undefined) continue;
- return new Response(body, { status, headers });
+ const location = relative_pathname(route, encode_pathname(pathname));
+ const redirect = (/** @type {Request} */ request) =>
+ new Response(null, {
+ status: 308,
+ headers: { location: location + new URL(request.url).search }
+ });
+ routes[route] = { GET: redirect, HEAD: redirect };
}
diff --git a/packages/adapter-bun/src/utils.js b/packages/adapter-bun/src/utils.js
index 4053cbea3b75..d9c33f132cc6 100644
--- a/packages/adapter-bun/src/utils.js
+++ b/packages/adapter-bun/src/utils.js
@@ -12,45 +12,3 @@ export function parse_as_bytes(value) {
return Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier;
}
-
-/**
- * @param {Headers} headers
- * @param {string} value
- */
-export function append_vary(headers, value) {
- const current = headers.get('vary');
- if (!current) {
- headers.set('vary', value);
- return;
- }
-
- const values = current.split(',').map((part) => part.trim().toLowerCase());
- if (!values.includes(value.toLowerCase()) && !values.includes('*')) {
- headers.set('vary', `${current}, ${value}`);
- }
-}
-
-/**
- * @param {string | null} header
- * @param {string} encoding
- * @returns {boolean}
- */
-export function accepts_encoding(header, encoding) {
- if (!header) return false;
-
- /** @type {boolean | undefined} */
- let wildcard;
- for (const item of header.split(',')) {
- const [name, ...parameters] = item.trim().toLowerCase().split(';');
- let quality = 1;
- for (const parameter of parameters) {
- const match = /^q\s*=\s*(0(?:\.\d+)?|1(?:\.0+)?)$/.exec(parameter.trim());
- if (match) quality = Number(match[1]);
- }
-
- if (name === encoding) return quality > 0;
- if (name === '*') wildcard = quality > 0;
- }
-
- return wildcard ?? false;
-}
diff --git a/packages/adapter-bun/src/utils.spec.ts b/packages/adapter-bun/src/utils.spec.ts
index e963bfc83c88..0a6904f00cdd 100644
--- a/packages/adapter-bun/src/utils.spec.ts
+++ b/packages/adapter-bun/src/utils.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
-import { accepts_encoding, append_vary, parse_as_bytes } from './utils.js';
+import { parse_as_bytes } from './utils.js';
describe('parse_as_bytes', () => {
test.each([
@@ -12,19 +12,3 @@ describe('parse_as_bytes', () => {
expect(parse_as_bytes(input)).toBe(expected);
});
});
-
-describe('accepts_encoding', () => {
- test('honors quality values and wildcards', () => {
- expect(accepts_encoding('gzip;q=0, *;q=1', 'gzip')).toBe(false);
- expect(accepts_encoding('gzip;q=0, *;q=1', 'br')).toBe(true);
- expect(accepts_encoding('br; q=0.5', 'br')).toBe(true);
- });
-});
-
-describe('append_vary', () => {
- test('does not add duplicate values', () => {
- const headers = new Headers({ vary: 'Origin, Accept-Encoding' });
- append_vary(headers, 'accept-encoding');
- expect(headers.get('vary')).toBe('Origin, Accept-Encoding');
- });
-});
diff --git a/packages/adapter-bun/test/apps/basic/static/encoded name.txt b/packages/adapter-bun/test/apps/basic/static/encoded name.txt
new file mode 100644
index 000000000000..4851e9b09386
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/static/encoded name.txt
@@ -0,0 +1 @@
+hello from an encoded filename
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 45d7b412462f..3fc2f4dd628b 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -17,43 +17,52 @@ test('provides Bun request context', async ({ request }) => {
});
test('serves static files with Bun file responses', async ({ request }) => {
- const response = await request.get('/data.json', {
- headers: { 'accept-encoding': 'identity' }
- });
+ const response = await request.get('/data.json');
expect(response.status()).toBe(200);
- expect(response.headers()['content-type']).toBe('application/json');
- expect(response.headers()['accept-ranges']).toBe('bytes');
- expect(response.headers()['vary']).toBe('Accept-Encoding');
+ expect(response.headers()['content-type']).toContain('application/json');
expect(await response.json()).toEqual({ message: 'hello from a static file' });
+
+ const head = await request.head('/data.json');
+ expect(head.status()).toBe(200);
+ expect(head.headers()['content-length']).toBe(response.headers()['content-length']);
+ expect(await head.text()).toBe('');
});
test('supports ranges and conditional requests for static files', async ({ request }) => {
- const initial = await request.get('/data.json', {
- headers: { 'accept-encoding': 'identity' }
- });
- const etag = initial.headers()['etag'];
+ const initial = await request.get('/data.json');
const last_modified = initial.headers()['last-modified'];
const body = await initial.text();
- expect(etag).toBeTruthy();
expect(last_modified).toBeTruthy();
- const not_modified = await request.get('/data.json', {
- headers: { 'accept-encoding': 'identity', 'if-none-match': etag }
- });
- expect(not_modified.status()).toBe(304);
const not_modified_since = await request.get('/data.json', {
- headers: { 'accept-encoding': 'identity', 'if-modified-since': last_modified }
+ headers: { 'if-modified-since': last_modified }
});
expect(not_modified_since.status()).toBe(304);
const range = await request.get('/data.json', {
- headers: { 'accept-encoding': 'identity', range: 'bytes=0-3' }
+ headers: { range: 'bytes=0-3' }
});
expect(range.status()).toBe(206);
+ expect(range.headers()['accept-ranges']).toBe('bytes');
expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`);
expect(await range.text()).toBe(body.slice(0, 4));
});
+test('serves URL-encoded static filenames', async ({ request }) => {
+ const response = await request.get('/encoded%20name.txt');
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toBe('hello from an encoded filename\n');
+});
+
+test('caches immutable client assets', async ({ request }) => {
+ const page = await request.get('/');
+ const asset = /["']([^"']*_app\/immutable\/[^"']+)["']/.exec(await page.text())?.[1];
+ expect(asset).toBeTruthy();
+
+ const asset_response = await request.get(/** @type {string} */ (asset));
+ expect(asset_response.headers()['cache-control']).toBe('public,max-age=31536000,immutable');
+});
+
test('does not serve static files for non-GET requests', async ({ request }) => {
const response = await request.post('/data.json');
expect(response.status()).not.toBe(200);
From c7e78cfc20c1dd2f4dcb0f433ad0b0d32ff4f77c Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 01:11:04 +0300
Subject: [PATCH 10/94] feat(adapter-bun): optimize file handling in file_route
function for improved performance
---
packages/adapter-bun/src/static.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/packages/adapter-bun/src/static.js b/packages/adapter-bun/src/static.js
index a66dc1124932..429e403d3a32 100644
--- a/packages/adapter-bun/src/static.js
+++ b/packages/adapter-bun/src/static.js
@@ -47,9 +47,10 @@ function file_route(directory, relative, client) {
}
const path = asset_path(directory, relative);
+ const file = Bun.file(path);
return {
- GET: new Response(Bun.file(path), { headers }),
- HEAD: new Response(Bun.file(path), { headers })
+ GET: new Response(file, { headers }),
+ HEAD: new Response(file, { headers })
};
}
From 0ec0a8bb227cf316723e4c790f076be6aa1ac0c4 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 10:42:39 +0300
Subject: [PATCH 11/94] feat(adapter-bun): update envPrefix documentation for
clarity on usage and examples
---
packages/adapter-bun/index.d.ts | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index f2fd3f752f7a..8212f044ac32 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -26,7 +26,19 @@ interface AdapterOptions {
*/
out?: string;
/**
- * A prefix for the environment variables used to configure the production server.
+ * If you need to change the name of the environment variables used to configure
+ * the deployment (for example, to deconflict with environment variables you
+ * don't control), you can specify a prefix:
+ *
+ * ```js
+ * envPrefix: 'MY_CUSTOM_'
+ * ```
+ *
+ * ```sh
+ * MY_CUSTOM_HOST=127.0.0.1 \
+ * MY_CUSTOM_PORT=4000 \
+ * node build
+ * ```
*/
envPrefix?: string;
/**
From 0920cea895c78265429335610f6fd830e7b49a32 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 11:43:58 +0300
Subject: [PATCH 12/94] feat(adapter-bun): enhance Bun adapter with TLS support
and improved asset handling
- Added TLS configuration options in serverOptions, allowing for certificate and key specification.
- Introduced embedded asset handling for serving static files with metadata (size, type, lastModified, etag).
- Updated file_route to support embedded assets and conditional requests.
- Enhanced compile options to reserve runtime target and module format, ensuring user configurations are safely composed.
- Added tests for TLS options and embedded asset functionality to ensure robustness.
- Improved documentation to clarify usage of new features and options.
---
.github/workflows/platform-tests-bun.yml | 13 +-
.../25-build-and-deploy/45-adapter-bun.md | 25 ++++
packages/adapter-bun/index.d.ts | 19 ++-
packages/adapter-bun/index.js | 129 +++++++++++++-----
packages/adapter-bun/index.spec.ts | 124 +++++++++++++++++
packages/adapter-bun/src/assets.js | 22 ++-
packages/adapter-bun/src/dir.js | 6 +-
packages/adapter-bun/src/index.js | 78 +----------
packages/adapter-bun/src/static.js | 114 +++++++++++++++-
packages/adapter-bun/src/tls.js | 86 ++++++++++++
packages/adapter-bun/src/tls.spec.ts | 66 +++++++++
.../apps/basic/src/routes/platform/+server.js | 7 +-
.../adapter-bun/test/apps/basic/test/test.js | 5 +
.../test/apps/basic/vite.config.js | 21 ++-
packages/adapter-bun/test/utils.js | 8 +-
packages/adapter-bun/tests/types.ts | 41 ++++++
packages/adapter-bun/tsconfig.json | 1 +
17 files changed, 638 insertions(+), 127 deletions(-)
create mode 100644 packages/adapter-bun/index.spec.ts
create mode 100644 packages/adapter-bun/src/tls.js
create mode 100644 packages/adapter-bun/src/tls.spec.ts
create mode 100644 packages/adapter-bun/tests/types.ts
diff --git a/.github/workflows/platform-tests-bun.yml b/.github/workflows/platform-tests-bun.yml
index be085a21a37a..8c621db7e2b6 100644
--- a/.github/workflows/platform-tests-bun.yml
+++ b/.github/workflows/platform-tests-bun.yml
@@ -47,8 +47,17 @@ jobs:
test-app-dir: packages/adapter-bun/test/apps/basic
os: ubuntu-latest
- - name: Compile executable
+ - name: Test compiled executable
working-directory: packages/adapter-bun/test/apps/basic
env:
COMPILE: 'true'
- run: bun run --bun build
+ run: pnpm test:platform
+
+ - name: Compile optimized cross-target executable
+ working-directory: packages/adapter-bun/test/apps/basic
+ env:
+ ADVANCED_COMPILE: 'true'
+ COMPILE_TARGET: 'bun-linux-x64'
+ run: |
+ bun run --bun build
+ test -x build/advanced-app
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index dc5120c688b6..4843d2e80fd3 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -34,6 +34,8 @@ bun ./build
The default output directory is `build`. Production dependencies are externalised in the same way as with [`adapter-node`](adapter-node): packages in `dependencies` must be installed alongside the build, while packages in `devDependencies` are bundled into it.
Client assets and prerendered pages are served through Bun's native `routes` and file responses. This includes streaming and range requests, conditional requests using `Last-Modified`, correct MIME types, and immutable caching for hashed SvelteKit assets.
+File responses are intentionally not buffered at startup: Bun can use `sendfile(2)` where available,
+keeps memory usage bounded for large assets, and retains native range and conditional-request handling.
## Options
@@ -74,6 +76,11 @@ Adds a prefix to all environment variables read by the production server. For ex
Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings such as `hostname`, `port`, `reusePort`, `ipv6Only`, `idleTimeout`, `development`, `maxRequestBodySize`, `tls`, `http3`, and `http1`. Environment variables override these defaults.
+TLS certificate, private-key, and CA values in `serverOptions` must be PEM strings or arrays of PEM
+strings. Use the TLS environment variables below when you want to configure file paths at deployment
+time. Other JSON-serializable Bun TLS settings, including mTLS, ALPN, and cipher settings, can be
+passed directly.
+
`fetch`, `routes`, `websocket`, and `error` handlers cannot be serialized. To use those APIs, create a [custom server](#Custom-server).
### compile
@@ -110,6 +117,10 @@ adapter({
```
Native dependencies and cross-compilation have the same constraints as [Bun's single-file executables](https://bun.com/docs/bundler/executables).
+The adapter reserves the top-level Bun build `target` and `format` because generated servers always
+run as Bun ESM. Set the executable target inside `compile.target`, as shown above. Minification,
+sourcemaps, and bytecode remain opt-in. Advanced compile options without an explicit `outfile` or
+`outdir` use `build/app`.
## Environment variables
@@ -183,6 +194,20 @@ export function GET({ platform }) {
}
```
+The server object also exposes Bun's native operational metrics. Applications can publish them
+through their own authenticated endpoint or instrumentation without the adapter reserving a URL:
+
+```js
+/** @type {import('./$types').RequestHandler} */
+export function GET({ platform }) {
+ return Response.json({
+ pendingRequests: platform.server.pendingRequests,
+ pendingWebSockets: platform.server.pendingWebSockets,
+ chatSubscribers: platform.server.subscriberCount('chat')
+ });
+}
+```
+
## Custom server
The build contains `index.js`, which starts the default server, and `handler.js`, which exports the Bun-native SvelteKit request handler. Import the handler when you need Bun routes, WebSockets, custom error handling, or other `Bun.serve` options that cannot be represented as JSON:
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 8212f044ac32..bbf9595792f0 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -9,14 +9,21 @@ declare global {
type ServerOptions = Omit<
Serve.BaseServeOptions & Serve.HostnamePortServeOptions,
- 'fetch' | 'routes' | 'websocket' | 'error'
+ 'fetch' | 'routes' | 'websocket' | 'error' | 'tls'
> & {
unix?: string;
- tls?: TLSOptions | TLSOptions[];
+ tls?: JSONTLSOptions | JSONTLSOptions[];
};
-type CompileOptions = Omit & {
- compile: NonNullable;
+type JSONTLSOptions = Omit & {
+ ca?: string | string[];
+ cert?: string | string[];
+ key?: string | string[];
+ ALPNProtocols?: string;
+};
+
+type CompileOptions = Omit & {
+ compile: Exclude, false>;
};
interface AdapterOptions {
@@ -49,7 +56,9 @@ interface AdapterOptions {
serverOptions?: ServerOptions;
/**
* Compile the build into a single executable containing the server and static assets.
- * Pass Bun build options directly for advanced configuration. The generated entrypoint is reserved.
+ * Pass Bun build options directly for advanced configuration. The generated entrypoint,
+ * top-level target, and module format are reserved. If neither an outfile nor outdir is
+ * specified, the executable is written to `/app`.
* @default false
*/
compile?: boolean | CompileOptions;
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 09b89971fe8d..add8e22bacec 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,8 +1,6 @@
-import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-const files = fileURLToPath(new URL('./files', import.meta.url).href);
+const files = resolve(import.meta.dirname, 'files');
/** @type {import('./index.js').default} */
export default function (opts = {}) {
@@ -35,7 +33,7 @@ export default function (opts = {}) {
builder.log.minor(compile ? 'Compiling executable' : 'Building server');
- const pkg = JSON.parse(await readFile('package.json', 'utf8'));
+ const pkg = await Bun.file('package.json').json();
const server = builder.getServerDirectory();
const entries = posixify(`${tmp}/entries`);
builder.copy(files, entries);
@@ -56,7 +54,7 @@ export default function (opts = {}) {
[server_options_file]: `export default ${serialize(serverOptions)};\n`
};
- const compile_options = compile === true ? { compile: { outfile: `${out}/app` } } : compile;
+ const compile_options = normalize_compile_options(compile, out);
const entrypoints = [`${entries}/index.js`, `${entries}/handler.js`, dir_id];
/** @type {Omit} */
let build_options = {
@@ -83,7 +81,7 @@ export default function (opts = {}) {
...prerendered_files.map((file) => `prerendered/${file}`)
];
const compile_file = `${out}/adapter-bun-compile.js`;
- virtual_files[compile_file] = create_compile_entrypoint(
+ virtual_files[compile_file] = await create_compile_entrypoint(
out,
assets,
`${entries}/index.js`,
@@ -106,15 +104,7 @@ export default function (opts = {}) {
});
build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, async ({ path }) => {
- let contents = await readFile(path, 'utf8');
- if (contents.includes('dirname(fileURLToPath(import.meta.url))')) {
- // Bun places shared modules two levels below the output directory
- contents = contents.replace(
- 'dirname(fileURLToPath(import.meta.url))',
- "fileURLToPath(new URL('../../', import.meta.url))"
- );
- }
- contents = contents
+ const contents = (await Bun.file(path).text())
.replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
.replace(
/\bORIGIN\b/g,
@@ -125,21 +115,29 @@ export default function (opts = {}) {
}
};
- const result = await Bun.build({
- target: 'bun',
- format: 'esm',
- conditions: ['bun', 'node'],
- ...build_options,
- entrypoints,
- files: {
- ...build_options.files,
- ...virtual_files
- },
- plugins: [...(build_options.plugins ?? []), adapter_plugin]
- });
+ let result;
+ try {
+ result = await Bun.build({
+ ...build_options,
+ target: 'bun',
+ format: 'esm',
+ conditions: merge_conditions(build_options.conditions),
+ entrypoints,
+ files: {
+ ...build_options.files,
+ ...virtual_files
+ },
+ plugins: [adapter_plugin, ...(build_options.plugins ?? [])]
+ });
+ } catch (error) {
+ if (error instanceof AggregateError) {
+ throw build_error(error.errors, error);
+ }
+ throw new Error('Bun server build failed', { cause: error });
+ }
if (!result.success) {
- throw new AggregateError(result.logs, 'Bun server build failed');
+ throw build_error(result.logs);
}
if (instrumentation && !compile) {
@@ -160,6 +158,58 @@ export default function (opts = {}) {
};
}
+/**
+ * @param {false | true | import('./index.js').CompileOptions} compile
+ * @param {string} out
+ * @returns {Omit | undefined}
+ */
+function normalize_compile_options(compile, out) {
+ if (!compile) return;
+
+ const outfile = `${out}/app`;
+ if (compile === true) return { compile: { outfile } };
+
+ const options = { ...compile };
+ if (!options.compile) {
+ throw new Error('adapter-bun compile options must enable Bun executable compilation');
+ }
+
+ if (options.outdir === undefined) {
+ if (options.compile === true) {
+ options.compile = { outfile };
+ } else if (typeof options.compile === 'string') {
+ options.compile = { target: options.compile, outfile };
+ } else if (options.compile.outfile === undefined) {
+ options.compile = { outfile, ...options.compile };
+ }
+ }
+
+ return options;
+}
+
+/**
+ * @param {string | string[] | undefined} configured
+ * @returns {string[]}
+ */
+function merge_conditions(configured) {
+ const conditions =
+ configured === undefined ? [] : Array.isArray(configured) ? configured : [configured];
+ return [...new Set(['bun', 'node', ...conditions])];
+}
+
+/**
+ * @param {Array<{ message?: string }>} logs
+ * @param {unknown} [cause]
+ */
+function build_error(logs, cause) {
+ const details = logs
+ .map((log) => log.message)
+ .filter(Boolean)
+ .join('\n');
+ const message = details ? `Bun server build failed:\n${details}` : 'Bun server build failed';
+ return new AggregateError(logs, message, { cause });
+}
+
/**
* @param {string[]} files
* @param {string} base
@@ -199,19 +249,34 @@ function serialize(value) {
* @param {string[]} assets
* @param {string} entrypoint
* @param {string | undefined} instrumentation
- * @returns {string}
+ * @returns {Promise}
*/
-function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
+async function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
const unique_assets = [...new Set(assets)];
+ const metadata = [];
+ for (const file of unique_assets) {
+ const source = Bun.file(resolve(out, file));
+ const hash = new Bun.CryptoHasher('sha256').update(await source.arrayBuffer()).digest('hex');
+ metadata.push({
+ file,
+ size: source.size,
+ type: source.type,
+ lastModified: new Date(source.lastModified).toUTCString(),
+ etag: `"${hash}"`
+ });
+ }
const imports = unique_assets.map(
(file, index) =>
`import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
);
- const entries = unique_assets.map((file, index) => [file, `asset_${index}`]);
+ const entries = metadata.map(({ file, ...metadata }, index) => [
+ file,
+ `{ path: asset_${index}, ...${JSON.stringify(metadata)} }`
+ ]);
return [
...imports,
`globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
- .map(([file, identifier]) => `[${JSON.stringify(file)}, ${identifier}]`)
+ .map(([file, value]) => `[${JSON.stringify(file)}, ${value}]`)
.join(',')}]);`,
instrumentation && `await import(${JSON.stringify(instrumentation)});`,
`await import(${JSON.stringify(entrypoint)});`
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
new file mode 100644
index 000000000000..871b25c037ed
--- /dev/null
+++ b/packages/adapter-bun/index.spec.ts
@@ -0,0 +1,124 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import adapter from './index.js';
+
+const { build, file } = vi.hoisted(() => {
+ const build = vi.fn(async (_options: any) => ({ success: true, logs: [], outputs: [] }));
+ const file = vi.fn(() => ({ json: async () => ({ dependencies: { dependency: '1.0.0' } }) }));
+ vi.stubGlobal('Bun', { build, file });
+ return { build, file };
+});
+
+afterEach(() => {
+ build.mockClear();
+ file.mockClear();
+});
+
+describe('Bun build options', () => {
+ test('reserves the runtime target and module format', async () => {
+ await adapter().adapt(builder());
+
+ const options = build.mock.calls[0][0];
+ expect(options).toMatchObject({
+ target: 'bun',
+ format: 'esm',
+ conditions: ['bun', 'node'],
+ outdir: 'build',
+ splitting: true
+ });
+ expect(options.entrypoints).toHaveLength(3);
+ expect(options.plugins[0].name).toBe('adapter-bun');
+ });
+
+ test('normalizes executable output and composes user configuration safely', async () => {
+ const user_plugin = { name: 'user-plugin', setup() {} };
+ await adapter({
+ out: 'dist',
+ serverOptions: {
+ tls: { cert: 'certificate', key: ['private key'], requestCert: true }
+ },
+ compile: {
+ compile: { target: 'bun-linux-x64' },
+ conditions: ['custom', 'bun'],
+ files: {
+ 'virtual:user': 'export default true',
+ '.svelte-kit/output/server/adapter-bun-manifest.js': 'invalid manifest'
+ },
+ plugins: [user_plugin],
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked'
+ }
+ }).adapt(builder());
+
+ const options = build.mock.calls[0][0];
+ expect(options.compile).toEqual({ target: 'bun-linux-x64', outfile: 'dist/app' });
+ expect(options.conditions).toEqual(['bun', 'node', 'custom']);
+ expect(options.plugins.map((plugin: { name: string }) => plugin.name)).toEqual([
+ 'adapter-bun',
+ 'user-plugin'
+ ]);
+ expect(options.files['virtual:user']).toBe('export default true');
+ expect(options.files['.svelte-kit/output/server/adapter-bun-manifest.js']).not.toBe(
+ 'invalid manifest'
+ );
+ expect(options.files['.svelte-kit/output/server/adapter-bun-options.js']).toContain(
+ '"requestCert":true'
+ );
+ expect(options).toMatchObject({
+ target: 'bun',
+ format: 'esm',
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked'
+ });
+ expect(options.entrypoints).toHaveLength(1);
+ });
+
+ test('preserves an explicit outdir for split executables', async () => {
+ await adapter({
+ compile: {
+ compile: true,
+ outdir: 'dist/executable',
+ splitting: true
+ }
+ }).adapt(builder());
+
+ expect(build.mock.calls[0][0]).toMatchObject({
+ compile: true,
+ outdir: 'dist/executable',
+ splitting: true
+ });
+ });
+
+ test('rejects advanced options that disable executable compilation', async () => {
+ const invalid = { compile: { compile: false } } as any;
+ await expect(adapter(invalid).adapt(builder())).rejects.toThrow('must enable Bun executable');
+ expect(build).not.toHaveBeenCalled();
+ });
+
+ test('reports serialization and build failures clearly', async () => {
+ await expect(
+ adapter({ serverOptions: { port: Infinity } as any }).adapt(builder())
+ ).rejects.toThrow('Could not serialize adapter-bun serverOptions');
+
+ build.mockRejectedValueOnce(new AggregateError([], 'native failure'));
+ await expect(adapter().adapt(builder())).rejects.toThrow('Bun server build failed');
+ });
+});
+
+function builder() {
+ return {
+ config: { kit: { paths: { base: '', origin: undefined } } },
+ prerendered: { paths: [] },
+ log: { minor() {} },
+ getBuildDirectory: () => '.svelte-kit/adapter-bun',
+ getServerDirectory: () => '.svelte-kit/output/server',
+ rimraf() {},
+ mkdirp() {},
+ writeClient: () => [],
+ writePrerendered: () => [],
+ copy() {},
+ generateManifest: () => '{}',
+ hasServerInstrumentationFile: () => false
+ } as any;
+}
diff --git a/packages/adapter-bun/src/assets.js b/packages/adapter-bun/src/assets.js
index 07eb05133c25..dcfde142977d 100644
--- a/packages/adapter-bun/src/assets.js
+++ b/packages/adapter-bun/src/assets.js
@@ -1,15 +1,33 @@
import { join } from 'node:path';
import { dir } from './dir.js';
-const embedded_files = /** @type {Map | undefined} */ (
+const embedded_files = /** @type {Map | undefined} */ (
/** @type {any} */ (globalThis)[Symbol.for('sveltekit.adapter-bun.assets')]
);
+/**
+ * @typedef {object} EmbeddedAsset
+ * @property {string} path
+ * @property {number} size
+ * @property {string} type
+ * @property {string} lastModified
+ * @property {string} etag
+ */
+
/**
* @param {'client' | 'prerendered'} directory
* @param {string} relative
* @returns {string}
*/
export function asset_path(directory, relative) {
- return embedded_files?.get(`${directory}/${relative}`) ?? join(dir, directory, relative);
+ return embedded_asset(directory, relative)?.path ?? join(dir, directory, relative);
+}
+
+/**
+ * @param {'client' | 'prerendered'} directory
+ * @param {string} relative
+ * @returns {EmbeddedAsset | undefined}
+ */
+export function embedded_asset(directory, relative) {
+ return embedded_files?.get(`${directory}/${relative}`);
}
diff --git a/packages/adapter-bun/src/dir.js b/packages/adapter-bun/src/dir.js
index 0d659cb20e31..4df206b2c99f 100644
--- a/packages/adapter-bun/src/dir.js
+++ b/packages/adapter-bun/src/dir.js
@@ -1,4 +1,4 @@
-import { dirname } from 'node:path';
-import { fileURLToPath } from 'node:url';
+import { resolve } from 'node:path';
-export const dir = dirname(fileURLToPath(import.meta.url));
+// Bun places shared modules in /server/chunks.
+export const dir = resolve(import.meta.dir, '../..');
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index f4b25137894f..133fb2e710ed 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -3,6 +3,7 @@ import server_options from 'SERVER_OPTIONS';
import { handler } from './handler.js';
import { boolean_env, env, number_env } from './env.js';
import { routes } from './static.js';
+import { get_tls_options } from './tls.js';
import { parse_as_bytes } from './utils.js';
const options = { ...server_options };
@@ -108,80 +109,3 @@ async function graceful_shutdown(reason) {
process.on('SIGTERM', () => void graceful_shutdown('SIGTERM'));
process.on('SIGINT', () => void graceful_shutdown('SIGINT'));
-
-/**
- * @param {unknown} configured
- * @returns {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined}
- */
-function get_tls_options(configured) {
- const cert = env('TLS_CERT');
- const key = env('TLS_KEY');
- const ca = env('TLS_CA');
- const passphrase = env('TLS_PASSPHRASE');
- const server_name = env('TLS_SERVER_NAME');
- const dh_params_file = env('TLS_DH_PARAMS_FILE');
- const low_memory_mode = boolean_env('TLS_LOW_MEMORY_MODE');
- const secure_options = number_env('TLS_SECURE_OPTIONS');
-
- if (
- cert === undefined &&
- key === undefined &&
- ca === undefined &&
- passphrase === undefined &&
- server_name === undefined &&
- dh_params_file === undefined &&
- low_memory_mode === undefined &&
- secure_options === undefined
- ) {
- return /** @type {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined} */ (
- configured
- );
- }
-
- if (Array.isArray(configured)) {
- throw new Error(
- 'TLS environment variables cannot be merged with an SNI array from serverOptions'
- );
- }
-
- const tls = /** @type {import('bun').TLSOptions} */ ({
- ...(configured && typeof configured === 'object' ? configured : {})
- });
- if (cert !== undefined) tls.cert = tls_files(cert);
- if (key !== undefined) tls.key = tls_files(key);
- if (ca !== undefined) tls.ca = tls_files(ca);
- if (passphrase !== undefined) tls.passphrase = passphrase;
- if (server_name !== undefined) tls.serverName = server_name;
- if (dh_params_file !== undefined) tls.dhParamsFile = dh_params_file;
- if (low_memory_mode !== undefined) tls.lowMemoryMode = low_memory_mode;
- if (secure_options !== undefined) tls.secureOptions = secure_options;
-
- if (!tls.cert || !tls.key) {
- throw new Error('TLS requires both TLS_CERT and TLS_KEY');
- }
-
- return tls;
-}
-
-/**
- * @param {string} value
- * @returns {import('bun').BunFile | import('bun').BunFile[]}
- */
-function tls_files(value) {
- /** @type {unknown} */
- let paths;
- try {
- paths = value.startsWith('[') ? JSON.parse(value) : value;
- } catch (error) {
- throw new Error('TLS file lists must be JSON arrays of paths', { cause: error });
- }
-
- if (Array.isArray(paths)) {
- if (!paths.every((path) => typeof path === 'string')) {
- throw new Error('TLS file paths must be strings');
- }
- return paths.map((path) => Bun.file(path));
- }
- if (typeof paths !== 'string') throw new Error('TLS file paths must be strings');
- return Bun.file(paths);
-}
diff --git a/packages/adapter-bun/src/static.js b/packages/adapter-bun/src/static.js
index 429e403d3a32..8cf287a1354f 100644
--- a/packages/adapter-bun/src/static.js
+++ b/packages/adapter-bun/src/static.js
@@ -1,5 +1,5 @@
import { client_files, manifest, prerendered_files, prerendered_paths } from 'MANIFEST';
-import { asset_path } from './assets.js';
+import { asset_path, embedded_asset } from './assets.js';
/**
* @param {string} pathname
@@ -38,7 +38,7 @@ function encode_pathname(pathname) {
* @param {'client' | 'prerendered'} directory
* @param {string} relative
* @param {boolean} client
- * @returns {Partial>}
+ * @returns {Partial Response)>>}
*/
function file_route(directory, relative, client) {
const headers = new Headers();
@@ -46,6 +46,14 @@ function file_route(directory, relative, client) {
headers.set('cache-control', 'public,max-age=31536000,immutable');
}
+ const embedded = embedded_asset(directory, relative);
+ if (embedded) {
+ return {
+ GET: (request) => embedded_file_response(request, embedded, headers, false),
+ HEAD: (request) => embedded_file_response(request, embedded, headers, true)
+ };
+ }
+
const path = asset_path(directory, relative);
const file = Bun.file(path);
return {
@@ -54,6 +62,108 @@ function file_route(directory, relative, client) {
};
}
+/**
+ * @param {Request} request
+ * @param {import('./assets.js').EmbeddedAsset} asset
+ * @param {Headers} route_headers
+ * @param {boolean} head
+ */
+function embedded_file_response(request, asset, route_headers, head) {
+ const headers = new Headers(route_headers);
+ headers.set('accept-ranges', 'bytes');
+ headers.set('content-length', String(asset.size));
+ if (asset.type) headers.set('content-type', asset.type);
+ headers.set('etag', asset.etag);
+ headers.set('last-modified', asset.lastModified);
+
+ if (is_not_modified(request, asset)) {
+ headers.delete('content-length');
+ return new Response(null, { status: 304, headers });
+ }
+
+ const range = get_range(request, asset);
+ if (range === null) {
+ headers.set('content-range', `bytes */${asset.size}`);
+ headers.set('content-length', '0');
+ return new Response(null, { status: 416, headers });
+ }
+
+ const file = Bun.file(asset.path);
+ if (range) {
+ const [start, end] = range;
+ headers.set('content-range', `bytes ${start}-${end}/${asset.size}`);
+ headers.set('content-length', String(end - start + 1));
+ return new Response(head ? null : file.slice(start, end + 1), { status: 206, headers });
+ }
+
+ return new Response(head ? null : file, { headers });
+}
+
+/**
+ * @param {Request} request
+ * @param {import('./assets.js').EmbeddedAsset} asset
+ */
+function is_not_modified(request, asset) {
+ const if_none_match = request.headers.get('if-none-match');
+ if (if_none_match !== null) {
+ return if_none_match.split(',').some((value) => {
+ const tag = value.trim();
+ return tag === '*' || tag === asset.etag || tag.replace(/^W\//, '') === asset.etag;
+ });
+ }
+
+ const if_modified_since = request.headers.get('if-modified-since');
+ if (if_modified_since === null) return false;
+ const modified_since = Date.parse(if_modified_since);
+ return Number.isFinite(modified_since) && modified_since >= Date.parse(asset.lastModified);
+}
+
+/**
+ * @param {Request} request
+ * @param {import('./assets.js').EmbeddedAsset} asset
+ * @returns {[number, number] | null | undefined}
+ */
+function get_range(request, asset) {
+ const value = request.headers.get('range');
+ if (value === null) return;
+
+ const if_range = request.headers.get('if-range');
+ if (if_range !== null) {
+ const date = Date.parse(if_range);
+ if (
+ if_range !== asset.etag &&
+ (!Number.isFinite(date) || date < Date.parse(asset.lastModified))
+ ) {
+ return;
+ }
+ }
+
+ const match = /^bytes=(\d*)-(\d*)$/.exec(value);
+ if (!match || (!match[1] && !match[2]) || asset.size === 0) return null;
+
+ let start;
+ let end;
+ if (match[1]) {
+ start = Number(match[1]);
+ end = match[2] ? Number(match[2]) : asset.size - 1;
+ } else {
+ const length = Number(match[2]);
+ if (length === 0) return null;
+ start = Math.max(0, asset.size - length);
+ end = asset.size - 1;
+ }
+
+ if (
+ !Number.isSafeInteger(start) ||
+ !Number.isSafeInteger(end) ||
+ start >= asset.size ||
+ end < start
+ ) {
+ return null;
+ }
+ return [start, Math.min(end, asset.size - 1)];
+}
+
/** @type {import('bun').Serve.Routes} */
export const routes = {};
diff --git a/packages/adapter-bun/src/tls.js b/packages/adapter-bun/src/tls.js
new file mode 100644
index 000000000000..d90752074dc0
--- /dev/null
+++ b/packages/adapter-bun/src/tls.js
@@ -0,0 +1,86 @@
+import { boolean_env, env, number_env } from './env.js';
+
+/**
+ * @param {unknown} configured
+ * @returns {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined}
+ */
+export function get_tls_options(configured) {
+ const cert = env('TLS_CERT');
+ const key = env('TLS_KEY');
+ const ca = env('TLS_CA');
+ const passphrase = env('TLS_PASSPHRASE');
+ const server_name = env('TLS_SERVER_NAME');
+ const dh_params_file = env('TLS_DH_PARAMS_FILE');
+ const low_memory_mode = boolean_env('TLS_LOW_MEMORY_MODE');
+ const secure_options = number_env('TLS_SECURE_OPTIONS');
+
+ if (
+ cert === undefined &&
+ key === undefined &&
+ ca === undefined &&
+ passphrase === undefined &&
+ server_name === undefined &&
+ dh_params_file === undefined &&
+ low_memory_mode === undefined &&
+ secure_options === undefined
+ ) {
+ return /** @type {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined} */ (
+ configured
+ );
+ }
+
+ if (Array.isArray(configured)) {
+ throw new Error(
+ 'TLS environment variables cannot be merged with an SNI array from serverOptions'
+ );
+ }
+
+ const tls = /** @type {import('bun').TLSOptions} */ ({
+ ...(configured && typeof configured === 'object' ? configured : {})
+ });
+ if (cert !== undefined) tls.cert = tls_files(cert);
+ if (key !== undefined) tls.key = tls_files(key);
+ if (ca !== undefined) tls.ca = tls_files(ca);
+ if (passphrase !== undefined) tls.passphrase = passphrase;
+ if (server_name !== undefined) tls.serverName = server_name;
+ if (dh_params_file !== undefined) tls.dhParamsFile = dh_params_file;
+ if (low_memory_mode !== undefined) tls.lowMemoryMode = low_memory_mode;
+ if (secure_options !== undefined) tls.secureOptions = secure_options;
+
+ if (!has_tls_value(tls.cert) || !has_tls_value(tls.key)) {
+ throw new Error('TLS configuration requires both a certificate and a private key');
+ }
+
+ return tls;
+}
+
+/**
+ * @param {string} value
+ * @returns {import('bun').BunFile | import('bun').BunFile[]}
+ */
+export function tls_files(value) {
+ /** @type {unknown} */
+ let paths;
+ try {
+ paths = value.startsWith('[') ? JSON.parse(value) : value;
+ } catch (error) {
+ throw new Error('TLS file lists must be JSON arrays of paths', { cause: error });
+ }
+
+ if (Array.isArray(paths)) {
+ if (paths.length === 0) throw new Error('TLS file path lists must not be empty');
+ if (!paths.every((path) => typeof path === 'string' && path.length > 0)) {
+ throw new Error('TLS file paths must be non-empty strings');
+ }
+ return paths.map((path) => Bun.file(path));
+ }
+ if (typeof paths !== 'string' || paths.length === 0) {
+ throw new Error('TLS file paths must be non-empty strings');
+ }
+ return Bun.file(paths);
+}
+
+/** @param {unknown} value */
+function has_tls_value(value) {
+ return Array.isArray(value) ? value.length > 0 : Boolean(value);
+}
diff --git a/packages/adapter-bun/src/tls.spec.ts b/packages/adapter-bun/src/tls.spec.ts
new file mode 100644
index 000000000000..fee89a2cacb9
--- /dev/null
+++ b/packages/adapter-bun/src/tls.spec.ts
@@ -0,0 +1,66 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import { get_tls_options, tls_files } from './tls.js';
+
+const { file } = vi.hoisted(() => {
+ const file = vi.fn((path: string) => ({ path }));
+ vi.stubGlobal('ENV_PREFIX', '');
+ vi.stubGlobal('Bun', { file });
+ return { file };
+});
+
+afterEach(() => {
+ vi.unstubAllEnvs();
+ file.mockClear();
+});
+
+describe('tls_files', () => {
+ test('converts a path to a BunFile', () => {
+ expect(tls_files('cert.pem')).toEqual({ path: 'cert.pem' });
+ expect(file).toHaveBeenCalledWith('cert.pem');
+ });
+
+ test('converts a JSON path list to BunFiles', () => {
+ expect(tls_files('["cert.pem","chain.pem"]')).toEqual([
+ { path: 'cert.pem' },
+ { path: 'chain.pem' }
+ ]);
+ });
+
+ test.each(['', '[]', '[""]', '[1]'])('rejects an invalid path list: %s', (value) => {
+ expect(() => tls_files(value)).toThrow(/must (?:not be empty|be non-empty strings)/);
+ });
+
+ test('reports malformed JSON arrays', () => {
+ expect(() => tls_files('["cert.pem"')).toThrow('must be JSON arrays of paths');
+ });
+});
+
+describe('get_tls_options', () => {
+ test('returns configured TLS options unchanged without environment overrides', () => {
+ const configured = { cert: 'certificate', key: 'private key', requestCert: true };
+ expect(get_tls_options(configured)).toBe(configured);
+ });
+
+ test('merges path environment variables with configured options', () => {
+ vi.stubEnv('TLS_CA', 'ca.pem');
+ expect(get_tls_options({ cert: 'certificate', key: 'private key' })).toEqual({
+ cert: 'certificate',
+ key: 'private key',
+ ca: { path: 'ca.pem' }
+ });
+ });
+
+ test('requires a certificate and private key when environment variables configure TLS', () => {
+ vi.stubEnv('TLS_CA', 'ca.pem');
+ expect(() => get_tls_options(undefined)).toThrow(
+ 'TLS configuration requires both a certificate and a private key'
+ );
+ });
+
+ test('does not merge environment variables into an SNI array', () => {
+ vi.stubEnv('TLS_CERT', 'cert.pem');
+ expect(() => get_tls_options([{ serverName: 'example.com' }])).toThrow(
+ 'cannot be merged with an SNI array'
+ );
+ });
+});
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js
index 456ea012cec7..fdb393aa68a2 100644
--- a/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js
+++ b/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js
@@ -5,6 +5,11 @@ export function GET({ getClientAddress, platform }) {
return json({
address: getClientAddress(),
request: platform?.request instanceof Request,
- server: typeof platform?.server?.requestIP === 'function'
+ server: typeof platform?.server?.requestIP === 'function',
+ id: platform?.server.id,
+ protocol: platform?.server.protocol,
+ pendingRequests: platform?.server.pendingRequests,
+ pendingWebSockets: platform?.server.pendingWebSockets,
+ subscribers: platform?.server.subscriberCount('adapter-bun-test')
});
}
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 3fc2f4dd628b..020663760c8b 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -14,6 +14,11 @@ test('provides Bun request context', async ({ request }) => {
expect(body.address).toBeTruthy();
expect(body.request).toBe(true);
expect(body.server).toBe(true);
+ expect(typeof body.id).toBe('string');
+ expect(body.protocol).toBe('http');
+ expect(body.pendingRequests).toBeGreaterThanOrEqual(1);
+ expect(body.pendingWebSockets).toBe(0);
+ expect(body.subscribers).toBe(0);
});
test('serves static files with Bun file responses', async ({ request }) => {
diff --git a/packages/adapter-bun/test/apps/basic/vite.config.js b/packages/adapter-bun/test/apps/basic/vite.config.js
index fe9700dff051..80fb15a881d4 100644
--- a/packages/adapter-bun/test/apps/basic/vite.config.js
+++ b/packages/adapter-bun/test/apps/basic/vite.config.js
@@ -2,6 +2,25 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import adapter from '../../../index.js';
+const compile =
+ process.env.ADVANCED_COMPILE === 'true'
+ ? {
+ compile: {
+ outfile: 'build/advanced-app',
+ ...(process.env.COMPILE_TARGET
+ ? {
+ target: /** @type {import('bun').Build.CompileTarget} */ (
+ process.env.COMPILE_TARGET
+ )
+ }
+ : {})
+ },
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked'
+ }
+ : process.env.COMPILE === 'true';
+
export default defineConfig({
build: {
minify: false
@@ -10,7 +29,7 @@ export default defineConfig({
sveltekit({
adapter: adapter({
envPrefix: 'MY_CUSTOM_',
- compile: process.env.COMPILE === 'true'
+ compile
})
})
]
diff --git a/packages/adapter-bun/test/utils.js b/packages/adapter-bun/test/utils.js
index 80ccd39dfa21..ca5e5e9f4d36 100644
--- a/packages/adapter-bun/test/utils.js
+++ b/packages/adapter-bun/test/utils.js
@@ -2,12 +2,16 @@ import { devices } from '@playwright/test';
import process from 'node:process';
import { number_from_env } from '../../../test-utils/index.js';
+const compiled = process.env.COMPILE === 'true';
+
/** @type {import('@playwright/test').PlaywrightTestConfig} */
export const config = {
forbidOnly: !!process.env.CI,
timeout: process.env.CI ? 45000 : 15000,
webServer: {
- command: 'bun run --bun build && bun run preview',
+ command: compiled
+ ? 'bun run --bun build && MY_CUSTOM_PORT=4174 ./build/app'
+ : 'bun run --bun build && bun run preview',
port: 4174
},
retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0),
@@ -20,7 +24,7 @@ export const config = {
...devices['Desktop Chrome'],
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
- channel: 'chromium'
+ channel: process.env.KIT_E2E_BROWSER ?? 'chromium'
},
workers: process.env.CI ? 2 : number_from_env('KIT_E2E_WORKERS', undefined),
reporter: 'list',
diff --git a/packages/adapter-bun/tests/types.ts b/packages/adapter-bun/tests/types.ts
new file mode 100644
index 000000000000..8891d4aba120
--- /dev/null
+++ b/packages/adapter-bun/tests/types.ts
@@ -0,0 +1,41 @@
+import adapter from '../index.js';
+
+adapter({
+ serverOptions: {
+ tls: {
+ cert: 'certificate',
+ key: ['private key'],
+ requestCert: true
+ }
+ },
+ compile: {
+ compile: { target: 'bun-linux-x64' },
+ minify: true,
+ bytecode: true
+ }
+});
+
+adapter({
+ compile: {
+ // @ts-expect-error false does not compile an executable
+ compile: false
+ }
+});
+
+adapter({
+ compile: {
+ compile: true,
+ // @ts-expect-error the adapter reserves the Bun runtime target
+ target: 'node'
+ }
+});
+
+adapter({
+ serverOptions: {
+ // @ts-expect-error BunFile values cannot be serialized into the generated server
+ tls: {
+ cert: Bun.file('cert.pem'),
+ key: 'private key'
+ }
+ }
+});
diff --git a/packages/adapter-bun/tsconfig.json b/packages/adapter-bun/tsconfig.json
index 946e9388ada8..140718bbd5bc 100644
--- a/packages/adapter-bun/tsconfig.json
+++ b/packages/adapter-bun/tsconfig.json
@@ -15,6 +15,7 @@
"include": [
"build.js",
"index.js",
+ "index.spec.ts",
"vitest.config.js",
"src/**/*.js",
"src/**/*.ts",
From 9a1658af1c1aed2a80ff08622c70f7009cb99137 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 12:36:47 +0300
Subject: [PATCH 13/94] feat(adapter-bun): remove TLS support and related
configurations from server options
---
.../25-build-and-deploy/45-adapter-bun.md | 22 +----
packages/adapter-bun/index.d.ts | 12 +--
packages/adapter-bun/index.spec.ts | 6 --
packages/adapter-bun/src/env.js | 10 ---
packages/adapter-bun/src/index.js | 22 +----
packages/adapter-bun/src/tls.js | 86 -------------------
packages/adapter-bun/src/tls.spec.ts | 66 --------------
packages/adapter-bun/tests/types.ts | 17 ----
8 files changed, 7 insertions(+), 234 deletions(-)
delete mode 100644 packages/adapter-bun/src/tls.js
delete mode 100644 packages/adapter-bun/src/tls.spec.ts
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 4843d2e80fd3..1ce2f98d6952 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -74,14 +74,9 @@ Adds a prefix to all environment variables read by the production server. For ex
### serverOptions
-Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings such as `hostname`, `port`, `reusePort`, `ipv6Only`, `idleTimeout`, `development`, `maxRequestBodySize`, `tls`, `http3`, and `http1`. Environment variables override these defaults.
+Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings such as `hostname`, `port`, `reusePort`, `ipv6Only`, `idleTimeout`, `development`, and `maxRequestBodySize`. Environment variables override these defaults.
-TLS certificate, private-key, and CA values in `serverOptions` must be PEM strings or arrays of PEM
-strings. Use the TLS environment variables below when you want to configure file paths at deployment
-time. Other JSON-serializable Bun TLS settings, including mTLS, ALPN, and cipher settings, can be
-passed directly.
-
-`fetch`, `routes`, `websocket`, and `error` handlers cannot be serialized. To use those APIs, create a [custom server](#Custom-server).
+`fetch`, `routes`, `websocket`, `error`, `tls`, `http3`, and `http1` cannot be configured this way. To use those APIs, create a [custom server](#Custom-server).
### compile
@@ -157,19 +152,6 @@ On `SIGINT` or `SIGTERM`, the server stops accepting connections and waits for i
Set `DEVELOPMENT=true` to enable Bun's contextual server error pages. It defaults to `false` in the generated production server.
-### TLS and HTTP/3
-
-Use `TLS_CERT` and `TLS_KEY` to provide certificate and private-key file paths. Each value may instead be a JSON array of file paths. The following additional variables are available:
-
-- `TLS_CA`
-- `TLS_PASSPHRASE`
-- `TLS_SERVER_NAME`
-- `TLS_DH_PARAMS_FILE`
-- `TLS_LOW_MEMORY_MODE`
-- `TLS_SECURE_OPTIONS`
-
-Set `HTTP3=true` to enable Bun's experimental HTTP/3 support. This requires TLS and cannot be combined with `SOCKET_PATH`. Set `HTTP1=false` together with `HTTP3=true` for an HTTP/3-only listener.
-
### Proxy headers
When [`paths.origin`](configuration#paths) is not configured, the adapter derives the request origin from Bun's request URL and the `host` header. Set `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` when a trusted reverse proxy exposes the public origin through other headers:
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index bbf9595792f0..f4f2dd0ffdd6 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -1,5 +1,5 @@
import type { Adapter } from '@sveltejs/kit';
-import type { BuildConfig, Serve, TLSOptions } from 'bun';
+import type { BuildConfig, Serve } from 'bun';
import './ambient.js';
declare global {
@@ -9,17 +9,9 @@ declare global {
type ServerOptions = Omit<
Serve.BaseServeOptions & Serve.HostnamePortServeOptions,
- 'fetch' | 'routes' | 'websocket' | 'error' | 'tls'
+ 'fetch' | 'routes' | 'websocket' | 'error' | 'tls' | 'http3' | 'http1'
> & {
unix?: string;
- tls?: JSONTLSOptions | JSONTLSOptions[];
-};
-
-type JSONTLSOptions = Omit & {
- ca?: string | string[];
- cert?: string | string[];
- key?: string | string[];
- ALPNProtocols?: string;
};
type CompileOptions = Omit & {
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 871b25c037ed..50c9e1c92901 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -33,9 +33,6 @@ describe('Bun build options', () => {
const user_plugin = { name: 'user-plugin', setup() {} };
await adapter({
out: 'dist',
- serverOptions: {
- tls: { cert: 'certificate', key: ['private key'], requestCert: true }
- },
compile: {
compile: { target: 'bun-linux-x64' },
conditions: ['custom', 'bun'],
@@ -61,9 +58,6 @@ describe('Bun build options', () => {
expect(options.files['.svelte-kit/output/server/adapter-bun-manifest.js']).not.toBe(
'invalid manifest'
);
- expect(options.files['.svelte-kit/output/server/adapter-bun-options.js']).toContain(
- '"requestCert":true'
- );
expect(options).toMatchObject({
target: 'bun',
format: 'esm',
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index 10d91f1e190a..bd0d76e902b4 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -9,16 +9,6 @@ const expected = new Set([
'IDLE_TIMEOUT',
'BODY_SIZE_LIMIT',
'DEVELOPMENT',
- 'HTTP3',
- 'HTTP1',
- 'TLS_CERT',
- 'TLS_KEY',
- 'TLS_CA',
- 'TLS_PASSPHRASE',
- 'TLS_SERVER_NAME',
- 'TLS_DH_PARAMS_FILE',
- 'TLS_LOW_MEMORY_MODE',
- 'TLS_SECURE_OPTIONS',
'XFF_DEPTH',
'ADDRESS_HEADER',
'PROTOCOL_HEADER',
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 133fb2e710ed..a96036dd114a 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -3,11 +3,13 @@ import server_options from 'SERVER_OPTIONS';
import { handler } from './handler.js';
import { boolean_env, env, number_env } from './env.js';
import { routes } from './static.js';
-import { get_tls_options } from './tls.js';
import { parse_as_bytes } from './utils.js';
const options = { ...server_options };
delete options.fetch;
+delete options.tls;
+delete options.http3;
+delete options.http1;
export const unix = env('SOCKET_PATH', /** @type {string | undefined} */ (options.unix));
export const hostname = env(
@@ -58,24 +60,6 @@ if (!Number.isSafeInteger(body_size_limit) || body_size_limit < 0) {
}
options.maxRequestBodySize = body_size_limit;
-const http3 = boolean_env('HTTP3', /** @type {boolean | undefined} */ (options.http3));
-const http1 = boolean_env('HTTP1', /** @type {boolean | undefined} */ (options.http1));
-if (http3 !== undefined) options.http3 = http3;
-if (http1 !== undefined) options.http1 = http1;
-
-const tls = get_tls_options(options.tls);
-if (tls) options.tls = tls;
-
-if (options.http3 && !options.tls) {
- throw new Error('HTTP3 requires TLS_CERT and TLS_KEY or TLS server options');
-}
-if (options.http1 === false && !options.http3) {
- throw new Error('HTTP1=false requires HTTP3=true');
-}
-if (unix && options.http3) {
- throw new Error('HTTP3 cannot be used with SOCKET_PATH');
-}
-
options.fetch = handler;
options.routes = routes;
diff --git a/packages/adapter-bun/src/tls.js b/packages/adapter-bun/src/tls.js
deleted file mode 100644
index d90752074dc0..000000000000
--- a/packages/adapter-bun/src/tls.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { boolean_env, env, number_env } from './env.js';
-
-/**
- * @param {unknown} configured
- * @returns {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined}
- */
-export function get_tls_options(configured) {
- const cert = env('TLS_CERT');
- const key = env('TLS_KEY');
- const ca = env('TLS_CA');
- const passphrase = env('TLS_PASSPHRASE');
- const server_name = env('TLS_SERVER_NAME');
- const dh_params_file = env('TLS_DH_PARAMS_FILE');
- const low_memory_mode = boolean_env('TLS_LOW_MEMORY_MODE');
- const secure_options = number_env('TLS_SECURE_OPTIONS');
-
- if (
- cert === undefined &&
- key === undefined &&
- ca === undefined &&
- passphrase === undefined &&
- server_name === undefined &&
- dh_params_file === undefined &&
- low_memory_mode === undefined &&
- secure_options === undefined
- ) {
- return /** @type {import('bun').TLSOptions | import('bun').TLSOptions[] | undefined} */ (
- configured
- );
- }
-
- if (Array.isArray(configured)) {
- throw new Error(
- 'TLS environment variables cannot be merged with an SNI array from serverOptions'
- );
- }
-
- const tls = /** @type {import('bun').TLSOptions} */ ({
- ...(configured && typeof configured === 'object' ? configured : {})
- });
- if (cert !== undefined) tls.cert = tls_files(cert);
- if (key !== undefined) tls.key = tls_files(key);
- if (ca !== undefined) tls.ca = tls_files(ca);
- if (passphrase !== undefined) tls.passphrase = passphrase;
- if (server_name !== undefined) tls.serverName = server_name;
- if (dh_params_file !== undefined) tls.dhParamsFile = dh_params_file;
- if (low_memory_mode !== undefined) tls.lowMemoryMode = low_memory_mode;
- if (secure_options !== undefined) tls.secureOptions = secure_options;
-
- if (!has_tls_value(tls.cert) || !has_tls_value(tls.key)) {
- throw new Error('TLS configuration requires both a certificate and a private key');
- }
-
- return tls;
-}
-
-/**
- * @param {string} value
- * @returns {import('bun').BunFile | import('bun').BunFile[]}
- */
-export function tls_files(value) {
- /** @type {unknown} */
- let paths;
- try {
- paths = value.startsWith('[') ? JSON.parse(value) : value;
- } catch (error) {
- throw new Error('TLS file lists must be JSON arrays of paths', { cause: error });
- }
-
- if (Array.isArray(paths)) {
- if (paths.length === 0) throw new Error('TLS file path lists must not be empty');
- if (!paths.every((path) => typeof path === 'string' && path.length > 0)) {
- throw new Error('TLS file paths must be non-empty strings');
- }
- return paths.map((path) => Bun.file(path));
- }
- if (typeof paths !== 'string' || paths.length === 0) {
- throw new Error('TLS file paths must be non-empty strings');
- }
- return Bun.file(paths);
-}
-
-/** @param {unknown} value */
-function has_tls_value(value) {
- return Array.isArray(value) ? value.length > 0 : Boolean(value);
-}
diff --git a/packages/adapter-bun/src/tls.spec.ts b/packages/adapter-bun/src/tls.spec.ts
deleted file mode 100644
index fee89a2cacb9..000000000000
--- a/packages/adapter-bun/src/tls.spec.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { afterEach, describe, expect, test, vi } from 'vitest';
-import { get_tls_options, tls_files } from './tls.js';
-
-const { file } = vi.hoisted(() => {
- const file = vi.fn((path: string) => ({ path }));
- vi.stubGlobal('ENV_PREFIX', '');
- vi.stubGlobal('Bun', { file });
- return { file };
-});
-
-afterEach(() => {
- vi.unstubAllEnvs();
- file.mockClear();
-});
-
-describe('tls_files', () => {
- test('converts a path to a BunFile', () => {
- expect(tls_files('cert.pem')).toEqual({ path: 'cert.pem' });
- expect(file).toHaveBeenCalledWith('cert.pem');
- });
-
- test('converts a JSON path list to BunFiles', () => {
- expect(tls_files('["cert.pem","chain.pem"]')).toEqual([
- { path: 'cert.pem' },
- { path: 'chain.pem' }
- ]);
- });
-
- test.each(['', '[]', '[""]', '[1]'])('rejects an invalid path list: %s', (value) => {
- expect(() => tls_files(value)).toThrow(/must (?:not be empty|be non-empty strings)/);
- });
-
- test('reports malformed JSON arrays', () => {
- expect(() => tls_files('["cert.pem"')).toThrow('must be JSON arrays of paths');
- });
-});
-
-describe('get_tls_options', () => {
- test('returns configured TLS options unchanged without environment overrides', () => {
- const configured = { cert: 'certificate', key: 'private key', requestCert: true };
- expect(get_tls_options(configured)).toBe(configured);
- });
-
- test('merges path environment variables with configured options', () => {
- vi.stubEnv('TLS_CA', 'ca.pem');
- expect(get_tls_options({ cert: 'certificate', key: 'private key' })).toEqual({
- cert: 'certificate',
- key: 'private key',
- ca: { path: 'ca.pem' }
- });
- });
-
- test('requires a certificate and private key when environment variables configure TLS', () => {
- vi.stubEnv('TLS_CA', 'ca.pem');
- expect(() => get_tls_options(undefined)).toThrow(
- 'TLS configuration requires both a certificate and a private key'
- );
- });
-
- test('does not merge environment variables into an SNI array', () => {
- vi.stubEnv('TLS_CERT', 'cert.pem');
- expect(() => get_tls_options([{ serverName: 'example.com' }])).toThrow(
- 'cannot be merged with an SNI array'
- );
- });
-});
diff --git a/packages/adapter-bun/tests/types.ts b/packages/adapter-bun/tests/types.ts
index 8891d4aba120..e24c5ae7bea7 100644
--- a/packages/adapter-bun/tests/types.ts
+++ b/packages/adapter-bun/tests/types.ts
@@ -1,13 +1,6 @@
import adapter from '../index.js';
adapter({
- serverOptions: {
- tls: {
- cert: 'certificate',
- key: ['private key'],
- requestCert: true
- }
- },
compile: {
compile: { target: 'bun-linux-x64' },
minify: true,
@@ -29,13 +22,3 @@ adapter({
target: 'node'
}
});
-
-adapter({
- serverOptions: {
- // @ts-expect-error BunFile values cannot be serialized into the generated server
- tls: {
- cert: Bun.file('cert.pem'),
- key: 'private key'
- }
- }
-});
From f5f23f58731fc5695137a024c94669dc3c3d02a1 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 13:15:56 +0300
Subject: [PATCH 14/94] feat(adapter-bun): refine server options type and
remove unused utility functions
---
packages/adapter-bun/internal.d.ts | 12 +++++-
packages/adapter-bun/src/index.js | 52 ++++++--------------------
packages/adapter-bun/src/utils.js | 14 -------
packages/adapter-bun/src/utils.spec.ts | 14 -------
4 files changed, 22 insertions(+), 70 deletions(-)
delete mode 100644 packages/adapter-bun/src/utils.js
delete mode 100644 packages/adapter-bun/src/utils.spec.ts
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 3745485373b7..f82ac60edd80 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -12,6 +12,16 @@ declare module 'SERVER' {
}
declare module 'SERVER_OPTIONS' {
- const options: Record;
+ const options: Pick<
+ import('bun').Serve.Options,
+ | 'development'
+ | 'hostname'
+ | 'port'
+ | 'idleTimeout'
+ | 'maxRequestBodySize'
+ | 'reusePort'
+ | 'unix'
+ | 'ipv6Only'
+ >;
export default options;
}
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index a96036dd114a..3034c2e8da36 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -3,20 +3,10 @@ import server_options from 'SERVER_OPTIONS';
import { handler } from './handler.js';
import { boolean_env, env, number_env } from './env.js';
import { routes } from './static.js';
-import { parse_as_bytes } from './utils.js';
-const options = { ...server_options };
-delete options.fetch;
-delete options.tls;
-delete options.http3;
-delete options.http1;
+const options = /** @type {import('bun').Serve.Options} */ ({ ...server_options });
-export const unix = env('SOCKET_PATH', /** @type {string | undefined} */ (options.unix));
-export const hostname = env(
- 'HOST',
- /** @type {string | undefined} */ (options.hostname) ?? '0.0.0.0'
-);
-export const port = env('PORT', options.port === undefined ? '3000' : String(options.port));
+export const unix = env('SOCKET_PATH', options.unix);
if (unix) {
options.unix = unix;
@@ -26,23 +16,13 @@ if (unix) {
delete options.ipv6Only;
} else {
delete options.unix;
- options.hostname = hostname;
- options.port = port;
- options.reusePort = boolean_env(
- 'REUSE_PORT',
- /** @type {boolean | undefined} */ (options.reusePort)
- );
- options.ipv6Only = boolean_env(
- 'IPV6_ONLY',
- /** @type {boolean | undefined} */ (options.ipv6Only)
- );
+ options.hostname = env('HOST', options.hostname);
+ options.port = env('PORT', options.port ? String(options.port) : undefined);
+ options.reusePort = boolean_env('REUSE_PORT', options.reusePort);
+ options.ipv6Only = boolean_env('IPV6_ONLY', options.ipv6Only);
}
-options.idleTimeout = number_env(
- 'IDLE_TIMEOUT',
- /** @type {number | undefined} */ (options.idleTimeout),
- { max: 255 }
-);
+options.idleTimeout = number_env('IDLE_TIMEOUT', options.idleTimeout, { max: 255 });
const development = boolean_env('DEVELOPMENT');
if (development !== undefined) {
options.development = development;
@@ -50,26 +30,16 @@ if (development !== undefined) {
options.development = false;
}
-const body_size_limit = parse_as_bytes(
- env('BODY_SIZE_LIMIT', String(options.maxRequestBodySize ?? '512K')) || ''
-);
-if (!Number.isSafeInteger(body_size_limit) || body_size_limit < 0) {
- throw new Error(
- `Invalid BODY_SIZE_LIMIT: ${JSON.stringify(env('BODY_SIZE_LIMIT'))}. Please provide a non-negative integer with an optional K, M, or G suffix.`
- );
-}
-options.maxRequestBodySize = body_size_limit;
+options.maxRequestBodySize = number_env('BODY_SIZE_LIMIT', options.maxRequestBodySize);
options.fetch = handler;
options.routes = routes;
-export const server = Bun.serve(
- /** @type {import('bun').Serve.Options} */ (/** @type {unknown} */ (options))
-);
+export const server = Bun.serve(options);
console.log(unix ? `Listening on ${unix}` : `Listening on ${server.url}`);
-const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT', 30) ?? 30;
+const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT') ?? 30;
let shutting_down = false;
/** @param {'SIGINT' | 'SIGTERM'} reason */
@@ -83,7 +53,7 @@ async function graceful_shutdown(reason) {
void server.stop(true);
}, shutdown_timeout * 1000);
- await server.stop(false);
+ await server.stop();
clearTimeout(timeout);
// @ts-expect-error custom events cannot be typed
process.emit('sveltekit:shutdown', reason);
diff --git a/packages/adapter-bun/src/utils.js b/packages/adapter-bun/src/utils.js
deleted file mode 100644
index d9c33f132cc6..000000000000
--- a/packages/adapter-bun/src/utils.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @param {string} value
- * @returns {number}
- */
-export function parse_as_bytes(value) {
- const multiplier =
- {
- K: 1024,
- M: 1024 * 1024,
- G: 1024 * 1024 * 1024
- }[value[value.length - 1]?.toUpperCase()] ?? 1;
-
- return Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier;
-}
diff --git a/packages/adapter-bun/src/utils.spec.ts b/packages/adapter-bun/src/utils.spec.ts
deleted file mode 100644
index 0a6904f00cdd..000000000000
--- a/packages/adapter-bun/src/utils.spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { describe, expect, test } from 'vitest';
-import { parse_as_bytes } from './utils.js';
-
-describe('parse_as_bytes', () => {
- test.each([
- ['200', 200],
- ['512K', 512 * 1024],
- ['200M', 200 * 1024 * 1024],
- ['1G', 1024 * 1024 * 1024],
- ['asdf', NaN]
- ] as const)('parses %s', (input, expected) => {
- expect(parse_as_bytes(input)).toBe(expected);
- });
-});
From 2a62fc81f33df773f44d1489c24158438c311268 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 13:41:21 +0300
Subject: [PATCH 15/94] feat(adapter-bun): improve graceful shutdown handling
and remove forced shutdown logic
---
packages/adapter-bun/src/index.js | 17 ++++++-----------
1 file changed, 6 insertions(+), 11 deletions(-)
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 3034c2e8da36..e0b51f7118b5 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -39,26 +39,21 @@ export const server = Bun.serve(options);
console.log(unix ? `Listening on ${unix}` : `Listening on ${server.url}`);
-const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT') ?? 30;
let shutting_down = false;
/** @param {'SIGINT' | 'SIGTERM'} reason */
async function graceful_shutdown(reason) {
- if (shutting_down) return;
+ if (shutting_down) return process.exit(1);
shutting_down = true;
- let forced = false;
- const timeout = setTimeout(() => {
- forced = true;
- void server.stop(true);
- }, shutdown_timeout * 1000);
-
+ if (server.pendingRequests !== 0) {
+ console.log(`Waiting for ${server.pendingRequests} requests to finish before shutting down...`);
+ console.log('Press Ctrl+C again to force shutdown.');
+ }
await server.stop();
- clearTimeout(timeout);
+
// @ts-expect-error custom events cannot be typed
process.emit('sveltekit:shutdown', reason);
-
- if (forced) console.warn(`Forced shutdown after ${shutdown_timeout} seconds`);
}
process.on('SIGTERM', () => void graceful_shutdown('SIGTERM'));
From e8e95f2f64e1c7ccdb6f8e141527554ec7b8fadf Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 14:45:22 +0300
Subject: [PATCH 16/94] feat(adapter-bun): enhance error messages for protocol,
host, and port validation
---
packages/adapter-bun/src/handler.js | 26 +++++++++++++++++---------
1 file changed, 17 insertions(+), 9 deletions(-)
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index db4a68c002b4..2708945eaa6d 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -66,27 +66,30 @@ export async function handler(request, bun_server) {
*/
function get_origin(request, url) {
const protocol = decodeURIComponent(
- (protocol_header ? request.headers.get(protocol_header) : null) || url.protocol.slice(0, -1)
+ (protocol_header ? request.headers.get(protocol_header) : null) ?? url.protocol.slice(0, -1)
);
if (protocol.includes(':')) {
throw new Error(
- `The ${protocol_header} header specified ${protocol}, which is invalid because it includes \`:\``
+ `The ${protocol_header} header specified ${protocol} which is an invalid because it includes \`:\`. It should only contain the protocol scheme (e.g. \`https\`)`
);
}
const host =
- (host_header ? request.headers.get(host_header) : null) ||
- request.headers.get('host') ||
+ (host_header ? request.headers.get(host_header) : null) ??
+ request.headers.get('host') ??
url.host;
if (!host) {
+ const header_names = host_header ? `${host_header} or host headers` : 'host header';
throw new Error(
- `The request must include a ${host_header ? `${host_header} or host` : 'host'} header`
+ `Could not determine host. The request must have a value provided by the ${header_names}`
);
}
const port = port_header ? request.headers.get(port_header) : null;
- if (port && !/^\d+$/.test(port)) {
- throw new Error(`The ${port_header} header specified an invalid port: ${port}`);
+ if (port && isNaN(+port)) {
+ throw new Error(
+ `The ${port_header} header specified ${port} which is an invalid port because it is not a number. The value should only contain the port number (e.g. 443)`
+ );
}
const value = port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
@@ -103,15 +106,20 @@ function get_client_address(request, bun_server) {
const value = request.headers.get(address_header);
if (value === null) {
throw new Error(
- `Address header was specified with ${env_prefix + 'ADDRESS_HEADER'}=${address_header} but is absent from request`
+ `Address header was specified with ${env_prefix}ADDRESS_HEADER=${address_header} but is absent from request`
);
}
if (address_header === 'x-forwarded-for') {
const addresses = value.split(',');
+
+ if (xff_depth < 1) {
+ throw new Error(`${env_prefix}XFF_DEPTH must be a positive integer`);
+ }
+
if (xff_depth > addresses.length) {
throw new Error(
- `${env_prefix + 'XFF_DEPTH'} is ${xff_depth}, but only found ${addresses.length} addresses`
+ `${env_prefix}XFF_DEPTH is ${xff_depth}, but only found ${addresses.length} addresses`
);
}
return addresses[addresses.length - xff_depth].trim();
From b07bc2c1b8a238f33f7c1f3480668d98596b8730 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 14:46:15 +0300
Subject: [PATCH 17/94] feat(adapter-bun): remove unused 'SHUTDOWN_TIMEOUT'
from expected environment variables
---
packages/adapter-bun/src/env.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index bd0d76e902b4..064d3d1628df 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -13,8 +13,7 @@ const expected = new Set([
'ADDRESS_HEADER',
'PROTOCOL_HEADER',
'HOST_HEADER',
- 'PORT_HEADER',
- 'SHUTDOWN_TIMEOUT'
+ 'PORT_HEADER'
]);
export const env_prefix = ENV_PREFIX;
From d89764b73e480bbfe572946dce8ea5295c1f8758 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 15:41:28 +0300
Subject: [PATCH 18/94] feat(adapter-bun): optimize asset handling by removing
metadata generation and updating related tests
---
packages/adapter-bun/index.js | 25 +---
packages/adapter-bun/index.spec.ts | 24 +++-
packages/adapter-bun/src/assets.js | 15 +-
packages/adapter-bun/src/static.js | 128 ++----------------
.../adapter-bun/test/apps/basic/test/test.js | 52 ++++---
5 files changed, 71 insertions(+), 173 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index add8e22bacec..95ea4057ed1c 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -81,7 +81,7 @@ export default function (opts = {}) {
...prerendered_files.map((file) => `prerendered/${file}`)
];
const compile_file = `${out}/adapter-bun-compile.js`;
- virtual_files[compile_file] = await create_compile_entrypoint(
+ virtual_files[compile_file] = create_compile_entrypoint(
out,
assets,
`${entries}/index.js`,
@@ -249,34 +249,19 @@ function serialize(value) {
* @param {string[]} assets
* @param {string} entrypoint
* @param {string | undefined} instrumentation
- * @returns {Promise}
+ * @returns {string}
*/
-async function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
+function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
const unique_assets = [...new Set(assets)];
- const metadata = [];
- for (const file of unique_assets) {
- const source = Bun.file(resolve(out, file));
- const hash = new Bun.CryptoHasher('sha256').update(await source.arrayBuffer()).digest('hex');
- metadata.push({
- file,
- size: source.size,
- type: source.type,
- lastModified: new Date(source.lastModified).toUTCString(),
- etag: `"${hash}"`
- });
- }
const imports = unique_assets.map(
(file, index) =>
`import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
);
- const entries = metadata.map(({ file, ...metadata }, index) => [
- file,
- `{ path: asset_${index}, ...${JSON.stringify(metadata)} }`
- ]);
+ const entries = unique_assets.map((file, index) => [file, `asset_${index}`]);
return [
...imports,
`globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
- .map(([file, value]) => `[${JSON.stringify(file)}, ${value}]`)
+ .map(([file, identifier]) => `[${JSON.stringify(file)}, ${identifier}]`)
.join(',')}]);`,
instrumentation && `await import(${JSON.stringify(instrumentation)});`,
`await import(${JSON.stringify(entrypoint)});`
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 50c9e1c92901..4b0f85d520eb 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -68,6 +68,21 @@ describe('Bun build options', () => {
expect(options.entrypoints).toHaveLength(1);
});
+ test('embeds executable assets without generating metadata', async () => {
+ await adapter({ compile: true }).adapt(
+ builder({ client_files: ['data.json'], prerendered_files: ['prerendered/index.html'] })
+ );
+
+ const source = build.mock.calls[0][0].files['build/adapter-bun-compile.js'];
+ expect(source).toContain("with { type: 'file' }");
+ expect(source).toContain('["client/data.json", asset_0]');
+ expect(source).toContain('["prerendered/prerendered/index.html", asset_1]');
+ expect(source).not.toContain('{ path:');
+ expect(source).not.toContain('size');
+ expect(source).not.toContain('lastModified');
+ expect(source).not.toContain('etag');
+ });
+
test('preserves an explicit outdir for split executables', async () => {
await adapter({
compile: {
@@ -100,7 +115,10 @@ describe('Bun build options', () => {
});
});
-function builder() {
+function builder({
+ client_files = [],
+ prerendered_files = []
+}: { client_files?: string[]; prerendered_files?: string[] } = {}) {
return {
config: { kit: { paths: { base: '', origin: undefined } } },
prerendered: { paths: [] },
@@ -109,8 +127,8 @@ function builder() {
getServerDirectory: () => '.svelte-kit/output/server',
rimraf() {},
mkdirp() {},
- writeClient: () => [],
- writePrerendered: () => [],
+ writeClient: () => client_files,
+ writePrerendered: () => prerendered_files,
copy() {},
generateManifest: () => '{}',
hasServerInstrumentationFile: () => false
diff --git a/packages/adapter-bun/src/assets.js b/packages/adapter-bun/src/assets.js
index dcfde142977d..e25275da990e 100644
--- a/packages/adapter-bun/src/assets.js
+++ b/packages/adapter-bun/src/assets.js
@@ -1,32 +1,23 @@
import { join } from 'node:path';
import { dir } from './dir.js';
-const embedded_files = /** @type {Map | undefined} */ (
+const embedded_files = /** @type {Map | undefined} */ (
/** @type {any} */ (globalThis)[Symbol.for('sveltekit.adapter-bun.assets')]
);
-/**
- * @typedef {object} EmbeddedAsset
- * @property {string} path
- * @property {number} size
- * @property {string} type
- * @property {string} lastModified
- * @property {string} etag
- */
-
/**
* @param {'client' | 'prerendered'} directory
* @param {string} relative
* @returns {string}
*/
export function asset_path(directory, relative) {
- return embedded_asset(directory, relative)?.path ?? join(dir, directory, relative);
+ return embedded_asset(directory, relative) ?? join(dir, directory, relative);
}
/**
* @param {'client' | 'prerendered'} directory
* @param {string} relative
- * @returns {EmbeddedAsset | undefined}
+ * @returns {string | undefined}
*/
export function embedded_asset(directory, relative) {
return embedded_files?.get(`${directory}/${relative}`);
diff --git a/packages/adapter-bun/src/static.js b/packages/adapter-bun/src/static.js
index 8cf287a1354f..d8522f761bf0 100644
--- a/packages/adapter-bun/src/static.js
+++ b/packages/adapter-bun/src/static.js
@@ -38,130 +38,20 @@ function encode_pathname(pathname) {
* @param {'client' | 'prerendered'} directory
* @param {string} relative
* @param {boolean} client
- * @returns {Partial Response)>>}
+ * @returns {Bun.BunFile | Response}
*/
function file_route(directory, relative, client) {
- const headers = new Headers();
- if (client && relative.startsWith(`${manifest.appPath}/immutable/`)) {
- headers.set('cache-control', 'public,max-age=31536000,immutable');
- }
-
const embedded = embedded_asset(directory, relative);
- if (embedded) {
- return {
- GET: (request) => embedded_file_response(request, embedded, headers, false),
- HEAD: (request) => embedded_file_response(request, embedded, headers, true)
- };
- }
-
- const path = asset_path(directory, relative);
- const file = Bun.file(path);
- return {
- GET: new Response(file, { headers }),
- HEAD: new Response(file, { headers })
- };
-}
-
-/**
- * @param {Request} request
- * @param {import('./assets.js').EmbeddedAsset} asset
- * @param {Headers} route_headers
- * @param {boolean} head
- */
-function embedded_file_response(request, asset, route_headers, head) {
- const headers = new Headers(route_headers);
- headers.set('accept-ranges', 'bytes');
- headers.set('content-length', String(asset.size));
- if (asset.type) headers.set('content-type', asset.type);
- headers.set('etag', asset.etag);
- headers.set('last-modified', asset.lastModified);
-
- if (is_not_modified(request, asset)) {
- headers.delete('content-length');
- return new Response(null, { status: 304, headers });
- }
-
- const range = get_range(request, asset);
- if (range === null) {
- headers.set('content-range', `bytes */${asset.size}`);
- headers.set('content-length', '0');
- return new Response(null, { status: 416, headers });
- }
-
- const file = Bun.file(asset.path);
- if (range) {
- const [start, end] = range;
- headers.set('content-range', `bytes ${start}-${end}/${asset.size}`);
- headers.set('content-length', String(end - start + 1));
- return new Response(head ? null : file.slice(start, end + 1), { status: 206, headers });
- }
-
- return new Response(head ? null : file, { headers });
-}
+ const file = Bun.file(embedded ?? asset_path(directory, relative));
+ const immutable = client && relative.startsWith(`${manifest.appPath}/immutable/`);
+ if (!embedded && !immutable) return file;
-/**
- * @param {Request} request
- * @param {import('./assets.js').EmbeddedAsset} asset
- */
-function is_not_modified(request, asset) {
- const if_none_match = request.headers.get('if-none-match');
- if (if_none_match !== null) {
- return if_none_match.split(',').some((value) => {
- const tag = value.trim();
- return tag === '*' || tag === asset.etag || tag.replace(/^W\//, '') === asset.etag;
- });
- }
-
- const if_modified_since = request.headers.get('if-modified-since');
- if (if_modified_since === null) return false;
- const modified_since = Date.parse(if_modified_since);
- return Number.isFinite(modified_since) && modified_since >= Date.parse(asset.lastModified);
-}
-
-/**
- * @param {Request} request
- * @param {import('./assets.js').EmbeddedAsset} asset
- * @returns {[number, number] | null | undefined}
- */
-function get_range(request, asset) {
- const value = request.headers.get('range');
- if (value === null) return;
-
- const if_range = request.headers.get('if-range');
- if (if_range !== null) {
- const date = Date.parse(if_range);
- if (
- if_range !== asset.etag &&
- (!Number.isFinite(date) || date < Date.parse(asset.lastModified))
- ) {
- return;
- }
- }
-
- const match = /^bytes=(\d*)-(\d*)$/.exec(value);
- if (!match || (!match[1] && !match[2]) || asset.size === 0) return null;
-
- let start;
- let end;
- if (match[1]) {
- start = Number(match[1]);
- end = match[2] ? Number(match[2]) : asset.size - 1;
- } else {
- const length = Number(match[2]);
- if (length === 0) return null;
- start = Math.max(0, asset.size - length);
- end = asset.size - 1;
- }
-
- if (
- !Number.isSafeInteger(start) ||
- !Number.isSafeInteger(end) ||
- start >= asset.size ||
- end < start
- ) {
- return null;
+ const headers = new Headers();
+ if (embedded) headers.set('content-type', Bun.file(relative).type);
+ if (immutable) {
+ headers.set('cache-control', 'public,max-age=31536000,immutable');
}
- return [start, Math.min(end, asset.size - 1)];
+ return new Response(file, { headers });
}
/** @type {import('bun').Serve.Routes} */
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 020663760c8b..c5c09d1d37ee 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -1,4 +1,7 @@
import { expect, test } from '@playwright/test';
+import process from 'node:process';
+
+const compiled = process.env.COMPILE === 'true';
test('renders and hydrates the app', async ({ page }) => {
await page.goto('/');
@@ -33,24 +36,35 @@ test('serves static files with Bun file responses', async ({ request }) => {
expect(await head.text()).toBe('');
});
-test('supports ranges and conditional requests for static files', async ({ request }) => {
+test('uses Bun validators and ranges for static files', async ({ request }) => {
const initial = await request.get('/data.json');
- const last_modified = initial.headers()['last-modified'];
const body = await initial.text();
- expect(last_modified).toBeTruthy();
-
- const not_modified_since = await request.get('/data.json', {
- headers: { 'if-modified-since': last_modified }
- });
- expect(not_modified_since.status()).toBe(304);
-
- const range = await request.get('/data.json', {
- headers: { range: 'bytes=0-3' }
- });
- expect(range.status()).toBe(206);
- expect(range.headers()['accept-ranges']).toBe('bytes');
- expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`);
- expect(await range.text()).toBe(body.slice(0, 4));
+
+ if (compiled) {
+ const etag = initial.headers()['etag'];
+ expect(etag).toBeTruthy();
+
+ const not_modified = await request.get('/data.json', {
+ headers: { 'if-none-match': etag }
+ });
+ expect(not_modified.status()).toBe(304);
+ } else {
+ const last_modified = initial.headers()['last-modified'];
+ expect(last_modified).toBeTruthy();
+
+ const not_modified = await request.get('/data.json', {
+ headers: { 'if-modified-since': last_modified }
+ });
+ expect(not_modified.status()).toBe(304);
+
+ const range = await request.get('/data.json', {
+ headers: { range: 'bytes=0-3' }
+ });
+ expect(range.status()).toBe(206);
+ expect(range.headers()['accept-ranges']).toBe('bytes');
+ expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`);
+ expect(await range.text()).toBe(body.slice(0, 4));
+ }
});
test('serves URL-encoded static filenames', async ({ request }) => {
@@ -68,10 +82,10 @@ test('caches immutable client assets', async ({ request }) => {
expect(asset_response.headers()['cache-control']).toBe('public,max-age=31536000,immutable');
});
-test('does not serve static files for non-GET requests', async ({ request }) => {
+test('uses Bun route method semantics for static files', async ({ request }) => {
const response = await request.post('/data.json');
- expect(response.status()).not.toBe(200);
- expect(await response.text()).not.toContain('hello from a static file');
+ expect(response.status()).toBe(200);
+ expect(await response.json()).toEqual({ message: 'hello from a static file' });
});
test('redirects prerendered paths to their canonical trailing slash', async ({ request }) => {
From 7f67d8e8e636a1beaacb394f4b25e5994aa7f8a5 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 16:57:14 +0300
Subject: [PATCH 19/94] feat(adapter-bun): enhance routing and asset handling
by adding routes module and removing unused assets
---
packages/adapter-bun/build.js | 2 +-
packages/adapter-bun/index.js | 189 ++++++++++++++++++++++++----
packages/adapter-bun/index.spec.ts | 64 ++++++++--
packages/adapter-bun/internal.d.ts | 8 +-
packages/adapter-bun/src/assets.js | 24 ----
packages/adapter-bun/src/handler.js | 4 +-
packages/adapter-bun/src/index.js | 2 +-
packages/adapter-bun/src/static.js | 86 -------------
8 files changed, 229 insertions(+), 150 deletions(-)
delete mode 100644 packages/adapter-bun/src/assets.js
delete mode 100644 packages/adapter-bun/src/static.js
diff --git a/packages/adapter-bun/build.js b/packages/adapter-bun/build.js
index 3213ca5a8f40..9aa4a7a61800 100644
--- a/packages/adapter-bun/build.js
+++ b/packages/adapter-bun/build.js
@@ -13,7 +13,7 @@ const result = await Bun.build({
chunk: 'chunks/[name]-[hash].[ext]'
},
// resolved at adapt time
- external: ['MANIFEST', 'SERVER', 'SERVER_OPTIONS']
+ external: ['MANIFEST', 'ROUTES', 'SERVER', 'SERVER_OPTIONS']
});
if (!result.success) {
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 95ea4057ed1c..5a178ccb2df8 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -40,21 +40,26 @@ export default function (opts = {}) {
const dir_id = `${entries}/dir.js`;
const manifest_file = `${server}/adapter-bun-manifest.js`;
+ const routes_file = `${server}/adapter-bun-routes.js`;
const server_options_file = `${server}/adapter-bun-options.js`;
const instrumentation = builder.hasServerInstrumentationFile()
? `${server}/instrumentation.server.js`
: undefined;
const virtual_files = {
- [manifest_file]: [
- `export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
- `export const client_files = new Set(${JSON.stringify(client_files)});`,
- `export const prerendered_files = new Set(${JSON.stringify(prerendered_files)});`,
- `export const prerendered_paths = new Set(${JSON.stringify(builder.prerendered.paths)});`
- ].join('\n\n'),
+ [manifest_file]: `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n`,
[server_options_file]: `export default ${serialize(serverOptions)};\n`
};
const compile_options = normalize_compile_options(compile, out);
+ virtual_files[routes_file] = create_routes({
+ out,
+ client_files,
+ prerendered_files,
+ prerendered_paths: builder.prerendered.paths,
+ app_path: builder.getAppPath(),
+ dir_id,
+ embed: compile_options !== undefined
+ });
const entrypoints = [`${entries}/index.js`, `${entries}/handler.js`, dir_id];
/** @type {Omit} */
let build_options = {
@@ -76,14 +81,8 @@ export default function (opts = {}) {
};
if (compile_options) {
- const assets = [
- ...client_files.map((file) => `client/${file}`),
- ...prerendered_files.map((file) => `prerendered/${file}`)
- ];
const compile_file = `${out}/adapter-bun-compile.js`;
virtual_files[compile_file] = create_compile_entrypoint(
- out,
- assets,
`${entries}/index.js`,
instrumentation
);
@@ -97,9 +96,10 @@ export default function (opts = {}) {
name: 'adapter-bun',
/** @param {import('bun').PluginBuilder} build */
setup(build) {
- build.onResolve({ filter: /^(SERVER|MANIFEST|SERVER_OPTIONS)$/ }, ({ path }) => {
+ build.onResolve({ filter: /^(SERVER|MANIFEST|ROUTES|SERVER_OPTIONS)$/ }, ({ path }) => {
if (path === 'SERVER') return { path: `${server}/index.js` };
if (path === 'MANIFEST') return { path: manifest_file };
+ if (path === 'ROUTES') return { path: routes_file };
if (path === 'SERVER_OPTIONS') return { path: server_options_file };
});
@@ -245,24 +245,12 @@ function serialize(value) {
}
/**
- * @param {string} out
- * @param {string[]} assets
* @param {string} entrypoint
* @param {string | undefined} instrumentation
* @returns {string}
*/
-function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
- const unique_assets = [...new Set(assets)];
- const imports = unique_assets.map(
- (file, index) =>
- `import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
- );
- const entries = unique_assets.map((file, index) => [file, `asset_${index}`]);
+function create_compile_entrypoint(entrypoint, instrumentation) {
return [
- ...imports,
- `globalThis[Symbol.for('sveltekit.adapter-bun.assets')] = new Map([${entries
- .map(([file, identifier]) => `[${JSON.stringify(file)}, ${identifier}]`)
- .join(',')}]);`,
instrumentation && `await import(${JSON.stringify(instrumentation)});`,
`await import(${JSON.stringify(entrypoint)});`
]
@@ -270,6 +258,155 @@ function create_compile_entrypoint(out, assets, entrypoint, instrumentation) {
.join('\n');
}
+/**
+ * @param {object} options
+ * @param {string} options.out
+ * @param {string[]} options.client_files
+ * @param {string[]} options.prerendered_files
+ * @param {string[]} options.prerendered_paths
+ * @param {string} options.app_path
+ * @param {string} options.dir_id
+ * @param {boolean} options.embed
+ * @returns {string}
+ */
+function create_routes({
+ out,
+ client_files,
+ prerendered_files,
+ prerendered_paths,
+ app_path,
+ dir_id,
+ embed
+}) {
+ /** @type {Map} */
+ const routes = new Map();
+ const prerendered_file_set = new Set(prerendered_files);
+
+ for (const file of client_files) {
+ routes.set(encode_pathname(`/${file}`), {
+ asset: `client/${file}`,
+ immutable: file.startsWith(`${app_path}/immutable/`)
+ });
+ }
+
+ for (const pathname of prerendered_paths) {
+ const file = find_prerendered_file(pathname, prerendered_file_set);
+ const route = encode_pathname(pathname);
+ if (file && !routes.has(route)) {
+ routes.set(route, { asset: `prerendered/${file}`, immutable: false });
+ }
+ }
+
+ for (const pathname of prerendered_paths) {
+ const inverted = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
+ if (!inverted) continue;
+
+ const route = encode_pathname(inverted);
+ if (routes.has(route)) continue;
+ routes.set(route, {
+ location: relative_pathname(route, encode_pathname(pathname))
+ });
+ }
+
+ const assets = [
+ ...client_files.map((file) => `client/${file}`),
+ ...prerendered_files.map((file) => `prerendered/${file}`)
+ ];
+ const unique_assets = [...new Set(assets)];
+ const identifiers = new Map(unique_assets.map((file, index) => [file, `asset_${index}`]));
+ const imports = embed
+ ? unique_assets.map(
+ (file, index) =>
+ `import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
+ )
+ : [
+ `import { join } from 'node:path';`,
+ `import { dir } from ${JSON.stringify(posixify(dir_id))};`
+ ];
+ const asset_entries = unique_assets.map(
+ (file) => `[${JSON.stringify(file)}, ${identifiers.get(file)}]`
+ );
+ const asset_path = embed
+ ? [
+ `const assets = new Map([${asset_entries.join(',')}]);`,
+ `export const asset_path = (file) => assets.get(file);`
+ ]
+ : [`export const asset_path = (file) => join(dir, file);`];
+ const declarations = [];
+ const entries = [];
+ let redirect_index = 0;
+
+ for (const [route, value] of routes) {
+ if ('asset' in value) {
+ const identifier = identifiers.get(value.asset);
+ const file = embed ? identifier : `asset_path(${JSON.stringify(value.asset)})`;
+ const response = `Bun.file(${file})`;
+ if (!embed && !value.immutable) {
+ entries.push(`${JSON.stringify(route)}: ${response}`);
+ continue;
+ }
+
+ /** @type {Record} */
+ const headers = {};
+ if (embed) headers['content-type'] = Bun.file(value.asset).type;
+ if (value.immutable) {
+ headers['cache-control'] = 'public,max-age=31536000,immutable';
+ }
+ entries.push(
+ `${JSON.stringify(route)}: new Response(${response}, { headers: ${JSON.stringify(headers)} })`
+ );
+ continue;
+ }
+
+ const identifier = `redirect_${redirect_index++}`;
+ declarations.push(
+ `const ${identifier} = (request) => new Response(null, { status: 308, headers: { location: ${JSON.stringify(value.location)} + new URL(request.url).search } });`
+ );
+ entries.push(`${JSON.stringify(route)}: { GET: ${identifier}, HEAD: ${identifier} }`);
+ }
+
+ return [
+ ...imports,
+ ...asset_path,
+ ...declarations,
+ `export const routes = {${entries.join(',')}};`
+ ].join('\n');
+}
+
+/**
+ * @param {string} pathname
+ * @param {Set} prerendered_files
+ * @returns {string | undefined}
+ */
+function find_prerendered_file(pathname, prerendered_files) {
+ const relative = pathname.slice(1);
+ return (
+ relative.endsWith('/')
+ ? [`${relative}index.html`]
+ : [relative, `${relative}.html`, `${relative}/index.html`]
+ ).find((candidate) => prerendered_files.has(candidate));
+}
+
+/**
+ * Relative reference from `from` to `to`, which must differ only by a trailing slash.
+ * Keep in sync with the copy in `packages/kit/src/utils/url.js`.
+ * @param {string} from
+ * @param {string} to
+ * @returns {string}
+ */
+function relative_pathname(from, to) {
+ const segment = to.replace(/\/$/, '').split('/').at(-1);
+ return from.endsWith('/') ? `../${segment}` : `${segment}/`;
+}
+
+/**
+ * @param {string} pathname
+ * @returns {string}
+ */
+function encode_pathname(pathname) {
+ return pathname.split('/').map(encodeURIComponent).join('/');
+}
+
/** @param {string} str */
function posixify(str) {
return str.replace(/\\/g, '/');
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 4b0f85d520eb..73e5b284237c 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -3,7 +3,14 @@ import adapter from './index.js';
const { build, file } = vi.hoisted(() => {
const build = vi.fn(async (_options: any) => ({ success: true, logs: [], outputs: [] }));
- const file = vi.fn(() => ({ json: async () => ({ dependencies: { dependency: '1.0.0' } }) }));
+ const file = vi.fn((path: string) => ({
+ json: async () => ({ dependencies: { dependency: '1.0.0' } }),
+ type: path.endsWith('.html')
+ ? 'text/html;charset=utf-8'
+ : path.endsWith('.json')
+ ? 'application/json;charset=utf-8'
+ : 'text/plain;charset=utf-8'
+ }));
vi.stubGlobal('Bun', { build, file });
return { build, file };
});
@@ -29,6 +36,29 @@ describe('Bun build options', () => {
expect(options.plugins[0].name).toBe('adapter-bun');
});
+ test('generates the route map at build time', async () => {
+ await adapter().adapt(
+ builder({
+ client_files: ['data.json', 'encoded name.txt', '_app/immutable/app.js', 'prerendered'],
+ prerendered_files: ['prerendered/index.html', 'other/index.html'],
+ prerendered_paths: ['/prerendered/', '/other/']
+ })
+ );
+
+ const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ expect(source).toContain('"/data.json": Bun.file(asset_path("client/data.json"))');
+ expect(source).toContain('"/encoded%20name.txt"');
+ expect(source).toContain('public,max-age=31536000,immutable');
+ expect(source).toContain('"/prerendered": Bun.file(asset_path("client/prerendered"))');
+ expect(source).toContain('"/prerendered/": Bun.file');
+ expect(source).toContain('"/other/": Bun.file');
+ expect(source).toContain('"/other": { GET: redirect_0, HEAD: redirect_0 }');
+ expect(source.match(/const redirect_/g)).toHaveLength(1);
+ expect(source).not.toContain('client_files');
+ expect(source).not.toContain('prerendered_files');
+ expect(source).not.toContain('for (');
+ });
+
test('normalizes executable output and composes user configuration safely', async () => {
const user_plugin = { name: 'user-plugin', setup() {} };
await adapter({
@@ -38,7 +68,8 @@ describe('Bun build options', () => {
conditions: ['custom', 'bun'],
files: {
'virtual:user': 'export default true',
- '.svelte-kit/output/server/adapter-bun-manifest.js': 'invalid manifest'
+ '.svelte-kit/output/server/adapter-bun-manifest.js': 'invalid manifest',
+ '.svelte-kit/output/server/adapter-bun-routes.js': 'invalid routes'
},
plugins: [user_plugin],
minify: true,
@@ -58,6 +89,9 @@ describe('Bun build options', () => {
expect(options.files['.svelte-kit/output/server/adapter-bun-manifest.js']).not.toBe(
'invalid manifest'
);
+ expect(options.files['.svelte-kit/output/server/adapter-bun-routes.js']).not.toBe(
+ 'invalid routes'
+ );
expect(options).toMatchObject({
target: 'bun',
format: 'esm',
@@ -68,19 +102,27 @@ describe('Bun build options', () => {
expect(options.entrypoints).toHaveLength(1);
});
- test('embeds executable assets without generating metadata', async () => {
+ test('embeds executable assets through the generated route map', async () => {
await adapter({ compile: true }).adapt(
builder({ client_files: ['data.json'], prerendered_files: ['prerendered/index.html'] })
);
- const source = build.mock.calls[0][0].files['build/adapter-bun-compile.js'];
+ const options = build.mock.calls[0][0];
+ const source = options.files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ const entrypoint = options.files['build/adapter-bun-compile.js'];
expect(source).toContain("with { type: 'file' }");
expect(source).toContain('["client/data.json", asset_0]');
expect(source).toContain('["prerendered/prerendered/index.html", asset_1]');
+ expect(source).toContain(
+ '"/data.json": new Response(Bun.file(asset_0), { headers: {"content-type":"application/json;charset=utf-8"} })'
+ );
+ expect(entrypoint).not.toContain("with { type: 'file' }");
+ expect(entrypoint).not.toContain('sveltekit.adapter-bun.assets');
expect(source).not.toContain('{ path:');
expect(source).not.toContain('size');
expect(source).not.toContain('lastModified');
expect(source).not.toContain('etag');
+ expect(source).not.toContain('sveltekit.adapter-bun.assets');
});
test('preserves an explicit outdir for split executables', async () => {
@@ -117,11 +159,18 @@ describe('Bun build options', () => {
function builder({
client_files = [],
- prerendered_files = []
-}: { client_files?: string[]; prerendered_files?: string[] } = {}) {
+ prerendered_files = [],
+ prerendered_paths = [],
+ app_path = '_app'
+}: {
+ client_files?: string[];
+ prerendered_files?: string[];
+ prerendered_paths?: string[];
+ app_path?: string;
+} = {}) {
return {
config: { kit: { paths: { base: '', origin: undefined } } },
- prerendered: { paths: [] },
+ prerendered: { paths: prerendered_paths },
log: { minor() {} },
getBuildDirectory: () => '.svelte-kit/adapter-bun',
getServerDirectory: () => '.svelte-kit/output/server',
@@ -131,6 +180,7 @@ function builder({
writePrerendered: () => prerendered_files,
copy() {},
generateManifest: () => '{}',
+ getAppPath: () => app_path,
hasServerInstrumentationFile: () => false
} as any;
}
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index f82ac60edd80..6c2dc0440f3c 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -1,10 +1,12 @@
declare module 'MANIFEST' {
import type { SSRManifest } from '@sveltejs/kit';
- export const client_files: Set;
export const manifest: SSRManifest;
- export const prerendered_files: Set;
- export const prerendered_paths: Set;
+}
+
+declare module 'ROUTES' {
+ export function asset_path(file: string): string;
+ export const routes: import('bun').Serve.Routes;
}
declare module 'SERVER' {
diff --git a/packages/adapter-bun/src/assets.js b/packages/adapter-bun/src/assets.js
deleted file mode 100644
index e25275da990e..000000000000
--- a/packages/adapter-bun/src/assets.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { join } from 'node:path';
-import { dir } from './dir.js';
-
-const embedded_files = /** @type {Map | undefined} */ (
- /** @type {any} */ (globalThis)[Symbol.for('sveltekit.adapter-bun.assets')]
-);
-
-/**
- * @param {'client' | 'prerendered'} directory
- * @param {string} relative
- * @returns {string}
- */
-export function asset_path(directory, relative) {
- return embedded_asset(directory, relative) ?? join(dir, directory, relative);
-}
-
-/**
- * @param {'client' | 'prerendered'} directory
- * @param {string} relative
- * @returns {string | undefined}
- */
-export function embedded_asset(directory, relative) {
- return embedded_files?.get(`${directory}/${relative}`);
-}
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 2708945eaa6d..9cd44f17da88 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -1,6 +1,6 @@
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';
-import { asset_path } from './assets.js';
+import { asset_path } from 'ROUTES';
import { env, env_prefix, number_env } from './env.js';
const server = new Server(manifest);
@@ -14,7 +14,7 @@ const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 }) ?? 1;
await server.init({
env: Bun.env,
- read: (file) => Bun.file(asset_path('client', file)).stream()
+ read: (file) => Bun.file(asset_path(`client/${file}`)).stream()
});
/**
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index e0b51f7118b5..ba95ed74123e 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -1,8 +1,8 @@
import process from 'node:process';
import server_options from 'SERVER_OPTIONS';
+import { routes } from 'ROUTES';
import { handler } from './handler.js';
import { boolean_env, env, number_env } from './env.js';
-import { routes } from './static.js';
const options = /** @type {import('bun').Serve.Options} */ ({ ...server_options });
diff --git a/packages/adapter-bun/src/static.js b/packages/adapter-bun/src/static.js
deleted file mode 100644
index d8522f761bf0..000000000000
--- a/packages/adapter-bun/src/static.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { client_files, manifest, prerendered_files, prerendered_paths } from 'MANIFEST';
-import { asset_path, embedded_asset } from './assets.js';
-
-/**
- * @param {string} pathname
- * @returns {string | undefined}
- */
-function find_prerendered_file(pathname) {
- const relative = pathname.slice(1);
- return (
- relative.endsWith('/')
- ? [`${relative}index.html`]
- : [relative, `${relative}.html`, `${relative}/index.html`]
- ).find((candidate) => prerendered_files.has(candidate));
-}
-
-/**
- * Relative reference from `from` to `to`, which must differ only by a trailing slash.
- * Keep in sync with the copy in `packages/kit/src/utils/url.js`.
- * @param {string} from
- * @param {string} to
- * @returns {string}
- */
-function relative_pathname(from, to) {
- const segment = to.replace(/\/$/, '').split('/').at(-1);
- return from.endsWith('/') ? `../${segment}` : `${segment}/`;
-}
-
-/**
- * @param {string} pathname
- * @returns {string}
- */
-function encode_pathname(pathname) {
- return pathname.split('/').map(encodeURIComponent).join('/');
-}
-
-/**
- * @param {'client' | 'prerendered'} directory
- * @param {string} relative
- * @param {boolean} client
- * @returns {Bun.BunFile | Response}
- */
-function file_route(directory, relative, client) {
- const embedded = embedded_asset(directory, relative);
- const file = Bun.file(embedded ?? asset_path(directory, relative));
- const immutable = client && relative.startsWith(`${manifest.appPath}/immutable/`);
- if (!embedded && !immutable) return file;
-
- const headers = new Headers();
- if (embedded) headers.set('content-type', Bun.file(relative).type);
- if (immutable) {
- headers.set('cache-control', 'public,max-age=31536000,immutable');
- }
- return new Response(file, { headers });
-}
-
-/** @type {import('bun').Serve.Routes} */
-export const routes = {};
-
-for (const file of client_files) {
- routes[encode_pathname(`/${file}`)] = file_route('client', file, true);
-}
-
-for (const pathname of prerendered_paths) {
- const file = find_prerendered_file(pathname);
- const route = encode_pathname(pathname);
- if (file && routes[route] === undefined) {
- routes[route] = file_route('prerendered', file, false);
- }
-}
-
-for (const pathname of prerendered_paths) {
- const inverted = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
- if (!inverted) continue;
-
- const route = encode_pathname(inverted);
- if (routes[route] !== undefined) continue;
-
- const location = relative_pathname(route, encode_pathname(pathname));
- const redirect = (/** @type {Request} */ request) =>
- new Response(null, {
- status: 308,
- headers: { location: location + new URL(request.url).search }
- });
- routes[route] = { GET: redirect, HEAD: redirect };
-}
From bfda4183b60d7f2f758baded128cf274efb6e229 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 17:25:54 +0300
Subject: [PATCH 20/94] feat(adapter-bun): simplify route redirection handling
by removing unnecessary declarations
---
packages/adapter-bun/index.js | 15 +++------------
1 file changed, 3 insertions(+), 12 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 5a178ccb2df8..d814192bbe15 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -332,9 +332,7 @@ function create_routes({
`export const asset_path = (file) => assets.get(file);`
]
: [`export const asset_path = (file) => join(dir, file);`];
- const declarations = [];
const entries = [];
- let redirect_index = 0;
for (const [route, value] of routes) {
if ('asset' in value) {
@@ -358,19 +356,12 @@ function create_routes({
continue;
}
- const identifier = `redirect_${redirect_index++}`;
- declarations.push(
- `const ${identifier} = (request) => new Response(null, { status: 308, headers: { location: ${JSON.stringify(value.location)} + new URL(request.url).search } });`
+ entries.push(
+ `${JSON.stringify(route)}: Response.redirect(${JSON.stringify(value.location)}, 308)`
);
- entries.push(`${JSON.stringify(route)}: { GET: ${identifier}, HEAD: ${identifier} }`);
}
- return [
- ...imports,
- ...asset_path,
- ...declarations,
- `export const routes = {${entries.join(',')}};`
- ].join('\n');
+ return [...imports, ...asset_path, `export const routes = {${entries.join(',\n')}};`].join('\n');
}
/**
From 1f86d75f08404d068d1c67626adfee195c1a25ca Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Thu, 6 Aug 2026 18:46:44 +0300
Subject: [PATCH 21/94] feat(adapter-bun): update serverOptions handling to
ensure JSON serialization and improve type definitions
---
packages/adapter-bun/index.d.ts | 19 +++++++++++--------
packages/adapter-bun/index.js | 26 +-------------------------
packages/adapter-bun/internal.d.ts | 6 ++++--
3 files changed, 16 insertions(+), 35 deletions(-)
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index f4f2dd0ffdd6..5c1d9ed8ea4d 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -7,13 +7,6 @@ declare global {
const ORIGIN: string | undefined;
}
-type ServerOptions = Omit<
- Serve.BaseServeOptions & Serve.HostnamePortServeOptions,
- 'fetch' | 'routes' | 'websocket' | 'error' | 'tls' | 'http3' | 'http1'
-> & {
- unix?: string;
-};
-
type CompileOptions = Omit & {
compile: Exclude, false>;
};
@@ -45,7 +38,17 @@ interface AdapterOptions {
* The options must be JSON-serializable. Use `build/handler.js` with a custom
* `Bun.serve` call for routes, WebSockets, or custom error handling.
*/
- serverOptions?: ServerOptions;
+ serverOptions?: Pick<
+ Serve.Options,
+ | 'development'
+ | 'hostname'
+ | 'port'
+ | 'idleTimeout'
+ | 'maxRequestBodySize'
+ | 'reusePort'
+ | 'unix'
+ | 'ipv6Only'
+ >;
/**
* Compile the build into a single executable containing the server and static assets.
* Pass Bun build options directly for advanced configuration. The generated entrypoint,
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index d814192bbe15..811f586c5474 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -47,7 +47,7 @@ export default function (opts = {}) {
: undefined;
const virtual_files = {
[manifest_file]: `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n`,
- [server_options_file]: `export default ${serialize(serverOptions)};\n`
+ [server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`
};
const compile_options = normalize_compile_options(compile, out);
@@ -220,30 +220,6 @@ function with_base(files, base) {
return files.map((file) => posixify(prefix ? `${prefix}/${file}` : file));
}
-/**
- * @param {unknown} value
- * @returns {string}
- */
-function serialize(value) {
- try {
- const serialized = JSON.stringify(value, (_key, item) => {
- if (typeof item === 'function' || typeof item === 'symbol' || typeof item === 'bigint') {
- throw new TypeError(`serverOptions must be JSON-serializable, received ${typeof item}`);
- }
- if (typeof item === 'number' && !Number.isFinite(item)) {
- throw new TypeError('serverOptions must contain only finite numbers');
- }
- return item;
- });
- if (serialized === undefined) {
- throw new TypeError('serverOptions must be a JSON-serializable object');
- }
- return serialized;
- } catch (error) {
- throw new Error('Could not serialize adapter-bun serverOptions', { cause: error });
- }
-}
-
/**
* @param {string} entrypoint
* @param {string | undefined} instrumentation
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 6c2dc0440f3c..00753efefe5d 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -1,3 +1,5 @@
+import type { Serve } from 'bun';
+
declare module 'MANIFEST' {
import type { SSRManifest } from '@sveltejs/kit';
@@ -6,7 +8,7 @@ declare module 'MANIFEST' {
declare module 'ROUTES' {
export function asset_path(file: string): string;
- export const routes: import('bun').Serve.Routes;
+ export const routes: Serve.Routes;
}
declare module 'SERVER' {
@@ -15,7 +17,7 @@ declare module 'SERVER' {
declare module 'SERVER_OPTIONS' {
const options: Pick<
- import('bun').Serve.Options,
+ Serve.Options,
| 'development'
| 'hostname'
| 'port'
From 73ce708a2fb72fbb3571d33324555f4cb2bedfd9 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Fri, 7 Aug 2026 01:31:53 +0300
Subject: [PATCH 22/94] feat(adapter-bun): simplify entrypoints and remove
unnecessary build options
---
packages/adapter-bun/build.js | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/packages/adapter-bun/build.js b/packages/adapter-bun/build.js
index 9aa4a7a61800..b54bbb342a19 100644
--- a/packages/adapter-bun/build.js
+++ b/packages/adapter-bun/build.js
@@ -3,15 +3,10 @@ import { rmSync } from 'node:fs';
rmSync('files', { recursive: true, force: true });
const result = await Bun.build({
- entrypoints: ['src/index.js', 'src/handler.js', 'src/dir.js'],
+ entrypoints: ['src/index.js'],
outdir: 'files',
target: 'bun',
format: 'esm',
- splitting: true,
- naming: {
- entry: '[name].[ext]',
- chunk: 'chunks/[name]-[hash].[ext]'
- },
// resolved at adapt time
external: ['MANIFEST', 'ROUTES', 'SERVER', 'SERVER_OPTIONS']
});
From 06b69c43de70261d4585f02023d79a268f658cd1 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 02:08:55 +0300
Subject: [PATCH 23/94] feat(adapter-bun): remove build script and streamline
type definitions for Bun integration
---
packages/adapter-bun/build.js | 16 -
packages/adapter-bun/index.d.ts | 23 +-
packages/adapter-bun/index.js | 467 ++++++++++-------------------
packages/adapter-bun/internal.d.ts | 4 +-
packages/adapter-bun/src/dir.js | 4 -
packages/adapter-bun/tsconfig.json | 1 -
6 files changed, 181 insertions(+), 334 deletions(-)
delete mode 100644 packages/adapter-bun/build.js
delete mode 100644 packages/adapter-bun/src/dir.js
diff --git a/packages/adapter-bun/build.js b/packages/adapter-bun/build.js
deleted file mode 100644
index b54bbb342a19..000000000000
--- a/packages/adapter-bun/build.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { rmSync } from 'node:fs';
-
-rmSync('files', { recursive: true, force: true });
-
-const result = await Bun.build({
- entrypoints: ['src/index.js'],
- outdir: 'files',
- target: 'bun',
- format: 'esm',
- // resolved at adapt time
- external: ['MANIFEST', 'ROUTES', 'SERVER', 'SERVER_OPTIONS']
-});
-
-if (!result.success) {
- throw new AggregateError(result.logs, 'Could not build adapter-bun');
-}
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 5c1d9ed8ea4d..5a8e9a4d279a 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -1,5 +1,4 @@
import type { Adapter } from '@sveltejs/kit';
-import type { BuildConfig, Serve } from 'bun';
import './ambient.js';
declare global {
@@ -7,10 +6,6 @@ declare global {
const ORIGIN: string | undefined;
}
-type CompileOptions = Omit & {
- compile: Exclude, false>;
-};
-
interface AdapterOptions {
/**
* The directory to build the server to.
@@ -39,7 +34,7 @@ interface AdapterOptions {
* `Bun.serve` call for routes, WebSockets, or custom error handling.
*/
serverOptions?: Pick<
- Serve.Options,
+ import('bun').Serve.Options,
| 'development'
| 'hostname'
| 'port'
@@ -50,13 +45,25 @@ interface AdapterOptions {
| 'ipv6Only'
>;
/**
- * Compile the build into a single executable containing the server and static assets.
+ * Build the server and static assets.
* Pass Bun build options directly for advanced configuration. The generated entrypoint,
* top-level target, and module format are reserved. If neither an outfile nor outdir is
* specified, the executable is written to `/app`.
* @default false
*/
- compile?: boolean | CompileOptions;
+ buildOptions?: Pick<
+ import('bun').BuildConfig,
+ | 'splitting'
+ | 'sourcemap'
+ | 'minify'
+ | 'bytecode'
+ | 'banner'
+ | 'footer'
+ | 'drop'
+ | 'features'
+ | 'optimizeImports'
+ | 'compile'
+ >;
}
export default function plugin(options?: AdapterOptions): Adapter;
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 811f586c5474..237e56331531 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,10 +1,29 @@
-import { resolve } from 'node:path';
+import { resolve, posix } from 'node:path';
+import { readdir } from 'node:fs/promises';
-const files = resolve(import.meta.dirname, 'files');
+/**
+ * @param {string} path
+ * @returns {Promise<{abs: string, rel: string}[]>}
+ */
+async function read_files_recursive(path) {
+ try {
+ const entries = await readdir(path, { recursive: true, withFileTypes: true });
+ return entries
+ .filter((entry) => entry.isFile())
+ .map((entry) => {
+ const abs = resolve(entry.parentPath, entry.name);
+ const rel = posix.relative(path, abs);
+ return { abs, rel };
+ })
+ .filter(({ rel }) => rel.split('/').every((segment) => !segment.startsWith('.')));
+ } catch {
+ return [];
+ }
+}
/** @type {import('./index.js').default} */
export default function (opts = {}) {
- const { out = 'build', envPrefix = '', serverOptions = {}, compile = false } = opts;
+ const { out = 'build', envPrefix = '', serverOptions = {}, buildOptions = {} } = opts;
return {
name: '@sveltejs/adapter-bun',
@@ -15,86 +34,39 @@ export default function (opts = {}) {
);
}
- const tmp = builder.getBuildDirectory('adapter-bun');
- const base = builder.config.kit.paths.base;
-
builder.rimraf(out);
- builder.rimraf(tmp);
- builder.mkdirp(tmp);
- builder.mkdirp(`${out}/client${base}`);
- builder.mkdirp(`${out}/prerendered${base}`);
-
- builder.log.minor('Copying assets');
- const client_files = with_base(builder.writeClient(`${out}/client${base}`), base);
- const prerendered_files = with_base(
- builder.writePrerendered(`${out}/prerendered${base}`),
- base
- );
- builder.log.minor(compile ? 'Compiling executable' : 'Building server');
+ builder.log.minor('Building server');
+
+ const entrypoints = [resolve(import.meta.dirname, 'src', 'index.js')];
+
+ if (builder.hasServerInstrumentationFile()) {
+ if (buildOptions.compile) {
+ throw new Error(
+ 'Instrumentation is not yet supported when using the Bun adapter with `compile: true`.'
+ );
+ }
+ entrypoints.push(`${builder.config.kit.outDir}/output/server/instrumentation.server.js`);
+ }
- const pkg = await Bun.file('package.json').json();
const server = builder.getServerDirectory();
- const entries = posixify(`${tmp}/entries`);
- builder.copy(files, entries);
- const dir_id = `${entries}/dir.js`;
const manifest_file = `${server}/adapter-bun-manifest.js`;
const routes_file = `${server}/adapter-bun-routes.js`;
const server_options_file = `${server}/adapter-bun-options.js`;
- const instrumentation = builder.hasServerInstrumentationFile()
- ? `${server}/instrumentation.server.js`
- : undefined;
const virtual_files = {
[manifest_file]: `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n`,
- [server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`
+ [server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`,
+ [routes_file]: await create_routes({
+ builder,
+ out,
+ embed: !!buildOptions.compile
+ })
};
- const compile_options = normalize_compile_options(compile, out);
- virtual_files[routes_file] = create_routes({
- out,
- client_files,
- prerendered_files,
- prerendered_paths: builder.prerendered.paths,
- app_path: builder.getAppPath(),
- dir_id,
- embed: compile_options !== undefined
- });
- const entrypoints = [`${entries}/index.js`, `${entries}/handler.js`, dir_id];
- /** @type {Omit} */
- let build_options = {
- outdir: out,
- sourcemap: 'linked',
- splitting: true,
- naming: {
- entry: '[name].[ext]',
- chunk: 'server/chunks/[name]-[hash].[ext]'
- },
- external: [
- 'bun',
- 'bun:*',
- ...Object.keys(pkg.dependencies || {}).flatMap((dependency) => [
- dependency,
- `${dependency}/*`
- ])
- ]
- };
-
- if (compile_options) {
- const compile_file = `${out}/adapter-bun-compile.js`;
- virtual_files[compile_file] = create_compile_entrypoint(
- `${entries}/index.js`,
- instrumentation
- );
- entrypoints.splice(0, entrypoints.length, compile_file);
- build_options = compile_options;
- } else if (instrumentation) {
- entrypoints.push(instrumentation);
- }
-
+ /** @type {import('bun').BunPlugin} */
const adapter_plugin = {
name: 'adapter-bun',
- /** @param {import('bun').PluginBuilder} build */
setup(build) {
build.onResolve({ filter: /^(SERVER|MANIFEST|ROUTES|SERVER_OPTIONS)$/ }, ({ path }) => {
if (path === 'SERVER') return { path: `${server}/index.js` };
@@ -102,268 +74,164 @@ export default function (opts = {}) {
if (path === 'ROUTES') return { path: routes_file };
if (path === 'SERVER_OPTIONS') return { path: server_options_file };
});
-
- build.onLoad({ filter: /[\\/]adapter-bun[\\/]entries[\\/].*\.js$/ }, async ({ path }) => {
- const contents = (await Bun.file(path).text())
- .replace(/\bENV_PREFIX\b/g, JSON.stringify(envPrefix))
- .replace(
- /\bORIGIN\b/g,
- JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
- );
- return { contents, loader: 'js' };
- });
}
};
- let result;
- try {
- result = await Bun.build({
- ...build_options,
- target: 'bun',
- format: 'esm',
- conditions: merge_conditions(build_options.conditions),
- entrypoints,
- files: {
- ...build_options.files,
- ...virtual_files
- },
- plugins: [adapter_plugin, ...(build_options.plugins ?? [])]
- });
- } catch (error) {
- if (error instanceof AggregateError) {
- throw build_error(error.errors, error);
- }
- throw new Error('Bun server build failed', { cause: error });
- }
-
+ const result = await Bun.build({
+ ...buildOptions,
+ entrypoints,
+ target: 'bun',
+ format: 'esm',
+ naming: '[name].[ext]',
+ plugins: [adapter_plugin],
+ define: {
+ ENV_PREFIX: JSON.stringify(envPrefix),
+ ORIGIN: JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
+ },
+ conditions: ['bun', 'node'],
+ throw: false,
+ files: virtual_files,
+ outdir: out,
+ compile: buildOptions.compile
+ ? {
+ outfile: 'server',
+ ...(typeof buildOptions.compile === 'string' ? { target: buildOptions.compile } : {}),
+ ...(typeof buildOptions.compile === 'object' ? buildOptions.compile : {})
+ }
+ : false
+ });
if (!result.success) {
- throw build_error(result.logs);
- }
-
- if (instrumentation && !compile) {
- builder.instrument({
- entrypoint: `${out}/index.js`,
- instrumentation: `${out}/instrumentation.server.js`,
- module: {
- exports: ['hostname', 'port', 'server', 'unix']
+ for (const log of result.logs) {
+ switch (log.level) {
+ case 'error':
+ builder.log.error(log.message);
+ break;
+ case 'warning':
+ builder.log.warn(log.message);
+ break;
+ default:
+ builder.log.info(log.message);
}
- });
+ }
+ throw new AggregateError(result.logs);
}
},
supports: {
- read: () => true,
+ read: () => false,
instrumentation: () => true
}
};
}
-/**
- * @param {false | true | import('./index.js').CompileOptions} compile
- * @param {string} out
- * @returns {Omit | undefined}
- */
-function normalize_compile_options(compile, out) {
- if (!compile) return;
-
- const outfile = `${out}/app`;
- if (compile === true) return { compile: { outfile } };
-
- const options = { ...compile };
- if (!options.compile) {
- throw new Error('adapter-bun compile options must enable Bun executable compilation');
- }
-
- if (options.outdir === undefined) {
- if (options.compile === true) {
- options.compile = { outfile };
- } else if (typeof options.compile === 'string') {
- options.compile = { target: options.compile, outfile };
- } else if (options.compile.outfile === undefined) {
- options.compile = { outfile, ...options.compile };
- }
- }
-
- return options;
-}
-
-/**
- * @param {string | string[] | undefined} configured
- * @returns {string[]}
- */
-function merge_conditions(configured) {
- const conditions =
- configured === undefined ? [] : Array.isArray(configured) ? configured : [configured];
- return [...new Set(['bun', 'node', ...conditions])];
-}
-
-/**
- * @param {Array<{ message?: string }>} logs
- * @param {unknown} [cause]
- */
-function build_error(logs, cause) {
- const details = logs
- .map((log) => log.message)
- .filter(Boolean)
- .join('\n');
- const message = details ? `Bun server build failed:\n${details}` : 'Bun server build failed';
- return new AggregateError(logs, message, { cause });
-}
-
-/**
- * @param {string[]} files
- * @param {string} base
- * @returns {string[]}
- */
-function with_base(files, base) {
- const prefix = base.slice(1);
- return files.map((file) => posixify(prefix ? `${prefix}/${file}` : file));
-}
-
-/**
- * @param {string} entrypoint
- * @param {string | undefined} instrumentation
- * @returns {string}
- */
-function create_compile_entrypoint(entrypoint, instrumentation) {
- return [
- instrumentation && `await import(${JSON.stringify(instrumentation)});`,
- `await import(${JSON.stringify(entrypoint)});`
- ]
- .filter(Boolean)
- .join('\n');
-}
-
/**
* @param {object} options
+ * @param {import('@sveltejs/kit').Builder} options.builder
* @param {string} options.out
- * @param {string[]} options.client_files
- * @param {string[]} options.prerendered_files
- * @param {string[]} options.prerendered_paths
- * @param {string} options.app_path
- * @param {string} options.dir_id
* @param {boolean} options.embed
- * @returns {string}
+ * @returns {Promise}
*/
-function create_routes({
- out,
- client_files,
- prerendered_files,
- prerendered_paths,
- app_path,
- dir_id,
- embed
-}) {
- /** @type {Map} */
- const routes = new Map();
- const prerendered_file_set = new Set(prerendered_files);
-
- for (const file of client_files) {
- routes.set(encode_pathname(`/${file}`), {
- asset: `client/${file}`,
- immutable: file.startsWith(`${app_path}/immutable/`)
- });
- }
-
- for (const pathname of prerendered_paths) {
- const file = find_prerendered_file(pathname, prerendered_file_set);
- const route = encode_pathname(pathname);
- if (file && !routes.has(route)) {
- routes.set(route, { asset: `prerendered/${file}`, immutable: false });
+async function create_routes({ builder, out, embed }) {
+ const app_path = builder.getAppPath();
+ const base = builder.config.kit.paths.base || '/';
+ const builtFiles = `${builder.config.kit.outDir}/output`;
+
+ console.log('app_path', app_path);
+
+ const client_files = embed
+ ? await read_files_recursive(`${builtFiles}/client`)
+ : builder
+ .writeClient(`${out}/client`)
+ .map((rel) => ({ rel, abs: resolve(`${out}/client`, rel) }));
+
+ const prerendered_files = embed
+ ? (
+ await Promise.all([
+ read_files_recursive(`${builtFiles}/prerendered/pages`),
+ read_files_recursive(`${builtFiles}/prerendered/dependencies`),
+ read_files_recursive(`${builtFiles}/prerendered/data`)
+ ])
+ ).flat()
+ : builder
+ .writePrerendered(`${out}/prerendered`)
+ .map((rel) => ({ rel, abs: resolve(`${out}/prerendered`, rel) }));
+
+ /** @type {string[]} */
+ const asset_imports = [];
+
+ /**
+ * @param {string} abspath
+ * @returns {string}
+ */
+ function make_asset(abspath) {
+ const relpath = posix.relative(out, abspath);
+ if (embed) {
+ const assetId = `asset_${asset_imports.length}`;
+ asset_imports.push(
+ `import ${assetId} from ${JSON.stringify(abspath)} with { type: 'file' };`
+ );
+ return assetId;
+ } else {
+ return `resolve(import.meta.dir, ${JSON.stringify(relpath)})`;
}
}
- for (const pathname of prerendered_paths) {
- const inverted = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
- if (!inverted) continue;
+ /**
+ * @param {string} abspath
+ * @param {boolean} [immutable]
+ * @returns {string}
+ */
+ function make_response(abspath, immutable = false) {
+ const bunFileStr = `Bun.file(${make_asset(abspath)})`;
+
+ if (!embed && !immutable) return bunFileStr;
- const route = encode_pathname(inverted);
- if (routes.has(route)) continue;
- routes.set(route, {
- location: relative_pathname(route, encode_pathname(pathname))
- });
+ /** @type {Record} */
+ const headers = {};
+ if (embed) headers['content-type'] = Bun.file(abspath).type;
+ if (immutable) headers['cache-control'] = 'public,max-age=31536000,immutable';
+
+ return `new Response(${bunFileStr}, { headers: ${JSON.stringify(headers)} })`;
}
- const assets = [
- ...client_files.map((file) => `client/${file}`),
- ...prerendered_files.map((file) => `prerendered/${file}`)
- ];
- const unique_assets = [...new Set(assets)];
- const identifiers = new Map(unique_assets.map((file, index) => [file, `asset_${index}`]));
- const imports = embed
- ? unique_assets.map(
- (file, index) =>
- `import asset_${index} from ${JSON.stringify(posixify(resolve(out, file)))} with { type: 'file' };`
- )
- : [
- `import { join } from 'node:path';`,
- `import { dir } from ${JSON.stringify(posixify(dir_id))};`
- ];
- const asset_entries = unique_assets.map(
- (file) => `[${JSON.stringify(file)}, ${identifiers.get(file)}]`
- );
- const asset_path = embed
- ? [
- `const assets = new Map([${asset_entries.join(',')}]);`,
- `export const asset_path = (file) => assets.get(file);`
- ]
- : [`export const asset_path = (file) => join(dir, file);`];
+ /** @type {Array<{ path: string; value: string }>} */
const entries = [];
- for (const [route, value] of routes) {
- if ('asset' in value) {
- const identifier = identifiers.get(value.asset);
- const file = embed ? identifier : `asset_path(${JSON.stringify(value.asset)})`;
- const response = `Bun.file(${file})`;
- if (!embed && !value.immutable) {
- entries.push(`${JSON.stringify(route)}: ${response}`);
- continue;
- }
+ for (const { rel, abs } of client_files) {
+ const path = posix.join(base, rel);
+ const immutable = path.startsWith(`/${app_path}/immutable/`);
+ entries.push({ path: rel, value: make_response(abs, immutable) });
+ }
- /** @type {Record} */
- const headers = {};
- if (embed) headers['content-type'] = Bun.file(value.asset).type;
- if (value.immutable) {
- headers['cache-control'] = 'public,max-age=31536000,immutable';
- }
- entries.push(
- `${JSON.stringify(route)}: new Response(${response}, { headers: ${JSON.stringify(headers)} })`
- );
- continue;
+ for (const [path, { file }] of builder.prerendered.pages) {
+ const fileIdx = prerendered_files.findIndex((f) => f.rel === file);
+ if (fileIdx === -1)
+ throw new Error(`Could not find prerendered page ${file} for route ${path}`);
+ const { abs } = prerendered_files.splice(fileIdx, 1)[0];
+ entries.push({ path, value: make_response(abs) });
+
+ const inverted = path.endsWith('/') ? path.slice(0, -1) : `${path}/`;
+ if (inverted) {
+ entries.push({
+ path: inverted,
+ value: `Response.redirect(${JSON.stringify(posix.join(base, path))}, 308)`
+ });
}
+ }
- entries.push(
- `${JSON.stringify(route)}: Response.redirect(${JSON.stringify(value.location)}, 308)`
- );
+ for (const { abs, rel } of prerendered_files) {
+ entries.push({ path: rel, value: make_response(abs) });
}
- return [...imports, ...asset_path, `export const routes = {${entries.join(',\n')}};`].join('\n');
-}
+ const asset_path = [`export const asset_path = (file) => join(import.meta.dir, file);`];
-/**
- * @param {string} pathname
- * @param {Set} prerendered_files
- * @returns {string | undefined}
- */
-function find_prerendered_file(pathname, prerendered_files) {
- const relative = pathname.slice(1);
- return (
- relative.endsWith('/')
- ? [`${relative}index.html`]
- : [relative, `${relative}.html`, `${relative}/index.html`]
- ).find((candidate) => prerendered_files.has(candidate));
-}
+ const imports = embed ? [] : [`import { join, resolve } from 'node:path';`];
-/**
- * Relative reference from `from` to `to`, which must differ only by a trailing slash.
- * Keep in sync with the copy in `packages/kit/src/utils/url.js`.
- * @param {string} from
- * @param {string} to
- * @returns {string}
- */
-function relative_pathname(from, to) {
- const segment = to.replace(/\/$/, '').split('/').at(-1);
- return from.endsWith('/') ? `../${segment}` : `${segment}/`;
+ const routes = entries.map(
+ (entry) => `${JSON.stringify(encode_pathname(posix.join(base, entry.path)))}: ${entry.value}`
+ );
+
+ return [...imports, ...asset_path, `export const routes = {${routes.join(',\n')}};`].join('\n');
}
/**
@@ -373,8 +241,3 @@ function relative_pathname(from, to) {
function encode_pathname(pathname) {
return pathname.split('/').map(encodeURIComponent).join('/');
}
-
-/** @param {string} str */
-function posixify(str) {
- return str.replace(/\\/g, '/');
-}
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 00753efefe5d..de0167e29a9c 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -1,5 +1,3 @@
-import type { Serve } from 'bun';
-
declare module 'MANIFEST' {
import type { SSRManifest } from '@sveltejs/kit';
@@ -17,7 +15,7 @@ declare module 'SERVER' {
declare module 'SERVER_OPTIONS' {
const options: Pick<
- Serve.Options,
+ import('bun').Serve.Options,
| 'development'
| 'hostname'
| 'port'
diff --git a/packages/adapter-bun/src/dir.js b/packages/adapter-bun/src/dir.js
deleted file mode 100644
index 4df206b2c99f..000000000000
--- a/packages/adapter-bun/src/dir.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { resolve } from 'node:path';
-
-// Bun places shared modules in /server/chunks.
-export const dir = resolve(import.meta.dir, '../..');
diff --git a/packages/adapter-bun/tsconfig.json b/packages/adapter-bun/tsconfig.json
index 140718bbd5bc..44db1b24cf9f 100644
--- a/packages/adapter-bun/tsconfig.json
+++ b/packages/adapter-bun/tsconfig.json
@@ -13,7 +13,6 @@
"types": ["bun-types", "node"]
},
"include": [
- "build.js",
"index.js",
"index.spec.ts",
"vitest.config.js",
From 398539e89073c1aeb02070c59c007546559fec20 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 02:27:53 +0300
Subject: [PATCH 24/94] feat(adapter-bun): update build output path and
refactor buildOptions usage in configuration
---
packages/adapter-bun/index.d.ts | 2 +-
packages/adapter-bun/test/apps/basic/vite.config.js | 10 ++++++----
packages/adapter-bun/test/utils.js | 2 +-
packages/adapter-bun/tests/types.ts | 7 +++----
4 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 5a8e9a4d279a..6d5f8eb52ec9 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -48,7 +48,7 @@ interface AdapterOptions {
* Build the server and static assets.
* Pass Bun build options directly for advanced configuration. The generated entrypoint,
* top-level target, and module format are reserved. If neither an outfile nor outdir is
- * specified, the executable is written to `/app`.
+ * specified, the executable is written to `/server`.
* @default false
*/
buildOptions?: Pick<
diff --git a/packages/adapter-bun/test/apps/basic/vite.config.js b/packages/adapter-bun/test/apps/basic/vite.config.js
index 80fb15a881d4..b795eb652ad2 100644
--- a/packages/adapter-bun/test/apps/basic/vite.config.js
+++ b/packages/adapter-bun/test/apps/basic/vite.config.js
@@ -2,11 +2,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import adapter from '../../../index.js';
-const compile =
+const buildOptions =
process.env.ADVANCED_COMPILE === 'true'
? {
compile: {
- outfile: 'build/advanced-app',
+ outfile: 'advanced-app',
...(process.env.COMPILE_TARGET
? {
target: /** @type {import('bun').Build.CompileTarget} */ (
@@ -19,7 +19,9 @@ const compile =
bytecode: true,
sourcemap: 'linked'
}
- : process.env.COMPILE === 'true';
+ : process.env.COMPILE === 'true'
+ ? { compile: true }
+ : {};
export default defineConfig({
build: {
@@ -29,7 +31,7 @@ export default defineConfig({
sveltekit({
adapter: adapter({
envPrefix: 'MY_CUSTOM_',
- compile
+ buildOptions
})
})
]
diff --git a/packages/adapter-bun/test/utils.js b/packages/adapter-bun/test/utils.js
index ca5e5e9f4d36..f9e2bec87579 100644
--- a/packages/adapter-bun/test/utils.js
+++ b/packages/adapter-bun/test/utils.js
@@ -10,7 +10,7 @@ export const config = {
timeout: process.env.CI ? 45000 : 15000,
webServer: {
command: compiled
- ? 'bun run --bun build && MY_CUSTOM_PORT=4174 ./build/app'
+ ? 'bun run --bun build && MY_CUSTOM_PORT=4174 ./build/server'
: 'bun run --bun build && bun run preview',
port: 4174
},
diff --git a/packages/adapter-bun/tests/types.ts b/packages/adapter-bun/tests/types.ts
index e24c5ae7bea7..c328d06048a6 100644
--- a/packages/adapter-bun/tests/types.ts
+++ b/packages/adapter-bun/tests/types.ts
@@ -1,7 +1,7 @@
import adapter from '../index.js';
adapter({
- compile: {
+ buildOptions: {
compile: { target: 'bun-linux-x64' },
minify: true,
bytecode: true
@@ -9,14 +9,13 @@ adapter({
});
adapter({
- compile: {
- // @ts-expect-error false does not compile an executable
+ buildOptions: {
compile: false
}
});
adapter({
- compile: {
+ buildOptions: {
compile: true,
// @ts-expect-error the adapter reserves the Bun runtime target
target: 'node'
From 149f7d3e9ce373f10862f947f44fa9e329e65ee1 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 02:28:20 +0300
Subject: [PATCH 25/94] feat(adapter-bun): enable read support and refactor
file handling in create_routes
---
packages/adapter-bun/index.js | 54 +++--
packages/adapter-bun/index.spec.ts | 212 +++++++++---------
packages/adapter-bun/internal.d.ts | 2 +-
packages/adapter-bun/src/handler.js | 8 +-
.../apps/basic/src/routes/read/+server.js | 6 +
.../test/apps/basic/src/routes/read/file.txt | 1 +
.../adapter-bun/test/apps/basic/test/test.js | 10 +-
7 files changed, 159 insertions(+), 134 deletions(-)
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/read/+server.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/read/file.txt
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 237e56331531..55974953c4cd 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -118,7 +118,7 @@ export default function (opts = {}) {
},
supports: {
- read: () => false,
+ read: () => true,
instrumentation: () => true
}
};
@@ -136,8 +136,6 @@ async function create_routes({ builder, out, embed }) {
const base = builder.config.kit.paths.base || '/';
const builtFiles = `${builder.config.kit.outDir}/output`;
- console.log('app_path', app_path);
-
const client_files = embed
? await read_files_recursive(`${builtFiles}/client`)
: builder
@@ -158,40 +156,43 @@ async function create_routes({ builder, out, embed }) {
/** @type {string[]} */
const asset_imports = [];
+ /** @type {string[]} */
+ const file_entries = [];
/**
+ * @param {string} file
* @param {string} abspath
* @returns {string}
*/
- function make_asset(abspath) {
+ function make_file(file, abspath) {
const relpath = posix.relative(out, abspath);
+ let asset;
if (embed) {
- const assetId = `asset_${asset_imports.length}`;
- asset_imports.push(
- `import ${assetId} from ${JSON.stringify(abspath)} with { type: 'file' };`
- );
- return assetId;
+ asset = `asset_${asset_imports.length}`;
+ asset_imports.push(`import ${asset} from ${JSON.stringify(abspath)} with { type: 'file' };`);
} else {
- return `resolve(import.meta.dir, ${JSON.stringify(relpath)})`;
+ asset = `resolve(import.meta.dir, ${JSON.stringify(relpath)})`;
}
+
+ file_entries.push(`[${JSON.stringify(file)}, Bun.file(${asset})]`);
+ return `files.get(${JSON.stringify(file)})`;
}
/**
+ * @param {string} file
* @param {string} abspath
* @param {boolean} [immutable]
* @returns {string}
*/
- function make_response(abspath, immutable = false) {
- const bunFileStr = `Bun.file(${make_asset(abspath)})`;
-
- if (!embed && !immutable) return bunFileStr;
+ function make_response(file, abspath, immutable = false) {
+ if (!embed && !immutable) return file;
/** @type {Record} */
const headers = {};
if (embed) headers['content-type'] = Bun.file(abspath).type;
if (immutable) headers['cache-control'] = 'public,max-age=31536000,immutable';
- return `new Response(${bunFileStr}, { headers: ${JSON.stringify(headers)} })`;
+ return `new Response(${file}, { headers: ${JSON.stringify(headers)} })`;
}
/** @type {Array<{ path: string; value: string }>} */
@@ -200,15 +201,17 @@ async function create_routes({ builder, out, embed }) {
for (const { rel, abs } of client_files) {
const path = posix.join(base, rel);
const immutable = path.startsWith(`/${app_path}/immutable/`);
- entries.push({ path: rel, value: make_response(abs, immutable) });
+ const file = make_file(`client/${rel}`, abs);
+ entries.push({ path: rel, value: make_response(file, abs, immutable) });
}
for (const [path, { file }] of builder.prerendered.pages) {
const fileIdx = prerendered_files.findIndex((f) => f.rel === file);
if (fileIdx === -1)
throw new Error(`Could not find prerendered page ${file} for route ${path}`);
- const { abs } = prerendered_files.splice(fileIdx, 1)[0];
- entries.push({ path, value: make_response(abs) });
+ const { abs, rel } = prerendered_files.splice(fileIdx, 1)[0];
+ const bun_file = make_file(`prerendered/${rel}`, abs);
+ entries.push({ path, value: make_response(bun_file, abs) });
const inverted = path.endsWith('/') ? path.slice(0, -1) : `${path}/`;
if (inverted) {
@@ -220,18 +223,23 @@ async function create_routes({ builder, out, embed }) {
}
for (const { abs, rel } of prerendered_files) {
- entries.push({ path: rel, value: make_response(abs) });
+ const file = make_file(`prerendered/${rel}`, abs);
+ entries.push({ path: rel, value: make_response(file, abs) });
}
- const asset_path = [`export const asset_path = (file) => join(import.meta.dir, file);`];
-
- const imports = embed ? [] : [`import { join, resolve } from 'node:path';`];
+ const imports = embed ? [] : [`import { resolve } from 'node:path';`];
+ const files = `export const files = new Map([${file_entries.join(',\n')}]);`;
const routes = entries.map(
(entry) => `${JSON.stringify(encode_pathname(posix.join(base, entry.path)))}: ${entry.value}`
);
- return [...imports, ...asset_path, `export const routes = {${routes.join(',\n')}};`].join('\n');
+ return [
+ ...imports,
+ ...asset_imports,
+ files,
+ `export const routes = {${routes.join(',\n')}};`
+ ].join('\n');
}
/**
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 73e5b284237c..51f7026c4d67 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -1,10 +1,16 @@
-import { afterEach, describe, expect, test, vi } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { readdir } from 'node:fs/promises';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import adapter from './index.js';
+vi.mock('node:fs/promises', async (import_original) => {
+ const actual = await import_original();
+ return { ...actual, readdir: vi.fn() };
+});
+
const { build, file } = vi.hoisted(() => {
const build = vi.fn(async (_options: any) => ({ success: true, logs: [], outputs: [] }));
const file = vi.fn((path: string) => ({
- json: async () => ({ dependencies: { dependency: '1.0.0' } }),
type: path.endsWith('.html')
? 'text/html;charset=utf-8'
: path.endsWith('.json')
@@ -15,6 +21,10 @@ const { build, file } = vi.hoisted(() => {
return { build, file };
});
+beforeEach(() => {
+ vi.mocked(readdir).mockResolvedValue([]);
+});
+
afterEach(() => {
build.mockClear();
file.mockClear();
@@ -22,7 +32,10 @@ afterEach(() => {
describe('Bun build options', () => {
test('reserves the runtime target and module format', async () => {
- await adapter().adapt(builder());
+ const instance = adapter();
+ expect(instance.supports?.read?.({ route: { id: '/read' }, config: {} })).toBe(true);
+
+ await instance.adapt(builder());
const options = build.mock.calls[0][0];
expect(options).toMatchObject({
@@ -30,155 +43,142 @@ describe('Bun build options', () => {
format: 'esm',
conditions: ['bun', 'node'],
outdir: 'build',
- splitting: true
+ compile: false
});
- expect(options.entrypoints).toHaveLength(3);
+ expect(options.entrypoints).toHaveLength(1);
expect(options.plugins[0].name).toBe('adapter-bun');
});
- test('generates the route map at build time', async () => {
+ test('shares Bun files between directory routes and server reads', async () => {
await adapter().adapt(
builder({
- client_files: ['data.json', 'encoded name.txt', '_app/immutable/app.js', 'prerendered'],
- prerendered_files: ['prerendered/index.html', 'other/index.html'],
- prerendered_paths: ['/prerendered/', '/other/']
+ client_files: ['data.json', 'encoded name.txt', '_app/immutable/assets/read.txt'],
+ prerendered_files: ['prerendered/index.html'],
+ prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]]
})
);
const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
- expect(source).toContain('"/data.json": Bun.file(asset_path("client/data.json"))');
- expect(source).toContain('"/encoded%20name.txt"');
- expect(source).toContain('public,max-age=31536000,immutable');
- expect(source).toContain('"/prerendered": Bun.file(asset_path("client/prerendered"))');
- expect(source).toContain('"/prerendered/": Bun.file');
- expect(source).toContain('"/other/": Bun.file');
- expect(source).toContain('"/other": { GET: redirect_0, HEAD: redirect_0 }');
- expect(source.match(/const redirect_/g)).toHaveLength(1);
- expect(source).not.toContain('client_files');
- expect(source).not.toContain('prerendered_files');
- expect(source).not.toContain('for (');
- });
-
- test('normalizes executable output and composes user configuration safely', async () => {
- const user_plugin = { name: 'user-plugin', setup() {} };
- await adapter({
- out: 'dist',
- compile: {
- compile: { target: 'bun-linux-x64' },
- conditions: ['custom', 'bun'],
- files: {
- 'virtual:user': 'export default true',
- '.svelte-kit/output/server/adapter-bun-manifest.js': 'invalid manifest',
- '.svelte-kit/output/server/adapter-bun-routes.js': 'invalid routes'
- },
- plugins: [user_plugin],
- minify: true,
- bytecode: true,
- sourcemap: 'linked'
- }
- }).adapt(builder());
-
- const options = build.mock.calls[0][0];
- expect(options.compile).toEqual({ target: 'bun-linux-x64', outfile: 'dist/app' });
- expect(options.conditions).toEqual(['bun', 'node', 'custom']);
- expect(options.plugins.map((plugin: { name: string }) => plugin.name)).toEqual([
- 'adapter-bun',
- 'user-plugin'
- ]);
- expect(options.files['virtual:user']).toBe('export default true');
- expect(options.files['.svelte-kit/output/server/adapter-bun-manifest.js']).not.toBe(
- 'invalid manifest'
+ expect(source).toContain(
+ '["client/data.json", Bun.file(resolve(import.meta.dir, "client/data.json"))]'
);
- expect(options.files['.svelte-kit/output/server/adapter-bun-routes.js']).not.toBe(
- 'invalid routes'
+ expect(source).toContain('"/data.json": files.get("client/data.json")');
+ expect(source).toContain(
+ 'new Response(files.get("client/_app/immutable/assets/read.txt"), { headers:'
);
- expect(options).toMatchObject({
- target: 'bun',
- format: 'esm',
- minify: true,
- bytecode: true,
- sourcemap: 'linked'
- });
- expect(options.entrypoints).toHaveLength(1);
+ expect(source).toContain(
+ '["prerendered/prerendered/index.html", Bun.file(resolve(import.meta.dir, "prerendered/prerendered/index.html"))]'
+ );
+ expect(source).not.toContain('asset_path');
});
- test('embeds executable assets through the generated route map', async () => {
- await adapter({ compile: true }).adapt(
- builder({ client_files: ['data.json'], prerendered_files: ['prerendered/index.html'] })
+ test('maps logical paths to embedded Bun files for executables', async () => {
+ mock_embedded_files({
+ client: ['data.json', '_app/immutable/assets/read.txt'],
+ pages: ['prerendered/index.html']
+ });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(
+ builder({
+ prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]]
+ })
);
const options = build.mock.calls[0][0];
const source = options.files['.svelte-kit/output/server/adapter-bun-routes.js'];
- const entrypoint = options.files['build/adapter-bun-compile.js'];
+ expect(options.compile).toEqual({ outfile: 'server' });
expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('["client/data.json", asset_0]');
- expect(source).toContain('["prerendered/prerendered/index.html", asset_1]');
- expect(source).toContain(
- '"/data.json": new Response(Bun.file(asset_0), { headers: {"content-type":"application/json;charset=utf-8"} })'
- );
- expect(entrypoint).not.toContain("with { type: 'file' }");
- expect(entrypoint).not.toContain('sveltekit.adapter-bun.assets');
- expect(source).not.toContain('{ path:');
- expect(source).not.toContain('size');
- expect(source).not.toContain('lastModified');
- expect(source).not.toContain('etag');
- expect(source).not.toContain('sveltekit.adapter-bun.assets');
+ expect(source).toContain('["client/data.json", Bun.file(asset_0)]');
+ expect(source).toContain('["client/_app/immutable/assets/read.txt", Bun.file(asset_1)]');
+ expect(source).toContain('"/data.json": new Response(files.get("client/data.json")');
+ expect(source).not.toContain('asset_path');
});
- test('preserves an explicit outdir for split executables', async () => {
+ test('passes supported advanced build options to Bun', async () => {
+ mock_embedded_files({ client: ['data.json'] });
+
await adapter({
- compile: {
- compile: true,
- outdir: 'dist/executable',
- splitting: true
+ out: 'dist',
+ buildOptions: {
+ compile: { outfile: 'advanced-app', target: 'bun-linux-x64' },
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked'
}
}).adapt(builder());
expect(build.mock.calls[0][0]).toMatchObject({
- compile: true,
- outdir: 'dist/executable',
- splitting: true
+ target: 'bun',
+ format: 'esm',
+ outdir: 'dist',
+ compile: { outfile: 'advanced-app', target: 'bun-linux-x64' },
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked'
});
});
+});
- test('rejects advanced options that disable executable compilation', async () => {
- const invalid = { compile: { compile: false } } as any;
- await expect(adapter(invalid).adapt(builder())).rejects.toThrow('must enable Bun executable');
- expect(build).not.toHaveBeenCalled();
- });
-
- test('reports serialization and build failures clearly', async () => {
- await expect(
- adapter({ serverOptions: { port: Infinity } as any }).adapt(builder())
- ).rejects.toThrow('Could not serialize adapter-bun serverOptions');
+test('the runtime reader reuses the generated Bun file', () => {
+ const source = readFileSync(new URL('./src/handler.js', import.meta.url), 'utf8');
+ expect(source).toContain('const asset = files.get(`client/${file}`)');
+ expect(source).toContain('return asset.stream()');
+ expect(source).not.toContain('Bun.file(');
+ expect(source).not.toContain('asset_path');
+});
- build.mockRejectedValueOnce(new AggregateError([], 'native failure'));
- await expect(adapter().adapt(builder())).rejects.toThrow('Bun server build failed');
+function mock_embedded_files({
+ client = [],
+ pages = [],
+ dependencies = [],
+ data = []
+}: {
+ client?: string[];
+ pages?: string[];
+ dependencies?: string[];
+ data?: string[];
+}) {
+ vi.mocked(readdir).mockImplementation(async (path) => {
+ const directory = String(path);
+ const files = directory.endsWith('/client')
+ ? client
+ : directory.endsWith('/prerendered/pages')
+ ? pages
+ : directory.endsWith('/prerendered/dependencies')
+ ? dependencies
+ : data;
+
+ return files.map((file) => {
+ const segments = file.split('/');
+ const name = /** @type {string} */ segments.pop();
+ return {
+ name,
+ parentPath: [directory, ...segments].join('/'),
+ isFile: () => true
+ };
+ }) as any;
});
-});
+}
function builder({
client_files = [],
prerendered_files = [],
- prerendered_paths = [],
+ prerendered_pages = [],
app_path = '_app'
}: {
client_files?: string[];
prerendered_files?: string[];
- prerendered_paths?: string[];
+ prerendered_pages?: Array<[string, { file: string }]>;
app_path?: string;
} = {}) {
return {
- config: { kit: { paths: { base: '', origin: undefined } } },
- prerendered: { paths: prerendered_paths },
- log: { minor() {} },
- getBuildDirectory: () => '.svelte-kit/adapter-bun',
+ config: { kit: { outDir: '.svelte-kit', paths: { base: '', origin: undefined } } },
+ prerendered: { pages: new Map(prerendered_pages) },
+ log: { minor() {}, error() {}, warn() {}, info() {} },
getServerDirectory: () => '.svelte-kit/output/server',
rimraf() {},
- mkdirp() {},
writeClient: () => client_files,
writePrerendered: () => prerendered_files,
- copy() {},
generateManifest: () => '{}',
getAppPath: () => app_path,
hasServerInstrumentationFile: () => false
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index de0167e29a9c..2d1c9bb2b2ea 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -5,7 +5,7 @@ declare module 'MANIFEST' {
}
declare module 'ROUTES' {
- export function asset_path(file: string): string;
+ export const files: Map;
export const routes: Serve.Routes;
}
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 9cd44f17da88..3eb431f24bd2 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -1,6 +1,6 @@
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';
-import { asset_path } from 'ROUTES';
+import { files } from 'ROUTES';
import { env, env_prefix, number_env } from './env.js';
const server = new Server(manifest);
@@ -14,7 +14,11 @@ const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 }) ?? 1;
await server.init({
env: Bun.env,
- read: (file) => Bun.file(asset_path(`client/${file}`)).stream()
+ read: (file) => {
+ const asset = files.get(`client/${file}`);
+ if (!asset) throw new Error(`Could not find server asset ${file}`);
+ return asset.stream();
+ }
});
/**
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js
new file mode 100644
index 000000000000..47717c4e0511
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js
@@ -0,0 +1,6 @@
+import { read } from '$app/server';
+import file from './file.txt?url';
+
+export function GET() {
+ return read(file);
+}
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/read/file.txt b/packages/adapter-bun/test/apps/basic/src/routes/read/file.txt
new file mode 100644
index 000000000000..7ef31b114b64
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/read/file.txt
@@ -0,0 +1 @@
+Hello from $app/server read
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index c5c09d1d37ee..4d3e1e8df4ed 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -73,6 +73,12 @@ test('serves URL-encoded static filenames', async ({ request }) => {
expect(await response.text()).toBe('hello from an encoded filename\n');
});
+test('reads an imported server asset', async ({ request }) => {
+ const response = await request.get('/read');
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toBe('Hello from $app/server read\n');
+});
+
test('caches immutable client assets', async ({ request }) => {
const page = await request.get('/');
const asset = /["']([^"']*_app\/immutable\/[^"']+)["']/.exec(await page.text())?.[1];
@@ -89,9 +95,9 @@ test('uses Bun route method semantics for static files', async ({ request }) =>
});
test('redirects prerendered paths to their canonical trailing slash', async ({ request }) => {
- const response = await request.get('/prerendered?value=1', { maxRedirects: 0 });
+ const response = await request.get('/prerendered', { maxRedirects: 0 });
expect(response.status()).toBe(308);
- expect(response.headers()['location']).toBe('prerendered/?value=1');
+ expect(response.headers()['location']).toBe('/prerendered/');
});
test('configures long-lived event streams', async ({ request }) => {
From defa6651b0e3924c7f106e37763addb89f697795 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 02:51:18 +0300
Subject: [PATCH 26/94] feat(adapter-bun): add server_assets support and
refactor file reading in handler
---
packages/adapter-bun/index.js | 7 +++++
packages/adapter-bun/index.spec.ts | 41 +++++++++++++++++++++--------
packages/adapter-bun/internal.d.ts | 1 +
packages/adapter-bun/src/handler.js | 8 ++----
4 files changed, 40 insertions(+), 17 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 55974953c4cd..b83dc0c491c2 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -229,6 +229,12 @@ async function create_routes({ builder, out, embed }) {
const imports = embed ? [] : [`import { resolve } from 'node:path';`];
const files = `export const files = new Map([${file_entries.join(',\n')}]);`;
+ const server_assets = builder.findServerAssets(
+ builder.routes.filter((route) => route.prerender !== true)
+ );
+ const readable_files = `export const server_assets = new Map([${server_assets
+ .map((file) => `[${JSON.stringify(file)}, files.get(${JSON.stringify(`client/${file}`)})]`)
+ .join(',\n')}]);`;
const routes = entries.map(
(entry) => `${JSON.stringify(encode_pathname(posix.join(base, entry.path)))}: ${entry.value}`
@@ -238,6 +244,7 @@ async function create_routes({ builder, out, embed }) {
...imports,
...asset_imports,
files,
+ readable_files,
`export const routes = {${routes.join(',\n')}};`
].join('\n');
}
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 51f7026c4d67..5ea91a91bd54 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -9,7 +9,7 @@ vi.mock('node:fs/promises', async (import_original) => {
});
const { build, file } = vi.hoisted(() => {
- const build = vi.fn(async (_options: any) => ({ success: true, logs: [], outputs: [] }));
+ const build = vi.fn((_options: any) => ({ success: true, logs: [], outputs: [] }));
const file = vi.fn((path: string) => ({
type: path.endsWith('.html')
? 'text/html;charset=utf-8'
@@ -50,13 +50,18 @@ describe('Bun build options', () => {
});
test('shares Bun files between directory routes and server reads', async () => {
- await adapter().adapt(
- builder({
- client_files: ['data.json', 'encoded name.txt', '_app/immutable/assets/read.txt'],
- prerendered_files: ['prerendered/index.html'],
- prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]]
- })
- );
+ const active_route = { id: '/read', prerender: false };
+ const prerendered_route = { id: '/prerendered', prerender: true };
+ const test_builder = builder({
+ client_files: ['data.json', 'encoded name.txt', '_app/immutable/assets/read.txt'],
+ prerendered_files: ['prerendered/index.html'],
+ prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]],
+ routes: [active_route, prerendered_route],
+ server_assets: ['_app/immutable/assets/read.txt']
+ });
+
+ await adapter().adapt(test_builder);
+ expect(test_builder.findServerAssets).toHaveBeenCalledWith([active_route]);
const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
expect(source).toContain(
@@ -69,6 +74,10 @@ describe('Bun build options', () => {
expect(source).toContain(
'["prerendered/prerendered/index.html", Bun.file(resolve(import.meta.dir, "prerendered/prerendered/index.html"))]'
);
+ expect(source).toContain(
+ 'export const server_assets = new Map([["_app/immutable/assets/read.txt", files.get("client/_app/immutable/assets/read.txt")]])'
+ );
+ expect(source).not.toContain('["data.json", files.get("client/data.json")]');
expect(source).not.toContain('asset_path');
});
@@ -80,7 +89,8 @@ describe('Bun build options', () => {
await adapter({ buildOptions: { compile: true } }).adapt(
builder({
- prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]]
+ prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]],
+ server_assets: ['_app/immutable/assets/read.txt']
})
);
@@ -90,6 +100,9 @@ describe('Bun build options', () => {
expect(source).toContain("with { type: 'file' }");
expect(source).toContain('["client/data.json", Bun.file(asset_0)]');
expect(source).toContain('["client/_app/immutable/assets/read.txt", Bun.file(asset_1)]');
+ expect(source).toContain(
+ '["_app/immutable/assets/read.txt", files.get("client/_app/immutable/assets/read.txt")]'
+ );
expect(source).toContain('"/data.json": new Response(files.get("client/data.json")');
expect(source).not.toContain('asset_path');
});
@@ -121,7 +134,7 @@ describe('Bun build options', () => {
test('the runtime reader reuses the generated Bun file', () => {
const source = readFileSync(new URL('./src/handler.js', import.meta.url), 'utf8');
- expect(source).toContain('const asset = files.get(`client/${file}`)');
+ expect(source).toContain('const asset = server_assets.get(file)');
expect(source).toContain('return asset.stream()');
expect(source).not.toContain('Bun.file(');
expect(source).not.toContain('asset_path');
@@ -138,7 +151,7 @@ function mock_embedded_files({
dependencies?: string[];
data?: string[];
}) {
- vi.mocked(readdir).mockImplementation(async (path) => {
+ vi.mocked(readdir).mockImplementation((path) => {
const directory = String(path);
const files = directory.endsWith('/client')
? client
@@ -164,21 +177,27 @@ function builder({
client_files = [],
prerendered_files = [],
prerendered_pages = [],
+ routes = [],
+ server_assets = [],
app_path = '_app'
}: {
client_files?: string[];
prerendered_files?: string[];
prerendered_pages?: Array<[string, { file: string }]>;
+ routes?: Array<{ id: string; prerender: boolean | string }>;
+ server_assets?: string[];
app_path?: string;
} = {}) {
return {
config: { kit: { outDir: '.svelte-kit', paths: { base: '', origin: undefined } } },
+ routes,
prerendered: { pages: new Map(prerendered_pages) },
log: { minor() {}, error() {}, warn() {}, info() {} },
getServerDirectory: () => '.svelte-kit/output/server',
rimraf() {},
writeClient: () => client_files,
writePrerendered: () => prerendered_files,
+ findServerAssets: vi.fn(() => server_assets),
generateManifest: () => '{}',
getAppPath: () => app_path,
hasServerInstrumentationFile: () => false
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 2d1c9bb2b2ea..ca1bea817673 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -6,6 +6,7 @@ declare module 'MANIFEST' {
declare module 'ROUTES' {
export const files: Map;
+ export const server_assets: Map;
export const routes: Serve.Routes;
}
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 3eb431f24bd2..04d091a337d7 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -1,6 +1,6 @@
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';
-import { files } from 'ROUTES';
+import { server_assets } from 'ROUTES';
import { env, env_prefix, number_env } from './env.js';
const server = new Server(manifest);
@@ -14,11 +14,7 @@ const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 }) ?? 1;
await server.init({
env: Bun.env,
- read: (file) => {
- const asset = files.get(`client/${file}`);
- if (!asset) throw new Error(`Could not find server asset ${file}`);
- return asset.stream();
- }
+ read: (file) => server_assets.get(file)?.stream() ?? null
});
/**
From bd04683729defb11582ee9a596a05fc021250b38 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 03:03:17 +0300
Subject: [PATCH 27/94] feat(adapter-bun): refactor file handling in
create_routes and update type definitions
---
packages/adapter-bun/index.js | 17 +++++++++++-----
packages/adapter-bun/index.spec.ts | 31 +++++++++++++++---------------
packages/adapter-bun/internal.d.ts | 1 -
3 files changed, 27 insertions(+), 22 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index b83dc0c491c2..0b8ea0625643 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -158,6 +158,8 @@ async function create_routes({ builder, out, embed }) {
const asset_imports = [];
/** @type {string[]} */
const file_entries = [];
+ /** @type {Map} */
+ const file_identifiers = new Map();
/**
* @param {string} file
@@ -174,8 +176,10 @@ async function create_routes({ builder, out, embed }) {
asset = `resolve(import.meta.dir, ${JSON.stringify(relpath)})`;
}
- file_entries.push(`[${JSON.stringify(file)}, Bun.file(${asset})]`);
- return `files.get(${JSON.stringify(file)})`;
+ const identifier = `file_${file_entries.length}`;
+ file_entries.push(`const ${identifier} = Bun.file(${asset});`);
+ file_identifiers.set(file, identifier);
+ return identifier;
}
/**
@@ -228,12 +232,15 @@ async function create_routes({ builder, out, embed }) {
}
const imports = embed ? [] : [`import { resolve } from 'node:path';`];
- const files = `export const files = new Map([${file_entries.join(',\n')}]);`;
const server_assets = builder.findServerAssets(
builder.routes.filter((route) => route.prerender !== true)
);
const readable_files = `export const server_assets = new Map([${server_assets
- .map((file) => `[${JSON.stringify(file)}, files.get(${JSON.stringify(`client/${file}`)})]`)
+ .map((file) => {
+ const identifier = file_identifiers.get(`client/${file}`);
+ if (!identifier) throw new Error(`Could not find server asset ${file} in client output`);
+ return `[${JSON.stringify(file)}, ${identifier}]`;
+ })
.join(',\n')}]);`;
const routes = entries.map(
@@ -243,7 +250,7 @@ async function create_routes({ builder, out, embed }) {
return [
...imports,
...asset_imports,
- files,
+ ...file_entries,
readable_files,
`export const routes = {${routes.join(',\n')}};`
].join('\n');
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 5ea91a91bd54..27992c0cdc06 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -65,19 +65,19 @@ describe('Bun build options', () => {
const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
expect(source).toContain(
- '["client/data.json", Bun.file(resolve(import.meta.dir, "client/data.json"))]'
+ 'const file_0 = Bun.file(resolve(import.meta.dir, "client/data.json"))'
);
- expect(source).toContain('"/data.json": files.get("client/data.json")');
+ expect(source).toContain('"/data.json": file_0');
+ expect(source).toContain('new Response(file_2, { headers:');
expect(source).toContain(
- 'new Response(files.get("client/_app/immutable/assets/read.txt"), { headers:'
+ 'const file_3 = Bun.file(resolve(import.meta.dir, "prerendered/prerendered/index.html"))'
);
expect(source).toContain(
- '["prerendered/prerendered/index.html", Bun.file(resolve(import.meta.dir, "prerendered/prerendered/index.html"))]'
+ 'export const server_assets = new Map([["_app/immutable/assets/read.txt", file_2]])'
);
- expect(source).toContain(
- 'export const server_assets = new Map([["_app/immutable/assets/read.txt", files.get("client/_app/immutable/assets/read.txt")]])'
- );
- expect(source).not.toContain('["data.json", files.get("client/data.json")]');
+ expect(source).not.toContain('["data.json", file_0]');
+ expect(source).not.toContain('export const files');
+ expect(source).not.toContain('files.get');
expect(source).not.toContain('asset_path');
});
@@ -98,12 +98,12 @@ describe('Bun build options', () => {
const source = options.files['.svelte-kit/output/server/adapter-bun-routes.js'];
expect(options.compile).toEqual({ outfile: 'server' });
expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('["client/data.json", Bun.file(asset_0)]');
- expect(source).toContain('["client/_app/immutable/assets/read.txt", Bun.file(asset_1)]');
- expect(source).toContain(
- '["_app/immutable/assets/read.txt", files.get("client/_app/immutable/assets/read.txt")]'
- );
- expect(source).toContain('"/data.json": new Response(files.get("client/data.json")');
+ expect(source).toContain('const file_0 = Bun.file(asset_0)');
+ expect(source).toContain('const file_1 = Bun.file(asset_1)');
+ expect(source).toContain('["_app/immutable/assets/read.txt", file_1]');
+ expect(source).toContain('"/data.json": new Response(file_0');
+ expect(source).not.toContain('export const files');
+ expect(source).not.toContain('files.get');
expect(source).not.toContain('asset_path');
});
@@ -134,8 +134,7 @@ describe('Bun build options', () => {
test('the runtime reader reuses the generated Bun file', () => {
const source = readFileSync(new URL('./src/handler.js', import.meta.url), 'utf8');
- expect(source).toContain('const asset = server_assets.get(file)');
- expect(source).toContain('return asset.stream()');
+ expect(source).toContain('server_assets.get(file)?.stream() ?? null');
expect(source).not.toContain('Bun.file(');
expect(source).not.toContain('asset_path');
});
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index ca1bea817673..100eafc2bebe 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -5,7 +5,6 @@ declare module 'MANIFEST' {
}
declare module 'ROUTES' {
- export const files: Map;
export const server_assets: Map;
export const routes: Serve.Routes;
}
From 3df0e97c97208d972c5f1f7225e25b677da75d91 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 03:13:18 +0300
Subject: [PATCH 28/94] feat(adapter-bun): enhance server instrumentation
handling and refactor entrypoint logic
---
packages/adapter-bun/index.js | 39 +++++++++++++++++++++++++----------
1 file changed, 28 insertions(+), 11 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 0b8ea0625643..42e4fc0cd2f3 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -38,18 +38,12 @@ export default function (opts = {}) {
builder.log.minor('Building server');
- const entrypoints = [resolve(import.meta.dirname, 'src', 'index.js')];
-
- if (builder.hasServerInstrumentationFile()) {
- if (buildOptions.compile) {
- throw new Error(
- 'Instrumentation is not yet supported when using the Bun adapter with `compile: true`.'
- );
- }
- entrypoints.push(`${builder.config.kit.outDir}/output/server/instrumentation.server.js`);
- }
-
const server = builder.getServerDirectory();
+ const adapter_entrypoint = resolve(import.meta.dirname, 'src', 'index.js');
+ const instrumentation = builder.hasServerInstrumentationFile()
+ ? `${server}/instrumentation.server.js`
+ : undefined;
+ const entrypoints = [adapter_entrypoint];
const manifest_file = `${server}/adapter-bun-manifest.js`;
const routes_file = `${server}/adapter-bun-routes.js`;
@@ -64,6 +58,19 @@ export default function (opts = {}) {
})
};
+ if (instrumentation) {
+ if (buildOptions.compile) {
+ const instrumented_entrypoint = `${server}/adapter-bun-instrumented.js`;
+ virtual_files[instrumented_entrypoint] = [
+ `import './instrumentation.server.js';`,
+ `await import(${JSON.stringify(adapter_entrypoint)});`
+ ].join('\n');
+ entrypoints[0] = instrumented_entrypoint;
+ } else {
+ entrypoints.push(instrumentation);
+ }
+ }
+
/** @type {import('bun').BunPlugin} */
const adapter_plugin = {
name: 'adapter-bun',
@@ -115,6 +122,16 @@ export default function (opts = {}) {
}
throw new AggregateError(result.logs);
}
+
+ if (instrumentation && !buildOptions.compile) {
+ builder.instrument({
+ entrypoint: `${out}/index.js`,
+ instrumentation: `${out}/instrumentation.server.js`,
+ module: {
+ exports: ['server', 'unix']
+ }
+ });
+ }
},
supports: {
From 6fcc9f7f03afc30f0e50d9a62629b28f306d2dce Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 03:13:26 +0300
Subject: [PATCH 29/94] feat(adapter-bun): add server instrumentation tests and
implement instrumentation logic
---
packages/adapter-bun/index.spec.ts | 38 ++++++++++++++++++-
.../apps/basic/src/instrumentation.server.js | 1 +
.../basic/src/routes/instrumented/+server.js | 3 ++
.../adapter-bun/test/apps/basic/test/test.js | 6 +++
4 files changed, 46 insertions(+), 2 deletions(-)
create mode 100644 packages/adapter-bun/test/apps/basic/src/instrumentation.server.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 27992c0cdc06..07baa28ebd34 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -130,6 +130,37 @@ describe('Bun build options', () => {
sourcemap: 'linked'
});
});
+
+ test('runs server instrumentation before starting the server', async () => {
+ const test_builder = builder({ instrumentation: true });
+
+ await adapter({ out: 'dist' }).adapt(test_builder);
+
+ expect(build.mock.calls[0][0].entrypoints).toEqual([
+ new URL('./src/index.js', import.meta.url).pathname,
+ '.svelte-kit/output/server/instrumentation.server.js'
+ ]);
+ expect(test_builder.instrument).toHaveBeenCalledWith({
+ entrypoint: 'dist/index.js',
+ instrumentation: 'dist/instrumentation.server.js',
+ module: {
+ exports: ['server', 'unix']
+ }
+ });
+ });
+
+ test('runs server instrumentation before starting a compiled executable', async () => {
+ const test_builder = builder({ instrumentation: true });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(test_builder);
+
+ const options = build.mock.calls[0][0];
+ expect(options.entrypoints).toEqual(['.svelte-kit/output/server/adapter-bun-instrumented.js']);
+ expect(options.files[options.entrypoints[0]]).toBe(
+ `import './instrumentation.server.js';\nawait import(${JSON.stringify(new URL('./src/index.js', import.meta.url).pathname)});`
+ );
+ expect(test_builder.instrument).not.toHaveBeenCalled();
+ });
});
test('the runtime reader reuses the generated Bun file', () => {
@@ -178,7 +209,8 @@ function builder({
prerendered_pages = [],
routes = [],
server_assets = [],
- app_path = '_app'
+ app_path = '_app',
+ instrumentation = false
}: {
client_files?: string[];
prerendered_files?: string[];
@@ -186,6 +218,7 @@ function builder({
routes?: Array<{ id: string; prerender: boolean | string }>;
server_assets?: string[];
app_path?: string;
+ instrumentation?: boolean;
} = {}) {
return {
config: { kit: { outDir: '.svelte-kit', paths: { base: '', origin: undefined } } },
@@ -199,6 +232,7 @@ function builder({
findServerAssets: vi.fn(() => server_assets),
generateManifest: () => '{}',
getAppPath: () => app_path,
- hasServerInstrumentationFile: () => false
+ hasServerInstrumentationFile: () => instrumentation,
+ instrument: vi.fn()
} as any;
}
diff --git a/packages/adapter-bun/test/apps/basic/src/instrumentation.server.js b/packages/adapter-bun/test/apps/basic/src/instrumentation.server.js
new file mode 100644
index 000000000000..00368839194c
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/instrumentation.server.js
@@ -0,0 +1 @@
+globalThis.__INSTRUMENTATION_RAN__ = true;
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js
new file mode 100644
index 000000000000..def88b135b5f
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js
@@ -0,0 +1,3 @@
+export function GET() {
+ return new Response(String(globalThis.__INSTRUMENTATION_RAN__ === true));
+}
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 4d3e1e8df4ed..7aa80ae104ac 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -24,6 +24,12 @@ test('provides Bun request context', async ({ request }) => {
expect(body.subscribers).toBe(0);
});
+test('runs server instrumentation before starting the server', async ({ request }) => {
+ const response = await request.get('/instrumented');
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toBe('true');
+});
+
test('serves static files with Bun file responses', async ({ request }) => {
const response = await request.get('/data.json');
expect(response.status()).toBe(200);
From 9a755f4b3a7fdd0e8a6c4b7b0bb15e3b73bd47b0 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 12:36:08 +0300
Subject: [PATCH 30/94] feat(adapter-bun): update build options and enhance
file handling in adapter logic
---
packages/adapter-bun/index.d.ts | 1 -
packages/adapter-bun/index.js | 61 +++++++++----------
packages/adapter-bun/index.spec.ts | 42 ++++++++++++-
packages/adapter-bun/package.json | 7 +--
.../basic/static/.well-known/adapter-bun.txt | 1 +
.../adapter-bun/test/apps/basic/test/test.js | 10 ++-
6 files changed, 79 insertions(+), 43 deletions(-)
create mode 100644 packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 6d5f8eb52ec9..8740a638b90c 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -53,7 +53,6 @@ interface AdapterOptions {
*/
buildOptions?: Pick<
import('bun').BuildConfig,
- | 'splitting'
| 'sourcemap'
| 'minify'
| 'bytecode'
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 42e4fc0cd2f3..53fe02420d9a 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -15,7 +15,7 @@ async function read_files_recursive(path) {
const rel = posix.relative(path, abs);
return { abs, rel };
})
- .filter(({ rel }) => rel.split('/').every((segment) => !segment.startsWith('.')));
+ .filter(({ rel }) => rel.split('/').every((segment) => segment !== '.vite'));
} catch {
return [];
}
@@ -39,11 +39,6 @@ export default function (opts = {}) {
builder.log.minor('Building server');
const server = builder.getServerDirectory();
- const adapter_entrypoint = resolve(import.meta.dirname, 'src', 'index.js');
- const instrumentation = builder.hasServerInstrumentationFile()
- ? `${server}/instrumentation.server.js`
- : undefined;
- const entrypoints = [adapter_entrypoint];
const manifest_file = `${server}/adapter-bun-manifest.js`;
const routes_file = `${server}/adapter-bun-routes.js`;
@@ -57,18 +52,23 @@ export default function (opts = {}) {
embed: !!buildOptions.compile
})
};
+ const src_dir = resolve(import.meta.dirname, 'src');
+ const index_file = resolve(src_dir, 'index.js');
+ const start_file = resolve(src_dir, 'start.js');
+
+ const instrumentation = builder.hasServerInstrumentationFile()
+ ? `${server}/instrumentation.server.js`
+ : undefined;
if (instrumentation) {
- if (buildOptions.compile) {
- const instrumented_entrypoint = `${server}/adapter-bun-instrumented.js`;
- virtual_files[instrumented_entrypoint] = [
- `import './instrumentation.server.js';`,
- `await import(${JSON.stringify(adapter_entrypoint)});`
- ].join('\n');
- entrypoints[0] = instrumented_entrypoint;
- } else {
- entrypoints.push(instrumentation);
- }
+ // Virtually rename index.js to start.js
+ virtual_files[start_file] = await Bun.file(index_file).text();
+
+ // Virtually create a new index.js that imports the instrumentation and then starts the server
+ virtual_files[index_file] = [
+ `import ${JSON.stringify(instrumentation)};`,
+ `await import(${JSON.stringify(start_file)});`
+ ].join('\n');
}
/** @type {import('bun').BunPlugin} */
@@ -86,10 +86,15 @@ export default function (opts = {}) {
const result = await Bun.build({
...buildOptions,
- entrypoints,
+ splitting: true,
+ entrypoints: [index_file],
target: 'bun',
format: 'esm',
- naming: '[name].[ext]',
+ naming: {
+ entry: '[name].[ext]',
+ chunk: 'server/chunks/[name]-[hash].[ext]',
+ asset: 'server/assets/[name]-[hash].[ext]'
+ },
plugins: [adapter_plugin],
define: {
ENV_PREFIX: JSON.stringify(envPrefix),
@@ -122,16 +127,6 @@ export default function (opts = {}) {
}
throw new AggregateError(result.logs);
}
-
- if (instrumentation && !buildOptions.compile) {
- builder.instrument({
- entrypoint: `${out}/index.js`,
- instrumentation: `${out}/instrumentation.server.js`,
- module: {
- exports: ['server', 'unix']
- }
- });
- }
},
supports: {
@@ -184,7 +179,7 @@ async function create_routes({ builder, out, embed }) {
* @returns {string}
*/
function make_file(file, abspath) {
- const relpath = posix.relative(out, abspath);
+ const relpath = posix.relative(resolve(out), abspath);
let asset;
if (embed) {
asset = `asset_${asset_imports.length}`;
@@ -223,7 +218,7 @@ async function create_routes({ builder, out, embed }) {
const path = posix.join(base, rel);
const immutable = path.startsWith(`/${app_path}/immutable/`);
const file = make_file(`client/${rel}`, abs);
- entries.push({ path: rel, value: make_response(file, abs, immutable) });
+ entries.push({ path, value: make_response(file, abs, immutable) });
}
for (const [path, { file }] of builder.prerendered.pages) {
@@ -238,14 +233,14 @@ async function create_routes({ builder, out, embed }) {
if (inverted) {
entries.push({
path: inverted,
- value: `Response.redirect(${JSON.stringify(posix.join(base, path))}, 308)`
+ value: `(request) => Response.redirect(${JSON.stringify(encode_pathname(path))} + new URL(request.url).search, 308)`
});
}
}
for (const { abs, rel } of prerendered_files) {
const file = make_file(`prerendered/${rel}`, abs);
- entries.push({ path: rel, value: make_response(file, abs) });
+ entries.push({ path: posix.join(base, rel), value: make_response(file, abs) });
}
const imports = embed ? [] : [`import { resolve } from 'node:path';`];
@@ -261,7 +256,7 @@ async function create_routes({ builder, out, embed }) {
.join(',\n')}]);`;
const routes = entries.map(
- (entry) => `${JSON.stringify(encode_pathname(posix.join(base, entry.path)))}: ${entry.value}`
+ (entry) => `${JSON.stringify(encode_pathname(entry.path))}: ${entry.value}`
);
return [
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 07baa28ebd34..11107ffd83be 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -45,7 +45,6 @@ describe('Bun build options', () => {
outdir: 'build',
compile: false
});
- expect(options.entrypoints).toHaveLength(1);
expect(options.plugins[0].name).toBe('adapter-bun');
});
@@ -107,6 +106,34 @@ describe('Bun build options', () => {
expect(source).not.toContain('asset_path');
});
+ test('preserves dotfiles other than Vite build metadata in executables', async () => {
+ mock_embedded_files({ client: ['.vite/manifest.json', '.well-known/assetlinks.json'] });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(builder());
+
+ const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ expect(source).toContain('assetlinks.json');
+ expect(source).toContain('"/.well-known/assetlinks.json"');
+ expect(source).not.toContain('.vite/manifest.json');
+ });
+
+ test('does not duplicate the base path for prerendered pages', async () => {
+ await adapter().adapt(
+ builder({
+ base: '/base',
+ app_path: 'base/_app',
+ prerendered_files: ['prerendered/index.html'],
+ prerendered_pages: [['/base/prerendered/', { file: 'prerendered/index.html' }]]
+ })
+ );
+
+ const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ expect(source).toContain('"/base/prerendered/": file_0');
+ expect(source).toContain('"/base/prerendered": (request) => Response.redirect');
+ expect(source).toContain('new URL(request.url).search');
+ expect(source).not.toContain('/base/base/');
+ });
+
test('passes supported advanced build options to Bun', async () => {
mock_embedded_files({ client: ['data.json'] });
@@ -138,6 +165,7 @@ describe('Bun build options', () => {
expect(build.mock.calls[0][0].entrypoints).toEqual([
new URL('./src/index.js', import.meta.url).pathname,
+ new URL('./src/handler.js', import.meta.url).pathname,
'.svelte-kit/output/server/instrumentation.server.js'
]);
expect(test_builder.instrument).toHaveBeenCalledWith({
@@ -170,6 +198,14 @@ test('the runtime reader reuses the generated Bun file', () => {
expect(source).not.toContain('asset_path');
});
+test('publishes the runtime sources without a stale build lifecycle', () => {
+ const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
+ expect(pkg.files).toContain('src/*.js');
+ expect(pkg.files).not.toContain('files');
+ expect(pkg.scripts.build).toBeUndefined();
+ expect(pkg.scripts.prepublishOnly).toBeUndefined();
+});
+
function mock_embedded_files({
client = [],
pages = [],
@@ -210,6 +246,7 @@ function builder({
routes = [],
server_assets = [],
app_path = '_app',
+ base = '',
instrumentation = false
}: {
client_files?: string[];
@@ -218,10 +255,11 @@ function builder({
routes?: Array<{ id: string; prerender: boolean | string }>;
server_assets?: string[];
app_path?: string;
+ base?: string;
instrumentation?: boolean;
} = {}) {
return {
- config: { kit: { outDir: '.svelte-kit', paths: { base: '', origin: undefined } } },
+ config: { kit: { outDir: '.svelte-kit', paths: { base, origin: undefined } } },
routes,
prerendered: { pages: new Map(prerendered_pages) },
log: { minor() {}, error() {}, warn() {}, info() {} },
diff --git a/packages/adapter-bun/package.json b/packages/adapter-bun/package.json
index 5abaeb389c70..50e09c9001da 100644
--- a/packages/adapter-bun/package.json
+++ b/packages/adapter-bun/package.json
@@ -27,19 +27,16 @@
},
"types": "index.d.ts",
"files": [
- "files",
+ "src/*.js",
"index.js",
"index.d.ts",
"ambient.d.ts"
],
"scripts": {
- "dev": "bun --watch build.js",
- "build": "bun build.js",
"test": "vitest run",
"check": "tsc",
"lint": "prettier --check .",
- "format": "pnpm lint --write",
- "prepublishOnly": "pnpm build"
+ "format": "pnpm lint --write"
},
"devDependencies": {
"@playwright/test": "catalog:",
diff --git a/packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt b/packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt
new file mode 100644
index 000000000000..f57fb296b834
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt
@@ -0,0 +1 @@
+adapter bun
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 7aa80ae104ac..9af38d2038ab 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -79,6 +79,12 @@ test('serves URL-encoded static filenames', async ({ request }) => {
expect(await response.text()).toBe('hello from an encoded filename\n');
});
+test('serves dotfiles from the static directory', async ({ request }) => {
+ const response = await request.get('/.well-known/adapter-bun.txt');
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toBe('adapter bun\n');
+});
+
test('reads an imported server asset', async ({ request }) => {
const response = await request.get('/read');
expect(response.status()).toBe(200);
@@ -101,9 +107,9 @@ test('uses Bun route method semantics for static files', async ({ request }) =>
});
test('redirects prerendered paths to their canonical trailing slash', async ({ request }) => {
- const response = await request.get('/prerendered', { maxRedirects: 0 });
+ const response = await request.get('/prerendered?source=test', { maxRedirects: 0 });
expect(response.status()).toBe(308);
- expect(response.headers()['location']).toBe('/prerendered/');
+ expect(response.headers()['location']).toBe('/prerendered/?source=test');
});
test('configures long-lived event streams', async ({ request }) => {
From fc7d844c9b5e7323ae509c4e33a5284a50d3192d Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 14:27:11 +0300
Subject: [PATCH 31/94] feat(adapter-bun): refactor file handling and enhance
virtual file management in server options
---
packages/adapter-bun/index.js | 20 +++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 53fe02420d9a..a75e34b6eaef 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -41,8 +41,11 @@ export default function (opts = {}) {
const server = builder.getServerDirectory();
const manifest_file = `${server}/adapter-bun-manifest.js`;
- const routes_file = `${server}/adapter-bun-routes.js`;
const server_options_file = `${server}/adapter-bun-options.js`;
+ const src_dir = resolve(import.meta.dirname, 'src');
+ const index_file = resolve(src_dir, 'index.js');
+ const routes_file = resolve(src_dir, 'routes.js');
+
const virtual_files = {
[manifest_file]: `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n`,
[server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`,
@@ -52,19 +55,14 @@ export default function (opts = {}) {
embed: !!buildOptions.compile
})
};
- const src_dir = resolve(import.meta.dirname, 'src');
- const index_file = resolve(src_dir, 'index.js');
- const start_file = resolve(src_dir, 'start.js');
const instrumentation = builder.hasServerInstrumentationFile()
? `${server}/instrumentation.server.js`
: undefined;
if (instrumentation) {
- // Virtually rename index.js to start.js
+ const start_file = resolve(src_dir, 'start.js'); // Virtual only
virtual_files[start_file] = await Bun.file(index_file).text();
-
- // Virtually create a new index.js that imports the instrumentation and then starts the server
virtual_files[index_file] = [
`import ${JSON.stringify(instrumentation)};`,
`await import(${JSON.stringify(start_file)});`
@@ -87,6 +85,7 @@ export default function (opts = {}) {
const result = await Bun.build({
...buildOptions,
splitting: true,
+ sourcemap: 'external',
entrypoints: [index_file],
target: 'bun',
format: 'esm',
@@ -148,6 +147,9 @@ async function create_routes({ builder, out, embed }) {
const base = builder.config.kit.paths.base || '/';
const builtFiles = `${builder.config.kit.outDir}/output`;
+ const imports = embed ? [] : [`import { dirname, resolve } from 'node:path';`];
+ const declarations = embed ? [] : [`const dir = dirname(Bun.main);`];
+
const client_files = embed
? await read_files_recursive(`${builtFiles}/client`)
: builder
@@ -185,7 +187,7 @@ async function create_routes({ builder, out, embed }) {
asset = `asset_${asset_imports.length}`;
asset_imports.push(`import ${asset} from ${JSON.stringify(abspath)} with { type: 'file' };`);
} else {
- asset = `resolve(import.meta.dir, ${JSON.stringify(relpath)})`;
+ asset = `resolve(dir, ${JSON.stringify(relpath)})`;
}
const identifier = `file_${file_entries.length}`;
@@ -243,7 +245,6 @@ async function create_routes({ builder, out, embed }) {
entries.push({ path: posix.join(base, rel), value: make_response(file, abs) });
}
- const imports = embed ? [] : [`import { resolve } from 'node:path';`];
const server_assets = builder.findServerAssets(
builder.routes.filter((route) => route.prerender !== true)
);
@@ -262,6 +263,7 @@ async function create_routes({ builder, out, embed }) {
return [
...imports,
...asset_imports,
+ ...declarations,
...file_entries,
readable_files,
`export const routes = {${routes.join(',\n')}};`
From baf935973e19843aec6cd7825030204980fe6843 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 14:31:53 +0300
Subject: [PATCH 32/94] feat(adapter-bun): change export of unix and server to
const for better encapsulation
---
packages/adapter-bun/src/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index ba95ed74123e..1c19842a0d2f 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -6,7 +6,7 @@ import { boolean_env, env, number_env } from './env.js';
const options = /** @type {import('bun').Serve.Options} */ ({ ...server_options });
-export const unix = env('SOCKET_PATH', options.unix);
+const unix = env('SOCKET_PATH', options.unix);
if (unix) {
options.unix = unix;
@@ -35,7 +35,7 @@ options.maxRequestBodySize = number_env('BODY_SIZE_LIMIT', options.maxRequestBod
options.fetch = handler;
options.routes = routes;
-export const server = Bun.serve(options);
+const server = Bun.serve(options);
console.log(unix ? `Listening on ${unix}` : `Listening on ${server.url}`);
From 130d737532937560ed9288a6d64c1a19a51d052f Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 15:07:32 +0300
Subject: [PATCH 33/94] feat(adapter-bun): enhance build options and add
bytes_env utility for better environment variable handling
---
.github/workflows/platform-tests-bun.yml | 4 --
.../25-build-and-deploy/45-adapter-bun.md | 57 ++++++-------------
packages/adapter-bun/index.d.ts | 11 ++--
packages/adapter-bun/index.js | 13 +++--
packages/adapter-bun/index.spec.ts | 52 +++++++++--------
packages/adapter-bun/src/env.js | 32 +++++++++++
packages/adapter-bun/src/env.spec.ts | 26 ++++++++-
packages/adapter-bun/src/handler.js | 3 +-
packages/adapter-bun/src/index.js | 4 +-
9 files changed, 118 insertions(+), 84 deletions(-)
diff --git a/.github/workflows/platform-tests-bun.yml b/.github/workflows/platform-tests-bun.yml
index 8c621db7e2b6..ed28bce1aec8 100644
--- a/.github/workflows/platform-tests-bun.yml
+++ b/.github/workflows/platform-tests-bun.yml
@@ -38,10 +38,6 @@ jobs:
with:
bun-version: 1.3.14
- - name: Build adapter-bun files
- working-directory: packages/adapter-bun
- run: pnpm prepublishOnly
-
- uses: ./.github/actions/platform-test
with:
test-app-dir: packages/adapter-bun/test/apps/basic
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 1ce2f98d6952..f6081519882d 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -57,7 +57,9 @@ export default defineConfig({
serverOptions: {
idleTimeout: 30
},
- compile: false
+ buildOptions: {
+ sourcemap: 'external'
+ }
})
})
]
@@ -76,16 +78,21 @@ Adds a prefix to all environment variables read by the production server. For ex
Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings such as `hostname`, `port`, `reusePort`, `ipv6Only`, `idleTimeout`, `development`, and `maxRequestBodySize`. Environment variables override these defaults.
-`fetch`, `routes`, `websocket`, `error`, `tls`, `http3`, and `http1` cannot be configured this way. To use those APIs, create a [custom server](#Custom-server).
+`fetch`, `routes`, `websocket`, `error`, `tls`, `http3`, and `http1` cannot be configured this way. The adapter does not generate a reusable request-handler entrypoint, so applications that require these options need a custom Bun integration instead of the generated server.
-### compile
+### buildOptions
-Set `compile: true` to generate `build/app`, a single executable containing the Bun runtime, your
+Pass options to Bun's build API through `buildOptions`. Set `buildOptions.compile: true` to generate
+`build/server`, a single executable containing the Bun runtime, your
server code, client assets, and prerendered pages. In this mode, the adapter builds the executable
directly instead of generating the JavaScript server files:
```js
-adapter({ compile: true });
+adapter({
+ buildOptions: {
+ compile: true
+ }
+});
```
The adapter uses the [`Bun.build`](https://bun.com/reference/bun/build) JavaScript API directly. The
@@ -99,9 +106,9 @@ With the default options, only the executable is required at runtime. It is spec
```js
adapter({
- compile: {
+ buildOptions: {
compile: {
- outfile: 'build/my-app',
+ outfile: 'my-app',
target: 'bun-linux-x64'
},
minify: true,
@@ -113,9 +120,9 @@ adapter({
Native dependencies and cross-compilation have the same constraints as [Bun's single-file executables](https://bun.com/docs/bundler/executables).
The adapter reserves the top-level Bun build `target` and `format` because generated servers always
-run as Bun ESM. Set the executable target inside `compile.target`, as shown above. Minification,
-sourcemaps, and bytecode remain opt-in. Advanced compile options without an explicit `outfile` or
-`outdir` use `build/app`.
+run as Bun ESM. Set the executable target inside `buildOptions.compile.target`, as shown above.
+Source maps default to `external`; set `sourcemap: 'none'` to disable them. Minification and bytecode
+remain opt-in. Compile options without an explicit `outfile` use `/server`.
## Environment variables
@@ -189,33 +196,3 @@ export function GET({ platform }) {
});
}
```
-
-## Custom server
-
-The build contains `index.js`, which starts the default server, and `handler.js`, which exports the Bun-native SvelteKit request handler. Import the handler when you need Bun routes, WebSockets, custom error handling, or other `Bun.serve` options that cannot be represented as JSON:
-
-```js
-/// file: server.js
-import { handler } from './build/handler.js';
-
-const server = Bun.serve({
- port: 3000,
- routes: {
- '/health': new Response('ok')
- },
- websocket: {
- message(socket, message) {
- socket.send(message);
- }
- },
- fetch: handler,
- error(error) {
- console.error(error);
- return new Response('Internal Server Error', { status: 500 });
- }
-});
-
-console.log(`Listening on ${server.url}`);
-```
-
-When using a custom server, implement lifecycle behavior such as signal handling and static file serving yourself. The handler only serves dynamic SvelteKit requests and reads the proxy-header environment variables described above.
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 8740a638b90c..84b51f1aa288 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -24,14 +24,13 @@ interface AdapterOptions {
* ```sh
* MY_CUSTOM_HOST=127.0.0.1 \
* MY_CUSTOM_PORT=4000 \
- * node build
+ * bun ./build
* ```
*/
envPrefix?: string;
/**
* Default options passed to `Bun.serve`. Environment variables take precedence.
- * The options must be JSON-serializable. Use `build/handler.js` with a custom
- * `Bun.serve` call for routes, WebSockets, or custom error handling.
+ * The options must be JSON-serializable.
*/
serverOptions?: Pick<
import('bun').Serve.Options,
@@ -45,10 +44,10 @@ interface AdapterOptions {
| 'ipv6Only'
>;
/**
- * Build the server and static assets.
* Pass Bun build options directly for advanced configuration. The generated entrypoint,
- * top-level target, and module format are reserved. If neither an outfile nor outdir is
- * specified, the executable is written to `/server`.
+ * output directory, top-level target, and module format are reserved. Set `compile` to
+ * create an executable; if it does not specify an outfile, the executable is written to
+ * `/server`.
* @default false
*/
buildOptions?: Pick<
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index a75e34b6eaef..dbebd85cf968 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,4 +1,4 @@
-import { resolve, posix } from 'node:path';
+import { relative, resolve, posix } from 'node:path';
import { readdir } from 'node:fs/promises';
/**
@@ -12,7 +12,7 @@ async function read_files_recursive(path) {
.filter((entry) => entry.isFile())
.map((entry) => {
const abs = resolve(entry.parentPath, entry.name);
- const rel = posix.relative(path, abs);
+ const rel = posixify(relative(path, abs));
return { abs, rel };
})
.filter(({ rel }) => rel.split('/').every((segment) => segment !== '.vite'));
@@ -85,7 +85,7 @@ export default function (opts = {}) {
const result = await Bun.build({
...buildOptions,
splitting: true,
- sourcemap: 'external',
+ sourcemap: buildOptions.sourcemap ?? 'external',
entrypoints: [index_file],
target: 'bun',
format: 'esm',
@@ -181,7 +181,7 @@ async function create_routes({ builder, out, embed }) {
* @returns {string}
*/
function make_file(file, abspath) {
- const relpath = posix.relative(resolve(out), abspath);
+ const relpath = posixify(relative(resolve(out), abspath));
let asset;
if (embed) {
asset = `asset_${asset_imports.length}`;
@@ -277,3 +277,8 @@ async function create_routes({ builder, out, embed }) {
function encode_pathname(pathname) {
return pathname.split('/').map(encodeURIComponent).join('/');
}
+
+/** @param {string} path */
+function posixify(path) {
+ return path.replace(/\\/g, '/');
+}
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 11107ffd83be..1d3a715046ca 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -3,14 +3,20 @@ import { readdir } from 'node:fs/promises';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import adapter from './index.js';
+const index_file = new URL('./src/index.js', import.meta.url).pathname;
+const routes_file = new URL('./src/routes.js', import.meta.url).pathname;
+const start_file = new URL('./src/start.js', import.meta.url).pathname;
+
vi.mock('node:fs/promises', async (import_original) => {
const actual = await import_original();
return { ...actual, readdir: vi.fn() };
});
-const { build, file } = vi.hoisted(() => {
+const { adapter_entrypoint, build, file } = vi.hoisted(() => {
+ const adapter_entrypoint = '// adapter entrypoint';
const build = vi.fn((_options: any) => ({ success: true, logs: [], outputs: [] }));
const file = vi.fn((path: string) => ({
+ text: () => adapter_entrypoint,
type: path.endsWith('.html')
? 'text/html;charset=utf-8'
: path.endsWith('.json')
@@ -18,7 +24,7 @@ const { build, file } = vi.hoisted(() => {
: 'text/plain;charset=utf-8'
}));
vi.stubGlobal('Bun', { build, file });
- return { build, file };
+ return { adapter_entrypoint, build, file };
});
beforeEach(() => {
@@ -43,6 +49,7 @@ describe('Bun build options', () => {
format: 'esm',
conditions: ['bun', 'node'],
outdir: 'build',
+ sourcemap: 'external',
compile: false
});
expect(options.plugins[0].name).toBe('adapter-bun');
@@ -62,14 +69,13 @@ describe('Bun build options', () => {
await adapter().adapt(test_builder);
expect(test_builder.findServerAssets).toHaveBeenCalledWith([active_route]);
- const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
- expect(source).toContain(
- 'const file_0 = Bun.file(resolve(import.meta.dir, "client/data.json"))'
- );
+ const source = build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain(`const dir = dirname(Bun.main);`);
+ expect(source).toContain('const file_0 = Bun.file(resolve(dir, "client/data.json"))');
expect(source).toContain('"/data.json": file_0');
expect(source).toContain('new Response(file_2, { headers:');
expect(source).toContain(
- 'const file_3 = Bun.file(resolve(import.meta.dir, "prerendered/prerendered/index.html"))'
+ 'const file_3 = Bun.file(resolve(dir, "prerendered/prerendered/index.html"))'
);
expect(source).toContain(
'export const server_assets = new Map([["_app/immutable/assets/read.txt", file_2]])'
@@ -94,7 +100,7 @@ describe('Bun build options', () => {
);
const options = build.mock.calls[0][0];
- const source = options.files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ const source = options.files[routes_file];
expect(options.compile).toEqual({ outfile: 'server' });
expect(source).toContain("with { type: 'file' }");
expect(source).toContain('const file_0 = Bun.file(asset_0)');
@@ -111,7 +117,7 @@ describe('Bun build options', () => {
await adapter({ buildOptions: { compile: true } }).adapt(builder());
- const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ const source = build.mock.calls[0][0].files[routes_file];
expect(source).toContain('assetlinks.json');
expect(source).toContain('"/.well-known/assetlinks.json"');
expect(source).not.toContain('.vite/manifest.json');
@@ -127,7 +133,7 @@ describe('Bun build options', () => {
})
);
- const source = build.mock.calls[0][0].files['.svelte-kit/output/server/adapter-bun-routes.js'];
+ const source = build.mock.calls[0][0].files[routes_file];
expect(source).toContain('"/base/prerendered/": file_0');
expect(source).toContain('"/base/prerendered": (request) => Response.redirect');
expect(source).toContain('new URL(request.url).search');
@@ -163,18 +169,13 @@ describe('Bun build options', () => {
await adapter({ out: 'dist' }).adapt(test_builder);
- expect(build.mock.calls[0][0].entrypoints).toEqual([
- new URL('./src/index.js', import.meta.url).pathname,
- new URL('./src/handler.js', import.meta.url).pathname,
- '.svelte-kit/output/server/instrumentation.server.js'
- ]);
- expect(test_builder.instrument).toHaveBeenCalledWith({
- entrypoint: 'dist/index.js',
- instrumentation: 'dist/instrumentation.server.js',
- module: {
- exports: ['server', 'unix']
- }
- });
+ const options = build.mock.calls[0][0];
+ expect(options.entrypoints).toEqual([index_file]);
+ expect(options.files[index_file]).toBe(
+ `import ".svelte-kit/output/server/instrumentation.server.js";\nawait import(${JSON.stringify(start_file)});`
+ );
+ expect(options.files[start_file]).toBe(adapter_entrypoint);
+ expect(test_builder.instrument).not.toHaveBeenCalled();
});
test('runs server instrumentation before starting a compiled executable', async () => {
@@ -183,10 +184,11 @@ describe('Bun build options', () => {
await adapter({ buildOptions: { compile: true } }).adapt(test_builder);
const options = build.mock.calls[0][0];
- expect(options.entrypoints).toEqual(['.svelte-kit/output/server/adapter-bun-instrumented.js']);
- expect(options.files[options.entrypoints[0]]).toBe(
- `import './instrumentation.server.js';\nawait import(${JSON.stringify(new URL('./src/index.js', import.meta.url).pathname)});`
+ expect(options.entrypoints).toEqual([index_file]);
+ expect(options.files[index_file]).toBe(
+ `import ".svelte-kit/output/server/instrumentation.server.js";\nawait import(${JSON.stringify(start_file)});`
);
+ expect(options.files[start_file]).toBe(adapter_entrypoint);
expect(test_builder.instrument).not.toHaveBeenCalled();
});
});
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index 064d3d1628df..35dc26dc1e22 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -89,3 +89,35 @@ export function number_env(name, fallback, limits = {}) {
return number;
}
+
+/**
+ * @param {string} name
+ * @param {number | undefined} [fallback]
+ * @returns {number | undefined}
+ */
+export function bytes_env(name, fallback) {
+ const value = env(name);
+ if (value === undefined) return fallback;
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:[KMG])?$/i.test(value)) {
+ throw new Error(
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative number with an optional K, M, or G suffix)`
+ );
+ }
+
+ const suffix = value.at(-1)?.toUpperCase();
+ const multiplier =
+ {
+ K: 1024,
+ M: 1024 * 1024,
+ G: 1024 * 1024 * 1024
+ }[/** @type {'K' | 'M' | 'G'} */ (suffix)] ?? 1;
+ const number = Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier;
+
+ if (!Number.isSafeInteger(number)) {
+ throw new Error(
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative number of whole bytes)`
+ );
+ }
+
+ return number;
+}
diff --git a/packages/adapter-bun/src/env.spec.ts b/packages/adapter-bun/src/env.spec.ts
index abeb41c86186..8770f8a4857b 100644
--- a/packages/adapter-bun/src/env.spec.ts
+++ b/packages/adapter-bun/src/env.spec.ts
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
-import { boolean_env, number_env } from './env.js';
+import { boolean_env, bytes_env, number_env } from './env.js';
vi.hoisted(() => {
vi.stubGlobal('ENV_PREFIX', '');
@@ -48,3 +48,27 @@ describe('number_env', () => {
expect(() => number_env('OPTION')).toThrow('expected a non-negative integer');
});
});
+
+describe('bytes_env', () => {
+ afterEach(() => vi.unstubAllEnvs());
+
+ test.each([
+ ['0', 0],
+ ['512', 512],
+ ['512K', 512 * 1024],
+ ['1.5M', 1.5 * 1024 * 1024],
+ ['2g', 2 * 1024 * 1024 * 1024]
+ ])('parses %s as a byte count', (value, expected) => {
+ vi.stubEnv('OPTION', value);
+ expect(bytes_env('OPTION')).toBe(expected);
+ });
+
+ test('uses the fallback when the variable is not set', () => {
+ expect(bytes_env('OPTION', 512 * 1024)).toBe(512 * 1024);
+ });
+
+ test.each(['', '-1', '1KB', 'one', '0.1'])('rejects invalid byte counts', (value) => {
+ vi.stubEnv('OPTION', value);
+ expect(() => bytes_env('OPTION')).toThrow('Invalid value for environment variable OPTION');
+ });
+});
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 04d091a337d7..66ab0729bf04 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -18,8 +18,7 @@ await server.init({
});
/**
- * The Bun-native SvelteKit request handler. Import it from `build/handler.js`
- * when an application needs to construct `Bun.serve` itself.
+ * The Bun-native SvelteKit request handler used by the generated server.
* @param {Request} request
* @param {import('bun').Server} bun_server
* @returns {Promise}
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 1c19842a0d2f..501e40b245d5 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -2,7 +2,7 @@ import process from 'node:process';
import server_options from 'SERVER_OPTIONS';
import { routes } from 'ROUTES';
import { handler } from './handler.js';
-import { boolean_env, env, number_env } from './env.js';
+import { boolean_env, bytes_env, env, number_env } from './env.js';
const options = /** @type {import('bun').Serve.Options} */ ({ ...server_options });
@@ -30,7 +30,7 @@ if (development !== undefined) {
options.development = false;
}
-options.maxRequestBodySize = number_env('BODY_SIZE_LIMIT', options.maxRequestBodySize);
+options.maxRequestBodySize = bytes_env('BODY_SIZE_LIMIT', options.maxRequestBodySize ?? 512 * 1024);
options.fetch = handler;
options.routes = routes;
From e65353c57a59e157d13d75f9812b99a737792144 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 20:19:59 +0300
Subject: [PATCH 34/94] feat(adapter-bun): fix port environment variable
handling for better clarity
---
packages/adapter-bun/src/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 501e40b245d5..e9ce41fbb31b 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -17,7 +17,7 @@ if (unix) {
} else {
delete options.unix;
options.hostname = env('HOST', options.hostname);
- options.port = env('PORT', options.port ? String(options.port) : undefined);
+ options.port = env('PORT', options.port !== undefined ? String(options.port) : undefined);
options.reusePort = boolean_env('REUSE_PORT', options.reusePort);
options.ipv6Only = boolean_env('IPV6_ONLY', options.ipv6Only);
}
From 8133d21e1f82d2908037285b7b743b5dece8e5a9 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 20:23:36 +0300
Subject: [PATCH 35/94] feat(adapter-bun): implement routes utility functions
and enhance manifest handling
---
packages/adapter-bun/index.js | 95 +++++++++++++++++++++----
packages/adapter-bun/internal.d.ts | 6 +-
packages/adapter-bun/src/routes-util.js | 88 +++++++++++++++++++++++
3 files changed, 172 insertions(+), 17 deletions(-)
create mode 100644 packages/adapter-bun/src/routes-util.js
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index dbebd85cf968..90e9a41fe73c 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -40,14 +40,17 @@ export default function (opts = {}) {
const server = builder.getServerDirectory();
- const manifest_file = `${server}/adapter-bun-manifest.js`;
- const server_options_file = `${server}/adapter-bun-options.js`;
const src_dir = resolve(import.meta.dirname, 'src');
const index_file = resolve(src_dir, 'index.js');
const routes_file = resolve(src_dir, 'routes.js');
+ const manifest_file = resolve(src_dir, 'manifest.js');
+ const server_options_file = resolve(src_dir, 'options.js');
const virtual_files = {
- [manifest_file]: `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n`,
+ [manifest_file]:
+ `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n` +
+ `export const base = ${builder.config.kit.paths.base || '/'};\n` +
+ `export const embed = ${!!buildOptions.compile};\n`,
[server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`,
[routes_file]: await create_routes({
builder,
@@ -135,6 +138,57 @@ export default function (opts = {}) {
};
}
+/**
+ * @param {object} options
+ * @param {import('@sveltejs/kit').Builder} options.builder
+ * @returns {Promise}
+ */
+async function create_routes_embed({ builder }) {
+ const builtFiles = `${builder.config.kit.outDir}/output`;
+
+ const [cl_files, pr_pages, pr_deps, pr_data] = await Promise.all([
+ read_files_recursive(`${builtFiles}/client`),
+ read_files_recursive(`${builtFiles}/prerendered/pages`),
+ read_files_recursive(`${builtFiles}/prerendered/dependencies`),
+ read_files_recursive(`${builtFiles}/prerendered/data`)
+ ]);
+
+ builder.prerendered.pages;
+
+ const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
+
+ const asset_imports = assets.map(({ abs }, i) => {
+ return `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`;
+ });
+
+ const cl_entries = cl_files.map(({ rel }, i) => {
+ return `client_asset(${JSON.stringify(rel)}, asset_${i})`;
+ });
+ const pr_pages_entries = [...builder.prerendered.pages].map(([path, { file }]) => {
+ const fileIdx = pr_pages.findIndex((f) => f.rel === file);
+ if (fileIdx === -1)
+ throw new Error(`Could not find prerendered page ${file} for route ${path}`);
+ return `...prerendered_page(${JSON.stringify(path)}, asset_${cl_files.length + fileIdx})`;
+ });
+ const pr_assets_entries = [...pr_deps, ...pr_data].map(({ rel }, i) => {
+ return `prerendered_asset(${JSON.stringify(rel)}, asset_${cl_files.length + pr_pages.length + i})`;
+ });
+ const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
+ return `prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
+ });
+
+ return [
+ `import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect } from './routes-util.js';`,
+ ...asset_imports,
+ `export const client_routes = Object.fromEntries([${[
+ ...cl_entries,
+ ...pr_pages_entries,
+ ...pr_assets_entries,
+ ...pr_redirects
+ ].join(',\n')}]);`
+ ].join('\n');
+}
+
/**
* @param {object} options
* @param {import('@sveltejs/kit').Builder} options.builder
@@ -147,14 +201,17 @@ async function create_routes({ builder, out, embed }) {
const base = builder.config.kit.paths.base || '/';
const builtFiles = `${builder.config.kit.outDir}/output`;
- const imports = embed ? [] : [`import { dirname, resolve } from 'node:path';`];
+ const imports = [
+ `import { manifest } from 'MANIFEST';`,
+ ...(embed ? [] : [`import { dirname, resolve } from 'node:path';`])
+ ];
const declarations = embed ? [] : [`const dir = dirname(Bun.main);`];
const client_files = embed
? await read_files_recursive(`${builtFiles}/client`)
: builder
.writeClient(`${out}/client`)
- .map((rel) => ({ rel, abs: resolve(`${out}/client`, rel) }));
+ .map((rel) => ({ rel, abs: resolve(out, 'client', rel) }));
const prerendered_files = embed
? (
@@ -166,7 +223,7 @@ async function create_routes({ builder, out, embed }) {
).flat()
: builder
.writePrerendered(`${out}/prerendered`)
- .map((rel) => ({ rel, abs: resolve(`${out}/prerendered`, rel) }));
+ .map((rel) => ({ rel, abs: resolve(out, 'prerendered', rel) }));
/** @type {string[]} */
const asset_imports = [];
@@ -198,19 +255,19 @@ async function create_routes({ builder, out, embed }) {
/**
* @param {string} file
+ * @param {string} pathname
* @param {string} abspath
* @param {boolean} [immutable]
* @returns {string}
*/
- function make_response(file, abspath, immutable = false) {
- if (!embed && !immutable) return file;
-
+ function make_response(file, pathname, abspath, immutable = false) {
/** @type {Record} */
const headers = {};
- if (embed) headers['content-type'] = Bun.file(abspath).type;
if (immutable) headers['cache-control'] = 'public,max-age=31536000,immutable';
- return `new Response(${file}, { headers: ${JSON.stringify(headers)} })`;
+ return `file_response(${file}, ${JSON.stringify(pathname)}, ${JSON.stringify(
+ Bun.file(abspath).type
+ )}, ${JSON.stringify(headers)})`;
}
/** @type {Array<{ path: string; value: string }>} */
@@ -220,7 +277,7 @@ async function create_routes({ builder, out, embed }) {
const path = posix.join(base, rel);
const immutable = path.startsWith(`/${app_path}/immutable/`);
const file = make_file(`client/${rel}`, abs);
- entries.push({ path, value: make_response(file, abs, immutable) });
+ entries.push({ path, value: make_response(file, path, abs, immutable) });
}
for (const [path, { file }] of builder.prerendered.pages) {
@@ -229,7 +286,7 @@ async function create_routes({ builder, out, embed }) {
throw new Error(`Could not find prerendered page ${file} for route ${path}`);
const { abs, rel } = prerendered_files.splice(fileIdx, 1)[0];
const bun_file = make_file(`prerendered/${rel}`, abs);
- entries.push({ path, value: make_response(bun_file, abs) });
+ entries.push({ path, value: make_response(bun_file, path, abs) });
const inverted = path.endsWith('/') ? path.slice(0, -1) : `${path}/`;
if (inverted) {
@@ -240,9 +297,14 @@ async function create_routes({ builder, out, embed }) {
}
}
+ for (const [path, { status, location }] of builder.prerendered.redirects) {
+ entries.push({ path, value: `Response.redirect(${JSON.stringify(location)}, ${status})` });
+ }
+
for (const { abs, rel } of prerendered_files) {
const file = make_file(`prerendered/${rel}`, abs);
- entries.push({ path: posix.join(base, rel), value: make_response(file, abs) });
+ const path = posix.join(base, rel);
+ entries.push({ path, value: make_response(file, path, abs) });
}
const server_assets = builder.findServerAssets(
@@ -266,6 +328,11 @@ async function create_routes({ builder, out, embed }) {
...declarations,
...file_entries,
readable_files,
+ `function file_response(file, pathname, fallback, headers) {
+ const type = manifest.mimeTypes[pathname.slice(pathname.lastIndexOf('.'))] || fallback;
+ if (type) headers['content-type'] = type;
+ return new Response(file, { headers });
+ }`,
`export const routes = {${routes.join(',\n')}};`
].join('\n');
}
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 100eafc2bebe..788451163c18 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -1,7 +1,7 @@
declare module 'MANIFEST' {
- import type { SSRManifest } from '@sveltejs/kit';
-
- export const manifest: SSRManifest;
+ export const manifest: import('@sveltejs/kit').SSRManifest;
+ export const base: string;
+ export const embed: boolean;
}
declare module 'ROUTES' {
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
new file mode 100644
index 000000000000..3a5aab5c34a1
--- /dev/null
+++ b/packages/adapter-bun/src/routes-util.js
@@ -0,0 +1,88 @@
+import { manifest, base, embed } from 'MANIFEST';
+import { dirname, resolve, posix } from 'node:path';
+
+const dir = dirname(Bun.main);
+
+/**
+ * @param {string} pathname
+ * @returns {string}
+ */
+function encode_pathname(pathname) {
+ return pathname.split('/').map(encodeURIComponent).join('/');
+}
+
+/**
+ * @param {string} urlPath
+ * @returns {string}
+ */
+function to_path(urlPath) {
+ return encode_pathname(posix.join(base, urlPath));
+}
+
+/**
+ * @param {string} urlPath
+ * @param {string} [filePath]
+ * @returns {[string, Response]}
+ */
+export function client_asset(urlPath, filePath = urlPath) {
+ const file = Bun.file(embed ? filePath : resolve(dir, 'client', filePath));
+
+ /** @type {Record} */
+ const headers = { 'content-type': file.type };
+
+ if (urlPath.startsWith(`/${manifest.appPath}/immutable/`)) {
+ headers['cache-control'] = 'public,max-age=31536000,immutable';
+ }
+
+ return [to_path(urlPath), new Response(file, { headers })];
+}
+
+/**
+ * @param {string} urlPath
+ * @param {string} [filePath]
+ * @returns {[string, Response]}
+ */
+export function prerendered_asset(urlPath, filePath = urlPath) {
+ const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
+ const headers = { 'content-type': file.type };
+ return [to_path(urlPath), new Response(file, { headers })];
+}
+
+/**
+ * @param {string} urlPath
+ * @param {string} filePath
+ * @returns {[[string, Response], [string, function]]}
+ */
+export function prerendered_page(urlPath, filePath) {
+ /**
+ * @param {import('bun').BunRequest} req
+ * @returns {Response}
+ */
+ function handle_redirect(req) {
+ const url = new URL(req.url);
+ const location = `${urlPath}${url.search}`;
+ return new Response(null, { status: 301, headers: { location } });
+ }
+
+ const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
+ const headers = { 'content-type': file.type };
+
+ const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
+
+ // path already contains base, no need to call to_path here
+ return [
+ [encode_pathname(urlPath), new Response(file, { headers })],
+ [encode_pathname(inverted), handle_redirect]
+ ];
+}
+
+/**
+ * @param {string} urlPath
+ * @param {number} status
+ * @param {string} location
+ * @returns {[string, Response]}
+ */
+export function prerendered_redirect(urlPath, status, location) {
+ // path already contains base, no need to call to_path here
+ return [encode_pathname(urlPath), Response.redirect(location, status)];
+}
From 2cde6d91259d59edc0de098dab118cf3519f80b8 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 20:25:56 +0300
Subject: [PATCH 36/94] feat(adapter-bun): enhance routing and prerendering
with file_response and redirect handling
---
packages/adapter-bun/index.spec.ts | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 1d3a715046ca..907aab784d66 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -72,8 +72,12 @@ describe('Bun build options', () => {
const source = build.mock.calls[0][0].files[routes_file];
expect(source).toContain(`const dir = dirname(Bun.main);`);
expect(source).toContain('const file_0 = Bun.file(resolve(dir, "client/data.json"))');
- expect(source).toContain('"/data.json": file_0');
- expect(source).toContain('new Response(file_2, { headers:');
+ expect(source).toContain('"/data.json": file_response(file_0, "/data.json"');
+ expect(source).toContain('"/_app/immutable/assets/read.txt": file_response(file_2');
+ expect(source).toContain("import { manifest } from 'MANIFEST';");
+ expect(source).toContain(
+ "manifest.mimeTypes[pathname.slice(pathname.lastIndexOf('.'))] || fallback"
+ );
expect(source).toContain(
'const file_3 = Bun.file(resolve(dir, "prerendered/prerendered/index.html"))'
);
@@ -106,7 +110,7 @@ describe('Bun build options', () => {
expect(source).toContain('const file_0 = Bun.file(asset_0)');
expect(source).toContain('const file_1 = Bun.file(asset_1)');
expect(source).toContain('["_app/immutable/assets/read.txt", file_1]');
- expect(source).toContain('"/data.json": new Response(file_0');
+ expect(source).toContain('"/data.json": file_response(file_0, "/data.json"');
expect(source).not.toContain('export const files');
expect(source).not.toContain('files.get');
expect(source).not.toContain('asset_path');
@@ -134,12 +138,23 @@ describe('Bun build options', () => {
);
const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('"/base/prerendered/": file_0');
+ expect(source).toContain('"/base/prerendered/": file_response(file_0, "/base/prerendered/"');
expect(source).toContain('"/base/prerendered": (request) => Response.redirect');
expect(source).toContain('new URL(request.url).search');
expect(source).not.toContain('/base/base/');
});
+ test('serves prerendered redirects from their original paths', async () => {
+ await adapter().adapt(
+ builder({
+ prerendered_redirects: [['/old', { status: 301, location: '/new' }]]
+ })
+ );
+
+ const source = build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain('"/old": Response.redirect("/new", 301)');
+ });
+
test('passes supported advanced build options to Bun', async () => {
mock_embedded_files({ client: ['data.json'] });
@@ -245,6 +260,7 @@ function builder({
client_files = [],
prerendered_files = [],
prerendered_pages = [],
+ prerendered_redirects = [],
routes = [],
server_assets = [],
app_path = '_app',
@@ -254,6 +270,7 @@ function builder({
client_files?: string[];
prerendered_files?: string[];
prerendered_pages?: Array<[string, { file: string }]>;
+ prerendered_redirects?: Array<[string, { status: number; location: string }]>;
routes?: Array<{ id: string; prerender: boolean | string }>;
server_assets?: string[];
app_path?: string;
@@ -263,7 +280,10 @@ function builder({
return {
config: { kit: { outDir: '.svelte-kit', paths: { base, origin: undefined } } },
routes,
- prerendered: { pages: new Map(prerendered_pages) },
+ prerendered: {
+ pages: new Map(prerendered_pages),
+ redirects: new Map(prerendered_redirects)
+ },
log: { minor() {}, error() {}, warn() {}, info() {} },
getServerDirectory: () => '.svelte-kit/output/server',
rimraf() {},
From 421de00f2488cd44ae0095e46a8bddd99b30fe04 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 21:25:15 +0300
Subject: [PATCH 37/94] feat(adapter-bun): change redirect status from 301 to
308 for improved handling
---
packages/adapter-bun/index.js | 208 ++++++------------------
packages/adapter-bun/src/routes-util.js | 2 +-
2 files changed, 52 insertions(+), 158 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 90e9a41fe73c..9e9308a459a5 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,4 +1,4 @@
-import { relative, resolve, posix } from 'node:path';
+import { relative, resolve } from 'node:path';
import { readdir } from 'node:fs/promises';
/**
@@ -43,14 +43,14 @@ export default function (opts = {}) {
const src_dir = resolve(import.meta.dirname, 'src');
const index_file = resolve(src_dir, 'index.js');
const routes_file = resolve(src_dir, 'routes.js');
- const manifest_file = resolve(src_dir, 'manifest.js');
+ const manifest_file = resolve(server, 'manifest.js');
const server_options_file = resolve(src_dir, 'options.js');
const virtual_files = {
[manifest_file]:
`export const manifest = ${builder.generateManifest({ relativePath: './' })};\n` +
- `export const base = ${builder.config.kit.paths.base || '/'};\n` +
- `export const embed = ${!!buildOptions.compile};\n`,
+ `export const base = ${JSON.stringify(builder.config.kit.paths.base || '/')};\n` +
+ `export const embed = ${JSON.stringify(!!buildOptions.compile)};\n`,
[server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`,
[routes_file]: await create_routes({
builder,
@@ -141,9 +141,9 @@ export default function (opts = {}) {
/**
* @param {object} options
* @param {import('@sveltejs/kit').Builder} options.builder
- * @returns {Promise}
+ * @returns {Promise<{imports: string[], entries: string[]}>}
*/
-async function create_routes_embed({ builder }) {
+async function get_embed_entries({ builder }) {
const builtFiles = `${builder.config.kit.outDir}/output`;
const [cl_files, pr_pages, pr_deps, pr_data] = await Promise.all([
@@ -153,11 +153,9 @@ async function create_routes_embed({ builder }) {
read_files_recursive(`${builtFiles}/prerendered/data`)
]);
- builder.prerendered.pages;
-
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
- const asset_imports = assets.map(({ abs }, i) => {
+ const imports = assets.map(({ abs }, i) => {
return `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`;
});
@@ -177,174 +175,70 @@ async function create_routes_embed({ builder }) {
return `prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
});
- return [
- `import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect } from './routes-util.js';`,
- ...asset_imports,
- `export const client_routes = Object.fromEntries([${[
- ...cl_entries,
- ...pr_pages_entries,
- ...pr_assets_entries,
- ...pr_redirects
- ].join(',\n')}]);`
- ].join('\n');
+ return {
+ imports,
+ entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects]
+ };
}
/**
* @param {object} options
* @param {import('@sveltejs/kit').Builder} options.builder
* @param {string} options.out
- * @param {boolean} options.embed
- * @returns {Promise}
+ * @returns {{imports: string[], entries: string[]}}
*/
-async function create_routes({ builder, out, embed }) {
- const app_path = builder.getAppPath();
- const base = builder.config.kit.paths.base || '/';
- const builtFiles = `${builder.config.kit.outDir}/output`;
-
- const imports = [
- `import { manifest } from 'MANIFEST';`,
- ...(embed ? [] : [`import { dirname, resolve } from 'node:path';`])
- ];
- const declarations = embed ? [] : [`const dir = dirname(Bun.main);`];
-
- const client_files = embed
- ? await read_files_recursive(`${builtFiles}/client`)
- : builder
- .writeClient(`${out}/client`)
- .map((rel) => ({ rel, abs: resolve(out, 'client', rel) }));
+function get_no_embed_entries({ builder, out }) {
+ const client_files = builder.writeClient(`${out}/client`);
+ const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
- const prerendered_files = embed
- ? (
- await Promise.all([
- read_files_recursive(`${builtFiles}/prerendered/pages`),
- read_files_recursive(`${builtFiles}/prerendered/dependencies`),
- read_files_recursive(`${builtFiles}/prerendered/data`)
- ])
- ).flat()
- : builder
- .writePrerendered(`${out}/prerendered`)
- .map((rel) => ({ rel, abs: resolve(out, 'prerendered', rel) }));
-
- /** @type {string[]} */
- const asset_imports = [];
- /** @type {string[]} */
- const file_entries = [];
- /** @type {Map} */
- const file_identifiers = new Map();
-
- /**
- * @param {string} file
- * @param {string} abspath
- * @returns {string}
- */
- function make_file(file, abspath) {
- const relpath = posixify(relative(resolve(out), abspath));
- let asset;
- if (embed) {
- asset = `asset_${asset_imports.length}`;
- asset_imports.push(`import ${asset} from ${JSON.stringify(abspath)} with { type: 'file' };`);
- } else {
- asset = `resolve(dir, ${JSON.stringify(relpath)})`;
- }
-
- const identifier = `file_${file_entries.length}`;
- file_entries.push(`const ${identifier} = Bun.file(${asset});`);
- file_identifiers.set(file, identifier);
- return identifier;
- }
-
- /**
- * @param {string} file
- * @param {string} pathname
- * @param {string} abspath
- * @param {boolean} [immutable]
- * @returns {string}
- */
- function make_response(file, pathname, abspath, immutable = false) {
- /** @type {Record} */
- const headers = {};
- if (immutable) headers['cache-control'] = 'public,max-age=31536000,immutable';
-
- return `file_response(${file}, ${JSON.stringify(pathname)}, ${JSON.stringify(
- Bun.file(abspath).type
- )}, ${JSON.stringify(headers)})`;
- }
-
- /** @type {Array<{ path: string; value: string }>} */
- const entries = [];
-
- for (const { rel, abs } of client_files) {
- const path = posix.join(base, rel);
- const immutable = path.startsWith(`/${app_path}/immutable/`);
- const file = make_file(`client/${rel}`, abs);
- entries.push({ path, value: make_response(file, path, abs, immutable) });
- }
+ const cl_entries = client_files.map((filePath) => {
+ return `client_asset(${JSON.stringify(filePath)})`;
+ });
- for (const [path, { file }] of builder.prerendered.pages) {
- const fileIdx = prerendered_files.findIndex((f) => f.rel === file);
- if (fileIdx === -1)
- throw new Error(`Could not find prerendered page ${file} for route ${path}`);
- const { abs, rel } = prerendered_files.splice(fileIdx, 1)[0];
- const bun_file = make_file(`prerendered/${rel}`, abs);
- entries.push({ path, value: make_response(bun_file, path, abs) });
+ const prerendered_pages = [...builder.prerendered.pages];
+ const prerendered_pages_files = new Set(prerendered_pages.map(([_, { file }]) => file));
- const inverted = path.endsWith('/') ? path.slice(0, -1) : `${path}/`;
- if (inverted) {
- entries.push({
- path: inverted,
- value: `(request) => Response.redirect(${JSON.stringify(encode_pathname(path))} + new URL(request.url).search, 308)`
- });
- }
- }
+ const pr_pages_entries = prerendered_pages.map(([path, { file }]) => {
+ return `...prerendered_page(${JSON.stringify(path)}, ${JSON.stringify(file)})`;
+ });
- for (const [path, { status, location }] of builder.prerendered.redirects) {
- entries.push({ path, value: `Response.redirect(${JSON.stringify(location)}, ${status})` });
- }
+ const pr_assets_entries = prerendered_files
+ .filter((filePath) => !prerendered_pages_files.has(filePath))
+ .map((filePath) => {
+ return `prerendered_asset(${JSON.stringify(filePath)})`;
+ });
- for (const { abs, rel } of prerendered_files) {
- const file = make_file(`prerendered/${rel}`, abs);
- const path = posix.join(base, rel);
- entries.push({ path, value: make_response(file, path, abs) });
- }
+ const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
+ return `prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
+ });
- const server_assets = builder.findServerAssets(
- builder.routes.filter((route) => route.prerender !== true)
- );
- const readable_files = `export const server_assets = new Map([${server_assets
- .map((file) => {
- const identifier = file_identifiers.get(`client/${file}`);
- if (!identifier) throw new Error(`Could not find server asset ${file} in client output`);
- return `[${JSON.stringify(file)}, ${identifier}]`;
- })
- .join(',\n')}]);`;
+ return {
+ imports: [],
+ entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects]
+ };
+}
- const routes = entries.map(
- (entry) => `${JSON.stringify(encode_pathname(entry.path))}: ${entry.value}`
- );
+/**
+ * @param {object} options
+ * @param {import('@sveltejs/kit').Builder} options.builder
+ * @param {string} options.out
+ * @param {boolean} options.embed
+ * @returns {Promise}
+ */
+async function create_routes({ builder, out, embed }) {
+ const { imports, entries } = embed
+ ? await get_embed_entries({ builder })
+ : get_no_embed_entries({ builder, out });
return [
+ `// eslint-disable-next-line @typescript-eslint/no-unused-vars`,
+ `import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect } from './routes-util.js';`,
...imports,
- ...asset_imports,
- ...declarations,
- ...file_entries,
- readable_files,
- `function file_response(file, pathname, fallback, headers) {
- const type = manifest.mimeTypes[pathname.slice(pathname.lastIndexOf('.'))] || fallback;
- if (type) headers['content-type'] = type;
- return new Response(file, { headers });
- }`,
- `export const routes = {${routes.join(',\n')}};`
+ `export const routes = Object.fromEntries([${entries.join(',\n')}]);`,
+ `export const server_assets = new Map();`
].join('\n');
}
-/**
- * @param {string} pathname
- * @returns {string}
- */
-function encode_pathname(pathname) {
- return pathname.split('/').map(encodeURIComponent).join('/');
-}
-
/** @param {string} path */
function posixify(path) {
return path.replace(/\\/g, '/');
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 3a5aab5c34a1..68c03afb6ff9 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -61,7 +61,7 @@ export function prerendered_page(urlPath, filePath) {
function handle_redirect(req) {
const url = new URL(req.url);
const location = `${urlPath}${url.search}`;
- return new Response(null, { status: 301, headers: { location } });
+ return new Response(null, { status: 308, headers: { location } });
}
const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
From bbcc6459c8abaa5ee9e9f066329f06186f0a6415 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 21:58:44 +0300
Subject: [PATCH 38/94] feat(adapter-bun): update response handling to use
RouteHandler for client and prerendered assets
---
packages/adapter-bun/src/routes-util.js | 28 +++++++++++++++++--------
1 file changed, 19 insertions(+), 9 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 68c03afb6ff9..f2299bc16fd1 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -3,6 +3,10 @@ import { dirname, resolve, posix } from 'node:path';
const dir = dirname(Bun.main);
+/**
+ * @typedef {import('bun').Serve.Routes[string]} RouteHandler
+ */
+
/**
* @param {string} pathname
* @returns {string}
@@ -22,7 +26,7 @@ function to_path(urlPath) {
/**
* @param {string} urlPath
* @param {string} [filePath]
- * @returns {[string, Response]}
+ * @returns {[string, RouteHandler]}
*/
export function client_asset(urlPath, filePath = urlPath) {
const file = Bun.file(embed ? filePath : resolve(dir, 'client', filePath));
@@ -34,24 +38,26 @@ export function client_asset(urlPath, filePath = urlPath) {
headers['cache-control'] = 'public,max-age=31536000,immutable';
}
- return [to_path(urlPath), new Response(file, { headers })];
+ const resp = new Response(file, { headers });
+ return [to_path(urlPath), { GET: resp, HEAD: resp }];
}
/**
* @param {string} urlPath
* @param {string} [filePath]
- * @returns {[string, Response]}
+ * @returns {[string, RouteHandler]}
*/
export function prerendered_asset(urlPath, filePath = urlPath) {
const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
const headers = { 'content-type': file.type };
- return [to_path(urlPath), new Response(file, { headers })];
+ const resp = new Response(file, { headers });
+ return [to_path(urlPath), { GET: resp, HEAD: resp }];
}
/**
* @param {string} urlPath
* @param {string} filePath
- * @returns {[[string, Response], [string, function]]}
+ * @returns {[[string, RouteHandler], [string, RouteHandler]]}
*/
export function prerendered_page(urlPath, filePath) {
/**
@@ -69,10 +75,12 @@ export function prerendered_page(urlPath, filePath) {
const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
+ const resp = new Response(file, { headers });
+
// path already contains base, no need to call to_path here
return [
- [encode_pathname(urlPath), new Response(file, { headers })],
- [encode_pathname(inverted), handle_redirect]
+ [encode_pathname(urlPath), { GET: resp, HEAD: resp }],
+ [encode_pathname(inverted), { GET: handle_redirect, HEAD: handle_redirect }]
];
}
@@ -80,9 +88,11 @@ export function prerendered_page(urlPath, filePath) {
* @param {string} urlPath
* @param {number} status
* @param {string} location
- * @returns {[string, Response]}
+ * @returns {[string, RouteHandler]}
*/
export function prerendered_redirect(urlPath, status, location) {
+ const resp = new Response(null, { status, headers: { location } });
+
// path already contains base, no need to call to_path here
- return [encode_pathname(urlPath), Response.redirect(location, status)];
+ return [encode_pathname(urlPath), { GET: resp, HEAD: resp }];
}
From 401f9f24730331197112703ba46223d8633d3e56 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 22:08:45 +0300
Subject: [PATCH 39/94] feat(tests): add tests for serving static files and
update server response handling
---
packages/adapter-bun/index.spec.ts | 38 +++++--------------
.../basic/src/routes/data.json/+server.js | 3 ++
.../apps/basic/src/routes/read/+server.js | 2 +-
.../test/apps/basic/static/asterisk*.txt | 1 +
.../adapter-bun/test/apps/basic/test/test.js | 17 +++++++--
5 files changed, 29 insertions(+), 32 deletions(-)
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js
create mode 100644 packages/adapter-bun/test/apps/basic/static/asterisk*.txt
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 907aab784d66..382acb82d501 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -70,24 +70,12 @@ describe('Bun build options', () => {
expect(test_builder.findServerAssets).toHaveBeenCalledWith([active_route]);
const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain(`const dir = dirname(Bun.main);`);
- expect(source).toContain('const file_0 = Bun.file(resolve(dir, "client/data.json"))');
- expect(source).toContain('"/data.json": file_response(file_0, "/data.json"');
- expect(source).toContain('"/_app/immutable/assets/read.txt": file_response(file_2');
- expect(source).toContain("import { manifest } from 'MANIFEST';");
+ expect(source).toContain('client_asset("data.json")');
+ expect(source).toContain('client_asset("_app/immutable/assets/read.txt")');
+ expect(source).toContain('...prerendered_page("/prerendered/", "prerendered/index.html")');
expect(source).toContain(
- "manifest.mimeTypes[pathname.slice(pathname.lastIndexOf('.'))] || fallback"
+ 'export const server_assets = new Map([["_app/immutable/assets/read.txt", server_asset("_app/immutable/assets/read.txt")]])'
);
- expect(source).toContain(
- 'const file_3 = Bun.file(resolve(dir, "prerendered/prerendered/index.html"))'
- );
- expect(source).toContain(
- 'export const server_assets = new Map([["_app/immutable/assets/read.txt", file_2]])'
- );
- expect(source).not.toContain('["data.json", file_0]');
- expect(source).not.toContain('export const files');
- expect(source).not.toContain('files.get');
- expect(source).not.toContain('asset_path');
});
test('maps logical paths to embedded Bun files for executables', async () => {
@@ -107,13 +95,9 @@ describe('Bun build options', () => {
const source = options.files[routes_file];
expect(options.compile).toEqual({ outfile: 'server' });
expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('const file_0 = Bun.file(asset_0)');
- expect(source).toContain('const file_1 = Bun.file(asset_1)');
- expect(source).toContain('["_app/immutable/assets/read.txt", file_1]');
- expect(source).toContain('"/data.json": file_response(file_0, "/data.json"');
- expect(source).not.toContain('export const files');
- expect(source).not.toContain('files.get');
- expect(source).not.toContain('asset_path');
+ expect(source).toContain('client_asset("data.json", asset_0)');
+ expect(source).toContain('client_asset("_app/immutable/assets/read.txt", asset_1)');
+ expect(source).toContain('server_asset("_app/immutable/assets/read.txt")');
});
test('preserves dotfiles other than Vite build metadata in executables', async () => {
@@ -123,7 +107,7 @@ describe('Bun build options', () => {
const source = build.mock.calls[0][0].files[routes_file];
expect(source).toContain('assetlinks.json');
- expect(source).toContain('"/.well-known/assetlinks.json"');
+ expect(source).toContain('client_asset(".well-known/assetlinks.json", asset_0)');
expect(source).not.toContain('.vite/manifest.json');
});
@@ -138,9 +122,7 @@ describe('Bun build options', () => {
);
const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('"/base/prerendered/": file_response(file_0, "/base/prerendered/"');
- expect(source).toContain('"/base/prerendered": (request) => Response.redirect');
- expect(source).toContain('new URL(request.url).search');
+ expect(source).toContain('...prerendered_page("/base/prerendered/", "prerendered/index.html")');
expect(source).not.toContain('/base/base/');
});
@@ -152,7 +134,7 @@ describe('Bun build options', () => {
);
const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('"/old": Response.redirect("/new", 301)');
+ expect(source).toContain('prerendered_redirect("/old", 301, "/new")');
});
test('passes supported advanced build options to Bun', async () => {
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js
new file mode 100644
index 000000000000..5dae4aac8917
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js
@@ -0,0 +1,3 @@
+export function POST() {
+ return Response.json({ message: 'hello from a server endpoint' });
+}
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js
index 47717c4e0511..3b844ed60555 100644
--- a/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js
+++ b/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js
@@ -1,5 +1,5 @@
import { read } from '$app/server';
-import file from './file.txt?url';
+import file from './file.txt?url&no-inline';
export function GET() {
return read(file);
diff --git a/packages/adapter-bun/test/apps/basic/static/asterisk*.txt b/packages/adapter-bun/test/apps/basic/static/asterisk*.txt
new file mode 100644
index 000000000000..e96c3e236d73
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/static/asterisk*.txt
@@ -0,0 +1 @@
+literal asterisk
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 9af38d2038ab..5d7266351547 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -79,6 +79,15 @@ test('serves URL-encoded static filenames', async ({ request }) => {
expect(await response.text()).toBe('hello from an encoded filename\n');
});
+test('serves filenames with a literal asterisk without creating a wildcard route', async ({ request }) => {
+ const asset = await request.get('/asterisk*.txt');
+ expect(asset.status()).toBe(200);
+ expect(await asset.text()).toBe('literal asterisk\n');
+
+ const page = await request.get('/platform');
+ expect(page.status()).toBe(200);
+});
+
test('serves dotfiles from the static directory', async ({ request }) => {
const response = await request.get('/.well-known/adapter-bun.txt');
expect(response.status()).toBe(200);
@@ -100,10 +109,12 @@ test('caches immutable client assets', async ({ request }) => {
expect(asset_response.headers()['cache-control']).toBe('public,max-age=31536000,immutable');
});
-test('uses Bun route method semantics for static files', async ({ request }) => {
- const response = await request.post('/data.json');
+test('passes non-GET requests for static paths to SvelteKit', async ({ request }) => {
+ const response = await request.post('/data.json', {
+ headers: { origin: 'http://localhost:4174' }
+ });
expect(response.status()).toBe(200);
- expect(await response.json()).toEqual({ message: 'hello from a static file' });
+ expect(await response.json()).toEqual({ message: 'hello from a server endpoint' });
});
test('redirects prerendered paths to their canonical trailing slash', async ({ request }) => {
From 77d051ec3ee01d2850b9ceebfb443c496c5d56eb Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 22:16:50 +0300
Subject: [PATCH 40/94] feat(routes-util): enhance encode_pathname to handle
asterisks in path segments
---
packages/adapter-bun/src/routes-util.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index f2299bc16fd1..0b233968087d 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -12,7 +12,10 @@ const dir = dirname(Bun.main);
* @returns {string}
*/
function encode_pathname(pathname) {
- return pathname.split('/').map(encodeURIComponent).join('/');
+ return pathname
+ .split('/')
+ .map((seg) => encodeURIComponent(seg).replace('*', '%2A'))
+ .join('/');
}
/**
From 7091dae3f5c206759841cddc4ab7da4fa7280095 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 22:31:50 +0300
Subject: [PATCH 41/94] feat(routes-util): streamline response handling in
client and prerendered asset functions
---
packages/adapter-bun/src/routes-util.js | 16 +++++-----------
1 file changed, 5 insertions(+), 11 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 0b233968087d..5a42bdd7e370 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -41,8 +41,7 @@ export function client_asset(urlPath, filePath = urlPath) {
headers['cache-control'] = 'public,max-age=31536000,immutable';
}
- const resp = new Response(file, { headers });
- return [to_path(urlPath), { GET: resp, HEAD: resp }];
+ return [to_path(urlPath), { GET: new Response(file, { headers }) }];
}
/**
@@ -53,8 +52,7 @@ export function client_asset(urlPath, filePath = urlPath) {
export function prerendered_asset(urlPath, filePath = urlPath) {
const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
const headers = { 'content-type': file.type };
- const resp = new Response(file, { headers });
- return [to_path(urlPath), { GET: resp, HEAD: resp }];
+ return [to_path(urlPath), { GET: new Response(file, { headers }) }];
}
/**
@@ -78,12 +76,10 @@ export function prerendered_page(urlPath, filePath) {
const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
- const resp = new Response(file, { headers });
-
// path already contains base, no need to call to_path here
return [
- [encode_pathname(urlPath), { GET: resp, HEAD: resp }],
- [encode_pathname(inverted), { GET: handle_redirect, HEAD: handle_redirect }]
+ [encode_pathname(urlPath), { GET: new Response(file, { headers }) }],
+ [encode_pathname(inverted), { GET: handle_redirect }]
];
}
@@ -94,8 +90,6 @@ export function prerendered_page(urlPath, filePath) {
* @returns {[string, RouteHandler]}
*/
export function prerendered_redirect(urlPath, status, location) {
- const resp = new Response(null, { status, headers: { location } });
-
// path already contains base, no need to call to_path here
- return [encode_pathname(urlPath), { GET: resp, HEAD: resp }];
+ return [encode_pathname(urlPath), { GET: new Response(null, { status, headers: { location } }) }];
}
From 06226e47b4ebcae496229fe37cf76282ec348ec3 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 22:35:31 +0300
Subject: [PATCH 42/94] fix(routes-util): correct appPath reference in
client_asset cache control logic
---
packages/adapter-bun/src/routes-util.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 5a42bdd7e370..84797ff3e119 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -37,7 +37,7 @@ export function client_asset(urlPath, filePath = urlPath) {
/** @type {Record} */
const headers = { 'content-type': file.type };
- if (urlPath.startsWith(`/${manifest.appPath}/immutable/`)) {
+ if (urlPath.startsWith(`${manifest.appDir}/immutable/`)) {
headers['cache-control'] = 'public,max-age=31536000,immutable';
}
From 3cce96f57c6e06100a0ad19a5075ec9f193e8ca7 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sat, 8 Aug 2026 23:21:02 +0300
Subject: [PATCH 43/94] feat(adapter-bun): enhance get_embed_entries and
get_no_embed_entries to include server_assets handling
---
packages/adapter-bun/index.js | 55 ++++++++++++++++++++++++++---------
1 file changed, 42 insertions(+), 13 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 9e9308a459a5..3a631367548c 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -141,9 +141,10 @@ export default function (opts = {}) {
/**
* @param {object} options
* @param {import('@sveltejs/kit').Builder} options.builder
- * @returns {Promise<{imports: string[], entries: string[]}>}
+ * @param {string[]} options.server_assets
+ * @returns {Promise<{imports: string[], entries: string[], server_assets: string[]}>}
*/
-async function get_embed_entries({ builder }) {
+async function get_embed_entries({ builder, server_assets }) {
const builtFiles = `${builder.config.kit.outDir}/output`;
const [cl_files, pr_pages, pr_deps, pr_data] = await Promise.all([
@@ -153,41 +154,58 @@ async function get_embed_entries({ builder }) {
read_files_recursive(`${builtFiles}/prerendered/data`)
]);
+ console.assert(
+ builder.prerendered.pages.size === pr_pages.length,
+ 'Mismatch between prerendered pages and files'
+ );
+
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
const imports = assets.map(({ abs }, i) => {
return `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`;
});
+ let offset = 0;
const cl_entries = cl_files.map(({ rel }, i) => {
- return `client_asset(${JSON.stringify(rel)}, asset_${i})`;
+ return `client_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
});
+
+ offset += cl_files.length;
const pr_pages_entries = [...builder.prerendered.pages].map(([path, { file }]) => {
const fileIdx = pr_pages.findIndex((f) => f.rel === file);
if (fileIdx === -1)
throw new Error(`Could not find prerendered page ${file} for route ${path}`);
- return `...prerendered_page(${JSON.stringify(path)}, asset_${cl_files.length + fileIdx})`;
+ return `...prerendered_page(${JSON.stringify(path)}, asset_${offset + fileIdx})`;
});
+
+ offset += pr_pages.length;
const pr_assets_entries = [...pr_deps, ...pr_data].map(({ rel }, i) => {
- return `prerendered_asset(${JSON.stringify(rel)}, asset_${cl_files.length + pr_pages.length + i})`;
+ return `prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
});
+
const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
return `prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
});
return {
imports,
- entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects]
+ entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects],
+ server_assets: server_assets.map((file) => {
+ const idx = assets.findIndex((f) => f.rel === file);
+ if (idx === -1) throw new Error(`Could not find server asset ${file}`);
+ return `Bun.file(asset_${idx})`;
+ })
};
}
/**
* @param {object} options
* @param {import('@sveltejs/kit').Builder} options.builder
+ * @param {string[]} options.server_assets
* @param {string} options.out
- * @returns {{imports: string[], entries: string[]}}
+ * @returns {{imports: string[], entries: string[], server_assets: string[]}}
*/
-function get_no_embed_entries({ builder, out }) {
+function get_no_embed_entries({ builder, server_assets, out }) {
const client_files = builder.writeClient(`${out}/client`);
const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
@@ -214,7 +232,10 @@ function get_no_embed_entries({ builder, out }) {
return {
imports: [],
- entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects]
+ entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects],
+ server_assets: server_assets.map((file) => {
+ return `Bun.file(resolve(dir, 'client', ${JSON.stringify(file)}))`;
+ })
};
}
@@ -226,16 +247,24 @@ function get_no_embed_entries({ builder, out }) {
* @returns {Promise}
*/
async function create_routes({ builder, out, embed }) {
- const { imports, entries } = embed
- ? await get_embed_entries({ builder })
- : get_no_embed_entries({ builder, out });
+ const server_assets = builder.findServerAssets(builder.routes);
+
+ const {
+ imports,
+ entries,
+ server_assets: resolved_server_assets
+ } = embed
+ ? await get_embed_entries({ builder, server_assets })
+ : get_no_embed_entries({ builder, out, server_assets });
return [
`// eslint-disable-next-line @typescript-eslint/no-unused-vars`,
`import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect } from './routes-util.js';`,
...imports,
`export const routes = Object.fromEntries([${entries.join(',\n')}]);`,
- `export const server_assets = new Map();`
+ `export const server_assets = new Map([${resolved_server_assets
+ .map((file, i) => `[${JSON.stringify(server_assets[i])}, ${file}]`)
+ .join(',\n')}]);`
].join('\n');
}
From cbca22804a0f7c414d758d79bcc4d022ce52d079 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 00:19:09 +0300
Subject: [PATCH 44/94] feat(adapter-bun): enhance asset handling and add
support for static directory indexes and prerendered non-HTML endpoints
---
.../25-build-and-deploy/45-adapter-bun.md | 4 +-
packages/adapter-bun/index.js | 35 ++++++++++------
packages/adapter-bun/index.spec.ts | 27 +++++++++---
packages/adapter-bun/src/routes-util.js | 42 +++++++++++++++----
.../test/apps/basic/src/routes/+page.js | 1 +
.../src/routes/prerendered.ico/+server.js | 7 ++++
.../test/apps/basic/static/sub/index.html | 3 ++
.../adapter-bun/test/apps/basic/test/test.js | 18 +++++++-
8 files changed, 109 insertions(+), 28 deletions(-)
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/+page.js
create mode 100644 packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js
create mode 100644 packages/adapter-bun/test/apps/basic/static/sub/index.html
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index f6081519882d..c96fae7d1959 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -149,11 +149,11 @@ Set `IPV6_ONLY=true` to enable `IPV6_V6ONLY` on an IPv6 listener.
The maximum request body size in bytes. It supports `K`, `M`, and `G` suffixes and defaults to `512K`.
-### `IDLE_TIMEOUT` and `SHUTDOWN_TIMEOUT`
+### `IDLE_TIMEOUT`
`IDLE_TIMEOUT` sets Bun's connection inactivity timeout in seconds. It must be between `0` and `255`; `0` disables the timeout. The adapter automatically disables the timeout for server-sent event responses.
-On `SIGINT` or `SIGTERM`, the server stops accepting connections and waits for in-flight requests. `SHUTDOWN_TIMEOUT` controls how many seconds it waits before forcefully closing active connections and defaults to `30`.
+On `SIGINT` or `SIGTERM`, the server stops accepting connections and waits for in-flight requests. Send a second signal to force the process to exit immediately.
### `DEVELOPMENT`
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 3a631367548c..765a91319cab 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -154,11 +154,6 @@ async function get_embed_entries({ builder, server_assets }) {
read_files_recursive(`${builtFiles}/prerendered/data`)
]);
- console.assert(
- builder.prerendered.pages.size === pr_pages.length,
- 'Mismatch between prerendered pages and files'
- );
-
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
const imports = assets.map(({ abs }, i) => {
@@ -167,16 +162,24 @@ async function get_embed_entries({ builder, server_assets }) {
let offset = 0;
const cl_entries = cl_files.map(({ rel }, i) => {
- return `client_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
+ return `...client_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
});
offset += cl_files.length;
+ const prerendered_pages_files = new Set(
+ [...builder.prerendered.pages].map(([_, { file }]) => file)
+ );
const pr_pages_entries = [...builder.prerendered.pages].map(([path, { file }]) => {
const fileIdx = pr_pages.findIndex((f) => f.rel === file);
if (fileIdx === -1)
throw new Error(`Could not find prerendered page ${file} for route ${path}`);
return `...prerendered_page(${JSON.stringify(path)}, asset_${offset + fileIdx})`;
});
+ const pr_page_assets_entries = pr_pages.flatMap(({ rel }, i) => {
+ return prerendered_pages_files.has(rel)
+ ? []
+ : [`prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`];
+ });
offset += pr_pages.length;
const pr_assets_entries = [...pr_deps, ...pr_data].map(({ rel }, i) => {
@@ -189,11 +192,17 @@ async function get_embed_entries({ builder, server_assets }) {
return {
imports,
- entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects],
+ entries: [
+ ...cl_entries,
+ ...pr_pages_entries,
+ ...pr_page_assets_entries,
+ ...pr_assets_entries,
+ ...pr_redirects
+ ],
server_assets: server_assets.map((file) => {
const idx = assets.findIndex((f) => f.rel === file);
if (idx === -1) throw new Error(`Could not find server asset ${file}`);
- return `Bun.file(asset_${idx})`;
+ return `server_asset(${JSON.stringify(file)}, asset_${idx})`;
})
};
}
@@ -210,7 +219,7 @@ function get_no_embed_entries({ builder, server_assets, out }) {
const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
const cl_entries = client_files.map((filePath) => {
- return `client_asset(${JSON.stringify(filePath)})`;
+ return `...client_asset(${JSON.stringify(filePath)})`;
});
const prerendered_pages = [...builder.prerendered.pages];
@@ -234,7 +243,7 @@ function get_no_embed_entries({ builder, server_assets, out }) {
imports: [],
entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects],
server_assets: server_assets.map((file) => {
- return `Bun.file(resolve(dir, 'client', ${JSON.stringify(file)}))`;
+ return `server_asset(${JSON.stringify(file)})`;
})
};
}
@@ -247,7 +256,9 @@ function get_no_embed_entries({ builder, server_assets, out }) {
* @returns {Promise}
*/
async function create_routes({ builder, out, embed }) {
- const server_assets = builder.findServerAssets(builder.routes);
+ const server_assets = builder.findServerAssets(
+ builder.routes.filter((route) => route.prerender !== true)
+ );
const {
imports,
@@ -259,7 +270,7 @@ async function create_routes({ builder, out, embed }) {
return [
`// eslint-disable-next-line @typescript-eslint/no-unused-vars`,
- `import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect } from './routes-util.js';`,
+ `import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.js';`,
...imports,
`export const routes = Object.fromEntries([${entries.join(',\n')}]);`,
`export const server_assets = new Map([${resolved_server_assets
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 382acb82d501..7305b5d042b9 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -70,8 +70,8 @@ describe('Bun build options', () => {
expect(test_builder.findServerAssets).toHaveBeenCalledWith([active_route]);
const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('client_asset("data.json")');
- expect(source).toContain('client_asset("_app/immutable/assets/read.txt")');
+ expect(source).toContain('...client_asset("data.json")');
+ expect(source).toContain('...client_asset("_app/immutable/assets/read.txt")');
expect(source).toContain('...prerendered_page("/prerendered/", "prerendered/index.html")');
expect(source).toContain(
'export const server_assets = new Map([["_app/immutable/assets/read.txt", server_asset("_app/immutable/assets/read.txt")]])'
@@ -95,9 +95,24 @@ describe('Bun build options', () => {
const source = options.files[routes_file];
expect(options.compile).toEqual({ outfile: 'server' });
expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('client_asset("data.json", asset_0)');
- expect(source).toContain('client_asset("_app/immutable/assets/read.txt", asset_1)');
- expect(source).toContain('server_asset("_app/immutable/assets/read.txt")');
+ expect(source).toContain('...client_asset("data.json", asset_0)');
+ expect(source).toContain('...client_asset("_app/immutable/assets/read.txt", asset_1)');
+ expect(source).toContain('server_asset("_app/immutable/assets/read.txt", asset_1)');
+ });
+
+ test('maps prerendered non-HTML assets into compiled executables', async () => {
+ mock_embedded_files({
+ pages: ['prerendered/index.html', 'prerendered.ico']
+ });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(
+ builder({
+ prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]]
+ })
+ );
+
+ const source = build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain('prerendered_asset("prerendered.ico", asset_1)');
});
test('preserves dotfiles other than Vite build metadata in executables', async () => {
@@ -107,7 +122,7 @@ describe('Bun build options', () => {
const source = build.mock.calls[0][0].files[routes_file];
expect(source).toContain('assetlinks.json');
- expect(source).toContain('client_asset(".well-known/assetlinks.json", asset_0)');
+ expect(source).toContain('...client_asset(".well-known/assetlinks.json", asset_0)');
expect(source).not.toContain('.vite/manifest.json');
});
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 84797ff3e119..5c27c001ed5e 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -26,10 +26,18 @@ function to_path(urlPath) {
return encode_pathname(posix.join(base, urlPath));
}
+/**
+ * @param {string} urlPath
+ * @returns {string}
+ */
+function to_directory_path(urlPath) {
+ return encode_pathname(`${posix.join(base, urlPath).replace(/\/$/, '')}/`);
+}
+
/**
* @param {string} urlPath
* @param {string} [filePath]
- * @returns {[string, RouteHandler]}
+ * @returns {Array<[string, RouteHandler]>}
*/
export function client_asset(urlPath, filePath = urlPath) {
const file = Bun.file(embed ? filePath : resolve(dir, 'client', filePath));
@@ -41,7 +49,24 @@ export function client_asset(urlPath, filePath = urlPath) {
headers['cache-control'] = 'public,max-age=31536000,immutable';
}
- return [to_path(urlPath), { GET: new Response(file, { headers }) }];
+ /** @type {Array<[string, RouteHandler]>} */
+ const entries = [[to_path(urlPath), { GET: new Response(file, { headers }) }]];
+
+ if (urlPath.endsWith('/index.html') || urlPath === 'index.html') {
+ const directory = urlPath.slice(0, -'index.html'.length);
+ entries.push([to_directory_path(directory), { GET: new Response(file, { headers }) }]);
+ }
+
+ return entries;
+}
+
+/**
+ * @param {string} urlPath
+ * @param {string} [filePath]
+ * @returns {import('bun').BunFile}
+ */
+export function server_asset(urlPath, filePath = urlPath) {
+ return Bun.file(embed ? filePath : resolve(dir, 'client', urlPath));
}
/**
@@ -58,7 +83,7 @@ export function prerendered_asset(urlPath, filePath = urlPath) {
/**
* @param {string} urlPath
* @param {string} filePath
- * @returns {[[string, RouteHandler], [string, RouteHandler]]}
+ * @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_page(urlPath, filePath) {
/**
@@ -75,12 +100,15 @@ export function prerendered_page(urlPath, filePath) {
const headers = { 'content-type': file.type };
const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
+ /** @type {Array<[string, RouteHandler]>} */
+ const entries = [[encode_pathname(urlPath), { GET: new Response(file, { headers }) }]];
+
+ if (inverted) {
+ entries.push([encode_pathname(inverted), { GET: handle_redirect }]);
+ }
// path already contains base, no need to call to_path here
- return [
- [encode_pathname(urlPath), { GET: new Response(file, { headers }) }],
- [encode_pathname(inverted), { GET: handle_redirect }]
- ];
+ return entries;
}
/**
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/+page.js b/packages/adapter-bun/test/apps/basic/src/routes/+page.js
new file mode 100644
index 000000000000..189f71e2e1b3
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/+page.js
@@ -0,0 +1 @@
+export const prerender = true;
diff --git a/packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js
new file mode 100644
index 000000000000..ea5bb293b791
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js
@@ -0,0 +1,7 @@
+export const prerender = true;
+
+export function GET() {
+ return new Response(new Uint8Array([0, 0, 1, 0]), {
+ headers: { 'content-type': 'image/x-icon' }
+ });
+}
diff --git a/packages/adapter-bun/test/apps/basic/static/sub/index.html b/packages/adapter-bun/test/apps/basic/static/sub/index.html
new file mode 100644
index 000000000000..c3b5888bb16d
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/static/sub/index.html
@@ -0,0 +1,3 @@
+
+Static directory index
+Static directory index
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index 5d7266351547..af6ee55323fd 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -42,6 +42,20 @@ test('serves static files with Bun file responses', async ({ request }) => {
expect(await head.text()).toBe('');
});
+test('serves static directory indexes', async ({ request }) => {
+ const response = await request.get('/sub/');
+ expect(response.status()).toBe(200);
+ expect(response.headers()['content-type']).toBe('text/html;charset=utf-8');
+ expect(await response.text()).toContain('Static directory index');
+});
+
+test('serves prerendered non-HTML endpoints', async ({ request }) => {
+ const response = await request.get('/prerendered.ico');
+ expect(response.status()).toBe(200);
+ expect(response.headers()['content-type']).toBe('image/x-icon');
+ expect(await response.body()).toEqual(Buffer.from([0, 0, 1, 0]));
+});
+
test('uses Bun validators and ranges for static files', async ({ request }) => {
const initial = await request.get('/data.json');
const body = await initial.text();
@@ -79,7 +93,9 @@ test('serves URL-encoded static filenames', async ({ request }) => {
expect(await response.text()).toBe('hello from an encoded filename\n');
});
-test('serves filenames with a literal asterisk without creating a wildcard route', async ({ request }) => {
+test('serves filenames with a literal asterisk without creating a wildcard route', async ({
+ request
+}) => {
const asset = await request.get('/asterisk*.txt');
expect(asset.status()).toBe(200);
expect(await asset.text()).toBe('literal asterisk\n');
From 1d7a1f013ca8f0fd999e776d1df4a006a27f98cc Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 00:27:27 +0300
Subject: [PATCH 45/94] feat(adapter-bun): refactor runtime configuration
handling and remove global identifiers
---
.changeset/quiet-buns-scope.md | 5 +++++
packages/adapter-bun/index.d.ts | 5 -----
packages/adapter-bun/index.js | 10 ++++------
packages/adapter-bun/index.spec.ts | 19 ++++++++++++++++++-
packages/adapter-bun/internal.d.ts | 2 ++
packages/adapter-bun/src/env.js | 3 +--
packages/adapter-bun/src/env.spec.ts | 4 +---
packages/adapter-bun/src/handler.js | 5 ++---
8 files changed, 33 insertions(+), 20 deletions(-)
create mode 100644 .changeset/quiet-buns-scope.md
diff --git a/.changeset/quiet-buns-scope.md b/.changeset/quiet-buns-scope.md
new file mode 100644
index 000000000000..f497663574de
--- /dev/null
+++ b/.changeset/quiet-buns-scope.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-bun': patch
+---
+
+fix: scope generated runtime configuration to adapter code
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 84b51f1aa288..7509e796b165 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -1,11 +1,6 @@
import type { Adapter } from '@sveltejs/kit';
import './ambient.js';
-declare global {
- const ENV_PREFIX: string;
- const ORIGIN: string | undefined;
-}
-
interface AdapterOptions {
/**
* The directory to build the server to.
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 765a91319cab..7a443b5da6fd 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -50,8 +50,10 @@ export default function (opts = {}) {
[manifest_file]:
`export const manifest = ${builder.generateManifest({ relativePath: './' })};\n` +
`export const base = ${JSON.stringify(builder.config.kit.paths.base || '/')};\n` +
- `export const embed = ${JSON.stringify(!!buildOptions.compile)};\n`,
- [server_options_file]: `export default ${JSON.stringify(serverOptions)};\n`,
+ `export const embed = ${JSON.stringify(!!buildOptions.compile)};\n` +
+ `export const env_prefix = ${JSON.stringify(envPrefix)};\n` +
+ `export const origin = ${JSON.stringify(builder.config.kit.paths.origin) || 'undefined'};`,
+ [server_options_file]: [`export default ${JSON.stringify(serverOptions)};`].join('\n'),
[routes_file]: await create_routes({
builder,
out,
@@ -98,10 +100,6 @@ export default function (opts = {}) {
asset: 'server/assets/[name]-[hash].[ext]'
},
plugins: [adapter_plugin],
- define: {
- ENV_PREFIX: JSON.stringify(envPrefix),
- ORIGIN: JSON.stringify(builder.config.kit.paths.origin) || 'undefined'
- },
conditions: ['bun', 'node'],
throw: false,
files: virtual_files,
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 7305b5d042b9..35fc12c3c8c1 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import adapter from './index.js';
const index_file = new URL('./src/index.js', import.meta.url).pathname;
+const options_file = new URL('./src/options.js', import.meta.url).pathname;
const routes_file = new URL('./src/routes.js', import.meta.url).pathname;
const start_file = new URL('./src/start.js', import.meta.url).pathname;
@@ -55,6 +56,20 @@ describe('Bun build options', () => {
expect(options.plugins[0].name).toBe('adapter-bun');
});
+ test('provides runtime constants without globally replacing application identifiers', async () => {
+ await adapter({ envPrefix: 'MY_', serverOptions: { port: 4000 } }).adapt(
+ builder({ origin: 'https://example.com' })
+ );
+
+ const options = build.mock.calls[0][0];
+ expect(options.define).toBeUndefined();
+ expect(options.files[options_file]).toBe(
+ 'export default {"port":4000};\n' +
+ 'export const env_prefix = "MY_";\n' +
+ 'export const origin = "https://example.com";'
+ );
+ });
+
test('shares Bun files between directory routes and server reads', async () => {
const active_route = { id: '/read', prerender: false };
const prerendered_route = { id: '/prerendered', prerender: true };
@@ -262,6 +277,7 @@ function builder({
server_assets = [],
app_path = '_app',
base = '',
+ origin,
instrumentation = false
}: {
client_files?: string[];
@@ -272,10 +288,11 @@ function builder({
server_assets?: string[];
app_path?: string;
base?: string;
+ origin?: string;
instrumentation?: boolean;
} = {}) {
return {
- config: { kit: { outDir: '.svelte-kit', paths: { base, origin: undefined } } },
+ config: { kit: { outDir: '.svelte-kit', paths: { base, origin } } },
routes,
prerendered: {
pages: new Map(prerendered_pages),
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index 788451163c18..d663850435d1 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -2,6 +2,8 @@ declare module 'MANIFEST' {
export const manifest: import('@sveltejs/kit').SSRManifest;
export const base: string;
export const embed: boolean;
+ export const env_prefix: string;
+ export const origin: string | undefined;
}
declare module 'ROUTES' {
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index 35dc26dc1e22..9f88e818d8a1 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -1,4 +1,5 @@
import process from 'node:process';
+import { env_prefix } from 'MANIFEST';
const expected = new Set([
'SOCKET_PATH',
@@ -16,8 +17,6 @@ const expected = new Set([
'PORT_HEADER'
]);
-export const env_prefix = ENV_PREFIX;
-
if (env_prefix) {
for (const name in process.env) {
if (name.startsWith(env_prefix)) {
diff --git a/packages/adapter-bun/src/env.spec.ts b/packages/adapter-bun/src/env.spec.ts
index 8770f8a4857b..21aab86315d0 100644
--- a/packages/adapter-bun/src/env.spec.ts
+++ b/packages/adapter-bun/src/env.spec.ts
@@ -1,9 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { boolean_env, bytes_env, number_env } from './env.js';
-vi.hoisted(() => {
- vi.stubGlobal('ENV_PREFIX', '');
-});
+vi.mock('SERVER_OPTIONS', () => ({ env_prefix: '' }));
describe('boolean_env', () => {
afterEach(() => vi.unstubAllEnvs());
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 66ab0729bf04..d7f3088fa6b4 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -1,10 +1,9 @@
import { Server } from 'SERVER';
-import { manifest } from 'MANIFEST';
+import { manifest, origin, env_prefix } from 'MANIFEST';
import { server_assets } from 'ROUTES';
-import { env, env_prefix, number_env } from './env.js';
+import { env, number_env } from './env.js';
const server = new Server(manifest);
-const origin = ORIGIN;
const address_header = env('ADDRESS_HEADER', '')?.toLowerCase();
const protocol_header = env('PROTOCOL_HEADER', '')?.toLowerCase();
From 7625896ce0e9a65ecd4d625e81fc6dd53eda4412 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 00:29:24 +0300
Subject: [PATCH 46/94] fix(adapter-bun): remove quiet-buns-scope changeset
file
---
.changeset/quiet-buns-scope.md | 5 -----
1 file changed, 5 deletions(-)
delete mode 100644 .changeset/quiet-buns-scope.md
diff --git a/.changeset/quiet-buns-scope.md b/.changeset/quiet-buns-scope.md
deleted file mode 100644
index f497663574de..000000000000
--- a/.changeset/quiet-buns-scope.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@sveltejs/adapter-bun': patch
----
-
-fix: scope generated runtime configuration to adapter code
From d0d93c234f3383f067fda04ed6049f8d90a9f942 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 00:39:14 +0300
Subject: [PATCH 47/94] feat(adapter-bun): add validation for wildcard
filenames and update tests
---
packages/adapter-bun/index.js | 12 +++++++++
packages/adapter-bun/index.spec.ts | 25 ++++++++++++++-----
packages/adapter-bun/src/env.spec.ts | 2 +-
.../test/apps/basic/static/asterisk*.txt | 1 -
.../adapter-bun/test/apps/basic/test/test.js | 11 --------
5 files changed, 32 insertions(+), 19 deletions(-)
delete mode 100644 packages/adapter-bun/test/apps/basic/static/asterisk*.txt
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 7a443b5da6fd..d14ea76df3bf 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -21,6 +21,16 @@ async function read_files_recursive(path) {
}
}
+/** @param {string[]} files */
+function validate_file_paths(files) {
+ const invalid = files.find((file) => file.includes('*'));
+ if (invalid !== undefined) {
+ throw new Error(
+ `Cannot build with ${JSON.stringify(invalid)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file to remove the \`*\` character.`
+ );
+ }
+}
+
/** @type {import('./index.js').default} */
export default function (opts = {}) {
const { out = 'build', envPrefix = '', serverOptions = {}, buildOptions = {} } = opts;
@@ -153,6 +163,7 @@ async function get_embed_entries({ builder, server_assets }) {
]);
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
+ validate_file_paths(assets.map(({ rel }) => rel));
const imports = assets.map(({ abs }, i) => {
return `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`;
@@ -215,6 +226,7 @@ async function get_embed_entries({ builder, server_assets }) {
function get_no_embed_entries({ builder, server_assets, out }) {
const client_files = builder.writeClient(`${out}/client`);
const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
+ validate_file_paths([...client_files, ...prerendered_files]);
const cl_entries = client_files.map((filePath) => {
return `...client_asset(${JSON.stringify(filePath)})`;
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
index 35fc12c3c8c1..46ef8da9d77c 100644
--- a/packages/adapter-bun/index.spec.ts
+++ b/packages/adapter-bun/index.spec.ts
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import adapter from './index.js';
const index_file = new URL('./src/index.js', import.meta.url).pathname;
-const options_file = new URL('./src/options.js', import.meta.url).pathname;
+const manifest_file = new URL('./.svelte-kit/output/server/manifest.js', import.meta.url).pathname;
const routes_file = new URL('./src/routes.js', import.meta.url).pathname;
const start_file = new URL('./src/start.js', import.meta.url).pathname;
@@ -63,11 +63,8 @@ describe('Bun build options', () => {
const options = build.mock.calls[0][0];
expect(options.define).toBeUndefined();
- expect(options.files[options_file]).toBe(
- 'export default {"port":4000};\n' +
- 'export const env_prefix = "MY_";\n' +
- 'export const origin = "https://example.com";'
- );
+ expect(options.files[manifest_file]).toContain('export const env_prefix = "MY_";');
+ expect(options.files[manifest_file]).toContain('export const origin = "https://example.com";');
});
test('shares Bun files between directory routes and server reads', async () => {
@@ -141,6 +138,22 @@ describe('Bun build options', () => {
expect(source).not.toContain('.vite/manifest.json');
});
+ test('rejects literal wildcard filenames in regular builds', async () => {
+ await expect(adapter().adapt(builder({ client_files: ['asterisk*.txt'] }))).rejects.toThrow(
+ 'Rename the file to remove the `*` character'
+ );
+ expect(build).not.toHaveBeenCalled();
+ });
+
+ test('rejects literal wildcard filenames in compiled executables', async () => {
+ mock_embedded_files({ client: ['asterisk*.txt'] });
+
+ await expect(adapter({ buildOptions: { compile: true } }).adapt(builder())).rejects.toThrow(
+ 'Rename the file to remove the `*` character'
+ );
+ expect(build).not.toHaveBeenCalled();
+ });
+
test('does not duplicate the base path for prerendered pages', async () => {
await adapter().adapt(
builder({
diff --git a/packages/adapter-bun/src/env.spec.ts b/packages/adapter-bun/src/env.spec.ts
index 21aab86315d0..67a48eec7d56 100644
--- a/packages/adapter-bun/src/env.spec.ts
+++ b/packages/adapter-bun/src/env.spec.ts
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { boolean_env, bytes_env, number_env } from './env.js';
-vi.mock('SERVER_OPTIONS', () => ({ env_prefix: '' }));
+vi.mock('MANIFEST', () => ({ env_prefix: '' }));
describe('boolean_env', () => {
afterEach(() => vi.unstubAllEnvs());
diff --git a/packages/adapter-bun/test/apps/basic/static/asterisk*.txt b/packages/adapter-bun/test/apps/basic/static/asterisk*.txt
deleted file mode 100644
index e96c3e236d73..000000000000
--- a/packages/adapter-bun/test/apps/basic/static/asterisk*.txt
+++ /dev/null
@@ -1 +0,0 @@
-literal asterisk
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
index af6ee55323fd..cc461e0f3982 100644
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ b/packages/adapter-bun/test/apps/basic/test/test.js
@@ -93,17 +93,6 @@ test('serves URL-encoded static filenames', async ({ request }) => {
expect(await response.text()).toBe('hello from an encoded filename\n');
});
-test('serves filenames with a literal asterisk without creating a wildcard route', async ({
- request
-}) => {
- const asset = await request.get('/asterisk*.txt');
- expect(asset.status()).toBe(200);
- expect(await asset.text()).toBe('literal asterisk\n');
-
- const page = await request.get('/platform');
- expect(page.status()).toBe(200);
-});
-
test('serves dotfiles from the static directory', async ({ request }) => {
const response = await request.get('/.well-known/adapter-bun.txt');
expect(response.status()).toBe(200);
From 9a7ecaa77d7a62e850e12a1fcabb3f69e548227c Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 01:49:03 +0300
Subject: [PATCH 48/94] feat(adapter-bun): add Playwright tests for browser and
server rendering
- Introduced new Playwright tests for browser rendering in `browser.test.js`, verifying server-side rendering and hydration.
- Added comprehensive server tests in `server.test.js`, covering platform information, static file serving, and request handling.
- Removed outdated test file `test.js` to streamline test structure.
- Updated Vite configuration to ensure proper sourcemap handling.
- Implemented environment variable tests in `env.spec.ts` to validate prefix handling and type parsing.
- Created handler tests in `handler.spec.ts` to ensure correct initialization and request handling.
- Added route tests in `routes.spec.ts` to verify asset serving and redirect behavior.
- Developed start tests in `start.spec.ts` to validate server startup and environment variable overrides.
- Enhanced utility functions in `utils.js` for better test configuration.
- Updated TypeScript definitions in `types.ts` to reflect new server options and build configurations.
- Adjusted TypeScript configuration in `tsconfig.json` for improved file inclusion.
- Configured Vitest to include specific test files for better test organization.
---
.../25-build-and-deploy/45-adapter-bun.md | 172 ++++++---
packages/adapter-bun/README.md | 23 +-
packages/adapter-bun/index.spec.ts | 325 ----------------
packages/adapter-bun/src/env.spec.ts | 72 ----
packages/adapter-bun/test/adapter.spec.ts | 364 ++++++++++++++++++
.../test/apps/basic/test/browser.test.js | 10 +
.../test/apps/basic/test/server.test.js | 124 ++++++
.../adapter-bun/test/apps/basic/test/test.js | 135 -------
.../test/apps/basic/vite.config.js | 2 +-
packages/adapter-bun/test/env.spec.ts | 160 ++++++++
packages/adapter-bun/test/handler.spec.ts | 223 +++++++++++
packages/adapter-bun/test/routes.spec.ts | 118 ++++++
packages/adapter-bun/test/start.spec.ts | 168 ++++++++
packages/adapter-bun/test/utils.js | 11 +-
packages/adapter-bun/tests/types.ts | 33 +-
packages/adapter-bun/tsconfig.json | 5 +-
packages/adapter-bun/vitest.config.js | 8 +-
17 files changed, 1336 insertions(+), 617 deletions(-)
delete mode 100644 packages/adapter-bun/index.spec.ts
delete mode 100644 packages/adapter-bun/src/env.spec.ts
create mode 100644 packages/adapter-bun/test/adapter.spec.ts
create mode 100644 packages/adapter-bun/test/apps/basic/test/browser.test.js
create mode 100644 packages/adapter-bun/test/apps/basic/test/server.test.js
delete mode 100644 packages/adapter-bun/test/apps/basic/test/test.js
create mode 100644 packages/adapter-bun/test/env.spec.ts
create mode 100644 packages/adapter-bun/test/handler.spec.ts
create mode 100644 packages/adapter-bun/test/routes.spec.ts
create mode 100644 packages/adapter-bun/test/start.spec.ts
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index c96fae7d1959..a1a67de6ba3c 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -2,11 +2,17 @@
title: Bun servers
---
-To generate a standalone [Bun](https://bun.com/) server, use [`adapter-bun`](https://github.com/sveltejs/kit/tree/main/packages/adapter-bun). The generated server uses [`Bun.serve`](https://bun.com/docs/runtime/http/server) and `Bun.file` directly.
+[`adapter-bun`](https://github.com/sveltejs/kit/tree/main/packages/adapter-bun) builds a SvelteKit application into a standalone [Bun](https://bun.com/) server. The generated server uses `Bun.serve` for requests and Bun file responses for client assets, prerendered output, and files read with [`$app/server`](https://svelte.dev/docs/kit/$app-server#read).
## Usage
-Install with `bun add -D @sveltejs/adapter-bun`, then add the adapter to your `vite.config.js`:
+Install the adapter:
+
+```sh
+bun add -D @sveltejs/adapter-bun
+```
+
+Configure it in `vite.config.js`:
```js
// @errors: 2307 2554
@@ -24,22 +30,25 @@ export default defineConfig({
});
```
-The adapter uses Bun's bundler and must run inside Bun. Build your app with `bun run --bun build`,
-then start it with:
+The adapter calls Bun's build API, so the production build itself must run in Bun. The `--bun` flag overrides Vite's Node.js shebang:
+
+```sh
+bun run --bun build
+```
+
+The default build is written to `build`. Start it with:
```sh
bun ./build
```
-The default output directory is `build`. Production dependencies are externalised in the same way as with [`adapter-node`](adapter-node): packages in `dependencies` must be installed alongside the build, while packages in `devDependencies` are bundled into it.
+The JavaScript server, client files, and prerendered files in the output directory are all required at runtime. Application imports are processed according to Bun's bundler behavior.
-Client assets and prerendered pages are served through Bun's native `routes` and file responses. This includes streaming and range requests, conditional requests using `Last-Modified`, correct MIME types, and immutable caching for hashed SvelteKit assets.
-File responses are intentionally not buffered at startup: Bun can use `sendfile(2)` where available,
-keeps memory usage bounded for large assets, and retains native range and conditional-request handling.
+Client assets and prerendered output are registered as native Bun routes. Only `GET` and the corresponding automatic `HEAD` requests are served by those routes; other methods continue to SvelteKit. Bun supplies MIME types, conditional-request validators, byte ranges for filesystem-backed files, and streaming without buffering every asset in memory. Files below SvelteKit's `immutable` directory receive `Cache-Control: public,max-age=31536000,immutable`.
-## Options
+> [!NOTE] Bun treats `*` in a route pathname as a wildcard. The adapter rejects client and prerendered filenames that contain a literal `*`; rename those files before building.
-The adapter accepts these options:
+## Options
```js
// @errors: 2307 2554
@@ -68,24 +77,48 @@ export default defineConfig({
### out
-The directory to build the server to. It defaults to `build`.
+The output directory. It defaults to `build`.
### envPrefix
-Adds a prefix to all environment variables read by the production server. For example, with `envPrefix: 'MY_'`, configure the server with `MY_HOST`, `MY_PORT`, and `MY_REUSE_PORT`.
+A prefix for every deployment environment variable documented below. This is useful when the unprefixed names conflict with variables managed by your host:
+
+```js
+adapter({ envPrefix: 'MY_APP_' });
+```
+
+```sh
+MY_APP_HOST=127.0.0.1 MY_APP_PORT=4000 bun ./build
+```
+
+When a prefix is configured, the server fails at startup if it finds an unknown environment variable with that prefix. This catches collisions and misspellings.
### serverOptions
-Provides JSON-serializable defaults for `Bun.serve`. This is useful for settings such as `hostname`, `port`, `reusePort`, `ipv6Only`, `idleTimeout`, `development`, and `maxRequestBodySize`. Environment variables override these defaults.
+JSON-serializable defaults passed to `Bun.serve`. The supported properties are:
-`fetch`, `routes`, `websocket`, `error`, `tls`, `http3`, and `http1` cannot be configured this way. The adapter does not generate a reusable request-handler entrypoint, so applications that require these options need a custom Bun integration instead of the generated server.
+- `hostname`
+- `port`
+- `unix`
+- `reusePort`
+- `ipv6Only`
+- `idleTimeout`
+- `maxRequestBodySize`
+- `development`
+
+Environment variables take precedence over these defaults. A configured Unix socket takes precedence over `hostname`, `port`, `reusePort`, and `ipv6Only`.
+
+The generated server owns `fetch` and `routes`. It does not expose `websocket`, `error`, TLS, HTTP/3, or HTTP/1 configuration through `serverOptions`. Use a custom Bun integration if your application requires those `Bun.serve` options.
### buildOptions
-Pass options to Bun's build API through `buildOptions`. Set `buildOptions.compile: true` to generate
-`build/server`, a single executable containing the Bun runtime, your
-server code, client assets, and prerendered pages. In this mode, the adapter builds the executable
-directly instead of generating the JavaScript server files:
+Advanced Bun build settings can be supplied with `buildOptions`. The adapter currently accepts `sourcemap`, `minify`, `bytecode`, `banner`, `footer`, `drop`, `features`, `optimizeImports`, and `compile`.
+
+The generated entrypoint, output directory, top-level `target`, and module `format` are reserved. Generated servers target Bun and use ESM. Source maps default to `external`; set `sourcemap: 'none'` to disable them.
+
+#### Compiled executables
+
+Set `compile: true` to generate a single executable at `/server`:
```js
adapter({
@@ -95,20 +128,21 @@ adapter({
});
```
-The adapter uses the [`Bun.build`](https://bun.com/reference/bun/build) JavaScript API directly. The
-`--bun` flag is required because Vite normally respects its Node.js shebang:
+Build and run it without a separately installed Bun runtime:
```sh
bun run --bun build
+./build/server
```
-With the default options, only the executable is required at runtime. It is specific to the platform on which it was built. For advanced configuration, pass [`Bun.BuildConfig`](https://bun.com/reference/bun/BuildConfig) options directly. The adapter supplies the generated `entrypoints`, so that property is not configurable. Options such as code splitting may emit additional runtime files:
+The executable embeds the server code, client assets, prerendered output, and Bun runtime. `compile` can also be a Bun target string, which keeps the default `server` filename, or an options object. To change the executable name or cross-compile, provide an options object:
```js
adapter({
+ out: 'dist',
buildOptions: {
compile: {
- outfile: 'my-app',
+ outfile: 'application',
target: 'bun-linux-x64'
},
minify: true,
@@ -118,81 +152,97 @@ adapter({
});
```
-Native dependencies and cross-compilation have the same constraints as [Bun's single-file executables](https://bun.com/docs/bundler/executables).
-The adapter reserves the top-level Bun build `target` and `format` because generated servers always
-run as Bun ESM. Set the executable target inside `buildOptions.compile.target`, as shown above.
-Source maps default to `external`; set `sourcemap: 'none'` to disable them. Minification and bytecode
-remain opt-in. Compile options without an explicit `outfile` use `/server`.
+The result in this example is `dist/application`. Platform targets, native dependencies, and other limitations follow [Bun's executable compilation rules](https://bun.com/docs/bundler/executables).
## Environment variables
-In production, Bun automatically reads `.env` files. All of the following variables can be prefixed using `envPrefix`.
+Bun loads `.env` files automatically. If `envPrefix` is set, add that prefix to each name in this section.
-### `PORT`, `HOST`, and `SOCKET_PATH`
+### Listener
-The server listens on `0.0.0.0:3000` by default. Configure a TCP listener with `HOST` and `PORT`, or set `SOCKET_PATH` to use a Unix domain socket instead:
+`HOST` and `PORT` configure the TCP listener. Without either value or a `serverOptions` default, Bun uses its own listener defaults.
```sh
HOST=127.0.0.1 PORT=4000 bun ./build
+```
+
+`SOCKET_PATH` selects a Unix domain socket instead. When it is present, TCP-only options are ignored:
+
+```sh
SOCKET_PATH=/tmp/sveltekit.sock bun ./build
```
-On Linux, `SOCKET_PATH` may begin with a null byte to use an abstract namespace socket.
+`REUSE_PORT` enables Bun's `reusePort` option and `IPV6_ONLY` enables `ipv6Only`. Boolean variables accept `1`, `true`, `yes`, and `on`, or `0`, `false`, `no`, and `off`, without regard to letter case.
-### `REUSE_PORT` and `IPV6_ONLY`
+### Request limits and diagnostics
-Set `REUSE_PORT=true` to let multiple Bun processes bind the same port. The operating system load balances requests between them. `SO_REUSEPORT` is supported on Linux; macOS and Windows ignore it.
+`BODY_SIZE_LIMIT` controls `Bun.serve`'s `maxRequestBodySize`. It defaults to `512K`. The value must resolve to a whole number of bytes and may use a case-insensitive binary `K`, `M`, or `G` suffix, such as `768K` or `1.5M`.
-Set `IPV6_ONLY=true` to enable `IPV6_V6ONLY` on an IPv6 listener.
+`IDLE_TIMEOUT` sets Bun's per-request inactivity timeout in seconds. It must be an integer from `0` through `255`; `0` disables the timeout. The generated handler disables the timeout for responses whose content type starts with `text/event-stream` and also adds `X-Accel-Buffering: no`.
-### `BODY_SIZE_LIMIT`
+`DEVELOPMENT` enables Bun's development-mode error pages. It defaults to `false` for the generated server.
-The maximum request body size in bytes. It supports `K`, `M`, and `G` suffixes and defaults to `512K`.
+### Public origin behind a proxy
-### `IDLE_TIMEOUT`
+If [`paths.origin`](configuration#paths) is configured, that value is the trusted origin for every request. Otherwise, the adapter derives the origin from the incoming request URL and `Host` header.
-`IDLE_TIMEOUT` sets Bun's connection inactivity timeout in seconds. It must be between `0` and `255`; `0` disables the timeout. The adapter automatically disables the timeout for server-sent event responses.
+Behind a trusted reverse proxy, `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` name headers that contain the public scheme, host, and port:
-On `SIGINT` or `SIGTERM`, the server stops accepting connections and waits for in-flight requests. Send a second signal to force the process to exit immediately.
+```sh
+PROTOCOL_HEADER=x-forwarded-proto \
+HOST_HEADER=x-forwarded-host \
+PORT_HEADER=x-forwarded-port \
+bun ./build
+```
-### `DEVELOPMENT`
+The protocol header must contain only a scheme such as `https`, without a colon. The port header must contain a number. Invalid values produce a `400 Bad Request` response.
-Set `DEVELOPMENT=true` to enable Bun's contextual server error pages. It defaults to `false` in the generated production server.
+> [!CAUTION] Only trust forwarded headers when requests can reach the server through a proxy you control. A direct client can spoof these headers.
-### Proxy headers
+### Client addresses behind a proxy
-When [`paths.origin`](configuration#paths) is not configured, the adapter derives the request origin from Bun's request URL and the `host` header. Set `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` when a trusted reverse proxy exposes the public origin through other headers:
+[`event.getClientAddress()`](https://svelte.dev/docs/kit/@sveltejs-kit#RequestEvent) uses `server.requestIP(request).address` by default. Set `ADDRESS_HEADER` to the name of a trusted proxy header when the direct peer is a proxy:
```sh
-PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host bun ./build
+ADDRESS_HEADER=true-client-ip bun ./build
```
-Set `ADDRESS_HEADER` to the trusted proxy header containing the client address. If it is `x-forwarded-for`, set `XFF_DEPTH` to the number of trusted proxies and the adapter will select the address from the right-hand side of the list.
-
-Only use these variables behind a trusted proxy because clients can spoof forwarded headers.
+For `x-forwarded-for`, also set `XFF_DEPTH` to the number of trusted proxies. The default depth is `1`, and the adapter selects from the right side of the comma-separated list so client-supplied entries to the left cannot change the trusted result:
-## Platform-specific context
+```sh
+ADDRESS_HEADER=x-forwarded-for XFF_DEPTH=2 bun ./build
+```
-The `platform` property contains the original `Request` and Bun `Server`:
+`XFF_DEPTH` must be an integer of at least `1`. `getClientAddress()` throws if the configured header is absent or contains fewer addresses than the configured depth.
-```js
-/** @type {import('./$types').RequestHandler} */
-export function GET({ platform }) {
- const address = platform.server.requestIP(platform.request);
- return Response.json(address);
-}
-```
+## Platform API
-The server object also exposes Bun's native operational metrics. Applications can publish them
-through their own authenticated endpoint or instrumentation without the adapter reserving a URL:
+The request event's `platform` property exposes the original Web API request received by Bun and the Bun server instance:
```js
/** @type {import('./$types').RequestHandler} */
-export function GET({ platform }) {
+export function GET({ getClientAddress, platform }) {
return Response.json({
+ address: getClientAddress(),
+ requestUrl: platform.request.url,
+ serverId: platform.server.id,
pendingRequests: platform.server.pendingRequests,
- pendingWebSockets: platform.server.pendingWebSockets,
- chatSubscribers: platform.server.subscriberCount('chat')
+ pendingWebSockets: platform.server.pendingWebSockets
});
}
```
+
+`platform.request` remains the original request even when the adapter normalizes the request URL to a configured or proxy-derived public origin before passing it to SvelteKit.
+
+## Graceful shutdown
+
+On `SIGINT` or `SIGTERM`, the generated server calls `server.stop()`. Bun stops accepting new connections and the adapter waits for pending requests before emitting a `sveltekit:shutdown` process event with the signal name:
+
+```js
+process.on('sveltekit:shutdown', async (reason) => {
+ await jobs.stop();
+ await db.close();
+});
+```
+
+Sending a second shutdown signal forces the process to exit with status `1`.
diff --git a/packages/adapter-bun/README.md b/packages/adapter-bun/README.md
index f2b3b06bb449..98d1b28dda77 100644
--- a/packages/adapter-bun/README.md
+++ b/packages/adapter-bun/README.md
@@ -1,14 +1,29 @@
# @sveltejs/adapter-bun
-[Adapter](https://svelte.dev/docs/kit/adapters) for SvelteKit apps that generates a standalone Bun server.
+SvelteKit adapter that builds a standalone server for the [Bun](https://bun.com/) runtime.
-## Docs
+```sh
+bun add -D @sveltejs/adapter-bun
+```
-[Docs](https://svelte.dev/docs/kit/adapter-bun)
+```js
+import adapter from '@sveltejs/adapter-bun';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [sveltekit({ adapter: adapter() })]
+});
+```
+
+Build with `bun run --bun build`, then run the default output with `bun ./build`.
+
+See the [adapter-bun documentation](https://svelte.dev/docs/kit/adapter-bun) for configuration,
+environment variables, compiled executables, proxy setup, and the Bun-specific platform API.
## Changelog
-[The Changelog for this package is available on GitHub](https://github.com/sveltejs/kit/blob/main/packages/adapter-bun/CHANGELOG.md).
+[View the package changelog](https://github.com/sveltejs/kit/blob/main/packages/adapter-bun/CHANGELOG.md).
## License
diff --git a/packages/adapter-bun/index.spec.ts b/packages/adapter-bun/index.spec.ts
deleted file mode 100644
index 46ef8da9d77c..000000000000
--- a/packages/adapter-bun/index.spec.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-import { readFileSync } from 'node:fs';
-import { readdir } from 'node:fs/promises';
-import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
-import adapter from './index.js';
-
-const index_file = new URL('./src/index.js', import.meta.url).pathname;
-const manifest_file = new URL('./.svelte-kit/output/server/manifest.js', import.meta.url).pathname;
-const routes_file = new URL('./src/routes.js', import.meta.url).pathname;
-const start_file = new URL('./src/start.js', import.meta.url).pathname;
-
-vi.mock('node:fs/promises', async (import_original) => {
- const actual = await import_original();
- return { ...actual, readdir: vi.fn() };
-});
-
-const { adapter_entrypoint, build, file } = vi.hoisted(() => {
- const adapter_entrypoint = '// adapter entrypoint';
- const build = vi.fn((_options: any) => ({ success: true, logs: [], outputs: [] }));
- const file = vi.fn((path: string) => ({
- text: () => adapter_entrypoint,
- type: path.endsWith('.html')
- ? 'text/html;charset=utf-8'
- : path.endsWith('.json')
- ? 'application/json;charset=utf-8'
- : 'text/plain;charset=utf-8'
- }));
- vi.stubGlobal('Bun', { build, file });
- return { adapter_entrypoint, build, file };
-});
-
-beforeEach(() => {
- vi.mocked(readdir).mockResolvedValue([]);
-});
-
-afterEach(() => {
- build.mockClear();
- file.mockClear();
-});
-
-describe('Bun build options', () => {
- test('reserves the runtime target and module format', async () => {
- const instance = adapter();
- expect(instance.supports?.read?.({ route: { id: '/read' }, config: {} })).toBe(true);
-
- await instance.adapt(builder());
-
- const options = build.mock.calls[0][0];
- expect(options).toMatchObject({
- target: 'bun',
- format: 'esm',
- conditions: ['bun', 'node'],
- outdir: 'build',
- sourcemap: 'external',
- compile: false
- });
- expect(options.plugins[0].name).toBe('adapter-bun');
- });
-
- test('provides runtime constants without globally replacing application identifiers', async () => {
- await adapter({ envPrefix: 'MY_', serverOptions: { port: 4000 } }).adapt(
- builder({ origin: 'https://example.com' })
- );
-
- const options = build.mock.calls[0][0];
- expect(options.define).toBeUndefined();
- expect(options.files[manifest_file]).toContain('export const env_prefix = "MY_";');
- expect(options.files[manifest_file]).toContain('export const origin = "https://example.com";');
- });
-
- test('shares Bun files between directory routes and server reads', async () => {
- const active_route = { id: '/read', prerender: false };
- const prerendered_route = { id: '/prerendered', prerender: true };
- const test_builder = builder({
- client_files: ['data.json', 'encoded name.txt', '_app/immutable/assets/read.txt'],
- prerendered_files: ['prerendered/index.html'],
- prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]],
- routes: [active_route, prerendered_route],
- server_assets: ['_app/immutable/assets/read.txt']
- });
-
- await adapter().adapt(test_builder);
- expect(test_builder.findServerAssets).toHaveBeenCalledWith([active_route]);
-
- const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('...client_asset("data.json")');
- expect(source).toContain('...client_asset("_app/immutable/assets/read.txt")');
- expect(source).toContain('...prerendered_page("/prerendered/", "prerendered/index.html")');
- expect(source).toContain(
- 'export const server_assets = new Map([["_app/immutable/assets/read.txt", server_asset("_app/immutable/assets/read.txt")]])'
- );
- });
-
- test('maps logical paths to embedded Bun files for executables', async () => {
- mock_embedded_files({
- client: ['data.json', '_app/immutable/assets/read.txt'],
- pages: ['prerendered/index.html']
- });
-
- await adapter({ buildOptions: { compile: true } }).adapt(
- builder({
- prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]],
- server_assets: ['_app/immutable/assets/read.txt']
- })
- );
-
- const options = build.mock.calls[0][0];
- const source = options.files[routes_file];
- expect(options.compile).toEqual({ outfile: 'server' });
- expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('...client_asset("data.json", asset_0)');
- expect(source).toContain('...client_asset("_app/immutable/assets/read.txt", asset_1)');
- expect(source).toContain('server_asset("_app/immutable/assets/read.txt", asset_1)');
- });
-
- test('maps prerendered non-HTML assets into compiled executables', async () => {
- mock_embedded_files({
- pages: ['prerendered/index.html', 'prerendered.ico']
- });
-
- await adapter({ buildOptions: { compile: true } }).adapt(
- builder({
- prerendered_pages: [['/prerendered/', { file: 'prerendered/index.html' }]]
- })
- );
-
- const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('prerendered_asset("prerendered.ico", asset_1)');
- });
-
- test('preserves dotfiles other than Vite build metadata in executables', async () => {
- mock_embedded_files({ client: ['.vite/manifest.json', '.well-known/assetlinks.json'] });
-
- await adapter({ buildOptions: { compile: true } }).adapt(builder());
-
- const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('assetlinks.json');
- expect(source).toContain('...client_asset(".well-known/assetlinks.json", asset_0)');
- expect(source).not.toContain('.vite/manifest.json');
- });
-
- test('rejects literal wildcard filenames in regular builds', async () => {
- await expect(adapter().adapt(builder({ client_files: ['asterisk*.txt'] }))).rejects.toThrow(
- 'Rename the file to remove the `*` character'
- );
- expect(build).not.toHaveBeenCalled();
- });
-
- test('rejects literal wildcard filenames in compiled executables', async () => {
- mock_embedded_files({ client: ['asterisk*.txt'] });
-
- await expect(adapter({ buildOptions: { compile: true } }).adapt(builder())).rejects.toThrow(
- 'Rename the file to remove the `*` character'
- );
- expect(build).not.toHaveBeenCalled();
- });
-
- test('does not duplicate the base path for prerendered pages', async () => {
- await adapter().adapt(
- builder({
- base: '/base',
- app_path: 'base/_app',
- prerendered_files: ['prerendered/index.html'],
- prerendered_pages: [['/base/prerendered/', { file: 'prerendered/index.html' }]]
- })
- );
-
- const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('...prerendered_page("/base/prerendered/", "prerendered/index.html")');
- expect(source).not.toContain('/base/base/');
- });
-
- test('serves prerendered redirects from their original paths', async () => {
- await adapter().adapt(
- builder({
- prerendered_redirects: [['/old', { status: 301, location: '/new' }]]
- })
- );
-
- const source = build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('prerendered_redirect("/old", 301, "/new")');
- });
-
- test('passes supported advanced build options to Bun', async () => {
- mock_embedded_files({ client: ['data.json'] });
-
- await adapter({
- out: 'dist',
- buildOptions: {
- compile: { outfile: 'advanced-app', target: 'bun-linux-x64' },
- minify: true,
- bytecode: true,
- sourcemap: 'linked'
- }
- }).adapt(builder());
-
- expect(build.mock.calls[0][0]).toMatchObject({
- target: 'bun',
- format: 'esm',
- outdir: 'dist',
- compile: { outfile: 'advanced-app', target: 'bun-linux-x64' },
- minify: true,
- bytecode: true,
- sourcemap: 'linked'
- });
- });
-
- test('runs server instrumentation before starting the server', async () => {
- const test_builder = builder({ instrumentation: true });
-
- await adapter({ out: 'dist' }).adapt(test_builder);
-
- const options = build.mock.calls[0][0];
- expect(options.entrypoints).toEqual([index_file]);
- expect(options.files[index_file]).toBe(
- `import ".svelte-kit/output/server/instrumentation.server.js";\nawait import(${JSON.stringify(start_file)});`
- );
- expect(options.files[start_file]).toBe(adapter_entrypoint);
- expect(test_builder.instrument).not.toHaveBeenCalled();
- });
-
- test('runs server instrumentation before starting a compiled executable', async () => {
- const test_builder = builder({ instrumentation: true });
-
- await adapter({ buildOptions: { compile: true } }).adapt(test_builder);
-
- const options = build.mock.calls[0][0];
- expect(options.entrypoints).toEqual([index_file]);
- expect(options.files[index_file]).toBe(
- `import ".svelte-kit/output/server/instrumentation.server.js";\nawait import(${JSON.stringify(start_file)});`
- );
- expect(options.files[start_file]).toBe(adapter_entrypoint);
- expect(test_builder.instrument).not.toHaveBeenCalled();
- });
-});
-
-test('the runtime reader reuses the generated Bun file', () => {
- const source = readFileSync(new URL('./src/handler.js', import.meta.url), 'utf8');
- expect(source).toContain('server_assets.get(file)?.stream() ?? null');
- expect(source).not.toContain('Bun.file(');
- expect(source).not.toContain('asset_path');
-});
-
-test('publishes the runtime sources without a stale build lifecycle', () => {
- const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
- expect(pkg.files).toContain('src/*.js');
- expect(pkg.files).not.toContain('files');
- expect(pkg.scripts.build).toBeUndefined();
- expect(pkg.scripts.prepublishOnly).toBeUndefined();
-});
-
-function mock_embedded_files({
- client = [],
- pages = [],
- dependencies = [],
- data = []
-}: {
- client?: string[];
- pages?: string[];
- dependencies?: string[];
- data?: string[];
-}) {
- vi.mocked(readdir).mockImplementation((path) => {
- const directory = String(path);
- const files = directory.endsWith('/client')
- ? client
- : directory.endsWith('/prerendered/pages')
- ? pages
- : directory.endsWith('/prerendered/dependencies')
- ? dependencies
- : data;
-
- return files.map((file) => {
- const segments = file.split('/');
- const name = /** @type {string} */ segments.pop();
- return {
- name,
- parentPath: [directory, ...segments].join('/'),
- isFile: () => true
- };
- }) as any;
- });
-}
-
-function builder({
- client_files = [],
- prerendered_files = [],
- prerendered_pages = [],
- prerendered_redirects = [],
- routes = [],
- server_assets = [],
- app_path = '_app',
- base = '',
- origin,
- instrumentation = false
-}: {
- client_files?: string[];
- prerendered_files?: string[];
- prerendered_pages?: Array<[string, { file: string }]>;
- prerendered_redirects?: Array<[string, { status: number; location: string }]>;
- routes?: Array<{ id: string; prerender: boolean | string }>;
- server_assets?: string[];
- app_path?: string;
- base?: string;
- origin?: string;
- instrumentation?: boolean;
-} = {}) {
- return {
- config: { kit: { outDir: '.svelte-kit', paths: { base, origin } } },
- routes,
- prerendered: {
- pages: new Map(prerendered_pages),
- redirects: new Map(prerendered_redirects)
- },
- log: { minor() {}, error() {}, warn() {}, info() {} },
- getServerDirectory: () => '.svelte-kit/output/server',
- rimraf() {},
- writeClient: () => client_files,
- writePrerendered: () => prerendered_files,
- findServerAssets: vi.fn(() => server_assets),
- generateManifest: () => '{}',
- getAppPath: () => app_path,
- hasServerInstrumentationFile: () => instrumentation,
- instrument: vi.fn()
- } as any;
-}
diff --git a/packages/adapter-bun/src/env.spec.ts b/packages/adapter-bun/src/env.spec.ts
deleted file mode 100644
index 67a48eec7d56..000000000000
--- a/packages/adapter-bun/src/env.spec.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { afterEach, describe, expect, test, vi } from 'vitest';
-import { boolean_env, bytes_env, number_env } from './env.js';
-
-vi.mock('MANIFEST', () => ({ env_prefix: '' }));
-
-describe('boolean_env', () => {
- afterEach(() => vi.unstubAllEnvs());
-
- test.each(['1', 'true', 'YES', 'on'])('parses %s as true', (value) => {
- vi.stubEnv('OPTION', value);
- expect(boolean_env('OPTION')).toBe(true);
- });
-
- test.each(['0', 'false', 'NO', 'off'])('parses %s as false', (value) => {
- vi.stubEnv('OPTION', value);
- expect(boolean_env('OPTION')).toBe(false);
- });
-
- test('uses the fallback when the variable is not set', () => {
- expect(boolean_env('OPTION', true)).toBe(true);
- });
-
- test('rejects other values', () => {
- vi.stubEnv('OPTION', 'maybe');
- expect(() => boolean_env('OPTION')).toThrow('expected a boolean');
- });
-});
-
-describe('number_env', () => {
- afterEach(() => vi.unstubAllEnvs());
-
- test('parses non-negative integers', () => {
- vi.stubEnv('OPTION', '0');
- expect(number_env('OPTION')).toBe(0);
- });
-
- test('enforces limits', () => {
- vi.stubEnv('OPTION', '256');
- expect(() => number_env('OPTION', undefined, { max: 255 })).toThrow(
- 'expected an integer between 0 and 255'
- );
- });
-
- test('rejects non-integers', () => {
- vi.stubEnv('OPTION', '1.5');
- expect(() => number_env('OPTION')).toThrow('expected a non-negative integer');
- });
-});
-
-describe('bytes_env', () => {
- afterEach(() => vi.unstubAllEnvs());
-
- test.each([
- ['0', 0],
- ['512', 512],
- ['512K', 512 * 1024],
- ['1.5M', 1.5 * 1024 * 1024],
- ['2g', 2 * 1024 * 1024 * 1024]
- ])('parses %s as a byte count', (value, expected) => {
- vi.stubEnv('OPTION', value);
- expect(bytes_env('OPTION')).toBe(expected);
- });
-
- test('uses the fallback when the variable is not set', () => {
- expect(bytes_env('OPTION', 512 * 1024)).toBe(512 * 1024);
- });
-
- test.each(['', '-1', '1KB', 'one', '0.1'])('rejects invalid byte counts', (value) => {
- vi.stubEnv('OPTION', value);
- expect(() => bytes_env('OPTION')).toThrow('Invalid value for environment variable OPTION');
- });
-});
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
new file mode 100644
index 000000000000..2ad1f49637ab
--- /dev/null
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -0,0 +1,364 @@
+import { readdir } from 'node:fs/promises';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import adapter from '../index.js';
+
+const package_dir = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
+const index_file = `${package_dir}/src/index.js`;
+const manifest_file = `${package_dir}/.svelte-kit/output/server/manifest.js`;
+const routes_file = `${package_dir}/src/routes.js`;
+const options_file = `${package_dir}/src/options.js`;
+const start_file = `${package_dir}/src/start.js`;
+
+vi.mock('node:fs/promises', async (import_original) => {
+ const actual = await import_original();
+ return { ...actual, readdir: vi.fn() };
+});
+
+const bun = vi.hoisted(() => ({
+ entrypoint: '// generated server entrypoint',
+ build: vi.fn(async (_options: any): Promise => ({ success: true, logs: [], outputs: [] })),
+ file: vi.fn((_path: string) => ({ text: async () => '// generated server entrypoint' }))
+}));
+
+beforeEach(() => {
+ vi.stubGlobal('Bun', { build: bun.build, file: bun.file });
+ vi.mocked(readdir).mockResolvedValue([]);
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.clearAllMocks();
+});
+
+describe('adapter contract', () => {
+ test('identifies itself and declares supported SvelteKit features', () => {
+ const instance = adapter();
+
+ expect(instance.name).toBe('@sveltejs/adapter-bun');
+ expect(instance.supports?.read?.({ route: { id: '/file' }, config: {} })).toBe(true);
+ expect(instance.supports?.instrumentation?.()).toBe(true);
+ });
+
+ test('requires the SvelteKit build to run in Bun', async () => {
+ vi.stubGlobal('Bun', undefined);
+
+ await expect(adapter().adapt(create_builder())).rejects.toThrow(
+ 'adapter-bun requires running the SvelteKit build with Bun'
+ );
+ });
+});
+
+describe('Bun build configuration', () => {
+ test('cleans the output and supplies production defaults', async () => {
+ const builder = create_builder();
+ await adapter().adapt(builder);
+
+ expect(builder.rimraf).toHaveBeenCalledWith('build');
+ expect(builder.log.minor).toHaveBeenCalledWith('Building server');
+
+ const options = bun.build.mock.calls[0][0];
+ expect(options).toMatchObject({
+ entrypoints: [index_file],
+ outdir: 'build',
+ target: 'bun',
+ format: 'esm',
+ splitting: true,
+ sourcemap: 'external',
+ conditions: ['bun', 'node'],
+ throw: false,
+ compile: false
+ });
+ expect(options.naming).toEqual({
+ entry: '[name].[ext]',
+ chunk: 'server/chunks/[name]-[hash].[ext]',
+ asset: 'server/assets/[name]-[hash].[ext]'
+ });
+ expect(options.plugins).toHaveLength(1);
+ expect(options.plugins[0].name).toBe('adapter-bun');
+ });
+
+ test('generates manifest and server-option modules', async () => {
+ const builder = create_builder({ base: '/docs', origin: 'https://example.com' });
+ await adapter({
+ envPrefix: 'APP_',
+ serverOptions: { hostname: '127.0.0.1', port: 4000, development: true }
+ }).adapt(builder);
+
+ const files = bun.build.mock.calls[0][0].files;
+ expect(files[manifest_file]).toBe(
+ 'export const manifest = {"appDir":"_app"};\n' +
+ 'export const base = "/docs";\n' +
+ 'export const embed = false;\n' +
+ 'export const env_prefix = "APP_";\n' +
+ 'export const origin = "https://example.com";'
+ );
+ expect(files[options_file]).toBe(
+ 'export default {"hostname":"127.0.0.1","port":4000,"development":true};'
+ );
+ expect(builder.generateManifest).toHaveBeenCalledWith({ relativePath: './' });
+ });
+
+ test('resolves generated runtime modules through the Bun plugin', async () => {
+ await adapter().adapt(create_builder());
+ const on_resolve = vi.fn();
+ bun.build.mock.calls[0][0].plugins[0].setup({ onResolve: on_resolve });
+
+ expect(on_resolve).toHaveBeenCalledWith(
+ { filter: /^(SERVER|MANIFEST|ROUTES|SERVER_OPTIONS)$/ },
+ expect.any(Function)
+ );
+ const resolve_module = on_resolve.mock.calls[0][1];
+ expect(resolve_module({ path: 'SERVER' })).toEqual({
+ path: '.svelte-kit/output/server/index.js'
+ });
+ expect(resolve_module({ path: 'MANIFEST' })).toEqual({ path: manifest_file });
+ expect(resolve_module({ path: 'ROUTES' })).toEqual({ path: routes_file });
+ expect(resolve_module({ path: 'SERVER_OPTIONS' })).toEqual({ path: options_file });
+ });
+
+ test('passes supported advanced options while retaining reserved options', async () => {
+ await adapter({
+ out: 'dist',
+ buildOptions: {
+ compile: { outfile: 'application', target: 'bun-linux-x64' },
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked',
+ drop: ['debugger']
+ }
+ }).adapt(create_builder());
+
+ expect(bun.build.mock.calls[0][0]).toMatchObject({
+ outdir: 'dist',
+ target: 'bun',
+ format: 'esm',
+ minify: true,
+ bytecode: true,
+ sourcemap: 'linked',
+ drop: ['debugger'],
+ compile: { outfile: 'application', target: 'bun-linux-x64' }
+ });
+ });
+
+ test.each([
+ [true, { outfile: 'server' }],
+ ['bun-linux-x64', { outfile: 'server', target: 'bun-linux-x64' }],
+ [
+ { target: 'bun-windows-x64', windows: { hideConsole: true } },
+ {
+ outfile: 'server',
+ target: 'bun-windows-x64',
+ windows: { hideConsole: true }
+ }
+ ]
+ ] as const)('normalizes compile option %j', async (compile, expected) => {
+ await adapter({ buildOptions: { compile } }).adapt(create_builder());
+
+ expect(bun.build.mock.calls[0][0].compile).toEqual(expected);
+ });
+
+ test('loads instrumentation before the generated server entrypoint', async () => {
+ const builder = create_builder({ instrumentation: true });
+ await adapter().adapt(builder);
+
+ const files = bun.build.mock.calls[0][0].files;
+ expect(files[index_file]).toBe(
+ `import ".svelte-kit/output/server/instrumentation.server.js";\nawait import(${JSON.stringify(start_file)});`
+ );
+ expect(files[start_file]).toBe(bun.entrypoint);
+ expect(builder.instrument).not.toHaveBeenCalled();
+ });
+
+ test('reports every Bun diagnostic before failing the build', async () => {
+ bun.build.mockResolvedValueOnce({
+ success: false,
+ logs: [
+ { level: 'error', message: 'broken' },
+ { level: 'warning', message: 'careful' },
+ { level: 'info', message: 'context' }
+ ],
+ outputs: []
+ });
+ const builder = create_builder();
+
+ await expect(adapter().adapt(builder)).rejects.toBeInstanceOf(AggregateError);
+ expect(builder.log.error).toHaveBeenCalledWith('broken');
+ expect(builder.log.warn).toHaveBeenCalledWith('careful');
+ expect(builder.log.info).toHaveBeenCalledWith('context');
+ });
+});
+
+describe('generated routes', () => {
+ test('writes regular-build assets and excludes prerendered routes from server reads', async () => {
+ const dynamic = { id: '/read', prerender: false };
+ const prerendered = { id: '/prerendered', prerender: true };
+ const builder = create_builder({
+ client_files: ['data.json', '_app/immutable/read.txt'],
+ prerendered_files: ['page/index.html', 'icon.png'],
+ prerendered_pages: [['/page/', { file: 'page/index.html' }]],
+ prerendered_redirects: [['/old', { status: 301, location: '/new' }]],
+ routes: [dynamic, prerendered],
+ server_assets: ['_app/immutable/read.txt']
+ });
+
+ await adapter().adapt(builder);
+
+ expect(builder.findServerAssets).toHaveBeenCalledWith([dynamic]);
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain('...client_asset("data.json")');
+ expect(source).toContain('...client_asset("_app/immutable/read.txt")');
+ expect(source).toContain('...prerendered_page("/page/", "page/index.html")');
+ expect(source).toContain('prerendered_asset("icon.png")');
+ expect(source).toContain('prerendered_redirect("/old", 301, "/new")');
+ expect(source).toContain(
+ '["_app/immutable/read.txt", server_asset("_app/immutable/read.txt")]'
+ );
+ });
+
+ test('does not prepend the base to prerendered route paths a second time', async () => {
+ await adapter().adapt(
+ create_builder({
+ base: '/base',
+ prerendered_files: ['page/index.html'],
+ prerendered_pages: [['/base/page/', { file: 'page/index.html' }]]
+ })
+ );
+
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain('...prerendered_page("/base/page/", "page/index.html")');
+ expect(source).not.toContain('/base/base/');
+ });
+
+ test('embeds assets in compiled executables and ignores Vite metadata', async () => {
+ mock_files({
+ client: ['data.json', '.vite/manifest.json', '.well-known/asset.txt', '_app/read.txt'],
+ pages: ['page/index.html', 'favicon.ico'],
+ dependencies: ['dependency.json'],
+ data: ['page/__data.json']
+ });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(
+ create_builder({
+ prerendered_pages: [['/page/', { file: 'page/index.html' }]],
+ server_assets: ['_app/read.txt']
+ })
+ );
+
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain("with { type: 'file' }");
+ expect(source).toContain('...client_asset("data.json", asset_0)');
+ expect(source).toContain('...client_asset(".well-known/asset.txt", asset_1)');
+ expect(source).toContain('...prerendered_page("/page/", asset_3)');
+ expect(source).toContain('prerendered_asset("favicon.ico", asset_4)');
+ expect(source).toContain('prerendered_asset("dependency.json", asset_5)');
+ expect(source).toContain('prerendered_asset("page/__data.json", asset_6)');
+ expect(source).toContain('["_app/read.txt", server_asset("_app/read.txt", asset_2)]');
+ expect(source).not.toContain('.vite/manifest.json');
+ });
+
+ test.each([false, true])('rejects wildcard filenames when compile is %s', async (compile) => {
+ if (compile) mock_files({ client: ['literal*.txt'] });
+ const builder = create_builder({ client_files: ['literal*.txt'] });
+
+ await expect(adapter({ buildOptions: { compile } }).adapt(builder)).rejects.toThrow(
+ 'Bun treats literal `*` characters in route paths as wildcards'
+ );
+ expect(bun.build).not.toHaveBeenCalled();
+ });
+
+ test('fails when a prerendered page is absent from compiled build output', async () => {
+ await expect(
+ adapter({ buildOptions: { compile: true } }).adapt(
+ create_builder({ prerendered_pages: [['/missing', { file: 'missing.html' }]] })
+ )
+ ).rejects.toThrow('Could not find prerendered page missing.html for route /missing');
+ });
+
+ test('fails when a server-readable asset is absent from compiled build output', async () => {
+ await expect(
+ adapter({ buildOptions: { compile: true } }).adapt(
+ create_builder({ server_assets: ['missing.txt'] })
+ )
+ ).rejects.toThrow('Could not find server asset missing.txt');
+ });
+});
+
+function mock_files({
+ client = [],
+ pages = [],
+ dependencies = [],
+ data = []
+}: {
+ client?: string[];
+ pages?: string[];
+ dependencies?: string[];
+ data?: string[];
+}) {
+ vi.mocked(readdir).mockImplementation((path) => {
+ const directory = String(path);
+ const files = directory.endsWith('/client')
+ ? client
+ : directory.endsWith('/prerendered/pages')
+ ? pages
+ : directory.endsWith('/prerendered/dependencies')
+ ? dependencies
+ : data;
+
+ return Promise.resolve(
+ files.map((file) => {
+ const segments = file.split('/');
+ const name = /** @type {string} */ segments.pop();
+ return {
+ name,
+ parentPath: [directory, ...segments].join('/'),
+ isFile: () => true
+ };
+ })
+ ) as unknown as ReturnType;
+ });
+}
+
+function create_builder({
+ client_files = [],
+ prerendered_files = [],
+ prerendered_pages = [],
+ prerendered_redirects = [],
+ routes = [],
+ server_assets = [],
+ base = '',
+ origin,
+ instrumentation = false
+}: {
+ client_files?: string[];
+ prerendered_files?: string[];
+ prerendered_pages?: Array<[string, { file: string }]>;
+ prerendered_redirects?: Array<[string, { status: number; location: string }]>;
+ routes?: Array<{ id: string; prerender: boolean | string }>;
+ server_assets?: string[];
+ base?: string;
+ origin?: string;
+ instrumentation?: boolean;
+} = {}) {
+ return {
+ config: { kit: { outDir: '.svelte-kit', paths: { base, origin } } },
+ routes,
+ prerendered: {
+ pages: new Map(prerendered_pages),
+ redirects: new Map(prerendered_redirects)
+ },
+ log: {
+ minor: vi.fn(),
+ error: vi.fn(),
+ warn: vi.fn(),
+ info: vi.fn()
+ },
+ getServerDirectory: () => '.svelte-kit/output/server',
+ rimraf: vi.fn(),
+ writeClient: vi.fn(() => client_files),
+ writePrerendered: vi.fn(() => prerendered_files),
+ findServerAssets: vi.fn(() => server_assets),
+ generateManifest: vi.fn(() => '{"appDir":"_app"}'),
+ hasServerInstrumentationFile: () => instrumentation,
+ instrument: vi.fn()
+ } as any;
+}
diff --git a/packages/adapter-bun/test/apps/basic/test/browser.test.js b/packages/adapter-bun/test/apps/basic/test/browser.test.js
new file mode 100644
index 000000000000..f7a83740b119
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/test/browser.test.js
@@ -0,0 +1,10 @@
+import { expect, test } from '@playwright/test';
+
+test('renders on the server and hydrates in the browser', async ({ page }) => {
+ await page.goto('/');
+
+ await expect(page.locator('h1')).toHaveText('Bun adapter fixture');
+ await expect(page.getByRole('button')).toHaveText('Count: 0');
+ await page.getByRole('button').click();
+ await expect(page.getByRole('button')).toHaveText('Count: 1');
+});
diff --git a/packages/adapter-bun/test/apps/basic/test/server.test.js b/packages/adapter-bun/test/apps/basic/test/server.test.js
new file mode 100644
index 000000000000..27283ffcbaaf
--- /dev/null
+++ b/packages/adapter-bun/test/apps/basic/test/server.test.js
@@ -0,0 +1,124 @@
+import { expect, test } from '@playwright/test';
+import process from 'node:process';
+
+const compiled = process.env.COMPILE === 'true';
+
+test('provides the original request and Bun server on platform', async ({ request }) => {
+ const response = await request.get('/platform');
+ const platform = await response.json();
+
+ expect(platform.address).toBeTruthy();
+ expect(platform.request).toBe(true);
+ expect(platform.server).toBe(true);
+ expect(platform.id).toEqual(expect.any(String));
+ expect(platform.protocol).toBe('http');
+ expect(platform.pendingRequests).toBeGreaterThanOrEqual(1);
+ expect(platform.pendingWebSockets).toBe(0);
+ expect(platform.subscribers).toBe(0);
+});
+
+test('runs server instrumentation before accepting requests', async ({ request }) => {
+ const response = await request.get('/instrumented');
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toBe('instrumentation-ready');
+});
+
+test('serves static files and implements HEAD natively', async ({ request }) => {
+ const response = await request.get('/data.json');
+ expect(response.status()).toBe(200);
+ expect(response.headers()['content-type']).toContain('application/json');
+ expect(await response.json()).toEqual({ source: 'static' });
+
+ const head = await request.head('/data.json');
+ expect(head.status()).toBe(200);
+ expect(head.headers()['content-length']).toBe(response.headers()['content-length']);
+ expect(await head.text()).toBe('');
+});
+
+for (const [url, content_type, body] of [
+ ['/sub/', 'text/html;charset=utf-8', 'directory index'],
+ ['/encoded%20name.txt', 'text/plain;charset=utf-8', 'encoded filename'],
+ ['/.well-known/adapter-bun.txt', 'text/plain;charset=utf-8', 'well-known asset']
+]) {
+ test(`serves ${url} with its MIME type`, async ({ request }) => {
+ const response = await request.get(url);
+ expect(response.status()).toBe(200);
+ expect(response.headers()['content-type']).toBe(content_type);
+ expect(await response.text()).toContain(body);
+ });
+}
+
+test('uses Bun conditional requests and byte ranges for filesystem assets', async ({ request }) => {
+ const initial = await request.get('/data.json');
+ const body = await initial.text();
+
+ if (compiled) {
+ const etag = initial.headers()['etag'];
+ expect(etag).toBeTruthy();
+ const cached = await request.get('/data.json', { headers: { 'if-none-match': etag } });
+ expect(cached.status()).toBe(304);
+ } else {
+ const last_modified = initial.headers()['last-modified'];
+ expect(last_modified).toBeTruthy();
+ const cached = await request.get('/data.json', {
+ headers: { 'if-modified-since': last_modified }
+ });
+ expect(cached.status()).toBe(304);
+
+ const range = await request.get('/data.json', { headers: { range: 'bytes=0-3' } });
+ expect(range.status()).toBe(206);
+ expect(range.headers()['accept-ranges']).toBe('bytes');
+ expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`);
+ expect(await range.text()).toBe(body.slice(0, 4));
+ }
+});
+
+test('serves prerendered pages, endpoints, and canonical redirects', async ({ request }) => {
+ const page = await request.get('/prerendered/');
+ expect(page.status()).toBe(200);
+ expect(await page.text()).toContain('Prerendered by SvelteKit');
+
+ const icon = await request.get('/prerendered.ico');
+ expect(icon.status()).toBe(200);
+ expect(icon.headers()['content-type']).toBe('image/x-icon');
+ expect(await icon.body()).toEqual(Buffer.from([0, 0, 1, 0]));
+
+ const redirect = await request.get('/prerendered?via=test', { maxRedirects: 0 });
+ expect(redirect.status()).toBe(308);
+ expect(redirect.headers()['location']).toBe('/prerendered/?via=test');
+});
+
+test('uses SvelteKit for non-GET requests that share a static pathname', async ({ request }) => {
+ const response = await request.post('/data.json', {
+ headers: { origin: 'http://localhost:4174' }
+ });
+ expect(response.status()).toBe(200);
+ expect(await response.json()).toEqual({ source: 'endpoint' });
+});
+
+test('makes imported assets available to $app/server read', async ({ request }) => {
+ const response = await request.get('/read');
+ expect(response.status()).toBe(200);
+ expect(await response.text()).toBe('server-readable asset\n');
+});
+
+test('sets immutable caching only on generated immutable assets', async ({ request }) => {
+ const document = await request.get('/');
+ const asset = /["']([^"']*_app\/immutable\/[^"']+)["']/.exec(await document.text())?.[1];
+ expect(asset).toBeTruthy();
+
+ const immutable = await request.get(/** @type {string} */ (asset));
+ expect(immutable.headers()['cache-control']).toBe('public,max-age=31536000,immutable');
+
+ const regular = await request.get('/data.json');
+ expect(regular.headers()['cache-control']).toBeUndefined();
+});
+
+test('disables timeouts and proxy buffering for server-sent events', async ({ request }) => {
+ const events = await request.get('/event-stream');
+ expect(events.headers()['content-type']).toContain('text/event-stream');
+ expect(events.headers()['x-accel-buffering']).toBe('no');
+
+ const regular = await request.get('/platform');
+ expect(regular.headers()['x-accel-buffering']).toBeUndefined();
+});
diff --git a/packages/adapter-bun/test/apps/basic/test/test.js b/packages/adapter-bun/test/apps/basic/test/test.js
deleted file mode 100644
index cc461e0f3982..000000000000
--- a/packages/adapter-bun/test/apps/basic/test/test.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { expect, test } from '@playwright/test';
-import process from 'node:process';
-
-const compiled = process.env.COMPILE === 'true';
-
-test('renders and hydrates the app', async ({ page }) => {
- await page.goto('/');
- await expect(page.locator('h1')).toHaveText('Hello from Bun!');
- await expect(page.locator('button')).toHaveText('Toggle: false');
- await page.locator('button').click();
- await expect(page.locator('button')).toHaveText('Toggle: true');
-});
-
-test('provides Bun request context', async ({ request }) => {
- const response = await request.get('/platform');
- const body = await response.json();
- expect(body.address).toBeTruthy();
- expect(body.request).toBe(true);
- expect(body.server).toBe(true);
- expect(typeof body.id).toBe('string');
- expect(body.protocol).toBe('http');
- expect(body.pendingRequests).toBeGreaterThanOrEqual(1);
- expect(body.pendingWebSockets).toBe(0);
- expect(body.subscribers).toBe(0);
-});
-
-test('runs server instrumentation before starting the server', async ({ request }) => {
- const response = await request.get('/instrumented');
- expect(response.status()).toBe(200);
- expect(await response.text()).toBe('true');
-});
-
-test('serves static files with Bun file responses', async ({ request }) => {
- const response = await request.get('/data.json');
- expect(response.status()).toBe(200);
- expect(response.headers()['content-type']).toContain('application/json');
- expect(await response.json()).toEqual({ message: 'hello from a static file' });
-
- const head = await request.head('/data.json');
- expect(head.status()).toBe(200);
- expect(head.headers()['content-length']).toBe(response.headers()['content-length']);
- expect(await head.text()).toBe('');
-});
-
-test('serves static directory indexes', async ({ request }) => {
- const response = await request.get('/sub/');
- expect(response.status()).toBe(200);
- expect(response.headers()['content-type']).toBe('text/html;charset=utf-8');
- expect(await response.text()).toContain('Static directory index');
-});
-
-test('serves prerendered non-HTML endpoints', async ({ request }) => {
- const response = await request.get('/prerendered.ico');
- expect(response.status()).toBe(200);
- expect(response.headers()['content-type']).toBe('image/x-icon');
- expect(await response.body()).toEqual(Buffer.from([0, 0, 1, 0]));
-});
-
-test('uses Bun validators and ranges for static files', async ({ request }) => {
- const initial = await request.get('/data.json');
- const body = await initial.text();
-
- if (compiled) {
- const etag = initial.headers()['etag'];
- expect(etag).toBeTruthy();
-
- const not_modified = await request.get('/data.json', {
- headers: { 'if-none-match': etag }
- });
- expect(not_modified.status()).toBe(304);
- } else {
- const last_modified = initial.headers()['last-modified'];
- expect(last_modified).toBeTruthy();
-
- const not_modified = await request.get('/data.json', {
- headers: { 'if-modified-since': last_modified }
- });
- expect(not_modified.status()).toBe(304);
-
- const range = await request.get('/data.json', {
- headers: { range: 'bytes=0-3' }
- });
- expect(range.status()).toBe(206);
- expect(range.headers()['accept-ranges']).toBe('bytes');
- expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`);
- expect(await range.text()).toBe(body.slice(0, 4));
- }
-});
-
-test('serves URL-encoded static filenames', async ({ request }) => {
- const response = await request.get('/encoded%20name.txt');
- expect(response.status()).toBe(200);
- expect(await response.text()).toBe('hello from an encoded filename\n');
-});
-
-test('serves dotfiles from the static directory', async ({ request }) => {
- const response = await request.get('/.well-known/adapter-bun.txt');
- expect(response.status()).toBe(200);
- expect(await response.text()).toBe('adapter bun\n');
-});
-
-test('reads an imported server asset', async ({ request }) => {
- const response = await request.get('/read');
- expect(response.status()).toBe(200);
- expect(await response.text()).toBe('Hello from $app/server read\n');
-});
-
-test('caches immutable client assets', async ({ request }) => {
- const page = await request.get('/');
- const asset = /["']([^"']*_app\/immutable\/[^"']+)["']/.exec(await page.text())?.[1];
- expect(asset).toBeTruthy();
-
- const asset_response = await request.get(/** @type {string} */ (asset));
- expect(asset_response.headers()['cache-control']).toBe('public,max-age=31536000,immutable');
-});
-
-test('passes non-GET requests for static paths to SvelteKit', async ({ request }) => {
- const response = await request.post('/data.json', {
- headers: { origin: 'http://localhost:4174' }
- });
- expect(response.status()).toBe(200);
- expect(await response.json()).toEqual({ message: 'hello from a server endpoint' });
-});
-
-test('redirects prerendered paths to their canonical trailing slash', async ({ request }) => {
- const response = await request.get('/prerendered?source=test', { maxRedirects: 0 });
- expect(response.status()).toBe(308);
- expect(response.headers()['location']).toBe('/prerendered/?source=test');
-});
-
-test('configures long-lived event streams', async ({ request }) => {
- const response = await request.get('/event-stream');
- expect(response.headers()['content-type']).toContain('text/event-stream');
- expect(response.headers()['x-accel-buffering']).toBe('no');
-});
diff --git a/packages/adapter-bun/test/apps/basic/vite.config.js b/packages/adapter-bun/test/apps/basic/vite.config.js
index b795eb652ad2..c90053293e35 100644
--- a/packages/adapter-bun/test/apps/basic/vite.config.js
+++ b/packages/adapter-bun/test/apps/basic/vite.config.js
@@ -17,7 +17,7 @@ const buildOptions =
},
minify: true,
bytecode: true,
- sourcemap: 'linked'
+ sourcemap: /** @type {const} */ ('linked')
}
: process.env.COMPILE === 'true'
? { compile: true }
diff --git a/packages/adapter-bun/test/env.spec.ts b/packages/adapter-bun/test/env.spec.ts
new file mode 100644
index 000000000000..c04abea359f6
--- /dev/null
+++ b/packages/adapter-bun/test/env.spec.ts
@@ -0,0 +1,160 @@
+import process from 'node:process';
+import { afterEach, describe, expect, test, vi } from 'vitest';
+
+const changed = new Set();
+
+afterEach(() => {
+ for (const name of changed) delete process.env[name];
+ changed.clear();
+ vi.resetModules();
+ vi.doUnmock('MANIFEST');
+});
+
+describe('env', () => {
+ test('uses the prefixed value when present and otherwise returns the fallback', async () => {
+ set_env('APP_PORT', '4000');
+ const { env } = await load_env('APP_');
+
+ expect(env('PORT', '3000')).toBe('4000');
+ expect(env('HOST', 'localhost')).toBe('localhost');
+ });
+
+ test('treats an explicitly empty value as present', async () => {
+ set_env('APP_HOST', '');
+ const { env } = await load_env('APP_');
+
+ expect(env('HOST', 'localhost')).toBe('');
+ });
+
+ test('rejects unexpected variables that use a configured prefix', async () => {
+ set_env('UNIQUE_ADAPTER_OPTION', 'value');
+
+ await expect(load_env('UNIQUE_ADAPTER_')).rejects.toThrow(
+ 'unexpectedly saw UNIQUE_ADAPTER_OPTION'
+ );
+ });
+});
+
+describe('boolean_env', () => {
+ test.each(['1', 'true', 'TRUE', 'yes', 'YES', 'on', 'ON'])('parses %s as true', async (value) => {
+ set_env('OPTION', value);
+ const { boolean_env } = await load_env();
+ expect(boolean_env('OPTION')).toBe(true);
+ });
+
+ test.each(['0', 'false', 'FALSE', 'no', 'NO', 'off', 'OFF'])(
+ 'parses %s as false',
+ async (value) => {
+ set_env('OPTION', value);
+ const { boolean_env } = await load_env();
+ expect(boolean_env('OPTION')).toBe(false);
+ }
+ );
+
+ test('returns the fallback for an absent variable', async () => {
+ const { boolean_env } = await load_env();
+ expect(boolean_env('OPTION', true)).toBe(true);
+ });
+
+ test('rejects any other value', async () => {
+ set_env('OPTION', 'enabled');
+ const { boolean_env } = await load_env();
+ expect(() => boolean_env('OPTION')).toThrow(
+ 'Invalid value for environment variable OPTION: "enabled" (expected a boolean)'
+ );
+ });
+});
+
+describe('number_env', () => {
+ test.each([
+ ['0', 0],
+ ['42', 42],
+ ['9007199254740991', Number.MAX_SAFE_INTEGER]
+ ])('parses %s as %d', async (value, expected) => {
+ set_env('OPTION', value);
+ const { number_env } = await load_env();
+ expect(number_env('OPTION')).toBe(expected);
+ });
+
+ test('returns the fallback for an absent variable', async () => {
+ const { number_env } = await load_env();
+ expect(number_env('OPTION', 10)).toBe(10);
+ });
+
+ test.each(['-1', '+1', '1.5', '1e2', ' 1', ''])(
+ 'rejects non-integer syntax %j',
+ async (value) => {
+ set_env('OPTION', value);
+ const { number_env } = await load_env();
+ expect(() => number_env('OPTION')).toThrow('expected a non-negative integer');
+ }
+ );
+
+ test('enforces a minimum', async () => {
+ set_env('OPTION', '0');
+ const { number_env } = await load_env();
+ expect(() => number_env('OPTION', undefined, { min: 1 })).toThrow(
+ 'expected an integer at least 1'
+ );
+ });
+
+ test('enforces a bounded range', async () => {
+ set_env('OPTION', '256');
+ const { number_env } = await load_env();
+ expect(() => number_env('OPTION', undefined, { max: 255 })).toThrow(
+ 'expected an integer between 0 and 255'
+ );
+ });
+
+ test('rejects integers beyond the safe range', async () => {
+ set_env('OPTION', '9007199254740992');
+ const { number_env } = await load_env();
+ expect(() => number_env('OPTION')).toThrow('expected an integer at least 0');
+ });
+});
+
+describe('bytes_env', () => {
+ test.each([
+ ['0', 0],
+ ['512', 512],
+ ['.5K', 512],
+ ['512K', 512 * 1024],
+ ['1.5M', 1.5 * 1024 * 1024],
+ ['2g', 2 * 1024 * 1024 * 1024]
+ ])('parses %s as %d bytes', async (value, expected) => {
+ set_env('OPTION', value);
+ const { bytes_env } = await load_env();
+ expect(bytes_env('OPTION')).toBe(expected);
+ });
+
+ test('returns the fallback for an absent variable', async () => {
+ const { bytes_env } = await load_env();
+ expect(bytes_env('OPTION', 512 * 1024)).toBe(512 * 1024);
+ });
+
+ test.each(['', '-1', '1KB', '1T', 'one'])('rejects invalid syntax %j', async (value) => {
+ set_env('OPTION', value);
+ const { bytes_env } = await load_env();
+ expect(() => bytes_env('OPTION')).toThrow('expected a non-negative number');
+ });
+
+ test.each(['0.1', '9007199254740992'])(
+ 'rejects a non-whole or unsafe byte count %s',
+ async (value) => {
+ set_env('OPTION', value);
+ const { bytes_env } = await load_env();
+ expect(() => bytes_env('OPTION')).toThrow('expected a non-negative number of whole bytes');
+ }
+ );
+});
+
+async function load_env(prefix = '') {
+ vi.resetModules();
+ vi.doMock('MANIFEST', () => ({ env_prefix: prefix }));
+ return import('../src/env.js');
+}
+
+function set_env(name: string, value: string) {
+ changed.add(name);
+ process.env[name] = value;
+}
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
new file mode 100644
index 000000000000..9ccd8c7c0af4
--- /dev/null
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -0,0 +1,223 @@
+import process from 'node:process';
+import { afterEach, expect, test, vi } from 'vitest';
+
+const environment = new Set();
+
+afterEach(() => {
+ for (const name of environment) delete process.env[name];
+ environment.clear();
+ vi.resetModules();
+ vi.doUnmock('SERVER');
+ vi.doUnmock('MANIFEST');
+ vi.doUnmock('ROUTES');
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+test('initializes SvelteKit with Bun environment variables and server-readable assets', async () => {
+ const loaded = await load_handler();
+
+ expect(loaded.construct).toHaveBeenCalledWith(loaded.manifest);
+ expect(loaded.init).toHaveBeenCalledWith({
+ env: loaded.bun_env,
+ read: expect.any(Function)
+ });
+ const { read } = loaded.init.mock.calls[0][0];
+ expect(read('asset.txt')).toBe(loaded.stream);
+ expect(read('missing.txt')).toBeNull();
+});
+
+test('normalizes the request origin from the Host header', async () => {
+ const loaded = await load_handler();
+ const original = new Request('http://127.0.0.1:3000/path?query=yes', {
+ headers: { host: 'public.example:8080' }
+ });
+
+ await loaded.handler(original, loaded.bun_server);
+
+ const [request, options] = loaded.respond.mock.calls[0];
+ expect(request.url).toBe('http://public.example:8080/path?query=yes');
+ expect(options.platform).toEqual({ request: original, server: loaded.bun_server });
+ loaded.request_ip.mockReturnValue({ address: '127.0.0.1', port: 5000, family: 'IPv4' });
+ expect(options.getClientAddress()).toBe('127.0.0.1');
+});
+
+test('uses paths.origin as the trusted request origin when configured', async () => {
+ const loaded = await load_handler({ origin: 'https://canonical.example' });
+
+ await loaded.handler(new Request('http://internal/path'), loaded.bun_server);
+
+ expect(loaded.respond.mock.calls[0][0].url).toBe('https://canonical.example/path');
+});
+
+test('derives the public origin from configured proxy headers', async () => {
+ set_env('APP_PROTOCOL_HEADER', 'x-forwarded-proto');
+ set_env('APP_HOST_HEADER', 'x-forwarded-host');
+ set_env('APP_PORT_HEADER', 'x-forwarded-port');
+ const loaded = await load_handler({ envPrefix: 'APP_' });
+ const request = new Request('http://internal/path', {
+ headers: {
+ 'x-forwarded-proto': 'https',
+ 'x-forwarded-host': 'public.example',
+ 'x-forwarded-port': '8443'
+ }
+ });
+
+ await loaded.handler(request, loaded.bun_server);
+
+ expect(loaded.respond.mock.calls[0][0].url).toBe('https://public.example:8443/path');
+});
+
+test.each([
+ ['APP_PROTOCOL_HEADER', 'x-proto', { 'x-proto': 'https%3A' }, 'includes `:`'],
+ ['APP_PORT_HEADER', 'x-port', { 'x-port': 'not-a-port' }, 'invalid port']
+])('returns 400 for an invalid origin from %s', async (name, value, headers, message) => {
+ set_env(name, value);
+ const loaded = await load_handler({ envPrefix: 'APP_' });
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ const response = await loaded.handler(
+ new Request('http://internal/path', { headers }),
+ loaded.bun_server
+ );
+
+ expect(response.status).toBe(400);
+ expect(await response.text()).toBe('Bad Request');
+ expect(loaded.respond).not.toHaveBeenCalled();
+ expect(error).toHaveBeenCalledWith(expect.stringContaining(message));
+});
+
+test('reads a configured client address header', async () => {
+ set_env('APP_ADDRESS_HEADER', 'true-client-ip');
+ const loaded = await load_handler({ envPrefix: 'APP_' });
+
+ await loaded.handler(
+ new Request('http://localhost/', { headers: { 'true-client-ip': '203.0.113.10' } }),
+ loaded.bun_server
+ );
+
+ const get_client_address = loaded.respond.mock.calls[0][1].getClientAddress;
+ expect(get_client_address()).toBe('203.0.113.10');
+});
+
+test('selects a trusted X-Forwarded-For address from the right', async () => {
+ set_env('APP_ADDRESS_HEADER', 'x-forwarded-for');
+ set_env('APP_XFF_DEPTH', '2');
+ const loaded = await load_handler({ envPrefix: 'APP_' });
+
+ await loaded.handler(
+ new Request('http://localhost/', {
+ headers: { 'x-forwarded-for': 'spoofed, 203.0.113.10, 10.0.0.2' }
+ }),
+ loaded.bun_server
+ );
+
+ expect(loaded.respond.mock.calls[0][1].getClientAddress()).toBe('203.0.113.10');
+});
+
+test('reports absent and too-short forwarded address headers', async () => {
+ set_env('APP_ADDRESS_HEADER', 'x-forwarded-for');
+ set_env('APP_XFF_DEPTH', '3');
+ const loaded = await load_handler({ envPrefix: 'APP_' });
+
+ await loaded.handler(new Request('http://localhost/'), loaded.bun_server);
+ let get_client_address = loaded.respond.mock.calls[0][1].getClientAddress;
+ expect(() => get_client_address()).toThrow(
+ 'APP_ADDRESS_HEADER=x-forwarded-for but is absent from request'
+ );
+
+ loaded.respond.mockClear();
+ await loaded.handler(
+ new Request('http://localhost/', { headers: { 'x-forwarded-for': 'client, proxy' } }),
+ loaded.bun_server
+ );
+ get_client_address = loaded.respond.mock.calls[0][1].getClientAddress;
+ expect(() => get_client_address()).toThrow('APP_XFF_DEPTH is 3, but only found 2 addresses');
+});
+
+test('reports when Bun cannot determine the peer address', async () => {
+ const loaded = await load_handler();
+ loaded.request_ip.mockReturnValue(null);
+
+ await loaded.handler(new Request('http://localhost/'), loaded.bun_server);
+
+ expect(() => loaded.respond.mock.calls[0][1].getClientAddress()).toThrow(
+ 'Could not determine client address'
+ );
+});
+
+test('disables timeouts and proxy buffering for event streams', async () => {
+ const loaded = await load_handler({
+ response: new Response('data: ready\n\n', { headers: { 'content-type': 'text/event-stream' } })
+ });
+ const request = new Request('http://localhost/events');
+
+ const response = await loaded.handler(request, loaded.bun_server);
+
+ expect(loaded.timeout).toHaveBeenCalledWith(request, 0);
+ expect(response.headers.get('x-accel-buffering')).toBe('no');
+});
+
+test('leaves ordinary responses and their timeouts unchanged', async () => {
+ const loaded = await load_handler({ response: new Response('ok') });
+
+ const response = await loaded.handler(new Request('http://localhost/'), loaded.bun_server);
+
+ expect(loaded.timeout).not.toHaveBeenCalled();
+ expect(response.headers.has('x-accel-buffering')).toBe(false);
+});
+
+async function load_handler({
+ origin,
+ envPrefix = '',
+ response = new Response('ok')
+}: { origin?: string; envPrefix?: string; response?: Response } = {}) {
+ vi.resetModules();
+ const manifest = { appDir: '_app' };
+ const bun_env = { PUBLIC_VALUE: 'available' };
+ const stream = new ReadableStream();
+ const asset = { stream: vi.fn(() => stream) };
+ const construct = vi.fn();
+ const init = vi.fn(async (_options: any) => {});
+ const respond = vi.fn(async (_request: Request, _options: any) => response);
+
+ class Server {
+ constructor(value: unknown) {
+ construct(value);
+ }
+ init = init;
+ respond = respond;
+ }
+
+ vi.doMock('SERVER', () => ({ Server }));
+ vi.doMock('MANIFEST', () => ({ manifest, origin, env_prefix: envPrefix }));
+ vi.doMock('ROUTES', () => ({ server_assets: new Map([['asset.txt', asset]]) }));
+ vi.stubGlobal('Bun', { env: bun_env });
+
+ const request_ip = vi.fn((_request: Request): any => ({
+ address: '127.0.0.1',
+ port: 5000,
+ family: 'IPv4'
+ }));
+ const timeout = vi.fn((_request: Request, _seconds: number) => {});
+ const bun_server = { requestIP: request_ip, timeout } as any;
+ const { handler } = await import('../src/handler.js');
+
+ return {
+ handler,
+ manifest,
+ bun_env,
+ stream,
+ construct,
+ init,
+ respond,
+ request_ip,
+ timeout,
+ bun_server
+ };
+}
+
+function set_env(name: string, value: string) {
+ environment.add(name);
+ process.env[name] = value;
+}
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
new file mode 100644
index 000000000000..b84d74fb6f07
--- /dev/null
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -0,0 +1,118 @@
+import { afterEach, expect, test, vi } from 'vitest';
+
+afterEach(() => {
+ vi.resetModules();
+ vi.doUnmock('MANIFEST');
+ vi.unstubAllGlobals();
+});
+
+test('client assets use the configured base and URL-encode path segments', async () => {
+ const { routes, file } = await load_routes({ base: '/base' });
+
+ const entries = routes.client_asset('folder/encoded name#1.txt');
+
+ expect(entries).toHaveLength(1);
+ expect(entries[0][0]).toBe('/base/folder/encoded%20name%231.txt');
+ expect(file).toHaveBeenCalledWith('/app/build/client/folder/encoded name#1.txt');
+ expect(entries[0][1]).toHaveProperty('GET');
+ expect((entries[0][1] as any).GET.headers.get('content-type')).toBe('text/plain;charset=utf-8');
+});
+
+test('client index files are also available at their directory URL', async () => {
+ const { routes } = await load_routes({ base: '/base' });
+
+ expect(routes.client_asset('index.html').map(([path]) => path)).toEqual([
+ '/base/index.html',
+ '/base/'
+ ]);
+ expect(routes.client_asset('docs/index.html').map(([path]) => path)).toEqual([
+ '/base/docs/index.html',
+ '/base/docs/'
+ ]);
+});
+
+test('immutable SvelteKit assets receive a long-lived cache policy', async () => {
+ const { routes } = await load_routes({ appDir: '_app' });
+
+ const immutable = (routes.client_asset('_app/immutable/chunk.js')[0][1] as any).GET;
+ const mutable = (routes.client_asset('favicon.ico')[0][1] as any).GET;
+
+ expect(immutable.headers.get('cache-control')).toBe('public,max-age=31536000,immutable');
+ expect(mutable.headers.has('cache-control')).toBe(false);
+});
+
+test('embedded routes use the imported asset instead of a filesystem path', async () => {
+ const { routes, file } = await load_routes({ embed: true });
+
+ routes.client_asset('asset.txt', '/embedded/client.txt');
+ routes.prerendered_asset('asset.txt', '/embedded/prerendered.txt');
+ const server_file = routes.server_asset('asset.txt', '/embedded/server.txt');
+
+ expect(file).toHaveBeenNthCalledWith(1, '/embedded/client.txt');
+ expect(file).toHaveBeenNthCalledWith(2, '/embedded/prerendered.txt');
+ expect(file).toHaveBeenNthCalledWith(3, '/embedded/server.txt');
+ expect(server_file).toMatchObject({ path: '/embedded/server.txt' });
+});
+
+test('server assets resolve from the client output in regular builds', async () => {
+ const { routes, file } = await load_routes();
+
+ const result = routes.server_asset('nested/read.txt');
+
+ expect(file).toHaveBeenCalledWith('/app/build/client/nested/read.txt');
+ expect(result).toMatchObject({ path: '/app/build/client/nested/read.txt' });
+});
+
+test('prerendered assets use the base path and preserve their content type', async () => {
+ const { routes, file } = await load_routes({ base: '/base' });
+ file.mockImplementationOnce((path) => ({ path, type: 'image/x-icon' }));
+
+ const [path, handler] = routes.prerendered_asset('icon.ico');
+
+ expect(path).toBe('/base/icon.ico');
+ expect((handler as any).GET.headers.get('content-type')).toBe('image/x-icon');
+});
+
+test.each([
+ ['/page/', '/page', '/page/?from=test'],
+ ['/page', '/page/', '/page?from=test']
+])(
+ 'prerendered page %s redirects its alternate form %s to the canonical URL',
+ async (canonical, alternate, location) => {
+ const { routes } = await load_routes();
+ const entries = routes.prerendered_page(canonical, 'page.html');
+
+ expect(entries[0][0]).toBe(canonical);
+ expect(entries[1][0]).toBe(alternate);
+ const response = (entries[1][1] as any).GET(
+ new Request(`http://localhost${alternate}?from=test`)
+ );
+ expect(response.status).toBe(308);
+ expect(response.headers.get('location')).toBe(location);
+ }
+);
+
+test('a prerendered root page has no duplicate alternate route', async () => {
+ const { routes } = await load_routes();
+
+ expect(routes.prerendered_page('/', 'index.html')).toHaveLength(1);
+});
+
+test('prerendered redirects retain their status and location', async () => {
+ const { routes } = await load_routes();
+
+ const [path, handler] = routes.prerendered_redirect('/old path', 307, '/new');
+
+ expect(path).toBe('/old%20path');
+ expect((handler as any).GET.status).toBe(307);
+ expect((handler as any).GET.headers.get('location')).toBe('/new');
+});
+
+async function load_routes({ base = '', embed = false, appDir = '_app' } = {}) {
+ vi.resetModules();
+ vi.doMock('MANIFEST', () => ({ manifest: { appDir }, base, embed }));
+ const file = vi.fn((path: string) => ({ path, type: 'text/plain;charset=utf-8' }));
+ vi.stubGlobal('Bun', { main: '/app/build/index.js', file });
+
+ return { routes: await import('../src/routes-util.js'), file };
+}
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
new file mode 100644
index 000000000000..b4b8edf5f6d1
--- /dev/null
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -0,0 +1,168 @@
+import { afterEach, expect, test, vi } from 'vitest';
+
+afterEach(() => {
+ vi.resetModules();
+ vi.doUnmock('node:process');
+ vi.doUnmock('MANIFEST');
+ vi.doUnmock('ROUTES');
+ vi.doUnmock('SERVER_OPTIONS');
+ vi.doUnmock('../src/handler.js');
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+test('starts Bun with production defaults and generated request routes', async () => {
+ const loaded = await load_start();
+
+ expect(loaded.serve).toHaveBeenCalledWith(
+ expect.objectContaining({
+ development: false,
+ maxRequestBodySize: 512 * 1024,
+ fetch: loaded.handler,
+ routes: loaded.routes
+ })
+ );
+ expect(loaded.log).toHaveBeenCalledWith('Listening on http://localhost:3000/');
+});
+
+test('environment variables override TCP server defaults', async () => {
+ const loaded = await load_start({
+ serverOptions: {
+ hostname: 'default-host',
+ port: 3000,
+ reusePort: false,
+ ipv6Only: false,
+ idleTimeout: 10,
+ maxRequestBodySize: 1000,
+ development: false
+ },
+ env: {
+ APP_HOST: '127.0.0.1',
+ APP_PORT: '4000',
+ APP_REUSE_PORT: 'true',
+ APP_IPV6_ONLY: 'yes',
+ APP_IDLE_TIMEOUT: '30',
+ APP_BODY_SIZE_LIMIT: '2M',
+ APP_DEVELOPMENT: 'on'
+ },
+ envPrefix: 'APP_'
+ });
+
+ expect(loaded.serve).toHaveBeenCalledWith(
+ expect.objectContaining({
+ hostname: '127.0.0.1',
+ port: '4000',
+ reusePort: true,
+ ipv6Only: true,
+ idleTimeout: 30,
+ maxRequestBodySize: 2 * 1024 * 1024,
+ development: true
+ })
+ );
+});
+
+test('a Unix socket takes precedence over TCP-only options', async () => {
+ const loaded = await load_start({
+ serverOptions: {
+ hostname: 'default-host',
+ port: 3000,
+ reusePort: true,
+ ipv6Only: true
+ },
+ env: { SOCKET_PATH: '/tmp/application.sock' }
+ });
+
+ const options = loaded.serve.mock.calls[0][0];
+ expect(options.unix).toBe('/tmp/application.sock');
+ expect(options).not.toHaveProperty('hostname');
+ expect(options).not.toHaveProperty('port');
+ expect(options).not.toHaveProperty('reusePort');
+ expect(options).not.toHaveProperty('ipv6Only');
+ expect(loaded.log).toHaveBeenCalledWith('Listening on /tmp/application.sock');
+});
+
+test.each([
+ [{ IDLE_TIMEOUT: '256' }, 'between 0 and 255'],
+ [{ BODY_SIZE_LIMIT: '1.1' }, 'whole bytes'],
+ [{ DEVELOPMENT: 'sometimes' }, 'expected a boolean']
+])('rejects invalid startup environment %j', async (env, message) => {
+ await expect(load_start({ env })).rejects.toThrow(message);
+});
+
+test.each(['SIGINT', 'SIGTERM'] as const)(
+ 'gracefully stops the server and emits sveltekit:shutdown for %s',
+ async (signal) => {
+ const loaded = await load_start({ pendingRequests: 2 });
+
+ await loaded.listeners.get(signal)?.();
+
+ expect(loaded.stop).toHaveBeenCalledOnce();
+ expect(loaded.emit).toHaveBeenCalledWith('sveltekit:shutdown', signal);
+ expect(loaded.log).toHaveBeenCalledWith(
+ 'Waiting for 2 requests to finish before shutting down...'
+ );
+ }
+);
+
+test('a second shutdown signal forces the process to exit', async () => {
+ let finish_stop: (() => void) | undefined;
+ const loaded = await load_start({
+ stop: () => new Promise((resolve) => (finish_stop = resolve))
+ });
+
+ const first = loaded.listeners.get('SIGINT')?.();
+ await loaded.listeners.get('SIGTERM')?.();
+ expect(loaded.exit).toHaveBeenCalledWith(1);
+
+ finish_stop?.();
+ await first;
+});
+
+async function load_start({
+ serverOptions = {},
+ env = {},
+ envPrefix = '',
+ pendingRequests = 0,
+ stop: stop_implementation
+}: {
+ serverOptions?: Record;
+ env?: Record;
+ envPrefix?: string;
+ pendingRequests?: number;
+ stop?: () => Promise;
+} = {}) {
+ vi.resetModules();
+ const listeners = new Map Promise | void>();
+ const emit = vi.fn();
+ const exit = vi.fn();
+ const fake_process = {
+ env,
+ on: vi.fn((name: string, callback: () => Promise | void) =>
+ listeners.set(name, callback)
+ ),
+ emit,
+ exit
+ };
+ vi.doMock('node:process', () => ({ default: fake_process }));
+ vi.doMock('MANIFEST', () => ({ env_prefix: envPrefix }));
+
+ const routes = { '/asset': { GET: new Response('asset') } };
+ const handler = vi.fn();
+ vi.doMock('ROUTES', () => ({ routes }));
+ vi.doMock('SERVER_OPTIONS', () => ({ default: serverOptions }));
+ vi.doMock('../src/handler.js', () => ({ handler }));
+
+ const stop = vi.fn(stop_implementation ?? (async () => {}));
+ const server = {
+ url: new URL('http://localhost:3000'),
+ pendingRequests,
+ stop
+ };
+ const serve = vi.fn((_options: any) => server);
+ vi.stubGlobal('Bun', { serve });
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ await import('../src/index.js');
+
+ return { listeners, emit, exit, routes, handler, stop, serve, log };
+}
diff --git a/packages/adapter-bun/test/utils.js b/packages/adapter-bun/test/utils.js
index f9e2bec87579..212bd3745797 100644
--- a/packages/adapter-bun/test/utils.js
+++ b/packages/adapter-bun/test/utils.js
@@ -3,6 +3,7 @@ import process from 'node:process';
import { number_from_env } from '../../../test-utils/index.js';
const compiled = process.env.COMPILE === 'true';
+const port = 4174;
/** @type {import('@playwright/test').PlaywrightTestConfig} */
export const config = {
@@ -10,16 +11,12 @@ export const config = {
timeout: process.env.CI ? 45000 : 15000,
webServer: {
command: compiled
- ? 'bun run --bun build && MY_CUSTOM_PORT=4174 ./build/server'
+ ? `bun run --bun build && MY_CUSTOM_PORT=${port} ./build/server`
: 'bun run --bun build && bun run preview',
- port: 4174
+ port
},
retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0),
- projects: [
- {
- name: 'chromium'
- }
- ],
+ projects: [{ name: 'chromium' }],
use: {
...devices['Desktop Chrome'],
screenshot: 'only-on-failure',
diff --git a/packages/adapter-bun/tests/types.ts b/packages/adapter-bun/tests/types.ts
index c328d06048a6..f5918c0e8c4e 100644
--- a/packages/adapter-bun/tests/types.ts
+++ b/packages/adapter-bun/tests/types.ts
@@ -1,23 +1,44 @@
import adapter from '../index.js';
+adapter();
+
adapter({
+ out: 'dist',
+ envPrefix: 'APP_',
+ serverOptions: {
+ development: false,
+ hostname: '127.0.0.1',
+ port: 4000,
+ idleTimeout: 30,
+ maxRequestBodySize: 1024,
+ reusePort: true,
+ ipv6Only: false
+ },
buildOptions: {
- compile: { target: 'bun-linux-x64' },
+ compile: { outfile: 'application', target: 'bun-linux-x64' },
minify: true,
- bytecode: true
+ bytecode: true,
+ sourcemap: 'linked',
+ drop: ['debugger']
}
});
adapter({
- buildOptions: {
- compile: false
- }
+ serverOptions: { unix: '/tmp/application.sock' },
+ buildOptions: { compile: false }
});
adapter({
buildOptions: {
compile: true,
- // @ts-expect-error the adapter reserves the Bun runtime target
+ // @ts-expect-error the adapter reserves the top-level runtime target
target: 'node'
}
});
+
+adapter({
+ serverOptions: {
+ // @ts-expect-error the generated server owns its fetch handler
+ fetch: () => new Response('custom')
+ }
+});
diff --git a/packages/adapter-bun/tsconfig.json b/packages/adapter-bun/tsconfig.json
index 44db1b24cf9f..d97024f0a4a0 100644
--- a/packages/adapter-bun/tsconfig.json
+++ b/packages/adapter-bun/tsconfig.json
@@ -14,11 +14,10 @@
},
"include": [
"index.js",
- "index.spec.ts",
"vitest.config.js",
"src/**/*.js",
- "src/**/*.ts",
- "test/utils.js",
+ "test/*.js",
+ "test/*.ts",
"tests/**/*.js",
"tests/**/*.ts",
"./internal.d.ts"
diff --git a/packages/adapter-bun/vitest.config.js b/packages/adapter-bun/vitest.config.js
index a15b3a470a6d..34663efc6c8b 100644
--- a/packages/adapter-bun/vitest.config.js
+++ b/packages/adapter-bun/vitest.config.js
@@ -1,5 +1,7 @@
-// we need this file to prevent Vitest from resolving a Vitest config from another directory
-
import { defineConfig } from 'vitest/config';
-export default defineConfig({});
+export default defineConfig({
+ test: {
+ include: ['test/*.spec.ts']
+ }
+});
From 0f733e493bd0ce87e2d727a2e6ad9f05f79b6ed5 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 01:54:04 +0300
Subject: [PATCH 49/94] feat(adapter-bun): update browser and server tests for
improved output validation
---
.../adapter-bun/test/apps/basic/test/browser.test.js | 6 +++---
.../adapter-bun/test/apps/basic/test/server.test.js | 12 ++++++------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/packages/adapter-bun/test/apps/basic/test/browser.test.js b/packages/adapter-bun/test/apps/basic/test/browser.test.js
index f7a83740b119..981b5e09e8cc 100644
--- a/packages/adapter-bun/test/apps/basic/test/browser.test.js
+++ b/packages/adapter-bun/test/apps/basic/test/browser.test.js
@@ -3,8 +3,8 @@ import { expect, test } from '@playwright/test';
test('renders on the server and hydrates in the browser', async ({ page }) => {
await page.goto('/');
- await expect(page.locator('h1')).toHaveText('Bun adapter fixture');
- await expect(page.getByRole('button')).toHaveText('Count: 0');
+ await expect(page.locator('h1')).toHaveText('Hello from Bun!');
+ await expect(page.getByRole('button')).toHaveText('Toggle: false');
await page.getByRole('button').click();
- await expect(page.getByRole('button')).toHaveText('Count: 1');
+ await expect(page.getByRole('button')).toHaveText('Toggle: true');
});
diff --git a/packages/adapter-bun/test/apps/basic/test/server.test.js b/packages/adapter-bun/test/apps/basic/test/server.test.js
index 27283ffcbaaf..b22a2f3b0210 100644
--- a/packages/adapter-bun/test/apps/basic/test/server.test.js
+++ b/packages/adapter-bun/test/apps/basic/test/server.test.js
@@ -20,14 +20,14 @@ test('provides the original request and Bun server on platform', async ({ reques
test('runs server instrumentation before accepting requests', async ({ request }) => {
const response = await request.get('/instrumented');
expect(response.status()).toBe(200);
- expect(await response.text()).toBe('instrumentation-ready');
+ expect(await response.text()).toBe('true');
});
test('serves static files and implements HEAD natively', async ({ request }) => {
const response = await request.get('/data.json');
expect(response.status()).toBe(200);
expect(response.headers()['content-type']).toContain('application/json');
- expect(await response.json()).toEqual({ source: 'static' });
+ expect(await response.json()).toEqual({ message: 'hello from a static file' });
const head = await request.head('/data.json');
expect(head.status()).toBe(200);
@@ -38,7 +38,7 @@ test('serves static files and implements HEAD natively', async ({ request }) =>
for (const [url, content_type, body] of [
['/sub/', 'text/html;charset=utf-8', 'directory index'],
['/encoded%20name.txt', 'text/plain;charset=utf-8', 'encoded filename'],
- ['/.well-known/adapter-bun.txt', 'text/plain;charset=utf-8', 'well-known asset']
+ ['/.well-known/adapter-bun.txt', 'text/plain;charset=utf-8', 'adapter bun']
]) {
test(`serves ${url} with its MIME type`, async ({ request }) => {
const response = await request.get(url);
@@ -76,7 +76,7 @@ test('uses Bun conditional requests and byte ranges for filesystem assets', asyn
test('serves prerendered pages, endpoints, and canonical redirects', async ({ request }) => {
const page = await request.get('/prerendered/');
expect(page.status()).toBe(200);
- expect(await page.text()).toContain('Prerendered by SvelteKit');
+ expect(await page.text()).toContain('Prerendered');
const icon = await request.get('/prerendered.ico');
expect(icon.status()).toBe(200);
@@ -93,13 +93,13 @@ test('uses SvelteKit for non-GET requests that share a static pathname', async (
headers: { origin: 'http://localhost:4174' }
});
expect(response.status()).toBe(200);
- expect(await response.json()).toEqual({ source: 'endpoint' });
+ expect(await response.json()).toEqual({ message: 'hello from a server endpoint' });
});
test('makes imported assets available to $app/server read', async ({ request }) => {
const response = await request.get('/read');
expect(response.status()).toBe(200);
- expect(await response.text()).toBe('server-readable asset\n');
+ expect(await response.text()).toBe('Hello from $app/server read\n');
});
test('sets immutable caching only on generated immutable assets', async ({ request }) => {
From 1904b9cdb2ed961f062fc084571c969b38bb5b7b Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 02:57:32 +0300
Subject: [PATCH 50/94] fix(adapter-bun): simplify pathname encoding by
removing wildcard replacement
---
packages/adapter-bun/src/routes-util.js | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 5c27c001ed5e..b8727b289079 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -12,10 +12,7 @@ const dir = dirname(Bun.main);
* @returns {string}
*/
function encode_pathname(pathname) {
- return pathname
- .split('/')
- .map((seg) => encodeURIComponent(seg).replace('*', '%2A'))
- .join('/');
+ return pathname.split('/').map(encodeURIComponent).join('/');
}
/**
From 4b37413217b54bb11589996a5ca3950b23a65dc7 Mon Sep 17 00:00:00 2001
From: Michael Youssry <16242325+Black-Hack@users.noreply.github.com>
Date: Sun, 9 Aug 2026 03:52:36 +0300
Subject: [PATCH 51/94] refactor(adapter-bun): streamline log handling for
builder messages
---
packages/adapter-bun/index.js | 15 +++++----------
1 file changed, 5 insertions(+), 10 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index d14ea76df3bf..8ad6c21fe6b6 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -124,16 +124,11 @@ export default function (opts = {}) {
});
if (!result.success) {
for (const log of result.logs) {
- switch (log.level) {
- case 'error':
- builder.log.error(log.message);
- break;
- case 'warning':
- builder.log.warn(log.message);
- break;
- default:
- builder.log.info(log.message);
- }
+ if (log.level === 'error') console.error(log);
+ else if (log.level === 'warning') console.warn(log);
+ else if (log.level === 'info') console.info(log);
+ else if (log.level === 'debug' || log.level === 'verbose') console.debug(log);
+ else console.log(log);
}
throw new AggregateError(result.logs);
}
From ddd8915e8d3442e487230c2e22faf873d907edbf Mon Sep 17 00:00:00 2001
From: Tee Ming
Date: Tue, 11 Aug 2026 02:14:24 +0800
Subject: [PATCH 52/94] Delete packages/adapter-bun/LICENSE
---
packages/adapter-bun/LICENSE | 22 ----------------------
1 file changed, 22 deletions(-)
delete mode 100644 packages/adapter-bun/LICENSE
diff --git a/packages/adapter-bun/LICENSE b/packages/adapter-bun/LICENSE
deleted file mode 100644
index b0306577d9da..000000000000
--- a/packages/adapter-bun/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-MIT License
-
-Copyright (c) 2026 Svelte contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
From 67343d38a094439b8de7b222c5d0bea56a0203e6 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:13:22 -0400
Subject: [PATCH 53/94] fix: force-close connections after SHUTDOWN_TIMEOUT
server.stop() waits forever on idle connections such as open event
streams, and once it is pending its promise never settles even after a
force-close, so sveltekit:shutdown never fired. Race it against a
deadline like adapter-node.
---
.../25-build-and-deploy/45-adapter-bun.md | 2 ++
packages/adapter-bun/src/env.js | 1 +
packages/adapter-bun/src/index.js | 21 ++++++++++++++++---
packages/adapter-bun/test/start.spec.ts | 20 ++++++++++++++++++
4 files changed, 41 insertions(+), 3 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index a1a67de6ba3c..22ae0106a480 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -245,4 +245,6 @@ process.on('sveltekit:shutdown', async (reason) => {
});
```
+Connections that are still open after `SHUTDOWN_TIMEOUT` seconds are closed forcefully, so idle connections such as open event streams cannot delay the shutdown indefinitely. The value must be a non-negative integer and defaults to `30`.
+
Sending a second shutdown signal forces the process to exit with status `1`.
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index 9f88e818d8a1..bd60db63a4f9 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -9,6 +9,7 @@ const expected = new Set([
'IPV6_ONLY',
'IDLE_TIMEOUT',
'BODY_SIZE_LIMIT',
+ 'SHUTDOWN_TIMEOUT',
'DEVELOPMENT',
'XFF_DEPTH',
'ADDRESS_HEADER',
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index e9ce41fbb31b..d559098f32c8 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -32,6 +32,8 @@ if (development !== undefined) {
options.maxRequestBodySize = bytes_env('BODY_SIZE_LIMIT', options.maxRequestBodySize ?? 512 * 1024);
+const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT', 30) ?? 30;
+
options.fetch = handler;
options.routes = routes;
@@ -50,11 +52,24 @@ async function graceful_shutdown(reason) {
console.log(`Waiting for ${server.pendingRequests} requests to finish before shutting down...`);
console.log('Press Ctrl+C again to force shutdown.');
}
- await server.stop();
+
+ // stop() waits forever on idle connections such as open event streams, and once it
+ // is pending its promise never settles even after a force-close, so race it instead
+ /** @type {ReturnType | undefined} */
+ let deadline;
+ const drained = await Promise.race([
+ server.stop().then(() => true),
+ new Promise((resolve) => {
+ deadline = setTimeout(() => resolve(false), shutdown_timeout * 1000);
+ })
+ ]);
+ clearTimeout(deadline);
+
+ if (!drained) void server.stop(true);
// @ts-expect-error custom events cannot be typed
process.emit('sveltekit:shutdown', reason);
}
-process.on('SIGTERM', () => void graceful_shutdown('SIGTERM'));
-process.on('SIGINT', () => void graceful_shutdown('SIGINT'));
+process.on('SIGTERM', () => graceful_shutdown('SIGTERM'));
+process.on('SIGINT', () => graceful_shutdown('SIGINT'));
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
index b4b8edf5f6d1..b5e756092572 100644
--- a/packages/adapter-bun/test/start.spec.ts
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -104,6 +104,26 @@ test.each(['SIGINT', 'SIGTERM'] as const)(
}
);
+test('force-closes lingering connections after SHUTDOWN_TIMEOUT', async () => {
+ vi.useFakeTimers();
+ try {
+ const loaded = await load_start({
+ env: { SHUTDOWN_TIMEOUT: '5' },
+ stop: () => new Promise(() => {})
+ });
+
+ const shutdown = loaded.listeners.get('SIGTERM')?.();
+ await vi.advanceTimersByTimeAsync(5000);
+ await shutdown;
+
+ expect(loaded.stop).toHaveBeenCalledTimes(2);
+ expect(loaded.stop).toHaveBeenLastCalledWith(true);
+ expect(loaded.emit).toHaveBeenCalledWith('sveltekit:shutdown', 'SIGTERM');
+ } finally {
+ vi.useRealTimers();
+ }
+});
+
test('a second shutdown signal forces the process to exit', async () => {
let finish_stop: (() => void) | undefined;
const loaded = await load_start({
From 65d5464dbd1d34dd384a400c9155f804bdbcc138 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:13:42 -0400
Subject: [PATCH 54/94] fix: log Bun build diagnostics via builder.log
BuildMessage properties are not enumerable, so console.error printed
`BuildMessage {}`. This is also what the existing test asserts.
---
packages/adapter-bun/index.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 8ad6c21fe6b6..199646c2d0f1 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -124,11 +124,11 @@ export default function (opts = {}) {
});
if (!result.success) {
for (const log of result.logs) {
- if (log.level === 'error') console.error(log);
- else if (log.level === 'warning') console.warn(log);
- else if (log.level === 'info') console.info(log);
- else if (log.level === 'debug' || log.level === 'verbose') console.debug(log);
- else console.log(log);
+ // BuildMessage properties are not enumerable, so console.error(log) prints `{}`
+ const message = log.message ?? String(log);
+ if (log.level === 'error') builder.log.error(message);
+ else if (log.level === 'warning') builder.log.warn(message);
+ else builder.log.info(message);
}
throw new AggregateError(result.logs);
}
From 0e92dd6444589256c1978ef81245011534bf5627 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:14:10 -0400
Subject: [PATCH 55/94] feat: respect buildOptions.splitting
splitting was forced after spreading buildOptions. splitting: false is
the only workaround for Bun output-path collisions on identically-hashed
chunks (oven-sh/bun#17674).
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/index.d.ts | 1 +
packages/adapter-bun/index.js | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 22ae0106a480..4bfc6c415289 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -112,7 +112,7 @@ The generated server owns `fetch` and `routes`. It does not expose `websocket`,
### buildOptions
-Advanced Bun build settings can be supplied with `buildOptions`. The adapter currently accepts `sourcemap`, `minify`, `bytecode`, `banner`, `footer`, `drop`, `features`, `optimizeImports`, and `compile`.
+Advanced Bun build settings can be supplied with `buildOptions`. The adapter currently accepts `sourcemap`, `minify`, `bytecode`, `banner`, `footer`, `drop`, `features`, `optimizeImports`, `splitting`, and `compile`. Code splitting is enabled by default; `splitting: false` bundles the server into a single file, which works around [`Bun.build` output path collisions](https://github.com/oven-sh/bun/issues/17674) on applications whose module graph produces identically-hashed chunks.
The generated entrypoint, output directory, top-level `target`, and module `format` are reserved. Generated servers target Bun and use ESM. Source maps default to `external`; set `sourcemap: 'none'` to disable them.
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 7509e796b165..38bc0610bb75 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -55,6 +55,7 @@ interface AdapterOptions {
| 'drop'
| 'features'
| 'optimizeImports'
+ | 'splitting'
| 'compile'
>;
}
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 199646c2d0f1..00f9a87df3c8 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -99,7 +99,7 @@ export default function (opts = {}) {
const result = await Bun.build({
...buildOptions,
- splitting: true,
+ splitting: buildOptions.splitting ?? true,
sourcemap: buildOptions.sourcemap ?? 'external',
entrypoints: [index_file],
target: 'bun',
From 5da1d4f423be17962214ae8b69081e14fb4ca15f Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:14:28 -0400
Subject: [PATCH 56/94] fix: only treat ENOENT as an absent output directory
Other readdir failures were swallowed and could ship an executable with
zero client assets.
---
packages/adapter-bun/index.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 00f9a87df3c8..3a5786b01abd 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -16,7 +16,8 @@ async function read_files_recursive(path) {
return { abs, rel };
})
.filter(({ rel }) => rel.split('/').every((segment) => segment !== '.vite'));
- } catch {
+ } catch (error) {
+ if (/** @type {NodeJS.ErrnoException} */ (error)?.code !== 'ENOENT') throw error;
return [];
}
}
From 32748fa48bf8e2fc6514dff6d4aa6eb534441dbe Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:16:01 -0400
Subject: [PATCH 57/94] fix: serve static files at the paths browsers request
Bun matches route keys against the raw request pathname and browsers
send sub-delims literally, so /a%26b.txt never matched a request for
/a&b.txt. Register keys the way user agents send them, with the
fully-encoded form as an alias.
---
packages/adapter-bun/index.js | 10 ++--
packages/adapter-bun/src/routes-util.js | 71 ++++++++++++++++++------
packages/adapter-bun/test/routes.spec.ts | 19 ++++++-
3 files changed, 76 insertions(+), 24 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 3a5786b01abd..52b4a577d2a3 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -183,16 +183,16 @@ async function get_embed_entries({ builder, server_assets }) {
const pr_page_assets_entries = pr_pages.flatMap(({ rel }, i) => {
return prerendered_pages_files.has(rel)
? []
- : [`prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`];
+ : [`...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`];
});
offset += pr_pages.length;
const pr_assets_entries = [...pr_deps, ...pr_data].map(({ rel }, i) => {
- return `prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
+ return `...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
});
const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
- return `prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
+ return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
});
return {
@@ -238,11 +238,11 @@ function get_no_embed_entries({ builder, server_assets, out }) {
const pr_assets_entries = prerendered_files
.filter((filePath) => !prerendered_pages_files.has(filePath))
.map((filePath) => {
- return `prerendered_asset(${JSON.stringify(filePath)})`;
+ return `...prerendered_asset(${JSON.stringify(filePath)})`;
});
const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
- return `prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
+ return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
});
return {
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index b8727b289079..6d1b9b343b7d 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -7,28 +7,55 @@ const dir = dirname(Bun.main);
* @typedef {import('bun').Serve.Routes[string]} RouteHandler
*/
+// RFC 3986 pchar minus percent-escapes: characters user agents send raw in a path
+const RAW_PATH_CHAR = /^[A-Za-z0-9\-._~!$&'()+,;=:@]$/;
+
/**
+ * Percent-encodes only what user agents themselves encode when requesting the path,
+ * because Bun matches route keys against the raw request pathname.
* @param {string} pathname
* @returns {string}
*/
function encode_pathname(pathname) {
- return pathname.split('/').map(encodeURIComponent).join('/');
+ return pathname
+ .split('/')
+ .map((segment) =>
+ [...segment]
+ .map((char, i) => {
+ if (char === ':' && i === 0) return '%3A'; // Bun route parameter marker
+ return RAW_PATH_CHAR.test(char) ? char : encodeURIComponent(char);
+ })
+ .join('')
+ )
+ .join('/');
+}
+
+/**
+ * Registers a path both as user agents send it and fully percent-encoded,
+ * so clients that escape sub-delims still match.
+ * @param {string} pathname
+ * @returns {string[]}
+ */
+function route_paths(pathname) {
+ const minimal = encode_pathname(pathname);
+ const full = pathname.split('/').map(encodeURIComponent).join('/');
+ return minimal === full ? [minimal] : [minimal, full];
}
/**
* @param {string} urlPath
- * @returns {string}
+ * @returns {string[]}
*/
-function to_path(urlPath) {
- return encode_pathname(posix.join(base, urlPath));
+function to_paths(urlPath) {
+ return route_paths(posix.join(base, urlPath));
}
/**
* @param {string} urlPath
- * @returns {string}
+ * @returns {string[]}
*/
-function to_directory_path(urlPath) {
- return encode_pathname(`${posix.join(base, urlPath).replace(/\/$/, '')}/`);
+function to_directory_paths(urlPath) {
+ return route_paths(`${posix.join(base, urlPath).replace(/\/$/, '')}/`);
}
/**
@@ -47,11 +74,13 @@ export function client_asset(urlPath, filePath = urlPath) {
}
/** @type {Array<[string, RouteHandler]>} */
- const entries = [[to_path(urlPath), { GET: new Response(file, { headers }) }]];
+ const entries = to_paths(urlPath).map((path) => [path, { GET: new Response(file, { headers }) }]);
if (urlPath.endsWith('/index.html') || urlPath === 'index.html') {
const directory = urlPath.slice(0, -'index.html'.length);
- entries.push([to_directory_path(directory), { GET: new Response(file, { headers }) }]);
+ for (const path of to_directory_paths(directory)) {
+ entries.push([path, { GET: new Response(file, { headers }) }]);
+ }
}
return entries;
@@ -69,12 +98,12 @@ export function server_asset(urlPath, filePath = urlPath) {
/**
* @param {string} urlPath
* @param {string} [filePath]
- * @returns {[string, RouteHandler]}
+ * @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_asset(urlPath, filePath = urlPath) {
const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
const headers = { 'content-type': file.type };
- return [to_path(urlPath), { GET: new Response(file, { headers }) }];
+ return to_paths(urlPath).map((path) => [path, { GET: new Response(file, { headers }) }]);
}
/**
@@ -97,14 +126,19 @@ export function prerendered_page(urlPath, filePath) {
const headers = { 'content-type': file.type };
const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
+ // path already contains base, no need to add it here
/** @type {Array<[string, RouteHandler]>} */
- const entries = [[encode_pathname(urlPath), { GET: new Response(file, { headers }) }]];
+ const entries = route_paths(urlPath).map((path) => [
+ path,
+ { GET: new Response(file, { headers }) }
+ ]);
if (inverted) {
- entries.push([encode_pathname(inverted), { GET: handle_redirect }]);
+ for (const path of route_paths(inverted)) {
+ entries.push([path, { GET: handle_redirect }]);
+ }
}
- // path already contains base, no need to call to_path here
return entries;
}
@@ -112,9 +146,12 @@ export function prerendered_page(urlPath, filePath) {
* @param {string} urlPath
* @param {number} status
* @param {string} location
- * @returns {[string, RouteHandler]}
+ * @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_redirect(urlPath, status, location) {
- // path already contains base, no need to call to_path here
- return [encode_pathname(urlPath), { GET: new Response(null, { status, headers: { location } }) }];
+ // path already contains base, no need to add it here
+ return route_paths(urlPath).map((path) => [
+ path,
+ { GET: new Response(null, { status, headers: { location } }) }
+ ]);
}
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index b84d74fb6f07..6361f6718964 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -31,6 +31,21 @@ test('client index files are also available at their directory URL', async () =>
]);
});
+test('sub-delims stay raw in route paths with a fully-encoded alias', async () => {
+ const { routes } = await load_routes({ base: '/base' });
+
+ expect(routes.client_asset('a&b.txt').map(([path]) => path)).toEqual([
+ '/base/a&b.txt',
+ '/base/a%26b.txt'
+ ]);
+});
+
+test('segments starting with a colon are escaped to avoid Bun route parameters', async () => {
+ const { routes } = await load_routes({ base: '/base' });
+
+ expect(routes.client_asset(':tag.txt').map(([path]) => path)).toEqual(['/base/%3Atag.txt']);
+});
+
test('immutable SvelteKit assets receive a long-lived cache policy', async () => {
const { routes } = await load_routes({ appDir: '_app' });
@@ -67,7 +82,7 @@ test('prerendered assets use the base path and preserve their content type', asy
const { routes, file } = await load_routes({ base: '/base' });
file.mockImplementationOnce((path) => ({ path, type: 'image/x-icon' }));
- const [path, handler] = routes.prerendered_asset('icon.ico');
+ const [[path, handler]] = routes.prerendered_asset('icon.ico');
expect(path).toBe('/base/icon.ico');
expect((handler as any).GET.headers.get('content-type')).toBe('image/x-icon');
@@ -101,7 +116,7 @@ test('a prerendered root page has no duplicate alternate route', async () => {
test('prerendered redirects retain their status and location', async () => {
const { routes } = await load_routes();
- const [path, handler] = routes.prerendered_redirect('/old path', 307, '/new');
+ const [[path, handler]] = routes.prerendered_redirect('/old path', 307, '/new');
expect(path).toBe('/old%20path');
expect((handler as any).GET.status).toBe(307);
From 90fce970800c4825479310a52bcb2be4ed08c58c Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:16:42 -0400
Subject: [PATCH 58/94] fix: serve /dir and extensionless .html like sirv
sirv resolves /dir to dir/index.html and /page to page.html; both
regressed to 404 coming from adapter-node.
---
packages/adapter-bun/src/routes-util.js | 15 ++++++++++++++-
packages/adapter-bun/test/routes.spec.ts | 15 +++++++++++++--
2 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 6d1b9b343b7d..d7f48bbce999 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -55,7 +55,15 @@ function to_paths(urlPath) {
* @returns {string[]}
*/
function to_directory_paths(urlPath) {
- return route_paths(`${posix.join(base, urlPath).replace(/\/$/, '')}/`);
+ const directory = `${posix.join(base, urlPath).replace(/\/$/, '')}/`;
+ const paths = route_paths(directory);
+
+ // `/dir` serves `dir/index.html` like sirv does in adapter-node
+ if (directory !== '/') {
+ paths.push(...route_paths(directory.slice(0, -1)));
+ }
+
+ return paths;
}
/**
@@ -81,6 +89,11 @@ export function client_asset(urlPath, filePath = urlPath) {
for (const path of to_directory_paths(directory)) {
entries.push([path, { GET: new Response(file, { headers }) }]);
}
+ } else if (urlPath.endsWith('.html')) {
+ // sirv also serves `page.html` at `/page`
+ for (const path of to_paths(urlPath.slice(0, -'.html'.length))) {
+ entries.push([path, { GET: new Response(file, { headers }) }]);
+ }
}
return entries;
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index 6361f6718964..91fd9a4e632c 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -23,11 +23,22 @@ test('client index files are also available at their directory URL', async () =>
expect(routes.client_asset('index.html').map(([path]) => path)).toEqual([
'/base/index.html',
- '/base/'
+ '/base/',
+ '/base'
]);
expect(routes.client_asset('docs/index.html').map(([path]) => path)).toEqual([
'/base/docs/index.html',
- '/base/docs/'
+ '/base/docs/',
+ '/base/docs'
+ ]);
+});
+
+test('other client HTML files are also available without their extension', async () => {
+ const { routes } = await load_routes({ base: '/base' });
+
+ expect(routes.client_asset('page.html').map(([path]) => path)).toEqual([
+ '/base/page.html',
+ '/base/page'
]);
});
From 8986a87b10563f676d39df47f0bd4d7e95a460d4 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:17:14 -0400
Subject: [PATCH 59/94] fix: percent-encode the trailing-slash redirect
location
Non-ASCII locations throw on header assignment per the ByteString rule.
---
packages/adapter-bun/src/routes-util.js | 4 +++-
packages/adapter-bun/test/routes.spec.ts | 8 ++++++++
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index d7f48bbce999..8d83481c633d 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -125,13 +125,15 @@ export function prerendered_asset(urlPath, filePath = urlPath) {
* @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_page(urlPath, filePath) {
+ const canonical = encode_pathname(urlPath);
+
/**
* @param {import('bun').BunRequest} req
* @returns {Response}
*/
function handle_redirect(req) {
const url = new URL(req.url);
- const location = `${urlPath}${url.search}`;
+ const location = `${canonical}${url.search}`;
return new Response(null, { status: 308, headers: { location } });
}
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index 91fd9a4e632c..6e263694db1c 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -118,6 +118,14 @@ test.each([
}
);
+test('redirects to non-ASCII canonical URLs use a percent-encoded location', async () => {
+ const { routes } = await load_routes();
+ const entries = routes.prerendered_page('/café/', 'cafe.html');
+
+ const response = (entries[1][1] as any).GET(new Request('http://localhost/caf%C3%A9'));
+ expect(response.headers.get('location')).toBe('/caf%C3%A9/');
+});
+
test('a prerendered root page has no duplicate alternate route', async () => {
const { routes } = await load_routes();
From c481a61f4c484735dbf339f65b61360f5190bf47 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:17:54 -0400
Subject: [PATCH 60/94] fix: stop serving dotfiles except .well-known
Every file written by writeClient became a public route, exposing
static/.env after a switch from adapter-node.
---
packages/adapter-bun/index.js | 17 +++++++++++++++--
packages/adapter-bun/test/adapter.spec.ts | 23 +++++++++++++++++++++++
2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 52b4a577d2a3..360c42f6000a 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -22,6 +22,17 @@ async function read_files_recursive(path) {
}
}
+/**
+ * Matches sirv's default behaviour in adapter-node: dotfiles are not served,
+ * with an exception for the `.well-known` directory.
+ * @param {string} path
+ */
+function is_dotfile(path) {
+ return path
+ .split('/')
+ .some((segment, i) => segment.startsWith('.') && !(i === 0 && segment === '.well-known'));
+}
+
/** @param {string[]} files */
function validate_file_paths(files) {
const invalid = files.find((file) => file.includes('*'));
@@ -151,13 +162,15 @@ export default function (opts = {}) {
async function get_embed_entries({ builder, server_assets }) {
const builtFiles = `${builder.config.kit.outDir}/output`;
- const [cl_files, pr_pages, pr_deps, pr_data] = await Promise.all([
+ const [all_cl_files, pr_pages, pr_deps, pr_data] = await Promise.all([
read_files_recursive(`${builtFiles}/client`),
read_files_recursive(`${builtFiles}/prerendered/pages`),
read_files_recursive(`${builtFiles}/prerendered/dependencies`),
read_files_recursive(`${builtFiles}/prerendered/data`)
]);
+ const cl_files = all_cl_files.filter(({ rel }) => !is_dotfile(rel));
+
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
validate_file_paths(assets.map(({ rel }) => rel));
@@ -220,7 +233,7 @@ async function get_embed_entries({ builder, server_assets }) {
* @returns {{imports: string[], entries: string[], server_assets: string[]}}
*/
function get_no_embed_entries({ builder, server_assets, out }) {
- const client_files = builder.writeClient(`${out}/client`);
+ const client_files = builder.writeClient(`${out}/client`).filter((file) => !is_dotfile(file));
const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
validate_file_paths([...client_files, ...prerendered_files]);
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 2ad1f49637ab..b4fc90efe5b4 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -266,6 +266,29 @@ describe('generated routes', () => {
expect(bun.build).not.toHaveBeenCalled();
});
+ test('does not register dotfiles apart from .well-known', async () => {
+ const builder = create_builder({
+ client_files: ['.env', '.well-known/security.txt', 'ok.txt']
+ });
+
+ await adapter().adapt(builder);
+
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).not.toContain('.env');
+ expect(source).toContain('...client_asset(".well-known/security.txt")');
+ expect(source).toContain('...client_asset("ok.txt")');
+ });
+
+ test('excludes dotfiles from embedded assets', async () => {
+ mock_files({ client: ['.secret', 'public.txt'] });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(create_builder());
+
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).not.toContain('.secret');
+ expect(source).toContain('...client_asset("public.txt", asset_0)');
+ });
+
test('fails when a prerendered page is absent from compiled build output', async () => {
await expect(
adapter({ buildOptions: { compile: true } }).adapt(
From 7fe7313cae4865e3e368488233a528ec2c06640d Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:18:40 -0400
Subject: [PATCH 61/94] fix: reject wildcards in prerendered route paths
Only file paths were validated, so a crawled redirect source containing
`*` became a Bun wildcard route.
---
packages/adapter-bun/index.js | 7 ++++++-
packages/adapter-bun/test/adapter.spec.ts | 11 +++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 360c42f6000a..4adeb9bd4d13 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -38,7 +38,7 @@ function validate_file_paths(files) {
const invalid = files.find((file) => file.includes('*'));
if (invalid !== undefined) {
throw new Error(
- `Cannot build with ${JSON.stringify(invalid)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file to remove the \`*\` character.`
+ `Cannot build with ${JSON.stringify(invalid)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file or route to remove the \`*\` character.`
);
}
}
@@ -275,6 +275,11 @@ function get_no_embed_entries({ builder, server_assets, out }) {
* @returns {Promise}
*/
async function create_routes({ builder, out, embed }) {
+ validate_file_paths([
+ ...builder.prerendered.pages.keys(),
+ ...builder.prerendered.redirects.keys()
+ ]);
+
const server_assets = builder.findServerAssets(
builder.routes.filter((route) => route.prerender !== true)
);
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index b4fc90efe5b4..8ac1eb04f2a0 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -289,6 +289,17 @@ describe('generated routes', () => {
expect(source).toContain('...client_asset("public.txt", asset_0)');
});
+ test('rejects wildcard characters in prerendered redirect sources', async () => {
+ const builder = create_builder({
+ prerendered_redirects: [['/docs/*', { status: 308, location: '/new' }]]
+ });
+
+ await expect(adapter().adapt(builder)).rejects.toThrow(
+ 'Bun treats literal `*` characters in route paths as wildcards'
+ );
+ expect(bun.build).not.toHaveBeenCalled();
+ });
+
test('fails when a prerendered page is absent from compiled build output', async () => {
await expect(
adapter({ buildOptions: { compile: true } }).adapt(
From 68f1873640cb4ce3ad1d07108e1f084a73a8a0ff Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:19:15 -0400
Subject: [PATCH 62/94] fix: return 400 for unsupported forwarded protocol
schemes
x-forwarded-proto: foo passed the colon check, but its origin is the
string 'null' and the later URL construction threw an uncaught 500.
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/src/handler.js | 4 ++--
packages/adapter-bun/test/handler.spec.ts | 3 ++-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 4bfc6c415289..cb8acf802d42 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -195,7 +195,7 @@ PORT_HEADER=x-forwarded-port \
bun ./build
```
-The protocol header must contain only a scheme such as `https`, without a colon. The port header must contain a number. Invalid values produce a `400 Bad Request` response.
+The protocol header must contain `http` or `https`, without a colon. The port header must contain a number. Invalid values produce a `400 Bad Request` response.
> [!CAUTION] Only trust forwarded headers when requests can reach the server through a proxy you control. A direct client can spoof these headers.
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index d7f3088fa6b4..6d9d8869b6c4 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -66,9 +66,9 @@ function get_origin(request, url) {
const protocol = decodeURIComponent(
(protocol_header ? request.headers.get(protocol_header) : null) ?? url.protocol.slice(0, -1)
);
- if (protocol.includes(':')) {
+ if (!/^https?$/i.test(protocol)) {
throw new Error(
- `The ${protocol_header} header specified ${protocol} which is an invalid because it includes \`:\`. It should only contain the protocol scheme (e.g. \`https\`)`
+ `The ${protocol_header} header specified ${protocol} which is an invalid protocol scheme. It should only contain the protocol scheme (e.g. \`https\`)`
);
}
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
index 9ccd8c7c0af4..4da7ca9b1ce9 100644
--- a/packages/adapter-bun/test/handler.spec.ts
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -69,7 +69,8 @@ test('derives the public origin from configured proxy headers', async () => {
});
test.each([
- ['APP_PROTOCOL_HEADER', 'x-proto', { 'x-proto': 'https%3A' }, 'includes `:`'],
+ ['APP_PROTOCOL_HEADER', 'x-proto', { 'x-proto': 'https%3A' }, 'invalid protocol scheme'],
+ ['APP_PROTOCOL_HEADER', 'x-proto', { 'x-proto': 'foo' }, 'invalid protocol scheme'],
['APP_PORT_HEADER', 'x-port', { 'x-port': 'not-a-port' }, 'invalid port']
])('returns 400 for an invalid origin from %s', async (name, value, headers, message) => {
set_env(name, value);
From dcfd13a888c5309ba6d21a970bfdb732217aa5fb Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:19:47 -0400
Subject: [PATCH 63/94] fix: ignore proxy headers that are present but empty
An empty x-forwarded-proto produced '://host' and a 400 instead of
falling back like adapter-node.
---
.../docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/src/handler.js | 6 ++----
packages/adapter-bun/test/handler.spec.ts | 13 +++++++++++++
3 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index cb8acf802d42..2e8d34be2b38 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -195,7 +195,7 @@ PORT_HEADER=x-forwarded-port \
bun ./build
```
-The protocol header must contain `http` or `https`, without a colon. The port header must contain a number. Invalid values produce a `400 Bad Request` response.
+The protocol header must contain `http` or `https`, without a colon. The port header must contain a number. Invalid values produce a `400 Bad Request` response. A header that is present but empty is ignored in favour of the fallback.
> [!CAUTION] Only trust forwarded headers when requests can reach the server through a proxy you control. A direct client can spoof these headers.
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 6d9d8869b6c4..462b9f1fd876 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -64,7 +64,7 @@ export async function handler(request, bun_server) {
*/
function get_origin(request, url) {
const protocol = decodeURIComponent(
- (protocol_header ? request.headers.get(protocol_header) : null) ?? url.protocol.slice(0, -1)
+ (protocol_header && request.headers.get(protocol_header)) || url.protocol.slice(0, -1)
);
if (!/^https?$/i.test(protocol)) {
throw new Error(
@@ -73,9 +73,7 @@ function get_origin(request, url) {
}
const host =
- (host_header ? request.headers.get(host_header) : null) ??
- request.headers.get('host') ??
- url.host;
+ (host_header && request.headers.get(host_header)) || request.headers.get('host') || url.host;
if (!host) {
const header_names = host_header ? `${host_header} or host headers` : 'host header';
throw new Error(
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
index 4da7ca9b1ce9..e2a2dc868aec 100644
--- a/packages/adapter-bun/test/handler.spec.ts
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -88,6 +88,19 @@ test.each([
expect(error).toHaveBeenCalledWith(expect.stringContaining(message));
});
+test('falls back past proxy headers that are present but empty', async () => {
+ set_env('APP_PROTOCOL_HEADER', 'x-forwarded-proto');
+ set_env('APP_HOST_HEADER', 'x-forwarded-host');
+ const loaded = await load_handler({ envPrefix: 'APP_' });
+ const request = new Request('http://internal/path', {
+ headers: { 'x-forwarded-proto': '', 'x-forwarded-host': '' }
+ });
+
+ await loaded.handler(request, loaded.bun_server);
+
+ expect(loaded.respond.mock.calls[0][0].url).toBe('http://internal/path');
+});
+
test('reads a configured client address header', async () => {
set_env('APP_ADDRESS_HEADER', 'true-client-ip');
const loaded = await load_handler({ envPrefix: 'APP_' });
From 4ec9d4998e6d1617b70840f7dfaed25ee7ec72ec Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:20:03 -0400
Subject: [PATCH 64/94] chore: remove unreachable XFF_DEPTH check
number_env already enforces the minimum at startup.
---
packages/adapter-bun/src/handler.js | 4 ----
1 file changed, 4 deletions(-)
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 462b9f1fd876..94f2f1234d70 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -109,10 +109,6 @@ function get_client_address(request, bun_server) {
if (address_header === 'x-forwarded-for') {
const addresses = value.split(',');
- if (xff_depth < 1) {
- throw new Error(`${env_prefix}XFF_DEPTH must be a positive integer`);
- }
-
if (xff_depth > addresses.length) {
throw new Error(
`${env_prefix}XFF_DEPTH is ${xff_depth}, but only found ${addresses.length} addresses`
From a1478743cac662cdbdf3def8a27532f8a2e6e9b8 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:20:17 -0400
Subject: [PATCH 65/94] chore: fix the ROUTES route table type
Bare Serve.Routes does not resolve; skipLibCheck hid the error.
---
packages/adapter-bun/internal.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts
index d663850435d1..b3e0ceed137a 100644
--- a/packages/adapter-bun/internal.d.ts
+++ b/packages/adapter-bun/internal.d.ts
@@ -8,7 +8,7 @@ declare module 'MANIFEST' {
declare module 'ROUTES' {
export const server_assets: Map;
- export const routes: Serve.Routes;
+ export const routes: import('bun').Serve.Routes;
}
declare module 'SERVER' {
From 794418b6060865f4476037679ce0f8d418194fe8 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:20:29 -0400
Subject: [PATCH 66/94] chore: correct the buildOptions default
The default is {}; false belongs to the nested compile option.
---
packages/adapter-bun/index.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index 38bc0610bb75..fbc47053cee9 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -43,7 +43,7 @@ interface AdapterOptions {
* output directory, top-level target, and module format are reserved. Set `compile` to
* create an executable; if it does not specify an outfile, the executable is written to
* `/server`.
- * @default false
+ * @default {}
*/
buildOptions?: Pick<
import('bun').BuildConfig,
From 211f4ce9b9ab9583d03c8c11f89f8624cbc17375 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 15:20:32 -0400
Subject: [PATCH 67/94] chore: drop the eslint-disable comment from generated
routes
---
packages/adapter-bun/index.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 4adeb9bd4d13..be09316126e5 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -293,7 +293,6 @@ async function create_routes({ builder, out, embed }) {
: get_no_embed_entries({ builder, out, server_assets });
return [
- `// eslint-disable-next-line @typescript-eslint/no-unused-vars`,
`import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.js';`,
...imports,
`export const routes = Object.fromEntries([${entries.join(',\n')}]);`,
From 4df7ecddf3fcfd7f39ef4f48d5ccd2e309945cdf Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 16:58:20 -0400
Subject: [PATCH 68/94] chore: fix the lockfile after the version-3 merge
Every CI job died in pnpm install: the merge kept adapter-bun's
pre-merge @playwright/test resolution while the catalog moved on.
---
pnpm-lock.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c8c051cbbb0a..b0289e2b61b7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -149,7 +149,7 @@ importers:
devDependencies:
'@playwright/test':
specifier: 'catalog:'
- version: 1.61.1
+ version: 1.62.1
'@sveltejs/kit':
specifier: workspace:^
version: link:../kit
@@ -161,13 +161,13 @@ importers:
version: 6.0.3
vitest:
specifier: 'catalog:'
- version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))
+ version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@22.19.19)(@vitest/browser-playwright@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.4.2)(yaml@2.9.0))
packages/adapter-bun/test/apps/basic:
devDependencies:
'@playwright/test':
specifier: 'catalog:'
- version: 1.61.1
+ version: 1.62.1
'@sveltejs/kit':
specifier: workspace:^
version: link:../../../../kit
From 57384d51001885c80f465218ad42756da451ab77 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 17:30:26 -0400
Subject: [PATCH 69/94] feat: revalidate static assets with build-time ETags
Bun only auto-generates ETags for in-memory static routes, not the
Bun.file-backed responses registered here, so every repeat visit
re-downloaded every asset. Hash each asset during adapt, carry the hash
through the generated routes module, and serve static entries from small
route functions that answer if-none-match with a 304. HEAD handlers are
registered explicitly because Bun does not route HEAD to a GET handler.
---
packages/adapter-bun/index.js | 90 +++++++++++++++--------
packages/adapter-bun/src/routes-util.js | 78 +++++++++++++++-----
packages/adapter-bun/test/adapter.spec.ts | 42 +++++++----
packages/adapter-bun/test/routes.spec.ts | 74 +++++++++++++++----
4 files changed, 202 insertions(+), 82 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index be09316126e5..fcc4c1d64263 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -33,6 +33,17 @@ function is_dotfile(path) {
.some((segment, i) => segment.startsWith('.') && !(i === 0 && segment === '.well-known'));
}
+/**
+ * The build-time validator for conditional requests: Bun only generates ETags for
+ * in-memory static routes, not file-backed responses, so the adapter ships its own.
+ * @param {string} path
+ * @returns {Promise}
+ */
+async function asset_meta(path) {
+ const hash = Bun.hash(await Bun.file(path).arrayBuffer()).toString(16);
+ return JSON.stringify({ hash });
+}
+
/** @param {string[]} files */
function validate_file_paths(files) {
const invalid = files.find((file) => file.includes('*'));
@@ -179,30 +190,41 @@ async function get_embed_entries({ builder, server_assets }) {
});
let offset = 0;
- const cl_entries = cl_files.map(({ rel }, i) => {
- return `...client_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
- });
+ const cl_entries = await Promise.all(
+ cl_files.map(async ({ abs, rel }, i) => {
+ return `...client_asset(${JSON.stringify(rel)}, asset_${offset + i}, ${await asset_meta(abs)})`;
+ })
+ );
offset += cl_files.length;
const prerendered_pages_files = new Set(
[...builder.prerendered.pages].map(([_, { file }]) => file)
);
- const pr_pages_entries = [...builder.prerendered.pages].map(([path, { file }]) => {
- const fileIdx = pr_pages.findIndex((f) => f.rel === file);
- if (fileIdx === -1)
- throw new Error(`Could not find prerendered page ${file} for route ${path}`);
- return `...prerendered_page(${JSON.stringify(path)}, asset_${offset + fileIdx})`;
- });
- const pr_page_assets_entries = pr_pages.flatMap(({ rel }, i) => {
- return prerendered_pages_files.has(rel)
- ? []
- : [`...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`];
- });
+ const pr_pages_entries = await Promise.all(
+ [...builder.prerendered.pages].map(async ([path, { file }]) => {
+ const fileIdx = pr_pages.findIndex((f) => f.rel === file);
+ if (fileIdx === -1)
+ throw new Error(`Could not find prerendered page ${file} for route ${path}`);
+ return `...prerendered_page(${JSON.stringify(path)}, asset_${offset + fileIdx}, ${await asset_meta(pr_pages[fileIdx].abs)})`;
+ })
+ );
+ const pr_page_assets_entries = await Promise.all(
+ pr_pages.flatMap(({ abs, rel }, i) => {
+ return prerendered_pages_files.has(rel)
+ ? []
+ : [
+ (async () =>
+ `...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i}, ${await asset_meta(abs)})`)()
+ ];
+ })
+ );
offset += pr_pages.length;
- const pr_assets_entries = [...pr_deps, ...pr_data].map(({ rel }, i) => {
- return `...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i})`;
- });
+ const pr_assets_entries = await Promise.all(
+ [...pr_deps, ...pr_data].map(async ({ abs, rel }, i) => {
+ return `...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i}, ${await asset_meta(abs)})`;
+ })
+ );
const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
@@ -230,29 +252,35 @@ async function get_embed_entries({ builder, server_assets }) {
* @param {import('@sveltejs/kit').Builder} options.builder
* @param {string[]} options.server_assets
* @param {string} options.out
- * @returns {{imports: string[], entries: string[], server_assets: string[]}}
+ * @returns {Promise<{imports: string[], entries: string[], server_assets: string[]}>}
*/
-function get_no_embed_entries({ builder, server_assets, out }) {
+async function get_no_embed_entries({ builder, server_assets, out }) {
const client_files = builder.writeClient(`${out}/client`).filter((file) => !is_dotfile(file));
const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
validate_file_paths([...client_files, ...prerendered_files]);
- const cl_entries = client_files.map((filePath) => {
- return `...client_asset(${JSON.stringify(filePath)})`;
- });
+ const cl_entries = await Promise.all(
+ client_files.map(async (filePath) => {
+ return `...client_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/client/${filePath}`)})`;
+ })
+ );
const prerendered_pages = [...builder.prerendered.pages];
const prerendered_pages_files = new Set(prerendered_pages.map(([_, { file }]) => file));
- const pr_pages_entries = prerendered_pages.map(([path, { file }]) => {
- return `...prerendered_page(${JSON.stringify(path)}, ${JSON.stringify(file)})`;
- });
+ const pr_pages_entries = await Promise.all(
+ prerendered_pages.map(async ([path, { file }]) => {
+ return `...prerendered_page(${JSON.stringify(path)}, ${JSON.stringify(file)}, ${await asset_meta(`${out}/prerendered/${file}`)})`;
+ })
+ );
- const pr_assets_entries = prerendered_files
- .filter((filePath) => !prerendered_pages_files.has(filePath))
- .map((filePath) => {
- return `...prerendered_asset(${JSON.stringify(filePath)})`;
- });
+ const pr_assets_entries = await Promise.all(
+ prerendered_files
+ .filter((filePath) => !prerendered_pages_files.has(filePath))
+ .map(async (filePath) => {
+ return `...prerendered_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/prerendered/${filePath}`)})`;
+ })
+ );
const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
@@ -290,7 +318,7 @@ async function create_routes({ builder, out, embed }) {
server_assets: resolved_server_assets
} = embed
? await get_embed_entries({ builder, server_assets })
- : get_no_embed_entries({ builder, out, server_assets });
+ : await get_no_embed_entries({ builder, out, server_assets });
return [
`import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.js';`,
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 8d83481c633d..d0948c9cf52e 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -5,6 +5,7 @@ const dir = dirname(Bun.main);
/**
* @typedef {import('bun').Serve.Routes[string]} RouteHandler
+ * @typedef {{ hash: string }} AssetMeta
*/
// RFC 3986 pchar minus percent-escapes: characters user agents send raw in a path
@@ -66,33 +67,73 @@ function to_directory_paths(urlPath) {
return paths;
}
+/**
+ * @param {Request} request
+ * @param {string} etag
+ * @returns {boolean}
+ */
+function if_none_match(request, etag) {
+ const header = request.headers.get('if-none-match');
+ if (header === null) return false;
+ return header.split(',').some((value) => {
+ const tag = value.trim().replace(/^W\//, '');
+ return tag === '*' || tag === etag;
+ });
+}
+
+/**
+ * Serves one file with its build-time validator. Registered for GET and HEAD
+ * because Bun does not route HEAD requests to a GET handler.
+ * @param {string} path
+ * @param {Record} headers
+ * @param {AssetMeta} meta
+ * @returns {RouteHandler}
+ */
+function file_route(path, headers, meta) {
+ const etag = `"${meta.hash}"`;
+
+ /** @param {import('bun').BunRequest} request */
+ const handler = (request) => {
+ const response_headers = { ...headers, etag };
+ if (if_none_match(request, etag)) {
+ return new Response(null, { status: 304, headers: response_headers });
+ }
+ return new Response(Bun.file(path), { headers: response_headers });
+ };
+
+ return { GET: handler, HEAD: handler };
+}
+
/**
* @param {string} urlPath
- * @param {string} [filePath]
+ * @param {string | undefined} filePath
+ * @param {AssetMeta} meta
* @returns {Array<[string, RouteHandler]>}
*/
-export function client_asset(urlPath, filePath = urlPath) {
- const file = Bun.file(embed ? filePath : resolve(dir, 'client', filePath));
+export function client_asset(urlPath, filePath = urlPath, meta) {
+ const path = embed ? filePath : resolve(dir, 'client', filePath);
/** @type {Record} */
- const headers = { 'content-type': file.type };
+ const headers = { 'content-type': Bun.file(path).type };
if (urlPath.startsWith(`${manifest.appDir}/immutable/`)) {
headers['cache-control'] = 'public,max-age=31536000,immutable';
}
+ const route = file_route(path, headers, meta);
+
/** @type {Array<[string, RouteHandler]>} */
- const entries = to_paths(urlPath).map((path) => [path, { GET: new Response(file, { headers }) }]);
+ const entries = to_paths(urlPath).map((path) => [path, route]);
if (urlPath.endsWith('/index.html') || urlPath === 'index.html') {
const directory = urlPath.slice(0, -'index.html'.length);
for (const path of to_directory_paths(directory)) {
- entries.push([path, { GET: new Response(file, { headers }) }]);
+ entries.push([path, route]);
}
} else if (urlPath.endsWith('.html')) {
// sirv also serves `page.html` at `/page`
for (const path of to_paths(urlPath.slice(0, -'.html'.length))) {
- entries.push([path, { GET: new Response(file, { headers }) }]);
+ entries.push([path, route]);
}
}
@@ -110,21 +151,23 @@ export function server_asset(urlPath, filePath = urlPath) {
/**
* @param {string} urlPath
- * @param {string} [filePath]
+ * @param {string | undefined} filePath
+ * @param {AssetMeta} meta
* @returns {Array<[string, RouteHandler]>}
*/
-export function prerendered_asset(urlPath, filePath = urlPath) {
- const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
- const headers = { 'content-type': file.type };
- return to_paths(urlPath).map((path) => [path, { GET: new Response(file, { headers }) }]);
+export function prerendered_asset(urlPath, filePath = urlPath, meta) {
+ const path = embed ? filePath : resolve(dir, 'prerendered', filePath);
+ const route = file_route(path, { 'content-type': Bun.file(path).type }, meta);
+ return to_paths(urlPath).map((path) => [path, route]);
}
/**
* @param {string} urlPath
* @param {string} filePath
+ * @param {AssetMeta} meta
* @returns {Array<[string, RouteHandler]>}
*/
-export function prerendered_page(urlPath, filePath) {
+export function prerendered_page(urlPath, filePath, meta) {
const canonical = encode_pathname(urlPath);
/**
@@ -137,16 +180,13 @@ export function prerendered_page(urlPath, filePath) {
return new Response(null, { status: 308, headers: { location } });
}
- const file = Bun.file(embed ? filePath : resolve(dir, 'prerendered', filePath));
- const headers = { 'content-type': file.type };
+ const path = embed ? filePath : resolve(dir, 'prerendered', filePath);
+ const route = file_route(path, { 'content-type': Bun.file(path).type }, meta);
const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
// path already contains base, no need to add it here
/** @type {Array<[string, RouteHandler]>} */
- const entries = route_paths(urlPath).map((path) => [
- path,
- { GET: new Response(file, { headers }) }
- ]);
+ const entries = route_paths(urlPath).map((path) => [path, route]);
if (inverted) {
for (const path of route_paths(inverted)) {
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 8ac1eb04f2a0..58a4f9174380 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -17,11 +17,15 @@ vi.mock('node:fs/promises', async (import_original) => {
const bun = vi.hoisted(() => ({
entrypoint: '// generated server entrypoint',
build: vi.fn(async (_options: any): Promise => ({ success: true, logs: [], outputs: [] })),
- file: vi.fn((_path: string) => ({ text: async () => '// generated server entrypoint' }))
+ file: vi.fn((_path: string) => ({
+ text: async () => '// generated server entrypoint',
+ arrayBuffer: async () => new ArrayBuffer(0)
+ })),
+ hash: vi.fn(() => 0xabcn)
}));
beforeEach(() => {
- vi.stubGlobal('Bun', { build: bun.build, file: bun.file });
+ vi.stubGlobal('Bun', { build: bun.build, file: bun.file, hash: bun.hash });
vi.mocked(readdir).mockResolvedValue([]);
});
@@ -205,10 +209,12 @@ describe('generated routes', () => {
expect(builder.findServerAssets).toHaveBeenCalledWith([dynamic]);
const source = bun.build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('...client_asset("data.json")');
- expect(source).toContain('...client_asset("_app/immutable/read.txt")');
- expect(source).toContain('...prerendered_page("/page/", "page/index.html")');
- expect(source).toContain('prerendered_asset("icon.png")');
+ expect(source).toContain('...client_asset("data.json", undefined, {"hash":"abc"})');
+ expect(source).toContain(
+ '...client_asset("_app/immutable/read.txt", undefined, {"hash":"abc"})'
+ );
+ expect(source).toContain('...prerendered_page("/page/", "page/index.html", {"hash":"abc"})');
+ expect(source).toContain('prerendered_asset("icon.png", undefined, {"hash":"abc"})');
expect(source).toContain('prerendered_redirect("/old", 301, "/new")');
expect(source).toContain(
'["_app/immutable/read.txt", server_asset("_app/immutable/read.txt")]'
@@ -225,7 +231,9 @@ describe('generated routes', () => {
);
const source = bun.build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('...prerendered_page("/base/page/", "page/index.html")');
+ expect(source).toContain(
+ '...prerendered_page("/base/page/", "page/index.html", {"hash":"abc"})'
+ );
expect(source).not.toContain('/base/base/');
});
@@ -246,12 +254,12 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('...client_asset("data.json", asset_0)');
- expect(source).toContain('...client_asset(".well-known/asset.txt", asset_1)');
- expect(source).toContain('...prerendered_page("/page/", asset_3)');
- expect(source).toContain('prerendered_asset("favicon.ico", asset_4)');
- expect(source).toContain('prerendered_asset("dependency.json", asset_5)');
- expect(source).toContain('prerendered_asset("page/__data.json", asset_6)');
+ expect(source).toContain('...client_asset("data.json", asset_0, {"hash":"abc"})');
+ expect(source).toContain('...client_asset(".well-known/asset.txt", asset_1, {"hash":"abc"})');
+ expect(source).toContain('...prerendered_page("/page/", asset_3, {"hash":"abc"})');
+ expect(source).toContain('prerendered_asset("favicon.ico", asset_4, {"hash":"abc"})');
+ expect(source).toContain('prerendered_asset("dependency.json", asset_5, {"hash":"abc"})');
+ expect(source).toContain('prerendered_asset("page/__data.json", asset_6, {"hash":"abc"})');
expect(source).toContain('["_app/read.txt", server_asset("_app/read.txt", asset_2)]');
expect(source).not.toContain('.vite/manifest.json');
});
@@ -275,8 +283,10 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).not.toContain('.env');
- expect(source).toContain('...client_asset(".well-known/security.txt")');
- expect(source).toContain('...client_asset("ok.txt")');
+ expect(source).toContain(
+ '...client_asset(".well-known/security.txt", undefined, {"hash":"abc"})'
+ );
+ expect(source).toContain('...client_asset("ok.txt", undefined, {"hash":"abc"})');
});
test('excludes dotfiles from embedded assets', async () => {
@@ -286,7 +296,7 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).not.toContain('.secret');
- expect(source).toContain('...client_asset("public.txt", asset_0)');
+ expect(source).toContain('...client_asset("public.txt", asset_0, {"hash":"abc"})');
});
test('rejects wildcard characters in prerendered redirect sources', async () => {
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index 6e263694db1c..b5907591a0f2 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -1,5 +1,7 @@
import { afterEach, expect, test, vi } from 'vitest';
+const meta = { hash: 'abc' };
+
afterEach(() => {
vi.resetModules();
vi.doUnmock('MANIFEST');
@@ -9,24 +11,25 @@ afterEach(() => {
test('client assets use the configured base and URL-encode path segments', async () => {
const { routes, file } = await load_routes({ base: '/base' });
- const entries = routes.client_asset('folder/encoded name#1.txt');
+ const entries = routes.client_asset('folder/encoded name#1.txt', undefined, meta);
expect(entries).toHaveLength(1);
expect(entries[0][0]).toBe('/base/folder/encoded%20name%231.txt');
expect(file).toHaveBeenCalledWith('/app/build/client/folder/encoded name#1.txt');
expect(entries[0][1]).toHaveProperty('GET');
- expect((entries[0][1] as any).GET.headers.get('content-type')).toBe('text/plain;charset=utf-8');
+ const response = (entries[0][1] as any).GET(new Request('http://localhost/'));
+ expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8');
});
test('client index files are also available at their directory URL', async () => {
const { routes } = await load_routes({ base: '/base' });
- expect(routes.client_asset('index.html').map(([path]) => path)).toEqual([
+ expect(routes.client_asset('index.html', undefined, meta).map(([path]) => path)).toEqual([
'/base/index.html',
'/base/',
'/base'
]);
- expect(routes.client_asset('docs/index.html').map(([path]) => path)).toEqual([
+ expect(routes.client_asset('docs/index.html', undefined, meta).map(([path]) => path)).toEqual([
'/base/docs/index.html',
'/base/docs/',
'/base/docs'
@@ -36,7 +39,7 @@ test('client index files are also available at their directory URL', async () =>
test('other client HTML files are also available without their extension', async () => {
const { routes } = await load_routes({ base: '/base' });
- expect(routes.client_asset('page.html').map(([path]) => path)).toEqual([
+ expect(routes.client_asset('page.html', undefined, meta).map(([path]) => path)).toEqual([
'/base/page.html',
'/base/page'
]);
@@ -45,7 +48,7 @@ test('other client HTML files are also available without their extension', async
test('sub-delims stay raw in route paths with a fully-encoded alias', async () => {
const { routes } = await load_routes({ base: '/base' });
- expect(routes.client_asset('a&b.txt').map(([path]) => path)).toEqual([
+ expect(routes.client_asset('a&b.txt', undefined, meta).map(([path]) => path)).toEqual([
'/base/a&b.txt',
'/base/a%26b.txt'
]);
@@ -54,24 +57,62 @@ test('sub-delims stay raw in route paths with a fully-encoded alias', async () =
test('segments starting with a colon are escaped to avoid Bun route parameters', async () => {
const { routes } = await load_routes({ base: '/base' });
- expect(routes.client_asset(':tag.txt').map(([path]) => path)).toEqual(['/base/%3Atag.txt']);
+ expect(routes.client_asset(':tag.txt', undefined, meta).map(([path]) => path)).toEqual([
+ '/base/%3Atag.txt'
+ ]);
});
test('immutable SvelteKit assets receive a long-lived cache policy', async () => {
const { routes } = await load_routes({ appDir: '_app' });
- const immutable = (routes.client_asset('_app/immutable/chunk.js')[0][1] as any).GET;
- const mutable = (routes.client_asset('favicon.ico')[0][1] as any).GET;
+ const request = new Request('http://localhost/');
+ const immutable = (
+ routes.client_asset('_app/immutable/chunk.js', undefined, meta)[0][1] as any
+ ).GET(request);
+ const mutable = (routes.client_asset('favicon.ico', undefined, meta)[0][1] as any).GET(request);
expect(immutable.headers.get('cache-control')).toBe('public,max-age=31536000,immutable');
expect(mutable.headers.has('cache-control')).toBe(false);
});
+test('static routes revalidate against the build-time hash', async () => {
+ const { routes } = await load_routes();
+
+ const route = routes.client_asset('data.json', undefined, meta)[0][1] as any;
+
+ const fresh = route.GET(new Request('http://localhost/data.json'));
+ expect(fresh.status).toBe(200);
+ expect(fresh.headers.get('etag')).toBe('"abc"');
+
+ const revalidated = route.GET(
+ new Request('http://localhost/data.json', { headers: { 'if-none-match': '"abc"' } })
+ );
+ expect(revalidated.status).toBe(304);
+ expect(revalidated.headers.get('etag')).toBe('"abc"');
+
+ const weak = route.GET(
+ new Request('http://localhost/data.json', { headers: { 'if-none-match': 'W/"abc", "other"' } })
+ );
+ expect(weak.status).toBe(304);
+
+ const stale = route.GET(
+ new Request('http://localhost/data.json', { headers: { 'if-none-match': '"old"' } })
+ );
+ expect(stale.status).toBe(200);
+});
+
+test('static routes answer HEAD with the same handler', async () => {
+ const { routes } = await load_routes();
+
+ const route = routes.client_asset('data.json', undefined, meta)[0][1] as any;
+ expect(route.HEAD).toBe(route.GET);
+});
+
test('embedded routes use the imported asset instead of a filesystem path', async () => {
const { routes, file } = await load_routes({ embed: true });
- routes.client_asset('asset.txt', '/embedded/client.txt');
- routes.prerendered_asset('asset.txt', '/embedded/prerendered.txt');
+ routes.client_asset('asset.txt', '/embedded/client.txt', meta);
+ routes.prerendered_asset('asset.txt', '/embedded/prerendered.txt', meta);
const server_file = routes.server_asset('asset.txt', '/embedded/server.txt');
expect(file).toHaveBeenNthCalledWith(1, '/embedded/client.txt');
@@ -93,10 +134,11 @@ test('prerendered assets use the base path and preserve their content type', asy
const { routes, file } = await load_routes({ base: '/base' });
file.mockImplementationOnce((path) => ({ path, type: 'image/x-icon' }));
- const [[path, handler]] = routes.prerendered_asset('icon.ico');
+ const [[path, handler]] = routes.prerendered_asset('icon.ico', undefined, meta);
expect(path).toBe('/base/icon.ico');
- expect((handler as any).GET.headers.get('content-type')).toBe('image/x-icon');
+ const response = (handler as any).GET(new Request('http://localhost/base/icon.ico'));
+ expect(response.headers.get('content-type')).toBe('image/x-icon');
});
test.each([
@@ -106,7 +148,7 @@ test.each([
'prerendered page %s redirects its alternate form %s to the canonical URL',
async (canonical, alternate, location) => {
const { routes } = await load_routes();
- const entries = routes.prerendered_page(canonical, 'page.html');
+ const entries = routes.prerendered_page(canonical, 'page.html', meta);
expect(entries[0][0]).toBe(canonical);
expect(entries[1][0]).toBe(alternate);
@@ -120,7 +162,7 @@ test.each([
test('redirects to non-ASCII canonical URLs use a percent-encoded location', async () => {
const { routes } = await load_routes();
- const entries = routes.prerendered_page('/café/', 'cafe.html');
+ const entries = routes.prerendered_page('/café/', 'cafe.html', meta);
const response = (entries[1][1] as any).GET(new Request('http://localhost/caf%C3%A9'));
expect(response.headers.get('location')).toBe('/caf%C3%A9/');
@@ -129,7 +171,7 @@ test('redirects to non-ASCII canonical URLs use a percent-encoded location', asy
test('a prerendered root page has no duplicate alternate route', async () => {
const { routes } = await load_routes();
- expect(routes.prerendered_page('/', 'index.html')).toHaveLength(1);
+ expect(routes.prerendered_page('/', 'index.html', meta)).toHaveLength(1);
});
test('prerendered redirects retain their status and location', async () => {
From ac4f83a9316964d2b2cea47bd8c02761e456d55e Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 17:34:37 -0400
Subject: [PATCH 70/94] feat: precompress option with accept-encoding
negotiation
Mirrors adapter-node's precompress: builder.compress writes .br and .gz
variants during adapt, the generated meta records which variants exist,
and the static route handlers serve the smallest accepted encoding with
a per-variant ETag. Range requests stay on the identity representation.
Ignored under buildOptions.compile, where assets are imported by
identity path.
---
.../25-build-and-deploy/45-adapter-bun.md | 6 ++-
packages/adapter-bun/index.d.ts | 8 +++
packages/adapter-bun/index.js | 43 ++++++++++++----
packages/adapter-bun/src/routes-util.js | 48 ++++++++++++++---
packages/adapter-bun/test/adapter.spec.ts | 29 +++++++++++
packages/adapter-bun/test/routes.spec.ts | 51 +++++++++++++++++++
6 files changed, 168 insertions(+), 17 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 2e8d34be2b38..496a3f7b7cb6 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -44,7 +44,7 @@ bun ./build
The JavaScript server, client files, and prerendered files in the output directory are all required at runtime. Application imports are processed according to Bun's bundler behavior.
-Client assets and prerendered output are registered as native Bun routes. Only `GET` and the corresponding automatic `HEAD` requests are served by those routes; other methods continue to SvelteKit. Bun supplies MIME types, conditional-request validators, byte ranges for filesystem-backed files, and streaming without buffering every asset in memory. Files below SvelteKit's `immutable` directory receive `Cache-Control: public,max-age=31536000,immutable`.
+Client assets and prerendered output are registered as native Bun routes. Only `GET` and `HEAD` requests are served by those routes; other methods continue to SvelteKit. Every asset carries an ETag computed during the build, so conditional requests revalidate with an empty `304` response. Bun supplies MIME types, byte ranges for filesystem-backed files, and streaming without buffering every asset in memory. Files below SvelteKit's `immutable` directory receive `Cache-Control: public,max-age=31536000,immutable`.
> [!NOTE] Bun treats `*` in a route pathname as a wildcard. The adapter rejects client and prerendered filenames that contain a literal `*`; rename those files before building.
@@ -79,6 +79,10 @@ export default defineConfig({
The output directory. It defaults to `build`.
+### precompress
+
+Set `precompress: true` to generate `.br` and `.gz` variants of client and prerendered assets during the build. The generated routes negotiate `Accept-Encoding` per request and serve the smallest accepted variant with its own ETag. The option is ignored when `buildOptions.compile` is set, because embedded assets are imported by identity path.
+
### envPrefix
A prefix for every deployment environment variable documented below. This is useful when the unprefixed names conflict with variables managed by your host:
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index fbc47053cee9..c07734d1c952 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -7,6 +7,14 @@ interface AdapterOptions {
* @default 'build'
*/
out?: string;
+ /**
+ * Generate `.br` and `.gz` variants of client and prerendered assets during the
+ * build. The generated routes negotiate `Accept-Encoding` per request and serve
+ * the smallest accepted variant. Ignored when `buildOptions.compile` is set,
+ * because embedded assets are imported by identity path.
+ * @default false
+ */
+ precompress?: boolean;
/**
* If you need to change the name of the environment variables used to configure
* the deployment (for example, to deconflict with environment variables you
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index fcc4c1d64263..dd315ef76437 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -37,11 +37,20 @@ function is_dotfile(path) {
* The build-time validator for conditional requests: Bun only generates ETags for
* in-memory static routes, not file-backed responses, so the adapter ships its own.
* @param {string} path
+ * @param {boolean} [precompress]
* @returns {Promise}
*/
-async function asset_meta(path) {
+async function asset_meta(path, precompress = false) {
const hash = Bun.hash(await Bun.file(path).arrayBuffer()).toString(16);
- return JSON.stringify({ hash });
+
+ /** @type {{ hash: string, br?: boolean, gz?: boolean }} */
+ const meta = { hash };
+ if (precompress) {
+ if (await Bun.file(`${path}.br`).exists()) meta.br = true;
+ if (await Bun.file(`${path}.gz`).exists()) meta.gz = true;
+ }
+
+ return JSON.stringify(meta);
}
/** @param {string[]} files */
@@ -56,7 +65,13 @@ function validate_file_paths(files) {
/** @type {import('./index.js').default} */
export default function (opts = {}) {
- const { out = 'build', envPrefix = '', serverOptions = {}, buildOptions = {} } = opts;
+ const {
+ out = 'build',
+ envPrefix = '',
+ precompress = false,
+ serverOptions = {},
+ buildOptions = {}
+ } = opts;
return {
name: '@sveltejs/adapter-bun',
@@ -90,7 +105,9 @@ export default function (opts = {}) {
[routes_file]: await create_routes({
builder,
out,
- embed: !!buildOptions.compile
+ embed: !!buildOptions.compile,
+ // embedded assets are imported by identity path, so variants cannot ride along
+ precompress: precompress && !buildOptions.compile
})
};
@@ -252,16 +269,21 @@ async function get_embed_entries({ builder, server_assets }) {
* @param {import('@sveltejs/kit').Builder} options.builder
* @param {string[]} options.server_assets
* @param {string} options.out
+ * @param {boolean} options.precompress
* @returns {Promise<{imports: string[], entries: string[], server_assets: string[]}>}
*/
-async function get_no_embed_entries({ builder, server_assets, out }) {
+async function get_no_embed_entries({ builder, server_assets, out, precompress }) {
const client_files = builder.writeClient(`${out}/client`).filter((file) => !is_dotfile(file));
const prerendered_files = builder.writePrerendered(`${out}/prerendered`);
validate_file_paths([...client_files, ...prerendered_files]);
+ if (precompress) {
+ await Promise.all([builder.compress(`${out}/client`), builder.compress(`${out}/prerendered`)]);
+ }
+
const cl_entries = await Promise.all(
client_files.map(async (filePath) => {
- return `...client_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/client/${filePath}`)})`;
+ return `...client_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/client/${filePath}`, precompress)})`;
})
);
@@ -270,7 +292,7 @@ async function get_no_embed_entries({ builder, server_assets, out }) {
const pr_pages_entries = await Promise.all(
prerendered_pages.map(async ([path, { file }]) => {
- return `...prerendered_page(${JSON.stringify(path)}, ${JSON.stringify(file)}, ${await asset_meta(`${out}/prerendered/${file}`)})`;
+ return `...prerendered_page(${JSON.stringify(path)}, ${JSON.stringify(file)}, ${await asset_meta(`${out}/prerendered/${file}`, precompress)})`;
})
);
@@ -278,7 +300,7 @@ async function get_no_embed_entries({ builder, server_assets, out }) {
prerendered_files
.filter((filePath) => !prerendered_pages_files.has(filePath))
.map(async (filePath) => {
- return `...prerendered_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/prerendered/${filePath}`)})`;
+ return `...prerendered_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/prerendered/${filePath}`, precompress)})`;
})
);
@@ -300,9 +322,10 @@ async function get_no_embed_entries({ builder, server_assets, out }) {
* @param {import('@sveltejs/kit').Builder} options.builder
* @param {string} options.out
* @param {boolean} options.embed
+ * @param {boolean} options.precompress
* @returns {Promise}
*/
-async function create_routes({ builder, out, embed }) {
+async function create_routes({ builder, out, embed, precompress }) {
validate_file_paths([
...builder.prerendered.pages.keys(),
...builder.prerendered.redirects.keys()
@@ -318,7 +341,7 @@ async function create_routes({ builder, out, embed }) {
server_assets: resolved_server_assets
} = embed
? await get_embed_entries({ builder, server_assets })
- : await get_no_embed_entries({ builder, out, server_assets });
+ : await get_no_embed_entries({ builder, out, server_assets, precompress });
return [
`import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.js';`,
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index d0948c9cf52e..549e896fedde 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -5,9 +5,11 @@ const dir = dirname(Bun.main);
/**
* @typedef {import('bun').Serve.Routes[string]} RouteHandler
- * @typedef {{ hash: string }} AssetMeta
+ * @typedef {{ hash: string, br?: boolean, gz?: boolean }} AssetMeta
*/
+const CONTENT_ENCODING = { br: 'br', gz: 'gzip' };
+
// RFC 3986 pchar minus percent-escapes: characters user agents send raw in a path
const RAW_PATH_CHAR = /^[A-Za-z0-9\-._~!$&'()+,;=:@]$/;
@@ -82,23 +84,57 @@ function if_none_match(request, etag) {
}
/**
- * Serves one file with its build-time validator. Registered for GET and HEAD
- * because Bun does not route HEAD requests to a GET handler.
+ * @param {string | null} accept
+ * @param {AssetMeta} meta
+ * @returns {'br' | 'gz' | null}
+ */
+function negotiate(accept, meta) {
+ if (accept === null || (!meta.br && !meta.gz)) return null;
+
+ const accepted = new Set();
+ for (const part of accept.split(',')) {
+ const [name = '', ...params] = part.trim().toLowerCase().split(';');
+ if (params.some((param) => /^q=0(\.0*)?$/.test(param.trim()))) continue;
+ accepted.add(name.trim());
+ }
+
+ if (meta.br && (accepted.has('br') || accepted.has('*'))) return 'br';
+ if (meta.gz && (accepted.has('gzip') || accepted.has('*'))) return 'gz';
+ return null;
+}
+
+/**
+ * Serves one file with its build-time validator and precompressed variants.
+ * Registered for GET and HEAD because Bun does not route HEAD requests to a
+ * GET handler.
* @param {string} path
* @param {Record} headers
* @param {AssetMeta} meta
* @returns {RouteHandler}
*/
function file_route(path, headers, meta) {
- const etag = `"${meta.hash}"`;
-
/** @param {import('bun').BunRequest} request */
const handler = (request) => {
+ // Bun serializes Range itself for file bodies; ranges apply to the identity representation
+ const encoding =
+ request.headers.get('range') === null
+ ? negotiate(request.headers.get('accept-encoding'), meta)
+ : null;
+ const etag = encoding === null ? `"${meta.hash}"` : `"${meta.hash}-${encoding}"`;
+
+ /** @type {Record} */
const response_headers = { ...headers, etag };
+ if (meta.br || meta.gz) response_headers['vary'] = 'accept-encoding';
+
if (if_none_match(request, etag)) {
return new Response(null, { status: 304, headers: response_headers });
}
- return new Response(Bun.file(path), { headers: response_headers });
+ if (encoding === null) {
+ return new Response(Bun.file(path), { headers: response_headers });
+ }
+
+ response_headers['content-encoding'] = CONTENT_ENCODING[encoding];
+ return new Response(Bun.file(`${path}.${encoding}`), { headers: response_headers });
};
return { GET: handler, HEAD: handler };
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 58a4f9174380..71b409c0f483 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -274,6 +274,34 @@ describe('generated routes', () => {
expect(bun.build).not.toHaveBeenCalled();
});
+ test('precompresses assets and marks the variants in the generated routes', async () => {
+ const previous = bun.file.getMockImplementation();
+ bun.file.mockImplementation((path: string) => ({
+ text: async () => '// generated server entrypoint',
+ arrayBuffer: async () => new ArrayBuffer(0),
+ exists: async () => path.endsWith('.br') || path.endsWith('.gz')
+ }));
+ const builder = create_builder({ client_files: ['app.js'] });
+
+ await adapter({ precompress: true }).adapt(builder);
+
+ expect(builder.compress).toHaveBeenCalledWith('build/client');
+ expect(builder.compress).toHaveBeenCalledWith('build/prerendered');
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain(
+ '...client_asset("app.js", undefined, {"hash":"abc","br":true,"gz":true})'
+ );
+ bun.file.mockImplementation(previous!);
+ });
+
+ test('does not compress by default', async () => {
+ const builder = create_builder({ client_files: ['app.js'] });
+
+ await adapter().adapt(builder);
+
+ expect(builder.compress).not.toHaveBeenCalled();
+ });
+
test('does not register dotfiles apart from .well-known', async () => {
const builder = create_builder({
client_files: ['.env', '.well-known/security.txt', 'ok.txt']
@@ -400,6 +428,7 @@ function create_builder({
rimraf: vi.fn(),
writeClient: vi.fn(() => client_files),
writePrerendered: vi.fn(() => prerendered_files),
+ compress: vi.fn(async () => {}),
findServerAssets: vi.fn(() => server_assets),
generateManifest: vi.fn(() => '{"appDir":"_app"}'),
hasServerInstrumentationFile: () => instrumentation,
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index b5907591a0f2..1c723309764a 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -108,6 +108,57 @@ test('static routes answer HEAD with the same handler', async () => {
expect(route.HEAD).toBe(route.GET);
});
+test('precompressed variants are negotiated with their own validators', async () => {
+ const { routes, file } = await load_routes();
+
+ const route = routes.client_asset('app.js', undefined, {
+ hash: 'abc',
+ br: true,
+ gz: true
+ })[0][1] as any;
+
+ const br = route.GET(
+ new Request('http://localhost/app.js', { headers: { 'accept-encoding': 'br, gzip' } })
+ );
+ expect(br.headers.get('content-encoding')).toBe('br');
+ expect(br.headers.get('etag')).toBe('"abc-br"');
+ expect(br.headers.get('vary')).toBe('accept-encoding');
+ expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js.br');
+
+ const gzip = route.GET(
+ new Request('http://localhost/app.js', { headers: { 'accept-encoding': 'br;q=0, gzip' } })
+ );
+ expect(gzip.headers.get('content-encoding')).toBe('gzip');
+ expect(gzip.headers.get('etag')).toBe('"abc-gz"');
+ expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js.gz');
+
+ const identity = route.GET(new Request('http://localhost/app.js'));
+ expect(identity.headers.has('content-encoding')).toBe(false);
+ expect(identity.headers.get('etag')).toBe('"abc"');
+
+ const revalidated = route.GET(
+ new Request('http://localhost/app.js', {
+ headers: { 'accept-encoding': 'br', 'if-none-match': '"abc-br"' }
+ })
+ );
+ expect(revalidated.status).toBe(304);
+});
+
+test('range requests are served from the identity representation', async () => {
+ const { routes, file } = await load_routes();
+
+ const route = routes.client_asset('app.js', undefined, { hash: 'abc', br: true })[0][1] as any;
+ const response = route.GET(
+ new Request('http://localhost/app.js', {
+ headers: { 'accept-encoding': 'br', range: 'bytes=0-9' }
+ })
+ );
+
+ expect(response.headers.has('content-encoding')).toBe(false);
+ expect(response.headers.get('etag')).toBe('"abc"');
+ expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js');
+});
+
test('embedded routes use the imported asset instead of a filesystem path', async () => {
const { routes, file } = await load_routes({ embed: true });
From d48e89aae66674dfb6fe535c4560bd8df9e2bd96 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 17:42:49 -0400
Subject: [PATCH 71/94] fix: preserve last-modified revalidation
Frozen file routes got If-Modified-Since 304s from Bun natively; function
routes do not. Carry the build-time mtime through the generated meta and
answer date validators in the handler, If-None-Match taking precedence.
---
packages/adapter-bun/index.js | 7 ++--
packages/adapter-bun/src/routes-util.js | 27 ++++++++++-----
packages/adapter-bun/test/adapter.spec.ts | 42 ++++++++++++++---------
packages/adapter-bun/test/routes.spec.ts | 35 +++++++++++++++++--
4 files changed, 81 insertions(+), 30 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index dd315ef76437..f2851c64b1ac 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -41,10 +41,11 @@ function is_dotfile(path) {
* @returns {Promise}
*/
async function asset_meta(path, precompress = false) {
- const hash = Bun.hash(await Bun.file(path).arrayBuffer()).toString(16);
+ const file = Bun.file(path);
+ const hash = Bun.hash(await file.arrayBuffer()).toString(16);
- /** @type {{ hash: string, br?: boolean, gz?: boolean }} */
- const meta = { hash };
+ /** @type {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} */
+ const meta = { hash, mtime: file.lastModified };
if (precompress) {
if (await Bun.file(`${path}.br`).exists()) meta.br = true;
if (await Bun.file(`${path}.gz`).exists()) meta.gz = true;
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 549e896fedde..b20933ab7c15 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -5,7 +5,7 @@ const dir = dirname(Bun.main);
/**
* @typedef {import('bun').Serve.Routes[string]} RouteHandler
- * @typedef {{ hash: string, br?: boolean, gz?: boolean }} AssetMeta
+ * @typedef {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} AssetMeta
*/
const CONTENT_ENCODING = { br: 'br', gz: 'gzip' };
@@ -70,17 +70,24 @@ function to_directory_paths(urlPath) {
}
/**
+ * If-None-Match takes precedence over If-Modified-Since (RFC 9110 §13.1.3);
+ * dates compare at whole-second precision because HTTP dates have none finer.
* @param {Request} request
* @param {string} etag
+ * @param {number} mtime
* @returns {boolean}
*/
-function if_none_match(request, etag) {
+function is_fresh(request, etag, mtime) {
const header = request.headers.get('if-none-match');
- if (header === null) return false;
- return header.split(',').some((value) => {
- const tag = value.trim().replace(/^W\//, '');
- return tag === '*' || tag === etag;
- });
+ if (header !== null) {
+ return header.split(',').some((value) => {
+ const tag = value.trim().replace(/^W\//, '');
+ return tag === '*' || tag === etag;
+ });
+ }
+
+ const since = Date.parse(request.headers.get('if-modified-since') ?? '');
+ return Number.isFinite(since) && Math.trunc(mtime / 1000) <= Math.trunc(since / 1000);
}
/**
@@ -113,6 +120,8 @@ function negotiate(accept, meta) {
* @returns {RouteHandler}
*/
function file_route(path, headers, meta) {
+ const last_modified = new Date(meta.mtime).toUTCString();
+
/** @param {import('bun').BunRequest} request */
const handler = (request) => {
// Bun serializes Range itself for file bodies; ranges apply to the identity representation
@@ -123,10 +132,10 @@ function file_route(path, headers, meta) {
const etag = encoding === null ? `"${meta.hash}"` : `"${meta.hash}-${encoding}"`;
/** @type {Record} */
- const response_headers = { ...headers, etag };
+ const response_headers = { ...headers, etag, 'last-modified': last_modified };
if (meta.br || meta.gz) response_headers['vary'] = 'accept-encoding';
- if (if_none_match(request, etag)) {
+ if (is_fresh(request, etag, meta.mtime)) {
return new Response(null, { status: 304, headers: response_headers });
}
if (encoding === null) {
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 71b409c0f483..29e6c7bd7a5b 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -19,7 +19,8 @@ const bun = vi.hoisted(() => ({
build: vi.fn(async (_options: any): Promise => ({ success: true, logs: [], outputs: [] })),
file: vi.fn((_path: string) => ({
text: async () => '// generated server entrypoint',
- arrayBuffer: async () => new ArrayBuffer(0)
+ arrayBuffer: async () => new ArrayBuffer(0),
+ lastModified: 0
})),
hash: vi.fn(() => 0xabcn)
}));
@@ -209,12 +210,14 @@ describe('generated routes', () => {
expect(builder.findServerAssets).toHaveBeenCalledWith([dynamic]);
const source = bun.build.mock.calls[0][0].files[routes_file];
- expect(source).toContain('...client_asset("data.json", undefined, {"hash":"abc"})');
+ expect(source).toContain('...client_asset("data.json", undefined, {"hash":"abc","mtime":0})');
expect(source).toContain(
- '...client_asset("_app/immutable/read.txt", undefined, {"hash":"abc"})'
+ '...client_asset("_app/immutable/read.txt", undefined, {"hash":"abc","mtime":0})'
);
- expect(source).toContain('...prerendered_page("/page/", "page/index.html", {"hash":"abc"})');
- expect(source).toContain('prerendered_asset("icon.png", undefined, {"hash":"abc"})');
+ expect(source).toContain(
+ '...prerendered_page("/page/", "page/index.html", {"hash":"abc","mtime":0})'
+ );
+ expect(source).toContain('prerendered_asset("icon.png", undefined, {"hash":"abc","mtime":0})');
expect(source).toContain('prerendered_redirect("/old", 301, "/new")');
expect(source).toContain(
'["_app/immutable/read.txt", server_asset("_app/immutable/read.txt")]'
@@ -232,7 +235,7 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).toContain(
- '...prerendered_page("/base/page/", "page/index.html", {"hash":"abc"})'
+ '...prerendered_page("/base/page/", "page/index.html", {"hash":"abc","mtime":0})'
);
expect(source).not.toContain('/base/base/');
});
@@ -254,12 +257,18 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).toContain("with { type: 'file' }");
- expect(source).toContain('...client_asset("data.json", asset_0, {"hash":"abc"})');
- expect(source).toContain('...client_asset(".well-known/asset.txt", asset_1, {"hash":"abc"})');
- expect(source).toContain('...prerendered_page("/page/", asset_3, {"hash":"abc"})');
- expect(source).toContain('prerendered_asset("favicon.ico", asset_4, {"hash":"abc"})');
- expect(source).toContain('prerendered_asset("dependency.json", asset_5, {"hash":"abc"})');
- expect(source).toContain('prerendered_asset("page/__data.json", asset_6, {"hash":"abc"})');
+ expect(source).toContain('...client_asset("data.json", asset_0, {"hash":"abc","mtime":0})');
+ expect(source).toContain(
+ '...client_asset(".well-known/asset.txt", asset_1, {"hash":"abc","mtime":0})'
+ );
+ expect(source).toContain('...prerendered_page("/page/", asset_3, {"hash":"abc","mtime":0})');
+ expect(source).toContain('prerendered_asset("favicon.ico", asset_4, {"hash":"abc","mtime":0})');
+ expect(source).toContain(
+ 'prerendered_asset("dependency.json", asset_5, {"hash":"abc","mtime":0})'
+ );
+ expect(source).toContain(
+ 'prerendered_asset("page/__data.json", asset_6, {"hash":"abc","mtime":0})'
+ );
expect(source).toContain('["_app/read.txt", server_asset("_app/read.txt", asset_2)]');
expect(source).not.toContain('.vite/manifest.json');
});
@@ -279,6 +288,7 @@ describe('generated routes', () => {
bun.file.mockImplementation((path: string) => ({
text: async () => '// generated server entrypoint',
arrayBuffer: async () => new ArrayBuffer(0),
+ lastModified: 0,
exists: async () => path.endsWith('.br') || path.endsWith('.gz')
}));
const builder = create_builder({ client_files: ['app.js'] });
@@ -289,7 +299,7 @@ describe('generated routes', () => {
expect(builder.compress).toHaveBeenCalledWith('build/prerendered');
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).toContain(
- '...client_asset("app.js", undefined, {"hash":"abc","br":true,"gz":true})'
+ '...client_asset("app.js", undefined, {"hash":"abc","mtime":0,"br":true,"gz":true})'
);
bun.file.mockImplementation(previous!);
});
@@ -312,9 +322,9 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).not.toContain('.env');
expect(source).toContain(
- '...client_asset(".well-known/security.txt", undefined, {"hash":"abc"})'
+ '...client_asset(".well-known/security.txt", undefined, {"hash":"abc","mtime":0})'
);
- expect(source).toContain('...client_asset("ok.txt", undefined, {"hash":"abc"})');
+ expect(source).toContain('...client_asset("ok.txt", undefined, {"hash":"abc","mtime":0})');
});
test('excludes dotfiles from embedded assets', async () => {
@@ -324,7 +334,7 @@ describe('generated routes', () => {
const source = bun.build.mock.calls[0][0].files[routes_file];
expect(source).not.toContain('.secret');
- expect(source).toContain('...client_asset("public.txt", asset_0, {"hash":"abc"})');
+ expect(source).toContain('...client_asset("public.txt", asset_0, {"hash":"abc","mtime":0})');
});
test('rejects wildcard characters in prerendered redirect sources', async () => {
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index 1c723309764a..f0e4f03b6ae3 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, expect, test, vi } from 'vitest';
-const meta = { hash: 'abc' };
+const meta = { hash: 'abc', mtime: 0 };
afterEach(() => {
vi.resetModules();
@@ -101,6 +101,32 @@ test('static routes revalidate against the build-time hash', async () => {
expect(stale.status).toBe(200);
});
+test('static routes revalidate by date when the client has no ETag', async () => {
+ const { routes } = await load_routes();
+
+ const route = routes.client_asset('data.json', undefined, meta)[0][1] as any;
+
+ const fresh = route.GET(new Request('http://localhost/data.json'));
+ expect(fresh.headers.get('last-modified')).toBe('Thu, 01 Jan 1970 00:00:00 GMT');
+
+ const dated = route.GET(
+ new Request('http://localhost/data.json', {
+ headers: { 'if-modified-since': 'Thu, 01 Jan 1970 00:00:00 GMT' }
+ })
+ );
+ expect(dated.status).toBe(304);
+
+ const stale_etag_wins = route.GET(
+ new Request('http://localhost/data.json', {
+ headers: {
+ 'if-modified-since': 'Thu, 01 Jan 1970 00:00:00 GMT',
+ 'if-none-match': '"old"'
+ }
+ })
+ );
+ expect(stale_etag_wins.status).toBe(200);
+});
+
test('static routes answer HEAD with the same handler', async () => {
const { routes } = await load_routes();
@@ -113,6 +139,7 @@ test('precompressed variants are negotiated with their own validators', async ()
const route = routes.client_asset('app.js', undefined, {
hash: 'abc',
+ mtime: 0,
br: true,
gz: true
})[0][1] as any;
@@ -147,7 +174,11 @@ test('precompressed variants are negotiated with their own validators', async ()
test('range requests are served from the identity representation', async () => {
const { routes, file } = await load_routes();
- const route = routes.client_asset('app.js', undefined, { hash: 'abc', br: true })[0][1] as any;
+ const route = routes.client_asset('app.js', undefined, {
+ hash: 'abc',
+ mtime: 0,
+ br: true
+ })[0][1] as any;
const response = route.GET(
new Request('http://localhost/app.js', {
headers: { 'accept-encoding': 'br', range: 'bytes=0-9' }
From e7bf26f5b16a8b9e619b8049bdbb9aaffe24261e Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 18:26:12 -0400
Subject: [PATCH 72/94] fix: answer HEAD requests on trailing-slash redirects
Bun does not route HEAD to a GET handler, so the function-route redirects
missed HEAD and fell through to SSR. Static Response routes handle HEAD
natively and are unaffected.
---
packages/adapter-bun/src/routes-util.js | 16 ++++++++++++----
packages/adapter-bun/test/routes.spec.ts | 1 +
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index b20933ab7c15..a428f598f727 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -110,10 +110,18 @@ function negotiate(accept, meta) {
return null;
}
+/**
+ * Bun does not route HEAD requests to a GET handler, so every function route
+ * registers both methods.
+ * @param {(request: import('bun').BunRequest) => Response} handler
+ * @returns {RouteHandler}
+ */
+function handlers(handler) {
+ return { GET: handler, HEAD: handler };
+}
+
/**
* Serves one file with its build-time validator and precompressed variants.
- * Registered for GET and HEAD because Bun does not route HEAD requests to a
- * GET handler.
* @param {string} path
* @param {Record} headers
* @param {AssetMeta} meta
@@ -146,7 +154,7 @@ function file_route(path, headers, meta) {
return new Response(Bun.file(`${path}.${encoding}`), { headers: response_headers });
};
- return { GET: handler, HEAD: handler };
+ return handlers(handler);
}
/**
@@ -235,7 +243,7 @@ export function prerendered_page(urlPath, filePath, meta) {
if (inverted) {
for (const path of route_paths(inverted)) {
- entries.push([path, { GET: handle_redirect }]);
+ entries.push([path, handlers(handle_redirect)]);
}
}
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index f0e4f03b6ae3..e72ce927ab0e 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -234,6 +234,7 @@ test.each([
expect(entries[0][0]).toBe(canonical);
expect(entries[1][0]).toBe(alternate);
+ expect((entries[1][1] as any).HEAD).toBe((entries[1][1] as any).GET);
const response = (entries[1][1] as any).GET(
new Request(`http://localhost${alternate}?from=test`)
);
From b08c74ff736b58e67e47b4dae48a44ab029d5f45 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 18:26:39 -0400
Subject: [PATCH 73/94] chore: deduplicate route generation
The embed and no-embed generators repeated the redirect emission, the
prerendered-pages set, and the server-asset entry zip (which coupled the
two functions through array indexes); recent fixes had to land twice
because of it. Redirects now emit once in create_routes, server-asset
entries are self-contained strings, each generator awaits one combined
barrier instead of one per group, and the lookup scans became Maps.
client_asset collects its alias paths before mapping them to the shared
route, and file_route owns the content-type header. precompress combined
with compile now warns instead of being silently dropped.
---
packages/adapter-bun/index.js | 157 ++++++++++------------
packages/adapter-bun/src/routes-util.js | 51 +++----
packages/adapter-bun/test/adapter.spec.ts | 11 ++
3 files changed, 103 insertions(+), 116 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index f2851c64b1ac..f7f395a63c72 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -38,7 +38,7 @@ function is_dotfile(path) {
* in-memory static routes, not file-backed responses, so the adapter ships its own.
* @param {string} path
* @param {boolean} [precompress]
- * @returns {Promise}
+ * @returns {Promise<{ hash: string, mtime: number, br?: boolean, gz?: boolean }>}
*/
async function asset_meta(path, precompress = false) {
const file = Bun.file(path);
@@ -47,11 +47,15 @@ async function asset_meta(path, precompress = false) {
/** @type {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} */
const meta = { hash, mtime: file.lastModified };
if (precompress) {
- if (await Bun.file(`${path}.br`).exists()) meta.br = true;
- if (await Bun.file(`${path}.gz`).exists()) meta.gz = true;
+ const [br, gz] = await Promise.all([
+ Bun.file(`${path}.br`).exists(),
+ Bun.file(`${path}.gz`).exists()
+ ]);
+ if (br) meta.br = true;
+ if (gz) meta.gz = true;
}
- return JSON.stringify(meta);
+ return meta;
}
/** @param {string[]} files */
@@ -87,6 +91,12 @@ export default function (opts = {}) {
builder.log.minor('Building server');
+ if (precompress && buildOptions.compile) {
+ builder.log.warn(
+ 'precompress is ignored with buildOptions.compile: embedded assets are imported by identity path'
+ );
+ }
+
const server = builder.getServerDirectory();
const src_dir = resolve(import.meta.dirname, 'src');
@@ -102,12 +112,11 @@ export default function (opts = {}) {
`export const embed = ${JSON.stringify(!!buildOptions.compile)};\n` +
`export const env_prefix = ${JSON.stringify(envPrefix)};\n` +
`export const origin = ${JSON.stringify(builder.config.kit.paths.origin) || 'undefined'};`,
- [server_options_file]: [`export default ${JSON.stringify(serverOptions)};`].join('\n'),
+ [server_options_file]: `export default ${JSON.stringify(serverOptions)};`,
[routes_file]: await create_routes({
builder,
out,
embed: !!buildOptions.compile,
- // embedded assets are imported by identity path, so variants cannot ride along
precompress: precompress && !buildOptions.compile
})
};
@@ -203,64 +212,43 @@ async function get_embed_entries({ builder, server_assets }) {
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
validate_file_paths(assets.map(({ rel }) => rel));
+ const asset_index = new Map(assets.map(({ rel }, i) => [rel, i]));
const imports = assets.map(({ abs }, i) => {
return `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`;
});
- let offset = 0;
- const cl_entries = await Promise.all(
- cl_files.map(async ({ abs, rel }, i) => {
- return `...client_asset(${JSON.stringify(rel)}, asset_${offset + i}, ${await asset_meta(abs)})`;
- })
- );
-
- offset += cl_files.length;
- const prerendered_pages_files = new Set(
- [...builder.prerendered.pages].map(([_, { file }]) => file)
- );
- const pr_pages_entries = await Promise.all(
- [...builder.prerendered.pages].map(async ([path, { file }]) => {
- const fileIdx = pr_pages.findIndex((f) => f.rel === file);
- if (fileIdx === -1)
+ /**
+ * @param {{ abs: string, rel: string }} file
+ * @param {string} helper
+ * @param {string} [urlPath]
+ */
+ const entry = async ({ abs, rel }, helper, urlPath = rel) =>
+ `...${helper}(${JSON.stringify(urlPath)}, asset_${asset_index.get(rel)}, ${JSON.stringify(await asset_meta(abs))})`;
+
+ const page_files = new Map(pr_pages.map((file) => [file.rel, file]));
+ const page_rels = new Set([...builder.prerendered.pages].map(([_, { file }]) => file));
+
+ const entries = await Promise.all([
+ ...cl_files.map((file) => entry(file, 'client_asset')),
+ ...[...builder.prerendered.pages].map(([path, { file }]) => {
+ const page = page_files.get(file);
+ if (page === undefined)
throw new Error(`Could not find prerendered page ${file} for route ${path}`);
- return `...prerendered_page(${JSON.stringify(path)}, asset_${offset + fileIdx}, ${await asset_meta(pr_pages[fileIdx].abs)})`;
- })
- );
- const pr_page_assets_entries = await Promise.all(
- pr_pages.flatMap(({ abs, rel }, i) => {
- return prerendered_pages_files.has(rel)
- ? []
- : [
- (async () =>
- `...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i}, ${await asset_meta(abs)})`)()
- ];
- })
- );
-
- offset += pr_pages.length;
- const pr_assets_entries = await Promise.all(
- [...pr_deps, ...pr_data].map(async ({ abs, rel }, i) => {
- return `...prerendered_asset(${JSON.stringify(rel)}, asset_${offset + i}, ${await asset_meta(abs)})`;
- })
- );
-
- const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
- return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
- });
+ return entry(page, 'prerendered_page', path);
+ }),
+ ...pr_pages
+ .filter(({ rel }) => !page_rels.has(rel))
+ .map((file) => entry(file, 'prerendered_asset')),
+ ...[...pr_deps, ...pr_data].map((file) => entry(file, 'prerendered_asset'))
+ ]);
return {
imports,
- entries: [
- ...cl_entries,
- ...pr_pages_entries,
- ...pr_page_assets_entries,
- ...pr_assets_entries,
- ...pr_redirects
- ],
+ entries,
server_assets: server_assets.map((file) => {
- const idx = assets.findIndex((f) => f.rel === file);
- if (idx === -1) throw new Error(`Could not find server asset ${file}`);
- return `server_asset(${JSON.stringify(file)}, asset_${idx})`;
+ const idx = asset_index.get(file);
+ if (idx === undefined) throw new Error(`Could not find server asset ${file}`);
+ return `[${JSON.stringify(file)}, server_asset(${JSON.stringify(file)}, asset_${idx})]`;
})
};
}
@@ -282,38 +270,31 @@ async function get_no_embed_entries({ builder, server_assets, out, precompress }
await Promise.all([builder.compress(`${out}/client`), builder.compress(`${out}/prerendered`)]);
}
- const cl_entries = await Promise.all(
- client_files.map(async (filePath) => {
- return `...client_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/client/${filePath}`, precompress)})`;
- })
- );
-
- const prerendered_pages = [...builder.prerendered.pages];
- const prerendered_pages_files = new Set(prerendered_pages.map(([_, { file }]) => file));
-
- const pr_pages_entries = await Promise.all(
- prerendered_pages.map(async ([path, { file }]) => {
- return `...prerendered_page(${JSON.stringify(path)}, ${JSON.stringify(file)}, ${await asset_meta(`${out}/prerendered/${file}`, precompress)})`;
- })
- );
-
- const pr_assets_entries = await Promise.all(
- prerendered_files
- .filter((filePath) => !prerendered_pages_files.has(filePath))
- .map(async (filePath) => {
- return `...prerendered_asset(${JSON.stringify(filePath)}, undefined, ${await asset_meta(`${out}/prerendered/${filePath}`, precompress)})`;
- })
- );
-
- const pr_redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
- return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
- });
+ /**
+ * @param {string} helper
+ * @param {string} urlPath
+ * @param {string} dir
+ * @param {string} [filePath]
+ */
+ const entry = async (helper, urlPath, dir, filePath) =>
+ `...${helper}(${JSON.stringify(urlPath)}, ${JSON.stringify(filePath)}, ${JSON.stringify(await asset_meta(`${out}/${dir}/${filePath ?? urlPath}`, precompress))})`;
+
+ const pages = [...builder.prerendered.pages];
+ const page_files = new Set(pages.map(([_, { file }]) => file));
+
+ const entries = await Promise.all([
+ ...client_files.map((file) => entry('client_asset', file, 'client')),
+ ...pages.map(([path, { file }]) => entry('prerendered_page', path, 'prerendered', file)),
+ ...prerendered_files
+ .filter((file) => !page_files.has(file))
+ .map((file) => entry('prerendered_asset', file, 'prerendered'))
+ ]);
return {
imports: [],
- entries: [...cl_entries, ...pr_pages_entries, ...pr_assets_entries, ...pr_redirects],
+ entries,
server_assets: server_assets.map((file) => {
- return `server_asset(${JSON.stringify(file)})`;
+ return `[${JSON.stringify(file)}, server_asset(${JSON.stringify(file)})]`;
})
};
}
@@ -344,13 +325,15 @@ async function create_routes({ builder, out, embed, precompress }) {
? await get_embed_entries({ builder, server_assets })
: await get_no_embed_entries({ builder, out, server_assets, precompress });
+ const redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => {
+ return `...prerendered_redirect(${JSON.stringify(src)}, ${status}, ${JSON.stringify(location)})`;
+ });
+
return [
`import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.js';`,
...imports,
- `export const routes = Object.fromEntries([${entries.join(',\n')}]);`,
- `export const server_assets = new Map([${resolved_server_assets
- .map((file, i) => `[${JSON.stringify(server_assets[i])}, ${file}]`)
- .join(',\n')}]);`
+ `export const routes = Object.fromEntries([${[...entries, ...redirects].join(',\n')}]);`,
+ `export const server_assets = new Map([${resolved_server_assets.join(',\n')}]);`
].join('\n');
}
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index a428f598f727..0dcd597ef0a6 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -123,11 +123,12 @@ function handlers(handler) {
/**
* Serves one file with its build-time validator and precompressed variants.
* @param {string} path
- * @param {Record} headers
* @param {AssetMeta} meta
+ * @param {Record} [extra_headers]
* @returns {RouteHandler}
*/
-function file_route(path, headers, meta) {
+function file_route(path, meta, extra_headers = {}) {
+ const content_type = Bun.file(path).type;
const last_modified = new Date(meta.mtime).toUTCString();
/** @param {import('bun').BunRequest} request */
@@ -140,7 +141,12 @@ function file_route(path, headers, meta) {
const etag = encoding === null ? `"${meta.hash}"` : `"${meta.hash}-${encoding}"`;
/** @type {Record} */
- const response_headers = { ...headers, etag, 'last-modified': last_modified };
+ const response_headers = {
+ 'content-type': content_type,
+ ...extra_headers,
+ etag,
+ 'last-modified': last_modified
+ };
if (meta.br || meta.gz) response_headers['vary'] = 'accept-encoding';
if (is_fresh(request, etag, meta.mtime)) {
@@ -164,33 +170,22 @@ function file_route(path, headers, meta) {
* @returns {Array<[string, RouteHandler]>}
*/
export function client_asset(urlPath, filePath = urlPath, meta) {
- const path = embed ? filePath : resolve(dir, 'client', filePath);
-
- /** @type {Record} */
- const headers = { 'content-type': Bun.file(path).type };
-
- if (urlPath.startsWith(`${manifest.appDir}/immutable/`)) {
- headers['cache-control'] = 'public,max-age=31536000,immutable';
- }
-
- const route = file_route(path, headers, meta);
-
- /** @type {Array<[string, RouteHandler]>} */
- const entries = to_paths(urlPath).map((path) => [path, route]);
-
+ const immutable = urlPath.startsWith(`${manifest.appDir}/immutable/`);
+ const route = file_route(
+ embed ? filePath : resolve(dir, 'client', filePath),
+ meta,
+ immutable ? { 'cache-control': 'public,max-age=31536000,immutable' } : {}
+ );
+
+ const paths = to_paths(urlPath);
if (urlPath.endsWith('/index.html') || urlPath === 'index.html') {
- const directory = urlPath.slice(0, -'index.html'.length);
- for (const path of to_directory_paths(directory)) {
- entries.push([path, route]);
- }
+ paths.push(...to_directory_paths(urlPath.slice(0, -'index.html'.length)));
} else if (urlPath.endsWith('.html')) {
// sirv also serves `page.html` at `/page`
- for (const path of to_paths(urlPath.slice(0, -'.html'.length))) {
- entries.push([path, route]);
- }
+ paths.push(...to_paths(urlPath.slice(0, -'.html'.length)));
}
- return entries;
+ return paths.map((path) => /** @type {[string, RouteHandler]} */ ([path, route]));
}
/**
@@ -209,8 +204,7 @@ export function server_asset(urlPath, filePath = urlPath) {
* @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_asset(urlPath, filePath = urlPath, meta) {
- const path = embed ? filePath : resolve(dir, 'prerendered', filePath);
- const route = file_route(path, { 'content-type': Bun.file(path).type }, meta);
+ const route = file_route(embed ? filePath : resolve(dir, 'prerendered', filePath), meta);
return to_paths(urlPath).map((path) => [path, route]);
}
@@ -233,8 +227,7 @@ export function prerendered_page(urlPath, filePath, meta) {
return new Response(null, { status: 308, headers: { location } });
}
- const path = embed ? filePath : resolve(dir, 'prerendered', filePath);
- const route = file_route(path, { 'content-type': Bun.file(path).type }, meta);
+ const route = file_route(embed ? filePath : resolve(dir, 'prerendered', filePath), meta);
const inverted = urlPath.endsWith('/') ? urlPath.slice(0, -1) : `${urlPath}/`;
// path already contains base, no need to add it here
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 29e6c7bd7a5b..1e3db8fc3157 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -304,6 +304,17 @@ describe('generated routes', () => {
bun.file.mockImplementation(previous!);
});
+ test('warns when precompress is combined with compile', async () => {
+ const builder = create_builder();
+
+ await adapter({ precompress: true, buildOptions: { compile: true } }).adapt(builder);
+
+ expect(builder.log.warn).toHaveBeenCalledWith(
+ expect.stringContaining('precompress is ignored')
+ );
+ expect(builder.compress).not.toHaveBeenCalled();
+ });
+
test('does not compress by default', async () => {
const builder = create_builder({ client_files: ['app.js'] });
From 6fcf8dfcf206ef95801f01cfae77bdbd67bf7def Mon Sep 17 00:00:00 2001
From: Nic Polumeyv
Date: Mon, 10 Aug 2026 18:32:49 -0400
Subject: [PATCH 74/94] Update
packages/adapter-bun/test/apps/basic/tsconfig.json
Co-authored-by: Tee Ming
---
packages/adapter-bun/test/apps/basic/tsconfig.json | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/packages/adapter-bun/test/apps/basic/tsconfig.json b/packages/adapter-bun/test/apps/basic/tsconfig.json
index 030becfe8111..5c34318c2147 100644
--- a/packages/adapter-bun/test/apps/basic/tsconfig.json
+++ b/packages/adapter-bun/test/apps/basic/tsconfig.json
@@ -1,15 +1 @@
-{
- "extends": "$app/tsconfig",
- "include": ["src"],
- "compilerOptions": {
- "allowJs": true,
- "checkJs": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "sourceMap": true,
- "strict": true,
- "moduleResolution": "bundler"
- }
}
From 73346302c79ce4c9803714bf3b86e757f5022c1c Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 18:48:17 -0400
Subject: [PATCH 75/94] fix: restore the test app tsconfig
The committed suggestion was authored against pre-rebase line numbers and
left only a closing brace.
---
packages/adapter-bun/test/apps/basic/tsconfig.json | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/adapter-bun/test/apps/basic/tsconfig.json b/packages/adapter-bun/test/apps/basic/tsconfig.json
index 5c34318c2147..00ef9b61a37b 100644
--- a/packages/adapter-bun/test/apps/basic/tsconfig.json
+++ b/packages/adapter-bun/test/apps/basic/tsconfig.json
@@ -1 +1,4 @@
+{
+ "extends": "$app/tsconfig",
+ "include": ["src"]
}
From cd6c8d21267c04fd619c233c55c539351b54fedb Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:05:44 -0400
Subject: [PATCH 76/94] test: cover the root base, conditional-request
wildcards, and readdir errors
---
packages/adapter-bun/test/adapter.spec.ts | 11 +++++++++++
packages/adapter-bun/test/handler.spec.ts | 4 +++-
packages/adapter-bun/test/routes.spec.ts | 22 +++++++++++++++++++++-
3 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 1e3db8fc3157..7658e4527f2d 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -338,6 +338,17 @@ describe('generated routes', () => {
expect(source).toContain('...client_asset("ok.txt", undefined, {"hash":"abc","mtime":0})');
});
+ test('embedded builds tolerate absent output directories but propagate other errors', async () => {
+ vi.mocked(readdir).mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }));
+ await adapter({ buildOptions: { compile: true } }).adapt(create_builder());
+ expect(bun.build).toHaveBeenCalledOnce();
+
+ vi.mocked(readdir).mockRejectedValue(Object.assign(new Error('denied'), { code: 'EACCES' }));
+ await expect(
+ adapter({ buildOptions: { compile: true } }).adapt(create_builder())
+ ).rejects.toThrow('denied');
+ });
+
test('excludes dotfiles from embedded assets', async () => {
mock_files({ client: ['.secret', 'public.txt'] });
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
index e2a2dc868aec..69c077289a13 100644
--- a/packages/adapter-bun/test/handler.spec.ts
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -162,7 +162,9 @@ test('reports when Bun cannot determine the peer address', async () => {
test('disables timeouts and proxy buffering for event streams', async () => {
const loaded = await load_handler({
- response: new Response('data: ready\n\n', { headers: { 'content-type': 'text/event-stream' } })
+ response: new Response('data: ready\n\n', {
+ headers: { 'content-type': 'text/event-stream; charset=utf-8' }
+ })
});
const request = new Request('http://localhost/events');
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index e72ce927ab0e..3d58730496ff 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -62,6 +62,16 @@ test('segments starting with a colon are escaped to avoid Bun route parameters',
]);
});
+test('a root deployment registers routes with a leading slash', async () => {
+ const { routes } = await load_routes();
+
+ expect(routes.client_asset('data.json', undefined, meta)[0][0]).toBe('/data.json');
+ expect(routes.client_asset('index.html', undefined, meta).map(([path]) => path)).toEqual([
+ '/index.html',
+ '/'
+ ]);
+});
+
test('immutable SvelteKit assets receive a long-lived cache policy', async () => {
const { routes } = await load_routes({ appDir: '_app' });
@@ -99,6 +109,11 @@ test('static routes revalidate against the build-time hash', async () => {
new Request('http://localhost/data.json', { headers: { 'if-none-match': '"old"' } })
);
expect(stale.status).toBe(200);
+
+ const wildcard = route.GET(
+ new Request('http://localhost/data.json', { headers: { 'if-none-match': '*' } })
+ );
+ expect(wildcard.status).toBe(304);
});
test('static routes revalidate by date when the client has no ETag', async () => {
@@ -159,6 +174,11 @@ test('precompressed variants are negotiated with their own validators', async ()
expect(gzip.headers.get('etag')).toBe('"abc-gz"');
expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js.gz');
+ const any = route.GET(
+ new Request('http://localhost/app.js', { headers: { 'accept-encoding': '*' } })
+ );
+ expect(any.headers.get('content-encoding')).toBe('br');
+
const identity = route.GET(new Request('http://localhost/app.js'));
expect(identity.headers.has('content-encoding')).toBe(false);
expect(identity.headers.get('etag')).toBe('"abc"');
@@ -267,7 +287,7 @@ test('prerendered redirects retain their status and location', async () => {
expect((handler as any).GET.headers.get('location')).toBe('/new');
});
-async function load_routes({ base = '', embed = false, appDir = '_app' } = {}) {
+async function load_routes({ base = '/', embed = false, appDir = '_app' } = {}) {
vi.resetModules();
vi.doMock('MANIFEST', () => ({ manifest: { appDir }, base, embed }));
const file = vi.fn((path: string) => ({ path, type: 'text/plain;charset=utf-8' }));
From dbef236e73babc48135ae66308a4dc7d33b0c888 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:05:44 -0400
Subject: [PATCH 77/94] test: collapse the boolean spellings into one pass
---
packages/adapter-bun/test/env.spec.ts | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/packages/adapter-bun/test/env.spec.ts b/packages/adapter-bun/test/env.spec.ts
index c04abea359f6..02ee3dc93206 100644
--- a/packages/adapter-bun/test/env.spec.ts
+++ b/packages/adapter-bun/test/env.spec.ts
@@ -36,20 +36,18 @@ describe('env', () => {
});
describe('boolean_env', () => {
- test.each(['1', 'true', 'TRUE', 'yes', 'YES', 'on', 'ON'])('parses %s as true', async (value) => {
- set_env('OPTION', value);
+ test('parses the accepted truthy and falsy spellings', async () => {
const { boolean_env } = await load_env();
- expect(boolean_env('OPTION')).toBe(true);
- });
- test.each(['0', 'false', 'FALSE', 'no', 'NO', 'off', 'OFF'])(
- 'parses %s as false',
- async (value) => {
+ for (const value of ['1', 'true', 'TRUE', 'yes', 'YES', 'on', 'ON']) {
set_env('OPTION', value);
- const { boolean_env } = await load_env();
- expect(boolean_env('OPTION')).toBe(false);
+ expect(boolean_env('OPTION'), value).toBe(true);
}
- );
+ for (const value of ['0', 'false', 'FALSE', 'no', 'NO', 'off', 'OFF']) {
+ set_env('OPTION', value);
+ expect(boolean_env('OPTION'), value).toBe(false);
+ }
+ });
test('returns the fallback for an absent variable', async () => {
const { boolean_env } = await load_env();
From b72a83a7b4e8bfd884f6ed020c93e3df46e46221 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:11:23 -0400
Subject: [PATCH 78/94] fix: answer HEAD requests on prerendered redirects
---
packages/adapter-bun/src/routes-util.js | 6 ++----
packages/adapter-bun/test/routes.spec.ts | 1 +
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 0dcd597ef0a6..b8ab9429f20b 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -250,9 +250,7 @@ export function prerendered_page(urlPath, filePath, meta) {
* @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_redirect(urlPath, status, location) {
+ const response = new Response(null, { status, headers: { location } });
// path already contains base, no need to add it here
- return route_paths(urlPath).map((path) => [
- path,
- { GET: new Response(null, { status, headers: { location } }) }
- ]);
+ return route_paths(urlPath).map((path) => [path, { GET: response, HEAD: response }]);
}
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index 3d58730496ff..d1b30fcbeaf1 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -285,6 +285,7 @@ test('prerendered redirects retain their status and location', async () => {
expect(path).toBe('/old%20path');
expect((handler as any).GET.status).toBe(307);
expect((handler as any).GET.headers.get('location')).toBe('/new');
+ expect((handler as any).HEAD).toBe((handler as any).GET);
});
async function load_routes({ base = '/', embed = false, appDir = '_app' } = {}) {
From e1203a3857ca95d19a8bea79042f95b128c3e635 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv
Date: Mon, 10 Aug 2026 19:41:24 -0400
Subject: [PATCH 79/94] Update
documentation/docs/25-build-and-deploy/45-adapter-bun.md
Co-authored-by: Rich Harris
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 496a3f7b7cb6..de1ac6fe4201 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -2,7 +2,7 @@
title: Bun servers
---
-[`adapter-bun`](https://github.com/sveltejs/kit/tree/main/packages/adapter-bun) builds a SvelteKit application into a standalone [Bun](https://bun.com/) server. The generated server uses `Bun.serve` for requests and Bun file responses for client assets, prerendered output, and files read with [`$app/server`](https://svelte.dev/docs/kit/$app-server#read).
+[`adapter-bun`](https://github.com/sveltejs/kit/tree/main/packages/adapter-bun) builds a SvelteKit application into a standalone [Bun](https://bun.com/) server. The generated server uses `Bun.serve` for requests and `Bun.file` responses for client assets, prerendered output, and files read with [`read`](https://svelte.dev/docs/kit/$app-server#read) from `$app/server`.
## Usage
From 40e727652aabc444afc5161f6189e42e59d35db4 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 20:09:44 -0400
Subject: [PATCH 80/94] fix: address review findings across route keys,
generation, and shutdown
Widen the raw character set with [ ] ^ | (WHATWG parsing sends them
unencoded, so both registered variants missed real browser requests).
Reject route segments starting with ':' at build time instead of
registering an unreachable percent-encoded key. Reverse the generated
route table so the first entry for a path wins, restoring sirv's lookup
order for extensionless and directory aliases. Key the embed asset index
by identity so client and prerendered files sharing a relative path keep
distinct imports. Stream asset hashing with a bounded number of open
files so build memory no longer scales with total asset size. Reject a
present-but-empty Host header again. Wait briefly for the force-close
before emitting sveltekit:shutdown so cleanup listeners do not tear down
resources in-flight handlers still hold. Reuse handlers() for redirect
routes and correct the precompress docs to say brotli is preferred over
gzip rather than promising the smallest variant.
---
.../25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/index.d.ts | 6 +-
packages/adapter-bun/index.js | 70 ++++++++++++++----
packages/adapter-bun/src/handler.js | 9 ++-
packages/adapter-bun/src/index.js | 15 +++-
packages/adapter-bun/src/routes-util.js | Bin 7819 -> 7609 bytes
packages/adapter-bun/test/adapter.spec.ts | 32 +++++++-
packages/adapter-bun/test/handler.spec.ts | 13 ++++
packages/adapter-bun/test/routes.spec.ts | 7 +-
packages/adapter-bun/test/start.spec.ts | 6 +-
10 files changed, 130 insertions(+), 30 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index de1ac6fe4201..99020cde0869 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -81,7 +81,7 @@ The output directory. It defaults to `build`.
### precompress
-Set `precompress: true` to generate `.br` and `.gz` variants of client and prerendered assets during the build. The generated routes negotiate `Accept-Encoding` per request and serve the smallest accepted variant with its own ETag. The option is ignored when `buildOptions.compile` is set, because embedded assets are imported by identity path.
+Set `precompress: true` to generate `.br` and `.gz` variants of client and prerendered assets during the build. The generated routes negotiate `Accept-Encoding` per request, preferring brotli over gzip, and each variant carries its own ETag. The option is ignored when `buildOptions.compile` is set, because embedded assets are imported by identity path.
### envPrefix
diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts
index c07734d1c952..47898a6355fc 100644
--- a/packages/adapter-bun/index.d.ts
+++ b/packages/adapter-bun/index.d.ts
@@ -9,9 +9,9 @@ interface AdapterOptions {
out?: string;
/**
* Generate `.br` and `.gz` variants of client and prerendered assets during the
- * build. The generated routes negotiate `Accept-Encoding` per request and serve
- * the smallest accepted variant. Ignored when `buildOptions.compile` is set,
- * because embedded assets are imported by identity path.
+ * build. The generated routes negotiate `Accept-Encoding` per request, preferring
+ * brotli over gzip. Ignored when `buildOptions.compile` is set, because embedded
+ * assets are imported by identity path.
* @default false
*/
precompress?: boolean;
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index f7f395a63c72..6ba863fa0843 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -33,6 +33,37 @@ function is_dotfile(path) {
.some((segment, i) => segment.startsWith('.') && !(i === 0 && segment === '.well-known'));
}
+// bounds open file handles while every asset hashes concurrently
+const MAX_OPEN_FILES = 64;
+let open_files = 0;
+/** @type {Array<() => void>} */
+const file_waiters = [];
+
+/**
+ * Streams the file through the hasher so build memory stays bounded by chunk
+ * size instead of total asset size.
+ * @param {string} path
+ * @returns {Promise}
+ */
+async function hash_file(path) {
+ if (open_files === MAX_OPEN_FILES) {
+ await new Promise((resolve) => {
+ file_waiters.push(() => resolve(undefined));
+ });
+ }
+ open_files++;
+ try {
+ const hasher = new Bun.CryptoHasher('blake2b256');
+ for await (const chunk of Bun.file(path).stream()) {
+ hasher.update(chunk);
+ }
+ return hasher.digest('hex').slice(0, 16);
+ } finally {
+ open_files--;
+ file_waiters.shift()?.();
+ }
+}
+
/**
* The build-time validator for conditional requests: Bun only generates ETags for
* in-memory static routes, not file-backed responses, so the adapter ships its own.
@@ -41,11 +72,10 @@ function is_dotfile(path) {
* @returns {Promise<{ hash: string, mtime: number, br?: boolean, gz?: boolean }>}
*/
async function asset_meta(path, precompress = false) {
- const file = Bun.file(path);
- const hash = Bun.hash(await file.arrayBuffer()).toString(16);
+ const hash = await hash_file(path);
/** @type {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} */
- const meta = { hash, mtime: file.lastModified };
+ const meta = { hash, mtime: Bun.file(path).lastModified };
if (precompress) {
const [br, gz] = await Promise.all([
Bun.file(`${path}.br`).exists(),
@@ -60,11 +90,18 @@ async function asset_meta(path, precompress = false) {
/** @param {string[]} files */
function validate_file_paths(files) {
- const invalid = files.find((file) => file.includes('*'));
- if (invalid !== undefined) {
- throw new Error(
- `Cannot build with ${JSON.stringify(invalid)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file or route to remove the \`*\` character.`
- );
+ for (const file of files) {
+ if (file.includes('*')) {
+ throw new Error(
+ `Cannot build with ${JSON.stringify(file)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file or route to remove the \`*\` character.`
+ );
+ }
+ // a leading ':' would need percent-encoding, but browsers request the colon raw
+ if (file.split('/').some((segment) => segment.startsWith(':'))) {
+ throw new Error(
+ `Cannot build with ${JSON.stringify(file)} because Bun treats a route segment starting with \`:\` as a parameter. Rename the file or route so no segment starts with \`:\`.`
+ );
+ }
}
}
@@ -212,7 +249,8 @@ async function get_embed_entries({ builder, server_assets }) {
const assets = [...cl_files, ...pr_pages, ...pr_deps, ...pr_data];
validate_file_paths(assets.map(({ rel }) => rel));
- const asset_index = new Map(assets.map(({ rel }, i) => [rel, i]));
+ // keyed by identity: client and prerendered trees can contain the same relative path
+ const asset_index = new Map(assets.map((file, i) => [file, i]));
const imports = assets.map(({ abs }, i) => {
return `import asset_${i} from ${JSON.stringify(abs)} with { type: 'file' };`;
});
@@ -222,8 +260,8 @@ async function get_embed_entries({ builder, server_assets }) {
* @param {string} helper
* @param {string} [urlPath]
*/
- const entry = async ({ abs, rel }, helper, urlPath = rel) =>
- `...${helper}(${JSON.stringify(urlPath)}, asset_${asset_index.get(rel)}, ${JSON.stringify(await asset_meta(abs))})`;
+ const entry = async (file, helper, urlPath = file.rel) =>
+ `...${helper}(${JSON.stringify(urlPath)}, asset_${asset_index.get(file)}, ${JSON.stringify(await asset_meta(file.abs))})`;
const page_files = new Map(pr_pages.map((file) => [file.rel, file]));
const page_rels = new Set([...builder.prerendered.pages].map(([_, { file }]) => file));
@@ -242,11 +280,15 @@ async function get_embed_entries({ builder, server_assets }) {
...[...pr_deps, ...pr_data].map((file) => entry(file, 'prerendered_asset'))
]);
+ const index_by_rel = new Map(
+ assets.map(({ rel }, i) => /** @type {[string, number]} */ ([rel, i])).reverse()
+ );
+
return {
imports,
entries,
server_assets: server_assets.map((file) => {
- const idx = asset_index.get(file);
+ const idx = index_by_rel.get(file);
if (idx === undefined) throw new Error(`Could not find server asset ${file}`);
return `[${JSON.stringify(file)}, server_asset(${JSON.stringify(file)}, asset_${idx})]`;
})
@@ -332,7 +374,9 @@ async function create_routes({ builder, out, embed, precompress }) {
return [
`import { client_asset, prerendered_asset, prerendered_page, prerendered_redirect, server_asset } from './routes-util.js';`,
...imports,
- `export const routes = Object.fromEntries([${[...entries, ...redirects].join(',\n')}]);`,
+ // reversed because Object.fromEntries keeps the last duplicate: the first generated
+ // entry for a path must win so exact files beat aliases, like sirv's lookup order
+ `export const routes = Object.fromEntries([${[...entries, ...redirects].join(',\n')}].reverse());`,
`export const server_assets = new Map([${resolved_server_assets.join(',\n')}]);`
].join('\n');
}
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 94f2f1234d70..41e68c5abbc0 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -23,10 +23,12 @@ await server.init({
* @returns {Promise}
*/
export async function handler(request, bun_server) {
- const url = new URL(request.url);
-
let request_origin = origin;
+ /** @type {URL} */
+ let url;
try {
+ // an empty Host header makes request.url relative, so parsing belongs in the try
+ url = new URL(request.url);
request_origin ||= get_origin(request, url);
} catch (error) {
console.error(
@@ -72,8 +74,9 @@ function get_origin(request, url) {
);
}
+ // an empty forwarded header falls through, but a present-and-empty Host is rejected below
const host =
- (host_header && request.headers.get(host_header)) || request.headers.get('host') || url.host;
+ (host_header && request.headers.get(host_header)) || (request.headers.get('host') ?? url.host);
if (!host) {
const header_names = host_header ? `${host_header} or host headers` : 'host header';
throw new Error(
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index d559098f32c8..735c9bd89d71 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -65,7 +65,20 @@ async function graceful_shutdown(reason) {
]);
clearTimeout(deadline);
- if (!drained) void server.stop(true);
+ if (!drained) {
+ // give the force-close a moment to abort in-flight handlers, so shutdown
+ // listeners do not tear down resources those handlers still hold; the stop
+ // promise may never settle, so the timer bounds the wait
+ /** @type {ReturnType | undefined} */
+ let grace;
+ await Promise.race([
+ server.stop(true),
+ new Promise((resolve) => {
+ grace = setTimeout(resolve, 1000);
+ })
+ ]);
+ clearTimeout(grace);
+ }
// @ts-expect-error custom events cannot be typed
process.emit('sveltekit:shutdown', reason);
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index b8ab9429f20bfbfe62c129489c52c6ef588d1ef2..5e481729049a7178f642ba551311d7e6b91e1ee1 100644
GIT binary patch
delta 401
zcmZXQu};EJ6oy3>Qzc;)qn>DpKnh5V3kZrxWN;vWVX%Z=E(cnzExGqrEl79(2Zz20
z36qnHui#_&3dUQElV?5Of6kZxrTA8uo1vL75Uv|n&=kb(6V^ZAFe1W$5i*LlNTW@i
zJ_1Lf!pL$t=y4q+3c98PcR=Pn;DUHynF1NH6zbSqK($#Z*Q*!Zdbw5WR%+!26d`-F
zlHQ%AHFuIb9LSl%-faJ*bgECLaZT%Fhi*YpvZ<5;DQMTqt@h=w3rA)W%N!G7I(9yI
z3HU^)fSxCigDdwyhSFBLy8;}0i9DJNRp=ol5(rM7yEfbtK@FKL`=
delta 600
zcmY*W!EVz)5Eb;&SV2O)AynxhN^FP3u2ewOmH{`E=2%6QkSHNqoN2r?Uc0+%DyTv}
z08aQvkb2@n_#f6z5bE*GyqS6L&F}Dg^o!qGj@}HgxBvVZiX>$c85gBOK{6qs`=k<9
zkP1-uSfWWPl!_$S%(N90i76zz1Q%c^m`<%E7fNFkznvV%C&S5L7>^La|8N@j-?9GJ
z-TwajzBl>yV5eg{?&IDvjDlC=9Y3@zzuUFY#qqz}BFU$ukc;^jT&7IlxU5sk6wN;y
z0yRWKApuDjB`M9t42IDt>eWj>Qo@XI9FzhX(@ARPWL|2jrni4CtyarZ#hh!$_HDOD
z87mw|(JV7IT|_TTt##^o-nuld@tRdf(*v(rxN7Pxeg>yWGm0YEfsIZF95vW=jYM5a
z0sGNjY=`iD92^-@jToa>vW!g3Mx;C^xmvfLxRI%+jm_qWiLE^
z3P0ssICkTy@wLLfef?zP&t|pN-r77XMWT5ws)uc_8nwOaAMJ~~7OBGgk<90+JKImI
d!S=a*g`>lhz{p> ({
build: vi.fn(async (_options: any): Promise => ({ success: true, logs: [], outputs: [] })),
file: vi.fn((_path: string) => ({
text: async () => '// generated server entrypoint',
- arrayBuffer: async () => new ArrayBuffer(0),
+ stream: () => new Blob([]).stream(),
lastModified: 0
})),
- hash: vi.fn(() => 0xabcn)
+ CryptoHasher: class {
+ update() {}
+ digest() {
+ return 'abc';
+ }
+ }
}));
beforeEach(() => {
- vi.stubGlobal('Bun', { build: bun.build, file: bun.file, hash: bun.hash });
+ vi.stubGlobal('Bun', { build: bun.build, file: bun.file, CryptoHasher: bun.CryptoHasher });
vi.mocked(readdir).mockResolvedValue([]);
});
@@ -287,7 +292,7 @@ describe('generated routes', () => {
const previous = bun.file.getMockImplementation();
bun.file.mockImplementation((path: string) => ({
text: async () => '// generated server entrypoint',
- arrayBuffer: async () => new ArrayBuffer(0),
+ stream: () => new Blob([]).stream(),
lastModified: 0,
exists: async () => path.endsWith('.br') || path.endsWith('.gz')
}));
@@ -359,6 +364,25 @@ describe('generated routes', () => {
expect(source).toContain('...client_asset("public.txt", asset_0, {"hash":"abc","mtime":0})');
});
+ test('rejects route segments starting with a colon', async () => {
+ const builder = create_builder({ client_files: [':tag.txt'] });
+
+ await expect(adapter().adapt(builder)).rejects.toThrow('starts with `:`');
+ expect(bun.build).not.toHaveBeenCalled();
+ });
+
+ test('embedded assets with the same relative path keep distinct imports', async () => {
+ mock_files({ client: ['page.html'], pages: ['page.html'] });
+
+ await adapter({ buildOptions: { compile: true } }).adapt(
+ create_builder({ prerendered_pages: [['/page/', { file: 'page.html' }]] })
+ );
+
+ const source = bun.build.mock.calls[0][0].files[routes_file];
+ expect(source).toContain('...client_asset("page.html", asset_0, {"hash":"abc","mtime":0})');
+ expect(source).toContain('...prerendered_page("/page/", asset_1, {"hash":"abc","mtime":0})');
+ });
+
test('rejects wildcard characters in prerendered redirect sources', async () => {
const builder = create_builder({
prerendered_redirects: [['/docs/*', { status: 308, location: '/new' }]]
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
index 69c077289a13..180ff9147c84 100644
--- a/packages/adapter-bun/test/handler.spec.ts
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -88,6 +88,19 @@ test.each([
expect(error).toHaveBeenCalledWith(expect.stringContaining(message));
});
+test('rejects a present but empty Host header', async () => {
+ const loaded = await load_handler();
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {});
+ const request = new Request('http://internal/path');
+ request.headers.set('host', '');
+
+ const response = await loaded.handler(request, loaded.bun_server);
+
+ expect(response.status).toBe(400);
+ expect(loaded.respond).not.toHaveBeenCalled();
+ expect(error).toHaveBeenCalledWith(expect.stringContaining('Could not determine host'));
+});
+
test('falls back past proxy headers that are present but empty', async () => {
set_env('APP_PROTOCOL_HEADER', 'x-forwarded-proto');
set_env('APP_HOST_HEADER', 'x-forwarded-host');
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index d1b30fcbeaf1..a60eb8bc8bbe 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -54,11 +54,12 @@ test('sub-delims stay raw in route paths with a fully-encoded alias', async () =
]);
});
-test('segments starting with a colon are escaped to avoid Bun route parameters', async () => {
+test('route paths use WHATWG serialization, the form user agents send', async () => {
const { routes } = await load_routes({ base: '/base' });
- expect(routes.client_asset(':tag.txt', undefined, meta).map(([path]) => path)).toEqual([
- '/base/%3Atag.txt'
+ expect(routes.client_asset('photo[1]^a|b.png', undefined, meta).map(([path]) => path)).toEqual([
+ '/base/photo[1]^a|b.png',
+ '/base/photo%5B1%5D%5Ea%7Cb.png'
]);
});
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
index b5e756092572..daa82b9db18f 100644
--- a/packages/adapter-bun/test/start.spec.ts
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -114,10 +114,12 @@ test('force-closes lingering connections after SHUTDOWN_TIMEOUT', async () => {
const shutdown = loaded.listeners.get('SIGTERM')?.();
await vi.advanceTimersByTimeAsync(5000);
- await shutdown;
-
expect(loaded.stop).toHaveBeenCalledTimes(2);
expect(loaded.stop).toHaveBeenLastCalledWith(true);
+ expect(loaded.emit).not.toHaveBeenCalled();
+
+ await vi.advanceTimersByTimeAsync(1000);
+ await shutdown;
expect(loaded.emit).toHaveBeenCalledWith('sveltekit:shutdown', 'SIGTERM');
} finally {
vi.useRealTimers();
From d0eb4084b660c6ab63d8d4f0afb282cd4edd9d75 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 20:35:34 -0400
Subject: [PATCH 81/94] chore: apply review suggestions
Namespace imports for node:path and node:fs, sync fs calls in the
build-time code with an existsSync early return instead of catching
ENOENT, fs.rmSync in place of the deprecated builder.rimraf, and
snake_case with url/filename naming for internal variables.
---
packages/adapter-bun/index.js | 102 ++++++++++------------
packages/adapter-bun/src/routes-util.js | Bin 7609 -> 7576 bytes
packages/adapter-bun/test/adapter.spec.ts | 55 ++++++------
3 files changed, 71 insertions(+), 86 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 6ba863fa0843..f007b0f01e3f 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,34 +1,30 @@
-import { relative, resolve } from 'node:path';
-import { readdir } from 'node:fs/promises';
+import fs from 'node:fs';
+import path from 'node:path';
/**
- * @param {string} path
- * @returns {Promise<{abs: string, rel: string}[]>}
+ * @param {string} dir
+ * @returns {{abs: string, rel: string}[]}
*/
-async function read_files_recursive(path) {
- try {
- const entries = await readdir(path, { recursive: true, withFileTypes: true });
- return entries
- .filter((entry) => entry.isFile())
- .map((entry) => {
- const abs = resolve(entry.parentPath, entry.name);
- const rel = posixify(relative(path, abs));
- return { abs, rel };
- })
- .filter(({ rel }) => rel.split('/').every((segment) => segment !== '.vite'));
- } catch (error) {
- if (/** @type {NodeJS.ErrnoException} */ (error)?.code !== 'ENOENT') throw error;
- return [];
- }
+function read_files_recursive(dir) {
+ if (!fs.existsSync(dir)) return [];
+ return fs
+ .readdirSync(dir, { recursive: true, withFileTypes: true })
+ .filter((entry) => entry.isFile())
+ .map((entry) => {
+ const abs = path.resolve(entry.parentPath, entry.name);
+ const rel = posixify(path.relative(dir, abs));
+ return { abs, rel };
+ })
+ .filter(({ rel }) => rel.split('/').every((segment) => segment !== '.vite'));
}
/**
* Matches sirv's default behaviour in adapter-node: dotfiles are not served,
* with an exception for the `.well-known` directory.
- * @param {string} path
+ * @param {string} file
*/
-function is_dotfile(path) {
- return path
+function is_dotfile(file) {
+ return file
.split('/')
.some((segment, i) => segment.startsWith('.') && !(i === 0 && segment === '.well-known'));
}
@@ -42,10 +38,10 @@ const file_waiters = [];
/**
* Streams the file through the hasher so build memory stays bounded by chunk
* size instead of total asset size.
- * @param {string} path
+ * @param {string} file
* @returns {Promise}
*/
-async function hash_file(path) {
+async function hash_file(file) {
if (open_files === MAX_OPEN_FILES) {
await new Promise((resolve) => {
file_waiters.push(() => resolve(undefined));
@@ -54,7 +50,7 @@ async function hash_file(path) {
open_files++;
try {
const hasher = new Bun.CryptoHasher('blake2b256');
- for await (const chunk of Bun.file(path).stream()) {
+ for await (const chunk of Bun.file(file).stream()) {
hasher.update(chunk);
}
return hasher.digest('hex').slice(0, 16);
@@ -67,22 +63,18 @@ async function hash_file(path) {
/**
* The build-time validator for conditional requests: Bun only generates ETags for
* in-memory static routes, not file-backed responses, so the adapter ships its own.
- * @param {string} path
+ * @param {string} file
* @param {boolean} [precompress]
* @returns {Promise<{ hash: string, mtime: number, br?: boolean, gz?: boolean }>}
*/
-async function asset_meta(path, precompress = false) {
- const hash = await hash_file(path);
+async function asset_meta(file, precompress = false) {
+ const hash = await hash_file(file);
/** @type {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} */
- const meta = { hash, mtime: Bun.file(path).lastModified };
+ const meta = { hash, mtime: Bun.file(file).lastModified };
if (precompress) {
- const [br, gz] = await Promise.all([
- Bun.file(`${path}.br`).exists(),
- Bun.file(`${path}.gz`).exists()
- ]);
- if (br) meta.br = true;
- if (gz) meta.gz = true;
+ if (fs.existsSync(`${file}.br`)) meta.br = true;
+ if (fs.existsSync(`${file}.gz`)) meta.gz = true;
}
return meta;
@@ -124,7 +116,7 @@ export default function (opts = {}) {
);
}
- builder.rimraf(out);
+ fs.rmSync(out, { recursive: true, force: true });
builder.log.minor('Building server');
@@ -136,11 +128,11 @@ export default function (opts = {}) {
const server = builder.getServerDirectory();
- const src_dir = resolve(import.meta.dirname, 'src');
- const index_file = resolve(src_dir, 'index.js');
- const routes_file = resolve(src_dir, 'routes.js');
- const manifest_file = resolve(server, 'manifest.js');
- const server_options_file = resolve(src_dir, 'options.js');
+ const src_dir = path.resolve(import.meta.dirname, 'src');
+ const index_file = path.resolve(src_dir, 'index.js');
+ const routes_file = path.resolve(src_dir, 'routes.js');
+ const manifest_file = path.resolve(server, 'manifest.js');
+ const server_options_file = path.resolve(src_dir, 'options.js');
const virtual_files = {
[manifest_file]:
@@ -163,7 +155,7 @@ export default function (opts = {}) {
: undefined;
if (instrumentation) {
- const start_file = resolve(src_dir, 'start.js'); // Virtual only
+ const start_file = path.resolve(src_dir, 'start.js'); // Virtual only
virtual_files[start_file] = await Bun.file(index_file).text();
virtual_files[index_file] = [
`import ${JSON.stringify(instrumentation)};`,
@@ -235,14 +227,12 @@ export default function (opts = {}) {
* @returns {Promise<{imports: string[], entries: string[], server_assets: string[]}>}
*/
async function get_embed_entries({ builder, server_assets }) {
- const builtFiles = `${builder.config.kit.outDir}/output`;
+ const built_files = `${builder.config.kit.outDir}/output`;
- const [all_cl_files, pr_pages, pr_deps, pr_data] = await Promise.all([
- read_files_recursive(`${builtFiles}/client`),
- read_files_recursive(`${builtFiles}/prerendered/pages`),
- read_files_recursive(`${builtFiles}/prerendered/dependencies`),
- read_files_recursive(`${builtFiles}/prerendered/data`)
- ]);
+ const all_cl_files = read_files_recursive(`${built_files}/client`);
+ const pr_pages = read_files_recursive(`${built_files}/prerendered/pages`);
+ const pr_deps = read_files_recursive(`${built_files}/prerendered/dependencies`);
+ const pr_data = read_files_recursive(`${built_files}/prerendered/data`);
const cl_files = all_cl_files.filter(({ rel }) => !is_dotfile(rel));
@@ -258,10 +248,10 @@ async function get_embed_entries({ builder, server_assets }) {
/**
* @param {{ abs: string, rel: string }} file
* @param {string} helper
- * @param {string} [urlPath]
+ * @param {string} [url]
*/
- const entry = async (file, helper, urlPath = file.rel) =>
- `...${helper}(${JSON.stringify(urlPath)}, asset_${asset_index.get(file)}, ${JSON.stringify(await asset_meta(file.abs))})`;
+ const entry = async (file, helper, url = file.rel) =>
+ `...${helper}(${JSON.stringify(url)}, asset_${asset_index.get(file)}, ${JSON.stringify(await asset_meta(file.abs))})`;
const page_files = new Map(pr_pages.map((file) => [file.rel, file]));
const page_rels = new Set([...builder.prerendered.pages].map(([_, { file }]) => file));
@@ -314,12 +304,12 @@ async function get_no_embed_entries({ builder, server_assets, out, precompress }
/**
* @param {string} helper
- * @param {string} urlPath
+ * @param {string} url
* @param {string} dir
- * @param {string} [filePath]
+ * @param {string} [filename]
*/
- const entry = async (helper, urlPath, dir, filePath) =>
- `...${helper}(${JSON.stringify(urlPath)}, ${JSON.stringify(filePath)}, ${JSON.stringify(await asset_meta(`${out}/${dir}/${filePath ?? urlPath}`, precompress))})`;
+ const entry = async (helper, url, dir, filename) =>
+ `...${helper}(${JSON.stringify(url)}, ${JSON.stringify(filename)}, ${JSON.stringify(await asset_meta(`${out}/${dir}/${filename ?? url}`, precompress))})`;
const pages = [...builder.prerendered.pages];
const page_files = new Set(pages.map(([_, { file }]) => file));
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 5e481729049a7178f642ba551311d7e6b91e1ee1..fcf44895aba7514d357db4a44b0cf30152f17a43 100644
GIT binary patch
delta 759
zcmdmKJ;Qo}B}+kKNyfww1y&HPx3MIdQ8=d500?wJlwlf(G*8=noADphxIL4z(Q6E5Etwh6X$@boBT((oLkeHi!(D#0c67DHWB&BD@Cj)zYsB+EGHU2xlz=V
z4dV65+e8gv%;%!IlM5w8IHTd_n2LD`U|2t0OcC8`DWo7n@=Jz1PEY74mXsFdDHP?GmZZiP07Z*6z$WNrdLV`*)E|WGNXYFEV0ogXW
zlT%@GDQ^;lbDP%|%-Af$SIGloUnid
z$SpbwX_+~xAQsRCPW=4F&vg8tv7j^w9({q(vc8j)n)o2jBPSo)UfI)0J|2P@?(L4P|F1XBYzzb
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index be0431669e4c..838f08b9553a 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -1,4 +1,4 @@
-import { readdir } from 'node:fs/promises';
+import fs from 'node:fs';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import adapter from '../index.js';
@@ -9,9 +9,10 @@ const routes_file = `${package_dir}/src/routes.js`;
const options_file = `${package_dir}/src/options.js`;
const start_file = `${package_dir}/src/start.js`;
-vi.mock('node:fs/promises', async (import_original) => {
- const actual = await import_original();
- return { ...actual, readdir: vi.fn() };
+vi.mock('node:fs', async (import_original) => {
+ const actual = await import_original();
+ const mocked = { ...actual, readdirSync: vi.fn(), existsSync: vi.fn(), rmSync: vi.fn() };
+ return { ...mocked, default: mocked };
});
const bun = vi.hoisted(() => ({
@@ -32,7 +33,8 @@ const bun = vi.hoisted(() => ({
beforeEach(() => {
vi.stubGlobal('Bun', { build: bun.build, file: bun.file, CryptoHasher: bun.CryptoHasher });
- vi.mocked(readdir).mockResolvedValue([]);
+ vi.mocked(fs.readdirSync).mockReturnValue([]);
+ vi.mocked(fs.existsSync).mockReturnValue(true);
});
afterEach(() => {
@@ -63,7 +65,7 @@ describe('Bun build configuration', () => {
const builder = create_builder();
await adapter().adapt(builder);
- expect(builder.rimraf).toHaveBeenCalledWith('build');
+ expect(fs.rmSync).toHaveBeenCalledWith('build', { recursive: true, force: true });
expect(builder.log.minor).toHaveBeenCalledWith('Building server');
const options = bun.build.mock.calls[0][0];
@@ -289,13 +291,6 @@ describe('generated routes', () => {
});
test('precompresses assets and marks the variants in the generated routes', async () => {
- const previous = bun.file.getMockImplementation();
- bun.file.mockImplementation((path: string) => ({
- text: async () => '// generated server entrypoint',
- stream: () => new Blob([]).stream(),
- lastModified: 0,
- exists: async () => path.endsWith('.br') || path.endsWith('.gz')
- }));
const builder = create_builder({ client_files: ['app.js'] });
await adapter({ precompress: true }).adapt(builder);
@@ -306,7 +301,6 @@ describe('generated routes', () => {
expect(source).toContain(
'...client_asset("app.js", undefined, {"hash":"abc","mtime":0,"br":true,"gz":true})'
);
- bun.file.mockImplementation(previous!);
});
test('warns when precompress is combined with compile', async () => {
@@ -343,12 +337,16 @@ describe('generated routes', () => {
expect(source).toContain('...client_asset("ok.txt", undefined, {"hash":"abc","mtime":0})');
});
- test('embedded builds tolerate absent output directories but propagate other errors', async () => {
- vi.mocked(readdir).mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }));
+ test('embedded builds tolerate absent output directories but propagate readdir errors', async () => {
+ vi.mocked(fs.existsSync).mockReturnValue(false);
await adapter({ buildOptions: { compile: true } }).adapt(create_builder());
expect(bun.build).toHaveBeenCalledOnce();
+ expect(fs.readdirSync).not.toHaveBeenCalled();
- vi.mocked(readdir).mockRejectedValue(Object.assign(new Error('denied'), { code: 'EACCES' }));
+ vi.mocked(fs.existsSync).mockReturnValue(true);
+ vi.mocked(fs.readdirSync).mockImplementation(() => {
+ throw Object.assign(new Error('denied'), { code: 'EACCES' });
+ });
await expect(
adapter({ buildOptions: { compile: true } }).adapt(create_builder())
).rejects.toThrow('denied');
@@ -422,7 +420,7 @@ function mock_files({
dependencies?: string[];
data?: string[];
}) {
- vi.mocked(readdir).mockImplementation((path) => {
+ vi.mocked(fs.readdirSync).mockImplementation((path) => {
const directory = String(path);
const files = directory.endsWith('/client')
? client
@@ -432,17 +430,15 @@ function mock_files({
? dependencies
: data;
- return Promise.resolve(
- files.map((file) => {
- const segments = file.split('/');
- const name = /** @type {string} */ segments.pop();
- return {
- name,
- parentPath: [directory, ...segments].join('/'),
- isFile: () => true
- };
- })
- ) as unknown as ReturnType;
+ return files.map((file) => {
+ const segments = file.split('/');
+ const name = /** @type {string} */ segments.pop();
+ return {
+ name,
+ parentPath: [directory, ...segments].join('/'),
+ isFile: () => true
+ };
+ }) as unknown as ReturnType;
});
}
@@ -481,7 +477,6 @@ function create_builder({
info: vi.fn()
},
getServerDirectory: () => '.svelte-kit/output/server',
- rimraf: vi.fn(),
writeClient: vi.fn(() => client_files),
writePrerendered: vi.fn(() => prerendered_files),
compress: vi.fn(async () => {}),
From 7a4ca92c90c6e06590fc9b24542113801b7ecc4c Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 20:43:19 -0400
Subject: [PATCH 82/94] chore: hoist type imports into @import comments
Matches kit's convention of one @import comment per module instead of
inline import() types at every use site.
---
packages/adapter-bun/index.js | 10 ++++++----
packages/adapter-bun/src/handler.js | 5 +++--
packages/adapter-bun/src/index.js | 3 ++-
packages/adapter-bun/src/routes-util.js | 11 ++++++-----
4 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index f007b0f01e3f..4860e38f0977 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -1,3 +1,5 @@
+/** @import { Builder } from '@sveltejs/kit' */
+/** @import { BunPlugin } from 'bun' */
import fs from 'node:fs';
import path from 'node:path';
@@ -163,7 +165,7 @@ export default function (opts = {}) {
].join('\n');
}
- /** @type {import('bun').BunPlugin} */
+ /** @type {BunPlugin} */
const adapter_plugin = {
name: 'adapter-bun',
setup(build) {
@@ -222,7 +224,7 @@ export default function (opts = {}) {
/**
* @param {object} options
- * @param {import('@sveltejs/kit').Builder} options.builder
+ * @param {Builder} options.builder
* @param {string[]} options.server_assets
* @returns {Promise<{imports: string[], entries: string[], server_assets: string[]}>}
*/
@@ -287,7 +289,7 @@ async function get_embed_entries({ builder, server_assets }) {
/**
* @param {object} options
- * @param {import('@sveltejs/kit').Builder} options.builder
+ * @param {Builder} options.builder
* @param {string[]} options.server_assets
* @param {string} options.out
* @param {boolean} options.precompress
@@ -333,7 +335,7 @@ async function get_no_embed_entries({ builder, server_assets, out, precompress }
/**
* @param {object} options
- * @param {import('@sveltejs/kit').Builder} options.builder
+ * @param {Builder} options.builder
* @param {string} options.out
* @param {boolean} options.embed
* @param {boolean} options.precompress
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 41e68c5abbc0..7c51b22dc62b 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -1,3 +1,4 @@
+/** @import { Server as BunServer } from 'bun' */
import { Server } from 'SERVER';
import { manifest, origin, env_prefix } from 'MANIFEST';
import { server_assets } from 'ROUTES';
@@ -19,7 +20,7 @@ await server.init({
/**
* The Bun-native SvelteKit request handler used by the generated server.
* @param {Request} request
- * @param {import('bun').Server} bun_server
+ * @param {BunServer} bun_server
* @returns {Promise}
*/
export async function handler(request, bun_server) {
@@ -97,7 +98,7 @@ function get_origin(request, url) {
/**
* @param {Request} request
- * @param {import('bun').Server} bun_server
+ * @param {BunServer} bun_server
* @returns {string}
*/
function get_client_address(request, bun_server) {
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 735c9bd89d71..bd13aaee1f46 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -1,10 +1,11 @@
+/** @import { Serve } from 'bun' */
import process from 'node:process';
import server_options from 'SERVER_OPTIONS';
import { routes } from 'ROUTES';
import { handler } from './handler.js';
import { boolean_env, bytes_env, env, number_env } from './env.js';
-const options = /** @type {import('bun').Serve.Options} */ ({ ...server_options });
+const options = /** @type {Serve.Options} */ ({ ...server_options });
const unix = env('SOCKET_PATH', options.unix);
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index fcf44895aba7..ff97ecdde400 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -1,10 +1,11 @@
+/** @import { BunFile, BunRequest, Serve } from 'bun' */
import { manifest, base, embed } from 'MANIFEST';
import path from 'node:path';
const dir = path.dirname(Bun.main);
/**
- * @typedef {import('bun').Serve.Routes[string]} RouteHandler
+ * @typedef {Serve.Routes[string]} RouteHandler
* @typedef {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} AssetMeta
*/
@@ -103,7 +104,7 @@ function negotiate(accept, meta) {
/**
* Bun does not route HEAD requests to a GET function handler, so every route
* registers both methods.
- * @param {((request: import('bun').BunRequest) => Response) | Response} handler
+ * @param {((request: BunRequest) => Response) | Response} handler
* @returns {RouteHandler}
*/
function handlers(handler) {
@@ -121,7 +122,7 @@ function file_route(file, meta, extra_headers = {}) {
const content_type = Bun.file(file).type;
const last_modified = new Date(meta.mtime).toUTCString();
- /** @param {import('bun').BunRequest} request */
+ /** @param {BunRequest} request */
const handler = (request) => {
// Bun serializes Range itself for file bodies; ranges apply to the identity representation
const encoding =
@@ -181,7 +182,7 @@ export function client_asset(url, filename = url, meta) {
/**
* @param {string} url
* @param {string} [filename]
- * @returns {import('bun').BunFile}
+ * @returns {BunFile}
*/
export function server_asset(url, filename = url) {
return Bun.file(embed ? filename : path.resolve(dir, 'client', url));
@@ -208,7 +209,7 @@ export function prerendered_page(url, filename, meta) {
const canonical = encode_pathname(url);
/**
- * @param {import('bun').BunRequest} req
+ * @param {BunRequest} req
* @returns {Response}
*/
function handle_redirect(req) {
From acf52037ef551c2b215f1b4b70c81e717f575ded Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Mon, 10 Aug 2026 21:00:24 -0400
Subject: [PATCH 83/94] chore: silence no-control-regex on the path encode set
Control characters are deliberately part of the URL Standard's path
percent-encode set.
---
packages/adapter-bun/src/routes-util.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index ff97ecdde400..c8104277c247 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -12,6 +12,7 @@ const dir = path.dirname(Bun.main);
const CONTENT_ENCODING = { br: 'br', gz: 'gzip' };
// the URL Standard's path percent-encode set, plus `%` and `\` so they stay literal
+// eslint-disable-next-line no-control-regex -- control characters are part of the encode set
const ESCAPED_PATH_CHAR = /[\u0000-\u001f\u007f-\u{10ffff} "#<>?`{}%\\]/gu;
/**
From 344b22219071be6580a986efbe6d939780198c12 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:10:04 -0400
Subject: [PATCH 84/94] fix: assume https for proxied requests without a
protocol header
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/src/handler.js | 4 +++-
packages/adapter-bun/test/handler.spec.ts | 4 ++--
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 99020cde0869..8642eb2a07f6 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -188,7 +188,7 @@ SOCKET_PATH=/tmp/sveltekit.sock bun ./build
### Public origin behind a proxy
-If [`paths.origin`](configuration#paths) is configured, that value is the trusted origin for every request. Otherwise, the adapter derives the origin from the incoming request URL and `Host` header.
+If [`paths.origin`](configuration#paths) is configured, that value is the trusted origin for every request. Otherwise, the adapter derives the host from the `Host` header and assumes the scheme is `https`, since production deployments usually terminate TLS upstream. Configure `paths.origin` or `PROTOCOL_HEADER` if that assumption is wrong, for example when serving plain HTTP directly.
Behind a trusted reverse proxy, `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` name headers that contain the public scheme, host, and port:
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index 7c51b22dc62b..bb3a5257d191 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -66,8 +66,10 @@ export async function handler(request, bun_server) {
* @returns {string}
*/
function get_origin(request, url) {
+ // assume TLS terminates upstream, like adapter-node; a plain-HTTP url.protocol would
+ // make the browser's https Origin mismatch the computed origin and fail every CSRF check
const protocol = decodeURIComponent(
- (protocol_header && request.headers.get(protocol_header)) || url.protocol.slice(0, -1)
+ (protocol_header && request.headers.get(protocol_header)) || 'https'
);
if (!/^https?$/i.test(protocol)) {
throw new Error(
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
index 180ff9147c84..939a47d3dc0d 100644
--- a/packages/adapter-bun/test/handler.spec.ts
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -36,7 +36,7 @@ test('normalizes the request origin from the Host header', async () => {
await loaded.handler(original, loaded.bun_server);
const [request, options] = loaded.respond.mock.calls[0];
- expect(request.url).toBe('http://public.example:8080/path?query=yes');
+ expect(request.url).toBe('https://public.example:8080/path?query=yes');
expect(options.platform).toEqual({ request: original, server: loaded.bun_server });
loaded.request_ip.mockReturnValue({ address: '127.0.0.1', port: 5000, family: 'IPv4' });
expect(options.getClientAddress()).toBe('127.0.0.1');
@@ -111,7 +111,7 @@ test('falls back past proxy headers that are present but empty', async () => {
await loaded.handler(request, loaded.bun_server);
- expect(loaded.respond.mock.calls[0][0].url).toBe('http://internal/path');
+ expect(loaded.respond.mock.calls[0][0].url).toBe('https://internal/path');
});
test('reads a configured client address header', async () => {
From 7a6743f0f1eec1bffc8738717e67ea95003a0430 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:10:21 -0400
Subject: [PATCH 85/94] fix: return undefined from getClientAddress when there
is no peer address
---
packages/adapter-bun/src/handler.js | 6 +++---
packages/adapter-bun/test/handler.spec.ts | 6 ++----
2 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index bb3a5257d191..cfe7a691c8fa 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -126,7 +126,7 @@ function get_client_address(request, bun_server) {
return value;
}
- const address = bun_server.requestIP(request)?.address;
- if (!address) throw new Error('Could not determine client address');
- return address;
+ // requestIP() is null over unix sockets; adapter-node returns undefined there too
+ // rather than turning every getClientAddress() call into a 500
+ return /** @type {string} */ (bun_server.requestIP(request)?.address);
}
diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts
index 939a47d3dc0d..f277a314f25e 100644
--- a/packages/adapter-bun/test/handler.spec.ts
+++ b/packages/adapter-bun/test/handler.spec.ts
@@ -162,15 +162,13 @@ test('reports absent and too-short forwarded address headers', async () => {
expect(() => get_client_address()).toThrow('APP_XFF_DEPTH is 3, but only found 2 addresses');
});
-test('reports when Bun cannot determine the peer address', async () => {
+test('returns undefined when Bun cannot determine the peer address', async () => {
const loaded = await load_handler();
loaded.request_ip.mockReturnValue(null);
await loaded.handler(new Request('http://localhost/'), loaded.bun_server);
- expect(() => loaded.respond.mock.calls[0][1].getClientAddress()).toThrow(
- 'Could not determine client address'
- );
+ expect(loaded.respond.mock.calls[0][1].getClientAddress()).toBeUndefined();
});
test('disables timeouts and proxy buffering for event streams', async () => {
From 362687a3c5c24665f8d33a9ea6e83dea0065029d Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:10:44 -0400
Subject: [PATCH 86/94] fix: remove a stale socket file before listening
---
packages/adapter-bun/src/index.js | 10 ++++++++++
packages/adapter-bun/test/start.spec.ts | 11 +++++++++++
2 files changed, 21 insertions(+)
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index bd13aaee1f46..3e534859f6a5 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -1,4 +1,5 @@
/** @import { Serve } from 'bun' */
+import fs from 'node:fs';
import process from 'node:process';
import server_options from 'SERVER_OPTIONS';
import { routes } from 'ROUTES';
@@ -15,6 +16,15 @@ if (unix) {
delete options.port;
delete options.reusePort;
delete options.ipv6Only;
+
+ // an unclean shutdown leaves the socket file behind and the next listen would
+ // fail with EADDRINUSE; the zero-size check (same heuristic as adapter-node)
+ // avoids deleting a regular file that happens to sit at this path
+ try {
+ if (fs.statSync(unix).size === 0) fs.rmSync(unix);
+ } catch {
+ // ignore
+ }
} else {
delete options.unix;
options.hostname = env('HOST', options.hostname);
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
index daa82b9db18f..172b1c7b822c 100644
--- a/packages/adapter-bun/test/start.spec.ts
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -2,6 +2,7 @@ import { afterEach, expect, test, vi } from 'vitest';
afterEach(() => {
vi.resetModules();
+ vi.doUnmock('node:fs');
vi.doUnmock('node:process');
vi.doUnmock('MANIFEST');
vi.doUnmock('ROUTES');
@@ -81,6 +82,16 @@ test('a Unix socket takes precedence over TCP-only options', async () => {
expect(loaded.log).toHaveBeenCalledWith('Listening on /tmp/application.sock');
});
+test('removes a stale socket file before listening', async () => {
+ const statSync = vi.fn(() => ({ size: 0 }));
+ const rmSync = vi.fn();
+ vi.doMock('node:fs', () => ({ default: { statSync, rmSync } }));
+
+ await load_start({ env: { SOCKET_PATH: '/tmp/application.sock' } });
+
+ expect(rmSync).toHaveBeenCalledWith('/tmp/application.sock');
+});
+
test.each([
[{ IDLE_TIMEOUT: '256' }, 'between 0 and 255'],
[{ BODY_SIZE_LIMIT: '1.1' }, 'whole bytes'],
From 5bc7987b8c09c143a222dfb01fcc6f820f1d7da0 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:11:05 -0400
Subject: [PATCH 87/94] fix: allow BODY_SIZE_LIMIT=Infinity to disable the
limit
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/src/env.js | 4 +++-
packages/adapter-bun/test/env.spec.ts | 3 ++-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 8642eb2a07f6..f6ca74dc96f4 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -180,7 +180,7 @@ SOCKET_PATH=/tmp/sveltekit.sock bun ./build
### Request limits and diagnostics
-`BODY_SIZE_LIMIT` controls `Bun.serve`'s `maxRequestBodySize`. It defaults to `512K`. The value must resolve to a whole number of bytes and may use a case-insensitive binary `K`, `M`, or `G` suffix, such as `768K` or `1.5M`.
+`BODY_SIZE_LIMIT` controls `Bun.serve`'s `maxRequestBodySize`. It defaults to `512K`. The value must resolve to a whole number of bytes and may use a case-insensitive binary `K`, `M`, or `G` suffix, such as `768K` or `1.5M`. `Infinity` disables the limit.
`IDLE_TIMEOUT` sets Bun's per-request inactivity timeout in seconds. It must be an integer from `0` through `255`; `0` disables the timeout. The generated handler disables the timeout for responses whose content type starts with `text/event-stream` and also adds `X-Accel-Buffering: no`.
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index bd60db63a4f9..2aaf0cc18584 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -98,9 +98,11 @@ export function number_env(name, fallback, limits = {}) {
export function bytes_env(name, fallback) {
const value = env(name);
if (value === undefined) return fallback;
+ // adapter-node documents Infinity as the value that disables the limit
+ if (value === 'Infinity') return Infinity;
if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:[KMG])?$/i.test(value)) {
throw new Error(
- `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative number with an optional K, M, or G suffix)`
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative number with an optional K, M, or G suffix, or Infinity)`
);
}
diff --git a/packages/adapter-bun/test/env.spec.ts b/packages/adapter-bun/test/env.spec.ts
index 02ee3dc93206..413e46070ca4 100644
--- a/packages/adapter-bun/test/env.spec.ts
+++ b/packages/adapter-bun/test/env.spec.ts
@@ -118,7 +118,8 @@ describe('bytes_env', () => {
['.5K', 512],
['512K', 512 * 1024],
['1.5M', 1.5 * 1024 * 1024],
- ['2g', 2 * 1024 * 1024 * 1024]
+ ['2g', 2 * 1024 * 1024 * 1024],
+ ['Infinity', Infinity]
])('parses %s as %d bytes', async (value, expected) => {
set_env('OPTION', value);
const { bytes_env } = await load_env();
From d56bd6f7e2865a7ddfc95e6e7a6560204bc30c3e Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:11:30 -0400
Subject: [PATCH 88/94] fix: rename IDLE_TIMEOUT to CONNECTION_IDLE_TIMEOUT
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/src/env.js | 2 +-
packages/adapter-bun/src/index.js | 4 +++-
packages/adapter-bun/test/start.spec.ts | 4 ++--
4 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index f6ca74dc96f4..3ce4381d1695 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -182,7 +182,7 @@ SOCKET_PATH=/tmp/sveltekit.sock bun ./build
`BODY_SIZE_LIMIT` controls `Bun.serve`'s `maxRequestBodySize`. It defaults to `512K`. The value must resolve to a whole number of bytes and may use a case-insensitive binary `K`, `M`, or `G` suffix, such as `768K` or `1.5M`. `Infinity` disables the limit.
-`IDLE_TIMEOUT` sets Bun's per-request inactivity timeout in seconds. It must be an integer from `0` through `255`; `0` disables the timeout. The generated handler disables the timeout for responses whose content type starts with `text/event-stream` and also adds `X-Accel-Buffering: no`.
+`CONNECTION_IDLE_TIMEOUT` sets Bun's per-request inactivity timeout in seconds. It must be an integer from `0` through `255`; `0` disables the timeout. The generated handler disables the timeout for responses whose content type starts with `text/event-stream` and also adds `X-Accel-Buffering: no`. It is deliberately not called `IDLE_TIMEOUT`, which on adapter-node means something different (shut the server down after N seconds without requests).
`DEVELOPMENT` enables Bun's development-mode error pages. It defaults to `false` for the generated server.
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index 2aaf0cc18584..69167e07620a 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -7,7 +7,7 @@ const expected = new Set([
'PORT',
'REUSE_PORT',
'IPV6_ONLY',
- 'IDLE_TIMEOUT',
+ 'CONNECTION_IDLE_TIMEOUT',
'BODY_SIZE_LIMIT',
'SHUTDOWN_TIMEOUT',
'DEVELOPMENT',
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 3e534859f6a5..09a923a0d625 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -33,7 +33,9 @@ if (unix) {
options.ipv6Only = boolean_env('IPV6_ONLY', options.ipv6Only);
}
-options.idleTimeout = number_env('IDLE_TIMEOUT', options.idleTimeout, { max: 255 });
+// not IDLE_TIMEOUT: that name means idle-shutdown seconds on adapter-node, and a
+// carried-over value would crash on the 255 cap or silently kill slow requests
+options.idleTimeout = number_env('CONNECTION_IDLE_TIMEOUT', options.idleTimeout, { max: 255 });
const development = boolean_env('DEVELOPMENT');
if (development !== undefined) {
options.development = development;
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
index 172b1c7b822c..09952262ffc2 100644
--- a/packages/adapter-bun/test/start.spec.ts
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -42,7 +42,7 @@ test('environment variables override TCP server defaults', async () => {
APP_PORT: '4000',
APP_REUSE_PORT: 'true',
APP_IPV6_ONLY: 'yes',
- APP_IDLE_TIMEOUT: '30',
+ APP_CONNECTION_IDLE_TIMEOUT: '30',
APP_BODY_SIZE_LIMIT: '2M',
APP_DEVELOPMENT: 'on'
},
@@ -93,7 +93,7 @@ test('removes a stale socket file before listening', async () => {
});
test.each([
- [{ IDLE_TIMEOUT: '256' }, 'between 0 and 255'],
+ [{ CONNECTION_IDLE_TIMEOUT: '256' }, 'between 0 and 255'],
[{ BODY_SIZE_LIMIT: '1.1' }, 'whole bytes'],
[{ DEVELOPMENT: 'sometimes' }, 'expected a boolean']
])('rejects invalid startup environment %j', async (env, message) => {
From 765570b6da6607d3dc3e6a317f1824603411b238 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:12:12 -0400
Subject: [PATCH 89/94] fix: always set an explicit port so Bun ignores
unprefixed PORT variables
---
documentation/docs/25-build-and-deploy/45-adapter-bun.md | 2 +-
packages/adapter-bun/src/index.js | 4 +++-
packages/adapter-bun/test/start.spec.ts | 1 +
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
index 3ce4381d1695..bbd9da070a00 100644
--- a/documentation/docs/25-build-and-deploy/45-adapter-bun.md
+++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md
@@ -164,7 +164,7 @@ Bun loads `.env` files automatically. If `envPrefix` is set, add that prefix to
### Listener
-`HOST` and `PORT` configure the TCP listener. Without either value or a `serverOptions` default, Bun uses its own listener defaults.
+`HOST` and `PORT` configure the TCP listener. Without either value or a `serverOptions` default, the server listens on port `3000`.
```sh
HOST=127.0.0.1 PORT=4000 bun ./build
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 09a923a0d625..5f234f04bdba 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -28,7 +28,9 @@ if (unix) {
} else {
delete options.unix;
options.hostname = env('HOST', options.hostname);
- options.port = env('PORT', options.port !== undefined ? String(options.port) : undefined);
+ // always set an explicit port: left undefined, Bun.serve reads the unprefixed
+ // BUN_PORT/PORT/NODE_PORT itself, bypassing envPrefix isolation
+ options.port = env('PORT', options.port !== undefined ? String(options.port) : undefined) ?? 3000;
options.reusePort = boolean_env('REUSE_PORT', options.reusePort);
options.ipv6Only = boolean_env('IPV6_ONLY', options.ipv6Only);
}
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
index 09952262ffc2..7031e4deb189 100644
--- a/packages/adapter-bun/test/start.spec.ts
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -18,6 +18,7 @@ test('starts Bun with production defaults and generated request routes', async (
expect(loaded.serve).toHaveBeenCalledWith(
expect.objectContaining({
development: false,
+ port: 3000,
maxRequestBodySize: 512 * 1024,
fetch: loaded.handler,
routes: loaded.routes
From 757c0bb7f25c26729589b47f6d971865391d67c1 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:12:12 -0400
Subject: [PATCH 90/94] fix: resolve asset directories from the module URL
instead of Bun.main
---
packages/adapter-bun/src/routes-util.js | 5 ++++-
packages/adapter-bun/test/routes.spec.ts | 18 +++++++++++-------
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index c8104277c247..6251d8857f0f 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -1,8 +1,11 @@
/** @import { BunFile, BunRequest, Serve } from 'bun' */
import { manifest, base, embed } from 'MANIFEST';
import path from 'node:path';
+import { fileURLToPath } from 'node:url';
-const dir = path.dirname(Bun.main);
+// not Bun.main: when the built server is imported from a wrapper script rather than
+// run directly, Bun.main is the wrapper and every asset path resolves wrong
+const dir = path.dirname(fileURLToPath(import.meta.url));
/**
* @typedef {Serve.Routes[string]} RouteHandler
diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts
index a60eb8bc8bbe..9efe18447dcf 100644
--- a/packages/adapter-bun/test/routes.spec.ts
+++ b/packages/adapter-bun/test/routes.spec.ts
@@ -1,6 +1,10 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
import { afterEach, expect, test, vi } from 'vitest';
const meta = { hash: 'abc', mtime: 0 };
+// the module resolves assets from its own directory, which is src/ under vitest
+const dir = path.dirname(fileURLToPath(new URL('../src/routes-util.js', import.meta.url)));
afterEach(() => {
vi.resetModules();
@@ -15,7 +19,7 @@ test('client assets use the configured base and URL-encode path segments', async
expect(entries).toHaveLength(1);
expect(entries[0][0]).toBe('/base/folder/encoded%20name%231.txt');
- expect(file).toHaveBeenCalledWith('/app/build/client/folder/encoded name#1.txt');
+ expect(file).toHaveBeenCalledWith(`${dir}/client/folder/encoded name#1.txt`);
expect(entries[0][1]).toHaveProperty('GET');
const response = (entries[0][1] as any).GET(new Request('http://localhost/'));
expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8');
@@ -166,14 +170,14 @@ test('precompressed variants are negotiated with their own validators', async ()
expect(br.headers.get('content-encoding')).toBe('br');
expect(br.headers.get('etag')).toBe('"abc-br"');
expect(br.headers.get('vary')).toBe('accept-encoding');
- expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js.br');
+ expect(file).toHaveBeenLastCalledWith(`${dir}/client/app.js.br`);
const gzip = route.GET(
new Request('http://localhost/app.js', { headers: { 'accept-encoding': 'br;q=0, gzip' } })
);
expect(gzip.headers.get('content-encoding')).toBe('gzip');
expect(gzip.headers.get('etag')).toBe('"abc-gz"');
- expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js.gz');
+ expect(file).toHaveBeenLastCalledWith(`${dir}/client/app.js.gz`);
const any = route.GET(
new Request('http://localhost/app.js', { headers: { 'accept-encoding': '*' } })
@@ -208,7 +212,7 @@ test('range requests are served from the identity representation', async () => {
expect(response.headers.has('content-encoding')).toBe(false);
expect(response.headers.get('etag')).toBe('"abc"');
- expect(file).toHaveBeenLastCalledWith('/app/build/client/app.js');
+ expect(file).toHaveBeenLastCalledWith(`${dir}/client/app.js`);
});
test('embedded routes use the imported asset instead of a filesystem path', async () => {
@@ -229,8 +233,8 @@ test('server assets resolve from the client output in regular builds', async ()
const result = routes.server_asset('nested/read.txt');
- expect(file).toHaveBeenCalledWith('/app/build/client/nested/read.txt');
- expect(result).toMatchObject({ path: '/app/build/client/nested/read.txt' });
+ expect(file).toHaveBeenCalledWith(`${dir}/client/nested/read.txt`);
+ expect(result).toMatchObject({ path: `${dir}/client/nested/read.txt` });
});
test('prerendered assets use the base path and preserve their content type', async () => {
@@ -293,7 +297,7 @@ async function load_routes({ base = '/', embed = false, appDir = '_app' } = {})
vi.resetModules();
vi.doMock('MANIFEST', () => ({ manifest: { appDir }, base, embed }));
const file = vi.fn((path: string) => ({ path, type: 'text/plain;charset=utf-8' }));
- vi.stubGlobal('Bun', { main: '/app/build/index.js', file });
+ vi.stubGlobal('Bun', { file });
return { routes: await import('../src/routes-util.js'), file };
}
From e34cfe19b90a77a1be2eb016ee7d9bf09c998543 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 18:12:24 -0400
Subject: [PATCH 91/94] fix: emit the instrumentation start module as an
entrypoint so asset paths resolve from the output root
---
packages/adapter-bun/index.js | 7 ++++++-
packages/adapter-bun/test/adapter.spec.ts | 10 ++++++++++
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 4860e38f0977..65a3da100621 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -156,6 +156,8 @@ export default function (opts = {}) {
? `${server}/instrumentation.server.js`
: undefined;
+ const entrypoints = [index_file];
+
if (instrumentation) {
const start_file = path.resolve(src_dir, 'start.js'); // Virtual only
virtual_files[start_file] = await Bun.file(index_file).text();
@@ -163,6 +165,9 @@ export default function (opts = {}) {
`import ${JSON.stringify(instrumentation)};`,
`await import(${JSON.stringify(start_file)});`
].join('\n');
+
+ // as a split chunk, start.js would resolve assets from server/chunks/ instead of the output root
+ if (!buildOptions.compile) entrypoints.push(start_file);
}
/** @type {BunPlugin} */
@@ -182,7 +187,7 @@ export default function (opts = {}) {
...buildOptions,
splitting: buildOptions.splitting ?? true,
sourcemap: buildOptions.sourcemap ?? 'external',
- entrypoints: [index_file],
+ entrypoints,
target: 'bun',
format: 'esm',
naming: {
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 838f08b9553a..c175f20d17f8 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -179,6 +179,16 @@ describe('Bun build configuration', () => {
);
expect(files[start_file]).toBe(bun.entrypoint);
expect(builder.instrument).not.toHaveBeenCalled();
+
+ // start.js must be its own entrypoint so asset paths resolve from the output root
+ expect(bun.build.mock.calls[0][0].entrypoints).toEqual([index_file, start_file]);
+ });
+
+ test('keeps a single entrypoint when compiling with instrumentation', async () => {
+ const builder = create_builder({ instrumentation: true });
+ await adapter({ buildOptions: { compile: true } }).adapt(builder);
+
+ expect(bun.build.mock.calls[0][0].entrypoints).toEqual([index_file]);
});
test('reports every Bun diagnostic before failing the build', async () => {
From ea4cc1331ca58fe84d10a9a9dd404705d83d4cf3 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Tue, 11 Aug 2026 20:06:28 -0400
Subject: [PATCH 92/94] readability and slop cleanups
---
packages/adapter-bun/index.js | 2 +-
packages/adapter-bun/src/env.js | 90 ++++++++++++---------
packages/adapter-bun/src/handler.js | 102 ++++++++++++------------
packages/adapter-bun/src/index.js | 17 ++--
packages/adapter-bun/src/routes-util.js | 80 ++++++++++---------
packages/adapter-bun/test/start.spec.ts | 2 +-
6 files changed, 156 insertions(+), 137 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 65a3da100621..9092a6342d29 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -142,7 +142,7 @@ export default function (opts = {}) {
`export const base = ${JSON.stringify(builder.config.kit.paths.base || '/')};\n` +
`export const embed = ${JSON.stringify(!!buildOptions.compile)};\n` +
`export const env_prefix = ${JSON.stringify(envPrefix)};\n` +
- `export const origin = ${JSON.stringify(builder.config.kit.paths.origin) || 'undefined'};`,
+ `export const origin = ${JSON.stringify(builder.config.kit.paths.origin) ?? 'undefined'};`,
[server_options_file]: `export default ${JSON.stringify(serverOptions)};`,
[routes_file]: await create_routes({
builder,
diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js
index 69167e07620a..dc28490d1a68 100644
--- a/packages/adapter-bun/src/env.js
+++ b/packages/adapter-bun/src/env.js
@@ -20,56 +20,75 @@ const expected = new Set([
if (env_prefix) {
for (const name in process.env) {
- if (name.startsWith(env_prefix)) {
- const unprefixed = name.slice(env_prefix.length);
- if (!expected.has(unprefixed)) {
- throw new Error(
- `You should change envPrefix (${env_prefix}) to avoid conflicts with existing environment variables — unexpectedly saw ${name}`
- );
- }
+ if (name.startsWith(env_prefix) && !expected.has(name.slice(env_prefix.length))) {
+ throw new Error(
+ `You should change envPrefix (${env_prefix}) to avoid conflicts with existing environment variables — unexpectedly saw ${name}`
+ );
}
}
}
/**
* @param {string} name
- * @param {string | undefined} [fallback]
- * @returns {string | undefined}
+ * @param {string} value
+ * @param {string} expected
+ * @returns {never}
+ */
+function parsing_error(name, value, expected) {
+ throw new Error(
+ `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected ${expected})`
+ );
+}
+
+/**
+ * @template {string | undefined} [T=undefined]
+ * @param {string} name
+ * @param {T} [fallback]
+ * @returns {string | T}
*/
export function env(name, fallback) {
const prefixed = env_prefix + name;
- return prefixed in process.env ? process.env[prefixed] : fallback;
+ return prefixed in process.env
+ ? /** @type {string} */ (process.env[prefixed])
+ : /** @type {T} */ (fallback);
}
+/** @type {Record} */
+const BOOLEANS = {
+ 1: true,
+ true: true,
+ yes: true,
+ on: true,
+ 0: false,
+ false: false,
+ no: false,
+ off: false
+};
+
/**
+ * @template {boolean | undefined} [T=undefined]
* @param {string} name
- * @param {boolean | undefined} [fallback]
- * @returns {boolean | undefined}
+ * @param {T} [fallback]
+ * @returns {boolean | T}
*/
export function boolean_env(name, fallback) {
const value = env(name);
- if (value === undefined) return fallback;
- if (/^(?:1|true|yes|on)$/i.test(value)) return true;
- if (/^(?:0|false|no|off)$/i.test(value)) return false;
-
- throw new Error(
- `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a boolean)`
- );
+ if (value === undefined) return /** @type {T} */ (fallback);
+ return BOOLEANS[value.toLowerCase()] ?? parsing_error(name, value, 'a boolean');
}
/**
+ * @template {number | undefined} [T=undefined]
* @param {string} name
- * @param {number | undefined} [fallback]
+ * @param {T} [fallback]
* @param {{ min?: number; max?: number }} [limits]
- * @returns {number | undefined}
+ * @returns {number | T}
*/
export function number_env(name, fallback, limits = {}) {
const value = env(name);
- if (value === undefined) return fallback;
+ if (value === undefined) return /** @type {T} */ (fallback);
if (!/^\d+$/.test(value)) {
- throw new Error(
- `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative integer)`
- );
+ parsing_error(name, value, 'a non-negative integer');
}
const number = Number(value);
@@ -82,27 +101,28 @@ export function number_env(name, fallback, limits = {}) {
limits.max === undefined
? `at least ${limits.min ?? 0}`
: `between ${limits.min ?? 0} and ${limits.max}`;
- throw new Error(
- `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected an integer ${range})`
- );
+ parsing_error(name, value, `an integer ${range}`);
}
return number;
}
/**
+ * @template {number | undefined} [T=undefined]
* @param {string} name
- * @param {number | undefined} [fallback]
- * @returns {number | undefined}
+ * @param {T} [fallback]
+ * @returns {number | T}
*/
export function bytes_env(name, fallback) {
const value = env(name);
- if (value === undefined) return fallback;
+ if (value === undefined) return /** @type {T} */ (fallback);
// adapter-node documents Infinity as the value that disables the limit
if (value === 'Infinity') return Infinity;
if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:[KMG])?$/i.test(value)) {
- throw new Error(
- `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative number with an optional K, M, or G suffix, or Infinity)`
+ parsing_error(
+ name,
+ value,
+ 'a non-negative number with an optional K, M, or G suffix, or Infinity'
);
}
@@ -116,9 +136,7 @@ export function bytes_env(name, fallback) {
const number = Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier;
if (!Number.isSafeInteger(number)) {
- throw new Error(
- `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected a non-negative number of whole bytes)`
- );
+ parsing_error(name, value, 'a non-negative number of whole bytes');
}
return number;
diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js
index cfe7a691c8fa..467bb8cabe64 100644
--- a/packages/adapter-bun/src/handler.js
+++ b/packages/adapter-bun/src/handler.js
@@ -6,11 +6,11 @@ import { env, number_env } from './env.js';
const server = new Server(manifest);
-const address_header = env('ADDRESS_HEADER', '')?.toLowerCase();
-const protocol_header = env('PROTOCOL_HEADER', '')?.toLowerCase();
-const host_header = env('HOST_HEADER', '')?.toLowerCase();
-const port_header = env('PORT_HEADER', '')?.toLowerCase();
-const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 }) ?? 1;
+const address_header = env('ADDRESS_HEADER', '').toLowerCase();
+const protocol_header = env('PROTOCOL_HEADER', '').toLowerCase();
+const host_header = env('HOST_HEADER', '').toLowerCase();
+const port_header = env('PORT_HEADER', '').toLowerCase();
+const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 });
await server.init({
env: Bun.env,
@@ -24,25 +24,8 @@ await server.init({
* @returns {Promise}
*/
export async function handler(request, bun_server) {
- let request_origin = origin;
- /** @type {URL} */
- let url;
- try {
- // an empty Host header makes request.url relative, so parsing belongs in the try
- url = new URL(request.url);
- request_origin ||= get_origin(request, url);
- } catch (error) {
- console.error(
- `Could not determine request origin: ${error instanceof Error ? error.message : String(error)}`
- );
- return new Response('Bad Request', { status: 400 });
- }
-
- let normalized_request = request;
- if (request_origin !== url.origin) {
- const normalized_url = new URL(url.pathname + url.search, request_origin);
- normalized_request = new Request(normalized_url, request);
- }
+ const normalized_request = normalize_request(request);
+ if (normalized_request instanceof Response) return normalized_request;
const response = await server.respond(normalized_request, {
platform: {
@@ -60,14 +43,34 @@ export async function handler(request, bun_server) {
return response;
}
+/**
+ * Rewrites the request onto the public origin the user actually requested.
+ * @param {Request} request
+ * @returns {Request | Response}
+ */
+function normalize_request(request) {
+ try {
+ // an empty Host header makes request.url relative, so parsing belongs in the try
+ const url = new URL(request.url);
+ const request_origin = origin || get_origin(request, url);
+ return request_origin === url.origin
+ ? request
+ : new Request(request_origin + url.pathname + url.search, request);
+ } catch (error) {
+ console.error(
+ `Could not determine request origin: ${error instanceof Error ? error.message : String(error)}`
+ );
+ return new Response('Bad Request', { status: 400 });
+ }
+}
+
/**
* @param {Request} request
* @param {URL} url
* @returns {string}
*/
function get_origin(request, url) {
- // assume TLS terminates upstream, like adapter-node; a plain-HTTP url.protocol would
- // make the browser's https Origin mismatch the computed origin and fail every CSRF check
+ // assume TLS terminates upstream, like adapter-node; an http origin would fail CSRF checks
const protocol = decodeURIComponent(
(protocol_header && request.headers.get(protocol_header)) || 'https'
);
@@ -77,13 +80,11 @@ function get_origin(request, url) {
);
}
- // an empty forwarded header falls through, but a present-and-empty Host is rejected below
const host =
(host_header && request.headers.get(host_header)) || (request.headers.get('host') ?? url.host);
if (!host) {
- const header_names = host_header ? `${host_header} or host headers` : 'host header';
throw new Error(
- `Could not determine host. The request must have a value provided by the ${header_names}`
+ `Could not determine host from the ${host_header ? `${host_header} or ` : ''}host header`
);
}
@@ -94,8 +95,8 @@ function get_origin(request, url) {
);
}
- const value = port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
- return new URL(value).origin;
+ // canonicalized so the caller's comparison with url.origin matches (case, default ports)
+ return new URL(`${protocol}://${host}${port ? `:${port}` : ''}`).origin;
}
/**
@@ -104,29 +105,24 @@ function get_origin(request, url) {
* @returns {string}
*/
function get_client_address(request, bun_server) {
- if (address_header) {
- const value = request.headers.get(address_header);
- if (value === null) {
- throw new Error(
- `Address header was specified with ${env_prefix}ADDRESS_HEADER=${address_header} but is absent from request`
- );
- }
-
- if (address_header === 'x-forwarded-for') {
- const addresses = value.split(',');
-
- if (xff_depth > addresses.length) {
- throw new Error(
- `${env_prefix}XFF_DEPTH is ${xff_depth}, but only found ${addresses.length} addresses`
- );
- }
- return addresses[addresses.length - xff_depth].trim();
- }
+ if (!address_header) {
+ // requestIP() is null over unix sockets; undefined matches adapter-node
+ return /** @type {string} */ (bun_server.requestIP(request)?.address);
+ }
- return value;
+ const value = request.headers.get(address_header);
+ if (value === null) {
+ throw new Error(
+ `Address header was specified with ${env_prefix}ADDRESS_HEADER=${address_header} but is absent from request`
+ );
}
+ if (address_header !== 'x-forwarded-for') return value;
- // requestIP() is null over unix sockets; adapter-node returns undefined there too
- // rather than turning every getClientAddress() call into a 500
- return /** @type {string} */ (bun_server.requestIP(request)?.address);
+ const addresses = value.split(',');
+ if (xff_depth > addresses.length) {
+ throw new Error(
+ `${env_prefix}XFF_DEPTH is ${xff_depth}, but only found ${addresses.length} addresses`
+ );
+ }
+ return addresses[addresses.length - xff_depth].trim();
}
diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js
index 5f234f04bdba..4cfa4998f72e 100644
--- a/packages/adapter-bun/src/index.js
+++ b/packages/adapter-bun/src/index.js
@@ -30,7 +30,7 @@ if (unix) {
options.hostname = env('HOST', options.hostname);
// always set an explicit port: left undefined, Bun.serve reads the unprefixed
// BUN_PORT/PORT/NODE_PORT itself, bypassing envPrefix isolation
- options.port = env('PORT', options.port !== undefined ? String(options.port) : undefined) ?? 3000;
+ options.port = env('PORT', options.port?.toString()) ?? 3000;
options.reusePort = boolean_env('REUSE_PORT', options.reusePort);
options.ipv6Only = boolean_env('IPV6_ONLY', options.ipv6Only);
}
@@ -38,16 +38,11 @@ if (unix) {
// not IDLE_TIMEOUT: that name means idle-shutdown seconds on adapter-node, and a
// carried-over value would crash on the 255 cap or silently kill slow requests
options.idleTimeout = number_env('CONNECTION_IDLE_TIMEOUT', options.idleTimeout, { max: 255 });
-const development = boolean_env('DEVELOPMENT');
-if (development !== undefined) {
- options.development = development;
-} else if (options.development === undefined) {
- options.development = false;
-}
+options.development = boolean_env('DEVELOPMENT') ?? options.development ?? false;
options.maxRequestBodySize = bytes_env('BODY_SIZE_LIMIT', options.maxRequestBodySize ?? 512 * 1024);
-const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT', 30) ?? 30;
+const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT', 30);
options.fetch = handler;
options.routes = routes;
@@ -64,8 +59,10 @@ async function graceful_shutdown(reason) {
shutting_down = true;
if (server.pendingRequests !== 0) {
- console.log(`Waiting for ${server.pendingRequests} requests to finish before shutting down...`);
- console.log('Press Ctrl+C again to force shutdown.');
+ console.log(
+ `Waiting for ${server.pendingRequests} requests to finish before shutting down...\n` +
+ 'Press Ctrl+C again to force shutdown.'
+ );
}
// stop() waits forever on idle connections such as open event streams, and once it
diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js
index 6251d8857f0f..d20d2fbb0ed1 100644
--- a/packages/adapter-bun/src/routes-util.js
+++ b/packages/adapter-bun/src/routes-util.js
@@ -7,6 +7,17 @@ import { fileURLToPath } from 'node:url';
// run directly, Bun.main is the wrapper and every asset path resolves wrong
const dir = path.dirname(fileURLToPath(import.meta.url));
+/**
+ * Embedded assets are imported by identity path; on-disk assets live under the
+ * output directory next to this module.
+ * @param {string} subdir
+ * @param {string} filename
+ * @returns {string}
+ */
+function resolve_file(subdir, filename) {
+ return embed ? filename : path.resolve(dir, subdir, filename);
+}
+
/**
* @typedef {Serve.Routes[string]} RouteHandler
* @typedef {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} AssetMeta
@@ -56,7 +67,6 @@ function to_directory_paths(url) {
const directory = `${path.posix.join(base, url).replace(/\/$/, '')}/`;
const paths = route_paths(directory);
- // `/dir` serves `dir/index.html` like sirv does in adapter-node
if (directory !== '/') {
paths.push(...route_paths(directory.slice(0, -1)));
}
@@ -75,10 +85,9 @@ function to_directory_paths(url) {
function is_fresh(request, etag, mtime) {
const header = request.headers.get('if-none-match');
if (header !== null) {
- return header.split(',').some((value) => {
- const tag = value.trim().replace(/^W\//, '');
- return tag === '*' || tag === etag;
- });
+ return header
+ .split(',')
+ .some((value) => ['*', etag].includes(value.trim().replace(/^W\//, '')));
}
const since = Date.parse(request.headers.get('if-modified-since') ?? '');
@@ -115,6 +124,15 @@ function handlers(handler) {
return { GET: handler, HEAD: handler };
}
+/**
+ * @param {string[]} paths
+ * @param {RouteHandler} route
+ * @returns {Array<[string, RouteHandler]>}
+ */
+function route_entries(paths, route) {
+ return paths.map((route_path) => [route_path, route]);
+}
+
/**
* Serves one file with its build-time validator and precompressed variants.
* @param {string} file
@@ -147,12 +165,13 @@ function file_route(file, meta, extra_headers = {}) {
if (is_fresh(request, etag, meta.mtime)) {
return new Response(null, { status: 304, headers: response_headers });
}
- if (encoding === null) {
- return new Response(Bun.file(file), { headers: response_headers });
- }
- response_headers['content-encoding'] = CONTENT_ENCODING[encoding];
- return new Response(Bun.file(`${file}.${encoding}`), { headers: response_headers });
+ let body_file = file;
+ if (encoding !== null) {
+ response_headers['content-encoding'] = CONTENT_ENCODING[encoding];
+ body_file = `${file}.${encoding}`;
+ }
+ return new Response(Bun.file(body_file), { headers: response_headers });
};
return handlers(handler);
@@ -167,7 +186,7 @@ function file_route(file, meta, extra_headers = {}) {
export function client_asset(url, filename = url, meta) {
const immutable = url.startsWith(`${manifest.appDir}/immutable/`);
const route = file_route(
- embed ? filename : path.resolve(dir, 'client', filename),
+ resolve_file('client', filename),
meta,
immutable ? { 'cache-control': 'public,max-age=31536000,immutable' } : {}
);
@@ -180,7 +199,7 @@ export function client_asset(url, filename = url, meta) {
paths.push(...to_paths(url.slice(0, -'.html'.length)));
}
- return paths.map((route_path) => /** @type {[string, RouteHandler]} */ ([route_path, route]));
+ return route_entries(paths, route);
}
/**
@@ -189,7 +208,7 @@ export function client_asset(url, filename = url, meta) {
* @returns {BunFile}
*/
export function server_asset(url, filename = url) {
- return Bun.file(embed ? filename : path.resolve(dir, 'client', url));
+ return Bun.file(resolve_file('client', filename));
}
/**
@@ -199,8 +218,8 @@ export function server_asset(url, filename = url) {
* @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_asset(url, filename = url, meta) {
- const route = file_route(embed ? filename : path.resolve(dir, 'prerendered', filename), meta);
- return to_paths(url).map((route_path) => [route_path, route]);
+ const route = file_route(resolve_file('prerendered', filename), meta);
+ return route_entries(to_paths(url), route);
}
/**
@@ -210,29 +229,18 @@ export function prerendered_asset(url, filename = url, meta) {
* @returns {Array<[string, RouteHandler]>}
*/
export function prerendered_page(url, filename, meta) {
- const canonical = encode_pathname(url);
-
- /**
- * @param {BunRequest} req
- * @returns {Response}
- */
- function handle_redirect(req) {
- const request_url = new URL(req.url);
- const location = `${canonical}${request_url.search}`;
- return new Response(null, { status: 308, headers: { location } });
- }
-
- const route = file_route(embed ? filename : path.resolve(dir, 'prerendered', filename), meta);
-
- const inverted = url.endsWith('/') ? url.slice(0, -1) : `${url}/`;
+ const route = file_route(resolve_file('prerendered', filename), meta);
// path already contains base, no need to add it here
- /** @type {Array<[string, RouteHandler]>} */
- const entries = route_paths(url).map((route_path) => [route_path, route]);
+ const entries = route_entries(route_paths(url), route);
+ const inverted = url.endsWith('/') ? url.slice(0, -1) : `${url}/`;
if (inverted) {
- for (const route_path of route_paths(inverted)) {
- entries.push([route_path, handlers(handle_redirect)]);
- }
+ const canonical = encode_pathname(url);
+ const redirect = handlers((req) => {
+ const location = canonical + new URL(req.url).search;
+ return new Response(null, { status: 308, headers: { location } });
+ });
+ entries.push(...route_entries(route_paths(inverted), redirect));
}
return entries;
@@ -247,5 +255,5 @@ export function prerendered_page(url, filename, meta) {
export function prerendered_redirect(url, status, location) {
const route = handlers(new Response(null, { status, headers: { location } }));
// path already contains base, no need to add it here
- return route_paths(url).map((route_path) => [route_path, route]);
+ return route_entries(route_paths(url), route);
}
diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts
index 7031e4deb189..59ae924624f1 100644
--- a/packages/adapter-bun/test/start.spec.ts
+++ b/packages/adapter-bun/test/start.spec.ts
@@ -111,7 +111,7 @@ test.each(['SIGINT', 'SIGTERM'] as const)(
expect(loaded.stop).toHaveBeenCalledOnce();
expect(loaded.emit).toHaveBeenCalledWith('sveltekit:shutdown', signal);
expect(loaded.log).toHaveBeenCalledWith(
- 'Waiting for 2 requests to finish before shutting down...'
+ expect.stringContaining('Waiting for 2 requests to finish before shutting down...')
);
}
);
From 50b89dd800a9566536809636fdb641ceeb2637b2 Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Thu, 13 Aug 2026 21:49:36 -0400
Subject: [PATCH 93/94] read config off the builder directly, config.kit is
deprecated after the svelte.config.js removal
---
packages/adapter-bun/index.js | 6 +++---
packages/adapter-bun/test/adapter.spec.ts | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index 9092a6342d29..f2ade8406504 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -139,10 +139,10 @@ export default function (opts = {}) {
const virtual_files = {
[manifest_file]:
`export const manifest = ${builder.generateManifest({ relativePath: './' })};\n` +
- `export const base = ${JSON.stringify(builder.config.kit.paths.base || '/')};\n` +
+ `export const base = ${JSON.stringify(builder.config.paths.base || '/')};\n` +
`export const embed = ${JSON.stringify(!!buildOptions.compile)};\n` +
`export const env_prefix = ${JSON.stringify(envPrefix)};\n` +
- `export const origin = ${JSON.stringify(builder.config.kit.paths.origin) ?? 'undefined'};`,
+ `export const origin = ${JSON.stringify(builder.config.paths.origin) ?? 'undefined'};`,
[server_options_file]: `export default ${JSON.stringify(serverOptions)};`,
[routes_file]: await create_routes({
builder,
@@ -234,7 +234,7 @@ export default function (opts = {}) {
* @returns {Promise<{imports: string[], entries: string[], server_assets: string[]}>}
*/
async function get_embed_entries({ builder, server_assets }) {
- const built_files = `${builder.config.kit.outDir}/output`;
+ const built_files = `${builder.config.outDir}/output`;
const all_cl_files = read_files_recursive(`${built_files}/client`);
const pr_pages = read_files_recursive(`${built_files}/prerendered/pages`);
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index c175f20d17f8..33ca089f57f9 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -474,7 +474,7 @@ function create_builder({
instrumentation?: boolean;
} = {}) {
return {
- config: { kit: { outDir: '.svelte-kit', paths: { base, origin } } },
+ config: { outDir: '.svelte-kit', paths: { base, origin } },
routes,
prerendered: {
pages: new Map(prerendered_pages),
From b5a372dbc4620ab701c922326e2e39e86875138d Mon Sep 17 00:00:00 2001
From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Date: Thu, 13 Aug 2026 19:30:05 -0400
Subject: [PATCH 94/94] work around Bun duplicating side-effect-only chunks
Bun.build emits a copy of a side-effect-only chunk (import 'x'; export {};)
per importer and every copy targets the same output path, failing real-world
apps with 'Multiple files share the same output path'. Resolve each importer's
copy to a distinct synthetic module so the copies no longer collide.
---
packages/adapter-bun/index.js | 34 +++++++++++
packages/adapter-bun/test/adapter.spec.ts | 74 ++++++++++++++++++++++-
2 files changed, 105 insertions(+), 3 deletions(-)
diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js
index f2ade8406504..4f6ccf666cf7 100644
--- a/packages/adapter-bun/index.js
+++ b/packages/adapter-bun/index.js
@@ -170,6 +170,10 @@ export default function (opts = {}) {
if (!buildOptions.compile) entrypoints.push(start_file);
}
+ const chunks_dir = path.resolve(server, 'chunks');
+ /** @type {Map} */
+ const side_effect_sources = new Map();
+
/** @type {BunPlugin} */
const adapter_plugin = {
name: 'adapter-bun',
@@ -180,6 +184,36 @@ export default function (opts = {}) {
if (path === 'ROUTES') return { path: routes_file };
if (path === 'SERVER_OPTIONS') return { path: server_options_file };
});
+
+ // Side-effect-only chunks (e.g. Svelte's events.js, kit's env re-export) compile to
+ // identical stubs whose content hashes collide on one output path, failing the build
+ // with "Multiple files share the same output path" (oven-sh/bun#37576). Resolving a
+ // distinct identity per importer keeps every emitted copy unique; delete this once
+ // the Bun fix ships.
+ build.onResolve({ filter: /\.js$/ }, (args) => {
+ const file = path.resolve(args.resolveDir, args.path);
+ if (path.dirname(file) !== chunks_dir) return;
+ let source = side_effect_sources.get(file);
+ if (source === undefined) {
+ const text = fs.readFileSync(file, 'utf8');
+ source = /^import\s+["'][^"']+["'];\s*export\s*\{\s*\};?\s*$/.test(text) && text;
+ side_effect_sources.set(file, source);
+ }
+ if (source === false) return;
+ // The `?` suffix keeps dirname(path) inside chunks/ — Bun resolves the synthetic
+ // module's relative imports against that, ignoring onLoad's resolveDir.
+ return {
+ path: `${file}?${Bun.hash(args.importer).toString(16)}`,
+ namespace: 'adapter-bun-side-effect'
+ };
+ });
+ build.onLoad({ filter: /.*/, namespace: 'adapter-bun-side-effect' }, (args) => {
+ const file = args.path.slice(0, args.path.indexOf('?'));
+ return {
+ loader: 'js',
+ contents: `${side_effect_sources.get(file)}\nSymbol.for('adapter-bun:${Bun.hash(args.path).toString(16)}');`
+ };
+ });
}
};
diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts
index 33ca089f57f9..0c325e3a48c8 100644
--- a/packages/adapter-bun/test/adapter.spec.ts
+++ b/packages/adapter-bun/test/adapter.spec.ts
@@ -1,4 +1,5 @@
import fs from 'node:fs';
+import path from 'node:path';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import adapter from '../index.js';
@@ -11,7 +12,13 @@ const start_file = `${package_dir}/src/start.js`;
vi.mock('node:fs', async (import_original) => {
const actual = await import_original();
- const mocked = { ...actual, readdirSync: vi.fn(), existsSync: vi.fn(), rmSync: vi.fn() };
+ const mocked = {
+ ...actual,
+ readdirSync: vi.fn(),
+ existsSync: vi.fn(),
+ rmSync: vi.fn(),
+ readFileSync: vi.fn()
+ };
return { ...mocked, default: mocked };
});
@@ -28,11 +35,21 @@ const bun = vi.hoisted(() => ({
digest() {
return 'abc';
}
+ },
+ hash: (input: string) => {
+ let hash = 0n;
+ for (const char of input) hash = hash * 31n + BigInt(char.charCodeAt(0));
+ return hash;
}
}));
beforeEach(() => {
- vi.stubGlobal('Bun', { build: bun.build, file: bun.file, CryptoHasher: bun.CryptoHasher });
+ vi.stubGlobal('Bun', {
+ build: bun.build,
+ file: bun.file,
+ CryptoHasher: bun.CryptoHasher,
+ hash: bun.hash
+ });
vi.mocked(fs.readdirSync).mockReturnValue([]);
vi.mocked(fs.existsSync).mockReturnValue(true);
});
@@ -113,7 +130,7 @@ describe('Bun build configuration', () => {
test('resolves generated runtime modules through the Bun plugin', async () => {
await adapter().adapt(create_builder());
const on_resolve = vi.fn();
- bun.build.mock.calls[0][0].plugins[0].setup({ onResolve: on_resolve });
+ bun.build.mock.calls[0][0].plugins[0].setup({ onResolve: on_resolve, onLoad: vi.fn() });
expect(on_resolve).toHaveBeenCalledWith(
{ filter: /^(SERVER|MANIFEST|ROUTES|SERVER_OPTIONS)$/ },
@@ -128,6 +145,57 @@ describe('Bun build configuration', () => {
expect(resolve_module({ path: 'SERVER_OPTIONS' })).toEqual({ path: options_file });
});
+ test('gives side-effect-only chunks a per-importer identity so their copies cannot collide', async () => {
+ await adapter().adapt(create_builder());
+ const on_resolve = vi.fn();
+ const on_load = vi.fn();
+ bun.build.mock.calls[0][0].plugins[0].setup({ onResolve: on_resolve, onLoad: on_load });
+
+ const chunks_dir = path.resolve('.svelte-kit/output/server/chunks');
+ const resolve_chunk = on_resolve.mock.calls.find(
+ ([options]) => String(options.filter) === String(/\.js$/)
+ )![1];
+ const load_chunk = on_load.mock.calls.find(
+ ([options]) => options.namespace === 'adapter-bun-side-effect'
+ )![1];
+
+ vi.mocked(fs.readFileSync).mockReturnValue("import './other.js';\nexport {};\n");
+ const first = resolve_chunk({
+ path: './events.js',
+ resolveDir: chunks_dir,
+ importer: `${chunks_dir}/a.js`
+ });
+ const second = resolve_chunk({
+ path: './events.js',
+ resolveDir: chunks_dir,
+ importer: `${chunks_dir}/b.js`
+ });
+ expect(first.namespace).toBe('adapter-bun-side-effect');
+ expect(first.path.startsWith(`${chunks_dir}/events.js?`)).toBe(true);
+ expect(first.path).not.toBe(second.path);
+ expect(fs.readFileSync).toHaveBeenCalledTimes(1);
+
+ const first_load = load_chunk({ path: first.path });
+ const second_load = load_chunk({ path: second.path });
+ expect(first_load.contents).toContain("import './other.js';");
+ expect(first_load.contents).not.toBe(second_load.contents);
+
+ // chunks with real exports keep their shared identity
+ vi.mocked(fs.readFileSync).mockReturnValue('export const x = 1;\n');
+ expect(
+ resolve_chunk({ path: './real.js', resolveDir: chunks_dir, importer: `${chunks_dir}/a.js` })
+ ).toBeUndefined();
+
+ // modules outside chunks/ are never rewritten
+ expect(
+ resolve_chunk({
+ path: './index.js',
+ resolveDir: path.resolve('.svelte-kit/output/server'),
+ importer: `${chunks_dir}/a.js`
+ })
+ ).toBeUndefined();
+ });
+
test('passes supported advanced options while retaining reserved options', async () => {
await adapter({
out: 'dist',