bosia 0.1.8 → 0.1.9
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/core/cookies.ts +7 -0
- package/src/core/hooks.ts +2 -0
- package/src/core/html.ts +19 -8
- package/src/core/renderer.ts +2 -2
- package/src/core/server.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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/core/cookies.ts
CHANGED
|
@@ -41,6 +41,7 @@ export class CookieJar implements Cookies {
|
|
|
41
41
|
private _incoming: Record<string, string>;
|
|
42
42
|
private _outgoing: string[] = [];
|
|
43
43
|
private _defaults: CookieOptions;
|
|
44
|
+
private _accessed = false;
|
|
44
45
|
|
|
45
46
|
constructor(cookieHeader: string, dev = false) {
|
|
46
47
|
this._incoming = parseCookies(cookieHeader);
|
|
@@ -51,13 +52,19 @@ export class CookieJar implements Cookies {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
get(name: string): string | undefined {
|
|
55
|
+
this._accessed = true;
|
|
54
56
|
return this._incoming[name];
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
getAll(): Record<string, string> {
|
|
60
|
+
this._accessed = true;
|
|
58
61
|
return { ...this._incoming };
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
get accessed(): boolean {
|
|
65
|
+
return this._accessed;
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
set(name: string, value: string, options?: CookieOptions): void {
|
|
62
69
|
if (!VALID_COOKIE_NAME.test(name)) throw new Error(`Invalid cookie name: ${name}`);
|
|
63
70
|
const opts = { ...this._defaults, ...options };
|
package/src/core/hooks.ts
CHANGED
|
@@ -70,6 +70,8 @@ export type Metadata = {
|
|
|
70
70
|
title?: string;
|
|
71
71
|
description?: string;
|
|
72
72
|
meta?: Array<{ name?: string; property?: string; content: string }>;
|
|
73
|
+
lang?: string;
|
|
74
|
+
link?: Array<{ rel: string; href: string; hreflang?: string }>;
|
|
73
75
|
data?: Record<string, any>;
|
|
74
76
|
};
|
|
75
77
|
|
package/src/core/html.ts
CHANGED
|
@@ -64,6 +64,7 @@ export function buildHtml(
|
|
|
64
64
|
layoutData: any[],
|
|
65
65
|
csr = true,
|
|
66
66
|
formData: any = null,
|
|
67
|
+
lang?: string,
|
|
67
68
|
): string {
|
|
68
69
|
const cssLinks = (distManifest.css ?? [])
|
|
69
70
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
@@ -87,7 +88,7 @@ export function buildHtml(
|
|
|
87
88
|
: "";
|
|
88
89
|
|
|
89
90
|
return `<!DOCTYPE html>
|
|
90
|
-
<html lang="en">
|
|
91
|
+
<html lang="${lang || "en"}">
|
|
91
92
|
<head>
|
|
92
93
|
<meta charset="UTF-8">
|
|
93
94
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -108,15 +109,17 @@ export function buildHtml(
|
|
|
108
109
|
|
|
109
110
|
import type { Metadata } from "./hooks.ts";
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
const _shellOpenCache = new Map<string, string>();
|
|
112
113
|
|
|
113
114
|
/** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
|
|
114
|
-
export function buildHtmlShellOpen(): string {
|
|
115
|
-
|
|
115
|
+
export function buildHtmlShellOpen(lang?: string): string {
|
|
116
|
+
const key = lang || "en";
|
|
117
|
+
const cached = _shellOpenCache.get(key);
|
|
118
|
+
if (cached) return cached;
|
|
116
119
|
const cssLinks = (distManifest.css ?? [])
|
|
117
120
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
118
121
|
.join("\n ");
|
|
119
|
-
|
|
122
|
+
const result = `<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
|
|
120
123
|
` <meta charset="UTF-8">\n` +
|
|
121
124
|
` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
|
|
122
125
|
` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
|
|
@@ -124,7 +127,8 @@ export function buildHtmlShellOpen(): string {
|
|
|
124
127
|
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
125
128
|
` <script>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
126
129
|
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`;
|
|
127
|
-
|
|
130
|
+
_shellOpenCache.set(key, result);
|
|
131
|
+
return result;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
const SPINNER = `<div id="__bs__"><style>` +
|
|
@@ -148,6 +152,13 @@ export function buildMetadataChunk(metadata: Metadata | null): string {
|
|
|
148
152
|
out += ` <meta ${attrs} content="${escapeAttr(m.content)}">\n`;
|
|
149
153
|
}
|
|
150
154
|
}
|
|
155
|
+
if (metadata.link) {
|
|
156
|
+
for (const l of metadata.link) {
|
|
157
|
+
let attrs = `rel="${escapeAttr(l.rel)}" href="${escapeAttr(l.href)}"`;
|
|
158
|
+
if (l.hreflang) attrs += ` hreflang="${escapeAttr(l.hreflang)}"`;
|
|
159
|
+
out += ` <link ${attrs}>\n`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
151
162
|
} else {
|
|
152
163
|
out += ` <title>Bosia App</title>\n`;
|
|
153
164
|
}
|
|
@@ -192,8 +203,8 @@ export function buildHtmlTail(
|
|
|
192
203
|
|
|
193
204
|
// ─── Gzip Compression ────────────────────────────────────
|
|
194
205
|
|
|
195
|
-
export function compress(body: string, contentType: string, req: Request, status = 200): Response {
|
|
196
|
-
const headers: Record<string, string> = { "Content-Type": contentType, "Vary": "Accept-Encoding" };
|
|
206
|
+
export function compress(body: string, contentType: string, req: Request, status = 200, extraHeaders?: Record<string, string>): Response {
|
|
207
|
+
const headers: Record<string, string> = { "Content-Type": contentType, "Vary": "Accept-Encoding", ...extraHeaders };
|
|
197
208
|
const accept = req.headers.get("accept-encoding") ?? "";
|
|
198
209
|
// Skip compression in dev — the dev proxy's fetch() auto-decompresses gzip
|
|
199
210
|
// responses but keeps the Content-Encoding header, causing ERR_CONTENT_DECODING_FAILED.
|
package/src/core/renderer.ts
CHANGED
|
@@ -203,8 +203,8 @@ export async function renderSSRStream(
|
|
|
203
203
|
|
|
204
204
|
const stream = new ReadableStream<Uint8Array>({
|
|
205
205
|
async start(controller) {
|
|
206
|
-
// Chunk 1: head opening (CSS, modulepreload — cached)
|
|
207
|
-
controller.enqueue(enc.encode(buildHtmlShellOpen()));
|
|
206
|
+
// Chunk 1: head opening (CSS, modulepreload — cached per lang)
|
|
207
|
+
controller.enqueue(enc.encode(buildHtmlShellOpen(metadata?.lang)));
|
|
208
208
|
|
|
209
209
|
// Chunk 2: metadata tags, close </head>, open <body> + spinner
|
|
210
210
|
controller.enqueue(enc.encode(buildMetadataChunk(metadata)));
|
package/src/core/server.ts
CHANGED
|
@@ -131,7 +131,10 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
131
131
|
try {
|
|
132
132
|
const pageMatch = findMatch(serverRoutes, routeUrl.pathname);
|
|
133
133
|
const data = await loadRouteData(routeUrl, locals, request, cookies);
|
|
134
|
-
if (!data)
|
|
134
|
+
if (!data) {
|
|
135
|
+
const cc = (cookies as CookieJar).accessed ? "private, no-cache" : "public, max-age=0, must-revalidate";
|
|
136
|
+
return compress(JSON.stringify({ pageData: {}, layoutData: [] }), "application/json", request, 200, { "Cache-Control": cc });
|
|
137
|
+
}
|
|
135
138
|
|
|
136
139
|
// Include metadata for client-side title/description updates
|
|
137
140
|
let metadata = null;
|
|
@@ -142,7 +145,12 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
142
145
|
} catch { /* non-fatal */ }
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
|
|
148
|
+
const cacheControl = (cookies as CookieJar).accessed
|
|
149
|
+
? "private, no-cache"
|
|
150
|
+
: "public, max-age=0, must-revalidate";
|
|
151
|
+
const cacheHeaders = { "Cache-Control": cacheControl };
|
|
152
|
+
|
|
153
|
+
return compress(JSON.stringify({ ...data, metadata }), "application/json", request, 200, cacheHeaders);
|
|
146
154
|
} catch (err) {
|
|
147
155
|
if (err instanceof Redirect) {
|
|
148
156
|
return compress(JSON.stringify({ redirect: err.location, status: err.status }), "application/json", request);
|