@vertz/ui-server 0.2.28 → 0.2.30
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/dist/bun-dev-server.d.ts +24 -1
- package/dist/bun-dev-server.js +99 -68
- package/dist/bun-plugin/index.d.ts +2 -2
- package/dist/index.d.ts +21 -0
- package/dist/index.js +17 -476
- package/dist/shared/chunk-bd0sgykf.js +1468 -0
- package/dist/ssr/index.d.ts +75 -2
- package/dist/ssr/index.js +1 -1
- package/package.json +5 -5
- package/dist/shared/chunk-yr65qdge.js +0 -764
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearGlobalSSRTimeout,
|
|
3
|
+
createSSRAdapter,
|
|
4
|
+
getSSRQueries,
|
|
5
|
+
installDomShim,
|
|
6
|
+
setGlobalSSRTimeout,
|
|
7
|
+
ssrStorage,
|
|
8
|
+
toVNode
|
|
9
|
+
} from "./chunk-gcwqkynf.js";
|
|
10
|
+
|
|
11
|
+
// src/html-serializer.ts
|
|
12
|
+
var VOID_ELEMENTS = new Set([
|
|
13
|
+
"area",
|
|
14
|
+
"base",
|
|
15
|
+
"br",
|
|
16
|
+
"col",
|
|
17
|
+
"embed",
|
|
18
|
+
"hr",
|
|
19
|
+
"img",
|
|
20
|
+
"input",
|
|
21
|
+
"link",
|
|
22
|
+
"meta",
|
|
23
|
+
"param",
|
|
24
|
+
"source",
|
|
25
|
+
"track",
|
|
26
|
+
"wbr"
|
|
27
|
+
]);
|
|
28
|
+
var RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
|
|
29
|
+
function escapeHtml(text) {
|
|
30
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
31
|
+
}
|
|
32
|
+
function escapeAttr(value) {
|
|
33
|
+
const str = typeof value === "string" ? value : String(value);
|
|
34
|
+
return str.replace(/&/g, "&").replace(/"/g, """);
|
|
35
|
+
}
|
|
36
|
+
function serializeAttrs(attrs) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
39
|
+
parts.push(` ${key}="${escapeAttr(value)}"`);
|
|
40
|
+
}
|
|
41
|
+
return parts.join("");
|
|
42
|
+
}
|
|
43
|
+
function isRawHtml(value) {
|
|
44
|
+
return typeof value === "object" && "__raw" in value && value.__raw === true;
|
|
45
|
+
}
|
|
46
|
+
function serializeToHtml(node) {
|
|
47
|
+
if (typeof node === "string") {
|
|
48
|
+
return escapeHtml(node);
|
|
49
|
+
}
|
|
50
|
+
if (isRawHtml(node)) {
|
|
51
|
+
return node.html;
|
|
52
|
+
}
|
|
53
|
+
const { tag, attrs, children } = node;
|
|
54
|
+
if (tag === "fragment") {
|
|
55
|
+
return children.map((child) => serializeToHtml(child)).join("");
|
|
56
|
+
}
|
|
57
|
+
const attrStr = serializeAttrs(attrs);
|
|
58
|
+
if (VOID_ELEMENTS.has(tag)) {
|
|
59
|
+
return `<${tag}${attrStr}>`;
|
|
60
|
+
}
|
|
61
|
+
const isRawText = RAW_TEXT_ELEMENTS.has(tag);
|
|
62
|
+
const childrenHtml = children.map((child) => {
|
|
63
|
+
if (typeof child === "string" && isRawText) {
|
|
64
|
+
return child;
|
|
65
|
+
}
|
|
66
|
+
return serializeToHtml(child);
|
|
67
|
+
}).join("");
|
|
68
|
+
return `<${tag}${attrStr}>${childrenHtml}</${tag}>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/slot-placeholder.ts
|
|
72
|
+
var slotCounter = 0;
|
|
73
|
+
function resetSlotCounter() {
|
|
74
|
+
slotCounter = 0;
|
|
75
|
+
}
|
|
76
|
+
function createSlotPlaceholder(fallback) {
|
|
77
|
+
const id = slotCounter++;
|
|
78
|
+
const placeholder = {
|
|
79
|
+
tag: "div",
|
|
80
|
+
attrs: { id: `v-slot-${id}` },
|
|
81
|
+
children: typeof fallback === "string" ? [fallback] : [fallback],
|
|
82
|
+
_slotId: id
|
|
83
|
+
};
|
|
84
|
+
return placeholder;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/streaming.ts
|
|
88
|
+
var encoder = new TextEncoder;
|
|
89
|
+
var decoder = new TextDecoder;
|
|
90
|
+
function encodeChunk(html) {
|
|
91
|
+
return encoder.encode(html);
|
|
92
|
+
}
|
|
93
|
+
async function streamToString(stream) {
|
|
94
|
+
const reader = stream.getReader();
|
|
95
|
+
const parts = [];
|
|
96
|
+
for (;; ) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done)
|
|
99
|
+
break;
|
|
100
|
+
parts.push(decoder.decode(value, { stream: true }));
|
|
101
|
+
}
|
|
102
|
+
parts.push(decoder.decode());
|
|
103
|
+
return parts.join("");
|
|
104
|
+
}
|
|
105
|
+
async function collectStreamChunks(stream) {
|
|
106
|
+
const reader = stream.getReader();
|
|
107
|
+
const chunks = [];
|
|
108
|
+
for (;; ) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done)
|
|
111
|
+
break;
|
|
112
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
113
|
+
}
|
|
114
|
+
return chunks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/template-chunk.ts
|
|
118
|
+
function createTemplateChunk(slotId, resolvedHtml, nonce) {
|
|
119
|
+
const tmplId = `v-tmpl-${slotId}`;
|
|
120
|
+
const slotRef = `v-slot-${slotId}`;
|
|
121
|
+
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
122
|
+
return `<template id="${tmplId}">${resolvedHtml}</template>` + `<script${nonceAttr}>` + `(function(){` + `var s=document.getElementById("${slotRef}");` + `var t=document.getElementById("${tmplId}");` + `if(s&&t){s.replaceWith(t.content.cloneNode(true));t.remove()}` + `})()` + "</script>";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/render-to-stream.ts
|
|
126
|
+
function isSuspenseNode(node) {
|
|
127
|
+
return typeof node === "object" && "tag" in node && node.tag === "__suspense" && "_resolve" in node;
|
|
128
|
+
}
|
|
129
|
+
function renderToStream(tree, options) {
|
|
130
|
+
const pendingBoundaries = [];
|
|
131
|
+
function walkAndSerialize(node) {
|
|
132
|
+
if (typeof node === "string") {
|
|
133
|
+
return escapeHtml(node);
|
|
134
|
+
}
|
|
135
|
+
if (isRawHtml(node)) {
|
|
136
|
+
return node.html;
|
|
137
|
+
}
|
|
138
|
+
if (isSuspenseNode(node)) {
|
|
139
|
+
const placeholder = createSlotPlaceholder(node._fallback);
|
|
140
|
+
pendingBoundaries.push({
|
|
141
|
+
slotId: placeholder._slotId,
|
|
142
|
+
resolve: node._resolve
|
|
143
|
+
});
|
|
144
|
+
return serializeToHtml(placeholder);
|
|
145
|
+
}
|
|
146
|
+
const { tag, attrs, children } = node;
|
|
147
|
+
if (tag === "fragment") {
|
|
148
|
+
return children.map((child) => walkAndSerialize(child)).join("");
|
|
149
|
+
}
|
|
150
|
+
const isRawText = RAW_TEXT_ELEMENTS.has(tag);
|
|
151
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
|
|
152
|
+
if (VOID_ELEMENTS.has(tag)) {
|
|
153
|
+
return `<${tag}${attrStr}>`;
|
|
154
|
+
}
|
|
155
|
+
const childrenHtml = children.map((child) => {
|
|
156
|
+
if (typeof child === "string" && isRawText) {
|
|
157
|
+
return child;
|
|
158
|
+
}
|
|
159
|
+
return walkAndSerialize(child);
|
|
160
|
+
}).join("");
|
|
161
|
+
return `<${tag}${attrStr}>${childrenHtml}</${tag}>`;
|
|
162
|
+
}
|
|
163
|
+
return new ReadableStream({
|
|
164
|
+
async start(controller) {
|
|
165
|
+
const mainHtml = walkAndSerialize(tree);
|
|
166
|
+
controller.enqueue(encodeChunk(mainHtml));
|
|
167
|
+
if (pendingBoundaries.length > 0) {
|
|
168
|
+
const nonce = options?.nonce;
|
|
169
|
+
const resolutions = pendingBoundaries.map(async (boundary) => {
|
|
170
|
+
try {
|
|
171
|
+
const resolved = await boundary.resolve;
|
|
172
|
+
const resolvedHtml = serializeToHtml(resolved);
|
|
173
|
+
return createTemplateChunk(boundary.slotId, resolvedHtml, nonce);
|
|
174
|
+
} catch (_err) {
|
|
175
|
+
const errorHtml = `<div data-v-ssr-error="true" id="v-ssr-error-${boundary.slotId}">` + "<!--SSR error--></div>";
|
|
176
|
+
return createTemplateChunk(boundary.slotId, errorHtml, nonce);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const chunks = await Promise.all(resolutions);
|
|
180
|
+
for (const chunk of chunks) {
|
|
181
|
+
controller.enqueue(encodeChunk(chunk));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
controller.close();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/ssr-streaming-runtime.ts
|
|
190
|
+
function safeSerialize(data) {
|
|
191
|
+
return JSON.stringify(data).replace(/</g, "\\u003c");
|
|
192
|
+
}
|
|
193
|
+
function getStreamingRuntimeScript(nonce) {
|
|
194
|
+
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
195
|
+
return `<script${nonceAttr}>` + "window.__VERTZ_SSR_DATA__=[];" + "window.__VERTZ_SSR_PUSH__=function(k,d){" + "window.__VERTZ_SSR_DATA__.push({key:k,data:d});" + 'document.dispatchEvent(new CustomEvent("vertz:ssr-data",{detail:{key:k,data:d}}))' + "};" + "</script>";
|
|
196
|
+
}
|
|
197
|
+
function createSSRDataChunk(key, data, nonce) {
|
|
198
|
+
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
199
|
+
const serialized = safeSerialize(data);
|
|
200
|
+
return `<script${nonceAttr}>window.__VERTZ_SSR_PUSH__(${safeSerialize(key)},${serialized})</script>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/ssr-render.ts
|
|
204
|
+
import { compileTheme } from "@vertz/ui";
|
|
205
|
+
import { EntityStore, MemoryCache, QueryEnvelopeStore } from "@vertz/ui/internals";
|
|
206
|
+
var compiledThemeCache = new WeakMap;
|
|
207
|
+
function compileThemeCached(theme, fallbackMetrics) {
|
|
208
|
+
const cached = compiledThemeCache.get(theme);
|
|
209
|
+
if (cached)
|
|
210
|
+
return cached;
|
|
211
|
+
const compiled = compileTheme(theme, { fallbackMetrics });
|
|
212
|
+
compiledThemeCache.set(theme, compiled);
|
|
213
|
+
return compiled;
|
|
214
|
+
}
|
|
215
|
+
function createRequestContext(url) {
|
|
216
|
+
return {
|
|
217
|
+
url,
|
|
218
|
+
adapter: createSSRAdapter(),
|
|
219
|
+
subscriber: null,
|
|
220
|
+
readValueCb: null,
|
|
221
|
+
cleanupStack: [],
|
|
222
|
+
batchDepth: 0,
|
|
223
|
+
pendingEffects: new Map,
|
|
224
|
+
contextScope: null,
|
|
225
|
+
entityStore: new EntityStore,
|
|
226
|
+
envelopeStore: new QueryEnvelopeStore,
|
|
227
|
+
queryCache: new MemoryCache({ maxSize: Infinity }),
|
|
228
|
+
inflight: new Map,
|
|
229
|
+
queries: [],
|
|
230
|
+
errors: []
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
var domShimInstalled = false;
|
|
234
|
+
function ensureDomShim() {
|
|
235
|
+
if (domShimInstalled && typeof document !== "undefined")
|
|
236
|
+
return;
|
|
237
|
+
domShimInstalled = true;
|
|
238
|
+
installDomShim();
|
|
239
|
+
}
|
|
240
|
+
function resolveAppFactory(module) {
|
|
241
|
+
const createApp = module.default || module.App;
|
|
242
|
+
if (typeof createApp !== "function") {
|
|
243
|
+
throw new Error("App entry must export a default function or named App function");
|
|
244
|
+
}
|
|
245
|
+
return createApp;
|
|
246
|
+
}
|
|
247
|
+
function collectCSS(themeCss, module) {
|
|
248
|
+
const alreadyIncluded = new Set;
|
|
249
|
+
if (themeCss)
|
|
250
|
+
alreadyIncluded.add(themeCss);
|
|
251
|
+
if (module.styles) {
|
|
252
|
+
for (const s of module.styles)
|
|
253
|
+
alreadyIncluded.add(s);
|
|
254
|
+
}
|
|
255
|
+
const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
|
|
256
|
+
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
257
|
+
const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
|
|
258
|
+
`)}</style>` : "";
|
|
259
|
+
const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
|
|
260
|
+
`)}</style>` : "";
|
|
261
|
+
return [themeTag, globalTag, componentTag].filter(Boolean).join(`
|
|
262
|
+
`);
|
|
263
|
+
}
|
|
264
|
+
async function ssrRenderToString(module, url, options) {
|
|
265
|
+
const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
|
|
266
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
267
|
+
ensureDomShim();
|
|
268
|
+
const ctx = createRequestContext(normalizedUrl);
|
|
269
|
+
if (options?.ssrAuth) {
|
|
270
|
+
ctx.ssrAuth = options.ssrAuth;
|
|
271
|
+
}
|
|
272
|
+
return ssrStorage.run(ctx, async () => {
|
|
273
|
+
try {
|
|
274
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
275
|
+
const createApp = resolveAppFactory(module);
|
|
276
|
+
let themeCss = "";
|
|
277
|
+
let themePreloadTags = "";
|
|
278
|
+
if (module.theme) {
|
|
279
|
+
try {
|
|
280
|
+
const compiled = compileThemeCached(module.theme, options?.fallbackMetrics);
|
|
281
|
+
themeCss = compiled.css;
|
|
282
|
+
themePreloadTags = compiled.preloadTags;
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
createApp();
|
|
288
|
+
if (ctx.ssrRedirect) {
|
|
289
|
+
return {
|
|
290
|
+
html: "",
|
|
291
|
+
css: "",
|
|
292
|
+
ssrData: [],
|
|
293
|
+
headTags: "",
|
|
294
|
+
redirect: ctx.ssrRedirect
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const store = ssrStorage.getStore();
|
|
298
|
+
if (store) {
|
|
299
|
+
if (store.pendingRouteComponents?.size) {
|
|
300
|
+
const entries = Array.from(store.pendingRouteComponents.entries());
|
|
301
|
+
const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
|
|
302
|
+
promise.then((mod) => ({ route, factory: mod.default })),
|
|
303
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
|
|
304
|
+
])));
|
|
305
|
+
store.resolvedComponents = new Map;
|
|
306
|
+
for (const result of results) {
|
|
307
|
+
if (result.status === "fulfilled") {
|
|
308
|
+
const { route, factory } = result.value;
|
|
309
|
+
store.resolvedComponents.set(route, factory);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
store.pendingRouteComponents = undefined;
|
|
313
|
+
}
|
|
314
|
+
if (!store.resolvedComponents) {
|
|
315
|
+
store.resolvedComponents = new Map;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const queries = getSSRQueries();
|
|
319
|
+
const resolvedQueries = [];
|
|
320
|
+
if (queries.length > 0) {
|
|
321
|
+
await Promise.allSettled(queries.map(({ promise, timeout, resolve, key }) => Promise.race([
|
|
322
|
+
promise.then((data) => {
|
|
323
|
+
resolve(data);
|
|
324
|
+
resolvedQueries.push({ key, data });
|
|
325
|
+
return "resolved";
|
|
326
|
+
}),
|
|
327
|
+
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
|
|
328
|
+
])));
|
|
329
|
+
if (store)
|
|
330
|
+
store.queries = [];
|
|
331
|
+
}
|
|
332
|
+
const app = createApp();
|
|
333
|
+
const vnode = toVNode(app);
|
|
334
|
+
const stream = renderToStream(vnode);
|
|
335
|
+
const html = await streamToString(stream);
|
|
336
|
+
const css = collectCSS(themeCss, module);
|
|
337
|
+
const ssrData = resolvedQueries.length > 0 ? resolvedQueries.map(({ key, data }) => ({ key, data })) : [];
|
|
338
|
+
return {
|
|
339
|
+
html,
|
|
340
|
+
css,
|
|
341
|
+
ssrData,
|
|
342
|
+
headTags: themePreloadTags,
|
|
343
|
+
discoveredRoutes: ctx.discoveredRoutes,
|
|
344
|
+
matchedRoutePatterns: ctx.matchedRoutePatterns
|
|
345
|
+
};
|
|
346
|
+
} finally {
|
|
347
|
+
clearGlobalSSRTimeout();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async function ssrDiscoverQueries(module, url, options) {
|
|
352
|
+
const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
|
|
353
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
354
|
+
ensureDomShim();
|
|
355
|
+
const ctx = createRequestContext(normalizedUrl);
|
|
356
|
+
return ssrStorage.run(ctx, async () => {
|
|
357
|
+
try {
|
|
358
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
359
|
+
const createApp = resolveAppFactory(module);
|
|
360
|
+
createApp();
|
|
361
|
+
const queries = getSSRQueries();
|
|
362
|
+
const resolvedQueries = [];
|
|
363
|
+
const pendingKeys = [];
|
|
364
|
+
if (queries.length > 0) {
|
|
365
|
+
await Promise.allSettled(queries.map(({ promise, timeout, resolve, key }) => {
|
|
366
|
+
let settled = false;
|
|
367
|
+
return Promise.race([
|
|
368
|
+
promise.then((data) => {
|
|
369
|
+
if (settled)
|
|
370
|
+
return "late";
|
|
371
|
+
settled = true;
|
|
372
|
+
resolve(data);
|
|
373
|
+
resolvedQueries.push({ key, data });
|
|
374
|
+
return "resolved";
|
|
375
|
+
}),
|
|
376
|
+
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => {
|
|
377
|
+
if (settled)
|
|
378
|
+
return "already-resolved";
|
|
379
|
+
settled = true;
|
|
380
|
+
pendingKeys.push(key);
|
|
381
|
+
return "timeout";
|
|
382
|
+
})
|
|
383
|
+
]);
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
resolved: resolvedQueries.map(({ key, data }) => ({
|
|
388
|
+
key,
|
|
389
|
+
data: JSON.parse(JSON.stringify(data))
|
|
390
|
+
})),
|
|
391
|
+
pending: pendingKeys
|
|
392
|
+
};
|
|
393
|
+
} finally {
|
|
394
|
+
clearGlobalSSRTimeout();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async function ssrStreamNavQueries(module, url, options) {
|
|
399
|
+
const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
|
|
400
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
401
|
+
const navTimeout = options?.navSsrTimeout ?? 5000;
|
|
402
|
+
ensureDomShim();
|
|
403
|
+
const ctx = createRequestContext(normalizedUrl);
|
|
404
|
+
const queries = await ssrStorage.run(ctx, async () => {
|
|
405
|
+
try {
|
|
406
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
407
|
+
const createApp = resolveAppFactory(module);
|
|
408
|
+
createApp();
|
|
409
|
+
const discovered = getSSRQueries();
|
|
410
|
+
return discovered.map((q) => ({
|
|
411
|
+
promise: q.promise,
|
|
412
|
+
timeout: q.timeout || ssrTimeout,
|
|
413
|
+
resolve: q.resolve,
|
|
414
|
+
key: q.key
|
|
415
|
+
}));
|
|
416
|
+
} finally {
|
|
417
|
+
clearGlobalSSRTimeout();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
if (queries.length === 0) {
|
|
421
|
+
const encoder3 = new TextEncoder;
|
|
422
|
+
return new ReadableStream({
|
|
423
|
+
start(controller) {
|
|
424
|
+
controller.enqueue(encoder3.encode(`event: done
|
|
425
|
+
data: {}
|
|
426
|
+
|
|
427
|
+
`));
|
|
428
|
+
controller.close();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const encoder2 = new TextEncoder;
|
|
433
|
+
let remaining = queries.length;
|
|
434
|
+
return new ReadableStream({
|
|
435
|
+
start(controller) {
|
|
436
|
+
let closed = false;
|
|
437
|
+
function safeEnqueue(chunk) {
|
|
438
|
+
if (closed)
|
|
439
|
+
return;
|
|
440
|
+
try {
|
|
441
|
+
controller.enqueue(chunk);
|
|
442
|
+
} catch {
|
|
443
|
+
closed = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function safeClose() {
|
|
447
|
+
if (closed)
|
|
448
|
+
return;
|
|
449
|
+
closed = true;
|
|
450
|
+
try {
|
|
451
|
+
controller.close();
|
|
452
|
+
} catch {}
|
|
453
|
+
}
|
|
454
|
+
function checkDone() {
|
|
455
|
+
if (remaining === 0) {
|
|
456
|
+
safeEnqueue(encoder2.encode(`event: done
|
|
457
|
+
data: {}
|
|
458
|
+
|
|
459
|
+
`));
|
|
460
|
+
safeClose();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const { promise, resolve, key } of queries) {
|
|
464
|
+
let settled = false;
|
|
465
|
+
promise.then((data) => {
|
|
466
|
+
if (settled)
|
|
467
|
+
return;
|
|
468
|
+
settled = true;
|
|
469
|
+
resolve(data);
|
|
470
|
+
const entry = { key, data };
|
|
471
|
+
safeEnqueue(encoder2.encode(`event: data
|
|
472
|
+
data: ${safeSerialize(entry)}
|
|
473
|
+
|
|
474
|
+
`));
|
|
475
|
+
remaining--;
|
|
476
|
+
checkDone();
|
|
477
|
+
}, () => {
|
|
478
|
+
if (settled)
|
|
479
|
+
return;
|
|
480
|
+
settled = true;
|
|
481
|
+
remaining--;
|
|
482
|
+
checkDone();
|
|
483
|
+
});
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
if (settled)
|
|
486
|
+
return;
|
|
487
|
+
settled = true;
|
|
488
|
+
remaining--;
|
|
489
|
+
checkDone();
|
|
490
|
+
}, navTimeout);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/ssr-access-evaluator.ts
|
|
497
|
+
function toPrefetchSession(ssrAuth, accessSet) {
|
|
498
|
+
if (!ssrAuth || ssrAuth.status !== "authenticated" || !ssrAuth.user) {
|
|
499
|
+
return { status: "unauthenticated" };
|
|
500
|
+
}
|
|
501
|
+
const roles = ssrAuth.user.role ? [ssrAuth.user.role] : undefined;
|
|
502
|
+
const entitlements = accessSet != null ? Object.fromEntries(Object.entries(accessSet.entitlements).map(([name, check]) => [name, check.allowed])) : undefined;
|
|
503
|
+
return {
|
|
504
|
+
status: "authenticated",
|
|
505
|
+
roles,
|
|
506
|
+
entitlements,
|
|
507
|
+
tenantId: ssrAuth.user.tenantId
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function evaluateAccessRule(rule, session) {
|
|
511
|
+
switch (rule.type) {
|
|
512
|
+
case "public":
|
|
513
|
+
return true;
|
|
514
|
+
case "authenticated":
|
|
515
|
+
return session.status === "authenticated";
|
|
516
|
+
case "role":
|
|
517
|
+
if (session.status !== "authenticated")
|
|
518
|
+
return false;
|
|
519
|
+
return session.roles?.some((r) => rule.roles.includes(r)) === true;
|
|
520
|
+
case "entitlement":
|
|
521
|
+
if (session.status !== "authenticated")
|
|
522
|
+
return false;
|
|
523
|
+
return session.entitlements?.[rule.value] === true;
|
|
524
|
+
case "where":
|
|
525
|
+
return true;
|
|
526
|
+
case "fva":
|
|
527
|
+
return session.status === "authenticated";
|
|
528
|
+
case "deny":
|
|
529
|
+
return false;
|
|
530
|
+
case "all":
|
|
531
|
+
return rule.rules.every((r) => evaluateAccessRule(r, session));
|
|
532
|
+
case "any":
|
|
533
|
+
return rule.rules.some((r) => evaluateAccessRule(r, session));
|
|
534
|
+
default:
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/ssr-access-set.ts
|
|
540
|
+
function getAccessSetForSSR(jwtPayload) {
|
|
541
|
+
if (!jwtPayload)
|
|
542
|
+
return null;
|
|
543
|
+
const acl = jwtPayload.acl;
|
|
544
|
+
if (!acl)
|
|
545
|
+
return null;
|
|
546
|
+
if (acl.overflow)
|
|
547
|
+
return null;
|
|
548
|
+
if (!acl.set)
|
|
549
|
+
return null;
|
|
550
|
+
return {
|
|
551
|
+
entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => [
|
|
552
|
+
name,
|
|
553
|
+
{
|
|
554
|
+
allowed: check.allowed,
|
|
555
|
+
reasons: check.reasons ?? [],
|
|
556
|
+
...check.reason ? { reason: check.reason } : {},
|
|
557
|
+
...check.meta ? { meta: check.meta } : {}
|
|
558
|
+
}
|
|
559
|
+
])),
|
|
560
|
+
flags: acl.set.flags,
|
|
561
|
+
plan: acl.set.plan,
|
|
562
|
+
plans: acl.set.plans ?? {},
|
|
563
|
+
computedAt: acl.set.computedAt
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function createAccessSetScript(accessSet, nonce) {
|
|
567
|
+
const json = JSON.stringify(accessSet);
|
|
568
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
569
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr2(nonce)}"` : "";
|
|
570
|
+
return `<script${nonceAttr}>window.__VERTZ_ACCESS_SET__=${escaped}</script>`;
|
|
571
|
+
}
|
|
572
|
+
function escapeAttr2(s) {
|
|
573
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/ssr-route-matcher.ts
|
|
577
|
+
function matchUrlToPatterns(url, patterns) {
|
|
578
|
+
const path = (url.split("?")[0] ?? "").split("#")[0] ?? "";
|
|
579
|
+
const matches = [];
|
|
580
|
+
for (const pattern of patterns) {
|
|
581
|
+
const result = matchPattern(path, pattern);
|
|
582
|
+
if (result) {
|
|
583
|
+
matches.push(result);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
matches.sort((a, b) => {
|
|
587
|
+
const aSegments = a.pattern.split("/").length;
|
|
588
|
+
const bSegments = b.pattern.split("/").length;
|
|
589
|
+
return aSegments - bSegments;
|
|
590
|
+
});
|
|
591
|
+
return matches;
|
|
592
|
+
}
|
|
593
|
+
function matchPattern(path, pattern) {
|
|
594
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
595
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
596
|
+
if (patternSegments.length > pathSegments.length)
|
|
597
|
+
return;
|
|
598
|
+
const params = {};
|
|
599
|
+
for (let i = 0;i < patternSegments.length; i++) {
|
|
600
|
+
const seg = patternSegments[i];
|
|
601
|
+
const val = pathSegments[i];
|
|
602
|
+
if (seg.startsWith(":")) {
|
|
603
|
+
params[seg.slice(1)] = val;
|
|
604
|
+
} else if (seg !== val) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return { pattern, params };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/ssr-manifest-prefetch.ts
|
|
612
|
+
function reconstructDescriptors(queries, routeParams, apiClient) {
|
|
613
|
+
if (!apiClient)
|
|
614
|
+
return [];
|
|
615
|
+
const result = [];
|
|
616
|
+
for (const query of queries) {
|
|
617
|
+
const descriptor = reconstructSingle(query, routeParams, apiClient);
|
|
618
|
+
if (descriptor) {
|
|
619
|
+
result.push(descriptor);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
function reconstructSingle(query, routeParams, apiClient) {
|
|
625
|
+
const { entity, operation } = query;
|
|
626
|
+
if (!entity || !operation)
|
|
627
|
+
return;
|
|
628
|
+
const entitySdk = apiClient[entity];
|
|
629
|
+
if (!entitySdk)
|
|
630
|
+
return;
|
|
631
|
+
const method = entitySdk[operation];
|
|
632
|
+
if (typeof method !== "function")
|
|
633
|
+
return;
|
|
634
|
+
const args = buildFactoryArgs(query, routeParams);
|
|
635
|
+
if (args === undefined)
|
|
636
|
+
return;
|
|
637
|
+
try {
|
|
638
|
+
const descriptor = method(...args);
|
|
639
|
+
if (!descriptor || typeof descriptor._key !== "string" || typeof descriptor._fetch !== "function") {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
return { key: descriptor._key, fetch: descriptor._fetch };
|
|
643
|
+
} catch {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function buildFactoryArgs(query, routeParams) {
|
|
648
|
+
const { operation, idParam, queryBindings } = query;
|
|
649
|
+
if (operation === "get") {
|
|
650
|
+
if (idParam) {
|
|
651
|
+
const id = routeParams[idParam];
|
|
652
|
+
if (!id)
|
|
653
|
+
return;
|
|
654
|
+
const options = resolveQueryBindings(queryBindings, routeParams);
|
|
655
|
+
if (options === undefined && queryBindings)
|
|
656
|
+
return;
|
|
657
|
+
return options ? [id, options] : [id];
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!queryBindings)
|
|
662
|
+
return [];
|
|
663
|
+
const resolved = resolveQueryBindings(queryBindings, routeParams);
|
|
664
|
+
if (resolved === undefined)
|
|
665
|
+
return;
|
|
666
|
+
return [resolved];
|
|
667
|
+
}
|
|
668
|
+
function resolveQueryBindings(bindings, routeParams) {
|
|
669
|
+
if (!bindings)
|
|
670
|
+
return;
|
|
671
|
+
const resolved = {};
|
|
672
|
+
if (bindings.where) {
|
|
673
|
+
const where = {};
|
|
674
|
+
for (const [key, value] of Object.entries(bindings.where)) {
|
|
675
|
+
if (value === null)
|
|
676
|
+
return;
|
|
677
|
+
if (typeof value === "string" && value.startsWith("$")) {
|
|
678
|
+
const paramName = value.slice(1);
|
|
679
|
+
const paramValue = routeParams[paramName];
|
|
680
|
+
if (!paramValue)
|
|
681
|
+
return;
|
|
682
|
+
where[key] = paramValue;
|
|
683
|
+
} else {
|
|
684
|
+
where[key] = value;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
resolved.where = where;
|
|
688
|
+
}
|
|
689
|
+
if (bindings.select)
|
|
690
|
+
resolved.select = bindings.select;
|
|
691
|
+
if (bindings.include)
|
|
692
|
+
resolved.include = bindings.include;
|
|
693
|
+
if (bindings.orderBy)
|
|
694
|
+
resolved.orderBy = bindings.orderBy;
|
|
695
|
+
if (bindings.limit !== undefined)
|
|
696
|
+
resolved.limit = bindings.limit;
|
|
697
|
+
return resolved;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/ssr-single-pass.ts
|
|
701
|
+
async function ssrRenderSinglePass(module, url, options) {
|
|
702
|
+
if (options?.prefetch === false) {
|
|
703
|
+
return ssrRenderToString(module, url, options);
|
|
704
|
+
}
|
|
705
|
+
const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
|
|
706
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
707
|
+
ensureDomShim2();
|
|
708
|
+
const zeroDiscoveryData = attemptZeroDiscovery(normalizedUrl, module, options, ssrTimeout);
|
|
709
|
+
if (zeroDiscoveryData) {
|
|
710
|
+
return renderWithPrefetchedData(module, normalizedUrl, zeroDiscoveryData, options);
|
|
711
|
+
}
|
|
712
|
+
const discoveredData = await runDiscoveryPhase(normalizedUrl, ssrTimeout, module, options);
|
|
713
|
+
if ("redirect" in discoveredData) {
|
|
714
|
+
return {
|
|
715
|
+
html: "",
|
|
716
|
+
css: "",
|
|
717
|
+
ssrData: [],
|
|
718
|
+
headTags: "",
|
|
719
|
+
redirect: discoveredData.redirect
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const renderCtx = createRequestContext(normalizedUrl);
|
|
723
|
+
if (options?.ssrAuth) {
|
|
724
|
+
renderCtx.ssrAuth = options.ssrAuth;
|
|
725
|
+
}
|
|
726
|
+
for (const { key, data } of discoveredData.resolvedQueries) {
|
|
727
|
+
renderCtx.queryCache.set(key, data);
|
|
728
|
+
}
|
|
729
|
+
renderCtx.resolvedComponents = discoveredData.resolvedComponents ?? new Map;
|
|
730
|
+
return ssrStorage.run(renderCtx, async () => {
|
|
731
|
+
try {
|
|
732
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
733
|
+
const createApp = resolveAppFactory2(module);
|
|
734
|
+
let themeCss = "";
|
|
735
|
+
let themePreloadTags = "";
|
|
736
|
+
if (module.theme) {
|
|
737
|
+
try {
|
|
738
|
+
const compiled = compileThemeCached(module.theme, options?.fallbackMetrics);
|
|
739
|
+
themeCss = compiled.css;
|
|
740
|
+
themePreloadTags = compiled.preloadTags;
|
|
741
|
+
} catch (e) {
|
|
742
|
+
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const app = createApp();
|
|
746
|
+
const vnode = toVNode(app);
|
|
747
|
+
const stream = renderToStream(vnode);
|
|
748
|
+
const html = await streamToString(stream);
|
|
749
|
+
const css = collectCSS2(themeCss, module);
|
|
750
|
+
const ssrData = discoveredData.resolvedQueries.map(({ key, data }) => ({
|
|
751
|
+
key,
|
|
752
|
+
data
|
|
753
|
+
}));
|
|
754
|
+
return {
|
|
755
|
+
html,
|
|
756
|
+
css,
|
|
757
|
+
ssrData,
|
|
758
|
+
headTags: themePreloadTags,
|
|
759
|
+
discoveredRoutes: renderCtx.discoveredRoutes,
|
|
760
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns
|
|
761
|
+
};
|
|
762
|
+
} finally {
|
|
763
|
+
clearGlobalSSRTimeout();
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
async function ssrRenderProgressive(module, url, options) {
|
|
768
|
+
const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
|
|
769
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
770
|
+
ensureDomShim2();
|
|
771
|
+
const discoveryResult = await runDiscoveryPhase(normalizedUrl, ssrTimeout, module, options);
|
|
772
|
+
if ("redirect" in discoveryResult) {
|
|
773
|
+
return {
|
|
774
|
+
css: "",
|
|
775
|
+
ssrData: [],
|
|
776
|
+
headTags: "",
|
|
777
|
+
redirect: discoveryResult.redirect
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const renderCtx = createRequestContext(normalizedUrl);
|
|
781
|
+
if (options?.ssrAuth) {
|
|
782
|
+
renderCtx.ssrAuth = options.ssrAuth;
|
|
783
|
+
}
|
|
784
|
+
for (const { key, data } of discoveryResult.resolvedQueries) {
|
|
785
|
+
renderCtx.queryCache.set(key, data);
|
|
786
|
+
}
|
|
787
|
+
renderCtx.resolvedComponents = discoveryResult.resolvedComponents ?? new Map;
|
|
788
|
+
return ssrStorage.run(renderCtx, () => {
|
|
789
|
+
try {
|
|
790
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
791
|
+
const createApp = resolveAppFactory2(module);
|
|
792
|
+
let themeCss = "";
|
|
793
|
+
let themePreloadTags = "";
|
|
794
|
+
if (module.theme) {
|
|
795
|
+
try {
|
|
796
|
+
const compiled = compileThemeCached(module.theme, options?.fallbackMetrics);
|
|
797
|
+
themeCss = compiled.css;
|
|
798
|
+
themePreloadTags = compiled.preloadTags;
|
|
799
|
+
} catch (e) {
|
|
800
|
+
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const app = createApp();
|
|
804
|
+
const vnode = toVNode(app);
|
|
805
|
+
const renderStream = renderToStream(vnode);
|
|
806
|
+
const css = collectCSS2(themeCss, module);
|
|
807
|
+
const ssrData = discoveryResult.resolvedQueries.map(({ key, data }) => ({
|
|
808
|
+
key,
|
|
809
|
+
data
|
|
810
|
+
}));
|
|
811
|
+
return {
|
|
812
|
+
css,
|
|
813
|
+
ssrData,
|
|
814
|
+
headTags: themePreloadTags,
|
|
815
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns,
|
|
816
|
+
renderStream
|
|
817
|
+
};
|
|
818
|
+
} finally {
|
|
819
|
+
clearGlobalSSRTimeout();
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async function runDiscoveryPhase(normalizedUrl, ssrTimeout, module, options) {
|
|
824
|
+
const discoveryCtx = createRequestContext(normalizedUrl);
|
|
825
|
+
if (options?.ssrAuth) {
|
|
826
|
+
discoveryCtx.ssrAuth = options.ssrAuth;
|
|
827
|
+
}
|
|
828
|
+
return ssrStorage.run(discoveryCtx, async () => {
|
|
829
|
+
try {
|
|
830
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
831
|
+
const createApp = resolveAppFactory2(module);
|
|
832
|
+
createApp();
|
|
833
|
+
if (discoveryCtx.ssrRedirect) {
|
|
834
|
+
return { redirect: discoveryCtx.ssrRedirect };
|
|
835
|
+
}
|
|
836
|
+
if (discoveryCtx.pendingRouteComponents?.size) {
|
|
837
|
+
const entries = Array.from(discoveryCtx.pendingRouteComponents.entries());
|
|
838
|
+
const results = await Promise.allSettled(entries.map(([route, promise]) => Promise.race([
|
|
839
|
+
promise.then((mod) => ({ route, factory: mod.default })),
|
|
840
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("lazy route timeout")), ssrTimeout))
|
|
841
|
+
])));
|
|
842
|
+
discoveryCtx.resolvedComponents = new Map;
|
|
843
|
+
for (const result of results) {
|
|
844
|
+
if (result.status === "fulfilled") {
|
|
845
|
+
const { route, factory } = result.value;
|
|
846
|
+
discoveryCtx.resolvedComponents.set(route, factory);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
discoveryCtx.pendingRouteComponents = undefined;
|
|
850
|
+
}
|
|
851
|
+
const queries = getSSRQueries();
|
|
852
|
+
const eligibleQueries = filterByEntityAccess(queries, options?.manifest?.entityAccess, options?.prefetchSession);
|
|
853
|
+
const resolvedQueries = [];
|
|
854
|
+
if (eligibleQueries.length > 0) {
|
|
855
|
+
await Promise.allSettled(eligibleQueries.map(({ promise, timeout, resolve, key }) => Promise.race([
|
|
856
|
+
promise.then((data) => {
|
|
857
|
+
resolve(data);
|
|
858
|
+
resolvedQueries.push({ key, data });
|
|
859
|
+
return "resolved";
|
|
860
|
+
}),
|
|
861
|
+
new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
|
|
862
|
+
])));
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
resolvedQueries,
|
|
866
|
+
resolvedComponents: discoveryCtx.resolvedComponents
|
|
867
|
+
};
|
|
868
|
+
} finally {
|
|
869
|
+
clearGlobalSSRTimeout();
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
function attemptZeroDiscovery(url, module, options, ssrTimeout) {
|
|
874
|
+
const manifest = options?.manifest;
|
|
875
|
+
if (!manifest?.routeEntries || !module.api)
|
|
876
|
+
return null;
|
|
877
|
+
const matches = matchUrlToPatterns(url, manifest.routePatterns);
|
|
878
|
+
if (matches.length === 0)
|
|
879
|
+
return null;
|
|
880
|
+
const allQueries = [];
|
|
881
|
+
let mergedParams = {};
|
|
882
|
+
for (const match of matches) {
|
|
883
|
+
const entry = manifest.routeEntries[match.pattern];
|
|
884
|
+
if (entry) {
|
|
885
|
+
allQueries.push(...entry.queries);
|
|
886
|
+
}
|
|
887
|
+
mergedParams = { ...mergedParams, ...match.params };
|
|
888
|
+
}
|
|
889
|
+
if (allQueries.length === 0)
|
|
890
|
+
return null;
|
|
891
|
+
const descriptors = reconstructDescriptors(allQueries, mergedParams, module.api);
|
|
892
|
+
if (descriptors.length === 0)
|
|
893
|
+
return null;
|
|
894
|
+
return prefetchFromDescriptors(descriptors, ssrTimeout);
|
|
895
|
+
}
|
|
896
|
+
async function prefetchFromDescriptors(descriptors, ssrTimeout) {
|
|
897
|
+
const resolvedQueries = [];
|
|
898
|
+
await Promise.allSettled(descriptors.map(({ key, fetch: fetchFn }) => Promise.race([
|
|
899
|
+
fetchFn().then((result) => {
|
|
900
|
+
const data = unwrapResult(result);
|
|
901
|
+
resolvedQueries.push({ key, data });
|
|
902
|
+
return "resolved";
|
|
903
|
+
}),
|
|
904
|
+
new Promise((r) => setTimeout(r, ssrTimeout)).then(() => "timeout")
|
|
905
|
+
])));
|
|
906
|
+
return { resolvedQueries };
|
|
907
|
+
}
|
|
908
|
+
function unwrapResult(result) {
|
|
909
|
+
if (result && typeof result === "object" && "ok" in result && "data" in result) {
|
|
910
|
+
const r = result;
|
|
911
|
+
if (r.ok)
|
|
912
|
+
return r.data;
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
916
|
+
async function renderWithPrefetchedData(module, normalizedUrl, prefetchedData, options) {
|
|
917
|
+
const data = await prefetchedData;
|
|
918
|
+
const ssrTimeout = options?.ssrTimeout ?? 300;
|
|
919
|
+
const renderCtx = createRequestContext(normalizedUrl);
|
|
920
|
+
if (options?.ssrAuth) {
|
|
921
|
+
renderCtx.ssrAuth = options.ssrAuth;
|
|
922
|
+
}
|
|
923
|
+
for (const { key, data: queryData } of data.resolvedQueries) {
|
|
924
|
+
renderCtx.queryCache.set(key, queryData);
|
|
925
|
+
}
|
|
926
|
+
renderCtx.resolvedComponents = new Map;
|
|
927
|
+
return ssrStorage.run(renderCtx, async () => {
|
|
928
|
+
try {
|
|
929
|
+
setGlobalSSRTimeout(ssrTimeout);
|
|
930
|
+
const createApp = resolveAppFactory2(module);
|
|
931
|
+
let themeCss = "";
|
|
932
|
+
let themePreloadTags = "";
|
|
933
|
+
if (module.theme) {
|
|
934
|
+
try {
|
|
935
|
+
const compiled = compileThemeCached(module.theme, options?.fallbackMetrics);
|
|
936
|
+
themeCss = compiled.css;
|
|
937
|
+
themePreloadTags = compiled.preloadTags;
|
|
938
|
+
} catch (e) {
|
|
939
|
+
console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const app = createApp();
|
|
943
|
+
const vnode = toVNode(app);
|
|
944
|
+
const stream = renderToStream(vnode);
|
|
945
|
+
const html = await streamToString(stream);
|
|
946
|
+
if (renderCtx.ssrRedirect) {
|
|
947
|
+
return {
|
|
948
|
+
html: "",
|
|
949
|
+
css: "",
|
|
950
|
+
ssrData: [],
|
|
951
|
+
headTags: "",
|
|
952
|
+
redirect: renderCtx.ssrRedirect,
|
|
953
|
+
discoveredRoutes: renderCtx.discoveredRoutes,
|
|
954
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const css = collectCSS2(themeCss, module);
|
|
958
|
+
const ssrData = data.resolvedQueries.map(({ key, data: d }) => ({
|
|
959
|
+
key,
|
|
960
|
+
data: d
|
|
961
|
+
}));
|
|
962
|
+
return {
|
|
963
|
+
html,
|
|
964
|
+
css,
|
|
965
|
+
ssrData,
|
|
966
|
+
headTags: themePreloadTags,
|
|
967
|
+
discoveredRoutes: renderCtx.discoveredRoutes,
|
|
968
|
+
matchedRoutePatterns: renderCtx.matchedRoutePatterns
|
|
969
|
+
};
|
|
970
|
+
} finally {
|
|
971
|
+
clearGlobalSSRTimeout();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
var domShimInstalled2 = false;
|
|
976
|
+
function ensureDomShim2() {
|
|
977
|
+
if (domShimInstalled2 && typeof document !== "undefined")
|
|
978
|
+
return;
|
|
979
|
+
domShimInstalled2 = true;
|
|
980
|
+
installDomShim();
|
|
981
|
+
}
|
|
982
|
+
function resolveAppFactory2(module) {
|
|
983
|
+
const createApp = module.default || module.App;
|
|
984
|
+
if (typeof createApp !== "function") {
|
|
985
|
+
throw new Error("App entry must export a default function or named App function");
|
|
986
|
+
}
|
|
987
|
+
return createApp;
|
|
988
|
+
}
|
|
989
|
+
function filterByEntityAccess(queries, entityAccess, session) {
|
|
990
|
+
if (!entityAccess || !session)
|
|
991
|
+
return queries;
|
|
992
|
+
return queries.filter(({ key }) => {
|
|
993
|
+
const entity = extractEntityFromKey(key);
|
|
994
|
+
const method = extractMethodFromKey(key);
|
|
995
|
+
if (!entity)
|
|
996
|
+
return true;
|
|
997
|
+
const entityRules = entityAccess[entity];
|
|
998
|
+
if (!entityRules)
|
|
999
|
+
return true;
|
|
1000
|
+
const rule = entityRules[method];
|
|
1001
|
+
if (!rule)
|
|
1002
|
+
return true;
|
|
1003
|
+
return evaluateAccessRule(rule, session);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function extractEntityFromKey(key) {
|
|
1007
|
+
const pathStart = key.indexOf(":/");
|
|
1008
|
+
if (pathStart === -1)
|
|
1009
|
+
return;
|
|
1010
|
+
const path = key.slice(pathStart + 2);
|
|
1011
|
+
const firstSlash = path.indexOf("/");
|
|
1012
|
+
const questionMark = path.indexOf("?");
|
|
1013
|
+
if (firstSlash === -1 && questionMark === -1)
|
|
1014
|
+
return path;
|
|
1015
|
+
if (firstSlash === -1)
|
|
1016
|
+
return path.slice(0, questionMark);
|
|
1017
|
+
if (questionMark === -1)
|
|
1018
|
+
return path.slice(0, firstSlash);
|
|
1019
|
+
return path.slice(0, Math.min(firstSlash, questionMark));
|
|
1020
|
+
}
|
|
1021
|
+
function extractMethodFromKey(key) {
|
|
1022
|
+
const pathStart = key.indexOf(":/");
|
|
1023
|
+
if (pathStart === -1)
|
|
1024
|
+
return "list";
|
|
1025
|
+
const path = key.slice(pathStart + 2);
|
|
1026
|
+
const cleanPath = path.split("?")[0] ?? "";
|
|
1027
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
1028
|
+
return segments.length > 1 ? "get" : "list";
|
|
1029
|
+
}
|
|
1030
|
+
function collectCSS2(themeCss, module) {
|
|
1031
|
+
const alreadyIncluded = new Set;
|
|
1032
|
+
if (themeCss)
|
|
1033
|
+
alreadyIncluded.add(themeCss);
|
|
1034
|
+
if (module.styles) {
|
|
1035
|
+
for (const s of module.styles)
|
|
1036
|
+
alreadyIncluded.add(s);
|
|
1037
|
+
}
|
|
1038
|
+
const componentCss = module.getInjectedCSS ? module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s)) : [];
|
|
1039
|
+
const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
|
|
1040
|
+
const globalTag = module.styles && module.styles.length > 0 ? `<style data-vertz-css>${module.styles.join(`
|
|
1041
|
+
`)}</style>` : "";
|
|
1042
|
+
const componentTag = componentCss.length > 0 ? `<style data-vertz-css>${componentCss.join(`
|
|
1043
|
+
`)}</style>` : "";
|
|
1044
|
+
return [themeTag, globalTag, componentTag].filter(Boolean).join(`
|
|
1045
|
+
`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/ssr-session.ts
|
|
1049
|
+
function createSessionScript(session, nonce) {
|
|
1050
|
+
const json = JSON.stringify(session);
|
|
1051
|
+
const escaped = json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1052
|
+
const nonceAttr = nonce ? ` nonce="${escapeAttr3(nonce)}"` : "";
|
|
1053
|
+
return `<script${nonceAttr}>window.__VERTZ_SESSION__=${escaped}</script>`;
|
|
1054
|
+
}
|
|
1055
|
+
function escapeAttr3(s) {
|
|
1056
|
+
return s.replace(/[&"'<>]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/template-inject.ts
|
|
1060
|
+
function injectIntoTemplate(options) {
|
|
1061
|
+
const { template, appHtml, appCss, ssrData, nonce, headTags, sessionScript } = options;
|
|
1062
|
+
let html;
|
|
1063
|
+
if (template.includes("<!--ssr-outlet-->")) {
|
|
1064
|
+
html = template.replace("<!--ssr-outlet-->", appHtml);
|
|
1065
|
+
} else {
|
|
1066
|
+
html = replaceAppDivContent(template, appHtml);
|
|
1067
|
+
}
|
|
1068
|
+
if (headTags) {
|
|
1069
|
+
html = html.replace("</head>", `${headTags}
|
|
1070
|
+
</head>`);
|
|
1071
|
+
}
|
|
1072
|
+
if (appCss) {
|
|
1073
|
+
html = html.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
|
|
1074
|
+
<noscript>${match}</noscript>`);
|
|
1075
|
+
html = html.replace("</head>", `${appCss}
|
|
1076
|
+
</head>`);
|
|
1077
|
+
}
|
|
1078
|
+
if (sessionScript) {
|
|
1079
|
+
html = html.replace("</body>", `${sessionScript}
|
|
1080
|
+
</body>`);
|
|
1081
|
+
}
|
|
1082
|
+
if (ssrData.length > 0) {
|
|
1083
|
+
const nonceAttr = nonce != null ? ` nonce="${nonce}"` : "";
|
|
1084
|
+
const ssrDataScript = `<script${nonceAttr}>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>`;
|
|
1085
|
+
html = html.replace("</body>", `${ssrDataScript}
|
|
1086
|
+
</body>`);
|
|
1087
|
+
}
|
|
1088
|
+
return html;
|
|
1089
|
+
}
|
|
1090
|
+
function replaceAppDivContent(template, appHtml) {
|
|
1091
|
+
const openMatch = template.match(/<div[^>]*id="app"[^>]*>/);
|
|
1092
|
+
if (!openMatch || openMatch.index == null)
|
|
1093
|
+
return template;
|
|
1094
|
+
const openTag = openMatch[0];
|
|
1095
|
+
const contentStart = openMatch.index + openTag.length;
|
|
1096
|
+
let depth = 1;
|
|
1097
|
+
let i = contentStart;
|
|
1098
|
+
const len = template.length;
|
|
1099
|
+
while (i < len && depth > 0) {
|
|
1100
|
+
if (template[i] === "<") {
|
|
1101
|
+
if (template.startsWith("</div>", i)) {
|
|
1102
|
+
depth--;
|
|
1103
|
+
if (depth === 0)
|
|
1104
|
+
break;
|
|
1105
|
+
i += 6;
|
|
1106
|
+
} else if (template.startsWith("<div", i) && /^<div[\s>]/.test(template.slice(i, i + 5))) {
|
|
1107
|
+
depth++;
|
|
1108
|
+
i += 4;
|
|
1109
|
+
} else {
|
|
1110
|
+
i++;
|
|
1111
|
+
}
|
|
1112
|
+
} else {
|
|
1113
|
+
i++;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (depth !== 0) {
|
|
1117
|
+
return template;
|
|
1118
|
+
}
|
|
1119
|
+
return template.slice(0, contentStart) + appHtml + template.slice(i);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/ssr-progressive-response.ts
|
|
1123
|
+
function buildProgressiveResponse(options) {
|
|
1124
|
+
const { headChunk, renderStream, tailChunk, ssrData, nonce, headers } = options;
|
|
1125
|
+
const stream = new ReadableStream({
|
|
1126
|
+
async start(controller) {
|
|
1127
|
+
controller.enqueue(encodeChunk(headChunk));
|
|
1128
|
+
const reader = renderStream.getReader();
|
|
1129
|
+
let renderError;
|
|
1130
|
+
try {
|
|
1131
|
+
for (;; ) {
|
|
1132
|
+
const { done, value } = await reader.read();
|
|
1133
|
+
if (done)
|
|
1134
|
+
break;
|
|
1135
|
+
controller.enqueue(value);
|
|
1136
|
+
}
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
renderError = err instanceof Error ? err : new Error(String(err));
|
|
1139
|
+
}
|
|
1140
|
+
if (renderError) {
|
|
1141
|
+
console.error("[SSR] Render error after head sent:", renderError.message);
|
|
1142
|
+
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
1143
|
+
const errorScript = `<script${nonceAttr}>document.dispatchEvent(new CustomEvent('vertz:ssr-error',` + `{detail:{message:${safeSerialize(renderError.message)}}}))</script>`;
|
|
1144
|
+
controller.enqueue(encodeChunk(errorScript));
|
|
1145
|
+
}
|
|
1146
|
+
let tail = "";
|
|
1147
|
+
if (ssrData.length > 0) {
|
|
1148
|
+
const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
1149
|
+
tail += `<script${nonceAttr}>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>`;
|
|
1150
|
+
}
|
|
1151
|
+
tail += tailChunk;
|
|
1152
|
+
controller.enqueue(encodeChunk(tail));
|
|
1153
|
+
controller.close();
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
const responseHeaders = {
|
|
1157
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1158
|
+
...headers
|
|
1159
|
+
};
|
|
1160
|
+
return new Response(stream, { status: 200, headers: responseHeaders });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/template-split.ts
|
|
1164
|
+
function splitTemplate(template, options) {
|
|
1165
|
+
let processed = template;
|
|
1166
|
+
if (options?.inlineCSS) {
|
|
1167
|
+
processed = inlineCSSAssets(processed, options.inlineCSS);
|
|
1168
|
+
}
|
|
1169
|
+
const outletMarker = "<!--ssr-outlet-->";
|
|
1170
|
+
const outletIndex = processed.indexOf(outletMarker);
|
|
1171
|
+
if (outletIndex !== -1) {
|
|
1172
|
+
return {
|
|
1173
|
+
headTemplate: processed.slice(0, outletIndex),
|
|
1174
|
+
tailTemplate: processed.slice(outletIndex + outletMarker.length)
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const appDivMatch = processed.match(/<div[^>]*id="app"[^>]*>/);
|
|
1178
|
+
if (appDivMatch && appDivMatch.index != null) {
|
|
1179
|
+
const openTag = appDivMatch[0];
|
|
1180
|
+
const contentStart = appDivMatch.index + openTag.length;
|
|
1181
|
+
const closingIndex = findMatchingDivClose(processed, contentStart);
|
|
1182
|
+
return {
|
|
1183
|
+
headTemplate: processed.slice(0, contentStart),
|
|
1184
|
+
tailTemplate: processed.slice(closingIndex)
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
throw new Error('Could not find <!--ssr-outlet--> or <div id="app"> in the HTML template. ' + "The template must contain one of these markers for SSR content injection.");
|
|
1188
|
+
}
|
|
1189
|
+
function findMatchingDivClose(html, startAfterOpen) {
|
|
1190
|
+
let depth = 1;
|
|
1191
|
+
let i = startAfterOpen;
|
|
1192
|
+
const len = html.length;
|
|
1193
|
+
while (i < len && depth > 0) {
|
|
1194
|
+
if (html[i] === "<") {
|
|
1195
|
+
if (html.startsWith("</div>", i)) {
|
|
1196
|
+
depth--;
|
|
1197
|
+
if (depth === 0)
|
|
1198
|
+
return i;
|
|
1199
|
+
i += 6;
|
|
1200
|
+
} else if (html.startsWith("<div", i) && /^<div[\s>]/.test(html.slice(i, i + 5))) {
|
|
1201
|
+
depth++;
|
|
1202
|
+
i += 4;
|
|
1203
|
+
} else {
|
|
1204
|
+
i++;
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
i++;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return len;
|
|
1211
|
+
}
|
|
1212
|
+
function inlineCSSAssets(html, inlineCSS) {
|
|
1213
|
+
let result = html;
|
|
1214
|
+
for (const [href, css] of Object.entries(inlineCSS)) {
|
|
1215
|
+
const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1216
|
+
const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
|
|
1217
|
+
const safeCss = css.replace(/<\//g, "<\\/");
|
|
1218
|
+
result = result.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
|
|
1219
|
+
}
|
|
1220
|
+
result = result.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
|
|
1221
|
+
<noscript>${match}</noscript>`);
|
|
1222
|
+
return result;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/ssr-handler.ts
|
|
1226
|
+
function sanitizeLinkHref(href) {
|
|
1227
|
+
return href.replace(/[<>,;\s"']/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1228
|
+
}
|
|
1229
|
+
function sanitizeLinkParam(value) {
|
|
1230
|
+
return value.replace(/[^a-zA-Z0-9/_.-]/g, "");
|
|
1231
|
+
}
|
|
1232
|
+
function buildLinkHeader(items) {
|
|
1233
|
+
return items.map((item) => {
|
|
1234
|
+
const parts = [
|
|
1235
|
+
`<${sanitizeLinkHref(item.href)}>`,
|
|
1236
|
+
"rel=preload",
|
|
1237
|
+
`as=${sanitizeLinkParam(item.as)}`
|
|
1238
|
+
];
|
|
1239
|
+
if (item.type)
|
|
1240
|
+
parts.push(`type=${sanitizeLinkParam(item.type)}`);
|
|
1241
|
+
if (item.crossorigin)
|
|
1242
|
+
parts.push("crossorigin");
|
|
1243
|
+
return parts.join("; ");
|
|
1244
|
+
}).join(", ");
|
|
1245
|
+
}
|
|
1246
|
+
function buildModulepreloadTags(paths) {
|
|
1247
|
+
return paths.map((p) => `<link rel="modulepreload" href="${escapeAttr(p)}">`).join(`
|
|
1248
|
+
`);
|
|
1249
|
+
}
|
|
1250
|
+
function resolveRouteModulepreload(routeChunkManifest, matchedPatterns, fallback) {
|
|
1251
|
+
if (routeChunkManifest && matchedPatterns?.length) {
|
|
1252
|
+
const chunkPaths = new Set;
|
|
1253
|
+
for (const pattern of matchedPatterns) {
|
|
1254
|
+
const chunks = routeChunkManifest.routes[pattern];
|
|
1255
|
+
if (chunks) {
|
|
1256
|
+
for (const chunk of chunks) {
|
|
1257
|
+
chunkPaths.add(chunk);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (chunkPaths.size > 0) {
|
|
1262
|
+
return buildModulepreloadTags([...chunkPaths]);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return fallback;
|
|
1266
|
+
}
|
|
1267
|
+
function createSSRHandler(options) {
|
|
1268
|
+
const {
|
|
1269
|
+
module,
|
|
1270
|
+
ssrTimeout,
|
|
1271
|
+
inlineCSS,
|
|
1272
|
+
nonce,
|
|
1273
|
+
fallbackMetrics,
|
|
1274
|
+
modulepreload,
|
|
1275
|
+
routeChunkManifest,
|
|
1276
|
+
cacheControl,
|
|
1277
|
+
sessionResolver,
|
|
1278
|
+
manifest,
|
|
1279
|
+
progressiveHTML
|
|
1280
|
+
} = options;
|
|
1281
|
+
let template = options.template;
|
|
1282
|
+
if (inlineCSS) {
|
|
1283
|
+
for (const [href, css] of Object.entries(inlineCSS)) {
|
|
1284
|
+
const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1285
|
+
const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
|
|
1286
|
+
const safeCss = css.replace(/<\//g, "<\\/");
|
|
1287
|
+
template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
let linkHeader;
|
|
1291
|
+
if (module.theme) {
|
|
1292
|
+
const compiled = compileThemeCached(module.theme, fallbackMetrics);
|
|
1293
|
+
if (compiled.preloadItems.length > 0) {
|
|
1294
|
+
linkHeader = buildLinkHeader(compiled.preloadItems);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const modulepreloadTags = modulepreload?.length ? buildModulepreloadTags(modulepreload) : undefined;
|
|
1298
|
+
const splitResult = progressiveHTML ? splitTemplate(template) : undefined;
|
|
1299
|
+
if (splitResult && module.theme) {
|
|
1300
|
+
splitResult.headTemplate = splitResult.headTemplate.replace(/<link\s+rel="stylesheet"\s+href="([^"]+)"[^>]*>/g, (match, href) => `<link rel="stylesheet" href="${href}" media="print" onload="this.media='all'">
|
|
1301
|
+
<noscript>${match}</noscript>`);
|
|
1302
|
+
}
|
|
1303
|
+
return async (request) => {
|
|
1304
|
+
const url = new URL(request.url);
|
|
1305
|
+
const pathname = url.pathname;
|
|
1306
|
+
if (request.headers.get("x-vertz-nav") === "1") {
|
|
1307
|
+
return handleNavRequest(module, pathname, ssrTimeout);
|
|
1308
|
+
}
|
|
1309
|
+
let sessionScript = "";
|
|
1310
|
+
let ssrAuth;
|
|
1311
|
+
if (sessionResolver) {
|
|
1312
|
+
try {
|
|
1313
|
+
const sessionResult = await sessionResolver(request);
|
|
1314
|
+
if (sessionResult) {
|
|
1315
|
+
ssrAuth = {
|
|
1316
|
+
status: "authenticated",
|
|
1317
|
+
user: sessionResult.session.user,
|
|
1318
|
+
expiresAt: sessionResult.session.expiresAt
|
|
1319
|
+
};
|
|
1320
|
+
const scripts = [];
|
|
1321
|
+
scripts.push(createSessionScript(sessionResult.session, nonce));
|
|
1322
|
+
if (sessionResult.accessSet != null) {
|
|
1323
|
+
scripts.push(createAccessSetScript(sessionResult.accessSet, nonce));
|
|
1324
|
+
}
|
|
1325
|
+
sessionScript = scripts.join(`
|
|
1326
|
+
`);
|
|
1327
|
+
} else {
|
|
1328
|
+
ssrAuth = { status: "unauthenticated" };
|
|
1329
|
+
}
|
|
1330
|
+
} catch (resolverErr) {
|
|
1331
|
+
console.warn("[Server] Session resolver failed:", resolverErr instanceof Error ? resolverErr.message : resolverErr);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const useProgressive = progressiveHTML && splitResult && !(manifest?.routeEntries && Object.keys(manifest.routeEntries).length > 0);
|
|
1335
|
+
if (useProgressive) {
|
|
1336
|
+
return handleProgressiveHTMLRequest(module, splitResult, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest);
|
|
1337
|
+
}
|
|
1338
|
+
return handleHTMLRequest(module, template, pathname + url.search, ssrTimeout, nonce, fallbackMetrics, linkHeader, modulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest);
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async function handleNavRequest(module, url, ssrTimeout) {
|
|
1342
|
+
try {
|
|
1343
|
+
const stream = await ssrStreamNavQueries(module, url, { ssrTimeout });
|
|
1344
|
+
return new Response(stream, {
|
|
1345
|
+
status: 200,
|
|
1346
|
+
headers: {
|
|
1347
|
+
"Content-Type": "text/event-stream",
|
|
1348
|
+
"Cache-Control": "no-cache"
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
} catch {
|
|
1352
|
+
return new Response(`event: done
|
|
1353
|
+
data: {}
|
|
1354
|
+
|
|
1355
|
+
`, {
|
|
1356
|
+
status: 200,
|
|
1357
|
+
headers: {
|
|
1358
|
+
"Content-Type": "text/event-stream",
|
|
1359
|
+
"Cache-Control": "no-cache"
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async function handleProgressiveHTMLRequest(module, split, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, staticModulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest) {
|
|
1365
|
+
try {
|
|
1366
|
+
const result = await ssrRenderProgressive(module, url, {
|
|
1367
|
+
ssrTimeout,
|
|
1368
|
+
fallbackMetrics,
|
|
1369
|
+
ssrAuth,
|
|
1370
|
+
manifest
|
|
1371
|
+
});
|
|
1372
|
+
if (result.redirect) {
|
|
1373
|
+
return new Response(null, {
|
|
1374
|
+
status: 302,
|
|
1375
|
+
headers: { Location: result.redirect.to }
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
const modulepreloadTags = resolveRouteModulepreload(routeChunkManifest, result.matchedRoutePatterns, staticModulepreloadTags);
|
|
1379
|
+
let headChunk = split.headTemplate;
|
|
1380
|
+
const headCloseIdx = headChunk.lastIndexOf("</head>");
|
|
1381
|
+
if (headCloseIdx !== -1) {
|
|
1382
|
+
const injections = [];
|
|
1383
|
+
if (result.css)
|
|
1384
|
+
injections.push(result.css);
|
|
1385
|
+
if (result.headTags)
|
|
1386
|
+
injections.push(result.headTags);
|
|
1387
|
+
if (modulepreloadTags)
|
|
1388
|
+
injections.push(modulepreloadTags);
|
|
1389
|
+
if (sessionScript)
|
|
1390
|
+
injections.push(sessionScript);
|
|
1391
|
+
if (injections.length > 0) {
|
|
1392
|
+
headChunk = headChunk.slice(0, headCloseIdx) + injections.join(`
|
|
1393
|
+
`) + `
|
|
1394
|
+
` + headChunk.slice(headCloseIdx);
|
|
1395
|
+
}
|
|
1396
|
+
} else {
|
|
1397
|
+
if (result.css)
|
|
1398
|
+
headChunk += result.css;
|
|
1399
|
+
if (result.headTags)
|
|
1400
|
+
headChunk += result.headTags;
|
|
1401
|
+
if (modulepreloadTags)
|
|
1402
|
+
headChunk += modulepreloadTags;
|
|
1403
|
+
if (sessionScript)
|
|
1404
|
+
headChunk += sessionScript;
|
|
1405
|
+
}
|
|
1406
|
+
const headers = {};
|
|
1407
|
+
if (linkHeader)
|
|
1408
|
+
headers.Link = linkHeader;
|
|
1409
|
+
if (cacheControl)
|
|
1410
|
+
headers["Cache-Control"] = cacheControl;
|
|
1411
|
+
return buildProgressiveResponse({
|
|
1412
|
+
headChunk,
|
|
1413
|
+
renderStream: result.renderStream,
|
|
1414
|
+
tailChunk: split.tailTemplate,
|
|
1415
|
+
ssrData: result.ssrData,
|
|
1416
|
+
nonce,
|
|
1417
|
+
headers
|
|
1418
|
+
});
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
1421
|
+
return new Response("Internal Server Error", {
|
|
1422
|
+
status: 500,
|
|
1423
|
+
headers: { "Content-Type": "text/plain" }
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
async function handleHTMLRequest(module, template, url, ssrTimeout, nonce, fallbackMetrics, linkHeader, staticModulepreloadTags, routeChunkManifest, cacheControl, sessionScript, ssrAuth, manifest) {
|
|
1428
|
+
try {
|
|
1429
|
+
const result = await ssrRenderSinglePass(module, url, {
|
|
1430
|
+
ssrTimeout,
|
|
1431
|
+
fallbackMetrics,
|
|
1432
|
+
ssrAuth,
|
|
1433
|
+
manifest
|
|
1434
|
+
});
|
|
1435
|
+
if (result.redirect) {
|
|
1436
|
+
return new Response(null, {
|
|
1437
|
+
status: 302,
|
|
1438
|
+
headers: { Location: result.redirect.to }
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
const modulepreloadTags = resolveRouteModulepreload(routeChunkManifest, result.matchedRoutePatterns, staticModulepreloadTags);
|
|
1442
|
+
const allHeadTags = [result.headTags, modulepreloadTags].filter(Boolean).join(`
|
|
1443
|
+
`);
|
|
1444
|
+
const html = injectIntoTemplate({
|
|
1445
|
+
template,
|
|
1446
|
+
appHtml: result.html,
|
|
1447
|
+
appCss: result.css,
|
|
1448
|
+
ssrData: result.ssrData,
|
|
1449
|
+
nonce,
|
|
1450
|
+
headTags: allHeadTags || undefined,
|
|
1451
|
+
sessionScript
|
|
1452
|
+
});
|
|
1453
|
+
const headers = { "Content-Type": "text/html; charset=utf-8" };
|
|
1454
|
+
if (linkHeader)
|
|
1455
|
+
headers.Link = linkHeader;
|
|
1456
|
+
if (cacheControl)
|
|
1457
|
+
headers["Cache-Control"] = cacheControl;
|
|
1458
|
+
return new Response(html, { status: 200, headers });
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
console.error("[SSR] Render failed:", err instanceof Error ? err.message : err);
|
|
1461
|
+
return new Response("Internal Server Error", {
|
|
1462
|
+
status: 500,
|
|
1463
|
+
headers: { "Content-Type": "text/plain" }
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, compileThemeCached, createRequestContext, ssrRenderToString, ssrDiscoverQueries, toPrefetchSession, evaluateAccessRule, getAccessSetForSSR, createAccessSetScript, matchUrlToPatterns, reconstructDescriptors, ssrRenderSinglePass, createSessionScript, injectIntoTemplate, createSSRHandler };
|