ripple 0.3.61 → 0.3.63

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,56 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.63
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
8
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Parse nested `<tsrx>` islands
10
+ inside `<tsx>` expression containers as native TSRX so setup declarations and
11
+ references keep Volar mappings, and hydrate deeply nested `<tsx>`/`<tsrx>`
12
+ expression values without skipping server markers.
13
+
14
+ - [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
15
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
16
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Avoid duplicating plain text
17
+ when hydrating mixed TSRX collection values.
18
+
19
+ - [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
20
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
21
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Fix to_ts output for nested
22
+ `<tsrx>` islands inside `<tsx>` blocks.
23
+
24
+ Type JSX expression values as `TSRXElement` so IntelliSense reports assigned
25
+ TSX/TSRX fragments as renderable values instead of `void`.
26
+
27
+ Fix TextMate highlighting for nested `<tsrx>` and `<tsx>` tags inside JSX
28
+ expression containers.
29
+
30
+ - [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
31
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
32
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Render nested `<tsx>` and
33
+ `<tsrx>` expression values, including arrays returned from JSX-style
34
+ expressions.
35
+
36
+ - Updated dependencies
37
+ [[`2acbbea`](https://github.com/Ripple-TS/ripple/commit/2acbbea9253ac8f516fe0d3a7a38331490e6fd8b),
38
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04),
39
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04),
40
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04),
41
+ [`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)]:
42
+ - @tsrx/core@0.1.12
43
+ - @tsrx/ripple@0.1.12
44
+
45
+ ## 0.3.62
46
+
47
+ ### Patch Changes
48
+
49
+ - [#1144](https://github.com/Ripple-TS/ripple/pull/1144)
50
+ [`0e8baf2`](https://github.com/Ripple-TS/ripple/commit/0e8baf278e4105ae019929138956938cd5189035)
51
+ Thanks [@aleclarson](https://github.com/aleclarson)! - Remove the stale self
52
+ peer dependency from the Ripple runtime package.
53
+
3
54
  ## 0.3.61
4
55
 
5
56
  ### 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.61",
6
+ "version": "0.3.63",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -74,19 +74,12 @@
74
74
  "clsx": "^2.1.1",
75
75
  "devalue": "^5.8.1",
76
76
  "esm-env": "^1.2.2",
77
- "@types/estree": "^1.0.8",
78
- "@types/estree-jsx": "^1.0.5",
79
- "@tsrx/core": "0.1.11",
80
- "@tsrx/ripple": "0.1.11"
77
+ "@tsrx/core": "0.1.12",
78
+ "@tsrx/ripple": "0.1.12"
81
79
  },
82
80
  "devDependencies": {
83
- "@types/node": "^24.3.0",
84
- "@typescript-eslint/types": "^8.40.0",
85
- "typescript": "^5.9.3",
86
- "@volar/language-core": "~2.4.28",
87
- "vscode-languageserver-types": "^3.17.5"
88
- },
89
- "peerDependencies": {
90
- "ripple": "0.3.61"
81
+ "@types/estree": "^1.0.8",
82
+ "@types/estree-jsx": "^1.0.5",
83
+ "@types/node": "^24.3.0"
91
84
  }
92
85
  }
@@ -1,9 +1,10 @@
1
- import type { AddEventObject } from '#public';
1
+ import type { AddEventObject, TSRXElement } from '#public';
2
2
  import type { Nullable } from '#helpers';
3
3
 
4
4
  /**
5
5
  * Ripple JSX Runtime Type Definitions
6
- * Ripple components are imperative and don't return JSX elements
6
+ * Ripple components are imperative, but JSX expressions still represent
7
+ * renderable TSRX values when used in expression positions.
7
8
  */
8
9
 
9
10
  // Ripple components don't return JSX elements - they're imperative
@@ -17,13 +18,13 @@ export function jsx(
17
18
  type: string | ComponentType<any>,
18
19
  props?: any,
19
20
  key?: string | number | null,
20
- ): void;
21
+ ): TSRXElement;
21
22
 
22
23
  export function rsx(
23
24
  type: string | ComponentType<any>,
24
25
  props?: any,
25
26
  key?: string | number | null,
26
- ): void;
27
+ ): TSRXElement;
27
28
 
28
29
  /**
29
30
  * Create a JSX element with static children (optimization for multiple children)
@@ -33,13 +34,13 @@ export function jsxs(
33
34
  type: string | ComponentType<any>,
34
35
  props?: any,
35
36
  key?: string | number | null,
36
- ): void;
37
+ ): TSRXElement;
37
38
 
38
39
  /**
39
40
  * JSX Fragment component
40
- * In Ripple, fragments are imperative and don't return anything
41
+ * Ripple fragments are renderable expression values.
41
42
  */
42
- export function Fragment(props: { children?: any }): void;
43
+ export function Fragment(props: { children?: any }): TSRXElement;
43
44
 
44
45
  export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;
45
46
 
@@ -819,8 +820,8 @@ interface SVGTextAttributes {
819
820
  // Global JSX namespace for TypeScript
820
821
  declare global {
821
822
  namespace JSX {
822
- // In Ripple, JSX expressions don't return elements - they're imperative
823
- type Element = void;
823
+ type Element = TSRXElement;
824
+ type ElementType = keyof IntrinsicElements | ComponentType<any>;
824
825
 
825
826
  interface IntrinsicElements {
826
827
  // Document metadata
@@ -1,10 +1,12 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
+ import { is_array } from '@tsrx/core/runtime/language-helpers';
3
4
  import { branch, destroy_block, render } from './blocks.js';
4
5
  import { BRANCH_BLOCK, UNINITIALIZED } from './constants.js';
5
6
  import { create_text, get_next_sibling } from './operations.js';
7
+ import { assign_nodes } from './template.js';
6
8
  import { active_block } from './runtime.js';
7
- import { hydrating, set_hydrate_node } from './hydration.js';
9
+ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
8
10
  import { COMMENT_NODE, HYDRATION_END, HYDRATION_START, TEXT_NODE } from '../../../constants.js';
9
11
  import { is_tsrx_element } from '../../element.js';
10
12
 
@@ -23,6 +25,93 @@ function find_enclosing_branch(block) {
23
25
  return null;
24
26
  }
25
27
 
28
+ /**
29
+ * @param {any[]} value
30
+ * @param {ChildNode} anchor
31
+ * @param {Block} block
32
+ * @returns {void}
33
+ */
34
+ function render_tsrx_collection(value, anchor, block) {
35
+ if (hydrating) {
36
+ assign_nodes(/** @type {Node} */ (hydrate_node ?? anchor), anchor);
37
+ render_tsrx_collection_items(value, anchor, block);
38
+ return;
39
+ }
40
+
41
+ var start = document.createComment('');
42
+ var end = document.createComment('');
43
+
44
+ anchor.before(start, end);
45
+ assign_nodes(start, end);
46
+ render_tsrx_collection_items(value, end, block);
47
+ }
48
+
49
+ /**
50
+ * @param {any[]} value
51
+ * @param {ChildNode} anchor
52
+ * @param {Block} block
53
+ * @returns {void}
54
+ */
55
+ function render_tsrx_collection_items(value, anchor, block) {
56
+ for (var i = 0; i < value.length; i++) {
57
+ var item = value[i];
58
+
59
+ if (is_tsrx_element(item)) {
60
+ item.render(anchor, block);
61
+ } else if (is_array(item)) {
62
+ render_tsrx_collection_items(item, anchor, block);
63
+ } else if (item != null) {
64
+ render_tsrx_collection_text(item + '', anchor);
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @param {string} value
71
+ * @param {ChildNode} anchor
72
+ * @returns {void}
73
+ */
74
+ function render_tsrx_collection_text(value, anchor) {
75
+ if (!hydrating) {
76
+ anchor.before(create_text(value));
77
+ return;
78
+ }
79
+
80
+ var node = hydrate_node;
81
+
82
+ if (node?.nodeType === TEXT_NODE) {
83
+ var current_value = /** @type {Text} */ (node).nodeValue ?? '';
84
+
85
+ if (current_value !== value) {
86
+ /** @type {Text} */ (node).nodeValue = value;
87
+
88
+ if (current_value.startsWith(value)) {
89
+ var remaining = current_value.slice(value.length);
90
+
91
+ if (remaining !== '') {
92
+ var remaining_text = create_text(remaining);
93
+ /** @type {ChildNode} */ (node).after(remaining_text);
94
+ set_hydrate_node(remaining_text);
95
+ return;
96
+ }
97
+ }
98
+ }
99
+
100
+ set_hydrate_node(get_next_sibling(node) ?? anchor);
101
+ return;
102
+ }
103
+
104
+ var new_text = create_text(value);
105
+
106
+ if (node !== null && node !== anchor) {
107
+ /** @type {ChildNode} */ (node).before(new_text);
108
+ } else {
109
+ anchor.before(new_text);
110
+ }
111
+
112
+ set_hydrate_node(node ?? anchor);
113
+ }
114
+
26
115
  /**
27
116
  * @param {Node} node
28
117
  * @param {() => any} get_value
@@ -47,7 +136,8 @@ export function expression(node, get_value) {
47
136
 
48
137
  render(() => {
49
138
  var next_value = get_value();
50
- var next_is_element = is_tsrx_element(next_value);
139
+ var next_is_collection = is_array(next_value);
140
+ var next_is_element = next_is_collection || is_tsrx_element(next_value);
51
141
  var is_hydration_marker = hydrating && anchor.nodeType === COMMENT_NODE;
52
142
 
53
143
  if (is_hydration_marker) {
@@ -93,8 +183,12 @@ export function expression(node, get_value) {
93
183
  var parent_branch = find_enclosing_branch(active_block);
94
184
 
95
185
  child_block = branch(() => {
96
- var block = active_block;
97
- next_value.render(end ?? anchor, block);
186
+ var block = /** @type {Block} */ (active_block);
187
+ if (next_is_collection) {
188
+ render_tsrx_collection(next_value, end ?? anchor, block);
189
+ } else {
190
+ next_value.render(end ?? anchor, block);
191
+ }
98
192
  });
99
193
 
100
194
  // Update parent branch's s.start to include content inserted before anchor.
@@ -1,6 +1,9 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
3
  import {
4
+ COMMENT_NODE,
5
+ HYDRATION_END,
6
+ HYDRATION_START,
4
7
  TEMPLATE_FRAGMENT,
5
8
  TEMPLATE_USE_IMPORT_NODE,
6
9
  TEMPLATE_SVG_NAMESPACE,
@@ -177,6 +180,10 @@ export function append(anchor, dom, skip_advance) {
177
180
  if (s !== null) {
178
181
  s.end = /** @type {Node} */ (hydrate_node);
179
182
  }
183
+
184
+ if (is_after_hydration_block(dom, hydrate_node)) {
185
+ return;
186
+ }
180
187
  }
181
188
 
182
189
  // Only advance if there's a next sibling. At the end of a component's
@@ -187,6 +194,44 @@ export function append(anchor, dom, skip_advance) {
187
194
  anchor.before(/** @type {Node} */ (dom));
188
195
  }
189
196
 
197
+ /**
198
+ * @param {Node} start
199
+ * @param {Node | null} target
200
+ * @returns {boolean}
201
+ */
202
+ function is_after_hydration_block(start, target) {
203
+ if (
204
+ target === null ||
205
+ start.nodeType !== COMMENT_NODE ||
206
+ /** @type {Comment} */ (start).data !== HYDRATION_START
207
+ ) {
208
+ return false;
209
+ }
210
+
211
+ var current = get_next_sibling(start);
212
+ var depth = 0;
213
+
214
+ while (current !== null && current !== target) {
215
+ if (current.nodeType === COMMENT_NODE) {
216
+ var data = /** @type {Comment} */ (current).data;
217
+
218
+ if (data === HYDRATION_START) {
219
+ depth += 1;
220
+ } else if (data === HYDRATION_END) {
221
+ if (depth === 0) {
222
+ return true;
223
+ }
224
+
225
+ depth -= 1;
226
+ }
227
+ }
228
+
229
+ current = get_next_sibling(current);
230
+ }
231
+
232
+ return false;
233
+ }
234
+
190
235
  export function text(data = '') {
191
236
  if (hydrating) {
192
237
  assign_nodes(/** @type {Node} */ (hydrate_node), /** @type {Node} */ (hydrate_node));
@@ -28,7 +28,7 @@ import {
28
28
  } from '../client/constants.js';
29
29
  import { DEV } from 'esm-env';
30
30
  import { is_ripple_object } from '../client/utils.js';
31
- import { array_slice } from '@tsrx/core/runtime/language-helpers';
31
+ import { array_slice, is_array } from '@tsrx/core/runtime/language-helpers';
32
32
  import {
33
33
  escape,
34
34
  escape_script,
@@ -83,6 +83,24 @@ export class TrackAsyncRunError extends Error {
83
83
 
84
84
  export function noop() {}
85
85
 
86
+ /**
87
+ * @param {any[]} value
88
+ * @returns {void}
89
+ */
90
+ function render_tsrx_collection(value) {
91
+ for (var i = 0; i < value.length; i++) {
92
+ var item = value[i];
93
+
94
+ if (is_tsrx_element(item)) {
95
+ item.render({});
96
+ } else if (is_array(item)) {
97
+ render_tsrx_collection(item);
98
+ } else if (item != null) {
99
+ output_push(escape(item));
100
+ }
101
+ }
102
+ }
103
+
86
104
  /**
87
105
  * @param {any} value
88
106
  * @returns {void}
@@ -92,6 +110,8 @@ export function render_expression(value) {
92
110
 
93
111
  if (is_tsrx_element(value)) {
94
112
  value.render({});
113
+ } else if (is_array(value)) {
114
+ render_tsrx_collection(value);
95
115
  } else {
96
116
  output_push(escape(value ?? ''));
97
117
  }
@@ -2,6 +2,17 @@ import { RippleArray, flushSync, track } from 'ripple';
2
2
  import { TRACKED_ARRAY } from '../../../src/runtime/internal/client/constants.js';
3
3
 
4
4
  describe('basic client > collections', () => {
5
+ function countCommentNodes(node: Node) {
6
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT);
7
+ let count = 0;
8
+
9
+ while (walker.nextNode()) {
10
+ count++;
11
+ }
12
+
13
+ return count;
14
+ }
15
+
5
16
  it('renders with simple reactive objects', () => {
6
17
  component Basic() {
7
18
  let &[user] = track({
@@ -106,4 +117,113 @@ describe('basic client > collections', () => {
106
117
  expect(pre1.textContent).toBe('4');
107
118
  expect(pre2.textContent).toBe('2');
108
119
  });
120
+
121
+ it('cleans up mixed tsrx collection sentinels and trailing text on rerender', () => {
122
+ component App() {
123
+ let &[primary] = track(true);
124
+
125
+ <div class="frame">
126
+ {<tsx>
127
+ {primary
128
+ ? [
129
+ 'first:',
130
+ <strong class="middle">
131
+ {'one'}
132
+ </strong>,
133
+ ':tail',
134
+ ]
135
+ : [
136
+ 'second:',
137
+ <strong class="middle">
138
+ {'two'}
139
+ </strong>,
140
+ ':done',
141
+ ]}
142
+ </tsx>}
143
+ </div>
144
+ <button
145
+ onClick={() => {
146
+ primary = !primary;
147
+ }}
148
+ >
149
+ {'toggle'}
150
+ </button>
151
+ }
152
+
153
+ render(App);
154
+
155
+ const frame = container.querySelector('.frame');
156
+ const button = container.querySelector('button');
157
+ const initialCommentCount = countCommentNodes(frame);
158
+
159
+ expect(frame.textContent).toBe('first:one:tail');
160
+ expect(frame.querySelectorAll('.middle')).toHaveLength(1);
161
+
162
+ button.click();
163
+ flushSync();
164
+
165
+ expect(frame.textContent).toBe('second:two:done');
166
+ expect(frame.querySelectorAll('.middle')).toHaveLength(1);
167
+ expect(countCommentNodes(frame)).toBe(initialCommentCount);
168
+
169
+ button.click();
170
+ flushSync();
171
+
172
+ expect(frame.textContent).toBe('first:one:tail');
173
+ expect(frame.querySelectorAll('.middle')).toHaveLength(1);
174
+ expect(countCommentNodes(frame)).toBe(initialCommentCount);
175
+ });
176
+
177
+ it('flattens nested primitive arrays inside mixed tsrx collections', () => {
178
+ component App() {
179
+ <div class="frame">
180
+ {<tsx>
181
+ {[
182
+ 'start:',
183
+ ['one', 2, true, null, false],
184
+ <strong>
185
+ {'!'}
186
+ </strong>,
187
+ ':end',
188
+ ]}
189
+ </tsx>}
190
+ </div>
191
+ }
192
+
193
+ render(App);
194
+
195
+ expect(container.querySelector('.frame').textContent).toBe('start:one2truefalse!:end');
196
+ });
197
+
198
+ it('flattens direct primitive array expressions', () => {
199
+ component App() {
200
+ const items = ['start:', ['one', 2], null, true, false, ':end'];
201
+
202
+ <div class="frame">{['start:', ['one', 2], null, true, false, ':end']}</div>
203
+ <div class="bound-frame">{items}</div>
204
+ }
205
+
206
+ render(App);
207
+
208
+ expect(container.querySelector('.frame').textContent).toBe('start:one2truefalse:end');
209
+ expect(container.querySelector('.bound-frame').textContent).toBe('start:one2truefalse:end');
210
+ });
211
+
212
+ it('flattens conditional primitive array expressions', () => {
213
+ component App() {
214
+ const condition = true;
215
+ const ternary_items = condition
216
+ ? ['start:', ['one', 2], null, true, false, ':end']
217
+ : ['fallback'];
218
+ const logical_items = condition && ['start:', ['one', 2], null, true, false, ':end'];
219
+
220
+ <div class="ternary-frame">{ternary_items}</div>
221
+ <div class="logical-frame">{logical_items}</div>
222
+ }
223
+
224
+ render(App);
225
+
226
+ expect(container.querySelector('.ternary-frame').textContent).toBe('start:one2truefalse:end');
227
+ expect(container.querySelector('.logical-frame').textContent).toBe('start:one2truefalse:end');
228
+ });
109
229
  });
@@ -350,6 +350,25 @@ const Inline = component(props) {
350
350
  expect(result).not.toContain('const Inline = (__anchor, props, __block) => {');
351
351
  });
352
352
 
353
+ it('emits function calls with nested template returns as expressions in client output', () => {
354
+ const source = `
355
+ component App() {
356
+ function make(flag) {
357
+ if (flag) {
358
+ return <tsx><span>{'nested'}</span></tsx>;
359
+ }
360
+
361
+ return null;
362
+ }
363
+
364
+ <div>{make(true)}</div>
365
+ }
366
+ `;
367
+ const result = compile(source, 'nested-template-return.tsrx', { mode: 'client' }).code;
368
+
369
+ expect(result).toContain('_$_.expression(expression, () => make(true))');
370
+ });
371
+
353
372
  // it(
354
373
  // 'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
355
374
  // () => {
@@ -460,6 +479,69 @@ component App() {
460
479
  expect(track_result).not.toContain('lazy0');
461
480
  });
462
481
 
482
+ it('lowers nested tsrx inside tsx in to_ts output', () => {
483
+ const source = `
484
+ component App() {
485
+ const content = <tsx>
486
+ {<tsrx>
487
+ const nested = <tsx>
488
+ <span class="nested-tsx">
489
+ {'inside nested tsx'}
490
+ </span>
491
+ </tsx>;
492
+ <div class="native">{nested}</div>
493
+ </tsrx>}
494
+ </tsx>;
495
+
496
+ {content}
497
+ }
498
+ `;
499
+ const result = compile_to_volar_mappings(source, 'test.tsrx').code;
500
+
501
+ expect(result).not.toContain('<tsrx>');
502
+ expect(result).not.toContain('</tsrx>');
503
+ expect(result).not.toContain('<tsx>');
504
+ expect(result).not.toContain('</tsx>');
505
+ expect(result).toContain('const nested = <>');
506
+ expect(result).toContain('children.push(<div class="native">');
507
+ });
508
+
509
+ it('maps identifiers from nested tsrx inside tsx in to_ts output', () => {
510
+ const source = `
511
+ component App() {
512
+ const content = <tsx>
513
+ {<tsrx>
514
+ const nested = <tsx>
515
+ <span class="nested-tsx">
516
+ {'inside nested tsx'}
517
+ </span>
518
+ </tsx>;
519
+ <div class="native">{nested}</div>
520
+ </tsrx>}
521
+ </tsx>;
522
+
523
+ {content}
524
+ }
525
+ `;
526
+ const result = compile_to_volar_mappings(source, 'test.tsrx', { loose: true });
527
+ const source_declaration = source.indexOf('nested =');
528
+ const source_reference = source.indexOf('nested}</div>');
529
+ const generated_declaration = result.code.indexOf('const nested') + 'const '.length;
530
+ const generated_reference = result.code.indexOf('nested;', generated_declaration);
531
+
532
+ function find_mapping(source_offset: number, generated_offset: number) {
533
+ return result.mappings.find(
534
+ (mapping) => mapping.sourceOffsets[0] === source_offset &&
535
+ mapping.generatedOffsets[0] === generated_offset &&
536
+ mapping.lengths[0] === 'nested'.length &&
537
+ mapping.generatedLengths[0] === 'nested'.length,
538
+ );
539
+ }
540
+
541
+ expect(find_mapping(source_declaration, generated_declaration)).toBeDefined();
542
+ expect(find_mapping(source_reference, generated_reference)).toBeDefined();
543
+ });
544
+
463
545
  it('preserves optional markers in to_ts TypeScript output', () => {
464
546
  const source = `
465
547
  export type OptionalTuple = [bar: string, baz?: string];