@llui/vike 0.9.0 → 0.11.0

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,4 @@
1
- import type { Renderable } from '@llui/dom';
1
+ import type { Renderable, CollectedHead } from '@llui/dom';
2
2
  import type { DomEnv } from '@llui/dom/ssr';
3
3
  import type { VikePageContextData } from './vike-namespace.js';
4
4
  /**
@@ -52,8 +52,16 @@ export interface DocumentContext {
52
52
  html: string;
53
53
  /** JSON-serialized hydration envelope (chain-aware when Layout is configured) */
54
54
  state: string;
55
- /** Head content from pageContext.head (e.g. from +Head.ts) */
55
+ /** Head content: static `pageContext.head` (e.g. from +Head.ts) merged with the
56
+ * head collected from `title`/`meta`/`link` primitives in the render tree
57
+ * (component entries override colliding static tags). */
56
58
  head: string;
59
+ /** Attribute string for the `<html>` tag (leading space included), from
60
+ * `htmlAttr(...)` primitives. Interpolate as `<html${htmlAttrs}>`. */
61
+ htmlAttrs: string;
62
+ /** Attribute string for the `<body>` tag (leading space included), from
63
+ * `bodyAttr(...)` primitives. Interpolate as `<body${bodyAttrs}>`. */
64
+ bodyAttrs: string;
57
65
  /** Full page context for custom logic */
58
66
  pageContext: PageContext;
59
67
  }
@@ -167,6 +175,7 @@ interface HydrationEnvelope {
167
175
  export declare function _renderChain(chain: LayoutChain, chainData: readonly unknown[], env: DomEnv): {
168
176
  html: string;
169
177
  envelope: HydrationEnvelope;
178
+ collectedHead: CollectedHead;
170
179
  };
171
180
  export {};
172
181
  //# 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,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE9D;;;;;;GAMG;AACH;;;;;;;;;GASG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,IAAI,IAAI,OAAO,CAAA;IACf,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAA;IAC7C,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAAA;IAC9B,QAAQ,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;CAC9D;AAED,KAAK,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;AAE1C;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAA;IACd,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,QAAQ,GAAG,WAAW,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,KAAK,WAAW,CAAC,CAAA;IAE7E;;;;;;;;;;;;;;;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;AAwDD;;;;;;;;;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,CAiG/C"}
1
+ {"version":3,"file":"on-render-html.d.ts","sourceRoot":"","sources":["../src/on-render-html.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,12 @@
1
- import { renderNodes, serializeNodes } from '@llui/dom';
1
+ import { renderNodes, serializeNodes, collectHeadSink, mergeStaticHead, HEAD_SINK } from '@llui/dom';
2
2
  import { _consumePendingSlot, _resetPendingSlot } from './page-slot.js';
3
- const DEFAULT_DOCUMENT = ({ html, state, head }) => `<!DOCTYPE html>
4
- <html>
3
+ const DEFAULT_DOCUMENT = ({ html, state, head, htmlAttrs, bodyAttrs, }) => `<!DOCTYPE html>
4
+ <html${htmlAttrs}>
5
5
  <head>
6
6
  <meta charset="utf-8" />
7
7
  ${head}
8
8
  </head>
9
- <body>
9
+ <body${bodyAttrs}>
10
10
  <div id="app">${html}</div>
11
11
  <script>window.__LLUI_STATE__ = ${state}</script>
12
12
  </body>
@@ -85,11 +85,20 @@ async function renderPage(pageContext, options) {
85
85
  // (the page) since Vike's pageContext always has a Page.
86
86
  const chain = [...layoutChain, pageContext.Page];
87
87
  const chainData = [...layoutData, pageContext.data];
88
- const { html, envelope } = _renderChain(chain, chainData, env);
88
+ const { html, envelope, collectedHead } = _renderChain(chain, chainData, env);
89
89
  const document = options.document ?? DEFAULT_DOCUMENT;
90
- const head = pageContext.head ?? '';
90
+ // Static +Head.ts head, with component head merged in (components override
91
+ // colliding title/meta so the document never carries two <title>s).
92
+ const head = mergeStaticHead(pageContext.head ?? '', collectedHead);
91
93
  const state = serializeStateForScript(envelope);
92
- const documentHtml = document({ html, state, head, pageContext });
94
+ const documentHtml = document({
95
+ html,
96
+ state,
97
+ head,
98
+ htmlAttrs: collectedHead.htmlAttrs,
99
+ bodyAttrs: collectedHead.bodyAttrs,
100
+ pageContext,
101
+ });
93
102
  return {
94
103
  // Vike's dangerouslySkipEscape format — the document template is
95
104
  // trusted (authored by the developer, not user input)
@@ -119,12 +128,20 @@ export function _renderChain(chain, chainData, env) {
119
128
  }
120
129
  // Defensive: ensure no stale slot leaks in from a prior failed render.
121
130
  _resetPendingSlot();
131
+ // One head collector for the whole chain. Seeded into the outermost layer's
132
+ // root contexts; pageSlot() captures it (it's in-scope), so it threads inward
133
+ // to every nested layer's separate build/mount pass. Each request gets a fresh
134
+ // collector — no cross-request shared state.
135
+ const headSink = collectHeadSink();
136
+ const rootContexts = new Map([[HEAD_SINK.id, headSink]]);
122
137
  const envelopeLayouts = [];
123
138
  let envelopePage = null;
124
139
  let outermostNodes = [];
125
140
  const disposers = [];
126
141
  let currentSlotAnchor = null;
127
- let currentSlotContexts = undefined;
142
+ // Seed the outermost layer with the head collector; subsequent layers inherit
143
+ // it via their parent's captured pageSlot() contexts.
144
+ let currentSlotContexts = rootContexts;
128
145
  for (let i = 0; i < chain.length; i++) {
129
146
  const def = chain[i];
130
147
  const layerData = chainData[i];
@@ -180,6 +197,8 @@ export function _renderChain(chain, chainData, env) {
180
197
  currentSlotContexts = slot?.contexts;
181
198
  }
182
199
  const html = serializeNodes(outermostNodes);
200
+ // Serialize collected head BEFORE disposing (dispose releases the writers).
201
+ const collectedHead = headSink.serialize(env);
183
202
  // Dispose every layer's build now that the composed tree is serialized.
184
203
  for (const d of disposers)
185
204
  d();
@@ -193,6 +212,7 @@ export function _renderChain(chain, chainData, env) {
193
212
  layouts: envelopeLayouts,
194
213
  page: envelopePage,
195
214
  },
215
+ collectedHead,
196
216
  };
197
217
  }
198
218
  /** The seed state a layer's `init()` produces (used for the envelope when no
@@ -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 { Renderable } from '@llui/dom'\nimport type { DomEnv } from '@llui/dom/ssr'\nimport { _consumePendingSlot, _resetPendingSlot } from './page-slot.js'\nimport type { VikePageContextData } from './vike-namespace.js'\n\n/**\n * A type-erased signal component as the adapter sees it. Layouts and pages are\n * `SignalComponentDef<S, M, E>` for concrete S/M/E; the adapter handles them\n * uniformly with the type params erased — the runtime doesn't use them. Unlike\n * the legacy `ComponentDef`, the signal `init()` takes NO data argument, so\n * per-layer data flows in as a seed-STATE override (see `renderPage`).\n */\n/**\n * Type-erased layer def at the adapter boundary. Declared with METHOD syntax and\n * a single `unknown` view-bag param so a concrete `SignalComponentDef<S,M,E>`\n * assigns in for ANY S/M/E — `SignalComponentDef<unknown,unknown,unknown>` can't\n * be that erasure, because `view(bag: ComponentBag<S,M>)` couples covariant\n * `state` with contravariant `send` and neither variance direction admits a\n * heterogeneous chain. This interface is itself assignable to\n * `SignalComponentDef<unknown,unknown,unknown>`, so `renderNodes(layer)` type-\n * checks. Mirrors the legacy `AnyComponentDef`.\n */\nexport interface AnyLayer {\n readonly name?: string\n init(): unknown\n update(state: unknown, msg: unknown): unknown\n view(bag: unknown): Renderable\n onEffect?(effect: unknown, api: unknown): void | (() => void)\n}\n\ntype LayoutChain = ReadonlyArray<AnyLayer>\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 *\n * In the signal runtime a component's `init()` takes no data argument, so\n * each layer's `data` slice is used directly as that layer's seed STATE\n * when present; when absent, the layer's own `init()` provides the seed.\n */\nexport interface PageContext {\n Page: AnyLayer\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 `SignalComponentDef` — becomes a one-layout chain.\n * - An array of `SignalComponentDef`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?: AnyLayer | 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 AnyLayer]\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.js' // ← 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\n/**\n * Serialize the hydration envelope for safe embedding inside an inline\n * `<script>` tag. `JSON.stringify` alone is NOT script-safe: a state string\n * containing `</script>` (or `<!--`, `<script`) breaks out of the script\n * element, and the JSON-legal raw line separators U+2028 / U+2029 are invalid\n * inside a JS string literal. Escaping `<` to its `<` form neutralizes\n * every HTML-sensitive sequence (`</script>`, `<!--`, `<script`) while\n * remaining valid JSON, since `<` never appears in JSON syntax outside string\n * contents. The document template is trusted; the serialized STATE is data and\n * must be treated as untrusted.\n */\nfunction serializeStateForScript(envelope: HydrationEnvelope): string {\n return JSON.stringify(envelope)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\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 = serializeStateForScript(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/** Resolve a layer's seed state. In the signal runtime `init()` takes no data,\n * so a present data slice IS the seed state; an absent slice falls back to the\n * layer's own `init()` (renderNodes does this when given `undefined`). */\nfunction seedFor(data: unknown): unknown | undefined {\n return data === undefined ? undefined : data\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 * Contexts provided above a slot are replayed into the nested layer's\n * build so they reach the nested page.\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 const envelopeLayouts: HydrationEnvelope['layouts'] = []\n let envelopePage: HydrationEnvelope['page'] | null = null\n\n let outermostNodes: readonly Node[] = []\n const disposers: Array<() => void> = []\n let currentSlotAnchor: Comment | null = null\n let currentSlotContexts: ReadonlyMap<symbol, unknown> | 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 // Build this layer's tree against the server DomEnv. Per-layer data is the\n // seed state (signal init() takes no data); contexts captured at the parent\n // layer's pageSlot() are replayed so providers above the slot reach here.\n const { nodes, dispose } = renderNodes(def, seedFor(layerData), env, currentSlotContexts)\n disposers.push(dispose)\n\n if (i === 0) {\n outermostNodes = nodes\n } else {\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 // Insert this layer's nodes immediately after the anchor, then an end\n // sentinel — preserving any trailing siblings of the anchor.\n const insertPoint = currentSlotAnchor.nextSibling\n for (const node of nodes) {\n parentNode.insertBefore(node, insertPoint)\n }\n const endSentinel = env.createComment('llui-mount-end')\n parentNode.insertBefore(endSentinel, insertPoint)\n }\n\n // Record this layer's seed state in the envelope. Page goes under `page`,\n // everything else under `layouts[]` ordered outer-to-inner.\n const layerState = seedFor(layerData) ?? normalizeInitState(def)\n if (isInnermost) {\n envelopePage = { name: def.name ?? 'Page', state: layerState }\n } else {\n envelopeLayouts.push({ name: def.name ?? 'Layout', state: layerState })\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 currentSlotContexts = slot?.contexts\n }\n\n const html = serializeNodes(outermostNodes)\n\n // Dispose every layer's build now that the composed tree is serialized.\n for (const d of disposers) d()\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\n/** The seed state a layer's `init()` produces (used for the envelope when no\n * data slice overrides it). `init()` may return `S` or `[S, E[]]`. */\nfunction normalizeInitState(def: AnyLayer): unknown {\n const r = def.init()\n if (Array.isArray(r) && r.length === 2 && Array.isArray((r as [unknown, unknown[]])[1])) {\n return (r as [unknown, unknown[]])[0]\n }\n return r\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, collectHeadSink, mergeStaticHead, HEAD_SINK } from '@llui/dom'\nimport type { Renderable, CollectedHead } from '@llui/dom'\nimport type { DomEnv } from '@llui/dom/ssr'\nimport { _consumePendingSlot, _resetPendingSlot } from './page-slot.js'\nimport type { VikePageContextData } from './vike-namespace.js'\n\n/**\n * A type-erased signal component as the adapter sees it. Layouts and pages are\n * `SignalComponentDef<S, M, E>` for concrete S/M/E; the adapter handles them\n * uniformly with the type params erased — the runtime doesn't use them. Unlike\n * the legacy `ComponentDef`, the signal `init()` takes NO data argument, so\n * per-layer data flows in as a seed-STATE override (see `renderPage`).\n */\n/**\n * Type-erased layer def at the adapter boundary. Declared with METHOD syntax and\n * a single `unknown` view-bag param so a concrete `SignalComponentDef<S,M,E>`\n * assigns in for ANY S/M/E — `SignalComponentDef<unknown,unknown,unknown>` can't\n * be that erasure, because `view(bag: ComponentBag<S,M>)` couples covariant\n * `state` with contravariant `send` and neither variance direction admits a\n * heterogeneous chain. This interface is itself assignable to\n * `SignalComponentDef<unknown,unknown,unknown>`, so `renderNodes(layer)` type-\n * checks. Mirrors the legacy `AnyComponentDef`.\n */\nexport interface AnyLayer {\n readonly name?: string\n init(): unknown\n update(state: unknown, msg: unknown): unknown\n view(bag: unknown): Renderable\n onEffect?(effect: unknown, api: unknown): void | (() => void)\n}\n\ntype LayoutChain = ReadonlyArray<AnyLayer>\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 *\n * In the signal runtime a component's `init()` takes no data argument, so\n * each layer's `data` slice is used directly as that layer's seed STATE\n * when present; when absent, the layer's own `init()` provides the seed.\n */\nexport interface PageContext {\n Page: AnyLayer\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: static `pageContext.head` (e.g. from +Head.ts) merged with the\n * head collected from `title`/`meta`/`link` primitives in the render tree\n * (component entries override colliding static tags). */\n head: string\n /** Attribute string for the `<html>` tag (leading space included), from\n * `htmlAttr(...)` primitives. Interpolate as `<html${htmlAttrs}>`. */\n htmlAttrs: string\n /** Attribute string for the `<body>` tag (leading space included), from\n * `bodyAttr(...)` primitives. Interpolate as `<body${bodyAttrs}>`. */\n bodyAttrs: 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 = ({\n html,\n state,\n head,\n htmlAttrs,\n bodyAttrs,\n}: DocumentContext): string => `<!DOCTYPE html>\n<html${htmlAttrs}>\n <head>\n <meta charset=\"utf-8\" />\n ${head}\n </head>\n <body${bodyAttrs}>\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 `SignalComponentDef` — becomes a one-layout chain.\n * - An array of `SignalComponentDef`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?: AnyLayer | 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 AnyLayer]\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.js' // ← 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\n/**\n * Serialize the hydration envelope for safe embedding inside an inline\n * `<script>` tag. `JSON.stringify` alone is NOT script-safe: a state string\n * containing `</script>` (or `<!--`, `<script`) breaks out of the script\n * element, and the JSON-legal raw line separators U+2028 / U+2029 are invalid\n * inside a JS string literal. Escaping `<` to its `<` form neutralizes\n * every HTML-sensitive sequence (`</script>`, `<!--`, `<script`) while\n * remaining valid JSON, since `<` never appears in JSON syntax outside string\n * contents. The document template is trusted; the serialized STATE is data and\n * must be treated as untrusted.\n */\nfunction serializeStateForScript(envelope: HydrationEnvelope): string {\n return JSON.stringify(envelope)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\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, collectedHead } = _renderChain(chain, chainData, env)\n\n const document = options.document ?? DEFAULT_DOCUMENT\n // Static +Head.ts head, with component head merged in (components override\n // colliding title/meta so the document never carries two <title>s).\n const head = mergeStaticHead(pageContext.head ?? '', collectedHead)\n const state = serializeStateForScript(envelope)\n const documentHtml = document({\n html,\n state,\n head,\n htmlAttrs: collectedHead.htmlAttrs,\n bodyAttrs: collectedHead.bodyAttrs,\n pageContext,\n })\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/** Resolve a layer's seed state. In the signal runtime `init()` takes no data,\n * so a present data slice IS the seed state; an absent slice falls back to the\n * layer's own `init()` (renderNodes does this when given `undefined`). */\nfunction seedFor(data: unknown): unknown | undefined {\n return data === undefined ? undefined : data\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 * Contexts provided above a slot are replayed into the nested layer's\n * build so they reach the nested page.\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; collectedHead: CollectedHead } {\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 // One head collector for the whole chain. Seeded into the outermost layer's\n // root contexts; pageSlot() captures it (it's in-scope), so it threads inward\n // to every nested layer's separate build/mount pass. Each request gets a fresh\n // collector — no cross-request shared state.\n const headSink = collectHeadSink()\n const rootContexts: ReadonlyMap<symbol, unknown> = new Map([[HEAD_SINK.id, headSink]])\n\n const envelopeLayouts: HydrationEnvelope['layouts'] = []\n let envelopePage: HydrationEnvelope['page'] | null = null\n\n let outermostNodes: readonly Node[] = []\n const disposers: Array<() => void> = []\n let currentSlotAnchor: Comment | null = null\n // Seed the outermost layer with the head collector; subsequent layers inherit\n // it via their parent's captured pageSlot() contexts.\n let currentSlotContexts: ReadonlyMap<symbol, unknown> | undefined = rootContexts\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 // Build this layer's tree against the server DomEnv. Per-layer data is the\n // seed state (signal init() takes no data); contexts captured at the parent\n // layer's pageSlot() are replayed so providers above the slot reach here.\n const { nodes, dispose } = renderNodes(def, seedFor(layerData), env, currentSlotContexts)\n disposers.push(dispose)\n\n if (i === 0) {\n outermostNodes = nodes\n } else {\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 // Insert this layer's nodes immediately after the anchor, then an end\n // sentinel — preserving any trailing siblings of the anchor.\n const insertPoint = currentSlotAnchor.nextSibling\n for (const node of nodes) {\n parentNode.insertBefore(node, insertPoint)\n }\n const endSentinel = env.createComment('llui-mount-end')\n parentNode.insertBefore(endSentinel, insertPoint)\n }\n\n // Record this layer's seed state in the envelope. Page goes under `page`,\n // everything else under `layouts[]` ordered outer-to-inner.\n const layerState = seedFor(layerData) ?? normalizeInitState(def)\n if (isInnermost) {\n envelopePage = { name: def.name ?? 'Page', state: layerState }\n } else {\n envelopeLayouts.push({ name: def.name ?? 'Layout', state: layerState })\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 currentSlotContexts = slot?.contexts\n }\n\n const html = serializeNodes(outermostNodes)\n // Serialize collected head BEFORE disposing (dispose releases the writers).\n const collectedHead = headSink.serialize(env)\n\n // Dispose every layer's build now that the composed tree is serialized.\n for (const d of disposers) d()\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 collectedHead,\n }\n}\n\n/** The seed state a layer's `init()` produces (used for the envelope when no\n * data slice overrides it). `init()` may return `S` or `[S, E[]]`. */\nfunction normalizeInitState(def: AnyLayer): unknown {\n const r = def.init()\n if (Array.isArray(r) && r.length === 2 && Array.isArray((r as [unknown, unknown[]])[1])) {\n return (r as [unknown, unknown[]])[0]\n }\n return r\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/vike",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -21,7 +21,7 @@
21
21
  "dist"
22
22
  ],
23
23
  "peerDependencies": {
24
- "@llui/dom": "^0.9.0",
24
+ "@llui/dom": "^0.11.0",
25
25
  "jsdom": "*"
26
26
  },
27
27
  "peerDependenciesMeta": {
@@ -30,7 +30,7 @@
30
30
  }
31
31
  },
32
32
  "devDependencies": {
33
- "@llui/dom": "0.9.0"
33
+ "@llui/dom": "0.11.0"
34
34
  },
35
35
  "description": "LLui Vike SSR adapter — onRenderHtml, onRenderClient hooks",
36
36
  "keywords": [