ripple 0.3.84 → 0.3.86

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 (59) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/package.json +3 -3
  3. package/src/constants.js +4 -0
  4. package/src/runtime/internal/client/blocks.js +53 -0
  5. package/src/runtime/internal/client/component.js +3 -3
  6. package/src/runtime/internal/client/constants.js +4 -0
  7. package/src/runtime/internal/client/for.js +87 -14
  8. package/src/runtime/internal/client/if.js +18 -2
  9. package/src/runtime/internal/client/index.js +1 -0
  10. package/src/runtime/internal/client/operations.js +25 -0
  11. package/src/runtime/internal/client/runtime.js +62 -10
  12. package/src/runtime/internal/client/switch.js +33 -5
  13. package/src/runtime/internal/client/template.js +10 -2
  14. package/src/runtime/internal/client/try.js +12 -2
  15. package/src/runtime/internal/client/types.d.ts +4 -0
  16. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -2
  17. package/tests/client/compiler/compiler.basic.test.tsrx +5 -2
  18. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -4
  19. package/tests/client/for-single-root-items.test.tsrx +130 -0
  20. package/tests/client/scoped-flush.test.tsrx +105 -0
  21. package/tests/hydration/compiled/client/basic.js +198 -214
  22. package/tests/hydration/compiled/client/composite.js +23 -72
  23. package/tests/hydration/compiled/client/for.js +66 -72
  24. package/tests/hydration/compiled/client/hmr.js +2 -13
  25. package/tests/hydration/compiled/client/html.js +619 -479
  26. package/tests/hydration/compiled/client/if-children.js +72 -84
  27. package/tests/hydration/compiled/client/if-fragment-controlflow.js +463 -0
  28. package/tests/hydration/compiled/client/if.js +87 -92
  29. package/tests/hydration/compiled/client/mixed-control-flow.js +120 -160
  30. package/tests/hydration/compiled/client/nested-control-flow.js +288 -360
  31. package/tests/hydration/compiled/client/portal.js +15 -21
  32. package/tests/hydration/compiled/client/reactivity.js +7 -12
  33. package/tests/hydration/compiled/client/switch.js +113 -115
  34. package/tests/hydration/compiled/client/track-async-serialization.js +71 -122
  35. package/tests/hydration/compiled/client/try.js +26 -41
  36. package/tests/hydration/compiled/server/basic.js +295 -556
  37. package/tests/hydration/compiled/server/composite.js +31 -50
  38. package/tests/hydration/compiled/server/events.js +37 -173
  39. package/tests/hydration/compiled/server/for.js +236 -847
  40. package/tests/hydration/compiled/server/head.js +110 -241
  41. package/tests/hydration/compiled/server/hmr.js +14 -41
  42. package/tests/hydration/compiled/server/html-in-template.js +14 -38
  43. package/tests/hydration/compiled/server/html.js +951 -1176
  44. package/tests/hydration/compiled/server/if-children.js +59 -493
  45. package/tests/hydration/compiled/server/if-fragment-controlflow.js +227 -0
  46. package/tests/hydration/compiled/server/if.js +57 -209
  47. package/tests/hydration/compiled/server/mixed-control-flow.js +144 -250
  48. package/tests/hydration/compiled/server/nested-control-flow.js +309 -559
  49. package/tests/hydration/compiled/server/portal.js +94 -156
  50. package/tests/hydration/compiled/server/reactivity.js +23 -65
  51. package/tests/hydration/compiled/server/return.js +6 -16
  52. package/tests/hydration/compiled/server/switch.js +91 -219
  53. package/tests/hydration/compiled/server/track-async-serialization.js +211 -256
  54. package/tests/hydration/compiled/server/try.js +67 -126
  55. package/tests/hydration/components/html.tsrx +75 -0
  56. package/tests/hydration/components/if-fragment-controlflow.tsrx +145 -0
  57. package/tests/hydration/html.test.js +50 -0
  58. package/tests/hydration/if-fragment-controlflow.test.js +95 -0
  59. package/tests/server/compiler.test.tsrx +6 -2
@@ -1,8 +1,8 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
- import { branch, destroy_block, render } from './blocks.js';
3
+ import { branch, destroy_block, get_first_node, get_last_node, 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
 
@@ -13,8 +13,22 @@ import { append } from './template.js';
13
13
  * @returns {void}
14
14
  */
15
15
  function move(block, anchor) {
16
- var node = block.s.start;
17
- var end = block.s.end;
16
+ // Fast path: a normal case records its own range. Only an optimized single
17
+ // control-flow / component root case (DOM rendered through a descendant
18
+ // block, `s.start` null) needs the descent via `get_first_node`/`get_last_node`.
19
+ var s = block.s;
20
+ var node = s !== null ? s.start : null;
21
+ var end;
22
+
23
+ if (node === null) {
24
+ node = get_first_node(block);
25
+ if (node === null) {
26
+ return;
27
+ }
28
+ end = get_last_node(block);
29
+ } else {
30
+ end = s.end;
31
+ }
18
32
  /** @type {Node | null} */
19
33
  var sibling;
20
34
 
@@ -32,10 +46,20 @@ function move(block, anchor) {
32
46
  /**
33
47
  * @param {ChildNode} anchor
34
48
  * @param {() => ((anchor: ChildNode) => void)[] | null} fn
49
+ * @param {boolean} [root_controlled] When true the block renders before the
50
+ * component's `__anchor` (no `<!>` wrapper); during hydration the SSR boundary
51
+ * marker is handed to `append()` afterwards for the context-aware cursor
52
+ * advance the eliminated wrapper used to perform.
35
53
  * @returns {void}
36
54
  */
37
- export function switch_block(anchor, fn) {
55
+ export function switch_block(anchor, fn, root_controlled) {
56
+ /** @type {Node | undefined} */
57
+ var boundary;
58
+
38
59
  if (hydrating) {
60
+ if (root_controlled) {
61
+ boundary = /** @type {Node} */ (hydrate_node);
62
+ }
39
63
  hydrate_next();
40
64
  }
41
65
 
@@ -82,4 +106,8 @@ export function switch_block(anchor, fn) {
82
106
  null,
83
107
  SWITCH_BLOCK,
84
108
  );
109
+
110
+ if (hydrating && root_controlled) {
111
+ append(anchor, /** @type {Node} */ (boundary));
112
+ }
85
113
  }
@@ -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
  `;
@@ -0,0 +1,130 @@
1
+ import { RippleArray, flushSync, track } from 'ripple';
2
+
3
+ // Regression tests for the 0.3.85 optimization (PR #1307) that elides the
4
+ // `<!>` wrapper for single control-flow / component root scopes. When such a
5
+ // scope renders its DOM through a descendant block, the owning block's
6
+ // `s.start` is null, so keyed-for reconciliation must resolve the real first
7
+ // node by descending child blocks instead of reading `s.start` directly.
8
+ // Previously this threw `Cannot use 'in' operator to search for 'parent' in
9
+ // null` from `append()` on a reactive insert.
10
+
11
+ describe('keyed for with single-root item bodies (#1307 regression)', () => {
12
+ it(
13
+ 'prepends into a keyed @for whose body is a single component (single control-flow root)',
14
+ () => {
15
+ function MuzeCard(props) @{
16
+ @if (true) {
17
+ <p>{props.muze.muzeId}</p>
18
+ }
19
+ }
20
+ function App() @{
21
+ const muzes = new RippleArray({ muzeId: 'b' }, { muzeId: 'c' });
22
+ <>
23
+ <button onClick={() => muzes.unshift({ muzeId: 'a' })}>{'add'}</button>
24
+ @for (const muze of muzes; key muze.muzeId) {
25
+ <MuzeCard {muze} />
26
+ }
27
+ </>
28
+ }
29
+ render(App);
30
+ expect(container.querySelectorAll('p').length).toBe(2);
31
+
32
+ container.querySelector('button').click();
33
+ flushSync();
34
+
35
+ expect(Array.from(container.querySelectorAll('p')).map((p) => p.textContent)).toEqual([
36
+ 'a',
37
+ 'b',
38
+ 'c',
39
+ ]);
40
+ },
41
+ );
42
+
43
+ it('reorders a keyed @for of single-control-flow-root components', () => {
44
+ function MuzeCard(props) @{
45
+ @if (true) {
46
+ <p>{props.muze.muzeId}</p>
47
+ }
48
+ }
49
+ function App() @{
50
+ const muzes = new RippleArray({ muzeId: 'a' }, { muzeId: 'b' }, { muzeId: 'c' });
51
+ <>
52
+ <button onClick={() => muzes.reverse()}>{'reverse'}</button>
53
+ @for (const muze of muzes; key muze.muzeId) {
54
+ <MuzeCard {muze} />
55
+ }
56
+ </>
57
+ }
58
+ render(App);
59
+
60
+ container.querySelector('button').click();
61
+ flushSync();
62
+
63
+ expect(Array.from(container.querySelectorAll('p')).map((p) => p.textContent)).toEqual([
64
+ 'c',
65
+ 'b',
66
+ 'a',
67
+ ]);
68
+ });
69
+
70
+ it('inserts in the middle of a keyed @for whose item body is a single @for', () => {
71
+ function App() @{
72
+ const groups = new RippleArray({ id: 'g1', items: ['1'] }, { id: 'g3', items: ['3'] });
73
+ <>
74
+ <button onClick={() => groups.splice(1, 0, { id: 'g2', items: ['2'] })}>{'insert'}</button>
75
+ @for (const group of groups; key group.id) {
76
+ @for (const item of group.items; key item) {
77
+ <p>{item}</p>
78
+ }
79
+ }
80
+ </>
81
+ }
82
+ render(App);
83
+
84
+ container.querySelector('button').click();
85
+ flushSync();
86
+
87
+ expect(Array.from(container.querySelectorAll('p')).map((p) => p.textContent)).toEqual([
88
+ '1',
89
+ '2',
90
+ '3',
91
+ ]);
92
+ });
93
+
94
+ it('realistic shape: keyed @for of components inside an @if > fragment', () => {
95
+ function MuzeCard(props) @{
96
+ @if (true) {
97
+ <p class="muze">{props.muze.muzeId}</p>
98
+ }
99
+ }
100
+ function App() @{
101
+ let &[loaded, loadedT] = track(true);
102
+ const muzes = new RippleArray({ muzeId: 'b' }, { muzeId: 'c' });
103
+ <>
104
+ <button onClick={() => muzes.unshift({ muzeId: 'a' })}>{'add'}</button>
105
+ @if (loaded) {
106
+ <>
107
+ @for (const muze of muzes; key muze.muzeId) {
108
+ <MuzeCard {muze} />
109
+ }
110
+ @if (muzes.length > 0) {
111
+ <span class="count">{String(muzes.length)}</span>
112
+ }
113
+ </>
114
+ }
115
+ </>
116
+ }
117
+ render(App);
118
+ expect(container.querySelectorAll('.muze').length).toBe(2);
119
+
120
+ container.querySelector('button').click();
121
+ flushSync();
122
+
123
+ expect(Array.from(container.querySelectorAll('.muze')).map((p) => p.textContent)).toEqual([
124
+ 'a',
125
+ 'b',
126
+ 'c',
127
+ ]);
128
+ expect(container.querySelector('.count').textContent).toBe('3');
129
+ });
130
+ });
@@ -0,0 +1,105 @@
1
+ import { flushSync, track, Context } from 'ripple';
2
+
3
+ describe('scoped flush', () => {
4
+ // Scoped descent must not change observable behaviour: a tracked shared across
5
+ // sibling subtrees via a module-level variable (escaping its owner subtree)
6
+ // must still update its out-of-subtree subscriber.
7
+ it('updates a sibling subtree that reads a module-smuggled tracked', () => {
8
+ let shared = null;
9
+ let bump = null;
10
+
11
+ function Producer() @{
12
+ let &[v, vT] = track(0);
13
+ shared = vT;
14
+ bump = () => {
15
+ v += 1;
16
+ };
17
+ <div class="producer">{'producer'}</div>
18
+ }
19
+
20
+ function Consumer() @{
21
+ <div class="consumer">
22
+ {shared ? shared.value : -1}
23
+ </div>
24
+ }
25
+
26
+ function App() @{
27
+ <>
28
+ <Producer />
29
+ <Consumer />
30
+ </>
31
+ }
32
+
33
+ render(App);
34
+
35
+ expect(container.querySelector('.consumer').textContent).toBe('0');
36
+
37
+ flushSync(() => bump());
38
+
39
+ expect(container.querySelector('.consumer').textContent).toBe('1');
40
+
41
+ flushSync(() => bump());
42
+
43
+ expect(container.querySelector('.consumer').textContent).toBe('2');
44
+ });
45
+
46
+ // The common, scope-safe case: a Context-provided tracked read at many leaves.
47
+ // A root update must fan out to all leaves; a scoped update must reach only the
48
+ // targeted subtree's leaf and leave the others untouched.
49
+ it('fans out a root update and scopes a nested update', () => {
50
+ const Ctx = new Context(null);
51
+ let bumpRoot = null;
52
+ let bumpLeftLocal = null;
53
+
54
+ function Leaf(props) @{
55
+ const rootT = Ctx.get();
56
+ <span class={'leaf-' + props.id}>
57
+ {rootT ? rootT.value : -1}
58
+ </span>
59
+ }
60
+
61
+ function Branch(props) @{
62
+ let &[local, localT] = track(0);
63
+ if (props.side === 'L') {
64
+ bumpLeftLocal = () => {
65
+ local += 1;
66
+ };
67
+ }
68
+ <div class={'branch-' + props.side}>
69
+ <Leaf id={props.side + '0'} />
70
+ <Leaf id={props.side + '1'} />
71
+ </div>
72
+ }
73
+
74
+ function App() @{
75
+ let &[root, rootT] = track(0);
76
+ Ctx.set(rootT);
77
+ bumpRoot = () => {
78
+ root += 1;
79
+ };
80
+ <div>
81
+ <Branch side="L" />
82
+ <Branch side="R" />
83
+ </div>
84
+ }
85
+
86
+ render(App);
87
+
88
+ const leaves = () => ['L0', 'L1', 'R0', 'R1'].map(
89
+ (id) => container.querySelector('.leaf-' + id).textContent,
90
+ );
91
+
92
+ expect(leaves()).toEqual(['0', '0', '0', '0']);
93
+
94
+ flushSync(() => bumpRoot());
95
+ expect(leaves()).toEqual(['1', '1', '1', '1']);
96
+
97
+ flushSync(() => bumpLeftLocal());
98
+ // Local bump touches no Context value, so leaf text is unchanged, but the
99
+ // update must flush without error and not disturb sibling subtrees.
100
+ expect(leaves()).toEqual(['1', '1', '1', '1']);
101
+
102
+ flushSync(() => bumpRoot());
103
+ expect(leaves()).toEqual(['2', '2', '2', '2']);
104
+ });
105
+ });