ripple 0.3.43 → 0.3.45

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,32 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.45
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1047](https://github.com/Ripple-TS/ripple/pull/1047)
8
+ [`d1acf12`](https://github.com/Ripple-TS/ripple/commit/d1acf129cdd0bf2ee596dbab26ec4df829a33880)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Removes duplicate utils,
10
+ moves most utils to @tsrx/core, include their tests.
11
+
12
+ Fixes some types
13
+
14
+ - Updated dependencies
15
+ [[`d1acf12`](https://github.com/Ripple-TS/ripple/commit/d1acf129cdd0bf2ee596dbab26ec4df829a33880),
16
+ [`d1acf12`](https://github.com/Ripple-TS/ripple/commit/d1acf129cdd0bf2ee596dbab26ec4df829a33880),
17
+ [`3928ac8`](https://github.com/Ripple-TS/ripple/commit/3928ac8816399f9eccfd40081d480042a9d74030)]:
18
+ - @tsrx/ripple@0.0.27
19
+ - ripple@0.3.45
20
+
21
+ ## 0.3.44
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies
26
+ [[`f5a3c1b`](https://github.com/Ripple-TS/ripple/commit/f5a3c1b9e915c250c8cd1a7dcf4e80c44abe720f)]:
27
+ - @tsrx/ripple@0.0.26
28
+ - ripple@0.3.44
29
+
3
30
  ## 0.3.43
4
31
 
5
32
  ### 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.43",
6
+ "version": "0.3.45",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -76,7 +76,7 @@
76
76
  "esm-env": "^1.2.2",
77
77
  "@types/estree": "^1.0.8",
78
78
  "@types/estree-jsx": "^1.0.5",
79
- "@tsrx/ripple": "0.0.25"
79
+ "@tsrx/ripple": "0.0.27"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@types/node": "^24.3.0",
@@ -84,9 +84,9 @@
84
84
  "typescript": "^5.9.3",
85
85
  "@volar/language-core": "~2.4.28",
86
86
  "vscode-languageserver-types": "^3.17.5",
87
- "@tsrx/core": "0.0.23"
87
+ "@tsrx/core": "0.0.25"
88
88
  },
89
89
  "peerDependencies": {
90
- "ripple": "0.3.43"
90
+ "ripple": "0.3.45"
91
91
  }
92
92
  }
@@ -3,11 +3,11 @@
3
3
  * @typedef {EventTarget & Record<string, any>} DelegatedEventTarget
4
4
  */
5
5
  import {
6
- event_name_from_capture,
7
- is_capture_event,
8
- is_non_delegated,
9
- is_passive_event,
10
- } from '../../../utils/events.js';
6
+ eventNameFromCapture as event_name_from_capture,
7
+ isCaptureEvent as is_capture_event,
8
+ isNonDelegated as is_non_delegated,
9
+ isPassiveEvent as is_passive_event,
10
+ } from '@tsrx/core';
11
11
  import {
12
12
  active_block,
13
13
  active_reaction,
@@ -9,10 +9,13 @@ import {
9
9
  is_ripple_object,
10
10
  } from './utils.js';
11
11
  import { event } from './events.js';
12
- import { get_attribute_event_name, is_event_attribute } from '../../../utils/events.js';
12
+ import {
13
+ getAttributeEventName as get_attribute_event_name,
14
+ isEventAttribute as is_event_attribute,
15
+ } from '@tsrx/core';
13
16
  import { get } from './runtime.js';
14
17
  import { clsx } from 'clsx';
15
- import { normalize_css_property_name } from '../../../utils/normalize_css_property_name.js';
18
+ import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
16
19
 
17
20
  /**
18
21
  * @param {Text} text
@@ -28,10 +28,10 @@ import {
28
28
  } from '../client/constants.js';
29
29
  import { DEV } from 'esm-env';
30
30
  import { is_ripple_object, array_slice } from '../client/utils.js';
31
- import { escape, escape_script } from '../../../utils/escaping.js';
32
- import { is_boolean_attribute } from '../../../utils/attributes.js';
31
+ import { escape, escapeScript as escape_script } from '@tsrx/core';
32
+ import { isBooleanAttribute as is_boolean_attribute } from '@tsrx/core';
33
33
  import { clsx } from 'clsx';
34
- import { normalize_css_property_name } from '../../../utils/normalize_css_property_name.js';
34
+ import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
35
35
  import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
36
36
  import { is_tsrx_element, normalize_children, tsrx_element } from '../../element.js';
37
37
  import {
@@ -7,10 +7,13 @@ import { safe_scope, derived } from './internal/client/runtime.js';
7
7
  * @type {new <V>(fn: () => V, start: () => void | (() => void)) => ReactiveValueT<V>}
8
8
  */
9
9
  export const ReactiveValue = /** @type {any} */ (
10
- function ReactiveValue(
11
- /** @type {() => any} */ fn,
12
- /** @type {() => void | (() => void)} */ start,
13
- ) {
10
+ /**
11
+ * @template V
12
+ * @param {() => V} fn
13
+ * @param {() => void | (() => void)} start
14
+ * @returns {Derived}
15
+ */
16
+ function ReactiveValue(fn, start) {
14
17
  if (!new.target) {
15
18
  throw new TypeError('`ReactiveValue` must be called with new');
16
19
  }
@@ -26,7 +29,7 @@ export const ReactiveValue = /** @type {any} */ (
26
29
  s();
27
30
  return fn();
28
31
  },
29
- (/** @type {any} */ _, /** @type {any} */ prev) => prev,
32
+ (_, prev) => prev,
30
33
  );
31
34
  }
32
35
  );
@@ -347,6 +347,9 @@ describe('basic client > components & composition', () => {
347
347
  button: component({ children }: PropsWithChildren<{}>) {
348
348
  <button>{children}</button>
349
349
  },
350
+ arrowButton: component({ children }: PropsWithChildren<{}>) => {
351
+ <button class="arrow-button">{children}</button>
352
+ },
350
353
  };
351
354
 
352
355
  component App() {
@@ -358,6 +361,7 @@ describe('basic client > components & composition', () => {
358
361
  <h1>{'Component as Property Test'}</h1>
359
362
  <UI.span />
360
363
  <UI.button {children} />
364
+ <UI.arrowButton {children} />
361
365
  </div>
362
366
  }
363
367
 
@@ -367,10 +371,13 @@ describe('basic client > components & composition', () => {
367
371
  const span = container.querySelector('span');
368
372
  const button = container.querySelector('button');
369
373
  const buttonSpan = button.querySelector('span');
374
+ const arrowButton = container.querySelector('.arrow-button');
375
+ const arrowButtonSpan = arrowButton.querySelector('span');
370
376
 
371
377
  expect(heading.textContent).toBe('Component as Property Test');
372
378
  expect(span.textContent).toBe('Hello from Span');
373
379
  expect(buttonSpan.textContent).toBe('Click me!');
380
+ expect(arrowButtonSpan.textContent).toBe('Click me!');
374
381
  });
375
382
 
376
383
  it('handles empty string children', () => {
@@ -55,6 +55,20 @@ second"</pre>
55
55
  expect(container.querySelector('.multiline').textContent).toBe('first\nsecond');
56
56
  });
57
57
 
58
+ it('renders bare double-quoted text in if-else branches', () => {
59
+ component App() {
60
+ let condition = false;
61
+
62
+ if (condition) {
63
+ "Hello Ripple"
64
+ } else "Hello React"
65
+ }
66
+
67
+ render(App);
68
+
69
+ expect(container.textContent).toBe('Hello React');
70
+ });
71
+
58
72
  it('renders dynamic text', () => {
59
73
  component Basic() {
60
74
  let &[message] = track('Hello World');
@@ -79,6 +79,48 @@ describe('compiler > basics', () => {
79
79
  );
80
80
  });
81
81
 
82
+ it('parses backtick text inside fragments as JSX text', () => {
83
+ const source = `let a = component () {
84
+ <>
85
+ \`333\`
86
+ </>
87
+ }`;
88
+
89
+ const ast = parse(source);
90
+ const declaration = (ast.body[0] as AST.VariableDeclaration).declarations[0];
91
+ const component_node = declaration.init as unknown as AST.Component;
92
+ const fragment = component_node.body[0] as any;
93
+
94
+ expect(fragment.type).toBe('Tsx');
95
+ expect(fragment.children[0].type).toBe('JSXText');
96
+ expect(fragment.children[0].value).toContain('`333`');
97
+ });
98
+
99
+ it('parses backtick text around JSX elements inside fragments', () => {
100
+ const source = `let a = component () {
101
+ <>
102
+ \`
103
+ <b></b>
104
+ \`
105
+ </>
106
+ }`;
107
+
108
+ const ast = parse(source);
109
+ const declaration = (ast.body[0] as AST.VariableDeclaration).declarations[0];
110
+ const component_node = declaration.init as unknown as AST.Component;
111
+ const fragment = component_node.body[0] as any;
112
+
113
+ expect(fragment.type).toBe('Tsx');
114
+ expect(fragment.children.map((child: any) => child.type)).toEqual([
115
+ 'JSXText',
116
+ 'JSXElement',
117
+ 'JSXText',
118
+ ]);
119
+ expect(fragment.children[0].value).toContain('`');
120
+ expect(fragment.children[1].openingElement.name.name).toBe('b');
121
+ expect(fragment.children[2].value).toContain('`');
122
+ });
123
+
82
124
  it('renders without crashing', () => {
83
125
  component App() {
84
126
  let foo: Record<string, number>;
@@ -282,6 +324,32 @@ describe('compiler > basics', () => {
282
324
  expect(js.code).not.toMatch(/_\$_\.template\(`<!><!>`,\s*1,\s*3\)/);
283
325
  });
284
326
 
327
+ it('emits anonymous component expressions as arrows in client output', () => {
328
+ const source = `
329
+ const Inline = component(props) => {
330
+ <div>{props.x}</div>
331
+ }
332
+ `;
333
+ const result = compile(source, 'anonymous-component.tsrx', { mode: 'client' }).js.code;
334
+
335
+ expect(result).toContain('const Inline = (__anchor, props, __block) => {');
336
+ expect(result).not.toContain('function Inline');
337
+ expect(result).not.toContain('function (__anchor');
338
+ });
339
+
340
+ it('emits legacy anonymous component expressions as functions in client output', () => {
341
+ const source = `
342
+ const Inline = component(props) {
343
+ <div>{props.x}</div>
344
+ }
345
+ `;
346
+ const result = compile(source, 'anonymous-component.tsrx', { mode: 'client' }).js.code;
347
+
348
+ expect(result).toContain('const Inline = function (__anchor, props, __block) {');
349
+ expect(result).not.toContain('function Inline');
350
+ expect(result).not.toContain('const Inline = (__anchor, props, __block) => {');
351
+ });
352
+
285
353
  // it(
286
354
  // 'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
287
355
  // () => {
@@ -426,6 +494,32 @@ export component MyComponent<Item>(props: Props<Item>) {
426
494
  expect(result).toContain('export function MyComponent<Item>(props: Props<Item>)');
427
495
  });
428
496
 
497
+ it('emits anonymous component expressions as arrows in to_ts output', () => {
498
+ const source = `
499
+ const Inline = component(props: { x: string }) => {
500
+ <div>{props.x}</div>
501
+ }
502
+ `;
503
+ const result = compile_to_volar_mappings(source, 'test.tsrx').code;
504
+
505
+ expect(result).toContain('const Inline = (props: { x: string }) => {');
506
+ expect(result).not.toContain('function Inline');
507
+ expect(result).not.toContain('function (props');
508
+ });
509
+
510
+ it('emits legacy anonymous component expressions as functions in to_ts output', () => {
511
+ const source = `
512
+ const Inline = component(props: { x: string }) {
513
+ <div>{props.x}</div>
514
+ }
515
+ `;
516
+ const result = compile_to_volar_mappings(source, 'test.tsrx').code;
517
+
518
+ expect(result).toContain('const Inline = function(props: { x: string }) {');
519
+ expect(result).not.toContain('function Inline');
520
+ expect(result).not.toContain('const Inline = (props: { x: string }) => {');
521
+ });
522
+
429
523
  it('preserves generic type arguments on JSX component tags in to_ts output', () => {
430
524
  const source = `
431
525
  type User = { name: string };
@@ -882,6 +976,22 @@ export component App() {}
882
976
  );
883
977
  });
884
978
 
979
+ it('does not let nested for...of continues satisfy an outer if body', () => {
980
+ const source = `
981
+ export component App({ items }: { items: string[] }) {
982
+ if (items.length) {
983
+ for (const item of items) {
984
+ if (!item) continue
985
+ }
986
+ }
987
+ }`;
988
+
989
+ const result = compile(source, 'test.tsrx', { collect: true });
990
+ expect(result.errors.map((error) => error.message)).toContain(
991
+ 'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
992
+ );
993
+ });
994
+
885
995
  it('preserves class extends generic type arguments in volar output', () => {
886
996
  const source = `class StringMap extends Map<string, string> {}
887
997
  export component App() {}`;
@@ -1,4 +1,5 @@
1
1
  import { RippleArray, flushSync, track } from 'ripple';
2
+ import { compile } from '@tsrx/ripple';
2
3
 
3
4
  describe('for statements', () => {
4
5
  it('renders a simple static array', () => {
@@ -15,6 +16,71 @@ describe('for statements', () => {
15
16
  expect(container).toMatchSnapshot();
16
17
  });
17
18
 
19
+ it('allows continue to skip an iteration', () => {
20
+ component App() {
21
+ const items = ['Item 1', '', 'Item 3'];
22
+
23
+ for (const item of items) {
24
+ if (!item) continue;
25
+ <div class="item">{item}</div>
26
+ }
27
+ }
28
+
29
+ render(App);
30
+
31
+ expect(Array.from(container.querySelectorAll('.item')).map((el) => el.textContent)).toEqual([
32
+ 'Item 1',
33
+ 'Item 3',
34
+ ]);
35
+ });
36
+
37
+ it('allows continue after setup statements to skip an iteration', () => {
38
+ const skipped = [];
39
+
40
+ component App() {
41
+ const items = ['Item 1', '', 'Item 3'];
42
+
43
+ for (const item of items) {
44
+ if (!item) {
45
+ skipped.push('skip');
46
+ continue;
47
+ }
48
+ <div class="item">{item}</div>
49
+ }
50
+ }
51
+
52
+ render(App);
53
+
54
+ expect(skipped).toEqual(['skip']);
55
+ expect(Array.from(container.querySelectorAll('.item')).map((el) => el.textContent)).toEqual([
56
+ 'Item 1',
57
+ 'Item 3',
58
+ ]);
59
+ });
60
+
61
+ it('does not emit JavaScript continue in for...of skip callbacks', () => {
62
+ const { js } = compile(
63
+ `component App() {
64
+ const items = ['Item 1', '', 'Item 3'];
65
+ const skipped = [];
66
+
67
+ for (const item of items) {
68
+ if (!item) {
69
+ skipped.push('skip');
70
+ continue;
71
+ }
72
+ <div class="item">{item}</div>
73
+ }
74
+ }`,
75
+ 'App.tsrx',
76
+ { mode: 'client' },
77
+ );
78
+
79
+ expect(js.code).toContain('skipped.push(\'skip\')');
80
+ expect(js.code).not.toContain('continue;');
81
+ expect(js.code).not.toMatch(/continue;\s*return/);
82
+ });
83
+
18
84
  it('renders a simple dynamic array', () => {
19
85
  component App() {
20
86
  const items = new RippleArray('Item 1', 'Item 2', 'Item 3');
@@ -172,6 +172,9 @@ describe('basic server > components & composition', () => {
172
172
  button: component({ children }: PropsWithChildren<{}>) {
173
173
  <button>{children}</button>
174
174
  },
175
+ arrowButton: component({ children }: PropsWithChildren<{}>) => {
176
+ <button class="arrow-button">{children}</button>
177
+ },
175
178
  };
176
179
 
177
180
  component App() {
@@ -183,6 +186,7 @@ describe('basic server > components & composition', () => {
183
186
  <h1>{'Component as Property Test'}</h1>
184
187
  <UI.span />
185
188
  <UI.button {children} />
189
+ <UI.arrowButton {children} />
186
190
  </div>
187
191
  }
188
192
 
@@ -193,10 +197,13 @@ describe('basic server > components & composition', () => {
193
197
  const span = document.querySelector('span');
194
198
  const button = document.querySelector('button');
195
199
  const buttonSpan = button.querySelector('span');
200
+ const arrowButton = document.querySelector('.arrow-button');
201
+ const arrowButtonSpan = arrowButton.querySelector('span');
196
202
 
197
203
  expect(heading.textContent).toBe('Component as Property Test');
198
204
  expect(span.textContent).toBe('Hello from Span');
199
205
  expect(buttonSpan.textContent).toBe('Click me!');
206
+ expect(arrowButtonSpan.textContent).toBe('Click me!');
200
207
  });
201
208
 
202
209
  it('handles empty string children', async () => {
@@ -87,6 +87,44 @@ export default component A() {
87
87
  expect(result).not.toContain(`"Hello";`);
88
88
  });
89
89
 
90
+ it('decodes JSX-style entities before server text escaping', () => {
91
+ const result = compile(
92
+ `component App() {
93
+ <div>"Rock &amp; &quot;Roll&quot;"</div>
94
+ }`,
95
+ '/src/App.tsrx',
96
+ { mode: 'server' },
97
+ );
98
+
99
+ expect(result.js.code).toContain(`_$_.output_push('Rock &amp; "Roll"')`);
100
+ });
101
+
102
+ it('emits anonymous component expressions as arrows in SSR output', () => {
103
+ const source = `
104
+ const Inline = component(props) => {
105
+ <div>{props.x}</div>
106
+ }
107
+ `;
108
+ const result = compile(source, 'anonymous-component.tsrx', { mode: 'server' }).js.code;
109
+
110
+ expect(result).toContain('const Inline = (props) => {');
111
+ expect(result).not.toContain('function Inline');
112
+ expect(result).not.toContain('function (props');
113
+ });
114
+
115
+ it('emits legacy anonymous component expressions as functions in SSR output', () => {
116
+ const source = `
117
+ const Inline = component(props) {
118
+ <div>{props.x}</div>
119
+ }
120
+ `;
121
+ const result = compile(source, 'anonymous-component.tsrx', { mode: 'server' }).js.code;
122
+
123
+ expect(result).toContain('const Inline = function (props) {');
124
+ expect(result).not.toContain('function Inline');
125
+ expect(result).not.toContain('const Inline = (props) => {');
126
+ });
127
+
90
128
  it('throws error for calling children as a function in SSR mode', () => {
91
129
  const source = `
92
130
  export component Layout({ children }) {
@@ -95,4 +95,71 @@ describe('for statements in SSR', () => {
95
95
  '<div class="Item 1 0">Item 1 0</div><div class="Item 2 1">Item 2 1</div><div class="Item 3 2">Item 3 2</div>',
96
96
  );
97
97
  });
98
+
99
+ it('allows continue to skip a for...of iteration', async () => {
100
+ component App() {
101
+ const items = ['Item 1', '', 'Item 3'];
102
+
103
+ for (const item of items) {
104
+ if (!item) continue;
105
+ <div>{item}</div>
106
+ }
107
+ }
108
+
109
+ const { body } = await render(App);
110
+ expect(body).toBeHtml('<div>Item 1</div><div>Item 3</div>');
111
+ });
112
+
113
+ it('allows ordinary function control flow inside for...of loops', async () => {
114
+ component App() {
115
+ const items = ['Item 1', '', 'Item 3'];
116
+
117
+ for (const item of items) {
118
+ function label(value) {
119
+ for (let i = 0; i < 1; i++) {
120
+ while (i < 0) {
121
+ break;
122
+ }
123
+ if (!value) return 'missing';
124
+ }
125
+ return value;
126
+ }
127
+
128
+ <div>{label(item)}</div>
129
+ }
130
+ }
131
+
132
+ const { body } = await render(App);
133
+ expect(body).toBeHtml('<div>Item 1</div><div>missing</div><div>Item 3</div>');
134
+ });
135
+
136
+ it('throws for return statements inside for...of loops', () => {
137
+ expect(
138
+ () => compile(
139
+ `component App(items) {
140
+ for (const item of items) {
141
+ if (!item) return
142
+ <div>{item}</div>
143
+ }
144
+ }`,
145
+ 'App.tsrx',
146
+ { mode: 'server' },
147
+ ),
148
+ ).toThrow('Return statements are not allowed inside component for...of loops');
149
+ });
150
+
151
+ it('throws for break statements targeting for...of loops', () => {
152
+ expect(
153
+ () => compile(
154
+ `component App(items) {
155
+ for (const item of items) {
156
+ if (!item) break
157
+ <div>{item}</div>
158
+ }
159
+ }`,
160
+ 'App.tsrx',
161
+ { mode: 'server' },
162
+ ),
163
+ ).toThrow('Break statements are not allowed inside component for...of loops');
164
+ });
98
165
  });
@@ -27,6 +27,19 @@ describe('if statements in SSR', () => {
27
27
  expect(body).toBeHtml('<div>Else block</div>');
28
28
  });
29
29
 
30
+ it('renders bare double-quoted text in if-else branches', async () => {
31
+ component App() {
32
+ let condition = false;
33
+
34
+ if (condition) {
35
+ "Hello Ripple"
36
+ } else "Hello React"
37
+ }
38
+
39
+ const { body } = await render(App);
40
+ expect(body).toBeHtml('Hello React');
41
+ });
42
+
30
43
  it('renders else if block when condition is true', async () => {
31
44
  component App() {
32
45
  let value = 'b';
package/types/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { ExtendedEventOptions } from '@tsrx/core/types';
2
+ export type { AddEventOptions, AddEventObject, ExtendedEventOptions } from '@tsrx/core/types';
3
+
1
4
  export type Component<T = Record<string, any>> = (props: T) => void;
2
5
 
3
6
  declare const TSRX_ELEMENT: unique symbol;
@@ -201,16 +204,6 @@ export function trackPending<V>(value: Tracked<V> | (() => any)): boolean;
201
204
 
202
205
  export function peek<V>(tracked: Tracked<V>): V;
203
206
 
204
- export interface AddEventOptions extends ExtendedEventOptions {
205
- customName?: string;
206
- }
207
-
208
- export interface AddEventObject extends AddEventOptions, EventListenerObject {}
209
-
210
- export interface ExtendedEventOptions extends AddEventListenerOptions, EventListenerOptions {
211
- delegated?: boolean;
212
- }
213
-
214
207
  export type OnEventListenerRemover = () => void;
215
208
 
216
209
  export function on<Type extends keyof WindowEventMap>(
@@ -364,7 +357,7 @@ export const RippleURL: RippleURLConstructor;
364
357
  export function createSubscriber(start: () => void | (() => void)): () => void;
365
358
 
366
359
  declare const REACTIVE_VALUE_BRAND: unique symbol;
367
- interface ReactiveValue<V> extends Tracked<V> {
360
+ export interface ReactiveValue<V> extends Tracked<V> {
368
361
  new (fn: () => Tracked<V>, start: () => void | (() => void)): Tracked<V>;
369
362
  [REACTIVE_VALUE_BRAND]: void;
370
363
  }