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 +1 -1
- package/src/cli/start.ts +3 -1
- package/src/core/dev.ts +10 -19
- package/src/core/port.ts +16 -0
- package/src/core/server.ts +37 -18
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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() {
|
package/src/core/port.ts
ADDED
|
@@ -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
|
+
}
|
package/src/core/server.ts
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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) {
|