@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.
@@ -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 any preset like
5
- * `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected
6
- * by `createOnRenderClient`.
24
+ * `routeTransition()` from `@llui/transitions`, or a preset like `fade`
25
+ * / `slide`) into the `onLeave` / `onEnter` pair expected by
26
+ * `createOnRenderClient`.
7
27
  *
8
- * ```typescript
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 container element itself its
18
- * opacity / transform fades out the outgoing page, then the new page
19
- * fades in when it mounts. If the preset doesn't restore its starting
20
- * style on `leave`, the container may still carry leftover properties
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
- // Track the current app handle so we can dispose it on client navigation.
42
- // Module-level state: there's exactly one Vike-managed app per page load.
43
- let currentHandle = null;
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 the current handle (if any) and clears
46
- * the module-level state so subsequent calls behave as a first mount.
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 _resetCurrentHandleForTest() {
50
- if (currentHandle) {
51
- currentHandle.dispose();
52
- currentHandle = null;
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 if
57
- * `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`
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
- * ```typescript
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
- * container: '#root',
72
- * onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
73
- * onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
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 el = container;
89
- // Dispose the previous page's component on client navigation. If the
90
- // caller supplied an onLeave hook and this isn't the initial hydration,
91
- // await it BEFORE tearing down — that's the only moment where the
92
- // outgoing page's DOM still exists for an animation to read/write.
93
- if (currentHandle) {
94
- if (!pageContext.isHydration && options.onLeave) {
95
- await options.onLeave(el);
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
- if (pageContext.isHydration) {
101
- const serverState = window.__LLUI_STATE__;
102
- currentHandle = hydrateApp(el, Page, serverState);
103
- }
104
- else {
105
- // Clear old DOM before mounting the new page
106
- el.textContent = '';
107
- currentHandle = mountApp(el, Page, pageContext.data);
108
- // onEnter fires AFTER mount so the hook can animate the freshly
109
- // rendered children. It's intentionally sync a promise return is
110
- // ignored, matching typical enter-animation ergonomics (fire-and-forget).
111
- options.onEnter?.(el);
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"]}
@@ -1,13 +1,21 @@
1
- import type { ComponentDef } from '@llui/dom';
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: ComponentDef<unknown, unknown, unknown, unknown>;
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 initial state */
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
- * Default onRenderHtml hook for simple cases.
27
- * Uses a minimal HTML document template.
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
- * ```typescript
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
- * <head>${head}<link rel="stylesheet" href="/styles.css" /></head>
41
- * <body><div id="app">${html}</div>
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
- document: (ctx: DocumentContext) => string;
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,YAAY,EAAE,MAAM,WAAW,CAAA;AAE7C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,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;;;GAGG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEtF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,QAAQ,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,CAAA;CAC3C,GAAG,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAE1D"}
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"}