ripple 0.2.207 → 0.2.210

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 (110) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/package.json +2 -6
  4. package/shims/rollup-estree-types.d.ts +1 -1
  5. package/src/compiler/index.d.ts +1 -0
  6. package/src/compiler/index.js +7 -1
  7. package/src/compiler/phases/1-parse/index.js +15 -6
  8. package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
  9. package/src/compiler/phases/2-analyze/index.js +215 -2
  10. package/src/compiler/phases/3-transform/client/index.js +388 -50
  11. package/src/compiler/phases/3-transform/segments.js +123 -39
  12. package/src/compiler/phases/3-transform/server/index.js +266 -13
  13. package/src/compiler/types/index.d.ts +16 -3
  14. package/src/compiler/utils.js +1 -15
  15. package/src/constants.js +0 -2
  16. package/src/helpers.d.ts +4 -0
  17. package/src/html-tree-validation.js +211 -0
  18. package/src/jsx-runtime.d.ts +260 -259
  19. package/src/jsx-runtime.js +12 -12
  20. package/src/runtime/array.js +17 -17
  21. package/src/runtime/create-subscriber.js +1 -1
  22. package/src/runtime/index-client.js +1 -5
  23. package/src/runtime/index-server.js +15 -0
  24. package/src/runtime/internal/client/bindings.js +26 -20
  25. package/src/runtime/internal/client/compat.js +3 -3
  26. package/src/runtime/internal/client/composite.js +6 -1
  27. package/src/runtime/internal/client/head.js +50 -4
  28. package/src/runtime/internal/client/html.js +73 -12
  29. package/src/runtime/internal/client/hydration.js +12 -0
  30. package/src/runtime/internal/client/index.js +1 -1
  31. package/src/runtime/internal/client/portal.js +54 -29
  32. package/src/runtime/internal/client/rpc.js +3 -1
  33. package/src/runtime/internal/client/switch.js +5 -0
  34. package/src/runtime/internal/client/template.js +117 -11
  35. package/src/runtime/internal/client/try.js +1 -0
  36. package/src/runtime/internal/server/index.js +113 -1
  37. package/src/runtime/internal/server/rpc.js +4 -4
  38. package/src/runtime/map.js +2 -2
  39. package/src/runtime/object.js +6 -6
  40. package/src/runtime/proxy.js +12 -11
  41. package/src/runtime/reactive-value.js +9 -1
  42. package/src/runtime/set.js +12 -7
  43. package/src/runtime/url-search-params.js +0 -1
  44. package/src/server/index.js +4 -0
  45. package/src/utils/hashing.js +15 -0
  46. package/src/utils/normalize_css_property_name.js +1 -1
  47. package/tests/client/array/array.mutations.test.ripple +8 -8
  48. package/tests/client/basic/basic.errors.test.ripple +28 -0
  49. package/tests/client/basic/basic.events.test.ripple +6 -3
  50. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  51. package/tests/client/compiler/compiler.regex.test.ripple +10 -8
  52. package/tests/client/composite/composite.generics.test.ripple +5 -2
  53. package/tests/client/dynamic-elements.test.ripple +30 -1
  54. package/tests/client/function-overload-import.ripple +6 -7
  55. package/tests/client/html.test.ripple +0 -1
  56. package/tests/client/input-value.test.ripple +539 -469
  57. package/tests/client/object.test.ripple +2 -2
  58. package/tests/client/portal.test.ripple +3 -3
  59. package/tests/client/return.test.ripple +2500 -0
  60. package/tests/client/try.test.ripple +69 -0
  61. package/tests/client/typescript-generics.test.ripple +1 -1
  62. package/tests/client/url/url.derived.test.ripple +1 -1
  63. package/tests/client/url/url.parsing.test.ripple +3 -3
  64. package/tests/client/url/url.partial-removal.test.ripple +7 -7
  65. package/tests/client/url/url.reactivity.test.ripple +15 -15
  66. package/tests/client/url/url.serialization.test.ripple +2 -2
  67. package/tests/hydration/basic.test.js +23 -0
  68. package/tests/hydration/build-components.js +10 -4
  69. package/tests/hydration/compiled/client/basic.js +165 -3
  70. package/tests/hydration/compiled/client/for.js +1140 -23
  71. package/tests/hydration/compiled/client/head.js +234 -0
  72. package/tests/hydration/compiled/client/html.js +135 -0
  73. package/tests/hydration/compiled/client/portal.js +172 -0
  74. package/tests/hydration/compiled/client/reactivity.js +3 -1
  75. package/tests/hydration/compiled/client/return.js +1976 -0
  76. package/tests/hydration/compiled/client/switch.js +162 -0
  77. package/tests/hydration/compiled/server/basic.js +249 -0
  78. package/tests/hydration/compiled/server/events.js +1 -1
  79. package/tests/hydration/compiled/server/for.js +891 -1
  80. package/tests/hydration/compiled/server/head.js +291 -0
  81. package/tests/hydration/compiled/server/html.js +133 -0
  82. package/tests/hydration/compiled/server/if.js +1 -1
  83. package/tests/hydration/compiled/server/portal.js +250 -0
  84. package/tests/hydration/compiled/server/reactivity.js +1 -1
  85. package/tests/hydration/compiled/server/return.js +1969 -0
  86. package/tests/hydration/compiled/server/switch.js +130 -0
  87. package/tests/hydration/components/basic.ripple +55 -0
  88. package/tests/hydration/components/for.ripple +403 -0
  89. package/tests/hydration/components/head.ripple +111 -0
  90. package/tests/hydration/components/html.ripple +38 -0
  91. package/tests/hydration/components/portal.ripple +49 -0
  92. package/tests/hydration/components/return.ripple +564 -0
  93. package/tests/hydration/components/switch.ripple +51 -0
  94. package/tests/hydration/for.test.js +363 -0
  95. package/tests/hydration/head.test.js +105 -0
  96. package/tests/hydration/html.test.js +46 -0
  97. package/tests/hydration/portal.test.js +71 -0
  98. package/tests/hydration/return.test.js +544 -0
  99. package/tests/hydration/switch.test.js +42 -0
  100. package/tests/server/basic.attributes.test.ripple +1 -1
  101. package/tests/server/compiler.test.ripple +22 -0
  102. package/tests/server/composite.test.ripple +5 -2
  103. package/tests/server/html-nesting-validation.test.ripple +237 -0
  104. package/tests/server/return.test.ripple +1379 -0
  105. package/tests/setup-hydration.js +6 -1
  106. package/tests/utils/escaping.test.js +3 -1
  107. package/tests/utils/normalize_css_property_name.test.js +0 -1
  108. package/tests/utils/patterns.test.js +6 -2
  109. package/tests/utils/sanitize_template_string.test.js +3 -2
  110. package/types/server.d.ts +16 -0
@@ -13,15 +13,15 @@
13
13
  * @returns {void} Ripple components don't return anything
14
14
  */
15
15
  export function jsx(type, props, key) {
16
- // Ripple components are imperative - they don't return JSX elements
17
- // This is a placeholder for the actual Ripple rendering logic
18
- if (typeof type === 'function') {
19
- // Call the Ripple component function
20
- type(props);
21
- } else {
22
- // Handle DOM elements
23
- console.warn('DOM element rendering not implemented in jsx runtime:', type, props);
24
- }
16
+ // Ripple components are imperative - they don't return JSX elements
17
+ // This is a placeholder for the actual Ripple rendering logic
18
+ if (typeof type === 'function') {
19
+ // Call the Ripple component function
20
+ type(props);
21
+ } else {
22
+ // Handle DOM elements
23
+ console.warn('DOM element rendering not implemented in jsx runtime:', type, props);
24
+ }
25
25
  }
26
26
 
27
27
  /**
@@ -32,7 +32,7 @@ export function jsx(type, props, key) {
32
32
  * @returns {void} Ripple components don't return anything
33
33
  */
34
34
  export function jsxs(type, props, key) {
35
- return jsx(type, props, key);
35
+ return jsx(type, props, key);
36
36
  }
37
37
 
38
38
  /**
@@ -41,6 +41,6 @@ export function jsxs(type, props, key) {
41
41
  * @returns {void} Ripple fragments don't return anything
42
42
  */
43
43
  export function Fragment(props) {
44
- // Ripple fragments are imperative
45
- console.warn('Fragment rendering not implemented in jsx runtime:', props);
44
+ // Ripple fragments are imperative
45
+ console.warn('Fragment rendering not implemented in jsx runtime:', props);
46
46
  }
@@ -9,12 +9,12 @@ import { array_proxy } from './proxy.js';
9
9
  * @returns {TrackedArray<T>}
10
10
  */
11
11
  export function TrackedArray(...elements) {
12
- if (!new.target) {
13
- throw new Error("TrackedArray must be called with 'new'");
14
- }
12
+ if (!new.target) {
13
+ throw new Error("TrackedArray must be called with 'new'");
14
+ }
15
15
 
16
- var block = safe_scope();
17
- return array_proxy({ elements, block });
16
+ var block = safe_scope();
17
+ return array_proxy({ elements, block });
18
18
  }
19
19
 
20
20
  /**
@@ -25,9 +25,9 @@ export function TrackedArray(...elements) {
25
25
  * @returns {TrackedArray<T>}
26
26
  */
27
27
  TrackedArray.from = function (arrayLike, mapFn, thisArg) {
28
- var block = safe_scope();
29
- var elements = mapFn ? Array.from(arrayLike, mapFn, thisArg) : Array.from(arrayLike);
30
- return array_proxy({ elements, block, from_static: true });
28
+ var block = safe_scope();
29
+ var elements = mapFn ? Array.from(arrayLike, mapFn, thisArg) : Array.from(arrayLike);
30
+ return array_proxy({ elements, block, from_static: true });
31
31
  };
32
32
 
33
33
  /**
@@ -36,9 +36,9 @@ TrackedArray.from = function (arrayLike, mapFn, thisArg) {
36
36
  * @returns {TrackedArray<T>}
37
37
  */
38
38
  TrackedArray.of = function (...items) {
39
- var block = safe_scope();
40
- var elements = Array.of(...items);
41
- return array_proxy({ elements, block, from_static: true });
39
+ var block = safe_scope();
40
+ var elements = Array.of(...items);
41
+ return array_proxy({ elements, block, from_static: true });
42
42
  };
43
43
 
44
44
  /**
@@ -49,11 +49,11 @@ TrackedArray.of = function (...items) {
49
49
  * @returns {Promise<TrackedArray<T>>}
50
50
  */
51
51
  TrackedArray.fromAsync = async function (arrayLike, mapFn, thisArg) {
52
- var block = safe_scope();
53
- var elements = mapFn
54
- ? await Array.fromAsync(arrayLike, mapFn, thisArg)
55
- : await Array.fromAsync(arrayLike);
56
- return array_proxy({ elements, block, from_static: true });
52
+ var block = safe_scope();
53
+ var elements = mapFn
54
+ ? await Array.fromAsync(arrayLike, mapFn, thisArg)
55
+ : await Array.fromAsync(arrayLike);
56
+ return array_proxy({ elements, block, from_static: true });
57
57
  };
58
58
 
59
59
  /**
@@ -63,5 +63,5 @@ TrackedArray.fromAsync = async function (arrayLike, mapFn, thisArg) {
63
63
  * @returns {TrackedArray<T>}
64
64
  */
65
65
  export function tracked_array(elements, block) {
66
- return array_proxy({ elements, block, from_static: true });
66
+ return array_proxy({ elements, block, from_static: true });
67
67
  }
@@ -1,6 +1,6 @@
1
1
  /** @import { createSubscriber } from '#public' */
2
2
  import { untrack, queue_microtask } from './internal/client/runtime.js';
3
- import { effect } from './internal/client/blocks.js'
3
+ import { effect } from './internal/client/blocks.js';
4
4
 
5
5
  /** @type {createSubscriber} */
6
6
  export function createSubscriber(start) {
@@ -17,7 +17,7 @@ import {
17
17
  set_hydrate_node,
18
18
  set_hydrating,
19
19
  } from './internal/client/hydration.js';
20
- import { COMMENT_NODE, HYDRATION_ERROR, HYDRATION_START } from '../constants.js';
20
+ import { COMMENT_NODE, HYDRATION_START } from '../constants.js';
21
21
 
22
22
  // Re-export JSX runtime functions for jsxImportSource: "ripple"
23
23
  export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
@@ -84,10 +84,6 @@ export function hydrate(component, options) {
84
84
  set_hydrate_node(/** @type {Comment} */ (anchor));
85
85
  hydrate_next();
86
86
 
87
- if (!anchor) {
88
- throw HYDRATION_ERROR;
89
- }
90
-
91
87
  _root = root(() => {
92
88
  component(/** @type {Comment} */ (anchor), props, active_block);
93
89
  }, options.compat);
@@ -56,3 +56,18 @@ export const bindTextContent = noop;
56
56
  export const bindNode = noop;
57
57
  export const bindOffsetWidth = noop;
58
58
  export const bindOffsetHeight = noop;
59
+
60
+ /**
61
+ * Portal component noop for server-side rendering.
62
+ * Portals are client-only and do not render on the server.
63
+ * However, we need to output a marker comment so hydration can work correctly.
64
+ * @param {any} output
65
+ * @param {any} __
66
+ */
67
+ export function Portal(output, __) {
68
+ // Portals are client-only, but we need to output a marker for hydration
69
+ // Output an empty HTML comment as a placeholder
70
+ if (output && typeof output.push === 'function') {
71
+ output.push('<!--portal-->');
72
+ }
73
+ }
@@ -248,15 +248,35 @@ export function bindValue(maybe_tracked, set_func = undefined) {
248
248
  });
249
249
  } else {
250
250
  var input = /** @type {HTMLInputElement} */ (node);
251
- var selection_restore_needed = false;
252
251
 
253
252
  clear_event = on(input, 'input', () => {
254
- selection_restore_needed = true;
255
253
  /** @type {any} */
256
254
  var value = input.value;
257
255
  value = is_numberlike_input(input) ? to_number(value) : value;
258
- // the setter will schedule a microtask and the render block below will run
259
256
  setter(value);
257
+ const getter_value = getter();
258
+
259
+ // Check the getter to see if it's different from the input.value
260
+ // The setter may have decided not to update its track value or update it to something else
261
+ // We treat the getter as the source of truth since we cannot verify the change otherwise
262
+ // If getter() !== input.value, we set the input value right away
263
+ // the `render` block may be scheduled only if the tracked value has changed
264
+ // but it will not do anything if getter() === input.value
265
+ // The result is: the `render` block will ALWAYS exit early if the microtask
266
+ // came from this event handler
267
+ if (value !== getter_value) {
268
+ var start = input.selectionStart;
269
+ var end = input.selectionEnd;
270
+
271
+ input.value = getter_value ?? '';
272
+
273
+ if (end !== null && start !== null) {
274
+ end = Math.min(end, input.value.length);
275
+ start = Math.min(start, end);
276
+ input.selectionStart = start;
277
+ input.selectionEnd = end;
278
+ }
279
+ }
260
280
  });
261
281
 
262
282
  render(() => {
@@ -271,23 +291,9 @@ export function bindValue(maybe_tracked, set_func = undefined) {
271
291
  }
272
292
 
273
293
  if (value !== input.value) {
274
- if (selection_restore_needed) {
275
- var start = input.selectionStart;
276
- var end = input.selectionEnd;
277
-
278
- input.value = value ?? '';
279
-
280
- // Restore selection
281
- if (end !== null && start !== null) {
282
- end = Math.min(end, input.value.length);
283
- start = Math.min(start, end);
284
- input.selectionStart = start;
285
- input.selectionEnd = end;
286
- }
287
- selection_restore_needed = false;
288
- } else {
289
- input.value = value ?? '';
290
- }
294
+ // this can only get here if the tracked value was changed directly,
295
+ // and not via the input event
296
+ input.value = value ?? '';
291
297
  }
292
298
  });
293
299
 
@@ -1,13 +1,13 @@
1
1
  /** @import { CompatApi } from '#client' */
2
2
 
3
- import { ROOT_BLOCK } from "./constants.js";
4
- import { active_block } from "./runtime.js";
3
+ import { ROOT_BLOCK } from './constants.js';
4
+ import { active_block } from './runtime.js';
5
5
 
6
6
  /**
7
7
  * @param {string} kind
8
8
  * @returns {CompatApi | null}
9
9
  */
10
- function get_compat_from_root(kind) {
10
+ function get_compat_from_root(kind) {
11
11
  var current = active_block;
12
12
 
13
13
  while (current !== null) {
@@ -15,7 +15,12 @@ import { top_element_to_ns } from './utils.js';
15
15
  */
16
16
  export function composite(get_component, node, props) {
17
17
  if (hydrating) {
18
- hydrate_next();
18
+ // During hydration, `node` may already point at the first real SSR node
19
+ // (e.g. layout children). Only skip forward when we are on an empty
20
+ // comment anchor from a client template placeholder.
21
+ if (node.nodeType === 8 && /** @type {Comment} */ (node).data === '') {
22
+ hydrate_next();
23
+ }
19
24
  }
20
25
 
21
26
  var anchor = node;
@@ -1,14 +1,60 @@
1
+ /** @import { TemplateNode } from '#client' */
1
2
  import { render } from './blocks.js';
2
3
  import { HEAD_BLOCK } from './constants.js';
3
- import { create_text } from './operations.js';
4
+ import { COMMENT_NODE } from '../../../constants.js';
5
+ import { create_text, get_first_child, get_next_sibling } from './operations.js';
6
+ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './hydration.js';
4
7
 
5
8
  /**
9
+ * @param {string} hash
6
10
  * @param {(anchor: Node) => void} render_fn
7
11
  * @returns {void}
8
12
  */
9
- export function head(render_fn) {
13
+ export function head(hash, render_fn) {
14
+ // The head function may be called after the first hydration pass and ssr comment nodes may still be present,
15
+ // therefore we need to skip that when we detect that we're not in hydration mode.
16
+ let previous_hydrate_node = null;
17
+ let was_hydrating = hydrating;
18
+
10
19
  /** @type {Comment | Text} */
11
- var anchor = document.head.appendChild(create_text());
20
+ var anchor;
21
+
22
+ if (hydrating) {
23
+ previous_hydrate_node = hydrate_node;
24
+
25
+ var head_anchor = get_first_child(document.head);
26
+
27
+ // There might be multiple head blocks in our app, and they could have been
28
+ // rendered in an arbitrary order — find one corresponding to this component
29
+ while (
30
+ head_anchor !== null &&
31
+ (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
32
+ ) {
33
+ head_anchor = get_next_sibling(head_anchor);
34
+ }
35
+
36
+ // If we can't find an opening hydration marker, skip hydration (this can happen
37
+ // if a framework rendered body but not head content)
38
+ if (head_anchor === null) {
39
+ set_hydrating(false);
40
+ } else {
41
+ var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
42
+ head_anchor.remove(); // in case this component is repeated
43
+
44
+ set_hydrate_node(start);
45
+ }
46
+ }
47
+
48
+ if (!hydrating) {
49
+ anchor = document.head.appendChild(create_text());
50
+ }
12
51
 
13
- render(() => render_fn(anchor), null, HEAD_BLOCK);
52
+ try {
53
+ render(() => render_fn(anchor), null, HEAD_BLOCK);
54
+ } finally {
55
+ if (was_hydrating) {
56
+ set_hydrating(true);
57
+ set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
58
+ }
59
+ }
14
60
  }
@@ -1,9 +1,11 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
3
  import { remove_block_dom, render } from './blocks.js';
4
- import { get_first_child } from './operations.js';
4
+ import { get_first_child, get_next_sibling } from './operations.js';
5
5
  import { active_block } from './runtime.js';
6
6
  import { assign_nodes, create_fragment_from_html } from './template.js';
7
+ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
8
+ import { COMMENT_NODE } from '../../../constants.js';
7
9
 
8
10
  /**
9
11
  * Renders dynamic HTML content into the DOM by inserting it before the anchor node.
@@ -13,14 +15,41 @@ import { assign_nodes, create_fragment_from_html } from './template.js';
13
15
  * @returns {void}
14
16
  */
15
17
  export function html(node, get_html, svg = false, mathml = false) {
16
- /** @type {ChildNode} */
17
18
  var anchor = node;
18
- /** @type {string} */
19
19
  var html = '';
20
20
 
21
21
  render(() => {
22
22
  var block = /** @type {Block} */ (active_block);
23
- html = get_html() + '';
23
+ var new_html = get_html() + '';
24
+
25
+ // If the HTML hasn't changed, skip the update (but still hydrate on first run)
26
+ if (html === new_html) {
27
+ // During hydration, we need to skip past the content and end marker even if value hasn't changed
28
+ if (hydrating) {
29
+ // The anchor is the hash comment - we need to skip past it and its content
30
+ set_hydrate_node(anchor);
31
+ var next = hydrate_next();
32
+
33
+ // Walk until we find the empty comment end marker
34
+ while (
35
+ next !== null &&
36
+ (next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '')
37
+ ) {
38
+ next = get_next_sibling(next);
39
+ }
40
+
41
+ // Move past the end marker (if next sibling exists)
42
+ if (next !== null) {
43
+ var next_node = get_next_sibling(next);
44
+ if (next_node !== null) {
45
+ set_hydrate_node(next_node);
46
+ }
47
+ }
48
+ }
49
+ return;
50
+ }
51
+
52
+ html = new_html;
24
53
 
25
54
  if (svg) html = `<svg>${html}</svg>`;
26
55
  else if (mathml) html = `<math>${html}</math>`;
@@ -30,25 +59,57 @@ export function html(node, get_html, svg = false, mathml = false) {
30
59
  block.s.start = block.s.end = null;
31
60
  }
32
61
 
62
+ if (hydrating) {
63
+ set_hydrate_node(anchor);
64
+
65
+ /** @type {Node | null} */
66
+ var next = hydrate_next();
67
+
68
+ // Walk through content nodes until we hit the empty comment end marker
69
+ while (
70
+ next !== null &&
71
+ (next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '')
72
+ ) {
73
+ next = get_next_sibling(next);
74
+ }
75
+
76
+ if (next === null) {
77
+ throw new Error('Hydration mismatch: expected end marker for HTML block');
78
+ }
79
+
80
+ // Include the hash comment and end marker in the assigned nodes
81
+ // This follows Svelte's approach where these markers are part of the effect's managed DOM
82
+ // They will be cleaned up when/if the content updates
83
+ assign_nodes(anchor, next);
84
+
85
+ // Move past the end marker for the next operation
86
+ // For empty HTML or end of container, next sibling might be null - that's okay
87
+ var next_node = get_next_sibling(next);
88
+ if (next_node !== null) {
89
+ set_hydrate_node(next_node);
90
+ }
91
+ return;
92
+ }
93
+
33
94
  if (html === '') return;
34
- /** @type {DocumentFragment | Element} */
35
- var node = create_fragment_from_html(html);
95
+
96
+ var fragment = create_fragment_from_html(html);
36
97
 
37
98
  if (svg || mathml) {
38
- node = /** @type {Element} */ (get_first_child(node));
99
+ fragment = /** @type {DocumentFragment} */ (get_first_child(fragment));
39
100
  }
40
101
 
41
102
  assign_nodes(
42
- /** @type {Element} */ (get_first_child(node)),
43
- /** @type {Element} */ (node.lastChild),
103
+ /** @type {Node} */ (get_first_child(fragment)),
104
+ /** @type {Node} */ (fragment.lastChild),
44
105
  );
45
106
 
46
107
  if (svg || mathml) {
47
- while (get_first_child(node)) {
48
- anchor.before(/** @type {Element} */ (get_first_child(node)));
108
+ while (get_first_child(fragment)) {
109
+ anchor.before(/** @type {Node} */ (get_first_child(fragment)));
49
110
  }
50
111
  } else {
51
- anchor.before(node);
112
+ anchor.before(fragment);
52
113
  }
53
114
  });
54
115
  }
@@ -28,6 +28,18 @@ export function hydrate_next() {
28
28
  return set_hydrate_node(get_next_sibling(/** @type {Node} */ (hydrate_node)));
29
29
  }
30
30
 
31
+ export function next(n = 1) {
32
+ if (hydrating) {
33
+ var node = hydrate_node;
34
+
35
+ for (var i = 0; i < n; i++) {
36
+ node = get_next_sibling(/** @type {Node} */ (node));
37
+ }
38
+
39
+ hydrate_node = node;
40
+ }
41
+ }
42
+
31
43
  /** @param {Node} node */
32
44
  export function pop(node) {
33
45
  if (!hydrating) return;
@@ -88,4 +88,4 @@ export { tsx_compat } from './compat.js';
88
88
 
89
89
  export { TRY_BLOCK } from './constants.js';
90
90
 
91
- export { pop } from './hydration.js';
91
+ export { pop, next } from './hydration.js';
@@ -5,6 +5,13 @@ import { UNINITIALIZED } from './constants.js';
5
5
  import { handle_root_events } from './events.js';
6
6
  import { create_text } from './operations.js';
7
7
  import { active_block } from './runtime.js';
8
+ import {
9
+ hydrating,
10
+ hydrate_next,
11
+ hydrate_node,
12
+ set_hydrating,
13
+ set_hydrate_node,
14
+ } from './hydration.js';
8
15
 
9
16
  /**
10
17
  * @param {any} _
@@ -12,6 +19,11 @@ import { active_block } from './runtime.js';
12
19
  * @returns {void}
13
20
  */
14
21
  export function Portal(_, props) {
22
+ // Portals are client-only and don't participate in hydration
23
+ // The compiler-generated code already handles getting the node via sibling()
24
+ var was_hydrating = hydrating;
25
+ var previous_hydrate_node = hydrate_node;
26
+
15
27
  /** @type {Element | symbol} */
16
28
  let target = UNINITIALIZED;
17
29
  /** @type {((anchor: Node, props: {}, block: Block) => void) | symbol} */
@@ -25,42 +37,55 @@ export function Portal(_, props) {
25
37
  /** @type {Node | null} */
26
38
  var dom_end = null;
27
39
 
28
- render(() => {
29
- if (target === (target = props.target)) return;
30
- if (children === (children = props.children)) return;
40
+ // Temporarily disable hydration for portal content
41
+ if (was_hydrating) {
42
+ set_hydrating(false);
43
+ }
31
44
 
32
- if (b !== null) {
33
- destroy_block(b);
34
- }
45
+ try {
46
+ render(() => {
47
+ if (target === (target = props.target)) return;
48
+ if (children === (children = props.children)) return;
35
49
 
36
- if (anchor !== null) {
37
- anchor.remove();
38
- }
50
+ if (b !== null) {
51
+ destroy_block(b);
52
+ }
39
53
 
40
- dom_start = dom_end = null;
54
+ if (anchor !== null) {
55
+ anchor.remove();
56
+ }
41
57
 
42
- anchor = create_text();
43
- /** @type {Element} */ (target).append(anchor);
58
+ dom_start = dom_end = null;
44
59
 
45
- const cleanup_events = handle_root_events(/** @type {Element} */ (target));
60
+ anchor = create_text();
61
+ /** @type {Element} */ (target).append(anchor);
46
62
 
47
- var block = /** @type {Block} */ (active_block);
63
+ const cleanup_events = handle_root_events(/** @type {Element} */ (target));
48
64
 
49
- b = branch(() => {
50
- if (typeof children === 'function') {
51
- children(/** @type {Text} */ (anchor), {}, block);
52
- }
53
- });
65
+ var block = /** @type {Block} */ (active_block);
54
66
 
55
- dom_start = b?.s?.start;
56
- dom_end = b?.s?.end;
67
+ b = branch(() => {
68
+ if (typeof children === 'function') {
69
+ children(/** @type {Text} */ (anchor), {}, block);
70
+ }
71
+ });
57
72
 
58
- return () => {
59
- cleanup_events();
60
- /** @type {Text} */ (anchor).remove();
61
- if (dom_start && dom_end) {
62
- remove_block_dom(dom_start, dom_end);
63
- }
64
- };
65
- });
73
+ dom_start = b?.s?.start;
74
+ dom_end = b?.s?.end;
75
+
76
+ return () => {
77
+ cleanup_events();
78
+ /** @type {Text} */ (anchor).remove();
79
+ if (dom_start && dom_end) {
80
+ remove_block_dom(dom_start, dom_end);
81
+ }
82
+ };
83
+ });
84
+ } finally {
85
+ // Restore hydration state
86
+ if (was_hydrating) {
87
+ set_hydrating(true);
88
+ set_hydrate_node(/** @type {any} */ (previous_hydrate_node));
89
+ }
90
+ }
66
91
  }
@@ -22,7 +22,9 @@ export async function rpc(hash, args) {
22
22
  }
23
23
 
24
24
  if (data === '') {
25
- throw new Error('The server function end-point did not return a response. Are you running a Ripple server?');
25
+ throw new Error(
26
+ 'The server function end-point did not return a response. Are you running a Ripple server?',
27
+ );
26
28
  }
27
29
 
28
30
  return devalue.parse(data).value;
@@ -2,6 +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
6
  import { next_sibling } from './operations.js';
6
7
  import { append } from './template.js';
7
8
 
@@ -34,6 +35,10 @@ function move(block, anchor) {
34
35
  * @returns {void}
35
36
  */
36
37
  export function switch_block(anchor, fn) {
38
+ if (hydrating) {
39
+ hydrate_next();
40
+ }
41
+
37
42
  /** @type {((anchor: ChildNode) => void)[]} */
38
43
  var prev = [];
39
44
  /** @type {Map<(anchor: ChildNode) => void, Block>} */