bosia 0.2.2 → 0.3.0

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.
Files changed (87) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -53
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +8 -8
  8. package/src/cli/feat.ts +291 -132
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -153
  15. package/src/core/client/appState.svelte.ts +57 -0
  16. package/src/core/client/enhance.ts +112 -0
  17. package/src/core/client/hydrate.ts +97 -65
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +165 -168
  25. package/src/core/env.ts +155 -128
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +192 -139
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -118
  34. package/src/core/renderer.ts +359 -265
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +543 -370
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/client.ts +12 -0
  41. package/src/lib/index.ts +8 -8
  42. package/src/lib/utils.ts +44 -30
  43. package/templates/default/.prettierignore +5 -0
  44. package/templates/default/.prettierrc.json +9 -0
  45. package/templates/default/README.md +5 -5
  46. package/templates/default/package.json +22 -18
  47. package/templates/default/src/app.css +80 -80
  48. package/templates/default/src/app.d.ts +3 -3
  49. package/templates/default/src/routes/+error.svelte +7 -10
  50. package/templates/default/src/routes/+layout.svelte +2 -2
  51. package/templates/default/src/routes/+page.svelte +31 -29
  52. package/templates/default/src/routes/about/+page.svelte +3 -3
  53. package/templates/default/tsconfig.json +20 -20
  54. package/templates/demo/.prettierignore +5 -0
  55. package/templates/demo/.prettierrc.json +9 -0
  56. package/templates/demo/README.md +9 -9
  57. package/templates/demo/package.json +22 -17
  58. package/templates/demo/src/app.css +80 -80
  59. package/templates/demo/src/app.d.ts +3 -3
  60. package/templates/demo/src/hooks.server.ts +9 -9
  61. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  62. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  63. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  64. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  65. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  67. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  68. package/templates/demo/src/routes/+error.svelte +10 -7
  69. package/templates/demo/src/routes/+layout.server.ts +4 -4
  70. package/templates/demo/src/routes/+layout.svelte +2 -2
  71. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  72. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  73. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  74. package/templates/demo/tsconfig.json +20 -20
  75. package/templates/todo/.prettierignore +5 -0
  76. package/templates/todo/.prettierrc.json +9 -0
  77. package/templates/todo/README.md +9 -9
  78. package/templates/todo/package.json +22 -17
  79. package/templates/todo/src/app.css +80 -80
  80. package/templates/todo/src/app.d.ts +7 -7
  81. package/templates/todo/src/hooks.server.ts +9 -9
  82. package/templates/todo/src/routes/+error.svelte +10 -7
  83. package/templates/todo/src/routes/+layout.server.ts +4 -4
  84. package/templates/todo/src/routes/+layout.svelte +2 -2
  85. package/templates/todo/src/routes/+page.svelte +44 -44
  86. package/templates/todo/template.json +1 -1
  87. package/templates/todo/tsconfig.json +20 -20
package/src/core/hooks.ts CHANGED
@@ -8,71 +8,71 @@
8
8
  // ─── Cookie Types ─────────────────────────────────────────
9
9
 
10
10
  export interface CookieOptions {
11
- path?: string;
12
- domain?: string;
13
- /** Max-Age in seconds */
14
- maxAge?: number;
15
- expires?: Date;
16
- httpOnly?: boolean;
17
- secure?: boolean;
18
- sameSite?: "Strict" | "Lax" | "None";
11
+ path?: string;
12
+ domain?: string;
13
+ /** Max-Age in seconds */
14
+ maxAge?: number;
15
+ expires?: Date;
16
+ httpOnly?: boolean;
17
+ secure?: boolean;
18
+ sameSite?: "Strict" | "Lax" | "None";
19
19
  }
20
20
 
21
21
  export interface Cookies {
22
- /** Get a cookie value by name */
23
- get(name: string): string | undefined;
24
- /** Get all incoming cookies as a plain object */
25
- getAll(): Record<string, string>;
26
- /** Set a cookie (added to the response as Set-Cookie) */
27
- set(name: string, value: string, options?: CookieOptions): void;
28
- /** Delete a cookie by setting Max-Age=0 */
29
- delete(name: string, options?: Pick<CookieOptions, "path" | "domain">): void;
22
+ /** Get a cookie value by name */
23
+ get(name: string): string | undefined;
24
+ /** Get all incoming cookies as a plain object */
25
+ getAll(): Record<string, string>;
26
+ /** Set a cookie (added to the response as Set-Cookie) */
27
+ set(name: string, value: string, options?: CookieOptions): void;
28
+ /** Delete a cookie by setting Max-Age=0 */
29
+ delete(name: string, options?: Pick<CookieOptions, "path" | "domain">): void;
30
30
  }
31
31
 
32
32
  // ─── Event Types ──────────────────────────────────────────
33
33
 
34
34
  export type RequestEvent = {
35
- request: Request;
36
- url: URL;
37
- locals: Record<string, any>;
38
- params: Record<string, string>;
39
- cookies: Cookies;
35
+ request: Request;
36
+ url: URL;
37
+ locals: Record<string, any>;
38
+ params: Record<string, string>;
39
+ cookies: Cookies;
40
40
  };
41
41
 
42
42
  export type LoadEvent = {
43
- url: URL;
44
- params: Record<string, string>;
45
- locals: Record<string, any>;
46
- cookies: Cookies;
47
- fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
48
- parent: () => Promise<Record<string, any>>;
49
- metadata: Record<string, any> | null;
43
+ url: URL;
44
+ params: Record<string, string>;
45
+ locals: Record<string, any>;
46
+ cookies: Cookies;
47
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
48
+ parent: () => Promise<Record<string, any>>;
49
+ metadata: Record<string, any> | null;
50
50
  };
51
51
 
52
52
  export type ResolveFunction = (event: RequestEvent) => MaybePromise<Response>;
53
53
 
54
54
  export type Handle = (input: {
55
- event: RequestEvent;
56
- resolve: ResolveFunction;
55
+ event: RequestEvent;
56
+ resolve: ResolveFunction;
57
57
  }) => MaybePromise<Response>;
58
58
 
59
59
  // ─── Metadata Types ──────────────────────────────────────
60
60
 
61
61
  export type MetadataEvent = {
62
- params: Record<string, string>;
63
- url: URL;
64
- locals: Record<string, any>;
65
- cookies: Cookies;
66
- fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
62
+ params: Record<string, string>;
63
+ url: URL;
64
+ locals: Record<string, any>;
65
+ cookies: Cookies;
66
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
67
67
  };
68
68
 
69
69
  export type Metadata = {
70
- title?: string;
71
- description?: string;
72
- meta?: Array<{ name?: string; property?: string; content: string }>;
73
- lang?: string;
74
- link?: Array<{ rel: string; href: string; hreflang?: string }>;
75
- data?: Record<string, any>;
70
+ title?: string;
71
+ description?: string;
72
+ meta?: Array<{ name?: string; property?: string; content: string }>;
73
+ lang?: string;
74
+ link?: Array<{ rel: string; href: string; hreflang?: string }>;
75
+ data?: Record<string, any>;
76
76
  };
77
77
 
78
78
  type MaybePromise<T> = T | Promise<T>;
@@ -84,13 +84,13 @@ type MaybePromise<T> = T | Promise<T>;
84
84
  * Each handler's `resolve` points to the next handler in the chain.
85
85
  */
86
86
  export function sequence(...handlers: Handle[]): Handle {
87
- return ({ event, resolve }) => {
88
- let next = resolve;
89
- for (let i = handlers.length - 1; i >= 0; i--) {
90
- const handler = handlers[i]!;
91
- const prev = next;
92
- next = (e: RequestEvent) => handler({ event: e, resolve: prev });
93
- }
94
- return next(event);
95
- };
87
+ return ({ event, resolve }) => {
88
+ let next = resolve;
89
+ for (let i = handlers.length - 1; i >= 0; i--) {
90
+ const handler = handlers[i]!;
91
+ const prev = next;
92
+ next = (e: RequestEvent) => handler({ event: e, resolve: prev });
93
+ }
94
+ return next(event);
95
+ };
96
96
  }
package/src/core/html.ts CHANGED
@@ -6,10 +6,10 @@ import { getDeclaredEnvKeys } from "./env.ts";
6
6
  // Cached at startup; server restarts on rebuild in dev anyway.
7
7
 
8
8
  export const distManifest: { js: string[]; css: string[]; entry: string } = (() => {
9
- const p = "./dist/manifest.json";
10
- return existsSync(p)
11
- ? JSON.parse(readFileSync(p, "utf-8"))
12
- : { js: [], css: [], entry: "hydrate.js" };
9
+ const p = "./dist/manifest.json";
10
+ return existsSync(p)
11
+ ? JSON.parse(readFileSync(p, "utf-8"))
12
+ : { js: [], css: [], entry: "hydrate.js" };
13
13
  })();
14
14
 
15
15
  export const isDev = process.env.NODE_ENV !== "production";
@@ -19,21 +19,21 @@ const cacheBust = isDev ? `?v=${Date.now()}` : "";
19
19
 
20
20
  /** Escapes JSON for safe embedding inside <script> tags. Prevents XSS via </script> injection. */
21
21
  export function safeJsonStringify(data: unknown): string {
22
- const map: Record<string, string> = {
23
- "<": "\\u003c",
24
- ">": "\\u003e",
25
- "&": "\\u0026",
26
- "\u2028": "\\u2028",
27
- "\u2029": "\\u2029",
28
- };
29
- let json: string;
30
- try {
31
- json = JSON.stringify(data);
32
- } catch {
33
- console.error("safeJsonStringify: failed to serialize data (circular reference?)");
34
- json = "null";
35
- }
36
- return json.replace(/[<>&\u2028\u2029]/g, c => map[c]);
22
+ const map: Record<string, string> = {
23
+ "<": "\\u003c",
24
+ ">": "\\u003e",
25
+ "&": "\\u0026",
26
+ "\u2028": "\\u2028",
27
+ "\u2029": "\\u2029",
28
+ };
29
+ let json: string;
30
+ try {
31
+ json = JSON.stringify(data);
32
+ } catch {
33
+ console.error("safeJsonStringify: failed to serialize data (circular reference?)");
34
+ json = "null";
35
+ }
36
+ return json.replace(/[<>&\u2028\u2029]/g, (c) => map[c]);
37
37
  }
38
38
 
39
39
  // ─── Public Env Injection ─────────────────────────────────
@@ -44,51 +44,60 @@ export function safeJsonStringify(data: unknown): string {
44
44
  * that happen to start with PUBLIC_.
45
45
  */
46
46
  function getPublicDynamicEnv(): Record<string, string> {
47
- const declared = getDeclaredEnvKeys();
48
- const result: Record<string, string> = {};
49
- for (const key of declared) {
50
- if (key.startsWith("PUBLIC_") && !key.startsWith("PUBLIC_STATIC_")) {
51
- const value = process.env[key];
52
- if (value !== undefined) result[key] = value;
53
- }
54
- }
55
- return result;
47
+ const declared = getDeclaredEnvKeys();
48
+ const result: Record<string, string> = {};
49
+ for (const key of declared) {
50
+ if (key.startsWith("PUBLIC_") && !key.startsWith("PUBLIC_STATIC_")) {
51
+ const value = process.env[key];
52
+ if (value !== undefined) result[key] = value;
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ // ─── Lang Validation ──────────────────────────────────────
59
+
60
+ const LANG_RE = /^[a-zA-Z0-9-]{1,35}$/;
61
+ export function safeLang(lang?: string): string {
62
+ return lang && LANG_RE.test(lang) ? lang : "en";
56
63
  }
57
64
 
58
65
  // ─── HTML Builder ─────────────────────────────────────────
59
66
 
60
67
  export function buildHtml(
61
- body: string,
62
- head: string,
63
- pageData: any,
64
- layoutData: any[],
65
- csr = true,
66
- formData: any = null,
67
- lang?: string,
68
+ body: string,
69
+ head: string,
70
+ pageData: any,
71
+ layoutData: any[],
72
+ csr = true,
73
+ formData: any = null,
74
+ lang?: string,
75
+ ssr = true,
68
76
  ): string {
69
- const cssLinks = (distManifest.css ?? [])
70
- .map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
71
- .join("\n ");
77
+ const cssLinks = (distManifest.css ?? [])
78
+ .map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
79
+ .join("\n ");
72
80
 
73
- const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
81
+ const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
74
82
 
75
- const publicEnv = getPublicDynamicEnv();
76
- const envScript = Object.keys(publicEnv).length > 0
77
- ? `\n <script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
78
- : "";
83
+ const publicEnv = getPublicDynamicEnv();
84
+ const envScript =
85
+ Object.keys(publicEnv).length > 0
86
+ ? `\n <script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
87
+ : "";
79
88
 
80
- const formScript = formData != null
81
- ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};`
82
- : "";
89
+ const formScript =
90
+ formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
91
+ const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
83
92
 
84
- const scripts = csr
85
- ? `${envScript}\n <script>window.__BOSIA_PAGE_DATA__=${safeJsonStringify(pageData)};window.__BOSIA_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formScript}</script>\n <script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`
86
- : isDev
87
- ? `\n <script>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`
88
- : "";
93
+ const scripts = csr
94
+ ? `${envScript}\n <script>${ssrFlag}window.__BOSIA_PAGE_DATA__=${safeJsonStringify(pageData)};window.__BOSIA_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formScript}</script>\n <script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`
95
+ : isDev
96
+ ? `\n <script>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`
97
+ : "";
89
98
 
90
- return `<!DOCTYPE html>
91
- <html lang="${lang || "en"}">
99
+ return `<!DOCTYPE html>
100
+ <html lang="${safeLang(lang)}">
92
101
  <head>
93
102
  <meta charset="UTF-8">
94
103
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -113,113 +122,157 @@ const _shellOpenCache = new Map<string, string>();
113
122
 
114
123
  /** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
115
124
  export function buildHtmlShellOpen(lang?: string): string {
116
- const key = lang || "en";
117
- const cached = _shellOpenCache.get(key);
118
- if (cached) return cached;
119
- const cssLinks = (distManifest.css ?? [])
120
- .map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
121
- .join("\n ");
122
- const result = `<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
123
- ` <meta charset="UTF-8">\n` +
124
- ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
125
- ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
126
- ` ${cssLinks}\n` +
127
- ` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
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` +
129
- ` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`;
130
- _shellOpenCache.set(key, result);
131
- return result;
125
+ const key = safeLang(lang);
126
+ const cached = _shellOpenCache.get(key);
127
+ if (cached) return cached;
128
+ const cssLinks = (distManifest.css ?? [])
129
+ .map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
130
+ .join("\n ");
131
+ const result =
132
+ `<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
133
+ ` <meta charset="UTF-8">\n` +
134
+ ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
135
+ ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
136
+ ` ${cssLinks}\n` +
137
+ ` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
138
+ ` <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` +
139
+ ` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`;
140
+ _shellOpenCache.set(key, result);
141
+ return result;
132
142
  }
133
143
 
134
- const SPINNER = `<div id="__bs__"><style>` +
135
- `:root{--bosia-loading-color:#f73b27}` +
136
- `#__bs__{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}` +
137
- `#__bs__ i{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--bosia-loading-color);` +
138
- `border-radius:50%;animation:__bs__ .8s linear infinite}` +
139
- `@keyframes __bs__{to{transform:rotate(360deg)}}</style><i></i></div>`;
144
+ const SPINNER =
145
+ `<div id="__bs__"><style>` +
146
+ `:root{--bosia-loading-color:#f73b27}` +
147
+ `#__bs__{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}` +
148
+ `#__bs__ i{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--bosia-loading-color);` +
149
+ `border-radius:50%;animation:__bs__ .8s linear infinite}` +
150
+ `@keyframes __bs__{to{transform:rotate(360deg)}}</style><i></i></div>`;
140
151
 
141
152
  /** Chunk 2: metadata tags + close </head> + open <body> + spinner */
142
153
  export function buildMetadataChunk(metadata: Metadata | null): string {
143
- let out = "\n";
144
- if (metadata) {
145
- if (metadata.title) out += ` <title>${escapeHtml(metadata.title)}</title>\n`;
146
- if (metadata.description) {
147
- out += ` <meta name="description" content="${escapeAttr(metadata.description)}">\n`;
148
- }
149
- if (metadata.meta) {
150
- for (const m of metadata.meta) {
151
- const attrs = m.name ? `name="${escapeAttr(m.name)}"` : `property="${escapeAttr(m.property ?? "")}"`;
152
- out += ` <meta ${attrs} content="${escapeAttr(m.content)}">\n`;
153
- }
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
- }
162
- } else {
163
- out += ` <title>Bosia App</title>\n`;
164
- }
165
- out += `</head>\n<body>\n${SPINNER}`;
166
- return out;
154
+ let out = "\n";
155
+ if (metadata) {
156
+ if (metadata.title) out += ` <title>${escapeHtml(metadata.title)}</title>\n`;
157
+ if (metadata.description) {
158
+ out += ` <meta name="description" content="${escapeAttr(metadata.description)}">\n`;
159
+ }
160
+ if (metadata.meta) {
161
+ for (const m of metadata.meta) {
162
+ const attrs = m.name
163
+ ? `name="${escapeAttr(m.name)}"`
164
+ : `property="${escapeAttr(m.property ?? "")}"`;
165
+ out += ` <meta ${attrs} content="${escapeAttr(m.content)}">\n`;
166
+ }
167
+ }
168
+ if (metadata.link) {
169
+ for (const l of metadata.link) {
170
+ let attrs = `rel="${escapeAttr(l.rel)}" href="${escapeAttr(l.href)}"`;
171
+ if (l.hreflang) attrs += ` hreflang="${escapeAttr(l.hreflang)}"`;
172
+ out += ` <link ${attrs}>\n`;
173
+ }
174
+ }
175
+ } else {
176
+ out += ` <title>Bosia App</title>\n`;
177
+ }
178
+ out += `</head>\n<body>\n${SPINNER}`;
179
+ return out;
167
180
  }
168
181
 
169
- function escapeHtml(s: string): string {
170
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
182
+ export function escapeHtml(s: string): string {
183
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
171
184
  }
172
185
 
173
- function escapeAttr(s: string): string {
174
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
186
+ export function escapeAttr(s: string): string {
187
+ return s
188
+ .replace(/&/g, "&amp;")
189
+ .replace(/"/g, "&quot;")
190
+ .replace(/</g, "&lt;")
191
+ .replace(/>/g, "&gt;");
175
192
  }
176
193
 
177
194
  export function buildHtmlTail(
178
- body: string,
179
- head: string,
180
- pageData: any,
181
- layoutData: any[],
182
- csr: boolean,
183
- formData: any = null,
195
+ body: string,
196
+ head: string,
197
+ pageData: any,
198
+ layoutData: any[],
199
+ csr: boolean,
200
+ formData: any = null,
201
+ ssr = true,
184
202
  ): string {
185
- let out = `<script>document.getElementById('__bs__').remove()</script>`;
186
- out += `\n<div id="app">${body}</div>`;
187
- if (head) out += `\n<script>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
188
- if (csr) {
189
- const publicEnv = getPublicDynamicEnv();
190
- if (Object.keys(publicEnv).length > 0) {
191
- out += `\n<script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`;
192
- }
193
- const formInject = formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
194
- out += `\n<script>window.__BOSIA_PAGE_DATA__=${safeJsonStringify(pageData)};` +
195
- `window.__BOSIA_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formInject}</script>`;
196
- out += `\n<script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`;
197
- } else if (isDev) {
198
- out += `\n<script>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
199
- }
200
- out += `\n</body>\n</html>`;
201
- return out;
203
+ let out = `<script>document.getElementById('__bs__').remove()</script>`;
204
+ out += `\n<div id="app">${body}</div>`;
205
+ if (head)
206
+ out += `\n<script>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
207
+ if (csr) {
208
+ const publicEnv = getPublicDynamicEnv();
209
+ if (Object.keys(publicEnv).length > 0) {
210
+ out += `\n<script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`;
211
+ }
212
+ const formInject =
213
+ formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
214
+ const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
215
+ out +=
216
+ `\n<script>${ssrFlag}window.__BOSIA_PAGE_DATA__=${safeJsonStringify(pageData)};` +
217
+ `window.__BOSIA_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formInject}</script>`;
218
+ out += `\n<script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`;
219
+ } else if (isDev) {
220
+ out += `\n<script>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
221
+ }
222
+ out += `\n</body>\n</html>`;
223
+ return out;
202
224
  }
203
225
 
204
226
  // ─── Gzip Compression ────────────────────────────────────
205
227
 
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 };
208
- const accept = req.headers.get("accept-encoding") ?? "";
209
- // Skip compression in dev — the dev proxy's fetch() auto-decompresses gzip
210
- // responses but keeps the Content-Encoding header, causing ERR_CONTENT_DECODING_FAILED.
211
- if (!isDev && body.length > 1024 && accept.includes("gzip")) {
212
- return new Response(Bun.gzipSync(body), { status, headers: { ...headers, "Content-Encoding": "gzip" } });
213
- }
214
- return new Response(body, { status, headers });
228
+ const GZIP_MIN_BYTES = 2048;
229
+
230
+ export function compress(
231
+ body: string,
232
+ contentType: string,
233
+ req: Request,
234
+ status = 200,
235
+ extraHeaders?: Record<string, string>,
236
+ ): Response {
237
+ const headers: Record<string, string> = {
238
+ "Content-Type": contentType,
239
+ Vary: "Accept-Encoding",
240
+ ...extraHeaders,
241
+ };
242
+ const accept = req.headers.get("accept-encoding") ?? "";
243
+ const bytes = new TextEncoder().encode(body);
244
+ // Skip compression in dev — the dev proxy's fetch() auto-decompresses gzip
245
+ // responses but keeps the Content-Encoding header, causing ERR_CONTENT_DECODING_FAILED.
246
+ if (!isDev && bytes.length > GZIP_MIN_BYTES && accept.includes("gzip")) {
247
+ return new Response(Bun.gzipSync(bytes), {
248
+ status,
249
+ headers: { ...headers, "Content-Encoding": "gzip" },
250
+ });
251
+ }
252
+ return new Response(bytes, { status, headers });
215
253
  }
216
254
 
217
255
  // ─── Static File Detection ────────────────────────────────
218
256
 
219
- const STATIC_EXTS = new Set([".ico", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".css", ".js", ".woff", ".woff2", ".ttf", ".xml", ".txt"]);
257
+ const STATIC_EXTS = new Set([
258
+ ".ico",
259
+ ".png",
260
+ ".jpg",
261
+ ".jpeg",
262
+ ".gif",
263
+ ".webp",
264
+ ".svg",
265
+ ".css",
266
+ ".js",
267
+ ".woff",
268
+ ".woff2",
269
+ ".ttf",
270
+ ".xml",
271
+ ".txt",
272
+ ]);
220
273
 
221
274
  export function isStaticPath(path: string): boolean {
222
- if (path.startsWith("/dist/") || path.startsWith("/__bosia/")) return true;
223
- const dot = path.lastIndexOf(".");
224
- return dot !== -1 && STATIC_EXTS.has(path.slice(dot));
275
+ if (path.startsWith("/dist/") || path.startsWith("/__bosia/")) return true;
276
+ const dot = path.lastIndexOf(".");
277
+ return dot !== -1 && STATIC_EXTS.has(path.slice(dot));
225
278
  }