@vertz/ui-server 0.2.0 → 0.2.4

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.
@@ -0,0 +1,150 @@
1
+ // @bun
2
+ // src/bun-plugin/fast-refresh-dom-state.ts
3
+ function captureDOMState(element) {
4
+ return {
5
+ formFields: captureFormFields(element),
6
+ focus: captureFocus(element),
7
+ scrollPositions: captureScrollPositions(element)
8
+ };
9
+ }
10
+ function captureFormFields(element) {
11
+ const fields = new Map;
12
+ const inputs = element.querySelectorAll("input, textarea, select");
13
+ for (const el of inputs) {
14
+ const name = el.getAttribute("name");
15
+ if (!name)
16
+ continue;
17
+ const type = el.type ?? "";
18
+ if (type === "file")
19
+ continue;
20
+ fields.set(name, {
21
+ value: el.value ?? "",
22
+ checked: el.checked ?? false,
23
+ selectedIndex: el.selectedIndex ?? -1,
24
+ type
25
+ });
26
+ }
27
+ return fields;
28
+ }
29
+ function captureFocus(element) {
30
+ const active = element.ownerDocument?.activeElement;
31
+ if (!active)
32
+ return null;
33
+ if (!element.contains(active))
34
+ return null;
35
+ const name = active.getAttribute("name");
36
+ const id = active.getAttribute("id");
37
+ const matchKey = name ?? id;
38
+ if (!matchKey)
39
+ return null;
40
+ let selectionStart = -1;
41
+ let selectionEnd = -1;
42
+ const inputLike = active;
43
+ if ("selectionStart" in active && inputLike.selectionStart != null) {
44
+ selectionStart = inputLike.selectionStart;
45
+ selectionEnd = inputLike.selectionEnd ?? selectionStart;
46
+ }
47
+ return {
48
+ matchKey,
49
+ matchBy: name ? "name" : "id",
50
+ selectionStart,
51
+ selectionEnd
52
+ };
53
+ }
54
+ function captureScrollPositions(element) {
55
+ const positions = [];
56
+ walkElements(element, (el) => {
57
+ if (el.scrollTop === 0 && el.scrollLeft === 0)
58
+ return;
59
+ const id = el.getAttribute("id");
60
+ if (id) {
61
+ positions.push({
62
+ matchKey: id,
63
+ matchBy: "id",
64
+ scrollTop: el.scrollTop,
65
+ scrollLeft: el.scrollLeft
66
+ });
67
+ return;
68
+ }
69
+ const selector = `${el.tagName.toLowerCase()}.${el.className}`;
70
+ if (el.className) {
71
+ positions.push({
72
+ matchKey: selector,
73
+ matchBy: "selector",
74
+ scrollTop: el.scrollTop,
75
+ scrollLeft: el.scrollLeft
76
+ });
77
+ }
78
+ });
79
+ return positions;
80
+ }
81
+ function restoreDOMState(newElement, snapshot) {
82
+ restoreFormFields(newElement, snapshot.formFields);
83
+ restoreFocus(newElement, snapshot.focus);
84
+ restoreScrollPositions(newElement, snapshot.scrollPositions);
85
+ }
86
+ function restoreFormFields(element, fields) {
87
+ if (fields.size === 0)
88
+ return;
89
+ const inputs = element.querySelectorAll("input, textarea, select");
90
+ for (const el of inputs) {
91
+ const name = el.getAttribute("name");
92
+ if (!name)
93
+ continue;
94
+ const saved = fields.get(name);
95
+ if (!saved)
96
+ continue;
97
+ if (saved.type === "file")
98
+ continue;
99
+ const input = el;
100
+ if (el.tagName === "SELECT") {
101
+ el.selectedIndex = saved.selectedIndex;
102
+ } else if (saved.type === "checkbox" || saved.type === "radio") {
103
+ input.checked = saved.checked;
104
+ } else {
105
+ input.value = saved.value;
106
+ }
107
+ }
108
+ }
109
+ function restoreFocus(element, focus) {
110
+ if (!focus)
111
+ return;
112
+ const target = focus.matchBy === "name" ? element.querySelector(`[name="${focus.matchKey}"]`) : element.querySelector(`#${focus.matchKey}`);
113
+ if (!target)
114
+ return;
115
+ if (target.disabled)
116
+ return;
117
+ if (typeof target.focus === "function") {
118
+ target.focus();
119
+ }
120
+ if (focus.selectionStart >= 0 && "setSelectionRange" in target && typeof target.setSelectionRange === "function") {
121
+ try {
122
+ target.setSelectionRange(focus.selectionStart, focus.selectionEnd);
123
+ } catch (_) {}
124
+ }
125
+ }
126
+ function restoreScrollPositions(element, positions) {
127
+ for (const pos of positions) {
128
+ let target = null;
129
+ if (pos.matchBy === "id") {
130
+ target = element.querySelector(`#${pos.matchKey}`);
131
+ } else {
132
+ target = element.querySelector(pos.matchKey);
133
+ }
134
+ if (!target)
135
+ continue;
136
+ target.scrollTop = pos.scrollTop;
137
+ target.scrollLeft = pos.scrollLeft;
138
+ }
139
+ }
140
+ function walkElements(root, callback) {
141
+ callback(root);
142
+ const children = root.children;
143
+ for (let i = 0;i < children.length; i++) {
144
+ const child = children[i];
145
+ if (child)
146
+ walkElements(child, callback);
147
+ }
148
+ }
149
+
150
+ export { captureDOMState, restoreDOMState };
@@ -0,0 +1,564 @@
1
+ import {
2
+ SSRElement,
3
+ installDomShim,
4
+ removeDomShim,
5
+ toVNode
6
+ } from "./chunk-4t0ekdyv.js";
7
+
8
+ // src/html-serializer.ts
9
+ var VOID_ELEMENTS = new Set([
10
+ "area",
11
+ "base",
12
+ "br",
13
+ "col",
14
+ "embed",
15
+ "hr",
16
+ "img",
17
+ "input",
18
+ "link",
19
+ "meta",
20
+ "param",
21
+ "source",
22
+ "track",
23
+ "wbr"
24
+ ]);
25
+ var RAW_TEXT_ELEMENTS = new Set(["script", "style"]);
26
+ function escapeHtml(text) {
27
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
28
+ }
29
+ function escapeAttr(value) {
30
+ const str = typeof value === "string" ? value : String(value);
31
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
32
+ }
33
+ function serializeAttrs(attrs) {
34
+ const parts = [];
35
+ for (const [key, value] of Object.entries(attrs)) {
36
+ parts.push(` ${key}="${escapeAttr(value)}"`);
37
+ }
38
+ return parts.join("");
39
+ }
40
+ function isRawHtml(value) {
41
+ return typeof value === "object" && "__raw" in value && value.__raw === true;
42
+ }
43
+ function serializeToHtml(node) {
44
+ if (typeof node === "string") {
45
+ return escapeHtml(node);
46
+ }
47
+ if (isRawHtml(node)) {
48
+ return node.html;
49
+ }
50
+ const { tag, attrs, children } = node;
51
+ const attrStr = serializeAttrs(attrs);
52
+ if (VOID_ELEMENTS.has(tag)) {
53
+ return `<${tag}${attrStr}>`;
54
+ }
55
+ const isRawText = RAW_TEXT_ELEMENTS.has(tag);
56
+ const childrenHtml = children.map((child) => {
57
+ if (typeof child === "string" && isRawText) {
58
+ return child;
59
+ }
60
+ return serializeToHtml(child);
61
+ }).join("");
62
+ return `<${tag}${attrStr}>${childrenHtml}</${tag}>`;
63
+ }
64
+
65
+ // src/slot-placeholder.ts
66
+ var slotCounter = 0;
67
+ function resetSlotCounter() {
68
+ slotCounter = 0;
69
+ }
70
+ function createSlotPlaceholder(fallback) {
71
+ const id = slotCounter++;
72
+ const placeholder = {
73
+ tag: "div",
74
+ attrs: { id: `v-slot-${id}` },
75
+ children: typeof fallback === "string" ? [fallback] : [fallback],
76
+ _slotId: id
77
+ };
78
+ return placeholder;
79
+ }
80
+
81
+ // src/streaming.ts
82
+ var encoder = new TextEncoder;
83
+ var decoder = new TextDecoder;
84
+ function encodeChunk(html) {
85
+ return encoder.encode(html);
86
+ }
87
+ async function streamToString(stream) {
88
+ const reader = stream.getReader();
89
+ const parts = [];
90
+ for (;; ) {
91
+ const { done, value } = await reader.read();
92
+ if (done)
93
+ break;
94
+ parts.push(decoder.decode(value, { stream: true }));
95
+ }
96
+ parts.push(decoder.decode());
97
+ return parts.join("");
98
+ }
99
+ async function collectStreamChunks(stream) {
100
+ const reader = stream.getReader();
101
+ const chunks = [];
102
+ for (;; ) {
103
+ const { done, value } = await reader.read();
104
+ if (done)
105
+ break;
106
+ chunks.push(decoder.decode(value, { stream: true }));
107
+ }
108
+ return chunks;
109
+ }
110
+
111
+ // src/template-chunk.ts
112
+ function createTemplateChunk(slotId, resolvedHtml, nonce) {
113
+ const tmplId = `v-tmpl-${slotId}`;
114
+ const slotRef = `v-slot-${slotId}`;
115
+ const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
116
+ 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>";
117
+ }
118
+
119
+ // src/render-to-stream.ts
120
+ function isSuspenseNode(node) {
121
+ return typeof node === "object" && "tag" in node && node.tag === "__suspense" && "_resolve" in node;
122
+ }
123
+ function renderToStream(tree, options) {
124
+ const pendingBoundaries = [];
125
+ function walkAndSerialize(node) {
126
+ if (typeof node === "string") {
127
+ return escapeHtml(node);
128
+ }
129
+ if (isRawHtml(node)) {
130
+ return node.html;
131
+ }
132
+ if (isSuspenseNode(node)) {
133
+ const placeholder = createSlotPlaceholder(node._fallback);
134
+ pendingBoundaries.push({
135
+ slotId: placeholder._slotId,
136
+ resolve: node._resolve
137
+ });
138
+ return serializeToHtml(placeholder);
139
+ }
140
+ const { tag, attrs, children } = node;
141
+ const isRawText = RAW_TEXT_ELEMENTS.has(tag);
142
+ const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
143
+ if (VOID_ELEMENTS.has(tag)) {
144
+ return `<${tag}${attrStr}>`;
145
+ }
146
+ const childrenHtml = children.map((child) => {
147
+ if (typeof child === "string" && isRawText) {
148
+ return child;
149
+ }
150
+ return walkAndSerialize(child);
151
+ }).join("");
152
+ return `<${tag}${attrStr}>${childrenHtml}</${tag}>`;
153
+ }
154
+ return new ReadableStream({
155
+ async start(controller) {
156
+ const mainHtml = walkAndSerialize(tree);
157
+ controller.enqueue(encodeChunk(mainHtml));
158
+ if (pendingBoundaries.length > 0) {
159
+ const nonce = options?.nonce;
160
+ const resolutions = pendingBoundaries.map(async (boundary) => {
161
+ try {
162
+ const resolved = await boundary.resolve;
163
+ const resolvedHtml = serializeToHtml(resolved);
164
+ return createTemplateChunk(boundary.slotId, resolvedHtml, nonce);
165
+ } catch (_err) {
166
+ const errorHtml = `<div data-v-ssr-error="true" id="v-ssr-error-${boundary.slotId}">` + "<!--SSR error--></div>";
167
+ return createTemplateChunk(boundary.slotId, errorHtml, nonce);
168
+ }
169
+ });
170
+ const chunks = await Promise.all(resolutions);
171
+ for (const chunk of chunks) {
172
+ controller.enqueue(encodeChunk(chunk));
173
+ }
174
+ }
175
+ controller.close();
176
+ }
177
+ });
178
+ }
179
+
180
+ // src/ssr-context.ts
181
+ import { AsyncLocalStorage } from "node:async_hooks";
182
+ var ssrStorage = new AsyncLocalStorage;
183
+ function isInSSR() {
184
+ return ssrStorage.getStore() !== undefined;
185
+ }
186
+ function getSSRUrl() {
187
+ return ssrStorage.getStore()?.url;
188
+ }
189
+ function registerSSRQuery(entry) {
190
+ ssrStorage.getStore()?.queries.push(entry);
191
+ }
192
+ function getSSRQueries() {
193
+ return ssrStorage.getStore()?.queries ?? [];
194
+ }
195
+ function setGlobalSSRTimeout(timeout) {
196
+ const store = ssrStorage.getStore();
197
+ if (store)
198
+ store.globalSSRTimeout = timeout;
199
+ }
200
+ function clearGlobalSSRTimeout() {
201
+ const store = ssrStorage.getStore();
202
+ if (store)
203
+ store.globalSSRTimeout = undefined;
204
+ }
205
+ function getGlobalSSRTimeout() {
206
+ return ssrStorage.getStore()?.globalSSRTimeout;
207
+ }
208
+ globalThis.__VERTZ_IS_SSR__ = isInSSR;
209
+ globalThis.__VERTZ_SSR_REGISTER_QUERY__ = registerSSRQuery;
210
+ globalThis.__VERTZ_GET_GLOBAL_SSR_TIMEOUT__ = getGlobalSSRTimeout;
211
+
212
+ // src/ssr-streaming-runtime.ts
213
+ function safeSerialize(data) {
214
+ return JSON.stringify(data).replace(/</g, "\\u003c");
215
+ }
216
+ function getStreamingRuntimeScript(nonce) {
217
+ const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
218
+ 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>";
219
+ }
220
+ function createSSRDataChunk(key, data, nonce) {
221
+ const nonceAttr = nonce != null ? ` nonce="${escapeAttr(nonce)}"` : "";
222
+ const serialized = safeSerialize(data);
223
+ return `<script${nonceAttr}>window.__VERTZ_SSR_PUSH__(${safeSerialize(key)},${serialized})</script>`;
224
+ }
225
+
226
+ // src/ssr-render.ts
227
+ import { compileTheme } from "@vertz/ui";
228
+ var renderLock = Promise.resolve();
229
+ function withRenderLock(fn) {
230
+ const prev = renderLock;
231
+ let release;
232
+ renderLock = new Promise((r) => {
233
+ release = r;
234
+ });
235
+ return prev.then(fn).finally(() => release());
236
+ }
237
+ function resolveAppFactory(module) {
238
+ const createApp = module.default || module.App;
239
+ if (typeof createApp !== "function") {
240
+ throw new Error("App entry must export a default function or named App function");
241
+ }
242
+ return createApp;
243
+ }
244
+ function collectCSS(themeCss, module) {
245
+ const themeTag = themeCss ? `<style data-vertz-css>${themeCss}</style>` : "";
246
+ const globalTags = module.styles ? module.styles.map((s) => `<style data-vertz-css>${s}</style>`).join(`
247
+ `) : "";
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
+ let componentCss;
256
+ if (module.getInjectedCSS) {
257
+ componentCss = module.getInjectedCSS().filter((s) => !alreadyIncluded.has(s));
258
+ } else {
259
+ componentCss = [];
260
+ const head = globalThis.document?.head;
261
+ if (head instanceof SSRElement) {
262
+ for (const child of head.children) {
263
+ if (child instanceof SSRElement && child.tag === "style" && "data-vertz-css" in child.attrs && child.textContent && !alreadyIncluded.has(child.textContent)) {
264
+ componentCss.push(child.textContent);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ const componentStyles = componentCss.map((s) => `<style data-vertz-css>${s}</style>`).join(`
270
+ `);
271
+ return [themeTag, globalTags, componentStyles].filter(Boolean).join(`
272
+ `);
273
+ }
274
+ async function ssrRenderToString(module, url, options) {
275
+ return withRenderLock(() => ssrRenderToStringUnsafe(module, url, options));
276
+ }
277
+ async function ssrRenderToStringUnsafe(module, url, options) {
278
+ const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
279
+ const ssrTimeout = options?.ssrTimeout ?? 300;
280
+ return ssrStorage.run({ url: normalizedUrl, errors: [], queries: [] }, async () => {
281
+ globalThis.__SSR_URL__ = normalizedUrl;
282
+ installDomShim();
283
+ globalThis.__VERTZ_CLEAR_QUERY_CACHE__?.();
284
+ try {
285
+ setGlobalSSRTimeout(ssrTimeout);
286
+ const createApp = resolveAppFactory(module);
287
+ let themeCss = "";
288
+ if (module.theme) {
289
+ try {
290
+ themeCss = compileTheme(module.theme).css;
291
+ } catch (e) {
292
+ console.error("[vertz] Failed to compile theme export. Ensure your theme is created with defineTheme().", e);
293
+ }
294
+ }
295
+ globalThis.__VERTZ_SSR_SYNC_ROUTER__?.(normalizedUrl);
296
+ createApp();
297
+ const queries = getSSRQueries();
298
+ const resolvedQueries = [];
299
+ if (queries.length > 0) {
300
+ await Promise.allSettled(queries.map(({ promise, timeout, resolve, key }) => Promise.race([
301
+ promise.then((data) => {
302
+ resolve(data);
303
+ resolvedQueries.push({ key, data });
304
+ return "resolved";
305
+ }),
306
+ new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => "timeout")
307
+ ])));
308
+ const store = ssrStorage.getStore();
309
+ if (store)
310
+ store.queries = [];
311
+ }
312
+ const app = createApp();
313
+ const vnode = toVNode(app);
314
+ const stream = renderToStream(vnode);
315
+ const html = await streamToString(stream);
316
+ const css = collectCSS(themeCss, module);
317
+ const ssrData = resolvedQueries.length > 0 ? resolvedQueries.map(({ key, data }) => ({
318
+ key,
319
+ data: JSON.parse(JSON.stringify(data))
320
+ })) : [];
321
+ return { html, css, ssrData };
322
+ } finally {
323
+ clearGlobalSSRTimeout();
324
+ removeDomShim();
325
+ delete globalThis.__SSR_URL__;
326
+ }
327
+ });
328
+ }
329
+ async function ssrDiscoverQueries(module, url, options) {
330
+ return withRenderLock(() => ssrDiscoverQueriesUnsafe(module, url, options));
331
+ }
332
+ async function ssrDiscoverQueriesUnsafe(module, url, options) {
333
+ const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
334
+ const ssrTimeout = options?.ssrTimeout ?? 300;
335
+ return ssrStorage.run({ url: normalizedUrl, errors: [], queries: [] }, async () => {
336
+ globalThis.__SSR_URL__ = normalizedUrl;
337
+ installDomShim();
338
+ globalThis.__VERTZ_CLEAR_QUERY_CACHE__?.();
339
+ try {
340
+ setGlobalSSRTimeout(ssrTimeout);
341
+ const createApp = resolveAppFactory(module);
342
+ globalThis.__VERTZ_SSR_SYNC_ROUTER__?.(normalizedUrl);
343
+ createApp();
344
+ const queries = getSSRQueries();
345
+ const resolvedQueries = [];
346
+ const pendingKeys = [];
347
+ if (queries.length > 0) {
348
+ await Promise.allSettled(queries.map(({ promise, timeout, resolve, key }) => {
349
+ let settled = false;
350
+ return Promise.race([
351
+ promise.then((data) => {
352
+ if (settled)
353
+ return "late";
354
+ settled = true;
355
+ resolve(data);
356
+ resolvedQueries.push({ key, data });
357
+ return "resolved";
358
+ }),
359
+ new Promise((r) => setTimeout(r, timeout || ssrTimeout)).then(() => {
360
+ if (settled)
361
+ return "already-resolved";
362
+ settled = true;
363
+ pendingKeys.push(key);
364
+ return "timeout";
365
+ })
366
+ ]);
367
+ }));
368
+ }
369
+ return {
370
+ resolved: resolvedQueries.map(({ key, data }) => ({
371
+ key,
372
+ data: JSON.parse(JSON.stringify(data))
373
+ })),
374
+ pending: pendingKeys
375
+ };
376
+ } finally {
377
+ clearGlobalSSRTimeout();
378
+ removeDomShim();
379
+ delete globalThis.__SSR_URL__;
380
+ }
381
+ });
382
+ }
383
+ async function ssrStreamNavQueries(module, url, options) {
384
+ const normalizedUrl = url.endsWith("/index.html") ? url.slice(0, -"/index.html".length) || "/" : url;
385
+ const ssrTimeout = options?.ssrTimeout ?? 300;
386
+ const navTimeout = options?.navSsrTimeout ?? 5000;
387
+ const queries = await withRenderLock(() => ssrStorage.run({ url: normalizedUrl, errors: [], queries: [] }, async () => {
388
+ globalThis.__SSR_URL__ = normalizedUrl;
389
+ installDomShim();
390
+ globalThis.__VERTZ_CLEAR_QUERY_CACHE__?.();
391
+ try {
392
+ setGlobalSSRTimeout(ssrTimeout);
393
+ const createApp = resolveAppFactory(module);
394
+ globalThis.__VERTZ_SSR_SYNC_ROUTER__?.(normalizedUrl);
395
+ createApp();
396
+ const discovered = getSSRQueries();
397
+ return discovered.map((q) => ({
398
+ promise: q.promise,
399
+ timeout: q.timeout || ssrTimeout,
400
+ resolve: q.resolve,
401
+ key: q.key
402
+ }));
403
+ } finally {
404
+ clearGlobalSSRTimeout();
405
+ removeDomShim();
406
+ delete globalThis.__SSR_URL__;
407
+ }
408
+ }));
409
+ if (queries.length === 0) {
410
+ const encoder3 = new TextEncoder;
411
+ return new ReadableStream({
412
+ start(controller) {
413
+ controller.enqueue(encoder3.encode(`event: done
414
+ data: {}
415
+
416
+ `));
417
+ controller.close();
418
+ }
419
+ });
420
+ }
421
+ const encoder2 = new TextEncoder;
422
+ let remaining = queries.length;
423
+ return new ReadableStream({
424
+ start(controller) {
425
+ let closed = false;
426
+ function safeEnqueue(chunk) {
427
+ if (closed)
428
+ return;
429
+ try {
430
+ controller.enqueue(chunk);
431
+ } catch {
432
+ closed = true;
433
+ }
434
+ }
435
+ function safeClose() {
436
+ if (closed)
437
+ return;
438
+ closed = true;
439
+ try {
440
+ controller.close();
441
+ } catch {}
442
+ }
443
+ function checkDone() {
444
+ if (remaining === 0) {
445
+ safeEnqueue(encoder2.encode(`event: done
446
+ data: {}
447
+
448
+ `));
449
+ safeClose();
450
+ }
451
+ }
452
+ for (const { promise, resolve, key } of queries) {
453
+ let settled = false;
454
+ promise.then((data) => {
455
+ if (settled)
456
+ return;
457
+ settled = true;
458
+ resolve(data);
459
+ const entry = { key, data: JSON.parse(JSON.stringify(data)) };
460
+ safeEnqueue(encoder2.encode(`event: data
461
+ data: ${safeSerialize(entry)}
462
+
463
+ `));
464
+ remaining--;
465
+ checkDone();
466
+ }, () => {
467
+ if (settled)
468
+ return;
469
+ settled = true;
470
+ remaining--;
471
+ checkDone();
472
+ });
473
+ setTimeout(() => {
474
+ if (settled)
475
+ return;
476
+ settled = true;
477
+ remaining--;
478
+ checkDone();
479
+ }, navTimeout);
480
+ }
481
+ }
482
+ });
483
+ }
484
+
485
+ // src/ssr-handler.ts
486
+ function injectIntoTemplate(template, appHtml, appCss, ssrData, nonce) {
487
+ let html;
488
+ if (template.includes("<!--ssr-outlet-->")) {
489
+ html = template.replace("<!--ssr-outlet-->", appHtml);
490
+ } else {
491
+ html = template.replace(/(<div[^>]*id="app"[^>]*>)([\s\S]*?)(<\/div>)/, `$1${appHtml}$3`);
492
+ }
493
+ if (appCss) {
494
+ html = html.replace("</head>", `${appCss}
495
+ </head>`);
496
+ }
497
+ if (ssrData.length > 0) {
498
+ const nonceAttr = nonce != null ? ` nonce="${nonce}"` : "";
499
+ const ssrDataScript = `<script${nonceAttr}>window.__VERTZ_SSR_DATA__=${safeSerialize(ssrData)};</script>`;
500
+ html = html.replace("</body>", `${ssrDataScript}
501
+ </body>`);
502
+ }
503
+ return html;
504
+ }
505
+ function createSSRHandler(options) {
506
+ const { module, ssrTimeout, inlineCSS, nonce } = options;
507
+ let template = options.template;
508
+ if (inlineCSS) {
509
+ for (const [href, css] of Object.entries(inlineCSS)) {
510
+ const escapedHref = href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
511
+ const linkPattern = new RegExp(`<link[^>]*href=["']${escapedHref}["'][^>]*>`);
512
+ const safeCss = css.replace(/<\//g, "<\\/");
513
+ template = template.replace(linkPattern, `<style data-vertz-css>${safeCss}</style>`);
514
+ }
515
+ }
516
+ return async (request) => {
517
+ const url = new URL(request.url);
518
+ const pathname = url.pathname;
519
+ if (request.headers.get("x-vertz-nav") === "1") {
520
+ return handleNavRequest(module, pathname, ssrTimeout);
521
+ }
522
+ return handleHTMLRequest(module, template, pathname, ssrTimeout, nonce);
523
+ };
524
+ }
525
+ async function handleNavRequest(module, url, ssrTimeout) {
526
+ try {
527
+ const stream = await ssrStreamNavQueries(module, url, { ssrTimeout });
528
+ return new Response(stream, {
529
+ status: 200,
530
+ headers: {
531
+ "Content-Type": "text/event-stream",
532
+ "Cache-Control": "no-cache"
533
+ }
534
+ });
535
+ } catch {
536
+ return new Response(`event: done
537
+ data: {}
538
+
539
+ `, {
540
+ status: 200,
541
+ headers: {
542
+ "Content-Type": "text/event-stream",
543
+ "Cache-Control": "no-cache"
544
+ }
545
+ });
546
+ }
547
+ }
548
+ async function handleHTMLRequest(module, template, url, ssrTimeout, nonce) {
549
+ try {
550
+ const result = await ssrRenderToString(module, url, { ssrTimeout });
551
+ const html = injectIntoTemplate(template, result.html, result.css, result.ssrData, nonce);
552
+ return new Response(html, {
553
+ status: 200,
554
+ headers: { "Content-Type": "text/html; charset=utf-8" }
555
+ });
556
+ } catch {
557
+ return new Response("Internal Server Error", {
558
+ status: 500,
559
+ headers: { "Content-Type": "text/plain" }
560
+ });
561
+ }
562
+ }
563
+
564
+ export { escapeHtml, escapeAttr, serializeToHtml, resetSlotCounter, createSlotPlaceholder, encodeChunk, streamToString, collectStreamChunks, createTemplateChunk, renderToStream, ssrStorage, isInSSR, getSSRUrl, registerSSRQuery, getSSRQueries, setGlobalSSRTimeout, clearGlobalSSRTimeout, getGlobalSSRTimeout, safeSerialize, getStreamingRuntimeScript, createSSRDataChunk, ssrRenderToString, ssrDiscoverQueries, createSSRHandler };