bosia 0.2.3 → 0.3.1

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 (86) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -54
  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 +7 -9
  8. package/src/cli/feat.ts +266 -258
  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 -156
  15. package/src/core/client/appState.svelte.ts +33 -31
  16. package/src/core/client/enhance.ts +83 -78
  17. package/src/core/client/hydrate.ts +95 -81
  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 +196 -168
  25. package/src/core/env.ts +160 -148
  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 +184 -145
  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 -122
  34. package/src/core/renderer.ts +359 -286
  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 +538 -424
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/index.ts +8 -8
  41. package/src/lib/utils.ts +44 -30
  42. package/templates/default/.prettierignore +5 -0
  43. package/templates/default/.prettierrc.json +9 -0
  44. package/templates/default/README.md +5 -5
  45. package/templates/default/package.json +22 -18
  46. package/templates/default/src/app.css +80 -80
  47. package/templates/default/src/app.d.ts +3 -3
  48. package/templates/default/src/routes/+error.svelte +7 -10
  49. package/templates/default/src/routes/+layout.svelte +2 -2
  50. package/templates/default/src/routes/+page.svelte +30 -32
  51. package/templates/default/src/routes/about/+page.svelte +3 -3
  52. package/templates/default/tsconfig.json +20 -20
  53. package/templates/demo/.prettierignore +5 -0
  54. package/templates/demo/.prettierrc.json +9 -0
  55. package/templates/demo/README.md +9 -9
  56. package/templates/demo/package.json +22 -17
  57. package/templates/demo/src/app.css +80 -80
  58. package/templates/demo/src/app.d.ts +3 -3
  59. package/templates/demo/src/hooks.server.ts +9 -9
  60. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  61. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  62. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  63. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  64. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  65. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  67. package/templates/demo/src/routes/+error.svelte +10 -7
  68. package/templates/demo/src/routes/+layout.server.ts +4 -4
  69. package/templates/demo/src/routes/+layout.svelte +2 -2
  70. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  71. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  72. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  73. package/templates/demo/tsconfig.json +20 -20
  74. package/templates/todo/.prettierignore +5 -0
  75. package/templates/todo/.prettierrc.json +9 -0
  76. package/templates/todo/README.md +9 -9
  77. package/templates/todo/package.json +22 -17
  78. package/templates/todo/src/app.css +80 -80
  79. package/templates/todo/src/app.d.ts +7 -7
  80. package/templates/todo/src/hooks.server.ts +9 -9
  81. package/templates/todo/src/routes/+error.svelte +10 -7
  82. package/templates/todo/src/routes/+layout.server.ts +4 -4
  83. package/templates/todo/src/routes/+layout.svelte +2 -2
  84. package/templates/todo/src/routes/+page.svelte +44 -44
  85. package/templates/todo/template.json +1 -1
  86. 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,59 +44,59 @@ 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
56
  }
57
57
 
58
58
  // ─── Lang Validation ──────────────────────────────────────
59
59
 
60
60
  const LANG_RE = /^[a-zA-Z0-9-]{1,35}$/;
61
- function safeLang(lang?: string): string {
62
- return lang && LANG_RE.test(lang) ? lang : "en";
61
+ export function safeLang(lang?: string): string {
62
+ return lang && LANG_RE.test(lang) ? lang : "en";
63
63
  }
64
64
 
65
65
  // ─── HTML Builder ─────────────────────────────────────────
66
66
 
67
67
  export function buildHtml(
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
+ body: string,
69
+ head: string,
70
+ pageData: any,
71
+ layoutData: any[],
72
+ csr = true,
73
+ formData: any = null,
74
+ lang?: string,
75
+ ssr = true,
76
76
  ): string {
77
- const cssLinks = (distManifest.css ?? [])
78
- .map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
79
- .join("\n ");
77
+ const cssLinks = (distManifest.css ?? [])
78
+ .map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
79
+ .join("\n ");
80
80
 
81
- const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
81
+ const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
82
82
 
83
- const publicEnv = getPublicDynamicEnv();
84
- const envScript = Object.keys(publicEnv).length > 0
85
- ? `\n <script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
86
- : "";
83
+ const publicEnv = getPublicDynamicEnv();
84
+ const envScript =
85
+ Object.keys(publicEnv).length > 0
86
+ ? `\n <script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
87
+ : "";
87
88
 
88
- const formScript = formData != null
89
- ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};`
90
- : "";
91
- const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
89
+ const formScript =
90
+ formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
91
+ const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
92
92
 
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
- : "";
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
+ : "";
98
98
 
99
- return `<!DOCTYPE html>
99
+ return `<!DOCTYPE html>
100
100
  <html lang="${safeLang(lang)}">
101
101
  <head>
102
102
  <meta charset="UTF-8">
@@ -122,118 +122,157 @@ const _shellOpenCache = new Map<string, string>();
122
122
 
123
123
  /** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
124
124
  export function buildHtmlShellOpen(lang?: string): string {
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 = `<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
132
- ` <meta charset="UTF-8">\n` +
133
- ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
134
- ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
135
- ` ${cssLinks}\n` +
136
- ` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
137
- ` <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` +
138
- ` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`;
139
- _shellOpenCache.set(key, result);
140
- 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;
141
142
  }
142
143
 
143
- const SPINNER = `<div id="__bs__"><style>` +
144
- `:root{--bosia-loading-color:#f73b27}` +
145
- `#__bs__{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}` +
146
- `#__bs__ i{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:var(--bosia-loading-color);` +
147
- `border-radius:50%;animation:__bs__ .8s linear infinite}` +
148
- `@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>`;
149
151
 
150
152
  /** Chunk 2: metadata tags + close </head> + open <body> + spinner */
151
153
  export function buildMetadataChunk(metadata: Metadata | null): string {
152
- let out = "\n";
153
- if (metadata) {
154
- if (metadata.title) out += ` <title>${escapeHtml(metadata.title)}</title>\n`;
155
- if (metadata.description) {
156
- out += ` <meta name="description" content="${escapeAttr(metadata.description)}">\n`;
157
- }
158
- if (metadata.meta) {
159
- for (const m of metadata.meta) {
160
- const attrs = m.name ? `name="${escapeAttr(m.name)}"` : `property="${escapeAttr(m.property ?? "")}"`;
161
- out += ` <meta ${attrs} content="${escapeAttr(m.content)}">\n`;
162
- }
163
- }
164
- if (metadata.link) {
165
- for (const l of metadata.link) {
166
- let attrs = `rel="${escapeAttr(l.rel)}" href="${escapeAttr(l.href)}"`;
167
- if (l.hreflang) attrs += ` hreflang="${escapeAttr(l.hreflang)}"`;
168
- out += ` <link ${attrs}>\n`;
169
- }
170
- }
171
- } else {
172
- out += ` <title>Bosia App</title>\n`;
173
- }
174
- out += `</head>\n<body>\n${SPINNER}`;
175
- 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;
176
180
  }
177
181
 
178
- function escapeHtml(s: string): string {
179
- 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;");
180
184
  }
181
185
 
182
- function escapeAttr(s: string): string {
183
- 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;");
184
192
  }
185
193
 
186
194
  export function buildHtmlTail(
187
- body: string,
188
- head: string,
189
- pageData: any,
190
- layoutData: any[],
191
- csr: boolean,
192
- formData: any = null,
193
- ssr = true,
195
+ body: string,
196
+ head: string,
197
+ pageData: any,
198
+ layoutData: any[],
199
+ csr: boolean,
200
+ formData: any = null,
201
+ ssr = true,
194
202
  ): string {
195
- let out = `<script>document.getElementById('__bs__').remove()</script>`;
196
- out += `\n<div id="app">${body}</div>`;
197
- if (head) out += `\n<script>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
198
- if (csr) {
199
- const publicEnv = getPublicDynamicEnv();
200
- if (Object.keys(publicEnv).length > 0) {
201
- out += `\n<script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`;
202
- }
203
- const formInject = formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
204
- const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
205
- out += `\n<script>${ssrFlag}window.__BOSIA_PAGE_DATA__=${safeJsonStringify(pageData)};` +
206
- `window.__BOSIA_LAYOUT_DATA__=${safeJsonStringify(layoutData)};${formInject}</script>`;
207
- out += `\n<script type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`;
208
- } else if (isDev) {
209
- 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>`;
210
- }
211
- out += `\n</body>\n</html>`;
212
- 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;
213
224
  }
214
225
 
215
226
  // ─── Gzip Compression ────────────────────────────────────
216
227
 
217
228
  const GZIP_MIN_BYTES = 2048;
218
229
 
219
- export function compress(body: string, contentType: string, req: Request, status = 200, extraHeaders?: Record<string, string>): Response {
220
- const headers: Record<string, string> = { "Content-Type": contentType, "Vary": "Accept-Encoding", ...extraHeaders };
221
- const accept = req.headers.get("accept-encoding") ?? "";
222
- const bytes = new TextEncoder().encode(body);
223
- // Skip compression in dev — the dev proxy's fetch() auto-decompresses gzip
224
- // responses but keeps the Content-Encoding header, causing ERR_CONTENT_DECODING_FAILED.
225
- if (!isDev && bytes.length > GZIP_MIN_BYTES && accept.includes("gzip")) {
226
- return new Response(Bun.gzipSync(bytes), { status, headers: { ...headers, "Content-Encoding": "gzip" } });
227
- }
228
- return new Response(bytes, { status, headers });
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 });
229
253
  }
230
254
 
231
255
  // ─── Static File Detection ────────────────────────────────
232
256
 
233
- 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
+ ]);
234
273
 
235
274
  export function isStaticPath(path: string): boolean {
236
- if (path.startsWith("/dist/") || path.startsWith("/__bosia/")) return true;
237
- const dot = path.lastIndexOf(".");
238
- 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));
239
278
  }