bosia 0.9.9 → 0.9.10
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 +3 -3
- package/package.json +2 -4
- package/src/core/backend.ts +238 -0
- package/src/core/build.ts +1 -2
- package/src/core/config.ts +1 -1
- package/src/core/dev.ts +1 -1
- package/src/core/plugins/inspector/index.ts +6 -10
- package/src/core/plugins/server-timing.ts +1 -1
- package/src/core/server.ts +32 -54
- package/src/core/server.workers.ts +1 -2
- package/src/core/types/plugin.ts +5 -6
- package/src/lib/server.ts +1 -0
- package/templates/default/README.md +1 -1
- package/templates/default/src/routes/(public)/+page.svelte +1 -1
- package/templates/demo/src/routes/(public)/+page.svelte +2 -2
- package/templates/demo/src/routes/(public)/about/+page.svelte +2 -2
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
> Full documentation: [bosia.dev](https://bosia.dev)
|
|
4
4
|
|
|
5
|
-
A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun
|
|
5
|
+
A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun.
|
|
6
6
|
|
|
7
7
|
**Production-ready out of the box** — built-in security (CSRF, XSS escaping, secure cookies, security headers), performance (response cache, gzip, static asset caching, prerendering), and reliability (graceful shutdown drain, request backpressure, crash backoff).
|
|
8
8
|
|
|
9
|
-
File-based routing inspired by SvelteKit, built on top of the Bun runtime
|
|
9
|
+
File-based routing inspired by SvelteKit, built on top of the Bun runtime with its own tiny HTTP layer. No Node.js, no Vite, no adapters.
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
@@ -53,7 +53,7 @@ bun run start
|
|
|
53
53
|
| Layer | Technology |
|
|
54
54
|
| ----------- | ------------------------------------------ |
|
|
55
55
|
| Runtime | [Bun](https://bun.sh) |
|
|
56
|
-
| HTTP Server |
|
|
56
|
+
| HTTP Server | Built-in (`Bun.serve`) |
|
|
57
57
|
| UI | [Svelte 5](https://svelte.dev) (Runes) |
|
|
58
58
|
| CSS | [Tailwind CSS v4](https://tailwindcss.com) |
|
|
59
59
|
| Bundler | Bun.build |
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun
|
|
5
|
+
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun. File-based routing. No Node.js, no Vite. Runs on Bun or Cloudflare Workers.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bun",
|
|
8
8
|
"svelte",
|
|
9
9
|
"ssr",
|
|
10
|
-
"elysia",
|
|
11
10
|
"fullstack",
|
|
12
11
|
"framework"
|
|
13
12
|
],
|
|
@@ -55,7 +54,6 @@
|
|
|
55
54
|
"@clack/prompts": "^1.1.0",
|
|
56
55
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
57
56
|
"@tailwindcss/cli": "^4.2.1",
|
|
58
|
-
"elysia": "^1.4.26",
|
|
59
57
|
"magic-string": "^0.30.0",
|
|
60
58
|
"svelte": "^5.56.3",
|
|
61
59
|
"tailwind-merge": "^3.5.0",
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// ─── Bosia Backend App ────────────────────────────────────
|
|
2
|
+
// A tiny Elysia-shaped HTTP app: chainable routes + onRequest / onAfterHandle /
|
|
3
|
+
// onError hooks, one `fetch(request)` entry for Bun.serve and Workers alike.
|
|
4
|
+
// Only the subset the framework and plugins use — not a general router.
|
|
5
|
+
|
|
6
|
+
/** Outbound response bag — merged into whatever the handler returns. */
|
|
7
|
+
export interface ResponseSet {
|
|
8
|
+
status?: number;
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HandlerContext {
|
|
13
|
+
request: Request;
|
|
14
|
+
/**
|
|
15
|
+
* Parsed by content-type: JSON → object, form → object, else text. Only for
|
|
16
|
+
* exact-path routes — the framework's "*" catch-all reads the body itself.
|
|
17
|
+
*/
|
|
18
|
+
body: unknown;
|
|
19
|
+
query: Record<string, string>;
|
|
20
|
+
set: ResponseSet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Handler = (ctx: HandlerContext) => unknown;
|
|
24
|
+
|
|
25
|
+
export type RequestHook = (ctx: { request: Request; set: ResponseSet }) => unknown;
|
|
26
|
+
|
|
27
|
+
export type AfterHandleHook = (ctx: {
|
|
28
|
+
request: Request;
|
|
29
|
+
response: unknown;
|
|
30
|
+
set: ResponseSet;
|
|
31
|
+
}) => unknown;
|
|
32
|
+
|
|
33
|
+
export type ErrorHook = (ctx: { request: Request; error: unknown; set: ResponseSet }) => unknown;
|
|
34
|
+
|
|
35
|
+
export interface ServeOptions {
|
|
36
|
+
maxRequestBodySize?: number;
|
|
37
|
+
idleTimeout?: number;
|
|
38
|
+
reusePort?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "*";
|
|
42
|
+
|
|
43
|
+
interface Route {
|
|
44
|
+
method: Method;
|
|
45
|
+
path: string;
|
|
46
|
+
handler: Handler;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const BODYLESS = new Set(["GET", "HEAD"]);
|
|
50
|
+
|
|
51
|
+
export class BosiaApp {
|
|
52
|
+
private routes: Route[] = [];
|
|
53
|
+
private requestHooks: RequestHook[] = [];
|
|
54
|
+
private afterHooks: AfterHandleHook[] = [];
|
|
55
|
+
private errorHooks: ErrorHook[] = [];
|
|
56
|
+
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
57
|
+
|
|
58
|
+
constructor(private config: { serve?: ServeOptions } = {}) {}
|
|
59
|
+
|
|
60
|
+
get(path: string, handler: Handler): this {
|
|
61
|
+
return this.route("GET", path, handler);
|
|
62
|
+
}
|
|
63
|
+
post(path: string, handler: Handler): this {
|
|
64
|
+
return this.route("POST", path, handler);
|
|
65
|
+
}
|
|
66
|
+
put(path: string, handler: Handler): this {
|
|
67
|
+
return this.route("PUT", path, handler);
|
|
68
|
+
}
|
|
69
|
+
patch(path: string, handler: Handler): this {
|
|
70
|
+
return this.route("PATCH", path, handler);
|
|
71
|
+
}
|
|
72
|
+
delete(path: string, handler: Handler): this {
|
|
73
|
+
return this.route("DELETE", path, handler);
|
|
74
|
+
}
|
|
75
|
+
options(path: string, handler: Handler): this {
|
|
76
|
+
return this.route("OPTIONS", path, handler);
|
|
77
|
+
}
|
|
78
|
+
/** Any method. */
|
|
79
|
+
all(path: string, handler: Handler): this {
|
|
80
|
+
return this.route("*", path, handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Runs first on every request. Returning a value short-circuits routing. */
|
|
84
|
+
onRequest(hook: RequestHook): this {
|
|
85
|
+
this.requestHooks.push(hook);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Runs after the handler. Returning a value replaces the response. */
|
|
90
|
+
onAfterHandle(hook: AfterHandleHook): this {
|
|
91
|
+
this.afterHooks.push(hook);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Runs in registration order; the first hook to return a value answers. */
|
|
96
|
+
onError(hook: ErrorHook): this {
|
|
97
|
+
this.errorHooks.push(hook);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Paths match exactly, or "*" matches everything. Enough for plugin endpoints
|
|
102
|
+
// like /__bosia/locate; add `:param` matching here when a plugin needs it.
|
|
103
|
+
private route(method: Method, path: string, handler: Handler): this {
|
|
104
|
+
this.routes.push({ method, path, handler });
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private match(method: string, pathname: string): Route | null {
|
|
109
|
+
let wildcard: Route | null = null;
|
|
110
|
+
for (const r of this.routes) {
|
|
111
|
+
if (r.method !== "*" && r.method !== method) continue;
|
|
112
|
+
// Exact paths beat "*" regardless of registration order, like Elysia.
|
|
113
|
+
if (r.path === pathname) return r;
|
|
114
|
+
if (r.path === "*" && !wildcard) wildcard = r;
|
|
115
|
+
}
|
|
116
|
+
return wildcard;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private allowed(pathname: string): string[] {
|
|
120
|
+
const methods = new Set<string>();
|
|
121
|
+
for (const r of this.routes) {
|
|
122
|
+
if (r.path !== pathname && r.path !== "*") continue;
|
|
123
|
+
if (r.method === "*") return [];
|
|
124
|
+
methods.add(r.method);
|
|
125
|
+
}
|
|
126
|
+
if (methods.has("GET")) methods.add("HEAD");
|
|
127
|
+
return [...methods];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fetch = async (request: Request): Promise<Response> => {
|
|
131
|
+
const set: ResponseSet = { headers: {} };
|
|
132
|
+
const isHead = request.method === "HEAD";
|
|
133
|
+
const method = isHead ? "GET" : request.method.toUpperCase();
|
|
134
|
+
try {
|
|
135
|
+
const res = await this.dispatch(request, method, set);
|
|
136
|
+
return isHead ? new Response(null, res) : res;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
for (const hook of this.errorHooks) {
|
|
139
|
+
const out = await hook({ request, error, set });
|
|
140
|
+
if (out !== undefined) return toResponse(out, set);
|
|
141
|
+
}
|
|
142
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
private async dispatch(request: Request, method: string, set: ResponseSet): Promise<Response> {
|
|
147
|
+
for (const hook of this.requestHooks) {
|
|
148
|
+
const out = await hook({ request, set });
|
|
149
|
+
if (out !== undefined) return toResponse(out, set);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const url = new URL(request.url);
|
|
153
|
+
const route = this.match(method, url.pathname);
|
|
154
|
+
let response: unknown;
|
|
155
|
+
if (route) {
|
|
156
|
+
response = await route.handler({
|
|
157
|
+
request,
|
|
158
|
+
body: BODYLESS.has(method) || route.path === "*" ? undefined : await parseBody(request),
|
|
159
|
+
query: Object.fromEntries(url.searchParams),
|
|
160
|
+
set,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
const allow = this.allowed(url.pathname);
|
|
164
|
+
set.status = 405;
|
|
165
|
+
if (allow.length > 0) set.headers["allow"] = allow.join(", ");
|
|
166
|
+
response = "Method Not Allowed";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const hook of this.afterHooks) {
|
|
170
|
+
const out = await hook({ request, response, set });
|
|
171
|
+
if (out !== undefined) response = out;
|
|
172
|
+
}
|
|
173
|
+
return toResponse(response, set);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Bind on Bun. Throws synchronously on EADDRINUSE, like Bun.serve. */
|
|
177
|
+
listen(port: number, callback?: () => void): this {
|
|
178
|
+
const serve = this.config.serve ?? {};
|
|
179
|
+
this.server = Bun.serve({
|
|
180
|
+
port,
|
|
181
|
+
fetch: this.fetch,
|
|
182
|
+
maxRequestBodySize: serve.maxRequestBodySize,
|
|
183
|
+
idleTimeout: serve.idleTimeout,
|
|
184
|
+
reusePort: serve.reusePort ?? false,
|
|
185
|
+
});
|
|
186
|
+
callback?.();
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async stop(closeActiveConnections = false): Promise<void> {
|
|
191
|
+
await this.server?.stop(closeActiveConnections);
|
|
192
|
+
this.server = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function parseBody(request: Request): Promise<unknown> {
|
|
197
|
+
const type = (request.headers.get("content-type") ?? "").toLowerCase();
|
|
198
|
+
try {
|
|
199
|
+
if (type.includes("application/json")) {
|
|
200
|
+
const text = await request.clone().text();
|
|
201
|
+
return text ? JSON.parse(text) : undefined;
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
type.includes("multipart/form-data") ||
|
|
205
|
+
type.includes("application/x-www-form-urlencoded")
|
|
206
|
+
) {
|
|
207
|
+
return Object.fromEntries(await request.clone().formData());
|
|
208
|
+
}
|
|
209
|
+
const text = await request.clone().text();
|
|
210
|
+
return text || undefined;
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Handler return value → Response, with `set` status/headers merged in. */
|
|
217
|
+
function toResponse(value: unknown, set: ResponseSet): Response {
|
|
218
|
+
const extra = Object.entries(set.headers);
|
|
219
|
+
if (value instanceof Response) {
|
|
220
|
+
if (extra.length === 0 && set.status === undefined) return value;
|
|
221
|
+
// Rebuilt rather than mutated: a fetch() Response has immutable headers.
|
|
222
|
+
const headers = new Headers(value.headers);
|
|
223
|
+
for (const [k, v] of extra) headers.set(k, v);
|
|
224
|
+
return new Response(value.body, {
|
|
225
|
+
status: set.status ?? value.status,
|
|
226
|
+
statusText: set.status === undefined ? value.statusText : undefined,
|
|
227
|
+
headers,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const status = set.status ?? 200;
|
|
231
|
+
if (value === undefined || value === null) return new Response(null, { status, headers: extra });
|
|
232
|
+
if (typeof value === "string") {
|
|
233
|
+
const headers = new Headers(extra);
|
|
234
|
+
if (!headers.has("content-type")) headers.set("content-type", "text/plain;charset=utf-8");
|
|
235
|
+
return new Response(value, { status, headers });
|
|
236
|
+
}
|
|
237
|
+
return Response.json(value, { status, headers: extra });
|
|
238
|
+
}
|
package/src/core/build.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { workersGuardReport } from "./workersGuard.ts";
|
|
|
28
28
|
const CORE_DIR = import.meta.dir;
|
|
29
29
|
|
|
30
30
|
// Runtime externals: never bundled into dist/hooks.server.js or dist/bosia.config.js
|
|
31
|
-
const BOSIA_RUNTIME_EXTERNALS = ["bosia", "
|
|
31
|
+
const BOSIA_RUNTIME_EXTERNALS = ["bosia", "bun", "svelte", "svelte/server"];
|
|
32
32
|
|
|
33
33
|
// ─── Entry Point ─────────────────────────────────────────
|
|
34
34
|
|
|
@@ -233,7 +233,6 @@ const serverPromise = Bun.build({
|
|
|
233
233
|
naming: { entry: "index.[ext]", chunk: "[name]-[hash].[ext]" },
|
|
234
234
|
minify: isProduction,
|
|
235
235
|
sourcemap: isProduction ? "none" : "linked",
|
|
236
|
-
external: ["elysia"],
|
|
237
236
|
plugins: [serverPlugin, ...userServerBunPlugins, makeBosiaSvelteCompiler("bun")],
|
|
238
237
|
});
|
|
239
238
|
|
package/src/core/config.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<Bosi
|
|
|
55
55
|
entrypoints: [configPath],
|
|
56
56
|
target: "bun",
|
|
57
57
|
format: "esm",
|
|
58
|
-
external: ["bosia", "
|
|
58
|
+
external: ["bosia", "bun", "svelte", "svelte/server"],
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
if (!result.success || !result.outputs[0]) {
|
package/src/core/dev.ts
CHANGED
|
@@ -210,7 +210,7 @@ async function startAppServer() {
|
|
|
210
210
|
NODE_ENV: "development",
|
|
211
211
|
// Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
|
|
212
212
|
PORT: String(APP_PORT),
|
|
213
|
-
// Allow externalized deps
|
|
213
|
+
// Allow externalized deps to resolve from bosia's node_modules
|
|
214
214
|
NODE_PATH: BOSIA_NODE_PATH,
|
|
215
215
|
// Point the server child at dev's output dir so its OUT_DIR reads match what build wrote.
|
|
216
216
|
BOSIA_OUT_DIR: DEV_OUT_DIR,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import type { Elysia } from "elysia";
|
|
4
3
|
import { createInspectorBunPlugin } from "./bun-plugin.ts";
|
|
5
4
|
import { getOverlayScript } from "./overlay.ts";
|
|
6
5
|
import { resolveFrame, resolveStack } from "./sourcemap.ts";
|
|
@@ -142,7 +141,7 @@ function installProcessListeners() {
|
|
|
142
141
|
// live in a separately-bundled module graph from this disk-loaded plugin —
|
|
143
142
|
// can notify the inspector without an import dependency. Renderer catches
|
|
144
143
|
// (page load, layout load, SSR load) swallow errors and return a rendered
|
|
145
|
-
// error page, so they never reach
|
|
144
|
+
// error page, so they never reach the backend `.onError()` on their own. The
|
|
146
145
|
// global is typed in core/devErrorReport.ts.
|
|
147
146
|
function installGlobalReporter() {
|
|
148
147
|
globalThis.__BOSIA_REPORT_ERROR__ = (e) => {
|
|
@@ -179,10 +178,7 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
179
178
|
|
|
180
179
|
backend: {
|
|
181
180
|
before(app) {
|
|
182
|
-
|
|
183
|
-
// the generic so the plugin return type drifts unless we widen back.
|
|
184
|
-
let chained: Elysia = app as unknown as Elysia;
|
|
185
|
-
chained = chained.post(endpoint, async ({ body }: { body: unknown }) => {
|
|
181
|
+
let chained = app.post(endpoint, async ({ body }) => {
|
|
186
182
|
const data = (body ?? {}) as {
|
|
187
183
|
file?: string;
|
|
188
184
|
line?: number;
|
|
@@ -270,7 +266,7 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
270
266
|
installProcessListeners();
|
|
271
267
|
installGlobalReporter();
|
|
272
268
|
|
|
273
|
-
//
|
|
269
|
+
// Backend onError — runs because server.ts registers plugin
|
|
274
270
|
// backend.before() ahead of the base .onError() responder.
|
|
275
271
|
chained = chained.onError(({ error }) => {
|
|
276
272
|
const e = error as Error;
|
|
@@ -281,12 +277,12 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
281
277
|
});
|
|
282
278
|
// Fall through to the base handler.
|
|
283
279
|
return undefined;
|
|
284
|
-
})
|
|
280
|
+
});
|
|
285
281
|
|
|
286
282
|
// Live SSE stream. New clients also get a flush of the bounded
|
|
287
283
|
// replay buffer so errors that fired during a failing render
|
|
288
284
|
// (before the 500 page's overlay could subscribe) are visible.
|
|
289
|
-
chained = chained.get("/__bosia/errors", ({ request }
|
|
285
|
+
chained = chained.get("/__bosia/errors", ({ request }) => {
|
|
290
286
|
const stream = new ReadableStream<Uint8Array>({
|
|
291
287
|
start(ctrl) {
|
|
292
288
|
sseClients.add(ctrl);
|
|
@@ -322,7 +318,7 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
322
318
|
Connection: "keep-alive",
|
|
323
319
|
},
|
|
324
320
|
});
|
|
325
|
-
})
|
|
321
|
+
});
|
|
326
322
|
}
|
|
327
323
|
|
|
328
324
|
return chained;
|
|
@@ -39,7 +39,7 @@ export function serverTiming(options: ServerTimingOptions = {}): BosiaPlugin {
|
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Otherwise, push into
|
|
42
|
+
// Otherwise, push into the outbound header bag.
|
|
43
43
|
const headers = (set.headers ??= {}) as Record<string, string>;
|
|
44
44
|
headers[headerName] = value;
|
|
45
45
|
});
|
package/src/core/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BosiaApp } from "./backend.ts";
|
|
2
2
|
|
|
3
3
|
import { findMatch, compileRoutes, canonicalPathname } from "./matcher.ts";
|
|
4
4
|
import { resolveApiMatch } from "./apiResolver.ts";
|
|
@@ -1205,7 +1205,7 @@ export function inFlightCount(): number {
|
|
|
1205
1205
|
return inFlight;
|
|
1206
1206
|
}
|
|
1207
1207
|
|
|
1208
|
-
// ───
|
|
1208
|
+
// ─── Backend App ──────────────────────────────────────────
|
|
1209
1209
|
|
|
1210
1210
|
// Read the build-time route manifest so plugins.backend.after can introspect routes.
|
|
1211
1211
|
function loadBuiltManifest(): RouteManifest {
|
|
@@ -1233,12 +1233,13 @@ function loadBuiltManifest(): RouteManifest {
|
|
|
1233
1233
|
export type CreateAppOptions = {
|
|
1234
1234
|
/** `handle` from the user's `src/hooks.server.ts`. */
|
|
1235
1235
|
handle?: Handle | null;
|
|
1236
|
-
/** Elysia adapter. Omitted → Bun (`Bun.serve`). */
|
|
1237
|
-
adapter?: ElysiaAdapter;
|
|
1238
1236
|
};
|
|
1239
1237
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1238
|
+
const frameworkHandler = ({ request }: { request: Request }) =>
|
|
1239
|
+
handleRequest(request, new URL(request.url));
|
|
1240
|
+
|
|
1241
|
+
/** Build the backend app: plugins, the framework's catch-all routes, error handling. */
|
|
1242
|
+
export async function createApp(options: CreateAppOptions = {}): Promise<BosiaApp> {
|
|
1242
1243
|
userHandle = options.handle ?? null;
|
|
1243
1244
|
|
|
1244
1245
|
const plugins = await loadPlugins(process.cwd());
|
|
@@ -1246,25 +1247,22 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Elysia>
|
|
|
1246
1247
|
console.log(`🔌 Loaded ${plugins.length} plugin(s): ${plugins.map((p) => p.name).join(", ")}`);
|
|
1247
1248
|
}
|
|
1248
1249
|
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
reusePort: process.env.BOSIA_REUSE_PORT === "1",
|
|
1261
|
-
},
|
|
1262
|
-
})) as unknown as Elysia;
|
|
1250
|
+
// serve options only apply on Bun (`listen`); Workers calls `fetch` directly.
|
|
1251
|
+
let app = new BosiaApp({
|
|
1252
|
+
serve: {
|
|
1253
|
+
maxRequestBodySize: BODY_SIZE_LIMIT,
|
|
1254
|
+
idleTimeout: IDLE_TIMEOUT,
|
|
1255
|
+
// SO_REUSEPORT lets a second server silently join the port and the kernel
|
|
1256
|
+
// splits traffic between two different builds. Opt in only for deliberate
|
|
1257
|
+
// N-worker clustering.
|
|
1258
|
+
reusePort: process.env.BOSIA_REUSE_PORT === "1",
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1263
1261
|
|
|
1264
1262
|
// Plugins.backend.before — runs before framework middleware/routes.
|
|
1265
1263
|
// Plugin-registered routes here BYPASS the framework (CSRF, hooks, etc.).
|
|
1266
|
-
// Plugins register their own `.onError()` handlers here.
|
|
1267
|
-
//
|
|
1264
|
+
// Plugins register their own `.onError()` handlers here. onError handlers
|
|
1265
|
+
// fire in registration order; plugin handlers run first and (when they
|
|
1268
1266
|
// return undefined) fall through to the base 500 responder chained after this
|
|
1269
1267
|
// loop. Registering the base responder before the loop would short-circuit
|
|
1270
1268
|
// every plugin handler.
|
|
@@ -1279,40 +1277,20 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Elysia>
|
|
|
1279
1277
|
}
|
|
1280
1278
|
}
|
|
1281
1279
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
else console.error("Uncaught server error:", (error as Error)?.message ?? error);
|
|
1285
|
-
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
1286
|
-
}) as unknown as Elysia;
|
|
1287
|
-
|
|
1280
|
+
// Static files are served by resolve() with path traversal protection and security headers.
|
|
1281
|
+
// Only the verbs Bosia answers are routed — anything else (TRACE, …) gets a 405.
|
|
1288
1282
|
app = app
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return handleRequest(request, url);
|
|
1294
|
-
})
|
|
1295
|
-
// Non-GET catch-alls route every method through handleRequest()
|
|
1296
|
-
.post("*", ({ request }: { request: Request }) => {
|
|
1297
|
-
const url = new URL(request.url);
|
|
1298
|
-
return handleRequest(request, url);
|
|
1299
|
-
})
|
|
1300
|
-
.put("*", ({ request }: { request: Request }) => {
|
|
1301
|
-
const url = new URL(request.url);
|
|
1302
|
-
return handleRequest(request, url);
|
|
1303
|
-
})
|
|
1304
|
-
.patch("*", ({ request }: { request: Request }) => {
|
|
1305
|
-
const url = new URL(request.url);
|
|
1306
|
-
return handleRequest(request, url);
|
|
1307
|
-
})
|
|
1308
|
-
.delete("*", ({ request }: { request: Request }) => {
|
|
1309
|
-
const url = new URL(request.url);
|
|
1310
|
-
return handleRequest(request, url);
|
|
1283
|
+
.onError(({ error }) => {
|
|
1284
|
+
if (isDev) console.error("Uncaught server error:", error);
|
|
1285
|
+
else console.error("Uncaught server error:", (error as Error)?.message ?? error);
|
|
1286
|
+
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
1311
1287
|
})
|
|
1312
|
-
.
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1288
|
+
.get("*", frameworkHandler)
|
|
1289
|
+
.post("*", frameworkHandler)
|
|
1290
|
+
.put("*", frameworkHandler)
|
|
1291
|
+
.patch("*", frameworkHandler)
|
|
1292
|
+
.delete("*", frameworkHandler)
|
|
1293
|
+
.options("*", frameworkHandler);
|
|
1316
1294
|
|
|
1317
1295
|
// Plugins.backend.after — runs after framework routes; receives the route manifest.
|
|
1318
1296
|
for (const plugin of plugins) {
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// config arrive as static imports (workersCodegen.ts), static files are served
|
|
4
4
|
// by Workers Static Assets before the worker runs.
|
|
5
5
|
|
|
6
|
-
import { CloudflareAdapter } from "elysia/adapter/cloudflare-worker";
|
|
7
6
|
import { config, handle } from "bosia:workers-runtime";
|
|
8
7
|
import { twMerge } from "tailwind-merge";
|
|
9
8
|
|
|
@@ -19,7 +18,7 @@ disableCompression();
|
|
|
19
18
|
// isolate startup, which has its own CPU budget, not in the first request.
|
|
20
19
|
twMerge("");
|
|
21
20
|
|
|
22
|
-
const app =
|
|
21
|
+
const app = await createApp({ handle });
|
|
23
22
|
|
|
24
23
|
// The first request in an isolate paid ~5ms of V8 compiling the render path, on a
|
|
25
24
|
// 10ms CPU budget. Render `/` once now instead — startup CPU is budgeted apart.
|
package/src/core/types/plugin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─── Bosia Plugin Types ──────────────────────────────────
|
|
2
2
|
// Public surface for first-party and third-party plugins.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { BosiaApp } from "../backend.ts";
|
|
5
5
|
import type { BunPlugin } from "bun";
|
|
6
6
|
import type { RouteManifest } from "../types.ts";
|
|
7
7
|
import type { Metadata } from "../hooks.ts";
|
|
@@ -34,15 +34,14 @@ export interface BosiaPlugin {
|
|
|
34
34
|
name: string;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Mount points around the framework's HTTP backend
|
|
38
|
-
*
|
|
39
|
-
* abstract so the API survives a future backend swap.
|
|
37
|
+
* Mount points around the framework's HTTP backend — Bosia's own small,
|
|
38
|
+
* Elysia-shaped app (`get/post/…`, `onRequest`, `onAfterHandle`, `onError`).
|
|
40
39
|
*/
|
|
41
40
|
backend?: {
|
|
42
41
|
/** Runs before framework middleware/routes — can register routes that bypass Bosia. */
|
|
43
|
-
before?: (app:
|
|
42
|
+
before?: (app: BosiaApp) => MaybePromise<BosiaApp>;
|
|
44
43
|
/** Runs after framework routes — receives the route manifest for introspection. */
|
|
45
|
-
after?: (app:
|
|
44
|
+
after?: (app: BosiaApp, ctx: { manifest: RouteManifest }) => MaybePromise<BosiaApp>;
|
|
46
45
|
};
|
|
47
46
|
|
|
48
47
|
/** Build pipeline hooks. */
|
package/src/lib/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
A [Bosia](https://github.com/bosapi/bosia) project — SSR · Svelte 5 · Bun
|
|
3
|
+
A [Bosia](https://github.com/bosapi/bosia) project — SSR · Svelte 5 · Bun.
|
|
4
4
|
|
|
5
5
|
Bosia is **production-ready out of the box** — security (CSRF, XSS, secure cookies, headers), performance (response cache, gzip, prerendering), and reliability (graceful shutdown, request backpressure, crash backoff) are all built in.
|
|
6
6
|
|
|
@@ -2,7 +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">A Bosia project — SSR + Svelte 5 + Bun
|
|
5
|
+
<p class="text-muted-foreground text-lg">A Bosia project — SSR + Svelte 5 + Bun</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<div class="mt-4 flex gap-3">
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
<svelte:head>
|
|
40
40
|
<title>Bosia Demo</title>
|
|
41
|
-
<meta name="description" content="Bosia Demo — SSR + Svelte 5 + Bun
|
|
41
|
+
<meta name="description" content="Bosia Demo — SSR + Svelte 5 + Bun" />
|
|
42
42
|
</svelte:head>
|
|
43
43
|
|
|
44
44
|
<div class="space-y-12">
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
Bosia Demo
|
|
50
50
|
</h1>
|
|
51
51
|
<p class="text-xl text-muted-foreground max-w-xl">
|
|
52
|
-
A minimalist fullstack framework — SSR, Svelte 5 Runes,
|
|
52
|
+
A minimalist fullstack framework — SSR, Svelte 5 Runes, and Bun.
|
|
53
53
|
</p>
|
|
54
54
|
</div>
|
|
55
55
|
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
<div class="space-y-6 max-w-2xl">
|
|
6
6
|
<h1 class="text-4xl font-bold tracking-tight">About Bosia Demo</h1>
|
|
7
7
|
<p class="text-muted-foreground text-lg">
|
|
8
|
-
A minimalist fullstack framework built on Bun
|
|
8
|
+
A minimalist fullstack framework built on Bun and Svelte 5.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<ul class="space-y-2 text-foreground">
|
|
12
|
-
{#each ["Bun runtime — fast builds, native TypeScript", "
|
|
12
|
+
{#each ["Bun runtime — fast builds, native TypeScript", "Svelte 5 Runes — fine-grained reactivity", "Isomorphic SSR with client hydration", "File-based routing (SvelteKit-compatible conventions)", "Nested layouts and route groups (public), (auth), (admin)", "Dynamic params [slug] and catch-all [...rest]", "Server loaders with parent() data threading", "Hooks — sequence() middleware for auth, logging, etc.", "Component registry — bun x bosia@latest add button", "Feature registry — bosia feat login"] as item}
|
|
13
13
|
<li class="flex items-start gap-2">
|
|
14
14
|
<span class="text-primary mt-0.5">✓</span>
|
|
15
15
|
<span>{item}</span>
|