ripple 0.2.216 → 0.3.1

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 (155) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +16 -7
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/identifier-utils.js +2 -0
  5. package/src/compiler/index.d.ts +2 -6
  6. package/src/compiler/phases/1-parse/index.js +171 -233
  7. package/src/compiler/phases/2-analyze/index.js +192 -16
  8. package/src/compiler/phases/2-analyze/prune.js +2 -2
  9. package/src/compiler/phases/3-transform/client/index.js +308 -91
  10. package/src/compiler/phases/3-transform/segments.js +43 -15
  11. package/src/compiler/phases/3-transform/server/index.js +71 -21
  12. package/src/compiler/scope.js +31 -12
  13. package/src/compiler/source-map-utils.js +4 -6
  14. package/src/compiler/types/acorn.d.ts +11 -0
  15. package/src/compiler/types/estree-jsx.d.ts +11 -0
  16. package/src/compiler/types/estree.d.ts +11 -0
  17. package/src/compiler/types/import.d.ts +32 -18
  18. package/src/compiler/types/index.d.ts +75 -23
  19. package/src/compiler/types/parse.d.ts +7 -10
  20. package/src/compiler/utils.js +48 -0
  21. package/src/runtime/array.js +53 -22
  22. package/src/runtime/date.js +15 -5
  23. package/src/runtime/index-client.js +41 -7
  24. package/src/runtime/index-server.js +7 -7
  25. package/src/runtime/internal/client/bindings.js +2 -2
  26. package/src/runtime/internal/client/blocks.js +40 -1
  27. package/src/runtime/internal/client/context.js +8 -0
  28. package/src/runtime/internal/client/for.js +3 -3
  29. package/src/runtime/internal/client/index.js +27 -5
  30. package/src/runtime/internal/client/render.js +20 -8
  31. package/src/runtime/internal/client/runtime.js +9 -7
  32. package/src/runtime/internal/client/try.js +15 -22
  33. package/src/runtime/internal/client/utils.js +1 -1
  34. package/src/runtime/internal/server/context.js +8 -0
  35. package/src/runtime/internal/server/index.js +99 -6
  36. package/src/runtime/map.js +7 -7
  37. package/src/runtime/media-query.js +10 -1
  38. package/src/runtime/object.js +6 -6
  39. package/src/runtime/proxy.js +6 -6
  40. package/src/runtime/set.js +11 -11
  41. package/src/runtime/url-search-params.js +13 -2
  42. package/src/runtime/url.js +15 -5
  43. package/src/utils/builders.js +13 -3
  44. package/tests/client/array/array.copy-within.test.ripple +11 -11
  45. package/tests/client/array/array.derived.test.ripple +42 -42
  46. package/tests/client/array/array.iteration.test.ripple +12 -12
  47. package/tests/client/array/array.mutations.test.ripple +25 -25
  48. package/tests/client/array/array.static.test.ripple +103 -106
  49. package/tests/client/array/array.to-methods.test.ripple +8 -8
  50. package/tests/client/async-suspend.test.ripple +94 -0
  51. package/tests/client/basic/basic.attributes.test.ripple +31 -31
  52. package/tests/client/basic/basic.collections.test.ripple +7 -7
  53. package/tests/client/basic/basic.components.test.ripple +48 -10
  54. package/tests/client/basic/basic.errors.test.ripple +46 -31
  55. package/tests/client/basic/basic.events.test.ripple +11 -11
  56. package/tests/client/basic/basic.get-set.test.ripple +18 -18
  57. package/tests/client/basic/basic.reactivity.test.ripple +47 -42
  58. package/tests/client/basic/basic.rendering.test.ripple +7 -7
  59. package/tests/client/basic/basic.utilities.test.ripple +4 -4
  60. package/tests/client/boundaries.test.ripple +7 -7
  61. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +2 -2
  62. package/tests/client/compiler/compiler.assignments.test.ripple +21 -21
  63. package/tests/client/compiler/compiler.basic.test.ripple +223 -82
  64. package/tests/client/compiler/compiler.tracked-access.test.ripple +8 -9
  65. package/tests/client/composite/composite.dynamic-components.test.ripple +8 -8
  66. package/tests/client/composite/composite.generics.test.ripple +4 -4
  67. package/tests/client/composite/composite.props.test.ripple +9 -9
  68. package/tests/client/composite/composite.reactivity.test.ripple +32 -26
  69. package/tests/client/composite/composite.render.test.ripple +13 -4
  70. package/tests/client/computed-properties.test.ripple +3 -3
  71. package/tests/client/context.test.ripple +3 -3
  72. package/tests/client/css/global-additional-cases.test.ripple +4 -4
  73. package/tests/client/css/style-identifier.test.ripple +49 -41
  74. package/tests/client/date.test.ripple +40 -40
  75. package/tests/client/dynamic-elements.test.ripple +165 -30
  76. package/tests/client/events.test.ripple +25 -25
  77. package/tests/client/for.test.ripple +76 -8
  78. package/tests/client/function-overload.test.ripple +0 -1
  79. package/tests/client/head.test.ripple +7 -7
  80. package/tests/client/html.test.ripple +2 -2
  81. package/tests/client/input-value.test.ripple +174 -176
  82. package/tests/client/map.test.ripple +21 -21
  83. package/tests/client/media-query.test.ripple +4 -4
  84. package/tests/client/object.test.ripple +12 -12
  85. package/tests/client/portal.test.ripple +4 -4
  86. package/tests/client/ref.test.ripple +5 -5
  87. package/tests/client/return.test.ripple +17 -17
  88. package/tests/client/set.test.ripple +16 -16
  89. package/tests/client/svg.test.ripple +6 -7
  90. package/tests/client/switch.test.ripple +10 -10
  91. package/tests/client/tracked-expression.test.ripple +1 -3
  92. package/tests/client/try.test.ripple +33 -4
  93. package/tests/client/url/url.derived.test.ripple +10 -9
  94. package/tests/client/url/url.parsing.test.ripple +10 -10
  95. package/tests/client/url/url.partial-removal.test.ripple +10 -10
  96. package/tests/client/url/url.reactivity.test.ripple +17 -17
  97. package/tests/client/url/url.serialization.test.ripple +4 -4
  98. package/tests/client/url-search-params/url-search-params.derived.test.ripple +11 -10
  99. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +5 -7
  100. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +13 -13
  101. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +19 -19
  102. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +17 -17
  103. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +5 -5
  104. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +5 -5
  105. package/tests/hydration/compiled/client/events.js +8 -11
  106. package/tests/hydration/compiled/client/for.js +20 -23
  107. package/tests/hydration/compiled/client/head.js +17 -19
  108. package/tests/hydration/compiled/client/hmr.js +1 -3
  109. package/tests/hydration/compiled/client/html.js +1 -15
  110. package/tests/hydration/compiled/client/if-children.js +7 -9
  111. package/tests/hydration/compiled/client/if.js +5 -7
  112. package/tests/hydration/compiled/client/mixed-control-flow.js +3 -5
  113. package/tests/hydration/compiled/client/portal.js +1 -1
  114. package/tests/hydration/compiled/client/reactivity.js +9 -11
  115. package/tests/hydration/compiled/client/return.js +11 -13
  116. package/tests/hydration/compiled/client/switch.js +4 -6
  117. package/tests/hydration/compiled/server/basic.js +0 -1
  118. package/tests/hydration/compiled/server/composite.js +0 -3
  119. package/tests/hydration/compiled/server/events.js +8 -12
  120. package/tests/hydration/compiled/server/for.js +20 -23
  121. package/tests/hydration/compiled/server/head.js +17 -19
  122. package/tests/hydration/compiled/server/hmr.js +1 -4
  123. package/tests/hydration/compiled/server/html.js +1 -35
  124. package/tests/hydration/compiled/server/if-children.js +7 -11
  125. package/tests/hydration/compiled/server/if.js +5 -7
  126. package/tests/hydration/compiled/server/mixed-control-flow.js +3 -5
  127. package/tests/hydration/compiled/server/portal.js +1 -9
  128. package/tests/hydration/compiled/server/reactivity.js +9 -11
  129. package/tests/hydration/compiled/server/return.js +11 -13
  130. package/tests/hydration/compiled/server/switch.js +4 -6
  131. package/tests/hydration/components/events.ripple +8 -9
  132. package/tests/hydration/components/for.ripple +20 -21
  133. package/tests/hydration/components/head.ripple +6 -8
  134. package/tests/hydration/components/hmr.ripple +1 -2
  135. package/tests/hydration/components/html.ripple +1 -3
  136. package/tests/hydration/components/if-children.ripple +7 -8
  137. package/tests/hydration/components/if.ripple +5 -6
  138. package/tests/hydration/components/mixed-control-flow.ripple +4 -6
  139. package/tests/hydration/components/portal.ripple +1 -1
  140. package/tests/hydration/components/reactivity.ripple +9 -10
  141. package/tests/hydration/components/return.ripple +11 -12
  142. package/tests/hydration/components/switch.ripple +6 -8
  143. package/tests/server/await.test.ripple +2 -2
  144. package/tests/server/basic.attributes.test.ripple +19 -21
  145. package/tests/server/basic.components.test.ripple +13 -7
  146. package/tests/server/basic.test.ripple +20 -21
  147. package/tests/server/compiler.test.ripple +5 -5
  148. package/tests/server/composite.props.test.ripple +6 -7
  149. package/tests/server/composite.test.ripple +4 -4
  150. package/tests/server/context.test.ripple +1 -3
  151. package/tests/server/dynamic-elements.test.ripple +24 -24
  152. package/tests/server/head.test.ripple +5 -7
  153. package/tests/server/style-identifier.test.ripple +16 -17
  154. package/types/index.d.ts +266 -62
  155. package/types/server.d.ts +6 -6
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { track, flushSync } from 'ripple';
2
+ import { flushSync } from 'ripple';
3
3
 
4
4
  describe('basic client > rendering & text', () => {
5
5
  it('renders static text', () => {
@@ -27,7 +27,7 @@ describe('basic client > rendering & text', () => {
27
27
 
28
28
  it('renders dynamic text', () => {
29
29
  component Basic() {
30
- let text = track('Hello World');
30
+ let text = #ripple.track('Hello World');
31
31
 
32
32
  <button
33
33
  onClick={() => {
@@ -104,8 +104,8 @@ describe('basic client > rendering & text', () => {
104
104
 
105
105
  it('renders with mixed static and dynamic content', () => {
106
106
  component Basic() {
107
- let name = track('World');
108
- let count = track(0);
107
+ let name = #ripple.track('World');
108
+ let count = #ripple.track(0);
109
109
  const staticMessage = 'Welcome to Ripple!';
110
110
 
111
111
  <div class="mixed-content">
@@ -151,7 +151,7 @@ describe('basic client > rendering & text', () => {
151
151
 
152
152
  it('basic operations', () => {
153
153
  component App() {
154
- let count = track(0);
154
+ let count = #ripple.track(0);
155
155
  <div>{@count++}</div>
156
156
  <div>{++@count}</div>
157
157
  <div>{5}</div>
@@ -164,8 +164,8 @@ describe('basic client > rendering & text', () => {
164
164
 
165
165
  it('renders with conditional rendering using if statements', () => {
166
166
  component Basic() {
167
- let showContent = track(false);
168
- let userRole = track('guest');
167
+ let showContent = #ripple.track(false);
168
+ let userRole = #ripple.track('guest');
169
169
 
170
170
  <button
171
171
  onClick={() => {
@@ -1,4 +1,4 @@
1
- import { track, effect, untrack, tick } from 'ripple';
1
+ import { tick } from 'ripple';
2
2
 
3
3
  describe('basic client > utilities', () => {
4
4
  it('tick function', async () => {
@@ -6,9 +6,9 @@ describe('basic client > utilities', () => {
6
6
  const promise = new Promise<void>((res) => (resolve = res));
7
7
 
8
8
  component Basic() {
9
- let value = track(0);
10
- effect(() => {
11
- untrack(() => {
9
+ let value = #ripple.track(0);
10
+ #ripple.effect(() => {
11
+ #ripple.untrack(() => {
12
12
  @value++;
13
13
  tick().then(() => resolve());
14
14
  });
@@ -1,13 +1,13 @@
1
- import { flushSync, effect, track, type Tracked } from 'ripple';
1
+ import { flushSync, type Tracked } from 'ripple';
2
2
 
3
3
  describe('passing reactivity between boundaries tests', () => {
4
4
  it('can pass reactivity between functions with simple arrays and destructuring', () => {
5
5
  let log: string[] = [];
6
6
 
7
7
  function createDouble([count]: [Tracked<number>]) {
8
- const double = track(() => @count * 2);
8
+ const double = #ripple.track(() => @count * 2);
9
9
 
10
- effect(() => {
10
+ #ripple.effect(() => {
11
11
  log.push('Count:' + @count);
12
12
  });
13
13
 
@@ -15,7 +15,7 @@ describe('passing reactivity between boundaries tests', () => {
15
15
  }
16
16
 
17
17
  component App() {
18
- let count = track(0);
18
+ let count = #ripple.track(0);
19
19
 
20
20
  const [double] = createDouble([count]);
21
21
 
@@ -54,9 +54,9 @@ describe('passing reactivity between boundaries tests', () => {
54
54
  let log: string[] = [];
55
55
 
56
56
  function createDouble({ count }: { count: Tracked<number> }) {
57
- const double = track(() => @count * 2);
57
+ const double = #ripple.track(() => @count * 2);
58
58
 
59
- effect(() => {
59
+ #ripple.effect(() => {
60
60
  log.push('Count:' + @count);
61
61
  });
62
62
 
@@ -64,7 +64,7 @@ describe('passing reactivity between boundaries tests', () => {
64
64
  }
65
65
 
66
66
  component App() {
67
- let count = track(0);
67
+ let count = #ripple.track(0);
68
68
 
69
69
  const { double } = createDouble({ count });
70
70
 
@@ -3,10 +3,10 @@
3
3
  exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(count);"`;
4
4
 
5
5
  exports[`compiler > assignments > compiles tracked values in effect with update expressions 1`] = `
6
- "_$_.with_scope(__block, () => untrack(() => {
6
+ "_$_.untrack(() => {
7
7
  state.preIncrement = _$_.update_pre(count);
8
8
  state.postIncrement = _$_.update(count);
9
9
  state.preDecrement = _$_.update_pre(count, -1);
10
10
  state.postDecrement = _$_.update(count, -1);
11
- }));"
11
+ });"
12
12
  `;
@@ -1,15 +1,17 @@
1
- import { track, TrackedArray } from 'ripple';
1
+ import { RippleArray } from 'ripple';
2
2
  import { compile } from 'ripple/compiler';
3
3
 
4
+ const EFFECT_BODY_REGEX = /_\$\_\.effect\(\(\) => \{([\s\S]*?)\n\t\}\);/;
5
+
4
6
  describe('compiler > assignments', () => {
5
7
  it('properly handles JS assignments, reads and updates to array indices', () => {
6
8
  const logs: (number | undefined)[] = [];
7
9
 
8
10
  component App() {
9
11
  let items: number[] = [];
10
- let tracked_items = track<number[]>([]);
12
+ let tracked_items = #ripple.track<number[]>([]);
11
13
  let items2 = new Array();
12
- let items3 = new TrackedArray<number>();
14
+ let items3: RippleArray<number> = #ripple[];
13
15
  let i = 0;
14
16
 
15
17
  logs.push(items[0]);
@@ -107,34 +109,32 @@ describe('compiler > assignments', () => {
107
109
 
108
110
  it('compiles tracked values in effect with assignment expression', () => {
109
111
  const source = `component App() {
110
- let count = track(0);
112
+ let count = #ripple.track(0);
111
113
 
112
- effect(() => {
113
- state.count = @count;
114
- })
114
+ #ripple.effect(() => {
115
+ state.count = @count;
116
+ });
115
117
  }`;
116
118
  const result = compile(source, 'test.ripple');
117
- // Extract just the effect callback body
118
- const effectMatch = result.js.code.match(/effect\(\(\) => \{([^}]+)\}\)/s);
119
- expect(effectMatch?.[1].trim()).toMatchSnapshot();
119
+ const effect_match = result.js.code.match(EFFECT_BODY_REGEX);
120
+ expect(effect_match?.[1].trim()).toMatchSnapshot();
120
121
  });
121
122
 
122
123
  it('compiles tracked values in effect with update expressions', () => {
123
124
  const source = `component App() {
124
- let count = track(5);
125
+ let count = #ripple.track(5);
125
126
 
126
- effect(() => {
127
- untrack(() => {
128
- state.preIncrement = ++@count;
129
- state.postIncrement = @count++;
130
- state.preDecrement = --@count;
131
- state.postDecrement = @count--;
127
+ #ripple.effect(() => {
128
+ #ripple.untrack(() => {
129
+ state.preIncrement = ++@count;
130
+ state.postIncrement = @count++;
131
+ state.preDecrement = --@count;
132
+ state.postDecrement = @count--;
133
+ });
132
134
  });
133
- })
134
135
  }`;
135
136
  const result = compile(source, 'test.ripple');
136
- // Extract just the effect callback body
137
- const effectMatch = result.js.code.match(/effect\(\(\) => \{([\s\S]+?)\n\t\}\)\)/);
138
- expect(effectMatch?.[1].trim()).toMatchSnapshot();
137
+ const effect_match = result.js.code.match(EFFECT_BODY_REGEX);
138
+ expect(effect_match?.[1].trim()).toMatchSnapshot();
139
139
  });
140
140
  });
@@ -1,5 +1,8 @@
1
1
  import { parse, compile, compile_to_volar_mappings } from 'ripple/compiler';
2
- import { obfuscate_identifier } from 'ripple/compiler/internal/identifier/utils';
2
+ import {
3
+ obfuscate_identifier,
4
+ RIPPLE_NAMESPACE_IDENTIFIER,
5
+ } from 'ripple/compiler/internal/identifier/utils';
3
6
  import type * as AST from 'estree';
4
7
 
5
8
  function count_occurrences(string: string, subString: string): number {
@@ -19,25 +22,25 @@ describe('compiler > basics', () => {
19
22
  const source = `export component App() {
20
23
  <div id="myid" class="myclass">{"Hello World"}</div>
21
24
 
22
- <style>#style</style>
25
+ <style>#ripple.style</style>
23
26
  }`;
24
27
  const style1 = '.myid {color: green }';
25
28
  const style2 = '#myid {color: green }';
26
29
  const style3 = 'div {color: green }';
27
30
 
28
- let input = source.replace('#style', style1);
31
+ let input = source.replace('#ripple.style', style1);
29
32
  let ast = parse(input);
30
33
  expect(
31
34
  ((ast.body[0] as AST.ExportNamedDeclaration).declaration as unknown as AST.Component)?.css.source,
32
35
  ).toEqual(style1);
33
36
 
34
- input = source.replace('#style', style2);
37
+ input = source.replace('#ripple.style', style2);
35
38
  ast = parse(input);
36
39
  expect(
37
40
  ((ast.body[0] as AST.ExportNamedDeclaration).declaration as unknown as AST.Component)?.css.source,
38
41
  ).toEqual(style2);
39
42
 
40
- input = source.replace('#style', style3);
43
+ input = source.replace('#ripple.style', style3);
41
44
  ast = parse(input);
42
45
  expect(
43
46
  ((ast.body[0] as AST.ExportNamedDeclaration).declaration as unknown as AST.Component)?.css.source,
@@ -197,8 +200,8 @@ describe('compiler > basics', () => {
197
200
  class Box<T> {
198
201
  value: T;
199
202
 
200
- method<T>(): T {
201
- return this.value;
203
+ method<U extends T>(): U {
204
+ return this.value as U;
202
205
  }
203
206
 
204
207
  constructor(value: T) {
@@ -247,91 +250,152 @@ describe('compiler > basics', () => {
247
250
  expect(js.code).not.toMatch(/_\$_\.template\(`<!><!>`,\s*1,\s*3\)/);
248
251
  });
249
252
 
250
- it(
251
- 'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
252
- () => {
253
- const source = `
254
- import {
255
- TrackedArray,
256
- TrackedObject,
257
- TrackedSet,
258
- TrackedMap,
259
- createRefKey,
260
- } from 'ripple';
253
+ // it(
254
+ // 'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
255
+ // () => {
256
+ // const source = `
257
+ // import { RippleArray, RippleObject, RippleSet, RippleMap, createRefKey } from 'ripple';
258
+ // component App() {
259
+ // const items = #ripple[1, 2, 3];
260
+ // const obj = #ripple{ a: 1, b: 2, c: 3 };
261
+ // const set = #ripple.set([1, 2, 3]);
262
+ // const map = #ripple.map([['a', 1], ['b', 2], ['c', 3]]);
263
+
264
+ // <div {ref () => {}} />
265
+ // }
266
+ // `;
267
+ // const result = compile_to_volar_mappings(source, 'test.ripple').code;
268
+
269
+ // expect(count_occurrences(result, 'RippleArray')).toBe(1);
270
+ // expect(count_occurrences(result, 'RippleObject')).toBe(1);
271
+ // expect(count_occurrences(result, 'RippleSet')).toBe(1);
272
+ // expect(count_occurrences(result, 'RippleMap')).toBe(1);
273
+ // expect(count_occurrences(result, 'createRefKey')).toBe(1);
274
+ // },
275
+ // );
276
+
277
+ // it(
278
+ // 'adds obfuscated imports and keeps renamed Tracked imports intact when encountering shorthand syntax',
279
+ // () => {
280
+ // const source = `
281
+ // import { RippleArray as TA, RippleObject as TO, RippleSet as TS, RippleMap as TM, createRefKey as crk } from 'ripple';
282
+ // component App() {
283
+ // const items = #ripple[1, 2, 3];
284
+ // const obj = #ripple{ a: 1, b: 2, c: 3 };
285
+ // const set = #ripple.set([1, 2, 3]);
286
+ // const map = #ripple.map([['a', 1], ['b', 2], ['c', 3]]);
287
+
288
+ // <div {ref () => {}} />
289
+ // }
290
+ // `;
291
+ // const result = compile_to_volar_mappings(source, 'test.ripple').code;
292
+
293
+ // expect(count_occurrences(result, obfuscate_identifier('RippleArray'))).toBe(2);
294
+ // expect(count_occurrences(result, 'TA')).toBe(1);
295
+ // expect(count_occurrences(result, obfuscate_identifier('RippleObject'))).toBe(2);
296
+ // expect(count_occurrences(result, 'TO')).toBe(1);
297
+ // expect(count_occurrences(result, obfuscate_identifier('RippleSet'))).toBe(2);
298
+ // expect(count_occurrences(result, 'TS')).toBe(1);
299
+ // expect(count_occurrences(result, obfuscate_identifier('RippleMap'))).toBe(2);
300
+ // expect(count_occurrences(result, 'TM')).toBe(1);
301
+ // expect(count_occurrences(result, obfuscate_identifier('createRefKey'))).toBe(2);
302
+ // expect(count_occurrences(result, 'crk')).toBe(1);
303
+ // },
304
+ // );
305
+
306
+ // it('adds hidden obfuscated imports for shorthand syntax', () => {
307
+ // const source = `
308
+ // component App() {
309
+ // const items = #ripple[1, 2, 3];
310
+ // const obj = #ripple{ a: 1, b: 2, c: 3 };
311
+ // const set = #ripple.set([1, 2, 3]);
312
+ // const map = #ripple.map([['a', 1], ['b', 2], ['c', 3]]);
313
+
314
+ // <div {ref () => {}} />
315
+ // }
316
+ // `;
317
+ // const result = compile_to_volar_mappings(source, 'test.ripple').code;
318
+
319
+ // expect(count_occurrences(result, obfuscate_identifier('RippleArray'))).toBe(2);
320
+ // expect(count_occurrences(result, obfuscate_identifier('RippleObject'))).toBe(2);
321
+ // expect(count_occurrences(result, obfuscate_identifier('RippleSet'))).toBe(2);
322
+ // expect(count_occurrences(result, obfuscate_identifier('RippleMap'))).toBe(2);
323
+ // expect(count_occurrences(result, obfuscate_identifier('createRefKey'))).toBe(2);
324
+ // });
325
+
326
+ it('prints longhand tracked property values in to_ts output while preserving [\'#v\']', () => {
327
+ const source = `
328
+ import { createRefKey } from 'ripple';
261
329
 
262
330
  component App() {
263
- const items = #[1, 2, 3];
264
- const obj = #{ a: 1, b: 2, c: 3 };
265
- const set = new #Set([1, 2, 3]);
266
- const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
267
-
268
- <div {ref () => {}} />
331
+ let value = #ripple.track('test');
332
+ function inputRef(node) {}
333
+
334
+ const props = {
335
+ id: 'example',
336
+ @value,
337
+ [createRefKey()]: inputRef,
338
+ };
269
339
  }
270
340
  `;
271
- const result = compile_to_volar_mappings(source, 'test.ripple').code;
272
-
273
- expect(count_occurrences(result, 'TrackedArray')).toBe(1);
274
- expect(count_occurrences(result, 'TrackedObject')).toBe(1);
275
- expect(count_occurrences(result, 'TrackedSet')).toBe(1);
276
- expect(count_occurrences(result, 'TrackedMap')).toBe(1);
277
- expect(count_occurrences(result, 'createRefKey')).toBe(1);
278
- },
279
- );
280
-
281
- it(
282
- 'adds obfuscated imports and keeps renamed Tracked imports intact when encountering shorthand syntax',
283
- () => {
284
- const source = `
285
- import {
286
- TrackedArray as TA,
287
- TrackedObject as TO,
288
- TrackedSet as TS,
289
- TrackedMap as TM,
290
- createRefKey as crk,
291
- } from 'ripple';
292
341
 
293
- component App() {
294
- const items = #[1, 2, 3];
295
- const obj = #{ a: 1, b: 2, c: 3 };
296
- const set = new #Set([1, 2, 3]);
297
- const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
342
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
343
+
344
+ expect(result).toMatch(/value:\s*value\??\.\['#v'\]/);
345
+ });
298
346
 
299
- <div {ref () => {}} />
347
+ it('rewrites standalone #ripple to the hidden namespace identifier in to_ts output', () => {
348
+ const source = `
349
+ export component App() {
350
+ #ripple
300
351
  }
301
352
  `;
302
- const result = compile_to_volar_mappings(source, 'test.ripple').code;
303
-
304
- expect(count_occurrences(result, obfuscate_identifier('TrackedArray'))).toBe(2);
305
- expect(count_occurrences(result, 'TA')).toBe(1);
306
- expect(count_occurrences(result, obfuscate_identifier('TrackedObject'))).toBe(2);
307
- expect(count_occurrences(result, 'TO')).toBe(1);
308
- expect(count_occurrences(result, obfuscate_identifier('TrackedSet'))).toBe(2);
309
- expect(count_occurrences(result, 'TS')).toBe(1);
310
- expect(count_occurrences(result, obfuscate_identifier('TrackedMap'))).toBe(2);
311
- expect(count_occurrences(result, 'TM')).toBe(1);
312
- expect(count_occurrences(result, obfuscate_identifier('createRefKey'))).toBe(2);
313
- expect(count_occurrences(result, 'crk')).toBe(1);
314
- },
315
- );
316
-
317
- it('adds hidden obfuscated imports for shorthand syntax', () => {
353
+
354
+ const result = compile_to_volar_mappings(source, 'test.ripple', { loose: true }).code;
355
+
356
+ expect(result).toContain(RIPPLE_NAMESPACE_IDENTIFIER);
357
+ expect(result).toContain(`${RIPPLE_NAMESPACE_IDENTIFIER};`);
358
+ expect(result).not.toContain('#ripple;');
359
+ });
360
+
361
+ it('preserves generic type args in interface extends for Volar mappings', () => {
318
362
  const source = `
319
- component App() {
320
- const items = #[1, 2, 3];
321
- const obj = #{ a: 1, b: 2, c: 3 };
322
- const set = new #Set([1, 2, 3]);
323
- const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
363
+ interface PolymorphicProps<T extends keyof HTMLElementTagNameMap> {
364
+ as?: T;
365
+ }
324
366
 
325
- <div {ref () => {}} />
367
+ interface Props extends PolymorphicProps<'div'> {
368
+ id: string;
369
+ }
370
+
371
+ export component App(props: Props) {
372
+ <div id={props.id} />
326
373
  }
327
374
  `;
375
+
328
376
  const result = compile_to_volar_mappings(source, 'test.ripple').code;
329
377
 
330
- expect(count_occurrences(result, obfuscate_identifier('TrackedArray'))).toBe(2);
331
- expect(count_occurrences(result, obfuscate_identifier('TrackedObject'))).toBe(2);
332
- expect(count_occurrences(result, obfuscate_identifier('TrackedSet'))).toBe(2);
333
- expect(count_occurrences(result, obfuscate_identifier('TrackedMap'))).toBe(2);
334
- expect(count_occurrences(result, obfuscate_identifier('createRefKey'))).toBe(2);
378
+ expect(result).toContain('extends PolymorphicProps<\'div\'>');
379
+ });
380
+
381
+ it('handles if-else expression statements in Volar mappings', () => {
382
+ const source = `
383
+ export component App() {
384
+ let level = #ripple.track(1);
385
+
386
+ <button
387
+ onClick={() => {
388
+ if (@level === 1) @level = 2;
389
+ else if (@level === 2) @level = 3;
390
+ else @level = 1;
391
+ }}
392
+ >
393
+ {'Toggle'}
394
+ </button>
395
+ }
396
+ `;
397
+
398
+ expect(() => compile_to_volar_mappings(source, 'test.ripple')).not.toThrow();
335
399
  });
336
400
 
337
401
  it('should not error on having js below markup in the same scope', () => {
@@ -357,6 +421,23 @@ export component App() {
357
421
  expect(() => compile(code, 'test.ripple')).not.toThrow();
358
422
  });
359
423
 
424
+ it('converts nested component children into props in Volar mappings', () => {
425
+ const source = `
426
+ export component App() {
427
+ <ark.div class="host-class" data-value="42">
428
+ component asChild({ children, href, ...rest }: { href: string; [key: string]: any }) {
429
+ <a id="aschild-anchor" {href} {...rest} data-extra="yes">{'Link'}</a>
430
+ }
431
+ </ark.div>
432
+ }
433
+ `;
434
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
435
+
436
+ expect(result).toContain('<ark.div class="host-class" data-value="42" asChild={');
437
+ expect(result).not.toContain('children={() =>');
438
+ expect(result).not.toContain('function asChild');
439
+ });
440
+
360
441
  it('should not error on `this` MemberExpression with a UpdateExpression', () => {
361
442
  const code = `
362
443
  class Test {
@@ -374,14 +455,13 @@ export component App() {
374
455
  expect(() => compile(code, 'test.ripple')).not.toThrow();
375
456
  });
376
457
 
377
- it('should inject __block for track() calls inside class constructors', () => {
458
+ it('should inject __block for #ripple.track() calls inside class constructors', () => {
378
459
  const source = `
379
- import { track } from 'ripple';
380
460
 
381
461
  class Store {
382
462
  constructor() {
383
- this.count = track(0);
384
- this.items = #[1, 2, 3];
463
+ this.count = #ripple.track(0);
464
+ this.items = #ripple[1, 2, 3];
385
465
  }
386
466
  }
387
467
 
@@ -397,4 +477,65 @@ export component App() {
397
477
  expect(code).toContain('__block');
398
478
  expect(code).toContain('_$_.scope()');
399
479
  });
480
+
481
+ it('parses #ripple.effect and #ripple.untrack calls', () => {
482
+ const source = `
483
+ component App() {
484
+ let count = #ripple.track(0);
485
+
486
+ #ripple.effect(() => {
487
+ const snapshot = #ripple.untrack(() => @count);
488
+ console.log(snapshot);
489
+ });
490
+ }
491
+ `;
492
+
493
+ const ast = parse(source);
494
+ const ast_json = JSON.stringify(ast);
495
+
496
+ expect(ast_json).toContain('"source_name":"#ripple.effect"');
497
+ expect(ast_json).toContain('"source_name":"#ripple.untrack"');
498
+ });
499
+
500
+ it('collects duplicate declaration parser errors in loose mode', () => {
501
+ const source = `
502
+ export component App() {
503
+ let test = #ripple.track(false);
504
+ let test = 'hey';
505
+ }
506
+ `;
507
+
508
+ expect(() => compile(source, 'test.ripple')).toThrow(
509
+ 'Identifier \'test\' has already been declared',
510
+ );
511
+
512
+ const result = compile_to_volar_mappings(source, 'test.ripple', { loose: true });
513
+
514
+ expect(
515
+ result.errors.some(
516
+ (item) => item.message.includes('Identifier \'test\' has already been declared'),
517
+ ),
518
+ ).toBe(true);
519
+ });
520
+
521
+ it('parses bare #ripple in loose mode for autocomplete recovery', () => {
522
+ const source = `
523
+ export component App() {
524
+ #ripple
525
+ }
526
+ `;
527
+
528
+ expect(() => compile_to_volar_mappings(source, 'test.ripple', { loose: true })).not.toThrow();
529
+
530
+ const result = compile_to_volar_mappings(source, 'test.ripple', { loose: true });
531
+ expect(result.errors).toEqual([]);
532
+
533
+ const ripple_offset = source.indexOf('#ripple');
534
+ const mapping = result.mappings.find(
535
+ (mapping) => mapping.sourceOffsets[0] === ripple_offset &&
536
+ mapping.lengths[0] === '#ripple'.length,
537
+ );
538
+
539
+ expect(mapping?.data.customData.suppressedDiagnostics).toContain(18016);
540
+ });
400
541
  });
@@ -1,11 +1,10 @@
1
1
  import { compile } from 'ripple/compiler';
2
- import { track } from 'ripple';
3
2
 
4
3
  describe('Compiler: Tracked Object Direct Access Checks', () => {
5
4
  it('should error on direct access to __v of a tracked object', () => {
6
5
  const code = `
7
6
  export default component App() {
8
- let count = track(0);
7
+ let count = #ripple.track(0);
9
8
  console.log(count.__v);
10
9
  }
11
10
  `;
@@ -17,7 +16,7 @@ describe('Compiler: Tracked Object Direct Access Checks', () => {
17
16
  it('should error on direct access to "a" (get/set config) of a tracked object', () => {
18
17
  const code = `
19
18
  export default component App() {
20
- let myTracked = track(0);
19
+ let myTracked = #ripple.track(0);
21
20
  console.log(myTracked.a);
22
21
  }
23
22
  `;
@@ -29,7 +28,7 @@ describe('Compiler: Tracked Object Direct Access Checks', () => {
29
28
  it('should error on direct access to "b" (block) of a tracked object', () => {
30
29
  const code = `
31
30
  export default component App() {
32
- let myTracked = track(0);
31
+ let myTracked = #ripple.track(0);
33
32
  console.log(myTracked.b);
34
33
  }
35
34
  `;
@@ -41,7 +40,7 @@ describe('Compiler: Tracked Object Direct Access Checks', () => {
41
40
  it('should error on direct access to "c" (clock) of a tracked object', () => {
42
41
  const code = `
43
42
  export default component App() {
44
- let myTracked = track(0);
43
+ let myTracked = #ripple.track(0);
45
44
  console.log(myTracked.c);
46
45
  }
47
46
  `;
@@ -53,7 +52,7 @@ describe('Compiler: Tracked Object Direct Access Checks', () => {
53
52
  it('should error on direct access to "f" (flags) of a tracked object', () => {
54
53
  const code = `
55
54
  export default component App() {
56
- let myTracked = track(0);
55
+ let myTracked = #ripple.track(0);
57
56
  console.log(myTracked.f);
58
57
  }
59
58
  `;
@@ -65,7 +64,7 @@ describe('Compiler: Tracked Object Direct Access Checks', () => {
65
64
  it('should compile successfully with correct @ syntax access', () => {
66
65
  const code = `
67
66
  export default component App() {
68
- let count = track(0);
67
+ let count = #ripple.track(0);
69
68
  console.log(@count);
70
69
  }
71
70
  `;
@@ -74,9 +73,9 @@ describe('Compiler: Tracked Object Direct Access Checks', () => {
74
73
 
75
74
  it('should compile successfully with correct get() function access', () => {
76
75
  const code = `
77
- import { get, track } from 'ripple';
76
+ import { get } from 'ripple';
78
77
  export default component App() {
79
- let count = track(0);
78
+ let count = #ripple.track(0);
80
79
  console.log(get(count));
81
80
  }
82
81
  `;