@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,304 @@
1
+ /**
2
+ * Assignment commit: placing assigned nodes, restoring fallback content
3
+ * and firing slotchange.
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 { inBrowser } from './polyfills.js';
11
+ import { ensureSlotState, hasSlotState, keyOfName } from './state.js';
12
+ import { FRAMEWORK_DETACHED, LIGHT_SLOT_ATTR, PROJECTION_ACTUAL, PROJECTION_ATTR, PROJECTION_FALLBACK, SLOT_FALLBACK_FRAG, SLOT_OWNER, SLOT_OWNER_ATTR } from './symbols.js';
13
+
14
+ /**
15
+ * True when this host's current markup is FRAMEWORK-RENDERED output rather
16
+ * than author-written children: a rendered light template carries the
17
+ * framework's own `slot[data-webjs-light]` elements, an attribute only the
18
+ * renderer / SSR ever stamps (own-slot filtered so a nested serialized
19
+ * component inside genuinely-authored children does not misfire; and
20
+ * `data-wj-host` is NOT usable here, since connectedCallback stamps it on
21
+ * every light host before this check runs). The connectedCallback branch
22
+ * chooser uses this STRUCTURAL signal to pick adopt-not-capture for a
23
+ * framework-rendered subtree, so a back/forward snapshot restore
24
+ * (post-hydration HTML, no `webjs-hydrate` marker) adopts the projected
25
+ * children instead of hoovering the rendered tree into `authored` (the #1006
26
+ * duplication shape on the restore path).
27
+ *
28
+ * @param {Element} host
29
+ * @returns {boolean}
30
+ */
31
+ export function hasFrameworkRenderedSubtree(host) {
32
+ if (!inBrowser) return false;
33
+ // BOTH attributes are required. data-webjs-light alone is stamped at
34
+ // TEMPLATE COMPILE time on every <slot> in every html template, including a
35
+ // slot FORWARDED as an authored child of a nested component tag
36
+ // (html`<inner-shell><slot>fallback</slot></inner-shell>`), so matching it
37
+ // alone would misfire on the forwarding shape at a client-side first mount
38
+ // and adopt (discard) the forwarded slot. data-projection is stamped only
39
+ // when the framework has PLACED the slot (SSR substitution or the apply
40
+ // step), so light + projection together mean genuinely rendered output.
41
+ for (const el of host.querySelectorAll(`slot[${LIGHT_SLOT_ATTR}][${PROJECTION_ATTR}]`)) {
42
+ if (isOwnSlot(host, el)) return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * True when `slot` belongs to `host` directly: no OTHER custom element
49
+ * sits between them. A slot nested inside a child custom element belongs
50
+ * to THAT component and is applied from its own record.
51
+ *
52
+ * SUBTLETY: for a slot in a fully DETACHED chain the walk ends at null
53
+ * without reaching `host` and returns vacuously true. Callers that must not
54
+ * treat a FOREIGN detached slot as owned (the prune rule, isVirtualChild)
55
+ * pair this with a `host.contains(p)` gate; adopted-SSR children in the
56
+ * detached old chain survive pruning via their FRAMEWORK_DETACHED mark, not
57
+ * via this walk.
58
+ *
59
+ * @param {Element} host
60
+ * @param {Element} slot
61
+ * @returns {boolean}
62
+ */
63
+ export function isOwnSlot(host, slot) {
64
+ // Template-ownership is authoritative when known: a forwarded slot sits
65
+ // physically inside a child component but belongs to the host whose
66
+ // template rendered it. Two carriers of the SAME fact, consulted in order:
67
+ // 1. the SLOT_OWNER symbol, stamped by the client renderer at render
68
+ // time (createInstance) and thus present once this host has rendered;
69
+ // 2. the data-wj-slot-owner attribute, the SSR carrier, which is the
70
+ // ACTIVE resolver on the adopt/hydration path (adoptSSRAssignments
71
+ // runs in connectedCallback, before the deferred first render stamps
72
+ // the symbol), resolved by nearest-matching-tag ancestor.
73
+ const owner = /** @type {any} */ (slot)[SLOT_OWNER];
74
+ if (owner) return owner === host;
75
+ const ownerTag =
76
+ typeof slot.getAttribute === 'function' ? slot.getAttribute(SLOT_OWNER_ATTR) : null;
77
+ if (ownerTag) {
78
+ const resolved = ownerHostFor(slot, ownerTag);
79
+ if (resolved) return resolved === host;
80
+ // Unresolvable (a detached chain the owner is no longer an ancestor of):
81
+ // fall through to the structural walk rather than falsely denying.
82
+ }
83
+ // Structural fallback: no OTHER custom element sits between slot and host.
84
+ for (let p = slot.parentElement; p && p !== host; p = p.parentElement) {
85
+ if (p.tagName.includes('-')) return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * The host a `data-wj-slot-owner="<tag>"` attribute resolves to: the nearest
92
+ * ANCESTOR whose tag matches, by tag alone (NOT gated on SLOT_STATE). The
93
+ * gate would be wrong during the connect-time chooser, where an inner host
94
+ * has not upgraded yet, so a forwarded slot would fail to resolve to its
95
+ * outer owner and the outer would wrongly capture-hoover the SSR subtree.
96
+ * One-level forwarding resolves cleanly; same-tag-nested forwarding picks
97
+ * the nearest (the accepted edge, no worse than the structural walk it
98
+ * replaces).
99
+ *
100
+ * @param {Element} slot
101
+ * @param {string} ownerTag
102
+ * @returns {Element | null}
103
+ */
104
+ function ownerHostFor(slot, ownerTag) {
105
+ const want = ownerTag.toLowerCase();
106
+ for (let p = slot.parentElement; p; p = p.parentElement) {
107
+ if (p.tagName.toLowerCase() === want) return p;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Set a slot to actual-assignment mode and move the given nodes into it.
114
+ * Preserves DOM identity by re-using the same Node references when they
115
+ * are already inside the slot in the same order.
116
+ *
117
+ * @param {SlotState} state
118
+ * @param {HTMLSlotElement} slot
119
+ * @param {Node[]} assigned
120
+ * @returns {boolean} True if the slot's assignment changed compared to
121
+ * its last snapshot (so slotchange should fire).
122
+ */
123
+ export function applyActualAssignment(state, slot, assigned) {
124
+ // These nodes are being placed into an own actual slot, so they are
125
+ // author-live now: clear the prune exemption on EVERY path (including the
126
+ // unchanged and in-place fast paths below, which the router's morph hits).
127
+ // Missing this leaves a reprojected node permanently exempt, so a later
128
+ // el.remove() / cross-host move on it would not be pruned (zombie / theft).
129
+ for (const n of assigned) FRAMEWORK_DETACHED.delete(n);
130
+ const wasFallback = slot.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL;
131
+ const prev = state.lastSnapshot.get(slot) || [];
132
+ const setChanged = wasFallback || !arraysEqual(prev, assigned);
133
+
134
+ // Physical fast path: the assigned nodes are ALREADY the slot's children in
135
+ // the same order (the idempotent no-change pass, and the router's morph
136
+ // which reconciles in place then syncs the record). Settle the snapshot;
137
+ // slotchange reflects the SET change vs the previous snapshot. Verifying
138
+ // physically (never trusting the snapshot alone) also makes the apply
139
+ // self-repairing after a bypass move pulled a node out of the slot.
140
+ if (!wasFallback && arraysEqual(Array.from(slot.childNodes), assigned)) {
141
+ state.lastSnapshot.set(slot, assigned.slice());
142
+ return setChanged;
143
+ }
144
+
145
+ // Preserve fallback content. If the slot currently holds fallback nodes
146
+ // (either because we just hydrated from SSR's data-projection="fallback"
147
+ // or because the slot-part placed them there at bind time), move them
148
+ // back into the part-owned holding fragment so they survive for a later
149
+ // transition. Identified via the SLOT_FALLBACK_FRAG symbol that the
150
+ // slot-part wrote to the element.
151
+ const fallbackFrag = /** @type {DocumentFragment | undefined} */ (
152
+ /** @type {any} */ (slot)[SLOT_FALLBACK_FRAG]
153
+ );
154
+ if (wasFallback && fallbackFrag) {
155
+ while (slot.firstChild) fallbackFrag.appendChild(slot.firstChild);
156
+ }
157
+
158
+ // Incremental reconcile, native parity: an assignment change must never
159
+ // reparent a SURVIVING assigned node (native shadow assignment does not
160
+ // move host children), or appending one sibling would bounce every nested
161
+ // custom element through disconnect/connect, drop focus, and reload an
162
+ // <iframe>/<video> in the projected content. Remove departures, then
163
+ // position each assigned node touching ONLY the new, departing, or
164
+ // out-of-order ones.
165
+ const want = new Set(assigned);
166
+ for (const c of Array.from(slot.childNodes)) {
167
+ if (!want.has(c)) slot.removeChild(c);
168
+ }
169
+ let cursor = slot.firstChild;
170
+ for (const node of assigned) {
171
+ if (node === cursor) {
172
+ cursor = cursor.nextSibling;
173
+ continue;
174
+ }
175
+ slot.insertBefore(node, cursor);
176
+ }
177
+ slot.setAttribute(PROJECTION_ATTR, PROJECTION_ACTUAL);
178
+ state.lastSnapshot.set(slot, assigned.slice());
179
+ return setChanged;
180
+ }
181
+
182
+ /**
183
+ * Set a slot to fallback mode: clear any actual-assignment children and
184
+ * restore the part-owned fallback fragment. The record keeps the nodes
185
+ * (they are values now), so a later authored write or slot re-creation
186
+ * re-places them.
187
+ *
188
+ * @param {SlotState} state
189
+ * @param {HTMLSlotElement} slot
190
+ * @returns {boolean} True if the slot transitioned from actual to
191
+ * fallback this pass.
192
+ */
193
+ export function applyFallback(state, slot) {
194
+ const wasActual = slot.getAttribute(PROJECTION_ATTR) === PROJECTION_ACTUAL;
195
+ slot.setAttribute(PROJECTION_ATTR, PROJECTION_FALLBACK);
196
+ if (!wasActual) {
197
+ // Already fallback. Make sure the fallback content is materialised
198
+ // in the slot if the slot-part has a holding fragment with nodes.
199
+ restoreFallbackInto(slot);
200
+ return false;
201
+ }
202
+
203
+ // Slot transitioning from actual to fallback (the record no longer has
204
+ // content for this name).
205
+ state.lastSnapshot.delete(slot);
206
+ while (slot.firstChild) slot.removeChild(slot.firstChild);
207
+ restoreFallbackInto(slot);
208
+ return true;
209
+ }
210
+
211
+ /**
212
+ * Move the slot-part's holding fragment back into the slot. No-op if no
213
+ * fragment is attached or it is already empty.
214
+ *
215
+ * @param {HTMLSlotElement} slot
216
+ */
217
+ function restoreFallbackInto(slot) {
218
+ const frag = /** @type {DocumentFragment | undefined} */ (
219
+ /** @type {any} */ (slot)[SLOT_FALLBACK_FRAG]
220
+ );
221
+ if (!frag || frag.childNodes.length === 0) return;
222
+ slot.appendChild(frag);
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Renderer teardown hook (called from render-client.js)
227
+ // ---------------------------------------------------------------------------
228
+
229
+ /**
230
+ * Detach a slot's record-owned children from the slot element before the
231
+ * renderer's template teardown disposes the slot's subtree (a conditional
232
+ * fragment collapsing). The RECORD keeps the node references, so when a
233
+ * re-render re-creates the slot, `applySlotAssignments` re-places the very
234
+ * same nodes: children are values, teardown never disposes consumer nodes.
235
+ *
236
+ * @param {Element} host
237
+ * @param {HTMLSlotElement} slot
238
+ */
239
+ export function rescueAssignedNodes(host, slot) {
240
+ if (!hasSlotState(host)) return;
241
+ const state = ensureSlotState(host);
242
+ const name = keyOfName(slot.getAttribute('name'));
243
+ const assigned = state.assignedByName.get(name);
244
+ if (assigned) {
245
+ for (const node of assigned) {
246
+ if (node.parentNode === slot) {
247
+ // Framework-detached on teardown: the record keeps the ref (children
248
+ // are values), so the prune rule must not drop it while it is parked
249
+ // out of the tree waiting for a re-created slot to re-place it.
250
+ FRAMEWORK_DETACHED.add(node);
251
+ slot.removeChild(node);
252
+ }
253
+ }
254
+ }
255
+ state.lastSnapshot.delete(slot);
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // slotchange event dispatch
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /** Fire a `slotchange` event on the slot (bubbles, not composed; per spec). */
263
+ export function fireSlotChange(slot) {
264
+ slot.dispatchEvent(new Event('slotchange', { bubbles: true, composed: false }));
265
+ }
266
+
267
+ /**
268
+ * Queue a coalesced `slotchange` for a slot. The event is dispatched on a
269
+ * microtask, and a slot that changes more than once before the flush fires
270
+ * exactly once (native `slotchange` timing: async and coalesced per slot).
271
+ * A slot detached before the flush is skipped.
272
+ *
273
+ * @param {SlotState} state
274
+ * @param {HTMLSlotElement} slot
275
+ */
276
+ export function queueSlotChange(state, slot) {
277
+ if (!state.pendingSlotChanges) state.pendingSlotChanges = new Set();
278
+ state.pendingSlotChanges.add(slot);
279
+ if (state.slotChangeScheduled) return;
280
+ state.slotChangeScheduled = true;
281
+ queueMicrotask(() => {
282
+ state.slotChangeScheduled = false;
283
+ const pending = state.pendingSlotChanges || new Set();
284
+ state.pendingSlotChanges = new Set();
285
+ // Fire regardless of connectivity: native slot assignment (and its
286
+ // slotchange) works in disconnected trees, and dropping the event here
287
+ // would lose it forever for a detached-then-reused host.
288
+ for (const s of pending) fireSlotChange(s);
289
+ });
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Misc helpers
294
+ // ---------------------------------------------------------------------------
295
+
296
+ /** Strict per-index equality on two arrays. */
297
+ export function arraysEqual(a, b) {
298
+ if (a === b) return true;
299
+ if (a.length !== b.length) return false;
300
+ for (let i = 0; i < a.length; i++) {
301
+ if (a[i] !== b[i]) return false;
302
+ }
303
+ return true;
304
+ }