ripple 0.3.84 → 0.3.85

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 (53) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +3 -3
  3. package/src/constants.js +4 -0
  4. package/src/runtime/internal/client/component.js +3 -3
  5. package/src/runtime/internal/client/constants.js +4 -0
  6. package/src/runtime/internal/client/for.js +22 -1
  7. package/src/runtime/internal/client/if.js +18 -2
  8. package/src/runtime/internal/client/index.js +1 -0
  9. package/src/runtime/internal/client/operations.js +25 -0
  10. package/src/runtime/internal/client/runtime.js +62 -10
  11. package/src/runtime/internal/client/switch.js +16 -2
  12. package/src/runtime/internal/client/template.js +10 -2
  13. package/src/runtime/internal/client/try.js +12 -2
  14. package/src/runtime/internal/client/types.d.ts +4 -0
  15. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -2
  16. package/tests/client/compiler/compiler.basic.test.tsrx +5 -2
  17. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -4
  18. package/tests/client/scoped-flush.test.tsrx +105 -0
  19. package/tests/hydration/compiled/client/basic.js +198 -214
  20. package/tests/hydration/compiled/client/composite.js +23 -72
  21. package/tests/hydration/compiled/client/for.js +66 -72
  22. package/tests/hydration/compiled/client/hmr.js +2 -13
  23. package/tests/hydration/compiled/client/html.js +619 -479
  24. package/tests/hydration/compiled/client/if-children.js +72 -84
  25. package/tests/hydration/compiled/client/if.js +87 -92
  26. package/tests/hydration/compiled/client/mixed-control-flow.js +120 -160
  27. package/tests/hydration/compiled/client/nested-control-flow.js +288 -360
  28. package/tests/hydration/compiled/client/portal.js +15 -21
  29. package/tests/hydration/compiled/client/reactivity.js +7 -12
  30. package/tests/hydration/compiled/client/switch.js +113 -115
  31. package/tests/hydration/compiled/client/track-async-serialization.js +71 -122
  32. package/tests/hydration/compiled/client/try.js +26 -41
  33. package/tests/hydration/compiled/server/basic.js +268 -556
  34. package/tests/hydration/compiled/server/composite.js +31 -50
  35. package/tests/hydration/compiled/server/events.js +37 -173
  36. package/tests/hydration/compiled/server/for.js +236 -847
  37. package/tests/hydration/compiled/server/head.js +110 -241
  38. package/tests/hydration/compiled/server/hmr.js +14 -41
  39. package/tests/hydration/compiled/server/html-in-template.js +14 -38
  40. package/tests/hydration/compiled/server/html.js +951 -1176
  41. package/tests/hydration/compiled/server/if-children.js +59 -493
  42. package/tests/hydration/compiled/server/if.js +57 -209
  43. package/tests/hydration/compiled/server/mixed-control-flow.js +144 -250
  44. package/tests/hydration/compiled/server/nested-control-flow.js +309 -559
  45. package/tests/hydration/compiled/server/portal.js +94 -156
  46. package/tests/hydration/compiled/server/reactivity.js +23 -65
  47. package/tests/hydration/compiled/server/return.js +6 -16
  48. package/tests/hydration/compiled/server/switch.js +91 -219
  49. package/tests/hydration/compiled/server/track-async-serialization.js +211 -256
  50. package/tests/hydration/compiled/server/try.js +67 -126
  51. package/tests/hydration/components/html.tsrx +75 -0
  52. package/tests/hydration/html.test.js +50 -0
  53. package/tests/server/compiler.test.tsrx +6 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.85
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1307](https://github.com/Ripple-TS/ripple/pull/1307)
8
+ [`f55466b`](https://github.com/Ripple-TS/ripple/commit/f55466bde65d0cff00c0c4525af9d68ae794ffd2)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Skip the wrapper anchor for
10
+ single control-flow / code-block / component root scopes. When a scope's entire
11
+ renderable output is a single `@if`, `@switch`, `@for`, `@try`, or static child
12
+ component — i.e. a component body, a control-flow branch, or a `@{}` body whose
13
+ only output after setup is one of these — the compiler now renders it directly
14
+ before the parent-provided `__anchor` instead of synthesizing a `<!>` fragment
15
+ wrapper and an extra append + clone. For deep recursive trees this measurably
16
+ cuts mount time and shrinks generated output; in the recursive-context benchmark
17
+ it brought mount DOM operations to one clone + one append per element (from
18
+ ~1.5×) and halved the comment-anchor nodes.
19
+
20
+ Hydration is preserved. The control-flow runtimes
21
+ (`if_block`/`switch_block`/`for_block`/`for_block_keyed`/`try_block`) capture
22
+ the SSR boundary marker and hand it to `append()` afterward, so the existing
23
+ context-aware cursor advance still runs — including for a root scope used as a
24
+ child of a composite/slot with following siblings. Single-component roots need
25
+ no runtime change at all, since a component's own content advances the hydration
26
+ cursor.
27
+
28
+ Also relaxes the compiler's text-expression detection: `string + anything` (e.g.
29
+ `{a + '|' + b}`) is now recognized as text and lowered to the fast `set_text`
30
+ path without requiring an explicit `as string`, since such an expression always
31
+ evaluates to a string in JS.
32
+
33
+ - [#1307](https://github.com/Ripple-TS/ripple/pull/1307)
34
+ [`f55466b`](https://github.com/Ripple-TS/ripple/commit/f55466bde65d0cff00c0c4525af9d68ae794ffd2)
35
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Scope the client flush
36
+ traversal to the updated subtree. Previously every flush walked the whole root
37
+ block tree to find dirty subscribers, so a deeply-scoped update (e.g. mutating
38
+ state read by only a small subtree) paid a cost proportional to the entire tree
39
+ rather than the affected branch. `flush_updates` now descends only along the
40
+ routing path to each directly-scheduled block and fully scans just that block's
41
+ subtree, where its subscribers live. A tracked read from outside its owner's
42
+ subtree (e.g. smuggled across sibling subtrees via a module-level variable) is
43
+ detected in `register_dependency` and transparently falls back to the original
44
+ full-tree scan, so behavior is unchanged.
45
+
46
+ - Updated dependencies
47
+ [[`ba498cd`](https://github.com/Ripple-TS/ripple/commit/ba498cde76e9f83235ce91da825f403a28441bff),
48
+ [`313b351`](https://github.com/Ripple-TS/ripple/commit/313b3513e4a959dd80b546da41c798066c5ccb0f),
49
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
50
+ [`bbe6e74`](https://github.com/Ripple-TS/ripple/commit/bbe6e7422c690558f0dfcb3abe5452d4f4cdde91),
51
+ [`0e9f523`](https://github.com/Ripple-TS/ripple/commit/0e9f52358a615c2fc7759544e96c43dccb533c86),
52
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
53
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
54
+ [`2b65285`](https://github.com/Ripple-TS/ripple/commit/2b65285bfcd4c6a0aa93d7fa0b25082e6ec74e1f),
55
+ [`35ac700`](https://github.com/Ripple-TS/ripple/commit/35ac70052d79efae41bb1df2440fee3f052ca115),
56
+ [`f55466b`](https://github.com/Ripple-TS/ripple/commit/f55466bde65d0cff00c0c4525af9d68ae794ffd2),
57
+ [`b887deb`](https://github.com/Ripple-TS/ripple/commit/b887debf5f47e63d73184ac218ec8b3542a5e21c),
58
+ [`3668c5f`](https://github.com/Ripple-TS/ripple/commit/3668c5fe9cdaca4862707d653d23af94780f42af),
59
+ [`bbc3843`](https://github.com/Ripple-TS/ripple/commit/bbc384387e33c538234be36c07cc4b30ef6ce136)]:
60
+ - @tsrx/ripple@0.1.33
61
+ - @tsrx/core@0.1.33
62
+
3
63
  ## 0.3.84
4
64
 
5
65
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.84",
6
+ "version": "0.3.85",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -73,8 +73,8 @@
73
73
  "clsx": "^2.1.1",
74
74
  "devalue": "^5.8.1",
75
75
  "esm-env": "^1.2.2",
76
- "@tsrx/core": "0.1.32",
77
- "@tsrx/ripple": "0.1.32"
76
+ "@tsrx/core": "0.1.33",
77
+ "@tsrx/ripple": "0.1.33"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/estree": "^1.0.8",
package/src/constants.js CHANGED
@@ -2,6 +2,10 @@ export const TEMPLATE_FRAGMENT = 1;
2
2
  export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
3
3
  export const IS_CONTROLLED = 1 << 2;
4
4
  export const IS_INDEXED = 1 << 3;
5
+ // A @for whose component body is its sole root: renders before the parent
6
+ // `__anchor` (sibling semantics, no `<!>` wrapper). Distinct from IS_CONTROLLED,
7
+ // which renders inside a parent element.
8
+ export const ROOT_CONTROLLED = 1 << 4;
5
9
  export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
6
10
  export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
7
11
 
@@ -1,4 +1,4 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { AppendIntoAnchor, Block } from '#client' */
2
2
 
3
3
  import { is_tsrx_element } from '../../element.js';
4
4
  import { render_value } from './expression.js';
@@ -6,7 +6,7 @@ import { active_block, pop_component, push_component } from './runtime.js';
6
6
 
7
7
  /**
8
8
  * @param {Function} fn
9
- * @param {Node} anchor
9
+ * @param {Node | AppendIntoAnchor} anchor
10
10
  * @param {Record<string, any>} props
11
11
  * @param {Block | null} [block=active_block]
12
12
  * @returns {void}
@@ -21,7 +21,7 @@ export function render_component(fn, anchor, props, block = active_block) {
21
21
 
22
22
  /**
23
23
  * @param {Function} fn
24
- * @param {Node} anchor
24
+ * @param {Node | AppendIntoAnchor} anchor
25
25
  * @param {Record<string, any>} props
26
26
  * @param {Block | null} [block=active_block]
27
27
  * @returns {void}
@@ -19,6 +19,10 @@ export var DERIVED = 1 << 18;
19
19
  export var DEFERRED = 1 << 19;
20
20
  export var PAUSED = 1 << 20;
21
21
  export var DESTROYED = 1 << 21;
22
+ // Marks a block that was directly scheduled (the owner of a mutated tracked).
23
+ // Its whole subtree is scanned for dirty subscribers during flush; blocks above
24
+ // it are only traversed along the CONTAINS_UPDATE routing path.
25
+ export var UPDATE_SOURCE = 1 << 22;
22
26
 
23
27
  export var CONTROL_FLOW_BLOCK = FOR_BLOCK | IF_BLOCK | SWITCH_BLOCK | TRY_BLOCK | COMPOSITE_BLOCK;
24
28
 
@@ -1,10 +1,11 @@
1
1
  /** @import { Block, Tracked } from '#client' */
2
2
 
3
- import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
3
+ import { IS_CONTROLLED, IS_INDEXED, ROOT_CONTROLLED } from '../../../constants.js';
4
4
  import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
5
5
  import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
6
6
  import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
7
7
  import { create_text, get_first_child, get_last_child, next_sibling } from './operations.js';
8
+ import { append } from './template.js';
8
9
  import { active_block, set, tracked, untrack } from './runtime.js';
9
10
  import { array_from, is_array } from '@tsrx/core/runtime/language-helpers';
10
11
 
@@ -121,7 +122,10 @@ function collection_to_array(collection) {
121
122
  export function for_block(node, get_collection, render_fn, flags, render_empty) {
122
123
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
123
124
  var is_indexed = (flags & IS_INDEXED) !== 0;
125
+ var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
124
126
  var anchor = /** @type {Element | Text} */ (node);
127
+ /** @type {Node | undefined} */
128
+ var boundary;
125
129
 
126
130
  if (is_controlled) {
127
131
  if (hydrating) {
@@ -133,6 +137,9 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
133
137
  }
134
138
 
135
139
  if (hydrating) {
140
+ if (root_controlled) {
141
+ boundary = /** @type {Node} */ (hydrate_node);
142
+ }
136
143
  hydrate_next();
137
144
  }
138
145
 
@@ -153,6 +160,10 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
153
160
  null,
154
161
  FOR_BLOCK,
155
162
  );
163
+
164
+ if (hydrating && root_controlled) {
165
+ append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
166
+ }
156
167
  }
157
168
 
158
169
  /**
@@ -169,7 +180,10 @@ export function for_block(node, get_collection, render_fn, flags, render_empty)
169
180
  export function for_block_keyed(node, get_collection, render_fn, flags, get_key, render_empty) {
170
181
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
171
182
  var is_indexed = (flags & IS_INDEXED) !== 0;
183
+ var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
172
184
  var anchor = /** @type {Element | Text} */ (node);
185
+ /** @type {Node | undefined} */
186
+ var boundary;
173
187
 
174
188
  if (is_controlled) {
175
189
  var parent_node = /** @type {Element} */ (node);
@@ -183,6 +197,9 @@ export function for_block_keyed(node, get_collection, render_fn, flags, get_key,
183
197
  }
184
198
 
185
199
  if (hydrating) {
200
+ if (root_controlled) {
201
+ boundary = /** @type {Node} */ (hydrate_node);
202
+ }
186
203
  hydrate_next();
187
204
  }
188
205
 
@@ -208,6 +225,10 @@ export function for_block_keyed(node, get_collection, render_fn, flags, get_key,
208
225
  null,
209
226
  FOR_BLOCK,
210
227
  );
228
+
229
+ if (hydrating && root_controlled) {
230
+ append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
231
+ }
211
232
  }
212
233
 
213
234
  /**
@@ -2,15 +2,27 @@
2
2
 
3
3
  import { branch, destroy_block, render } from './blocks.js';
4
4
  import { IF_BLOCK, UNINITIALIZED } from './constants.js';
5
- import { hydrate_next, hydrating } from './hydration.js';
5
+ import { hydrate_next, hydrate_node, hydrating } from './hydration.js';
6
+ import { append } from './template.js';
6
7
 
7
8
  /**
8
9
  * @param {Node} node
9
10
  * @param {(set_branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
11
+ * @param {boolean} [root_controlled] When true the block renders directly before
12
+ * the component's `__anchor` (no synthesized `<!>` wrapper). During hydration
13
+ * the SSR boundary start marker sits at the cursor; we hand it to `append()`
14
+ * afterwards so it performs the same context-aware boundary advance the
15
+ * eliminated wrapper's `append()` used to do.
10
16
  * @returns {void}
11
17
  */
12
- export function if_block(node, fn) {
18
+ export function if_block(node, fn, root_controlled) {
19
+ /** @type {Node | undefined} */
20
+ var boundary;
21
+
13
22
  if (hydrating) {
23
+ if (root_controlled) {
24
+ boundary = /** @type {Node} */ (hydrate_node);
25
+ }
14
26
  hydrate_next();
15
27
  }
16
28
 
@@ -52,4 +64,8 @@ export function if_block(node, fn) {
52
64
  null,
53
65
  IF_BLOCK,
54
66
  );
67
+
68
+ if (hydrating && root_controlled) {
69
+ append(/** @type {ChildNode} */ (anchor), /** @type {Node} */ (boundary));
70
+ }
55
71
  }
@@ -2,6 +2,7 @@ export {
2
2
  first_child as child,
3
3
  first_child_frag,
4
4
  next_sibling as sibling,
5
+ append_into,
5
6
  document,
6
7
  create_text,
7
8
  init_operations,
@@ -1,3 +1,5 @@
1
+ /** @import { AppendIntoAnchor } from '#client' */
2
+
1
3
  import { TEXT_NODE } from '../../../constants.js';
2
4
  import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
3
5
  import { get_descriptor } from '@tsrx/core/runtime/language-helpers';
@@ -83,6 +85,29 @@ export function first_child(node, is_text) {
83
85
  return child;
84
86
  }
85
87
 
88
+ /**
89
+ * Anchor sentinel for all-component children: components render directly into
90
+ * `parent` instead of each inserting before a placeholder comment. `append()`
91
+ * detects the sentinel (no `nodeType`) and appendChild()s into `parent`. During
92
+ * hydration we descend the cursor into `parent` (mirroring first_child) so the
93
+ * first appended component adopts the server's first child of `parent`.
94
+ * @param {Node} parent
95
+ * @returns {AppendIntoAnchor}
96
+ */
97
+ export function append_into(parent) {
98
+ if (hydrating) {
99
+ var child = get_first_child(/** @type {Node} */ (hydrate_node));
100
+
101
+ if (child === null) {
102
+ child = /** @type {Node} */ (hydrate_node).appendChild(create_text());
103
+ }
104
+
105
+ set_hydrate_node(child);
106
+ }
107
+
108
+ return { parent };
109
+ }
110
+
86
111
  /**
87
112
  * @template {Node} N
88
113
  * @param {N} node
@@ -34,6 +34,7 @@ import {
34
34
  SUSPENSE_REJECTED,
35
35
  TRY_BLOCK,
36
36
  DIRECT_CHILD_BLOCK,
37
+ UPDATE_SOURCE,
37
38
  } from './constants.js';
38
39
  import {
39
40
  begin_boundary_request,
@@ -93,6 +94,12 @@ let is_micro_task_queued = false;
93
94
  let clock = 0;
94
95
  /** @type {Block[]} */
95
96
  let queued_root_blocks = [];
97
+ // Flush normally only scans the subtree of each directly-scheduled block, since a
98
+ // tracked's subscribers live inside the subtree of the block that owns it. If a
99
+ // tracked is ever read from outside its owner's subtree (e.g. smuggled across
100
+ // sibling subtrees via a module-level variable), that invariant breaks, so we
101
+ // permanently fall back to scanning the whole root tree to stay correct.
102
+ let disable_scoped_flush = false;
96
103
  /** @type {(() => void)[]} */
97
104
  let queued_microtasks = [];
98
105
  /** @type {number} */
@@ -939,22 +946,33 @@ function trigger_track_get(fn, v) {
939
946
  function flush_updates(root_block) {
940
947
  /** @type {Block | null} */
941
948
  var current = root_block;
942
- var containing_update = null;
943
949
  var pre_effects = [];
944
950
  var other_blocks = [];
945
951
  var effects = [];
946
- var containing_update_head = null;
952
+ // The nearest enclosing directly-scheduled block ("update source"). While it is
953
+ // non-null we are inside a source's subtree and scan every descendant. Above
954
+ // sources we only follow the CONTAINS_UPDATE routing path, so sibling subtrees
955
+ // that contain no update are skipped. When scoping is disabled we treat the
956
+ // whole root tree as one source — the original full-tree scan.
957
+ /** @type {Block | null} */
958
+ var scope_root = disable_scoped_flush ? root_block : null;
947
959
 
948
960
  while (current !== null) {
949
961
  var flags = current.f;
962
+ var on_path = (flags & CONTAINS_UPDATE) !== 0;
950
963
 
951
- if ((flags & CONTAINS_UPDATE) !== 0) {
964
+ if (on_path) {
952
965
  current.f ^= CONTAINS_UPDATE;
953
- containing_update_head = { v: containing_update, n: containing_update_head };
954
- containing_update = current;
955
966
  }
956
967
 
957
- if ((flags & PAUSED) === 0 && containing_update !== null) {
968
+ if ((flags & UPDATE_SOURCE) !== 0) {
969
+ current.f ^= UPDATE_SOURCE;
970
+ if (scope_root === null) {
971
+ scope_root = current;
972
+ }
973
+ }
974
+
975
+ if ((flags & PAUSED) === 0 && (on_path || scope_root !== null)) {
958
976
  if ((flags & PRE_EFFECT_BLOCK) !== 0) {
959
977
  pre_effects.push(current);
960
978
  } else if ((flags & EFFECT_BLOCK) !== 0) {
@@ -976,10 +994,8 @@ function flush_updates(root_block) {
976
994
  current = current.next;
977
995
 
978
996
  while (current === null && parent !== null) {
979
- if (parent === containing_update) {
980
- var head = /** @type {{ v: Block | null, n: any }} */ (containing_update_head);
981
- containing_update = head.v;
982
- containing_update_head = head.n;
997
+ if (parent === scope_root) {
998
+ scope_root = null;
983
999
  }
984
1000
  current = parent.next;
985
1001
  parent = parent.p;
@@ -1115,6 +1131,10 @@ export function schedule_update(block) {
1115
1131
  if (scheduler_mode === FLUSH_MICROTASK) {
1116
1132
  queue_microtask();
1117
1133
  }
1134
+ // The scheduled block roots the subtree that flush_updates scans for dirty
1135
+ // subscribers. Mark it even if it (or an ancestor) is already on a routing
1136
+ // path, so the early return below still records it as a scan root.
1137
+ block.f |= UPDATE_SOURCE;
1118
1138
  let current = block;
1119
1139
 
1120
1140
  while (current !== null) {
@@ -1134,6 +1154,38 @@ export function schedule_update(block) {
1134
1154
  * @param {Tracked | Derived} tracked
1135
1155
  */
1136
1156
  function register_dependency(tracked) {
1157
+ if (!disable_scoped_flush && active_block !== null && active_block !== tracked.b) {
1158
+ // Scoped flush only scans the owner's subtree for dirty subscribers, valid
1159
+ // only while every subscriber lives inside it. The subscriber↔owner
1160
+ // ancestry is structural and stable for a block's lifetime (its `.p` chain
1161
+ // never changes — DOM moves don't reparent blocks), so we only need to
1162
+ // verify *new* dependency edges: if this reaction already depended on
1163
+ // `tracked` on its previous run (it's in the prior chain), the ancestry
1164
+ // walk was already done — skip it. This keeps the (possibly deep) walk off
1165
+ // the hot path for the common case of stable subscriptions.
1166
+ var already_seen = false;
1167
+ var prev_dep = active_reaction === null ? null : active_reaction.d;
1168
+ while (prev_dep !== null) {
1169
+ if (prev_dep.t === tracked) {
1170
+ already_seen = true;
1171
+ break;
1172
+ }
1173
+ prev_dep = prev_dep.n;
1174
+ }
1175
+
1176
+ if (!already_seen) {
1177
+ var owner = tracked.b;
1178
+ /** @type {Block | null} */
1179
+ var node = active_block;
1180
+ while (node !== null && node !== owner) {
1181
+ node = node.p;
1182
+ }
1183
+ if (node === null) {
1184
+ disable_scoped_flush = true;
1185
+ }
1186
+ }
1187
+ }
1188
+
1137
1189
  var dependency = active_dependency;
1138
1190
 
1139
1191
  if (dependency === null) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { branch, destroy_block, render } from './blocks.js';
4
4
  import { SWITCH_BLOCK } from './constants.js';
5
- import { hydrate_next, hydrating } from './hydration.js';
5
+ import { hydrate_next, hydrate_node, hydrating } from './hydration.js';
6
6
  import { next_sibling } from './operations.js';
7
7
  import { append } from './template.js';
8
8
 
@@ -32,10 +32,20 @@ function move(block, anchor) {
32
32
  /**
33
33
  * @param {ChildNode} anchor
34
34
  * @param {() => ((anchor: ChildNode) => void)[] | null} fn
35
+ * @param {boolean} [root_controlled] When true the block renders before the
36
+ * component's `__anchor` (no `<!>` wrapper); during hydration the SSR boundary
37
+ * marker is handed to `append()` afterwards for the context-aware cursor
38
+ * advance the eliminated wrapper used to perform.
35
39
  * @returns {void}
36
40
  */
37
- export function switch_block(anchor, fn) {
41
+ export function switch_block(anchor, fn, root_controlled) {
42
+ /** @type {Node | undefined} */
43
+ var boundary;
44
+
38
45
  if (hydrating) {
46
+ if (root_controlled) {
47
+ boundary = /** @type {Node} */ (hydrate_node);
48
+ }
39
49
  hydrate_next();
40
50
  }
41
51
 
@@ -82,4 +92,8 @@ export function switch_block(anchor, fn) {
82
92
  null,
83
93
  SWITCH_BLOCK,
84
94
  );
95
+
96
+ if (hydrating && root_controlled) {
97
+ append(anchor, /** @type {Node} */ (boundary));
98
+ }
85
99
  }
@@ -1,4 +1,4 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { AppendIntoAnchor, Block } from '#client' */
2
2
 
3
3
  import {
4
4
  COMMENT_NODE,
@@ -137,7 +137,7 @@ export function template(content, flags, count = 1) {
137
137
 
138
138
  /**
139
139
  * Appends a DOM node before the anchor node.
140
- * @param {ChildNode} anchor - The anchor node.
140
+ * @param {ChildNode | AppendIntoAnchor} anchor - The anchor node.
141
141
  * @param {Node} dom - The DOM node to append.
142
142
  * @param {boolean} [skip_advance] - If true, don't advance hydrate_node (used when next() already positioned it).
143
143
  */
@@ -191,6 +191,14 @@ export function append(anchor, dom, skip_advance) {
191
191
  hydrate_advance();
192
192
  return;
193
193
  }
194
+ if ('parent' in anchor) {
195
+ // Append-into-parent sentinel: an all-component-children element passes a
196
+ // `{ parent }` object (no `nodeType`) so each component's root appends as
197
+ // the host's last child instead of inserting before a placeholder comment.
198
+ // The block still self-marks its range via assign_nodes, so teardown works.
199
+ anchor.parent.appendChild(dom);
200
+ return;
201
+ }
194
202
  anchor.before(/** @type {Node} */ (dom));
195
203
  }
196
204
 
@@ -9,7 +9,8 @@ import {
9
9
  resume_block,
10
10
  } from './blocks.js';
11
11
  import { TRY_BLOCK } from './constants.js';
12
- import { hydrate_next, hydrating } from './hydration.js';
12
+ import { hydrate_next, hydrate_node, hydrating } from './hydration.js';
13
+ import { append } from './template.js';
13
14
  import {
14
15
  active_block,
15
16
  queue_microtask,
@@ -34,8 +35,10 @@ import {
34
35
  * @param {PendingFunction | null} [pending_fn=null]
35
36
  * @returns {void}
36
37
  */
37
- export function try_block(node, try_fn, catch_fn, pending_fn = null) {
38
+ export function try_block(node, try_fn, catch_fn, pending_fn = null, root_controlled = false) {
38
39
  var anchor = node;
40
+ /** @type {Node | undefined} */
41
+ var boundary;
39
42
  var pending_count = 0;
40
43
  var request_version = 0;
41
44
  /** @type {Set<number>} */
@@ -316,12 +319,19 @@ export function try_block(node, try_fn, catch_fn, pending_fn = null) {
316
319
  if (pending_fn !== null) {
317
320
  has_resolved = true;
318
321
  }
322
+ if (root_controlled) {
323
+ boundary = /** @type {Node} */ (hydrate_node);
324
+ }
319
325
  hydrate_next(); // consume <!--[-->
320
326
  }
321
327
 
322
328
  try_block = create_try_block(() => {
323
329
  resolved_branch = boundary_fn_running_block(() => try_fn(anchor));
324
330
  }, state);
331
+
332
+ if (hydrating && root_controlled) {
333
+ append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
334
+ }
325
335
  }
326
336
 
327
337
  /**
@@ -26,6 +26,10 @@ export type DeferredTrackedEntry = {
26
26
  r: number; // request version id
27
27
  };
28
28
 
29
+ export type AppendIntoAnchor = {
30
+ parent: Node;
31
+ };
32
+
29
33
  export type Block = {
30
34
  co: null | Component;
31
35
  d: null | Dependency;
@@ -319,7 +319,6 @@ exports[`for statements > renders a simple dynamic array 2`] = `
319
319
 
320
320
  exports[`for statements > renders a simple static array 1`] = `
321
321
  <div>
322
- <!---->
323
322
  <div
324
323
  class="Item 1"
325
324
  >
@@ -335,7 +334,6 @@ exports[`for statements > renders a simple static array 1`] = `
335
334
  >
336
335
  Item 3
337
336
  </div>
338
- <!---->
339
337
 
340
338
  </div>
341
339
  `;
@@ -579,8 +579,11 @@ function App() @{
579
579
  `;
580
580
  const result = compile_to_volar_mappings(source, 'test.tsrx').code;
581
581
 
582
- expect(result).toContain('const nested = <span class="nested-tsx">');
583
- expect(result).toContain('return content;');
582
+ // An authored `<>…</>` is kept verbatim — both as a value (the `nested`
583
+ // initializer) and as the component's render output (`<>{content}</>`)
584
+ // instead of unwrapping to its single child.
585
+ expect(result).toContain('const nested = <><span class="nested-tsx">');
586
+ expect(result).toContain('return <>{content}</>;');
584
587
  expect(result).toContain('const content = <div class="native">');
585
588
  });
586
589
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  exports[`composite > render > correct handles passing through component props and children 1`] = `
4
4
  <div>
5
- <!---->
6
5
  <div>
7
6
  <div>
8
7
  I am A
@@ -17,21 +16,18 @@ exports[`composite > render > correct handles passing through component props an
17
16
  </div>
18
17
  <!---->
19
18
  </div>
20
- <!---->
21
19
 
22
20
  </div>
23
21
  `;
24
22
 
25
23
  exports[`composite > render > render simple text as children 1`] = `
26
24
  <div>
27
- <!---->
28
25
  <button
29
26
  class="my-button"
30
27
  >
31
28
  Click Me
32
29
  <!---->
33
30
  </button>
34
- <!---->
35
31
 
36
32
  </div>
37
33
  `;