ripple 0.2.111 → 0.2.113

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/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.2.111",
6
+ "version": "0.2.113",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -38,6 +38,11 @@
38
38
  "require": "./src/compiler/index.js",
39
39
  "default": "./src/compiler/index.js"
40
40
  },
41
+ "./bindings": {
42
+ "types": "./src/bindings/index.d.ts",
43
+ "require": "./src/bindings/index.js",
44
+ "default": "./src/bindings/index.js"
45
+ },
41
46
  "./validator": {
42
47
  "types": "./types/index.d.ts",
43
48
  "require": "./validator/index.js",
@@ -67,6 +72,7 @@
67
72
  "@sveltejs/acorn-typescript": "^1.0.6",
68
73
  "acorn": "^8.15.0",
69
74
  "clsx": "^2.1.1",
75
+ "devalue": "^5.3.2",
70
76
  "esm-env": "^1.2.2",
71
77
  "esrap": "^2.1.0",
72
78
  "is-reference": "^3.0.3",
@@ -0,0 +1,13 @@
1
+ import type { Tracked } from "ripple";
2
+
3
+ /**
4
+ * @param {Tracked<V>} tracked
5
+ * @returns {(node: HTMLInputElement) => void}
6
+ */
7
+ export declare function value<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
8
+
9
+ /**
10
+ * @param {Tracked<V>} tracked
11
+ * @returns {(node: HTMLInputElement) => void}
12
+ */
13
+ export declare function checked<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
@@ -0,0 +1,79 @@
1
+ /** @import {Block, Tracked} from '#client' */
2
+
3
+ import { active_block, get, set, tick } from '../runtime/internal/client';
4
+ import { on } from '../runtime/internal/client/events';
5
+ import { is_tracked_object } from '../runtime/internal/client/utils';
6
+
7
+ /**
8
+ * @param {string} value
9
+ */
10
+ function to_number(value) {
11
+ return value === '' ? null : +value;
12
+ }
13
+
14
+ /**
15
+ * @param {HTMLInputElement} input
16
+ */
17
+ function is_numberlike_input(input) {
18
+ var type = input.type;
19
+ return type === 'number' || type === 'range';
20
+ }
21
+
22
+ /**
23
+ * @param {unknown} maybe_tracked
24
+ * @returns {(node: HTMLInputElement) => void}
25
+ */
26
+ export function value(maybe_tracked) {
27
+ if (!is_tracked_object(maybe_tracked)) {
28
+ throw new TypeError('value() argument is not a tracked object');
29
+ }
30
+
31
+ const block = /** @type {Block} */ (active_block);
32
+ const tracked = /** @type {Tracked} */ (maybe_tracked);
33
+
34
+ return (input) => {
35
+ const clear_event = on(input, 'input', async () => {
36
+ /** @type {any} */
37
+ var value = input.value;
38
+ value = is_numberlike_input(input) ? to_number(value) : value;
39
+ set(tracked, value, block);
40
+
41
+ await tick();
42
+
43
+ if (value !== (value = get(tracked))) {
44
+ var start = input.selectionStart;
45
+ var end = input.selectionEnd;
46
+ input.value = value ?? '';
47
+
48
+ // Restore selection
49
+ if (end !== null) {
50
+ input.selectionStart = start;
51
+ input.selectionEnd = Math.min(end, input.value.length);
52
+ }
53
+ }
54
+ });
55
+
56
+ return clear_event;
57
+ };
58
+ }
59
+
60
+ /**
61
+ * @param {unknown} maybe_tracked
62
+ * @returns {(node: HTMLInputElement) => void}
63
+ */
64
+ export function checked(maybe_tracked) {
65
+ if (!is_tracked_object(maybe_tracked)) {
66
+ throw new TypeError('checked() argument is not a tracked object');
67
+ }
68
+
69
+ const block = /** @type {any} */ (active_block);
70
+ const tracked = /** @type {Tracked<any>} */ (maybe_tracked);
71
+
72
+ return (input) => {
73
+ const clear_event = on(input, 'change', () => {
74
+ set(tracked, input.checked, block);
75
+ });
76
+
77
+ return clear_event;
78
+ };
79
+ }
@@ -106,7 +106,11 @@ function RipplePlugin(config) {
106
106
  if (inComponent) {
107
107
  // Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
108
108
  // At component top-level (scopeStack.length <= 4), apply JSX detection logic
109
- if (this.scopeStack.length >= 5) {
109
+ // BUT: if the < is followed by /, it's a closing JSX tag, not a less-than operator
110
+ const nextChar = this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
111
+ const isClosingTag = nextChar === 47; // '/'
112
+
113
+ if (this.scopeStack.length >= 5 && !isClosingTag) {
110
114
  // Inside function - treat as TypeScript generic, not JSX
111
115
  ++this.pos;
112
116
  return this.finishToken(tt.relational, '<');
@@ -59,6 +59,10 @@ function visit_function(node, context) {
59
59
 
60
60
  for (const param of node.params) {
61
61
  delete param.typeAnnotation;
62
+ // Handle AssignmentPattern (parameters with default values)
63
+ if (param.type === 'AssignmentPattern' && param.left) {
64
+ delete param.left.typeAnnotation;
65
+ }
62
66
  }
63
67
 
64
68
  if (metadata?.hoisted === true) {
@@ -55,7 +55,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
55
55
  continue; // Not a whole word match, keep searching
56
56
  }
57
57
  }
58
-
58
+
59
59
  sourceIndex = i + text.length;
60
60
  return i;
61
61
  }
@@ -87,7 +87,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
87
87
  continue; // Not a whole word match, keep searching
88
88
  }
89
89
  }
90
-
90
+
91
91
  generatedIndex = i + text.length;
92
92
  return i;
93
93
  }
@@ -98,7 +98,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
98
98
  // Collect text tokens from AST nodes
99
99
  /** @type {string[]} */
100
100
  const tokens = [];
101
-
101
+
102
102
  // We have to visit everything in generated order to maintain correct indices
103
103
  walk(ast, null, {
104
104
  _(node, { visit }) {
@@ -194,24 +194,24 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
194
194
  return;
195
195
  } else if (node.type === 'JSXElement') {
196
196
  // Manually visit in source order: opening element, children, closing element
197
-
197
+
198
198
  // 1. Visit opening element (name and attributes)
199
199
  if (node.openingElement) {
200
200
  visit(node.openingElement);
201
201
  }
202
-
202
+
203
203
  // 2. Visit children in order
204
204
  if (node.children) {
205
205
  for (const child of node.children) {
206
206
  visit(child);
207
207
  }
208
208
  }
209
-
209
+
210
210
  // 3. Push closing tag name (not visited by AST walker)
211
211
  if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
212
212
  tokens.push(node.closingElement.name.name);
213
213
  }
214
-
214
+
215
215
  return;
216
216
  } else if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
217
217
  // Visit in source order: id, params, body
@@ -991,7 +991,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
991
991
  for (const text of tokens) {
992
992
  const sourcePos = findInSource(text);
993
993
  const genPos = findInGenerated(text);
994
-
994
+
995
995
  if (sourcePos !== null && genPos !== null) {
996
996
  mappings.push({
997
997
  sourceOffsets: [sourcePos],
@@ -1009,4 +1009,4 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
1009
1009
  code: generated_code,
1010
1010
  mappings,
1011
1011
  };
1012
- }
1012
+ }
@@ -130,6 +130,13 @@ const visitors = {
130
130
  return context.next();
131
131
  },
132
132
 
133
+ NewExpression(node, context) {
134
+ if (!context.state.to_ts) {
135
+ delete node.typeArguments;
136
+ }
137
+ return context.next();
138
+ },
139
+
133
140
  PropertyDefinition(node, context) {
134
141
  if (!context.state.to_ts) {
135
142
  delete node.typeAnnotation;
@@ -143,6 +150,10 @@ const visitors = {
143
150
  delete node.typeParameters;
144
151
  for (const param of node.params) {
145
152
  delete param.typeAnnotation;
153
+ // Handle AssignmentPattern (parameters with default values)
154
+ if (param.type === 'AssignmentPattern' && param.left) {
155
+ delete param.left.typeAnnotation;
156
+ }
146
157
  }
147
158
  }
148
159
  return context.next();
@@ -154,6 +165,10 @@ const visitors = {
154
165
  delete node.typeParameters;
155
166
  for (const param of node.params) {
156
167
  delete param.typeAnnotation;
168
+ // Handle AssignmentPattern (parameters with default values)
169
+ if (param.type === 'AssignmentPattern' && param.left) {
170
+ delete param.left.typeAnnotation;
171
+ }
157
172
  }
158
173
  }
159
174
  return context.next();
@@ -165,6 +180,10 @@ const visitors = {
165
180
  delete node.typeParameters;
166
181
  for (const param of node.params) {
167
182
  delete param.typeAnnotation;
183
+ // Handle AssignmentPattern (parameters with default values)
184
+ if (param.type === 'AssignmentPattern' && param.left) {
185
+ delete param.left.typeAnnotation;
186
+ }
168
187
  }
169
188
  }
170
189
  return context.next();
@@ -1,14 +1,29 @@
1
+ import * as devalue from 'devalue';
1
2
 
2
3
  /**
3
- * @param {string} hash
4
- * @param {any[]} args
4
+ * @param {string} hash
5
+ * @param {any[]} args
5
6
  */
6
- export function rpc(hash, args) {
7
- return fetch('/_$_ripple_rpc_$_/' + hash, {
8
- method: 'POST',
9
- headers: {
10
- 'Content-Type': 'application/json'
11
- },
12
- body: JSON.stringify(args)
13
- }).then(res => res.json());
14
- }
7
+ export async function rpc(hash, args) {
8
+ const body = devalue.stringify(args);
9
+ let data;
10
+
11
+ try {
12
+ const response = await fetch('/_$_ripple_rpc_$_/' + hash, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ body,
18
+ });
19
+ data = await response.text();
20
+ } catch (err) {
21
+ throw new Error('An error occurred while trying to call the server function.');
22
+ }
23
+
24
+ if (data === '') {
25
+ throw new Error('The server function end-point did not return a response. Are you running a Ripple server?');
26
+ }
27
+
28
+ return devalue.parse(data).value;
29
+ }
@@ -17,12 +17,12 @@ export type Dependency = {
17
17
  n: null | Dependency;
18
18
  };
19
19
 
20
- export type Tracked = {
20
+ export type Tracked<V = any> = {
21
21
  a: { get?: Function, set?: Function };
22
22
  b: Block;
23
23
  c: number;
24
24
  f: number;
25
- v: any;
25
+ v: V;
26
26
  };
27
27
 
28
28
  export type Derived = {
@@ -2,7 +2,7 @@
2
2
  import { DERIVED, UNINITIALIZED } from '../client/constants.js';
3
3
  import { is_tracked_object } from '../client/utils.js';
4
4
  import { escape } from '../../../utils/escaping.js';
5
- import { is_boolean_attribute } from '../../../compiler/utils';
5
+ import { is_boolean_attribute } from '../../../compiler/utils.js';
6
6
  import { clsx } from 'clsx';
7
7
 
8
8
  export { escape };
@@ -0,0 +1,13 @@
1
+ import * as devalue from 'devalue';
2
+
3
+ /**
4
+ * @template {any[]} T
5
+ * @template V
6
+ * @param {(...args: T) => Promise<V>} fn
7
+ * @param {string} rpc_arguments_string
8
+ */
9
+ export async function executeServerFunction(fn, rpc_arguments_string) {
10
+ const rpc_arguments = devalue.parse(rpc_arguments_string);
11
+ const result = await fn.apply(null, rpc_arguments);
12
+ return devalue.stringify({ value: result });
13
+ }
@@ -1,2 +1,3 @@
1
1
  export { render } from '../runtime/internal/server/index.js';
2
2
  export { get_css_for_hashes } from '../runtime/internal/server/css-registry.js';
3
+ export { executeServerFunction } from '../runtime/internal/server/rpc.js';
@@ -23,3 +23,11 @@ exports[`compiler success tests > compiles tracked values in effect with update
23
23
  state.postDecrement = _$_.update(count, __block, -1);
24
24
  }));"
25
25
  `;
26
+
27
+ exports[`compiler success tests > removes type assertions from function parameters and leaves default values 1`] = `
28
+ "import * as _$_ from 'ripple/internal/client';
29
+
30
+ function getString(e = 'test') {
31
+ return e;
32
+ }"
33
+ `;
@@ -510,4 +510,32 @@ const errorMap = new ErrorMap();`;
510
510
 
511
511
  expect(result.js.code).toMatchSnapshot();
512
512
  });
513
+
514
+ it('removes type assertions from function parameters and leaves default values', () => {
515
+ const source = `
516
+ function getString(e: string = 'test') {
517
+ return e;
518
+ }`;
519
+
520
+ const result = compile(source, 'test.ripple', { mode: 'client' });
521
+
522
+ expect(result.js.code).toMatchSnapshot();
523
+ });
524
+
525
+ it('compiles without needing semicolons between statements and JSX', () => {
526
+ const source = `export component App() {
527
+ <div>const code4 = 4
528
+
529
+ const code3 = 3
530
+ <div>
531
+ <div>
532
+ const code = 1
533
+ </div>
534
+ const code2 = 2
535
+ </div>
536
+ </div>
537
+ }`;
538
+
539
+ const result = compile(source, 'test.ripple', { mode: 'client' });
540
+ });
513
541
  });
@@ -341,7 +341,7 @@ describe('composite components', () => {
341
341
  component App() {
342
342
  // Ambiguous generics vs JSX / less-than parsing scenarios
343
343
 
344
- // // 7. Generic following optional chaining
344
+ // 7. Generic following optional chaining
345
345
  const maybe = {
346
346
  factory<T>() {
347
347
  return {
@@ -353,11 +353,11 @@ describe('composite components', () => {
353
353
  };
354
354
  const g = maybe?.factory<number>()?.make<boolean>();
355
355
 
356
- // // 8. Comparison operator (ensure '<' here NOT misparsed as generics)
356
+ // 8. Comparison operator (ensure '<' here NOT misparsed as generics)
357
357
  let x = 10, y = 20;
358
358
  const h = x < y ? 'lt' : 'ge';
359
359
 
360
- // // 9. Chained comparisons with intervening generics
360
+ // 9. Chained comparisons with intervening generics
361
361
  class Box<T> {
362
362
  value: T;
363
363
  constructor(value?: T) {
@@ -375,11 +375,11 @@ describe('composite components', () => {
375
375
  <span>{'Test'}</span>
376
376
  </div>
377
377
 
378
- // // 11. Generic function call vs Element: Identifier followed by generic args
379
- // function identity<T>(value: T): T {
380
- // return value;
381
- // }
382
- // const j = identity<number>(42);
378
+ // 11. Generic function call vs Element: Identifier followed by generic args
379
+ function identity<T>(value: T): T {
380
+ return value;
381
+ }
382
+ const j = identity<number>(42);
383
383
 
384
384
  // 12. Member + generic call immediately followed by another call
385
385
  class Factory {
@@ -390,25 +390,25 @@ describe('composite components', () => {
390
390
  const factory = new Factory();
391
391
  const k = factory.create<number>()(123);
392
392
 
393
- // // 13. Multiple generic segments in chain
394
- // function foo<T>() {
395
- // return {
396
- // bar<U>() {
397
- // return {
398
- // baz<V>() {
399
- // return true;
400
- // }
401
- // };
402
- // }
403
- // };
404
- // }
405
- // const l = foo<number>().bar<string>().baz<boolean>();
393
+ // 13. Multiple generic segments in chain
394
+ function foo<T>() {
395
+ return {
396
+ bar<U>() {
397
+ return {
398
+ baz<V>() {
399
+ return true;
400
+ }
401
+ };
402
+ }
403
+ };
404
+ }
405
+ const l = foo<number>().bar<string>().baz<boolean>();
406
406
 
407
407
  // 14. Generic with constraint + default
408
408
  type Extractor<T extends { id: number } = { id: number }> = (v: T) => number;
409
409
  const m: Extractor = (v) => v.id;
410
410
 
411
- // // 15. Generic in angle after "new" + trailing call
411
+ // 15. Generic in angle after "new" + trailing call
412
412
  class Wrapper<T> {
413
413
  value: T;
414
414
  constructor() {
@@ -420,21 +420,21 @@ describe('composite components', () => {
420
420
  }
421
421
  const n = new Wrapper<number>().unwrap<string>();
422
422
 
423
- // // 16. Angle brackets inside type assertion vs generic call
424
- // function getUnknown(): unknown {
425
- // return { a: 1 };
426
- // }
427
- // getUnknown.factory = function<T>() {
428
- // return {
429
- // make<U>() {
430
- // return 2;
431
- // }
432
- // };
433
- // };
434
- // const raw = getUnknown();
435
- // const o = (raw as Map<string, number>).get('a');
436
-
437
- // // 17. Generic with comma + trailing less-than comparison on next token
423
+ // 16. Angle brackets inside type assertion vs generic call
424
+ function getUnknown(): unknown {
425
+ return new Map<string, number>([['a', 1]]);
426
+ }
427
+ getUnknown.factory = function<T>() {
428
+ return {
429
+ make<U>() {
430
+ return 2;
431
+ }
432
+ };
433
+ };
434
+ const raw = getUnknown();
435
+ const o = (raw as Map<string, number>).get('a');
436
+
437
+ // 17. Generic with comma + trailing less-than comparison on next token
438
438
  class Pair<T1, T2> {
439
439
  first: T1;
440
440
  second: T2;
@@ -446,7 +446,7 @@ describe('composite components', () => {
446
446
  const p = new Pair<number, string>();
447
447
  const q = 1 < 2 ? p : null;
448
448
 
449
- // // 18. Nested generics with line breaks resembling JSX indentation
449
+ // 18. Nested generics with line breaks resembling JSX indentation
450
450
  interface Node<T> {
451
451
  value: T;
452
452
  }
@@ -466,7 +466,7 @@ describe('composite components', () => {
466
466
  Edge<number>
467
467
  >();
468
468
 
469
- // // 19. Ternary containing generics in both branches
469
+ // 19. Ternary containing generics in both branches
470
470
  let flag = true;
471
471
  const s = flag ? new Box<number>() : new Box<string>();
472
472
 
@@ -477,55 +477,54 @@ describe('composite components', () => {
477
477
  const registry = new TrackedMap<string, number>();
478
478
  const u = registry.get<number>('id')?.toString();
479
479
 
480
- // // 22. Generic call used as callee for another call
481
- // function make<T>() {
482
- // return (value: T) => value;
483
- // }
484
- // const v = make<number>()(10);
485
-
486
- // // 23. Generic followed by tagged template (ensure not confused with JSX)
487
- // function tagFn<T>(strings: TemplateStringsArray, ...values: T[]) {
488
- // return values[0];
489
- // }
490
- // const tagResult = tagFn<number>`value`;
480
+ // 22. Generic call used as callee for another call
481
+ function make<T>() {
482
+ return (value: T) => value;
483
+ }
484
+ const v = make<number>()(10);
491
485
 
492
- // // 24. Sequence mixing: (a < b) + generic call in same statement
493
- // function compute<T>(x: T, y: T): T {
494
- // return y;
495
- // }
486
+ // 23. Generic followed by tagged template (ensure not confused with JSX)
487
+ function tagFn<T>(strings: TemplateStringsArray, ...values: T[]) {
488
+ return values[0];
489
+ }
490
+ const tagResult = tagFn<number>`value`;
496
491
 
497
- // const w = (x < y) && compute<number>(x, y);
492
+ // 24. Sequence mixing: (a < b) + generic call in same statement
493
+ function compute<T>(x: T, y: T): T {
494
+ return y;
495
+ }
498
496
 
497
+ const w = (x < y) && compute<number>(x, y);
499
498
 
500
499
 
501
500
  // Additional component focusing on edge crankers
502
501
 
503
- // // 28. Generic after parenthesized new expression
502
+ // 28. Generic after parenthesized new expression
504
503
  const aa = (new Box<number>()).open<string>();
505
504
 
506
- // // 29. Generic chain right after closing paren of IIFE
507
- // class Builder<Kind> {
508
- // finalize<Result>() {
509
- // return {
510
- // result: null as unknown as Result
511
- // };
512
- // }
513
- // }
514
- // const builder = new Builder<Number>();
515
- // const result = (function(){ return builder; })()<Number>().finalize<boolean>();
516
-
517
- // // 30. Angle bracket start of conditional expression line
518
- // function adjust<T>(value: T): T {
519
- // return value;
520
- // }
521
- // const val =
522
- // new Wrapper<number>()
523
- // .value < 100
524
- // ? adjust<number>(10)
525
- // : adjust<number>(20);
526
-
527
-
528
- // // 32. Generic with comments inside angle list
505
+ // 29. Generic chain right after closing paren of IIFE
506
+ class Builder<Kind> {
507
+ finalize<Result>() {
508
+ return {
509
+ result: null as unknown as Result
510
+ };
511
+ }
512
+ }
513
+ const builder = new Builder<Number>();
514
+ const result = ((function(){ return builder; })() as Builder<Number>).finalize<boolean>();
515
+
516
+ // 30. Angle bracket start of conditional expression line
517
+ function adjust<T>(value: T): T {
518
+ return value;
519
+ }
520
+ const val =
521
+ new Wrapper<number>()
522
+ .value < 100
523
+ ? adjust<number>(10)
524
+ : adjust<number>(20);
525
+
526
+
527
+ // 32. Generic with comments inside angle list
529
528
  class Mapper<Key, Value> {
530
529
  map: Map<Key, Value>;
531
530
  constructor() {
@@ -539,7 +538,7 @@ describe('composite components', () => {
539
538
  number
540
539
  >();
541
540
 
542
- // // 33. Map of generic instance as key
541
+ // 33. Map of generic instance as key
543
542
  const mm = new Map<TrackedArray<number>, TrackedArray<string>>();
544
543
  }
545
544
 
@@ -667,9 +666,9 @@ describe('composite components', () => {
667
666
  effect(() => {
668
667
  logs.push(@count);
669
668
  })
670
-
669
+
671
670
  <button onClick={() => @count = @count + 1}>{'+'}</button>
672
- }
671
+ }
673
672
 
674
673
  component App() {
675
674
  const count = track(0);
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, track, effect } from 'ripple';
3
+ import { value, checked } from 'ripple/bindings';
4
+
5
+ describe('use value()', () => {
6
+ let container;
7
+
8
+ function render(component) {
9
+ mount(component, {
10
+ target: container,
11
+ });
12
+ }
13
+
14
+ beforeEach(() => {
15
+ container = document.createElement('div');
16
+ document.body.appendChild(container);
17
+ });
18
+
19
+ afterEach(() => {
20
+ document.body.removeChild(container);
21
+ container = null;
22
+ });
23
+
24
+ it('should update value on input', () => {
25
+ component App() {
26
+ const text = track('');
27
+
28
+ effect(() => {
29
+ console.log('text changed', @text);
30
+ });
31
+
32
+ <input type="text" {ref value(text)} />
33
+ }
34
+ render(App);
35
+ flushSync();
36
+
37
+ const input = container.querySelector('input');
38
+ input.value = 'Hello';
39
+ input.dispatchEvent(new Event('input'));
40
+ flushSync();
41
+ expect(input.value).toBe('Hello');
42
+ });
43
+
44
+ it('should update checked on input', () => {
45
+ component App() {
46
+ const value = track(false);
47
+
48
+ effect(() => {
49
+ console.log('checked changed', @value);
50
+ });
51
+
52
+ <input type="checkbox" {ref checked(value)} />
53
+ }
54
+ render(App);
55
+ flushSync();
56
+
57
+ const input = container.querySelector('input');
58
+ input.checked = true;
59
+ input.dispatchEvent(new Event('input'));
60
+ flushSync();
61
+ expect(input.checked).toBe(true);
62
+ });
63
+ });
@@ -35,3 +35,11 @@ export async function App(__output) {
35
35
  });
36
36
  }"
37
37
  `;
38
+
39
+ exports[`compiler success tests > removes type assertions from function parameters and leaves default values 1`] = `
40
+ "import * as _$_ from 'ripple/internal/client';
41
+
42
+ function getString(e = 'test') {
43
+ return e;
44
+ }"
45
+ `;
@@ -36,4 +36,15 @@ export component App() {
36
36
  expect(result.js.code).toContain('await ChildComponent');
37
37
  expect(result.js.code).toMatchSnapshot();
38
38
  });
39
+
40
+ it('removes type assertions from function parameters and leaves default values', () => {
41
+ const source = `
42
+ function getString(e: string = 'test') {
43
+ return e;
44
+ }`;
45
+
46
+ const result = compile(source, 'test.ripple', { mode: 'client' });
47
+
48
+ expect(result.js.code).toMatchSnapshot();
49
+ });
39
50
  });
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from 'ripple/server';
3
+ import { TrackedArray, TrackedMap } from 'ripple';
4
+
5
+ describe('generics', () => {
6
+ it('handles advanced generic ambiguity and edge cases', () => {
7
+ component App() {
8
+ // Ambiguous generics vs JSX / less-than parsing scenarios
9
+
10
+ // 7. Generic following optional chaining
11
+ const maybe = {
12
+ factory<T>() {
13
+ return {
14
+ make<U>() {
15
+ return 1;
16
+ }
17
+ };
18
+ }
19
+ };
20
+ const g = maybe?.factory<number>()?.make<boolean>();
21
+
22
+ // 8. Comparison operator (ensure '<' here NOT misparsed as generics)
23
+ let x = 10, y = 20;
24
+ const h = x < y ? 'lt' : 'ge';
25
+
26
+ // 9. Chained comparisons with intervening generics
27
+ class Box<T> {
28
+ value: T;
29
+ constructor(value?: T) {
30
+ this.value = value;
31
+ }
32
+ open<U>() {
33
+ return new Box<U>();
34
+ }
35
+ }
36
+ const limit = 100;
37
+ const i = new Box<number>().value < limit ? 'ok' : 'no';
38
+
39
+ // 10. JSX / Element should still work
40
+ <div class="still-works">
41
+ <span>{'Test'}</span>
42
+ </div>
43
+
44
+ // 11. Generic function call vs Element: Identifier followed by generic args
45
+ function identity<T>(value: T): T {
46
+ return value;
47
+ }
48
+ const j = identity<number>(42);
49
+
50
+ // 12. Member + generic call immediately followed by another call
51
+ class Factory {
52
+ create<T>() {
53
+ return (value: T) => value;
54
+ }
55
+ }
56
+ const factory = new Factory();
57
+ const k = factory.create<number>()(123);
58
+
59
+ // 13. Multiple generic segments in chain
60
+ function foo<T>() {
61
+ return {
62
+ bar<U>() {
63
+ return {
64
+ baz<V>() {
65
+ return true;
66
+ }
67
+ };
68
+ }
69
+ };
70
+ }
71
+ const l = foo<number>().bar<string>().baz<boolean>();
72
+
73
+ // 14. Generic with constraint + default
74
+ type Extractor<T extends { id: number } = { id: number }> = (v: T) => number;
75
+ const m: Extractor = (v) => v.id;
76
+
77
+ // 15. Generic in angle after "new" + trailing call
78
+ class Wrapper<T> {
79
+ value: T;
80
+ constructor() {
81
+ this.value = null as unknown as T;
82
+ }
83
+ unwrap<U>() {
84
+ return null as unknown as U;
85
+ }
86
+ }
87
+ const n = new Wrapper<number>().unwrap<string>();
88
+
89
+ // 16. Angle brackets inside type assertion vs generic call
90
+ function getUnknown(): unknown {
91
+ return new Map<string, number>([['a', 1]]);
92
+ }
93
+ getUnknown.factory = function<T>() {
94
+ return {
95
+ make<U>() {
96
+ return 2;
97
+ }
98
+ };
99
+ };
100
+ const raw = getUnknown();
101
+ const o = (raw as Map<string, number>).get('a');
102
+
103
+ // 17. Generic with comma + trailing less-than comparison on next token
104
+ class Pair<T1, T2> {
105
+ first: T1;
106
+ second: T2;
107
+ constructor() {
108
+ this.first = null as unknown as T1;
109
+ this.second = null as unknown as T2;
110
+ }
111
+ }
112
+ const p = new Pair<number, string>();
113
+ const q = 1 < 2 ? p : null;
114
+
115
+ // 18. Nested generics with line breaks resembling JSX indentation
116
+ interface Node<T> {
117
+ value: T;
118
+ }
119
+ interface Edge<W> {
120
+ weight: W;
121
+ }
122
+ class Graph<N, E> {
123
+ nodes: N[];
124
+ edges: E[];
125
+ constructor() {
126
+ this.nodes = [];
127
+ this.edges = [];
128
+ }
129
+ }
130
+ const r = new Graph<
131
+ Node<string>,
132
+ Edge<number>
133
+ >();
134
+
135
+ // 19. Ternary containing generics in both branches
136
+ let flag = true;
137
+ const s = flag ? new Box<number>() : new Box<string>();
138
+
139
+ // 20. Generic inside template expression
140
+ const t = `length=${new TrackedArray<number>().length}`;
141
+
142
+ // 21. Optional chaining + generic + property access
143
+ const registry = new TrackedMap<string, number>();
144
+ const u = registry.get<number>('id')?.toString();
145
+
146
+ // 22. Generic call used as callee for another call
147
+ function make<T>() {
148
+ return (value: T) => value;
149
+ }
150
+ const v = make<number>()(10);
151
+
152
+ // 23. Generic followed by tagged template (ensure not confused with JSX)
153
+ function tagFn<T>(strings: TemplateStringsArray, ...values: T[]) {
154
+ return values[0];
155
+ }
156
+ const tagResult = tagFn<number>`value`;
157
+
158
+ // 24. Sequence mixing: (a < b) + generic call in same statement
159
+ function compute<T>(x: T, y: T): T {
160
+ return y;
161
+ }
162
+
163
+ const w = (x < y) && compute<number>(x, y);
164
+
165
+
166
+ // Additional component focusing on edge crankers
167
+
168
+ // 28. Generic after parenthesized new expression
169
+ const aa = (new Box<number>()).open<string>();
170
+
171
+ // 29. Generic chain right after closing paren of IIFE
172
+ class Builder<Kind> {
173
+ finalize<Result>() {
174
+ return {
175
+ result: null as unknown as Result
176
+ };
177
+ }
178
+ }
179
+ const builder = new Builder<Number>();
180
+ const result = ((function(){ return builder; })() as Builder<Number>).finalize<boolean>();
181
+
182
+ // 30. Angle bracket start of conditional expression line
183
+ function adjust<T>(value: T): T {
184
+ return value;
185
+ }
186
+ const val =
187
+ new Wrapper<number>()
188
+ .value < 100
189
+ ? adjust<number>(10)
190
+ : adjust<number>(20);
191
+
192
+
193
+ // 32. Generic with comments inside angle list
194
+ class Mapper<Key, Value> {
195
+ map: Map<Key, Value>;
196
+ constructor() {
197
+ this.map = new Map<Key, Value>();
198
+ }
199
+ }
200
+ const gg = new Mapper<
201
+ // key type
202
+ string,
203
+ /* value type */
204
+ number
205
+ >();
206
+
207
+ // 33. Map of generic instance as key
208
+ const mm = new Map<TrackedArray<number>, TrackedArray<string>>();
209
+ }
210
+
211
+ render(App);
212
+ });
213
+ });