@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.
Files changed (64) 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 +3 -3
  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/css.d.ts +9 -0
  16. package/src/escape.d.ts +2 -0
  17. package/src/html.d.ts +9 -0
  18. package/src/render-client/parts.js +2039 -0
  19. package/src/render-client/reconciler.js +388 -0
  20. package/src/render-client/template-compiler.js +525 -0
  21. package/src/render-client.js +1 -2938
  22. package/src/render-server/attrs.js +160 -0
  23. package/src/render-server/dsd.js +445 -0
  24. package/src/render-server/html-scan.js +402 -0
  25. package/src/render-server/slots.js +213 -0
  26. package/src/render-server/stream.js +163 -0
  27. package/src/render-server/template-renderer.js +938 -0
  28. package/src/render-server/text.js +163 -0
  29. package/src/render-server.js +6 -2405
  30. package/src/repeat.d.ts +15 -0
  31. package/src/rich-fetch.d.ts +4 -0
  32. package/src/router-client/anchors.js +44 -0
  33. package/src/router-client/boundaries.js +221 -0
  34. package/src/router-client/constants.js +206 -0
  35. package/src/router-client/diagnostics.js +377 -0
  36. package/src/router-client/dom-differ.js +709 -0
  37. package/src/router-client/dom-parse.js +150 -0
  38. package/src/router-client/events.js +182 -0
  39. package/src/router-client/fetch-apply.js +432 -0
  40. package/src/router-client/form-encoder.js +135 -0
  41. package/src/router-client/frames.js +329 -0
  42. package/src/router-client/head-merge.js +344 -0
  43. package/src/router-client/nav-error.js +105 -0
  44. package/src/router-client/navigator.js +952 -0
  45. package/src/router-client/prefetch.js +831 -0
  46. package/src/router-client/scroll.js +308 -0
  47. package/src/router-client/snapshot-cache.js +113 -0
  48. package/src/router-client/state.js +155 -0
  49. package/src/router-client/stream.js +206 -0
  50. package/src/router-client/swap.js +454 -0
  51. package/src/router-client/upgrade.js +261 -0
  52. package/src/router-client/view-transition.js +223 -0
  53. package/src/router-client.d.ts +11 -2
  54. package/src/router-client.js +78 -5345
  55. package/src/slot/assignment.js +304 -0
  56. package/src/slot/interception.js +558 -0
  57. package/src/slot/polyfills.js +365 -0
  58. package/src/slot/project.js +432 -0
  59. package/src/slot/sensors.js +303 -0
  60. package/src/slot/state.js +245 -0
  61. package/src/slot/symbols.js +85 -0
  62. package/src/slot.js +7 -2199
  63. package/src/suspense.d.ts +9 -0
  64. package/src/websocket-client.d.ts +22 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Native slot API capture and the light-DOM implementations behind it
3
+ * (assignedNodes / assignedElements / assignedSlot / assign).
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 { applySlotAssignments } from './project.js';
12
+ import { ensureSlotState, repartition } from './state.js';
13
+ import { FLATTEN_MAX_DEPTH, LIGHT_SLOT_ATTR, PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_STATE } from './symbols.js';
14
+
15
+ function detectBrowser() {
16
+ return typeof HTMLElement !== 'undefined' && typeof HTMLSlotElement !== 'undefined';
17
+ }
18
+
19
+ export let inBrowser = detectBrowser();
20
+
21
+ let NATIVE_assignedNodes = null;
22
+ let NATIVE_assignedElements = null;
23
+ let NATIVE_assignedSlot_desc = null;
24
+ let NATIVE_assign = null;
25
+ let polyfillsInstalled = false;
26
+
27
+ /**
28
+ * Install the slot DOM-API polyfills on HTMLSlotElement.prototype and
29
+ * Element.prototype if the current realm has those globals. Idempotent.
30
+ * No-op when the realm has no DOM (server-side import-only path).
31
+ */
32
+ export function installSlotPolyfills() {
33
+ if (polyfillsInstalled) return;
34
+ inBrowser = detectBrowser();
35
+ if (!inBrowser) return;
36
+ NATIVE_assignedNodes = HTMLSlotElement.prototype.assignedNodes;
37
+ NATIVE_assignedElements = HTMLSlotElement.prototype.assignedElements;
38
+ NATIVE_assignedSlot_desc = Object.getOwnPropertyDescriptor(Element.prototype, 'assignedSlot');
39
+
40
+ HTMLSlotElement.prototype.assignedNodes = function patchedAssignedNodes(options) {
41
+ // Two conditions must both hold for the polyfill to take over:
42
+ // 1. The slot carries the framework's data-webjs-light marker.
43
+ // 2. The slot is NOT currently inside a shadow root.
44
+ // Slots that end up in a shadow tree (their host has a ShadowRoot)
45
+ // delegate to native projection, which the browser performs from the
46
+ // host's light-DOM children. discoverSlots cannot tell at template
47
+ // compile time whether the template will be cloned into a light or
48
+ // shadow render root (templates cache by strings identity), so the
49
+ // shadow-vs-light determination has to happen on every API call.
50
+ if (this.hasAttribute(LIGHT_SLOT_ATTR) && !isInShadowRoot(this)) {
51
+ return lightAssignedNodes(this, options);
52
+ }
53
+ return NATIVE_assignedNodes ? NATIVE_assignedNodes.call(this, options) : [];
54
+ };
55
+
56
+ HTMLSlotElement.prototype.assignedElements = function patchedAssignedElements(options) {
57
+ if (this.hasAttribute(LIGHT_SLOT_ATTR) && !isInShadowRoot(this)) {
58
+ return lightAssignedNodes(this, options).filter((n) => n.nodeType === 1);
59
+ }
60
+ return NATIVE_assignedElements ? NATIVE_assignedElements.call(this, options) : [];
61
+ };
62
+
63
+ Object.defineProperty(Element.prototype, 'assignedSlot', {
64
+ configurable: true,
65
+ enumerable: true,
66
+ get: function patchedAssignedSlot() {
67
+ const native =
68
+ NATIVE_assignedSlot_desc && NATIVE_assignedSlot_desc.get
69
+ ? NATIVE_assignedSlot_desc.get.call(this)
70
+ : null;
71
+ if (native) return native;
72
+ return findLightAssignedSlot(this);
73
+ },
74
+ });
75
+
76
+ // Native `assignedSlot` lives on the Slottable mixin, which covers Text as
77
+ // well as Element: a projected text child reports its slot in shadow DOM,
78
+ // so the light parity read must answer for Text too.
79
+ const NATIVE_text_assignedSlot_desc = Object.getOwnPropertyDescriptor(
80
+ Text.prototype,
81
+ 'assignedSlot',
82
+ );
83
+ Object.defineProperty(Text.prototype, 'assignedSlot', {
84
+ configurable: true,
85
+ enumerable: true,
86
+ get: function patchedTextAssignedSlot() {
87
+ const native =
88
+ NATIVE_text_assignedSlot_desc && NATIVE_text_assignedSlot_desc.get
89
+ ? NATIVE_text_assignedSlot_desc.get.call(this)
90
+ : null;
91
+ if (native) return native;
92
+ return findLightAssignedSlot(this);
93
+ },
94
+ });
95
+
96
+ NATIVE_assign = HTMLSlotElement.prototype.assign;
97
+ HTMLSlotElement.prototype.assign = function patchedAssign(...nodes) {
98
+ if (this.hasAttribute(LIGHT_SLOT_ATTR) && !isInShadowRoot(this)) {
99
+ // Manual slot assignment (imperative, overrides attribute mode). Bound
100
+ // to THIS slot element (native binds slottables to the receiving
101
+ // element, not its name), held via WeakRef (native holds manually
102
+ // assigned slottables weakly), honored by repartition through
103
+ // effectiveKeyOf and by the placement step's per-element routing.
104
+ // NOTE this is a deliberate EXTENSION of native: real manual mode
105
+ // requires slotAssignment 'manual' on the whole shadow root and turns
106
+ // name matching off; here assign() overlays per-node while name
107
+ // matching keeps working for everything else.
108
+ const host = hostOfSlot(this);
109
+ if (host) {
110
+ const state = ensureSlotState(host);
111
+ if (!state.manualAssign) state.manualAssign = new Map();
112
+ const list = nodes.filter(Boolean);
113
+ // LAST-assign-wins: a node handed to this slot leaves any other
114
+ // slot's manual list.
115
+ for (const [slotEl, refs] of state.manualAssign) {
116
+ if (slotEl === this) continue;
117
+ const kept = refs.filter((r) => {
118
+ const n = r.deref();
119
+ return n !== undefined && list.indexOf(n) === -1;
120
+ });
121
+ if (kept.length !== refs.length) {
122
+ if (kept.length) state.manualAssign.set(slotEl, kept);
123
+ else state.manualAssign.delete(slotEl);
124
+ }
125
+ }
126
+ if (list.length) {
127
+ state.manualAssign.set(this, list.map((n) => new WeakRef(n)));
128
+ } else {
129
+ state.manualAssign.delete(this);
130
+ }
131
+ if (state.applying && state.pendingRecordNodes) {
132
+ for (const n of list) state.pendingRecordNodes.add(n);
133
+ } else {
134
+ state.pendingRecordNodes = new Set(list);
135
+ }
136
+ repartition(state);
137
+ applySlotAssignments(host);
138
+ }
139
+ return undefined;
140
+ }
141
+ return NATIVE_assign ? NATIVE_assign.apply(this, nodes) : undefined;
142
+ };
143
+ polyfillsInstalled = true;
144
+ }
145
+
146
+ /**
147
+ * The slot ELEMENT a node is manually assigned to via `assign()`, or null.
148
+ * Dead WeakRefs are compacted on the way through.
149
+ *
150
+ * @param {SlotState} state
151
+ * @param {Node} node
152
+ * @returns {HTMLSlotElement | null}
153
+ */
154
+ export function manualSlotFor(state, node) {
155
+ const manual = state.manualAssign;
156
+ if (!manual || !manual.size) return null;
157
+ for (const [slotEl, refs] of manual) {
158
+ let found = false;
159
+ const live = refs.filter((r) => {
160
+ const n = r.deref();
161
+ if (n === undefined) return false;
162
+ if (n === node) found = true;
163
+ return true;
164
+ });
165
+ if (live.length !== refs.length) {
166
+ if (live.length) manual.set(slotEl, live);
167
+ else manual.delete(slotEl);
168
+ }
169
+ if (found) {
170
+ // A manual entry is honoured only while its RECEIVING element is still
171
+ // part of this host's tree. A torn-down (conditionally re-rendered)
172
+ // slot element leaves the entry DORMANT: the node falls back to its
173
+ // slot= attribute (routed by name or parked, native's
174
+ // unassigned-but-connected behaviour) instead of being excluded from
175
+ // every slot and lost. The entry is kept, not deleted, so a re-attached
176
+ // element (the cache directive) resumes its assignment, matching
177
+ // native's element-bound persistence.
178
+ const hostEl = state.host;
179
+ if (hostEl && (slotEl === hostEl || hostEl.contains(slotEl))) return slotEl;
180
+ return null;
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * Walk up from a light slot to its owning host: the nearest SLOT_STATE
188
+ * ancestor that actually OWNS the slot. A slot separated from that ancestor
189
+ * by another custom element (a forwarded slot inside a foreign / elided
190
+ * component) is nobody's here: attributing it to the outer host would
191
+ * redirect the outer host's own same-named slot, so `assign()` on such a
192
+ * slot is inert instead (the same carve-out as every other elided-component
193
+ * write).
194
+ */
195
+ function hostOfSlot(slot) {
196
+ for (let p = slot.parentElement; p; p = p.parentElement) {
197
+ if (/** @type {any} */ (p)[SLOT_STATE]) {
198
+ return isOwnSlot(p, slot) ? p : null;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+
204
+ // First-chance install at module load.
205
+ installSlotPolyfills();
206
+
207
+ /**
208
+ * True when the given node lives inside a shadow root (so native browser
209
+ * slot projection applies). A ShadowRoot exposes its owning element as
210
+ * `host`; the document does not. Walks the parentNode chain manually
211
+ * with a depth cap to avoid hangs on accidentally cyclic DOMs (e.g.,
212
+ * test fixtures that wire two slots into each other).
213
+ *
214
+ * @param {Node} node
215
+ * @returns {boolean}
216
+ */
217
+ function isInShadowRoot(node) {
218
+ let n = node;
219
+ for (let depth = 0; depth < 128; depth++) {
220
+ const parent = n.parentNode;
221
+ if (!parent) return false;
222
+ if (parent === n) return false;
223
+ // A real ShadowRoot is a DocumentFragment (nodeType 11) exposing its
224
+ // owner as `.host`. `.host` truthiness ALONE misfires on ordinary
225
+ // elements: HTMLAnchorElement/HTMLAreaElement expose a URL-derived
226
+ // `.host`, so a slot inside an <a> card read as "in shadow DOM".
227
+ if (parent.nodeType === 11 && /** @type {any} */ (parent).host) return true;
228
+ n = parent;
229
+ }
230
+ return false;
231
+ }
232
+
233
+ /**
234
+ * Resolve assigned nodes for a light-DOM slot. Per spec, returns []
235
+ * when the slot is displaying fallback content.
236
+ *
237
+ * @param {HTMLSlotElement} slot
238
+ * @param {{ flatten?: boolean }} [options]
239
+ * @returns {Node[]}
240
+ */
241
+ function lightAssignedNodes(slot, options) {
242
+ // Only an APPLIED actual slot has assigned nodes. A fallback slot reports
243
+ // [], and so does a slot with NO data-projection at all (an orphan slot
244
+ // rendered outside any host, or one not yet placed): its children are
245
+ // cloned fallback content, and native returns [] for a slot outside a
246
+ // shadow tree.
247
+ if (slot.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL) return [];
248
+ const direct = Array.from(slot.childNodes);
249
+ if (!options || !options.flatten) return direct;
250
+ return flattenAssignedNodes(direct, new Set(), 0);
251
+ }
252
+
253
+ /**
254
+ * Walk a node list, expanding any data-webjs-light slot into its assigned
255
+ * nodes recursively. Native shadow slots encountered in the chain delegate
256
+ * to their native assignedNodes({flatten: true}).
257
+ *
258
+ * @param {Node[]} nodes
259
+ * @param {Set<HTMLSlotElement>} visited
260
+ * @param {number} depth
261
+ * @returns {Node[]}
262
+ */
263
+ function flattenAssignedNodes(nodes, visited, depth) {
264
+ if (depth >= FLATTEN_MAX_DEPTH) return nodes.slice();
265
+ const out = [];
266
+ for (const node of nodes) {
267
+ if (node.nodeType === 1 && /** @type {Element} */ (node).tagName === 'SLOT') {
268
+ const slot = /** @type {HTMLSlotElement} */ (node);
269
+ if (visited.has(slot)) continue;
270
+ visited.add(slot);
271
+ if (slot.hasAttribute(LIGHT_SLOT_ATTR)) {
272
+ const inner = lightAssignedNodes(slot, { flatten: false });
273
+ if (inner.length > 0) {
274
+ for (const n of flattenAssignedNodes(inner, visited, depth + 1)) out.push(n);
275
+ } else {
276
+ // Fallback content contributes its children.
277
+ for (const n of flattenAssignedNodes(Array.from(slot.childNodes), visited, depth + 1)) {
278
+ out.push(n);
279
+ }
280
+ }
281
+ } else if (NATIVE_assignedNodes) {
282
+ const inner = NATIVE_assignedNodes.call(slot, { flatten: true });
283
+ for (const n of inner) out.push(n);
284
+ } else {
285
+ out.push(node);
286
+ }
287
+ } else {
288
+ out.push(node);
289
+ }
290
+ }
291
+ return out;
292
+ }
293
+
294
+ /**
295
+ * Consult a node's DIRECT parent to find a data-webjs-light slot it is
296
+ * currently projected into. Returns null if the element is in a fallback
297
+ * slot or no light slot at all.
298
+ *
299
+ * @param {Element} el
300
+ * @returns {HTMLSlotElement | null}
301
+ */
302
+ function findLightAssignedSlot(el) {
303
+ // Native `assignedSlot` answers only for a SLOTTABLE itself. In the light
304
+ // parity model, assigned nodes are exactly the slot element's DIRECT
305
+ // children, so only the immediate parent is consulted: a DESCENDANT of
306
+ // assigned content correctly reads null, matching shadow DOM (where only
307
+ // the host's direct children are slottables).
308
+ const p = el.parentElement;
309
+ if (p && p.tagName === 'SLOT' && p.hasAttribute(LIGHT_SLOT_ATTR)) {
310
+ return p.getAttribute(PROJECTION_ATTR) === PROJECTION_ACTUAL
311
+ ? /** @type {HTMLSlotElement} */ (p)
312
+ : null;
313
+ }
314
+ return null;
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Per-host state: the slot record
319
+ // ---------------------------------------------------------------------------
320
+
321
+ /**
322
+ * @typedef {Object} SlotState
323
+ * @property {Element} host The owning host element (back-reference so
324
+ * state-only helpers can test containment).
325
+ * @property {Node[]} authored The ordered source of truth: every authored
326
+ * child of the host, in host-child order. `assignedByName` is DERIVED from
327
+ * this by `repartition` (grouping each node by its current `slot=`
328
+ * attribute), so there is one place a node's assignment is decided.
329
+ * @property {Map<string|null, Node[]>} assignedByName The DERIVED slot record:
330
+ * `authored` grouped per slot name (null is the default slot). Never mutated
331
+ * directly; always rebuilt by `repartition`.
332
+ * @property {WeakMap<HTMLSlotElement, Node[]>} lastSnapshot Per-slot
333
+ * record of the previous assigned-node set for slotchange equality.
334
+ * @property {Set<HTMLSlotElement>} [pendingSlotChanges] Slots whose
335
+ * assignment changed since the last microtask flush (coalesced slotchange).
336
+ * @property {boolean} [slotChangeScheduled] True while a coalesced
337
+ * slotchange flush is queued for this host.
338
+ * @property {MutationObserver} [backstop] Sensor for raw direct-child writes
339
+ * that bypass the patched methods (never moves nodes; folds into `authored`).
340
+ * @property {MutationObserver} [flipSensor] Sensor for `slot=` / `name=`
341
+ * attribute flips (never moves nodes; re-derives + re-places).
342
+ * @property {Set<Node> | undefined} [pendingRecordNodes] The nodes the
343
+ * current record op touched (inserted, moved, or removed by an interceptor
344
+ * / assign() / router splice), consumed by the next apply pass: the resync
345
+ * step honours record positions for exactly these nodes and adopts
346
+ * physical order for everything else (node-scoped order authority).
347
+ * @property {boolean} [applying] Re-entrancy latch: true while an apply
348
+ * pass runs; a nested call flags `reapply` and returns.
349
+ * @property {boolean} [reapply] Set by a nested apply attempt; the outer
350
+ * pass loops until a full pass completes without it.
351
+ * @property {boolean} [adopted] True when this state was populated by the
352
+ * ADOPT connect branch (SSR hydration / serialized restore): the host's
353
+ * pre-first-render children are rendered markup, so the reconnect fold
354
+ * must not hoover them.
355
+ * @property {WeakMap<Node, string|null>} [adoptedKey] The slot key a
356
+ * self-heal fold ADOPTED for a node a non-record writer placed inside a
357
+ * named slot (its own attribute would key it elsewhere); cleared when the
358
+ * author explicitly changes the node's slot= attribute.
359
+ * @property {Map<HTMLSlotElement, WeakRef<Node>[]>} [manualAssign] Overlay for
360
+ * `HTMLSlotElement.assign()` manual assignment, keyed by the RECEIVING slot
361
+ * element (native binds slottables to the element, so a rename follows the
362
+ * element and duplicates route correctly); nodes held via WeakRef (native
363
+ * holds manually assigned slottables weakly). A node here goes to its
364
+ * assigned slot regardless of its `slot=` attribute.
365
+ */