@webjsdev/core 0.7.40 → 0.7.41
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 +2 -2
- package/dist/webjs-core-testing.js +19 -19
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +4 -4
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/component.js +47 -51
- package/src/render-client.js +188 -46
- package/src/render-server.js +18 -27
- package/src/router-client.js +164 -16
- package/src/slot.js +1755 -153
package/src/router-client.js
CHANGED
|
@@ -18,7 +18,7 @@ import { scanSeeds } from './action-seed-client.js';
|
|
|
18
18
|
// reused hydrated light-DOM component across a soft nav (#908).
|
|
19
19
|
import {
|
|
20
20
|
SLOT_STATE, LIGHT_SLOT_ATTR, PROJECTION_ATTR, PROJECTION_ACTUAL,
|
|
21
|
-
|
|
21
|
+
projectAuthored, keyOfName, isAuthoredContentSlot,
|
|
22
22
|
} from './slot.js';
|
|
23
23
|
|
|
24
24
|
/** The content type a content-negotiated stream-action response carries (#248). */
|
|
@@ -2605,6 +2605,23 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild, incomingSrc)
|
|
|
2605
2605
|
// carriers, so the inert payload never lands in the live document.
|
|
2606
2606
|
try { scanSeeds(doc); } catch { /* seeding is best-effort */ }
|
|
2607
2607
|
|
|
2608
|
+
// Every host in this parsed doc is FRAMEWORK-SERIALIZED markup (an SSR
|
|
2609
|
+
// fragment or a back/forward snapshot of post-hydration HTML), never
|
|
2610
|
+
// author-written children. Stamp them so connectedCallback's slot chooser
|
|
2611
|
+
// ADOPTS instead of capture-hoovering the rendered tree, which matters for
|
|
2612
|
+
// a restored host whose serialized shape carries no projected slot (a
|
|
2613
|
+
// conditionally closed slot at snapshot time) where the structural
|
|
2614
|
+
// slot-marker detector has nothing to see. The chooser consumes and removes
|
|
2615
|
+
// the attribute on upgrade.
|
|
2616
|
+
// (An ELIDED display-only host never upgrades, so its stamp is retained as
|
|
2617
|
+
// an inert attribute; diffElementInPlace never copies it onto a live host,
|
|
2618
|
+
// and the upgrade path consumes it, so it cannot mis-route anything.)
|
|
2619
|
+
try {
|
|
2620
|
+
for (const el of doc.querySelectorAll('[data-wj-host]')) {
|
|
2621
|
+
el.setAttribute('data-wj-serialized', '');
|
|
2622
|
+
}
|
|
2623
|
+
} catch { /* stamping is best-effort */ }
|
|
2624
|
+
|
|
2608
2625
|
// Any clean swap (no importmap mismatch, including cache restores
|
|
2609
2626
|
// and frame swaps where we don't even run the mismatch check) is a
|
|
2610
2627
|
// signal that the user successfully navigated, so clear the reload
|
|
@@ -2806,6 +2823,15 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild, incomingSrc)
|
|
|
2806
2823
|
// REPLACE anchors at a parent already compared equal; the other tiers
|
|
2807
2824
|
// require no change at all), and the fresh deeper keys arrive via the
|
|
2808
2825
|
// physically replaced boundary comments inside the range.
|
|
2826
|
+
//
|
|
2827
|
+
// When the swapped range lives INSIDE a light-DOM slot (a layout whose
|
|
2828
|
+
// ${children} render inside a slotted shell component), the raw swap
|
|
2829
|
+
// just rewrote nodes the slot runtime believes it owns, so its record is
|
|
2830
|
+
// now stale. Resync the owning host's record from the slot's real
|
|
2831
|
+
// children through the one public seam, or the host's next
|
|
2832
|
+
// applySlotAssignments would wipe the freshly swapped content and
|
|
2833
|
+
// restore the stale list.
|
|
2834
|
+
resyncEnclosingHostSlots(live.start, incoming.start);
|
|
2809
2835
|
blurOutgoingFocus();
|
|
2810
2836
|
}, () => upgradeCustomElementsInRange(live));
|
|
2811
2837
|
forwardSuspenseResolvers(doc.body);
|
|
@@ -3078,6 +3104,14 @@ function diffElementInPlace(dst, src) {
|
|
|
3078
3104
|
for (const attr of src.attributes) {
|
|
3079
3105
|
srcAttrs.add(attr.name);
|
|
3080
3106
|
if (LIVE_ATTRS.has(attr.name)) continue;
|
|
3107
|
+
// The serialized-restore stamp is a message to a NOT-YET-UPGRADED
|
|
3108
|
+
// element's connectedCallback; copying it onto a live reused host would
|
|
3109
|
+
// leave a consume-once marker lingering in the live DOM forever. Note
|
|
3110
|
+
// the REMOVAL loop below never strips an existing stamp either (the
|
|
3111
|
+
// stamp is in srcAttrs, added before this skip) and that retention is
|
|
3112
|
+
// load-bearing: a not-yet-upgraded `static lazy` host must KEEP its
|
|
3113
|
+
// stamp across an intervening morph so its late upgrade still adopts.
|
|
3114
|
+
if (attr.name === 'data-wj-serialized') continue;
|
|
3081
3115
|
if (dst.getAttribute(attr.name) !== attr.value) {
|
|
3082
3116
|
dst.setAttribute(attr.name, attr.value);
|
|
3083
3117
|
}
|
|
@@ -3133,7 +3167,12 @@ function diffElementInPlace(dst, src) {
|
|
|
3133
3167
|
* @returns {boolean}
|
|
3134
3168
|
*/
|
|
3135
3169
|
function isHydratedComponent(el) {
|
|
3136
|
-
|
|
3170
|
+
// Opaque to the router when it has rendered (INSTANCE) OR merely has slot
|
|
3171
|
+
// state installed but has not yet run its deferred first render (SLOT_STATE):
|
|
3172
|
+
// in that window a same-task morph would otherwise reconcile INTO the host
|
|
3173
|
+
// through the slot interception.
|
|
3174
|
+
const a = /** @type {any} */ (el);
|
|
3175
|
+
return a[Symbol.for('webjs.instance')] != null || a[SLOT_STATE] != null;
|
|
3137
3176
|
}
|
|
3138
3177
|
|
|
3139
3178
|
/**
|
|
@@ -3167,7 +3206,31 @@ function ownActualLightSlots(host) {
|
|
|
3167
3206
|
for (const slot of host.querySelectorAll(sel)) {
|
|
3168
3207
|
const s = /** @type {HTMLSlotElement} */ (slot);
|
|
3169
3208
|
if (!isOwnLightSlot(s, host)) continue;
|
|
3170
|
-
|
|
3209
|
+
// The runtime's invariant applies here too: a slot inside AUTHORED
|
|
3210
|
+
// content (an author-relocated rendered chunk, a spoofed stamp) is
|
|
3211
|
+
// inert content, never a reprojection target; collecting it would
|
|
3212
|
+
// project into (or evict from) a slot the apply refuses to place.
|
|
3213
|
+
if (isAuthoredContentSlot(host, s)) continue;
|
|
3214
|
+
// Src-side (parsed doc) hosts have no record, so the authored test is
|
|
3215
|
+
// inert there; the SERIALIZED shape of content is structural instead: a
|
|
3216
|
+
// slot nested inside an ACTUAL-mode light slot of the same host is
|
|
3217
|
+
// content (an SSR'd forwarded slot rides inside the inner host's own
|
|
3218
|
+
// actual slot), never a reprojection target. A slot inside a
|
|
3219
|
+
// FALLBACK-mode container stays collectable: fallback content is
|
|
3220
|
+
// template markup, and a slot there is legitimate.
|
|
3221
|
+
let nestedInActual = false;
|
|
3222
|
+
for (let a = s.parentElement; a && a !== host; a = a.parentElement) {
|
|
3223
|
+
if (
|
|
3224
|
+
a.tagName === 'SLOT' &&
|
|
3225
|
+
a.hasAttribute(LIGHT_SLOT_ATTR) &&
|
|
3226
|
+
a.getAttribute(PROJECTION_ATTR) === PROJECTION_ACTUAL
|
|
3227
|
+
) {
|
|
3228
|
+
nestedInActual = true;
|
|
3229
|
+
break;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (nestedInActual) continue;
|
|
3233
|
+
const name = keyOfName(s.getAttribute('name'));
|
|
3171
3234
|
if (!byName.has(name)) byName.set(name, s);
|
|
3172
3235
|
}
|
|
3173
3236
|
return byName;
|
|
@@ -3193,19 +3256,104 @@ function ownActualLightSlots(host) {
|
|
|
3193
3256
|
* on the page-authored slot children, exactly as #908 shipped.
|
|
3194
3257
|
* - actual->fallback (content REMOVED) and fallback->actual (content ADDED):
|
|
3195
3258
|
* a slot's fallback is RENDER-OWNED (the compiled fallback template held by
|
|
3196
|
-
* the slot-part), so these are NOT a raw reconcile.
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3199
|
-
*
|
|
3200
|
-
*
|
|
3201
|
-
* pass
|
|
3202
|
-
*
|
|
3203
|
-
* let this component's assignment reach into a nested child's same-named
|
|
3204
|
-
* slot and clobber its render-owned nodes (the #906 hazard, one level down).
|
|
3259
|
+
* the slot-part), so these are NOT a raw reconcile. All three cases route
|
|
3260
|
+
* through `projectAuthored`, the record seam, whose apply pass restores or
|
|
3261
|
+
* swaps the render-owned fallback without reconciling any lit-html part
|
|
3262
|
+
* (#912). The #906 one-level-down hazard (this component's assignment
|
|
3263
|
+
* reaching a nested child's same-named slot) is answered by the apply
|
|
3264
|
+
* pass's own-slot filtering (`isOwnSlot` + the authored-content
|
|
3265
|
+
* exclusion), not by surgical single-slot application.
|
|
3205
3266
|
*
|
|
3206
3267
|
* @param {Element} dst Live hydrated component host.
|
|
3207
3268
|
* @param {Element} src Incoming SSR copy of the same component.
|
|
3208
3269
|
*/
|
|
3270
|
+
/**
|
|
3271
|
+
* After a boundary swap, if the swapped range's parent is a light-DOM slot,
|
|
3272
|
+
* resync the owning host's slot record from the slot's REAL children through
|
|
3273
|
+
* the one public seam (`projectAuthored`). The router's raw range write is the
|
|
3274
|
+
* one sanctioned write into a region the slot runtime also places (a layout's
|
|
3275
|
+
* `${children}` rendered inside a slotted shell puts the `wj:children` markers
|
|
3276
|
+
* INSIDE that shell's slot), so without this sync the record goes stale and
|
|
3277
|
+
* the host's next apply would wipe the swapped-in page content and restore the
|
|
3278
|
+
* pruned old list. Walking up from the slot, the owner is the nearest
|
|
3279
|
+
* `SLOT_STATE` host with no other custom element in between; anything else
|
|
3280
|
+
* (a nested stateless component, a shadow slot) bails.
|
|
3281
|
+
*
|
|
3282
|
+
* @param {Comment} startMarker
|
|
3283
|
+
*/
|
|
3284
|
+
function resyncEnclosingSlotRecord(startMarker) {
|
|
3285
|
+
const p = startMarker.parentNode;
|
|
3286
|
+
if (!p || p.nodeType !== 1) return;
|
|
3287
|
+
const slotEl = /** @type {Element} */ (p);
|
|
3288
|
+
if (slotEl.tagName !== 'SLOT' || !slotEl.hasAttribute(LIGHT_SLOT_ATTR)) return;
|
|
3289
|
+
let host = null;
|
|
3290
|
+
for (let a = slotEl.parentElement; a; a = a.parentElement) {
|
|
3291
|
+
if (/** @type {any} */ (a)[SLOT_STATE]) { host = a; break; }
|
|
3292
|
+
if (a.tagName.includes('-')) return; // belongs to a stateless nested element
|
|
3293
|
+
}
|
|
3294
|
+
if (!host) return;
|
|
3295
|
+
projectAuthored(host, keyOfName(slotEl.getAttribute('name')), [...slotEl.childNodes]);
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
/**
|
|
3299
|
+
* Resync the ENCLOSING HOST's slots after a boundary swap whose markers live
|
|
3300
|
+
* inside a light-DOM slot (a layout whose `${children}` render inside a
|
|
3301
|
+
* slotted shell). The boundary swap only rewrites the DEFAULT slice (the
|
|
3302
|
+
* `wj:children` markers always partition to the default slot), so a page that
|
|
3303
|
+
* emits top-level `slot=`-attributed children left the shell's NAMED slots
|
|
3304
|
+
* showing the previous page's content (#1024). This resyncs the enclosing
|
|
3305
|
+
* (default) slot from its just-swapped LIVE children, then reprojects the
|
|
3306
|
+
* sibling NAMED slots from the INCOMING parsed host.
|
|
3307
|
+
*
|
|
3308
|
+
* @param {Comment} liveStart
|
|
3309
|
+
* @param {Comment} incStart
|
|
3310
|
+
*/
|
|
3311
|
+
function resyncEnclosingHostSlots(liveStart, incStart) {
|
|
3312
|
+
const lp = liveStart.parentNode;
|
|
3313
|
+
if (!lp || lp.nodeType !== 1) return;
|
|
3314
|
+
const liveSlot = /** @type {Element} */ (lp);
|
|
3315
|
+
if (liveSlot.tagName !== 'SLOT' || !liveSlot.hasAttribute(LIGHT_SLOT_ATTR)) return;
|
|
3316
|
+
let liveHost = null;
|
|
3317
|
+
for (let a = liveSlot.parentElement; a; a = a.parentElement) {
|
|
3318
|
+
if (/** @type {any} */ (a)[SLOT_STATE]) { liveHost = a; break; }
|
|
3319
|
+
if (a.tagName.includes('-')) return; // enclosing slot belongs to a stateless nested element
|
|
3320
|
+
}
|
|
3321
|
+
if (!liveHost) return;
|
|
3322
|
+
const enclosingName = keyOfName(liveSlot.getAttribute('name'));
|
|
3323
|
+
// 1. The enclosing (boundary) slot, from its own just-swapped live children.
|
|
3324
|
+
projectAuthored(liveHost, enclosingName, [...liveSlot.childNodes]);
|
|
3325
|
+
|
|
3326
|
+
// 2. The sibling NAMED slots, from the incoming parsed host. Find the
|
|
3327
|
+
// incoming host structurally (the parsed copy is not upgraded, so no
|
|
3328
|
+
// SLOT_STATE): the nearest custom-element ancestor of the incoming
|
|
3329
|
+
// boundary marker's enclosing slot.
|
|
3330
|
+
const ip = incStart.parentNode;
|
|
3331
|
+
if (!ip || ip.nodeType !== 1) return;
|
|
3332
|
+
const incSlot = /** @type {Element} */ (ip);
|
|
3333
|
+
if (incSlot.tagName !== 'SLOT') return;
|
|
3334
|
+
let incHost = null;
|
|
3335
|
+
for (let a = incSlot.parentElement; a; a = a.parentElement) {
|
|
3336
|
+
if (a.tagName.includes('-')) { incHost = a; break; }
|
|
3337
|
+
}
|
|
3338
|
+
if (!incHost || incHost.tagName !== liveHost.tagName) return;
|
|
3339
|
+
|
|
3340
|
+
const liveSlots = ownActualLightSlots(liveHost);
|
|
3341
|
+
const incSlots = ownActualLightSlots(incHost);
|
|
3342
|
+
for (const name of new Set([...liveSlots.keys(), ...incSlots.keys()])) {
|
|
3343
|
+
if (name === enclosingName) continue; // handled in (1); never re-reconcile the swapped range
|
|
3344
|
+
const inc = incSlots.get(name);
|
|
3345
|
+
if (inc) {
|
|
3346
|
+
projectAuthored(
|
|
3347
|
+
liveHost,
|
|
3348
|
+
name,
|
|
3349
|
+
[...inc.childNodes].map((n) => document.importNode(n, true)),
|
|
3350
|
+
);
|
|
3351
|
+
} else if (liveSlots.get(name)) {
|
|
3352
|
+
projectAuthored(liveHost, name, null); // dropped by the incoming page: revert to fallback
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3209
3357
|
function reprojectSlottedContent(dst, src) {
|
|
3210
3358
|
// Only a light-DOM component that tracks slot assignments has placed
|
|
3211
3359
|
// page-authored content to update. No slot state (no <slot>, or a shadow-DOM
|
|
@@ -3218,7 +3366,7 @@ function reprojectSlottedContent(dst, src) {
|
|
|
3218
3366
|
if (liveSlots.size === 0 && incSlots.size === 0) return;
|
|
3219
3367
|
|
|
3220
3368
|
// #1015: slotted children are VALUES pushed through the ONE public seam,
|
|
3221
|
-
//
|
|
3369
|
+
// projectAuthored (no cross-module state surgery; the slot runtime owns the
|
|
3222
3370
|
// record, fires slotchange on a genuine set change, and re-applies). The
|
|
3223
3371
|
// union walk covers boundary transitions (a name present on only one side).
|
|
3224
3372
|
const names = new Set([...liveSlots.keys(), ...incSlots.keys()]);
|
|
@@ -3233,13 +3381,13 @@ function reprojectSlottedContent(dst, src) {
|
|
|
3233
3381
|
// set-equality check makes slotchange fire exactly on an
|
|
3234
3382
|
// add/remove/replace and stay silent on a pure text edit (#912).
|
|
3235
3383
|
reconcileChildren(liveSlot, incSlot);
|
|
3236
|
-
|
|
3384
|
+
projectAuthored(dst, name, [...liveSlot.childNodes]);
|
|
3237
3385
|
} else if (incSlot) {
|
|
3238
3386
|
// fallback->actual: incoming ADDED content. Import and push.
|
|
3239
|
-
|
|
3387
|
+
projectAuthored(dst, name, [...incSlot.childNodes].map((n) => document.importNode(n, true)));
|
|
3240
3388
|
} else {
|
|
3241
3389
|
// actual->fallback: incoming DROPPED the content. Reset to fallback.
|
|
3242
|
-
|
|
3390
|
+
projectAuthored(dst, name, null);
|
|
3243
3391
|
}
|
|
3244
3392
|
}
|
|
3245
3393
|
}
|