bosia 0.5.3 → 0.5.5
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 -3
- package/src/core/build.ts +9 -9
- package/src/core/client/enhance.ts +8 -5
- package/src/core/dev.ts +12 -4
- package/src/core/html.ts +2 -1
- package/src/core/paths.ts +5 -0
- package/src/core/plugins/inspector/bun-plugin.ts +9 -2
- package/src/core/prerender.ts +14 -14
- package/src/core/server.ts +31 -12
- package/templates/default/.prettierrc.json +1 -1
- package/templates/default/src/routes/+page.svelte +3 -6
- package/templates/demo/.prettierrc.json +1 -1
- package/templates/demo/src/routes/(public)/+layout.svelte +4 -11
- package/templates/demo/src/routes/(public)/blog/+page.svelte +2 -4
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +3 -5
- package/templates/todo/.prettierrc.json +1 -1
- package/templates/todo/src/routes/+page.svelte +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
package/src/cli/start.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { spawn } from "bun";
|
|
2
2
|
import { loadEnv } from "../core/env.ts";
|
|
3
|
-
import { BOSIA_NODE_PATH } from "../core/paths.ts";
|
|
3
|
+
import { BOSIA_NODE_PATH, OUT_DIR } from "../core/paths.ts";
|
|
4
4
|
|
|
5
5
|
export async function runStart() {
|
|
6
6
|
loadEnv("production");
|
|
7
7
|
|
|
8
8
|
let serverEntry = "index.js";
|
|
9
9
|
try {
|
|
10
|
-
const manifest = await Bun.file(
|
|
10
|
+
const manifest = await Bun.file(`${OUT_DIR}/manifest.json`).json();
|
|
11
11
|
serverEntry = manifest.serverEntry ?? "index.js";
|
|
12
12
|
} catch {}
|
|
13
13
|
|
|
14
|
-
const proc = spawn(["bun", "run",
|
|
14
|
+
const proc = spawn(["bun", "run", `${OUT_DIR}/server/${serverEntry}`], {
|
|
15
15
|
stdout: "inherit",
|
|
16
16
|
stderr: "inherit",
|
|
17
17
|
cwd: process.cwd(),
|
package/src/core/build.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { makeBosiaSvelteCompiler } from "./svelteCompiler.ts";
|
|
|
9
9
|
import { prerenderStaticRoutes, generateStaticSite } from "./prerender.ts";
|
|
10
10
|
import { loadEnv, classifyEnvVars } from "./env.ts";
|
|
11
11
|
import { generateEnvModules } from "./envCodegen.ts";
|
|
12
|
-
import { BOSIA_NODE_PATH, resolveBosiaBin } from "./paths.ts";
|
|
12
|
+
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin } from "./paths.ts";
|
|
13
13
|
import { loadPlugins } from "./config.ts";
|
|
14
14
|
import type { BuildContext } from "./types/plugin.ts";
|
|
15
15
|
|
|
@@ -47,7 +47,7 @@ const classifiedEnv = classifyEnvVars(envVars);
|
|
|
47
47
|
|
|
48
48
|
// 0b. Clean all generated output first
|
|
49
49
|
try {
|
|
50
|
-
rmSync(
|
|
50
|
+
rmSync(OUT_DIR, { recursive: true, force: true });
|
|
51
51
|
} catch {}
|
|
52
52
|
try {
|
|
53
53
|
rmSync("./.bosia", { recursive: true, force: true });
|
|
@@ -125,7 +125,7 @@ for (const [key, value] of Object.entries(classifiedEnv.privateStatic)) {
|
|
|
125
125
|
console.log("\n📦 Building Tailwind + client + server...");
|
|
126
126
|
const clientPromise = Bun.build({
|
|
127
127
|
entrypoints: [join(CORE_DIR, "client", "hydrate.ts")],
|
|
128
|
-
outdir:
|
|
128
|
+
outdir: `${OUT_DIR}/client`,
|
|
129
129
|
target: "browser",
|
|
130
130
|
splitting: true,
|
|
131
131
|
naming: { chunk: "[name]-[hash].[ext]" },
|
|
@@ -139,7 +139,7 @@ const clientPromise = Bun.build({
|
|
|
139
139
|
|
|
140
140
|
const serverPromise = Bun.build({
|
|
141
141
|
entrypoints: [join(CORE_DIR, "server.ts")],
|
|
142
|
-
outdir:
|
|
142
|
+
outdir: `${OUT_DIR}/server`,
|
|
143
143
|
target: "bun",
|
|
144
144
|
splitting: true,
|
|
145
145
|
naming: { entry: "index.[ext]", chunk: "[name]-[hash].[ext]" },
|
|
@@ -177,7 +177,7 @@ if (!serverResult.success) {
|
|
|
177
177
|
const jsFiles: string[] = [];
|
|
178
178
|
const cssFiles: string[] = [];
|
|
179
179
|
for (const output of clientResult.outputs) {
|
|
180
|
-
const rel = relative(
|
|
180
|
+
const rel = relative(`${OUT_DIR}/client`, output.path);
|
|
181
181
|
if (output.path.endsWith(".js")) jsFiles.push(rel);
|
|
182
182
|
if (output.path.endsWith(".css")) cssFiles.push(rel);
|
|
183
183
|
}
|
|
@@ -190,7 +190,7 @@ const serverEntry =
|
|
|
190
190
|
.pop() ?? "index.js";
|
|
191
191
|
|
|
192
192
|
// 8. Write dist/manifest.json
|
|
193
|
-
mkdirSync(
|
|
193
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
194
194
|
const distManifest = {
|
|
195
195
|
js: jsFiles,
|
|
196
196
|
css: cssFiles,
|
|
@@ -200,12 +200,12 @@ const distManifest = {
|
|
|
200
200
|
"hydrate.js",
|
|
201
201
|
serverEntry,
|
|
202
202
|
};
|
|
203
|
-
writeFileSync(
|
|
203
|
+
writeFileSync(`${OUT_DIR}/manifest.json`, JSON.stringify(distManifest, null, 2));
|
|
204
204
|
console.log(`✅ Client bundle: ${jsFiles.join(", ")}`);
|
|
205
|
-
console.log(`✅ Server entry:
|
|
205
|
+
console.log(`✅ Server entry: ${OUT_DIR}/server/${serverEntry}`);
|
|
206
206
|
|
|
207
207
|
// 8b. Persist route manifest for runtime plugins (backend.after consumers like OpenAPI).
|
|
208
|
-
writeFileSync(
|
|
208
|
+
writeFileSync(`${OUT_DIR}/route-manifest.json`, JSON.stringify(manifest, null, 2));
|
|
209
209
|
|
|
210
210
|
// 9. Prerender static routes
|
|
211
211
|
await prerenderStaticRoutes(manifest);
|
|
@@ -68,11 +68,14 @@ export function enhance(form: HTMLFormElement, submit?: SubmitFunction) {
|
|
|
68
68
|
const submitter = (event.submitter as HTMLElement | null) ?? null;
|
|
69
69
|
const formData = new FormData(form, submitter as HTMLElement | undefined);
|
|
70
70
|
|
|
71
|
-
// Resolve action URL — preserve `?/actionName` if the submitter or form sets it
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// Resolve action URL — preserve `?/actionName` if the submitter or form sets it.
|
|
72
|
+
// `submitter.formAction` reflects the document URL when no `formaction` attribute
|
|
73
|
+
// is set (HTML spec), which would silently drop the form's `action="?/foo"`. So
|
|
74
|
+
// only honor the submitter override when it actually has the attribute.
|
|
75
|
+
const submitterEl = submitter as HTMLButtonElement | HTMLInputElement | null;
|
|
76
|
+
const submitterAction =
|
|
77
|
+
submitterEl && submitterEl.hasAttribute("formaction") ? submitterEl.formAction : "";
|
|
78
|
+
const actionAttr = submitterAction || form.action || window.location.href;
|
|
76
79
|
const action = new URL(actionAttr, window.location.href);
|
|
77
80
|
|
|
78
81
|
let cancelled = false;
|
package/src/core/dev.ts
CHANGED
|
@@ -2,6 +2,13 @@ import { spawn, type Subprocess } from "bun";
|
|
|
2
2
|
import { watch } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { loadEnv, resetDeclaredKeys } from "./env.ts";
|
|
5
|
+
import { BOSIA_NODE_PATH } from "./paths.ts";
|
|
6
|
+
|
|
7
|
+
// Dev always writes to .bosia/dev so a parallel `bun run build` (writing to ./dist)
|
|
8
|
+
// can't clobber the live preview. Hardcoded — BOSIA_OUT_DIR is a build-mode knob,
|
|
9
|
+
// not a dev knob; we pass it to spawned children to redirect build.ts/server output,
|
|
10
|
+
// but dev.ts itself never reads it.
|
|
11
|
+
const DEV_OUT_DIR = ".bosia/dev";
|
|
5
12
|
|
|
6
13
|
// Snapshot pure shell env BEFORE any loadEnv call pollutes process.env.
|
|
7
14
|
// On `.env*` change we restore from this snapshot, then re-run loadEnv,
|
|
@@ -49,8 +56,6 @@ function broadcastReload() {
|
|
|
49
56
|
|
|
50
57
|
// ─── Build ────────────────────────────────────────────────
|
|
51
58
|
|
|
52
|
-
import { BOSIA_NODE_PATH } from "./paths.ts";
|
|
53
|
-
|
|
54
59
|
const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
|
|
55
60
|
|
|
56
61
|
async function runBuild(): Promise<boolean> {
|
|
@@ -59,6 +64,7 @@ async function runBuild(): Promise<boolean> {
|
|
|
59
64
|
stdout: "inherit",
|
|
60
65
|
stderr: "inherit",
|
|
61
66
|
cwd: process.cwd(),
|
|
67
|
+
env: { ...process.env, BOSIA_OUT_DIR: DEV_OUT_DIR },
|
|
62
68
|
});
|
|
63
69
|
return (await proc.exited) === 0;
|
|
64
70
|
}
|
|
@@ -79,11 +85,11 @@ async function startAppServer() {
|
|
|
79
85
|
// Read the server entry filename from the manifest written by build.ts
|
|
80
86
|
let serverEntry = "index.js";
|
|
81
87
|
try {
|
|
82
|
-
const manifest = await Bun.file(
|
|
88
|
+
const manifest = await Bun.file(`${DEV_OUT_DIR}/manifest.json`).json();
|
|
83
89
|
serverEntry = manifest.serverEntry ?? "index.js";
|
|
84
90
|
} catch {}
|
|
85
91
|
|
|
86
|
-
appProcess = spawn(["bun", "run",
|
|
92
|
+
appProcess = spawn(["bun", "run", `${DEV_OUT_DIR}/server/${serverEntry}`], {
|
|
87
93
|
stdout: "inherit",
|
|
88
94
|
stderr: "inherit",
|
|
89
95
|
cwd: process.cwd(),
|
|
@@ -94,6 +100,8 @@ async function startAppServer() {
|
|
|
94
100
|
PORT: String(APP_PORT),
|
|
95
101
|
// Allow externalized deps (elysia, etc.) to resolve from bosia's node_modules
|
|
96
102
|
NODE_PATH: BOSIA_NODE_PATH,
|
|
103
|
+
// Point the server child at dev's output dir so its OUT_DIR reads match what build wrote.
|
|
104
|
+
BOSIA_OUT_DIR: DEV_OUT_DIR,
|
|
97
105
|
// Dev proxy injects X-Forwarded-Host/Proto reflecting the public DEV_PORT, so CSRF
|
|
98
106
|
// origin checks must honour them. Safe in dev because the proxy controls these
|
|
99
107
|
// headers — no untrusted client can spoof them.
|
package/src/core/html.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { getDeclaredEnvKeys } from "./env.ts";
|
|
3
3
|
import { nonceAttr } from "./csp.ts";
|
|
4
|
+
import { OUT_DIR } from "./paths.ts";
|
|
4
5
|
|
|
5
6
|
// ─── Dist Manifest ───────────────────────────────────────
|
|
6
7
|
// Maps hashed filenames → script/link tags.
|
|
7
8
|
// Cached at startup; server restarts on rebuild in dev anyway.
|
|
8
9
|
|
|
9
10
|
export const distManifest: { js: string[]; css: string[]; entry: string } = (() => {
|
|
10
|
-
const p =
|
|
11
|
+
const p = `${OUT_DIR}/manifest.json`;
|
|
11
12
|
return existsSync(p)
|
|
12
13
|
? JSON.parse(readFileSync(p, "utf-8"))
|
|
13
14
|
: { js: [], css: [], entry: "hydrate.js" };
|
package/src/core/paths.ts
CHANGED
|
@@ -18,6 +18,11 @@ const HOISTED_NM = isInstalledAsDep ? parentDir : null;
|
|
|
18
18
|
/** NODE_PATH value covering both nested and hoisted dependency locations */
|
|
19
19
|
export const BOSIA_NODE_PATH = HOISTED_NM ? [NESTED_NM, HOISTED_NM].join(":") : NESTED_NM;
|
|
20
20
|
|
|
21
|
+
// On-disk output directory. URL namespace (/dist/client/...) stays stable;
|
|
22
|
+
// only the on-disk location moves so dev (.bosia/dev) and a parallel
|
|
23
|
+
// `bun run build` (./dist) don't clobber each other.
|
|
24
|
+
export const OUT_DIR = process.env.BOSIA_OUT_DIR ?? "./dist";
|
|
25
|
+
|
|
21
26
|
/** Find a binary from bosia's dependencies (handles hoisting) */
|
|
22
27
|
export function resolveBosiaBin(name: string): string {
|
|
23
28
|
const nested = join(NESTED_NM, ".bin", name);
|
|
@@ -121,8 +121,15 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
let js = result.js.code;
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// Skip empty CSS — multiple +page.svelte routes with no <style> would
|
|
125
|
+
// otherwise each emit an empty CSS chunk, all hashing to the same
|
|
126
|
+
// content → Bun fails with "Multiple files share the same output path".
|
|
127
|
+
// Also strip dots from basename: Bun's `[name]-[hash].[ext]` chunk
|
|
128
|
+
// naming treats the first `.` as the extension boundary, so
|
|
129
|
+
// `+page.svelte-…` collapses to `[name]="+page"` for every route.
|
|
130
|
+
if (result.css?.code?.trim() && generate !== "server") {
|
|
131
|
+
const safeBase = basename(args.path).replace(/\./g, "_");
|
|
132
|
+
const uid = `${safeBase}-${fnv(args.path)}-style.css`;
|
|
126
133
|
const virtualName = `${VIRTUAL_NS}:${uid}`;
|
|
127
134
|
virtualCss.set(virtualName, result.css.code);
|
|
128
135
|
js += `\nimport ${JSON.stringify(virtualName)};`;
|
package/src/core/prerender.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createServer } from "net";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import type { RouteManifest, TrailingSlash } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
import { BOSIA_NODE_PATH } from "./paths.ts";
|
|
6
|
+
import { BOSIA_NODE_PATH, OUT_DIR } from "./paths.ts";
|
|
7
7
|
|
|
8
8
|
/** Acquire an OS-assigned ephemeral port. Tiny TOCTOU race window; acceptable for build-time use. */
|
|
9
9
|
export function getEphemeralPort(): Promise<number> {
|
|
@@ -78,9 +78,9 @@ export function canonicalRouteFor(routePath: string, ts: TrailingSlash): string
|
|
|
78
78
|
* mode so static hosts serve the right file on direct URL hits.
|
|
79
79
|
*/
|
|
80
80
|
export function prerenderOutPath(routePath: string, ts: TrailingSlash): string {
|
|
81
|
-
if (routePath === "/") return
|
|
82
|
-
if (ts === "never") return
|
|
83
|
-
return
|
|
81
|
+
if (routePath === "/") return `${OUT_DIR}/prerendered/index.html`;
|
|
82
|
+
if (ts === "never") return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}.html`;
|
|
83
|
+
return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}/index.html`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** Data-payload filename for a prerendered route — matches client `dataUrl()`. */
|
|
@@ -94,7 +94,7 @@ export function prerenderDataPath(routePath: string): string {
|
|
|
94
94
|
* `/api/foo.json` regardless of the request URL's slash).
|
|
95
95
|
*/
|
|
96
96
|
export function prerenderApiOutPath(routePath: string): string {
|
|
97
|
-
return
|
|
97
|
+
return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}.json`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
async function detectPrerenderRoutes(manifest: RouteManifest): Promise<PrerenderTarget[]> {
|
|
@@ -179,7 +179,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
179
179
|
console.log(`\n🖨️ Prerendering ${targets.length} route(s)...`);
|
|
180
180
|
|
|
181
181
|
const port = await getEphemeralPort();
|
|
182
|
-
const child = Bun.spawn(["bun", "run",
|
|
182
|
+
const child = Bun.spawn(["bun", "run", `${OUT_DIR}/server/index.js`], {
|
|
183
183
|
env: {
|
|
184
184
|
...process.env,
|
|
185
185
|
NODE_ENV: "production",
|
|
@@ -219,7 +219,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
mkdirSync(
|
|
222
|
+
mkdirSync(`${OUT_DIR}/prerendered`, { recursive: true });
|
|
223
223
|
|
|
224
224
|
for (const { path: routePath, kind, trailingSlash: ts } of targets) {
|
|
225
225
|
try {
|
|
@@ -260,7 +260,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
260
260
|
});
|
|
261
261
|
if (dataRes.ok) {
|
|
262
262
|
const dataJson = await dataRes.text();
|
|
263
|
-
const dataOutPath =
|
|
263
|
+
const dataOutPath = `${OUT_DIR}/prerendered/__bosia/data${dataPath}`;
|
|
264
264
|
mkdirSync(dataOutPath.substring(0, dataOutPath.lastIndexOf("/")), {
|
|
265
265
|
recursive: true,
|
|
266
266
|
});
|
|
@@ -295,24 +295,24 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
|
|
|
295
295
|
// ─── Static Site Output ──────────────────────────────────
|
|
296
296
|
|
|
297
297
|
export function generateStaticSite(): void {
|
|
298
|
-
if (!existsSync(
|
|
298
|
+
if (!existsSync(`${OUT_DIR}/prerendered`)) {
|
|
299
299
|
console.log("\n⏭️ No prerendered pages — skipping static site output");
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
console.log("\n📦 Generating static site...");
|
|
304
|
-
mkdirSync(
|
|
304
|
+
mkdirSync(`${OUT_DIR}/static`, { recursive: true });
|
|
305
305
|
|
|
306
306
|
// 1. HTML files from prerendering
|
|
307
|
-
cpSync(
|
|
307
|
+
cpSync(`${OUT_DIR}/prerendered`, `${OUT_DIR}/static`, { recursive: true });
|
|
308
308
|
|
|
309
309
|
// 2. Client JS/CSS — preserves /dist/client/... absolute paths used in HTML
|
|
310
|
-
cpSync(
|
|
310
|
+
cpSync(`${OUT_DIR}/client`, `${OUT_DIR}/static/dist/client`, { recursive: true });
|
|
311
311
|
|
|
312
312
|
// 3. Public assets (bosia-tw.css, favicon, etc.) — preserves /bosia-tw.css path
|
|
313
313
|
if (existsSync("./public")) {
|
|
314
|
-
cpSync("./public",
|
|
314
|
+
cpSync("./public", `${OUT_DIR}/static`, { recursive: true });
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
console.log(
|
|
317
|
+
console.log(`✅ Static site generated: ${OUT_DIR}/static/`);
|
|
318
318
|
}
|
package/src/core/server.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { applyCorsVary, getCorsHeaders, handlePreflight } from "./cors.ts";
|
|
|
22
22
|
import type { CorsConfig } from "./cors.ts";
|
|
23
23
|
import { buildCspHeader, CSP_DIRECTIVES_TEMPLATE, CSP_ENABLED, generateNonce } from "./csp.ts";
|
|
24
24
|
import { isDev, compress, isStaticPath } from "./html.ts";
|
|
25
|
+
import { OUT_DIR } from "./paths.ts";
|
|
25
26
|
import { dedup, dedupKey } from "./dedup.ts";
|
|
26
27
|
import {
|
|
27
28
|
loadRouteData,
|
|
@@ -288,7 +289,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
288
289
|
// dist/client: serve with cache headers based on whether filename is hashed
|
|
289
290
|
if (path.startsWith("/dist/client/")) {
|
|
290
291
|
const resolved = safePath(
|
|
291
|
-
|
|
292
|
+
`${OUT_DIR}/client`,
|
|
292
293
|
path.split("?")[0].slice("/dist/client".length),
|
|
293
294
|
);
|
|
294
295
|
if (resolved) {
|
|
@@ -308,12 +309,12 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
308
309
|
const pub = Bun.file(pubPath);
|
|
309
310
|
if (await pub.exists()) return new Response(pub);
|
|
310
311
|
}
|
|
311
|
-
const distPath = safePath(
|
|
312
|
+
const distPath = safePath(OUT_DIR, path);
|
|
312
313
|
if (distPath) {
|
|
313
314
|
const dist = Bun.file(distPath);
|
|
314
315
|
if (await dist.exists()) return new Response(dist);
|
|
315
316
|
}
|
|
316
|
-
const staticPath = safePath(
|
|
317
|
+
const staticPath = safePath(`${OUT_DIR}/static`, path);
|
|
317
318
|
if (staticPath) {
|
|
318
319
|
const staticFile = Bun.file(staticPath);
|
|
319
320
|
if (await staticFile.exists()) return new Response(staticFile);
|
|
@@ -326,7 +327,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
326
327
|
const prerenderCandidates =
|
|
327
328
|
path === "/" ? ["index.html"] : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
|
|
328
329
|
for (const candidate of prerenderCandidates) {
|
|
329
|
-
const prerenderPath = safePath(
|
|
330
|
+
const prerenderPath = safePath(`${OUT_DIR}/prerendered`, candidate);
|
|
330
331
|
if (!prerenderPath) continue;
|
|
331
332
|
const prerenderFile = Bun.file(prerenderPath);
|
|
332
333
|
if (await prerenderFile.exists()) {
|
|
@@ -691,6 +692,24 @@ if (BODY_SIZE_LIMIT === 0) {
|
|
|
691
692
|
console.log(`📦 Body size limit: ${BODY_SIZE_LIMIT} bytes`);
|
|
692
693
|
}
|
|
693
694
|
|
|
695
|
+
// ─── Idle Timeout ─────────────────────────────────────────
|
|
696
|
+
// Parsed once at startup from IDLE_TIMEOUT env var.
|
|
697
|
+
// Integer seconds; Bun caps it at 255. Default: 10 (Bun's default).
|
|
698
|
+
// Raise when API routes hold streaming responses with long gaps
|
|
699
|
+
// between chunks (e.g. AI tool calls that shell out and wait).
|
|
700
|
+
|
|
701
|
+
function parseIdleTimeout(value?: string): number {
|
|
702
|
+
if (!value) return 10;
|
|
703
|
+
const n = parseInt(value, 10);
|
|
704
|
+
if (!Number.isFinite(n) || n < 0) throw new Error(`Invalid IDLE_TIMEOUT: "${value}"`);
|
|
705
|
+
if (n > 255) throw new Error(`Invalid IDLE_TIMEOUT: "${value}" (max 255)`);
|
|
706
|
+
return n;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const IDLE_TIMEOUT = parseIdleTimeout(process.env.IDLE_TIMEOUT);
|
|
710
|
+
|
|
711
|
+
console.log(`⏱ Idle timeout: ${IDLE_TIMEOUT}s`);
|
|
712
|
+
|
|
694
713
|
// ─── Graceful Shutdown State ──────────────────────────────
|
|
695
714
|
|
|
696
715
|
let shuttingDown = false;
|
|
@@ -706,7 +725,7 @@ if (plugins.length > 0) {
|
|
|
706
725
|
|
|
707
726
|
// Read the build-time route manifest so plugins.backend.after can introspect routes.
|
|
708
727
|
function loadBuiltManifest(): RouteManifest {
|
|
709
|
-
const path =
|
|
728
|
+
const path = `${OUT_DIR}/route-manifest.json`;
|
|
710
729
|
if (existsSync(path)) {
|
|
711
730
|
try {
|
|
712
731
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -737,13 +756,13 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : isDev ? 9001 :
|
|
|
737
756
|
|
|
738
757
|
// Elysia's chained generics drift when plugins add routes — track the app as a
|
|
739
758
|
// loose `Elysia` so plugin-extended types stay assignable.
|
|
740
|
-
let app: Elysia = new Elysia({
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
},
|
|
746
|
-
) as unknown as Elysia;
|
|
759
|
+
let app: Elysia = new Elysia({
|
|
760
|
+
serve: { maxRequestBodySize: BODY_SIZE_LIMIT, idleTimeout: IDLE_TIMEOUT },
|
|
761
|
+
}).onError(({ error }) => {
|
|
762
|
+
if (isDev) console.error("Uncaught server error:", error);
|
|
763
|
+
else console.error("Uncaught server error:", (error as Error)?.message ?? error);
|
|
764
|
+
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
765
|
+
}) as unknown as Elysia;
|
|
747
766
|
|
|
748
767
|
// Plugins.backend.before — runs before framework middleware/routes.
|
|
749
768
|
// Plugin-registered routes here BYPASS the framework (CSRF, hooks, etc.).
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
<div class="flex flex-col items-center gap-3 text-center">
|
|
3
3
|
<img src="/favicon.svg" alt="" class="size-16" />
|
|
4
4
|
<h1 class="text-4xl font-bold tracking-tight">Welcome to Bosia</h1>
|
|
5
|
-
<p class="text-muted-foreground text-lg">
|
|
6
|
-
A Bosia project — SSR + Svelte 5 + Bun + ElysiaJS
|
|
7
|
-
</p>
|
|
5
|
+
<p class="text-muted-foreground text-lg">A Bosia project — SSR + Svelte 5 + Bun + ElysiaJS</p>
|
|
8
6
|
</div>
|
|
9
7
|
|
|
10
8
|
<div class="mt-4 flex gap-3">
|
|
@@ -27,8 +25,7 @@
|
|
|
27
25
|
</div>
|
|
28
26
|
|
|
29
27
|
<p class="text-muted-foreground mt-6 text-sm">
|
|
30
|
-
Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
|
|
31
|
-
|
|
32
|
-
> to get started
|
|
28
|
+
Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs">src/routes/+page.svelte</code> to
|
|
29
|
+
get started
|
|
33
30
|
</p>
|
|
34
31
|
</main>
|
|
@@ -10,25 +10,18 @@
|
|
|
10
10
|
<a href="/" class="font-bold tracking-tight flex items-center gap-2"
|
|
11
11
|
><img src="/favicon.svg" alt="" class="size-5" /> {data.appName}</a
|
|
12
12
|
>
|
|
13
|
-
<a
|
|
14
|
-
href="/"
|
|
15
|
-
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
13
|
+
<a href="/" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
16
14
|
>Home</a
|
|
17
15
|
>
|
|
18
|
-
<a
|
|
19
|
-
href="/about"
|
|
20
|
-
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
16
|
+
<a href="/about" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
21
17
|
>About</a
|
|
22
18
|
>
|
|
23
|
-
<a
|
|
24
|
-
href="/blog"
|
|
25
|
-
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
19
|
+
<a href="/blog" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
26
20
|
>Blog</a
|
|
27
21
|
>
|
|
28
22
|
<a
|
|
29
23
|
href="/all/foo/bar"
|
|
30
|
-
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
31
|
-
>Catch-all</a
|
|
24
|
+
class="text-sm text-muted-foreground hover:text-foreground transition-colors">Catch-all</a
|
|
32
25
|
>
|
|
33
26
|
<a
|
|
34
27
|
href="/api/hello"
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
slug: "route-groups",
|
|
12
12
|
title: "Route Groups Explained",
|
|
13
13
|
date: "2026-03-04",
|
|
14
|
-
excerpt:
|
|
15
|
-
"How (public), (auth), (admin) groups work — invisible in URLs, share layouts.",
|
|
14
|
+
excerpt: "How (public), (auth), (admin) groups work — invisible in URLs, share layouts.",
|
|
16
15
|
tags: ["routing", "layouts"],
|
|
17
16
|
},
|
|
18
17
|
{
|
|
@@ -50,8 +49,7 @@
|
|
|
50
49
|
</div>
|
|
51
50
|
<div class="flex gap-1 shrink-0">
|
|
52
51
|
{#each post.tags as tag}
|
|
53
|
-
<span
|
|
54
|
-
class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
|
|
52
|
+
<span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
|
|
55
53
|
>{tag}</span
|
|
56
54
|
>
|
|
57
55
|
{/each}
|
|
@@ -13,16 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
{#if post}
|
|
15
15
|
<article class="space-y-6 max-w-2xl">
|
|
16
|
-
<a
|
|
17
|
-
|
|
18
|
-
class="text-sm text-muted-foreground hover:text-foreground transition-colors">← Blog</a
|
|
16
|
+
<a href="/blog" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
17
|
+
>← Blog</a
|
|
19
18
|
>
|
|
20
19
|
|
|
21
20
|
<div class="space-y-2">
|
|
22
21
|
<div class="flex flex-wrap gap-1">
|
|
23
22
|
{#each post.tags as tag}
|
|
24
|
-
<span
|
|
25
|
-
class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
|
|
23
|
+
<span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
|
|
26
24
|
>{tag}</span
|
|
27
25
|
>
|
|
28
26
|
{/each}
|