ripple 0.2.211 → 0.2.213

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 (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +3 -0
  3. package/package.json +5 -2
  4. package/src/compiler/phases/1-parse/index.js +50 -2
  5. package/src/compiler/phases/2-analyze/index.js +13 -29
  6. package/src/compiler/phases/3-transform/client/index.js +145 -5
  7. package/src/compiler/types/index.d.ts +4 -7
  8. package/src/compiler/types/rpc.d.ts +5 -0
  9. package/src/runtime/internal/client/hydration.js +4 -0
  10. package/src/runtime/internal/client/rpc.js +31 -3
  11. package/src/runtime/internal/client/template.js +4 -2
  12. package/tests/client/compiler/compiler.basic.test.ripple +24 -0
  13. package/tests/client/compiler/compiler.try-in-function.test.ripple +159 -0
  14. package/tests/hydration/basic.test.js +23 -0
  15. package/tests/hydration/build-components.js +2 -2
  16. package/tests/hydration/compiled/client/basic.js +67 -1
  17. package/tests/hydration/compiled/client/composite.js +1 -0
  18. package/tests/hydration/compiled/client/events.js +1 -0
  19. package/tests/hydration/compiled/client/for.js +1 -0
  20. package/tests/hydration/compiled/client/head.js +1 -0
  21. package/tests/hydration/compiled/client/html.js +1 -0
  22. package/tests/hydration/compiled/client/if-children.js +407 -0
  23. package/tests/hydration/compiled/client/if.js +1 -0
  24. package/tests/hydration/compiled/client/portal.js +4 -0
  25. package/tests/hydration/compiled/client/reactivity.js +1 -0
  26. package/tests/hydration/compiled/client/return.js +1 -0
  27. package/tests/hydration/compiled/client/switch.js +1 -0
  28. package/tests/hydration/compiled/server/basic.js +108 -1
  29. package/tests/hydration/compiled/server/composite.js +1 -0
  30. package/tests/hydration/compiled/server/events.js +1 -0
  31. package/tests/hydration/compiled/server/for.js +1 -0
  32. package/tests/hydration/compiled/server/head.js +1 -0
  33. package/tests/hydration/compiled/server/html.js +1 -0
  34. package/tests/hydration/compiled/server/if-children.js +686 -0
  35. package/tests/hydration/compiled/server/if.js +1 -0
  36. package/tests/hydration/compiled/server/portal.js +1 -0
  37. package/tests/hydration/compiled/server/reactivity.js +1 -0
  38. package/tests/hydration/compiled/server/return.js +1 -0
  39. package/tests/hydration/compiled/server/switch.js +1 -0
  40. package/tests/hydration/components/basic.ripple +32 -1
  41. package/tests/hydration/components/composite.ripple +3 -1
  42. package/tests/hydration/components/if-children.ripple +196 -0
  43. package/tests/hydration/if-children.test.js +272 -0
  44. package/tests/hydration/tsconfig.json +12 -0
  45. package/tests/hydration.d.ts +14 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # ripple
2
2
 
3
+ ## 0.2.213
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.2.213
9
+
10
+ ## 0.2.212
11
+
12
+ ### Patch Changes
13
+
14
+ - Fix hydration error when component is last sibling - added `hydrate_advance()`
15
+ to safely advance hydration position at end of component content without
16
+ throwing when no next sibling exists
17
+
18
+ - Updated dependencies []:
19
+ - ripple@0.2.212
20
+
3
21
  ## 0.2.211
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -5,5 +5,8 @@
5
5
 
6
6
  # What is Ripple?
7
7
 
8
+ [![npm version](https://img.shields.io/npm/v/ripple?logo=npm)](https://www.npmjs.com/package/ripple)
9
+ [![npm downloads](https://img.shields.io/npm/dm/ripple?logo=npm&label=downloads)](https://www.npmjs.com/package/ripple)
10
+
8
11
  Ripple is an elegant TypeScript UI framework. To find out more, view
9
12
  [Ripple's Github README](https://github.com/Ripple-TS/ripple).
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.211",
6
+ "version": "0.2.213",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -41,6 +41,9 @@
41
41
  "./compiler/internal/import": {
42
42
  "types": "./src/compiler/types/import.d.ts"
43
43
  },
44
+ "./compiler/internal/rpc": {
45
+ "types": "./src/compiler/types/rpc.d.ts"
46
+ },
44
47
  "./compiler/internal/identifier/utils": {
45
48
  "default": "./src/compiler/identifier-utils.js"
46
49
  },
@@ -93,6 +96,6 @@
93
96
  "vscode-languageserver-types": "^3.17.5"
94
97
  },
95
98
  "peerDependencies": {
96
- "ripple": "0.2.211"
99
+ "ripple": "0.2.213"
97
100
  }
98
101
  }
@@ -2667,7 +2667,7 @@ function get_comment_handlers(source, comments, index = 0) {
2667
2667
  },
2668
2668
 
2669
2669
  /**
2670
- * @param {AST.Node} ast
2670
+ * @param {AST.Node | AST.CSS.StyleSheet} ast
2671
2671
  */
2672
2672
  add_comments: (ast) => {
2673
2673
  if (comments.length === 0) return;
@@ -2685,7 +2685,7 @@ function get_comment_handlers(source, comments, index = 0) {
2685
2685
 
2686
2686
  walk(ast, null, {
2687
2687
  _(node, { next, path }) {
2688
- const metadata = node?.metadata;
2688
+ const metadata = /** @type {AST.Node} */ (node)?.metadata;
2689
2689
 
2690
2690
  /**
2691
2691
  * Check if a comment is inside an attribute expression
@@ -2758,6 +2758,36 @@ function get_comment_handlers(source, comments, index = 0) {
2758
2758
  return element;
2759
2759
  }
2760
2760
 
2761
+ // Skip CSS nodes entirely - they use CSS-local positions (relative to
2762
+ // the <style> tag content) which would incorrectly match against
2763
+ // absolute source positions of JS/HTML comments. Also consume any
2764
+ // CSS comments (which have absolute positions) that fall within the
2765
+ // parent <style> element's content range so they don't leak to
2766
+ // subsequent JS nodes.
2767
+ if (node.type === 'StyleSheet') {
2768
+ const styleElement = /** @type {AST.Element & AST.NodeWithLocation | undefined} */ (
2769
+ path.findLast(
2770
+ (ancestor) =>
2771
+ ancestor &&
2772
+ ancestor.type === 'Element' &&
2773
+ ancestor.id &&
2774
+ /** @type {AST.Identifier} */ (ancestor.id).name === 'style',
2775
+ )
2776
+ );
2777
+ if (styleElement) {
2778
+ const cssStart =
2779
+ /** @type {AST.NodeWithLocation} */ (styleElement.openingElement)?.end ??
2780
+ styleElement.start;
2781
+ const cssEnd =
2782
+ /** @type {AST.NodeWithLocation} */ (styleElement.closingElement)?.start ??
2783
+ styleElement.end;
2784
+ while (comments[0] && comments[0].start >= cssStart && comments[0].end <= cssEnd) {
2785
+ comments.shift();
2786
+ }
2787
+ }
2788
+ return;
2789
+ }
2790
+
2761
2791
  if (metadata && metadata.commentContainerId !== undefined) {
2762
2792
  // For empty template elements, keep comments as `innerComments`.
2763
2793
  // The Prettier plugin uses `innerComments` to preserve them and
@@ -2771,6 +2801,24 @@ function get_comment_handlers(source, comments, index = 0) {
2771
2801
  comments[0].context.containerId === metadata.commentContainerId &&
2772
2802
  comments[0].context.beforeMeaningfulChild
2773
2803
  ) {
2804
+ // Check that the comment is actually in this element's own content
2805
+ // area, not positionally inside a child element. This handles the
2806
+ // case where jsx_parseOpeningElementAt() triggers jsx_readToken()
2807
+ // before the child element is pushed to the parser's #path, causing
2808
+ // comments inside the child to get the parent's containerId.
2809
+ const commentStart = comments[0].start;
2810
+ const isInsideChildElement = /** @type {AST.NodeWithChildren} */ (
2811
+ node
2812
+ ).children?.some(
2813
+ (child) =>
2814
+ child &&
2815
+ child.start !== undefined &&
2816
+ child.end !== undefined &&
2817
+ commentStart >= child.start &&
2818
+ commentStart < child.end,
2819
+ );
2820
+ if (isInsideChildElement) break;
2821
+
2774
2822
  const elementComment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2775
2823
 
2776
2824
  (metadata.elementLeadingComments ||= []).push(elementComment);
@@ -285,35 +285,19 @@ const visitors = {
285
285
  }
286
286
  }
287
287
 
288
- if (
289
- binding?.kind === 'prop' ||
290
- binding?.kind === 'prop_fallback' ||
291
- binding?.kind === 'for_pattern'
292
- ) {
293
- mark_as_tracked(context.path);
294
- if (context.state.metadata?.tracking === false) {
295
- context.state.metadata.tracking = true;
296
- }
297
- }
298
-
299
- if (
300
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
301
- node.tracked &&
302
- binding?.node !== node
303
- ) {
304
- mark_as_tracked(context.path);
305
- if (context.state.metadata?.tracking === false) {
306
- context.state.metadata.tracking = true;
307
- }
308
- }
309
-
310
- if (
311
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
312
- node.tracked &&
313
- binding?.node !== node
314
- ) {
315
- if (context.state.metadata?.tracking === false) {
316
- context.state.metadata.tracking = true;
288
+ if (node.tracked && binding) {
289
+ if (
290
+ binding.kind === 'prop' ||
291
+ binding.kind === 'prop_fallback' ||
292
+ binding.kind === 'for_pattern' ||
293
+ (is_reference(node, /** @type {AST.Node} */ (parent)) &&
294
+ node.tracked &&
295
+ binding.node !== node)
296
+ ) {
297
+ mark_as_tracked(context.path);
298
+ if (context.state.metadata?.tracking === false) {
299
+ context.state.metadata.tracking = true;
300
+ }
317
301
  }
318
302
  }
319
303
 
@@ -116,7 +116,6 @@ function visit_function(node, context) {
116
116
  if (
117
117
  metadata?.tracked === true &&
118
118
  !is_inside_component(context, true) &&
119
- is_component_level_function(context) &&
120
119
  body.type === 'BlockStatement'
121
120
  ) {
122
121
  body = { ...body, body: [b.var('__block', b.call('_$_.scope')), ...body.body] };
@@ -2308,13 +2307,17 @@ const visitors = {
2308
2307
  },
2309
2308
 
2310
2309
  TryStatement(node, context) {
2311
- if (context.state.to_ts) {
2312
- return transform_ts_child(node, context);
2313
- }
2314
-
2315
2310
  if (!is_inside_component(context)) {
2311
+ if (context.state.to_ts) {
2312
+ return transform_ts_child(node, SetContextForOutsideComponent(context));
2313
+ }
2314
+
2316
2315
  return context.next();
2317
2316
  }
2317
+
2318
+ if (context.state.to_ts) {
2319
+ return transform_ts_child(node, context);
2320
+ }
2318
2321
  context.state.template?.push('<!>');
2319
2322
 
2320
2323
  const id = context.state.flush_node?.();
@@ -2999,6 +3002,72 @@ function collect_returns_from_children(children) {
2999
3002
  return returns;
3000
3003
  }
3001
3004
 
3005
+ /**
3006
+ * Check if an Element has any dynamic content that would trigger flush_node().
3007
+ * An Element has dynamic content if it has:
3008
+ * - Dynamic attributes (tracked expressions in attribute values)
3009
+ * - Control flow children (IfStatement, ForOfStatement, etc.)
3010
+ * - Dynamic text children (non-Literal Text nodes)
3011
+ * - Non-DOM element children (components)
3012
+ * - Html children
3013
+ * - Dynamic descendants (recursive)
3014
+ * @param {AST.Element} element
3015
+ * @returns {boolean}
3016
+ */
3017
+ function element_has_dynamic_content(element) {
3018
+ // Check for dynamic attributes
3019
+ for (const attr of element.attributes) {
3020
+ if (attr.type === 'Attribute') {
3021
+ // Dynamic value expression (not null, not Literal)
3022
+ if (attr.value !== null && attr.value.type !== 'Literal') {
3023
+ return true;
3024
+ }
3025
+ // Tracked attribute name
3026
+ if (attr.name.tracked) {
3027
+ return true;
3028
+ }
3029
+ } else if (attr.type === 'SpreadAttribute' || attr.type === 'RefAttribute') {
3030
+ return true;
3031
+ }
3032
+ }
3033
+
3034
+ // Check children for dynamic content
3035
+ for (const child of element.children) {
3036
+ if (
3037
+ child.type === 'IfStatement' ||
3038
+ child.type === 'TryStatement' ||
3039
+ child.type === 'ForOfStatement' ||
3040
+ child.type === 'SwitchStatement' ||
3041
+ child.type === 'TsxCompat' ||
3042
+ child.type === 'Html'
3043
+ ) {
3044
+ return true;
3045
+ }
3046
+ if (child.type === 'Text' && child.expression.type !== 'Literal') {
3047
+ return true;
3048
+ }
3049
+ // Non-DOM element (component)
3050
+ if (
3051
+ child.type === 'Element' &&
3052
+ (child.id.type !== 'Identifier' || !is_element_dom_element(child))
3053
+ ) {
3054
+ return true;
3055
+ }
3056
+ // Recursively check DOM element children
3057
+ if (
3058
+ child.type === 'Element' &&
3059
+ child.id.type === 'Identifier' &&
3060
+ is_element_dom_element(child)
3061
+ ) {
3062
+ if (element_has_dynamic_content(child)) {
3063
+ return true;
3064
+ }
3065
+ }
3066
+ }
3067
+
3068
+ return false;
3069
+ }
3070
+
3002
3071
  /**
3003
3072
  *
3004
3073
  * @param {AST.Node[]} children
@@ -3286,6 +3355,77 @@ function transform_children(children, context) {
3286
3355
  flush_node: /** @type {TransformClientState['flush_node']} */ (flush_node),
3287
3356
  namespace: state.namespace,
3288
3357
  });
3358
+
3359
+ // After processing an element's children via child()/sibling() navigation,
3360
+ // hydrate_node is left deep inside the element. If there's a next sibling,
3361
+ // we need to restore hydrate_node so sibling() navigation works correctly.
3362
+ //
3363
+ // We only need pop() when we actually DESCEND into the element, which happens when:
3364
+ // - There are Element children (including DOM elements like <button>)
3365
+ // - There are non-literal Text children (we navigate to set text content)
3366
+ // - There are control flow / Html / component children
3367
+ //
3368
+ // The Element visitor already adds pop() for non-literal text, control flow,
3369
+ // Html, and component (non-DOM element) children. We need to ALSO add pop()
3370
+ // when there are DOM element children, which the Element visitor doesn't cover.
3371
+ const next_node = normalized[node_idx + 1];
3372
+ if (next_node && is_element_dom_element(node) && node.children.length > 0) {
3373
+ // Check if any child is a DOM element - this causes navigation but
3374
+ // the Element visitor doesn't add pop() for it
3375
+ const has_dom_element_children = node.children.some(
3376
+ (child) =>
3377
+ child.type === 'Element' &&
3378
+ child.id.type === 'Identifier' &&
3379
+ is_element_dom_element(child),
3380
+ );
3381
+
3382
+ // Check if the Element visitor already added pop()
3383
+ const element_visitor_adds_pop = node.children.some(
3384
+ (child) =>
3385
+ child.type === 'IfStatement' ||
3386
+ child.type === 'TryStatement' ||
3387
+ child.type === 'ForOfStatement' ||
3388
+ child.type === 'SwitchStatement' ||
3389
+ child.type === 'TsxCompat' ||
3390
+ child.type === 'Html' ||
3391
+ (child.type === 'Element' &&
3392
+ (child.id.type !== 'Identifier' || !is_element_dom_element(child))) ||
3393
+ (child.type === 'Text' && child.expression.type !== 'Literal'),
3394
+ );
3395
+
3396
+ // Add pop() if we have DOM element children AND the Element visitor didn't already add pop()
3397
+ if (has_dom_element_children && !element_visitor_adds_pop) {
3398
+ // Only add pop() if next_node will actually generate a sibling() call.
3399
+ // Static Text nodes (Literals) and static Elements don't call flush_node().
3400
+ let needs_sibling_call = false;
3401
+ if (next_node.type === 'Element') {
3402
+ // Static DOM elements with no dynamic content don't generate sibling()
3403
+ if (is_element_dom_element(next_node)) {
3404
+ needs_sibling_call = element_has_dynamic_content(next_node);
3405
+ } else {
3406
+ // Components always generate sibling()
3407
+ needs_sibling_call = true;
3408
+ }
3409
+ } else if (next_node.type === 'Text') {
3410
+ // Only dynamic text generates sibling()
3411
+ needs_sibling_call = next_node.expression.type !== 'Literal';
3412
+ } else if (
3413
+ next_node.type === 'Html' ||
3414
+ next_node.type === 'IfStatement' ||
3415
+ next_node.type === 'TryStatement' ||
3416
+ next_node.type === 'ForOfStatement' ||
3417
+ next_node.type === 'SwitchStatement' ||
3418
+ next_node.type === 'TsxCompat'
3419
+ ) {
3420
+ needs_sibling_call = true;
3421
+ }
3422
+
3423
+ if (needs_sibling_call) {
3424
+ const id = flush_node();
3425
+ state.init?.push(b.stmt(b.call('_$_.pop', id)));
3426
+ }
3427
+ }
3428
+ }
3289
3429
  } else if (node.type === 'TsxCompat') {
3290
3430
  skipped = 0;
3291
3431
 
@@ -8,12 +8,6 @@ import type { RippleCompileError, CompileOptions } from 'ripple/compiler';
8
8
  import type { Position } from 'acorn';
9
9
  import type { RequireAllOrNone } from '#helpers';
10
10
 
11
- export type RpcModules = Map<string, [string, string]>;
12
-
13
- declare global {
14
- var rpc_modules: RpcModules | undefined;
15
- }
16
-
17
11
  export type NameSpace = keyof typeof NAMESPACE_URI;
18
12
  interface BaseNodeMetaData {
19
13
  scoped?: boolean;
@@ -140,6 +134,7 @@ declare module 'estree' {
140
134
  ServerIdentifier: ServerIdentifier;
141
135
  Text: TextNode;
142
136
  JSXEmptyExpression: ESTreeJSX.JSXEmptyExpression;
137
+ ParenthesizedExpression: ParenthesizedExpression;
143
138
  }
144
139
 
145
140
  // Missing estree type
@@ -399,7 +394,9 @@ declare module 'estree' {
399
394
  value: AST.Property['value'] | Component;
400
395
  }
401
396
 
402
- export type RippleAttribute = Attribute | SpreadAttribute | RefAttribute;
397
+ export type RippleAttribute = AST.Attribute | AST.SpreadAttribute | AST.RefAttribute;
398
+
399
+ export type NodeWithChildren = AST.Element | AST.TsxCompat;
403
400
 
404
401
  export namespace CSS {
405
402
  export interface BaseNode {
@@ -0,0 +1,5 @@
1
+ export type RpcModules = Map<string, [string, string]>;
2
+
3
+ declare global {
4
+ var rpc_modules: RpcModules | undefined;
5
+ }
@@ -28,6 +28,10 @@ export function hydrate_next() {
28
28
  return set_hydrate_node(get_next_sibling(/** @type {Node} */ (hydrate_node)));
29
29
  }
30
30
 
31
+ export function hydrate_advance() {
32
+ hydrate_node = get_next_sibling(/** @type {Node} */ (hydrate_node));
33
+ }
34
+
31
35
  export function next(n = 1) {
32
36
  if (hydrating) {
33
37
  var node = hydrate_node;
@@ -6,21 +6,49 @@ import * as devalue from 'devalue';
6
6
  */
7
7
  export async function rpc(hash, args) {
8
8
  const body = devalue.stringify(args);
9
- let data;
9
+ /** @type {Response} */
10
+ let response;
10
11
 
11
12
  try {
12
- const response = await fetch('/_$_ripple_rpc_$_/' + hash, {
13
+ response = await fetch('/_$_ripple_rpc_$_/' + hash, {
13
14
  method: 'POST',
14
15
  headers: {
15
16
  'Content-Type': 'application/json',
16
17
  },
17
18
  body,
18
19
  });
19
- data = await response.text();
20
20
  } catch (err) {
21
21
  throw new Error('An error occurred while trying to call the server function.');
22
22
  }
23
23
 
24
+ if (!response.ok) {
25
+ let message = `Server function call failed with status ${response.status}`;
26
+ let error_body;
27
+ try {
28
+ error_body = await response.text();
29
+ } catch {
30
+ // ignore parse errors, use default message
31
+ }
32
+
33
+ if (error_body) {
34
+ try {
35
+ const parsed = JSON.parse(error_body);
36
+
37
+ if (parsed && typeof parsed.error === 'string' && parsed.error.length > 0) {
38
+ message = parsed.error;
39
+ } else {
40
+ message = error_body;
41
+ }
42
+ } catch {
43
+ message = error_body;
44
+ }
45
+ }
46
+
47
+ throw new Error(message);
48
+ }
49
+
50
+ const data = await response.text();
51
+
24
52
  if (data === '') {
25
53
  throw new Error(
26
54
  'The server function end-point did not return a response. Are you running a Ripple server?',
@@ -8,7 +8,7 @@ import {
8
8
  HYDRATION_START,
9
9
  HYDRATION_END,
10
10
  } from '../../../constants.js';
11
- import { hydrate_next, hydrate_node, hydrating, pop } from './hydration.js';
11
+ import { hydrate_advance, hydrate_next, hydrate_node, hydrating, pop } from './hydration.js';
12
12
  import { create_text, get_first_child, get_next_sibling, is_firefox } from './operations.js';
13
13
  import { active_block, active_namespace } from './runtime.js';
14
14
 
@@ -217,7 +217,9 @@ export function append(anchor, dom, skip_advance) {
217
217
  }
218
218
  }
219
219
 
220
- hydrate_next();
220
+ // Only advance if there's a next sibling. At the end of a component's
221
+ // content, there might not be more siblings, and that's fine.
222
+ hydrate_advance();
221
223
  return;
222
224
  }
223
225
  anchor.before(/** @type {Node} */ (dom));
@@ -357,4 +357,28 @@ export component App() {
357
357
  `;
358
358
  expect(() => compile(code, 'test.ripple')).not.toThrow();
359
359
  });
360
+
361
+ it('should inject __block for track() calls inside class constructors', () => {
362
+ const source = `
363
+ import { track } from 'ripple';
364
+
365
+ class Store {
366
+ constructor() {
367
+ this.count = track(0);
368
+ this.items = #[1, 2, 3];
369
+ }
370
+ }
371
+
372
+ export component App() {
373
+ const store = new Store();
374
+ <div>{store.count}</div>
375
+ }
376
+ `;
377
+ const result = compile(source, 'test.ripple', { mode: 'client' });
378
+ const code = result.js.code;
379
+
380
+ // The constructor's compiled output should contain __block = _$_.scope()
381
+ expect(code).toContain('__block');
382
+ expect(code).toContain('_$_.scope()');
383
+ });
360
384
  });
@@ -0,0 +1,159 @@
1
+ import { compile_to_volar_mappings } from 'ripple/compiler';
2
+
3
+ function count_occurrences(string: string, sub_string: string): number {
4
+ let count = 0;
5
+ let pos = string.indexOf(sub_string);
6
+
7
+ while (pos !== -1) {
8
+ count++;
9
+ pos = string.indexOf(sub_string, pos + sub_string.length);
10
+ }
11
+
12
+ return count;
13
+ }
14
+
15
+ // TryStatement in the Volar transform should behave like IfStatement and
16
+ // SwitchStatement: try blocks inside functions within a component must not
17
+ // be duplicated into the component body.
18
+
19
+ describe('compiler > Volar transform does not duplicate try blocks from functions', () => {
20
+ it('try inside an async function', () => {
21
+ const source = `export component App() {
22
+ async function doWork() {
23
+ try {
24
+ await fetch('/api');
25
+ } catch (e) {
26
+ console.error(e);
27
+ }
28
+ }
29
+ <div onclick={doWork}>{"click"}</div>
30
+ }`;
31
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
32
+
33
+ expect(count_occurrences(result, 'try {')).toBe(1);
34
+ expect(count_occurrences(result, 'catch')).toBe(1);
35
+ expect(count_occurrences(result, 'await fetch')).toBe(1);
36
+ });
37
+
38
+ it('try inside an arrow function', () => {
39
+ const source = `export component App() {
40
+ const doWork = async () => {
41
+ try {
42
+ await fetch('/api');
43
+ } catch (e) {}
44
+ };
45
+ <div onclick={doWork}>{"click"}</div>
46
+ }`;
47
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
48
+
49
+ expect(count_occurrences(result, 'try {')).toBe(1);
50
+ expect(count_occurrences(result, 'await fetch')).toBe(1);
51
+ });
52
+
53
+ it('try-catch-finally inside a function', () => {
54
+ const source = `export component App() {
55
+ async function save() {
56
+ try {
57
+ await fetch('/save');
58
+ } catch (e) {
59
+ console.error(e);
60
+ } finally {
61
+ console.log('done');
62
+ }
63
+ }
64
+ <div onclick={save}>{"save"}</div>
65
+ }`;
66
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
67
+
68
+ expect(count_occurrences(result, 'try {')).toBe(1);
69
+ expect(count_occurrences(result, 'catch')).toBe(1);
70
+ expect(count_occurrences(result, 'finally')).toBe(1);
71
+ });
72
+
73
+ it('try at component top level is preserved', () => {
74
+ const source = `export component App() {
75
+ try {
76
+ await fetch('/api');
77
+ } catch (e) {}
78
+ <div>{"hi"}</div>
79
+ }`;
80
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
81
+
82
+ expect(count_occurrences(result, 'try {')).toBe(1);
83
+ expect(count_occurrences(result, 'await fetch')).toBe(1);
84
+ });
85
+
86
+ it('component-level try and function-level try coexist without duplication', () => {
87
+ const source = `export component App() {
88
+ try {
89
+ await fetch('/init');
90
+ } catch (e) {}
91
+
92
+ async function refresh() {
93
+ try {
94
+ await fetch('/refresh');
95
+ } catch (e) {}
96
+ }
97
+ <div onclick={refresh}>{"click"}</div>
98
+ }`;
99
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
100
+
101
+ expect(count_occurrences(result, 'try {')).toBe(2);
102
+ expect(count_occurrences(result, 'await fetch(\'/init\')')).toBe(1);
103
+ expect(count_occurrences(result, 'await fetch(\'/refresh\')')).toBe(1);
104
+ });
105
+
106
+ it('try in nested functions', () => {
107
+ const source = `export component App() {
108
+ function outer() {
109
+ async function inner() {
110
+ try {
111
+ await fetch('/api');
112
+ } catch (e) {}
113
+ }
114
+ }
115
+ <div>{"hi"}</div>
116
+ }`;
117
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
118
+
119
+ expect(count_occurrences(result, 'try {')).toBe(1);
120
+ expect(count_occurrences(result, 'await fetch')).toBe(1);
121
+ });
122
+
123
+ it('multiple functions with try blocks each appear once', () => {
124
+ const source = `export component App() {
125
+ async function load() {
126
+ try {
127
+ await fetch('/load');
128
+ } catch (e) {}
129
+ }
130
+ async function save() {
131
+ try {
132
+ await fetch('/save');
133
+ } catch (e) {}
134
+ }
135
+ <div>{"hi"}</div>
136
+ }`;
137
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
138
+
139
+ expect(count_occurrences(result, 'try {')).toBe(2);
140
+ expect(count_occurrences(result, 'await fetch')).toBe(2);
141
+ });
142
+
143
+ it('try in object method', () => {
144
+ const source = `export component App() {
145
+ const handlers = {
146
+ async onClick() {
147
+ try {
148
+ await fetch('/api');
149
+ } catch (e) {}
150
+ }
151
+ };
152
+ <div>{"hi"}</div>
153
+ }`;
154
+ const result = compile_to_volar_mappings(source, 'test.ripple').code;
155
+
156
+ expect(count_occurrences(result, 'try {')).toBe(1);
157
+ expect(count_occurrences(result, 'await fetch')).toBe(1);
158
+ });
159
+ });