@llui/vike 0.0.23 → 0.0.25
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 +1 -1
- package/dist/on-render-html.d.ts +26 -3
- package/dist/on-render-html.d.ts.map +1 -1
- package/dist/on-render-html.js +14 -9
- package/dist/on-render-html.js.map +1 -1
- package/dist/page-slot.d.ts.map +1 -1
- package/dist/page-slot.js +1 -5
- package/dist/page-slot.js.map +1 -1
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ On the initial hydration render, `onLeave` and `onEnter` are both skipped — th
|
|
|
264
264
|
|
|
265
265
|
### Server (`onRenderHtml`)
|
|
266
266
|
|
|
267
|
-
Renders the component to HTML via `renderToString()`.
|
|
267
|
+
Renders the component to HTML via `renderToString()`. Each render gets a fresh `DomEnv` from the factory passed to `createOnRenderHtml({ domEnv })` — use `jsdomEnv` from `@llui/dom/ssr/jsdom` for Node targets, or `linkedomEnv` from `@llui/dom/ssr/linkedom` for Cloudflare Workers (jsdom's transitive deps don't resolve under workerd). The default `onRenderHtml` export wires up jsdom for zero-config Node setups; `createOnRenderHtml` requires an explicit `domEnv` factory so the bundler can tree-shake whichever DOM you don't use.
|
|
268
268
|
|
|
269
269
|
### Client (`onRenderClient`)
|
|
270
270
|
|
package/dist/on-render-html.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyComponentDef } from '@llui/dom';
|
|
1
|
+
import type { AnyComponentDef, DomEnv } from '@llui/dom';
|
|
2
2
|
import type { VikePageContextData } from './vike-namespace.js';
|
|
3
3
|
type LayoutChain = ReadonlyArray<AnyComponentDef>;
|
|
4
4
|
/**
|
|
@@ -58,9 +58,32 @@ export interface RenderHtmlOptions {
|
|
|
58
58
|
* layer-by-layer.
|
|
59
59
|
*/
|
|
60
60
|
Layout?: AnyComponentDef | LayoutChain | ((pageContext: PageContext) => LayoutChain);
|
|
61
|
+
/**
|
|
62
|
+
* Factory that returns the `DomEnv` backing SSR render. Call with
|
|
63
|
+
* either `jsdomEnv` (from `@llui/dom/ssr/jsdom`) or `linkedomEnv`
|
|
64
|
+
* (from `@llui/dom/ssr/linkedom`). The factory is invoked once per
|
|
65
|
+
* page render, so each request gets a fresh DOM — safe under
|
|
66
|
+
* concurrency, no `globalThis` mutation.
|
|
67
|
+
*
|
|
68
|
+
* On Cloudflare Workers use `linkedomEnv` — jsdom's transitive deps
|
|
69
|
+
* (whatwg-url, tr46, punycode) don't resolve under workerd.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { jsdomEnv } from '@llui/dom/ssr/jsdom'
|
|
74
|
+
* createOnRenderHtml({ Layout: MyLayout, domEnv: jsdomEnv })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
domEnv: () => DomEnv | Promise<DomEnv>;
|
|
61
78
|
}
|
|
62
79
|
/**
|
|
63
|
-
* Default onRenderHtml hook — no layout, minimal document template
|
|
80
|
+
* Default onRenderHtml hook — no layout, minimal document template,
|
|
81
|
+
* jsdom-backed DOM env. For Cloudflare Workers (no jsdom support) or
|
|
82
|
+
* a custom layout / document, use `createOnRenderHtml({ domEnv, … })`
|
|
83
|
+
* with `linkedomEnv` from `@llui/dom/ssr/linkedom`.
|
|
84
|
+
*
|
|
85
|
+
* The lazy import below keeps jsdom out of the client bundle —
|
|
86
|
+
* Rollup's graph walker only pulls it when this server hook executes.
|
|
64
87
|
*/
|
|
65
88
|
export declare function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult>;
|
|
66
89
|
/**
|
|
@@ -112,7 +135,7 @@ interface HydrationEnvelope {
|
|
|
112
135
|
*
|
|
113
136
|
* @internal — exported for unit testing only (`_renderChain`).
|
|
114
137
|
*/
|
|
115
|
-
export declare function _renderChain(chain: LayoutChain, chainData: readonly unknown[]): {
|
|
138
|
+
export declare function _renderChain(chain: LayoutChain, chainData: readonly unknown[], env: DomEnv): {
|
|
116
139
|
html: string;
|
|
117
140
|
envelope: HydrationEnvelope;
|
|
118
141
|
};
|
|
@@ -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,EAAqB,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"on-render-html.d.ts","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,EAAE,MAAM,WAAW,CAAA;AAE3E,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;IAEpF;;;;;;;;;;;;;;;OAeG;IACH,MAAM,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACvC;AAcD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAGtF;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;AA+BD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,SAAS,OAAO,EAAE,EAC7B,GAAG,EAAE,MAAM,GACV;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAiH/C"}
|
package/dist/on-render-html.js
CHANGED
|
@@ -22,10 +22,17 @@ function resolveLayoutChain(layoutOption, pageContext) {
|
|
|
22
22
|
return [layoutOption];
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Default onRenderHtml hook — no layout, minimal document template
|
|
25
|
+
* Default onRenderHtml hook — no layout, minimal document template,
|
|
26
|
+
* jsdom-backed DOM env. For Cloudflare Workers (no jsdom support) or
|
|
27
|
+
* a custom layout / document, use `createOnRenderHtml({ domEnv, … })`
|
|
28
|
+
* with `linkedomEnv` from `@llui/dom/ssr/linkedom`.
|
|
29
|
+
*
|
|
30
|
+
* The lazy import below keeps jsdom out of the client bundle —
|
|
31
|
+
* Rollup's graph walker only pulls it when this server hook executes.
|
|
26
32
|
*/
|
|
27
33
|
export async function onRenderHtml(pageContext) {
|
|
28
|
-
|
|
34
|
+
const { jsdomEnv } = await import('@llui/dom/ssr/jsdom');
|
|
35
|
+
return renderPage(pageContext, { domEnv: jsdomEnv });
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Factory to create a customized onRenderHtml hook.
|
|
@@ -54,16 +61,14 @@ export function createOnRenderHtml(options) {
|
|
|
54
61
|
return (pageContext) => renderPage(pageContext, options);
|
|
55
62
|
}
|
|
56
63
|
async function renderPage(pageContext, options) {
|
|
57
|
-
|
|
58
|
-
const { initSsrDom } = await import('@llui/dom/ssr');
|
|
59
|
-
await initSsrDom();
|
|
64
|
+
const env = await options.domEnv();
|
|
60
65
|
const layoutChain = resolveLayoutChain(options.Layout, pageContext);
|
|
61
66
|
const layoutData = pageContext.lluiLayoutData ?? [];
|
|
62
67
|
// Full chain: every layout, then the page. Always at least one entry
|
|
63
68
|
// (the page) since Vike's pageContext always has a Page.
|
|
64
69
|
const chain = [...layoutChain, pageContext.Page];
|
|
65
70
|
const chainData = [...layoutData, pageContext.data];
|
|
66
|
-
const { html, envelope } = _renderChain(chain, chainData);
|
|
71
|
+
const { html, envelope } = _renderChain(chain, chainData, env);
|
|
67
72
|
const document = options.document ?? DEFAULT_DOCUMENT;
|
|
68
73
|
const head = pageContext.head ?? '';
|
|
69
74
|
const state = JSON.stringify(envelope);
|
|
@@ -85,7 +90,7 @@ async function renderPage(pageContext, options) {
|
|
|
85
90
|
*
|
|
86
91
|
* @internal — exported for unit testing only (`_renderChain`).
|
|
87
92
|
*/
|
|
88
|
-
export function _renderChain(chain, chainData) {
|
|
93
|
+
export function _renderChain(chain, chainData, env) {
|
|
89
94
|
if (chain.length === 0) {
|
|
90
95
|
throw new Error('[llui/vike] renderChain called with empty chain');
|
|
91
96
|
}
|
|
@@ -111,7 +116,7 @@ export function _renderChain(chain, chainData) {
|
|
|
111
116
|
// Cross from type-erased AnyComponentDef into the concrete signature
|
|
112
117
|
// renderNodes expects. Same pattern as the client mount path —
|
|
113
118
|
// renderNodes is generic but the runtime doesn't use the type params.
|
|
114
|
-
const { nodes, inst } = renderNodes(def, initialState, currentSlotScope);
|
|
119
|
+
const { nodes, inst } = renderNodes(def, initialState, env, currentSlotScope);
|
|
115
120
|
allBindings.push(...inst.allBindings);
|
|
116
121
|
if (i === 0) {
|
|
117
122
|
outermostNodes = nodes;
|
|
@@ -138,7 +143,7 @@ export function _renderChain(chain, chainData) {
|
|
|
138
143
|
parentNode.insertBefore(node, insertPoint);
|
|
139
144
|
}
|
|
140
145
|
// Synthesize an end sentinel that brackets the owned region.
|
|
141
|
-
const endSentinel =
|
|
146
|
+
const endSentinel = env.createComment('llui-mount-end');
|
|
142
147
|
parentNode.insertBefore(endSentinel, insertPoint);
|
|
143
148
|
}
|
|
144
149
|
// Record this layer's state in the envelope. Page goes under
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-render-html.js","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEvD,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAuCvE,MAAM,gBAAgB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAmB,EAAU,EAAE,CAAC;;;;MAIvE,IAAI;;;oBAGU,IAAI;sCACc,KAAK;;QAEnC,CAAA;AA2BR,SAAS,kBAAkB,CACzB,YAAyC,EACzC,WAAwB;IAExB,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;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAwB;IACzD,OAAO,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AACpC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA0B;IAE1B,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;AAC1D,CAAC;AAYD,KAAK,UAAU,UAAU,CACvB,WAAwB,EACxB,OAA0B;IAE1B,wEAAwE;IACxE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;IACpD,MAAM,UAAU,EAAE,CAAA;IAElB,MAAM,WAAW,GAAG,kBAAkB,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACnE,MAAM,UAAU,GAAG,WAAW,CAAC,cAAc,IAAI,EAAE,CAAA;IAEnD,qEAAqE;IACrE,yDAAyD;IACzD,MAAM,KAAK,GAAgB,CAAC,GAAG,WAAW,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IAC7D,MAAM,SAAS,GAAuB,CAAC,GAAG,UAAU,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;IAEvE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAEzD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,gBAAgB,CAAA;IACrD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,IAAI,EAAE,CAAA;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IACtC,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAEjE,OAAO;QACL,iEAAiE;QACjE,sDAAsD;QACtD,YAAY,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;QACxC,WAAW,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE;KACrC,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAkB,EAClB,SAA6B;IAE7B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACpE,CAAC;IAED,uEAAuE;IACvE,iBAAiB,EAAE,CAAA;IAEnB,kEAAkE;IAClE,8DAA8D;IAC9D,iBAAiB;IACjB,MAAM,WAAW,GAAc,EAAE,CAAA;IACjC,MAAM,eAAe,GAAiC,EAAE,CAAA;IACxD,IAAI,YAAY,GAAqC,IAAI,CAAA;IAEzD,IAAI,cAAc,GAAW,EAAE,CAAA;IAC/B,IAAI,iBAAiB,GAAmB,IAAI,CAAA;IAC5C,IAAI,gBAAgB,GAAyB,SAAS,CAAA;IAEtD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,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,mEAAmE;QACnE,oEAAoE;QACpE,8DAA8D;QAC9D,MAAM,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAE1C,qEAAqE;QACrE,+DAA+D;QAC/D,sEAAsE;QACtE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,WAAW,CACjC,GAAmD,EACnD,YAAY,EACZ,gBAAgB,CACjB,CAAA;QACD,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAA;QAErC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACZ,cAAc,GAAG,KAAK,CAAA;QACxB,CAAC;aAAM,CAAC;YACN,8DAA8D;YAC9D,yDAAyD;YACzD,kEAAkE;YAClE,+DAA+D;YAC/D,iEAAiE;YACjE,2DAA2D;YAC3D,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACvB,2DAA2D;gBAC3D,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,MAAM,GAAG,CAAC,IAAI,uBAAuB,CAAC,CAAA;YAC9F,CAAC;YACD,MAAM,UAAU,GAAG,iBAAiB,CAAC,UAAU,CAAA;YAC/C,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CACb,+CAA+C,CAAC,MAAM,GAAG,CAAC,IAAI,gBAAgB,CAC/E,CAAA;YACH,CAAC;YACD,gEAAgE;YAChE,mEAAmE;YACnE,MAAM,WAAW,GAAG,iBAAiB,CAAC,WAAW,CAAA;YACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,UAAU,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;YAC5C,CAAC;YACD,6DAA6D;YAC7D,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAA;YAC5D,UAAU,CAAC,YAAY,CAAC,WAAW,EAAE,WAAW,CAAC,CAAA;QACnD,CAAC;QAED,6DAA6D;QAC7D,oEAAoE;QACpE,IAAI,WAAW,EAAE,CAAC;YAChB,YAAY,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,EAAE,CAAA;QACxD,CAAC;aAAM,CAAC;YACN,eAAe,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;QAC/D,CAAC;QAED,iEAAiE;QACjE,mEAAmE;QACnE,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAA;QAClC,IAAI,WAAW,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACb,gBAAgB,GAAG,CAAC,IAAI,4CAA4C;gBAClE,sEAAsE,CACzE,CAAA;QACH,CAAC;QACD,IAAI,CAAC,WAAW,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,gBAAgB,GAAG,CAAC,IAAI,gCAAgC,CAAC,eAAe;gBACtE,4CAA4C,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ;gBACxE,mDAAmD,CACtD,CAAA;QACH,CAAC;QAED,iBAAiB,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAA;QACxC,gBAAgB,GAAG,IAAI,EAAE,YAAY,CAAA;IACvC,CAAC;IAED,MAAM,IAAI,GAAG,cAAc,CAAC,cAAc,EAAE,WAAW,CAAC,CAAA;IAExD,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,oEAAoE;QACpE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;IAC7E,CAAC;IAED,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE;YACR,OAAO,EAAE,eAAe;YACxB,IAAI,EAAE,YAAY;SACnB;KACF,CAAA;AACH,CAAC","sourcesContent":["import { renderNodes, serializeNodes } from '@llui/dom'\nimport type { AnyComponentDef, Binding, Lifetime } 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: Lifetime | 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?.slotLifetime\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, Lifetime, DomEnv } 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 /**\n * Factory that returns the `DomEnv` backing SSR render. Call with\n * either `jsdomEnv` (from `@llui/dom/ssr/jsdom`) or `linkedomEnv`\n * (from `@llui/dom/ssr/linkedom`). The factory is invoked once per\n * page render, so each request gets a fresh DOM — safe under\n * concurrency, no `globalThis` mutation.\n *\n * On Cloudflare Workers use `linkedomEnv` — jsdom's transitive deps\n * (whatwg-url, tr46, punycode) don't resolve under workerd.\n *\n * @example\n * ```ts\n * import { jsdomEnv } from '@llui/dom/ssr/jsdom'\n * createOnRenderHtml({ Layout: MyLayout, domEnv: jsdomEnv })\n * ```\n */\n domEnv: () => DomEnv | Promise<DomEnv>\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 * jsdom-backed DOM env. For Cloudflare Workers (no jsdom support) or\n * a custom layout / document, use `createOnRenderHtml({ domEnv, … })`\n * with `linkedomEnv` from `@llui/dom/ssr/linkedom`.\n *\n * The lazy import below keeps jsdom out of the client bundle —\n * Rollup's graph walker only pulls it when this server hook executes.\n */\nexport async function onRenderHtml(pageContext: PageContext): Promise<RenderHtmlResult> {\n const { jsdomEnv } = await import('@llui/dom/ssr/jsdom')\n return renderPage(pageContext, { domEnv: jsdomEnv })\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 const env = await options.domEnv()\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, env)\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 env: DomEnv,\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: Lifetime | 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 env,\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 = env.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?.slotLifetime\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"]}
|
package/dist/page-slot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-slot.d.ts","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAQzC;;;;;;GAMG;AACH,UAAU,WAAW;IACnB,YAAY,EAAE,QAAQ,CAAA;IACtB,MAAM,EAAE,OAAO,CAAA;CAChB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,QAAQ,IAAI,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"page-slot.d.ts","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAQzC;;;;;;GAMG;AACH,UAAU,WAAW;IACnB,YAAY,EAAE,QAAQ,CAAA;IACtB,MAAM,EAAE,OAAO,CAAA;CAChB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,QAAQ,IAAI,IAAI,EAAE,CAcjC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,GAAG,IAAI,CAIxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
package/dist/page-slot.js
CHANGED
|
@@ -44,10 +44,6 @@ let pendingSlot = null;
|
|
|
44
44
|
* view throws.
|
|
45
45
|
*/
|
|
46
46
|
export function pageSlot() {
|
|
47
|
-
if (typeof document === 'undefined') {
|
|
48
|
-
throw new Error('[llui/vike] pageSlot() called without a DOM environment. ' +
|
|
49
|
-
'Call from inside a component view() that runs during mount, hydrate, or SSR.');
|
|
50
|
-
}
|
|
51
47
|
if (pendingSlot !== null) {
|
|
52
48
|
throw new Error('[llui/vike] pageSlot() was called more than once in the same layout. ' +
|
|
53
49
|
'A layout has exactly one nested-content slot — if you need two independent ' +
|
|
@@ -56,7 +52,7 @@ export function pageSlot() {
|
|
|
56
52
|
}
|
|
57
53
|
const ctx = getRenderContext('pageSlot');
|
|
58
54
|
const slotLifetime = createLifetime(ctx.rootLifetime);
|
|
59
|
-
const anchor =
|
|
55
|
+
const anchor = ctx.dom.createComment('llui-page-slot');
|
|
60
56
|
pendingSlot = { slotLifetime, anchor };
|
|
61
57
|
return [anchor];
|
|
62
58
|
}
|
package/dist/page-slot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-slot.js","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAmBrE,IAAI,WAAW,GAAuB,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,UAAU,QAAQ;IACtB,IAAI,
|
|
1
|
+
{"version":3,"file":"page-slot.js","sourceRoot":"","sources":["../src/page-slot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAmBrE,IAAI,WAAW,GAAuB,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,UAAU,QAAQ;IACtB,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,YAAY,GAAG,cAAc,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,gBAAgB,CAAY,CAAA;IACjE,WAAW,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtC,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 { Lifetime } from '@llui/dom'\nimport { getRenderContext, createLifetime } 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 slotLifetime: Lifetime\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 (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 slotLifetime = createLifetime(ctx.rootLifetime)\n const anchor = ctx.dom.createComment('llui-page-slot') as Comment\n pendingSlot = { slotLifetime, 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llui/vike",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -21,8 +21,15 @@
|
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
|
|
24
|
+
"@llui/dom": "0.0.25"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"jsdom": "*"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"jsdom": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
26
33
|
},
|
|
27
34
|
"description": "LLui Vike SSR adapter — onRenderHtml, onRenderClient hooks",
|
|
28
35
|
"keywords": [
|