@llui/vike 0.0.18 → 0.0.20

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,4 +1,5 @@
1
- import type { AnyComponentDef, TransitionOptions } from '@llui/dom';
1
+ import type { AnyComponentDef, TransitionOptions, Scope } from '@llui/dom';
2
+ import type { VikePageContextData } from './vike-namespace.js';
2
3
  export { pageSlot } from './page-slot.js';
3
4
  declare global {
4
5
  interface Window {
@@ -10,6 +11,12 @@ declare global {
10
11
  * `Page` and `data` fields come from whichever `+Page.ts` and `+data.ts`
11
12
  * Vike resolved for the current route.
12
13
  *
14
+ * `data` is derived from the global `Vike.PageContext` namespace — the
15
+ * convention users already know from Vike. Consumer augmentations of
16
+ * `Vike.PageContext { interface PageContext { data?: MyData } }` flow
17
+ * through to every callback here without a cast. Unaugmented projects
18
+ * fall back to `unknown`.
19
+ *
13
20
  * `lluiLayoutData` is optional and carries per-layer data for the layout
14
21
  * chain configured via `createOnRenderClient({ Layout })`. It's indexed
15
22
  * outermost-to-innermost, one entry per layout layer. Absent entries
@@ -19,7 +26,7 @@ declare global {
19
26
  */
20
27
  export interface ClientPageContext {
21
28
  Page: AnyComponentDef;
22
- data?: unknown;
29
+ data?: VikePageContextData;
23
30
  lluiLayoutData?: readonly unknown[];
24
31
  isHydration?: boolean;
25
32
  }
@@ -177,4 +184,26 @@ export declare function onRenderClient(pageContext: ClientPageContext): Promise<
177
184
  * ```
178
185
  */
179
186
  export declare function createOnRenderClient(options: RenderClientOptions): (pageContext: ClientPageContext) => Promise<void>;
187
+ interface MountOpts {
188
+ mode: 'mount' | 'hydrate';
189
+ /** For hydration: the full `window.__LLUI_STATE__` envelope. */
190
+ serverStateEnvelope?: unknown;
191
+ }
192
+ /**
193
+ * Mount (or hydrate) `chain[startAt..end]` into `initialTarget`, with
194
+ * the initial layer's rootScope parented at `initialParentScope`.
195
+ * Threads slot → next-target → next-parentScope through the chain.
196
+ *
197
+ * `initialTarget` is `HTMLElement` for the outermost layer (container-
198
+ * based mount/hydrate) and `Comment` for inner layers that mount relative
199
+ * to a `pageSlot()` anchor.
200
+ *
201
+ * Fails loudly if a non-innermost layer forgot to call `pageSlot()`,
202
+ * or if the innermost layer called `pageSlot()` unnecessarily.
203
+ *
204
+ * @internal — test helper. Exported so `client-page-slot.test.ts` can
205
+ * test anchor-mount/dispose contracts directly with hand-built DOM.
206
+ * Not part of the public API.
207
+ */
208
+ export declare function _mountChainSuffix(chain: LayoutChain, chainData: readonly unknown[], startAt: number, initialTarget: HTMLElement | Comment, initialParentScope: Scope | undefined, opts: MountOpts): void;
180
209
  //# sourceMappingURL=on-render-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"on-render-client.d.ts","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAa,iBAAiB,EAAS,MAAM,WAAW,CAAA;AAKrF,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,cAAc,CAAC,EAAE,OAAO,CAAA;KACzB;CACF;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,cAAc,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;IACnC,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,KAAK,WAAW,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;AAoBjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,WAAW,EAAE,iBAAiB,KAAK,WAAW,CAAC,CAAA;IAE1F;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAA;IAEnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,iBAAiB,GACnB,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC,CAgBlD;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAOzC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,mBAAmB,GAC3B,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAEnD"}
1
+ {"version":3,"file":"on-render-client.d.ts","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAa,iBAAiB,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAErF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAI9D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,cAAc,CAAC,EAAE,OAAO,CAAA;KACzB;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,CAAC,EAAE,mBAAmB,CAAA;IAC1B,cAAc,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;IACnC,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,KAAK,WAAW,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;AAoBjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,WAAW,GAAG,CAAC,CAAC,WAAW,EAAE,iBAAiB,KAAK,WAAW,CAAC,CAAA;IAE1F;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAA;IAEnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,iBAAiB,GACnB,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC,CAgBlD;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAOzC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,mBAAmB,GAC3B,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAEnD;AAyID,UAAU,SAAS;IACjB,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;IACzB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,SAAS,OAAO,EAAE,EAC7B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,WAAW,GAAG,OAAO,EACpC,kBAAkB,EAAE,KAAK,GAAG,SAAS,EACrC,IAAI,EAAE,SAAS,GACd,IAAI,CAmGN"}
@@ -1,4 +1,4 @@
1
- import { hydrateApp, mountApp } from '@llui/dom';
1
+ import { hydrateApp, mountApp, mountAtAnchor, hydrateAtAnchor } from '@llui/dom';
2
2
  import { _consumePendingSlot, _resetPendingSlot } from './page-slot.js';
3
3
  // Re-exported so `@llui/vike/client` is a one-stop-shop for everything
4
4
  // a pages/+onRenderClient.ts / +Layout.ts file needs.
@@ -174,11 +174,11 @@ async function renderClient(pageContext, options) {
174
174
  entry.handle.send(msg);
175
175
  }
176
176
  }
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);
177
+ // Determine whether this nav replaces the entire root or only a suffix.
178
+ // For the root swap, the outermost layer mounts/hydrates via mountApp/
179
+ // hydrateApp on rootEl. For a deeper swap, the mount target is an
180
+ // anchor comment owned by the surviving layer's slot.
181
+ const isRootSwap = firstMismatch === 0;
182
182
  // If everything matches (same chain end-to-end with same defs), this
183
183
  // is effectively a no-op nav — the page def hasn't changed. We still
184
184
  // fire onMount so callers can run per-render side effects, and the
@@ -193,29 +193,37 @@ async function renderClient(pageContext, options) {
193
193
  // Skip on the very first mount — there's no outgoing page to leave.
194
194
  const isFirstMount = chainHandles.length === 0;
195
195
  if (options.onLeave && !isFirstMount) {
196
- await options.onLeave(leaveTarget);
196
+ const leaveTargetEl = isRootSwap
197
+ ? rootEl
198
+ : (chainHandles[firstMismatch - 1].slotAnchor?.parentElement ?? rootEl);
199
+ await options.onLeave(leaveTargetEl);
197
200
  }
198
201
  // Dispose the divergent suffix, innermost first. Each handle.dispose()
199
202
  // calls disposeScope on that layer's rootScope, which cascades through
200
203
  // every child scope the layer owned (bindings, portals, onMount
201
204
  // cleanups, dialog focus traps, etc.). The surviving layers are
202
205
  // untouched because their scopes live above the disposal roots.
206
+ // For anchor-based mounts, dispose() also removes the owned DOM region
207
+ // between the anchor and end sentinel — no additional textContent clear needed.
203
208
  for (let i = chainHandles.length - 1; i >= firstMismatch; i--) {
204
209
  chainHandles[i].handle.dispose();
205
210
  }
206
211
  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
212
  // Mount the new suffix starting at firstMismatch.
213
+ // For a root swap, the target is the container HTMLElement.
214
+ // For a deeper swap, the target is the surviving layer's slot anchor (Comment).
213
215
  const parentScope = firstMismatch === 0 ? undefined : (chainHandles[firstMismatch - 1].slotScope ?? undefined);
214
- mountChainSuffix(newChain, newChainData, firstMismatch, leaveTarget, parentScope, {
216
+ const mountTargetArg = firstMismatch === 0 ? rootEl : chainHandles[firstMismatch - 1].slotAnchor;
217
+ mountChainSuffix(newChain, newChainData, firstMismatch, mountTargetArg, parentScope, {
215
218
  mode: 'mount',
216
219
  });
217
220
  // onEnter fires after the new suffix is in place. Fire-and-forget.
218
- options.onEnter?.(leaveTarget);
221
+ if (options.onEnter) {
222
+ const enterTargetEl = isRootSwap
223
+ ? rootEl
224
+ : (chainHandles[firstMismatch - 1].slotAnchor?.parentElement ?? rootEl);
225
+ options.onEnter(enterTargetEl);
226
+ }
219
227
  options.onMount?.();
220
228
  }
221
229
  /**
@@ -231,10 +239,18 @@ async function mountOrHydrateChain(chain, chainData, rootEl, opts) {
231
239
  * the initial layer's rootScope parented at `initialParentScope`.
232
240
  * Threads slot → next-target → next-parentScope through the chain.
233
241
  *
242
+ * `initialTarget` is `HTMLElement` for the outermost layer (container-
243
+ * based mount/hydrate) and `Comment` for inner layers that mount relative
244
+ * to a `pageSlot()` anchor.
245
+ *
234
246
  * Fails loudly if a non-innermost layer forgot to call `pageSlot()`,
235
247
  * or if the innermost layer called `pageSlot()` unnecessarily.
248
+ *
249
+ * @internal — test helper. Exported so `client-page-slot.test.ts` can
250
+ * test anchor-mount/dispose contracts directly with hand-built DOM.
251
+ * Not part of the public API.
236
252
  */
237
- function mountChainSuffix(chain, chainData, startAt, initialTarget, initialParentScope, opts) {
253
+ export function _mountChainSuffix(chain, chainData, startAt, initialTarget, initialParentScope, opts) {
238
254
  let mountTarget = initialTarget;
239
255
  let parentScope = initialParentScope;
240
256
  for (let i = startAt; i < chain.length; i++) {
@@ -251,18 +267,28 @@ function mountChainSuffix(chain, chainData, startAt, initialTarget, initialParen
251
267
  // client mismatch throws with a clear error instead of silently
252
268
  // hydrating the wrong state into the wrong instance.
253
269
  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 });
270
+ if (mountTarget.nodeType === 1) {
271
+ // HTMLElement outermost layer, use hydrateApp (container-based).
272
+ // Cross from the type-erased AnyComponentDef back into a concrete
273
+ // ComponentDef<unknown, unknown, unknown, unknown> for the mount
274
+ // primitive's signature. The cast is safe — mountApp / hydrateApp
275
+ // don't use the type parameters at runtime.
276
+ handle = hydrateApp(mountTarget, def, layerState, { parentScope });
277
+ }
278
+ else {
279
+ // Comment anchor — inner layer, use hydrateAtAnchor.
280
+ handle = hydrateAtAnchor(mountTarget, def, layerState, { parentScope });
281
+ }
261
282
  }
262
283
  else {
263
- handle = mountApp(mountTarget, def, layerData, {
264
- parentScope,
265
- });
284
+ if (mountTarget.nodeType === 1) {
285
+ // HTMLElement — outermost layer, use mountApp (container-based).
286
+ handle = mountApp(mountTarget, def, layerData, { parentScope });
287
+ }
288
+ else {
289
+ // Comment anchor — inner layer, use mountAtAnchor.
290
+ handle = mountAtAnchor(mountTarget, def, layerData, { parentScope });
291
+ }
266
292
  }
267
293
  const slot = _consumePendingSlot();
268
294
  if (isInnermost && slot !== null) {
@@ -285,16 +311,20 @@ function mountChainSuffix(chain, chainData, startAt, initialTarget, initialParen
285
311
  chainHandles.push({
286
312
  def,
287
313
  handle,
288
- slotMarker: slot?.marker ?? null,
314
+ slotAnchor: slot?.anchor ?? null,
289
315
  slotScope: slot?.slotScope ?? null,
290
316
  data: layerData,
291
317
  });
292
318
  if (slot !== null) {
293
- mountTarget = slot.marker;
319
+ // Next layer mounts relative to the slot's comment anchor.
320
+ mountTarget = slot.anchor;
294
321
  parentScope = slot.slotScope;
295
322
  }
296
323
  }
297
324
  }
325
+ // Internal alias used by renderClient and mountOrHydrateChain.
326
+ // The public-named export above carries the @internal doc.
327
+ const mountChainSuffix = _mountChainSuffix;
298
328
  /**
299
329
  * Pull the per-layer state from the hydration envelope. Supports both
300
330
  * the new chain-aware shape (`{ layouts: [...], page: {...} }`) and the
@@ -1 +1 @@
1
- {"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEhD,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEvE,uEAAuE;AACvE,sDAAsD;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AA6BzC;;;;;GAKG;AACH,SAAS,kBAAkB,CACzB,YAA2C,EAC3C,WAA8B;IAE9B,IAAI,CAAC,YAAY;QAAE,OAAO,EAAE,CAAA;IAC5B,IAAI,OAAO,YAAY,KAAK,UAAU,EAAE,CAAC;QACvC,OAAO,YAAY,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;IACxC,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC;QAAE,OAAO,YAAY,CAAA;IACpD,OAAO,CAAC,YAA+B,CAAC,CAAA;AAC1C,CAAC;AA4FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,cAAc,CAC5B,CAAoB;IAEpB,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,KAAK;YACd,CAAC,CAAC,CAAC,EAAE,EAAwB,EAAE;gBAC3B,MAAM,MAAM,GAAG,CAAC,CAAC,KAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC7B,OAAO,MAAM,IAAI,OAAQ,MAAwB,CAAC,IAAI,KAAK,UAAU;oBACnE,CAAC,CAAE,MAAwB;oBAC3B,CAAC,CAAC,SAAS,CAAA;YACf,CAAC;YACH,CAAC,CAAC,SAAS;QACb,OAAO,EAAE,CAAC,CAAC,KAAK;YACd,CAAC,CAAC,CAAC,EAAE,EAAQ,EAAE;gBACX,CAAC,CAAC,KAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAChB,CAAC;YACH,CAAC,CAAC,SAAS;KACd,CAAA;AACH,CAAC;AAuBD,oEAAoE;AACpE,gDAAgD;AAChD,IAAI,YAAY,GAAiB,EAAE,CAAA;AAEnC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,6DAA6D;IAC7D,KAAK,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAClD,YAAY,CAAC,CAAC,CAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAA;IACnC,CAAC;IACD,YAAY,GAAG,EAAE,CAAA;IACjB,iBAAiB,EAAE,CAAA;AACrB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B;IACxC,kBAAkB,EAAE,CAAA;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,WAA8B;IACjE,MAAM,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAA4B;IAE5B,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;AAC5D,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,WAA8B,EAC9B,OAA4B;IAE5B,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAA;IAC5C,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;IAClD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,oBAAoB,CAAC,CAAA;IACzE,CAAC;IACD,MAAM,MAAM,GAAG,SAAwB,CAAA;IAEvC,kEAAkE;IAClE,2DAA2D;IAC3D,MAAM,WAAW,GAAG,kBAAkB,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACnE,MAAM,UAAU,GAAG,WAAW,CAAC,cAAc,IAAI,EAAE,CAAA;IACnD,MAAM,QAAQ,GAAgB,CAAC,GAAG,WAAW,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IAChE,MAAM,YAAY,GAAuB,CAAC,GAAG,UAAU,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IAE1E,IAAI,WAAW,CAAC,WAAW,EAAE,CAAC;QAC5B,iEAAiE;QACjE,iEAAiE;QACjE,MAAM,mBAAmB,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE;YACxD,IAAI,EAAE,SAAS;YACf,mBAAmB,EAAE,MAAM,CAAC,cAAc;SAC3C,CAAC,CAAA;QACF,OAAO,CAAC,OAAO,EAAE,EAAE,CAAA;QACnB,OAAM;IACR,CAAC;IAED,gEAAgE;IAChE,IAAI,aAAa,GAAG,CAAC,CAAA;IACrB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC7D,OAAO,aAAa,GAAG,MAAM,IAAI,YAAY,CAAC,aAAa,CAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QAC9F,aAAa,EAAE,CAAA;IACjB,CAAC;IAED,uEAAuE;IACvE,kEAAkE;IAClE,oEAAoE;IACpE,mEAAmE;IACnE,mEAAmE;IACnE,oEAAoE;IACpE,iEAAiE;IACjE,EAAE;IACF,oEAAoE;IACpE,sEAAsE;IACtE,qEAAqE;IACrE,mEAAmE;IACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAE,CAAA;QAC9B,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;QAC/B,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC;YAAE,SAAQ;QAClD,KAAK,CAAC,IAAI,GAAG,OAAO,CAAA;QACpB,MAAM,QAAQ,GAAI,KAAK,CAAC,GAAiD,CAAC,QAAQ,CAAA;QAClF,IAAI,OAAO,QAAQ,KAAK,UAAU;YAAE,SAAQ;QAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC7B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC;IACH,CAAC;IAED,oEAAoE;IACpE,mEAAmE;IACnE,8DAA8D;IAC9D,gEAAgE;IAChE,MAAM,WAAW,GACf,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,GAAG,CAAC,CAAE,CAAC,UAAU,IAAI,MAAM,CAAC,CAAA;IAExF,qEAAqE;IACrE,qEAAqE;IACrE,mEAAmE;IACnE,sEAAsE;IACtE,qDAAqD;IACrD,MAAM,MAAM,GAAG,aAAa,KAAK,YAAY,CAAC,MAAM,IAAI,aAAa,KAAK,QAAQ,CAAC,MAAM,CAAA;IACzF,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,CAAC,OAAO,EAAE,EAAE,CAAA;QACnB,OAAM;IACR,CAAC;IAED,qEAAqE;IACrE,oEAAoE;IACpE,MAAM,YAAY,GAAG,YAAY,CAAC,MAAM,KAAK,CAAC,CAAA;IAC9C,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;QACrC,MAAM,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IACpC,CAAC;IAED,uEAAuE;IACvE,uEAAuE;IACvE,gEAAgE;IAChE,gEAAgE;IAChE,gEAAgE;IAChE,KAAK,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9D,YAAY,CAAC,CAAC,CAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAA;IACnC,CAAC;IACD,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAA;IAEnD,0EAA0E;IAC1E,sEAAsE;IACtE,wEAAwE;IACxE,wDAAwD;IACxD,WAAW,CAAC,WAAW,GAAG,EAAE,CAAA;IAE5B,kDAAkD;IAClD,MAAM,WAAW,GACf,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,GAAG,CAAC,CAAE,CAAC,SAAS,IAAI,SAAS,CAAC,CAAA;IAC7F,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE;QAChF,IAAI,EAAE,OAAO;KACd,CAAC,CAAA;IAEF,mEAAmE;IACnE,OAAO,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,CAAA;IAC9B,OAAO,CAAC,OAAO,EAAE,EAAE,CAAA;AACrB,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,mBAAmB,CAChC,KAAkB,EAClB,SAA6B,EAC7B,MAAmB,EACnB,IAAe;IAEf,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;AAChE,CAAC;AAQD;;;;;;;GAOG;AACH,SAAS,gBAAgB,CACvB,KAAkB,EAClB,SAA6B,EAC7B,OAAe,EACf,aAA0B,EAC1B,kBAAqC,EACrC,IAAe;IAEf,IAAI,WAAW,GAAgB,aAAa,CAAA;IAC5C,IAAI,WAAW,GAAsB,kBAAkB,CAAA;IAEvD,KAAK,IAAI,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;QACrB,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;QAC9B,MAAM,WAAW,GAAG,CAAC,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QAE1C,6DAA6D;QAC7D,iBAAiB,EAAE,CAAA;QAEnB,IAAI,MAAiB,CAAA;QACrB,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC5B,gEAAgE;YAChE,gEAAgE;YAChE,kEAAkE;YAClE,gEAAgE;YAChE,qDAAqD;YACrD,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YACxF,kEAAkE;YAClE,iEAAiE;YACjE,kEAAkE;YAClE,iEAAiE;YACjE,kEAAkE;YAClE,gEAAgE;YAChE,MAAM,GAAG,UAAU,CACjB,WAAW,EACX,GAAkD,EAClD,UAAU,EACV,EAAE,WAAW,EAAE,CAChB,CAAA;QACH,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,QAAQ,CAAC,WAAW,EAAE,GAAgD,EAAE,SAAS,EAAE;gBAC1F,WAAW;aACZ,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAA;QAElC,IAAI,WAAW,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACjC,4DAA4D;YAC5D,gEAAgE;YAChE,MAAM,CAAC,OAAO,EAAE,CAAA;YAChB,MAAM,IAAI,KAAK,CACb,gBAAgB,GAAG,CAAC,IAAI,4CAA4C;gBAClE,sEAAsE;gBACtE,6DAA6D,CAChE,CAAA;QACH,CAAC;QACD,IAAI,CAAC,WAAW,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClC,iEAAiE;YACjE,6BAA6B;YAC7B,MAAM,CAAC,OAAO,EAAE,CAAA;YAChB,MAAM,IAAI,KAAK,CACb,gBAAgB,GAAG,CAAC,IAAI,gCAAgC,CAAC,eAAe;gBACtE,4CAA4C,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ;gBACxE,wEAAwE;gBACxE,6EAA6E,CAChF,CAAA;QACH,CAAC;QAED,YAAY,CAAC,IAAI,CAAC;YAChB,GAAG;YACH,MAAM;YACN,UAAU,EAAE,IAAI,EAAE,MAAM,IAAI,IAAI;YAChC,SAAS,EAAE,IAAI,EAAE,SAAS,IAAI,IAAI;YAClC,IAAI,EAAE,SAAS;SAChB,CAAC,CAAA;QAEF,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,WAAW,GAAG,IAAI,CAAC,MAAM,CAAA;YACzB,WAAW,GAAG,IAAI,CAAC,SAAS,CAAA;QAC9B,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH;;;;;;;;;;;;GAYG;AACH,SAAS,cAAc,CAAC,IAAa,EAAE,IAAa;IAClD,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IACvC,oEAAoE;IACpE,sCAAsC;IACtC,IACE,IAAI,KAAK,IAAI;QACb,IAAI,KAAK,IAAI;QACb,OAAO,IAAI,KAAK,QAAQ;QACxB,OAAO,IAAI,KAAK,QAAQ;QACxB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QACnB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EACnB,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,OAAO,GAAG,IAA+B,CAAA;IAC/C,MAAM,OAAO,GAAG,IAA+B,CAAA;IAC/C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACX,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;IACrD,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;IAC/B,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,qBAAqB,CAC5B,QAAiB,EACjB,UAAkB,EAClB,WAAmB,EACnB,GAAoB;IAEpB,oEAAoE;IACpE,gDAAgD;IAChD,MAAM,YAAY,GAChB,QAAQ,KAAK,IAAI;QACjB,OAAO,QAAQ,KAAK,QAAQ;QAC5B,CAAC,CAAC,SAAS,IAAK,QAAmB,CAAC;QACpC,CAAC,CAAC,MAAM,IAAK,QAAmB,CAAC,CAAA;IAEnC,IAAI,YAAY,EAAE,CAAC;QACjB,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,qEAAqE;gBACnE,sBAAsB,WAAW,sCAAsC;gBACvE,yEAAyE,CAC5E,CAAA;QACH,CAAC;QACD,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,MAAM,aAAa,GAAG,QAET,CAAA;IACb,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,2EAA2E;YACzE,6EAA6E,CAChF,CAAA;IACH,CAAC;IAED,MAAM,WAAW,GAAG,UAAU,KAAK,WAAW,GAAG,CAAC,CAAA;IAClD,MAAM,aAAa,GAAG,aAAa,CAAC,OAAO,IAAI,EAAE,CAAA;IACjD,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,CAAC,CAAA;IAE7E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,+DAA+D,UAAU,GAAG;YAC1E,KAAK,GAAG,CAAC,IAAI,uBAAuB,aAAa,CAAC,MAAM,cACtD,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAClC,qBAAqB,WAAW,iBAAiB,CACpD,CAAA;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,iDAAiD,UAAU,WAAW;YACpE,aAAa,QAAQ,CAAC,IAAI,sCAAsC,GAAG,CAAC,IAAI,KAAK;YAC7E,yEAAyE;YACzE,kDAAkD,CACrD,CAAA;IACH,CAAC;IAED,OAAO,QAAQ,CAAC,KAAK,CAAA;AACvB,CAAC","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
+ {"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { hydrateApp, mountApp, mountAtAnchor, hydrateAtAnchor } from '@llui/dom'\nimport type { AnyComponentDef, AppHandle, TransitionOptions, Scope } from '@llui/dom'\nimport { _consumePendingSlot, _resetPendingSlot } from './page-slot.js'\nimport type { VikePageContextData } from './vike-namespace.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 * `data` is derived from the global `Vike.PageContext` namespace — the\n * convention users already know from Vike. Consumer augmentations of\n * `Vike.PageContext { interface PageContext { data?: MyData } }` flow\n * through to every callback here without a cast. Unaugmented projects\n * fall back to `unknown`.\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?: VikePageContextData\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. `slotAnchor` / `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 slotAnchor: Comment | 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 // Determine whether this nav replaces the entire root or only a suffix.\n // For the root swap, the outermost layer mounts/hydrates via mountApp/\n // hydrateApp on rootEl. For a deeper swap, the mount target is an\n // anchor comment owned by the surviving layer's slot.\n const isRootSwap = firstMismatch === 0\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 const leaveTargetEl = isRootSwap\n ? rootEl\n : (chainHandles[firstMismatch - 1]!.slotAnchor?.parentElement ?? rootEl)\n await options.onLeave(leaveTargetEl)\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 anchor-based mounts, dispose() also removes the owned DOM region\n // between the anchor and end sentinel — no additional textContent clear needed.\n for (let i = chainHandles.length - 1; i >= firstMismatch; i--) {\n chainHandles[i]!.handle.dispose()\n }\n chainHandles = chainHandles.slice(0, firstMismatch)\n\n // Mount the new suffix starting at firstMismatch.\n // For a root swap, the target is the container HTMLElement.\n // For a deeper swap, the target is the surviving layer's slot anchor (Comment).\n const parentScope =\n firstMismatch === 0 ? undefined : (chainHandles[firstMismatch - 1]!.slotScope ?? undefined)\n const mountTargetArg: HTMLElement | Comment =\n firstMismatch === 0 ? rootEl : chainHandles[firstMismatch - 1]!.slotAnchor!\n mountChainSuffix(newChain, newChainData, firstMismatch, mountTargetArg, parentScope, {\n mode: 'mount',\n })\n\n // onEnter fires after the new suffix is in place. Fire-and-forget.\n if (options.onEnter) {\n const enterTargetEl = isRootSwap\n ? rootEl\n : (chainHandles[firstMismatch - 1]!.slotAnchor?.parentElement ?? rootEl)\n options.onEnter(enterTargetEl)\n }\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 * `initialTarget` is `HTMLElement` for the outermost layer (container-\n * based mount/hydrate) and `Comment` for inner layers that mount relative\n * to a `pageSlot()` anchor.\n *\n * Fails loudly if a non-innermost layer forgot to call `pageSlot()`,\n * or if the innermost layer called `pageSlot()` unnecessarily.\n *\n * @internal — test helper. Exported so `client-page-slot.test.ts` can\n * test anchor-mount/dispose contracts directly with hand-built DOM.\n * Not part of the public API.\n */\nexport function _mountChainSuffix(\n chain: LayoutChain,\n chainData: readonly unknown[],\n startAt: number,\n initialTarget: HTMLElement | Comment,\n initialParentScope: Scope | undefined,\n opts: MountOpts,\n): void {\n let mountTarget: HTMLElement | Comment = 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 if (mountTarget.nodeType === 1) {\n // HTMLElement — outermost layer, use hydrateApp (container-based).\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.\n handle = hydrateApp(\n mountTarget as HTMLElement,\n def as unknown as Parameters<typeof hydrateApp>[1],\n layerState,\n { parentScope },\n )\n } else {\n // Comment anchor — inner layer, use hydrateAtAnchor.\n handle = hydrateAtAnchor(\n mountTarget as Comment,\n def as unknown as Parameters<typeof hydrateAtAnchor>[1],\n layerState as never,\n { parentScope },\n )\n }\n } else {\n if (mountTarget.nodeType === 1) {\n // HTMLElement — outermost layer, use mountApp (container-based).\n handle = mountApp(\n mountTarget as HTMLElement,\n def as unknown as Parameters<typeof mountApp>[1],\n layerData,\n { parentScope },\n )\n } else {\n // Comment anchor — inner layer, use mountAtAnchor.\n handle = mountAtAnchor(\n mountTarget as Comment,\n def as unknown as Parameters<typeof mountAtAnchor>[1],\n layerData,\n { parentScope },\n )\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 slotAnchor: slot?.anchor ?? null,\n slotScope: slot?.slotScope ?? null,\n data: layerData,\n })\n\n if (slot !== null) {\n // Next layer mounts relative to the slot's comment anchor.\n mountTarget = slot.anchor\n parentScope = slot.slotScope\n }\n }\n}\n\n// Internal alias used by renderClient and mountOrHydrateChain.\n// The public-named export above carries the @internal doc.\nconst mountChainSuffix = _mountChainSuffix\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,14 +1,20 @@
1
1
  import type { AnyComponentDef } from '@llui/dom';
2
+ import type { VikePageContextData } from './vike-namespace.js';
2
3
  type LayoutChain = ReadonlyArray<AnyComponentDef>;
3
4
  /**
4
5
  * Page context shape as seen by `@llui/vike`'s server hook. `Page` and
5
6
  * `data` are whichever `+Page.ts` and `+data.ts` Vike resolved for the
6
7
  * current route; `lluiLayoutData` is an optional array of per-layer
7
8
  * layout data matching the chain configured on `createOnRenderHtml`.
9
+ *
10
+ * `data` is derived from the global `Vike.PageContext` namespace so that
11
+ * consumer-side augmentations (the Vike convention for typing data) flow
12
+ * into this hook's callbacks without any cast. When the consumer hasn't
13
+ * augmented the namespace, `data` falls back to `unknown`.
8
14
  */
9
15
  export interface PageContext {
10
16
  Page: AnyComponentDef;
11
- data?: unknown;
17
+ data?: VikePageContextData;
12
18
  lluiLayoutData?: readonly unknown[];
13
19
  head?: string;
14
20
  }
@@ -81,5 +87,34 @@ export declare function onRenderHtml(pageContext: PageContext): Promise<RenderHt
81
87
  * ```
82
88
  */
83
89
  export declare function createOnRenderHtml(options: RenderHtmlOptions): (pageContext: PageContext) => Promise<RenderHtmlResult>;
90
+ /**
91
+ * Hydration envelope emitted into `window.__LLUI_STATE__`. Chain-aware
92
+ * by default — every layer (layouts + page) is represented by its own
93
+ * entry, keyed by component name so server/client mismatches fail loud.
94
+ */
95
+ interface HydrationEnvelope {
96
+ layouts: Array<{
97
+ name: string;
98
+ state: unknown;
99
+ }>;
100
+ page: {
101
+ name: string;
102
+ state: unknown;
103
+ };
104
+ }
105
+ /**
106
+ * Render every layer of the chain into one composed DOM tree, then
107
+ * serialize. At each non-innermost layer, consume the pending
108
+ * `pageSlot()` registration and insert the next layer's nodes as
109
+ * siblings after the anchor comment, bracketed by an end sentinel.
110
+ * Scopes are threaded so inner layers inherit the outer layer's scope
111
+ * tree for context lookups.
112
+ *
113
+ * @internal — exported for unit testing only (`_renderChain`).
114
+ */
115
+ export declare function _renderChain(chain: LayoutChain, chainData: readonly unknown[]): {
116
+ html: string;
117
+ envelope: HydrationEnvelope;
118
+ };
84
119
  export {};
85
120
  //# 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,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"}
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;AAEhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE9D,KAAK,WAAW,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;AAEjD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,CAAC,EAAE,mBAAmB,CAAA;IAC1B,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;AAED;;;;GAIG;AACH,UAAU,iBAAiB;IACzB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAChD,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAA;CACvC;AAiCD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,SAAS,OAAO,EAAE,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAgH/C"}
@@ -63,7 +63,7 @@ async function renderPage(pageContext, options) {
63
63
  // (the page) since Vike's pageContext always has a Page.
64
64
  const chain = [...layoutChain, pageContext.Page];
65
65
  const chainData = [...layoutData, pageContext.data];
66
- const { html, envelope } = renderChain(chain, chainData);
66
+ const { html, envelope } = _renderChain(chain, chainData);
67
67
  const document = options.document ?? DEFAULT_DOCUMENT;
68
68
  const head = pageContext.head ?? '';
69
69
  const state = JSON.stringify(envelope);
@@ -78,11 +78,14 @@ async function renderPage(pageContext, options) {
78
78
  /**
79
79
  * Render every layer of the chain into one composed DOM tree, then
80
80
  * serialize. At each non-innermost layer, consume the pending
81
- * `pageSlot()` registration and append the next layer's nodes into
82
- * the slot marker. Scopes are threaded so inner layers inherit the
83
- * outer layer's scope tree for context lookups.
81
+ * `pageSlot()` registration and insert the next layer's nodes as
82
+ * siblings after the anchor comment, bracketed by an end sentinel.
83
+ * Scopes are threaded so inner layers inherit the outer layer's scope
84
+ * tree for context lookups.
85
+ *
86
+ * @internal — exported for unit testing only (`_renderChain`).
84
87
  */
85
- function renderChain(chain, chainData) {
88
+ export function _renderChain(chain, chainData) {
86
89
  if (chain.length === 0) {
87
90
  throw new Error('[llui/vike] renderChain called with empty chain');
88
91
  }
@@ -95,7 +98,7 @@ function renderChain(chain, chainData) {
95
98
  const envelopeLayouts = [];
96
99
  let envelopePage = null;
97
100
  let outermostNodes = [];
98
- let currentSlotMarker = null;
101
+ let currentSlotAnchor = null;
99
102
  let currentSlotScope = undefined;
100
103
  for (let i = 0; i < chain.length; i++) {
101
104
  const def = chain[i];
@@ -114,17 +117,29 @@ function renderChain(chain, chainData) {
114
117
  outermostNodes = nodes;
115
118
  }
116
119
  else {
117
- // Append this layer's nodes into the previous layer's slot marker.
118
- // The slot marker is an element owned by the previous layer's DOM;
119
- // appending here stitches this layer into the composed tree so
120
- // the final serialization pass emits one integrated HTML string.
121
- if (!currentSlotMarker) {
120
+ // Insert this layer's nodes as siblings immediately after the
121
+ // anchor comment, then place an end sentinel after them.
122
+ // The anchor is already attached to the composed DOM tree (it was
123
+ // produced by the previous layer's pageSlot() call). We insert
124
+ // before the anchor's next sibling so nodes land right after the
125
+ // anchor, preserving any trailing siblings that may exist.
126
+ if (!currentSlotAnchor) {
122
127
  // Unreachable given the error checks below, but defensive.
123
- throw new Error(`[llui/vike] internal: chain layer ${i} (<${def.name}>) has no slot marker`);
128
+ throw new Error(`[llui/vike] internal: chain layer ${i} (<${def.name}>) has no slot anchor`);
129
+ }
130
+ const parentNode = currentSlotAnchor.parentNode;
131
+ if (!parentNode) {
132
+ throw new Error(`[llui/vike] internal: slot anchor for layer ${i} (<${def.name}>) is detached`);
124
133
  }
134
+ // insertPoint is the node currently after the anchor; inserting
135
+ // before it keeps all new nodes in order immediately after anchor.
136
+ const insertPoint = currentSlotAnchor.nextSibling;
125
137
  for (const node of nodes) {
126
- currentSlotMarker.appendChild(node);
138
+ parentNode.insertBefore(node, insertPoint);
127
139
  }
140
+ // Synthesize an end sentinel that brackets the owned region.
141
+ const endSentinel = document.createComment('llui-mount-end');
142
+ parentNode.insertBefore(endSentinel, insertPoint);
128
143
  }
129
144
  // Record this layer's state in the envelope. Page goes under
130
145
  // `page`, everything else under `layouts[]` ordered outer-to-inner.
@@ -146,7 +161,7 @@ function renderChain(chain, chainData) {
146
161
  `call pageSlot() in its view(). There are ${chain.length - i - 1} more ` +
147
162
  `layer(s) to mount and no slot to mount them into.`);
148
163
  }
149
- currentSlotMarker = slot?.marker ?? null;
164
+ currentSlotAnchor = slot?.anchor ?? null;
150
165
  currentSlotScope = slot?.slotScope;
151
166
  }
152
167
  const html = serializeNodes(outermostNodes, allBindings);
@@ -1 +1 @@
1
- {"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"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","sourcesContent":["import { renderNodes, serializeNodes } from '@llui/dom'\nimport type { AnyComponentDef, Binding, Scope } from '@llui/dom'\nimport { _consumePendingSlot, _resetPendingSlot } from './page-slot.js'\n\ntype LayoutChain = ReadonlyArray<AnyComponentDef>\n\n/**\n * Page context shape as seen by `@llui/vike`'s server hook. `Page` and\n * `data` are whichever `+Page.ts` and `+data.ts` Vike resolved for the\n * current route; `lluiLayoutData` is an optional array of per-layer\n * layout data matching the chain configured on `createOnRenderHtml`.\n */\nexport interface PageContext {\n Page: AnyComponentDef\n data?: unknown\n lluiLayoutData?: readonly unknown[]\n head?: string\n}\n\nexport interface DocumentContext {\n /** Rendered component HTML (layout + page composed if a Layout is configured) */\n html: string\n /** JSON-serialized hydration envelope (chain-aware when Layout is configured) */\n state: string\n /** Head content from pageContext.head (e.g. from +Head.ts) */\n head: string\n /** Full page context for custom logic */\n pageContext: PageContext\n}\n\nexport interface RenderHtmlResult {\n documentHtml: string | { _escaped: string }\n pageContext: { lluiState: unknown }\n}\n\nconst DEFAULT_DOCUMENT = ({ html, state, head }: DocumentContext): string => `<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n ${head}\n </head>\n <body>\n <div id=\"app\">${html}</div>\n <script>window.__LLUI_STATE__ = ${state}</script>\n </body>\n</html>`\n\n/**\n * Options for the customized `createOnRenderHtml` factory. Mirrors\n * `@llui/vike/client`'s `RenderClientOptions.Layout` — the same chain\n * shape is accepted for consistency between server and client render.\n */\nexport interface RenderHtmlOptions {\n /** Custom HTML document template. Defaults to a minimal layout. */\n document?: (ctx: DocumentContext) => 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 first, innermost last.\n * Every layer except the innermost must call `pageSlot()` in its view.\n * - A function that returns a chain from the current `pageContext` —\n * enables per-route chains (e.g. reading Vike's `urlPathname`).\n *\n * The server renders the full chain as one composed HTML tree. Client\n * hydration reads the matching envelope and reconstructs the chain\n * layer-by-layer.\n */\n Layout?: AnyComponentDef | LayoutChain | ((pageContext: PageContext) => LayoutChain)\n}\n\nfunction resolveLayoutChain(\n layoutOption: RenderHtmlOptions['Layout'],\n pageContext: PageContext,\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 * Default onRenderHtml hook — no layout, minimal document template.\n */\nexport async function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult> {\n return renderPage(pageContext, {})\n}\n\n/**\n * Factory to create a customized onRenderHtml hook.\n *\n * **Do not name your layout file `+Layout.ts`.** Vike reserves `+Layout`\n * for its own framework-adapter config (`vike-react` / `vike-vue` /\n * `vike-solid`) and will conflict with `@llui/vike`'s `Layout` option.\n * Name the file `Layout.ts`, `app-layout.ts`, or anywhere outside\n * `/pages` that Vike won't scan, and import it here by path.\n *\n * ```ts\n * // pages/+onRenderHtml.ts\n * import { createOnRenderHtml } from '@llui/vike/server'\n * import { AppLayout } from './Layout' // ← NOT './+Layout'\n *\n * export const onRenderHtml = createOnRenderHtml({\n * Layout: AppLayout,\n * document: ({ html, state, head }) => `<!DOCTYPE html>\n * <html><head>${head}<link rel=\"stylesheet\" href=\"/styles.css\" /></head>\n * <body><div id=\"app\">${html}</div>\n * <script>window.__LLUI_STATE__ = ${state}</script></body></html>`,\n * })\n * ```\n */\nexport function createOnRenderHtml(\n options: RenderHtmlOptions,\n): (pageContext: PageContext) => Promise<RenderHtmlResult> {\n return (pageContext) => renderPage(pageContext, options)\n}\n\n/**\n * Hydration envelope emitted into `window.__LLUI_STATE__`. Chain-aware\n * by default — every layer (layouts + page) is represented by its own\n * entry, keyed by component name so server/client mismatches fail loud.\n */\ninterface HydrationEnvelope {\n layouts: Array<{ name: string; state: unknown }>\n page: { name: string; state: unknown }\n}\n\nasync function renderPage(\n pageContext: PageContext,\n options: RenderHtmlOptions,\n): Promise<RenderHtmlResult> {\n // Lazy-import to keep jsdom out of the client bundle's dependency graph\n const { initSsrDom } = await import('@llui/dom/ssr')\n await initSsrDom()\n\n const layoutChain = resolveLayoutChain(options.Layout, pageContext)\n const layoutData = pageContext.lluiLayoutData ?? []\n\n // Full chain: every layout, then the page. Always at least one entry\n // (the page) since Vike's pageContext always has a Page.\n const chain: LayoutChain = [...layoutChain, pageContext.Page]\n const chainData: readonly unknown[] = [...layoutData, pageContext.data]\n\n const { html, envelope } = renderChain(chain, chainData)\n\n const document = options.document ?? DEFAULT_DOCUMENT\n const head = pageContext.head ?? ''\n const state = JSON.stringify(envelope)\n const documentHtml = document({ html, state, head, pageContext })\n\n return {\n // Vike's dangerouslySkipEscape format — the document template is\n // trusted (authored by the developer, not user input)\n documentHtml: { _escaped: documentHtml },\n pageContext: { lluiState: envelope },\n }\n}\n\n/**\n * Render every layer of the chain into one composed DOM tree, then\n * serialize. At each non-innermost layer, consume the pending\n * `pageSlot()` registration and append the next layer's nodes into\n * the slot marker. Scopes are threaded so inner layers inherit the\n * outer layer's scope tree for context lookups.\n */\nfunction renderChain(\n chain: LayoutChain,\n chainData: readonly unknown[],\n): { html: string; envelope: HydrationEnvelope } {\n if (chain.length === 0) {\n throw new Error('[llui/vike] renderChain called with empty chain')\n }\n\n // Defensive: ensure no stale slot leaks in from a prior failed render.\n _resetPendingSlot()\n\n // Accumulate bindings from every layer — serializeNodes needs the\n // full set so hydrate markers are correctly placed across the\n // composed tree.\n const allBindings: Binding[] = []\n const envelopeLayouts: HydrationEnvelope['layouts'] = []\n let envelopePage: HydrationEnvelope['page'] | null = null\n\n let outermostNodes: Node[] = []\n let currentSlotMarker: HTMLElement | null = null\n let currentSlotScope: Scope | undefined = undefined\n\n for (let i = 0; i < chain.length; i++) {\n const def = chain[i]!\n const layerData = chainData[i]\n const isInnermost = i === chain.length - 1\n\n // Resolve the initial state from the layer's own init() applied to\n // its data slice, same as client-side mountApp(). renderNodes takes\n // the state post-init so that the view sees the right fields.\n const [initialState] = def.init(layerData)\n\n // Cross from type-erased AnyComponentDef into the concrete signature\n // renderNodes expects. Same pattern as the client mount path —\n // renderNodes is generic but the runtime doesn't use the type params.\n const { nodes, inst } = renderNodes(\n def as unknown as Parameters<typeof renderNodes>[0],\n initialState,\n currentSlotScope,\n )\n allBindings.push(...inst.allBindings)\n\n if (i === 0) {\n outermostNodes = nodes\n } else {\n // Append this layer's nodes into the previous layer's slot marker.\n // The slot marker is an element owned by the previous layer's DOM;\n // appending here stitches this layer into the composed tree so\n // the final serialization pass emits one integrated HTML string.\n if (!currentSlotMarker) {\n // Unreachable given the error checks below, but defensive.\n throw new Error(`[llui/vike] internal: chain layer ${i} (<${def.name}>) has no slot marker`)\n }\n for (const node of nodes) {\n currentSlotMarker.appendChild(node)\n }\n }\n\n // Record this layer's state in the envelope. Page goes under\n // `page`, everything else under `layouts[]` ordered outer-to-inner.\n if (isInnermost) {\n envelopePage = { name: def.name, state: initialState }\n } else {\n envelopeLayouts.push({ name: def.name, state: initialState })\n }\n\n // Consume this layer's pending slot registration (if any). Every\n // non-innermost layer MUST declare a slot; the innermost MUST NOT.\n const slot = _consumePendingSlot()\n if (isInnermost && slot !== null) {\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 )\n }\n if (!isInnermost && slot === null) {\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.`,\n )\n }\n\n currentSlotMarker = slot?.marker ?? null\n currentSlotScope = slot?.slotScope\n }\n\n const html = serializeNodes(outermostNodes, allBindings)\n\n if (envelopePage === null) {\n // Unreachable — chain is non-empty so the last iteration sets this.\n throw new Error('[llui/vike] internal: renderChain produced no page entry')\n }\n\n return {\n html,\n envelope: {\n layouts: envelopeLayouts,\n page: envelopePage,\n },\n }\n}\n"]}
1
+ {"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"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","sourcesContent":["import { renderNodes, serializeNodes } from '@llui/dom'\nimport type { AnyComponentDef, Binding, Scope } from '@llui/dom'\nimport { _consumePendingSlot, _resetPendingSlot } from './page-slot.js'\nimport type { VikePageContextData } from './vike-namespace.js'\n\ntype LayoutChain = ReadonlyArray<AnyComponentDef>\n\n/**\n * Page context shape as seen by `@llui/vike`'s server hook. `Page` and\n * `data` are whichever `+Page.ts` and `+data.ts` Vike resolved for the\n * current route; `lluiLayoutData` is an optional array of per-layer\n * layout data matching the chain configured on `createOnRenderHtml`.\n *\n * `data` is derived from the global `Vike.PageContext` namespace so that\n * consumer-side augmentations (the Vike convention for typing data) flow\n * into this hook's callbacks without any cast. When the consumer hasn't\n * augmented the namespace, `data` falls back to `unknown`.\n */\nexport interface PageContext {\n Page: AnyComponentDef\n data?: VikePageContextData\n lluiLayoutData?: readonly unknown[]\n head?: string\n}\n\nexport interface DocumentContext {\n /** Rendered component HTML (layout + page composed if a Layout is configured) */\n html: string\n /** JSON-serialized hydration envelope (chain-aware when Layout is configured) */\n state: string\n /** Head content from pageContext.head (e.g. from +Head.ts) */\n head: string\n /** Full page context for custom logic */\n pageContext: PageContext\n}\n\nexport interface RenderHtmlResult {\n documentHtml: string | { _escaped: string }\n pageContext: { lluiState: unknown }\n}\n\nconst DEFAULT_DOCUMENT = ({ html, state, head }: DocumentContext): string => `<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n ${head}\n </head>\n <body>\n <div id=\"app\">${html}</div>\n <script>window.__LLUI_STATE__ = ${state}</script>\n </body>\n</html>`\n\n/**\n * Options for the customized `createOnRenderHtml` factory. Mirrors\n * `@llui/vike/client`'s `RenderClientOptions.Layout` — the same chain\n * shape is accepted for consistency between server and client render.\n */\nexport interface RenderHtmlOptions {\n /** Custom HTML document template. Defaults to a minimal layout. */\n document?: (ctx: DocumentContext) => 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 first, innermost last.\n * Every layer except the innermost must call `pageSlot()` in its view.\n * - A function that returns a chain from the current `pageContext` —\n * enables per-route chains (e.g. reading Vike's `urlPathname`).\n *\n * The server renders the full chain as one composed HTML tree. Client\n * hydration reads the matching envelope and reconstructs the chain\n * layer-by-layer.\n */\n Layout?: AnyComponentDef | LayoutChain | ((pageContext: PageContext) => LayoutChain)\n}\n\nfunction resolveLayoutChain(\n layoutOption: RenderHtmlOptions['Layout'],\n pageContext: PageContext,\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 * Default onRenderHtml hook — no layout, minimal document template.\n */\nexport async function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult> {\n return renderPage(pageContext, {})\n}\n\n/**\n * Factory to create a customized onRenderHtml hook.\n *\n * **Do not name your layout file `+Layout.ts`.** Vike reserves `+Layout`\n * for its own framework-adapter config (`vike-react` / `vike-vue` /\n * `vike-solid`) and will conflict with `@llui/vike`'s `Layout` option.\n * Name the file `Layout.ts`, `app-layout.ts`, or anywhere outside\n * `/pages` that Vike won't scan, and import it here by path.\n *\n * ```ts\n * // pages/+onRenderHtml.ts\n * import { createOnRenderHtml } from '@llui/vike/server'\n * import { AppLayout } from './Layout' // ← NOT './+Layout'\n *\n * export const onRenderHtml = createOnRenderHtml({\n * Layout: AppLayout,\n * document: ({ html, state, head }) => `<!DOCTYPE html>\n * <html><head>${head}<link rel=\"stylesheet\" href=\"/styles.css\" /></head>\n * <body><div id=\"app\">${html}</div>\n * <script>window.__LLUI_STATE__ = ${state}</script></body></html>`,\n * })\n * ```\n */\nexport function createOnRenderHtml(\n options: RenderHtmlOptions,\n): (pageContext: PageContext) => Promise<RenderHtmlResult> {\n return (pageContext) => renderPage(pageContext, options)\n}\n\n/**\n * Hydration envelope emitted into `window.__LLUI_STATE__`. Chain-aware\n * by default — every layer (layouts + page) is represented by its own\n * entry, keyed by component name so server/client mismatches fail loud.\n */\ninterface HydrationEnvelope {\n layouts: Array<{ name: string; state: unknown }>\n page: { name: string; state: unknown }\n}\n\nasync function renderPage(\n pageContext: PageContext,\n options: RenderHtmlOptions,\n): Promise<RenderHtmlResult> {\n // Lazy-import to keep jsdom out of the client bundle's dependency graph\n const { initSsrDom } = await import('@llui/dom/ssr')\n await initSsrDom()\n\n const layoutChain = resolveLayoutChain(options.Layout, pageContext)\n const layoutData = pageContext.lluiLayoutData ?? []\n\n // Full chain: every layout, then the page. Always at least one entry\n // (the page) since Vike's pageContext always has a Page.\n const chain: LayoutChain = [...layoutChain, pageContext.Page]\n const chainData: readonly unknown[] = [...layoutData, pageContext.data]\n\n const { html, envelope } = _renderChain(chain, chainData)\n\n const document = options.document ?? DEFAULT_DOCUMENT\n const head = pageContext.head ?? ''\n const state = JSON.stringify(envelope)\n const documentHtml = document({ html, state, head, pageContext })\n\n return {\n // Vike's dangerouslySkipEscape format — the document template is\n // trusted (authored by the developer, not user input)\n documentHtml: { _escaped: documentHtml },\n pageContext: { lluiState: envelope },\n }\n}\n\n/**\n * Render every layer of the chain into one composed DOM tree, then\n * serialize. At each non-innermost layer, consume the pending\n * `pageSlot()` registration and insert the next layer's nodes as\n * siblings after the anchor comment, bracketed by an end sentinel.\n * Scopes are threaded so inner layers inherit the outer layer's scope\n * tree for context lookups.\n *\n * @internal — exported for unit testing only (`_renderChain`).\n */\nexport function _renderChain(\n chain: LayoutChain,\n chainData: readonly unknown[],\n): { html: string; envelope: HydrationEnvelope } {\n if (chain.length === 0) {\n throw new Error('[llui/vike] renderChain called with empty chain')\n }\n\n // Defensive: ensure no stale slot leaks in from a prior failed render.\n _resetPendingSlot()\n\n // Accumulate bindings from every layer — serializeNodes needs the\n // full set so hydrate markers are correctly placed across the\n // composed tree.\n const allBindings: Binding[] = []\n const envelopeLayouts: HydrationEnvelope['layouts'] = []\n let envelopePage: HydrationEnvelope['page'] | null = null\n\n let outermostNodes: Node[] = []\n let currentSlotAnchor: Comment | null = null\n let currentSlotScope: Scope | undefined = undefined\n\n for (let i = 0; i < chain.length; i++) {\n const def = chain[i]!\n const layerData = chainData[i]\n const isInnermost = i === chain.length - 1\n\n // Resolve the initial state from the layer's own init() applied to\n // its data slice, same as client-side mountApp(). renderNodes takes\n // the state post-init so that the view sees the right fields.\n const [initialState] = def.init(layerData)\n\n // Cross from type-erased AnyComponentDef into the concrete signature\n // renderNodes expects. Same pattern as the client mount path —\n // renderNodes is generic but the runtime doesn't use the type params.\n const { nodes, inst } = renderNodes(\n def as unknown as Parameters<typeof renderNodes>[0],\n initialState,\n currentSlotScope,\n )\n allBindings.push(...inst.allBindings)\n\n if (i === 0) {\n outermostNodes = nodes\n } else {\n // Insert this layer's nodes as siblings immediately after the\n // anchor comment, then place an end sentinel after them.\n // The anchor is already attached to the composed DOM tree (it was\n // produced by the previous layer's pageSlot() call). We insert\n // before the anchor's next sibling so nodes land right after the\n // anchor, preserving any trailing siblings that may exist.\n if (!currentSlotAnchor) {\n // Unreachable given the error checks below, but defensive.\n throw new Error(`[llui/vike] internal: chain layer ${i} (<${def.name}>) has no slot anchor`)\n }\n const parentNode = currentSlotAnchor.parentNode\n if (!parentNode) {\n throw new Error(\n `[llui/vike] internal: slot anchor for layer ${i} (<${def.name}>) is detached`,\n )\n }\n // insertPoint is the node currently after the anchor; inserting\n // before it keeps all new nodes in order immediately after anchor.\n const insertPoint = currentSlotAnchor.nextSibling\n for (const node of nodes) {\n parentNode.insertBefore(node, insertPoint)\n }\n // Synthesize an end sentinel that brackets the owned region.\n const endSentinel = document.createComment('llui-mount-end')\n parentNode.insertBefore(endSentinel, insertPoint)\n }\n\n // Record this layer's state in the envelope. Page goes under\n // `page`, everything else under `layouts[]` ordered outer-to-inner.\n if (isInnermost) {\n envelopePage = { name: def.name, state: initialState }\n } else {\n envelopeLayouts.push({ name: def.name, state: initialState })\n }\n\n // Consume this layer's pending slot registration (if any). Every\n // non-innermost layer MUST declare a slot; the innermost MUST NOT.\n const slot = _consumePendingSlot()\n if (isInnermost && slot !== null) {\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 )\n }\n if (!isInnermost && slot === null) {\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.`,\n )\n }\n\n currentSlotAnchor = slot?.anchor ?? null\n currentSlotScope = slot?.slotScope\n }\n\n const html = serializeNodes(outermostNodes, allBindings)\n\n if (envelopePage === null) {\n // Unreachable — chain is non-empty so the last iteration sets this.\n throw new Error('[llui/vike] internal: renderChain produced no page entry')\n }\n\n return {\n html,\n envelope: {\n layouts: envelopeLayouts,\n page: envelopePage,\n },\n }\n}\n"]}
@@ -8,7 +8,7 @@ import type { Scope } from '@llui/dom';
8
8
  */
9
9
  interface PendingSlot {
10
10
  slotScope: Scope;
11
- marker: HTMLElement;
11
+ anchor: Comment;
12
12
  }
13
13
  /**
14
14
  * Declare where a persistent layout renders its nested content — either
@@ -16,6 +16,11 @@ interface PendingSlot {
16
16
  * client and server render paths walk the layout chain, and each layer's
17
17
  * `pageSlot()` call records the position where the next layer mounts.
18
18
  *
19
+ * Emits a single `<!-- llui-page-slot -->` comment as an insertion
20
+ * anchor. The nested layer's DOM lives as siblings of this comment
21
+ * within the layout's own parent element; a synthesized end sentinel
22
+ * (`<!-- llui-mount-end -->`) brackets the owned region.
23
+ *
19
24
  * The slot is a real scope-tree node: the scope it creates is a child
20
25
  * of the current render scope, so contexts provided by the layout (via
21
26
  * `provide()`) above the slot are reachable from inside the nested
@@ -38,16 +43,15 @@ interface PendingSlot {
38
43
  * update: layoutUpdate,
39
44
  * view: (h) => [
40
45
  * div({ class: 'app-shell' }, [
41
- * header([... persistent chrome ...]),
42
- * main([pageSlot()]), // ← here the page goes
46
+ * header([...]),
47
+ * main([pageSlot()]), // ← here the page goes (no wrapper div)
43
48
  * ]),
44
49
  * ],
45
50
  * })
46
51
  * ```
47
52
  *
48
53
  * Call exactly once per layout. Calling more than once in a single
49
- * view throws — a layout with two slots would be ambiguous about which
50
- * one receives the nested content.
54
+ * view throws.
51
55
  */
52
56
  export declare function pageSlot(): Node[];
53
57
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"page-slot.d.ts","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAQtC;;;;;;GAMG;AACH,UAAU,WAAW;IACnB,SAAS,EAAE,KAAK,CAAA;IAChB,MAAM,EAAE,WAAW,CAAA;CACpB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,QAAQ,IAAI,IAAI,EAAE,CAuBjC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,GAAG,IAAI,CAIxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
1
+ {"version":3,"file":"page-slot.d.ts","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAQtC;;;;;;GAMG;AACH,UAAU,WAAW;IACnB,SAAS,EAAE,KAAK,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,QAAQ,IAAI,IAAI,EAAE,CAoBjC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,GAAG,IAAI,CAIxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
package/dist/page-slot.js CHANGED
@@ -6,6 +6,11 @@ let pendingSlot = null;
6
6
  * client and server render paths walk the layout chain, and each layer's
7
7
  * `pageSlot()` call records the position where the next layer mounts.
8
8
  *
9
+ * Emits a single `<!-- llui-page-slot -->` comment as an insertion
10
+ * anchor. The nested layer's DOM lives as siblings of this comment
11
+ * within the layout's own parent element; a synthesized end sentinel
12
+ * (`<!-- llui-mount-end -->`) brackets the owned region.
13
+ *
9
14
  * The slot is a real scope-tree node: the scope it creates is a child
10
15
  * of the current render scope, so contexts provided by the layout (via
11
16
  * `provide()`) above the slot are reachable from inside the nested
@@ -28,21 +33,18 @@ let pendingSlot = null;
28
33
  * update: layoutUpdate,
29
34
  * view: (h) => [
30
35
  * div({ class: 'app-shell' }, [
31
- * header([... persistent chrome ...]),
32
- * main([pageSlot()]), // ← here the page goes
36
+ * header([...]),
37
+ * main([pageSlot()]), // ← here the page goes (no wrapper div)
33
38
  * ]),
34
39
  * ],
35
40
  * })
36
41
  * ```
37
42
  *
38
43
  * Call exactly once per layout. Calling more than once in a single
39
- * view throws — a layout with two slots would be ambiguous about which
40
- * one receives the nested content.
44
+ * view throws.
41
45
  */
42
46
  export function pageSlot() {
43
47
  if (typeof document === 'undefined') {
44
- // Server path — @llui/dom's renderToString uses jsdom, so document
45
- // IS defined during SSR, but we guard anyway for safety.
46
48
  throw new Error('[llui/vike] pageSlot() called without a DOM environment. ' +
47
49
  'Call from inside a component view() that runs during mount, hydrate, or SSR.');
48
50
  }
@@ -54,10 +56,9 @@ export function pageSlot() {
54
56
  }
55
57
  const ctx = getRenderContext('pageSlot');
56
58
  const slotScope = createScope(ctx.rootScope);
57
- const marker = document.createElement('div');
58
- marker.dataset.lluiPageSlot = '';
59
- pendingSlot = { slotScope, marker };
60
- return [marker];
59
+ const anchor = document.createComment('llui-page-slot');
60
+ pendingSlot = { slotScope, anchor };
61
+ return [anchor];
61
62
  }
62
63
  /**
63
64
  * @internal — vike adapter only. Read and clear the slot registered by
@@ -1 +1 @@
1
- {"version":3,"file":"page-slot.js","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAmBlE,IAAI,WAAW,GAAuB,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,UAAU,QAAQ;IACtB,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,mEAAmE;QACnE,yDAAyD;QACzD,MAAM,IAAI,KAAK,CACb,2DAA2D;YACzD,8EAA8E,CACjF,CAAA;IACH,CAAC;IACD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,6EAA6E;YAC7E,2EAA2E;YAC3E,oEAAoE,CACvE,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC5C,MAAM,CAAC,OAAO,CAAC,YAAY,GAAG,EAAE,CAAA;IAChC,WAAW,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,CAAA;IACnC,OAAO,CAAC,MAAM,CAAC,CAAA;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,IAAI,GAAG,WAAW,CAAA;IACxB,WAAW,GAAG,IAAI,CAAA;IAClB,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,WAAW,GAAG,IAAI,CAAA;AACpB,CAAC","sourcesContent":["import type { Scope } from '@llui/dom'\nimport { getRenderContext, createScope } from '@llui/dom/internal'\n\n// @llui/dom/internal is the adapter-layer surface: low-level primitives\n// (render-context access, scope creation, disposer registration) that\n// framework adapters like @llui/vike need to build structural primitives\n// on top of. Not part of the public app-author API.\n\n/**\n * Transient handoff between a layout layer's render pass and the vike\n * adapter that's mounting the chain. `pageSlot()` populates this during\n * the layout's view() call; `consumePendingSlot()` reads and clears it\n * immediately after the mount returns. One slot per mount pass — calling\n * `pageSlot()` twice in the same layout is a bug the primitive reports.\n */\ninterface PendingSlot {\n slotScope: Scope\n marker: HTMLElement\n}\n\nlet pendingSlot: PendingSlot | null = null\n\n/**\n * Declare where a persistent layout renders its nested content — either\n * a nested layout or the route's page component. The vike adapter's\n * client and server render paths walk the layout chain, and each layer's\n * `pageSlot()` call records the position where the next layer mounts.\n *\n * The slot is a real scope-tree node: the scope it creates is a child\n * of the current render scope, so contexts provided by the layout (via\n * `provide()`) above the slot are reachable from inside the nested\n * page. That's how patterns like a layout-owned toast dispatcher work —\n * the page does `useContext(ToastContext)` and walks up through the\n * slot into the layout's providers.\n *\n * Do NOT name the file `+Layout.ts` — Vike reserves the `+` prefix for\n * its own framework config conventions. Use `Layout.ts`, `app-layout.ts`,\n * or anywhere outside `/pages` that Vike won't scan.\n *\n * ```ts\n * // pages/Layout.ts ← not +Layout.ts\n * import { component, div, main, header } from '@llui/dom'\n * import { pageSlot } from '@llui/vike/client'\n *\n * export const AppLayout = component<LayoutState, LayoutMsg>({\n * name: 'AppLayout',\n * init: () => [{ ... }, []],\n * update: layoutUpdate,\n * view: (h) => [\n * div({ class: 'app-shell' }, [\n * header([... persistent chrome ...]),\n * main([pageSlot()]), // ← here the page goes\n * ]),\n * ],\n * })\n * ```\n *\n * Call exactly once per layout. Calling more than once in a single\n * view throws — a layout with two slots would be ambiguous about which\n * one receives the nested content.\n */\nexport function pageSlot(): Node[] {\n if (typeof document === 'undefined') {\n // Server path — @llui/dom's renderToString uses jsdom, so document\n // IS defined during SSR, but we guard anyway for safety.\n throw new Error(\n '[llui/vike] pageSlot() called without a DOM environment. ' +\n 'Call from inside a component view() that runs during mount, hydrate, or SSR.',\n )\n }\n if (pendingSlot !== null) {\n throw new Error(\n '[llui/vike] pageSlot() was called more than once in the same layout. ' +\n 'A layout has exactly one nested-content slot — if you need two independent ' +\n 'regions that swap on navigation, build them as sibling nested layouts in ' +\n 'the Vike routing tree and use context to share state between them.',\n )\n }\n const ctx = getRenderContext('pageSlot')\n const slotScope = createScope(ctx.rootScope)\n const marker = document.createElement('div')\n marker.dataset.lluiPageSlot = ''\n pendingSlot = { slotScope, marker }\n return [marker]\n}\n\n/**\n * @internal — vike adapter only. Read and clear the slot registered by\n * the most recent `pageSlot()` call. Returns null if the layer being\n * mounted didn't call `pageSlot()` (meaning it's the innermost layer\n * and owns no nested content).\n */\nexport function _consumePendingSlot(): PendingSlot | null {\n const slot = pendingSlot\n pendingSlot = null\n return slot\n}\n\n/**\n * @internal — vike adapter only. Reset the pending slot without reading\n * it. Used defensively in error paths to avoid leaking a pending slot\n * registration into a subsequent mount attempt.\n */\nexport function _resetPendingSlot(): void {\n pendingSlot = null\n}\n"]}
1
+ {"version":3,"file":"page-slot.js","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAmBlE,IAAI,WAAW,GAAuB,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,UAAU,QAAQ;IACtB,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,2DAA2D;YACzD,8EAA8E,CACjF,CAAA;IACH,CAAC;IACD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,6EAA6E;YAC7E,2EAA2E;YAC3E,oEAAoE,CACvE,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAA;IACvD,WAAW,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,CAAA;IACnC,OAAO,CAAC,MAAM,CAAC,CAAA;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,IAAI,GAAG,WAAW,CAAA;IACxB,WAAW,GAAG,IAAI,CAAA;IAClB,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,WAAW,GAAG,IAAI,CAAA;AACpB,CAAC","sourcesContent":["import type { Scope } from '@llui/dom'\nimport { getRenderContext, createScope } from '@llui/dom/internal'\n\n// @llui/dom/internal is the adapter-layer surface: low-level primitives\n// (render-context access, scope creation, disposer registration) that\n// framework adapters like @llui/vike need to build structural primitives\n// on top of. Not part of the public app-author API.\n\n/**\n * Transient handoff between a layout layer's render pass and the vike\n * adapter that's mounting the chain. `pageSlot()` populates this during\n * the layout's view() call; `consumePendingSlot()` reads and clears it\n * immediately after the mount returns. One slot per mount pass — calling\n * `pageSlot()` twice in the same layout is a bug the primitive reports.\n */\ninterface PendingSlot {\n slotScope: Scope\n anchor: Comment\n}\n\nlet pendingSlot: PendingSlot | null = null\n\n/**\n * Declare where a persistent layout renders its nested content — either\n * a nested layout or the route's page component. The vike adapter's\n * client and server render paths walk the layout chain, and each layer's\n * `pageSlot()` call records the position where the next layer mounts.\n *\n * Emits a single `<!-- llui-page-slot -->` comment as an insertion\n * anchor. The nested layer's DOM lives as siblings of this comment\n * within the layout's own parent element; a synthesized end sentinel\n * (`<!-- llui-mount-end -->`) brackets the owned region.\n *\n * The slot is a real scope-tree node: the scope it creates is a child\n * of the current render scope, so contexts provided by the layout (via\n * `provide()`) above the slot are reachable from inside the nested\n * page. That's how patterns like a layout-owned toast dispatcher work —\n * the page does `useContext(ToastContext)` and walks up through the\n * slot into the layout's providers.\n *\n * Do NOT name the file `+Layout.ts` — Vike reserves the `+` prefix for\n * its own framework config conventions. Use `Layout.ts`, `app-layout.ts`,\n * or anywhere outside `/pages` that Vike won't scan.\n *\n * ```ts\n * // pages/Layout.ts ← not +Layout.ts\n * import { component, div, main, header } from '@llui/dom'\n * import { pageSlot } from '@llui/vike/client'\n *\n * export const AppLayout = component<LayoutState, LayoutMsg>({\n * name: 'AppLayout',\n * init: () => [{ ... }, []],\n * update: layoutUpdate,\n * view: (h) => [\n * div({ class: 'app-shell' }, [\n * header([...]),\n * main([pageSlot()]), // ← here the page goes (no wrapper div)\n * ]),\n * ],\n * })\n * ```\n *\n * Call exactly once per layout. Calling more than once in a single\n * view throws.\n */\nexport function pageSlot(): Node[] {\n if (typeof document === 'undefined') {\n throw new Error(\n '[llui/vike] pageSlot() called without a DOM environment. ' +\n 'Call from inside a component view() that runs during mount, hydrate, or SSR.',\n )\n }\n if (pendingSlot !== null) {\n throw new Error(\n '[llui/vike] pageSlot() was called more than once in the same layout. ' +\n 'A layout has exactly one nested-content slot — if you need two independent ' +\n 'regions that swap on navigation, build them as sibling nested layouts in ' +\n 'the Vike routing tree and use context to share state between them.',\n )\n }\n const ctx = getRenderContext('pageSlot')\n const slotScope = createScope(ctx.rootScope)\n const anchor = document.createComment('llui-page-slot')\n pendingSlot = { slotScope, anchor }\n return [anchor]\n}\n\n/**\n * @internal — vike adapter only. Read and clear the slot registered by\n * the most recent `pageSlot()` call. Returns null if the layer being\n * mounted didn't call `pageSlot()` (meaning it's the innermost layer\n * and owns no nested content).\n */\nexport function _consumePendingSlot(): PendingSlot | null {\n const slot = pendingSlot\n pendingSlot = null\n return slot\n}\n\n/**\n * @internal — vike adapter only. Reset the pending slot without reading\n * it. Used defensively in error paths to avoid leaking a pending slot\n * registration into a subsequent mount attempt.\n */\nexport function _resetPendingSlot(): void {\n pendingSlot = null\n}\n"]}
@@ -0,0 +1,28 @@
1
+ export {};
2
+ declare global {
3
+ namespace Vike {
4
+ interface PageContext {
5
+ }
6
+ interface PageContextServer {
7
+ }
8
+ interface PageContextClient {
9
+ }
10
+ }
11
+ }
12
+ /**
13
+ * Extracts the user-augmented `data` type from `Vike.PageContext` with a
14
+ * safe fallback via conditional inference. `extends { data?: infer T }`
15
+ * works for every case:
16
+ *
17
+ * - Not augmented (`interface PageContext {}`): `{}` matches the optional
18
+ * shape vacuously ⇒ `T` is inferred as `unknown` (the default).
19
+ * - Augmented with `data?: PageData`: resolves to `PageData | undefined`.
20
+ * - Augmented with `data: PageData` (non-optional): resolves to `PageData`.
21
+ *
22
+ * Using `keyof` + indexed access (`Vike.PageContext['data']`) doesn't work
23
+ * here — TypeScript refuses to index when the key isn't in the interface.
24
+ */
25
+ export type VikePageContextData = Vike.PageContext extends {
26
+ data?: infer T;
27
+ } ? T : unknown;
28
+ //# sourceMappingURL=vike-namespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vike-namespace.d.ts","sourceRoot":"","sources":["../src/vike-namespace.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,CAAA;AAET,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,IAAI,CAAC;QAEb,UAAU,WAAW;SAAG;QAExB,UAAU,iBAAiB;SAAG;QAE9B,UAAU,iBAAiB;SAAG;KAC/B;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,WAAW,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,OAAO,CAAA"}
@@ -0,0 +1,18 @@
1
+ // Ambient stub for Vike's global namespace.
2
+ //
3
+ // `@llui/vike` adapts to Vike but doesn't declare `vike` as a runtime
4
+ // dependency — the consuming app installs both. Vike itself ships a
5
+ // `VikeNamespace.d.ts` that declares `namespace Vike { interface PageContext { ... } }`
6
+ // globally. When an app also augments the namespace (recommended pattern
7
+ // for typing `pageContext.data`) TypeScript merges every declaration it
8
+ // sees into one interface.
9
+ //
10
+ // This stub lets `@llui/vike` type-check in isolation (without vike
11
+ // installed) AND participates in the same merge when vike IS installed
12
+ // alongside it in a consumer's project. Empty interfaces merge cleanly
13
+ // with anything; the user's augmentation propagates through.
14
+ //
15
+ // The file has no runtime side effects — TypeScript picks up the
16
+ // `declare global` from its inclusion in the source graph.
17
+ export {};
18
+ //# sourceMappingURL=vike-namespace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vike-namespace.js","sourceRoot":"","sources":["../src/vike-namespace.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,wFAAwF;AACxF,yEAAyE;AACzE,wEAAwE;AACxE,2BAA2B;AAC3B,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,uEAAuE;AACvE,6DAA6D;AAC7D,EAAE;AACF,iEAAiE;AACjE,2DAA2D;AAE3D,OAAO,EAAE,CAAA","sourcesContent":["// Ambient stub for Vike's global namespace.\n//\n// `@llui/vike` adapts to Vike but doesn't declare `vike` as a runtime\n// dependency — the consuming app installs both. Vike itself ships a\n// `VikeNamespace.d.ts` that declares `namespace Vike { interface PageContext { ... } }`\n// globally. When an app also augments the namespace (recommended pattern\n// for typing `pageContext.data`) TypeScript merges every declaration it\n// sees into one interface.\n//\n// This stub lets `@llui/vike` type-check in isolation (without vike\n// installed) AND participates in the same merge when vike IS installed\n// alongside it in a consumer's project. Empty interfaces merge cleanly\n// with anything; the user's augmentation propagates through.\n//\n// The file has no runtime side effects — TypeScript picks up the\n// `declare global` from its inclusion in the source graph.\n\nexport {}\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Vike {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface PageContext {}\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface PageContextServer {}\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface PageContextClient {}\n }\n}\n\n/**\n * Extracts the user-augmented `data` type from `Vike.PageContext` with a\n * safe fallback via conditional inference. `extends { data?: infer T }`\n * works for every case:\n *\n * - Not augmented (`interface PageContext {}`): `{}` matches the optional\n * shape vacuously ⇒ `T` is inferred as `unknown` (the default).\n * - Augmented with `data?: PageData`: resolves to `PageData | undefined`.\n * - Augmented with `data: PageData` (non-optional): resolves to `PageData`.\n *\n * Using `keyof` + indexed access (`Vike.PageContext['data']`) doesn't work\n * here — TypeScript refuses to index when the key isn't in the interface.\n */\nexport type VikePageContextData = Vike.PageContext extends { data?: infer T } ? T : unknown\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/vike",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "jsdom": "^26.1.0",
25
- "@llui/dom": "0.0.18"
25
+ "@llui/dom": "0.0.20"
26
26
  },
27
27
  "description": "LLui Vike SSR adapter — onRenderHtml, onRenderClient hooks",
28
28
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "homepage": "https://github.com/fponticelli/llui/tree/main/packages/vike#readme",
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.build.json",
47
- "check": "tsc --noEmit",
47
+ "check": "tsc --noEmit && tsc --noEmit -p tsconfig.test-types.json",
48
48
  "lint": "eslint src",
49
49
  "test": "vitest run",
50
50
  "test:coverage": "vitest run --coverage"