@webjsdev/core 0.7.16 → 0.7.18
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/webjs-core-browser.js +3 -3
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +19 -19
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +5 -5
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +4 -0
- package/index.d.ts +1 -0
- package/index.js +4 -0
- package/package.json +1 -1
- package/src/action-seed-client.d.ts +17 -0
- package/src/action-seed-client.js +107 -0
- package/src/render-server.js +43 -16
- package/src/router-client.js +10 -0
- package/src/webjs-config.d.ts +9 -0
package/index-browser.js
CHANGED
|
@@ -52,6 +52,10 @@ export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.
|
|
|
52
52
|
// Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
|
|
53
53
|
export { optimistic } from './src/optimistic.js';
|
|
54
54
|
|
|
55
|
+
// SSR action-seed consumer (#472): the generated RPC stub reads a seed on its
|
|
56
|
+
// first call so async-render hydration does not re-fetch the SSR'd data.
|
|
57
|
+
export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
|
|
58
|
+
|
|
55
59
|
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
56
60
|
// lit-html-parity set is re-exported here so the dist browser bundle (which
|
|
57
61
|
// the `@webjsdev/core/directives` subpath collapses onto in dist mode) carries
|
package/index.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export { Task, TaskStatus } from './src/task.js';
|
|
|
91
91
|
// src/*.d.ts (#388 fixed the index.d.ts drift from index.js).
|
|
92
92
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
93
93
|
export { cspNonce, setCspNonceProvider } from './src/csp-nonce.js';
|
|
94
|
+
export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
|
|
94
95
|
export { stringify, parse, serialize, deserialize } from './src/serialize.js';
|
|
95
96
|
export { WebjsFrame } from './src/webjs-frame.js';
|
|
96
97
|
export { WebjsStream, renderStream } from './src/webjs-stream.js';
|
package/index.js
CHANGED
|
@@ -34,6 +34,10 @@ export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.
|
|
|
34
34
|
// Optimistic-mutation helper (thin signal wrapper, rolls back on failure)
|
|
35
35
|
export { optimistic } from './src/optimistic.js';
|
|
36
36
|
|
|
37
|
+
// SSR action-seed consumer (#472): the generated RPC stub reads a seed on its
|
|
38
|
+
// first call so async-render hydration does not re-fetch. Inert server-side.
|
|
39
|
+
export { takeSeed, scanSeeds, SEED_MISS } from './src/action-seed-client.js';
|
|
40
|
+
|
|
37
41
|
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
38
42
|
// lit-html-parity set is re-exported so the bare specifier exposes the same
|
|
39
43
|
// directive surface in Node as the browser bundle does.
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Client-side SSR action-seed consumer (#472). Inert server-side. */
|
|
2
|
+
|
|
3
|
+
/** Returned by `takeSeed` when no seed matches; distinct from any real value. */
|
|
4
|
+
export const SEED_MISS: unique symbol;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merge any seeds found under `root` (or the whole document) into the global
|
|
8
|
+
* consume-once store, removing the carriers. Reads the page-level
|
|
9
|
+
* `#__webjs-seeds` JSON block and per-element `[data-webjs-seed]` carriers.
|
|
10
|
+
*/
|
|
11
|
+
export function scanSeeds(root?: ParentNode): void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Look up and CONSUME the seed for an action call, or return `SEED_MISS`.
|
|
15
|
+
* Keyed `hash/fn/argsKey`; the first call lazily scans the initial document.
|
|
16
|
+
*/
|
|
17
|
+
export function takeSeed(hash: string, fnName: string, argsKey: string): unknown;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side SSR action-seed consumer (#472).
|
|
3
|
+
*
|
|
4
|
+
* The server (`@webjsdev/server`'s `action-seed.js`) serializes each action
|
|
5
|
+
* result invoked during SSR into a `<script type="application/json"
|
|
6
|
+
* id="__webjs-seeds">` block (and, for streamed regions, per-element
|
|
7
|
+
* `data-webjs-seed` attributes). On the client, the generated RPC stub
|
|
8
|
+
* (`actions.js`) calls `takeSeed(hash, fn, argsKey)` on its FIRST invocation: a
|
|
9
|
+
* hit resolves the action synchronously with the SSR value (no RPC, no flicker
|
|
10
|
+
* on hydration); a miss falls through to the normal RPC.
|
|
11
|
+
*
|
|
12
|
+
* Consume-once: a hit removes the seed, so a later refetch / arg-change always
|
|
13
|
+
* goes to the network (the seed is a first-paint optimization, not a cache).
|
|
14
|
+
* Keyed `hash/fn/stringify(args)`, where `hash` and `stringify` match exactly
|
|
15
|
+
* what the server emitted, so distinct components and distinct args each map to
|
|
16
|
+
* their own seed. A miss is always safe (it just re-fetches).
|
|
17
|
+
*
|
|
18
|
+
* This module is imported by the generated stub via the bare `@webjsdev/core`
|
|
19
|
+
* specifier; on the server it is inert (its only DOM access is inside
|
|
20
|
+
* `scanSeeds`, never called server-side).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { parse } from './serialize.js';
|
|
24
|
+
|
|
25
|
+
/** Global consume-once seed store: `key -> value`. */
|
|
26
|
+
const seeds = new Map();
|
|
27
|
+
|
|
28
|
+
/** Returned by `takeSeed` when no seed matches; distinct from any real value. */
|
|
29
|
+
export const SEED_MISS = Symbol('webjs.seed.miss');
|
|
30
|
+
|
|
31
|
+
/** One-time eager scan guard (the initial-load document is scanned lazily). */
|
|
32
|
+
let scannedInitial = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge any seeds found under `root` into the global store, then remove the
|
|
36
|
+
* carriers so a re-scan (a streamed boundary, a soft navigation) never
|
|
37
|
+
* re-ingests stale data. Reads both the page-level `#__webjs-seeds` JSON block
|
|
38
|
+
* and per-element `[data-webjs-seed]` carriers. Idempotent and fail-open: a
|
|
39
|
+
* malformed payload is skipped, never thrown.
|
|
40
|
+
* @param {ParentNode & { querySelectorAll?: Function }} [root]
|
|
41
|
+
*/
|
|
42
|
+
export function scanSeeds(root) {
|
|
43
|
+
const scope = root || (typeof document !== 'undefined' ? document : null);
|
|
44
|
+
if (!scope || typeof scope.querySelectorAll !== 'function') return;
|
|
45
|
+
// Page-level JSON block(s).
|
|
46
|
+
for (const el of scope.querySelectorAll('script[type="application/json"]#__webjs-seeds, script[type="application/json"][data-webjs-seeds]')) {
|
|
47
|
+
ingest(el.textContent, el);
|
|
48
|
+
}
|
|
49
|
+
// Per-element carriers (streamed boundaries / future per-component seeding).
|
|
50
|
+
for (const el of scope.querySelectorAll('[data-webjs-seed]')) {
|
|
51
|
+
ingest(el.getAttribute('data-webjs-seed'), el, () => el.removeAttribute('data-webjs-seed'));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse one serialized seed payload and merge it (first write wins, so an
|
|
57
|
+
* already-consumed or earlier seed is never clobbered by a later duplicate),
|
|
58
|
+
* then run `cleanup` (or remove the element).
|
|
59
|
+
* @param {string | null} raw
|
|
60
|
+
* @param {Element} el
|
|
61
|
+
* @param {() => void} [cleanup]
|
|
62
|
+
*/
|
|
63
|
+
function ingest(raw, el, cleanup) {
|
|
64
|
+
if (raw) {
|
|
65
|
+
try {
|
|
66
|
+
const obj = parse(raw);
|
|
67
|
+
if (obj && typeof obj === 'object') {
|
|
68
|
+
for (const k in obj) if (!seeds.has(k)) seeds.set(k, obj[k]);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Malformed payload: ignore, the stub re-fetches.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (cleanup) cleanup();
|
|
75
|
+
else el.remove?.();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Look up and CONSUME the seed for an action call. Returns the seeded value
|
|
80
|
+
* (removing it) on a hit, or `SEED_MISS` when there is none. The first call
|
|
81
|
+
* lazily scans the initial document, so the boot path needs no wiring; the
|
|
82
|
+
* router calls `scanSeeds(subtree)` for content that arrives later.
|
|
83
|
+
* @param {string} hash the action file's hash (the RPC endpoint hash)
|
|
84
|
+
* @param {string} fnName the exported action name
|
|
85
|
+
* @param {string} argsKey `stringify(args)`, computed by the stub with the same
|
|
86
|
+
* serializer the server used, so identical args produce an identical key
|
|
87
|
+
* @returns {unknown | typeof SEED_MISS}
|
|
88
|
+
*/
|
|
89
|
+
export function takeSeed(hash, fnName, argsKey) {
|
|
90
|
+
if (!scannedInitial) {
|
|
91
|
+
scannedInitial = true;
|
|
92
|
+
scanSeeds();
|
|
93
|
+
}
|
|
94
|
+
const key = `${hash}/${fnName}/${argsKey}`;
|
|
95
|
+
if (seeds.has(key)) {
|
|
96
|
+
const v = seeds.get(key);
|
|
97
|
+
seeds.delete(key);
|
|
98
|
+
return v;
|
|
99
|
+
}
|
|
100
|
+
return SEED_MISS;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Test seam: drop all seeds and reset the lazy-scan guard. */
|
|
104
|
+
export function __resetSeeds() {
|
|
105
|
+
seeds.clear();
|
|
106
|
+
scannedInitial = false;
|
|
107
|
+
}
|
package/src/render-server.js
CHANGED
|
@@ -30,8 +30,14 @@ import { cspNonce } from './csp-nonce.js';
|
|
|
30
30
|
*/
|
|
31
31
|
export async function renderToString(value, opts = { ssr: true }) {
|
|
32
32
|
const ctx = opts && opts.suspenseCtx;
|
|
33
|
+
// The server `dev` flag drives prod-silence of SSR error states (#483).
|
|
34
|
+
// `opts.dev` wins; else inherit a `dev` already stamped on the ctx (so a
|
|
35
|
+
// streamed sub-context inherits it); back-fill the ctx so downstream renders
|
|
36
|
+
// sharing it see the same flag. Undefined stays undefined (NODE_ENV fallback).
|
|
37
|
+
const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
|
|
38
|
+
if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
|
|
33
39
|
const html = await render(value, ctx);
|
|
34
|
-
return opts && opts.ssr === false ? html : await injectDSD(html, ctx);
|
|
40
|
+
return opts && opts.ssr === false ? html : await injectDSD(html, ctx, [], dev);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -377,12 +383,21 @@ function isProd() {
|
|
|
377
383
|
* Dev surfaces the tag + message loudly so the failure is obvious; prod
|
|
378
384
|
* renders an empty (silent, isolated) element so no internal detail leaks.
|
|
379
385
|
*
|
|
386
|
+
* The prod-silence signal is the SERVER's `dev` flag, threaded through the SSR
|
|
387
|
+
* render context (#483). webjs keys prod on the CLI `dev` flag, not `NODE_ENV`,
|
|
388
|
+
* and `webjs start` does not export `NODE_ENV=production`, so a bare prod launch
|
|
389
|
+
* would otherwise leak the message. When `dev` is undefined (a context-free
|
|
390
|
+
* `renderToString` with no server signal, e.g. a bare unit test) it falls back
|
|
391
|
+
* to `isProd()` / `NODE_ENV`, preserving the prior behaviour for that path.
|
|
392
|
+
*
|
|
380
393
|
* @param {string} tag
|
|
381
394
|
* @param {Error} err
|
|
395
|
+
* @param {boolean} [dev] server dev flag; undefined falls back to NODE_ENV
|
|
382
396
|
* @returns {unknown} a TemplateResult (dev) or '' (prod)
|
|
383
397
|
*/
|
|
384
|
-
function defaultSSRErrorTemplate(tag, err) {
|
|
385
|
-
|
|
398
|
+
function defaultSSRErrorTemplate(tag, err, dev) {
|
|
399
|
+
const surface = dev === undefined ? !isProd() : !!dev;
|
|
400
|
+
if (!surface) return '';
|
|
386
401
|
const msg = err && err.message ? err.message : String(err);
|
|
387
402
|
return html`<div data-webjs-error="${tag}" style="border:1px solid #f5c2c7;background:#f8d7da;color:#842029;padding:8px 12px;border-radius:6px;font:13px/1.4 system-ui,sans-serif">
|
|
388
403
|
<strong><${tag}> failed to render</strong>
|
|
@@ -397,15 +412,18 @@ function defaultSSRErrorTemplate(tag, err) {
|
|
|
397
412
|
*
|
|
398
413
|
* @param {string} html
|
|
399
414
|
* @param {SuspenseCtx} [ctx]
|
|
415
|
+
* @param {any[]} [ancestors]
|
|
416
|
+
* @param {boolean} [dev] server dev flag, threaded to the per-component error
|
|
417
|
+
* template for prod-silence (#483); undefined falls back to NODE_ENV
|
|
400
418
|
* @returns {Promise<string>}
|
|
401
419
|
*/
|
|
402
|
-
async function injectDSD(html, ctx, ancestors = []) {
|
|
420
|
+
async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
403
421
|
// Resolve <webjs-suspense> boundaries first (#471): in a streaming context
|
|
404
422
|
// each becomes a fallback placeholder now, with its children pushed for
|
|
405
423
|
// out-of-order streaming; without a streaming context the children render
|
|
406
424
|
// inline (blocking). Run before the custom-element walk so a streamed
|
|
407
425
|
// boundary's children leave the main flow and are not double-processed.
|
|
408
|
-
html = await processSuspenseElements(html, ctx, ancestors);
|
|
426
|
+
html = await processSuspenseElements(html, ctx, ancestors, dev);
|
|
409
427
|
const tags = allTags();
|
|
410
428
|
if (!tags.length) return html;
|
|
411
429
|
// Sort longest tag name first so the regex alternation tries the most
|
|
@@ -487,7 +505,7 @@ async function injectDSD(html, ctx, ancestors = []) {
|
|
|
487
505
|
// Shadow DOM: native <slot> stays as-is in the DSD template. The
|
|
488
506
|
// browser handles projection from the host's light-DOM children
|
|
489
507
|
// into the shadow tree natively. No framework substitution here.
|
|
490
|
-
const innerProcessed = await injectDSD(rawInner, ctx, [...ancestors, instance]);
|
|
508
|
+
const innerProcessed = await injectDSD(rawInner, ctx, [...ancestors, instance], dev);
|
|
491
509
|
const rawStyles = /** @type any */ (Cls).styles;
|
|
492
510
|
const styleList = Array.isArray(rawStyles) ? rawStyles : rawStyles && isCSS(rawStyles) ? [rawStyles] : [];
|
|
493
511
|
const styleStr = stylesToString(styleList);
|
|
@@ -550,7 +568,7 @@ async function injectDSD(html, ctx, ancestors = []) {
|
|
|
550
568
|
}
|
|
551
569
|
const partitioned = partitionAuthoredBySlot(authoredInner);
|
|
552
570
|
const innerWithSlots = substituteSlotsInRender(rawInner, partitioned);
|
|
553
|
-
const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance]);
|
|
571
|
+
const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance], dev);
|
|
554
572
|
edits.push({
|
|
555
573
|
start: m.index,
|
|
556
574
|
end: closeEnd,
|
|
@@ -582,10 +600,10 @@ async function injectDSD(html, ctx, ancestors = []) {
|
|
|
582
600
|
if (instance && typeof instance.renderError === 'function') {
|
|
583
601
|
errTpl = instance.renderError(err);
|
|
584
602
|
}
|
|
585
|
-
if (errTpl === undefined) errTpl = defaultSSRErrorTemplate(tag, err);
|
|
603
|
+
if (errTpl === undefined) errTpl = defaultSSRErrorTemplate(tag, err, dev);
|
|
586
604
|
errorInner = await render(errTpl, ctx);
|
|
587
605
|
if (errorInner.trim()) {
|
|
588
|
-
errorInner = await injectDSD(errorInner, ctx, instance ? [...ancestors, instance] : ancestors);
|
|
606
|
+
errorInner = await injectDSD(errorInner, ctx, instance ? [...ancestors, instance] : ancestors, dev);
|
|
589
607
|
}
|
|
590
608
|
} catch (renderErrorThrew) {
|
|
591
609
|
console.error(`[webjs] renderError() for <${tag}> also threw:`, renderErrorThrew);
|
|
@@ -691,9 +709,11 @@ function isVoidElement(tag) {
|
|
|
691
709
|
* @param {string} html
|
|
692
710
|
* @param {SuspenseCtx} [ctx]
|
|
693
711
|
* @param {any[]} [ancestors]
|
|
712
|
+
* @param {boolean} [dev] server dev flag for prod-silence of a throwing
|
|
713
|
+
* component in an inline (ctx-absent) boundary (#483)
|
|
694
714
|
* @returns {Promise<string>}
|
|
695
715
|
*/
|
|
696
|
-
async function processSuspenseElements(html, ctx, ancestors = []) {
|
|
716
|
+
async function processSuspenseElements(html, ctx, ancestors = [], dev) {
|
|
697
717
|
if (html.indexOf('<webjs-suspense') === -1) return html;
|
|
698
718
|
const OPEN = /<webjs-suspense((?:"[^"]*"|'[^']*'|[^>])*?)>/i;
|
|
699
719
|
let result = '';
|
|
@@ -732,7 +752,7 @@ async function processSuspenseElements(html, ctx, ancestors = []) {
|
|
|
732
752
|
ctx.pending.push({ id, promise: Promise.resolve(unsafeHTML(inner)) });
|
|
733
753
|
result += `<webjs-suspense id="${id}">${fallbackHtml}</webjs-suspense>`;
|
|
734
754
|
} else {
|
|
735
|
-
const innerProcessed = await injectDSD(inner, ctx, ancestors);
|
|
755
|
+
const innerProcessed = await injectDSD(inner, ctx, ancestors, dev);
|
|
736
756
|
result += `<webjs-suspense>${innerProcessed}</webjs-suspense>`;
|
|
737
757
|
}
|
|
738
758
|
rest = afterClose;
|
|
@@ -1130,6 +1150,11 @@ function consumePropAttrs(attrs) {
|
|
|
1130
1150
|
*/
|
|
1131
1151
|
export function renderToStream(value, opts = { ssr: true }) {
|
|
1132
1152
|
const ctx = opts && opts.suspenseCtx;
|
|
1153
|
+
// Server dev flag for prod-silence of SSR error states (#483), same sourcing
|
|
1154
|
+
// as renderToString: opts.dev wins, else inherit from the ctx, else undefined
|
|
1155
|
+
// (NODE_ENV fallback). Back-fill the ctx so the streamed sub-renders share it.
|
|
1156
|
+
const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
|
|
1157
|
+
if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
|
|
1133
1158
|
return new ReadableStream({
|
|
1134
1159
|
async start(controller) {
|
|
1135
1160
|
try {
|
|
@@ -1141,13 +1166,13 @@ export function renderToStream(value, opts = { ssr: true }) {
|
|
|
1141
1166
|
// the full HTML), then enqueue the result. This matches the
|
|
1142
1167
|
// semantics of renderToString but still gives us a stream.
|
|
1143
1168
|
const html = await render(value, ctx);
|
|
1144
|
-
const full = await injectDSD(html, ctx);
|
|
1169
|
+
const full = await injectDSD(html, ctx, [], dev);
|
|
1145
1170
|
controller.enqueue(full);
|
|
1146
1171
|
}
|
|
1147
1172
|
|
|
1148
1173
|
// Stream resolved Suspense boundaries after the main content.
|
|
1149
1174
|
if (ctx && ctx.pending.length) {
|
|
1150
|
-
await streamSuspenseBoundaries(ctx, controller);
|
|
1175
|
+
await streamSuspenseBoundaries(ctx, controller, dev);
|
|
1151
1176
|
}
|
|
1152
1177
|
controller.close();
|
|
1153
1178
|
} catch (err) {
|
|
@@ -1404,8 +1429,10 @@ async function streamTemplate(tr, ctx, controller) {
|
|
|
1404
1429
|
*
|
|
1405
1430
|
* @param {SuspenseCtx} ctx
|
|
1406
1431
|
* @param {ReadableStreamDefaultController<string>} controller
|
|
1432
|
+
* @param {boolean} [dev] server dev flag for prod-silence of a rejected
|
|
1433
|
+
* streamed boundary (#483); undefined falls back to NODE_ENV
|
|
1407
1434
|
*/
|
|
1408
|
-
async function streamSuspenseBoundaries(ctx, controller) {
|
|
1435
|
+
async function streamSuspenseBoundaries(ctx, controller, dev) {
|
|
1409
1436
|
// Resolve the per-request nonce once per call. The provider in
|
|
1410
1437
|
// @webjsdev/server sources it from AsyncLocalStorage; outside a
|
|
1411
1438
|
// request scope (or in the browser) the helper returns '' and we
|
|
@@ -1421,7 +1448,7 @@ async function streamSuspenseBoundaries(ctx, controller) {
|
|
|
1421
1448
|
try {
|
|
1422
1449
|
const resolved = await promise;
|
|
1423
1450
|
const html = await render(resolved, ctx);
|
|
1424
|
-
const full = await injectDSD(html, ctx);
|
|
1451
|
+
const full = await injectDSD(html, ctx, [], dev);
|
|
1425
1452
|
controller.enqueue(
|
|
1426
1453
|
`<template data-webjs-resolve="${id}">${full}</template>` +
|
|
1427
1454
|
`<script${nonceAttr}>` +
|
|
@@ -1441,7 +1468,7 @@ async function streamSuspenseBoundaries(ctx, controller) {
|
|
|
1441
1468
|
// error render itself throwing) leaves the fallback in place.
|
|
1442
1469
|
try {
|
|
1443
1470
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
1444
|
-
const errHtml = await injectDSD(await render(defaultSSRErrorTemplate('webjs-suspense', e), ctx), ctx);
|
|
1471
|
+
const errHtml = await injectDSD(await render(defaultSSRErrorTemplate('webjs-suspense', e, dev), ctx), ctx, [], dev);
|
|
1445
1472
|
controller.enqueue(
|
|
1446
1473
|
`<template data-webjs-resolve="${id}">${errHtml}</template>` +
|
|
1447
1474
|
`<script${nonceAttr}>` +
|
package/src/router-client.js
CHANGED
|
@@ -11,6 +11,9 @@ import { renderStream } from './webjs-stream.js';
|
|
|
11
11
|
// Register <webjs-suspense> (the element-level streaming boundary, #471) so it
|
|
12
12
|
// is layout-neutral and available for the progressive soft-nav streaming apply.
|
|
13
13
|
import './webjs-suspense.js';
|
|
14
|
+
// Ingest SSR action seeds (#472) from an incoming soft-nav document before its
|
|
15
|
+
// components hydrate, so a navigated async component resolves from the seed.
|
|
16
|
+
import { scanSeeds } from './action-seed-client.js';
|
|
14
17
|
|
|
15
18
|
/** The content type a content-negotiated stream-action response carries (#248). */
|
|
16
19
|
const STREAM_MIME = 'text/vnd.webjs-stream.html';
|
|
@@ -2067,6 +2070,13 @@ function upgradeCustomElementsInRange(range) {
|
|
|
2067
2070
|
}
|
|
2068
2071
|
|
|
2069
2072
|
function applySwap(doc, frameId, revalidating, href, incomingBuild) {
|
|
2073
|
+
// SSR action seeding (#472): ingest any seed payload the incoming page
|
|
2074
|
+
// carries BEFORE its components are grafted into the live DOM and upgrade, so
|
|
2075
|
+
// a soft-navigated async component resolves from the seed instead of
|
|
2076
|
+
// re-fetching. Scanning `doc` (the detached parse) also strips the seed
|
|
2077
|
+
// carriers, so the inert payload never lands in the live document.
|
|
2078
|
+
try { scanSeeds(doc); } catch { /* seeding is best-effort */ }
|
|
2079
|
+
|
|
2070
2080
|
// Any clean swap (no importmap mismatch, including cache restores
|
|
2071
2081
|
// and frame swaps where we don't even run the mismatch check) is a
|
|
2072
2082
|
// signal that the user successfully navigated, so clear the reload
|
package/src/webjs-config.d.ts
CHANGED
|
@@ -109,6 +109,15 @@ export interface WebjsConfig {
|
|
|
109
109
|
*/
|
|
110
110
|
elide?: boolean;
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* SSR action-result seeding switch (#472). Default `true`. When on, a
|
|
114
|
+
* `'use server'` action's SSR result is serialized into the page so an
|
|
115
|
+
* `async render()` component does not re-fetch it over RPC on hydration. Set
|
|
116
|
+
* to `false` to disable (the client re-fetches as before). The `WEBJS_SEED`
|
|
117
|
+
* env override wins over this.
|
|
118
|
+
*/
|
|
119
|
+
seed?: boolean;
|
|
120
|
+
|
|
112
121
|
/** Per-path response-header rules, shaped like Next's. */
|
|
113
122
|
headers?: WebjsHeaderRule[];
|
|
114
123
|
|