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.
- package/README.md +39 -39
- package/package.json +56 -53
- 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 +291 -132
- 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 -153
- package/src/core/client/appState.svelte.ts +57 -0
- package/src/core/client/enhance.ts +112 -0
- package/src/core/client/hydrate.ts +97 -65
- 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 -128
- 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 +192 -139
- 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 -118
- package/src/core/renderer.ts +359 -265
- 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 +543 -370
- package/src/core/types.ts +25 -20
- package/src/lib/client.ts +12 -0
- 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,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
|
-
|
|
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
|
+
}
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
const cssLinks = (distManifest.css ?? [])
|
|
78
|
+
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
79
|
+
.join("\n ");
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
const formScript =
|
|
90
|
+
formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
|
|
91
|
+
const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
<html lang="${lang
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
182
|
+
export function escapeHtml(s: string): string {
|
|
183
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
171
184
|
}
|
|
172
185
|
|
|
173
|
-
function escapeAttr(s: string): string {
|
|
174
|
-
|
|
186
|
+
export function escapeAttr(s: string): string {
|
|
187
|
+
return s
|
|
188
|
+
.replace(/&/g, "&")
|
|
189
|
+
.replace(/"/g, """)
|
|
190
|
+
.replace(/</g, "<")
|
|
191
|
+
.replace(/>/g, ">");
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
export function buildHtmlTail(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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([
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
}
|