@llui/vike 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,6 +57,47 @@ export const onRenderClient = createOnRenderClient({
57
57
  })
58
58
  ```
59
59
 
60
+ ### Page Transitions
61
+
62
+ `createOnRenderClient` accepts `onLeave` and `onEnter` hooks that fire around the dispose-and-remount cycle on client navigation. `onLeave` is awaited — return a promise to defer the swap until a leave animation finishes:
63
+
64
+ ```ts
65
+ import { createOnRenderClient, fromTransition } from '@llui/vike/client'
66
+ import { routeTransition } from '@llui/transitions'
67
+
68
+ export const onRenderClient = createOnRenderClient({
69
+ ...fromTransition(routeTransition({ duration: 200 })),
70
+ })
71
+ ```
72
+
73
+ `fromTransition` adapts any `TransitionOptions` (the shape returned by `routeTransition`, `fade`, `slide`, etc.) into the hook pair. The transition operates on the container element — its opacity / transform fades out the outgoing page, then the new page fades in after mount.
74
+
75
+ For raw animations without `@llui/transitions`, write the hooks yourself:
76
+
77
+ ```ts
78
+ export const onRenderClient = createOnRenderClient({
79
+ onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
80
+ onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
81
+ })
82
+ ```
83
+
84
+ ### Client Navigation Lifecycle
85
+
86
+ When Vike fires a client-side navigation, `@llui/vike` runs this sequence inside `onRenderClient`:
87
+
88
+ 1. **`onLeave(el)`** — awaited. The outgoing page's DOM is still mounted; this is the only moment where a leave animation can read/write it.
89
+ 2. **`currentHandle.dispose()`** — tears down the outgoing component's scope tree. All `onMount` cleanups run here, portals are removed from their targets, focus traps are popped, body scroll locks release, sibling `aria-hidden` is restored. The regression test in `@llui/components/test/components/dialog-dispose.test.ts` covers this path explicitly.
90
+ 3. **`el.textContent = ''`** — the outgoing DOM is cleared from the container.
91
+ 4. **`mountApp(el, Page, data)`** — the new page mounts.
92
+ 5. **`onEnter(el)`** — synchronous; fire-and-forget. Promises are ignored here.
93
+ 6. **`onMount()`** — legacy hook, fires last on every render (including the initial hydration).
94
+
95
+ On the initial hydration render, `onLeave` and `onEnter` are both skipped — there's no outgoing page to leave, and hydration doesn't insert new DOM that needs an enter animation.
96
+
97
+ **AbortSignal semantics for in-flight effects.** When a component is disposed, its `AbortController` fires and `inst.signal.aborted` becomes `true`. Effect handlers should guard their `send()` calls against `signal.aborted` — the base package already does this in `@llui/effects`. Network requests that have already been accepted by the server are NOT cancelled by navigation; cancellation only applies to future `send()` dispatches into the now-aborted instance. This is intentional: cancelling a successful signup POST just because the user clicked a nav link would lose data.
98
+
99
+ **Scroll position is the host's problem.** Vike controls scroll-to-top behavior via `scrollToTop` in `+config.ts`. `@llui/vike` doesn't touch scroll — if you need custom scroll handling, configure it on the Vike side.
100
+
60
101
  ## How It Works
61
102
 
62
103
  ### Server (`onRenderHtml`)
@@ -69,11 +110,12 @@ Hydrates the server-rendered HTML on the client. Attaches event listeners and re
69
110
 
70
111
  ## API
71
112
 
72
- | Export | Sub-path | Description |
73
- | ---------------------- | ------------------- | ------------------------------------------- |
74
- | `onRenderHtml` | `@llui/vike/server` | Default server hook — minimal HTML template |
75
- | `createOnRenderHtml` | `@llui/vike/server` | Factory for custom document templates |
76
- | `onRenderClient` | `@llui/vike/client` | Default client hook — hydrate or mount |
77
- | `createOnRenderClient` | `@llui/vike/client` | Factory for custom container/lifecycle |
113
+ | Export | Sub-path | Description |
114
+ | ---------------------- | ------------------- | ---------------------------------------------------------------- |
115
+ | `onRenderHtml` | `@llui/vike/server` | Default server hook — minimal HTML template |
116
+ | `createOnRenderHtml` | `@llui/vike/server` | Factory for custom document templates |
117
+ | `onRenderClient` | `@llui/vike/client` | Default client hook — hydrate or mount |
118
+ | `createOnRenderClient` | `@llui/vike/client` | Factory for custom container + `onLeave` / `onEnter` / `onMount` |
119
+ | `fromTransition` | `@llui/vike/client` | Adapter: `TransitionOptions` → `{ onLeave, onEnter }` hook pair |
78
120
 
79
121
  The barrel export (`@llui/vike`) re-exports everything, but prefer sub-path imports to avoid bundling jsdom into the client.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { onRenderHtml, createOnRenderHtml } from './on-render-html';
2
- export type { PageContext, DocumentContext, RenderHtmlResult } from './on-render-html';
3
- export { onRenderClient, createOnRenderClient } from './on-render-client';
4
- export type { ClientPageContext, RenderClientOptions } from './on-render-client';
1
+ export { onRenderHtml, createOnRenderHtml } from './on-render-html.js';
2
+ export type { PageContext, DocumentContext, RenderHtmlResult } from './on-render-html.js';
3
+ export { onRenderClient, createOnRenderClient, fromTransition } from './on-render-client.js';
4
+ export type { ClientPageContext, RenderClientOptions } from './on-render-client.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACnE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEtF,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzE,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEzF,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC5F,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { onRenderHtml, createOnRenderHtml } from './on-render-html';
2
- export { onRenderClient, createOnRenderClient } from './on-render-client';
1
+ export { onRenderHtml, createOnRenderHtml } from './on-render-html.js';
2
+ export { onRenderClient, createOnRenderClient, fromTransition } from './on-render-client.js';
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAGnE,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAGtE,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA","sourcesContent":["export { onRenderHtml, createOnRenderHtml } from './on-render-html.js'\nexport type { PageContext, DocumentContext, RenderHtmlResult } from './on-render-html.js'\n\nexport { onRenderClient, createOnRenderClient, fromTransition } from './on-render-client.js'\nexport type { ClientPageContext, RenderClientOptions } from './on-render-client.js'\n"]}
@@ -1,4 +1,4 @@
1
- import type { ComponentDef } from '@llui/dom';
1
+ import type { ComponentDef, TransitionOptions } from '@llui/dom';
2
2
  declare global {
3
3
  interface Window {
4
4
  __LLUI_STATE__?: unknown;
@@ -9,15 +9,102 @@ export interface ClientPageContext {
9
9
  data?: unknown;
10
10
  isHydration?: boolean;
11
11
  }
12
+ /**
13
+ * Page-lifecycle hooks that fire around the dispose → clear → mount
14
+ * sequence on client navigation. Use these to animate page transitions,
15
+ * save scroll state, emit analytics events, or defer the swap behind
16
+ * any async work that must complete before the next page appears.
17
+ *
18
+ * The sequence is:
19
+ *
20
+ * ```
21
+ * client nav triggered
22
+ * │
23
+ * ▼
24
+ * onLeave(el) ← awaited if it returns a promise
25
+ * │ (the outgoing page's DOM is still mounted here)
26
+ * ▼
27
+ * currentHandle.dispose()
28
+ * │ (all scopes torn down — portals, focus traps,
29
+ * │ onMount cleanups all fire synchronously here)
30
+ * ▼
31
+ * el.textContent = ''
32
+ * │ (old DOM removed)
33
+ * ▼
34
+ * mountApp(el, Page, data)
35
+ * │ (new page mounted)
36
+ * ▼
37
+ * onEnter(el) ← not awaited; animate in-place
38
+ * │
39
+ * ▼
40
+ * onMount() ← legacy shim, still fires last
41
+ * ```
42
+ *
43
+ * On the initial render (hydration), `onLeave` and `onEnter` are NOT
44
+ * called — there's no outgoing page to leave and no animation to enter.
45
+ * If you need to run code after hydration, use `onMount`.
46
+ */
12
47
  export interface RenderClientOptions {
13
48
  /** CSS selector for the mount container. Default: '#app' */
14
49
  container?: string;
15
- /** Called after mount or hydration completes */
50
+ /**
51
+ * Called on the outgoing page's container BEFORE dispose + clear + mount.
52
+ * Return a promise to defer the swap until the leave animation finishes.
53
+ * The container element is passed as the argument — its children are
54
+ * still the previous page's DOM at this point.
55
+ *
56
+ * Not called on the initial hydration render.
57
+ */
58
+ onLeave?: (el: HTMLElement) => void | Promise<void>;
59
+ /**
60
+ * Called after the new page is mounted into the container. Use this to
61
+ * kick off an enter animation on the freshly-rendered content. Not
62
+ * awaited — if you return a promise, the resolution is ignored.
63
+ *
64
+ * Not called on the initial hydration render.
65
+ */
66
+ onEnter?: (el: HTMLElement) => void;
67
+ /**
68
+ * Called after mount or hydration completes. Fires on every render
69
+ * including the initial hydration. Use this for per-render side
70
+ * effects that don't fit the animation hooks (analytics, focus
71
+ * management, etc.).
72
+ */
16
73
  onMount?: () => void;
17
74
  }
18
75
  /**
19
- * Default onRenderClient hook.
20
- * Hydrates if isHydration is true, otherwise mounts fresh.
76
+ * Adapt a `TransitionOptions` object (e.g. the output of
77
+ * `routeTransition()` from `@llui/transitions`, or any preset like
78
+ * `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected
79
+ * by `createOnRenderClient`.
80
+ *
81
+ * ```typescript
82
+ * import { createOnRenderClient, fromTransition } from '@llui/vike/client'
83
+ * import { routeTransition } from '@llui/transitions'
84
+ *
85
+ * export const onRenderClient = createOnRenderClient({
86
+ * ...fromTransition(routeTransition({ duration: 200 })),
87
+ * })
88
+ * ```
89
+ *
90
+ * The transition operates on the container element itself — its
91
+ * opacity / transform fades out the outgoing page, then the new page
92
+ * fades in when it mounts. If the preset doesn't restore its starting
93
+ * style on `leave`, the container may still carry leftover properties
94
+ * when the new page mounts; use `enter` to reset them explicitly or
95
+ * pick presets that self-clean.
96
+ */
97
+ export declare function fromTransition(t: TransitionOptions): Pick<RenderClientOptions, 'onLeave' | 'onEnter'>;
98
+ /**
99
+ * @internal — test helper. Disposes the current handle (if any) and clears
100
+ * the module-level state so subsequent calls behave as a first mount.
101
+ * Not part of the public API; subject to change without notice.
102
+ */
103
+ export declare function _resetCurrentHandleForTest(): void;
104
+ /**
105
+ * Default onRenderClient hook — no animation hooks. Hydrates if
106
+ * `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`
107
+ * for the customizable factory form.
21
108
  */
22
109
  export declare function onRenderClient(pageContext: ClientPageContext): Promise<void>;
23
110
  /**
@@ -29,6 +116,8 @@ export declare function onRenderClient(pageContext: ClientPageContext): Promise<
29
116
  *
30
117
  * export const onRenderClient = createOnRenderClient({
31
118
  * container: '#root',
119
+ * onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
120
+ * onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
32
121
  * onMount: () => console.log('Page ready'),
33
122
  * })
34
123
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"on-render-client.d.ts","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,WAAW,CAAA;AAExD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,cAAc,CAAC,EAAE,OAAO,CAAA;KACzB;CACF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAKD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElF;AAED;;;;;;;;;;;;GAYG;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,YAAY,EAAa,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAE3E,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,cAAc,CAAC,EAAE,OAAO,CAAA;KACzB;CACF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;OAOG;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;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,iBAAiB,GACnB,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC,CAgBlD;AAMD;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,mBAAmB,GAC3B,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAEnD"}
@@ -1,12 +1,64 @@
1
1
  import { hydrateApp, mountApp } from '@llui/dom';
2
- // Track the current app handle so we can dispose it on client navigation
2
+ /**
3
+ * Adapt a `TransitionOptions` object (e.g. the output of
4
+ * `routeTransition()` from `@llui/transitions`, or any preset like
5
+ * `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected
6
+ * by `createOnRenderClient`.
7
+ *
8
+ * ```typescript
9
+ * import { createOnRenderClient, fromTransition } from '@llui/vike/client'
10
+ * import { routeTransition } from '@llui/transitions'
11
+ *
12
+ * export const onRenderClient = createOnRenderClient({
13
+ * ...fromTransition(routeTransition({ duration: 200 })),
14
+ * })
15
+ * ```
16
+ *
17
+ * The transition operates on the container element itself — its
18
+ * opacity / transform fades out the outgoing page, then the new page
19
+ * fades in when it mounts. If the preset doesn't restore its starting
20
+ * style on `leave`, the container may still carry leftover properties
21
+ * when the new page mounts; use `enter` to reset them explicitly or
22
+ * pick presets that self-clean.
23
+ */
24
+ export function fromTransition(t) {
25
+ return {
26
+ onLeave: t.leave
27
+ ? (el) => {
28
+ const result = t.leave([el]);
29
+ return result && typeof result.then === 'function'
30
+ ? result
31
+ : undefined;
32
+ }
33
+ : undefined,
34
+ onEnter: t.enter
35
+ ? (el) => {
36
+ t.enter([el]);
37
+ }
38
+ : undefined,
39
+ };
40
+ }
41
+ // Track the current app handle so we can dispose it on client navigation.
42
+ // Module-level state: there's exactly one Vike-managed app per page load.
3
43
  let currentHandle = null;
4
44
  /**
5
- * Default onRenderClient hook.
6
- * Hydrates if isHydration is true, otherwise mounts fresh.
45
+ * @internal test helper. Disposes the current handle (if any) and clears
46
+ * the module-level state so subsequent calls behave as a first mount.
47
+ * Not part of the public API; subject to change without notice.
48
+ */
49
+ export function _resetCurrentHandleForTest() {
50
+ if (currentHandle) {
51
+ currentHandle.dispose();
52
+ currentHandle = null;
53
+ }
54
+ }
55
+ /**
56
+ * Default onRenderClient hook — no animation hooks. Hydrates if
57
+ * `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`
58
+ * for the customizable factory form.
7
59
  */
8
60
  export async function onRenderClient(pageContext) {
9
- renderClient(pageContext, {});
61
+ await renderClient(pageContext, {});
10
62
  }
11
63
  /**
12
64
  * Factory to create a customized onRenderClient hook.
@@ -17,6 +69,8 @@ export async function onRenderClient(pageContext) {
17
69
  *
18
70
  * export const onRenderClient = createOnRenderClient({
19
71
  * container: '#root',
72
+ * onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,
73
+ * onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),
20
74
  * onMount: () => console.log('Page ready'),
21
75
  * })
22
76
  * ```
@@ -31,20 +85,30 @@ async function renderClient(pageContext, options) {
31
85
  if (!container) {
32
86
  throw new Error(`@llui/vike: container "${selector}" not found in DOM`);
33
87
  }
34
- // Dispose previous page's component on client navigation
88
+ const el = container;
89
+ // Dispose the previous page's component on client navigation. If the
90
+ // caller supplied an onLeave hook and this isn't the initial hydration,
91
+ // await it BEFORE tearing down — that's the only moment where the
92
+ // outgoing page's DOM still exists for an animation to read/write.
35
93
  if (currentHandle) {
94
+ if (!pageContext.isHydration && options.onLeave) {
95
+ await options.onLeave(el);
96
+ }
36
97
  currentHandle.dispose();
37
98
  currentHandle = null;
38
99
  }
39
- const el = container;
40
100
  if (pageContext.isHydration) {
41
101
  const serverState = window.__LLUI_STATE__;
42
102
  currentHandle = hydrateApp(el, Page, serverState);
43
103
  }
44
104
  else {
45
- // Clear old DOM before mounting new page
105
+ // Clear old DOM before mounting the new page
46
106
  el.textContent = '';
47
107
  currentHandle = mountApp(el, Page, pageContext.data);
108
+ // onEnter fires AFTER mount so the hook can animate the freshly
109
+ // rendered children. It's intentionally sync — a promise return is
110
+ // ignored, matching typical enter-animation ergonomics (fire-and-forget).
111
+ options.onEnter?.(el);
48
112
  }
49
113
  options.onMount?.();
50
114
  }
@@ -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;AAsBhD,yEAAyE;AACzE,IAAI,aAAa,GAAqB,IAAI,CAAA;AAE1C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,WAA8B;IACjE,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;;;;;;;;GAYG;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,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;IAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAA;IAC5C,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;IAElD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,oBAAoB,CAAC,CAAA;IACzE,CAAC;IAED,yDAAyD;IACzD,IAAI,aAAa,EAAE,CAAC;QAClB,aAAa,CAAC,OAAO,EAAE,CAAA;QACvB,aAAa,GAAG,IAAI,CAAA;IACtB,CAAC;IAED,MAAM,EAAE,GAAG,SAAwB,CAAA;IAEnC,IAAI,WAAW,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,MAAM,CAAC,cAAc,CAAA;QACzC,aAAa,GAAG,UAAU,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;IACnD,CAAC;SAAM,CAAC;QACN,yCAAyC;QACzC,EAAE,CAAC,WAAW,GAAG,EAAE,CAAA;QACnB,aAAa,GAAG,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IACtD,CAAC;IAED,OAAO,CAAC,OAAO,EAAE,EAAE,CAAA;AACrB,CAAC"}
1
+ {"version":3,"file":"on-render-client.js","sourceRoot":"","sources":["../src/on-render-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { hydrateApp, mountApp } from '@llui/dom'\nimport type { ComponentDef, AppHandle, TransitionOptions } from '@llui/dom'\n\ndeclare global {\n interface Window {\n __LLUI_STATE__?: unknown\n }\n}\n\nexport interface ClientPageContext {\n Page: ComponentDef<unknown, unknown, unknown, unknown>\n data?: unknown\n isHydration?: boolean\n}\n\n/**\n * Page-lifecycle hooks that fire around the dispose → clear → mount\n * sequence on client navigation. Use these to animate page transitions,\n * save scroll state, emit analytics events, or defer the swap behind\n * any async work that must complete before the next page appears.\n *\n * The sequence is:\n *\n * ```\n * client nav triggered\n * │\n * ▼\n * onLeave(el) ← awaited if it returns a promise\n * │ (the outgoing page's DOM is still mounted here)\n * ▼\n * currentHandle.dispose()\n * │ (all scopes torn down — portals, focus traps,\n * │ onMount cleanups all fire synchronously here)\n * ▼\n * el.textContent = ''\n * │ (old DOM removed)\n * ▼\n * mountApp(el, Page, data)\n * │ (new page mounted)\n * ▼\n * onEnter(el) ← not awaited; animate in-place\n * │\n * ▼\n * onMount() ← legacy shim, still fires last\n * ```\n *\n * On the initial render (hydration), `onLeave` and `onEnter` are NOT\n * called — there's no outgoing page to leave and no animation to enter.\n * If you need to run code after hydration, use `onMount`.\n */\nexport interface RenderClientOptions {\n /** CSS selector for the mount container. Default: '#app' */\n container?: string\n\n /**\n * Called on the outgoing page's container BEFORE dispose + clear + mount.\n * Return a promise to defer the swap until the leave animation finishes.\n * The container element is passed as the argument — its children are\n * still the previous page's DOM at this point.\n *\n * Not called on the initial hydration render.\n */\n onLeave?: (el: HTMLElement) => void | Promise<void>\n\n /**\n * Called after the new page is mounted into the container. Use this to\n * kick off an enter animation on the freshly-rendered content. Not\n * awaited — if you return a promise, the resolution is ignored.\n *\n * Not called on the initial hydration render.\n */\n onEnter?: (el: HTMLElement) => void\n\n /**\n * Called after mount or hydration completes. Fires on every render\n * including the initial hydration. Use this for per-render side\n * effects that don't fit the animation hooks (analytics, focus\n * management, etc.).\n */\n onMount?: () => void\n}\n\n/**\n * Adapt a `TransitionOptions` object (e.g. the output of\n * `routeTransition()` from `@llui/transitions`, or any preset like\n * `fade()` / `slide()`) into the `onLeave` / `onEnter` shape expected\n * by `createOnRenderClient`.\n *\n * ```typescript\n * import { createOnRenderClient, fromTransition } from '@llui/vike/client'\n * import { routeTransition } from '@llui/transitions'\n *\n * export const onRenderClient = createOnRenderClient({\n * ...fromTransition(routeTransition({ duration: 200 })),\n * })\n * ```\n *\n * The transition operates on the container element itself — its\n * opacity / transform fades out the outgoing page, then the new page\n * fades in when it mounts. If the preset doesn't restore its starting\n * style on `leave`, the container may still carry leftover properties\n * when the new page mounts; use `enter` to reset them explicitly or\n * pick presets that self-clean.\n */\nexport function fromTransition(\n t: TransitionOptions,\n): Pick<RenderClientOptions, 'onLeave' | 'onEnter'> {\n return {\n onLeave: t.leave\n ? (el): void | Promise<void> => {\n const result = t.leave!([el])\n return result && typeof (result as Promise<void>).then === 'function'\n ? (result as Promise<void>)\n : undefined\n }\n : undefined,\n onEnter: t.enter\n ? (el): void => {\n t.enter!([el])\n }\n : undefined,\n }\n}\n\n// Track the current app handle so we can dispose it on client navigation.\n// Module-level state: there's exactly one Vike-managed app per page load.\nlet currentHandle: AppHandle | null = null\n\n/**\n * @internal — test helper. Disposes the current handle (if any) and clears\n * the module-level state so subsequent calls behave as a first mount.\n * Not part of the public API; subject to change without notice.\n */\nexport function _resetCurrentHandleForTest(): void {\n if (currentHandle) {\n currentHandle.dispose()\n currentHandle = null\n }\n}\n\n/**\n * Default onRenderClient hook — no animation hooks. Hydrates if\n * `isHydration` is true, otherwise mounts fresh. Use `createOnRenderClient`\n * for the customizable factory form.\n */\nexport async function onRenderClient(pageContext: ClientPageContext): Promise<void> {\n await renderClient(pageContext, {})\n}\n\n/**\n * Factory to create a customized onRenderClient hook.\n *\n * ```typescript\n * // pages/+onRenderClient.ts\n * import { createOnRenderClient } from '@llui/vike/client'\n *\n * export const onRenderClient = createOnRenderClient({\n * container: '#root',\n * onLeave: (el) => el.animate({ opacity: [1, 0] }, 200).finished,\n * onEnter: (el) => el.animate({ opacity: [0, 1] }, 200),\n * onMount: () => console.log('Page ready'),\n * })\n * ```\n */\nexport function createOnRenderClient(\n options: RenderClientOptions,\n): (pageContext: ClientPageContext) => Promise<void> {\n return (pageContext) => renderClient(pageContext, options)\n}\n\nasync function renderClient(\n pageContext: ClientPageContext,\n options: RenderClientOptions,\n): Promise<void> {\n const { Page } = pageContext\n const selector = options.container ?? '#app'\n const container = document.querySelector(selector)\n\n if (!container) {\n throw new Error(`@llui/vike: container \"${selector}\" not found in DOM`)\n }\n\n const el = container as HTMLElement\n\n // Dispose the previous page's component on client navigation. If the\n // caller supplied an onLeave hook and this isn't the initial hydration,\n // await it BEFORE tearing down — that's the only moment where the\n // outgoing page's DOM still exists for an animation to read/write.\n if (currentHandle) {\n if (!pageContext.isHydration && options.onLeave) {\n await options.onLeave(el)\n }\n currentHandle.dispose()\n currentHandle = null\n }\n\n if (pageContext.isHydration) {\n const serverState = window.__LLUI_STATE__\n currentHandle = hydrateApp(el, Page, serverState)\n } else {\n // Clear old DOM before mounting the new page\n el.textContent = ''\n currentHandle = mountApp(el, Page, pageContext.data)\n // onEnter fires AFTER mount so the hook can animate the freshly\n // rendered children. It's intentionally sync — a promise return is\n // ignored, matching typical enter-animation ergonomics (fire-and-forget).\n options.onEnter?.(el)\n }\n\n options.onMount?.()\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAyB1C,MAAM,gBAAgB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAmB,EAAU,EAAE,CAAC;;;;MAIvE,IAAI;;;oBAGU,IAAI;sCACc,KAAK;;QAEnC,CAAA;AAER;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAwB;IACzD,OAAO,UAAU,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAElC;IACC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;AACnE,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,WAAwB,EACxB,QAA0C;IAE1C,wEAAwE;IACxE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;IACpD,MAAM,UAAU,EAAE,CAAA;IAElB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;IAClC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAA;IAEnC,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAEjE,OAAO;QACL,kEAAkE;QAClE,yDAAyD;QACzD,YAAY,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;QACxC,WAAW,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;KACzC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAyB1C,MAAM,gBAAgB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAmB,EAAU,EAAE,CAAC;;;;MAIvE,IAAI;;;oBAGU,IAAI;sCACc,KAAK;;QAEnC,CAAA;AAER;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAwB;IACzD,OAAO,UAAU,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAElC;IACC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;AACnE,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,WAAwB,EACxB,QAA0C;IAE1C,wEAAwE;IACxE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;IACpD,MAAM,UAAU,EAAE,CAAA;IAElB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;IAClC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAA;IAEnC,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAEjE,OAAO;QACL,kEAAkE;QAClE,yDAAyD;QACzD,YAAY,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;QACxC,WAAW,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;KACzC,CAAA;AACH,CAAC","sourcesContent":["import { renderToString } from '@llui/dom'\nimport type { ComponentDef } from '@llui/dom'\n\nexport interface PageContext {\n Page: ComponentDef<unknown, unknown, unknown, unknown>\n data?: unknown\n head?: string\n}\n\nexport interface DocumentContext {\n /** Rendered component HTML */\n html: string\n /** JSON-serialized initial state */\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 * Default onRenderHtml hook for simple cases.\n * Uses a minimal HTML document template.\n */\nexport async function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult> {\n return renderPage(pageContext, DEFAULT_DOCUMENT)\n}\n\n/**\n * Factory to create a customized onRenderHtml hook.\n *\n * ```typescript\n * // pages/+onRenderHtml.ts\n * import { createOnRenderHtml } from '@llui/vike'\n *\n * export const onRenderHtml = createOnRenderHtml({\n * document: ({ html, state, head }) => `<!DOCTYPE html>\n * <html>\n * <head>${head}<link rel=\"stylesheet\" href=\"/styles.css\" /></head>\n * <body><div id=\"app\">${html}</div>\n * <script>window.__LLUI_STATE__ = ${state}</script></body>\n * </html>`,\n * })\n * ```\n */\nexport function createOnRenderHtml(options: {\n document: (ctx: DocumentContext) => string\n}): (pageContext: PageContext) => Promise<RenderHtmlResult> {\n return (pageContext) => renderPage(pageContext, options.document)\n}\n\nasync function renderPage(\n pageContext: PageContext,\n document: (ctx: DocumentContext) => string,\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 { Page, data } = pageContext\n const [initialState] = Page.init(data)\n const html = renderToString(Page, initialState)\n const state = JSON.stringify(initialState)\n const head = pageContext.head ?? ''\n\n const documentHtml = document({ html, state, head, pageContext })\n\n return {\n // Use Vike's dangerouslySkipEscape format — the document template\n // is trusted (authored by the developer, not user input)\n documentHtml: { _escaped: documentHtml },\n pageContext: { lluiState: initialState },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/vike",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -20,16 +20,9 @@
20
20
  "files": [
21
21
  "dist"
22
22
  ],
23
- "scripts": {
24
- "build": "tsc -p tsconfig.build.json",
25
- "check": "tsc --noEmit",
26
- "lint": "eslint src",
27
- "test": "vitest run",
28
- "test:coverage": "vitest run --coverage"
29
- },
30
23
  "dependencies": {
31
- "@llui/dom": "^0.0.13",
32
- "jsdom": "^26.1.0"
24
+ "jsdom": "^26.1.0",
25
+ "@llui/dom": "0.0.15"
33
26
  },
34
27
  "description": "LLui Vike SSR adapter — onRenderHtml, onRenderClient hooks",
35
28
  "keywords": [
@@ -48,5 +41,12 @@
48
41
  "bugs": {
49
42
  "url": "https://github.com/fponticelli/llui/issues"
50
43
  },
51
- "homepage": "https://github.com/fponticelli/llui/tree/main/packages/vike#readme"
52
- }
44
+ "homepage": "https://github.com/fponticelli/llui/tree/main/packages/vike#readme",
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.build.json",
47
+ "check": "tsc --noEmit",
48
+ "lint": "eslint src",
49
+ "test": "vitest run",
50
+ "test:coverage": "vitest run --coverage"
51
+ }
52
+ }