@orochibraru/svelte-smol 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Svelte Smol Adapter
2
+
3
+ A [SvelteKit](https://svelte.dev/docs/kit) adapter that compiles your app into a
4
+ **single standalone executable** with `bun build --compile`. No `node_modules`,
5
+ no JS files to ship — just one binary plus its static assets.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add -d @orochibraru/svelte-adapter-bun
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ // svelte.config.js
17
+ import adapter from "@orochibraru/svelte-adapter-bun";
18
+
19
+ export default {
20
+ kit: {
21
+ adapter: adapter(),
22
+ },
23
+ };
24
+ ```
25
+
26
+ Because the adapter touches `Bun.*` at config-load time, run the build with the
27
+ Bun runtime:
28
+
29
+ ```bash
30
+ bun --bun vite build
31
+ ```
32
+
33
+ ## Output
34
+
35
+ ```text
36
+ build/
37
+ ├── server # the compiled executable
38
+ ├── client/ # static assets, served by the executable
39
+ └── prerendered/ # prerendered pages, served by the executable
40
+ ```
41
+
42
+ Deploy the whole `build/` directory (or just `server` if a proxy/CDN serves the
43
+ assets — see `serveAssets`). The executable locates `client/` and `prerendered/`
44
+ relative to its own path, so it can be run from any working directory:
45
+
46
+ ```bash
47
+ ./build/server
48
+ ```
49
+
50
+ ## Options
51
+
52
+ ```js
53
+ adapter({
54
+ out: "build", // output directory
55
+ name: "server", // executable filename within `out`
56
+ target: undefined, // cross-compile target, e.g. "bun-linux-x64"
57
+ bytecode: false, // embed a V8 bytecode cache (faster cold start, bigger binary)
58
+ minify: false, // minify the bundled server code
59
+ sourcemap: false, // embed a source map for server stack traces
60
+ precompress: false, // emit + serve .gz / .br sibling files
61
+ envPrefix: "", // prefix for the runtime env vars below
62
+ serveAssets: true, // serve client/ and prerendered/ from the binary
63
+ serveOptions: {}, // extra Bun.serve() options (tls, reusePort, …)
64
+ });
65
+ ```
66
+
67
+ ### Cross-compilation
68
+
69
+ `target` accepts any Bun compile target, e.g. `"bun-linux-x64"`,
70
+ `"bun-linux-arm64-musl"` (Alpine), `"bun-darwin-arm64"`, `"bun-windows-x64"`,
71
+ with optional `-modern` / `-baseline` SIMD suffixes. Bun downloads the matching
72
+ runtime the first time you use a target.
73
+
74
+ ## Runtime environment variables
75
+
76
+ | Variable | Default | Purpose |
77
+ | ------------------ | --------- | ----------------------------------------------------------------------------------------------- |
78
+ | `HOST` | `0.0.0.0` | Listen address |
79
+ | `PORT` | `3000` | Listen port |
80
+ | `SOCKET_PATH` | — | Listen on a Unix socket instead of `HOST`/`PORT` |
81
+ | `ASSETS_DIR` | — | Override where `client/` and `prerendered/` are looked up (absolute, or relative to the binary) |
82
+ | `ORIGIN` | — | Absolute origin used for request URL resolution |
83
+ | `PROTOCOL_HEADER` | — | Header carrying the forwarded protocol (e.g. `x-forwarded-proto`) |
84
+ | `HOST_HEADER` | — | Header carrying the forwarded host |
85
+ | `PORT_HEADER` | — | Header carrying the forwarded port |
86
+ | `ADDRESS_HEADER` | — | Header carrying the client address (e.g. `x-forwarded-for`) |
87
+ | `XFF_DEPTH` | `1` | Trusted-proxy depth when `ADDRESS_HEADER=x-forwarded-for` |
88
+ | `BODY_SIZE_LIMIT` | `512K` | Max request body size (`K`/`M`/`G` suffixes allowed) |
89
+ | `IDLE_TIMEOUT` | `10` | Bun socket idle timeout in seconds (SSE responses opt out) |
90
+ | `SHUTDOWN_TIMEOUT` | `30` | Seconds to wait for in-flight requests on `SIGINT`/`SIGTERM` |
91
+
92
+ Set `envPrefix` to namespace these (`envPrefix: "MY_APP_"` → `MY_APP_PORT`).
93
+
94
+ ## Notes
95
+
96
+ - The SvelteKit server code is JavaScript emitted by Vite; `--compile` embeds it
97
+ in the binary, so nothing but the executable ships. Native (`.node`) modules in
98
+ your dependencies are the one thing that can't be bundled this way.
99
+ - WebSockets, `read()` from `$app/server`, prerendering, and server
100
+ instrumentation are all supported.
package/index.ts ADDED
@@ -0,0 +1,238 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import type { Adapter, Builder } from "@sveltejs/kit";
3
+
4
+ export interface AdapterOptions {
5
+ /**
6
+ * Output directory. Holds the compiled executable plus the `client/` and
7
+ * `prerendered/` folders it serves.
8
+ * @default "build"
9
+ */
10
+ out?: string;
11
+ /**
12
+ * Filename of the compiled executable, written into `out`.
13
+ * @default "server"
14
+ */
15
+ name?: string;
16
+ /**
17
+ * Cross-compilation target for `bun build --compile`, e.g.
18
+ * `"bun-linux-x64"`, `"bun-linux-arm64-musl"`, `"bun-darwin-arm64"`,
19
+ * `"bun-windows-x64"` (optional `-modern` / `-baseline` SIMD suffix, and
20
+ * `-musl` for Alpine). Bun downloads the matching runtime on first use.
21
+ * @default the host platform
22
+ */
23
+ target?: Bun.Build.CompileTarget;
24
+ /**
25
+ * Emit a V8 bytecode cache into the executable for faster cold starts, at
26
+ * the cost of a larger binary.
27
+ * @default false
28
+ */
29
+ bytecode?: boolean;
30
+ /**
31
+ * Minify the bundled server code before embedding it.
32
+ * @default false
33
+ */
34
+ minify?: boolean;
35
+ /**
36
+ * Embed a source map so server stack traces point at original code.
37
+ * @default false
38
+ */
39
+ sourcemap?: boolean;
40
+ /**
41
+ * Pre-compress client assets and prerendered pages to `.gz` / `.br`
42
+ * siblings. The handler serves them with the matching `Content-Encoding`
43
+ * when the request's `Accept-Encoding` allows.
44
+ * @default false
45
+ */
46
+ precompress?: boolean;
47
+ /**
48
+ * Prefix for this adapter's own runtime env vars (`PORT`, `HOST`,
49
+ * `ORIGIN`, ...).
50
+ * @default ""
51
+ */
52
+ envPrefix?: string;
53
+ /**
54
+ * Serve the app's static assets and prerendered pages from the
55
+ * executable. The `client/` and `prerendered/` folders must sit next to
56
+ * the binary (or point `ASSETS_DIR` at their parent). Turn off when a
57
+ * reverse proxy or CDN serves them instead.
58
+ * @default true
59
+ */
60
+ serveAssets?: boolean;
61
+ /**
62
+ * Extra options merged into the `Bun.serve()` call (`tls`, `reusePort`,
63
+ * `maxConnections`, a custom `error` handler, ...). Applied *before* this
64
+ * adapter's own required fields (`idleTimeout`, `maxRequestBodySize`,
65
+ * `fetch`, `hostname`/`port`/`unix`, `websocket`), so it can't be used to
66
+ * override request handling, only to add to it. Use the existing env vars
67
+ * (`IDLE_TIMEOUT`, `BODY_SIZE_LIMIT`, `HOST`/`PORT`, `SOCKET_PATH`,
68
+ * `SHUTDOWN_TIMEOUT`) to change those instead.
69
+ * @default {}
70
+ */
71
+ serveOptions?: Record<string, unknown>;
72
+ }
73
+
74
+ // `node:url`, not `Bun.fileURLToPath`: this module is loaded at config-parse
75
+ // time by Node-based tooling too (svelte-check, the Svelte language server,
76
+ // `svelte-kit sync`), where `Bun` is undefined. Everything that actually needs
77
+ // the Bun runtime lives inside `adapt()`.
78
+ const templates = fileURLToPath(new URL("./templates", import.meta.url));
79
+
80
+ /**
81
+ * SvelteKit adapter that compiles the app into a single standalone executable
82
+ * with `bun build --compile`.
83
+ *
84
+ * `adapt()` writes the SvelteKit server and this package's entrypoint templates
85
+ * into a temp directory, then compiles them — `@sveltejs/kit` and every other
86
+ * pure-JS dependency bundled in — into one binary. The build must run under the
87
+ * Bun runtime (`bun --bun vite build`, or a `bunfig.toml` with `[run] bun =
88
+ * true`); loading the config alone works under Node too.
89
+ *
90
+ * Output, all written to {@link AdapterOptions.out | `out`}:
91
+ *
92
+ * ```text
93
+ * build/
94
+ * ├── server the executable (rename with `name`)
95
+ * ├── client/ static assets, served by the executable
96
+ * └── prerendered/ prerendered pages, served by the executable
97
+ * ```
98
+ *
99
+ * The executable resolves `client/` and `prerendered/` from its own location
100
+ * (`dirname(process.execPath)`, overridable with the `ASSETS_DIR` env var), so
101
+ * it can run from any working directory. Deploy the whole `out` directory, or
102
+ * just the binary when {@link AdapterOptions.serveAssets | `serveAssets`} is
103
+ * off and a proxy/CDN serves the assets.
104
+ *
105
+ * Runtime configuration (`HOST`, `PORT`, `ORIGIN`, `BODY_SIZE_LIMIT`, …) is
106
+ * read from environment variables, optionally namespaced by
107
+ * {@link AdapterOptions.envPrefix | `envPrefix`}.
108
+ *
109
+ * @param options - see {@link AdapterOptions}
110
+ * @returns the configured SvelteKit {@link Adapter}
111
+ *
112
+ * @example
113
+ * ```js
114
+ * // svelte.config.js
115
+ * import adapter from "@orochibraru/svelte-adapter-bun";
116
+ *
117
+ * export default {
118
+ * kit: {
119
+ * adapter: adapter({
120
+ * // cross-compile for an Alpine container from any host
121
+ * target: "bun-linux-x64-musl",
122
+ * bytecode: true,
123
+ * }),
124
+ * },
125
+ * };
126
+ * ```
127
+ *
128
+ * @see {@link https://bun.com/docs/bundler/executables | Bun — Single-file executables}
129
+ */
130
+ export default function adapter(options: AdapterOptions = {}): Adapter {
131
+ const {
132
+ out = "build",
133
+ name = "server",
134
+ target,
135
+ bytecode = false,
136
+ minify = false,
137
+ sourcemap = false,
138
+ precompress = false,
139
+ envPrefix = "",
140
+ serveAssets = true,
141
+ serveOptions = {},
142
+ } = options;
143
+
144
+ return {
145
+ name: "homerun-svelte-adapter-bun",
146
+ supports: {
147
+ instrumentation: () => true,
148
+ read: () => true,
149
+ },
150
+ async adapt(builder: Builder) {
151
+ const { base } = builder.config.kit.paths;
152
+ const tmp = builder.getBuildDirectory("adapter-bun");
153
+
154
+ builder.rimraf(out);
155
+ builder.rimraf(tmp);
156
+ builder.mkdirp(`${out}/`);
157
+ builder.mkdirp(tmp);
158
+
159
+ builder.log.minor("Copying assets");
160
+ builder.writeClient(`${out}/client${base}`);
161
+ builder.writePrerendered(`${out}/prerendered${base}`);
162
+
163
+ if (precompress) {
164
+ builder.log.minor("Compressing assets");
165
+ await Promise.all([
166
+ builder.compress(`${out}/client`),
167
+ builder.compress(`${out}/prerendered`),
168
+ ]);
169
+ }
170
+
171
+ builder.log.minor("Building server");
172
+ builder.writeServer(`${tmp}/server`);
173
+ await Bun.write(
174
+ `${tmp}/server/manifest.js`,
175
+ [
176
+ `export const manifest = ${builder.generateManifest({ relativePath: "./" })};`,
177
+ `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});`,
178
+ `export const base = ${JSON.stringify(base)};`,
179
+ ].join("\n\n"),
180
+ );
181
+
182
+ // Entry templates ship as raw `.ts` — `bun build --compile` runs them
183
+ // straight through. The virtual specifiers (`ENV`, `MANIFEST`,
184
+ // `SERVER`, `HANDLER`) and the `ENV_PREFIX` / `BUILD_OPTIONS` /
185
+ // `SERVE_OPTIONS` tokens are resolved by this raw word-boundary token
186
+ // swap over the copied source.
187
+ builder.log.minor("Copying entrypoint");
188
+ builder.copy(templates, tmp, {
189
+ replace: {
190
+ BUILD_OPTIONS: JSON.stringify({ serveAssets, precompress }),
191
+ ENV: "./env.ts",
192
+ ENV_PREFIX: JSON.stringify(envPrefix),
193
+ HANDLER: "./handler.ts",
194
+ MANIFEST: "./server/manifest.js",
195
+ SERVE_OPTIONS: JSON.stringify(serveOptions),
196
+ SERVER: "./server/index.js",
197
+ },
198
+ });
199
+
200
+ const entry = `${tmp}/index.ts`;
201
+ if (builder.hasServerInstrumentationFile?.()) {
202
+ // Instrumentation (OpenTelemetry &c.) has to load before anything
203
+ // else in the bundle. A compiled binary has no post-build
204
+ // entrypoint to rewrite, so prepend the import to the compile
205
+ // entrypoint instead.
206
+ await Bun.write(
207
+ entry,
208
+ `import "./server/instrumentation.server.js";\n${await Bun.file(entry).text()}`,
209
+ );
210
+ }
211
+
212
+ builder.log.minor(
213
+ target ? `Compiling executable (${target})` : "Compiling executable",
214
+ );
215
+ const outfile = `${out}/${name}`;
216
+ const result = await Bun.build({
217
+ entrypoints: [entry],
218
+ target: "bun",
219
+ minify,
220
+ bytecode,
221
+ sourcemap: sourcemap ? "linked" : "none",
222
+ compile: {
223
+ outfile,
224
+ ...(target ? { target } : {}),
225
+ },
226
+ });
227
+
228
+ if (!result.success) {
229
+ for (const message of result.logs) {
230
+ builder.log.error(String(message));
231
+ }
232
+ throw new Error("`bun build --compile` failed");
233
+ }
234
+
235
+ builder.log.success(`Compiled ${outfile}`);
236
+ },
237
+ };
238
+ }
package/internal.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Ambient declarations for the virtual specifiers/globals `templates/*.ts`
2
+ // reference. None of these are real modules or runtime globals at
3
+ // type-check time, `index.ts`'s adapt() resolves them via a raw text
4
+ // replacement over the compiled output (see its `builder.copy(..., {
5
+ // replace })` call), so this file exists purely so `templates/*.ts`
6
+ // typechecks against the shape that replacement actually produces.
7
+
8
+ declare module "ENV" {
9
+ export function env(name: string, fallback: string): string;
10
+ export function env(name: string, fallback: false): string | false;
11
+ export function env(name: string, fallback?: undefined): string | undefined;
12
+ }
13
+
14
+ declare module "HANDLER" {
15
+ export function getHandler(): {
16
+ fetch: (
17
+ request: Request,
18
+ server: Bun.Server<undefined>,
19
+ ) => Response | Promise<Response>;
20
+ websocket: Bun.WebSocketHandler<undefined> | undefined;
21
+ };
22
+ }
23
+
24
+ declare module "MANIFEST" {
25
+ import type { SSRManifest } from "@sveltejs/kit";
26
+
27
+ export const base: string;
28
+ export const manifest: SSRManifest;
29
+ export const prerendered: Set<string>;
30
+ }
31
+
32
+ declare module "SERVER" {
33
+ export { Server } from "@sveltejs/kit";
34
+ }
35
+
36
+ declare const ENV_PREFIX: string;
37
+ declare const BUILD_OPTIONS: { serveAssets: boolean; precompress: boolean };
38
+ declare const SERVE_OPTIONS: Record<string, unknown>;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@orochibraru/svelte-smol",
3
+ "version": "1.3.0",
4
+ "type": "module",
5
+ "main": "./index.ts",
6
+ "module": "./index.ts",
7
+ "types": "./index.ts",
8
+ "author": {
9
+ "email": "orochibraru@gmail.com",
10
+ "name": "orochibraru"
11
+ },
12
+ "exports": {
13
+ ".": "./index.ts"
14
+ },
15
+ "files": [
16
+ "index.ts",
17
+ "internal.d.ts",
18
+ "templates"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.coms/orochibraru/svelte-smol.git"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "typecheck": "tsc --noEmit -p tsconfig.json",
29
+ "test": "bun test index.test.ts",
30
+ "test:integration": "bun test test/integration.test.ts",
31
+ "lint": "biome check .",
32
+ "lint:fix": "biome check --write .",
33
+ "release": "semantic-release",
34
+ "release:dry": "semantic-release --dry-run --no-ci",
35
+ "prepare": "husky"
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "^2.5.11",
39
+ "@saithodev/semantic-release-gitea": "^2.1.0",
40
+ "@semantic-release/changelog": "^7.0.0",
41
+ "@semantic-release/git": "^11.0.1",
42
+ "@semantic-release/npm": "^13.1.5",
43
+ "@sveltejs/kit": "^2.70.3",
44
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
45
+ "@types/bun": "^1.4.0",
46
+ "husky": "^9.1.7",
47
+ "semantic-release": "^25.0.9",
48
+ "svelte": "^5.56.10",
49
+ "typescript": "^7",
50
+ "vite": "^8.2.2"
51
+ },
52
+ "peerDependencies": {
53
+ "@sveltejs/kit": "^2.0.0"
54
+ }
55
+ }
@@ -0,0 +1,38 @@
1
+ /* global ENV_PREFIX */
2
+
3
+ const expected = new Set([
4
+ "SOCKET_PATH",
5
+ "HOST",
6
+ "PORT",
7
+ "ASSETS_DIR",
8
+ "ORIGIN",
9
+ "XFF_DEPTH",
10
+ "ADDRESS_HEADER",
11
+ "PROTOCOL_HEADER",
12
+ "HOST_HEADER",
13
+ "PORT_HEADER",
14
+ "BODY_SIZE_LIMIT",
15
+ "IDLE_TIMEOUT",
16
+ "SHUTDOWN_TIMEOUT",
17
+ ]);
18
+
19
+ if (ENV_PREFIX) {
20
+ for (const name in Bun.env) {
21
+ if (name.startsWith(ENV_PREFIX)) {
22
+ const unprefixed = name.slice(ENV_PREFIX.length);
23
+ if (!expected.has(unprefixed)) {
24
+ throw new Error(
25
+ `You should change envPrefix (${ENV_PREFIX}) to avoid conflicts with existing environment variables — unexpectedly saw ${name}`,
26
+ );
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ export function env(name: string, fallback: string): string;
33
+ export function env(name: string, fallback: false): string | false;
34
+ export function env(name: string, fallback?: undefined): string | undefined;
35
+ export function env(name: string, fallback?: string | false) {
36
+ const prefixed = ENV_PREFIX + name;
37
+ return prefixed in Bun.env ? Bun.env[prefixed] : fallback;
38
+ }
@@ -0,0 +1,215 @@
1
+ /* global ENV_PREFIX, BUILD_OPTIONS */
2
+
3
+ import { env } from "ENV";
4
+ import { base, manifest, prerendered } from "MANIFEST";
5
+ import { Server } from "SERVER";
6
+ import { statSync } from "node:fs";
7
+ import { dirname, resolve } from "node:path";
8
+ import type { Server as SvelteKitServer } from "@sveltejs/kit";
9
+
10
+ const server = new Server(manifest) as SvelteKitServer & {
11
+ websocket?: () => Bun.WebSocketHandler<undefined> | undefined;
12
+ };
13
+
14
+ const { serveAssets, precompress } = BUILD_OPTIONS;
15
+
16
+ const origin = env("ORIGIN", undefined);
17
+ const xff_depth = Number.parseInt(env("XFF_DEPTH", "1"), 10);
18
+ const address_header = env("ADDRESS_HEADER", "").toLowerCase();
19
+ const protocol_header = env("PROTOCOL_HEADER", "").toLowerCase();
20
+ const host_header = env("HOST_HEADER", "").toLowerCase();
21
+ const port_header = env("PORT_HEADER", "").toLowerCase();
22
+
23
+ // Static assets and prerendered pages are deployed next to the executable:
24
+ // <dir>/<binary>
25
+ // <dir>/client/…
26
+ // <dir>/prerendered/…
27
+ // `ASSETS_DIR` overrides that parent directory (absolute, or relative to the
28
+ // binary). `import.meta.dir` is a virtual path inside a compiled binary, so
29
+ // the real on-disk location comes from `process.execPath`.
30
+ const assets_root = resolve(dirname(process.execPath), env("ASSETS_DIR", ""));
31
+ const client_dir = `${assets_root}/client${base}`;
32
+ const prerendered_dir = `${assets_root}/prerendered${base}`;
33
+ const immutable_prefix = `${base}/${manifest.appDir}/immutable/`;
34
+
35
+ await server.init({
36
+ env: Bun.env as Record<string, string>,
37
+ read: (file) => Bun.file(`${client_dir}/${file}`).stream(),
38
+ });
39
+
40
+ // Pre-compressed sibling files, tried in this order when `precompress` is on.
41
+ const encodings: Array<readonly [token: string, ext: string]> = precompress
42
+ ? [
43
+ ["br", ".br"],
44
+ ["gzip", ".gz"],
45
+ ]
46
+ : [];
47
+
48
+ /** `statSync` that yields `undefined` for a missing path or a non-file. */
49
+ function file_stat(path: string) {
50
+ try {
51
+ const stat = statSync(path);
52
+ return stat.isFile() ? stat : undefined;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ function asset_response(
59
+ file: string,
60
+ request: Request,
61
+ immutable = false,
62
+ ): Response | undefined {
63
+ let stat = file_stat(file);
64
+ if (!stat) {
65
+ return undefined;
66
+ }
67
+
68
+ const headers = new Headers();
69
+ headers.set(
70
+ "cache-control",
71
+ immutable
72
+ ? "public,max-age=31536000,immutable"
73
+ : "public,max-age=0,must-revalidate",
74
+ );
75
+
76
+ let body = file;
77
+ const accept = request.headers.get("accept-encoding") ?? "";
78
+ for (const [token, ext] of encodings) {
79
+ const compressed = accept.includes(token) && file_stat(`${file}${ext}`);
80
+ if (compressed) {
81
+ body = `${file}${ext}`;
82
+ stat = compressed;
83
+ headers.set("content-encoding", token);
84
+ headers.set("vary", "Accept-Encoding");
85
+ // The compressed sibling has no useful media type of its own.
86
+ headers.set("content-type", Bun.file(file).type);
87
+ break;
88
+ }
89
+ }
90
+
91
+ const { size, mtimeMs } = stat;
92
+ const etag = `W/"${size.toString(16)}-${Math.round(mtimeMs).toString(16)}"`;
93
+ if (request.headers.get("if-none-match") === etag) {
94
+ return new Response(null, { headers, status: 304 });
95
+ }
96
+ headers.set("etag", etag);
97
+
98
+ return new Response(Bun.file(body), { headers });
99
+ }
100
+
101
+ function serve_static(url: URL, request: Request): Response | undefined {
102
+ if (!serveAssets) {
103
+ return undefined;
104
+ }
105
+
106
+ const { pathname } = url;
107
+ const rel = pathname.startsWith(base)
108
+ ? pathname.slice(base.length)
109
+ : pathname;
110
+
111
+ if (prerendered.has(pathname)) {
112
+ const file =
113
+ rel === "/" || rel === ""
114
+ ? "index.html"
115
+ : rel.endsWith("/")
116
+ ? `${rel.slice(1)}index.html`
117
+ : `${rel.slice(1)}.html`;
118
+ return asset_response(`${prerendered_dir}/${file}`, request);
119
+ }
120
+
121
+ // The other trailing-slash form of a prerendered path redirects to the
122
+ // canonical one, query string preserved (matches SvelteKit's own
123
+ // trailingSlash handling).
124
+ const toggled = pathname.endsWith("/")
125
+ ? pathname.slice(0, -1)
126
+ : `${pathname}/`;
127
+ if (prerendered.has(toggled)) {
128
+ return new Response(null, {
129
+ headers: { location: toggled + url.search },
130
+ status: 308,
131
+ });
132
+ }
133
+
134
+ return asset_response(
135
+ `${client_dir}/${rel.slice(1)}`,
136
+ request,
137
+ pathname.startsWith(immutable_prefix),
138
+ );
139
+ }
140
+
141
+ const ssr = async (request: Request, bunServer: Bun.Server<undefined>) => {
142
+ const url = new URL(request.url);
143
+
144
+ const asset = serve_static(url, request);
145
+ if (asset) {
146
+ return asset;
147
+ }
148
+
149
+ const baseOrigin = origin || get_origin(request.headers);
150
+ const path = request.url.slice(request.url.split("/", 3).join("/").length);
151
+ const newRequest = new Request(baseOrigin + path, request);
152
+
153
+ const response = await server.respond(newRequest, {
154
+ getClientAddress() {
155
+ if (address_header) {
156
+ if (!request.headers.has(address_header)) {
157
+ throw new Error(
158
+ `Address header was specified with ${`${ENV_PREFIX}ADDRESS_HEADER`}=${address_header} but is absent from request`,
159
+ );
160
+ }
161
+
162
+ const value = request.headers.get(address_header) || "";
163
+
164
+ if (address_header === "x-forwarded-for") {
165
+ const addresses = value.split(",");
166
+
167
+ if (xff_depth < 1) {
168
+ throw new Error(
169
+ `${`${ENV_PREFIX}XFF_DEPTH`} must be a positive integer`,
170
+ );
171
+ }
172
+
173
+ if (xff_depth > addresses.length) {
174
+ throw new Error(
175
+ `${`${ENV_PREFIX}XFF_DEPTH`} is ${xff_depth}, but only found ${addresses.length} addresses`,
176
+ );
177
+ }
178
+ return addresses[addresses.length - xff_depth]?.trim() || "";
179
+ }
180
+
181
+ return value;
182
+ }
183
+
184
+ return bunServer.requestIP(request)?.address || "";
185
+ },
186
+ platform: { request, server: bunServer },
187
+ });
188
+
189
+ // SvelteKit streams Server-Sent Events with no Content-Length and long
190
+ // gaps between writes; Bun's idleTimeout (default 10s) would sever the
191
+ // connection between events. Disable it for the life of this response.
192
+ if (response.headers.get("content-type")?.startsWith("text/event-stream")) {
193
+ bunServer.timeout(request, 0);
194
+ }
195
+
196
+ return response;
197
+ };
198
+
199
+ export const getHandler = () => {
200
+ const websocket =
201
+ typeof server.websocket === "function" ? server.websocket() : undefined;
202
+
203
+ return {
204
+ fetch: ssr,
205
+ websocket,
206
+ };
207
+ };
208
+
209
+ function get_origin(headers: Headers) {
210
+ const protocol = (protocol_header && headers.get(protocol_header)) || "https";
211
+ const host = (host_header && headers.get(host_header)) || headers.get("host");
212
+ const port = port_header && headers.get(port_header);
213
+
214
+ return port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
215
+ }
@@ -0,0 +1,76 @@
1
+ import { env } from "ENV";
2
+ import { getHandler } from "HANDLER";
3
+ import process from "node:process";
4
+
5
+ export const path = env("SOCKET_PATH", false);
6
+ export const host = env("HOST", "0.0.0.0");
7
+ export const port = env("PORT", "3000");
8
+
9
+ const body_size_limit = parse_as_bytes(env("BODY_SIZE_LIMIT", "512K"));
10
+ if (Number.isNaN(body_size_limit)) {
11
+ throw new Error(
12
+ `Invalid BODY_SIZE_LIMIT: '${env("BODY_SIZE_LIMIT", "512K")}'. Please provide a numeric value.`,
13
+ );
14
+ }
15
+
16
+ const idle_timeout = Number.parseInt(env("IDLE_TIMEOUT", "10"), 10);
17
+ const { fetch: handlerFetch, websocket } = getHandler();
18
+
19
+ const options = {
20
+ ...SERVE_OPTIONS,
21
+ fetch: handlerFetch,
22
+ idleTimeout: idle_timeout,
23
+ maxRequestBodySize: body_size_limit,
24
+ ...(path ? { unix: path } : { hostname: host, port: port }),
25
+ ...(websocket ? { websocket } : {}),
26
+ };
27
+
28
+ const server = Bun.serve(options as Parameters<typeof Bun.serve>[0]);
29
+
30
+ console.log(`Listening on ${server.url} ${websocket ? "with WebSocket" : ""}`);
31
+
32
+ const shutdown_timeout_ms =
33
+ Number.parseInt(env("SHUTDOWN_TIMEOUT", "30"), 10) * 1000;
34
+ let shutting_down = false;
35
+
36
+ async function graceful_shutdown(reason: "SIGINT" | "SIGTERM" | "IDLE") {
37
+ if (shutting_down) {
38
+ console.info(`Received ${reason} again, forcing immediate shutdown.`);
39
+ process.exit(1);
40
+ }
41
+ shutting_down = true;
42
+
43
+ console.info(
44
+ `Stopping server (waiting up to ${shutdown_timeout_ms / 1000}s for in-flight requests to finish)...`,
45
+ );
46
+ process.emit("sveltekit:shutdown", reason);
47
+
48
+ const force_timer = setTimeout(() => {
49
+ console.warn(
50
+ `Graceful shutdown exceeded ${shutdown_timeout_ms / 1000}s, forcing.`,
51
+ );
52
+ server.stop(true).finally(() => process.exit(1));
53
+ }, shutdown_timeout_ms);
54
+
55
+ await server.stop();
56
+ clearTimeout(force_timer);
57
+ console.info("Stopped server");
58
+ process.exit(0);
59
+ }
60
+
61
+ process.on("SIGTERM", graceful_shutdown);
62
+ process.on("SIGINT", graceful_shutdown);
63
+
64
+ export { server };
65
+
66
+ function parse_as_bytes(value: string): number {
67
+ const units = value.at(-1)?.toUpperCase();
68
+ const multiplier =
69
+ {
70
+ B: 1,
71
+ G: 1024 * 1024 * 1024,
72
+ K: 1024,
73
+ M: 1024 * 1024,
74
+ }[units ?? "B"] ?? 1;
75
+ return Number(multiplier !== 1 ? value.slice(0, -1) : value) * multiplier;
76
+ }