@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/package.json
CHANGED
package/src/component.js
CHANGED
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
adoptSSRAssignments,
|
|
10
10
|
ensureSlotState,
|
|
11
11
|
hasSlotState,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
hasFrameworkRenderedSubtree,
|
|
13
|
+
installSlotInterception,
|
|
14
|
+
installSlotSensors,
|
|
15
|
+
teardownSlotSensors,
|
|
16
|
+
reconnectSweep,
|
|
15
17
|
} from './slot.js';
|
|
16
18
|
|
|
17
19
|
const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undefined';
|
|
@@ -843,17 +845,45 @@ class WebComponentBase extends Base {
|
|
|
843
845
|
// c. First mount, no SSR. Partition authored children into the
|
|
844
846
|
// record before _performRender wipes the host.
|
|
845
847
|
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
848
|
+
// After capture, installSlotInterception + installSlotSensors below make
|
|
849
|
+
// native DOM writes on a mounted host live (appendChild, slot= flips,
|
|
850
|
+
// innerHTML, HTMLSlotElement.assign), so light-DOM slots match shadow DOM
|
|
851
|
+
// through the standard API.
|
|
849
852
|
if (hasSlotState(this)) {
|
|
850
|
-
// (a) Reconnection. Record already populated
|
|
851
|
-
|
|
853
|
+
// (a) Reconnection. Record already populated. Sweep any direct child
|
|
854
|
+
// added by a raw bypass write while the host was disconnected (no
|
|
855
|
+
// sensor was live to catch it), then re-arm the sensors below.
|
|
856
|
+
reconnectSweep(this);
|
|
857
|
+
} else if (
|
|
858
|
+
this.__isHydrating() ||
|
|
859
|
+
this.hasAttribute('data-wj-serialized') ||
|
|
860
|
+
hasFrameworkRenderedSubtree(this)
|
|
861
|
+
) {
|
|
862
|
+
// (b) Framework-rendered markup, from any of three signals: boot-time
|
|
863
|
+
// SSR hydration (the webjs-hydrate marker), the router's
|
|
864
|
+
// data-wj-serialized stamp (applySwap marks every host in a parsed
|
|
865
|
+
// doc, covering a snapshot restore whose serialized shape has no
|
|
866
|
+
// projected slot to detect), or the structural detector (own
|
|
867
|
+
// data-webjs-light[data-projection] slots, attributes only the
|
|
868
|
+
// renderer / SSR stamps). Adopt-not-capture: capturing restored
|
|
869
|
+
// post-hydration HTML would hoover the rendered tree into the
|
|
870
|
+
// record and duplicate content on the next render (the #1006
|
|
871
|
+
// shape, on the restore path).
|
|
872
|
+
this.removeAttribute('data-wj-serialized');
|
|
852
873
|
ensureSlotState(this);
|
|
853
874
|
adoptSSRAssignments(this);
|
|
854
875
|
} else {
|
|
876
|
+
// (c) First mount with author-written children. Capture them.
|
|
855
877
|
captureAuthoredChildren(this);
|
|
856
878
|
}
|
|
879
|
+
// Install native-write interception AFTER capture (capture uses the
|
|
880
|
+
// host's still-native methods), then arm the sensors. Together they make
|
|
881
|
+
// appendChild / insertBefore / removeChild / innerHTML / slot= flips on a
|
|
882
|
+
// mounted light host drive the slot record, restoring full shadow-DOM
|
|
883
|
+
// parity through the standard DOM API. Interception installs once;
|
|
884
|
+
// sensors are armed on every connect and torn down on disconnect.
|
|
885
|
+
installSlotInterception(this);
|
|
886
|
+
installSlotSensors(this);
|
|
857
887
|
}
|
|
858
888
|
|
|
859
889
|
// Notify all controllers that the host is connected.
|
|
@@ -888,50 +918,13 @@ class WebComponentBase extends Base {
|
|
|
888
918
|
} catch (err) {
|
|
889
919
|
console.error(`[webjs] lifecycle hook threw during initial render:`, err);
|
|
890
920
|
}
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
921
|
+
// The renderer's slot parts place the record content as part of the
|
|
922
|
+
// commit; native DOM writes on the host (appendChild, slot= flips,
|
|
923
|
+
// innerHTML) drive the record live through the interception + sensors
|
|
924
|
+
// installed on connect. There is no WebJs-specific slot API.
|
|
894
925
|
});
|
|
895
926
|
}
|
|
896
927
|
|
|
897
|
-
/**
|
|
898
|
-
* Read view of this host's slot record (#1015): captured (or
|
|
899
|
-
* programmatically set) children per slot name, e.g.
|
|
900
|
-
* `this.slots.default` / `this.slots.header`. Fresh arrays each read.
|
|
901
|
-
* Enables conditional-on-slot rendering:
|
|
902
|
-
* `this.slots.header ? html\`...\` : ''`.
|
|
903
|
-
*
|
|
904
|
-
* @returns {Record<string, Node[]>}
|
|
905
|
-
*/
|
|
906
|
-
get slots() {
|
|
907
|
-
return slotsView(this);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Does this host's slot record carry content for `name` (#1015)?
|
|
912
|
-
* `hasSlot()` / `hasSlot('default')` query the default slot.
|
|
913
|
-
*
|
|
914
|
-
* @param {string | null} [name]
|
|
915
|
-
* @returns {boolean}
|
|
916
|
-
*/
|
|
917
|
-
hasSlot(name) {
|
|
918
|
-
return hasSlotContent(this, name);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Replace a slot's content (#1015): the ONE dynamic path for slotted
|
|
923
|
-
* children (external appendChild / slot="" flips after mount are inert
|
|
924
|
-
* by design). Accepts a Node, Node[], a string, or null to reset the
|
|
925
|
-
* slot to its fallback content. Fires `slotchange` when the assignment
|
|
926
|
-
* changes.
|
|
927
|
-
*
|
|
928
|
-
* @param {string | null} name
|
|
929
|
-
* @param {Node | Node[] | string | null} value
|
|
930
|
-
*/
|
|
931
|
-
setSlotContent(name, value) {
|
|
932
|
-
setSlotContent(this, name, value);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
928
|
/**
|
|
936
929
|
* True when this host's first child is the framework's hydration
|
|
937
930
|
* marker, meaning the SSR pipeline already rendered the template's
|
|
@@ -969,8 +962,11 @@ class WebComponentBase extends Base {
|
|
|
969
962
|
this.__hydrationObserver.disconnect();
|
|
970
963
|
this.__hydrationObserver = null;
|
|
971
964
|
}
|
|
972
|
-
//
|
|
973
|
-
//
|
|
965
|
+
// Tear down the slot sensors, processing any queued records first (a bare
|
|
966
|
+
// disconnect() drops them). The per-host slot record + interception are
|
|
967
|
+
// preserved so a reconnection picks up where it left off (sensors re-arm in
|
|
968
|
+
// connectedCallback).
|
|
969
|
+
teardownSlotSensors(this);
|
|
974
970
|
// Dispose the signal watcher so dependency edges drop. Without
|
|
975
971
|
// this the element holds references to module-scope signals
|
|
976
972
|
// (and vice versa) forever.
|
package/src/render-client.js
CHANGED
|
@@ -10,11 +10,28 @@ import {
|
|
|
10
10
|
PROJECTION_FALLBACK,
|
|
11
11
|
SLOT_FALLBACK_FRAG,
|
|
12
12
|
SLOT_STATE,
|
|
13
|
+
SLOT_OWNER,
|
|
14
|
+
RENDERING,
|
|
13
15
|
applySlotAssignments,
|
|
14
16
|
rescueAssignedNodes,
|
|
15
|
-
|
|
17
|
+
withRendererWrites,
|
|
18
|
+
drainRendererBackstop,
|
|
16
19
|
} from './slot.js';
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Open the renderer-write window on a light-DOM host while `fn` commits into
|
|
23
|
+
* it, so the host's patched slot-interception methods delegate to native and
|
|
24
|
+
* a renderer commit is never mistaken for authored content. A no-op (just runs
|
|
25
|
+
* `fn`) when `node` is not a slot host, so nested and non-host commits pay
|
|
26
|
+
* nothing. Covers the ASYNC commit paths (async directives, streaming) that
|
|
27
|
+
* run outside a synchronous render() call.
|
|
28
|
+
*/
|
|
29
|
+
function commitInto(node, fn) {
|
|
30
|
+
const host = node && /** @type {any} */ (node)[SLOT_STATE] ? node : null;
|
|
31
|
+
if (!host) return fn();
|
|
32
|
+
return withRendererWrites(host, fn);
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* Client-side renderer with **fine-grained** updates.
|
|
20
37
|
*
|
|
@@ -88,43 +105,67 @@ const INSTANCE = Symbol.for('webjs.instance');
|
|
|
88
105
|
*/
|
|
89
106
|
export function render(value, container) {
|
|
90
107
|
const host = /** @type any */ (container);
|
|
91
|
-
|
|
108
|
+
// Open the renderer-write window for the whole commit: every host-receiver
|
|
109
|
+
// write below (and in createInstance / updateInstance / clearInstance / all
|
|
110
|
+
// part commits they reach synchronously) then bypasses the slot interception
|
|
111
|
+
// that is patched onto a light host. This is the single discriminator
|
|
112
|
+
// between a renderer commit and an author write.
|
|
113
|
+
const prevRendering = host[RENDERING];
|
|
114
|
+
host[RENDERING] = true;
|
|
115
|
+
try {
|
|
116
|
+
const prev = host[INSTANCE];
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
if (isTemplate(value)) {
|
|
119
|
+
const tr = /** @type {import('./html.js').TemplateResult} */ (value);
|
|
120
|
+
if (prev && prev.strings === tr.strings) {
|
|
121
|
+
updateInstance(prev, tr.values);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (prev) clearInstance(prev, container);
|
|
125
|
+
|
|
126
|
+
// Light DOM hydration: if container has SSR content (marked by
|
|
127
|
+
// <!--webjs-hydrate-->), remove the marker and proceed with normal
|
|
128
|
+
// rendering. The content will be replaced with identical output -
|
|
129
|
+
// no visible flash because SSR and client render produce the same HTML.
|
|
130
|
+
const firstChild = container.firstChild;
|
|
131
|
+
if (firstChild && firstChild.nodeType === 8 && /** @type {Comment} */ (firstChild).data === 'webjs-hydrate') {
|
|
132
|
+
firstChild.remove();
|
|
133
|
+
}
|
|
100
134
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
// Pre-set the symbol to an explicit null BEFORE the commit,
|
|
136
|
+
// UNCONDITIONALLY: if createInstance throws after its replaceChildren
|
|
137
|
+
// (e.g. inside the slot-part apply loop), the finally-drain must see
|
|
138
|
+
// "rendered, no instance" (discard the commit's records), never
|
|
139
|
+
// "never rendered" (fold and corrupt) and never a STALE cleared prev
|
|
140
|
+
// instance on the template-swap path (whose bookends would misclassify
|
|
141
|
+
// the half-committed new roots as unowned and fold them).
|
|
142
|
+
host[INSTANCE] = null;
|
|
143
|
+
const inst = createInstance(tr, container);
|
|
144
|
+
host[INSTANCE] = inst;
|
|
145
|
+
return;
|
|
108
146
|
}
|
|
109
147
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for (const v of value) {
|
|
122
|
-
const text = document.createTextNode(String(v ?? ''));
|
|
123
|
-
container.appendChild(text);
|
|
148
|
+
// Non-template value: treat as a single text child.
|
|
149
|
+
if (prev) clearInstance(prev, container);
|
|
150
|
+
host[INSTANCE] = null;
|
|
151
|
+
container.replaceChildren();
|
|
152
|
+
if (value == null || value === false || value === true) return;
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (const v of value) {
|
|
155
|
+
const text = document.createTextNode(String(v ?? ''));
|
|
156
|
+
container.appendChild(text);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
124
159
|
}
|
|
125
|
-
|
|
160
|
+
container.appendChild(document.createTextNode(String(value)));
|
|
161
|
+
} finally {
|
|
162
|
+
host[RENDERING] = prevRendering;
|
|
163
|
+
// Outermost window closing: drain this commit's childList records off the
|
|
164
|
+
// slot backstop (drainRendererBackstop processes them with a renderer-output
|
|
165
|
+
// skip when an instance exists, else discards), so the backstop never folds
|
|
166
|
+
// renderer output. Mirrors withRendererWrites, used by the async paths.
|
|
167
|
+
if (!prevRendering) drainRendererBackstop(host);
|
|
126
168
|
}
|
|
127
|
-
container.appendChild(document.createTextNode(String(value)));
|
|
128
169
|
}
|
|
129
170
|
|
|
130
171
|
/* ================================================================
|
|
@@ -483,9 +524,21 @@ function createInstance(tr, container) {
|
|
|
483
524
|
// Slot parts have no value-hole to drive applyPart from the loop above.
|
|
484
525
|
// Apply them once now that the fragment is inserted into the live
|
|
485
526
|
// container, so each slot can locate its host by walking parents and
|
|
486
|
-
// schedule the first projection through slot.js.
|
|
527
|
+
// schedule the first projection through slot.js. Stamp each slot with the
|
|
528
|
+
// host whose TEMPLATE produced it (this container), so a FORWARDED slot
|
|
529
|
+
// (rendered here but nested inside a child component) routes to this host
|
|
530
|
+
// rather than to the child the structural walk would pick. Only an element
|
|
531
|
+
// container is a host; a fragment/shadow container leaves the structural
|
|
532
|
+
// path in place.
|
|
533
|
+
const ownerHost =
|
|
534
|
+
/** @type {any} */ (container).nodeType === 1 && /** @type {any} */ (container)[SLOT_STATE]
|
|
535
|
+
? container
|
|
536
|
+
: null;
|
|
487
537
|
for (const part of bound) {
|
|
488
|
-
if (part.kind === 'slot')
|
|
538
|
+
if (part.kind === 'slot') {
|
|
539
|
+
if (ownerHost && part.slotEl) /** @type {any} */ (part.slotEl)[SLOT_OWNER] = ownerHost;
|
|
540
|
+
applyPart(part, undefined, undefined, []);
|
|
541
|
+
}
|
|
489
542
|
}
|
|
490
543
|
|
|
491
544
|
return { strings: tr.strings, bound, lastValues, startNode, endNode };
|
|
@@ -665,9 +718,24 @@ function applyPart(part, value, _prev, allValues) {
|
|
|
665
718
|
if (part.applied) break;
|
|
666
719
|
const slotEl = part.slotEl;
|
|
667
720
|
const finalize = () => {
|
|
668
|
-
part.applied = true;
|
|
669
721
|
const host = findSlotHost(slotEl);
|
|
670
|
-
if (!host)
|
|
722
|
+
if (!host) {
|
|
723
|
+
part.applied = true;
|
|
724
|
+
return; // truly orphan slot (no owner, no structural host)
|
|
725
|
+
}
|
|
726
|
+
// A FORWARDED slot's owner is known immediately (the SLOT_OWNER
|
|
727
|
+
// stamp) but the child component has not yet PLACED the slot into
|
|
728
|
+
// the owner's subtree, so the owner's apply cannot find it yet.
|
|
729
|
+
// Re-defer until the child places it (host.contains becomes true);
|
|
730
|
+
// the child WILL place it, or the owner disconnects and the retries
|
|
731
|
+
// stop. A normal own slot is already inside its host, so this passes
|
|
732
|
+
// on the first call with no extra deferral.
|
|
733
|
+
if (!host.contains(slotEl)) {
|
|
734
|
+
if (host.isConnected) queueMicrotask(finalize);
|
|
735
|
+
else part.applied = true;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
part.applied = true;
|
|
671
739
|
// Shadow DOM: native projection. Leave fallback in place.
|
|
672
740
|
if (isInShadowRootEl(slotEl)) return;
|
|
673
741
|
// Light DOM: harvest the cloned fallback into a holding
|
|
@@ -681,7 +749,7 @@ function applyPart(part, value, _prev, allValues) {
|
|
|
681
749
|
queueMicrotask(() => applySlotAssignments(host));
|
|
682
750
|
};
|
|
683
751
|
const directHost = findSlotHost(slotEl);
|
|
684
|
-
if (directHost) {
|
|
752
|
+
if (directHost && directHost.contains(slotEl)) {
|
|
685
753
|
finalize();
|
|
686
754
|
} else {
|
|
687
755
|
queueMicrotask(finalize);
|
|
@@ -703,6 +771,10 @@ function applyPart(part, value, _prev, allValues) {
|
|
|
703
771
|
* @returns {Element | null}
|
|
704
772
|
*/
|
|
705
773
|
function findSlotHost(slotEl) {
|
|
774
|
+
// Template-owner stamp wins (a forwarded slot's true host), else the
|
|
775
|
+
// nearest SLOT_STATE ancestor structurally.
|
|
776
|
+
const owner = /** @type any */ (slotEl)[SLOT_OWNER];
|
|
777
|
+
if (owner && owner.isConnected) return owner;
|
|
706
778
|
let p = slotEl.parentElement;
|
|
707
779
|
while (p) {
|
|
708
780
|
if (/** @type any */ (p)[SLOT_STATE]) return p;
|
|
@@ -819,6 +891,22 @@ function applyChild(part, value) {
|
|
|
819
891
|
* @param {unknown} value
|
|
820
892
|
*/
|
|
821
893
|
function applyChildInner(part, value) {
|
|
894
|
+
// Open the renderer-write window around this commit. Most calls are
|
|
895
|
+
// synchronous inside render() (the window is already open, this nests
|
|
896
|
+
// harmlessly), but the async directive paths (until, watch, asyncAppend /
|
|
897
|
+
// asyncReplace, streaming) re-enter here from a promise / microtask OUTSIDE
|
|
898
|
+
// any render() window, so this is where those commits into a light host are
|
|
899
|
+
// marked as renderer writes rather than authored content.
|
|
900
|
+
return commitInto(part.marker && part.marker.parentNode, () =>
|
|
901
|
+
applyChildInnerRaw(part, value),
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* @param {Extract<BoundPart, {kind:'child'}>} part
|
|
907
|
+
* @param {unknown} value
|
|
908
|
+
*/
|
|
909
|
+
function applyChildInnerRaw(part, value) {
|
|
822
910
|
const marker = part.marker;
|
|
823
911
|
|
|
824
912
|
// unsafeHTML directive: inject raw HTML string as DOM nodes.
|
|
@@ -1072,6 +1160,16 @@ function buildDetached(tr) {
|
|
|
1072
1160
|
applyPart(bound[i], tr.values[i], undefined, tr.values);
|
|
1073
1161
|
lastValues.push(tr.values[i]);
|
|
1074
1162
|
}
|
|
1163
|
+
// Slot parts need their one-shot apply exactly like createInstance and the
|
|
1164
|
+
// nested-template path. The fragment is still detached here, so the
|
|
1165
|
+
// slot-part's own deferred finalize (a one-microtask retry when the parent
|
|
1166
|
+
// walk cannot reach a host yet) carries it: every caller inserts the
|
|
1167
|
+
// returned fragment synchronously in the same task, so the retry lands in
|
|
1168
|
+
// the live tree. Without this loop a <slot> inside a repeat() / array item
|
|
1169
|
+
// never finalizes and its content is never placeable.
|
|
1170
|
+
for (const p of bound) {
|
|
1171
|
+
if (p.kind === 'slot') applyPart(p, undefined, undefined, []);
|
|
1172
|
+
}
|
|
1075
1173
|
const outFrag = document.createDocumentFragment();
|
|
1076
1174
|
outFrag.appendChild(startNode);
|
|
1077
1175
|
while (frag.firstChild) outFrag.appendChild(frag.firstChild);
|
|
@@ -1516,6 +1614,35 @@ function applyCache(part, inner) {
|
|
|
1516
1614
|
// Reconcile values so any state changes since detachment apply.
|
|
1517
1615
|
updateInstance(cached.inst, tr.values);
|
|
1518
1616
|
part.child = cached.inst;
|
|
1617
|
+
// A re-attached instance may carry ALREADY-APPLIED slot parts, whose
|
|
1618
|
+
// finalize will never fire again, while the host's record moved on
|
|
1619
|
+
// during the stash (content for these slots was parked when an apply
|
|
1620
|
+
// ran with the slot unreachable). Re-run the apply for each owning
|
|
1621
|
+
// host so parked content is pulled back out. The collection walks the
|
|
1622
|
+
// re-attached DOM RANGE (not the instance tree): slot parts live on
|
|
1623
|
+
// whatever template level contains the <slot> (nested holes, repeat /
|
|
1624
|
+
// array items, streamed chunks), so a structural DOM walk is the only
|
|
1625
|
+
// shape that covers every composition uniformly. moveRange already
|
|
1626
|
+
// ran, so the range is live and each slot's parent walk reaches its
|
|
1627
|
+
// host.
|
|
1628
|
+
const hosts = new Set();
|
|
1629
|
+
for (
|
|
1630
|
+
let n = cached.inst.startNode.nextSibling;
|
|
1631
|
+
n && n !== cached.inst.endNode;
|
|
1632
|
+
n = n.nextSibling
|
|
1633
|
+
) {
|
|
1634
|
+
if (n.nodeType !== 1) continue;
|
|
1635
|
+
const el = /** @type {Element} */ (n);
|
|
1636
|
+
if (el.matches('slot[data-webjs-light]')) {
|
|
1637
|
+
const h = findSlotHost(el);
|
|
1638
|
+
if (h) hosts.add(h);
|
|
1639
|
+
}
|
|
1640
|
+
for (const s of el.querySelectorAll('slot[data-webjs-light]')) {
|
|
1641
|
+
const h = findSlotHost(s);
|
|
1642
|
+
if (h) hosts.add(h);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
for (const h of hosts) applySlotAssignments(h);
|
|
1519
1646
|
return;
|
|
1520
1647
|
}
|
|
1521
1648
|
}
|
|
@@ -1829,17 +1956,23 @@ async function consumeAsyncStream(state, part, dir) {
|
|
|
1829
1956
|
const mapped = dir.mapper ? dir.mapper(result.value, i) : result.value;
|
|
1830
1957
|
const newNodes = renderToNodes(mapped);
|
|
1831
1958
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1959
|
+
// This chunk commit runs in an async loop OUTSIDE any render() window,
|
|
1960
|
+
// so open the renderer-write window explicitly: without it, committing a
|
|
1961
|
+
// stream chunk into a light slot host would hit the patched insertBefore /
|
|
1962
|
+
// removeChild and fold the renderer's own output into `authored`.
|
|
1963
|
+
commitInto(marker.parentNode, () => {
|
|
1964
|
+
if (state.mode === 'replace') {
|
|
1965
|
+
for (const n of state.nodes) {
|
|
1966
|
+
if (n.parentNode) n.parentNode.removeChild(n);
|
|
1967
|
+
}
|
|
1968
|
+
state.nodes = [];
|
|
1835
1969
|
}
|
|
1836
|
-
state.nodes = [];
|
|
1837
|
-
}
|
|
1838
1970
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1971
|
+
const frag = document.createDocumentFragment();
|
|
1972
|
+
for (const n of newNodes) frag.appendChild(n);
|
|
1973
|
+
marker.parentNode?.insertBefore(frag, marker);
|
|
1974
|
+
state.nodes.push(...newNodes);
|
|
1975
|
+
});
|
|
1843
1976
|
|
|
1844
1977
|
i++;
|
|
1845
1978
|
}
|
|
@@ -1867,6 +2000,15 @@ function renderToNodes(value) {
|
|
|
1867
2000
|
for (let i = 0; i < tr.values.length; i++) {
|
|
1868
2001
|
applyPart(bound[i], tr.values[i], undefined, tr.values);
|
|
1869
2002
|
}
|
|
2003
|
+
// Slot parts need their one-shot apply here too (same contract as
|
|
2004
|
+
// createInstance / nested templates / buildDetached): the caller
|
|
2005
|
+
// (consumeAsyncStream) inserts these nodes synchronously in the same
|
|
2006
|
+
// task, so the slot-part's one-microtask finalize retry lands in the
|
|
2007
|
+
// live tree. Without this, a <slot> inside streamed chunk content never
|
|
2008
|
+
// finalizes and its name suppresses parking forever.
|
|
2009
|
+
for (const p of bound) {
|
|
2010
|
+
if (p.kind === 'slot') applyPart(p, undefined, undefined, []);
|
|
2011
|
+
}
|
|
1870
2012
|
return [...frag.childNodes];
|
|
1871
2013
|
}
|
|
1872
2014
|
if (Array.isArray(value)) {
|
package/src/render-server.js
CHANGED
|
@@ -8,7 +8,6 @@ import { isSuspense } from './suspense.js';
|
|
|
8
8
|
import { unsafeHTML, isUnsafeHTML, isLive, isKeyed, isGuard, isTemplateContent, isRef, isCache, isUntil, isAsyncAppend, isAsyncReplace, isWatch } from './directives.js';
|
|
9
9
|
import { stringify, parse } from './serialize.js';
|
|
10
10
|
import { cspNonce } from './csp-nonce.js';
|
|
11
|
-
import { ensureSlotState } from './slot.js';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Render a TemplateResult (or any renderable value) to an HTML string.
|
|
@@ -487,14 +486,12 @@ async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
|
487
486
|
seedServerAttrs(instance, attrMap);
|
|
488
487
|
applyAttrsToInstance(instance, attrMap, Cls);
|
|
489
488
|
for (const [k, v] of Object.entries(propValues)) instance[k] = v;
|
|
490
|
-
// Extract the authored inner HTML BEFORE the render (
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// the shadow branch is a read-only peek (its authored children stay
|
|
497
|
-
// in place for native projection, and its edit keeps the old end).
|
|
489
|
+
// Extract the authored inner HTML BEFORE the render (the injectDSD
|
|
490
|
+
// reorder): the source scan needs no render output, and hoisting it lets
|
|
491
|
+
// the light-DOM branch below project the authored children into the
|
|
492
|
+
// rendered slots. The shadow branch is a read-only peek (its authored
|
|
493
|
+
// children stay in place for native projection, and its edit keeps the
|
|
494
|
+
// old end).
|
|
498
495
|
let authoredInner = '';
|
|
499
496
|
let closeEnd = m.index + match.length;
|
|
500
497
|
if (!selfClose) {
|
|
@@ -515,20 +512,6 @@ async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
|
515
512
|
}
|
|
516
513
|
}
|
|
517
514
|
const partitioned = partitionAuthoredBySlot(authoredInner);
|
|
518
|
-
// Seed the slot record for SSR-side reads: LIGHT DOM only, matching the
|
|
519
|
-
// client (which only creates slot state on the light-DOM path, since
|
|
520
|
-
// shadow slots are native browser projection). Seeding shadow here
|
|
521
|
-
// would make hasSlot() true at SSR and false after hydration, flipping
|
|
522
|
-
// conditional-on-slot markup on the first client render. At SSR the
|
|
523
|
-
// record value is the authored RAW HTML (one string per name; there is
|
|
524
|
-
// no DOM to hold Nodes), so PRESENCE and KEYS match the client record,
|
|
525
|
-
// which is the conditional-on-slot contract. Node access is client-side.
|
|
526
|
-
if (!isShadow) {
|
|
527
|
-
const slotState = ensureSlotState(instance);
|
|
528
|
-
for (const [name, htmlChunk] of partitioned) {
|
|
529
|
-
if (htmlChunk && htmlChunk.length) slotState.assignedByName.set(name, [htmlChunk]);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
515
|
// Run the pre-render lifecycle (willUpdate, controllers' hostUpdate,
|
|
533
516
|
// then reflect reflect:true props) so derived state computed there is
|
|
534
517
|
// correct in the SSR'd HTML, matching how lit runs the update cycle at
|
|
@@ -613,7 +596,7 @@ async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
|
613
596
|
// 2. Recursively run injectDSD on the substituted output so
|
|
614
597
|
// nested custom elements (inside projected children) get
|
|
615
598
|
// their own DSD pass.
|
|
616
|
-
const innerWithSlots = substituteSlotsInRender(rawInner, partitioned);
|
|
599
|
+
const innerWithSlots = substituteSlotsInRender(rawInner, partitioned, tag);
|
|
617
600
|
const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance], dev);
|
|
618
601
|
edits.push({
|
|
619
602
|
start: m.index,
|
|
@@ -967,11 +950,19 @@ function appendStringToMap(map, key, value) {
|
|
|
967
950
|
* slots with the same name follow the first-wins rule per spec; later
|
|
968
951
|
* same-named slots fall back regardless of available projection.
|
|
969
952
|
*
|
|
953
|
+
* The `ownerTag` (the tag of the component whose template rendered these
|
|
954
|
+
* slots) is emitted as `data-wj-slot-owner` so the client resolves template
|
|
955
|
+
* ownership on hydration the same way the client renderer stamps SLOT_OWNER,
|
|
956
|
+
* which is what makes a FORWARDED slot (rendered by this component but nested
|
|
957
|
+
* inside a child) route to this component and not the child (#1023).
|
|
958
|
+
*
|
|
970
959
|
* @param {string} rendered
|
|
971
960
|
* @param {Map<string|null, string>} partitioned
|
|
961
|
+
* @param {string} ownerTag
|
|
972
962
|
* @returns {string}
|
|
973
963
|
*/
|
|
974
|
-
function substituteSlotsInRender(rendered, partitioned) {
|
|
964
|
+
function substituteSlotsInRender(rendered, partitioned, ownerTag) {
|
|
965
|
+
const ownerAttr = ownerTag ? ` data-wj-slot-owner="${escapeAttr(ownerTag)}"` : '';
|
|
975
966
|
/** @type {Set<string|null>} */
|
|
976
967
|
const consumedNames = new Set();
|
|
977
968
|
let result = '';
|
|
@@ -1016,9 +1007,9 @@ function substituteSlotsInRender(rendered, partitioned) {
|
|
|
1016
1007
|
const extraAttrs = otherAttrs ? ` ${otherAttrs}` : '';
|
|
1017
1008
|
if (projected !== undefined && !consumedNames.has(slotKey)) {
|
|
1018
1009
|
consumedNames.add(slotKey);
|
|
1019
|
-
result += `<slot data-webjs-light data-projection="actual"${nameAttr}${extraAttrs}>${projected}</slot>`;
|
|
1010
|
+
result += `<slot data-webjs-light data-projection="actual"${ownerAttr}${nameAttr}${extraAttrs}>${projected}</slot>`;
|
|
1020
1011
|
} else {
|
|
1021
|
-
result += `<slot data-webjs-light data-projection="fallback"${nameAttr}${extraAttrs}>${fallback}</slot>`;
|
|
1012
|
+
result += `<slot data-webjs-light data-projection="fallback"${ownerAttr}${nameAttr}${extraAttrs}>${fallback}</slot>`;
|
|
1022
1013
|
}
|
|
1023
1014
|
cursor = totalEnd;
|
|
1024
1015
|
slotRe.lastIndex = totalEnd;
|