@llui/vike 0.0.15 → 0.0.17
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/README.md +170 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/on-render-client.d.ts +107 -53
- package/dist/on-render-client.d.ts.map +1 -1
- package/dist/on-render-client.js +320 -52
- package/dist/on-render-client.js.map +1 -1
- package/dist/on-render-html.d.ts +51 -16
- package/dist/on-render-html.d.ts.map +1 -1
- package/dist/on-render-html.js +128 -20
- package/dist/on-render-html.js.map +1 -1
- package/dist/page-slot.d.ts +67 -0
- package/dist/page-slot.d.ts.map +1 -0
- package/dist/page-slot.js +81 -0
- package/dist/page-slot.js.map +1 -0
- package/package.json +2 -2
package/dist/on-render-client.js
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
import { hydrateApp, mountApp } from '@llui/dom';
|
|
2
|
+
import { _consumePendingSlot, _resetPendingSlot } from './page-slot.js';
|
|
3
|
+
// Re-exported so `@llui/vike/client` is a one-stop-shop for everything
|
|
4
|
+
// a pages/+onRenderClient.ts / +Layout.ts file needs.
|
|
5
|
+
export { pageSlot } from './page-slot.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the layout chain for a given pageContext. A single layout
|
|
8
|
+
* becomes a one-element chain; a function resolver gives callers full
|
|
9
|
+
* control to return different chains for different routes (e.g. nested
|
|
10
|
+
* layouts keyed on Vike's `pageContext.urlPathname`).
|
|
11
|
+
*/
|
|
12
|
+
function resolveLayoutChain(layoutOption, pageContext) {
|
|
13
|
+
if (!layoutOption)
|
|
14
|
+
return [];
|
|
15
|
+
if (typeof layoutOption === 'function') {
|
|
16
|
+
return layoutOption(pageContext) ?? [];
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(layoutOption))
|
|
19
|
+
return layoutOption;
|
|
20
|
+
return [layoutOption];
|
|
21
|
+
}
|
|
2
22
|
/**
|
|
3
23
|
* Adapt a `TransitionOptions` object (e.g. the output of
|
|
4
|
-
* `routeTransition()` from `@llui/transitions`, or
|
|
5
|
-
*
|
|
6
|
-
*
|
|
24
|
+
* `routeTransition()` from `@llui/transitions`, or a preset like `fade`
|
|
25
|
+
* / `slide`) into the `onLeave` / `onEnter` pair expected by
|
|
26
|
+
* `createOnRenderClient`.
|
|
7
27
|
*
|
|
8
|
-
* ```
|
|
28
|
+
* ```ts
|
|
9
29
|
* import { createOnRenderClient, fromTransition } from '@llui/vike/client'
|
|
10
30
|
* import { routeTransition } from '@llui/transitions'
|
|
11
31
|
*
|
|
12
32
|
* export const onRenderClient = createOnRenderClient({
|
|
33
|
+
* Layout: AppLayout,
|
|
13
34
|
* ...fromTransition(routeTransition({ duration: 200 })),
|
|
14
35
|
* })
|
|
15
36
|
* ```
|
|
16
37
|
*
|
|
17
|
-
* The transition operates on the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* when the new page mounts; use `enter` to reset them explicitly or
|
|
22
|
-
* pick presets that self-clean.
|
|
38
|
+
* The transition operates on the slot element — in a no-layout setup,
|
|
39
|
+
* the root container; in a layout setup, the innermost surviving
|
|
40
|
+
* layer's `pageSlot()` element. Opacity / transform fades apply to the
|
|
41
|
+
* outgoing page content, then the new page fades in.
|
|
23
42
|
*/
|
|
24
43
|
export function fromTransition(t) {
|
|
25
44
|
return {
|
|
@@ -38,40 +57,62 @@ export function fromTransition(t) {
|
|
|
38
57
|
: undefined,
|
|
39
58
|
};
|
|
40
59
|
}
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
let
|
|
60
|
+
// Live chain of mounted layers. Module-level state: there's exactly
|
|
61
|
+
// one chain per Vike-managed app per page load.
|
|
62
|
+
let chainHandles = [];
|
|
44
63
|
/**
|
|
45
|
-
* @internal — test helper. Disposes
|
|
46
|
-
* the module
|
|
47
|
-
* Not part of the public API; subject to change without notice.
|
|
64
|
+
* @internal — test helper. Disposes every layer in the current chain
|
|
65
|
+
* and clears the module state so subsequent calls behave as a first
|
|
66
|
+
* mount. Not part of the public API; subject to change without notice.
|
|
48
67
|
*/
|
|
49
|
-
export function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
export function _resetChainForTest() {
|
|
69
|
+
// Dispose innermost-first to match the normal teardown path.
|
|
70
|
+
for (let i = chainHandles.length - 1; i >= 0; i--) {
|
|
71
|
+
chainHandles[i].handle.dispose();
|
|
53
72
|
}
|
|
73
|
+
chainHandles = [];
|
|
74
|
+
_resetPendingSlot();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Back-compat alias for the pre-layout test helper name.
|
|
78
|
+
* @internal
|
|
79
|
+
* @deprecated — use `_resetChainForTest` instead.
|
|
80
|
+
*/
|
|
81
|
+
export function _resetCurrentHandleForTest() {
|
|
82
|
+
_resetChainForTest();
|
|
54
83
|
}
|
|
55
84
|
/**
|
|
56
|
-
* Default onRenderClient hook — no animation hooks. Hydrates
|
|
57
|
-
*
|
|
85
|
+
* Default onRenderClient hook — no layout, no animation hooks. Hydrates
|
|
86
|
+
* on first load, mounts fresh on subsequent navs. Use `createOnRenderClient`
|
|
58
87
|
* for the customizable factory form.
|
|
59
88
|
*/
|
|
60
89
|
export async function onRenderClient(pageContext) {
|
|
61
90
|
await renderClient(pageContext, {});
|
|
62
91
|
}
|
|
63
92
|
/**
|
|
64
|
-
* Factory to create a customized onRenderClient hook.
|
|
93
|
+
* Factory to create a customized onRenderClient hook. See `RenderClientOptions`
|
|
94
|
+
* for the full option surface — this is the entry point for persistent
|
|
95
|
+
* layouts, route transitions, and lifecycle hooks.
|
|
65
96
|
*
|
|
66
|
-
*
|
|
97
|
+
* **Do not name your layout file `+Layout.ts`.** Vike reserves the `+`
|
|
98
|
+
* prefix for its own framework config conventions, and `+Layout.ts` is
|
|
99
|
+
* interpreted by `vike-react` / `vike-vue` / `vike-solid` framework
|
|
100
|
+
* adapters as a native layout config. `@llui/vike` isn't a framework
|
|
101
|
+
* adapter in that sense — it's a render adapter, and `createOnRenderClient`
|
|
102
|
+
* consumes the layout component directly via the `Layout` option. Name
|
|
103
|
+
* the file `Layout.ts`, `app-layout.ts`, or anywhere outside `/pages`
|
|
104
|
+
* that Vike won't scan, and import it here by path.
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
67
107
|
* // pages/+onRenderClient.ts
|
|
68
|
-
* import { createOnRenderClient } from '@llui/vike/client'
|
|
108
|
+
* import { createOnRenderClient, fromTransition } from '@llui/vike/client'
|
|
109
|
+
* import { routeTransition } from '@llui/transitions'
|
|
110
|
+
* import { AppLayout } from './Layout' // ← NOT './+Layout'
|
|
69
111
|
*
|
|
70
112
|
* export const onRenderClient = createOnRenderClient({
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* onMount: () => console.log('Page ready'),
|
|
113
|
+
* Layout: AppLayout,
|
|
114
|
+
* ...fromTransition(routeTransition({ duration: 200 })),
|
|
115
|
+
* onMount: () => console.log('page rendered'),
|
|
75
116
|
* })
|
|
76
117
|
* ```
|
|
77
118
|
*/
|
|
@@ -79,37 +120,264 @@ export function createOnRenderClient(options) {
|
|
|
79
120
|
return (pageContext) => renderClient(pageContext, options);
|
|
80
121
|
}
|
|
81
122
|
async function renderClient(pageContext, options) {
|
|
82
|
-
const { Page } = pageContext;
|
|
83
123
|
const selector = options.container ?? '#app';
|
|
84
124
|
const container = document.querySelector(selector);
|
|
85
125
|
if (!container) {
|
|
86
126
|
throw new Error(`@llui/vike: container "${selector}" not found in DOM`);
|
|
87
127
|
}
|
|
88
|
-
const
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
const rootEl = container;
|
|
129
|
+
// Resolve the chain for this render. The page component is always
|
|
130
|
+
// the innermost entry, regardless of layout configuration.
|
|
131
|
+
const layoutChain = resolveLayoutChain(options.Layout, pageContext);
|
|
132
|
+
const layoutData = pageContext.lluiLayoutData ?? [];
|
|
133
|
+
const newChain = [...layoutChain, pageContext.Page];
|
|
134
|
+
const newChainData = [...layoutData, pageContext.data];
|
|
135
|
+
if (pageContext.isHydration) {
|
|
136
|
+
// First load — the chain starts empty and we hydrate every layer
|
|
137
|
+
// against server-rendered HTML. No onLeave/onEnter on hydration.
|
|
138
|
+
await mountOrHydrateChain(newChain, newChainData, rootEl, {
|
|
139
|
+
mode: 'hydrate',
|
|
140
|
+
serverStateEnvelope: window.__LLUI_STATE__,
|
|
141
|
+
});
|
|
142
|
+
options.onMount?.();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Subsequent nav — diff the chain to find the divergent suffix.
|
|
146
|
+
let firstMismatch = 0;
|
|
147
|
+
const minLen = Math.min(chainHandles.length, newChain.length);
|
|
148
|
+
while (firstMismatch < minLen && chainHandles[firstMismatch].def === newChain[firstMismatch]) {
|
|
149
|
+
firstMismatch++;
|
|
150
|
+
}
|
|
151
|
+
// Push fresh data into surviving layers (layers in the shared prefix).
|
|
152
|
+
// Without this, persistent layouts can't react to nav-driven data
|
|
153
|
+
// changes — pathname, breadcrumbs, session, nav-highlight state all
|
|
154
|
+
// belong to the layout but change on every client navigation. Each
|
|
155
|
+
// surviving layer's def can opt in via `propsMsg(data) => Msg`; we
|
|
156
|
+
// dispatch the resulting message through the handle's `send` so the
|
|
157
|
+
// layout's update loop processes it like any other state change.
|
|
158
|
+
//
|
|
159
|
+
// Diff is shallow-key Object.is on record-shaped data, falling back
|
|
160
|
+
// to whole-value Object.is for primitives / non-records. This matches
|
|
161
|
+
// child()'s prop-diff behavior, which is what the report asked us to
|
|
162
|
+
// mirror. Layers without `propsMsg` are skipped silently — opt-in.
|
|
163
|
+
for (let i = 0; i < firstMismatch; i++) {
|
|
164
|
+
const entry = chainHandles[i];
|
|
165
|
+
const newData = newChainData[i];
|
|
166
|
+
if (!hasDataChanged(entry.data, newData))
|
|
167
|
+
continue;
|
|
168
|
+
entry.data = newData;
|
|
169
|
+
const propsMsg = entry.def.propsMsg;
|
|
170
|
+
if (typeof propsMsg !== 'function')
|
|
171
|
+
continue;
|
|
172
|
+
const msg = propsMsg(newData);
|
|
173
|
+
if (msg !== null && msg !== undefined) {
|
|
174
|
+
entry.handle.send(msg);
|
|
96
175
|
}
|
|
97
|
-
currentHandle.dispose();
|
|
98
|
-
currentHandle = null;
|
|
99
176
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
177
|
+
// Find the slot element whose contents will change. Shared prefix =
|
|
178
|
+
// everything before firstMismatch. The slot we're about to replace
|
|
179
|
+
// content in sits in the layer at firstMismatch - 1 (if any);
|
|
180
|
+
// otherwise we're swapping the whole app at the root container.
|
|
181
|
+
const leaveTarget = firstMismatch === 0 ? rootEl : (chainHandles[firstMismatch - 1].slotMarker ?? rootEl);
|
|
182
|
+
// If everything matches (same chain end-to-end with same defs), this
|
|
183
|
+
// is effectively a no-op nav — the page def hasn't changed. We still
|
|
184
|
+
// fire onMount so callers can run per-render side effects, and the
|
|
185
|
+
// surviving-layer data updates above already ran. But there's nothing
|
|
186
|
+
// to dispose or mount, so skip the rest of the work.
|
|
187
|
+
const isNoOp = firstMismatch === chainHandles.length && firstMismatch === newChain.length;
|
|
188
|
+
if (isNoOp) {
|
|
189
|
+
options.onMount?.();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// onLeave runs BEFORE any teardown. Outgoing DOM still mounted here.
|
|
193
|
+
// Skip on the very first mount — there's no outgoing page to leave.
|
|
194
|
+
const isFirstMount = chainHandles.length === 0;
|
|
195
|
+
if (options.onLeave && !isFirstMount) {
|
|
196
|
+
await options.onLeave(leaveTarget);
|
|
112
197
|
}
|
|
198
|
+
// Dispose the divergent suffix, innermost first. Each handle.dispose()
|
|
199
|
+
// calls disposeScope on that layer's rootScope, which cascades through
|
|
200
|
+
// every child scope the layer owned (bindings, portals, onMount
|
|
201
|
+
// cleanups, dialog focus traps, etc.). The surviving layers are
|
|
202
|
+
// untouched because their scopes live above the disposal roots.
|
|
203
|
+
for (let i = chainHandles.length - 1; i >= firstMismatch; i--) {
|
|
204
|
+
chainHandles[i].handle.dispose();
|
|
205
|
+
}
|
|
206
|
+
chainHandles = chainHandles.slice(0, firstMismatch);
|
|
207
|
+
// Clear the slot element before mounting the new suffix. handle.dispose()
|
|
208
|
+
// above already did this for the innermost layer's container, but the
|
|
209
|
+
// slot at firstMismatch - 1 keeps its marker element (it's owned by the
|
|
210
|
+
// surviving layer) and we mount fresh children into it.
|
|
211
|
+
leaveTarget.textContent = '';
|
|
212
|
+
// Mount the new suffix starting at firstMismatch.
|
|
213
|
+
const parentScope = firstMismatch === 0 ? undefined : (chainHandles[firstMismatch - 1].slotScope ?? undefined);
|
|
214
|
+
mountChainSuffix(newChain, newChainData, firstMismatch, leaveTarget, parentScope, {
|
|
215
|
+
mode: 'mount',
|
|
216
|
+
});
|
|
217
|
+
// onEnter fires after the new suffix is in place. Fire-and-forget.
|
|
218
|
+
options.onEnter?.(leaveTarget);
|
|
113
219
|
options.onMount?.();
|
|
114
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Walk the full chain for the first mount or hydration. Starts from
|
|
223
|
+
* depth 0 at the root container, threads each layer's slot into the
|
|
224
|
+
* next layer's mount target + parentScope.
|
|
225
|
+
*/
|
|
226
|
+
async function mountOrHydrateChain(chain, chainData, rootEl, opts) {
|
|
227
|
+
mountChainSuffix(chain, chainData, 0, rootEl, undefined, opts);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Mount (or hydrate) `chain[startAt..end]` into `initialTarget`, with
|
|
231
|
+
* the initial layer's rootScope parented at `initialParentScope`.
|
|
232
|
+
* Threads slot → next-target → next-parentScope through the chain.
|
|
233
|
+
*
|
|
234
|
+
* Fails loudly if a non-innermost layer forgot to call `pageSlot()`,
|
|
235
|
+
* or if the innermost layer called `pageSlot()` unnecessarily.
|
|
236
|
+
*/
|
|
237
|
+
function mountChainSuffix(chain, chainData, startAt, initialTarget, initialParentScope, opts) {
|
|
238
|
+
let mountTarget = initialTarget;
|
|
239
|
+
let parentScope = initialParentScope;
|
|
240
|
+
for (let i = startAt; i < chain.length; i++) {
|
|
241
|
+
const def = chain[i];
|
|
242
|
+
const layerData = chainData[i];
|
|
243
|
+
const isInnermost = i === chain.length - 1;
|
|
244
|
+
// Defensive: clear any stale slot from a prior failed mount.
|
|
245
|
+
_resetPendingSlot();
|
|
246
|
+
let handle;
|
|
247
|
+
if (opts.mode === 'hydrate') {
|
|
248
|
+
// Hydration envelope: each layer pulls its own state slice. The
|
|
249
|
+
// envelope shape is `{ layouts: [...], page: {...} }` with each
|
|
250
|
+
// entry carrying `{ name, state }`. We match by name so a server/
|
|
251
|
+
// client mismatch throws with a clear error instead of silently
|
|
252
|
+
// hydrating the wrong state into the wrong instance.
|
|
253
|
+
const layerState = extractHydrationState(opts.serverStateEnvelope, i, chain.length, def);
|
|
254
|
+
// Cross from the type-erased AnyComponentDef back into a concrete
|
|
255
|
+
// ComponentDef<unknown, unknown, unknown, unknown> for the mount
|
|
256
|
+
// primitive's signature. The cast is safe — mountApp / hydrateApp
|
|
257
|
+
// don't use the type parameters at runtime, they just thread the
|
|
258
|
+
// def through createComponentInstance which accesses init/update/
|
|
259
|
+
// view by field name. AnyComponentDef has the same field shape.
|
|
260
|
+
handle = hydrateApp(mountTarget, def, layerState, { parentScope });
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
handle = mountApp(mountTarget, def, layerData, {
|
|
264
|
+
parentScope,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const slot = _consumePendingSlot();
|
|
268
|
+
if (isInnermost && slot !== null) {
|
|
269
|
+
// Innermost layer declared a slot with nothing to fill it —
|
|
270
|
+
// probably a misuse of pageSlot() in the page component itself.
|
|
271
|
+
handle.dispose();
|
|
272
|
+
throw new Error(`[llui/vike] <${def.name}> is the innermost component in the chain ` +
|
|
273
|
+
`but called pageSlot(). pageSlot() only belongs in layout components ` +
|
|
274
|
+
`that wrap a nested page or layout — not in the page itself.`);
|
|
275
|
+
}
|
|
276
|
+
if (!isInnermost && slot === null) {
|
|
277
|
+
// Non-innermost layer didn't declare a slot — there's nowhere to
|
|
278
|
+
// mount the remaining chain.
|
|
279
|
+
handle.dispose();
|
|
280
|
+
throw new Error(`[llui/vike] <${def.name}> is a layout layer at depth ${i} but did not ` +
|
|
281
|
+
`call pageSlot() in its view(). There are ${chain.length - i - 1} more ` +
|
|
282
|
+
`layer(s) to mount and no slot to mount them into. Add pageSlot() from ` +
|
|
283
|
+
`@llui/vike/client to the view at the position where nested content renders.`);
|
|
284
|
+
}
|
|
285
|
+
chainHandles.push({
|
|
286
|
+
def,
|
|
287
|
+
handle,
|
|
288
|
+
slotMarker: slot?.marker ?? null,
|
|
289
|
+
slotScope: slot?.slotScope ?? null,
|
|
290
|
+
data: layerData,
|
|
291
|
+
});
|
|
292
|
+
if (slot !== null) {
|
|
293
|
+
mountTarget = slot.marker;
|
|
294
|
+
parentScope = slot.slotScope;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Pull the per-layer state from the hydration envelope. Supports both
|
|
300
|
+
* the new chain-aware shape (`{ layouts: [...], page: {...} }`) and the
|
|
301
|
+
* legacy flat shape (`window.__LLUI_STATE__` is the state object itself)
|
|
302
|
+
* for backward compatibility with pages written against 0.0.15 or earlier.
|
|
303
|
+
*
|
|
304
|
+
* Throws on envelope shape mismatch — missing entries, wrong component
|
|
305
|
+
* name at a given index — so server/client drift fails loud instead of
|
|
306
|
+
* silently binding the wrong state to the wrong instance.
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* Shallow-key data diff for the persistent-layer prop-update path.
|
|
310
|
+
* Returns true when `next` differs from `prev` enough to warrant
|
|
311
|
+
* dispatching a `propsMsg`. Mirrors `child()`'s prop-diff semantics:
|
|
312
|
+
*
|
|
313
|
+
* - `Object.is(prev, next)` short-circuits identical references.
|
|
314
|
+
* - For two plain-object records, walks the union of keys and returns
|
|
315
|
+
* true on the first `Object.is` mismatch.
|
|
316
|
+
* - For anything else (primitives, arrays, class instances), falls
|
|
317
|
+
* back to the top-level `Object.is` result — covers the cases where
|
|
318
|
+
* the host populates `lluiLayoutData[i]` with a primitive or a
|
|
319
|
+
* referentially-stable object.
|
|
320
|
+
*/
|
|
321
|
+
function hasDataChanged(prev, next) {
|
|
322
|
+
if (Object.is(prev, next))
|
|
323
|
+
return false;
|
|
324
|
+
// Both must be plain object records to do a key walk; otherwise the
|
|
325
|
+
// Object.is above is the only signal.
|
|
326
|
+
if (prev === null ||
|
|
327
|
+
next === null ||
|
|
328
|
+
typeof prev !== 'object' ||
|
|
329
|
+
typeof next !== 'object' ||
|
|
330
|
+
Array.isArray(prev) ||
|
|
331
|
+
Array.isArray(next)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
const prevRec = prev;
|
|
335
|
+
const nextRec = next;
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
for (const k of Object.keys(prevRec)) {
|
|
338
|
+
seen.add(k);
|
|
339
|
+
if (!Object.is(prevRec[k], nextRec[k]))
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
for (const k of Object.keys(nextRec)) {
|
|
343
|
+
if (!seen.has(k))
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
function extractHydrationState(envelope, layerIndex, chainLength, def) {
|
|
349
|
+
// Legacy flat envelope — no layout chain at render time. Only valid
|
|
350
|
+
// when the chain has a single layer (the page).
|
|
351
|
+
const isLegacyFlat = envelope !== null &&
|
|
352
|
+
typeof envelope === 'object' &&
|
|
353
|
+
!('layouts' in envelope) &&
|
|
354
|
+
!('page' in envelope);
|
|
355
|
+
if (isLegacyFlat) {
|
|
356
|
+
if (chainLength !== 1) {
|
|
357
|
+
throw new Error(`[llui/vike] Hydration envelope is in the legacy flat shape but the ` +
|
|
358
|
+
`current render has ${chainLength} chain layers. The server must emit ` +
|
|
359
|
+
`the chain-aware shape ({ layouts, page }) when rendering with a layout.`);
|
|
360
|
+
}
|
|
361
|
+
return envelope;
|
|
362
|
+
}
|
|
363
|
+
const chainEnvelope = envelope;
|
|
364
|
+
if (!chainEnvelope) {
|
|
365
|
+
throw new Error(`[llui/vike] Hydration envelope is missing. Server-side onRenderHtml must ` +
|
|
366
|
+
`populate window.__LLUI_STATE__ with the full chain before client hydration.`);
|
|
367
|
+
}
|
|
368
|
+
const isPageLayer = layerIndex === chainLength - 1;
|
|
369
|
+
const layoutEntries = chainEnvelope.layouts ?? [];
|
|
370
|
+
const expected = isPageLayer ? chainEnvelope.page : layoutEntries[layerIndex];
|
|
371
|
+
if (!expected) {
|
|
372
|
+
throw new Error(`[llui/vike] Hydration envelope has no entry for chain layer ${layerIndex} ` +
|
|
373
|
+
`(<${def.name}>). Server rendered ${layoutEntries.length} layouts + ${chainEnvelope.page ? 'a page' : 'no page'}, client expected ${chainLength} total entries.`);
|
|
374
|
+
}
|
|
375
|
+
if (expected.name !== def.name) {
|
|
376
|
+
throw new Error(`[llui/vike] Hydration mismatch at chain layer ${layerIndex}: server ` +
|
|
377
|
+
`rendered <${expected.name}> but client is trying to hydrate <${def.name}>. ` +
|
|
378
|
+
`This usually means the layout chain resolver returns different layouts ` +
|
|
379
|
+
`on the server and the client for the same route.`);
|
|
380
|
+
}
|
|
381
|
+
return expected.state;
|
|
382
|
+
}
|
|
115
383
|
//# sourceMappingURL=on-render-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { hydrateApp, mountApp } from '@llui/dom'\nimport type { ComponentDef, AppHandle, TransitionOptions } from '@llui/dom'\n\ndeclare global {\n interface Window {\n __LLUI_STATE__?: unknown\n }\n}\n\nexport interface ClientPageContext {\n Page: ComponentDef<unknown, unknown, unknown, unknown>\n data?: unknown\n isHydration?: boolean\n}\n\n/**\n * Page-lifecycle hooks that fire around the dispose → clear → mount\n * sequence on client navigation. Use these to animate page transitions,\n * save scroll state, emit analytics events, or defer the swap behind\n * any async work that must complete before the next page appears.\n *\n * The sequence is:\n *\n * ```\n * client nav triggered\n * │\n * ▼\n * onLeave(el) ← awaited if it returns a promise\n * │ (the outgoing page's DOM is still mounted here)\n * ▼\n * currentHandle.dispose()\n * │ (all scopes torn down — portals, focus traps,\n * │ onMount cleanups all fire synchronously here)\n * ▼\n * el.textContent = ''\n * │ (old DOM removed)\n * ▼\n * mountApp(el, Page, data)\n * │ (new page mounted)\n * ▼\n * onEnter(el) ← not awaited; animate in-place\n * │\n * ▼\n * onMount() ← legacy shim, still fires last\n * ```\n *\n * On the initial render (hydration), `onLeave` and `onEnter` are NOT\n * called — there's no outgoing page to leave and no animation to enter.\n * If you need to run code after hydration, use `onMount`.\n */\nexport interface RenderClientOptions {\n /** CSS selector for the mount container. Default: '#app' */\n container?: string\n\n /**\n * Called on the outgoing page's container BEFORE dispose + clear + mount.\n * Return a promise to defer the swap until the leave animation finishes.\n * The container element is passed as the argument — its children are\n * still the previous page's DOM at this point.\n *\n * Not called on the initial hydration render.\n */\n onLeave?: (el: HTMLElement) => void | Promise<void>\n\n /**\n * Called after the new page is mounted into the container. Use this to\n * kick off an enter animation on the freshly-rendered content. Not\n * awaited — if you return a promise, the resolution is ignored.\n *\n * Not called on the initial hydration render.\n */\n onEnter?: (el: HTMLElement) => void\n\n /**\n * Called after mount or hydration completes. Fires on every render\n * including the initial hydration. Use this for per-render side\n * effects that don't fit the animation hooks (analytics, focus\n * management, etc.).\n */\n onMount?: () => void\n}\n\n/**\n * Adapt a `TransitionOptions` object (e.g. the output of\n * `routeTransition()` from `@llui/transitions`, or any preset like\n * `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected\n * by `createOnRenderClient`.\n *\n * ```typescript\n * import { createOnRenderClient, fromTransition } from '@llui/vike/client'\n * import { routeTransition } from '@llui/transitions'\n *\n * export const onRenderClient = createOnRenderClient({\n * ...fromTransition(routeTransition({ duration: 200 })),\n * })\n * ```\n *\n * The transition operates on the container element itself — its\n * opacity / transform fades out the outgoing page, then the new page\n * fades in when it mounts. If the preset doesn't restore its starting\n * style on `leave`, the container may still carry leftover properties\n * when the new page mounts; use `enter` to reset them explicitly or\n * pick presets that self-clean.\n */\nexport function fromTransition(\n t: TransitionOptions,\n): Pick<RenderClientOptions, 'onLeave' | 'onEnter'> {\n return {\n onLeave: t.leave\n ? (el): void | Promise<void> => {\n const result = t.leave!([el])\n return result && typeof (result as Promise<void>).then === 'function'\n ? (result as Promise<void>)\n : undefined\n }\n : undefined,\n onEnter: t.enter\n ? (el): void => {\n t.enter!([el])\n }\n : undefined,\n }\n}\n\n// Track the current app handle so we can dispose it on client navigation.\n// Module-level state: there's exactly one Vike-managed app per page load.\nlet currentHandle: AppHandle | null = null\n\n/**\n * @internal — test helper. Disposes the current handle (if any) and clears\n * the module-level state so subsequent calls behave as a first mount.\n * Not part of the public API; subject to change without notice.\n */\nexport function _resetCurrentHandleForTest(): void {\n if (currentHandle) {\n currentHandle.dispose()\n currentHandle = null\n }\n}\n\n/**\n * Default onRenderClient hook — no animation hooks. Hydrates if\n * `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`\n * for the customizable factory form.\n */\nexport async function onRenderClient(pageContext: ClientPageContext): Promise<void> {\n await renderClient(pageContext, {})\n}\n\n/**\n * Factory to create a customized onRenderClient hook.\n *\n * ```typescript\n * // pages/+onRenderClient.ts\n * import { createOnRenderClient } from '@llui/vike/client'\n *\n * export const onRenderClient = createOnRenderClient({\n * container: '#root',\n * onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,\n * onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),\n * onMount: () => console.log('Page ready'),\n * })\n * ```\n */\nexport function createOnRenderClient(\n options: RenderClientOptions,\n): (pageContext: ClientPageContext) => Promise<void> {\n return (pageContext) => renderClient(pageContext, options)\n}\n\nasync function renderClient(\n pageContext: ClientPageContext,\n options: RenderClientOptions,\n): Promise<void> {\n const { Page } = pageContext\n const selector = options.container ?? '#app'\n const container = document.querySelector(selector)\n\n if (!container) {\n throw new Error(`@llui/vike: container \"${selector}\" not found in DOM`)\n }\n\n const el = container as HTMLElement\n\n // Dispose the previous page's component on client navigation. If the\n // caller supplied an onLeave hook and this isn't the initial hydration,\n // await it BEFORE tearing down — that's the only moment where the\n // outgoing page's DOM still exists for an animation to read/write.\n if (currentHandle) {\n if (!pageContext.isHydration && options.onLeave) {\n await options.onLeave(el)\n }\n currentHandle.dispose()\n currentHandle = null\n }\n\n if (pageContext.isHydration) {\n const serverState = window.__LLUI_STATE__\n currentHandle = hydrateApp(el, Page, serverState)\n } else {\n // Clear old DOM before mounting the new page\n el.textContent = ''\n currentHandle = mountApp(el, Page, pageContext.data)\n // onEnter fires AFTER mount so the hook can animate the freshly\n // rendered children. It's intentionally sync — a promise return is\n // ignored, matching typical enter-animation ergonomics (fire-and-forget).\n options.onEnter?.(el)\n }\n\n options.onMount?.()\n}\n"]}
|
|
1
|
+
{"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { hydrateApp, mountApp } from '@llui/dom'\nimport type { AnyComponentDef, AppHandle, TransitionOptions, Scope } from '@llui/dom'\nimport { _consumePendingSlot, _resetPendingSlot } from './page-slot.js'\n\n// Re-exported so `@llui/vike/client` is a one-stop-shop for everything\n// a pages/+onRenderClient.ts / +Layout.ts file needs.\nexport { pageSlot } from './page-slot.js'\n\ndeclare global {\n interface Window {\n __LLUI_STATE__?: unknown\n }\n}\n\n/**\n * Page context shape as seen by `@llui/vike`'s client-side hooks. The\n * `Page` and `data` fields come from whichever `+Page.ts` and `+data.ts`\n * Vike resolved for the current route.\n *\n * `lluiLayoutData` is optional and carries per-layer data for the layout\n * chain configured via `createOnRenderClient({ Layout })`. It's indexed\n * outermost-to-innermost, one entry per layout layer. Absent entries\n * mean the corresponding layout's `init()` receives `undefined`. Users\n * wire this from their Vike `+data.ts` files by merging layout-owned\n * data under the `lluiLayoutData` key.\n */\nexport interface ClientPageContext {\n Page: AnyComponentDef\n data?: unknown\n lluiLayoutData?: readonly unknown[]\n isHydration?: boolean\n}\n\ntype LayoutChain = ReadonlyArray<AnyComponentDef>\n\n/**\n * Resolves the layout chain for a given pageContext. A single layout\n * becomes a one-element chain; a function resolver gives callers full\n * control to return different chains for different routes (e.g. nested\n * layouts keyed on Vike's `pageContext.urlPathname`).\n */\nfunction resolveLayoutChain(\n layoutOption: RenderClientOptions['Layout'],\n pageContext: ClientPageContext,\n): LayoutChain {\n if (!layoutOption) return []\n if (typeof layoutOption === 'function') {\n return layoutOption(pageContext) ?? []\n }\n if (Array.isArray(layoutOption)) return layoutOption\n return [layoutOption as AnyComponentDef]\n}\n\n/**\n * Page-lifecycle hooks that fire around the dispose → mount cycle on\n * client navigation. With persistent layouts in play the cycle only\n * tears down the *divergent* suffix of the layout chain — any layers\n * shared between the old and new routes stay mounted.\n *\n * Navigation sequence for an already-mounted app:\n *\n * ```\n * client nav triggered\n * │\n * ▼\n * compare old chain to new chain → find first mismatch index K\n * │\n * ▼\n * onLeave(leaveTarget) ← awaited; leaveTarget is the slot element\n * │ at depth K-1 (or the root container if K=0)\n * │ whose contents are about to be replaced\n * ▼\n * dispose chainHandles[K..end] innermost first\n * │\n * ▼\n * leaveTarget.textContent = ''\n * │\n * ▼\n * mount newChain[K..end] into leaveTarget, outermost first\n * │\n * ▼\n * onEnter(leaveTarget) ← fire-and-forget; fresh DOM in place\n * │\n * ▼\n * onMount()\n * ```\n *\n * On the initial hydration render, `onLeave` and `onEnter` are NOT\n * called — there's no outgoing page to leave and no animation to enter.\n * Use `onMount` for code that should run on every render including the\n * initial one.\n */\nexport interface RenderClientOptions {\n /** CSS selector for the mount container. Default: `'#app'`. */\n container?: string\n\n /**\n * Persistent layout chain. One of:\n *\n * - A single `ComponentDef` — becomes a one-layout chain.\n * - An array of `ComponentDef`s — outermost layout first, innermost\n * layout last. Every layer except the innermost must call\n * `pageSlot()` in its view to declare where nested content renders.\n * - A function that returns a chain from the current `pageContext` —\n * lets different routes use different chains, e.g. by reading\n * Vike's `pageContext.urlPathname` or `pageContext.config.Layout`.\n *\n * Layers that are shared between the previous and next navigation\n * stay mounted. Only the divergent suffix is disposed and re-mounted.\n * Dialogs, focus traps, and effect subscriptions rooted in a surviving\n * layer are unaffected by the nav.\n */\n Layout?: AnyComponentDef | LayoutChain | ((pageContext: ClientPageContext) => LayoutChain)\n\n /**\n * Called on the slot element whose contents are about to be replaced,\n * BEFORE the divergent suffix is disposed and re-mounted. The slot's\n * current DOM is still attached when this runs — the only moment a\n * leave animation can read/write it. Return a promise to defer the\n * swap until the animation completes.\n *\n * For a plain no-layout setup, the slot element is the root container.\n * Not called on the initial hydration render.\n */\n onLeave?: (el: HTMLElement) => void | Promise<void>\n\n /**\n * Called after the new divergent suffix is mounted, on the same slot\n * element that was passed to `onLeave`. Use this to kick off an enter\n * animation. Fire-and-forget — promise returns are ignored.\n *\n * Not called on the initial hydration render.\n */\n onEnter?: (el: HTMLElement) => void\n\n /**\n * Called after mount or hydration completes. Fires on every render\n * including the initial hydration. Use for per-render side effects\n * that don't fit the animation hooks.\n */\n onMount?: () => void\n}\n\n/**\n * Adapt a `TransitionOptions` object (e.g. the output of\n * `routeTransition()` from `@llui/transitions`, or a preset like `fade`\n * / `slide`) into the `onLeave` / `onEnter` pair expected by\n * `createOnRenderClient`.\n *\n * ```ts\n * import { createOnRenderClient, fromTransition } from '@llui/vike/client'\n * import { routeTransition } from '@llui/transitions'\n *\n * export const onRenderClient = createOnRenderClient({\n * Layout: AppLayout,\n * ...fromTransition(routeTransition({ duration: 200 })),\n * })\n * ```\n *\n * The transition operates on the slot element — in a no-layout setup,\n * the root container; in a layout setup, the innermost surviving\n * layer's `pageSlot()` element. Opacity / transform fades apply to the\n * outgoing page content, then the new page fades in.\n */\nexport function fromTransition(\n t: TransitionOptions,\n): Pick<RenderClientOptions, 'onLeave' | 'onEnter'> {\n return {\n onLeave: t.leave\n ? (el): void | Promise<void> => {\n const result = t.leave!([el])\n return result && typeof (result as Promise<void>).then === 'function'\n ? (result as Promise<void>)\n : undefined\n }\n : undefined,\n onEnter: t.enter\n ? (el): void => {\n t.enter!([el])\n }\n : undefined,\n }\n}\n\n/**\n * One element of the live chain the adapter keeps between navs.\n * `handle` is the AppHandle returned by mountApp/hydrateApp for this\n * layer. `slotMarker` / `slotScope` are set when the layer called\n * `pageSlot()` during its view pass; they're null for the innermost\n * layer (typically the page component, which doesn't have a slot).\n */\ninterface ChainEntry {\n def: AnyComponentDef\n handle: AppHandle\n slotMarker: HTMLElement | null\n slotScope: Scope | null\n /**\n * The data slice this layer was most recently mounted or updated\n * with. Compared shallow-key against the next nav's `lluiLayoutData[i]`\n * to decide whether a surviving layer needs a `propsMsg` dispatch.\n * Layers that didn't receive any layout data carry `undefined` here.\n */\n data: unknown\n}\n\n// Live chain of mounted layers. Module-level state: there's exactly\n// one chain per Vike-managed app per page load.\nlet chainHandles: ChainEntry[] = []\n\n/**\n * @internal — test helper. Disposes every layer in the current chain\n * and clears the module state so subsequent calls behave as a first\n * mount. Not part of the public API; subject to change without notice.\n */\nexport function _resetChainForTest(): void {\n // Dispose innermost-first to match the normal teardown path.\n for (let i = chainHandles.length - 1; i >= 0; i--) {\n chainHandles[i]!.handle.dispose()\n }\n chainHandles = []\n _resetPendingSlot()\n}\n\n/**\n * Back-compat alias for the pre-layout test helper name.\n * @internal\n * @deprecated — use `_resetChainForTest` instead.\n */\nexport function _resetCurrentHandleForTest(): void {\n _resetChainForTest()\n}\n\n/**\n * Default onRenderClient hook — no layout, no animation hooks. Hydrates\n * on first load, mounts fresh on subsequent navs. Use `createOnRenderClient`\n * for the customizable factory form.\n */\nexport async function onRenderClient(pageContext: ClientPageContext): Promise<void> {\n await renderClient(pageContext, {})\n}\n\n/**\n * Factory to create a customized onRenderClient hook. See `RenderClientOptions`\n * for the full option surface — this is the entry point for persistent\n * layouts, route transitions, and lifecycle hooks.\n *\n * **Do not name your layout file `+Layout.ts`.** Vike reserves the `+`\n * prefix for its own framework config conventions, and `+Layout.ts` is\n * interpreted by `vike-react` / `vike-vue` / `vike-solid` framework\n * adapters as a native layout config. `@llui/vike` isn't a framework\n * adapter in that sense — it's a render adapter, and `createOnRenderClient`\n * consumes the layout component directly via the `Layout` option. Name\n * the file `Layout.ts`, `app-layout.ts`, or anywhere outside `/pages`\n * that Vike won't scan, and import it here by path.\n *\n * ```ts\n * // pages/+onRenderClient.ts\n * import { createOnRenderClient, fromTransition } from '@llui/vike/client'\n * import { routeTransition } from '@llui/transitions'\n * import { AppLayout } from './Layout' // ← NOT './+Layout'\n *\n * export const onRenderClient = createOnRenderClient({\n * Layout: AppLayout,\n * ...fromTransition(routeTransition({ duration: 200 })),\n * onMount: () => console.log('page rendered'),\n * })\n * ```\n */\nexport function createOnRenderClient(\n options: RenderClientOptions,\n): (pageContext: ClientPageContext) => Promise<void> {\n return (pageContext) => renderClient(pageContext, options)\n}\n\nasync function renderClient(\n pageContext: ClientPageContext,\n options: RenderClientOptions,\n): Promise<void> {\n const selector = options.container ?? '#app'\n const container = document.querySelector(selector)\n if (!container) {\n throw new Error(`@llui/vike: container \"${selector}\" not found in DOM`)\n }\n const rootEl = container as HTMLElement\n\n // Resolve the chain for this render. The page component is always\n // the innermost entry, regardless of layout configuration.\n const layoutChain = resolveLayoutChain(options.Layout, pageContext)\n const layoutData = pageContext.lluiLayoutData ?? []\n const newChain: LayoutChain = [...layoutChain, pageContext.Page]\n const newChainData: readonly unknown[] = [...layoutData, pageContext.data]\n\n if (pageContext.isHydration) {\n // First load — the chain starts empty and we hydrate every layer\n // against server-rendered HTML. No onLeave/onEnter on hydration.\n await mountOrHydrateChain(newChain, newChainData, rootEl, {\n mode: 'hydrate',\n serverStateEnvelope: window.__LLUI_STATE__,\n })\n options.onMount?.()\n return\n }\n\n // Subsequent nav — diff the chain to find the divergent suffix.\n let firstMismatch = 0\n const minLen = Math.min(chainHandles.length, newChain.length)\n while (firstMismatch < minLen && chainHandles[firstMismatch]!.def === newChain[firstMismatch]) {\n firstMismatch++\n }\n\n // Push fresh data into surviving layers (layers in the shared prefix).\n // Without this, persistent layouts can't react to nav-driven data\n // changes — pathname, breadcrumbs, session, nav-highlight state all\n // belong to the layout but change on every client navigation. Each\n // surviving layer's def can opt in via `propsMsg(data) => Msg`; we\n // dispatch the resulting message through the handle's `send` so the\n // layout's update loop processes it like any other state change.\n //\n // Diff is shallow-key Object.is on record-shaped data, falling back\n // to whole-value Object.is for primitives / non-records. This matches\n // child()'s prop-diff behavior, which is what the report asked us to\n // mirror. Layers without `propsMsg` are skipped silently — opt-in.\n for (let i = 0; i < firstMismatch; i++) {\n const entry = chainHandles[i]!\n const newData = newChainData[i]\n if (!hasDataChanged(entry.data, newData)) continue\n entry.data = newData\n const propsMsg = (entry.def as { propsMsg?: (data: unknown) => unknown }).propsMsg\n if (typeof propsMsg !== 'function') continue\n const msg = propsMsg(newData)\n if (msg !== null && msg !== undefined) {\n entry.handle.send(msg)\n }\n }\n\n // Find the slot element whose contents will change. Shared prefix =\n // everything before firstMismatch. The slot we're about to replace\n // content in sits in the layer at firstMismatch - 1 (if any);\n // otherwise we're swapping the whole app at the root container.\n const leaveTarget =\n firstMismatch === 0 ? rootEl : (chainHandles[firstMismatch - 1]!.slotMarker ?? rootEl)\n\n // If everything matches (same chain end-to-end with same defs), this\n // is effectively a no-op nav — the page def hasn't changed. We still\n // fire onMount so callers can run per-render side effects, and the\n // surviving-layer data updates above already ran. But there's nothing\n // to dispose or mount, so skip the rest of the work.\n const isNoOp = firstMismatch === chainHandles.length && firstMismatch === newChain.length\n if (isNoOp) {\n options.onMount?.()\n return\n }\n\n // onLeave runs BEFORE any teardown. Outgoing DOM still mounted here.\n // Skip on the very first mount — there's no outgoing page to leave.\n const isFirstMount = chainHandles.length === 0\n if (options.onLeave && !isFirstMount) {\n await options.onLeave(leaveTarget)\n }\n\n // Dispose the divergent suffix, innermost first. Each handle.dispose()\n // calls disposeScope on that layer's rootScope, which cascades through\n // every child scope the layer owned (bindings, portals, onMount\n // cleanups, dialog focus traps, etc.). The surviving layers are\n // untouched because their scopes live above the disposal roots.\n for (let i = chainHandles.length - 1; i >= firstMismatch; i--) {\n chainHandles[i]!.handle.dispose()\n }\n chainHandles = chainHandles.slice(0, firstMismatch)\n\n // Clear the slot element before mounting the new suffix. handle.dispose()\n // above already did this for the innermost layer's container, but the\n // slot at firstMismatch - 1 keeps its marker element (it's owned by the\n // surviving layer) and we mount fresh children into it.\n leaveTarget.textContent = ''\n\n // Mount the new suffix starting at firstMismatch.\n const parentScope =\n firstMismatch === 0 ? undefined : (chainHandles[firstMismatch - 1]!.slotScope ?? undefined)\n mountChainSuffix(newChain, newChainData, firstMismatch, leaveTarget, parentScope, {\n mode: 'mount',\n })\n\n // onEnter fires after the new suffix is in place. Fire-and-forget.\n options.onEnter?.(leaveTarget)\n options.onMount?.()\n}\n\n/**\n * Walk the full chain for the first mount or hydration. Starts from\n * depth 0 at the root container, threads each layer's slot into the\n * next layer's mount target + parentScope.\n */\nasync function mountOrHydrateChain(\n chain: LayoutChain,\n chainData: readonly unknown[],\n rootEl: HTMLElement,\n opts: MountOpts,\n): Promise<void> {\n mountChainSuffix(chain, chainData, 0, rootEl, undefined, opts)\n}\n\ninterface MountOpts {\n mode: 'mount' | 'hydrate'\n /** For hydration: the full `window.__LLUI_STATE__` envelope. */\n serverStateEnvelope?: unknown\n}\n\n/**\n * Mount (or hydrate) `chain[startAt..end]` into `initialTarget`, with\n * the initial layer's rootScope parented at `initialParentScope`.\n * Threads slot → next-target → next-parentScope through the chain.\n *\n * Fails loudly if a non-innermost layer forgot to call `pageSlot()`,\n * or if the innermost layer called `pageSlot()` unnecessarily.\n */\nfunction mountChainSuffix(\n chain: LayoutChain,\n chainData: readonly unknown[],\n startAt: number,\n initialTarget: HTMLElement,\n initialParentScope: Scope | undefined,\n opts: MountOpts,\n): void {\n let mountTarget: HTMLElement = initialTarget\n let parentScope: Scope | undefined = initialParentScope\n\n for (let i = startAt; i < chain.length; i++) {\n const def = chain[i]!\n const layerData = chainData[i]\n const isInnermost = i === chain.length - 1\n\n // Defensive: clear any stale slot from a prior failed mount.\n _resetPendingSlot()\n\n let handle: AppHandle\n if (opts.mode === 'hydrate') {\n // Hydration envelope: each layer pulls its own state slice. The\n // envelope shape is `{ layouts: [...], page: {...} }` with each\n // entry carrying `{ name, state }`. We match by name so a server/\n // client mismatch throws with a clear error instead of silently\n // hydrating the wrong state into the wrong instance.\n const layerState = extractHydrationState(opts.serverStateEnvelope, i, chain.length, def)\n // Cross from the type-erased AnyComponentDef back into a concrete\n // ComponentDef<unknown, unknown, unknown, unknown> for the mount\n // primitive's signature. The cast is safe — mountApp / hydrateApp\n // don't use the type parameters at runtime, they just thread the\n // def through createComponentInstance which accesses init/update/\n // view by field name. AnyComponentDef has the same field shape.\n handle = hydrateApp(\n mountTarget,\n def as unknown as Parameters<typeof hydrateApp>[1],\n layerState,\n { parentScope },\n )\n } else {\n handle = mountApp(mountTarget, def as unknown as Parameters<typeof mountApp>[1], layerData, {\n parentScope,\n })\n }\n\n const slot = _consumePendingSlot()\n\n if (isInnermost && slot !== null) {\n // Innermost layer declared a slot with nothing to fill it —\n // probably a misuse of pageSlot() in the page component itself.\n handle.dispose()\n throw new Error(\n `[llui/vike] <${def.name}> is the innermost component in the chain ` +\n `but called pageSlot(). pageSlot() only belongs in layout components ` +\n `that wrap a nested page or layout — not in the page itself.`,\n )\n }\n if (!isInnermost && slot === null) {\n // Non-innermost layer didn't declare a slot — there's nowhere to\n // mount the remaining chain.\n handle.dispose()\n throw new Error(\n `[llui/vike] <${def.name}> is a layout layer at depth ${i} but did not ` +\n `call pageSlot() in its view(). There are ${chain.length - i - 1} more ` +\n `layer(s) to mount and no slot to mount them into. Add pageSlot() from ` +\n `@llui/vike/client to the view at the position where nested content renders.`,\n )\n }\n\n chainHandles.push({\n def,\n handle,\n slotMarker: slot?.marker ?? null,\n slotScope: slot?.slotScope ?? null,\n data: layerData,\n })\n\n if (slot !== null) {\n mountTarget = slot.marker\n parentScope = slot.slotScope\n }\n }\n}\n\n/**\n * Pull the per-layer state from the hydration envelope. Supports both\n * the new chain-aware shape (`{ layouts: [...], page: {...} }`) and the\n * legacy flat shape (`window.__LLUI_STATE__` is the state object itself)\n * for backward compatibility with pages written against 0.0.15 or earlier.\n *\n * Throws on envelope shape mismatch — missing entries, wrong component\n * name at a given index — so server/client drift fails loud instead of\n * silently binding the wrong state to the wrong instance.\n */\n/**\n * Shallow-key data diff for the persistent-layer prop-update path.\n * Returns true when `next` differs from `prev` enough to warrant\n * dispatching a `propsMsg`. Mirrors `child()`'s prop-diff semantics:\n *\n * - `Object.is(prev, next)` short-circuits identical references.\n * - For two plain-object records, walks the union of keys and returns\n * true on the first `Object.is` mismatch.\n * - For anything else (primitives, arrays, class instances), falls\n * back to the top-level `Object.is` result — covers the cases where\n * the host populates `lluiLayoutData[i]` with a primitive or a\n * referentially-stable object.\n */\nfunction hasDataChanged(prev: unknown, next: unknown): boolean {\n if (Object.is(prev, next)) return false\n // Both must be plain object records to do a key walk; otherwise the\n // Object.is above is the only signal.\n if (\n prev === null ||\n next === null ||\n typeof prev !== 'object' ||\n typeof next !== 'object' ||\n Array.isArray(prev) ||\n Array.isArray(next)\n ) {\n return true\n }\n const prevRec = prev as Record<string, unknown>\n const nextRec = next as Record<string, unknown>\n const seen = new Set<string>()\n for (const k of Object.keys(prevRec)) {\n seen.add(k)\n if (!Object.is(prevRec[k], nextRec[k])) return true\n }\n for (const k of Object.keys(nextRec)) {\n if (!seen.has(k)) return true\n }\n return false\n}\n\nfunction extractHydrationState(\n envelope: unknown,\n layerIndex: number,\n chainLength: number,\n def: AnyComponentDef,\n): unknown {\n // Legacy flat envelope — no layout chain at render time. Only valid\n // when the chain has a single layer (the page).\n const isLegacyFlat =\n envelope !== null &&\n typeof envelope === 'object' &&\n !('layouts' in (envelope as object)) &&\n !('page' in (envelope as object))\n\n if (isLegacyFlat) {\n if (chainLength !== 1) {\n throw new Error(\n `[llui/vike] Hydration envelope is in the legacy flat shape but the ` +\n `current render has ${chainLength} chain layers. The server must emit ` +\n `the chain-aware shape ({ layouts, page }) when rendering with a layout.`,\n )\n }\n return envelope\n }\n\n const chainEnvelope = envelope as\n | { layouts?: Array<{ name: string; state: unknown }>; page?: { name: string; state: unknown } }\n | undefined\n if (!chainEnvelope) {\n throw new Error(\n `[llui/vike] Hydration envelope is missing. Server-side onRenderHtml must ` +\n `populate window.__LLUI_STATE__ with the full chain before client hydration.`,\n )\n }\n\n const isPageLayer = layerIndex === chainLength - 1\n const layoutEntries = chainEnvelope.layouts ?? []\n const expected = isPageLayer ? chainEnvelope.page : layoutEntries[layerIndex]\n\n if (!expected) {\n throw new Error(\n `[llui/vike] Hydration envelope has no entry for chain layer ${layerIndex} ` +\n `(<${def.name}>). Server rendered ${layoutEntries.length} layouts + ${\n chainEnvelope.page ? 'a page' : 'no page'\n }, client expected ${chainLength} total entries.`,\n )\n }\n\n if (expected.name !== def.name) {\n throw new Error(\n `[llui/vike] Hydration mismatch at chain layer ${layerIndex}: server ` +\n `rendered <${expected.name}> but client is trying to hydrate <${def.name}>. ` +\n `This usually means the layout chain resolver returns different layouts ` +\n `on the server and the client for the same route.`,\n )\n }\n\n return expected.state\n}\n"]}
|
package/dist/on-render-html.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnyComponentDef } from '@llui/dom';
|
|
2
|
+
type LayoutChain = ReadonlyArray<AnyComponentDef>;
|
|
3
|
+
/**
|
|
4
|
+
* Page context shape as seen by `@llui/vike`'s server hook. `Page` and
|
|
5
|
+
* `data` are whichever `+Page.ts` and `+data.ts` Vike resolved for the
|
|
6
|
+
* current route; `lluiLayoutData` is an optional array of per-layer
|
|
7
|
+
* layout data matching the chain configured on `createOnRenderHtml`.
|
|
8
|
+
*/
|
|
2
9
|
export interface PageContext {
|
|
3
|
-
Page:
|
|
10
|
+
Page: AnyComponentDef;
|
|
4
11
|
data?: unknown;
|
|
12
|
+
lluiLayoutData?: readonly unknown[];
|
|
5
13
|
head?: string;
|
|
6
14
|
}
|
|
7
15
|
export interface DocumentContext {
|
|
8
|
-
/** Rendered component HTML */
|
|
16
|
+
/** Rendered component HTML (layout + page composed if a Layout is configured) */
|
|
9
17
|
html: string;
|
|
10
|
-
/** JSON-serialized
|
|
18
|
+
/** JSON-serialized hydration envelope (chain-aware when Layout is configured) */
|
|
11
19
|
state: string;
|
|
12
20
|
/** Head content from pageContext.head (e.g. from +Head.ts) */
|
|
13
21
|
head: string;
|
|
@@ -23,28 +31,55 @@ export interface RenderHtmlResult {
|
|
|
23
31
|
};
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* Options for the customized `createOnRenderHtml` factory. Mirrors
|
|
35
|
+
* `@llui/vike/client`'s `RenderClientOptions.Layout` — the same chain
|
|
36
|
+
* shape is accepted for consistency between server and client render.
|
|
37
|
+
*/
|
|
38
|
+
export interface RenderHtmlOptions {
|
|
39
|
+
/** Custom HTML document template. Defaults to a minimal layout. */
|
|
40
|
+
document?: (ctx: DocumentContext) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Persistent layout chain. One of:
|
|
43
|
+
*
|
|
44
|
+
* - A single `ComponentDef` — becomes a one-layout chain.
|
|
45
|
+
* - An array of `ComponentDef`s — outermost first, innermost last.
|
|
46
|
+
* Every layer except the innermost must call `pageSlot()` in its view.
|
|
47
|
+
* - A function that returns a chain from the current `pageContext` —
|
|
48
|
+
* enables per-route chains (e.g. reading Vike's `urlPathname`).
|
|
49
|
+
*
|
|
50
|
+
* The server renders the full chain as one composed HTML tree. Client
|
|
51
|
+
* hydration reads the matching envelope and reconstructs the chain
|
|
52
|
+
* layer-by-layer.
|
|
53
|
+
*/
|
|
54
|
+
Layout?: AnyComponentDef | LayoutChain | ((pageContext: PageContext) => LayoutChain);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Default onRenderHtml hook — no layout, minimal document template.
|
|
28
58
|
*/
|
|
29
59
|
export declare function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult>;
|
|
30
60
|
/**
|
|
31
61
|
* Factory to create a customized onRenderHtml hook.
|
|
32
62
|
*
|
|
33
|
-
*
|
|
63
|
+
* **Do not name your layout file `+Layout.ts`.** Vike reserves `+Layout`
|
|
64
|
+
* for its own framework-adapter config (`vike-react` / `vike-vue` /
|
|
65
|
+
* `vike-solid`) and will conflict with `@llui/vike`'s `Layout` option.
|
|
66
|
+
* Name the file `Layout.ts`, `app-layout.ts`, or anywhere outside
|
|
67
|
+
* `/pages` that Vike won't scan, and import it here by path.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
34
70
|
* // pages/+onRenderHtml.ts
|
|
35
|
-
* import { createOnRenderHtml } from '@llui/vike'
|
|
71
|
+
* import { createOnRenderHtml } from '@llui/vike/server'
|
|
72
|
+
* import { AppLayout } from './Layout' // ← NOT './+Layout'
|
|
36
73
|
*
|
|
37
74
|
* export const onRenderHtml = createOnRenderHtml({
|
|
75
|
+
* Layout: AppLayout,
|
|
38
76
|
* document: ({ html, state, head }) => `<!DOCTYPE html>
|
|
39
|
-
* <html>
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* <script>window.__LLUI_STATE__ = ${state}</script></body>
|
|
43
|
-
* </html>`,
|
|
77
|
+
* <html><head>${head}<link rel="stylesheet" href="/styles.css" /></head>
|
|
78
|
+
* <body><div id="app">${html}</div>
|
|
79
|
+
* <script>window.__LLUI_STATE__ = ${state}</script></body></html>`,
|
|
44
80
|
* })
|
|
45
81
|
* ```
|
|
46
82
|
*/
|
|
47
|
-
export declare function createOnRenderHtml(options:
|
|
48
|
-
|
|
49
|
-
}): (pageContext: PageContext) => Promise<RenderHtmlResult>;
|
|
83
|
+
export declare function createOnRenderHtml(options: RenderHtmlOptions): (pageContext: PageContext) => Promise<RenderHtmlResult>;
|
|
84
|
+
export {};
|
|
50
85
|
//# sourceMappingURL=on-render-html.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-render-html.d.ts","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"on-render-html.d.ts","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAGhE,KAAK,WAAW,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,cAAc,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,WAAW,EAAE,WAAW,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,WAAW,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAA;CACpC;AAcD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,CAAA;IAE3C;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,KAAK,WAAW,CAAC,CAAA;CACrF;AAcD;;GAEG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEtF;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,iBAAiB,GACzB,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAEzD"}
|