@sigx/server-renderer 0.2.1 → 0.2.2

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.
@@ -1,1157 +0,0 @@
1
- import { n as internals_exports, r as __commonJSMin } from "./types-DYlI_C8F.js";
2
- import { show } from "@sigx/runtime-dom";
3
- import { Comment, Fragment, Text, getCurrentInstance, isComponent, isDirective, signal } from "sigx";
4
- //#region src/builtin-ssr-directives.ts
5
- /**
6
- * Built-in directive SSR support — lazy patching.
7
- *
8
- * This module patches `getSSRProps` onto built-in directives (like `show`)
9
- * at runtime, keeping `@sigx/runtime-dom` free of SSR knowledge.
10
- *
11
- * Mirrors Vue 3's `initVShowForSSR()` / `initDirectivesForSSR()` pattern.
12
- *
13
- * @internal
14
- */
15
- var _initialized = false;
16
- /**
17
- * Patch `getSSRProps` onto the `show` directive for SSR support.
18
- *
19
- * Called lazily from `initDirectivesForSSR()` — not at import time,
20
- * so tree-shaking can eliminate this in client-only builds.
21
- */
22
- function initShowForSSR() {
23
- show.getSSRProps = ({ value }) => {
24
- if (!value) return { style: { display: "none" } };
25
- };
26
- }
27
- /**
28
- * Initialize SSR support for all built-in directives.
29
- *
30
- * Must be called before any SSR rendering occurs.
31
- * Safe to call multiple times — only patches once.
32
- */
33
- function initDirectivesForSSR() {
34
- if (_initialized) return;
35
- _initialized = true;
36
- initShowForSSR();
37
- }
38
- //#endregion
39
- //#region __vite-browser-external
40
- var require___vite_browser_external = /* @__PURE__ */ __commonJSMin(((exports, module) => {
41
- module.exports = {};
42
- }));
43
- //#endregion
44
- //#region src/server/context.ts
45
- /**
46
- * Create a new SSR context for rendering
47
- */
48
- function createSSRContext(options = {}) {
49
- let componentId = 0;
50
- const componentStack = [];
51
- const head = [];
52
- const pluginData = /* @__PURE__ */ new Map();
53
- return {
54
- _componentId: componentId,
55
- _componentStack: componentStack,
56
- _head: head,
57
- _pluginData: pluginData,
58
- _onComponentError: options.onComponentError,
59
- _streaming: false,
60
- _pendingAsync: [],
61
- nextId() {
62
- return ++componentId;
63
- },
64
- pushComponent(id) {
65
- componentStack.push(id);
66
- },
67
- popComponent() {
68
- return componentStack.pop();
69
- },
70
- addHead(html) {
71
- head.push(html);
72
- },
73
- getHead() {
74
- return head.join("\n");
75
- },
76
- getPluginData(pluginName) {
77
- return pluginData.get(pluginName);
78
- },
79
- setPluginData(pluginName, data) {
80
- pluginData.set(pluginName, data);
81
- }
82
- };
83
- }
84
- //#endregion
85
- //#region src/server/render-core.ts
86
- /**
87
- * Core rendering logic for SSR
88
- *
89
- * The async generator `renderToChunks` walks a VNode tree and yields HTML strings.
90
- * Handles text, fragments, host elements, and delegates components to the
91
- * component renderer.
92
- *
93
- * This module is strategy-agnostic. Island-specific logic (signal tracking,
94
- * hydration directives, async streaming) lives in @sigx/ssr-islands and is
95
- * injected through the SSRPlugin hooks.
96
- */
97
- var ESCAPE = {
98
- "&": "&",
99
- "<": "&lt;",
100
- ">": "&gt;",
101
- "\"": "&quot;",
102
- "'": "&#39;"
103
- };
104
- function escapeHtml$1(s) {
105
- return s.replace(/[&<>"']/g, (c) => ESCAPE[c]);
106
- }
107
- /** Cache for camelCase → kebab-case conversions (same properties repeat across elements) */
108
- var kebabCache = {};
109
- /** Void elements that cannot have children — hoisted to module scope as a Set for O(1) lookup */
110
- var VOID_ELEMENTS = new Set([
111
- "area",
112
- "base",
113
- "br",
114
- "col",
115
- "embed",
116
- "hr",
117
- "img",
118
- "input",
119
- "link",
120
- "meta",
121
- "param",
122
- "source",
123
- "track",
124
- "wbr"
125
- ]);
126
- function camelToKebab(str) {
127
- if (str.startsWith("--")) return str;
128
- return kebabCache[str] ||= str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
129
- }
130
- /**
131
- * Parse a CSS string into a style object.
132
- *
133
- * Handles edge cases: parens in values (e.g., `linear-gradient(...)`),
134
- * CSS comments, and colons in values.
135
- *
136
- * Adapted from Vue 3's `parseStringStyle` — battle-tested, split-based,
137
- * fast in V8.
138
- */
139
- var listDelimiterRE = /;(?![^(]*\))/g;
140
- var propertyDelimiterRE = /:([^]+)/;
141
- var styleCommentRE = /\/\*[^]*?\*\//g;
142
- function parseStringStyle(cssText) {
143
- const ret = {};
144
- cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => {
145
- if (item) {
146
- const tmp = item.split(propertyDelimiterRE);
147
- if (tmp.length > 1) ret[tmp[0].trim()] = tmp[1].trim();
148
- }
149
- });
150
- return ret;
151
- }
152
- /**
153
- * Serialize a style object to a CSS string.
154
- *
155
- * Uses for...in + string concat (avoids Object.entries/map/join allocations)
156
- * and cached kebab-case conversion.
157
- */
158
- function stringifyStyle(style) {
159
- let ret = "";
160
- for (const key in style) {
161
- const value = style[key];
162
- if (value != null && value !== "") ret += `${camelToKebab(key)}:${value};`;
163
- }
164
- return ret;
165
- }
166
- /**
167
- * Check if element will render as text content
168
- */
169
- function isTextContent(element) {
170
- if (element == null || element === false || element === true) return false;
171
- if (typeof element === "string" || typeof element === "number") return true;
172
- return element.type === Text;
173
- }
174
- /** Check if all children are leaf types (text, number, null, bool, Text vnode) */
175
- function allChildrenAreLeaves(children) {
176
- for (const child of children) {
177
- if (child == null || child === false || child === true) continue;
178
- if (typeof child === "string" || typeof child === "number") continue;
179
- if (child.type === Text) continue;
180
- return false;
181
- }
182
- return true;
183
- }
184
- /**
185
- * Merge style values for SSR (element style + directive SSR style).
186
- * Either value can be an object, string, or undefined.
187
- * String styles are parsed into objects before merging.
188
- */
189
- function mergeSSRStyles(elementStyle, directiveStyle) {
190
- if (!elementStyle) return directiveStyle;
191
- if (!directiveStyle) return elementStyle;
192
- const a = typeof elementStyle === "string" ? parseStringStyle(elementStyle) : typeof elementStyle === "object" ? elementStyle : {};
193
- const b = typeof directiveStyle === "string" ? parseStringStyle(directiveStyle) : typeof directiveStyle === "object" ? directiveStyle : {};
194
- return {
195
- ...a,
196
- ...b
197
- };
198
- }
199
- /**
200
- * Render element to string chunks (generator for streaming)
201
- * @param element - The JSX element to render
202
- * @param ctx - The SSR context for tracking state
203
- * @param parentCtx - The parent component context for provide/inject
204
- * @param appContext - The app context for app-level provides (from defineApp)
205
- */
206
- async function* renderToChunks(element, ctx, parentCtx = null, appContext = null) {
207
- if (element == null || element === false || element === true) return;
208
- if (element.type === Comment) {
209
- yield "<!---->";
210
- return;
211
- }
212
- if (typeof element === "string" || typeof element === "number") {
213
- yield escapeHtml$1(String(element));
214
- return;
215
- }
216
- const vnode = element;
217
- if (vnode.type === Text) {
218
- yield escapeHtml$1(String(vnode.text));
219
- return;
220
- }
221
- if (vnode.type === Fragment) {
222
- for (const child of vnode.children) yield* renderToChunks(child, ctx, parentCtx, appContext);
223
- return;
224
- }
225
- if (isComponent(vnode.type)) {
226
- const setup = vnode.type.__setup;
227
- const componentName = vnode.type.__name || "Anonymous";
228
- const { children, slots: slotsFromProps, $models: modelsData, ...propsData } = vnode.props || {};
229
- const id = ctx.nextId();
230
- ctx.pushComponent(id);
231
- const slots = {
232
- default: () => children ? Array.isArray(children) ? children : [children] : [],
233
- ...slotsFromProps
234
- };
235
- const ssrLoads = [];
236
- let componentCtx = {
237
- el: null,
238
- signal,
239
- props: (0, internals_exports.createPropsAccessor)(propsData),
240
- slots,
241
- emit: () => {},
242
- parent: parentCtx,
243
- onMounted: () => {},
244
- onUnmounted: () => {},
245
- onCreated: () => {},
246
- onUpdated: () => {},
247
- expose: () => {},
248
- renderFn: null,
249
- update: () => {},
250
- ssr: {
251
- load(fn) {
252
- ssrLoads.push(fn());
253
- },
254
- isServer: true,
255
- isHydrating: false
256
- },
257
- _ssrLoads: ssrLoads
258
- };
259
- if (ctx._plugins) for (const plugin of ctx._plugins) {
260
- const transformed = plugin.server?.transformComponentContext?.(ctx, vnode, componentCtx);
261
- if (transformed) componentCtx = transformed;
262
- }
263
- if (!parentCtx && appContext) (0, internals_exports.provideAppContext)(componentCtx, appContext);
264
- const prev = (0, internals_exports.setCurrentInstance)(componentCtx);
265
- try {
266
- let renderFn = setup(componentCtx);
267
- if (renderFn && typeof renderFn.then === "function") renderFn = await renderFn;
268
- if (ssrLoads.length > 0) {
269
- let asyncMode = ctx._streaming ? "stream" : "block";
270
- let asyncPlaceholder;
271
- let pluginHandled = false;
272
- if (ctx._plugins) for (const plugin of ctx._plugins) {
273
- const result = plugin.server?.handleAsyncSetup?.(id, ssrLoads, renderFn, ctx);
274
- if (result) {
275
- asyncMode = result.mode;
276
- asyncPlaceholder = result.placeholder;
277
- pluginHandled = true;
278
- break;
279
- }
280
- }
281
- if (asyncMode === "stream") {
282
- yield asyncPlaceholder || `<div data-async-placeholder="${id}" style="display:contents;">`;
283
- if (renderFn) {
284
- const result = renderFn();
285
- if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx, componentCtx, appContext);
286
- else yield* renderToChunks(result, ctx, componentCtx, appContext);
287
- }
288
- yield `</div>`;
289
- if (!pluginHandled) {
290
- const capturedRenderFn = renderFn;
291
- const capturedCtx = ctx;
292
- const capturedAppContext = appContext;
293
- const deferredRender = (async () => {
294
- await Promise.all(ssrLoads);
295
- let html = "";
296
- if (capturedRenderFn) {
297
- const result = capturedRenderFn();
298
- if (result) html = await renderVNodeToString(result, capturedCtx, capturedAppContext);
299
- }
300
- return html;
301
- })();
302
- ctx._pendingAsync.push({
303
- id,
304
- promise: deferredRender
305
- });
306
- }
307
- } else if (asyncMode === "skip") {} else {
308
- await Promise.all(ssrLoads);
309
- if (renderFn) {
310
- const result = renderFn();
311
- if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx, componentCtx, appContext);
312
- else yield* renderToChunks(result, ctx, componentCtx, appContext);
313
- }
314
- }
315
- } else if (renderFn) {
316
- const result = renderFn();
317
- if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx, componentCtx, appContext);
318
- else yield* renderToChunks(result, ctx, componentCtx, appContext);
319
- }
320
- } catch (e) {
321
- const error = e instanceof Error ? e : new Error(String(e));
322
- let fallbackHtml = null;
323
- if (ctx._onComponentError) fallbackHtml = ctx._onComponentError(error, componentName, id);
324
- if (fallbackHtml === null || fallbackHtml === void 0) fallbackHtml = `<!--ssr-error:${id}-->`;
325
- if (fallbackHtml) yield fallbackHtml;
326
- if (process.env.NODE_ENV !== "production") console.error(`Error rendering component ${componentName}:`, e);
327
- } finally {
328
- (0, internals_exports.setCurrentInstance)(prev || null);
329
- }
330
- if (ctx._plugins) for (const plugin of ctx._plugins) {
331
- const transformed = plugin.server?.afterRenderComponent?.(id, vnode, "", ctx);
332
- if (transformed) yield transformed;
333
- }
334
- yield `<!--$c:${id}-->`;
335
- ctx.popComponent();
336
- return;
337
- }
338
- if (typeof vnode.type === "string") {
339
- const tagName = vnode.type;
340
- let props = "";
341
- let directiveSSRProps = null;
342
- if (vnode.props) {
343
- for (const key in vnode.props) if (key.startsWith("use:")) {
344
- const propValue = vnode.props[key];
345
- let def;
346
- let value;
347
- if (isDirective(propValue)) {
348
- def = propValue;
349
- value = void 0;
350
- } else if (Array.isArray(propValue) && propValue.length >= 1 && isDirective(propValue[0])) {
351
- def = propValue[0];
352
- value = propValue[1];
353
- } else {
354
- const builtIn = (0, internals_exports.resolveBuiltInDirective)(key.slice(4));
355
- if (builtIn) {
356
- def = builtIn;
357
- value = propValue;
358
- } else {
359
- const custom = appContext?.directives.get(key.slice(4));
360
- if (custom) {
361
- def = custom;
362
- value = propValue;
363
- }
364
- }
365
- }
366
- if (def?.getSSRProps) {
367
- const ssrProps = def.getSSRProps({ value });
368
- if (ssrProps) {
369
- if (!directiveSSRProps) directiveSSRProps = {};
370
- for (const k in ssrProps) if (k === "style" && directiveSSRProps.style) directiveSSRProps.style = {
371
- ...directiveSSRProps.style,
372
- ...ssrProps.style
373
- };
374
- else if (k === "class" && directiveSSRProps.class) directiveSSRProps.class = directiveSSRProps.class + " " + ssrProps.class;
375
- else directiveSSRProps[k] = ssrProps[k];
376
- }
377
- }
378
- }
379
- }
380
- const allProps = directiveSSRProps ? {
381
- ...vnode.props,
382
- ...directiveSSRProps,
383
- style: mergeSSRStyles(vnode.props?.style, directiveSSRProps?.style)
384
- } : vnode.props;
385
- for (const key in allProps) {
386
- const value = allProps[key];
387
- if (key === "children" || key === "key" || key === "ref") continue;
388
- if (key.startsWith("client:")) continue;
389
- if (key.startsWith("use:")) continue;
390
- if (key === "style") {
391
- const styleString = typeof value === "object" ? stringifyStyle(value) : String(value);
392
- props += ` style="${escapeHtml$1(styleString)}"`;
393
- } else if (key === "className") props += ` class="${escapeHtml$1(String(value))}"`;
394
- else if (key.startsWith("on")) {} else if (value === true) props += ` ${key}`;
395
- else if (value !== false && value != null) props += ` ${key}="${escapeHtml$1(String(value))}"`;
396
- }
397
- if (VOID_ELEMENTS.has(tagName)) {
398
- yield `<${tagName}${props}>`;
399
- return;
400
- }
401
- if (vnode.children.length > 0 && allChildrenAreLeaves(vnode.children)) {
402
- let html = `<${tagName}${props}>`;
403
- let prevWasText = false;
404
- for (const child of vnode.children) {
405
- const isText = isTextContent(child);
406
- if (isText && prevWasText) html += "<!--t-->";
407
- if (child != null && child !== false && child !== true) {
408
- const cv = child;
409
- html += escapeHtml$1(String(cv.type === Text ? cv.text : child));
410
- }
411
- prevWasText = isText;
412
- }
413
- html += `</${tagName}>`;
414
- yield html;
415
- return;
416
- }
417
- yield `<${tagName}${props}>`;
418
- let prevWasText = false;
419
- for (const child of vnode.children) {
420
- const isText = isTextContent(child);
421
- if (isText && prevWasText) yield "<!--t-->";
422
- yield* renderToChunks(child, ctx, parentCtx, appContext);
423
- prevWasText = isText;
424
- }
425
- yield `</${tagName}>`;
426
- }
427
- }
428
- /**
429
- * Helper to render a VNode to string (for deferred async content)
430
- */
431
- async function renderVNodeToString(element, ctx, appContext = null) {
432
- let result = "";
433
- for await (const chunk of renderToChunks(element, ctx, null, appContext)) result += chunk;
434
- return result;
435
- }
436
- /**
437
- * Synchronous render-to-string that avoids async generator overhead.
438
- * Returns null if any async operation is encountered (caller should fall back
439
- * to the async generator path).
440
- *
441
- * For purely synchronous component trees this eliminates thousands of
442
- * microtask/Promise allocations from the AsyncGenerator protocol.
443
- */
444
- function renderToStringSync(element, ctx, parentCtx, appContext, buf) {
445
- if (element == null || element === false || element === true) return true;
446
- if (element.type === Comment) {
447
- buf.push("<!---->");
448
- return true;
449
- }
450
- if (typeof element === "string" || typeof element === "number") {
451
- buf.push(escapeHtml$1(String(element)));
452
- return true;
453
- }
454
- const vnode = element;
455
- if (vnode.type === Text) {
456
- buf.push(escapeHtml$1(String(vnode.text)));
457
- return true;
458
- }
459
- if (vnode.type === Fragment) {
460
- for (const child of vnode.children) if (!renderToStringSync(child, ctx, parentCtx, appContext, buf)) return false;
461
- return true;
462
- }
463
- if (isComponent(vnode.type)) {
464
- const setup = vnode.type.__setup;
465
- const componentName = vnode.type.__name || "Anonymous";
466
- const { children, slots: slotsFromProps, $models: modelsData, ...propsData } = vnode.props || {};
467
- const id = ctx.nextId();
468
- ctx.pushComponent(id);
469
- const slots = {
470
- default: () => children ? Array.isArray(children) ? children : [children] : [],
471
- ...slotsFromProps
472
- };
473
- const ssrLoads = [];
474
- let componentCtx = {
475
- el: null,
476
- signal,
477
- props: (0, internals_exports.createPropsAccessor)(propsData),
478
- slots,
479
- emit: () => {},
480
- parent: parentCtx,
481
- onMounted: () => {},
482
- onUnmounted: () => {},
483
- onCreated: () => {},
484
- onUpdated: () => {},
485
- expose: () => {},
486
- renderFn: null,
487
- update: () => {},
488
- ssr: {
489
- load(fn) {
490
- ssrLoads.push(fn());
491
- },
492
- isServer: true,
493
- isHydrating: false
494
- },
495
- _ssrLoads: ssrLoads
496
- };
497
- if (ctx._plugins) for (const plugin of ctx._plugins) {
498
- const transformed = plugin.server?.transformComponentContext?.(ctx, vnode, componentCtx);
499
- if (transformed) componentCtx = transformed;
500
- }
501
- if (!parentCtx && appContext) (0, internals_exports.provideAppContext)(componentCtx, appContext);
502
- const prev = (0, internals_exports.setCurrentInstance)(componentCtx);
503
- try {
504
- let renderFn = setup(componentCtx);
505
- if (renderFn && typeof renderFn.then === "function") {
506
- for (const p of ssrLoads) p.catch(() => {});
507
- (0, internals_exports.setCurrentInstance)(prev || null);
508
- ctx.popComponent();
509
- return false;
510
- }
511
- if (ssrLoads.length > 0) {
512
- for (const p of ssrLoads) p.catch(() => {});
513
- (0, internals_exports.setCurrentInstance)(prev || null);
514
- ctx.popComponent();
515
- return false;
516
- }
517
- if (renderFn) {
518
- const result = renderFn();
519
- if (result) {
520
- if (Array.isArray(result)) {
521
- for (const item of result) if (!renderToStringSync(item, ctx, componentCtx, appContext, buf)) {
522
- (0, internals_exports.setCurrentInstance)(prev || null);
523
- ctx.popComponent();
524
- return false;
525
- }
526
- } else if (!renderToStringSync(result, ctx, componentCtx, appContext, buf)) {
527
- (0, internals_exports.setCurrentInstance)(prev || null);
528
- ctx.popComponent();
529
- return false;
530
- }
531
- }
532
- }
533
- } catch (e) {
534
- const error = e instanceof Error ? e : new Error(String(e));
535
- let fallbackHtml = null;
536
- if (ctx._onComponentError) fallbackHtml = ctx._onComponentError(error, componentName, id);
537
- if (fallbackHtml === null || fallbackHtml === void 0) fallbackHtml = `<!--ssr-error:${id}-->`;
538
- if (fallbackHtml) buf.push(fallbackHtml);
539
- } finally {
540
- (0, internals_exports.setCurrentInstance)(prev || null);
541
- }
542
- if (ctx._plugins) for (const plugin of ctx._plugins) {
543
- const transformed = plugin.server?.afterRenderComponent?.(id, vnode, "", ctx);
544
- if (transformed) buf.push(transformed);
545
- }
546
- buf.push(`<!--$c:${id}-->`);
547
- ctx.popComponent();
548
- return true;
549
- }
550
- if (typeof vnode.type === "string") {
551
- const tagName = vnode.type;
552
- let props = "";
553
- let directiveSSRProps = null;
554
- if (vnode.props) {
555
- for (const key in vnode.props) if (key.startsWith("use:")) {
556
- const propValue = vnode.props[key];
557
- let def;
558
- let value;
559
- if (isDirective(propValue)) {
560
- def = propValue;
561
- value = void 0;
562
- } else if (Array.isArray(propValue) && propValue.length >= 1 && isDirective(propValue[0])) {
563
- def = propValue[0];
564
- value = propValue[1];
565
- } else {
566
- const builtIn = (0, internals_exports.resolveBuiltInDirective)(key.slice(4));
567
- if (builtIn) {
568
- def = builtIn;
569
- value = propValue;
570
- } else {
571
- const custom = appContext?.directives.get(key.slice(4));
572
- if (custom) {
573
- def = custom;
574
- value = propValue;
575
- }
576
- }
577
- }
578
- if (def?.getSSRProps) {
579
- const ssrProps = def.getSSRProps({ value });
580
- if (ssrProps) {
581
- if (!directiveSSRProps) directiveSSRProps = {};
582
- for (const k in ssrProps) if (k === "style" && directiveSSRProps.style) directiveSSRProps.style = {
583
- ...directiveSSRProps.style,
584
- ...ssrProps.style
585
- };
586
- else if (k === "class" && directiveSSRProps.class) directiveSSRProps.class = directiveSSRProps.class + " " + ssrProps.class;
587
- else directiveSSRProps[k] = ssrProps[k];
588
- }
589
- }
590
- }
591
- }
592
- const allProps = directiveSSRProps ? {
593
- ...vnode.props,
594
- ...directiveSSRProps,
595
- style: mergeSSRStyles(vnode.props?.style, directiveSSRProps?.style)
596
- } : vnode.props;
597
- for (const key in allProps) {
598
- const value = allProps[key];
599
- if (key === "children" || key === "key" || key === "ref") continue;
600
- if (key.startsWith("client:")) continue;
601
- if (key.startsWith("use:")) continue;
602
- if (key === "style") {
603
- const styleString = typeof value === "object" ? stringifyStyle(value) : String(value);
604
- props += ` style="${escapeHtml$1(styleString)}"`;
605
- } else if (key === "className") props += ` class="${escapeHtml$1(String(value))}"`;
606
- else if (key.startsWith("on")) {} else if (value === true) props += ` ${key}`;
607
- else if (value !== false && value != null) props += ` ${key}="${escapeHtml$1(String(value))}"`;
608
- }
609
- if (VOID_ELEMENTS.has(tagName)) {
610
- buf.push(`<${tagName}${props}>`);
611
- return true;
612
- }
613
- buf.push(`<${tagName}${props}>`);
614
- let prevWasText = false;
615
- for (const child of vnode.children) {
616
- const isText = isTextContent(child);
617
- if (isText && prevWasText) buf.push("<!--t-->");
618
- if (!renderToStringSync(child, ctx, parentCtx, appContext, buf)) return false;
619
- prevWasText = isText;
620
- }
621
- buf.push(`</${tagName}>`);
622
- return true;
623
- }
624
- return true;
625
- }
626
- //#endregion
627
- //#region src/server/streaming.ts
628
- /**
629
- * Core streaming utilities for async SSR
630
- *
631
- * Provides the client-side `$SIGX_REPLACE` function and replacement script
632
- * generation used by core async streaming. These are strategy-agnostic —
633
- * any async component with `ssr.load()` gets streamed without needing a plugin.
634
- *
635
- * Plugins (e.g., islands) can augment replacements via `onAsyncComponentResolved`.
636
- */
637
- /**
638
- * Escape a JSON string for safe embedding inside <script> tags.
639
- * Prevents XSS by replacing characters that could break out of the script context.
640
- */
641
- function escapeJsonForScript(json) {
642
- return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
643
- }
644
- /**
645
- * Generate the streaming bootstrap script (injected once before any replacements).
646
- * Defines `window.$SIGX_REPLACE` which swaps async placeholders with rendered HTML.
647
- */
648
- function generateStreamingScript() {
649
- return `
650
- <script>
651
- window.$SIGX_REPLACE = function(id, html) {
652
- var placeholder = document.querySelector('[data-async-placeholder="' + id + '"]');
653
- if (placeholder) {
654
- var template = document.createElement('template');
655
- template.innerHTML = html;
656
- placeholder.innerHTML = '';
657
- while (template.content.firstChild) {
658
- placeholder.appendChild(template.content.firstChild);
659
- }
660
- placeholder.dispatchEvent(new CustomEvent('sigx:async-ready', { bubbles: true, detail: { id: id } }));
661
- }
662
- };
663
- <\/script>`;
664
- }
665
- /**
666
- * Generate a replacement script for a resolved async component.
667
- */
668
- function generateReplacementScript(id, html, extraScript) {
669
- let script = `<script>$SIGX_REPLACE(${id}, ${escapeJsonForScript(JSON.stringify(html))});`;
670
- if (extraScript) script += extraScript;
671
- script += `<\/script>`;
672
- return script;
673
- }
674
- //#endregion
675
- //#region src/head.ts
676
- /**
677
- * Head management composable for SSR and client-side.
678
- *
679
- * Provides `useHead()` for managing `<head>` elements (title, meta, link, script)
680
- * from within components. Works during SSR (collects into SSRContext._head) and
681
- * on the client (updates DOM directly).
682
- *
683
- * @example
684
- * ```tsx
685
- * import { useHead } from '@sigx/server-renderer/head';
686
- *
687
- * function MyPage(ctx) {
688
- * useHead({
689
- * title: 'My Page',
690
- * meta: [
691
- * { name: 'description', content: 'A great page' },
692
- * { property: 'og:title', content: 'My Page' }
693
- * ],
694
- * link: [
695
- * { rel: 'canonical', href: 'https://example.com/my-page' }
696
- * ]
697
- * });
698
- *
699
- * return () => <div>Page content</div>;
700
- * }
701
- * ```
702
- */
703
- var _ssrHeadConfigs = [];
704
- var _isSSR = false;
705
- /**
706
- * Enable SSR mode for head management.
707
- * Called by the SSR renderer before rendering starts.
708
- */
709
- function enableSSRHead() {
710
- _isSSR = true;
711
- _ssrHeadConfigs = [];
712
- }
713
- /**
714
- * Disable SSR mode and return collected configs.
715
- */
716
- function collectSSRHead() {
717
- _isSSR = false;
718
- const configs = _ssrHeadConfigs;
719
- _ssrHeadConfigs = [];
720
- return configs;
721
- }
722
- /**
723
- * Render collected head configs to an HTML string.
724
- * Deduplicates meta tags by name/property and uses the last title.
725
- */
726
- function renderHeadToString(configs) {
727
- const parts = [];
728
- const seenMeta = /* @__PURE__ */ new Map();
729
- let finalTitle;
730
- let titleTemplate;
731
- for (const config of configs) {
732
- if (config.titleTemplate) titleTemplate = config.titleTemplate;
733
- if (config.title) finalTitle = config.title;
734
- if (config.meta) for (const meta of config.meta) {
735
- const key = meta.name ? `name:${meta.name}` : meta.property ? `property:${meta.property}` : meta["http-equiv"] ? `http-equiv:${meta["http-equiv"]}` : meta.charset ? "charset" : null;
736
- const tag = `<meta ${Object.entries(meta).filter(([, v]) => v !== void 0).map(([k, v]) => `${escapeAttr(k)}="${escapeAttr(String(v))}"`).join(" ")}>`;
737
- if (key) seenMeta.set(key, tag);
738
- else parts.push(tag);
739
- }
740
- if (config.link) for (const link of config.link) {
741
- const attrs = Object.entries(link).filter(([, v]) => v !== void 0).map(([k, v]) => `${escapeAttr(k)}="${escapeAttr(String(v))}"`).join(" ");
742
- parts.push(`<link ${attrs}>`);
743
- }
744
- if (config.script) for (const script of config.script) {
745
- const { innerHTML, ...rest } = script;
746
- const attrs = Object.entries(rest).filter(([, v]) => v !== void 0 && v !== false).map(([k, v]) => v === true ? escapeAttr(k) : `${escapeAttr(k)}="${escapeAttr(String(v))}"`).join(" ");
747
- if (innerHTML) parts.push(`<script ${attrs}>${innerHTML}<\/script>`);
748
- else parts.push(`<script ${attrs}><\/script>`);
749
- }
750
- }
751
- const result = [];
752
- if (finalTitle) {
753
- const title = titleTemplate ? titleTemplate.replace("%s", finalTitle) : finalTitle;
754
- result.push(`<title>${escapeHtml(title)}</title>`);
755
- }
756
- for (const tag of seenMeta.values()) result.push(tag);
757
- result.push(...parts);
758
- return result.join("\n");
759
- }
760
- var _headToken = 0;
761
- function applyHeadClient(config) {
762
- const managed = [];
763
- const token = ++_headToken;
764
- if (config.title) {
765
- const title = config.titleTemplate ? config.titleTemplate.replace("%s", config.title) : config.title;
766
- document.title = title;
767
- }
768
- if (config.meta) for (const meta of config.meta) {
769
- const selector = meta.name ? `meta[name="${meta.name}"]` : meta.property ? `meta[property="${meta.property}"]` : meta["http-equiv"] ? `meta[http-equiv="${meta["http-equiv"]}"]` : null;
770
- if (selector) {
771
- const existing = document.querySelector(selector);
772
- if (existing) existing.remove();
773
- }
774
- const el = document.createElement("meta");
775
- for (const [k, v] of Object.entries(meta)) if (v !== void 0) el.setAttribute(k, v);
776
- el.setAttribute("data-sigx-head", String(token));
777
- document.head.appendChild(el);
778
- managed.push(el);
779
- }
780
- if (config.link) for (const link of config.link) {
781
- const el = document.createElement("link");
782
- for (const [k, v] of Object.entries(link)) if (v !== void 0) el.setAttribute(k, v);
783
- el.setAttribute("data-sigx-head", String(token));
784
- document.head.appendChild(el);
785
- managed.push(el);
786
- }
787
- if (config.script) for (const script of config.script) {
788
- const { innerHTML, ...rest } = script;
789
- const el = document.createElement("script");
790
- for (const [k, v] of Object.entries(rest)) if (v === true) el.setAttribute(k, "");
791
- else if (v !== void 0 && v !== false) el.setAttribute(k, String(v));
792
- if (innerHTML) el.textContent = innerHTML;
793
- el.setAttribute("data-sigx-head", String(token));
794
- document.head.appendChild(el);
795
- managed.push(el);
796
- }
797
- if (config.htmlAttrs) {
798
- for (const [k, v] of Object.entries(config.htmlAttrs)) if (v !== void 0) document.documentElement.setAttribute(k, v);
799
- }
800
- if (config.bodyAttrs) {
801
- for (const [k, v] of Object.entries(config.bodyAttrs)) if (v !== void 0) document.body.setAttribute(k, v);
802
- }
803
- return () => {
804
- for (const el of managed) el.remove();
805
- };
806
- }
807
- /**
808
- * Manage `<head>` elements from within a component.
809
- *
810
- * During SSR, collects head configs for later rendering with `renderHeadToString()`.
811
- * On the client, updates the DOM directly. Cleans up on component unmount.
812
- *
813
- * @param config - Head configuration (title, meta, link, script, etc.)
814
- */
815
- function useHead(config) {
816
- if (_isSSR) {
817
- _ssrHeadConfigs.push(config);
818
- return;
819
- }
820
- const cleanup = applyHeadClient(config);
821
- const instance = getCurrentInstance();
822
- if (instance) instance.onUnmounted(() => cleanup());
823
- }
824
- function escapeHtml(s) {
825
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
826
- }
827
- function escapeAttr(s) {
828
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
829
- }
830
- //#endregion
831
- //#region src/ssr.ts
832
- var import___vite_browser_external = require___vite_browser_external();
833
- /**
834
- * Check if the input is an App instance (created via defineApp)
835
- */
836
- function isApp(input) {
837
- return input && typeof input === "object" && "_rootComponent" in input && "_context" in input;
838
- }
839
- /**
840
- * Extract the JSX element and optional AppContext from a render input.
841
- */
842
- function extractInput(input) {
843
- if (isApp(input)) return {
844
- element: input._rootComponent,
845
- appContext: input._context
846
- };
847
- return {
848
- element: input,
849
- appContext: null
850
- };
851
- }
852
- /**
853
- * Yield all async streaming chunks — core-managed and plugin-managed — interleaved
854
- * so the fastest component streams first regardless of who manages it.
855
- *
856
- * Core-managed: ctx._pendingAsync (from render-core when no plugin overrides)
857
- * Plugin-managed: plugin.server.getStreamingChunks() async generators
858
- *
859
- * Both are raced together using a unified promise race loop.
860
- */
861
- async function* streamAllAsyncChunks(ctx, plugins) {
862
- const hasCoreAsync = ctx._pendingAsync.length > 0;
863
- const pluginGenerators = [];
864
- for (const plugin of plugins) {
865
- const chunks = plugin.server?.getStreamingChunks?.(ctx);
866
- if (chunks) pluginGenerators.push(chunks);
867
- }
868
- const hasPluginStreaming = pluginGenerators.length > 0;
869
- if (!hasCoreAsync && !hasPluginStreaming) return;
870
- if (hasCoreAsync) yield generateStreamingScript();
871
- const corePromises = ctx._pendingAsync.map((pending, index) => pending.promise.then((html) => {
872
- let finalHtml = html;
873
- let extraScript = "";
874
- for (const plugin of plugins) {
875
- const result = plugin.server?.onAsyncComponentResolved?.(pending.id, finalHtml, ctx);
876
- if (result) {
877
- if (result.html !== void 0) finalHtml = result.html;
878
- if (result.script) extraScript += result.script;
879
- }
880
- }
881
- return {
882
- index,
883
- script: generateReplacementScript(pending.id, finalHtml, extraScript || void 0)
884
- };
885
- }).catch((error) => {
886
- if (process.env.NODE_ENV !== "production") console.error(`Error streaming async component ${pending.id}:`, error);
887
- return {
888
- index,
889
- script: generateReplacementScript(pending.id, `<div style="color:red;">Error loading component</div>`)
890
- };
891
- }));
892
- const totalCore = corePromises.length;
893
- const pumps = pluginGenerators.map((g) => ({
894
- generator: g,
895
- done: false
896
- }));
897
- const activePumps = /* @__PURE__ */ new Map();
898
- function pumpNext(pumpIdx) {
899
- const slotIndex = totalCore + pumpIdx;
900
- return pumps[pumpIdx].generator.next().then(({ value, done }) => {
901
- if (done) {
902
- pumps[pumpIdx].done = true;
903
- activePumps.delete(slotIndex);
904
- return {
905
- index: slotIndex,
906
- script: ""
907
- };
908
- }
909
- const nextP = pumpNext(pumpIdx);
910
- activePumps.set(slotIndex, nextP);
911
- return {
912
- index: slotIndex,
913
- script: value || ""
914
- };
915
- });
916
- }
917
- for (let i = 0; i < pumps.length; i++) {
918
- const slotIndex = totalCore + i;
919
- const p = pumpNext(i);
920
- activePumps.set(slotIndex, p);
921
- }
922
- const resolvedCore = /* @__PURE__ */ new Set();
923
- function getRaceablePromises() {
924
- const promises = [];
925
- for (let i = 0; i < totalCore; i++) if (!resolvedCore.has(i)) promises.push(corePromises[i]);
926
- for (const [, p] of activePumps) promises.push(p);
927
- return promises;
928
- }
929
- while (true) {
930
- const raceable = getRaceablePromises();
931
- if (raceable.length === 0) break;
932
- const winner = await Promise.race(raceable);
933
- if (winner.script) yield winner.script;
934
- if (winner.index < totalCore) resolvedCore.add(winner.index);
935
- }
936
- }
937
- /**
938
- * Create an SSR instance with plugin support.
939
- */
940
- function createSSR() {
941
- const plugins = [];
942
- function makeContext(options) {
943
- const ctx = options && "_componentId" in options ? options : createSSRContext(options);
944
- ctx._plugins = plugins;
945
- for (const plugin of plugins) plugin.server?.setup?.(ctx);
946
- return ctx;
947
- }
948
- return {
949
- use(plugin) {
950
- plugins.push(plugin);
951
- return this;
952
- },
953
- async render(input, options) {
954
- const { element, appContext } = extractInput(input);
955
- enableSSRHead();
956
- let result = "";
957
- let ctx;
958
- const syncCtx = makeContext(options);
959
- const buf = [];
960
- if (renderToStringSync(element, syncCtx, null, appContext, buf)) {
961
- result = buf.join("");
962
- ctx = syncCtx;
963
- } else {
964
- ctx = makeContext(options);
965
- for await (const chunk of renderToChunks(element, ctx, null, appContext)) result += chunk;
966
- }
967
- for (const plugin of plugins) {
968
- const injected = plugin.server?.getInjectedHTML?.(ctx);
969
- if (injected) result += typeof injected === "string" ? injected : await injected;
970
- }
971
- for (const plugin of plugins) {
972
- const chunks = plugin.server?.getStreamingChunks?.(ctx);
973
- if (chunks) for await (const chunk of chunks) result += chunk;
974
- }
975
- const headConfigs = collectSSRHead();
976
- if (headConfigs.length > 0) ctx.addHead(renderHeadToString(headConfigs));
977
- return result;
978
- },
979
- renderStream(input, options) {
980
- const ctx = makeContext(options);
981
- ctx._streaming = true;
982
- const { element, appContext } = extractInput(input);
983
- async function* generateAll() {
984
- enableSSRHead();
985
- let buffer = "";
986
- const FLUSH_THRESHOLD = 4096;
987
- for await (const chunk of renderToChunks(element, ctx, null, appContext)) {
988
- buffer += chunk;
989
- if (buffer.length >= FLUSH_THRESHOLD) {
990
- yield buffer;
991
- buffer = "";
992
- }
993
- }
994
- if (buffer) {
995
- yield buffer;
996
- buffer = "";
997
- }
998
- const headConfigs = collectSSRHead();
999
- if (headConfigs.length > 0) ctx.addHead(renderHeadToString(headConfigs));
1000
- for (const plugin of plugins) {
1001
- const injected = plugin.server?.getInjectedHTML?.(ctx);
1002
- if (injected) {
1003
- const html = typeof injected === "string" ? injected : await injected;
1004
- if (html) yield html;
1005
- }
1006
- }
1007
- for await (const chunk of streamAllAsyncChunks(ctx, plugins)) yield chunk;
1008
- yield `<script>window.__SIGX_STREAMING_COMPLETE__=true;window.dispatchEvent(new Event('sigx:ready'));<\/script>`;
1009
- }
1010
- const generator = generateAll();
1011
- return new ReadableStream({ async pull(controller) {
1012
- try {
1013
- const { value, done } = await generator.next();
1014
- if (done) controller.close();
1015
- else controller.enqueue(value);
1016
- } catch (error) {
1017
- controller.error(error);
1018
- }
1019
- } });
1020
- },
1021
- renderNodeStream(input, options) {
1022
- const ctx = makeContext(options);
1023
- ctx._streaming = true;
1024
- const { element, appContext } = extractInput(input);
1025
- async function* generate() {
1026
- enableSSRHead();
1027
- let buffer = "";
1028
- const FLUSH_THRESHOLD = 4096;
1029
- for await (const chunk of renderToChunks(element, ctx, null, appContext)) {
1030
- buffer += chunk;
1031
- if (buffer.length >= FLUSH_THRESHOLD) {
1032
- yield buffer;
1033
- buffer = "";
1034
- }
1035
- }
1036
- if (buffer) {
1037
- yield buffer;
1038
- buffer = "";
1039
- }
1040
- const headConfigs = collectSSRHead();
1041
- if (headConfigs.length > 0) ctx.addHead(renderHeadToString(headConfigs));
1042
- for (const plugin of plugins) {
1043
- const injected = plugin.server?.getInjectedHTML?.(ctx);
1044
- if (injected) {
1045
- const html = typeof injected === "string" ? injected : await injected;
1046
- if (html) yield html;
1047
- }
1048
- }
1049
- for await (const chunk of streamAllAsyncChunks(ctx, plugins)) yield chunk;
1050
- yield `<script>window.__SIGX_STREAMING_COMPLETE__=true;window.dispatchEvent(new Event('sigx:ready'));<\/script>`;
1051
- }
1052
- return import___vite_browser_external.Readable.from(generate(), { objectMode: true });
1053
- },
1054
- async renderStreamWithCallbacks(input, callbacks, options) {
1055
- const ctx = makeContext(options);
1056
- ctx._streaming = true;
1057
- const { element, appContext } = extractInput(input);
1058
- try {
1059
- enableSSRHead();
1060
- let shellHtml = "";
1061
- for await (const chunk of renderToChunks(element, ctx, null, appContext)) shellHtml += chunk;
1062
- const headConfigs = collectSSRHead();
1063
- if (headConfigs.length > 0) ctx.addHead(renderHeadToString(headConfigs));
1064
- for (const plugin of plugins) {
1065
- const injected = plugin.server?.getInjectedHTML?.(ctx);
1066
- if (injected) shellHtml += typeof injected === "string" ? injected : await injected;
1067
- }
1068
- shellHtml += `<script>window.__SIGX_STREAMING_COMPLETE__=true;window.dispatchEvent(new Event('sigx:ready'));<\/script>`;
1069
- callbacks.onShellReady(shellHtml);
1070
- for await (const chunk of streamAllAsyncChunks(ctx, plugins)) callbacks.onAsyncChunk(chunk);
1071
- callbacks.onComplete();
1072
- } catch (error) {
1073
- callbacks.onError(error);
1074
- }
1075
- },
1076
- createContext(options) {
1077
- return makeContext(options);
1078
- }
1079
- };
1080
- }
1081
- //#endregion
1082
- //#region src/server/render-api.ts
1083
- /** Shared no-plugin instance — created once, reused for all standalone calls. */
1084
- var _defaultSSR = createSSR();
1085
- /**
1086
- * Render JSX element or App to a ReadableStream.
1087
- *
1088
- * Internally delegates to `createSSR().renderStream()`.
1089
- *
1090
- * @example
1091
- * ```tsx
1092
- * // Simple usage with JSX
1093
- * renderToStream(<App />)
1094
- *
1095
- * // With App instance for DI/plugins
1096
- * const app = defineApp(<App />).use(router);
1097
- * renderToStream(app)
1098
- * ```
1099
- */
1100
- function renderToStream(input, context) {
1101
- return _defaultSSR.renderStream(input, context);
1102
- }
1103
- /**
1104
- * Render JSX element or App to a Node.js Readable stream.
1105
- *
1106
- * Faster than `renderToStream()` on Node.js because it bypasses WebStream
1107
- * overhead entirely. Recommended for Express, Fastify, H3, and other
1108
- * Node.js HTTP frameworks.
1109
- *
1110
- * @example
1111
- * ```tsx
1112
- * import { renderToNodeStream } from '@sigx/server-renderer/server';
1113
- *
1114
- * const stream = renderToNodeStream(<App />);
1115
- * stream.pipe(res);
1116
- * ```
1117
- */
1118
- function renderToNodeStream(input, context) {
1119
- return _defaultSSR.renderNodeStream(input, context);
1120
- }
1121
- /**
1122
- * Render with callbacks for fine-grained streaming control.
1123
- *
1124
- * Internally delegates to `createSSR().renderStreamWithCallbacks()`.
1125
- *
1126
- * @example
1127
- * ```tsx
1128
- * const app = defineApp(<App />).use(router);
1129
- * await renderToStreamWithCallbacks(app, callbacks)
1130
- * ```
1131
- */
1132
- async function renderToStreamWithCallbacks(input, callbacks, context) {
1133
- return _defaultSSR.renderStreamWithCallbacks(input, callbacks, context);
1134
- }
1135
- /**
1136
- * Render JSX element or App to string.
1137
- *
1138
- * Internally delegates to `createSSR().render()`.
1139
- *
1140
- * @example
1141
- * ```tsx
1142
- * const html = await renderToString(<App />);
1143
- *
1144
- * const app = defineApp(<App />).use(router);
1145
- * const html = await renderToString(app);
1146
- * ```
1147
- */
1148
- async function renderToString(input, context) {
1149
- return _defaultSSR.render(input, context);
1150
- }
1151
- //#endregion
1152
- //#region src/server/index.ts
1153
- initDirectivesForSSR();
1154
- //#endregion
1155
- export { createSSR as a, renderHeadToString as c, generateReplacementScript as d, generateStreamingScript as f, initDirectivesForSSR as h, renderToString as i, useHead as l, createSSRContext as m, renderToStream as n, collectSSRHead as o, renderVNodeToString as p, renderToStreamWithCallbacks as r, enableSSRHead as s, renderToNodeStream as t, escapeJsonForScript as u };
1156
-
1157
- //# sourceMappingURL=server-CiZxVKV9.js.map