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.
- 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 +7 -9
- 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 +196 -168
- package/src/core/env.ts +160 -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 +30 -32
- 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/renderer.ts
CHANGED
|
@@ -5,36 +5,43 @@ import { serverRoutes, errorPage } from "bosia:routes";
|
|
|
5
5
|
import type { Cookies } from "./hooks.ts";
|
|
6
6
|
import { HttpError, Redirect } from "./errors.ts";
|
|
7
7
|
import App from "./client/App.svelte";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildHtml,
|
|
10
|
+
buildHtmlShellOpen,
|
|
11
|
+
buildMetadataChunk,
|
|
12
|
+
buildHtmlTail,
|
|
13
|
+
compress,
|
|
14
|
+
isDev,
|
|
15
|
+
} from "./html.ts";
|
|
9
16
|
import type { Metadata } from "./hooks.ts";
|
|
10
17
|
|
|
11
18
|
// ─── Timeout Helpers ─────────────────────────────────────
|
|
12
19
|
|
|
13
20
|
class LoadTimeoutError extends Error {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
constructor(label: string, ms: number) {
|
|
22
|
+
super(`${label} timed out after ${ms}ms`);
|
|
23
|
+
this.name = "LoadTimeoutError";
|
|
24
|
+
}
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
function parseTimeout(raw: string | undefined, fallback: number): number {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
if (!raw || raw === "Infinity") return 0;
|
|
29
|
+
const n = parseInt(raw, 10);
|
|
30
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const LOAD_TIMEOUT = parseTimeout(process.env.LOAD_TIMEOUT, 5000);
|
|
27
34
|
const METADATA_TIMEOUT = parseTimeout(process.env.METADATA_TIMEOUT, 3000);
|
|
28
35
|
|
|
29
36
|
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
if (ms <= 0) return promise;
|
|
38
|
+
let timer: Timer;
|
|
39
|
+
return Promise.race([
|
|
40
|
+
promise.finally(() => clearTimeout(timer)),
|
|
41
|
+
new Promise<never>(
|
|
42
|
+
(_, reject) => (timer = setTimeout(() => reject(new LoadTimeoutError(label, ms)), ms)),
|
|
43
|
+
),
|
|
44
|
+
]);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
// ─── Internal-Host Allowlist ─────────────────────────────
|
|
@@ -43,20 +50,23 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
|
|
|
43
50
|
// (third-party APIs) gets no Cookie header by default.
|
|
44
51
|
|
|
45
52
|
const INTERNAL_HOSTS: Set<string> = (() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const raw =
|
|
54
|
+
process.env.INTERNAL_HOSTS?.split(",")
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter(Boolean) ?? [];
|
|
57
|
+
const valid = new Set<string>();
|
|
58
|
+
for (const entry of raw) {
|
|
59
|
+
try {
|
|
60
|
+
valid.add(new URL(entry).origin);
|
|
61
|
+
} catch {
|
|
62
|
+
console.warn(`⚠️ INTERNAL_HOSTS: ignoring invalid origin "${entry}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return valid;
|
|
56
66
|
})();
|
|
57
67
|
|
|
58
68
|
if (INTERNAL_HOSTS.size > 0) {
|
|
59
|
-
|
|
69
|
+
console.log(`🍪 Internal hosts (cookies forwarded): ${[...INTERNAL_HOSTS].join(", ")}`);
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
// ─── Session-Aware Fetch ─────────────────────────────────
|
|
@@ -66,33 +76,35 @@ if (INTERNAL_HOSTS.size > 0) {
|
|
|
66
76
|
// to arbitrary third-party hosts (which would leak the session token).
|
|
67
77
|
|
|
68
78
|
function makeFetch(req: Request, url: URL) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
const cookie = req.headers.get("cookie") ?? "";
|
|
80
|
+
const sameOrigin = url.origin;
|
|
81
|
+
|
|
82
|
+
return (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
83
|
+
let targetOrigin: string | null = null;
|
|
84
|
+
let resolved: RequestInfo | URL = input;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (typeof input === "string") {
|
|
88
|
+
const parsed = new URL(input, sameOrigin);
|
|
89
|
+
targetOrigin = parsed.origin;
|
|
90
|
+
resolved = parsed.href;
|
|
91
|
+
} else if (input instanceof URL) {
|
|
92
|
+
targetOrigin = input.origin;
|
|
93
|
+
} else {
|
|
94
|
+
targetOrigin = new URL(input.url).origin;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
targetOrigin = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const headers = new Headers(init?.headers);
|
|
101
|
+
const trusted =
|
|
102
|
+
targetOrigin !== null &&
|
|
103
|
+
(targetOrigin === sameOrigin || INTERNAL_HOSTS.has(targetOrigin));
|
|
104
|
+
if (cookie && trusted && !headers.has("cookie")) headers.set("cookie", cookie);
|
|
105
|
+
|
|
106
|
+
return globalThis.fetch(resolved, { ...init, headers });
|
|
107
|
+
};
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
// ─── Route Data Loader ───────────────────────────────────
|
|
@@ -100,192 +112,234 @@ function makeFetch(req: Request, url: URL) {
|
|
|
100
112
|
// Used by both SSR and the /__bosia/data JSON endpoint.
|
|
101
113
|
|
|
102
114
|
export async function loadRouteData(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
url: URL,
|
|
116
|
+
locals: Record<string, any>,
|
|
117
|
+
req: Request,
|
|
118
|
+
cookies: Cookies,
|
|
119
|
+
metadataData: Record<string, any> | null = null,
|
|
108
120
|
) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
121
|
+
const match = findMatch(serverRoutes, url.pathname);
|
|
122
|
+
if (!match) return null;
|
|
123
|
+
|
|
124
|
+
const { route, params } = match;
|
|
125
|
+
const fetch = makeFetch(req, url);
|
|
126
|
+
const layoutData: Record<string, any>[] = [];
|
|
127
|
+
|
|
128
|
+
// Run layout server loaders root → leaf, each gets parent() data
|
|
129
|
+
for (const ls of route.layoutServers) {
|
|
130
|
+
try {
|
|
131
|
+
const mod = await ls.loader();
|
|
132
|
+
if (typeof mod.load === "function") {
|
|
133
|
+
const parent = async () => {
|
|
134
|
+
const merged: Record<string, any> = {};
|
|
135
|
+
for (let d = 0; d < ls.depth; d++) Object.assign(merged, layoutData[d] ?? {});
|
|
136
|
+
return merged;
|
|
137
|
+
};
|
|
138
|
+
layoutData[ls.depth] =
|
|
139
|
+
(await withTimeout(
|
|
140
|
+
mod.load({ params, url, locals, cookies, parent, fetch, metadata: null }),
|
|
141
|
+
LOAD_TIMEOUT,
|
|
142
|
+
`layout load (depth=${ls.depth}, ${url.pathname})`,
|
|
143
|
+
)) ?? {};
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof HttpError || err instanceof Redirect) throw err;
|
|
147
|
+
if (isDev) console.error("Layout server load error:", err);
|
|
148
|
+
else console.error("Layout server load error:", (err as Error).message ?? err);
|
|
149
|
+
throw new HttpError(500, "Internal Server Error");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Run page server loader
|
|
154
|
+
let pageData: Record<string, any> = {};
|
|
155
|
+
let csr = true;
|
|
156
|
+
let ssr = true;
|
|
157
|
+
if (route.pageServer) {
|
|
158
|
+
try {
|
|
159
|
+
const mod = await route.pageServer();
|
|
160
|
+
if (mod.csr === false) csr = false;
|
|
161
|
+
if (mod.ssr === false) ssr = false;
|
|
162
|
+
if (typeof mod.load === "function") {
|
|
163
|
+
const parent = async () => {
|
|
164
|
+
const merged: Record<string, any> = {};
|
|
165
|
+
for (const d of layoutData) if (d) Object.assign(merged, d);
|
|
166
|
+
return merged;
|
|
167
|
+
};
|
|
168
|
+
pageData =
|
|
169
|
+
(await withTimeout(
|
|
170
|
+
mod.load({
|
|
171
|
+
params,
|
|
172
|
+
url,
|
|
173
|
+
locals,
|
|
174
|
+
cookies,
|
|
175
|
+
parent,
|
|
176
|
+
fetch,
|
|
177
|
+
metadata: metadataData,
|
|
178
|
+
}),
|
|
179
|
+
LOAD_TIMEOUT,
|
|
180
|
+
`page load (${url.pathname})`,
|
|
181
|
+
)) ?? {};
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err instanceof HttpError || err instanceof Redirect) throw err;
|
|
185
|
+
if (isDev) console.error("Page server load error:", err);
|
|
186
|
+
else console.error("Page server load error:", (err as Error).message ?? err);
|
|
187
|
+
throw new HttpError(500, "Internal Server Error");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { pageData: { ...pageData, params }, layoutData, csr, ssr };
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
// ─── Metadata Loader ─────────────────────────────────────
|
|
165
195
|
|
|
166
196
|
export async function loadMetadata(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
197
|
+
route: any,
|
|
198
|
+
params: Record<string, string>,
|
|
199
|
+
url: URL,
|
|
200
|
+
locals: Record<string, any>,
|
|
201
|
+
cookies: Cookies,
|
|
202
|
+
req: Request,
|
|
173
203
|
): Promise<Metadata | null> {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
if (!route.pageServer) return null;
|
|
205
|
+
try {
|
|
206
|
+
const mod = await route.pageServer();
|
|
207
|
+
if (typeof mod.metadata === "function") {
|
|
208
|
+
const fetch = makeFetch(req, url);
|
|
209
|
+
return (
|
|
210
|
+
(await withTimeout(
|
|
211
|
+
mod.metadata({ params, url, locals, cookies, fetch }),
|
|
212
|
+
METADATA_TIMEOUT,
|
|
213
|
+
`metadata (${url.pathname})`,
|
|
214
|
+
)) ?? null
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (isDev) console.error("Metadata load error:", err);
|
|
219
|
+
else console.error("Metadata load error:", (err as Error).message ?? err);
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
// ─── Streaming SSR Renderer ──────────────────────────────
|
|
189
225
|
|
|
190
226
|
export async function renderSSRStream(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
227
|
+
url: URL,
|
|
228
|
+
locals: Record<string, any>,
|
|
229
|
+
req: Request,
|
|
230
|
+
cookies: Cookies,
|
|
195
231
|
): Promise<Response | null> {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
232
|
+
const match = findMatch(serverRoutes, url.pathname);
|
|
233
|
+
if (!match) return null;
|
|
234
|
+
|
|
235
|
+
const { route, params } = match;
|
|
236
|
+
|
|
237
|
+
// ── Pre-stream phase: resolve metadata before committing to a 200 ──
|
|
238
|
+
// Errors here return a proper error response with correct status code.
|
|
239
|
+
let metadata: Metadata | null = null;
|
|
240
|
+
try {
|
|
241
|
+
metadata = await loadMetadata(route, params, url, locals, cookies, req);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err instanceof Redirect) {
|
|
244
|
+
return Response.redirect(err.location, err.status);
|
|
245
|
+
}
|
|
246
|
+
if (err instanceof HttpError) {
|
|
247
|
+
return renderErrorPage(err.status, err.message, url, req);
|
|
248
|
+
}
|
|
249
|
+
if (isDev) console.error("Metadata load error:", err);
|
|
250
|
+
else console.error("Metadata load error:", (err as Error).message ?? err);
|
|
251
|
+
// Continue with null metadata — don't break the page for a metadata failure
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Pre-stream phase: run load() + module imports in parallel before committing to a 200 ──
|
|
255
|
+
// This ensures HttpError/Redirect from load() can return a proper response before any bytes are sent.
|
|
256
|
+
const metadataData = metadata?.data ?? null;
|
|
257
|
+
let data: Awaited<ReturnType<typeof loadRouteData>>;
|
|
258
|
+
let pageMod: any;
|
|
259
|
+
let layoutMods: any[];
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
[data, pageMod, layoutMods] = await Promise.all([
|
|
263
|
+
loadRouteData(url, locals, req, cookies, metadataData),
|
|
264
|
+
route.pageModule(),
|
|
265
|
+
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
266
|
+
]);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (err instanceof Redirect) return Response.redirect(err.location, err.status);
|
|
269
|
+
if (err instanceof HttpError) return renderErrorPage(err.status, err.message, url, req);
|
|
270
|
+
if (isDev) console.error("SSR load error:", err);
|
|
271
|
+
else console.error("SSR load error:", (err as Error).message ?? err);
|
|
272
|
+
return renderErrorPage(500, "Internal Server Error", url, req);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!data) return renderErrorPage(404, "Not Found", url, req);
|
|
276
|
+
|
|
277
|
+
const enc = new TextEncoder();
|
|
278
|
+
|
|
279
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
280
|
+
async start(controller) {
|
|
281
|
+
// Chunk 1: head opening (CSS, modulepreload — cached per lang)
|
|
282
|
+
controller.enqueue(enc.encode(buildHtmlShellOpen(metadata?.lang)));
|
|
283
|
+
|
|
284
|
+
// Chunk 2: metadata tags, close </head>, open <body> + spinner
|
|
285
|
+
controller.enqueue(enc.encode(buildMetadataChunk(metadata)));
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
if (!data!.ssr) {
|
|
289
|
+
// ssr=false → skip render(); ship empty shell + hydration scripts.
|
|
290
|
+
// ssr=false && csr=false is meaningless (nothing renders) — force csr=true.
|
|
291
|
+
if (!data!.csr && isDev) {
|
|
292
|
+
console.warn(
|
|
293
|
+
`⚠️ ${url.pathname}: ssr=false && csr=false renders nothing — forcing csr=true`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
controller.enqueue(
|
|
297
|
+
enc.encode(
|
|
298
|
+
buildHtmlTail(
|
|
299
|
+
"",
|
|
300
|
+
"",
|
|
301
|
+
data!.pageData,
|
|
302
|
+
data!.layoutData,
|
|
303
|
+
true,
|
|
304
|
+
null,
|
|
305
|
+
false,
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
controller.close();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { body, head } = render(App, {
|
|
314
|
+
props: {
|
|
315
|
+
ssrMode: true,
|
|
316
|
+
ssrPageComponent: pageMod.default,
|
|
317
|
+
ssrLayoutComponents: layoutMods.map((m: any) => m.default),
|
|
318
|
+
ssrPageData: data!.pageData,
|
|
319
|
+
ssrLayoutData: data!.layoutData,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Chunk 3: rendered content
|
|
324
|
+
controller.enqueue(
|
|
325
|
+
enc.encode(
|
|
326
|
+
buildHtmlTail(body, head, data!.pageData, data!.layoutData, data!.csr),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
controller.close();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
// Only render() can throw here — data is already loaded successfully
|
|
332
|
+
if (isDev) console.error("SSR render error:", err);
|
|
333
|
+
else console.error("SSR render error:", (err as Error).message ?? err);
|
|
334
|
+
controller.enqueue(enc.encode(`<p>Internal Server Error</p></body></html>`));
|
|
335
|
+
controller.close();
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return new Response(stream, {
|
|
341
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
342
|
+
});
|
|
289
343
|
}
|
|
290
344
|
|
|
291
345
|
// ─── Form Action Page Renderer ───────────────────────────
|
|
@@ -293,68 +347,87 @@ export async function renderSSRStream(
|
|
|
293
347
|
// Uses non-streaming buildHtml so we can control the status code.
|
|
294
348
|
|
|
295
349
|
export async function renderPageWithFormData(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
350
|
+
url: URL,
|
|
351
|
+
locals: Record<string, any>,
|
|
352
|
+
req: Request,
|
|
353
|
+
cookies: Cookies,
|
|
354
|
+
formData: any,
|
|
355
|
+
status: number,
|
|
302
356
|
): Promise<Response> {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
357
|
+
const match = findMatch(serverRoutes, url.pathname);
|
|
358
|
+
if (!match) return renderErrorPage(404, "Not Found", url, req);
|
|
359
|
+
|
|
360
|
+
const { route } = match;
|
|
361
|
+
|
|
362
|
+
// Load components + data in parallel
|
|
363
|
+
const [data, pageMod, layoutMods] = await Promise.all([
|
|
364
|
+
loadRouteData(url, locals, req, cookies),
|
|
365
|
+
route.pageModule(),
|
|
366
|
+
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
if (!data) return renderErrorPage(404, "Not Found", url, req);
|
|
370
|
+
|
|
371
|
+
if (!data.ssr) {
|
|
372
|
+
if (!data.csr && isDev) {
|
|
373
|
+
console.warn(
|
|
374
|
+
`⚠️ ${url.pathname}: ssr=false && csr=false renders nothing — forcing csr=true`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const html = buildHtml(
|
|
378
|
+
"",
|
|
379
|
+
"",
|
|
380
|
+
data.pageData,
|
|
381
|
+
data.layoutData,
|
|
382
|
+
true,
|
|
383
|
+
formData,
|
|
384
|
+
undefined,
|
|
385
|
+
false,
|
|
386
|
+
);
|
|
387
|
+
return compress(html, "text/html; charset=utf-8", req, status);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { body, head } = render(App, {
|
|
391
|
+
props: {
|
|
392
|
+
ssrMode: true,
|
|
393
|
+
ssrPageComponent: pageMod.default,
|
|
394
|
+
ssrLayoutComponents: layoutMods.map((m: any) => m.default),
|
|
395
|
+
ssrPageData: data.pageData,
|
|
396
|
+
ssrLayoutData: data.layoutData,
|
|
397
|
+
ssrFormData: formData,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const html = buildHtml(body, head, data.pageData, data.layoutData, data.csr, formData);
|
|
402
|
+
return compress(html, "text/html; charset=utf-8", req, status);
|
|
338
403
|
}
|
|
339
404
|
|
|
340
405
|
// ─── Error Page Renderer ──────────────────────────────────
|
|
341
406
|
|
|
342
|
-
export async function renderErrorPage(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
407
|
+
export async function renderErrorPage(
|
|
408
|
+
status: number,
|
|
409
|
+
message: string,
|
|
410
|
+
url: URL,
|
|
411
|
+
req: Request,
|
|
412
|
+
): Promise<Response> {
|
|
413
|
+
if (errorPage) {
|
|
414
|
+
try {
|
|
415
|
+
const mod = await errorPage();
|
|
416
|
+
// Render the error component directly — NOT through App.svelte.
|
|
417
|
+
// App.svelte always remaps ssrPageData to a `data` prop, but +error.svelte
|
|
418
|
+
// expects `error` as a direct prop: `let { error } = $props()`.
|
|
419
|
+
const { body, head } = render(mod.default, {
|
|
420
|
+
props: { error: { status, message } },
|
|
421
|
+
});
|
|
422
|
+
const html = buildHtml(body, head, { status, message }, [], false);
|
|
423
|
+
return compress(html, "text/html; charset=utf-8", req, status);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (isDev) console.error("Error page render failed:", err);
|
|
426
|
+
else console.error("Error page render failed:", (err as Error).message ?? err);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return new Response(message, {
|
|
430
|
+
status,
|
|
431
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
432
|
+
});
|
|
360
433
|
}
|