@webjsdev/core 0.7.50 → 0.7.51

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.
Files changed (57) hide show
  1. package/dist/webjs-core-browser.js +5 -5
  2. package/dist/webjs-core-browser.js.map +3 -3
  3. package/dist/webjs-core-testing.js +20 -20
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +7 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +1 -1
  8. package/index.d.ts +1 -1
  9. package/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/component/lifecycle.js +1481 -0
  12. package/src/component/reactive.js +171 -0
  13. package/src/component/server-element.js +248 -0
  14. package/src/component.js +2 -1896
  15. package/src/render-client/parts.js +2007 -0
  16. package/src/render-client/reconciler.js +388 -0
  17. package/src/render-client/template-compiler.js +525 -0
  18. package/src/render-client.js +1 -2938
  19. package/src/render-server/attrs.js +160 -0
  20. package/src/render-server/dsd.js +445 -0
  21. package/src/render-server/html-scan.js +402 -0
  22. package/src/render-server/slots.js +213 -0
  23. package/src/render-server/stream.js +163 -0
  24. package/src/render-server/template-renderer.js +923 -0
  25. package/src/render-server/text.js +163 -0
  26. package/src/render-server.js +6 -2405
  27. package/src/router-client/anchors.js +44 -0
  28. package/src/router-client/boundaries.js +221 -0
  29. package/src/router-client/constants.js +187 -0
  30. package/src/router-client/diagnostics.js +377 -0
  31. package/src/router-client/dom-differ.js +709 -0
  32. package/src/router-client/dom-parse.js +150 -0
  33. package/src/router-client/events.js +139 -0
  34. package/src/router-client/fetch-apply.js +387 -0
  35. package/src/router-client/form-encoder.js +135 -0
  36. package/src/router-client/frames.js +329 -0
  37. package/src/router-client/head-merge.js +344 -0
  38. package/src/router-client/nav-error.js +105 -0
  39. package/src/router-client/navigator.js +781 -0
  40. package/src/router-client/prefetch.js +824 -0
  41. package/src/router-client/scroll.js +259 -0
  42. package/src/router-client/snapshot-cache.js +97 -0
  43. package/src/router-client/state.js +92 -0
  44. package/src/router-client/stream.js +206 -0
  45. package/src/router-client/swap.js +451 -0
  46. package/src/router-client/upgrade.js +261 -0
  47. package/src/router-client/view-transition.js +223 -0
  48. package/src/router-client.d.ts +10 -1
  49. package/src/router-client.js +75 -5345
  50. package/src/slot/assignment.js +304 -0
  51. package/src/slot/interception.js +558 -0
  52. package/src/slot/polyfills.js +365 -0
  53. package/src/slot/project.js +432 -0
  54. package/src/slot/sensors.js +303 -0
  55. package/src/slot/state.js +245 -0
  56. package/src/slot/symbols.js +85 -0
  57. package/src/slot.js +7 -2199
@@ -0,0 +1,558 @@
1
+ /**
2
+ * Interception of the native insertion API on a slotted host, so an
3
+ * authored write lands in the authored list rather than the rendered tree.
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 { inBrowser } from './polyfills.js';
12
+ import { applySlotAssignments } from './project.js';
13
+ import { INNER_HTML_DESC, N_append, N_appendChild, N_insertBefore, N_prepend, N_removeChild, N_replaceChild, N_replaceChildren, TEXT_CONTENT_DESC, captureNatives, withRendererWrites } from './sensors.js';
14
+ import { ensureSlotState, repartition } from './state.js';
15
+ import { FRAMEWORK_DETACHED, INTERCEPTED, LIGHT_SLOT_ATTR, PARK, RENDERING, SLOT_STATE } from './symbols.js';
16
+
17
+ /**
18
+ * True when `node` is renderer-owned: it is one of the instance's bookend
19
+ * markers (`wjm-s` / `wjm-e`) or sits between them. Object-identity check
20
+ * against the marker refs the instance holds, never comment-text sniffing.
21
+ */
22
+ export function instanceOwns(inst, node) {
23
+ if (!inst || !inst.startNode || !inst.endNode) return false;
24
+ if (node === inst.startNode || node === inst.endNode) return true;
25
+ for (let n = inst.startNode.nextSibling; n && n !== inst.endNode; n = n.nextSibling) {
26
+ if (n === node) return true;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ /** The host's hidden park element, created + attached lazily. */
32
+ export function parkFor(host) {
33
+ const h = /** @type {any} */ (host);
34
+ let park = h[PARK];
35
+ if (!park || !park.isConnected) {
36
+ if (!park) {
37
+ park = host.ownerDocument.createElement('wj-slot-park');
38
+ park.setAttribute('hidden', '');
39
+ park.style.display = 'none';
40
+ h[PARK] = park;
41
+ }
42
+ // Attach inside the host, after the rendered output, via the native path
43
+ // (never through the patched appendChild).
44
+ withRendererWrites(host, () => N_appendChild.call(host, park));
45
+ }
46
+ return park;
47
+ }
48
+
49
+ /**
50
+ * True for a REAL platform Node from any realm: a same-realm Node passes
51
+ * instanceof; a cross-realm Node passes via its own realm's constructor.
52
+ * KNOWN EDGE: a node from a DISCARDED iframe realm (defaultView null) and
53
+ * any cross-realm DOCUMENT (ownerDocument is null on a Document by spec)
54
+ * fail both arms where native would adopt or HierarchyRequestError:
55
+ * appendChild-shaped calls throw TypeError, and the variadic
56
+ * string-accepting calls stringify to text. Both outcomes are strictly
57
+ * safer than admitting an unverifiable object into the record.
58
+ *
59
+ * @param {any} n
60
+ * @returns {boolean}
61
+ */
62
+ function isRealmNode(n) {
63
+ return (
64
+ n instanceof Node ||
65
+ Boolean(
66
+ n &&
67
+ typeof n === 'object' &&
68
+ /** @type {any} */ (n).ownerDocument &&
69
+ /** @type {any} */ (n).ownerDocument.defaultView &&
70
+ n instanceof /** @type {any} */ (n).ownerDocument.defaultView.Node,
71
+ )
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Expand one argument of a DOM insertion call into a flat node list. A
77
+ * DocumentFragment is DRAINED (native contract: the fragment ends empty) and
78
+ * its children returned; a string becomes a Text node when `allowString`
79
+ * (append / prepend / replaceChildren accept strings, appendChild does not).
80
+ *
81
+ * @param {Element} host
82
+ * @param {any} arg
83
+ * @param {boolean} allowString
84
+ * @returns {Node[]}
85
+ */
86
+ function expandArg(host, arg, allowString) {
87
+ // WebIDL (Node or DOMString) coercion: append/prepend/replaceChildren
88
+ // stringify ANY argument that is not a real platform Node (a number, null,
89
+ // an object, even a duck-typed fake with a numeric nodeType); native
90
+ // host.append(42) appends the text "42" and host.append({}) appends
91
+ // "[object Object]".
92
+ if (allowString && !isRealmNode(arg)) {
93
+ // A template literal performs exactly ES ToString (ToPrimitive with hint
94
+ // "string": toString before valueOf, and a Symbol THROWS TypeError),
95
+ // matching WebIDL DOMString conversion; '' + x would use hint "default"
96
+ // (valueOf first) and diverge for objects overriding both.
97
+ return [host.ownerDocument.createTextNode(`${/** @type {any} */ (arg)}`)];
98
+ }
99
+ // Non-string path (appendChild / insertBefore / replaceChild): reject any
100
+ // non-platform-node BEFORE the fragment branch, or a duck-typed
101
+ // {nodeType: 11} fake would bypass guardInsertable entirely.
102
+ if (!isRealmNode(arg)) {
103
+ throw new TypeError('Failed to execute insertion on the host: parameter is not of type Node.');
104
+ }
105
+ if (arg && arg.nodeType === 11) {
106
+ const kids = Array.from(arg.childNodes);
107
+ // Guard BEFORE draining: a cycle error must leave the fragment intact
108
+ // (native throws with the fragment untouched).
109
+ guardInsertable(host, kids);
110
+ for (const k of kids) N_removeChild.call(arg, k);
111
+ return kids;
112
+ }
113
+ guardInsertable(host, [/** @type {Node} */ (arg)]);
114
+ return [/** @type {Node} */ (arg)];
115
+ }
116
+
117
+ /**
118
+ * Throw `HierarchyRequestError` (native parity) if any node would create a
119
+ * cycle: the host itself, or an ancestor of the host. Checked BEFORE the record
120
+ * is mutated, so a bad insert leaves `authored` untouched, like native.
121
+ *
122
+ * @param {Element} host
123
+ * @param {Node[]} nodes
124
+ */
125
+ function guardInsertable(host, nodes) {
126
+ guardCycle(host, nodes);
127
+ for (const n of nodes) {
128
+ // Node-TYPE validity is DOM pre-insert step 4, AFTER the ref's
129
+ // NotFoundError (step 3): callers run this via expandArg once the ref
130
+ // checks passed. A non-insertable type (an Attr, a Document, a doctype)
131
+ // is a HierarchyRequestError. Realm validity (TypeError for a
132
+ // duck-typed fake, which would otherwise mutate the record and wedge
133
+ // every later apply) is checked by callers BEFORE any DOM step, per
134
+ // WebIDL argument conversion.
135
+ if (!isRealmNode(n)) {
136
+ throw new TypeError('Failed to execute insertion on the host: parameter is not of type Node.');
137
+ }
138
+ const t = n.nodeType;
139
+ if (t !== 1 && t !== 3 && t !== 4 && t !== 7 && t !== 8 && t !== 11) {
140
+ throw new DOMException(
141
+ 'Failed to execute insertion on the host: the node type may not be inserted here.',
142
+ 'HierarchyRequestError',
143
+ );
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * DOM pre-insert step 2, the cycle check: run after ALL WebIDL argument
150
+ * conversions and before the ref's NotFoundError (step 3).
151
+ *
152
+ * @param {Element} host
153
+ * @param {Node[]} nodes
154
+ */
155
+ function guardCycle(host, nodes) {
156
+ for (const n of nodes) {
157
+ if (n === host || (n.nodeType === 1 && /** @type {Element} */ (n).contains(host))) {
158
+ throw new DOMException(
159
+ 'Failed to execute insertion on the host: the new child contains the parent.',
160
+ 'HierarchyRequestError',
161
+ );
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Splice `nodes` into `authored` before `ref` (a node already in authored) or
168
+ * at the end when `ref` is null. Nodes already present are removed from their
169
+ * current position first (native move semantics for a re-inserted child).
170
+ *
171
+ * @param {SlotState} state
172
+ * @param {Node[]} nodes
173
+ * @param {Node | null} ref
174
+ */
175
+ function authoredSplice(state, nodes, ref) {
176
+ const a = state.authored;
177
+ for (const n of nodes) {
178
+ const i = a.indexOf(n);
179
+ if (i !== -1) a.splice(i, 1);
180
+ }
181
+ let at = ref == null ? a.length : a.indexOf(ref);
182
+ if (at === -1) at = a.length;
183
+ a.splice(at, 0, ...nodes);
184
+ }
185
+
186
+ /**
187
+ * True when `el` is, or sits inside, an AUTHORED node of this host: such an
188
+ * element is CONTENT (a chunk the author wrote or moved in), never one of
189
+ * the host's own rendered slots, no matter what attributes it carries.
190
+ *
191
+ * Exported for the router's own-slot collection, which must apply the SAME
192
+ * invariant when picking reprojection targets.
193
+ *
194
+ * @param {Element} host
195
+ * @param {Element} el
196
+ * @returns {boolean}
197
+ */
198
+ export function isAuthoredContentSlot(host, el) {
199
+ const state = /** @type {SlotState | undefined} */ (
200
+ /** @type {any} */ (host)[SLOT_STATE]
201
+ );
202
+ if (!state) return false;
203
+ return isInsideAuthored(state, host, el);
204
+ }
205
+
206
+ /** @param {SlotState} state @param {Element} host @param {Element} el */
207
+ export function isInsideAuthored(state, host, el) {
208
+ for (let p = /** @type {Node | null} */ (el); p && p !== host; p = p.parentNode) {
209
+ if (state.authored.indexOf(p) !== -1) return true;
210
+ }
211
+ return false;
212
+ }
213
+
214
+ /**
215
+ * True when an authored node is still VIRTUALLY a child of the host: it
216
+ * physically sits in one of the host's own slots, the park, or the host
217
+ * itself, or the framework holds it detached as a record value (a rescued
218
+ * closed-slot child). A node the author moved elsewhere out-of-band (into a
219
+ * fragment, another element) is NOT a child anymore even while the record
220
+ * still lists it, and native removeChild / replaceChild / insertBefore-ref
221
+ * answer that with NotFoundError.
222
+ *
223
+ * @param {Element} host
224
+ * @param {Node} node
225
+ * @returns {boolean}
226
+ */
227
+ export function isVirtualChild(host, node) {
228
+ if (FRAMEWORK_DETACHED.has(node)) return true;
229
+ const p = node.parentNode;
230
+ if (p == null) return false;
231
+ if (p === host) return true;
232
+ if (p === /** @type {any} */ (host)[PARK]) return true;
233
+ const state = /** @type {SlotState | undefined} */ (
234
+ /** @type {any} */ (host)[SLOT_STATE]
235
+ );
236
+ // lastSnapshot membership proves ownership on its own (a detached forwarded
237
+ // slot's structural isOwnSlot would wrongly veto it); a contained slot still
238
+ // needs isOwnSlot so a foreign torn-down slot is not claimed.
239
+ return (
240
+ p.nodeType === 1 &&
241
+ /** @type {Element} */ (p).tagName === 'SLOT' &&
242
+ /** @type {Element} */ (p).hasAttribute(LIGHT_SLOT_ATTR) &&
243
+ ((state && state.lastSnapshot.has(/** @type {HTMLSlotElement} */ (p))) ||
244
+ (host.contains(p) && isOwnSlot(host, /** @type {Element} */ (p))))
245
+ );
246
+ }
247
+
248
+ /** The empty set handed to resync when no record op is pending. */
249
+ export const EMPTY_NODE_SET = new Set();
250
+
251
+ /**
252
+ * WebIDL for the variadic insertion methods converts EVERY argument before
253
+ * any node is moved: phase 1 validates each argument (a Symbol or fake
254
+ * throws with every fragment still intact), phase 2 drains fragments and
255
+ * builds the flat node list.
256
+ *
257
+ * @param {Element} host
258
+ * @param {any[]} args
259
+ * @returns {Node[]}
260
+ */
261
+ function convertVariadicArgs(host, args) {
262
+ // Phase 1 is PURE WebIDL conversion, left to right, with NO DOM validity
263
+ // interleaved: native converts every argument (running ToString side
264
+ // effects, throwing on a Symbol) before any operation-body step, so a
265
+ // later argument's conversion TypeError must preempt an earlier
266
+ // argument's HierarchyRequestError.
267
+ /** @type {Array<{ text?: string, frag?: DocumentFragment, node?: Node }>} */
268
+ const converted = [];
269
+ for (const arg of args) {
270
+ if (!isRealmNode(arg)) {
271
+ converted.push({ text: `${arg}` }); // Symbol throws here, nothing drained
272
+ } else if (arg.nodeType === 11) {
273
+ converted.push({ frag: /** @type {DocumentFragment} */ (arg) });
274
+ } else {
275
+ converted.push({ node: arg });
276
+ }
277
+ }
278
+ // Phase 2: DOM validity for every converted argument, THEN the build.
279
+ for (const c of converted) {
280
+ if (c.frag) guardCycle(host, Array.from(c.frag.childNodes));
281
+ else if (c.node) guardInsertable(host, [c.node]);
282
+ }
283
+ // The build mirrors native's "converting nodes into a node": append each
284
+ // converted item to a SCRATCH fragment in argument order. Appending a
285
+ // plain-node argument detaches it from wherever it currently sits,
286
+ // INCLUDING a later fragment argument, so append(a, fragContaining(a))
287
+ // nets [a, ...fragRest] exactly like native; a repeated Node argument is
288
+ // physically moved, netting keep-LAST order; and the record can never
289
+ // receive a duplicate entry (scratch children are unique by construction).
290
+ // ACCEPTED DIVERGENCE, documented here as its canonical spot: detaching a
291
+ // connected argument fires its [CEReactions] callbacks MID-call, where
292
+ // native defers them to the end of the outer variadic operation (a
293
+ // userland polyfill has no reactions queue). The net DOM converges
294
+ // through the latch + record ops; the one observable divergence is a
295
+ // same-host-mutating disconnect callback racing replaceChildren's
296
+ // wholesale displacement. (Connected variadic arguments also fire their
297
+ // disconnect reactions on detach, matching native's own per-node moves.)
298
+ const scratch = host.ownerDocument.createDocumentFragment();
299
+ for (const c of converted) {
300
+ if (c.text !== undefined) {
301
+ N_appendChild.call(scratch, host.ownerDocument.createTextNode(c.text));
302
+ } else if (c.frag) {
303
+ const kids = Array.from(c.frag.childNodes);
304
+ guardInsertable(host, kids);
305
+ for (const k of kids) N_appendChild.call(scratch, k);
306
+ } else if (c.node) {
307
+ // DEFENSE-IN-DEPTH early throw (the LOAD-BEARING check is the final
308
+ // assembled guard below): a reaction fired by an earlier argument's
309
+ // detach can reparent the HOST into this one; catching it here just
310
+ // surfaces the error before this arg enters the scratch.
311
+ guardCycle(host, [c.node]);
312
+ N_appendChild.call(scratch, c.node);
313
+ }
314
+ }
315
+ const nodes = Array.from(scratch.childNodes);
316
+ // The LOAD-BEARING assembled-validity check (DOM pre-insert runs validity
317
+ // AFTER the conversion step): whatever mid-conversion reactions did,
318
+ // including a LATER argument's reaction poisoning an EARLIER scratch
319
+ // child, the returned list never contains a host-containing node, so the
320
+ // CONVERSION window cannot feed one into the record. (The record-to-
321
+ // placement window is separately shielded by the placement-time skip in
322
+ // applyActualAssignment.)
323
+ guardCycle(host, nodes);
324
+ for (const n of nodes) N_removeChild.call(scratch, n);
325
+ return nodes;
326
+ }
327
+
328
+ /**
329
+ * Commit an authored mutation: record it, re-derive, re-place. `touched`
330
+ * lists the nodes this op inserted, moved, or removed, so the resync step
331
+ * honours the record's position for exactly those nodes (an expressed move)
332
+ * while adopting physical order for everything else.
333
+ *
334
+ * @param {Element} host
335
+ * @param {SlotState} state
336
+ * @param {Node[]} [touched]
337
+ */
338
+ function commitAuthored(host, state, touched) {
339
+ // UNION under the latch: a nested record op cannot run its own pass, so
340
+ // its touched set must survive until the outer loop's next iteration; a
341
+ // second nested op must not clobber the first one's order authority.
342
+ if (state.applying && state.pendingRecordNodes) {
343
+ for (const n of touched || []) state.pendingRecordNodes.add(n);
344
+ } else {
345
+ state.pendingRecordNodes = new Set(touched || []);
346
+ }
347
+ // An author record op on a node ends its self-heal adoption: once the
348
+ // author takes over, the node routes by its own attribute again (also
349
+ // covers an attribute change made while the sensors were down).
350
+ if (state.adoptedKey && touched) {
351
+ for (const n of touched) state.adoptedKey.delete(n);
352
+ }
353
+ repartition(state);
354
+ applySlotAssignments(host);
355
+ }
356
+
357
+ /**
358
+ * Install the per-instance interception on a LIGHT-DOM host so native DOM
359
+ * writes drive the slot record. Own data properties / accessors shadow the
360
+ * prototype methods; installed once, never removed (so a mutation while the
361
+ * host is disconnected still updates the record). No-op in shadow DOM.
362
+ *
363
+ * @param {Element} host
364
+ */
365
+ export function installSlotInterception(host) {
366
+ if (!inBrowser) return;
367
+ const h = /** @type {any} */ (host);
368
+ if (h[INTERCEPTED]) return;
369
+ captureNatives();
370
+ h[INTERCEPTED] = true;
371
+ const state = ensureSlotState(host);
372
+
373
+ h.appendChild = function (node) {
374
+ if (h[RENDERING]) return N_appendChild.call(this, node);
375
+ const nodes = expandArg(host, node, false);
376
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
377
+ authoredSplice(state, nodes, null);
378
+ commitAuthored(host, state, nodes);
379
+ return node;
380
+ };
381
+
382
+ h.insertBefore = function (node, ref) {
383
+ if (h[RENDERING]) return N_insertBefore.call(this, node, ref);
384
+ // WebIDL converts arguments LEFT TO RIGHT and DOM pre-insert validity
385
+ // runs the cycle (HierarchyRequestError) step before the ref
386
+ // (NotFoundError) step: validate parameter 1's type and insertability
387
+ // FIRST, without draining a fragment (the drain happens in expandArg
388
+ // only after every validity check passed, so an error leaves the
389
+ // fragment intact like native).
390
+ if (!isRealmNode(node)) {
391
+ throw new TypeError('Failed to execute insertBefore on the host: parameter 1 is not of type Node.');
392
+ }
393
+ if (ref != null && !isRealmNode(ref)) {
394
+ throw new TypeError('Failed to execute insertBefore on the host: parameter 2 is not of type Node.');
395
+ }
396
+ // Both conversions done; now DOM validity in step order (cycle, then the
397
+ // ref's NotFoundError below, then node-type inside expandArg).
398
+ guardCycle(host, node.nodeType === 11 ? Array.from(node.childNodes) : [node]);
399
+ // A non-null ref MUST be an assigned child (native throws NotFoundError
400
+ // otherwise), checked before the self-ref no-op so insertBefore(x, x) on
401
+ // a NON-child still throws like native.
402
+ if (
403
+ ref != null &&
404
+ (state.authored.indexOf(ref) === -1 || !isVirtualChild(host, ref))
405
+ ) {
406
+ throw new DOMException(
407
+ 'insertBefore: reference node is not an assigned child of this host',
408
+ 'NotFoundError',
409
+ );
410
+ }
411
+ // insertBefore(n, n) with n an existing child is a native no-op.
412
+ if (node === ref) return node;
413
+ const nodes = expandArg(host, node, false);
414
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
415
+ authoredSplice(state, nodes, ref || null);
416
+ commitAuthored(host, state, nodes);
417
+ return node;
418
+ };
419
+
420
+ h.removeChild = function (node) {
421
+ if (h[RENDERING]) return N_removeChild.call(this, node);
422
+ const i = state.authored.indexOf(node);
423
+ if (i === -1) return N_removeChild.call(this, node);
424
+ // Record-listed but physically moved elsewhere out-of-band: native
425
+ // answers "not a child" (the record heals on the next apply).
426
+ if (!isVirtualChild(host, node)) {
427
+ throw new DOMException(
428
+ 'removeChild: the node is not an assigned child of this host',
429
+ 'NotFoundError',
430
+ );
431
+ }
432
+ state.authored.splice(i, 1);
433
+ commitAuthored(host, state, [node]);
434
+ return node;
435
+ };
436
+
437
+ h.replaceChild = function (newNode, oldNode) {
438
+ if (h[RENDERING]) return N_replaceChild.call(this, newNode, oldNode);
439
+ const i = state.authored.indexOf(oldNode);
440
+ if (i === -1) return N_replaceChild.call(this, newNode, oldNode);
441
+ // WebIDL converts parameters left to right and the cycle check precedes
442
+ // the NotFound check: validate parameter 1 first, without draining.
443
+ if (!isRealmNode(newNode)) {
444
+ throw new TypeError('Failed to execute replaceChild on the host: parameter 1 is not of type Node.');
445
+ }
446
+ guardCycle(host, newNode.nodeType === 11 ? Array.from(newNode.childNodes) : [newNode]);
447
+ if (!isVirtualChild(host, oldNode)) {
448
+ throw new DOMException(
449
+ 'replaceChild: the node to be replaced is not an assigned child of this host',
450
+ 'NotFoundError',
451
+ );
452
+ }
453
+ if (newNode === oldNode) return oldNode; // native no-op
454
+ const nodes = expandArg(host, newNode, false); // guards cycle before draining
455
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
456
+ // Remove any incoming node already authored (so it MOVES to the new slot),
457
+ // but never oldNode itself: it is the replacement target, so skipping it
458
+ // keeps `at` valid even for the pathological replaceChild(fragmentWithOld,
459
+ // old) input, avoiding a splice(-1) that would corrupt an unrelated sibling.
460
+ for (const n of nodes) {
461
+ if (n === oldNode) continue;
462
+ const j = state.authored.indexOf(n);
463
+ if (j !== -1) state.authored.splice(j, 1);
464
+ }
465
+ const at = state.authored.indexOf(oldNode);
466
+ state.authored.splice(at, 1, ...nodes);
467
+ commitAuthored(host, state, [...nodes, oldNode]);
468
+ return oldNode;
469
+ };
470
+
471
+ h.append = function (...args) {
472
+ if (h[RENDERING]) return N_append.apply(this, args);
473
+ const nodes = convertVariadicArgs(host, args);
474
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
475
+ authoredSplice(state, nodes, null);
476
+ commitAuthored(host, state, nodes);
477
+ };
478
+
479
+ h.prepend = function (...args) {
480
+ if (h[RENDERING]) return N_prepend.apply(this, args);
481
+ const nodes = convertVariadicArgs(host, args);
482
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
483
+ // Remove any incoming node from its current position, then insert at the
484
+ // FRONT via unshift. Passing `authored[0]` as an authoredSplice ref would be
485
+ // wrong, because that ref is captured before the incoming-removal, so
486
+ // prepending the current first child loses the ref and appends at the end.
487
+ for (const n of nodes) {
488
+ const j = state.authored.indexOf(n);
489
+ if (j !== -1) state.authored.splice(j, 1);
490
+ }
491
+ state.authored.unshift(...nodes);
492
+ commitAuthored(host, state, nodes);
493
+ };
494
+
495
+ h.replaceChildren = function (...args) {
496
+ if (h[RENDERING]) return N_replaceChildren.apply(this, args);
497
+ const nodes = convertVariadicArgs(host, args);
498
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
499
+ const displaced = state.authored;
500
+ state.authored = nodes.slice();
501
+ commitAuthored(host, state, [...displaced, ...nodes]);
502
+ };
503
+
504
+ Object.defineProperty(h, 'innerHTML', {
505
+ configurable: true,
506
+ get() {
507
+ return INNER_HTML_DESC.get.call(this);
508
+ },
509
+ set(str) {
510
+ if (h[RENDERING]) {
511
+ INNER_HTML_DESC.set.call(this, str);
512
+ return;
513
+ }
514
+ // Parse in a DIV (the "in body" fragment context a custom-element host
515
+ // gets natively): a <template> retains table-section tokens (<td>,
516
+ // <tr>) that the real host context drops to text.
517
+ const tmp = host.ownerDocument.createElement('div');
518
+ // innerHTML IS [LegacyNullToEmptyString]: null maps to the empty
519
+ // string (the common clear idiom must clear, not insert the text
520
+ // "null"); undefined stringifies; a Symbol throws (ToString).
521
+ INNER_HTML_DESC.set.call(tmp, str === null ? '' : `${str}`);
522
+ const nodes = Array.from(tmp.childNodes);
523
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
524
+ const displaced = state.authored;
525
+ state.authored = nodes;
526
+ commitAuthored(host, state, [...displaced, ...nodes]);
527
+ },
528
+ });
529
+
530
+ Object.defineProperty(h, 'textContent', {
531
+ configurable: true,
532
+ get() {
533
+ return TEXT_CONTENT_DESC.get.call(this);
534
+ },
535
+ set(str) {
536
+ if (h[RENDERING]) {
537
+ TEXT_CONTENT_DESC.set.call(this, str);
538
+ return;
539
+ }
540
+ // Node.textContent is a NULLABLE DOMString? (LegacyNullToEmptyString
541
+ // belongs to innerHTML, not here): WebIDL converts undefined to null
542
+ // for nullable types, so BOTH null and undefined EMPTY, verified
543
+ // against all three engines. A Symbol still throws (ToString).
544
+ const nodes =
545
+ str == null || str === ''
546
+ ? []
547
+ : [host.ownerDocument.createTextNode(`${str}`)];
548
+ for (const n of nodes) FRAMEWORK_DETACHED.add(n); // prune-exempt until placed
549
+ const displaced = state.authored;
550
+ state.authored = nodes;
551
+ commitAuthored(host, state, [...displaced, ...nodes]);
552
+ },
553
+ });
554
+ }
555
+
556
+ // ---------------------------------------------------------------------------
557
+ // Render-owned slot application
558
+ // ---------------------------------------------------------------------------