@webjsdev/core 0.7.64 → 0.8.0

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 (58) 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 +21 -22
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +6 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +6 -2
  8. package/index.d.ts +7 -3
  9. package/index.js +6 -2
  10. package/package.json +1 -1
  11. package/src/attribute-directives.d.ts +6 -0
  12. package/src/attribute-directives.js +49 -0
  13. package/src/attribute-reader.js +37 -3
  14. package/src/binding-prefixes.js +10 -41
  15. package/src/component/lifecycle.js +261 -101
  16. package/src/component/reactive.js +5 -4
  17. package/src/component.d.ts +11 -5
  18. package/src/directives.d.ts +2 -0
  19. package/src/directives.js +2 -0
  20. package/src/escape.js +12 -2
  21. package/src/form-action.js +7 -7
  22. package/src/html.d.ts +4 -0
  23. package/src/html.js +22 -1
  24. package/src/optimistic.js +3 -3
  25. package/src/registry.d.ts +5 -0
  26. package/src/registry.js +35 -4
  27. package/src/render-client/adopt-slots.js +245 -0
  28. package/src/render-client/parts.js +53 -19
  29. package/src/render-client/reconciler.js +28 -12
  30. package/src/render-client/template-compiler.js +19 -5
  31. package/src/render-client.d.ts +2 -1
  32. package/src/render-server/attrs.js +5 -4
  33. package/src/render-server/compile.js +433 -0
  34. package/src/render-server/dsd.js +11 -435
  35. package/src/render-server/errors.js +84 -0
  36. package/src/render-server/expand.js +346 -0
  37. package/src/render-server/html-scan.js +19 -358
  38. package/src/render-server/served-attrs.js +79 -0
  39. package/src/render-server/stream.js +11 -13
  40. package/src/render-server/template-renderer.js +13 -936
  41. package/src/render-server/walk.js +798 -0
  42. package/src/router-client/anchors.js +3 -1
  43. package/src/router-client/boundaries.js +15 -1
  44. package/src/router-client/constants.js +30 -0
  45. package/src/router-client/fetch-apply.js +21 -0
  46. package/src/router-client/frames.js +6 -3
  47. package/src/router-client/head-merge.js +58 -10
  48. package/src/router-client/navigator.js +4 -0
  49. package/src/router-client/owned-styles.js +372 -0
  50. package/src/router-client/prefetch.js +3 -0
  51. package/src/router-client/snapshot-cache.js +2 -12
  52. package/src/router-client/stylesheets.js +191 -0
  53. package/src/router-client/swap.js +49 -9
  54. package/src/sentinels.d.ts +2 -0
  55. package/src/sentinels.js +4 -0
  56. package/src/slot.js +1 -1
  57. package/src/webjs-config.d.ts +1 -1
  58. package/src/render-server/slots.js +0 -213
@@ -96,3 +96,5 @@ export function asyncReplace<T>(
96
96
  export function isAsyncReplace(x: unknown): x is AsyncReplaceDirective;
97
97
  export function watch<T>(sig: { get(): T; __isSignal: true }): WatchDirective<T>;
98
98
  export function isWatch(x: unknown): x is WatchDirective;
99
+
100
+ export { classMap, styleMap, ifDefined } from './attribute-directives.js';
package/src/directives.js CHANGED
@@ -458,3 +458,5 @@ export function watch(sig) {
458
458
  export function isWatch(x) {
459
459
  return !!x && typeof x === 'object' && /** @type {any} */ (x)._$webjs === 'watch';
460
460
  }
461
+
462
+ export { classMap, styleMap, ifDefined } from './attribute-directives.js';
package/src/escape.js CHANGED
@@ -1,6 +1,14 @@
1
+ // Both escapers skip the three replaces when the string carries none of the
2
+ // characters they rewrite, which is the common case for a template value and
3
+ // was measurable on the SSR hot path (#1646).
4
+ const TEXT_SPECIAL = /[&<>]/;
5
+ const ATTR_SPECIAL = /[&"<]/;
6
+
1
7
  /** @param {string} s */
2
8
  export function escapeText(s) {
3
- return String(s)
9
+ const str = String(s);
10
+ if (!TEXT_SPECIAL.test(str)) return str;
11
+ return str
4
12
  .replace(/&/g, '&amp;')
5
13
  .replace(/</g, '&lt;')
6
14
  .replace(/>/g, '&gt;');
@@ -8,7 +16,9 @@ export function escapeText(s) {
8
16
 
9
17
  /** @param {string} s */
10
18
  export function escapeAttr(s) {
11
- return String(s)
19
+ const str = String(s);
20
+ if (!ATTR_SPECIAL.test(str)) return str;
21
+ return str
12
22
  .replace(/&/g, '&amp;')
13
23
  .replace(/"/g, '&quot;')
14
24
  .replace(/</g, '&lt;');
@@ -87,10 +87,10 @@ import { escapeAttr } from './escape.js';
87
87
  * it looks like from any one file and are easy to change one at a time. That
88
88
  * is why the predicates live here rather than at each site.
89
89
  *
90
- * The second SSR machine (`streamTemplate`) is reached only through
91
- * `renderToStream(v, { ssr: false })`, which no page render uses: the server
92
- * renders every page, Suspense included, via `renderToString`. It is kept in
93
- * lockstep for the public API surface rather than for a live page.
90
+ * Since #1646 there is ONE SSR interpreter (`render-server/walk.js`), shared
91
+ * by the buffered page render and `renderToStream(v, { ssr: false })`, so the
92
+ * two can no longer drift; the predicates still live here because the CLIENT
93
+ * renderer applies the same rules.
94
94
  */
95
95
 
96
96
  /**
@@ -967,9 +967,9 @@ function ensureIdentityField(form, id) {
967
967
  /**
968
968
  * SSR: judge one submitter's emitted START TAG, at its `>`.
969
969
  *
970
- * Called from BOTH state machines so the rules cannot drift between them, which
971
- * is the failure #1154 already had once (a guard shipped in `renderTemplate`
972
- * and not in `streamTemplate`). Judged at the `>` rather than at the hole
970
+ * Called from the one SSR interpreter (`render-server/walk.js`, #1646); there
971
+ * used to be two hand-synchronised state machines, which is the failure #1154
972
+ * had once (a guard shipped in one and not the other). Judged at the `>` rather than at the hole
973
973
  * because every input is an attribute the author may have written AFTER the
974
974
  * binding.
975
975
  *
package/src/html.d.ts CHANGED
@@ -2,8 +2,12 @@ export interface TemplateResult {
2
2
  _$webjs: 'template';
3
3
  strings: TemplateStringsArray | string[];
4
4
  values: unknown[];
5
+ /** Set by the `svg` tag: the client parses the strings inside an `<svg>` wrapper. */
6
+ svg?: boolean;
5
7
  }
6
8
 
7
9
  export function html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
10
+ /** lit's `svg` tag: a fragment parsed as SVG on the client, rendered like `html` on the server. */
11
+ export function svg(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
8
12
  export function isTemplate(x: unknown): x is TemplateResult;
9
13
  export const MARKER: 'wjm-';
package/src/html.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * A TemplateResult is an isomorphic description of HTML: the static string
5
5
  * pieces plus the dynamic values. Server and client renderers both consume it.
6
6
  *
7
- * @typedef {{ _$webjs: 'template', strings: TemplateStringsArray | string[], values: unknown[] }} TemplateResult
7
+ * @typedef {{ _$webjs: 'template', strings: TemplateStringsArray | string[], values: unknown[], svg?: boolean }} TemplateResult
8
8
  *
9
9
  * @param {TemplateStringsArray | string[]} strings
10
10
  * @param {...unknown} values
@@ -14,6 +14,27 @@ export function html(strings, ...values) {
14
14
  return { _$webjs: 'template', strings, values };
15
15
  }
16
16
 
17
+ /**
18
+ * Tagged template literal for an SVG FRAGMENT, lit's `svg` tag.
19
+ *
20
+ * Use it for markup that sits INSIDE an `<svg>` element but is built in its
21
+ * own template (a `<rect>` per data point, a `<path>` a helper returns).
22
+ * An `html` template is parsed as HTML on the client, so a `<rect>` there
23
+ * becomes an HTML element and never paints. An `svg` template is parsed
24
+ * inside an `<svg>` wrapper, so its elements are SVG elements. A whole
25
+ * `<svg>` element still belongs in `html`.
26
+ *
27
+ * The server renders it exactly like `html` (markup is text there), so the
28
+ * two differ only in how the client parses the static strings.
29
+ *
30
+ * @param {TemplateStringsArray | string[]} strings
31
+ * @param {...unknown} values
32
+ * @returns {TemplateResult}
33
+ */
34
+ export function svg(strings, ...values) {
35
+ return { _$webjs: 'template', strings, values, svg: true };
36
+ }
37
+
17
38
  /**
18
39
  * Identity check for TemplateResult.
19
40
  * @param {unknown} x
package/src/optimistic.js CHANGED
@@ -44,7 +44,7 @@ class OptimisticState {
44
44
  }
45
45
  }
46
46
 
47
- async function runLegacyOptimistic(signal, value, action) {
47
+ async function runImperativeOptimistic(signal, value, action) {
48
48
  const prev = signal.get();
49
49
  signal.set(value);
50
50
  let result;
@@ -105,7 +105,7 @@ async function runLegacyOptimistic(signal, value, action) {
105
105
  * 4. The returned `release()` fn removes the update by ID and re-renders.
106
106
  * 5. Concurrent updates stack; each release removes only its own entry.
107
107
  *
108
- * Backward-compatible imperative API (signal-based rollback):
108
+ * Imperative form (signal-based rollback), for a one-shot toggle:
109
109
  * await optimistic(signal, value, () => likePost(postId));
110
110
  *
111
111
  * Client-only: it calls `host.requestUpdate()` (client work), so a component
@@ -119,7 +119,7 @@ async function runLegacyOptimistic(signal, value, action) {
119
119
  */
120
120
  export function optimistic(first, second, third) {
121
121
  if (first && typeof first.get === 'function' && typeof first.set === 'function') {
122
- return runLegacyOptimistic(first, second, third);
122
+ return runImperativeOptimistic(first, second, third);
123
123
  }
124
124
  return new OptimisticState(first, second);
125
125
  }
package/src/registry.d.ts CHANGED
@@ -15,3 +15,8 @@ export function lookupModuleUrl(tag: string): string | undefined;
15
15
  export function isLazy(tag: string): boolean;
16
16
  export function tagOf(cls: WebComponentInstanceConstructor): string | undefined;
17
17
  export function allTags(): string[];
18
+ /**
19
+ * The registry's tag-set version: bumped on a first-time registration, so a
20
+ * memo keyed on it (the SSR scanner's tag regex) is valid while it is unchanged.
21
+ */
22
+ export function registryVersion(): number;
package/src/registry.js CHANGED
@@ -31,7 +31,7 @@
31
31
  * user-app: ThemeToggle.register('theme-toggle') → instance-A registry
32
32
  * server: lookup('theme-toggle') → instance-B registry (empty)
33
33
  *
34
- * SSR's `injectDSD` would then skip the tag and emit bare
34
+ * The SSR walk's `lookup(tag)` would then find no class and emit bare
35
35
  * `<theme-toggle></theme-toggle>` even though the user registered it
36
36
  * correctly. Symptom: components render with no children server-side
37
37
  * and only "appear" after the browser hydrates.
@@ -50,6 +50,21 @@ const registry = _g[REGISTRY_KEY] || (_g[REGISTRY_KEY] = new Map());
50
50
  /** @type {WeakMap<Function, string>} */
51
51
  const classToTag = _g[CLASS_TO_TAG_KEY] || (_g[CLASS_TO_TAG_KEY] = new WeakMap());
52
52
 
53
+ /**
54
+ * The registry's tag-set version (#1644): bumped whenever a NEW key enters
55
+ * the registry, by a class registration OR by `primeModuleUrl` (the prod
56
+ * component scan creates the entry before the class arrives), so the SSR
57
+ * scanner can memoize the regex it builds from every registered tag and
58
+ * rebuild it only when the set changed. Shared on
59
+ * `globalThis` like the registry itself, because under `bun --hot` the app
60
+ * imports a fresh copy of this module while the first run's server keeps
61
+ * rendering through the old one; a copy-local counter would leave that server
62
+ * blind to a tag the new copy registered.
63
+ * @type {{ v: number }}
64
+ */
65
+ const REGISTRY_VERSION_KEY = Symbol.for('webjs:registryVersion');
66
+ const version = _g[REGISTRY_VERSION_KEY] || (_g[REGISTRY_VERSION_KEY] = { v: 0 });
67
+
53
68
  const isBrowser =
54
69
  typeof window !== 'undefined' && typeof customElements !== 'undefined';
55
70
 
@@ -76,6 +91,8 @@ function registerInternal(tag, cls) {
76
91
  entry.lazy = lazy;
77
92
  } else {
78
93
  registry.set(tag, { cls, moduleUrl: null, lazy, tag });
94
+ // A new tag changes the set the SSR scanner matches on (#1644).
95
+ version.v += 1;
79
96
  }
80
97
  classToTag.set(cls, tag);
81
98
  }
@@ -126,9 +143,10 @@ if (!isBrowser) {
126
143
  * ------------------------------------------------------------------ */
127
144
 
128
145
  /**
129
- * @deprecated Low-level internal: prefer `customElements.define(tag, cls)`.
130
- * Kept as a minimal wrapper for back-compat with any framework code that
131
- * calls it directly.
146
+ * Register a component class under a tag. The primitive behind
147
+ * `Class.register('tag')` (the app-facing idiom): `customElements.define` in
148
+ * the browser, the SSR registry on the server. Reachable as
149
+ * `@webjsdev/core/registry` for framework tooling; apps use the static method.
132
150
  *
133
151
  * @param {string} tag
134
152
  * @param {typeof import('./component.js').WebComponent} cls
@@ -163,6 +181,10 @@ export function primeModuleUrl(tag, moduleUrl) {
163
181
  lazy: false,
164
182
  tag,
165
183
  });
184
+ // A primed tag is a new KEY, and `allTags()` lists keys: the SSR scanner's
185
+ // pattern must include it before its class arrives, or a pattern memoized
186
+ // while the registry was empty would stay empty for the process (#1644).
187
+ version.v += 1;
166
188
  }
167
189
 
168
190
  /** @param {string} tag */
@@ -188,3 +210,12 @@ export function tagOf(cls) {
188
210
  export function allTags() {
189
211
  return [...registry.keys()];
190
212
  }
213
+
214
+ /**
215
+ * The current tag-set version: moves only when a tag is registered for the
216
+ * first time. A memo keyed on it is valid for as long as it is unchanged.
217
+ * @returns {number}
218
+ */
219
+ export function registryVersion() {
220
+ return version.v;
221
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Hydration slot adoption: keep a light-DOM host's SSR-projected children in
3
+ * the document while its first client render replaces the SSR markup (#1666).
4
+ *
5
+ * Hydration renders the template from a fresh clone and swaps it in for the
6
+ * SSR markup. The projected children sit inside the SSR `<slot>`, so swapping
7
+ * the SSR tree out took them out of the document, and the first slot apply put
8
+ * them back a microtask later. A removal is not free: the HTML spec navigates
9
+ * a re-inserted iframe again (and `loading="lazy"` defers only the FIRST
10
+ * insertion, so a hidden lazy frame loads), and every nested custom element
11
+ * disconnects and reconnects.
12
+ *
13
+ * So for each of the template's own slots whose SSR counterpart holds exactly
14
+ * the nodes the host's record adopted, the SSR slot and its ancestor chain up
15
+ * to the host are KEPT: the parts that target a fresh element on that chain
16
+ * are pointed at its live twin, and the fresh siblings around the chain are
17
+ * inserted into the live elements instead of the live tree being replaced. No
18
+ * kept node moves, so the projected children never leave the document. The
19
+ * fresh slot's cloned fallback goes to the slot's holding fragment, where the
20
+ * slot part would have put it.
21
+ *
22
+ * Conservative by construction: a slot is adopted only when every link of
23
+ * both chains matches by tag, no link is a custom element (whose SSR markup is
24
+ * its own rendered template) or a `<form>` (form-action reconciliation inserts
25
+ * children into it), the slot's name is static, and the kept nodes sit in the
26
+ * same relative order on both sides. Anything else, a slot inside a
27
+ * conditional template among them, falls back to the plain swap.
28
+ *
29
+ * @module
30
+ */
31
+ import {
32
+ PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_FALLBACK_FRAG, SLOT_OWNER_ATTR,
33
+ ensureSlotState, hasSlotState, keyOfName,
34
+ } from '../slot.js';
35
+
36
+ /**
37
+ * Live SSR slots a hydration render adopted. The slot part's finalize reads
38
+ * it to leave the slot's children (the projected nodes) in place instead of
39
+ * harvesting them as fallback, since the holding fragment is already set.
40
+ * @type {WeakSet<Element>}
41
+ */
42
+ export const adoptedSlots = new WeakSet();
43
+
44
+ /**
45
+ * @typedef {{ map: Map<Node, Element>, slots: [Element, Element][], chains: [Element, Element][] }} AdoptionPlan
46
+ */
47
+
48
+ /**
49
+ * Plan the adoption for a hydration render, after the parts are bound and
50
+ * before any is applied. Returns null when no slot qualifies.
51
+ *
52
+ * @param {Element | DocumentFragment | ShadowRoot} container
53
+ * @param {DocumentFragment} frag the fresh clone
54
+ * @param {any[]} bound the template's bound parts
55
+ * @returns {AdoptionPlan | null}
56
+ */
57
+ export function planSlotAdoption(container, frag, bound) {
58
+ if (/** @type {any} */ (container).nodeType !== 1 || !hasSlotState(/** @type {Element} */ (container))) return null;
59
+ const state = ensureSlotState(/** @type {Element} */ (container));
60
+ /** @type {Set<Element>} */
61
+ const dynamicName = new Set();
62
+ for (const p of bound) {
63
+ if ((p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') && p.name === 'name') dynamicName.add(p.el);
64
+ }
65
+ /** @type {Map<Node, Element>} fresh element to its live twin */
66
+ const map = new Map();
67
+ /** @type {Set<Element>} */
68
+ const usedLive = new Set();
69
+ /** @type {[Element, Element][]} */
70
+ const slots = [];
71
+ /** @type {[Element, Element][]} */
72
+ const chains = [];
73
+ for (const p of bound) {
74
+ if (p.kind !== 'slot') continue;
75
+ const slotF = /** @type {Element} */ (p.slotEl);
76
+ if (dynamicName.has(slotF)) continue;
77
+ const assigned = state.assignedByName.get(keyOfName(slotF.getAttribute('name')));
78
+ if (!assigned || assigned.length === 0) continue;
79
+ const slotL = /** @type {Element | null} */ (assigned[0].parentNode);
80
+ if (!slotL || slotL.nodeType !== 1 || slotL.localName !== 'slot' || usedLive.has(slotL)) continue;
81
+ if (slotL.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL) continue;
82
+ if (!sameNodes(slotL.childNodes, assigned)) continue;
83
+ const fChain = ancestorsUpTo(slotF, frag);
84
+ const lChain = ancestorsUpTo(slotL, container);
85
+ if (!fChain || !lChain || fChain.length !== lChain.length) continue;
86
+ let ok = true;
87
+ for (let i = 0; i < fChain.length && ok; i++) {
88
+ const f = fChain[i];
89
+ const l = lChain[i];
90
+ const name = f.localName;
91
+ if (name !== l.localName || name.includes('-') || name === 'form') ok = false;
92
+ else {
93
+ const prior = map.get(f);
94
+ if (prior ? prior !== l : usedLive.has(l)) ok = false;
95
+ }
96
+ }
97
+ if (!ok) continue;
98
+ map.set(slotF, slotL);
99
+ usedLive.add(slotL);
100
+ slots.push([slotF, slotL]);
101
+ for (let i = 0; i < fChain.length; i++) {
102
+ if (map.has(fChain[i])) continue;
103
+ map.set(fChain[i], lChain[i]);
104
+ usedLive.add(lChain[i]);
105
+ chains.push([fChain[i], lChain[i]]);
106
+ }
107
+ }
108
+ if (slots.length === 0) return null;
109
+ // No kept node may have to move: under each kept parent the kept children
110
+ // must already sit in the order the template puts them in.
111
+ for (const fParent of [frag, ...chains.map(([f]) => f)]) {
112
+ let last = null;
113
+ for (const child of fParent.childNodes) {
114
+ const live = map.get(child);
115
+ if (!live) continue;
116
+ if (last && !(last.compareDocumentPosition(live) & Node.DOCUMENT_POSITION_FOLLOWING)) return null;
117
+ last = live;
118
+ }
119
+ }
120
+ return { map, slots, chains };
121
+ }
122
+
123
+ /**
124
+ * Point every bound part that targets a kept fresh element at its live twin,
125
+ * and give each kept element the fresh one's static attributes. Runs before
126
+ * the parts apply, so every dynamic value lands on the live element directly.
127
+ *
128
+ * @param {AdoptionPlan} plan
129
+ * @param {any[]} bound
130
+ */
131
+ export function retargetParts(plan, bound) {
132
+ /** @type {Map<Element, Set<string>>} attributes a part owns, per live element */
133
+ const owned = new Map();
134
+ for (const p of bound) {
135
+ if (p.kind === 'slot') {
136
+ const live = plan.map.get(p.slotEl);
137
+ if (live) p.slotEl = live;
138
+ continue;
139
+ }
140
+ if (!p.el) continue;
141
+ const live = plan.map.get(p.el);
142
+ if (!live) continue;
143
+ if (p.kind === 'event') {
144
+ p.el.removeEventListener(p.name, p.dispatcher);
145
+ live.addEventListener(p.name, p.dispatcher);
146
+ }
147
+ p.el = live;
148
+ if (p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') {
149
+ let names = owned.get(live);
150
+ if (!names) owned.set(live, names = new Set());
151
+ names.add(p.name);
152
+ }
153
+ }
154
+ for (const [fresh, live] of [...plan.chains, ...plan.slots]) {
155
+ const isSlot = live.localName === 'slot';
156
+ const names = owned.get(live);
157
+ for (const a of Array.from(live.attributes)) {
158
+ if (fresh.hasAttribute(a.name) || names?.has(a.name)) continue;
159
+ // The SSR projection stamps stay: the slot IS projected, and the owner
160
+ // carrier is what isOwnSlot reads until the owner symbol is stamped.
161
+ if (isSlot && (a.name === PROJECTION_ATTR || a.name === SLOT_OWNER_ATTR)) continue;
162
+ live.removeAttribute(a.name);
163
+ }
164
+ for (const a of Array.from(fresh.attributes)) {
165
+ if (live.getAttribute(a.name) !== a.value) live.setAttribute(a.name, a.value);
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Commit the fresh template into the container around the kept live nodes:
172
+ * every live child that is not kept is removed, every fresh node is inserted
173
+ * in template order, and no kept node moves. The fresh slots' cloned fallback
174
+ * goes to each kept slot's holding fragment.
175
+ *
176
+ * @param {AdoptionPlan} plan
177
+ * @param {Element} container
178
+ * @param {Node[]} top the container's new children, bookends included
179
+ */
180
+ export function commitAdoption(plan, container, top) {
181
+ const mapped = (/** @type {Node} */ n) => plan.map.get(n) || n;
182
+ const state = ensureSlotState(container);
183
+ for (const [fresh, live] of plan.chains) mergeChildren(live, Array.from(fresh.childNodes, mapped));
184
+ mergeChildren(container, top.map(mapped));
185
+ for (const [fresh, live] of plan.slots) {
186
+ const fallback = document.createDocumentFragment();
187
+ while (fresh.firstChild) fallback.appendChild(fresh.firstChild);
188
+ /** @type {any} */ (live)[SLOT_FALLBACK_FRAG] = fallback;
189
+ adoptedSlots.add(live);
190
+ // Forget the adopted snapshot so the first apply reports the initial
191
+ // projection (one slotchange), exactly as the plain swap did.
192
+ state.lastSnapshot.delete(live);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Make `parent`'s children exactly `want`, removing what is not wanted and
198
+ * inserting the rest, without moving a node already in place in order.
199
+ *
200
+ * @param {Node} parent
201
+ * @param {Node[]} want
202
+ */
203
+ function mergeChildren(parent, want) {
204
+ const keep = new Set(want);
205
+ for (const c of Array.from(parent.childNodes)) {
206
+ if (!keep.has(c)) parent.removeChild(c);
207
+ }
208
+ let cursor = parent.firstChild;
209
+ for (const node of want) {
210
+ if (node === cursor) {
211
+ cursor = cursor.nextSibling;
212
+ continue;
213
+ }
214
+ parent.insertBefore(node, cursor);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * The element ancestors of `node` below `root`, nearest first, or null when
220
+ * `root` is not an ancestor.
221
+ *
222
+ * @param {Node} node
223
+ * @param {Node} root
224
+ * @returns {Element[] | null}
225
+ */
226
+ function ancestorsUpTo(node, root) {
227
+ const out = [];
228
+ let p = node.parentNode;
229
+ while (p && p !== root) {
230
+ if (p.nodeType !== 1) return null;
231
+ out.push(/** @type {Element} */ (p));
232
+ p = p.parentNode;
233
+ }
234
+ return p === root ? out : null;
235
+ }
236
+
237
+ /**
238
+ * @param {NodeListOf<ChildNode>} list
239
+ * @param {Node[]} nodes
240
+ */
241
+ function sameNodes(list, nodes) {
242
+ if (list.length !== nodes.length) return false;
243
+ for (let i = 0; i < nodes.length; i++) if (list[i] !== nodes[i]) return false;
244
+ return true;
245
+ }