@orochibraru/svelte-smol 1.3.1 → 1.3.2

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 CHANGED
@@ -2,19 +2,19 @@
2
2
 
3
3
  A [SvelteKit](https://svelte.dev/docs/kit) adapter that compiles your app into a
4
4
  **single standalone executable** with `bun build --compile`. No `node_modules`,
5
- no JS files to ship just one binary plus its static assets.
5
+ no JS files to ship, just one binary plus its static assets.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- bun add -d @orochibraru/svelte-adapter-bun
10
+ bun add -d @orochibraru/svelte-smol
11
11
  ```
12
12
 
13
13
  ## Usage
14
14
 
15
15
  ```js
16
16
  // svelte.config.js
17
- import adapter from "@orochibraru/svelte-smol;
17
+ import adapter from "@orochibraru/svelte-smol";
18
18
 
19
19
  export default {
20
20
  kit: {
@@ -23,11 +23,10 @@ export default {
23
23
  };
24
24
  ```
25
25
 
26
- Because the adapter touches `Bun.*` at config-load time, run the build with the
27
- Bun runtime:
26
+ The compile step runs under the Bun runtime, so build with:
28
27
 
29
28
  ```bash
30
- bun --bun vite build
29
+ bun run vite build
31
30
  ```
32
31
 
33
32
  ## Output
@@ -40,7 +39,7 @@ build/
40
39
  ```
41
40
 
42
41
  Deploy the whole `build/` directory (or just `server` if a proxy/CDN serves the
43
- assets see `serveAssets`). The executable locates `client/` and `prerendered/`
42
+ assets, see `serveAssets`). The executable locates `client/` and `prerendered/`
44
43
  relative to its own path, so it can be run from any working directory:
45
44
 
46
45
  ```bash
@@ -58,6 +57,7 @@ adapter({
58
57
  minify: false, // minify the bundled server code
59
58
  sourcemap: false, // embed a source map for server stack traces
60
59
  precompress: false, // emit + serve .gz / .br sibling files
60
+ healthcheck: true, // also compile `build/healthcheck` + expose GET /_health
61
61
  envPrefix: "", // prefix for the runtime env vars below
62
62
  serveAssets: true, // serve client/ and prerendered/ from the binary
63
63
  serveOptions: {}, // extra Bun.serve() options (tls, reusePort, …)
@@ -88,9 +88,27 @@ runtime the first time you use a target.
88
88
  | `BODY_SIZE_LIMIT` | `512K` | Max request body size (`K`/`M`/`G` suffixes allowed) |
89
89
  | `IDLE_TIMEOUT` | `10` | Bun socket idle timeout in seconds (SSE responses opt out) |
90
90
  | `SHUTDOWN_TIMEOUT` | `30` | Seconds to wait for in-flight requests on `SIGINT`/`SIGTERM` |
91
+ | `HEALTHCHECK_PATH` | `/_health`| Endpoint the `healthcheck` binary probes (must match the `healthcheck` option) |
92
+ | `HEALTHCHECK_TIMEOUT` | `2000` | `healthcheck` binary request timeout in ms |
91
93
 
92
94
  Set `envPrefix` to namespace these (`envPrefix: "MY_APP_"` → `MY_APP_PORT`).
93
95
 
96
+ ## Health check
97
+
98
+ With `healthcheck` enabled (the default) the build also produces
99
+ `build/healthcheck` — a tiny executable that requests `GET /_health` over
100
+ loopback (or the Unix socket) and exits `0` when the server answers `200`,
101
+ `1` otherwise. `GET /_health` returns `{ "status": "ok", uptime, rss, pid,
102
+ timestamp }`. Drop it straight into Docker:
103
+
104
+ ```dockerfile
105
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
106
+ CMD ["./build/healthcheck"]
107
+ ```
108
+
109
+ It reads the same `HOST` / `PORT` / `SOCKET_PATH` as the server, so no extra
110
+ wiring is needed.
111
+
94
112
  ## Notes
95
113
 
96
114
  - The SvelteKit server code is JavaScript emitted by Vite; `--compile` embeds it
@@ -98,3 +116,14 @@ Set `envPrefix` to namespace these (`envPrefix: "MY_APP_"` → `MY_APP_PORT`).
98
116
  your dependencies are the one thing that can't be bundled this way.
99
117
  - WebSockets, `read()` from `$app/server`, prerendering, and server
100
118
  instrumentation are all supported.
119
+
120
+ ## Releases
121
+
122
+ Automated by [semantic-release](https://semantic-release.gitbook.io/) from
123
+ [Conventional Commits](https://www.conventionalcommits.org/):
124
+
125
+ - `fix:` / `perf:` → **patch**, `feat:` → **minor**, `feat!:` or a
126
+ `BREAKING CHANGE:` footer → **major**
127
+ - `docs:` `refactor:` `test:` `chore:` `build:` `ci:` `style:` → **no release**
128
+ - `feat:` / `fix:` scoped to `ci`, `build`, `deps`, `dev`, `repo`, `test`,
129
+ `example`, `release` → **no release** (they don't touch the published package)
package/index.ts CHANGED
@@ -44,6 +44,15 @@ export interface AdapterOptions {
44
44
  * @default false
45
45
  */
46
46
  precompress?: boolean;
47
+ /**
48
+ * Compile a second tiny executable, `healthcheck`, alongside the server
49
+ * and expose a matching `GET` endpoint. The binary probes that endpoint
50
+ * over loopback (or the Unix socket) and exits `0` when healthy, `1`
51
+ * otherwise — ready to drop into a Docker `HEALTHCHECK`. Pass an object
52
+ * to change the endpoint path.
53
+ * @default true
54
+ */
55
+ healthcheck?: boolean | { path?: string };
47
56
  /**
48
57
  * Prefix for this adapter's own runtime env vars (`PORT`, `HOST`,
49
58
  * `ORIGIN`, ...).
@@ -112,7 +121,7 @@ const templates = fileURLToPath(new URL("./templates", import.meta.url));
112
121
  * @example
113
122
  * ```js
114
123
  * // svelte.config.js
115
- * import adapter from "@orochibraru/svelte-smol;
124
+ * import adapter from "@orochibraru/svelte-smol";
116
125
  *
117
126
  * export default {
118
127
  * kit: {
@@ -136,13 +145,22 @@ export default function adapter(options: AdapterOptions = {}): Adapter {
136
145
  minify = false,
137
146
  sourcemap = false,
138
147
  precompress = false,
148
+ healthcheck = true,
139
149
  envPrefix = "",
140
150
  serveAssets = true,
141
151
  serveOptions = {},
142
152
  } = options;
143
153
 
154
+ const healthcheckConfig =
155
+ healthcheck === false
156
+ ? false
157
+ : {
158
+ path:
159
+ (healthcheck === true ? undefined : healthcheck.path) ?? "/_health",
160
+ };
161
+
144
162
  return {
145
- name: "homerun-svelte-adapter-bun",
163
+ name: "@orochibraru/svelte-smol",
146
164
  supports: {
147
165
  instrumentation: () => true,
148
166
  read: () => true,
@@ -187,7 +205,11 @@ export default function adapter(options: AdapterOptions = {}): Adapter {
187
205
  builder.log.minor("Copying entrypoint");
188
206
  builder.copy(templates, tmp, {
189
207
  replace: {
190
- BUILD_OPTIONS: JSON.stringify({ serveAssets, precompress }),
208
+ BUILD_OPTIONS: JSON.stringify({
209
+ serveAssets,
210
+ precompress,
211
+ healthcheck: healthcheckConfig,
212
+ }),
191
213
  ENV: "./env.ts",
192
214
  ENV_PREFIX: JSON.stringify(envPrefix),
193
215
  HANDLER: "./handler.ts",
@@ -209,30 +231,36 @@ export default function adapter(options: AdapterOptions = {}): Adapter {
209
231
  );
210
232
  }
211
233
 
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
- });
234
+ const compile = async (entrypoint: string, outName: string) => {
235
+ builder.log.minor(
236
+ target ? `Compiling ${outName} (${target})` : `Compiling ${outName}`,
237
+ );
238
+ const result = await Bun.build({
239
+ entrypoints: [entrypoint],
240
+ target: "bun",
241
+ minify,
242
+ bytecode,
243
+ sourcemap: sourcemap ? "linked" : "none",
244
+ compile: {
245
+ outfile: `${out}/${outName}`,
246
+ ...(target ? { target } : {}),
247
+ },
248
+ });
227
249
 
228
- if (!result.success) {
229
- for (const message of result.logs) {
230
- builder.log.error(String(message));
250
+ if (!result.success) {
251
+ for (const message of result.logs) {
252
+ builder.log.error(String(message));
253
+ }
254
+ throw new Error(`\`bun build --compile\` failed for ${outName}`);
231
255
  }
232
- throw new Error("`bun build --compile` failed");
233
- }
234
256
 
235
- builder.log.success(`Compiled ${outfile}`);
257
+ builder.log.success(`Compiled ${out}/${outName}`);
258
+ };
259
+
260
+ await compile(entry, name);
261
+ if (healthcheckConfig) {
262
+ await compile(`${tmp}/healthcheck.ts`, "healthcheck");
263
+ }
236
264
  },
237
265
  };
238
266
  }
package/internal.d.ts CHANGED
@@ -34,5 +34,9 @@ declare module "SERVER" {
34
34
  }
35
35
 
36
36
  declare const ENV_PREFIX: string;
37
- declare const BUILD_OPTIONS: { serveAssets: boolean; precompress: boolean };
37
+ declare const BUILD_OPTIONS: {
38
+ serveAssets: boolean;
39
+ precompress: boolean;
40
+ healthcheck: false | { path: string };
41
+ };
38
42
  declare const SERVE_OPTIONS: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orochibraru/svelte-smol",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "main": "./index.ts",
6
6
  "module": "./index.ts",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.coms/orochibraru/svelte-smol.git"
22
+ "url": "git+https://github.com/orochibraru/svelte-smol.git"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
@@ -36,13 +36,14 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@biomejs/biome": "^2.5.11",
39
- "@saithodev/semantic-release-gitea": "^2.1.0",
40
39
  "@semantic-release/changelog": "^7.0.0",
41
40
  "@semantic-release/git": "^11.0.1",
41
+ "@semantic-release/github": "^12.0.9",
42
42
  "@semantic-release/npm": "^13.1.5",
43
43
  "@sveltejs/kit": "^2.70.3",
44
44
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
45
45
  "@types/bun": "^1.4.0",
46
+ "conventional-changelog-conventionalcommits": "^10.4.0",
46
47
  "husky": "^9.1.7",
47
48
  "semantic-release": "^25.0.9",
48
49
  "svelte": "^5.56.10",
package/templates/env.ts CHANGED
@@ -14,6 +14,8 @@ const expected = new Set([
14
14
  "BODY_SIZE_LIMIT",
15
15
  "IDLE_TIMEOUT",
16
16
  "SHUTDOWN_TIMEOUT",
17
+ "HEALTHCHECK_PATH",
18
+ "HEALTHCHECK_TIMEOUT",
17
19
  ]);
18
20
 
19
21
  if (ENV_PREFIX) {
@@ -11,7 +11,7 @@ const server = new Server(manifest) as SvelteKitServer & {
11
11
  websocket?: () => Bun.WebSocketHandler<undefined> | undefined;
12
12
  };
13
13
 
14
- const { serveAssets, precompress } = BUILD_OPTIONS;
14
+ const { serveAssets, precompress, healthcheck } = BUILD_OPTIONS;
15
15
 
16
16
  const origin = env("ORIGIN", undefined);
17
17
  const xff_depth = Number.parseInt(env("XFF_DEPTH", "1"), 10);
@@ -141,6 +141,23 @@ function serve_static(url: URL, request: Request): Response | undefined {
141
141
  const ssr = async (request: Request, bunServer: Bun.Server<undefined>) => {
142
142
  const url = new URL(request.url);
143
143
 
144
+ if (
145
+ healthcheck &&
146
+ request.method === "GET" &&
147
+ url.pathname === healthcheck.path
148
+ ) {
149
+ return Response.json(
150
+ {
151
+ status: "ok",
152
+ uptime: Math.round(process.uptime()),
153
+ rss: process.memoryUsage.rss(),
154
+ pid: process.pid,
155
+ timestamp: new Date().toISOString(),
156
+ },
157
+ { headers: { "cache-control": "no-store" } },
158
+ );
159
+ }
160
+
144
161
  const asset = serve_static(url, request);
145
162
  if (asset) {
146
163
  return asset;
@@ -0,0 +1,55 @@
1
+ /* global BUILD_OPTIONS */
2
+
3
+ import { env } from "ENV";
4
+ import process from "node:process";
5
+
6
+ const { healthcheck } = BUILD_OPTIONS;
7
+
8
+ if (!healthcheck) {
9
+ console.error("healthcheck is disabled for this build");
10
+ process.exit(2);
11
+ }
12
+
13
+ const socket = env("SOCKET_PATH", false);
14
+ const timeout_ms = Number.parseInt(env("HEALTHCHECK_TIMEOUT", "2000"), 10);
15
+ const path = env("HEALTHCHECK_PATH", healthcheck.path);
16
+
17
+ // A server bound to a wildcard address is reached over loopback.
18
+ const raw_host = env("HOST", "0.0.0.0");
19
+ const host =
20
+ raw_host === "0.0.0.0" || raw_host === "::" || raw_host === ""
21
+ ? "127.0.0.1"
22
+ : raw_host;
23
+ const port = env("PORT", "3000");
24
+
25
+ const url = socket
26
+ ? `http://localhost${path}`
27
+ : `http://${host.includes(":") ? `[${host}]` : host}:${port}${path}`;
28
+
29
+ function fail(reason: string): never {
30
+ console.error(`unhealthy: ${reason}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ try {
35
+ const response = await fetch(url, {
36
+ headers: { "user-agent": "svelte-smol-healthcheck" },
37
+ signal: AbortSignal.timeout(timeout_ms),
38
+ ...(socket ? { unix: socket } : {}),
39
+ });
40
+
41
+ if (!response.ok) {
42
+ fail(`${url} -> ${response.status}`);
43
+ }
44
+
45
+ const body = (await response.json().catch(() => null)) as {
46
+ status?: string;
47
+ } | null;
48
+ if (body?.status && body.status !== "ok") {
49
+ fail(`status=${body.status}`);
50
+ }
51
+
52
+ process.exit(0);
53
+ } catch (error) {
54
+ fail(`${url} -> ${error instanceof Error ? error.message : String(error)}`);
55
+ }
@@ -25,12 +25,26 @@ const options = {
25
25
  ...(websocket ? { websocket } : {}),
26
26
  };
27
27
 
28
+ const shutdown_timeout_ms =
29
+ Number.parseInt(env("SHUTDOWN_TIMEOUT", "30"), 10) * 1000;
30
+
28
31
  const server = Bun.serve(options as Parameters<typeof Bun.serve>[0]);
29
32
 
30
- console.log(`Listening on ${server.url} ${websocket ? "with WebSocket" : ""}`);
33
+ const rows: Array<[string, string]> = [
34
+ ["Listening on", path ? `unix:${path}` : `${server.url}`],
35
+ ["WebSocket", websocket ? "enabled" : "disabled"],
36
+ ["Body limit", format_bytes(body_size_limit)],
37
+ ["Idle timeout", `${idle_timeout}s`],
38
+ ["Shutdown grace", `${shutdown_timeout_ms / 1000}s`],
39
+ ["Runtime", `Bun ${Bun.version} (${process.platform}/${process.arch})`],
40
+ ["PID", `${process.pid}`],
41
+ ];
42
+ console.log(
43
+ `\n SvelteKit server ready\n\n${rows
44
+ .map(([label, value]) => ` ${`${label}:`.padEnd(16)}${value}`)
45
+ .join("\n")}\n`,
46
+ );
31
47
 
32
- const shutdown_timeout_ms =
33
- Number.parseInt(env("SHUTDOWN_TIMEOUT", "30"), 10) * 1000;
34
48
  let shutting_down = false;
35
49
 
36
50
  async function graceful_shutdown(reason: "SIGINT" | "SIGTERM" | "IDLE") {
@@ -74,3 +88,14 @@ function parse_as_bytes(value: string): number {
74
88
  }[units ?? "B"] ?? 1;
75
89
  return Number(multiplier !== 1 ? value.slice(0, -1) : value) * multiplier;
76
90
  }
91
+
92
+ function format_bytes(bytes: number): string {
93
+ const units = ["B", "KB", "MB", "GB"];
94
+ let value = bytes;
95
+ let unit = 0;
96
+ while (value >= 1024 && unit < units.length - 1) {
97
+ value /= 1024;
98
+ unit++;
99
+ }
100
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;
101
+ }