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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.1.8",
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": [
@@ -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
- let _shellOpen: string | null = null;
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
- if (_shellOpen) return _shellOpen;
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
- _shellOpen = `<!DOCTYPE html>\n<html lang="en">\n<head>\n` +
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
- return _shellOpen;
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.
@@ -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)));
@@ -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) return compress(JSON.stringify({ pageData: {}, layoutData: [] }), "application/json", request);
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
- return compress(JSON.stringify({ ...data, metadata }), "application/json", request);
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);