@dom-expressions/runtime 0.50.0-next.15 → 0.50.0-next.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # dom-expressions
2
2
 
3
+ ## 0.50.0-next.17
4
+
5
+ ### Patch Changes
6
+
7
+ - ef2864e: Add a client-side asset registry and an `inline-style` server asset type, closing the CSS lifecycle loop for SSR'd applications.
8
+
9
+ **`acquireAsset(descriptor)` (client)** — ref-counted ownership of shared document assets. Consumers (routers, lazy wrappers, metadata components) acquire an asset when content that needs it mounts and call the returned release function on cleanup:
10
+
11
+ ```js
12
+ const release = acquireAsset({ type: "style", href: "/assets/route.css" });
13
+ // … on unmount:
14
+ release();
15
+ ```
16
+
17
+ - First acquire creates the element in `<head>` — or adopts an SSR/stream-emitted one (links matched by `href`, inline styles by their `data-asset` id) instead of duplicating it.
18
+ - Last release removes the element after a short grace period, so release/re-acquire cycles during route transitions keep the live stylesheet instead of flashing unstyled content.
19
+ - Supported descriptors: `{ type: "style", href, attrs? }`, `{ type: "inline-style", id, content?, attrs? }`, `{ type: "module", href }`.
20
+ - Additionally, `{ policy: "exclusive", key, value, get, set }` provides singleton-slot semantics (last-writer-wins with restore-on-release) as the substrate for future `<Title>`/`<Meta>`-style metadata components.
21
+
22
+ **`registerAsset("inline-style", { id, content, attrs? })` (server)** — registers CSS by content rather than URL, for styles that have no `.css` file to link (dev-mode CSS collected from the bundler's module graph, critical CSS). Entries dedupe by `id` and emit as `<style data-asset="…">` tags: in `<head>` for anything registered before the shell flushes, inline in the stream for late boundary styles. Extra `attrs` pass through to the tag (e.g. `data-vite-dev-id` so Vite's HMR client adopts the server-rendered style in dev). Unlike stylesheet links, inline styles never gate streamed fragment reveal — they are applied as soon as they are parsed.
23
+
24
+ - 241ff76: Fix a spread element with dynamic props being left unclaimed on hydration. `mergeProps` with a function source creates a memo, which consumes a hydration child id. The ssr generate evaluated `mergeProps(...)` in `ssrElement`'s argument position — before the element's own hydration key was allocated — while the client claims the element (`getNextElement`) before applying the spread. The element's id shifted by one on the server and the client re-created it instead of claiming (later siblings re-synced, hiding the drift; a `<title>` rendered this way duplicated on every hydration). The ssr generate now defers the merge behind a thunk when hydratable and `ssrElement` allocates the hydration key before resolving function props, matching the client's allocation order.
25
+ - 2c6852f: Root-level inserts no longer wipe foreign sibling nodes when clearing or replacing their content. Streaming appends late-flushed `<link rel="stylesheet">` tags to the end of `<body>`, inside the region a document-level hydration root tracks; previously a root expression that emptied (`textContent = ""` fast path) or swapped to text took those links with it, dropping loaded CSS (FOUC). `insert` now removes only the nodes it tracks when the parent contains children it doesn't own, keeping the fast path when it owns everything. Also documents that `registerModule` / `loadModuleAssets` mapping keys are opaque to the runtime — the reactive library chooses them (e.g. hydration ids) on both sides of the wire.
26
+ - 2275d59: Align SSR serialization of non-string attribute values (arrays, objects) with client-side `setAttribute` coercion so both environments produce the same final attribute string.
27
+
28
+ ## 0.50.0-next.16
29
+
30
+ ### Patch Changes
31
+
32
+ - f2e56fe: fix(client): re-claim a hole's live DOM region when a streamed `$df` fragment swap replaced its tracked nodes mid-hydration (solidjs/solid#2801 bug 1, pending-stream case). A Loading fallback claimed during hydration is swapped out by `$df` before the boundary resumes; insert's node bookkeeping still pointed at the removed fallback, so the content pass fabricated detached text nodes and the first post-hydration refresh appended duplicates. When the tracked nodes are disconnected while hydrating, insert now re-derives the region (parent children, or back to the matching `<!--$-->` for marker-bounded holes) so loose text re-claims positionally — elements already recovered via `_hk`.
33
+ - b431fe7: Handle module preload failures during hydration instead of hanging silently (solidjs/solid#2817 layer 3). `loadModuleAssets` drops rejected entries from the loading cache so later boundaries/navigations can retry, and the root `_assets` path in `hydrate()` falls back to a fresh client render (with a console diagnostic) instead of leaving the page permanently dead.
34
+ - 016b460: Server rendering a plain (non-template) object child now dev-warns and skips it, matching the client, instead of crashing with `Cannot read properties of undefined (reading 'fn')` (solidjs/solid#2801 bug 6)
35
+ - c40ac21: Fix style object updates so shared or constant style objects are not mutated while diffing, and nullish property values correctly remove the applied style.
36
+ - c2a542b: Fix hydration key mismatches when async holes defer past eager siblings
37
+ (solidjs/solid#2801 bug 2). Dynamic element children that can allocate
38
+ hydration ids (conditionals, component-children access, call expressions)
39
+ are now compiled with their own id scope on both generates: the dom and ssr
40
+ generates wrap the hole expression in a new `scope()` runtime helper using a
41
+ shared predicate, so marking cannot desync.
42
+
43
+ On the client, `scope(fn)` tags the accessor and `insert()` makes the outer
44
+ render effect non-transparent (its own id scope) for tagged accessors; the
45
+ inner unwrapping effect stays transparent so content ids keep a fixed depth.
46
+ On the server, `scope` (framework-provided via rxcore as `ssrScope`) reserves
47
+ one id slot at registration and evaluates the hole — including async retries
48
+ — under that reserved id with a zeroed child counter, so retry timing can no
49
+ longer shift sibling ids. The ssr generate's `orderedInsert` sibling
50
+ thunk-wrapping is removed; it is superseded by hole scopes.
51
+
52
+ Hole content ids gain one nesting level (e.g. `_hk=10` instead of `_hk=1`)
53
+ identically on both sides. rxcore implementations must provide an `ssrScope`
54
+ export and honor a `scope: true` effect option (mapped to a non-transparent
55
+ render effect).
56
+
57
+ - fa24389: Fix delegated events never reaching outer roots when a render root is
58
+ rendered inside another root's DOM (embedded widgets, microfrontends).
59
+ The first (innermost) container listener marked the event consumed for
60
+ every other root, so an outer root's delegated handlers were silently
61
+ skipped even though the native event bubbled through its elements: a
62
+ plain `addEventListener` on the same element fired while the delegated
63
+ handler didn't.
64
+
65
+ `$$EVENT_OWNER` now records the boundary of the most recent walk instead
66
+ of a consumed flag: an ancestor container whose subtree contains that boundary
67
+ resumes the handler walk from it up to its own boundary, so each root's
68
+ handlers fire exactly once, innermost-out, matching native bubbling.
69
+ `stopPropagation()` inside a nested root still suppresses outer roots (it
70
+ stops the native event before their listeners run), and hydration event
71
+ replay now relays queued events through all matching roots innermost-first
72
+ so pre- and post-hydration clicks behave identically. Apps that relied on
73
+ nested roots to isolate clicks from outer handlers should use
74
+ `stopPropagation()`, which remains the documented mechanism. Non-nested
75
+ apps are unaffected; the resume path is unreachable unless an inner root
76
+ already handled the event.
77
+
78
+ - 75b4ab2: Normalize manifest asset URL joining in `resolveAssets` (solidjs/solid#2817 layers 1-2). A non-string `_base` (e.g. a dev-manifest proxy answering every key) falls back to `/`, leading-slash `file` values no longer produce `//` URLs, and absolute/protocol-relative URLs pass through untouched — the server runtime emits sane module URLs for any reasonable manifest shape instead of relying on bundler plugins getting the contract exactly right.
79
+ - 668264f: Universal JSX now passes compile-time static host props to `createElement(tag, staticProps)` so custom renderers can configure nodes before children are inserted. Dynamic props and elements with spreads continue to use the existing `setProp` / `spread` paths.
80
+
3
81
  ## 0.50.0-next.15
4
82
 
5
83
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dom-expressions/runtime",
3
3
  "description": "A Fine-Grained Runtime for Performant DOM Rendering",
4
- "version": "0.50.0-next.15",
4
+ "version": "0.50.0-next.17",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "csstype": "^3.1",
27
27
  "seroval": "~1.5.0",
28
28
  "seroval-plugins": "~1.5.0",
29
- "@dom-expressions/babel-plugin-jsx": "0.50.0-next.15"
29
+ "@dom-expressions/babel-plugin-jsx": "0.50.0-next.17"
30
30
  },
31
31
  "scripts": {
32
32
  "test": "jest --no-cache",
package/src/client.d.ts CHANGED
@@ -23,6 +23,7 @@ export function render(
23
23
  * - `2` — the template html is wrapped; the outer tag is stripped at clone time.
24
24
  */
25
25
  export function template(html: string, flag?: 1 | 2): () => Element;
26
+ export function scope<T extends () => any>(fn: T): T;
26
27
  export function effect<T>(fn: (prev?: T) => T, effect: (value: T, prev?: T) => void): void;
27
28
  export function memo<T>(fn: () => T, equal: boolean): () => T;
28
29
  export function untrack<T>(fn: () => T): T;
@@ -102,6 +103,19 @@ export function getNextMatch(start: Node, elementName: string): Element;
102
103
  export function getNextMarker(start: Node): [Node, Array<Node>];
103
104
  export function useAssets(fn: () => JSX.Element): void;
104
105
  export function getAssets(): string;
106
+ export type AssetDescriptor =
107
+ | { type: "style"; href: string; attrs?: Record<string, string> }
108
+ | { type: "inline-style"; id: string; content?: string; attrs?: Record<string, string> }
109
+ | { type: "module"; href: string }
110
+ | ExclusiveAssetDescriptor<any>;
111
+ export interface ExclusiveAssetDescriptor<T> {
112
+ policy: "exclusive";
113
+ key: string;
114
+ value: T;
115
+ get(): T;
116
+ set(value: T): void;
117
+ }
118
+ export function acquireAsset(descriptor: AssetDescriptor): () => void;
105
119
  export function HydrationScript(props?: { nonce?: string; eventNames?: string[] }): JSX.Element;
106
120
  export function generateHydrationScript(options?: {
107
121
  nonce?: string;
package/src/client.js CHANGED
@@ -246,20 +246,45 @@ export function addEvent(node, name, handler, delegate) {
246
246
 
247
247
  export function style(node, value, prev) {
248
248
  if (!value) {
249
- if (prev) setAttribute(node, "style");
249
+ if (prev || node._$styles) {
250
+ setAttribute(node, "style");
251
+ node._$styles = undefined;
252
+ }
250
253
  return;
251
254
  }
252
255
  const nodeStyle = node.style;
253
- if (typeof value === "string") return (nodeStyle.cssText = value);
254
- typeof prev === "string" && (nodeStyle.cssText = prev = undefined);
255
- prev || (prev = {});
256
+ if (typeof value === "string") {
257
+ node._$styles = undefined;
258
+ return (nodeStyle.cssText = value);
259
+ }
260
+ if (typeof prev === "string") {
261
+ nodeStyle.cssText = "";
262
+ prev = undefined;
263
+ }
264
+ // Track declarations applied by style() itself. value/prev are user-owned
265
+ // and may be the same object on shared-effect reruns.
266
+ let applied = node._$styles;
267
+ if (!applied) {
268
+ // seed from prev so direct callers that track their own previous value
269
+ // still get removals on their first call here
270
+ applied = node._$styles = prev ? { ...prev } : {};
271
+ }
256
272
  let v, s;
273
+ for (s in applied) {
274
+ if (value[s] == null) {
275
+ nodeStyle.removeProperty(s);
276
+ delete applied[s];
277
+ }
278
+ }
279
+ // Diff against applied state so in-place mutations are detected without
280
+ // rewriting unchanged DOM styles.
257
281
  for (s in value) {
258
282
  v = value[s];
259
- if (v !== prev[s]) nodeStyle.setProperty(s, v);
260
- delete prev[s];
283
+ if (v != null && v !== applied[s]) {
284
+ nodeStyle.setProperty(s, v);
285
+ applied[s] = v;
286
+ }
261
287
  }
262
- for (s in prev) value[s] == null && nodeStyle.removeProperty(s);
263
288
  }
264
289
 
265
290
  export function setStyleProperty(node, name, value) {
@@ -311,20 +336,35 @@ export function ref(fn, element) {
311
336
  runWithOwner(null, () => applyRef(resolved, element));
312
337
  }
313
338
 
339
+ // Compiler tag for holes that can allocate hydration ids: the outer insert
340
+ // effect gets its own (non-transparent) id scope, mirroring the server's
341
+ // `scope()` owner. Keeps sibling ids stable no matter when the hole runs.
342
+ export function scope(fn) {
343
+ fn.$s = true;
344
+ return fn;
345
+ }
346
+
347
+ const SCOPE_OPTIONS = { scope: true };
348
+
349
+ // Drop the `<!--!$-->` text-hole separators the server emits so adjacent
350
+ // text nodes stay individually claimable; the array is compacted in place.
351
+ function stripTextSeparators(nodes) {
352
+ let j = 0;
353
+ for (let i = 0; i < nodes.length; i++) {
354
+ if (nodes[i].nodeType === 8 && nodes[i].nodeValue === "!$") nodes[i].remove();
355
+ else nodes[j++] = nodes[i];
356
+ }
357
+ nodes.length = j;
358
+ return nodes;
359
+ }
360
+
314
361
  export function insert(parent, accessor, marker, initial, options) {
315
362
  const multi = marker !== undefined;
316
363
  const host = options && options.host;
317
364
  if (multi && !initial) initial = [];
318
365
  if (isHydrating(parent)) {
319
366
  if (!multi && initial === undefined && parent) initial = [...parent.childNodes];
320
- if (Array.isArray(initial)) {
321
- let j = 0;
322
- for (let i = 0; i < initial.length; i++) {
323
- if (initial[i].nodeType === 8 && initial[i].nodeValue === "!$") initial[i].remove();
324
- else initial[j++] = initial[i];
325
- }
326
- initial.length = j;
327
- }
367
+ if (Array.isArray(initial)) stripTextSeparators(initial);
328
368
  }
329
369
  if (typeof accessor !== "function") {
330
370
  accessor = normalize(accessor, initial, multi, true);
@@ -340,12 +380,44 @@ export function insert(parent, accessor, marker, initial, options) {
340
380
  initial = [placeholder];
341
381
  }
342
382
  let current = initial;
383
+ // A streamed `$df` fragment swap replaces a hole's region out from under
384
+ // its bookkeeping (Loading fallback claimed during hydration, settled
385
+ // content swapped in later). When the tracked nodes are gone mid-hydration,
386
+ // re-claim the live region so the content pass can match loose text
387
+ // positionally — elements recover through the registry, text only has
388
+ // position. The region is `parent`'s children, or for marker-bounded holes
389
+ // the nodes back to the matching `<!--$-->` start marker.
390
+ function reclaimSwappedRegion() {
391
+ if (!sharedConfig.hydrating || !current || !parent.isConnected) return;
392
+ const first = Array.isArray(current) ? current[0] : current;
393
+ if (!first || !first.nodeType || first.isConnected) return;
394
+ let nodes;
395
+ if (marker) {
396
+ nodes = [];
397
+ let node = marker.previousSibling,
398
+ depth = 0;
399
+ while (node) {
400
+ if (node.nodeType === 8) {
401
+ const v = node.nodeValue;
402
+ if (v === "/") depth++;
403
+ else if (v === "$") {
404
+ if (depth === 0) break;
405
+ depth--;
406
+ }
407
+ }
408
+ nodes.unshift(node);
409
+ node = node.previousSibling;
410
+ }
411
+ } else nodes = [...parent.childNodes];
412
+ current = stripTextSeparators(nodes);
413
+ }
343
414
  effect(
344
415
  prev => {
416
+ reclaimSwappedRegion();
345
417
  const value = normalize(accessor(), current, multi, true);
346
418
  if (typeof value !== "function") return value;
347
419
  effect(
348
- () => normalize(value, current, multi),
420
+ () => (reclaimSwappedRegion(), normalize(value, current, multi)),
349
421
  inner => {
350
422
  insertExpression(parent, inner, current, marker);
351
423
  current = inner;
@@ -363,7 +435,9 @@ export function insert(parent, accessor, marker, initial, options) {
363
435
  current = value;
364
436
  host && tagHost(current, host);
365
437
  },
366
- options
438
+ // Only the OUTER effect takes the scope — the inner unwrapping effect
439
+ // stays transparent so content ids keep a fixed depth per hole.
440
+ accessor.$s ? (options ? { ...options, scope: true } : SCOPE_OPTIONS) : options
367
441
  );
368
442
  }
369
443
 
@@ -385,20 +459,147 @@ export function assign(node, props, skipChildren, prevProps = {}, skipRef = fals
385
459
  }
386
460
  }
387
461
 
388
- // Module asset loading for hydration
462
+ // ---- Asset Registry ----
463
+ //
464
+ // Ref-counted client-side ownership of shared document assets. Consumers
465
+ // (routers, lazy wrappers, metadata components) acquire an asset when content
466
+ // that needs it mounts and release it on cleanup; the element is created — or
467
+ // an SSR/stream-emitted one adopted — on the first acquire and removed
468
+ // shortly after the last release. The removal grace period lets quick
469
+ // release/re-acquire cycles (route transitions sharing CSS) reuse the live
470
+ // element instead of flashing unstyled content while it reloads.
471
+ //
472
+ // Descriptor forms:
473
+ // { type: "style", href, attrs? } → <link rel="stylesheet">
474
+ // { type: "inline-style", id, content?, attrs? } → <style data-asset={id}>
475
+ // { type: "module", href } → <link rel="modulepreload">
476
+ // { policy: "exclusive", key, value, get, set } → singleton slot
477
+ //
478
+ // Exclusive slots implement last-writer-wins with restore-on-release (the
479
+ // title/meta ownership model): the newest acquire's value is applied via
480
+ // `set`; releasing it re-applies the previous writer's value, and the
481
+ // original document value (captured with `get` on first acquire) once every
482
+ // writer has released.
483
+
484
+ const ASSET_REMOVAL_GRACE = 100;
485
+ const assetRegistry = new Map();
486
+
487
+ function assetEntryKey(descriptor) {
488
+ if (descriptor.policy === "exclusive") return "x|" + descriptor.key;
489
+ return descriptor.type === "inline-style"
490
+ ? "i|" + descriptor.id
491
+ : descriptor.type + "|" + descriptor.href;
492
+ }
493
+
494
+ // Attribute-compared lookup (instead of an attribute selector) so href/id
495
+ // values never need selector escaping.
496
+ function findAssetElement(selector, attr, value) {
497
+ const nodes = document.querySelectorAll(selector);
498
+ for (let i = 0; i < nodes.length; i++) {
499
+ if (nodes[i].getAttribute(attr) === value) return nodes[i];
500
+ }
501
+ return null;
502
+ }
503
+
504
+ function mountAssetElement(descriptor) {
505
+ let el;
506
+ if (descriptor.type === "inline-style") {
507
+ el = findAssetElement("style[data-asset]", "data-asset", descriptor.id);
508
+ if (!el) {
509
+ el = document.createElement("style");
510
+ el.setAttribute("data-asset", descriptor.id);
511
+ el.textContent = descriptor.content || "";
512
+ }
513
+ } else {
514
+ const rel = descriptor.type === "module" ? "modulepreload" : "stylesheet";
515
+ el = findAssetElement(`link[rel="${rel}"]`, "href", descriptor.href);
516
+ if (!el) {
517
+ el = document.createElement("link");
518
+ el.rel = rel;
519
+ el.href = descriptor.href;
520
+ }
521
+ }
522
+ if (descriptor.attrs) {
523
+ for (const name in descriptor.attrs) el.setAttribute(name, descriptor.attrs[name]);
524
+ }
525
+ if (!el.isConnected) document.head.appendChild(el);
526
+ return el;
527
+ }
528
+
529
+ export function acquireAsset(descriptor) {
530
+ const key = assetEntryKey(descriptor);
531
+ let entry = assetRegistry.get(key);
532
+ if (descriptor.policy === "exclusive") {
533
+ if (!entry) {
534
+ entry = { original: descriptor.get(), set: descriptor.set, writers: [] };
535
+ assetRegistry.set(key, entry);
536
+ }
537
+ const writer = { value: descriptor.value };
538
+ entry.writers.push(writer);
539
+ entry.set(writer.value);
540
+ let released = false;
541
+ return () => {
542
+ if (released) return;
543
+ released = true;
544
+ const index = entry.writers.indexOf(writer);
545
+ const wasTop = index === entry.writers.length - 1;
546
+ entry.writers.splice(index, 1);
547
+ if (!wasTop) return;
548
+ if (entry.writers.length) {
549
+ entry.set(entry.writers[entry.writers.length - 1].value);
550
+ } else {
551
+ entry.set(entry.original);
552
+ assetRegistry.delete(key);
553
+ }
554
+ };
555
+ }
556
+ if (!entry) {
557
+ entry = { count: 0, element: null, timer: null };
558
+ assetRegistry.set(key, entry);
559
+ }
560
+ if (entry.timer) {
561
+ clearTimeout(entry.timer);
562
+ entry.timer = null;
563
+ }
564
+ entry.count++;
565
+ if (!entry.element || !entry.element.isConnected) entry.element = mountAssetElement(descriptor);
566
+ let released = false;
567
+ return () => {
568
+ if (released) return;
569
+ released = true;
570
+ if (--entry.count > 0) return;
571
+ entry.timer = setTimeout(() => {
572
+ assetRegistry.delete(key);
573
+ entry.element && entry.element.remove();
574
+ }, ASSET_REMOVAL_GRACE);
575
+ };
576
+ }
577
+
578
+ // Module asset loading for hydration. `mapping` pairs opaque keys with
579
+ // client-loadable entry URLs. Keys are chosen by the reactive library's
580
+ // server-side lazy() (e.g. hydration ids) and are never interpreted here —
581
+ // they only need to match what the client-side lazy() looks up in
582
+ // `_$HY.modules` after preload.
389
583
  function loadModuleAssets(mapping) {
390
584
  const hy = globalThis._$HY;
391
585
  if (!hy) return;
392
586
  const pending = [];
393
- for (const moduleUrl in mapping) {
394
- if (hy.modules[moduleUrl]) continue;
395
- const entryUrl = mapping[moduleUrl];
396
- if (!hy.loading[moduleUrl]) {
397
- hy.loading[moduleUrl] = import(/* @vite-ignore */ entryUrl).then(mod => {
398
- hy.modules[moduleUrl] = mod;
399
- });
587
+ for (const key in mapping) {
588
+ if (hy.modules[key]) continue;
589
+ const entryUrl = mapping[key];
590
+ if (!hy.loading[key]) {
591
+ hy.loading[key] = import(/* @vite-ignore */ entryUrl).then(
592
+ mod => {
593
+ hy.modules[key] = mod;
594
+ },
595
+ err => {
596
+ // drop the rejected entry so a later boundary/navigation can retry
597
+ delete hy.loading[key];
598
+ throw err;
599
+ }
600
+ );
400
601
  }
401
- pending.push(hy.loading[moduleUrl]);
602
+ pending.push(hy.loading[key]);
402
603
  }
403
604
  return pending.length ? Promise.all(pending).then(() => {}) : undefined;
404
605
  }
@@ -460,8 +661,15 @@ export function hydrate(code, element, options = {}) {
460
661
  sharedConfig.hydrating = false;
461
662
  }
462
663
  },
463
- () => {
664
+ err => {
665
+ // A chunk failed to preload; hydration can't claim the server DOM
666
+ // (lazy components have no module). Fall back to a fresh client
667
+ // render replacing the server markup — lazy's own import() gets to
668
+ // retry through normal channels — instead of a silently dead page.
669
+ console.error("Hydration module preload failed, falling back to client render:", err);
464
670
  sharedConfig.hydrating = false;
671
+ sharedConfig.registry = undefined;
672
+ disposer = render(code, element, [...element.childNodes], options);
465
673
  }
466
674
  );
467
675
  return () => disposer && disposer();
@@ -597,14 +805,34 @@ export function runHydrationEvents() {
597
805
  const [el, e] = events[0];
598
806
  if (!completed.has(el)) return;
599
807
  events.shift();
600
- let match;
808
+ let matchContainer, matchState, matchDistance, matches;
601
809
  for (const [container, state] of delegatedContainers) {
602
810
  if (!state.handlers.has(e.type)) continue;
603
811
  const entry = findOwner(e.target, state);
604
- if (entry && (!match || entry.distance < match.distance))
605
- match = { container, state, distance: entry.distance };
812
+ if (!entry) continue;
813
+ if (matchContainer) {
814
+ if (!matches)
815
+ matches = [
816
+ {
817
+ container: matchContainer,
818
+ state: matchState,
819
+ distance: matchDistance
820
+ }
821
+ ];
822
+ matches.push({ container, state, distance: entry.distance });
823
+ } else {
824
+ matchContainer = container;
825
+ matchState = state;
826
+ matchDistance = entry.distance;
827
+ }
606
828
  }
607
- if (match) eventHandler(e, match.container, match.state);
829
+ if (matches) {
830
+ // Replay innermost-first so queued hydration events follow the same
831
+ // root-boundary handoff as live native bubbling.
832
+ matches.sort((a, b) => a.distance - b.distance);
833
+ for (let i = 0; i < matches.length; i++)
834
+ eventHandler(e, matches[i].container, matches[i].state);
835
+ } else if (matchContainer) eventHandler(e, matchContainer, matchState);
608
836
  }
609
837
  if (sharedConfig.done) {
610
838
  sharedConfig.events = _$HY.events = null;
@@ -696,7 +924,15 @@ function eventHandler(e, container, state) {
696
924
  if (sharedConfig.registry && sharedConfig.events) {
697
925
  if (sharedConfig.events.find(([el, ev]) => ev === e)) return;
698
926
  }
699
- if (e[$$EVENT_OWNER]) return;
927
+ const prev = e[$$EVENT_OWNER];
928
+ let resumeNode;
929
+ if (prev) {
930
+ // An inner root already walked its segment. Ancestor roots resume from
931
+ // that boundary; unrelated/shared containers must not see the event as
932
+ // theirs. Native stopPropagation still prevents this listener from running.
933
+ if (prev === true || prev === container || !container.contains(prev)) return;
934
+ resumeNode = prev;
935
+ }
700
936
  const owner =
701
937
  state &&
702
938
  (state.owners.size === 1 && state.owners.has(container)
@@ -705,7 +941,7 @@ function eventHandler(e, container, state) {
705
941
  if (state && !owner) return;
706
942
  e[$$EVENT_OWNER] = owner || true;
707
943
 
708
- let node = e.target;
944
+ let node = resumeNode || e.target;
709
945
  const key = `$$${e.type}`;
710
946
  const oriTarget = e.target;
711
947
  const boundary = owner || container || e.currentTarget;
@@ -742,7 +978,13 @@ function eventHandler(e, container, state) {
742
978
  return node || boundary || document;
743
979
  }
744
980
  });
745
- if (e.composedPath) {
981
+ if (resumeNode) {
982
+ // If the boundary was the target, the inner walk already fired it.
983
+ // Resume above it so boundary handlers do not run twice.
984
+ if (resumeNode === e.target)
985
+ node = resumeNode._$host || resumeNode.parentNode || resumeNode.host;
986
+ if (node && node !== boundary) walkUpTree();
987
+ } else if (e.composedPath) {
746
988
  const path = e.composedPath();
747
989
  if (path.length) {
748
990
  retarget(path[0]);
@@ -777,7 +1019,16 @@ function insertExpression(parent, value, current, marker) {
777
1019
  const tc = typeof current;
778
1020
  if (tc === "string" || tc === "number") {
779
1021
  parent.firstChild.data = value;
780
- } else parent.textContent = value;
1022
+ } else {
1023
+ const owned = ownedChildCount(current);
1024
+ if (owned === -1 || owned === parent.childNodes.length) parent.textContent = value;
1025
+ else {
1026
+ // Foreign nodes present (e.g. stream-injected stylesheet links) —
1027
+ // replace only our own nodes, keeping text content leading.
1028
+ removeOwnedChildren(parent, current);
1029
+ parent.insertBefore(document.createTextNode(value), parent.firstChild);
1030
+ }
1031
+ }
781
1032
  } else if (value === undefined) {
782
1033
  cleanChildren(parent, current, marker);
783
1034
  } else if (value.nodeType) {
@@ -806,7 +1057,7 @@ function insertExpression(parent, value, current, marker) {
806
1057
  appendNodes(parent, value, marker);
807
1058
  } else reconcileArrays(parent, current, value, marker);
808
1059
  } else {
809
- current && cleanChildren(parent);
1060
+ current && cleanChildren(parent, current);
810
1061
  appendNodes(parent, value);
811
1062
  }
812
1063
  } else if ("_DX_DEV_") console.warn(`Unrecognized value. Skipped inserting`, value);
@@ -855,8 +1106,45 @@ function appendNodes(parent, array, marker = null) {
855
1106
  }
856
1107
  }
857
1108
 
1109
+ // Number of parent children the tracked `current` value accounts for, or -1
1110
+ // when unknown (initial render / untracked content — the whole parent is
1111
+ // considered owned, preserving the designed clear-on-first-render behavior).
1112
+ function ownedChildCount(current) {
1113
+ if (Array.isArray(current)) return current.length;
1114
+ if (current == null) return -1;
1115
+ if (current === "") return 0; // `textContent = ""` left no node behind
1116
+ return 1; // single node, or a string/number rendered as one text node
1117
+ }
1118
+
1119
+ // Remove only the nodes tracked by `current` from a root-level (markerless)
1120
+ // region, leaving foreign siblings in place.
1121
+ function removeOwnedChildren(parent, current) {
1122
+ if (Array.isArray(current)) {
1123
+ for (let i = 0; i < current.length; i++) {
1124
+ const el = current[i];
1125
+ if (el.parentNode === parent) el.remove();
1126
+ }
1127
+ } else if (current.nodeType) {
1128
+ if (current.parentNode === parent) current.remove();
1129
+ } else {
1130
+ // string/number content lives in a leading text node (set via
1131
+ // `textContent = value`, before any foreign node was appended).
1132
+ const first = parent.firstChild;
1133
+ if (first && first.nodeType === 3) first.remove();
1134
+ }
1135
+ }
1136
+
858
1137
  function cleanChildren(parent, current, marker, replacement) {
859
- if (marker === undefined) return (parent.textContent = "");
1138
+ if (marker === undefined) {
1139
+ // Root-level clear (no marker). `textContent = ""` wipes every child,
1140
+ // which is only safe when the nodes we track are the parent's only
1141
+ // children. Streaming can append foreign nodes to the root (late-flushed
1142
+ // stylesheet <link>s land at the end of <body>) and those must survive a
1143
+ // re-render — wiping them drops loaded CSS.
1144
+ const owned = ownedChildCount(current);
1145
+ if (owned === -1 || owned === parent.childNodes.length) return (parent.textContent = "");
1146
+ return removeOwnedChildren(parent, current);
1147
+ }
860
1148
  if (current.length) {
861
1149
  let inserted = false;
862
1150
  for (let i = current.length - 1; i >= 0; i--) {
package/src/server.d.ts CHANGED
@@ -75,6 +75,7 @@ export function ssrStyle(value: string | { [k: string]: string }): string;
75
75
  export function ssrStyleProperty(name: string, value: any): string;
76
76
  export function ssrAttribute(key: string, value: any): string;
77
77
  export function ssrGroup<T extends () => any[]>(fn: T, n: number): T;
78
+ export function scope<T>(fn: () => T): () => unknown;
78
79
  export function ssrHydrationKey(): string;
79
80
  export function resolveSSRNode(node: any, result?: any, top?: boolean): any;
80
81
  export function escape(s: any, attr?: boolean): any;
@@ -191,3 +192,5 @@ export function ref(
191
192
  ): void;
192
193
  /** @deprecated not supported on the server side */
193
194
  export function setStyleProperty(node: Element, name: string, value: any): void;
195
+ /** @deprecated not supported on the server side — register assets through the render context instead */
196
+ export function acquireAsset(descriptor: unknown): () => void;
package/src/server.js CHANGED
@@ -7,6 +7,10 @@ import { createSerializer, getLocalHeaderScript } from "./serializer";
7
7
  // core, and a local copy here drifts from them (it resolved function sources
8
8
  // for key enumeration only, dropping their values in SSR output).
9
9
  export { createComponent, effect, memo, untrack, mergeProps } from "rxcore";
10
+ // Hole owner scope (`_$scope(...)` in compiled ssr output) — owner-creating
11
+ // wrapper for deferred child holes that can allocate hydration ids. The
12
+ // framework owns the implementation (owner creation + per-attempt reset).
13
+ export { ssrScope as scope } from "rxcore";
10
14
  export { getOwner };
11
15
 
12
16
  export {
@@ -23,9 +27,22 @@ export {
23
27
 
24
28
  // ---- Asset Manifest ----
25
29
 
30
+ // Join defensively rather than trusting the manifest's shape: dev manifests
31
+ // have answered `_base` with non-strings and emitted `file` values with a
32
+ // leading slash (solidjs/solid#2817 layers 1-2). Normalizing here keeps the
33
+ // emitted URLs sane for any reasonable manifest instead of playing contract
34
+ // ping-pong with bundler plugins.
35
+ function joinAssetPath(base, file) {
36
+ // absolute (`https://cdn/x.js`) and protocol-relative (`//cdn/x.js`) pass through
37
+ if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(file)) return file;
38
+ if (typeof base !== "string" || !base) base = "/";
39
+ if (base[base.length - 1] !== "/") base += "/";
40
+ return base + (file[0] === "/" ? file.slice(1) : file);
41
+ }
42
+
26
43
  function resolveAssets(moduleUrl, manifest) {
27
44
  if (!manifest) return null;
28
- const base = manifest._base || "/";
45
+ const base = manifest._base;
29
46
  const entry = manifest[moduleUrl];
30
47
  if (!entry) return null;
31
48
  const css = [];
@@ -36,8 +53,8 @@ function resolveAssets(moduleUrl, manifest) {
36
53
  visited.add(key);
37
54
  const e = manifest[key];
38
55
  if (!e) return;
39
- js.push(base + e.file);
40
- if (e.css) for (let i = 0; i < e.css.length; i++) css.push(base + e.css[i]);
56
+ js.push(joinAssetPath(base, e.file));
57
+ if (e.css) for (let i = 0; i < e.css.length; i++) css.push(joinAssetPath(base, e.css[i]));
41
58
  if (e.imports) for (let i = 0; i < e.imports.length; i++) walk(e.imports[i]);
42
59
  };
43
60
  walk(moduleUrl);
@@ -65,25 +82,49 @@ function createAssetTracking() {
65
82
  const boundaryModules = new Map();
66
83
  const boundaryStyles = new Map();
67
84
  const emittedAssets = new Set();
85
+ const inlineStyles = new Map();
68
86
  let currentBoundaryId = null;
69
87
  return {
70
88
  boundaryModules,
71
89
  boundaryStyles,
72
90
  emittedAssets,
91
+ inlineStyles,
92
+ // Inline styles (dev CSS collected from the module graph, critical CSS)
93
+ // dedupe by `id` — repeated registrations reuse the same entry object so
94
+ // boundary Sets and the head injection never emit the same style twice.
95
+ registerInlineStyle(desc) {
96
+ let entry = inlineStyles.get(desc.id);
97
+ if (!entry) {
98
+ entry = { id: desc.id, content: desc.content || "", attrs: desc.attrs, emitted: false };
99
+ inlineStyles.set(desc.id, entry);
100
+ }
101
+ if (currentBoundaryId) {
102
+ let styles = boundaryStyles.get(currentBoundaryId);
103
+ if (!styles) {
104
+ styles = new Set();
105
+ boundaryStyles.set(currentBoundaryId, styles);
106
+ }
107
+ styles.add(entry);
108
+ }
109
+ return entry;
110
+ },
73
111
  get currentBoundaryId() {
74
112
  return currentBoundaryId;
75
113
  },
76
114
  set currentBoundaryId(v) {
77
115
  currentBoundaryId = v;
78
116
  },
79
- registerModule(moduleUrl, entryUrl) {
117
+ // `key` is opaque to the runtime — the reactive library's server-side
118
+ // lazy() picks it (e.g. a hydration id) and its client-side counterpart
119
+ // looks preloaded modules up under the same key after loadModuleAssets.
120
+ registerModule(key, entryUrl) {
80
121
  const id = currentBoundaryId || "";
81
122
  let map = boundaryModules.get(id);
82
123
  if (!map) {
83
124
  map = {};
84
125
  boundaryModules.set(id, map);
85
126
  }
86
- map[moduleUrl] = entryUrl;
127
+ map[key] = entryUrl;
87
128
  },
88
129
  getBoundaryModules(id) {
89
130
  return boundaryModules.get(id) || null;
@@ -161,16 +202,20 @@ export function renderToString(code, options = {}) {
161
202
  }
162
203
  serializer.write(id, p);
163
204
  },
164
- registerAsset(type, url) {
205
+ registerAsset(type, value) {
206
+ if (type === "inline-style") {
207
+ tracking.registerInlineStyle(value);
208
+ return;
209
+ }
165
210
  if (tracking.currentBoundaryId && type === "style") {
166
211
  let styles = tracking.boundaryStyles.get(tracking.currentBoundaryId);
167
212
  if (!styles) {
168
213
  styles = new Set();
169
214
  tracking.boundaryStyles.set(tracking.currentBoundaryId, styles);
170
215
  }
171
- styles.add(url);
216
+ styles.add(value);
172
217
  }
173
- tracking.emittedAssets.add(url);
218
+ tracking.emittedAssets.add(value);
174
219
  }
175
220
  };
176
221
  applyAssetTracking(sharedConfig.context, tracking, manifest);
@@ -187,6 +232,7 @@ export function renderToString(code, options = {}) {
187
232
  serializer.close();
188
233
  html = injectAssets(sharedConfig.context.assets, html);
189
234
  html = injectPreloadLinks(tracking.emittedAssets, html, nonce);
235
+ html = injectInlineStyles(tracking.inlineStyles, html, nonce);
190
236
  if (scripts.length) html = injectScripts(html, scripts, options.nonce);
191
237
  return html;
192
238
  }
@@ -296,19 +342,30 @@ export function renderToStream(code, options = {}) {
296
342
  async: true,
297
343
  assets: [],
298
344
  nonce,
299
- registerAsset(type, url) {
345
+ registerAsset(type, value) {
346
+ if (type === "inline-style") {
347
+ const entry = tracking.registerInlineStyle(value);
348
+ // Boundary-attributed inline styles flush with their fragment; a late
349
+ // registration outside any boundary has no other emission point, so
350
+ // write the tag into the stream immediately.
351
+ if (firstFlushed && !tracking.currentBoundaryId && !entry.emitted) {
352
+ entry.emitted = true;
353
+ buffer.write(renderInlineStyle(entry, nonce));
354
+ }
355
+ return;
356
+ }
300
357
  if (tracking.currentBoundaryId && type === "style") {
301
358
  let styles = tracking.boundaryStyles.get(tracking.currentBoundaryId);
302
359
  if (!styles) {
303
360
  styles = new Set();
304
361
  tracking.boundaryStyles.set(tracking.currentBoundaryId, styles);
305
362
  }
306
- styles.add(url);
363
+ styles.add(value);
307
364
  }
308
- if (!tracking.emittedAssets.has(url)) {
309
- tracking.emittedAssets.add(url);
365
+ if (!tracking.emittedAssets.has(value)) {
366
+ tracking.emittedAssets.add(value);
310
367
  if (firstFlushed && type === "module") {
311
- buffer.write(`<link rel="modulepreload" href="${url}">`);
368
+ buffer.write(`<link rel="modulepreload" href="${value}">`);
312
369
  }
313
370
  }
314
371
  },
@@ -401,10 +458,15 @@ export function renderToStream(code, options = {}) {
401
458
  serializeFragmentAssets(key, tracking.boundaryModules, context);
402
459
  const styles = collectStreamStyles(key, tracking, headStyles);
403
460
  const deferActivation = !!revealGroup;
404
- if (styles.length) {
405
- emitTask(`$dfs("${key}",${styles.length},${deferActivation ? 1 : 0})`);
461
+ // Inline styles apply as soon as the parser sees them — emit
462
+ // before the template, no load gating needed.
463
+ for (let i = 0; i < styles.inline.length; i++) {
464
+ buffer.write(renderInlineStyle(styles.inline[i], nonce));
465
+ }
466
+ if (styles.links.length) {
467
+ emitTask(`$dfs("${key}",${styles.links.length},${deferActivation ? 1 : 0})`);
406
468
  writeTasks();
407
- for (const url of styles) {
469
+ for (const url of styles.links) {
408
470
  buffer.write(
409
471
  `<link rel="stylesheet" href="${url}" onload="$dfc('${key}')" onerror="$dfc('${key}')">`
410
472
  );
@@ -499,6 +561,7 @@ export function renderToStream(code, options = {}) {
499
561
  if (url.endsWith(".css")) headStyles.add(url);
500
562
  }
501
563
  html = injectPreloadLinks(tracking.emittedAssets, html, nonce);
564
+ html = injectInlineStyles(tracking.inlineStyles, html, nonce);
502
565
  serializeRootAssets();
503
566
  if (tasks.length) html = injectScripts(html, tasks, nonce);
504
567
  buffer.write(html);
@@ -853,11 +916,17 @@ export function ssrStyleProperty(name, value) {
853
916
 
854
917
  // review with new ssr
855
918
  export function ssrElement(tag, props, children, needsId) {
919
+ // The hydration key must be allocated before the props thunk runs: dynamic
920
+ // props (`mergeProps(() => ...)`) create a memo, which consumes a child id.
921
+ // The client claims the element (getNextElement) before applying the spread,
922
+ // so the server must allocate in the same order or the element's own id
923
+ // shifts by one and it is left unclaimed on hydration.
924
+ const hk = needsId ? ssrHydrationKey() : "";
856
925
  if (props == null) props = {};
857
926
  else if (typeof props === "function") props = props();
858
927
  const skipChildren = VOID_ELEMENTS.test(tag);
859
928
  const keys = Object.keys(props);
860
- let result = `<${tag}${needsId ? ssrHydrationKey() : ""} `;
929
+ let result = `<${tag}${hk} `;
861
930
  for (let i = 0; i < keys.length; i++) {
862
931
  const prop = keys[i];
863
932
  if (ChildProperties.has(prop)) {
@@ -918,7 +987,16 @@ export function escape(s, attr) {
918
987
  for (let i = 0; i < s.length; i++) s[i] = escape(s[i]);
919
988
  return s;
920
989
  }
921
- if (attr && t === "boolean") return s;
990
+ if (attr) {
991
+ // Nullish and boolean values pass through so callers can omit the
992
+ // attribute or emit it as a boolean attribute. Numbers can never
993
+ // contain `&` or `"`. Everything else (arrays, objects, symbols)
994
+ // would be stringified by the surrounding template literal anyway,
995
+ // so coerce to the final string here first — matching what the
996
+ // client DOM receives — and run it through the normal string path.
997
+ if (s == null || t === "boolean" || t === "number") return s;
998
+ return escape(String(s), attr);
999
+ }
922
1000
  return s;
923
1001
  }
924
1002
  // Fast path: single forward pass over the string. Most values (color
@@ -1083,16 +1161,56 @@ function propagateBoundaryStyles(childKey, parentKey, tracking) {
1083
1161
  }
1084
1162
  }
1085
1163
 
1164
+ // Boundary style sets hold two kinds of entries: url strings (stylesheet
1165
+ // links, load-gated via $dfs) and inline style entry objects (emitted as
1166
+ // <style> tags, ready as soon as parsed). Splits them for the fragment
1167
+ // flush, consuming inline entries so they emit at most once.
1086
1168
  function collectStreamStyles(key, tracking, headStyles) {
1087
1169
  const styles = tracking.getBoundaryStyles(key);
1088
- if (!styles) return [];
1089
- const result = [];
1090
- for (const url of styles) {
1091
- if (!headStyles || !headStyles.has(url)) {
1092
- result.push(url);
1170
+ const links = [];
1171
+ const inline = [];
1172
+ if (!styles) return { links, inline };
1173
+ for (const entry of styles) {
1174
+ if (typeof entry === "string") {
1175
+ if (!headStyles || !headStyles.has(entry)) links.push(entry);
1176
+ } else if (!entry.emitted) {
1177
+ entry.emitted = true;
1178
+ inline.push(entry);
1093
1179
  }
1094
1180
  }
1095
- return result;
1181
+ return { links, inline };
1182
+ }
1183
+
1184
+ // `</style` inside content would close the tag early; escaping the slash is
1185
+ // valid CSS and neutralizes the sequence.
1186
+ function escapeStyleContent(content) {
1187
+ return content.replace(/<\/(style)/gi, "<\\/$1");
1188
+ }
1189
+
1190
+ function renderInlineStyle(entry, nonce) {
1191
+ let attrs = "";
1192
+ if (entry.attrs) {
1193
+ for (const name in entry.attrs) {
1194
+ attrs += ` ${name}="${escape(String(entry.attrs[name]), true)}"`;
1195
+ }
1196
+ }
1197
+ return `<style${nonce ? ` nonce="${nonce}"` : ""} data-asset="${escape(
1198
+ entry.id,
1199
+ true
1200
+ )}"${attrs}>${escapeStyleContent(entry.content)}</style>`;
1201
+ }
1202
+
1203
+ function injectInlineStyles(inlineStyles, html, nonce) {
1204
+ if (!inlineStyles.size) return html;
1205
+ const index = html.indexOf("</head>");
1206
+ if (index === -1) return html;
1207
+ let out = "";
1208
+ for (const entry of inlineStyles.values()) {
1209
+ if (entry.emitted) continue;
1210
+ entry.emitted = true;
1211
+ out += renderInlineStyle(entry, nonce);
1212
+ }
1213
+ return out ? html.slice(0, index) + out + html.slice(index) : html;
1096
1214
  }
1097
1215
 
1098
1216
  function injectScripts(html, scripts, nonce) {
@@ -1178,6 +1296,12 @@ function tryResolveString(node) {
1178
1296
  return s;
1179
1297
  }
1180
1298
  if (node.h && node.h.length > 0) return { merge: node };
1299
+ if (node.t === undefined) {
1300
+ // Not a template object — mirror the client's dev warn-and-skip
1301
+ // instead of crashing downstream on a malformed template shape.
1302
+ if ("_DX_DEV_") console.warn(`Unrecognized value. Skipped inserting`, node);
1303
+ return "";
1304
+ }
1181
1305
  return Array.isArray(node.t) ? node.t[0] : node.t;
1182
1306
  }
1183
1307
  if (t === "function") {
@@ -1225,7 +1349,9 @@ function resolveSSRNode(
1225
1349
  result.h.push(...node.h);
1226
1350
  result.p.push(...node.p);
1227
1351
  }
1228
- } else result.t[result.t.length - 1] += node.t;
1352
+ } else if (node.t !== undefined) {
1353
+ result.t[result.t.length - 1] += node.t;
1354
+ } else if ("_DX_DEV_") console.warn(`Unrecognized value. Skipped inserting`, node);
1229
1355
  } else if (t === "function") {
1230
1356
  try {
1231
1357
  resolveSSRNode(node(), result);
@@ -1292,7 +1418,8 @@ export {
1292
1418
  notSup as getNextMarker,
1293
1419
  notSup as runHydrationEvents,
1294
1420
  notSup as ref,
1295
- notSup as setStyleProperty
1421
+ notSup as setStyleProperty,
1422
+ notSup as acquireAsset
1296
1423
  };
1297
1424
 
1298
1425
  function notSup() {
@@ -1,5 +1,5 @@
1
1
  export interface RendererOptions<NodeType> {
2
- createElement(tag: string): NodeType;
2
+ createElement(tag: string, staticProps?: Record<string, unknown>): NodeType;
3
3
  createTextNode(value: string): NodeType;
4
4
  createSentinel?(): NodeType;
5
5
  replaceText(textNode: NodeType, value: string): void;
@@ -18,7 +18,7 @@ export interface Renderer<NodeType> {
18
18
  effect<T>(fn: (prev?: T) => T, effect: (value: T, prev?: T) => void): void;
19
19
  memo<T>(fn: () => T, equal: boolean): () => T;
20
20
  createComponent<T>(Comp: (props: T) => NodeType, props: T): NodeType;
21
- createElement(tag: string): NodeType;
21
+ createElement(tag: string, staticProps?: Record<string, unknown>): NodeType;
22
22
  createTextNode(value: string): NodeType;
23
23
  insertNode(parent: NodeType, node: NodeType, anchor?: NodeType): void;
24
24
  insert<T>(parent: any, accessor: (() => T) | T, marker?: any | null, initial?: any): NodeType;