@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,455 +0,0 @@
1
- import { n as internals_exports, t as generateSignalKey } from "./types-DYlI_C8F.js";
2
- import { Comment, Fragment, Text, effect, getCurrentInstance, isComponent, isModel, render, signal } from "sigx";
3
- //#region src/client/hydrate-context.ts
4
- /**
5
- * Hydration context and state management
6
- *
7
- * Manages server state restoration, app context tracking,
8
- * client plugin registration, and the SSR context extension for components.
9
- *
10
- * Strategy-specific concerns (island data, async hydration) are handled
11
- * by plugins registered via `registerClientPlugin()`.
12
- */
13
- var _pendingServerState = null;
14
- var _currentAppContext = null;
15
- var _clientPlugins = [];
16
- /**
17
- * Register a client-side SSR plugin.
18
- * Plugins are called during hydration to intercept component processing,
19
- * skip default hydration walk, or run post-hydration logic.
20
- */
21
- function registerClientPlugin(plugin) {
22
- _clientPlugins.push(plugin);
23
- }
24
- /**
25
- * Get all registered client-side plugins.
26
- */
27
- function getClientPlugins() {
28
- return _clientPlugins;
29
- }
30
- /**
31
- * Clear all registered client plugins (useful for testing).
32
- */
33
- function clearClientPlugins() {
34
- _clientPlugins = [];
35
- }
36
- /**
37
- * Set server state that should be used for the next component mount.
38
- * Used internally when mounting async components after streaming.
39
- */
40
- function setPendingServerState(state) {
41
- _pendingServerState = state;
42
- }
43
- /** Get the current app context for deferred hydration */
44
- function getCurrentAppContext() {
45
- return _currentAppContext;
46
- }
47
- /** Set the current app context during hydration */
48
- function setCurrentAppContext(ctx) {
49
- _currentAppContext = ctx;
50
- }
51
- /**
52
- * Creates a signal function that restores state from server-captured values.
53
- * Used during hydration of async components to avoid re-fetching data.
54
- * Supports both primitive and object signals.
55
- */
56
- function createRestoringSignal(serverState) {
57
- let signalIndex = 0;
58
- let hasWarnedPositional = false;
59
- return function restoringSignal(initial, name) {
60
- const key = generateSignalKey(name, signalIndex++);
61
- if (process.env.NODE_ENV !== "production" && !name && !hasWarnedPositional) {
62
- hasWarnedPositional = true;
63
- console.warn(`[SSR Hydration] Signal restored without a name — using positional key "${key}". If signal declaration order differs between server and client builds, state will be silently mismatched. Use named signals: signal(value, "name")`);
64
- }
65
- if (key in serverState) return signal(serverState[key]);
66
- return signal(initial);
67
- };
68
- }
69
- /**
70
- * Normalize any element to VNode
71
- */
72
- function normalizeElement(element) {
73
- if (element == null || element === true || element === false) return null;
74
- if (typeof element === "string" || typeof element === "number") return {
75
- type: Text,
76
- props: {},
77
- key: null,
78
- children: [],
79
- dom: null,
80
- text: element
81
- };
82
- return element;
83
- }
84
- /**
85
- * Register the SSR context extension for all components.
86
- * This provides the `ssr` object with a no-op `load()` for client-side rendering.
87
- * Also handles server state restoration for async streamed components.
88
- */
89
- (0, internals_exports.registerContextExtension)((ctx) => {
90
- const serverState = _pendingServerState;
91
- if (serverState) {
92
- ctx._serverState = serverState;
93
- _pendingServerState = null;
94
- ctx.signal = createRestoringSignal(serverState);
95
- ctx.ssr = {
96
- load: (_fn) => {},
97
- isServer: false,
98
- isHydrating: true
99
- };
100
- } else if (ctx._serverState) ctx.ssr = {
101
- load: (_fn) => {},
102
- isServer: false,
103
- isHydrating: true
104
- };
105
- else ctx.ssr = {
106
- load: (fn) => {
107
- fn().catch((err) => console.error("[SSR] load error:", err));
108
- },
109
- isServer: false,
110
- isHydrating: false
111
- };
112
- });
113
- //#endregion
114
- //#region src/client/hydrate-component.ts
115
- /**
116
- * Component hydration logic — strategy-agnostic
117
- *
118
- * Handles running component setup, creating reactive effects,
119
- * and restoring server state for hydrated components.
120
- * Does not depend on islands or any specific SSR strategy.
121
- */
122
- /**
123
- * Hydrate a component - run setup and create reactive effect
124
- *
125
- * With trailing markers, the structure is: <content><!--$c:id-->
126
- * - dom points to start of content
127
- * - trailingMarker (if provided) is the anchor at the end
128
- *
129
- * @param vnode - The VNode to hydrate
130
- * @param dom - The DOM node to start from (content starts here)
131
- * @param parent - The parent node
132
- * @param serverState - Optional state captured from server for async components
133
- * @param trailingMarker - Optional trailing marker comment (the component anchor)
134
- */
135
- function hydrateComponent(vnode, dom, parent, serverState, trailingMarker) {
136
- const componentFactory = vnode.type;
137
- const setup = componentFactory.__setup;
138
- const componentName = componentFactory.__name || "Anonymous";
139
- let anchor = trailingMarker || null;
140
- if (!anchor) {
141
- let current = dom;
142
- let bestAnchor = null;
143
- let bestId = Infinity;
144
- let foundAnyMarker = false;
145
- while (current) {
146
- if (current.nodeType === Node.COMMENT_NODE) {
147
- const text = current.data;
148
- if (text.startsWith("$c:")) {
149
- const id = parseInt(text.slice(3), 10);
150
- if (id < bestId) {
151
- bestId = id;
152
- bestAnchor = current;
153
- }
154
- foundAnyMarker = true;
155
- }
156
- } else if (foundAnyMarker) break;
157
- current = current.nextSibling;
158
- }
159
- if (bestAnchor) anchor = bestAnchor;
160
- } else {
161
- const text = anchor.data;
162
- if (text.startsWith("$c:")) parseInt(text.slice(3), 10);
163
- }
164
- const internalVNode = vnode;
165
- const { children, slots: slotsFromProps, $models: modelsData, ...propsData } = (0, internals_exports.filterClientDirectives)(vnode.props || {});
166
- const propsWithModels = { ...propsData };
167
- if (modelsData) for (const modelKey in modelsData) {
168
- const modelValue = modelsData[modelKey];
169
- if (isModel(modelValue)) propsWithModels[modelKey] = modelValue;
170
- }
171
- const reactiveProps = signal(propsWithModels);
172
- internalVNode._componentProps = reactiveProps;
173
- const slots = (0, internals_exports.createSlots)(children, slotsFromProps);
174
- internalVNode._slots = slots;
175
- const mountHooks = [];
176
- const unmountHooks = [];
177
- const createdHooks = [];
178
- const updatedHooks = [];
179
- const parentInstance = getCurrentInstance();
180
- const signalFn = serverState ? createRestoringSignal(serverState) : signal;
181
- const ssrHelper = {
182
- load(_fn) {},
183
- isServer: false,
184
- isHydrating: !!serverState
185
- };
186
- const componentCtx = {
187
- el: parent,
188
- signal: signalFn,
189
- props: (0, internals_exports.createPropsAccessor)(reactiveProps),
190
- slots,
191
- emit: (0, internals_exports.createEmit)(reactiveProps),
192
- parent: parentInstance,
193
- onMounted: (fn) => {
194
- mountHooks.push(fn);
195
- },
196
- onUnmounted: (fn) => {
197
- unmountHooks.push(fn);
198
- },
199
- onCreated: (fn) => {
200
- createdHooks.push(fn);
201
- },
202
- onUpdated: (fn) => {
203
- updatedHooks.push(fn);
204
- },
205
- expose: () => {},
206
- renderFn: null,
207
- update: () => {},
208
- ssr: ssrHelper,
209
- _serverState: serverState
210
- };
211
- if (!parentInstance && getCurrentAppContext()) (0, internals_exports.provideAppContext)(componentCtx, getCurrentAppContext());
212
- const prev = (0, internals_exports.setCurrentInstance)(componentCtx);
213
- let renderFn;
214
- try {
215
- renderFn = setup(componentCtx);
216
- } catch (err) {
217
- if (process.env.NODE_ENV !== "production") console.error(`Error hydrating component ${componentName}:`, err);
218
- } finally {
219
- (0, internals_exports.setCurrentInstance)(prev);
220
- }
221
- let endDom = dom;
222
- if (renderFn) {
223
- componentCtx.renderFn = renderFn;
224
- let isFirstRender = true;
225
- const subTreeRef = { current: null };
226
- internalVNode._subTreeRef = subTreeRef;
227
- const componentEffect = effect(() => {
228
- const prevInstance = (0, internals_exports.setCurrentInstance)(componentCtx);
229
- try {
230
- const subTreeResult = componentCtx.renderFn();
231
- const prevSubTree = subTreeRef.current;
232
- if (subTreeResult == null) {
233
- if (isFirstRender) {
234
- if (!(dom != null && anchor != null && dom !== anchor)) isFirstRender = false;
235
- } else if (prevSubTree && prevSubTree.dom) {
236
- const patchContainer = prevSubTree.dom.parentNode || parent;
237
- const emptyNode = (0, internals_exports.normalizeSubTree)(null);
238
- (0, internals_exports.patch)(prevSubTree, emptyNode, patchContainer);
239
- subTreeRef.current = emptyNode;
240
- internalVNode._subTree = emptyNode;
241
- }
242
- return;
243
- }
244
- const subTree = (0, internals_exports.normalizeSubTree)(subTreeResult);
245
- if (isFirstRender) {
246
- isFirstRender = false;
247
- if (dom != null && dom !== anchor) endDom = hydrateNode(subTree, dom, parent);
248
- else (0, internals_exports.mount)(subTree, parent, anchor || null);
249
- subTreeRef.current = subTree;
250
- internalVNode._subTree = subTree;
251
- } else {
252
- if (prevSubTree) (0, internals_exports.patch)(prevSubTree, subTree, prevSubTree.dom?.parentNode || parent);
253
- else (0, internals_exports.mount)(subTree, parent, anchor || null);
254
- subTreeRef.current = subTree;
255
- internalVNode._subTree = subTree;
256
- }
257
- } finally {
258
- (0, internals_exports.setCurrentInstance)(prevInstance);
259
- }
260
- });
261
- internalVNode._effect = componentEffect;
262
- componentCtx.update = () => componentEffect();
263
- }
264
- vnode.dom = anchor || endDom;
265
- const mountCtx = { el: parent };
266
- createdHooks.forEach((hook) => hook());
267
- mountHooks.forEach((hook) => hook(mountCtx));
268
- vnode.cleanup = () => {
269
- unmountHooks.forEach((hook) => hook(mountCtx));
270
- };
271
- return anchor ? anchor.nextSibling : endDom;
272
- }
273
- //#endregion
274
- //#region src/client/hydrate-core.ts
275
- /**
276
- * Core hydration logic — strategy-agnostic
277
- *
278
- * Walks existing server-rendered DOM and attaches event handlers,
279
- * creates reactive effects, and delegates components to the component hydrator.
280
- *
281
- * Plugins registered via `registerClientPlugin()` can intercept component
282
- * hydration (e.g., for deferred/island-based hydration strategies).
283
- */
284
- /**
285
- * Hydrate a server-rendered app.
286
- *
287
- * This walks the existing DOM to attach event handlers, runs component
288
- * setup functions to establish reactivity, then uses runtime-dom for updates.
289
- *
290
- * Registered client plugins are called at appropriate points:
291
- * - `beforeHydrate`: before the DOM walk (return false to skip it entirely)
292
- * - `hydrateComponent`: for each component (return { next } to handle it)
293
- * - `afterHydrate`: after the DOM walk completes
294
- *
295
- * @param element - The root element/VNode to hydrate
296
- * @param container - The DOM container with SSR content
297
- * @param appContext - The app context for DI (provides, etc.)
298
- */
299
- function hydrate(element, container, appContext) {
300
- const vnode = normalizeElement(element);
301
- if (!vnode) return;
302
- setCurrentAppContext(appContext ?? null);
303
- const plugins = getClientPlugins();
304
- for (const plugin of plugins) if (plugin.client?.beforeHydrate?.(container) === false) {
305
- container._vnode = vnode;
306
- return;
307
- }
308
- hydrateNode(vnode, container.firstChild, container);
309
- for (const plugin of plugins) plugin.client?.afterHydrate?.(container);
310
- container._vnode = vnode;
311
- }
312
- /**
313
- * Hydrate a VNode against existing DOM
314
- * This only attaches event handlers and refs - no DOM creation
315
- */
316
- function hydrateNode(vnode, dom, parent) {
317
- if (!vnode) return dom;
318
- const isComponentVNode = isComponent(vnode.type);
319
- const isTextVNode = vnode.type === Text;
320
- const isCommentVNode = vnode.type === Comment;
321
- while (dom && dom.nodeType === Node.COMMENT_NODE) {
322
- if (isCommentVNode && dom.data === "") break;
323
- if (isComponentVNode) {
324
- if (dom.data.startsWith("$c:")) break;
325
- }
326
- if (isTextVNode && dom.data === "t") {
327
- const emptyText = document.createTextNode("");
328
- parent.replaceChild(emptyText, dom);
329
- dom = emptyText;
330
- break;
331
- }
332
- dom = dom.nextSibling;
333
- }
334
- if (vnode.type === Comment) {
335
- if (dom && dom.nodeType === Node.COMMENT_NODE) {
336
- vnode.dom = dom;
337
- return dom.nextSibling;
338
- }
339
- const comment = document.createComment("");
340
- if (dom) parent.insertBefore(comment, dom);
341
- else parent.appendChild(comment);
342
- vnode.dom = comment;
343
- return dom;
344
- }
345
- if (vnode.type === Text) {
346
- if (dom && dom.nodeType === Node.TEXT_NODE) {
347
- vnode.dom = dom;
348
- return dom.nextSibling;
349
- }
350
- const textNode = document.createTextNode(String(vnode.text ?? ""));
351
- if (dom) parent.insertBefore(textNode, dom);
352
- else parent.appendChild(textNode);
353
- vnode.dom = textNode;
354
- return dom;
355
- }
356
- if (vnode.type === Fragment) {
357
- let current = dom;
358
- for (const child of vnode.children) current = hydrateNode(child, current, parent);
359
- return current;
360
- }
361
- if (isComponent(vnode.type)) {
362
- const plugins = getClientPlugins();
363
- for (const plugin of plugins) {
364
- const result = plugin.client?.hydrateComponent?.(vnode, dom, parent);
365
- if (result !== void 0) return result;
366
- }
367
- return hydrateComponent(vnode, dom, parent);
368
- }
369
- if (typeof vnode.type === "string") {
370
- if (!dom || dom.nodeType !== Node.ELEMENT_NODE) {
371
- if (process.env.NODE_ENV !== "production") {
372
- const cls = vnode.props?.class || "";
373
- console.warn("[Hydrate] Expected element but got:", dom, "| tag:", vnode.type, "| class:", cls, "| parent:", parent?.nodeName);
374
- }
375
- return dom;
376
- }
377
- const el = dom;
378
- vnode.dom = el;
379
- if (vnode.props) {
380
- let hasDirectives = false;
381
- for (const key in vnode.props) {
382
- if (key === "children" || key === "key") continue;
383
- if (key.startsWith("client:")) continue;
384
- if (key.charCodeAt(0) === 117 && key.startsWith("use:")) {
385
- (0, internals_exports.patchDirective)(el, key.slice(4), null, vnode.props[key], getCurrentAppContext());
386
- hasDirectives = true;
387
- } else (0, internals_exports.patchProp)(el, key, null, vnode.props[key]);
388
- }
389
- if (hasDirectives) (0, internals_exports.onElementMounted)(el);
390
- if (vnode.props.ref) {
391
- if (typeof vnode.props.ref === "function") vnode.props.ref(el);
392
- else if (typeof vnode.props.ref === "object") vnode.props.ref.current = el;
393
- }
394
- }
395
- let childDom = el.firstChild;
396
- for (const child of vnode.children) childDom = hydrateNode(child, childDom, el);
397
- if (vnode.type === "select" && vnode.props) fixSelectValue(el, vnode.props);
398
- return el.nextSibling;
399
- }
400
- return dom;
401
- }
402
- /**
403
- * Fix select element value after hydrating children.
404
- * This is needed because <select>.value only works after <option> children exist in DOM.
405
- */
406
- function fixSelectValue(dom, props) {
407
- if (dom.tagName === "SELECT" && "value" in props) {
408
- const val = props.value;
409
- if (dom.multiple) {
410
- const options = dom.options;
411
- const valArray = Array.isArray(val) ? val : [val];
412
- for (let i = 0; i < options.length; i++) options[i].selected = valArray.includes(options[i].value);
413
- } else dom.value = String(val);
414
- }
415
- }
416
- //#endregion
417
- //#region src/client/plugin.ts
418
- /**
419
- * SSR Client Plugin
420
- *
421
- * Adds the hydrate() method to the app instance for client-side hydration.
422
- * Also registers the SSR context extension for all components.
423
- *
424
- * @example
425
- * ```tsx
426
- * import { defineApp } from 'sigx';
427
- * import { ssrClientPlugin } from '@sigx/server-renderer/client';
428
- *
429
- * const app = defineApp(<App />);
430
- * app.use(ssrClientPlugin)
431
- * .use(router)
432
- * .hydrate('#app');
433
- * ```
434
- */
435
- var ssrClientPlugin = {
436
- name: "@sigx/server-renderer/client",
437
- install(app) {
438
- app.hydrate = function(container) {
439
- const resolvedContainer = typeof container === "string" ? document.querySelector(container) : container;
440
- if (!resolvedContainer) throw new Error(`[ssrClientPlugin] Cannot find container: ${container}. Make sure the element exists in the DOM before calling hydrate().`);
441
- const rootComponent = app._rootComponent;
442
- if (!rootComponent) throw new Error("[ssrClientPlugin] No root component found on app. Make sure you created the app with defineApp(<Component />).");
443
- const hasSSRContent = resolvedContainer.firstElementChild !== null || resolvedContainer.firstChild !== null && resolvedContainer.firstChild.nodeType !== Node.COMMENT_NODE;
444
- const appContext = app._context;
445
- if (hasSSRContent) hydrate(rootComponent, resolvedContainer, appContext);
446
- else render(rootComponent, resolvedContainer, appContext);
447
- resolvedContainer._app = app;
448
- return app;
449
- };
450
- }
451
- };
452
- //#endregion
453
- export { clearClientPlugins as a, getCurrentAppContext as c, setPendingServerState as d, hydrateComponent as i, registerClientPlugin as l, hydrate as n, createRestoringSignal as o, hydrateNode as r, getClientPlugins as s, ssrClientPlugin as t, setCurrentAppContext as u };
454
-
455
- //# sourceMappingURL=client-CL2CTcTJ.js.map