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 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 · ElysiaJS.
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 and ElysiaJS HTTP server. No Node.js, no Vite, no adapters.
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 | [ElysiaJS](https://elysiajs.com) |
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.9",
3
+ "version": "0.9.10",
4
4
  "type": "module",
5
- "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing. No Node.js, no Vite. Runs on Bun or Cloudflare Workers.",
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", "elysia", "bun", "svelte", "svelte/server"];
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
 
@@ -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", "elysia", "bun", "svelte", "svelte/server"],
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 (elysia, etc.) to resolve from bosia's node_modules
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 Elysia `.onError()` on their own. The
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
- // Cast to the base Elysia type — chaining .post/.get/.onError narrows
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
- // Elysia onError — runs because server.ts registers plugin
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
- }) as unknown as Elysia;
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 }: { request: 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
- }) as unknown as Elysia;
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 Elysia's outbound header bag.
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
  });
@@ -1,4 +1,4 @@
1
- import { Elysia, type ElysiaAdapter } from "elysia";
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
- // ─── Elysia App ───────────────────────────────────────────
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
- /** Build the Elysia app: plugins, the framework's catch-all routes, error handling. */
1241
- export async function createApp(options: CreateAppOptions = {}): Promise<Elysia> {
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
- // Elysia's chained generics drift when plugins add routes — track the app as a
1250
- // loose `Elysia` so plugin-extended types stay assignable.
1251
- let app: Elysia = (options.adapter
1252
- ? new Elysia({ adapter: options.adapter })
1253
- : new Elysia({
1254
- serve: {
1255
- maxRequestBodySize: BODY_SIZE_LIMIT,
1256
- idleTimeout: IDLE_TIMEOUT,
1257
- // Elysia's Bun adapter defaults reusePort:true — SO_REUSEPORT lets a second
1258
- // server silently join the port and the kernel splits traffic between two
1259
- // different builds. Opt in only for deliberate N-worker clustering.
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. Elysia fires onError
1267
- // handlers in registration order; plugin handlers run first and (when they
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
- app = app.onError(({ error }) => {
1283
- if (isDev) console.error("Uncaught server error:", error);
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
- // Static files are served by resolve() with path traversal protection and security headers
1290
- // SSR pages
1291
- .get("*", ({ request }: { request: Request }) => {
1292
- const url = new URL(request.url);
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
- .options("*", ({ request }: { request: Request }) => {
1313
- const url = new URL(request.url);
1314
- return handleRequest(request, url);
1315
- }) as unknown as Elysia;
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 = (await createApp({ handle, adapter: CloudflareAdapter })).compile();
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.
@@ -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 { Elysia } from "elysia";
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. Currently typed as
38
- * `Elysia` (the underlying backend), but the namespace is intentionally
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: Elysia) => MaybePromise<Elysia>;
42
+ before?: (app: BosiaApp) => MaybePromise<BosiaApp>;
44
43
  /** Runs after framework routes — receives the route manifest for introspection. */
45
- after?: (app: Elysia, ctx: { manifest: RouteManifest }) => MaybePromise<Elysia>;
44
+ after?: (app: BosiaApp, ctx: { manifest: RouteManifest }) => MaybePromise<BosiaApp>;
46
45
  };
47
46
 
48
47
  /** Build pipeline hooks. */
package/src/lib/server.ts CHANGED
@@ -10,3 +10,4 @@
10
10
  // import { invalidate, invalidateAll } from "bosia/server";
11
11
 
12
12
  export { invalidate, invalidateAll } from "../core/cache.ts";
13
+ export type { BosiaApp, HandlerContext, ResponseSet } from "../core/backend.ts";
@@ -1,6 +1,6 @@
1
1
  # {{PROJECT_NAME}}
2
2
 
3
- A [Bosia](https://github.com/bosapi/bosia) project — SSR · Svelte 5 · Bun · ElysiaJS.
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 + ElysiaJS</p>
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 + ElysiaJS" />
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, Bun, and ElysiaJS.
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, ElysiaJS, and Svelte 5.
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", "ElysiaJS — HTTP server with type-safe routing", "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}
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>