@voltro/web 0.8.0 → 0.10.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.
package/dist/ssr.d.ts CHANGED
@@ -22,6 +22,11 @@ import { ReactNode } from 'react';
22
22
  * and never hydrates the root, so nothing ever consumes the registry
23
23
  * promises. Islands take their props from their own serialised payload, not
24
24
  * from loader data, so there is no value in wiring them in.
25
+ *
26
+ * The one place `renderMode` is NOT the deciding input is the SSR layout shell
27
+ * (`layoutShell` above): a `renderMode:'spa'` page under an SSR layout chain is
28
+ * streamed per request by dev/serve — so a LAYOUT loader may defer there — and
29
+ * written to a file by the build, where it may not.
25
30
  */
26
31
  export declare const assertDeferralSupported: (target: DeferralTarget) => void;
27
32
 
@@ -31,6 +36,23 @@ export declare interface DeferralTarget {
31
36
  readonly page: string;
32
37
  readonly renderMode: string;
33
38
  readonly interactive: string;
39
+ /**
40
+ * Set ONLY on the SSR-layout-shell path — a `renderMode:'spa'` page under an
41
+ * SSR layout chain, where the server renders the LAYOUTS around an empty page
42
+ * slot and runs only the layout loaders. `renderMode` is `'spa'` there, so
43
+ * without this the shell would be rejected by the rule below; the thing that
44
+ * actually decides whether it can stream is HOW the shell is delivered:
45
+ *
46
+ * - `'stream'` — a per-request response (`voltro dev` / `voltro serve`). It
47
+ * goes out through `renderPageToStream`, so a LAYOUT loader may `defer()`:
48
+ * the chain + empty slot flush immediately and the deferred value streams
49
+ * in behind its `<Await>` boundary, exactly as on an `ssr` page.
50
+ * - `'artefact'` — the build-time prerender, which writes an HTML FILE. A
51
+ * file has no "after", so `defer()` is refused there by name.
52
+ *
53
+ * Absent for every full-page render, where `renderMode` alone decides.
54
+ */
55
+ readonly layoutShell?: 'stream' | 'artefact' | undefined;
34
56
  }
35
57
 
36
58
  /** Brand key on the object `defer()` returns. A single explicit container
@@ -64,6 +86,13 @@ declare interface ErrorBoundaryProps {
64
86
  * re-run it on hydration (the flash this whole payload exists to remove).
65
87
  */
66
88
  export declare interface InlinedRouterState {
89
+ /** The pathname the server rendered THIS document for.
90
+ *
91
+ * Present so the client can tell "the server rendered my route" from "a
92
+ * static host handed me some other route's document as its SPA fallback".
93
+ * Without it the two are indistinguishable — the payload parses either way,
94
+ * and `mount()` would adopt markup for a route it is not on. */
95
+ readonly path?: string;
67
96
  /** The page (leaf) loader's result — exactly the value the server render
68
97
  * provided to the page's `LoaderDataContext`. */
69
98
  readonly page?: unknown;
@@ -77,6 +106,13 @@ export declare interface InlinedRouterState {
77
106
  readonly page?: boolean;
78
107
  readonly segments?: ReadonlyArray<number>;
79
108
  };
109
+ /** True when the server rendered the layout chain but left the page leaf as
110
+ * an EMPTY slot — the `renderMode:'spa'`-under-an-SSR-layout case. The
111
+ * server ran only the layout loaders; the page is client-only. The client's
112
+ * FIRST render must reproduce the identical empty slot so the layout chain
113
+ * hydrates without a mismatch, then mount the real page after commit. Absent
114
+ * (falsy) for every full-page SSR/ISR/static document. */
115
+ readonly pageClientOnly?: boolean;
80
116
  /** Deferred (streamed) loader fields: field name -> client-registry id, for
81
117
  * the page loader and per chain index. The VALUES are not here — they
82
118
  * arrive later, published by the settle `<script>` each `<Await>` boundary
@@ -243,20 +279,6 @@ export declare interface PreparedLoaderData {
243
279
  readonly deferredIds: Readonly<Record<string, string>> | undefined;
244
280
  }
245
281
 
246
- /**
247
- * The `<script>` every streaming SSR emitter puts in `<head>`, before the
248
- * shell. Must precede any settle script — which is automatic, since settle
249
- * scripts ride in the body.
250
- */
251
- export declare const renderDeferredRegistryScript: () => string;
252
-
253
- /**
254
- * Helper for the build pipeline: stringify the resolved meta into a
255
- * `<head>`-ready HTML fragment. Returns an empty string when no meta
256
- * is set so the caller can blindly inject the result.
257
- */
258
- export declare const renderMetaToHtml: (meta: PageMeta | null) => string;
259
-
260
282
  /**
261
283
  * How a page's HTML is produced.
262
284
  *
@@ -279,8 +301,33 @@ export declare const renderMetaToHtml: (meta: PageMeta | null) => string;
279
301
  * tenant-scoped first paint.
280
302
  * - 'isr' — Like 'ssr' on the first request, then cached and
281
303
  * revalidated in the background on a TTL.
304
+ *
305
+ * The value set is single-sourced from {@link RENDER_MODES} so the type and
306
+ * every runtime check derive from ONE literal list. There are no aliases — an
307
+ * unrecognised value is rejected at codegen/discovery time by the CLI, not
308
+ * silently passed through.
309
+ */
310
+ declare const RENDER_MODES: readonly ["static", "spa", "ssr", "isr"];
311
+
312
+ /**
313
+ * The `<script>` every streaming SSR emitter puts in `<head>`, before the
314
+ * shell. Must precede any settle script — which is automatic, since settle
315
+ * scripts ride in the body.
316
+ */
317
+ export declare const renderDeferredRegistryScript: () => string;
318
+
319
+ /**
320
+ * Helper for the build pipeline: stringify the resolved meta into a
321
+ * `<head>`-ready HTML fragment. Returns an empty string when no meta
322
+ * is set so the caller can blindly inject the result.
323
+ */
324
+ export declare const renderMetaToHtml: (meta: PageMeta | null) => string;
325
+
326
+ /**
327
+ * A page's render mode — one of {@link RENDER_MODES}, from which this type is
328
+ * derived. Closed set: an unrecognised value is rejected at codegen time.
282
329
  */
283
- declare type RenderMode = 'static' | 'spa' | 'ssr' | 'isr';
330
+ declare type RenderMode = (typeof RENDER_MODES)[number];
284
331
 
285
332
  export declare interface RenderPageOptions {
286
333
  /** Descriptor produced by codegen (`pageRoute(...)`). */
@@ -319,6 +366,14 @@ export declare interface RenderPageOptions {
319
366
  * catalog at meta-resolve time. Optional: when absent, page
320
367
  * `meta(ctx)` callbacks see `ctx.locale = 'en'` as a safe default. */
321
368
  readonly locale?: string;
369
+ /** Render the LAYOUT CHAIN ONLY, with an empty page slot at the leaf
370
+ * ({@link PageSlot}) instead of the page Component — the
371
+ * `renderMode:'spa'`-under-an-SSR-layout case. The caller runs only the
372
+ * layout loaders (not the page loader) and pairs this with a state script
373
+ * carrying `pageClientOnly: true`, so the client reproduces the empty slot
374
+ * on its first render and mounts the page after hydration. When set, the
375
+ * descriptor need not carry a `Component`. */
376
+ readonly pageSlot?: boolean;
322
377
  }
323
378
 
324
379
  export declare interface RenderPageResult {
@@ -406,6 +461,12 @@ export declare const renderRouterStateScript: (input: RouterStateInput) => strin
406
461
  export declare const ROUTER_STATE_SCRIPT_ID = "__voltro_state__";
407
462
 
408
463
  export declare interface RouterStateInput {
464
+ /** The pathname this render is FOR. Every emitter has it — it is the same
465
+ * value handed to the renderer as `pathname` — and passing it is required
466
+ * rather than optional on purpose: a payload without it cannot be matched
467
+ * against the document it lands in, which is the whole point of carrying
468
+ * it (see {@link parseRouterState}). */
469
+ readonly path: string;
409
470
  /** Page loader result passed to `renderPageToHtml` / `renderPageToStream`. */
410
471
  readonly loaderData: unknown;
411
472
  /** Layout loader results, keyed by chain index — the same object passed to
@@ -415,6 +476,12 @@ export declare interface RouterStateInput {
415
476
  * `loaderData` (a loader may legitimately resolve to `undefined`/`null`,
416
477
  * and the SSR paths default the field to `null` when there is no loader). */
417
478
  readonly pageLoaderRan: boolean;
479
+ /** Set by the SSR-layout-shell path (a `renderMode:'spa'` page under an SSR
480
+ * layout chain): the server rendered the layouts + an empty page slot and
481
+ * ran only the layout loaders. Tells the client to reproduce the empty slot
482
+ * on its first render, then mount the page after hydration. Omit for every
483
+ * full-page render. */
484
+ readonly pageClientOnly?: boolean | undefined;
418
485
  /** Page-loader deferred fields: name -> registry id. Produced by
419
486
  * `prepareDeferredLoaderData` in the SSR pipeline; omit when nothing
420
487
  * deferred (which is every non-streaming emitter). */
package/dist/ssr.js CHANGED
@@ -1,21 +1,21 @@
1
- import { _ as e, a as t, g as n, m as r, n as i, p as a, s as o, t as s } from "./routerState-ga64vk2B.js";
2
- import { c, d as l, r as u, t as d, x as f } from "./serverContext-BW0GF8fv.js";
3
- import { createElement as p } from "react";
4
- import { renderToPipeableStream as m, renderToString as h } from "react-dom/server";
1
+ import { _ as e, a as t, g as n, m as r, n as i, p as a, s as o, t as s } from "./routerState-cjTiUIum.js";
2
+ import { c, m as l, r as u, t as d, u as f, w as p } from "./serverContext-BP0ywwLZ.js";
3
+ import { createElement as m } from "react";
4
+ import { renderToPipeableStream as h, renderToString as g } from "react-dom/server";
5
5
  //#region src/ssr.tsx
6
- var g = (e, t, n, r) => {
7
- let i = p(c.Provider, { value: n }, p(e));
6
+ var _ = (e, t, n, r) => {
7
+ let i = m(c.Provider, { value: n }, m(e));
8
8
  if (!t) return i;
9
9
  for (let e = t.length - 1; e >= 0; e--) {
10
10
  let n = t[e];
11
11
  if (n.Layout) {
12
12
  let t = n.Layout;
13
- i = p(c.Provider, { value: r(e) }, p(t, { children: i }));
13
+ i = m(c.Provider, { value: r(e) }, m(t, { children: i }));
14
14
  }
15
15
  }
16
16
  return i;
17
- }, _ = (e) => {
18
- let { descriptor: t, params: n, pathname: r, loaderData: i, segmentLoaderData: a, requestContext: o, outerWrap: s, locale: c } = e, u = f(t.meta, n, i, c), m = {
17
+ }, v = (e) => {
18
+ let { descriptor: t, params: n, pathname: r, loaderData: i, segmentLoaderData: a, requestContext: o, outerWrap: s, locale: c, pageSlot: u } = e, h = p(t.meta, n, i, c), g = {
19
19
  pathname: r,
20
20
  search: "",
21
21
  params: n,
@@ -26,23 +26,23 @@ var g = (e, t, n, r) => {
26
26
  prefetch: () => {},
27
27
  registerBlocker: () => () => {},
28
28
  blocked: null
29
- }, h = t.Component;
30
- if (!h) throw Error(`SSR received a descriptor with no Component for "${r}". Lazy page routes are a client-only optimization and must not be used on the server.`);
31
- let _ = p(l.Provider, { value: m }, g(h, t.chain, i, (e) => a?.[e]));
32
- return o && (_ = p(d.Provider, { value: o }, _)), s && (_ = s(_)), {
33
- tree: _,
34
- meta: u
29
+ }, v = u ? f : t.Component;
30
+ if (!v) throw Error(`SSR received a descriptor with no Component for "${r}". Lazy page routes are a client-only optimization and must not be used on the server.`);
31
+ let y = m(l.Provider, { value: g }, _(v, t.chain, u ? void 0 : i, (e) => a?.[e]));
32
+ return o && (y = m(d.Provider, { value: o }, y)), s && (y = s(y)), {
33
+ tree: y,
34
+ meta: h
35
35
  };
36
- }, v = (e) => {
37
- let { tree: t, meta: n } = _(e);
36
+ }, y = (e) => {
37
+ let { tree: t, meta: n } = v(e);
38
38
  return {
39
- html: h(t),
39
+ html: g(t),
40
40
  meta: n
41
41
  };
42
- }, y = (e) => {
43
- let { tree: t, meta: n } = _(e);
42
+ }, b = (e) => {
43
+ let { tree: t, meta: n } = v(e);
44
44
  return {
45
- stream: m(t, {
45
+ stream: h(t, {
46
46
  ...e.bootstrapModules ? { bootstrapModules: [...e.bootstrapModules] } : {},
47
47
  ...e.onShellReady ? { onShellReady: e.onShellReady } : {},
48
48
  ...e.onShellError ? { onShellError: e.onShellError } : {},
@@ -50,20 +50,20 @@ var g = (e, t, n, r) => {
50
50
  }),
51
51
  meta: n
52
52
  };
53
- }, b = (e) => {
53
+ }, x = (e) => {
54
54
  if (!e) return "";
55
55
  let t = [];
56
- if (typeof e.title == "string" && t.push(`<title>${x(e.title)}</title>`), e.description && t.push(`<meta name="description" content="${S(e.description)}" />`), e.canonical && t.push(`<link rel="canonical" href="${S(e.canonical)}" />`), e.tags) for (let n of e.tags) {
57
- let e = n.name ? `name="${S(n.name)}"` : n.property ? `property="${S(n.property)}"` : "";
58
- e && t.push(`<meta ${e} content="${S(n.content)}" />`);
56
+ if (typeof e.title == "string" && t.push(`<title>${S(e.title)}</title>`), e.description && t.push(`<meta name="description" content="${C(e.description)}" />`), e.canonical && t.push(`<link rel="canonical" href="${C(e.canonical)}" />`), e.tags) for (let n of e.tags) {
57
+ let e = n.name ? `name="${C(n.name)}"` : n.property ? `property="${C(n.property)}"` : "";
58
+ e && t.push(`<meta ${e} content="${C(n.content)}" />`);
59
59
  }
60
60
  if (e.links) for (let n of e.links) {
61
61
  let e = [
62
- `rel="${S(n.rel)}"`,
63
- `href="${S(n.href)}"`,
64
- n.hreflang ? `hreflang="${S(n.hreflang)}"` : "",
65
- n.type ? `type="${S(n.type)}"` : "",
66
- n.title ? `title="${S(n.title)}"` : ""
62
+ `rel="${C(n.rel)}"`,
63
+ `href="${C(n.href)}"`,
64
+ n.hreflang ? `hreflang="${C(n.hreflang)}"` : "",
65
+ n.type ? `type="${C(n.type)}"` : "",
66
+ n.title ? `title="${C(n.title)}"` : ""
67
67
  ].filter(Boolean).join(" ");
68
68
  t.push(`<link ${e} />`);
69
69
  }
@@ -72,6 +72,6 @@ var g = (e, t, n, r) => {
72
72
  t.push(`<script type="application/ld+json" data-voltro-page-jsonld>${e}<\/script>`);
73
73
  }
74
74
  return t.join("\n ");
75
- }, x = (e) => e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"), S = (e) => e.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
75
+ }, S = (e) => e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"), C = (e) => e.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
76
76
  //#endregion
77
- export { s as ROUTER_STATE_SCRIPT_ID, o as assertDeferralSupported, i as encodeRouterState, a as isDeferredLoaderResult, u as parseCookieHeader, r as prepareDeferredLoaderData, n as renderDeferredRegistryScript, b as renderMetaToHtml, v as renderPageToHtml, y as renderPageToStream, t as renderRouterStateScript, e as serialiseStateForInlining };
77
+ export { s as ROUTER_STATE_SCRIPT_ID, o as assertDeferralSupported, i as encodeRouterState, a as isDeferredLoaderResult, u as parseCookieHeader, r as prepareDeferredLoaderData, n as renderDeferredRegistryScript, x as renderMetaToHtml, y as renderPageToHtml, b as renderPageToStream, t as renderRouterStateScript, e as serialiseStateForInlining };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/web",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "The Voltro web framework — file-based routing, render modes (SSR / SSG / islands), the page-export contract, data hooks, and the browser mount.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -52,8 +52,8 @@
52
52
  "node": ">=24.0.0"
53
53
  },
54
54
  "dependencies": {
55
- "@voltro/client": "0.8.0",
56
- "@voltro/ui": "0.8.0"
55
+ "@voltro/client": "0.10.0",
56
+ "@voltro/ui": "0.10.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@effect/platform": "^0.96.2",