@sigx/server-renderer 0.1.5 → 0.1.6
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/client/hydrate.d.ts +6 -1
- package/dist/client/hydrate.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -2
- 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/registry.d.ts +9 -1
- package/dist/client/registry.d.ts.map +1 -1
- package/dist/client/types.d.ts +1 -21
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client-DiLwBAD-.js +541 -0
- package/dist/client-DiLwBAD-.js.map +1 -0
- package/dist/client-directives.d.ts +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1172
- package/dist/server/index.js +1 -573
- package/dist/server/stream.d.ts +34 -6
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server-BCOJt2Bi.js +459 -0
- package/dist/server-BCOJt2Bi.js.map +1 -0
- package/dist/shared/utils.d.ts +9 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/package.json +7 -8
- package/dist/client/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1172 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//#region src/server/context.ts
|
|
6
|
-
/**
|
|
7
|
-
* Create a new SSR context for rendering
|
|
8
|
-
*/
|
|
9
|
-
function createSSRContext(options = {}) {
|
|
10
|
-
let componentId = 0;
|
|
11
|
-
const componentStack = [];
|
|
12
|
-
const islands = /* @__PURE__ */ new Map();
|
|
13
|
-
const head = [];
|
|
14
|
-
const pendingAsync = [];
|
|
15
|
-
return {
|
|
16
|
-
_componentId: componentId,
|
|
17
|
-
_componentStack: componentStack,
|
|
18
|
-
_islands: islands,
|
|
19
|
-
_head: head,
|
|
20
|
-
_pendingAsync: pendingAsync,
|
|
21
|
-
nextId() {
|
|
22
|
-
return ++componentId;
|
|
23
|
-
},
|
|
24
|
-
pushComponent(id) {
|
|
25
|
-
componentStack.push(id);
|
|
26
|
-
},
|
|
27
|
-
popComponent() {
|
|
28
|
-
return componentStack.pop();
|
|
29
|
-
},
|
|
30
|
-
registerIsland(id, info) {
|
|
31
|
-
islands.set(id, info);
|
|
32
|
-
},
|
|
33
|
-
getIslands() {
|
|
34
|
-
return islands;
|
|
35
|
-
},
|
|
36
|
-
addHead(html) {
|
|
37
|
-
head.push(html);
|
|
38
|
-
},
|
|
39
|
-
getHead() {
|
|
40
|
-
return head.join("\n");
|
|
41
|
-
},
|
|
42
|
-
addPendingAsync(pending) {
|
|
43
|
-
pendingAsync.push(pending);
|
|
44
|
-
},
|
|
45
|
-
getPendingAsync() {
|
|
46
|
-
return pendingAsync;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/server/stream.ts
|
|
53
|
-
/**
|
|
54
|
-
* Streaming SSR renderer with hydration markers
|
|
55
|
-
*/
|
|
56
|
-
/**
|
|
57
|
-
* Creates a tracking signal function that records signal names and values.
|
|
58
|
-
* Used during async setup to capture state for client hydration.
|
|
59
|
-
*/
|
|
60
|
-
function createTrackingSignal(signalMap) {
|
|
61
|
-
let signalIndex = 0;
|
|
62
|
-
return function trackingSignal(initial, name) {
|
|
63
|
-
const key = name ?? `$${signalIndex++}`;
|
|
64
|
-
const sig = signal(initial);
|
|
65
|
-
signalMap.set(key, initial);
|
|
66
|
-
return new Proxy(sig, {
|
|
67
|
-
get(target, prop) {
|
|
68
|
-
if (prop === "value") return target.value;
|
|
69
|
-
return target[prop];
|
|
70
|
-
},
|
|
71
|
-
set(target, prop, newValue) {
|
|
72
|
-
if (prop === "value") {
|
|
73
|
-
target.value = newValue;
|
|
74
|
-
signalMap.set(key, newValue);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
target[prop] = newValue;
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Serialize captured signal state for client hydration
|
|
85
|
-
*/
|
|
86
|
-
function serializeSignalState(signalMap) {
|
|
87
|
-
if (signalMap.size === 0) return void 0;
|
|
88
|
-
const state = {};
|
|
89
|
-
for (const [key, value] of signalMap) try {
|
|
90
|
-
JSON.stringify(value);
|
|
91
|
-
state[key] = value;
|
|
92
|
-
} catch {
|
|
93
|
-
console.warn(`SSR: Signal "${key}" has non-serializable value, skipping`);
|
|
94
|
-
}
|
|
95
|
-
return Object.keys(state).length > 0 ? state : void 0;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Check if a vnode type is a component (has __setup)
|
|
99
|
-
*/
|
|
100
|
-
function isComponent$2(type) {
|
|
101
|
-
return typeof type === "function" && "__setup" in type;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Creates a simple props accessor for SSR (no reactivity needed)
|
|
105
|
-
*/
|
|
106
|
-
function createSSRPropsAccessor(rawProps) {
|
|
107
|
-
let defaults = {};
|
|
108
|
-
const proxy = new Proxy(function propsAccessor() {}, {
|
|
109
|
-
get(_, key) {
|
|
110
|
-
if (typeof key === "symbol") return void 0;
|
|
111
|
-
const value = rawProps[key];
|
|
112
|
-
return value != null ? value : defaults[key];
|
|
113
|
-
},
|
|
114
|
-
apply(_, __, args) {
|
|
115
|
-
if (args[0] && typeof args[0] === "object") defaults = {
|
|
116
|
-
...defaults,
|
|
117
|
-
...args[0]
|
|
118
|
-
};
|
|
119
|
-
return proxy;
|
|
120
|
-
},
|
|
121
|
-
has(_, key) {
|
|
122
|
-
if (typeof key === "symbol") return false;
|
|
123
|
-
return key in rawProps || key in defaults;
|
|
124
|
-
},
|
|
125
|
-
ownKeys() {
|
|
126
|
-
return [...new Set([...Object.keys(rawProps), ...Object.keys(defaults)])];
|
|
127
|
-
},
|
|
128
|
-
getOwnPropertyDescriptor(_, key) {
|
|
129
|
-
if (typeof key === "symbol") return void 0;
|
|
130
|
-
if (key in rawProps || key in defaults) return {
|
|
131
|
-
enumerable: true,
|
|
132
|
-
configurable: true,
|
|
133
|
-
writable: false
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
return proxy;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Detect hydration directive from props
|
|
141
|
-
*/
|
|
142
|
-
function getHydrationDirective(props) {
|
|
143
|
-
if (props["client:load"] !== void 0) return { strategy: "load" };
|
|
144
|
-
if (props["client:idle"] !== void 0) return { strategy: "idle" };
|
|
145
|
-
if (props["client:visible"] !== void 0) return { strategy: "visible" };
|
|
146
|
-
if (props["client:only"] !== void 0) return { strategy: "only" };
|
|
147
|
-
if (props["client:media"] !== void 0) return {
|
|
148
|
-
strategy: "media",
|
|
149
|
-
media: props["client:media"]
|
|
150
|
-
};
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Filter out client directives from props
|
|
155
|
-
*/
|
|
156
|
-
function filterClientDirectives$1(props) {
|
|
157
|
-
const filtered = {};
|
|
158
|
-
for (const key in props) if (!key.startsWith("client:")) filtered[key] = props[key];
|
|
159
|
-
return filtered;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Serialize props for client hydration (must be JSON-safe)
|
|
163
|
-
*/
|
|
164
|
-
function serializeProps(props) {
|
|
165
|
-
const serialized = {};
|
|
166
|
-
for (const key in props) {
|
|
167
|
-
const value = props[key];
|
|
168
|
-
if (typeof value === "function" || typeof value === "symbol") continue;
|
|
169
|
-
if (key === "children" || key === "key" || key === "ref" || key === "slots") continue;
|
|
170
|
-
if (key.startsWith("on")) continue;
|
|
171
|
-
try {
|
|
172
|
-
JSON.stringify(value);
|
|
173
|
-
serialized[key] = value;
|
|
174
|
-
} catch {}
|
|
175
|
-
}
|
|
176
|
-
return serialized;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Render element to string chunks (generator for streaming)
|
|
180
|
-
*/
|
|
181
|
-
async function* renderToChunks(element, ctx) {
|
|
182
|
-
if (element == null || element === false || element === true) return;
|
|
183
|
-
if (typeof element === "string" || typeof element === "number") {
|
|
184
|
-
yield escapeHtml(String(element));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const vnode = element;
|
|
188
|
-
if (vnode.type === Text) {
|
|
189
|
-
yield escapeHtml(String(vnode.text));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (vnode.type === Fragment) {
|
|
193
|
-
for (const child of vnode.children) yield* renderToChunks(child, ctx);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (isComponent$2(vnode.type)) {
|
|
197
|
-
const setup = vnode.type.__setup;
|
|
198
|
-
const componentName = vnode.type.__name || "Anonymous";
|
|
199
|
-
const allProps = vnode.props || {};
|
|
200
|
-
const hydration = getHydrationDirective(allProps);
|
|
201
|
-
const { children, slots: slotsFromProps, ...propsData } = filterClientDirectives$1(allProps);
|
|
202
|
-
const id = ctx.nextId();
|
|
203
|
-
ctx.pushComponent(id);
|
|
204
|
-
yield `<!--$c:${id}-->`;
|
|
205
|
-
const signalMap = /* @__PURE__ */ new Map();
|
|
206
|
-
const shouldTrackState = !!hydration;
|
|
207
|
-
if (hydration) {
|
|
208
|
-
const islandInfo = {
|
|
209
|
-
strategy: hydration.strategy,
|
|
210
|
-
media: hydration.media,
|
|
211
|
-
props: serializeProps(propsData),
|
|
212
|
-
componentId: componentName
|
|
213
|
-
};
|
|
214
|
-
ctx.registerIsland(id, islandInfo);
|
|
215
|
-
yield `<!--$island:${hydration.strategy}:${id}${hydration.media ? `:${hydration.media}` : ""}-->`;
|
|
216
|
-
}
|
|
217
|
-
if (hydration?.strategy === "only") {
|
|
218
|
-
yield `<div data-island="${id}"></div>`;
|
|
219
|
-
yield `<!--/$c:${id}-->`;
|
|
220
|
-
ctx.popComponent();
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const slots = {
|
|
224
|
-
default: () => children ? Array.isArray(children) ? children : [children] : [],
|
|
225
|
-
...slotsFromProps
|
|
226
|
-
};
|
|
227
|
-
const signalFn = shouldTrackState ? createTrackingSignal(signalMap) : signal;
|
|
228
|
-
const ssrLoads = [];
|
|
229
|
-
const componentCtx = {
|
|
230
|
-
el: null,
|
|
231
|
-
signal: signalFn,
|
|
232
|
-
props: createSSRPropsAccessor(propsData),
|
|
233
|
-
slots,
|
|
234
|
-
emit: () => {},
|
|
235
|
-
parent: null,
|
|
236
|
-
onMount: () => {},
|
|
237
|
-
onCleanup: () => {},
|
|
238
|
-
expose: () => {},
|
|
239
|
-
renderFn: null,
|
|
240
|
-
update: () => {},
|
|
241
|
-
ssr: {
|
|
242
|
-
load(fn) {
|
|
243
|
-
ssrLoads.push(fn());
|
|
244
|
-
},
|
|
245
|
-
isServer: true,
|
|
246
|
-
isHydrating: false
|
|
247
|
-
},
|
|
248
|
-
_signals: shouldTrackState ? signalMap : void 0,
|
|
249
|
-
_ssrLoads: ssrLoads
|
|
250
|
-
};
|
|
251
|
-
const prev = setCurrentInstance(componentCtx);
|
|
252
|
-
try {
|
|
253
|
-
let renderFn = setup(componentCtx);
|
|
254
|
-
if (renderFn && typeof renderFn.then === "function") renderFn = await renderFn;
|
|
255
|
-
if (ssrLoads.length > 0 && hydration) {
|
|
256
|
-
const deferredRender = (async () => {
|
|
257
|
-
await Promise.all(ssrLoads);
|
|
258
|
-
let html = "";
|
|
259
|
-
if (renderFn) {
|
|
260
|
-
const result = renderFn();
|
|
261
|
-
if (result) html = await renderVNodeToString(result, ctx);
|
|
262
|
-
}
|
|
263
|
-
if (signalMap.size > 0) {
|
|
264
|
-
const state = serializeSignalState(signalMap);
|
|
265
|
-
if (state) {
|
|
266
|
-
const islandInfo$1 = ctx.getIslands().get(id);
|
|
267
|
-
if (islandInfo$1) islandInfo$1.state = state;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return html;
|
|
271
|
-
})();
|
|
272
|
-
const islandInfo = ctx.getIslands().get(id);
|
|
273
|
-
ctx.addPendingAsync({
|
|
274
|
-
id,
|
|
275
|
-
promise: deferredRender,
|
|
276
|
-
signalMap,
|
|
277
|
-
islandInfo
|
|
278
|
-
});
|
|
279
|
-
yield `<div data-async-placeholder="${id}" style="display:contents;">`;
|
|
280
|
-
if (renderFn) {
|
|
281
|
-
const result = renderFn();
|
|
282
|
-
if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx);
|
|
283
|
-
else yield* renderToChunks(result, ctx);
|
|
284
|
-
}
|
|
285
|
-
yield `</div>`;
|
|
286
|
-
} else {
|
|
287
|
-
if (ssrLoads.length > 0) await Promise.all(ssrLoads);
|
|
288
|
-
if (shouldTrackState && signalMap.size > 0) {
|
|
289
|
-
const state = serializeSignalState(signalMap);
|
|
290
|
-
if (state) {
|
|
291
|
-
const islandInfo = ctx.getIslands().get(id);
|
|
292
|
-
if (islandInfo) islandInfo.state = state;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (renderFn) {
|
|
296
|
-
const result = renderFn();
|
|
297
|
-
if (result) if (Array.isArray(result)) for (const item of result) yield* renderToChunks(item, ctx);
|
|
298
|
-
else yield* renderToChunks(result, ctx);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
} catch (e) {
|
|
302
|
-
console.error(`Error rendering component ${componentName}:`, e);
|
|
303
|
-
} finally {
|
|
304
|
-
setCurrentInstance(prev || null);
|
|
305
|
-
}
|
|
306
|
-
yield `<!--/$c:${id}-->`;
|
|
307
|
-
ctx.popComponent();
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (typeof vnode.type === "string") {
|
|
311
|
-
const tagName = vnode.type;
|
|
312
|
-
let props = "";
|
|
313
|
-
for (const key in vnode.props) {
|
|
314
|
-
const value = vnode.props[key];
|
|
315
|
-
if (key === "children" || key === "key" || key === "ref") continue;
|
|
316
|
-
if (key.startsWith("client:")) continue;
|
|
317
|
-
if (key === "style") {
|
|
318
|
-
const styleString = typeof value === "object" ? Object.entries(value).map(([k, v]) => `${camelToKebab(k)}:${v}`).join(";") : String(value);
|
|
319
|
-
props += ` style="${escapeHtml(styleString)}"`;
|
|
320
|
-
} else if (key === "className") props += ` class="${escapeHtml(String(value))}"`;
|
|
321
|
-
else if (key.startsWith("on")) {} else if (value === true) props += ` ${key}`;
|
|
322
|
-
else if (value !== false && value != null) props += ` ${key}="${escapeHtml(String(value))}"`;
|
|
323
|
-
}
|
|
324
|
-
if ([
|
|
325
|
-
"area",
|
|
326
|
-
"base",
|
|
327
|
-
"br",
|
|
328
|
-
"col",
|
|
329
|
-
"embed",
|
|
330
|
-
"hr",
|
|
331
|
-
"img",
|
|
332
|
-
"input",
|
|
333
|
-
"link",
|
|
334
|
-
"meta",
|
|
335
|
-
"param",
|
|
336
|
-
"source",
|
|
337
|
-
"track",
|
|
338
|
-
"wbr"
|
|
339
|
-
].includes(tagName)) {
|
|
340
|
-
yield `<${tagName}${props}>`;
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
yield `<${tagName}${props}>`;
|
|
344
|
-
let prevWasText = false;
|
|
345
|
-
for (const child of vnode.children) {
|
|
346
|
-
const isText = isTextContent(child);
|
|
347
|
-
if (isText && prevWasText) yield "<!--t-->";
|
|
348
|
-
yield* renderToChunks(child, ctx);
|
|
349
|
-
prevWasText = isText;
|
|
350
|
-
}
|
|
351
|
-
yield `</${tagName}>`;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Check if element will render as text content
|
|
356
|
-
*/
|
|
357
|
-
function isTextContent(element) {
|
|
358
|
-
if (element == null || element === false || element === true) return false;
|
|
359
|
-
if (typeof element === "string" || typeof element === "number") return true;
|
|
360
|
-
return element.type === Text;
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Helper to render a VNode to string (for deferred async content)
|
|
364
|
-
*/
|
|
365
|
-
async function renderVNodeToString(element, ctx) {
|
|
366
|
-
let result = "";
|
|
367
|
-
for await (const chunk of renderToChunks(element, ctx)) result += chunk;
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Render JSX element to a ReadableStream with streaming async support
|
|
372
|
-
*/
|
|
373
|
-
function renderToStream(element, context) {
|
|
374
|
-
const ctx = context || createSSRContext();
|
|
375
|
-
return new ReadableStream({ async start(controller) {
|
|
376
|
-
try {
|
|
377
|
-
for await (const chunk of renderToChunks(element, ctx)) controller.enqueue(chunk);
|
|
378
|
-
const pendingAsync = ctx.getPendingAsync();
|
|
379
|
-
if (pendingAsync.length > 0) {
|
|
380
|
-
controller.enqueue(generateStreamingScript());
|
|
381
|
-
await Promise.all(pendingAsync.map(async (pending) => {
|
|
382
|
-
try {
|
|
383
|
-
const html = await pending.promise;
|
|
384
|
-
const state = serializeSignalState(pending.signalMap);
|
|
385
|
-
if (state) pending.islandInfo.state = state;
|
|
386
|
-
controller.enqueue(generateReplacementScript(pending.id, html, state));
|
|
387
|
-
} catch (error) {
|
|
388
|
-
console.error(`Error streaming async component ${pending.id}:`, error);
|
|
389
|
-
controller.enqueue(generateReplacementScript(pending.id, `<div style="color:red;">Error loading component</div>`, void 0));
|
|
390
|
-
}
|
|
391
|
-
}));
|
|
392
|
-
}
|
|
393
|
-
if (ctx.getIslands().size > 0) controller.enqueue(generateHydrationScript(ctx));
|
|
394
|
-
controller.enqueue(`<script>window.__SIGX_STREAMING_COMPLETE__ = true; window.dispatchEvent(new Event('sigx:ready'));<\/script>`);
|
|
395
|
-
controller.close();
|
|
396
|
-
} catch (error) {
|
|
397
|
-
controller.error(error);
|
|
398
|
-
}
|
|
399
|
-
} });
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Render JSX element to string (convenience wrapper around stream)
|
|
403
|
-
* For renderToString, we wait for all async components to complete,
|
|
404
|
-
* then include the replacement scripts inline so the final HTML is complete.
|
|
405
|
-
*/
|
|
406
|
-
async function renderToString(element, context) {
|
|
407
|
-
const ctx = context || createSSRContext();
|
|
408
|
-
let result = "";
|
|
409
|
-
for await (const chunk of renderToChunks(element, ctx)) result += chunk;
|
|
410
|
-
const pendingAsync = ctx.getPendingAsync();
|
|
411
|
-
if (pendingAsync.length > 0) {
|
|
412
|
-
result += generateStreamingScript();
|
|
413
|
-
await Promise.all(pendingAsync.map(async (pending) => {
|
|
414
|
-
try {
|
|
415
|
-
const html = await pending.promise;
|
|
416
|
-
const state = serializeSignalState(pending.signalMap);
|
|
417
|
-
if (state) pending.islandInfo.state = state;
|
|
418
|
-
result += generateReplacementScript(pending.id, html, state);
|
|
419
|
-
} catch (error) {
|
|
420
|
-
console.error(`Error rendering async component ${pending.id}:`, error);
|
|
421
|
-
result += generateReplacementScript(pending.id, `<div style="color:red;">Error loading component</div>`, void 0);
|
|
422
|
-
}
|
|
423
|
-
}));
|
|
424
|
-
}
|
|
425
|
-
if (ctx.getIslands().size > 0) result += generateHydrationScript(ctx);
|
|
426
|
-
return result;
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Generate the streaming replacement script (injected once before any replacements)
|
|
430
|
-
* This script provides the $SIGX_REPLACE function used by replacement chunks
|
|
431
|
-
*/
|
|
432
|
-
function generateStreamingScript() {
|
|
433
|
-
return `
|
|
434
|
-
<script>
|
|
435
|
-
window.$SIGX_REPLACE = function(id, html, state) {
|
|
436
|
-
var placeholder = document.querySelector('[data-async-placeholder="' + id + '"]');
|
|
437
|
-
if (placeholder) {
|
|
438
|
-
// Create a template to parse the HTML
|
|
439
|
-
var template = document.createElement('template');
|
|
440
|
-
template.innerHTML = html;
|
|
441
|
-
|
|
442
|
-
// Replace placeholder content
|
|
443
|
-
placeholder.innerHTML = '';
|
|
444
|
-
while (template.content.firstChild) {
|
|
445
|
-
placeholder.appendChild(template.content.firstChild);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Update island state in the hydration data
|
|
449
|
-
if (state) {
|
|
450
|
-
var dataScript = document.getElementById('__SIGX_ISLANDS__');
|
|
451
|
-
if (dataScript) {
|
|
452
|
-
try {
|
|
453
|
-
var data = JSON.parse(dataScript.textContent || '{}');
|
|
454
|
-
if (data[id]) {
|
|
455
|
-
data[id].state = state;
|
|
456
|
-
dataScript.textContent = JSON.stringify(data);
|
|
457
|
-
}
|
|
458
|
-
} catch(e) {}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Dispatch event for hydration to pick up
|
|
463
|
-
placeholder.dispatchEvent(new CustomEvent('sigx:async-ready', { bubbles: true, detail: { id: id, state: state } }));
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
<\/script>`;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Generate a replacement script for a specific async component
|
|
470
|
-
*/
|
|
471
|
-
function generateReplacementScript(id, html, state) {
|
|
472
|
-
return `<script>$SIGX_REPLACE(${id}, ${JSON.stringify(html)}, ${state ? JSON.stringify(state) : "null"});<\/script>`;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Generate the hydration bootstrap script
|
|
476
|
-
*/
|
|
477
|
-
function generateHydrationScript(ctx) {
|
|
478
|
-
const islands = ctx.getIslands();
|
|
479
|
-
if (islands.size === 0) return "";
|
|
480
|
-
const islandData = {};
|
|
481
|
-
islands.forEach((info, id) => {
|
|
482
|
-
islandData[id] = info;
|
|
483
|
-
});
|
|
484
|
-
return `
|
|
485
|
-
<script type="application/json" id="__SIGX_ISLANDS__">${JSON.stringify(islandData)}<\/script>`;
|
|
486
|
-
}
|
|
487
|
-
const ESCAPE = {
|
|
488
|
-
"&": "&",
|
|
489
|
-
"<": "<",
|
|
490
|
-
">": ">",
|
|
491
|
-
"\"": """,
|
|
492
|
-
"'": "'"
|
|
493
|
-
};
|
|
494
|
-
function escapeHtml(s) {
|
|
495
|
-
return s.replace(/[&<>"']/g, (c) => ESCAPE[c]);
|
|
496
|
-
}
|
|
497
|
-
function camelToKebab(str) {
|
|
498
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/client/registry.ts
|
|
503
|
-
/**
|
|
504
|
-
* Registry mapping component names to component factories
|
|
505
|
-
*/
|
|
506
|
-
const componentRegistry = /* @__PURE__ */ new Map();
|
|
507
|
-
/**
|
|
508
|
-
* Register a component for island hydration.
|
|
509
|
-
* Components must be registered before hydrateIslands() is called.
|
|
510
|
-
*
|
|
511
|
-
* @example
|
|
512
|
-
* ```ts
|
|
513
|
-
* import { registerComponent } from '@sigx/server-renderer/client';
|
|
514
|
-
* import { Counter } from './components/Counter';
|
|
515
|
-
*
|
|
516
|
-
* registerComponent('Counter', Counter);
|
|
517
|
-
* ```
|
|
518
|
-
*/
|
|
519
|
-
function registerComponent(name, component) {
|
|
520
|
-
componentRegistry.set(name, component);
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Register multiple components at once
|
|
524
|
-
*
|
|
525
|
-
* @example
|
|
526
|
-
* ```ts
|
|
527
|
-
* import { registerComponents } from '@sigx/server-renderer/client';
|
|
528
|
-
* import * as Components from './components';
|
|
529
|
-
*
|
|
530
|
-
* registerComponents(Components);
|
|
531
|
-
* ```
|
|
532
|
-
*/
|
|
533
|
-
function registerComponents(components) {
|
|
534
|
-
for (const [name, component] of Object.entries(components)) if (isComponent$1(component)) registerComponent(name, component);
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Get a registered component by name
|
|
538
|
-
*/
|
|
539
|
-
function getComponent(name) {
|
|
540
|
-
return componentRegistry.get(name);
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Check if a value is a component factory
|
|
544
|
-
*/
|
|
545
|
-
function isComponent$1(value) {
|
|
546
|
-
return typeof value === "function" && "__setup" in value;
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Hydration Registry class for more advanced use cases
|
|
550
|
-
*/
|
|
551
|
-
var HydrationRegistry = class {
|
|
552
|
-
components = /* @__PURE__ */ new Map();
|
|
553
|
-
register(name, component) {
|
|
554
|
-
this.components.set(name, component);
|
|
555
|
-
return this;
|
|
556
|
-
}
|
|
557
|
-
registerAll(components) {
|
|
558
|
-
for (const [name, component] of Object.entries(components)) if (isComponent$1(component)) this.register(name, component);
|
|
559
|
-
return this;
|
|
560
|
-
}
|
|
561
|
-
get(name) {
|
|
562
|
-
return this.components.get(name);
|
|
563
|
-
}
|
|
564
|
-
has(name) {
|
|
565
|
-
return this.components.has(name);
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
//#endregion
|
|
570
|
-
//#region src/client/hydrate.ts
|
|
571
|
-
/**
|
|
572
|
-
* Client-side hydration for SSR'd content
|
|
573
|
-
*
|
|
574
|
-
* Hydration attaches the app to existing server-rendered DOM,
|
|
575
|
-
* then delegates all updates to the runtime-dom renderer.
|
|
576
|
-
*/
|
|
577
|
-
let _pendingServerState = null;
|
|
578
|
-
/**
|
|
579
|
-
* Set server state that should be used for the next component mount.
|
|
580
|
-
* Used internally when mounting async components after streaming.
|
|
581
|
-
*/
|
|
582
|
-
function setPendingServerState(state) {
|
|
583
|
-
_pendingServerState = state;
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Register the SSR context extension for all components.
|
|
587
|
-
* This provides the `ssr` object with a no-op `load()` for client-side rendering.
|
|
588
|
-
* Also handles server state restoration for async streamed components.
|
|
589
|
-
*/
|
|
590
|
-
registerContextExtension((ctx) => {
|
|
591
|
-
const serverState = _pendingServerState;
|
|
592
|
-
if (serverState) {
|
|
593
|
-
ctx._serverState = serverState;
|
|
594
|
-
_pendingServerState = null;
|
|
595
|
-
ctx.signal = createRestoringSignal(serverState);
|
|
596
|
-
ctx.ssr = {
|
|
597
|
-
load: (_fn) => {},
|
|
598
|
-
isServer: false,
|
|
599
|
-
isHydrating: true
|
|
600
|
-
};
|
|
601
|
-
} else if (ctx._serverState) ctx.ssr = {
|
|
602
|
-
load: (_fn) => {},
|
|
603
|
-
isServer: false,
|
|
604
|
-
isHydrating: true
|
|
605
|
-
};
|
|
606
|
-
else ctx.ssr = {
|
|
607
|
-
load: (fn) => {
|
|
608
|
-
fn().catch((err) => console.error("[SSR] load error:", err));
|
|
609
|
-
},
|
|
610
|
-
isServer: false,
|
|
611
|
-
isHydrating: false
|
|
612
|
-
};
|
|
613
|
-
});
|
|
614
|
-
/**
|
|
615
|
-
* Creates a signal function that restores state from server-captured values.
|
|
616
|
-
* Used during hydration of async components to avoid re-fetching data.
|
|
617
|
-
*/
|
|
618
|
-
function createRestoringSignal(serverState) {
|
|
619
|
-
let signalIndex = 0;
|
|
620
|
-
return function restoringSignal(initial, name) {
|
|
621
|
-
const key = name ?? `$${signalIndex++}`;
|
|
622
|
-
if (key in serverState) {
|
|
623
|
-
console.log(`[Hydrate] Restoring signal "${key}" from server state:`, serverState[key]);
|
|
624
|
-
return signal(serverState[key]);
|
|
625
|
-
}
|
|
626
|
-
return signal(initial);
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Hydrate a server-rendered app.
|
|
631
|
-
*
|
|
632
|
-
* This walks the existing DOM to attach event handlers, runs component
|
|
633
|
-
* setup functions to establish reactivity, then uses runtime-dom for updates.
|
|
634
|
-
*/
|
|
635
|
-
function hydrate(element, container) {
|
|
636
|
-
const vnode = normalizeElement(element);
|
|
637
|
-
if (!vnode) return;
|
|
638
|
-
hydrateNode(vnode, container.firstChild, container);
|
|
639
|
-
container._vnode = vnode;
|
|
640
|
-
}
|
|
641
|
-
let _cachedIslandData = null;
|
|
642
|
-
/**
|
|
643
|
-
* Invalidate the island data cache (called when async components stream in)
|
|
644
|
-
*/
|
|
645
|
-
function invalidateIslandCache() {
|
|
646
|
-
_cachedIslandData = null;
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Get island data from the __SIGX_ISLANDS__ script tag
|
|
650
|
-
*/
|
|
651
|
-
function getIslandData() {
|
|
652
|
-
if (_cachedIslandData !== null) return _cachedIslandData;
|
|
653
|
-
const dataScript = document.getElementById("__SIGX_ISLANDS__");
|
|
654
|
-
if (!dataScript) {
|
|
655
|
-
_cachedIslandData = {};
|
|
656
|
-
return _cachedIslandData;
|
|
657
|
-
}
|
|
658
|
-
try {
|
|
659
|
-
_cachedIslandData = JSON.parse(dataScript.textContent || "{}");
|
|
660
|
-
} catch {
|
|
661
|
-
console.error("Failed to parse island data");
|
|
662
|
-
_cachedIslandData = {};
|
|
663
|
-
}
|
|
664
|
-
return _cachedIslandData;
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* Get server state for a specific island/component by ID
|
|
668
|
-
*/
|
|
669
|
-
function getIslandServerState(componentId) {
|
|
670
|
-
return getIslandData()[String(componentId)]?.state;
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Normalize any element to VNode
|
|
674
|
-
*/
|
|
675
|
-
function normalizeElement(element) {
|
|
676
|
-
if (element == null || element === true || element === false) return null;
|
|
677
|
-
if (typeof element === "string" || typeof element === "number") return {
|
|
678
|
-
type: Text,
|
|
679
|
-
props: {},
|
|
680
|
-
key: null,
|
|
681
|
-
children: [],
|
|
682
|
-
dom: null,
|
|
683
|
-
text: element
|
|
684
|
-
};
|
|
685
|
-
return element;
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* Check if type is a component
|
|
689
|
-
*/
|
|
690
|
-
function isComponent(type) {
|
|
691
|
-
return typeof type === "function" && "__setup" in type;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Get hydration strategy from vnode props
|
|
695
|
-
*/
|
|
696
|
-
function getHydrationStrategy(props) {
|
|
697
|
-
if (props["client:load"] !== void 0) return { strategy: "load" };
|
|
698
|
-
if (props["client:idle"] !== void 0) return { strategy: "idle" };
|
|
699
|
-
if (props["client:visible"] !== void 0) return { strategy: "visible" };
|
|
700
|
-
if (props["client:only"] !== void 0) return { strategy: "only" };
|
|
701
|
-
if (props["client:media"] !== void 0) return {
|
|
702
|
-
strategy: "media",
|
|
703
|
-
media: props["client:media"]
|
|
704
|
-
};
|
|
705
|
-
return null;
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Hydrate a VNode against existing DOM
|
|
709
|
-
* This only attaches event handlers and refs - no DOM creation
|
|
710
|
-
*/
|
|
711
|
-
function hydrateNode(vnode, dom, parent) {
|
|
712
|
-
if (!vnode) return dom;
|
|
713
|
-
while (dom && dom.nodeType === Node.COMMENT_NODE) {
|
|
714
|
-
const commentText = dom.data;
|
|
715
|
-
if (commentText.startsWith("$c:") || commentText.startsWith("/$c:") || commentText.startsWith("$island:")) break;
|
|
716
|
-
dom = dom.nextSibling;
|
|
717
|
-
}
|
|
718
|
-
if (vnode.type === Text) {
|
|
719
|
-
if (dom && dom.nodeType === Node.TEXT_NODE) {
|
|
720
|
-
vnode.dom = dom;
|
|
721
|
-
return dom.nextSibling;
|
|
722
|
-
}
|
|
723
|
-
return dom;
|
|
724
|
-
}
|
|
725
|
-
if (vnode.type === Fragment) {
|
|
726
|
-
let current = dom;
|
|
727
|
-
for (const child of vnode.children) current = hydrateNode(child, current, parent);
|
|
728
|
-
return current;
|
|
729
|
-
}
|
|
730
|
-
if (isComponent(vnode.type)) {
|
|
731
|
-
const strategy = vnode.props ? getHydrationStrategy(vnode.props) : null;
|
|
732
|
-
if (strategy) return scheduleComponentHydration(vnode, dom, parent, strategy);
|
|
733
|
-
return hydrateComponent(vnode, dom, parent);
|
|
734
|
-
}
|
|
735
|
-
if (typeof vnode.type === "string") {
|
|
736
|
-
if (!dom || dom.nodeType !== Node.ELEMENT_NODE) {
|
|
737
|
-
console.warn("[Hydrate] Expected element but got:", dom);
|
|
738
|
-
return dom;
|
|
739
|
-
}
|
|
740
|
-
const el = dom;
|
|
741
|
-
vnode.dom = el;
|
|
742
|
-
if (vnode.props) for (const key in vnode.props) {
|
|
743
|
-
if (key === "children" || key === "key") continue;
|
|
744
|
-
if (key.startsWith("client:")) continue;
|
|
745
|
-
patchProp(el, key, null, vnode.props[key]);
|
|
746
|
-
}
|
|
747
|
-
let childDom = el.firstChild;
|
|
748
|
-
for (const child of vnode.children) childDom = hydrateNode(child, childDom, el);
|
|
749
|
-
if (vnode.type === "select" && vnode.props) fixSelectValue(el, vnode.props);
|
|
750
|
-
return el.nextSibling;
|
|
751
|
-
}
|
|
752
|
-
return dom;
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* Schedule component hydration based on strategy.
|
|
756
|
-
* Returns the next DOM node after this component's content.
|
|
757
|
-
*/
|
|
758
|
-
function scheduleComponentHydration(vnode, dom, parent, strategy) {
|
|
759
|
-
const { startNode, endNode } = findComponentBoundaries(dom);
|
|
760
|
-
const doHydrate = () => {
|
|
761
|
-
hydrateComponent(vnode, startNode, parent);
|
|
762
|
-
};
|
|
763
|
-
switch (strategy.strategy) {
|
|
764
|
-
case "load":
|
|
765
|
-
console.log("[Hydrate] client:load - hydrating immediately");
|
|
766
|
-
doHydrate();
|
|
767
|
-
break;
|
|
768
|
-
case "idle":
|
|
769
|
-
console.log("[Hydrate] client:idle - scheduling for idle time");
|
|
770
|
-
if ("requestIdleCallback" in window) requestIdleCallback(() => {
|
|
771
|
-
console.log("[Hydrate] client:idle - idle callback fired");
|
|
772
|
-
doHydrate();
|
|
773
|
-
});
|
|
774
|
-
else setTimeout(() => {
|
|
775
|
-
console.log("[Hydrate] client:idle - timeout fallback fired");
|
|
776
|
-
doHydrate();
|
|
777
|
-
}, 200);
|
|
778
|
-
break;
|
|
779
|
-
case "visible":
|
|
780
|
-
console.log("[Hydrate] client:visible - observing visibility");
|
|
781
|
-
observeComponentVisibility(startNode, endNode, doHydrate);
|
|
782
|
-
break;
|
|
783
|
-
case "media":
|
|
784
|
-
if (strategy.media) {
|
|
785
|
-
const mql = window.matchMedia(strategy.media);
|
|
786
|
-
if (mql.matches) doHydrate();
|
|
787
|
-
else {
|
|
788
|
-
const handler = (e) => {
|
|
789
|
-
if (e.matches) {
|
|
790
|
-
mql.removeEventListener("change", handler);
|
|
791
|
-
doHydrate();
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
mql.addEventListener("change", handler);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
break;
|
|
798
|
-
case "only":
|
|
799
|
-
doHydrate();
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
return endNode ? endNode.nextSibling : dom;
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Find component start/end markers in DOM
|
|
806
|
-
*/
|
|
807
|
-
function findComponentBoundaries(dom) {
|
|
808
|
-
let startNode = dom;
|
|
809
|
-
let endNode = null;
|
|
810
|
-
if (dom && dom.nodeType === Node.COMMENT_NODE) {
|
|
811
|
-
const text = dom.data;
|
|
812
|
-
if (text.startsWith("$c:")) {
|
|
813
|
-
const id = text.slice(3);
|
|
814
|
-
startNode = dom;
|
|
815
|
-
let current = dom.nextSibling;
|
|
816
|
-
while (current) {
|
|
817
|
-
if (current.nodeType === Node.COMMENT_NODE) {
|
|
818
|
-
if (current.data === `/$c:${id}`) {
|
|
819
|
-
endNode = current;
|
|
820
|
-
break;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
current = current.nextSibling;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return {
|
|
828
|
-
startNode,
|
|
829
|
-
endNode
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Observe when a component's DOM becomes visible
|
|
834
|
-
*/
|
|
835
|
-
function observeComponentVisibility(startNode, endNode, callback) {
|
|
836
|
-
let targetElement = null;
|
|
837
|
-
let current = startNode?.nextSibling || null;
|
|
838
|
-
while (current && current !== endNode) {
|
|
839
|
-
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
840
|
-
targetElement = current;
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
current = current.nextSibling;
|
|
844
|
-
}
|
|
845
|
-
if (!targetElement) {
|
|
846
|
-
callback();
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
const observer = new IntersectionObserver((entries) => {
|
|
850
|
-
for (const entry of entries) if (entry.isIntersecting) {
|
|
851
|
-
observer.disconnect();
|
|
852
|
-
callback();
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
|
-
}, { rootMargin: "50px" });
|
|
856
|
-
observer.observe(targetElement);
|
|
857
|
-
}
|
|
858
|
-
/**
|
|
859
|
-
* Hydrate a component - run setup and create reactive effect
|
|
860
|
-
* @param vnode - The VNode to hydrate
|
|
861
|
-
* @param dom - The DOM node to start from
|
|
862
|
-
* @param parent - The parent node
|
|
863
|
-
* @param serverState - Optional state captured from server for async components
|
|
864
|
-
*/
|
|
865
|
-
function hydrateComponent(vnode, dom, parent, serverState) {
|
|
866
|
-
const componentFactory = vnode.type;
|
|
867
|
-
const setup = componentFactory.__setup;
|
|
868
|
-
const componentName = componentFactory.__name || "Anonymous";
|
|
869
|
-
if (componentName && componentName !== "Anonymous") registerComponent(componentName, componentFactory);
|
|
870
|
-
let anchor = null;
|
|
871
|
-
let componentId = null;
|
|
872
|
-
if (dom && dom.nodeType === Node.COMMENT_NODE) {
|
|
873
|
-
const commentText = dom.data;
|
|
874
|
-
if (commentText.startsWith("$c:")) {
|
|
875
|
-
anchor = dom;
|
|
876
|
-
componentId = parseInt(commentText.slice(3), 10);
|
|
877
|
-
dom = dom.nextSibling;
|
|
878
|
-
while (dom && dom.nodeType === Node.COMMENT_NODE) if (dom.data.startsWith("$island:")) dom = dom.nextSibling;
|
|
879
|
-
else break;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
if (!serverState && componentId !== null) serverState = getIslandServerState(componentId);
|
|
883
|
-
const internalVNode = vnode;
|
|
884
|
-
const { children, slots: slotsFromProps, ...propsData } = filterClientDirectives(vnode.props || {});
|
|
885
|
-
const reactiveProps = signal(propsData);
|
|
886
|
-
internalVNode._componentProps = reactiveProps;
|
|
887
|
-
const slots = createSlots(children, slotsFromProps);
|
|
888
|
-
internalVNode._slots = slots;
|
|
889
|
-
const mountHooks = [];
|
|
890
|
-
const cleanupHooks = [];
|
|
891
|
-
const parentInstance = getCurrentInstance();
|
|
892
|
-
const signalFn = serverState ? createRestoringSignal(serverState) : signal;
|
|
893
|
-
const hasServerState = !!serverState;
|
|
894
|
-
const ssrHelper = {
|
|
895
|
-
load(_fn) {
|
|
896
|
-
if (hasServerState) console.log(`[Hydrate] Skipping ssr.load() - using restored server state`);
|
|
897
|
-
},
|
|
898
|
-
isServer: false,
|
|
899
|
-
isHydrating: hasServerState
|
|
900
|
-
};
|
|
901
|
-
const componentCtx = {
|
|
902
|
-
el: parent,
|
|
903
|
-
signal: signalFn,
|
|
904
|
-
props: createPropsAccessor(reactiveProps),
|
|
905
|
-
slots,
|
|
906
|
-
emit: (event, ...args) => {
|
|
907
|
-
const handler = reactiveProps[`on${event[0].toUpperCase() + event.slice(1)}`];
|
|
908
|
-
if (handler && typeof handler === "function") handler(...args);
|
|
909
|
-
},
|
|
910
|
-
parent: parentInstance,
|
|
911
|
-
onMount: (fn) => {
|
|
912
|
-
mountHooks.push(fn);
|
|
913
|
-
},
|
|
914
|
-
onCleanup: (fn) => {
|
|
915
|
-
cleanupHooks.push(fn);
|
|
916
|
-
},
|
|
917
|
-
expose: () => {},
|
|
918
|
-
renderFn: null,
|
|
919
|
-
update: () => {},
|
|
920
|
-
ssr: ssrHelper,
|
|
921
|
-
_serverState: serverState
|
|
922
|
-
};
|
|
923
|
-
const prev = setCurrentInstance(componentCtx);
|
|
924
|
-
let renderFn;
|
|
925
|
-
try {
|
|
926
|
-
renderFn = setup(componentCtx);
|
|
927
|
-
} catch (err) {
|
|
928
|
-
console.error(`Error hydrating component ${componentName}:`, err);
|
|
929
|
-
} finally {
|
|
930
|
-
setCurrentInstance(prev);
|
|
931
|
-
}
|
|
932
|
-
const startDom = anchor || dom;
|
|
933
|
-
let endDom = dom;
|
|
934
|
-
if (renderFn) {
|
|
935
|
-
componentCtx.renderFn = renderFn;
|
|
936
|
-
let isFirstRender = true;
|
|
937
|
-
const componentEffect = effect(() => {
|
|
938
|
-
const prevInstance = setCurrentInstance(componentCtx);
|
|
939
|
-
try {
|
|
940
|
-
const subTreeResult = componentCtx.renderFn();
|
|
941
|
-
if (subTreeResult == null) return;
|
|
942
|
-
const subTree = normalizeSubTree(subTreeResult);
|
|
943
|
-
const prevSubTree = internalVNode._subTree;
|
|
944
|
-
if (isFirstRender) {
|
|
945
|
-
isFirstRender = false;
|
|
946
|
-
endDom = hydrateNode(subTree, dom, parent);
|
|
947
|
-
internalVNode._subTree = subTree;
|
|
948
|
-
} else {
|
|
949
|
-
if (prevSubTree && prevSubTree.dom) {
|
|
950
|
-
const container = prevSubTree.dom.parentNode;
|
|
951
|
-
if (container) {
|
|
952
|
-
container._vnode = prevSubTree;
|
|
953
|
-
render(subTree, container);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
internalVNode._subTree = subTree;
|
|
957
|
-
}
|
|
958
|
-
} finally {
|
|
959
|
-
setCurrentInstance(prevInstance);
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
internalVNode._effect = componentEffect;
|
|
963
|
-
componentCtx.update = () => componentEffect();
|
|
964
|
-
}
|
|
965
|
-
vnode.dom = anchor || startDom;
|
|
966
|
-
const mountCtx = { el: parent };
|
|
967
|
-
mountHooks.forEach((hook) => hook(mountCtx));
|
|
968
|
-
vnode.cleanup = () => {
|
|
969
|
-
cleanupHooks.forEach((hook) => hook(mountCtx));
|
|
970
|
-
};
|
|
971
|
-
let result = endDom;
|
|
972
|
-
while (result && result.nodeType === Node.COMMENT_NODE) {
|
|
973
|
-
if (result.data.startsWith("/$c:")) {
|
|
974
|
-
result = result.nextSibling;
|
|
975
|
-
break;
|
|
976
|
-
}
|
|
977
|
-
result = result.nextSibling;
|
|
978
|
-
}
|
|
979
|
-
return result;
|
|
980
|
-
}
|
|
981
|
-
function filterClientDirectives(props) {
|
|
982
|
-
const filtered = {};
|
|
983
|
-
for (const key in props) if (!key.startsWith("client:")) filtered[key] = props[key];
|
|
984
|
-
return filtered;
|
|
985
|
-
}
|
|
986
|
-
/**
|
|
987
|
-
* Fix select element value after hydrating children.
|
|
988
|
-
* This is needed because <select>.value only works after <option> children exist in DOM.
|
|
989
|
-
*/
|
|
990
|
-
function fixSelectValue(dom, props) {
|
|
991
|
-
if (dom.tagName === "SELECT" && "value" in props) {
|
|
992
|
-
const val = props.value;
|
|
993
|
-
if (dom.multiple) {
|
|
994
|
-
const options = dom.options;
|
|
995
|
-
const valArray = Array.isArray(val) ? val : [val];
|
|
996
|
-
for (let i = 0; i < options.length; i++) options[i].selected = valArray.includes(options[i].value);
|
|
997
|
-
} else dom.value = String(val);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Hydrate islands based on their strategies (selective hydration)
|
|
1002
|
-
*/
|
|
1003
|
-
function hydrateIslands() {
|
|
1004
|
-
const dataScript = document.getElementById("__SIGX_ISLANDS__");
|
|
1005
|
-
if (!dataScript) return;
|
|
1006
|
-
let islandData;
|
|
1007
|
-
try {
|
|
1008
|
-
islandData = JSON.parse(dataScript.textContent || "{}");
|
|
1009
|
-
} catch {
|
|
1010
|
-
console.error("Failed to parse island data");
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
for (const [idStr, info] of Object.entries(islandData)) scheduleHydration(parseInt(idStr, 10), info);
|
|
1014
|
-
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Schedule hydration based on strategy
|
|
1017
|
-
*/
|
|
1018
|
-
function scheduleHydration(id, info) {
|
|
1019
|
-
const marker = findIslandMarker(id);
|
|
1020
|
-
if (!marker) {
|
|
1021
|
-
console.warn(`Island marker not found for id ${id}`);
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
const component = info.componentId ? getComponent(info.componentId) : null;
|
|
1025
|
-
if (!component && info.strategy !== "only") {
|
|
1026
|
-
console.warn(`Component "${info.componentId}" not registered for hydration`);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
switch (info.strategy) {
|
|
1030
|
-
case "load":
|
|
1031
|
-
hydrateIsland(marker, component, info);
|
|
1032
|
-
break;
|
|
1033
|
-
case "idle":
|
|
1034
|
-
if ("requestIdleCallback" in window) requestIdleCallback(() => hydrateIsland(marker, component, info));
|
|
1035
|
-
else setTimeout(() => hydrateIsland(marker, component, info), 200);
|
|
1036
|
-
break;
|
|
1037
|
-
case "visible":
|
|
1038
|
-
observeVisibility(marker, () => hydrateIsland(marker, component, info));
|
|
1039
|
-
break;
|
|
1040
|
-
case "media":
|
|
1041
|
-
if (info.media) {
|
|
1042
|
-
const mql = window.matchMedia(info.media);
|
|
1043
|
-
if (mql.matches) hydrateIsland(marker, component, info);
|
|
1044
|
-
else mql.addEventListener("change", function handler(e) {
|
|
1045
|
-
if (e.matches) {
|
|
1046
|
-
mql.removeEventListener("change", handler);
|
|
1047
|
-
hydrateIsland(marker, component, info);
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
break;
|
|
1052
|
-
case "only":
|
|
1053
|
-
if (component) mountClientOnly(marker, component, info);
|
|
1054
|
-
break;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
function findIslandMarker(id) {
|
|
1058
|
-
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null);
|
|
1059
|
-
let node;
|
|
1060
|
-
while (node = walker.nextNode()) if (node.data === `$c:${id}`) return node;
|
|
1061
|
-
return null;
|
|
1062
|
-
}
|
|
1063
|
-
function observeVisibility(marker, callback) {
|
|
1064
|
-
let node = marker.nextSibling;
|
|
1065
|
-
while (node && node.nodeType !== Node.ELEMENT_NODE) node = node.nextSibling;
|
|
1066
|
-
if (!node) return;
|
|
1067
|
-
const observer = new IntersectionObserver((entries) => {
|
|
1068
|
-
for (const entry of entries) if (entry.isIntersecting) {
|
|
1069
|
-
observer.disconnect();
|
|
1070
|
-
callback();
|
|
1071
|
-
break;
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
observer.observe(node);
|
|
1075
|
-
}
|
|
1076
|
-
function hydrateIsland(marker, component, info) {
|
|
1077
|
-
let container = marker.nextSibling;
|
|
1078
|
-
while (container && container.nodeType === Node.COMMENT_NODE) container = container.nextSibling;
|
|
1079
|
-
if (!container || container.nodeType !== Node.ELEMENT_NODE) {
|
|
1080
|
-
console.warn("No element found for island hydration");
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
const props = info.props || {};
|
|
1084
|
-
if (info.state) setPendingServerState(info.state);
|
|
1085
|
-
const vnode = {
|
|
1086
|
-
type: component,
|
|
1087
|
-
props,
|
|
1088
|
-
key: null,
|
|
1089
|
-
children: [],
|
|
1090
|
-
dom: null
|
|
1091
|
-
};
|
|
1092
|
-
const wrapper = document.createElement("div");
|
|
1093
|
-
wrapper.style.display = "contents";
|
|
1094
|
-
const parent = container.parentNode;
|
|
1095
|
-
parent.insertBefore(wrapper, container);
|
|
1096
|
-
parent.removeChild(container);
|
|
1097
|
-
render(vnode, wrapper);
|
|
1098
|
-
}
|
|
1099
|
-
function mountClientOnly(marker, component, info) {
|
|
1100
|
-
let placeholder = marker.nextSibling;
|
|
1101
|
-
while (placeholder && placeholder.nodeType === Node.COMMENT_NODE) placeholder = placeholder.nextSibling;
|
|
1102
|
-
if (!placeholder || !placeholder.hasAttribute?.("data-island")) return;
|
|
1103
|
-
const props = info.props || {};
|
|
1104
|
-
const container = placeholder;
|
|
1105
|
-
container.innerHTML = "";
|
|
1106
|
-
render({
|
|
1107
|
-
type: component,
|
|
1108
|
-
props,
|
|
1109
|
-
key: null,
|
|
1110
|
-
children: [],
|
|
1111
|
-
dom: null
|
|
1112
|
-
}, container);
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Set up listener for async components streaming in.
|
|
1116
|
-
* This is set up at module load time to catch events early.
|
|
1117
|
-
*/
|
|
1118
|
-
let _asyncListenerSetup = false;
|
|
1119
|
-
function ensureAsyncHydrationListener() {
|
|
1120
|
-
if (_asyncListenerSetup) return;
|
|
1121
|
-
_asyncListenerSetup = true;
|
|
1122
|
-
document.addEventListener("sigx:async-ready", (event) => {
|
|
1123
|
-
const { id, state } = event.detail || {};
|
|
1124
|
-
console.log(`[Hydrate] Async component ${id} ready, hydrating...`);
|
|
1125
|
-
invalidateIslandCache();
|
|
1126
|
-
const placeholder = document.querySelector(`[data-async-placeholder="${id}"]`);
|
|
1127
|
-
if (!placeholder) {
|
|
1128
|
-
console.warn(`[Hydrate] Could not find placeholder for async component ${id}`);
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
const info = getIslandData()[String(id)];
|
|
1132
|
-
if (!info) {
|
|
1133
|
-
console.warn(`[Hydrate] No island data for async component ${id}`);
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
hydrateAsyncComponent(placeholder, info);
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
if (typeof document !== "undefined") ensureAsyncHydrationListener();
|
|
1140
|
-
/**
|
|
1141
|
-
* Hydrate an async component that just streamed in.
|
|
1142
|
-
* The DOM has already been replaced with server-rendered content by $SIGX_REPLACE.
|
|
1143
|
-
* Since the streamed content doesn't include component markers, we mount fresh
|
|
1144
|
-
* with the server state restored via the context extension.
|
|
1145
|
-
*/
|
|
1146
|
-
function hydrateAsyncComponent(container, info) {
|
|
1147
|
-
if (!info.componentId) {
|
|
1148
|
-
console.error(`[Hydrate] No componentId in island info`);
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
const component = getComponent(info.componentId);
|
|
1152
|
-
if (!component) {
|
|
1153
|
-
console.error(`[Hydrate] Component "${info.componentId}" not registered`);
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
const props = info.props || {};
|
|
1157
|
-
const serverState = info.state;
|
|
1158
|
-
container.innerHTML = "";
|
|
1159
|
-
if (serverState) setPendingServerState(serverState);
|
|
1160
|
-
render({
|
|
1161
|
-
type: component,
|
|
1162
|
-
props,
|
|
1163
|
-
key: null,
|
|
1164
|
-
children: [],
|
|
1165
|
-
dom: null
|
|
1166
|
-
}, container);
|
|
1167
|
-
console.log(`[Hydrate] Async component "${info.componentId}" mounted with server state`);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
//#endregion
|
|
1171
|
-
export { HydrationRegistry, createSSRContext, hydrate, hydrateIslands, registerComponent, registerComponents, renderToStream, renderToString };
|
|
1172
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import { i as createSSRContext, r as renderToString, t as renderToStream } from "./server-BCOJt2Bi.js";
|
|
2
|
+
import { a as registerComponents, i as registerComponent, n as hydrateIslands, r as HydrationRegistry, t as ssrClientPlugin } from "./client-DiLwBAD-.js";
|
|
3
|
+
export { HydrationRegistry, createSSRContext, hydrateIslands, registerComponent, registerComponents, renderToStream, renderToString, ssrClientPlugin };
|