bosia 0.8.14 → 0.8.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.8.14",
3
+ "version": "0.8.15",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/cli/start.ts CHANGED
@@ -26,5 +26,7 @@ export async function runStart() {
26
26
  // mid-drain. The terminal delivers SIGINT to the whole process group,
27
27
  // so the child already gets the signal — no forwarding needed.
28
28
  for (const sig of ["SIGINT", "SIGTERM"] as const) process.on(sig, () => {});
29
- await proc.exited;
29
+ // Propagate the child's code — a refused boot must not report success to CI,
30
+ // Docker restart policies or orchestrators.
31
+ process.exit(await proc.exited);
30
32
  }
package/src/core/dev.ts CHANGED
@@ -3,6 +3,7 @@ import { readdirSync, statSync, watch, type Dirent } from "fs";
3
3
  import { join } from "path";
4
4
  import { loadEnv, resetDeclaredKeys } from "./env.ts";
5
5
  import { BOSIA_NODE_PATH } from "./paths.ts";
6
+ import { pidsOnPort } from "./port.ts";
6
7
  import { affectsRouteManifest, shouldIgnoreForRebuild } from "./devWatch.ts";
7
8
 
8
9
  // Dev always writes to .bosia/dev so a parallel `bun run build` (writing to ./dist)
@@ -159,26 +160,16 @@ const APP_PORT = DEV_PORT + 1; // internal, hidden from user
159
160
  // disk → "ENOENT reading +page-<hash>.js" 500s. Reap whatever is listening on
160
161
  // APP_PORT before we spawn, so a stale child can never shadow the fresh build.
161
162
  async function reapStaleAppServer(port: number) {
162
- try {
163
- const proc = spawn(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
164
- stdout: "pipe",
165
- stderr: "ignore",
166
- });
167
- const out = await new Response(proc.stdout).text();
168
- await proc.exited;
169
- const pids = [...new Set(out.split("\n").map((s) => s.trim()))]
170
- .map(Number)
171
- .filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
172
- for (const pid of pids) {
173
- try {
174
- process.kill(pid, "SIGKILL");
175
- console.log(`🧹 Reaped stale app server holding port ${port} (pid ${pid})`);
176
- } catch {}
177
- }
178
- if (pids.length) await Bun.sleep(200); // let the port free before we bind
179
- } catch {
180
- // lsof unavailable or errored — fail open; a real conflict surfaces on bind.
163
+ // pidsOnPort fails open when lsof is unavailable — a real conflict then
164
+ // surfaces on bind instead.
165
+ const pids = (await pidsOnPort(port)).filter((pid) => pid !== process.pid);
166
+ for (const pid of pids) {
167
+ try {
168
+ process.kill(pid, "SIGKILL");
169
+ console.log(`🧹 Reaped stale app server holding port ${port} (pid ${pid})`);
170
+ } catch {}
181
171
  }
172
+ if (pids.length) await Bun.sleep(200); // let the port free before we bind
182
173
  }
183
174
 
184
175
  async function startAppServer() {
@@ -0,0 +1,16 @@
1
+ /** PIDs listening on `port`. Fails open to [] — lsof may be absent. */
2
+ export async function pidsOnPort(port: number): Promise<number[]> {
3
+ try {
4
+ const proc = Bun.spawn(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
5
+ stdout: "pipe",
6
+ stderr: "ignore",
7
+ });
8
+ const out = await new Response(proc.stdout).text();
9
+ await proc.exited;
10
+ return [...new Set(out.split("\n").map((s) => s.trim()))]
11
+ .map(Number)
12
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
13
+ } catch {
14
+ return [];
15
+ }
16
+ }
@@ -24,6 +24,7 @@ import { buildCspHeader, CSP_DIRECTIVES_TEMPLATE, CSP_ENABLED, generateNonce } f
24
24
  import { isDev, compress, isStaticPath } from "./html.ts";
25
25
  import { dev500WithPlugins } from "./dev-500.ts";
26
26
  import { OUT_DIR } from "./paths.ts";
27
+ import { pidsOnPort } from "./port.ts";
27
28
  import { buildPrerenderManifest, buildStaticManifest, lookupStatic } from "./staticManifest.ts";
28
29
  import { dedup } from "./dedup.ts";
29
30
  import {
@@ -1092,7 +1093,14 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : isDev ? 9001 :
1092
1093
  // Elysia's chained generics drift when plugins add routes — track the app as a
1093
1094
  // loose `Elysia` so plugin-extended types stay assignable.
1094
1095
  let app: Elysia = new Elysia({
1095
- serve: { maxRequestBodySize: BODY_SIZE_LIMIT, idleTimeout: IDLE_TIMEOUT },
1096
+ serve: {
1097
+ maxRequestBodySize: BODY_SIZE_LIMIT,
1098
+ idleTimeout: IDLE_TIMEOUT,
1099
+ // Elysia's Bun adapter defaults reusePort:true — SO_REUSEPORT lets a second
1100
+ // server silently join the port and the kernel splits traffic between two
1101
+ // different builds. Opt in only for deliberate N-worker clustering.
1102
+ reusePort: process.env.BOSIA_REUSE_PORT === "1",
1103
+ },
1096
1104
  }) as unknown as Elysia;
1097
1105
 
1098
1106
  // Plugins.backend.before — runs before framework middleware/routes.
@@ -1160,23 +1168,34 @@ for (const plugin of plugins) {
1160
1168
  }
1161
1169
  }
1162
1170
 
1163
- app.listen(PORT, () => {
1164
- // In dev mode the proxy owns the user-facing port — don't print the internal port
1165
- if (!isDev) console.log(`⬡ Bosia server running at http://localhost:${PORT}`);
1166
- // Last line of startup on purpose — the cache identity contract is the one
1167
- // config mistake that leaks one user's page to another, so it stays visible.
1168
- if (CACHE_ENABLED) {
1169
- console.log(
1170
- `\n🔑 Response cache tells users apart ONLY by these cookies/headers: [${CACHE_KEYS.join(", ")}]\n` +
1171
- ` Using a different session cookie or auth header? Add its name to CACHE_KEYS,\n` +
1172
- ` or one user's personalised page can be served to another. Routes personalised\n` +
1173
- ` by anything else should set \`export const cache = false\`.\n` +
1174
- ` Note: the runtime auto-warns only on uncovered *cookie* reads — it CANNOT\n` +
1175
- ` detect header-based personalisation, so custom auth headers (X-Api-Key,\n` +
1176
- ` X-Auth-Token, …) must be added to CACHE_KEYS by hand.\n`,
1177
- );
1178
- }
1179
- });
1171
+ try {
1172
+ app.listen(PORT, () => {
1173
+ // In dev mode the proxy owns the user-facing port — don't print the internal port
1174
+ if (!isDev) console.log(`⬡ Bosia server running at http://localhost:${PORT}`);
1175
+ // Last line of startup on purpose — the cache identity contract is the one
1176
+ // config mistake that leaks one user's page to another, so it stays visible.
1177
+ if (CACHE_ENABLED) {
1178
+ console.log(
1179
+ `\n🔑 Response cache tells users apart ONLY by these cookies/headers: [${CACHE_KEYS.join(", ")}]\n` +
1180
+ ` Using a different session cookie or auth header? Add its name to CACHE_KEYS,\n` +
1181
+ ` or one user's personalised page can be served to another. Routes personalised\n` +
1182
+ ` by anything else should set \`export const cache = false\`.\n` +
1183
+ ` Note: the runtime auto-warns only on uncovered *cookie* reads — it CANNOT\n` +
1184
+ ` detect header-based personalisation, so custom auth headers (X-Api-Key,\n` +
1185
+ ` X-Auth-Token, …) must be added to CACHE_KEYS by hand.\n`,
1186
+ );
1187
+ }
1188
+ });
1189
+ } catch (err) {
1190
+ // Bun.serve runs inline inside .listen(), so a failed bind lands here.
1191
+ if ((err as { code?: string })?.code !== "EADDRINUSE") throw err;
1192
+ const [pid] = await pidsOnPort(PORT);
1193
+ console.error(
1194
+ `\n❌ Port ${PORT} is already serving${pid ? ` (pid ${pid})` : ""}.\n` +
1195
+ ` Stop it or set PORT to a free port.\n`,
1196
+ );
1197
+ process.exit(1);
1198
+ }
1180
1199
 
1181
1200
  async function shutdown() {
1182
1201
  if (shuttingDown) {