bosia 1.2.5 → 1.2.7
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 +5 -6
- package/package.json +2 -1
- package/src/core/backend.ts +87 -3
- package/src/core/basePath.ts +30 -0
- package/src/core/cache.ts +378 -30
- package/src/core/client/App.svelte +19 -11
- package/src/core/client/hydrate.ts +17 -9
- package/src/core/client/navigation.ts +1 -1
- package/src/core/client/prefetch.ts +66 -41
- package/src/core/client/router.svelte.ts +41 -22
- package/src/core/cors.ts +17 -1
- package/src/core/csrf.ts +32 -19
- package/src/core/dedup.ts +51 -7
- package/src/core/dev.ts +11 -1
- package/src/core/devWsProxy.ts +144 -0
- package/src/core/env.ts +7 -0
- package/src/core/errors.ts +23 -6
- package/src/core/hooks.ts +1 -1
- package/src/core/html.ts +13 -1
- package/src/core/plugins/access-log.ts +128 -0
- package/src/core/prerender.ts +143 -6
- package/src/core/proxy.ts +48 -0
- package/src/core/renderer.ts +70 -29
- package/src/core/routeTypes.ts +16 -1
- package/src/core/server.bun.ts +6 -1
- package/src/core/server.ts +198 -77
- package/src/core/server.workers.ts +3 -2
- package/src/core/types/plugin.ts +2 -1
- package/src/core/websocket.ts +232 -0
- package/src/core/workersGuard.ts +18 -3
- package/src/lib/index.ts +1 -0
- package/templates/default/.env.example +20 -5
- package/templates/default/tsconfig.json +1 -1
- package/templates/demo/.env.example +20 -5
- package/templates/demo/src/hooks.server.ts +3 -12
- package/templates/demo/tsconfig.json +1 -1
- package/templates/shop/src/app.d.ts +0 -1
- package/templates/shop/src/hooks.server.ts +3 -11
- package/templates/shop/src/routes/+layout.server.ts +0 -1
- package/templates/shop/tsconfig.json +1 -1
- package/templates/store/src/app.d.ts +0 -1
- package/templates/store/src/hooks.server.ts +3 -11
- package/templates/store/src/routes/+layout.server.ts +0 -1
- package/templates/store/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -144,16 +144,15 @@ const authHandle: Handle = async ({ event, resolve }) => {
|
|
|
144
144
|
return resolve(event);
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return res;
|
|
147
|
+
const langHandle: Handle = async ({ event, resolve }) => {
|
|
148
|
+
event.locals.lang = event.request.headers.get("accept-language")?.split(",")[0] ?? "en";
|
|
149
|
+
return resolve(event);
|
|
151
150
|
};
|
|
152
151
|
|
|
153
|
-
export const handle = sequence(authHandle,
|
|
152
|
+
export const handle = sequence(authHandle, langHandle);
|
|
154
153
|
```
|
|
155
154
|
|
|
156
|
-
`locals` set here are available in every loader and API handler.
|
|
155
|
+
`locals` set here are available in every loader and API handler. For an access log, use your reverse proxy's or add `accessLog()` from `bosia/plugins/access-log` to `bosia.config.ts` — not a `console.log` per request.
|
|
157
156
|
|
|
158
157
|
## Public API
|
|
159
158
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.4"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"./client": "./src/lib/client.ts",
|
|
38
38
|
"./server": "./src/lib/server.ts",
|
|
39
39
|
"./plugins/server-timing": "./src/core/plugins/server-timing.ts",
|
|
40
|
+
"./plugins/access-log": "./src/core/plugins/access-log.ts",
|
|
40
41
|
"./plugins/inspector": "./src/core/plugins/inspector/index.ts"
|
|
41
42
|
},
|
|
42
43
|
"bin": {
|
package/src/core/backend.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// ─── Bosia Backend App ────────────────────────────────────
|
|
2
2
|
// A tiny Elysia-shaped HTTP app: chainable routes + onRequest / onAfterHandle /
|
|
3
|
-
// onError hooks, one `fetch(request)` entry for Bun.serve and
|
|
3
|
+
// onError / onResponse hooks, one `fetch(request)` entry for Bun.serve and
|
|
4
|
+
// Workers alike.
|
|
4
5
|
// Only the subset the framework and plugins use — not a general router.
|
|
5
6
|
|
|
7
|
+
import type { Server } from "bun";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isUpgradeRequest,
|
|
11
|
+
isUpgraded,
|
|
12
|
+
runUpgrade,
|
|
13
|
+
websocketHandlers,
|
|
14
|
+
type WebSocketOptions,
|
|
15
|
+
} from "./websocket.ts";
|
|
16
|
+
|
|
6
17
|
/** Outbound response bag — merged into whatever the handler returns. */
|
|
7
18
|
export interface ResponseSet {
|
|
8
19
|
status?: number;
|
|
@@ -34,10 +45,28 @@ export type AfterHandleHook = (ctx: {
|
|
|
34
45
|
|
|
35
46
|
export type ErrorHook = (ctx: { request: Request; error: unknown; set: ResponseSet }) => unknown;
|
|
36
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Sees the final Response — after onRequest short-circuits, after-handle
|
|
50
|
+
* replacements, error hooks and the HEAD body drop — exactly once per request.
|
|
51
|
+
* `start`: `performance.now()` when the request entered `fetch`. Observe only:
|
|
52
|
+
* the return value is ignored and a throw or rejection is swallowed. `response`
|
|
53
|
+
* is the one the client gets — don't read its body or change its headers.
|
|
54
|
+
* `status` is the status the client got: 101 for a WebSocket upgrade, whose
|
|
55
|
+
* `response` is only a placeholder (a 101 Response can't be built).
|
|
56
|
+
*/
|
|
57
|
+
export type ResponseHook = (ctx: {
|
|
58
|
+
request: Request;
|
|
59
|
+
response: Response;
|
|
60
|
+
status: number;
|
|
61
|
+
upgraded: boolean;
|
|
62
|
+
start: number;
|
|
63
|
+
}) => void | Promise<unknown>;
|
|
64
|
+
|
|
37
65
|
export interface ServeOptions {
|
|
38
66
|
maxRequestBodySize?: number;
|
|
39
67
|
idleTimeout?: number;
|
|
40
68
|
reusePort?: boolean;
|
|
69
|
+
websocket?: WebSocketOptions;
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "*";
|
|
@@ -55,6 +84,7 @@ export class BosiaApp {
|
|
|
55
84
|
private requestHooks: RequestHook[] = [];
|
|
56
85
|
private afterHooks: AfterHandleHook[] = [];
|
|
57
86
|
private errorHooks: ErrorHook[] = [];
|
|
87
|
+
private responseHooks: ResponseHook[] = [];
|
|
58
88
|
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
59
89
|
|
|
60
90
|
constructor(private config: { serve?: ServeOptions } = {}) {}
|
|
@@ -100,6 +130,12 @@ export class BosiaApp {
|
|
|
100
130
|
return this;
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
/** Observes the final response of every request (see ResponseHook). */
|
|
134
|
+
onResponse(hook: ResponseHook): this {
|
|
135
|
+
this.responseHooks.push(hook);
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
// Paths match exactly, or "*" matches everything. Enough for plugin endpoints
|
|
104
140
|
// like /__bosia/locate; add `:param` matching here when a plugin needs it.
|
|
105
141
|
private route(method: Method, path: string, handler: Handler): this {
|
|
@@ -129,7 +165,52 @@ export class BosiaApp {
|
|
|
129
165
|
return [...methods];
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
|
|
168
|
+
/** `undefined` after a WebSocket upgrade — Bun has already answered with 101.
|
|
169
|
+
* Only Bun passes `server`, so Workers never upgrades. */
|
|
170
|
+
fetch = (request: Request, server?: Server<any>): Promise<Response | undefined> =>
|
|
171
|
+
server && isUpgradeRequest(request)
|
|
172
|
+
? runUpgrade(request, server, () => this.handle(request))
|
|
173
|
+
: this.handle(request);
|
|
174
|
+
|
|
175
|
+
private handle = async (request: Request): Promise<Response | undefined> => {
|
|
176
|
+
if (this.responseHooks.length === 0) {
|
|
177
|
+
const response = await this.respond(request);
|
|
178
|
+
return isUpgraded() ? undefined : response;
|
|
179
|
+
}
|
|
180
|
+
const start = performance.now();
|
|
181
|
+
let response: Response;
|
|
182
|
+
try {
|
|
183
|
+
response = await this.respond(request);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// An error hook threw: the runtime answers 500, so observers see that.
|
|
186
|
+
this.observe(request, new Response(null, { status: 500 }), start);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
this.observe(request, response, start);
|
|
190
|
+
return isUpgraded() ? undefined : response;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
private observe(request: Request, response: Response, start: number): void {
|
|
194
|
+
const upgraded = isUpgraded();
|
|
195
|
+
const status = upgraded ? 101 : response.status;
|
|
196
|
+
for (const hook of this.responseHooks) {
|
|
197
|
+
try {
|
|
198
|
+
const out = hook({ request, response, status, upgraded, start });
|
|
199
|
+
// An async observer's rejection would be unhandled — fatal in prod.
|
|
200
|
+
if (out instanceof Promise) out.catch(() => {});
|
|
201
|
+
} catch {
|
|
202
|
+
// Observers must not change what the client gets.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Answers without running onResponse observers — for the Workers warm-up
|
|
208
|
+
* render, which no client made and no access log should count. */
|
|
209
|
+
fetchUnobserved(request: Request): Promise<Response> {
|
|
210
|
+
return this.respond(request);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async respond(request: Request): Promise<Response> {
|
|
133
214
|
const set: ResponseSet = { headers: {} };
|
|
134
215
|
const isHead = request.method === "HEAD";
|
|
135
216
|
const method = isHead ? "GET" : request.method.toUpperCase();
|
|
@@ -143,7 +224,7 @@ export class BosiaApp {
|
|
|
143
224
|
}
|
|
144
225
|
return new Response("Internal Server Error", { status: 500 });
|
|
145
226
|
}
|
|
146
|
-
}
|
|
227
|
+
}
|
|
147
228
|
|
|
148
229
|
private async dispatch(request: Request, method: string, set: ResponseSet): Promise<Response> {
|
|
149
230
|
for (const hook of this.requestHooks) {
|
|
@@ -173,6 +254,8 @@ export class BosiaApp {
|
|
|
173
254
|
response = "Method Not Allowed";
|
|
174
255
|
}
|
|
175
256
|
|
|
257
|
+
// Bun already sent the 101: nothing after this can change the answer.
|
|
258
|
+
if (isUpgraded()) return response as Response;
|
|
176
259
|
for (const hook of this.afterHooks) {
|
|
177
260
|
const out = await hook({ request, response, set });
|
|
178
261
|
if (out !== undefined) response = out;
|
|
@@ -189,6 +272,7 @@ export class BosiaApp {
|
|
|
189
272
|
maxRequestBodySize: serve.maxRequestBodySize,
|
|
190
273
|
idleTimeout: serve.idleTimeout,
|
|
191
274
|
reusePort: serve.reusePort ?? false,
|
|
275
|
+
websocket: { ...websocketHandlers, ...serve.websocket, sendPings: true },
|
|
192
276
|
});
|
|
193
277
|
callback?.();
|
|
194
278
|
return this;
|
package/src/core/basePath.ts
CHANGED
|
@@ -30,6 +30,36 @@ export function withBase(base: string, path: string): string {
|
|
|
30
30
|
return base + path;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Never a navigation target. Test a parsed `URL.protocol`, not the raw string: the
|
|
34
|
+
* parser drops tabs, newlines and leading control chars, so `java\tscript:` is one too. */
|
|
35
|
+
export const DANGEROUS_PROTOCOL = /^(javascript|data|vbscript):$/;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What the client router matches: the path the server would see. Every URL goes
|
|
39
|
+
* through the URL parser against `pageHref`, like a link: `?page=2`, `b`, `../x`
|
|
40
|
+
* resolve, `/a b` and `/café` get encoded (params match the server's), `/\host`
|
|
41
|
+
* is another origin. Another origin comes back as its full URL so the router leaves the app.
|
|
42
|
+
*/
|
|
43
|
+
export function toRouterPath(url: string, pageHref: string): string {
|
|
44
|
+
const u = new URL(url, pageHref);
|
|
45
|
+
if (DANGEROUS_PROTOCOL.test(u.protocol)) {
|
|
46
|
+
throw new Error(`navigation: dangerous scheme in URL "${url}". Use a path or an http(s) URL.`);
|
|
47
|
+
}
|
|
48
|
+
// Scheme + host, not `origin`: a `blob:` URL shares the page's origin, and the URL
|
|
49
|
+
// parser gives app shells (`capacitor://localhost`) origin "null" for every host.
|
|
50
|
+
// A path starting `//` (from `/.//evil.com`) would read as another host once used as
|
|
51
|
+
// a URL, so it stays a full URL: the router leaves for this origin's `//evil.com` page.
|
|
52
|
+
const page = new URL(pageHref);
|
|
53
|
+
// No host (`file:`) is never ours: every file on disk would count as one app.
|
|
54
|
+
const ours =
|
|
55
|
+
u.host !== "" &&
|
|
56
|
+
u.protocol === page.protocol &&
|
|
57
|
+
u.host === page.host &&
|
|
58
|
+
!u.pathname.startsWith("//");
|
|
59
|
+
// `capacitor://localhost` has an empty path: that's the root.
|
|
60
|
+
return ours ? (u.pathname || "/") + u.search + u.hash : u.href;
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/**
|
|
34
64
|
* The in-app pathname for an incoming request, or `null` when the request is
|
|
35
65
|
* not ours to answer. `/sso` and `/sso/` both resolve to `/` so the root route
|