bosia 0.2.3 → 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.
- package/README.md +39 -39
- package/package.json +56 -54
- package/src/ambient.d.ts +31 -0
- package/src/cli/add.ts +120 -114
- package/src/cli/build.ts +10 -10
- package/src/cli/create.ts +142 -137
- package/src/cli/dev.ts +8 -8
- package/src/cli/feat.ts +266 -258
- package/src/cli/index.ts +51 -42
- package/src/cli/registry.ts +136 -115
- package/src/cli/start.ts +17 -17
- package/src/cli/test.ts +25 -0
- package/src/core/build.ts +72 -56
- package/src/core/client/App.svelte +177 -156
- package/src/core/client/appState.svelte.ts +33 -31
- package/src/core/client/enhance.ts +83 -78
- package/src/core/client/hydrate.ts +95 -81
- package/src/core/client/prefetch.ts +101 -94
- package/src/core/client/router.svelte.ts +64 -51
- package/src/core/cookies.ts +70 -66
- package/src/core/cors.ts +44 -35
- package/src/core/csrf.ts +38 -38
- package/src/core/dedup.ts +17 -17
- package/src/core/dev.ts +165 -168
- package/src/core/env.ts +155 -148
- package/src/core/envCodegen.ts +73 -73
- package/src/core/errors.ts +48 -49
- package/src/core/hooks.ts +50 -50
- package/src/core/html.ts +184 -145
- package/src/core/matcher.ts +130 -121
- package/src/core/paths.ts +8 -10
- package/src/core/plugin.ts +113 -107
- package/src/core/prerender.ts +191 -122
- package/src/core/renderer.ts +359 -286
- package/src/core/routeFile.ts +140 -127
- package/src/core/routeTypes.ts +144 -83
- package/src/core/scanner.ts +125 -95
- package/src/core/server.ts +538 -424
- package/src/core/types.ts +25 -20
- package/src/lib/index.ts +8 -8
- package/src/lib/utils.ts +44 -30
- package/templates/default/.prettierignore +5 -0
- package/templates/default/.prettierrc.json +9 -0
- package/templates/default/README.md +5 -5
- package/templates/default/package.json +22 -18
- package/templates/default/src/app.css +80 -80
- package/templates/default/src/app.d.ts +3 -3
- package/templates/default/src/routes/+error.svelte +7 -10
- package/templates/default/src/routes/+layout.svelte +2 -2
- package/templates/default/src/routes/+page.svelte +31 -29
- package/templates/default/src/routes/about/+page.svelte +3 -3
- package/templates/default/tsconfig.json +20 -20
- package/templates/demo/.prettierignore +5 -0
- package/templates/demo/.prettierrc.json +9 -0
- package/templates/demo/README.md +9 -9
- package/templates/demo/package.json +22 -17
- package/templates/demo/src/app.css +80 -80
- package/templates/demo/src/app.d.ts +3 -3
- package/templates/demo/src/hooks.server.ts +9 -9
- package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
- package/templates/demo/src/routes/(public)/+page.svelte +96 -67
- package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
- package/templates/demo/src/routes/+error.svelte +10 -7
- package/templates/demo/src/routes/+layout.server.ts +4 -4
- package/templates/demo/src/routes/+layout.svelte +2 -2
- package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
- package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
- package/templates/demo/src/routes/api/hello/+server.ts +25 -25
- package/templates/demo/tsconfig.json +20 -20
- package/templates/todo/.prettierignore +5 -0
- package/templates/todo/.prettierrc.json +9 -0
- package/templates/todo/README.md +9 -9
- package/templates/todo/package.json +22 -17
- package/templates/todo/src/app.css +80 -80
- package/templates/todo/src/app.d.ts +7 -7
- package/templates/todo/src/hooks.server.ts +9 -9
- package/templates/todo/src/routes/+error.svelte +10 -7
- package/templates/todo/src/routes/+layout.server.ts +4 -4
- package/templates/todo/src/routes/+layout.svelte +2 -2
- package/templates/todo/src/routes/+page.svelte +44 -44
- package/templates/todo/template.json +1 -1
- 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
55
|
+
event: RequestEvent;
|
|
56
|
+
resolve: ResolveFunction;
|
|
57
57
|
}) => MaybePromise<Response>;
|
|
58
58
|
|
|
59
59
|
// ─── Metadata Types ──────────────────────────────────────
|
|
60
60
|
|
|
61
61
|
export type MetadataEvent = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const cssLinks = (distManifest.css ?? [])
|
|
78
|
+
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
79
|
+
.join("\n ");
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
182
|
+
export function escapeHtml(s: string): string {
|
|
183
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
function escapeAttr(s: string): string {
|
|
183
|
-
|
|
186
|
+
export function escapeAttr(s: string): string {
|
|
187
|
+
return s
|
|
188
|
+
.replace(/&/g, "&")
|
|
189
|
+
.replace(/"/g, """)
|
|
190
|
+
.replace(/</g, "<")
|
|
191
|
+
.replace(/>/g, ">");
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
export function buildHtmlTail(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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([
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
}
|