@sigx/server-renderer 0.1.5 → 0.1.7
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/builtin-ssr-directives.d.ts +8 -0
- package/dist/builtin-ssr-directives.d.ts.map +1 -0
- package/dist/client/hydrate-component.d.ts +32 -0
- package/dist/client/hydrate-component.d.ts.map +1 -0
- package/dist/client/hydrate-context.d.ts +54 -0
- package/dist/client/hydrate-context.d.ts.map +1 -0
- package/dist/client/hydrate-core.d.ts +33 -0
- package/dist/client/hydrate-core.d.ts.map +1 -0
- package/dist/client/index.d.ts +9 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -661
- package/dist/client/plugin.d.ts +52 -0
- package/dist/client/plugin.d.ts.map +1 -0
- package/dist/client-directives.d.ts +4 -37
- package/dist/client-directives.d.ts.map +1 -1
- package/dist/client-ggDL-Wx2.js +309 -0
- package/dist/client-ggDL-Wx2.js.map +1 -0
- package/dist/directive-ssr-types.d.ts +23 -0
- package/dist/directive-ssr-types.d.ts.map +1 -0
- package/dist/head.d.ts +97 -0
- package/dist/head.d.ts.map +1 -0
- package/dist/index.d.ts +26 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1170
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +124 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/server/context.d.ts +52 -59
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/index.d.ts +9 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -574
- package/dist/server/render-api.d.ts +64 -0
- package/dist/server/render-api.d.ts.map +1 -0
- package/dist/server/render-core.d.ts +46 -0
- package/dist/server/render-core.d.ts.map +1 -0
- package/dist/server/streaming.d.ts +24 -0
- package/dist/server/streaming.d.ts.map +1 -0
- package/dist/server/types.d.ts +40 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server-UBcHtkm-.js +829 -0
- package/dist/server-UBcHtkm-.js.map +1 -0
- package/dist/ssr.d.ts +38 -0
- package/dist/ssr.d.ts.map +1 -0
- package/dist/types-B4Rf1Xot.js +6 -0
- package/dist/types-B4Rf1Xot.js.map +1 -0
- package/package.json +9 -15
- package/dist/client/hydrate.d.ts +0 -19
- package/dist/client/hydrate.d.ts.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/registry.d.ts +0 -46
- package/dist/client/registry.d.ts.map +0 -1
- package/dist/client/types.d.ts +0 -43
- package/dist/client/types.d.ts.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/server/stream.d.ts +0 -34
- package/dist/server/stream.d.ts.map +0 -1
- package/src/jsx.d.ts +0 -62
package/dist/server/index.js
CHANGED
|
@@ -1,574 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Create a new SSR context for rendering
|
|
6
|
-
*/
|
|
7
|
-
function createSSRContext(options = {}) {
|
|
8
|
-
let componentId = 0;
|
|
9
|
-
const componentStack = [];
|
|
10
|
-
const islands = /* @__PURE__ */ new Map();
|
|
11
|
-
const head = [];
|
|
12
|
-
const pendingAsync = [];
|
|
13
|
-
return {
|
|
14
|
-
_componentId: componentId,
|
|
15
|
-
_componentStack: componentStack,
|
|
16
|
-
_islands: islands,
|
|
17
|
-
_head: head,
|
|
18
|
-
_pendingAsync: pendingAsync,
|
|
19
|
-
nextId() {
|
|
20
|
-
return ++componentId;
|
|
21
|
-
},
|
|
22
|
-
pushComponent(id) {
|
|
23
|
-
componentStack.push(id);
|
|
24
|
-
},
|
|
25
|
-
popComponent() {
|
|
26
|
-
return componentStack.pop();
|
|
27
|
-
},
|
|
28
|
-
registerIsland(id, info) {
|
|
29
|
-
islands.set(id, info);
|
|
30
|
-
},
|
|
31
|
-
getIslands() {
|
|
32
|
-
return islands;
|
|
33
|
-
},
|
|
34
|
-
addHead(html) {
|
|
35
|
-
head.push(html);
|
|
36
|
-
},
|
|
37
|
-
getHead() {
|
|
38
|
-
return head.join("\n");
|
|
39
|
-
},
|
|
40
|
-
addPendingAsync(pending) {
|
|
41
|
-
pendingAsync.push(pending);
|
|
42
|
-
},
|
|
43
|
-
getPendingAsync() {
|
|
44
|
-
return pendingAsync;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/server/stream.ts
|
|
51
|
-
/**
|
|
52
|
-
* Streaming SSR renderer with hydration markers
|
|
53
|
-
*/
|
|
54
|
-
/**
|
|
55
|
-
* Creates a tracking signal function that records signal names and values.
|
|
56
|
-
* Used during async setup to capture state for client hydration.
|
|
57
|
-
*/
|
|
58
|
-
function createTrackingSignal(signalMap) {
|
|
59
|
-
let signalIndex = 0;
|
|
60
|
-
return function trackingSignal(initial, name) {
|
|
61
|
-
const key = name ?? `$${signalIndex++}`;
|
|
62
|
-
const sig = signal(initial);
|
|
63
|
-
signalMap.set(key, initial);
|
|
64
|
-
return new Proxy(sig, {
|
|
65
|
-
get(target, prop) {
|
|
66
|
-
if (prop === "value") return target.value;
|
|
67
|
-
return target[prop];
|
|
68
|
-
},
|
|
69
|
-
set(target, prop, newValue) {
|
|
70
|
-
if (prop === "value") {
|
|
71
|
-
target.value = newValue;
|
|
72
|
-
signalMap.set(key, newValue);
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
target[prop] = newValue;
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Serialize captured signal state for client hydration
|
|
83
|
-
*/
|
|
84
|
-
function serializeSignalState(signalMap) {
|
|
85
|
-
if (signalMap.size === 0) return void 0;
|
|
86
|
-
const state = {};
|
|
87
|
-
for (const [key, value] of signalMap) try {
|
|
88
|
-
JSON.stringify(value);
|
|
89
|
-
state[key] = value;
|
|
90
|
-
} catch {
|
|
91
|
-
console.warn(`SSR: Signal "${key}" has non-serializable value, skipping`);
|
|
92
|
-
}
|
|
93
|
-
return Object.keys(state).length > 0 ? state : void 0;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Check if a vnode type is a component (has __setup)
|
|
97
|
-
*/
|
|
98
|
-
function isComponent(type) {
|
|
99
|
-
return typeof type === "function" && "__setup" in type;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Creates a simple props accessor for SSR (no reactivity needed)
|
|
103
|
-
*/
|
|
104
|
-
function createSSRPropsAccessor(rawProps) {
|
|
105
|
-
let defaults = {};
|
|
106
|
-
const proxy = new Proxy(function propsAccessor() {}, {
|
|
107
|
-
get(_, key) {
|
|
108
|
-
if (typeof key === "symbol") return void 0;
|
|
109
|
-
const value = rawProps[key];
|
|
110
|
-
return value != null ? value : defaults[key];
|
|
111
|
-
},
|
|
112
|
-
apply(_, __, args) {
|
|
113
|
-
if (args[0] && typeof args[0] === "object") defaults = {
|
|
114
|
-
...defaults,
|
|
115
|
-
...args[0]
|
|
116
|
-
};
|
|
117
|
-
return proxy;
|
|
118
|
-
},
|
|
119
|
-
has(_, key) {
|
|
120
|
-
if (typeof key === "symbol") return false;
|
|
121
|
-
return key in rawProps || key in defaults;
|
|
122
|
-
},
|
|
123
|
-
ownKeys() {
|
|
124
|
-
return [...new Set([...Object.keys(rawProps), ...Object.keys(defaults)])];
|
|
125
|
-
},
|
|
126
|
-
getOwnPropertyDescriptor(_, key) {
|
|
127
|
-
if (typeof key === "symbol") return void 0;
|
|
128
|
-
if (key in rawProps || key in defaults) return {
|
|
129
|
-
enumerable: true,
|
|
130
|
-
configurable: true,
|
|
131
|
-
writable: false
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return proxy;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Detect hydration directive from props
|
|
139
|
-
*/
|
|
140
|
-
function getHydrationDirective(props) {
|
|
141
|
-
if (props["client:load"] !== void 0) return { strategy: "load" };
|
|
142
|
-
if (props["client:idle"] !== void 0) return { strategy: "idle" };
|
|
143
|
-
if (props["client:visible"] !== void 0) return { strategy: "visible" };
|
|
144
|
-
if (props["client:only"] !== void 0) return { strategy: "only" };
|
|
145
|
-
if (props["client:media"] !== void 0) return {
|
|
146
|
-
strategy: "media",
|
|
147
|
-
media: props["client:media"]
|
|
148
|
-
};
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Filter out client directives from props
|
|
153
|
-
*/
|
|
154
|
-
function filterClientDirectives(props) {
|
|
155
|
-
const filtered = {};
|
|
156
|
-
for (const key in props) if (!key.startsWith("client:")) filtered[key] = props[key];
|
|
157
|
-
return filtered;
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Serialize props for client hydration (must be JSON-safe)
|
|
161
|
-
*/
|
|
162
|
-
function serializeProps(props) {
|
|
163
|
-
const serialized = {};
|
|
164
|
-
for (const key in props) {
|
|
165
|
-
const value = props[key];
|
|
166
|
-
if (typeof value === "function" || typeof value === "symbol") continue;
|
|
167
|
-
if (key === "children" || key === "key" || key === "ref" || key === "slots") continue;
|
|
168
|
-
if (key.startsWith("on")) continue;
|
|
169
|
-
try {
|
|
170
|
-
JSON.stringify(value);
|
|
171
|
-
serialized[key] = value;
|
|
172
|
-
} catch {}
|
|
173
|
-
}
|
|
174
|
-
return serialized;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Render element to string chunks (generator for streaming)
|
|
178
|
-
*/
|
|
179
|
-
async function* renderToChunks(element, ctx) {
|
|
180
|
-
if (element == null || element === false || element === true) return;
|
|
181
|
-
if (typeof element === "string" || typeof element === "number") {
|
|
182
|
-
yield escapeHtml(String(element));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const vnode = element;
|
|
186
|
-
if (vnode.type === Text) {
|
|
187
|
-
yield escapeHtml(String(vnode.text));
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (vnode.type === Fragment) {
|
|
191
|
-
for (const child of vnode.children) yield* renderToChunks(child, ctx);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (isComponent(vnode.type)) {
|
|
195
|
-
const setup = vnode.type.__setup;
|
|
196
|
-
const componentName = vnode.type.__name || "Anonymous";
|
|
197
|
-
const allProps = vnode.props || {};
|
|
198
|
-
const hydration = getHydrationDirective(allProps);
|
|
199
|
-
const { children, slots: slotsFromProps, ...propsData } = filterClientDirectives(allProps);
|
|
200
|
-
const id = ctx.nextId();
|
|
201
|
-
ctx.pushComponent(id);
|
|
202
|
-
yield `<!--$c:${id}-->`;
|
|
203
|
-
const signalMap = /* @__PURE__ */ new Map();
|
|
204
|
-
const shouldTrackState = !!hydration;
|
|
205
|
-
if (hydration) {
|
|
206
|
-
const islandInfo = {
|
|
207
|
-
strategy: hydration.strategy,
|
|
208
|
-
media: hydration.media,
|
|
209
|
-
props: serializeProps(propsData),
|
|
210
|
-
componentId: componentName
|
|
211
|
-
};
|
|
212
|
-
ctx.registerIsland(id, islandInfo);
|
|
213
|
-
yield `<!--$island:${hydration.strategy}:${id}${hydration.media ? `:${hydration.media}` : ""}-->`;
|
|
214
|
-
}
|
|
215
|
-
if (hydration?.strategy === "only") {
|
|
216
|
-
yield `<div data-island="${id}"></div>`;
|
|
217
|
-
yield `<!--/$c:${id}-->`;
|
|
218
|
-
ctx.popComponent();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const slots = {
|
|
222
|
-
default: () => children ? Array.isArray(children) ? children : [children] : [],
|
|
223
|
-
...slotsFromProps
|
|
224
|
-
};
|
|
225
|
-
const signalFn = shouldTrackState ? createTrackingSignal(signalMap) : signal;
|
|
226
|
-
const ssrLoads = [];
|
|
227
|
-
const componentCtx = {
|
|
228
|
-
el: null,
|
|
229
|
-
signal: signalFn,
|
|
230
|
-
props: createSSRPropsAccessor(propsData),
|
|
231
|
-
slots,
|
|
232
|
-
emit: () => {},
|
|
233
|
-
parent: null,
|
|
234
|
-
onMount: () => {},
|
|
235
|
-
onCleanup: () => {},
|
|
236
|
-
expose: () => {},
|
|
237
|
-
renderFn: null,
|
|
238
|
-
update: () => {},
|
|
239
|
-
ssr: {
|
|
240
|
-
load(fn) {
|
|
241
|
-
ssrLoads.push(fn());
|
|
242
|
-
},
|
|
243
|
-
isServer: true,
|
|
244
|
-
isHydrating: false
|
|
245
|
-
},
|
|
246
|
-
_signals: shouldTrackState ? signalMap : void 0,
|
|
247
|
-
_ssrLoads: ssrLoads
|
|
248
|
-
};
|
|
249
|
-
const prev = setCurrentInstance(componentCtx);
|
|
250
|
-
try {
|
|
251
|
-
let renderFn = setup(componentCtx);
|
|
252
|
-
if (renderFn && typeof renderFn.then === "function") renderFn = await renderFn;
|
|
253
|
-
if (ssrLoads.length > 0 && hydration) {
|
|
254
|
-
const deferredRender = (async () => {
|
|
255
|
-
await Promise.all(ssrLoads);
|
|
256
|
-
let html = "";
|
|
257
|
-
if (renderFn) {
|
|
258
|
-
const result = renderFn();
|
|
259
|
-
if (result) html = await renderVNodeToString(result, ctx);
|
|
260
|
-
}
|
|
261
|
-
if (signalMap.size > 0) {
|
|
262
|
-
const state = serializeSignalState(signalMap);
|
|
263
|
-
if (state) {
|
|
264
|
-
const islandInfo$1 = ctx.getIslands().get(id);
|
|
265
|
-
if (islandInfo$1) islandInfo$1.state = state;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return html;
|
|
269
|
-
})();
|
|
270
|
-
const islandInfo = ctx.getIslands().get(id);
|
|
271
|
-
ctx.addPendingAsync({
|
|
272
|
-
id,
|
|
273
|
-
promise: deferredRender,
|
|
274
|
-
signalMap,
|
|
275
|
-
islandInfo
|
|
276
|
-
});
|
|
277
|
-
yield `<div data-async-placeholder="${id}" style="display:contents;">`;
|
|
278
|
-
if (renderFn) {
|
|
279
|
-
const result = renderFn();
|
|
280
|
-
if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx);
|
|
281
|
-
else yield* renderToChunks(result, ctx);
|
|
282
|
-
}
|
|
283
|
-
yield `</div>`;
|
|
284
|
-
} else {
|
|
285
|
-
if (ssrLoads.length > 0) await Promise.all(ssrLoads);
|
|
286
|
-
if (shouldTrackState && signalMap.size > 0) {
|
|
287
|
-
const state = serializeSignalState(signalMap);
|
|
288
|
-
if (state) {
|
|
289
|
-
const islandInfo = ctx.getIslands().get(id);
|
|
290
|
-
if (islandInfo) islandInfo.state = state;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (renderFn) {
|
|
294
|
-
const result = renderFn();
|
|
295
|
-
if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx);
|
|
296
|
-
else yield* renderToChunks(result, ctx);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} catch (e) {
|
|
300
|
-
console.error(`Error rendering component ${componentName}:`, e);
|
|
301
|
-
} finally {
|
|
302
|
-
setCurrentInstance(prev || null);
|
|
303
|
-
}
|
|
304
|
-
yield `<!--/$c:${id}-->`;
|
|
305
|
-
ctx.popComponent();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (typeof vnode.type === "string") {
|
|
309
|
-
const tagName = vnode.type;
|
|
310
|
-
let props = "";
|
|
311
|
-
for (const key in vnode.props) {
|
|
312
|
-
const value = vnode.props[key];
|
|
313
|
-
if (key === "children" || key === "key" || key === "ref") continue;
|
|
314
|
-
if (key.startsWith("client:")) continue;
|
|
315
|
-
if (key === "style") {
|
|
316
|
-
const styleString = typeof value === "object" ? Object.entries(value).map(([k, v]) => `${camelToKebab(k)}:${v}`).join(";") : String(value);
|
|
317
|
-
props += ` style="${escapeHtml(styleString)}"`;
|
|
318
|
-
} else if (key === "className") props += ` class="${escapeHtml(String(value))}"`;
|
|
319
|
-
else if (key.startsWith("on")) {} else if (value === true) props += ` ${key}`;
|
|
320
|
-
else if (value !== false && value != null) props += ` ${key}="${escapeHtml(String(value))}"`;
|
|
321
|
-
}
|
|
322
|
-
if ([
|
|
323
|
-
"area",
|
|
324
|
-
"base",
|
|
325
|
-
"br",
|
|
326
|
-
"col",
|
|
327
|
-
"embed",
|
|
328
|
-
"hr",
|
|
329
|
-
"img",
|
|
330
|
-
"input",
|
|
331
|
-
"link",
|
|
332
|
-
"meta",
|
|
333
|
-
"param",
|
|
334
|
-
"source",
|
|
335
|
-
"track",
|
|
336
|
-
"wbr"
|
|
337
|
-
].includes(tagName)) {
|
|
338
|
-
yield `<${tagName}${props}>`;
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
yield `<${tagName}${props}>`;
|
|
342
|
-
let prevWasText = false;
|
|
343
|
-
for (const child of vnode.children) {
|
|
344
|
-
const isText = isTextContent(child);
|
|
345
|
-
if (isText && prevWasText) yield "<!--t-->";
|
|
346
|
-
yield* renderToChunks(child, ctx);
|
|
347
|
-
prevWasText = isText;
|
|
348
|
-
}
|
|
349
|
-
yield `</${tagName}>`;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Check if element will render as text content
|
|
354
|
-
*/
|
|
355
|
-
function isTextContent(element) {
|
|
356
|
-
if (element == null || element === false || element === true) return false;
|
|
357
|
-
if (typeof element === "string" || typeof element === "number") return true;
|
|
358
|
-
return element.type === Text;
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Helper to render a VNode to string (for deferred async content)
|
|
362
|
-
*/
|
|
363
|
-
async function renderVNodeToString(element, ctx) {
|
|
364
|
-
let result = "";
|
|
365
|
-
for await (const chunk of renderToChunks(element, ctx)) result += chunk;
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Render JSX element to a ReadableStream with streaming async support
|
|
370
|
-
*/
|
|
371
|
-
function renderToStream(element, context) {
|
|
372
|
-
const ctx = context || createSSRContext();
|
|
373
|
-
return new ReadableStream({ async start(controller) {
|
|
374
|
-
try {
|
|
375
|
-
for await (const chunk of renderToChunks(element, ctx)) controller.enqueue(chunk);
|
|
376
|
-
const pendingAsync = ctx.getPendingAsync();
|
|
377
|
-
if (pendingAsync.length > 0) {
|
|
378
|
-
controller.enqueue(generateStreamingScript());
|
|
379
|
-
await Promise.all(pendingAsync.map(async (pending) => {
|
|
380
|
-
try {
|
|
381
|
-
const html = await pending.promise;
|
|
382
|
-
const state = serializeSignalState(pending.signalMap);
|
|
383
|
-
if (state) pending.islandInfo.state = state;
|
|
384
|
-
controller.enqueue(generateReplacementScript(pending.id, html, state));
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.error(`Error streaming async component ${pending.id}:`, error);
|
|
387
|
-
controller.enqueue(generateReplacementScript(pending.id, `<div style="color:red;">Error loading component</div>`, void 0));
|
|
388
|
-
}
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
if (ctx.getIslands().size > 0) controller.enqueue(generateHydrationScript(ctx));
|
|
392
|
-
controller.enqueue(`<script>window.__SIGX_STREAMING_COMPLETE__ = true; window.dispatchEvent(new Event('sigx:ready'));<\/script>`);
|
|
393
|
-
controller.close();
|
|
394
|
-
} catch (error) {
|
|
395
|
-
controller.error(error);
|
|
396
|
-
}
|
|
397
|
-
} });
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Render with callbacks for fine-grained streaming control.
|
|
401
|
-
* This allows the server to inject scripts between shell and async content.
|
|
402
|
-
*/
|
|
403
|
-
async function renderToStreamWithCallbacks(element, callbacks, context) {
|
|
404
|
-
const ctx = context || createSSRContext();
|
|
405
|
-
try {
|
|
406
|
-
let shellHtml = "";
|
|
407
|
-
for await (const chunk of renderToChunks(element, ctx)) shellHtml += chunk;
|
|
408
|
-
const pendingAsync = ctx.getPendingAsync();
|
|
409
|
-
const syncIslandIds = /* @__PURE__ */ new Set();
|
|
410
|
-
const pendingAsyncIds = new Set(pendingAsync.map((p) => p.id));
|
|
411
|
-
ctx.getIslands().forEach((_, id) => {
|
|
412
|
-
if (!pendingAsyncIds.has(id)) syncIslandIds.add(id);
|
|
413
|
-
});
|
|
414
|
-
if (syncIslandIds.size > 0) shellHtml += generateSyncHydrationScript(ctx, syncIslandIds);
|
|
415
|
-
if (pendingAsync.length > 0) shellHtml += `<script>window.__SIGX_PENDING_ISLANDS__ = {};<\/script>`;
|
|
416
|
-
shellHtml += `<script>window.__SIGX_STREAMING_COMPLETE__ = true; window.dispatchEvent(new Event('sigx:ready'));<\/script>`;
|
|
417
|
-
callbacks.onShellReady(shellHtml);
|
|
418
|
-
if (pendingAsync.length > 0) {
|
|
419
|
-
callbacks.onAsyncChunk(generateStreamingScript());
|
|
420
|
-
await Promise.all(pendingAsync.map(async (pending) => {
|
|
421
|
-
try {
|
|
422
|
-
const html = await pending.promise;
|
|
423
|
-
const state = serializeSignalState(pending.signalMap);
|
|
424
|
-
if (state) pending.islandInfo.state = state;
|
|
425
|
-
callbacks.onAsyncChunk(generateReplacementScriptWithIsland(pending.id, html, pending.islandInfo));
|
|
426
|
-
} catch (error) {
|
|
427
|
-
console.error(`Error streaming async component ${pending.id}:`, error);
|
|
428
|
-
callbacks.onAsyncChunk(generateReplacementScript(pending.id, `<div style="color:red;">Error loading component</div>`, void 0));
|
|
429
|
-
}
|
|
430
|
-
}));
|
|
431
|
-
callbacks.onAsyncChunk(`<script>window.dispatchEvent(new Event('sigx:async-complete'));<\/script>`);
|
|
432
|
-
}
|
|
433
|
-
callbacks.onComplete();
|
|
434
|
-
} catch (error) {
|
|
435
|
-
callbacks.onError(error);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Render JSX element to string (convenience wrapper around stream)
|
|
440
|
-
* For renderToString, we wait for all async components to complete,
|
|
441
|
-
* then include the replacement scripts inline so the final HTML is complete.
|
|
442
|
-
*/
|
|
443
|
-
async function renderToString(element, context) {
|
|
444
|
-
const ctx = context || createSSRContext();
|
|
445
|
-
let result = "";
|
|
446
|
-
for await (const chunk of renderToChunks(element, ctx)) result += chunk;
|
|
447
|
-
const pendingAsync = ctx.getPendingAsync();
|
|
448
|
-
if (pendingAsync.length > 0) {
|
|
449
|
-
result += generateStreamingScript();
|
|
450
|
-
await Promise.all(pendingAsync.map(async (pending) => {
|
|
451
|
-
try {
|
|
452
|
-
const html = await pending.promise;
|
|
453
|
-
const state = serializeSignalState(pending.signalMap);
|
|
454
|
-
if (state) pending.islandInfo.state = state;
|
|
455
|
-
result += generateReplacementScript(pending.id, html, state);
|
|
456
|
-
} catch (error) {
|
|
457
|
-
console.error(`Error rendering async component ${pending.id}:`, error);
|
|
458
|
-
result += generateReplacementScript(pending.id, `<div style="color:red;">Error loading component</div>`, void 0);
|
|
459
|
-
}
|
|
460
|
-
}));
|
|
461
|
-
}
|
|
462
|
-
if (ctx.getIslands().size > 0) result += generateHydrationScript(ctx);
|
|
463
|
-
return result;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Generate the streaming replacement script (injected once before any replacements)
|
|
467
|
-
* This script provides the $SIGX_REPLACE function used by replacement chunks
|
|
468
|
-
*/
|
|
469
|
-
function generateStreamingScript() {
|
|
470
|
-
return `
|
|
471
|
-
<script>
|
|
472
|
-
window.$SIGX_REPLACE = function(id, html, state) {
|
|
473
|
-
var placeholder = document.querySelector('[data-async-placeholder="' + id + '"]');
|
|
474
|
-
if (placeholder) {
|
|
475
|
-
// Create a template to parse the HTML
|
|
476
|
-
var template = document.createElement('template');
|
|
477
|
-
template.innerHTML = html;
|
|
478
|
-
|
|
479
|
-
// Replace placeholder content
|
|
480
|
-
placeholder.innerHTML = '';
|
|
481
|
-
while (template.content.firstChild) {
|
|
482
|
-
placeholder.appendChild(template.content.firstChild);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Update island state in the hydration data
|
|
486
|
-
if (state) {
|
|
487
|
-
var dataScript = document.getElementById('__SIGX_ISLANDS__');
|
|
488
|
-
if (dataScript) {
|
|
489
|
-
try {
|
|
490
|
-
var data = JSON.parse(dataScript.textContent || '{}');
|
|
491
|
-
if (data[id]) {
|
|
492
|
-
data[id].state = state;
|
|
493
|
-
dataScript.textContent = JSON.stringify(data);
|
|
494
|
-
}
|
|
495
|
-
} catch(e) {}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Dispatch event for hydration to pick up
|
|
500
|
-
placeholder.dispatchEvent(new CustomEvent('sigx:async-ready', { bubbles: true, detail: { id: id, state: state } }));
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
<\/script>`;
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Generate a replacement script for a specific async component
|
|
507
|
-
*/
|
|
508
|
-
function generateReplacementScript(id, html, state) {
|
|
509
|
-
return `<script>$SIGX_REPLACE(${id}, ${JSON.stringify(html)}, ${state ? JSON.stringify(state) : "null"});<\/script>`;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Generate a replacement script that also includes island data for async component
|
|
513
|
-
*/
|
|
514
|
-
function generateReplacementScriptWithIsland(id, html, islandInfo) {
|
|
515
|
-
const escapedHtml = JSON.stringify(html);
|
|
516
|
-
return `<script>
|
|
517
|
-
(function() {
|
|
518
|
-
// Add island data to the existing hydration data
|
|
519
|
-
var dataScript = document.getElementById('__SIGX_ISLANDS__');
|
|
520
|
-
if (dataScript) {
|
|
521
|
-
try {
|
|
522
|
-
var data = JSON.parse(dataScript.textContent || '{}');
|
|
523
|
-
data[${id}] = ${JSON.stringify(islandInfo)};
|
|
524
|
-
dataScript.textContent = JSON.stringify(data);
|
|
525
|
-
} catch(e) { console.error('Failed to update island data:', e); }
|
|
526
|
-
}
|
|
527
|
-
// Replace the placeholder content
|
|
528
|
-
$SIGX_REPLACE(${id}, ${escapedHtml}, ${islandInfo.state ? JSON.stringify(islandInfo.state) : "null"});
|
|
529
|
-
})();
|
|
530
|
-
<\/script>`;
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Generate hydration script for sync islands only
|
|
534
|
-
*/
|
|
535
|
-
function generateSyncHydrationScript(ctx, syncIslandIds) {
|
|
536
|
-
const islands = ctx.getIslands();
|
|
537
|
-
const islandData = {};
|
|
538
|
-
islands.forEach((info, id) => {
|
|
539
|
-
if (syncIslandIds.has(id)) islandData[id] = info;
|
|
540
|
-
});
|
|
541
|
-
if (Object.keys(islandData).length === 0) return "";
|
|
542
|
-
return `
|
|
543
|
-
<script type="application/json" id="__SIGX_ISLANDS__">${JSON.stringify(islandData)}<\/script>`;
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Generate the hydration bootstrap script
|
|
547
|
-
*/
|
|
548
|
-
function generateHydrationScript(ctx) {
|
|
549
|
-
const islands = ctx.getIslands();
|
|
550
|
-
if (islands.size === 0) return "";
|
|
551
|
-
const islandData = {};
|
|
552
|
-
islands.forEach((info, id) => {
|
|
553
|
-
islandData[id] = info;
|
|
554
|
-
});
|
|
555
|
-
return `
|
|
556
|
-
<script type="application/json" id="__SIGX_ISLANDS__">${JSON.stringify(islandData)}<\/script>`;
|
|
557
|
-
}
|
|
558
|
-
const ESCAPE = {
|
|
559
|
-
"&": "&",
|
|
560
|
-
"<": "<",
|
|
561
|
-
">": ">",
|
|
562
|
-
"\"": """,
|
|
563
|
-
"'": "'"
|
|
564
|
-
};
|
|
565
|
-
function escapeHtml(s) {
|
|
566
|
-
return s.replace(/[&<>"']/g, (c) => ESCAPE[c]);
|
|
567
|
-
}
|
|
568
|
-
function camelToKebab(str) {
|
|
569
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
//#endregion
|
|
573
|
-
export { createSSRContext, renderToStream, renderToStreamWithCallbacks, renderToString };
|
|
574
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import { d as generateStreamingScript, f as renderVNodeToString, l as escapeJsonForScript, n as renderToStreamWithCallbacks, p as createSSRContext, r as renderToString, t as renderToStream, u as generateReplacementScript } from "../server-UBcHtkm-.js";
|
|
2
|
+
import { t as generateSignalKey } from "../types-B4Rf1Xot.js";
|
|
3
|
+
export { createSSRContext, escapeJsonForScript, generateReplacementScript, generateSignalKey, generateStreamingScript, renderToStream, renderToStreamWithCallbacks, renderToString, renderVNodeToString };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public SSR rendering APIs — convenience wrappers
|
|
3
|
+
*
|
|
4
|
+
* These delegate to `createSSR()` internally so there is exactly one
|
|
5
|
+
* rendering pipeline. When no plugins are registered the plugin hooks
|
|
6
|
+
* are simply no-ops, making these equivalent to calling `createSSR()`
|
|
7
|
+
* directly — but with a simpler call signature for the common case.
|
|
8
|
+
*
|
|
9
|
+
* For plugin-driven rendering (islands, streaming async, etc.),
|
|
10
|
+
* use `createSSR().use(plugin).render()` from `@sigx/server-renderer`.
|
|
11
|
+
*
|
|
12
|
+
* Entry points:
|
|
13
|
+
* - `renderToString()` — full render to a single string
|
|
14
|
+
* - `renderToStream()` — ReadableStream
|
|
15
|
+
* - `renderToStreamWithCallbacks()` — callback-based streaming
|
|
16
|
+
*/
|
|
17
|
+
import type { JSXElement } from 'sigx';
|
|
18
|
+
import type { App } from 'sigx';
|
|
19
|
+
import type { SSRContext } from './context';
|
|
20
|
+
import type { StreamCallbacks } from './types';
|
|
21
|
+
export type { StreamCallbacks } from './types';
|
|
22
|
+
/**
|
|
23
|
+
* Render JSX element or App to a ReadableStream.
|
|
24
|
+
*
|
|
25
|
+
* Internally delegates to `createSSR().renderStream()`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* // Simple usage with JSX
|
|
30
|
+
* renderToStream(<App />)
|
|
31
|
+
*
|
|
32
|
+
* // With App instance for DI/plugins
|
|
33
|
+
* const app = defineApp(<App />).use(router);
|
|
34
|
+
* renderToStream(app)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderToStream(input: JSXElement | App, context?: SSRContext): ReadableStream<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Render with callbacks for fine-grained streaming control.
|
|
40
|
+
*
|
|
41
|
+
* Internally delegates to `createSSR().renderStreamWithCallbacks()`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const app = defineApp(<App />).use(router);
|
|
46
|
+
* await renderToStreamWithCallbacks(app, callbacks)
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderToStreamWithCallbacks(input: JSXElement | App, callbacks: StreamCallbacks, context?: SSRContext): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Render JSX element or App to string.
|
|
52
|
+
*
|
|
53
|
+
* Internally delegates to `createSSR().render()`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* const html = await renderToString(<App />);
|
|
58
|
+
*
|
|
59
|
+
* const app = defineApp(<App />).use(router);
|
|
60
|
+
* const html = await renderToString(app);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function renderToString(input: JSXElement | App, context?: SSRContext): Promise<string>;
|
|
64
|
+
//# sourceMappingURL=render-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-api.d.ts","sourceRoot":"","sources":["../../src/server/render-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,WAAW,CAAC;AAE/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC,CAEpG;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,2BAA2B,CAC7C,KAAK,EAAE,UAAU,GAAG,GAAG,EACvB,SAAS,EAAE,eAAe,EAC1B,OAAO,CAAC,EAAE,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnG"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core rendering logic for SSR
|
|
3
|
+
*
|
|
4
|
+
* The async generator `renderToChunks` walks a VNode tree and yields HTML strings.
|
|
5
|
+
* Handles text, fragments, host elements, and delegates components to the
|
|
6
|
+
* component renderer.
|
|
7
|
+
*
|
|
8
|
+
* This module is strategy-agnostic. Island-specific logic (signal tracking,
|
|
9
|
+
* hydration directives, async streaming) lives in @sigx/ssr-islands and is
|
|
10
|
+
* injected through the SSRPlugin hooks.
|
|
11
|
+
*/
|
|
12
|
+
import { JSXElement, ComponentSetupContext } from 'sigx';
|
|
13
|
+
import type { AppContext } from 'sigx';
|
|
14
|
+
import type { SSRContext } from './context';
|
|
15
|
+
export declare function escapeHtml(s: string): string;
|
|
16
|
+
export declare function camelToKebab(str: string): string;
|
|
17
|
+
export declare function parseStringStyle(cssText: string): Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Serialize a style object to a CSS string.
|
|
20
|
+
*
|
|
21
|
+
* Uses for...in + string concat (avoids Object.entries/map/join allocations)
|
|
22
|
+
* and cached kebab-case conversion.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stringifyStyle(style: Record<string, any>): string;
|
|
25
|
+
/**
|
|
26
|
+
* Render element to string chunks (generator for streaming)
|
|
27
|
+
* @param element - The JSX element to render
|
|
28
|
+
* @param ctx - The SSR context for tracking state
|
|
29
|
+
* @param parentCtx - The parent component context for provide/inject
|
|
30
|
+
* @param appContext - The app context for app-level provides (from defineApp)
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderToChunks(element: JSXElement, ctx: SSRContext, parentCtx?: ComponentSetupContext | null, appContext?: AppContext | null): AsyncGenerator<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Helper to render a VNode to string (for deferred async content)
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderVNodeToString(element: JSXElement, ctx: SSRContext, appContext?: AppContext | null): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Synchronous render-to-string that avoids async generator overhead.
|
|
39
|
+
* Returns null if any async operation is encountered (caller should fall back
|
|
40
|
+
* to the async generator path).
|
|
41
|
+
*
|
|
42
|
+
* For purely synchronous component trees this eliminates thousands of
|
|
43
|
+
* microtask/Promise allocations from the AsyncGenerator protocol.
|
|
44
|
+
*/
|
|
45
|
+
export declare function renderToStringSync(element: JSXElement, ctx: SSRContext, parentCtx: ComponentSetupContext | null, appContext: AppContext | null, buf: string[]): boolean;
|
|
46
|
+
//# sourceMappingURL=render-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-core.d.ts","sourceRoot":"","sources":["../../src/server/render-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAGH,UAAU,EACV,qBAAqB,EASxB,MAAM,MAAM,CAAC;AAGd,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAY5C,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAQD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD;AAiBD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAcxE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CASjE;AA4BD;;;;;;GAMG;AACH,wBAAuB,cAAc,CACjC,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,UAAU,EACf,SAAS,GAAE,qBAAqB,GAAG,IAAW,EAC9C,UAAU,GAAE,UAAU,GAAG,IAAW,GACrC,cAAc,CAAC,MAAM,CAAC,CAiWxB;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAE,UAAU,GAAG,IAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAMrI;AAID;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAC9B,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,qBAAqB,GAAG,IAAI,EACvC,UAAU,EAAE,UAAU,GAAG,IAAI,EAC7B,GAAG,EAAE,MAAM,EAAE,GACd,OAAO,CAmQT"}
|