@webjsdev/core 0.7.50 → 0.7.53
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 +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -20
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +3 -3
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/component/lifecycle.js +1481 -0
- package/src/component/reactive.js +171 -0
- package/src/component/server-element.js +248 -0
- package/src/component.js +2 -1896
- package/src/css.d.ts +9 -0
- package/src/escape.d.ts +2 -0
- package/src/html.d.ts +9 -0
- package/src/render-client/parts.js +2039 -0
- package/src/render-client/reconciler.js +388 -0
- package/src/render-client/template-compiler.js +525 -0
- package/src/render-client.js +1 -2938
- package/src/render-server/attrs.js +160 -0
- package/src/render-server/dsd.js +445 -0
- package/src/render-server/html-scan.js +402 -0
- package/src/render-server/slots.js +213 -0
- package/src/render-server/stream.js +163 -0
- package/src/render-server/template-renderer.js +938 -0
- package/src/render-server/text.js +163 -0
- package/src/render-server.js +6 -2405
- package/src/repeat.d.ts +15 -0
- package/src/rich-fetch.d.ts +4 -0
- package/src/router-client/anchors.js +44 -0
- package/src/router-client/boundaries.js +221 -0
- package/src/router-client/constants.js +206 -0
- package/src/router-client/diagnostics.js +377 -0
- package/src/router-client/dom-differ.js +709 -0
- package/src/router-client/dom-parse.js +150 -0
- package/src/router-client/events.js +182 -0
- package/src/router-client/fetch-apply.js +432 -0
- package/src/router-client/form-encoder.js +135 -0
- package/src/router-client/frames.js +329 -0
- package/src/router-client/head-merge.js +344 -0
- package/src/router-client/nav-error.js +105 -0
- package/src/router-client/navigator.js +952 -0
- package/src/router-client/prefetch.js +831 -0
- package/src/router-client/scroll.js +308 -0
- package/src/router-client/snapshot-cache.js +113 -0
- package/src/router-client/state.js +155 -0
- package/src/router-client/stream.js +206 -0
- package/src/router-client/swap.js +454 -0
- package/src/router-client/upgrade.js +261 -0
- package/src/router-client/view-transition.js +223 -0
- package/src/router-client.d.ts +11 -2
- package/src/router-client.js +78 -5345
- package/src/slot/assignment.js +304 -0
- package/src/slot/interception.js +558 -0
- package/src/slot/polyfills.js +365 -0
- package/src/slot/project.js +432 -0
- package/src/slot/sensors.js +303 -0
- package/src/slot/state.js +245 -0
- package/src/slot/symbols.js +85 -0
- package/src/slot.js +7 -2199
- package/src/suspense.d.ts +9 -0
- package/src/websocket-client.d.ts +22 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MutationObserver sensors that keep a host in sync with post-mount
|
|
3
|
+
* native writes, plus the renderer-write backstop.
|
|
4
|
+
*
|
|
5
|
+
* Moved verbatim out of the pre-split `slot.js`; see that barrel for the
|
|
6
|
+
* runtime's full contract.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { isOwnSlot } from './assignment.js';
|
|
11
|
+
import { instanceOwns } from './interception.js';
|
|
12
|
+
import { inBrowser } from './polyfills.js';
|
|
13
|
+
import { applySlotAssignments } from './project.js';
|
|
14
|
+
import { ensureSlotState } from './state.js';
|
|
15
|
+
import { FRAMEWORK_DETACHED, LIGHT_SLOT_ATTR, PARK, RENDERING, SLOT_STATE } from './symbols.js';
|
|
16
|
+
|
|
17
|
+
// Saved native references, captured once in the browser (Node has no `Node`).
|
|
18
|
+
export let N_appendChild = null;
|
|
19
|
+
export let N_insertBefore = null;
|
|
20
|
+
export let N_removeChild = null;
|
|
21
|
+
export let N_replaceChild = null;
|
|
22
|
+
export let N_append = null;
|
|
23
|
+
export let N_prepend = null;
|
|
24
|
+
export let N_replaceChildren = null;
|
|
25
|
+
export let INNER_HTML_DESC = null;
|
|
26
|
+
export let TEXT_CONTENT_DESC = null;
|
|
27
|
+
|
|
28
|
+
export function captureNatives() {
|
|
29
|
+
if (N_appendChild || !inBrowser) return;
|
|
30
|
+
N_appendChild = Node.prototype.appendChild;
|
|
31
|
+
N_insertBefore = Node.prototype.insertBefore;
|
|
32
|
+
N_removeChild = Node.prototype.removeChild;
|
|
33
|
+
N_replaceChild = Node.prototype.replaceChild;
|
|
34
|
+
N_append = Element.prototype.append;
|
|
35
|
+
N_prepend = Element.prototype.prepend;
|
|
36
|
+
N_replaceChildren = Element.prototype.replaceChildren;
|
|
37
|
+
INNER_HTML_DESC = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
|
|
38
|
+
TEXT_CONTENT_DESC = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run `fn` with the renderer-write window open on `host`, restoring the prior
|
|
43
|
+
* flag afterward (re-entrancy safe: nested renderer commits nest cleanly). The
|
|
44
|
+
* renderer wraps every host-receiver commit in this; a write to a light host
|
|
45
|
+
* inside the window bypasses the interception and hits the native DOM.
|
|
46
|
+
*
|
|
47
|
+
* @template T
|
|
48
|
+
* @param {any} host
|
|
49
|
+
* @param {() => T} fn
|
|
50
|
+
* @returns {T}
|
|
51
|
+
*/
|
|
52
|
+
export function withRendererWrites(host, fn) {
|
|
53
|
+
const prev = host[RENDERING];
|
|
54
|
+
host[RENDERING] = true;
|
|
55
|
+
try {
|
|
56
|
+
return fn();
|
|
57
|
+
} finally {
|
|
58
|
+
host[RENDERING] = prev;
|
|
59
|
+
// When the OUTERMOST window closes, drain the backstop synchronously before
|
|
60
|
+
// its async callback fires. The drain PROCESSES the records (renderer output
|
|
61
|
+
// is skipped structurally, a genuine bypass write is folded), so this
|
|
62
|
+
// commit's own childList churn is absorbed without losing a real author
|
|
63
|
+
// write that coincided in the same task. (The flip sensor is NOT drained: a
|
|
64
|
+
// renderer `name=` write on a slot is exactly what re-projects a dynamic
|
|
65
|
+
// `name=${...}`.)
|
|
66
|
+
if (!prev) drainRendererBackstop(host);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Install the two read-only sensors on a light host. Neither moves a node; each
|
|
72
|
+
* only folds a mutation into `authored` and calls the single renderer-owned
|
|
73
|
+
* writer. Installed on connect, torn down on disconnect.
|
|
74
|
+
* - Bypass backstop (childList, subtree:false): catches raw writes that skip
|
|
75
|
+
* the patched methods (`Node.prototype.appendChild.call`, Range ops).
|
|
76
|
+
* - Flip sensor (attributes slot/name, subtree:true): catches an `el.slot=`
|
|
77
|
+
* flip on a projected child and a slot `name=` change.
|
|
78
|
+
*
|
|
79
|
+
* @param {Element} host
|
|
80
|
+
*/
|
|
81
|
+
export function installSlotSensors(host) {
|
|
82
|
+
if (!inBrowser) return;
|
|
83
|
+
const h = /** @type {any} */ (host);
|
|
84
|
+
const state = ensureSlotState(host);
|
|
85
|
+
if (state.backstop) return;
|
|
86
|
+
|
|
87
|
+
state.backstop = new MutationObserver((records) => processBackstop(host, state, records));
|
|
88
|
+
state.backstop.observe(host, { childList: true, subtree: false });
|
|
89
|
+
|
|
90
|
+
state.flipSensor = new MutationObserver((records) => processFlip(host, records));
|
|
91
|
+
state.flipSensor.observe(host, {
|
|
92
|
+
attributes: true,
|
|
93
|
+
attributeFilter: ['slot', 'name'],
|
|
94
|
+
subtree: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Backstop callback body: fold a raw direct-child add / un-author a raw remove.
|
|
100
|
+
* A renderer-committed node (the render root and everything between the instance
|
|
101
|
+
* bookend markers) is SKIPPED structurally, so this is safe to run on the
|
|
102
|
+
* records drained at a renderer-write window close, not only on genuine
|
|
103
|
+
* author-bypass records. That is what keeps a real bypass write that happened
|
|
104
|
+
* to coincide with a commit in the same task from being silently dropped.
|
|
105
|
+
*/
|
|
106
|
+
export function processBackstop(host, state, records) {
|
|
107
|
+
const h = /** @type {any} */ (host);
|
|
108
|
+
const park = h[PARK];
|
|
109
|
+
const inst = h[Symbol.for('webjs.instance')];
|
|
110
|
+
let dirty = false;
|
|
111
|
+
// Nodes the AUTHOR touched in THIS pass (any addedNodes appearance): for
|
|
112
|
+
// them, the latest same-pass author action wins, so a following removal
|
|
113
|
+
// record splices even a marked node.
|
|
114
|
+
const touchedThisPass = new Set();
|
|
115
|
+
for (const r of records) {
|
|
116
|
+
for (const node of r.addedNodes) {
|
|
117
|
+
touchedThisPass.add(node);
|
|
118
|
+
if (node === park) continue;
|
|
119
|
+
if (inst && instanceOwns(inst, node)) continue; // renderer output, not authored
|
|
120
|
+
if (state.authored.indexOf(node) !== -1) {
|
|
121
|
+
// A raw bypass MOVE of an already-authored node back onto the host:
|
|
122
|
+
// the record is right but the node now physically sits outside its
|
|
123
|
+
// slot. Re-apply so the physically-verifying placement step repairs
|
|
124
|
+
// it (no slotchange fires: the assigned SET is unchanged).
|
|
125
|
+
dirty = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
FRAMEWORK_DETACHED.add(node);
|
|
129
|
+
state.authored.push(node);
|
|
130
|
+
dirty = true;
|
|
131
|
+
}
|
|
132
|
+
for (const node of r.removedNodes) {
|
|
133
|
+
const i = state.authored.indexOf(node);
|
|
134
|
+
if (i === -1) continue;
|
|
135
|
+
if (host.contains(node)) continue; // placement/park move: still ours
|
|
136
|
+
// Containment is evaluated at PROCESSING time, so a stale placement
|
|
137
|
+
// record (host to slot move) can be processed after a rescue detached
|
|
138
|
+
// the node as a record value: retain exactly that shape (marked,
|
|
139
|
+
// parentless, untouched by the author this pass). Everything else in
|
|
140
|
+
// a host-childList removal record was author-removed or author-moved
|
|
141
|
+
// (their same-pass add marks touchedThisPass; a placed node is
|
|
142
|
+
// unmarked; a re-homed node has a parent) and leaves the record.
|
|
143
|
+
const rescueValue =
|
|
144
|
+
FRAMEWORK_DETACHED.has(node) &&
|
|
145
|
+
node.parentNode == null &&
|
|
146
|
+
!touchedThisPass.has(node);
|
|
147
|
+
if (!rescueValue) {
|
|
148
|
+
state.authored.splice(i, 1);
|
|
149
|
+
dirty = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (dirty) applySlotAssignments(host);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Drain the host's backstop at a renderer-write window close, PROCESSING the
|
|
158
|
+
* records (renderer output is skipped structurally by processBackstop) so a
|
|
159
|
+
* genuine bypass write coinciding with the commit is not lost. Exported so
|
|
160
|
+
* render-client's render() window can share the exact same drain.
|
|
161
|
+
*
|
|
162
|
+
* @param {Element} host
|
|
163
|
+
*/
|
|
164
|
+
export function drainRendererBackstop(host) {
|
|
165
|
+
const h = /** @type {any} */ (host);
|
|
166
|
+
const state = /** @type {SlotState | undefined} */ (h[SLOT_STATE]);
|
|
167
|
+
if (!(state && state.backstop)) return;
|
|
168
|
+
const records = state.backstop.takeRecords();
|
|
169
|
+
// Only PROCESS when there is a rendered instance: processBackstop skips
|
|
170
|
+
// renderer output via instanceOwns, which needs the instance bookends. On the
|
|
171
|
+
// non-template render path (render() returns a string / array / number) the
|
|
172
|
+
// instance is null and the renderer's own text nodes are direct host
|
|
173
|
+
// children, so processing would fold them into the record and park them (the
|
|
174
|
+
// component would render blank). With no instance to discriminate, discard,
|
|
175
|
+
// matching the pre-processing behavior.
|
|
176
|
+
// Process when an instance exists OR the host has NEVER rendered (the
|
|
177
|
+
// symbol is absent: a pre-first-render bypass write must be folded so the
|
|
178
|
+
// first render's replaceChildren does not silently destroy it); discard
|
|
179
|
+
// only on the EXPLICIT null of the non-template render path, whose text
|
|
180
|
+
// output would otherwise be folded and parked.
|
|
181
|
+
const hasInstanceSym = Symbol.for('webjs.instance') in h;
|
|
182
|
+
if (!hasInstanceSym || h[Symbol.for('webjs.instance')]) {
|
|
183
|
+
processBackstop(host, state, records);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Flip-sensor callback body: a RELEVANT slot=/name= flip re-derives + re-places.
|
|
189
|
+
* Relevant = a `name=` change on one of the host's own light slots, or a `slot=`
|
|
190
|
+
* change on an authored (projected) child. An unrelated `name=` deep in the tree
|
|
191
|
+
* (e.g. an `<input name>`) is ignored, so common markup does not trigger a
|
|
192
|
+
* spurious full re-apply.
|
|
193
|
+
*/
|
|
194
|
+
function processFlip(host, records) {
|
|
195
|
+
const state = /** @type {SlotState | undefined} */ (
|
|
196
|
+
/** @type {any} */ (host)[SLOT_STATE]
|
|
197
|
+
);
|
|
198
|
+
if (!state) return;
|
|
199
|
+
// ONE pass over ALL records first: every explicit slot= change clears that
|
|
200
|
+
// node's self-heal adoption (the author's attribute is now the routing
|
|
201
|
+
// intent), unconditionally. Deleting for a node no longer authored is
|
|
202
|
+
// always safe, and gating on authored membership let a stale adoption
|
|
203
|
+
// outlive a same-task detach and mis-route a later re-append. Only then is
|
|
204
|
+
// relevance decided and the single apply run, so a batch with several
|
|
205
|
+
// relevant flips never leaves a later record's adoption uncleared.
|
|
206
|
+
let relevant = false;
|
|
207
|
+
for (const r of records) {
|
|
208
|
+
if (r.type !== 'attributes') continue;
|
|
209
|
+
const target = /** @type {Element} */ (r.target);
|
|
210
|
+
if (r.attributeName === 'name') {
|
|
211
|
+
if (
|
|
212
|
+
target.tagName === 'SLOT' &&
|
|
213
|
+
target.hasAttribute(LIGHT_SLOT_ATTR) &&
|
|
214
|
+
isOwnSlot(host, target)
|
|
215
|
+
) {
|
|
216
|
+
relevant = true;
|
|
217
|
+
}
|
|
218
|
+
} else if (r.attributeName === 'slot') {
|
|
219
|
+
if (state.adoptedKey) state.adoptedKey.delete(target);
|
|
220
|
+
if (state.authored.indexOf(target) !== -1) relevant = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (relevant) applySlotAssignments(host);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Tear down the sensors, PROCESSING any queued records first (a bare
|
|
228
|
+
* `disconnect()` drops them, which would lose a flip or bypass write captured
|
|
229
|
+
* but not yet delivered when the host disconnects).
|
|
230
|
+
*
|
|
231
|
+
* @param {Element} host
|
|
232
|
+
*/
|
|
233
|
+
export function teardownSlotSensors(host) {
|
|
234
|
+
const state = /** @type {SlotState | undefined} */ (
|
|
235
|
+
/** @type {any} */ (host)[SLOT_STATE]
|
|
236
|
+
);
|
|
237
|
+
if (!state) return;
|
|
238
|
+
if (state.backstop) {
|
|
239
|
+
processBackstop(host, state, state.backstop.takeRecords());
|
|
240
|
+
state.backstop.disconnect();
|
|
241
|
+
state.backstop = undefined;
|
|
242
|
+
}
|
|
243
|
+
if (state.flipSensor) {
|
|
244
|
+
processFlip(host, state.flipSensor.takeRecords());
|
|
245
|
+
state.flipSensor.disconnect();
|
|
246
|
+
state.flipSensor = undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Reconnect sweep: after a host is re-inserted, fold any direct host child that
|
|
252
|
+
* is not already authored, not the park, and not the render root into
|
|
253
|
+
* `authored` (covers a raw bypass write made while the host was disconnected,
|
|
254
|
+
* which no sensor was live to see). Then re-apply.
|
|
255
|
+
*
|
|
256
|
+
* @param {Element} host
|
|
257
|
+
*/
|
|
258
|
+
export function reconnectSweep(host) {
|
|
259
|
+
if (!inBrowser) return;
|
|
260
|
+
const h = /** @type {any} */ (host);
|
|
261
|
+
const state = /** @type {SlotState | undefined} */ (h[SLOT_STATE]);
|
|
262
|
+
if (!state) return;
|
|
263
|
+
const inst = h[Symbol.for('webjs.instance')];
|
|
264
|
+
const rendered = Symbol.for('webjs.instance') in h;
|
|
265
|
+
// Gate on the RECORDED connect branch (adoptSSRAssignments sets
|
|
266
|
+
// state.adopted), never on structural re-detection: a bypass write can
|
|
267
|
+
// itself carry a rendered-looking chunk (slot[data-webjs-light]
|
|
268
|
+
// [data-projection] under plain wrappers) and would spoof a structural
|
|
269
|
+
// check, suppressing the fold for unrelated writes in the same batch.
|
|
270
|
+
// The flag is also free per reconnect where the structural query walked
|
|
271
|
+
// the subtree.
|
|
272
|
+
const adoptedMarkup = !rendered && state.adopted === true;
|
|
273
|
+
let changed = false;
|
|
274
|
+
for (const node of Array.from(host.childNodes)) {
|
|
275
|
+
if (node === h[PARK]) continue;
|
|
276
|
+
if (state.authored.indexOf(node) !== -1) {
|
|
277
|
+
// Authored but physically a direct host child: a bypass MOVE made
|
|
278
|
+
// while disconnected pulled it out of its slot. Re-apply so the
|
|
279
|
+
// placement step repairs it.
|
|
280
|
+
changed = true;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// Skip the renderer's own top-level nodes. With an instance, ownership
|
|
284
|
+
// is checked via the bookends. With an EXPLICIT null instance (the
|
|
285
|
+
// non-template render path sets host[INSTANCE] = null: render() returned
|
|
286
|
+
// a string / number / array) the renderer's text output IS the direct
|
|
287
|
+
// host children, so folding is skipped entirely, like
|
|
288
|
+
// drainRendererBackstop's no-instance guard. A host that has NEVER
|
|
289
|
+
// rendered (the symbol is absent: moved before its deferred first
|
|
290
|
+
// render) has no CLIENT-renderer output, so a disconnected-window
|
|
291
|
+
// bypass write IS folded UNLESS the host carries adopted
|
|
292
|
+
// framework-rendered markup (SSR/hydration before the deferred first
|
|
293
|
+
// render, or a first render that threw): folding THAT subtree would
|
|
294
|
+
// push template wrappers into the record and brick placement on a
|
|
295
|
+
// HierarchyRequestError.
|
|
296
|
+
if (rendered && (!inst || instanceOwns(inst, node))) continue;
|
|
297
|
+
if (!rendered && adoptedMarkup) continue;
|
|
298
|
+
FRAMEWORK_DETACHED.add(node);
|
|
299
|
+
state.authored.push(node);
|
|
300
|
+
changed = true;
|
|
301
|
+
}
|
|
302
|
+
if (changed) applySlotAssignments(host);
|
|
303
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-host slot state: the authored-children capture, the name to node
|
|
3
|
+
* partition, and SSR assignment adoption.
|
|
4
|
+
*
|
|
5
|
+
* Moved verbatim out of the pre-split `slot.js`; see that barrel for the
|
|
6
|
+
* runtime's full contract.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { isOwnSlot } from './assignment.js';
|
|
11
|
+
import { isInsideAuthored } from './interception.js';
|
|
12
|
+
import { manualSlotFor } from './polyfills.js';
|
|
13
|
+
import { FRAMEWORK_DETACHED, LIGHT_SLOT_ATTR, PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_STATE } from './symbols.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lazily create and return the slot state for a host element.
|
|
17
|
+
*
|
|
18
|
+
* @param {Element} host
|
|
19
|
+
* @returns {SlotState}
|
|
20
|
+
*/
|
|
21
|
+
export function ensureSlotState(host) {
|
|
22
|
+
/** @type {any} */
|
|
23
|
+
const h = host;
|
|
24
|
+
let state = h[SLOT_STATE];
|
|
25
|
+
if (!state) {
|
|
26
|
+
state = {
|
|
27
|
+
host,
|
|
28
|
+
authored: [],
|
|
29
|
+
assignedByName: new Map(),
|
|
30
|
+
lastSnapshot: new WeakMap(),
|
|
31
|
+
};
|
|
32
|
+
h[SLOT_STATE] = state;
|
|
33
|
+
}
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True when the host has slot state initialised. */
|
|
38
|
+
export function hasSlotState(host) {
|
|
39
|
+
return Boolean(/** @type {any} */ (host)[SLOT_STATE]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Capture: once per host lifetime
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Move every authored child of `host` into the slot record, partitioning
|
|
48
|
+
* by each child's `slot=""` attribute. After this runs, `host` has no
|
|
49
|
+
* children; the renderer re-inserts them at slot-apply time inside the
|
|
50
|
+
* correct <slot> elements. Runs ONCE per host lifetime (first mount, no
|
|
51
|
+
* SSR); there is no later re-capture, so rendered nodes can never be
|
|
52
|
+
* misclassified as authored children (#1006, closed by construction).
|
|
53
|
+
*
|
|
54
|
+
* @param {Element} host
|
|
55
|
+
*/
|
|
56
|
+
export function captureAuthoredChildren(host) {
|
|
57
|
+
const state = ensureSlotState(host);
|
|
58
|
+
while (host.firstChild) {
|
|
59
|
+
const node = host.firstChild;
|
|
60
|
+
state.authored.push(node);
|
|
61
|
+
// Detached by the framework, awaiting placement at slot-apply time: the
|
|
62
|
+
// prune rule must not treat this parentless window as an author removal.
|
|
63
|
+
FRAMEWORK_DETACHED.add(node);
|
|
64
|
+
host.removeChild(node);
|
|
65
|
+
}
|
|
66
|
+
repartition(state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Rebuild `assignedByName` from `authored`: group every authored node by its
|
|
71
|
+
* current `slot=""` attribute (default = null key). Pure and idempotent, the
|
|
72
|
+
* single place a node's slot assignment is decided. Called after any change
|
|
73
|
+
* to `authored` and at the top of `applySlotAssignments`.
|
|
74
|
+
*
|
|
75
|
+
* @param {SlotState} state
|
|
76
|
+
*/
|
|
77
|
+
export function repartition(state) {
|
|
78
|
+
const byName = state.assignedByName;
|
|
79
|
+
byName.clear();
|
|
80
|
+
for (const node of state.authored) {
|
|
81
|
+
appendToMap(byName, effectiveKeyOf(state, node), node);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The slot key a node is ASSIGNED under: the manual `HTMLSlotElement.assign()`
|
|
87
|
+
* overlay when the node is named in one, else the node's `slot=""` attribute.
|
|
88
|
+
* The ONE key rule, shared by `repartition`, the park step, and the router
|
|
89
|
+
* seam, so a manually-assigned node is never judged by its overlay in one
|
|
90
|
+
* place and its raw attribute in another (which parked an assigned node out
|
|
91
|
+
* of its own slot).
|
|
92
|
+
*
|
|
93
|
+
* @param {SlotState} state
|
|
94
|
+
* @param {Node} node
|
|
95
|
+
* @returns {string | null}
|
|
96
|
+
*/
|
|
97
|
+
export function effectiveKeyOf(state, node) {
|
|
98
|
+
const m = manualSlotFor(state, node);
|
|
99
|
+
if (m) return keyOfName(m.getAttribute('name'));
|
|
100
|
+
// A node a NON-record writer placed inside a NAMED slot (folded by the
|
|
101
|
+
// self-heal resync) keeps the key of the container it was written into;
|
|
102
|
+
// deriving from its (absent or different) slot= attribute would teleport
|
|
103
|
+
// it to the default slot on the next apply. An explicit later slot=
|
|
104
|
+
// change clears the adoption (the flip sensor owns that).
|
|
105
|
+
if (state.adoptedKey) {
|
|
106
|
+
const adopted = state.adoptedKey.get(node);
|
|
107
|
+
if (adopted !== undefined) return adopted;
|
|
108
|
+
}
|
|
109
|
+
return slotNameOf(node);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* After SSR + hydration, projected children already live inside their
|
|
114
|
+
* <slot data-webjs-light> elements. Walk the host's render tree and
|
|
115
|
+
* record those existing assignments in the record without moving DOM.
|
|
116
|
+
* The capture-once counterpart for the hydration path.
|
|
117
|
+
*
|
|
118
|
+
* @param {Element} host
|
|
119
|
+
*/
|
|
120
|
+
export function adoptSSRAssignments(host) {
|
|
121
|
+
const state = ensureSlotState(host);
|
|
122
|
+
// Record the connect branch: reconnectSweep's pre-render fold gate keys on
|
|
123
|
+
// this flag (an adopted host's children ARE rendered markup; a captured
|
|
124
|
+
// host's are only bypass writes).
|
|
125
|
+
state.adopted = true;
|
|
126
|
+
/** @type {Set<string|null>} first-wins per name across the host's own slots */
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const slots = host.querySelectorAll(`slot[${LIGHT_SLOT_ATTR}]`);
|
|
129
|
+
for (const slot of slots) {
|
|
130
|
+
/** @type {HTMLSlotElement} */
|
|
131
|
+
const s = /** @type {any} */ (slot);
|
|
132
|
+
// Only the host's OWN slots. A nested component's slot (another custom
|
|
133
|
+
// element sits between it and the host) belongs to THAT component and is
|
|
134
|
+
// adopted from its own record. Without this filter, a nested actual slot
|
|
135
|
+
// that precedes the outer host's same-named slot in document order wins
|
|
136
|
+
// the first-wins `has(name)` check below, so the outer record adopts the
|
|
137
|
+
// inner component's children and the outer's first apply physically steals
|
|
138
|
+
// them. `applySlotAssignments` and the router both filter; this path must
|
|
139
|
+
// too.
|
|
140
|
+
if (!isOwnSlot(host, s)) continue;
|
|
141
|
+
if (s.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL) continue;
|
|
142
|
+
// The authored-content invariant applies at adopt time too: a slot
|
|
143
|
+
// sitting inside ALREADY-ADOPTED children (an SSR'd forwarded slot
|
|
144
|
+
// serialized as content of an earlier own slot) is content, not an own
|
|
145
|
+
// slot; adopting it would first-wins-collide with a later legitimate
|
|
146
|
+
// same-named own slot and destroy that slot's children. Document order
|
|
147
|
+
// guarantees a container slot precedes its descendants, so the check
|
|
148
|
+
// sees the container's children in `authored` by the time a nested slot
|
|
149
|
+
// is tested.
|
|
150
|
+
if (isInsideAuthored(state, host, s)) continue;
|
|
151
|
+
const name = keyOfName(s.getAttribute('name'));
|
|
152
|
+
if (!seen.has(name)) {
|
|
153
|
+
seen.add(name);
|
|
154
|
+
const children = Array.from(s.childNodes);
|
|
155
|
+
// The SSR'd projected children retain their own `slot=` attribute, so
|
|
156
|
+
// pushing them into `authored` and re-deriving reproduces the same
|
|
157
|
+
// per-name grouping without moving any DOM (no flash). Note: `authored`
|
|
158
|
+
// is rebuilt in slot-document order, not the original interleaved
|
|
159
|
+
// host-child order, which SSR did not preserve. Per-name grouping (all
|
|
160
|
+
// that placement uses) is exact; the only observable difference from a
|
|
161
|
+
// fresh mount is the cross-name ordering a post-hydration `insertBefore`
|
|
162
|
+
// with a ref in a DIFFERENT named slot would resolve against.
|
|
163
|
+
// A child whose OWN attribute keys elsewhere (a snapshot-restored
|
|
164
|
+
// adoption or manual assignment, provenance the HTML cannot carry) is
|
|
165
|
+
// re-adopted under the container's key, so the first client render
|
|
166
|
+
// never relocates a node out of the slot the restored markup showed it
|
|
167
|
+
// in.
|
|
168
|
+
for (const child of children) {
|
|
169
|
+
state.authored.push(child);
|
|
170
|
+
// No prune-exemption mark here: when createInstance detaches the old
|
|
171
|
+
// SSR subtree, these children still sit in the OLD slot, which this
|
|
172
|
+
// adopt just recorded in lastSnapshot, so the prune gate keeps them
|
|
173
|
+
// through the detach window. Skipping the mark also means an author
|
|
174
|
+
// removing an adopted child BEFORE the first apply (child.remove()
|
|
175
|
+
// in another component's boot hook) is honoured instead of the node
|
|
176
|
+
// being resurrected by the resync.
|
|
177
|
+
if (slotNameOf(child) !== name) {
|
|
178
|
+
if (!state.adoptedKey) state.adoptedKey = new WeakMap();
|
|
179
|
+
state.adoptedKey.set(child, name);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
state.lastSnapshot.set(s, children.slice());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// A serialized snapshot (back/forward restore) also carries the PARK: an
|
|
186
|
+
// authored child whose slot name matched no rendered slot at snapshot time
|
|
187
|
+
// sits inside <wj-slot-park>. Sweep its children into the record and drop
|
|
188
|
+
// the serialized park element itself (a fresh park is created on demand),
|
|
189
|
+
// so a parked node survives the restore and a later render that DOES emit
|
|
190
|
+
// its slot pulls it back out.
|
|
191
|
+
for (const oldPark of host.querySelectorAll('wj-slot-park')) {
|
|
192
|
+
if (!isOwnSlot(host, oldPark)) continue;
|
|
193
|
+
for (const child of Array.from(oldPark.childNodes)) {
|
|
194
|
+
// The children keep the DETACHED oldPark as parent until placement;
|
|
195
|
+
// the FRAMEWORK_DETACHED mark is what shields them from the prune
|
|
196
|
+
// rule across that window.
|
|
197
|
+
FRAMEWORK_DETACHED.add(child);
|
|
198
|
+
state.authored.push(child);
|
|
199
|
+
}
|
|
200
|
+
oldPark.remove();
|
|
201
|
+
}
|
|
202
|
+
repartition(state);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalise a slot name to the record key, applied at EVERY name read
|
|
207
|
+
* (capture, adopt, application, rescue, and the public API) so the record
|
|
208
|
+
* key is uniform end to end. The default slot is stored under `null`;
|
|
209
|
+
* `''` and `'default'` are aliases for it. Consequence: `default` is a
|
|
210
|
+
* RESERVED slot name (a literal `name="default"` slot addresses the
|
|
211
|
+
* default slot); the SSR substitution applies the same rule.
|
|
212
|
+
*
|
|
213
|
+
* @param {string | null | undefined} name
|
|
214
|
+
* @returns {string | null}
|
|
215
|
+
*/
|
|
216
|
+
export function keyOfName(name) {
|
|
217
|
+
return name == null || name === '' || name === 'default' ? null : name;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Read the slot="" attribute on an element child. Text and comment nodes
|
|
222
|
+
* always route to the default slot (key = null).
|
|
223
|
+
*
|
|
224
|
+
* @param {Node} node
|
|
225
|
+
* @returns {string | null}
|
|
226
|
+
*/
|
|
227
|
+
function slotNameOf(node) {
|
|
228
|
+
if (node.nodeType !== 1) return null;
|
|
229
|
+
const el = /** @type {Element} */ (node);
|
|
230
|
+
return keyOfName(el.getAttribute('slot'));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Append a value to a Map<K, V[]>, creating the array on first hit. */
|
|
234
|
+
function appendToMap(map, key, value) {
|
|
235
|
+
let arr = map.get(key);
|
|
236
|
+
if (!arr) {
|
|
237
|
+
arr = [];
|
|
238
|
+
map.set(key, arr);
|
|
239
|
+
}
|
|
240
|
+
arr.push(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Router coordination seam
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol keys, attribute names and shared constants for the light-DOM
|
|
3
|
+
* slot runtime. The leaf of the directory: it imports nothing.
|
|
4
|
+
*
|
|
5
|
+
* Moved verbatim out of the pre-split `slot.js`; see that barrel for the
|
|
6
|
+
* runtime's full contract.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Symbol-keyed slot state stored on each light-DOM WebComponent host.
|
|
13
|
+
* Lazily initialised by ensureSlotState(host).
|
|
14
|
+
*/
|
|
15
|
+
export const SLOT_STATE = Symbol('webjs.slot.state');
|
|
16
|
+
|
|
17
|
+
/** Marker attribute that opts a <slot> element into framework projection. */
|
|
18
|
+
export const LIGHT_SLOT_ATTR = 'data-webjs-light';
|
|
19
|
+
|
|
20
|
+
/** Records whether a slot is showing real assignment or fallback. */
|
|
21
|
+
export const PROJECTION_ATTR = 'data-projection';
|
|
22
|
+
|
|
23
|
+
export const PROJECTION_ACTUAL = 'actual';
|
|
24
|
+
export const PROJECTION_FALLBACK = 'fallback';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Symbol-keyed property on a slot element that holds a DocumentFragment
|
|
28
|
+
* containing the slot's fallback content (cloned from the compiled template
|
|
29
|
+
* by render-client.js at slot-part bind time). The apply step swaps these
|
|
30
|
+
* nodes into and out of the slot as the projection state toggles between
|
|
31
|
+
* "actual" and "fallback".
|
|
32
|
+
*/
|
|
33
|
+
export const SLOT_FALLBACK_FRAG = Symbol('webjs.slot.fallbackFrag');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The host whose TEMPLATE produced this `<slot>` (the render container the
|
|
37
|
+
* renderer cloned the template into). Authoritative over the structural
|
|
38
|
+
* `isOwnSlot` walk: a slot a template FORWARDS into a nested component
|
|
39
|
+
* (`html\`<inner><slot></slot></inner>\``) sits physically inside that child
|
|
40
|
+
* but is owned by the OUTER host, which the structural walk (a custom element
|
|
41
|
+
* sits between them) gets wrong. Stamped by the renderer at bind time
|
|
42
|
+
* (render-client) and resolved from the SSR `data-wj-slot-owner` attribute on
|
|
43
|
+
* hydration so client-only mount and hydration share one mechanism.
|
|
44
|
+
*/
|
|
45
|
+
export const SLOT_OWNER = Symbol('webjs.slot.templateOwner');
|
|
46
|
+
|
|
47
|
+
/** The SSR carrier for SLOT_OWNER (a symbol cannot cross the HTML boundary). */
|
|
48
|
+
export const SLOT_OWNER_ATTR = 'data-wj-slot-owner';
|
|
49
|
+
|
|
50
|
+
/** Maximum recursion depth for assignedNodes({flatten: true}); guards cycles. */
|
|
51
|
+
export const FLATTEN_MAX_DEPTH = 64;
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Saved native references and prototype polyfills
|
|
55
|
+
//
|
|
56
|
+
// Module-load tries to install polyfills immediately. In a pure Node
|
|
57
|
+
// process without a DOM library, HTMLSlotElement is undefined and the
|
|
58
|
+
// install is a no-op. Tests that set up linkedom AFTER module load can
|
|
59
|
+
// call installSlotPolyfills() explicitly to re-attempt the install.
|
|
60
|
+
// Subsequent calls are idempotent; native references are captured only
|
|
61
|
+
// on the first successful install.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set on a host WHILE the renderer is committing into it. The patched host
|
|
66
|
+
* methods check it and delegate to the saved native, so a renderer commit is
|
|
67
|
+
* never mistaken for authored content. This is the one discriminator between
|
|
68
|
+
* renderer writes and author writes: a synchronous framework-write window, set
|
|
69
|
+
* structurally (an own symbol), never inferred from comment markers.
|
|
70
|
+
*/
|
|
71
|
+
export const RENDERING = Symbol('webjs.slot.rendering');
|
|
72
|
+
|
|
73
|
+
/** Marks a host whose mutating methods have been patched (install once). */
|
|
74
|
+
export const INTERCEPTED = Symbol('webjs.slot.intercepted');
|
|
75
|
+
|
|
76
|
+
/** The per-host hidden holding element for authored nodes whose slot name
|
|
77
|
+
* matches no rendered slot (native keeps them connected but unrendered). */
|
|
78
|
+
export const PARK = Symbol('webjs.slot.park');
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Authored nodes the framework detached ON PURPOSE (capture, teardown rescue),
|
|
82
|
+
* so the prune rule does not treat them as author-removed while they sit
|
|
83
|
+
* parentless waiting to be (re)placed. Cleared once a node is placed.
|
|
84
|
+
*/
|
|
85
|
+
export const FRAMEWORK_DETACHED = new WeakSet();
|