ripple 0.3.12 → 0.3.14

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 (217) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/package.json +11 -30
  3. package/src/compiler/types/import.d.ts +0 -12
  4. package/src/helpers.d.ts +2 -0
  5. package/src/runtime/array.js +38 -38
  6. package/src/runtime/create-subscriber.js +2 -2
  7. package/src/runtime/index-client.js +15 -13
  8. package/src/runtime/index-server.js +18 -11
  9. package/src/runtime/internal/client/bindings.js +4 -6
  10. package/src/runtime/internal/client/blocks.js +19 -23
  11. package/src/runtime/internal/client/constants.js +20 -9
  12. package/src/runtime/internal/client/events.js +8 -3
  13. package/src/runtime/internal/client/hmr.js +5 -17
  14. package/src/runtime/internal/client/index.js +14 -4
  15. package/src/runtime/internal/client/runtime.js +436 -173
  16. package/src/runtime/internal/client/try.js +334 -156
  17. package/src/runtime/internal/client/types.d.ts +26 -0
  18. package/src/runtime/internal/server/blocks.js +181 -0
  19. package/src/runtime/internal/server/constants.js +7 -0
  20. package/src/runtime/internal/server/index.js +774 -150
  21. package/src/runtime/internal/server/types.d.ts +35 -0
  22. package/src/runtime/media-query.js +34 -33
  23. package/src/runtime/object.js +7 -10
  24. package/src/runtime/proxy.js +2 -3
  25. package/src/runtime/reactive-value.js +23 -21
  26. package/src/server/index.js +1 -1
  27. package/src/utils/ast.js +1 -1
  28. package/src/utils/async.js +35 -0
  29. package/src/utils/attributes.js +43 -0
  30. package/src/utils/builders.js +5 -3
  31. package/tests/client/__snapshots__/computed-properties.test.rsrx.snap +49 -0
  32. package/tests/client/__snapshots__/for.test.rsrx.snap +319 -0
  33. package/tests/client/__snapshots__/html.test.rsrx.snap +40 -0
  34. package/tests/client/_etc.test.rsrx +7 -0
  35. package/tests/client/array/{array.static.test.ripple → array.static.test.rsrx} +18 -20
  36. package/tests/client/async-suspend.test.rsrx +662 -0
  37. package/tests/client/basic/__snapshots__/basic.attributes.test.rsrx.snap +60 -0
  38. package/tests/client/basic/__snapshots__/basic.rendering.test.rsrx.snap +59 -0
  39. package/tests/client/basic/{basic.errors.test.ripple → basic.errors.test.rsrx} +3 -3
  40. package/tests/client/basic/{basic.styling.test.ripple → basic.styling.test.rsrx} +1 -1
  41. package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +12 -0
  42. package/tests/client/compiler/__snapshots__/compiler.typescript.test.rsrx.snap +46 -0
  43. package/tests/client/compiler/{compiler.assignments.test.ripple → compiler.assignments.test.rsrx} +1 -1
  44. package/tests/client/compiler/{compiler.attributes.test.ripple → compiler.attributes.test.rsrx} +1 -1
  45. package/tests/client/compiler/{compiler.basic.test.ripple → compiler.basic.test.rsrx} +13 -13
  46. package/tests/client/compiler/{compiler.tracked-access.test.ripple → compiler.tracked-access.test.rsrx} +1 -1
  47. package/tests/client/compiler/{compiler.try-in-function.test.ripple → compiler.try-in-function.test.rsrx} +9 -7
  48. package/tests/client/compiler/{compiler.typescript.test.ripple → compiler.typescript.test.rsrx} +1 -1
  49. package/tests/client/composite/__snapshots__/composite.render.test.rsrx.snap +37 -0
  50. package/tests/client/css/{global-additional-cases.test.ripple → global-additional-cases.test.rsrx} +1 -1
  51. package/tests/client/css/{global-advanced-selectors.test.ripple → global-advanced-selectors.test.rsrx} +1 -1
  52. package/tests/client/css/{global-at-rules.test.ripple → global-at-rules.test.rsrx} +1 -1
  53. package/tests/client/css/{global-basic.test.ripple → global-basic.test.rsrx} +1 -1
  54. package/tests/client/css/{global-classes-ids.test.ripple → global-classes-ids.test.rsrx} +1 -1
  55. package/tests/client/css/{global-combinators.test.ripple → global-combinators.test.rsrx} +1 -1
  56. package/tests/client/css/{global-complex-nesting.test.ripple → global-complex-nesting.test.rsrx} +1 -1
  57. package/tests/client/css/{global-edge-cases.test.ripple → global-edge-cases.test.rsrx} +1 -1
  58. package/tests/client/css/{global-keyframes.test.ripple → global-keyframes.test.rsrx} +1 -1
  59. package/tests/client/css/{global-nested.test.ripple → global-nested.test.rsrx} +1 -1
  60. package/tests/client/css/{global-pseudo.test.ripple → global-pseudo.test.rsrx} +1 -1
  61. package/tests/client/css/{global-scoping.test.ripple → global-scoping.test.rsrx} +1 -1
  62. package/tests/client/css/{style-identifier.test.ripple → style-identifier.test.rsrx} +1 -1
  63. package/tests/client/{function-overload.test.ripple → function-overload.test.rsrx} +1 -1
  64. package/tests/client/{return.test.ripple → return.test.rsrx} +1 -1
  65. package/tests/client/try.test.rsrx +1702 -0
  66. package/tests/hydration/build-components.js +6 -4
  67. package/tests/hydration/compiled/client/head.js +11 -11
  68. package/tests/hydration/compiled/client/mixed-control-flow.js +55 -70
  69. package/tests/hydration/compiled/client/nested-control-flow.js +72 -88
  70. package/tests/hydration/compiled/client/try.js +42 -54
  71. package/tests/hydration/compiled/server/basic.js +491 -369
  72. package/tests/hydration/compiled/server/composite.js +153 -128
  73. package/tests/hydration/compiled/server/events.js +166 -145
  74. package/tests/hydration/compiled/server/for.js +821 -677
  75. package/tests/hydration/compiled/server/head.js +200 -165
  76. package/tests/hydration/compiled/server/hmr.js +62 -54
  77. package/tests/hydration/compiled/server/html-in-template.js +64 -55
  78. package/tests/hydration/compiled/server/html.js +1477 -1360
  79. package/tests/hydration/compiled/server/if-children.js +448 -408
  80. package/tests/hydration/compiled/server/if.js +204 -171
  81. package/tests/hydration/compiled/server/mixed-control-flow.js +237 -195
  82. package/tests/hydration/compiled/server/nested-control-flow.js +533 -467
  83. package/tests/hydration/compiled/server/portal.js +94 -107
  84. package/tests/hydration/compiled/server/reactivity.js +87 -64
  85. package/tests/hydration/compiled/server/return.js +1424 -1174
  86. package/tests/hydration/compiled/server/switch.js +268 -238
  87. package/tests/hydration/compiled/server/try.js +98 -87
  88. package/tests/hydration/components/{mixed-control-flow.ripple → mixed-control-flow.rsrx} +2 -2
  89. package/tests/hydration/components/{try.ripple → try.rsrx} +4 -2
  90. package/tests/hydration/mixed-control-flow.test.js +14 -0
  91. package/tests/hydration/nested-control-flow.test.js +50 -48
  92. package/tests/hydration/try.test.js +25 -0
  93. package/tests/server/__snapshots__/compiler.test.ripple.snap +0 -32
  94. package/tests/server/__snapshots__/compiler.test.rsrx.snap +95 -0
  95. package/tests/server/{compiler.test.ripple → compiler.test.rsrx} +0 -17
  96. package/tests/server/{html-nesting-validation.test.ripple → html-nesting-validation.test.rsrx} +3 -3
  97. package/tests/server/streaming-ssr.test.rsrx +115 -0
  98. package/tests/server/{style-identifier.test.ripple → style-identifier.test.rsrx} +1 -1
  99. package/tests/server/try.test.rsrx +503 -0
  100. package/tests/setup-server.js +1 -1
  101. package/tests/utils/compiler-compat-config.test.js +4 -4
  102. package/tests/utils/vite-plugin-config.test.js +1 -1
  103. package/tests/utils/vite-plugin-hmr.test.js +5 -5
  104. package/tsconfig.json +2 -0
  105. package/types/index.d.ts +13 -23
  106. package/types/server.d.ts +43 -16
  107. package/src/compiler/comment-utils.js +0 -91
  108. package/src/compiler/errors.js +0 -77
  109. package/src/compiler/identifier-utils.js +0 -80
  110. package/src/compiler/index.d.ts +0 -127
  111. package/src/compiler/index.js +0 -89
  112. package/src/compiler/phases/1-parse/index.js +0 -2964
  113. package/src/compiler/phases/1-parse/style.js +0 -704
  114. package/src/compiler/phases/2-analyze/css-analyze.js +0 -160
  115. package/src/compiler/phases/2-analyze/index.js +0 -2238
  116. package/src/compiler/phases/2-analyze/prune.js +0 -1131
  117. package/src/compiler/phases/2-analyze/validation.js +0 -168
  118. package/src/compiler/phases/3-transform/client/index.js +0 -5301
  119. package/src/compiler/phases/3-transform/segments.js +0 -2129
  120. package/src/compiler/phases/3-transform/server/index.js +0 -1899
  121. package/src/compiler/phases/3-transform/stylesheet.js +0 -545
  122. package/src/compiler/scope.js +0 -476
  123. package/src/compiler/source-map-utils.js +0 -358
  124. package/src/compiler/types/acorn.d.ts +0 -11
  125. package/src/compiler/types/estree-jsx.d.ts +0 -11
  126. package/src/compiler/types/estree.d.ts +0 -11
  127. package/src/compiler/types/index.d.ts +0 -1404
  128. package/src/compiler/types/parse.d.ts +0 -1721
  129. package/src/compiler/utils.js +0 -1263
  130. package/tests/client/_etc.test.ripple +0 -5
  131. package/tests/client/async-suspend.test.ripple +0 -94
  132. package/tests/client/try.test.ripple +0 -196
  133. package/tests/server/streaming-ssr.test.ripple +0 -68
  134. package/tests/server/try.test.ripple +0 -82
  135. /package/tests/client/array/{array.copy-within.test.ripple → array.copy-within.test.rsrx} +0 -0
  136. /package/tests/client/array/{array.derived.test.ripple → array.derived.test.rsrx} +0 -0
  137. /package/tests/client/array/{array.iteration.test.ripple → array.iteration.test.rsrx} +0 -0
  138. /package/tests/client/array/{array.mutations.test.ripple → array.mutations.test.rsrx} +0 -0
  139. /package/tests/client/array/{array.to-methods.test.ripple → array.to-methods.test.rsrx} +0 -0
  140. /package/tests/client/basic/{basic.attributes.test.ripple → basic.attributes.test.rsrx} +0 -0
  141. /package/tests/client/basic/{basic.collections.test.ripple → basic.collections.test.rsrx} +0 -0
  142. /package/tests/client/basic/{basic.components.test.ripple → basic.components.test.rsrx} +0 -0
  143. /package/tests/client/basic/{basic.events.test.ripple → basic.events.test.rsrx} +0 -0
  144. /package/tests/client/basic/{basic.get-set.test.ripple → basic.get-set.test.rsrx} +0 -0
  145. /package/tests/client/basic/{basic.hmr.test.ripple → basic.hmr.test.rsrx} +0 -0
  146. /package/tests/client/basic/{basic.reactivity.test.ripple → basic.reactivity.test.rsrx} +0 -0
  147. /package/tests/client/basic/{basic.rendering.test.ripple → basic.rendering.test.rsrx} +0 -0
  148. /package/tests/client/basic/{basic.utilities.test.ripple → basic.utilities.test.rsrx} +0 -0
  149. /package/tests/client/{boundaries.test.ripple → boundaries.test.rsrx} +0 -0
  150. /package/tests/client/compiler/{compiler.regex.test.ripple → compiler.regex.test.rsrx} +0 -0
  151. /package/tests/client/composite/{composite.dynamic-components.test.ripple → composite.dynamic-components.test.rsrx} +0 -0
  152. /package/tests/client/composite/{composite.generics.test.ripple → composite.generics.test.rsrx} +0 -0
  153. /package/tests/client/composite/{composite.props.test.ripple → composite.props.test.rsrx} +0 -0
  154. /package/tests/client/composite/{composite.reactivity.test.ripple → composite.reactivity.test.rsrx} +0 -0
  155. /package/tests/client/composite/{composite.render.test.ripple → composite.render.test.rsrx} +0 -0
  156. /package/tests/client/{computed-properties.test.ripple → computed-properties.test.rsrx} +0 -0
  157. /package/tests/client/{context.test.ripple → context.test.rsrx} +0 -0
  158. /package/tests/client/{date.test.ripple → date.test.rsrx} +0 -0
  159. /package/tests/client/{dynamic-elements.test.ripple → dynamic-elements.test.rsrx} +0 -0
  160. /package/tests/client/{events.test.ripple → events.test.rsrx} +0 -0
  161. /package/tests/client/{for.test.ripple → for.test.rsrx} +0 -0
  162. /package/tests/client/{function-overload-import.ripple → function-overload-import.rsrx} +0 -0
  163. /package/tests/client/{head.test.ripple → head.test.rsrx} +0 -0
  164. /package/tests/client/{html.test.ripple → html.test.rsrx} +0 -0
  165. /package/tests/client/{input-value.test.ripple → input-value.test.rsrx} +0 -0
  166. /package/tests/client/{lazy-destructuring.test.ripple → lazy-destructuring.test.rsrx} +0 -0
  167. /package/tests/client/{map.test.ripple → map.test.rsrx} +0 -0
  168. /package/tests/client/{media-query.test.ripple → media-query.test.rsrx} +0 -0
  169. /package/tests/client/{object.test.ripple → object.test.rsrx} +0 -0
  170. /package/tests/client/{portal.test.ripple → portal.test.rsrx} +0 -0
  171. /package/tests/client/{ref.test.ripple → ref.test.rsrx} +0 -0
  172. /package/tests/client/{set.test.ripple → set.test.rsrx} +0 -0
  173. /package/tests/client/{svg.test.ripple → svg.test.rsrx} +0 -0
  174. /package/tests/client/{switch.test.ripple → switch.test.rsrx} +0 -0
  175. /package/tests/client/{tsx.test.ripple → tsx.test.rsrx} +0 -0
  176. /package/tests/client/{typescript-generics.test.ripple → typescript-generics.test.rsrx} +0 -0
  177. /package/tests/client/url/{url.derived.test.ripple → url.derived.test.rsrx} +0 -0
  178. /package/tests/client/url/{url.parsing.test.ripple → url.parsing.test.rsrx} +0 -0
  179. /package/tests/client/url/{url.partial-removal.test.ripple → url.partial-removal.test.rsrx} +0 -0
  180. /package/tests/client/url/{url.reactivity.test.ripple → url.reactivity.test.rsrx} +0 -0
  181. /package/tests/client/url/{url.serialization.test.ripple → url.serialization.test.rsrx} +0 -0
  182. /package/tests/client/url-search-params/{url-search-params.derived.test.ripple → url-search-params.derived.test.rsrx} +0 -0
  183. /package/tests/client/url-search-params/{url-search-params.initialization.test.ripple → url-search-params.initialization.test.rsrx} +0 -0
  184. /package/tests/client/url-search-params/{url-search-params.iteration.test.ripple → url-search-params.iteration.test.rsrx} +0 -0
  185. /package/tests/client/url-search-params/{url-search-params.mutation.test.ripple → url-search-params.mutation.test.rsrx} +0 -0
  186. /package/tests/client/url-search-params/{url-search-params.retrieval.test.ripple → url-search-params.retrieval.test.rsrx} +0 -0
  187. /package/tests/client/url-search-params/{url-search-params.serialization.test.ripple → url-search-params.serialization.test.rsrx} +0 -0
  188. /package/tests/client/url-search-params/{url-search-params.tracked-url.test.ripple → url-search-params.tracked-url.test.rsrx} +0 -0
  189. /package/tests/hydration/components/{basic.ripple → basic.rsrx} +0 -0
  190. /package/tests/hydration/components/{composite.ripple → composite.rsrx} +0 -0
  191. /package/tests/hydration/components/{events.ripple → events.rsrx} +0 -0
  192. /package/tests/hydration/components/{for.ripple → for.rsrx} +0 -0
  193. /package/tests/hydration/components/{head.ripple → head.rsrx} +0 -0
  194. /package/tests/hydration/components/{hmr.ripple → hmr.rsrx} +0 -0
  195. /package/tests/hydration/components/{html-in-template.ripple → html-in-template.rsrx} +0 -0
  196. /package/tests/hydration/components/{html.ripple → html.rsrx} +0 -0
  197. /package/tests/hydration/components/{if-children.ripple → if-children.rsrx} +0 -0
  198. /package/tests/hydration/components/{if.ripple → if.rsrx} +0 -0
  199. /package/tests/hydration/components/{nested-control-flow.ripple → nested-control-flow.rsrx} +0 -0
  200. /package/tests/hydration/components/{portal.ripple → portal.rsrx} +0 -0
  201. /package/tests/hydration/components/{reactivity.ripple → reactivity.rsrx} +0 -0
  202. /package/tests/hydration/components/{return.ripple → return.rsrx} +0 -0
  203. /package/tests/hydration/components/{switch.ripple → switch.rsrx} +0 -0
  204. /package/tests/server/{await.test.ripple → await.test.rsrx} +0 -0
  205. /package/tests/server/{basic.attributes.test.ripple → basic.attributes.test.rsrx} +0 -0
  206. /package/tests/server/{basic.components.test.ripple → basic.components.test.rsrx} +0 -0
  207. /package/tests/server/{basic.test.ripple → basic.test.rsrx} +0 -0
  208. /package/tests/server/{composite.props.test.ripple → composite.props.test.rsrx} +0 -0
  209. /package/tests/server/{composite.test.ripple → composite.test.rsrx} +0 -0
  210. /package/tests/server/{context.test.ripple → context.test.rsrx} +0 -0
  211. /package/tests/server/{dynamic-elements.test.ripple → dynamic-elements.test.rsrx} +0 -0
  212. /package/tests/server/{for.test.ripple → for.test.rsrx} +0 -0
  213. /package/tests/server/{head.test.ripple → head.test.rsrx} +0 -0
  214. /package/tests/server/{if.test.ripple → if.test.rsrx} +0 -0
  215. /package/tests/server/{lazy-destructuring.test.ripple → lazy-destructuring.test.rsrx} +0 -0
  216. /package/tests/server/{return.test.ripple → return.test.rsrx} +0 -0
  217. /package/tests/server/{switch.test.ripple → switch.test.rsrx} +0 -0
@@ -1,2238 +0,0 @@
1
- /** @import {AnalyzeOptions} from 'ripple/compiler' */
2
- /**
3
- @import {
4
- AnalysisResult,
5
- AnalysisState,
6
- AnalysisContext,
7
- Context,
8
- ScopeInterface,
9
- Visitors,
10
- TopScopedClasses,
11
- StyleClasses,
12
- } from '#compiler';
13
- */
14
- /**
15
- @import * as AST from 'estree';
16
- @import * as ESTreeJSX from 'estree-jsx';
17
- */
18
-
19
- import * as b from '../../../utils/builders.js';
20
- import { walk } from 'zimmerframe';
21
- import { create_scopes, ScopeRoot } from '../../scope.js';
22
- import {
23
- is_delegated_event,
24
- get_parent_block_node,
25
- is_element_dom_element,
26
- is_inside_component,
27
- is_ripple_track_call,
28
- is_void_element,
29
- is_children_template_expression as is_children_template_expression_in_scope,
30
- normalize_children,
31
- is_binding_function,
32
- is_inside_try_block,
33
- } from '../../utils.js';
34
- import { extract_paths } from '../../../utils/ast.js';
35
- import is_reference from 'is-reference';
36
- import { prune_css } from './prune.js';
37
- import { analyze_css } from './css-analyze.js';
38
- import { error } from '../../errors.js';
39
- import { is_event_attribute } from '../../../utils/events.js';
40
- import { validate_nesting } from './validation.js';
41
-
42
- const valid_in_head = new Set(['title', 'base', 'link', 'meta', 'style', 'script', 'noscript']);
43
-
44
- const mutating_method_names = new Set([
45
- 'add',
46
- 'append',
47
- 'clear',
48
- 'copyWithin',
49
- 'delete',
50
- 'fill',
51
- 'pop',
52
- 'push',
53
- 'reverse',
54
- 'set',
55
- 'shift',
56
- 'sort',
57
- 'splice',
58
- 'unshift',
59
- ]);
60
-
61
- /**
62
- * @param {AST.MemberExpression} node
63
- * @returns {string | null}
64
- */
65
- function get_member_name(node) {
66
- if (!node.computed && node.property.type === 'Identifier') {
67
- return node.property.name;
68
- }
69
-
70
- if (node.computed && node.property.type === 'Literal') {
71
- return typeof node.property.value === 'string' ? node.property.value : null;
72
- }
73
-
74
- return null;
75
- }
76
-
77
- /**
78
- * @param {AST.CallExpression} node
79
- * @returns {boolean}
80
- */
81
- function is_mutating_call_expression(node) {
82
- return (
83
- node.callee.type === 'MemberExpression' &&
84
- mutating_method_names.has(get_member_name(node.callee) ?? '')
85
- );
86
- }
87
-
88
- /**
89
- * Check if an expression contains side effects or other impure operations.
90
- * Template expressions should be pure reads.
91
- * @param {AST.Expression | AST.SpreadElement | AST.Super | AST.Pattern} node
92
- * @returns {boolean}
93
- */
94
- function expression_has_side_effects(node) {
95
- switch (node.type) {
96
- case 'AssignmentExpression':
97
- case 'UpdateExpression':
98
- return true;
99
- case 'SequenceExpression':
100
- return node.expressions.some(expression_has_side_effects);
101
- case 'ConditionalExpression':
102
- return (
103
- expression_has_side_effects(node.test) ||
104
- expression_has_side_effects(node.consequent) ||
105
- expression_has_side_effects(node.alternate)
106
- );
107
- case 'LogicalExpression':
108
- case 'BinaryExpression':
109
- return (
110
- expression_has_side_effects(/** @type {AST.Expression} */ (node.left)) ||
111
- expression_has_side_effects(node.right)
112
- );
113
- case 'UnaryExpression':
114
- // delete operator has side effects (removes object properties)
115
- if (node.operator === 'delete') return true;
116
- return expression_has_side_effects(node.argument);
117
- case 'AwaitExpression':
118
- return expression_has_side_effects(node.argument);
119
- case 'ChainExpression':
120
- return expression_has_side_effects(node.expression);
121
- case 'MemberExpression':
122
- return (
123
- expression_has_side_effects(node.object) ||
124
- (node.computed &&
125
- expression_has_side_effects(/** @type {AST.Expression} */ (node.property)))
126
- );
127
- case 'CallExpression':
128
- return (
129
- is_mutating_call_expression(node) ||
130
- expression_has_side_effects(node.callee) ||
131
- node.arguments.some(expression_has_side_effects)
132
- );
133
- case 'NewExpression':
134
- return (
135
- expression_has_side_effects(node.callee) || node.arguments.some(expression_has_side_effects)
136
- );
137
- case 'TemplateLiteral':
138
- return node.expressions.some(expression_has_side_effects);
139
- case 'TaggedTemplateExpression':
140
- return (
141
- expression_has_side_effects(node.tag) ||
142
- node.quasi.expressions.some(expression_has_side_effects)
143
- );
144
- case 'ArrayExpression':
145
- return node.elements.some((el) => el !== null && expression_has_side_effects(el));
146
- case 'ObjectExpression':
147
- return node.properties.some((prop) =>
148
- prop.type === 'SpreadElement'
149
- ? expression_has_side_effects(prop.argument)
150
- : expression_has_side_effects(prop.value) ||
151
- (prop.computed &&
152
- expression_has_side_effects(/** @type {AST.Expression} */ (prop.key))),
153
- );
154
- case 'SpreadElement':
155
- return expression_has_side_effects(node.argument);
156
- default:
157
- return false;
158
- }
159
- }
160
-
161
- /**
162
- * @param {AnalysisContext['path']} path
163
- */
164
- function mark_control_flow_has_template(path) {
165
- for (let i = path.length - 1; i >= 0; i -= 1) {
166
- const node = path[i];
167
-
168
- if (
169
- node.type === 'Component' ||
170
- node.type === 'FunctionExpression' ||
171
- node.type === 'ArrowFunctionExpression' ||
172
- node.type === 'FunctionDeclaration'
173
- ) {
174
- break;
175
- }
176
- if (
177
- node.type === 'ForStatement' ||
178
- node.type === 'ForInStatement' ||
179
- node.type === 'ForOfStatement' ||
180
- node.type === 'TryStatement' ||
181
- node.type === 'IfStatement' ||
182
- node.type === 'SwitchStatement' ||
183
- node.type === 'Tsx' ||
184
- node.type === 'TsxCompat'
185
- ) {
186
- node.metadata.has_template = true;
187
- }
188
- }
189
- }
190
-
191
- /**
192
- * Set up lazy destructuring transforms for bindings extracted from a lazy pattern.
193
- * Converts each destructured identifier into a binding that lazily accesses properties
194
- * on the source identifier (e.g., `a` → `source.a` for object, `a` → `source[0]` for array).
195
- * @param {AST.ObjectPattern | AST.ArrayPattern} pattern - The destructuring pattern with lazy: true
196
- * @param {AST.Identifier} source_id - The identifier to access properties on
197
- * @param {AnalysisState} state - The analysis state
198
- * @param {boolean} writable - Whether assignments/updates should be supported (let vs const)
199
- * @param {boolean} is_track_call - Whether the RHS is a Ripple track() call
200
- */
201
- function setup_lazy_transforms(pattern, source_id, state, writable, is_track_call) {
202
- // For ArrayPattern from track() calls, use direct get/set calls as a fast path
203
- // instead of going through prototype getters source[0]/source[1]
204
- if (pattern.type === 'ArrayPattern' && is_track_call) {
205
- setup_lazy_array_transforms(pattern, source_id, state, writable);
206
- return;
207
- }
208
-
209
- const paths = extract_paths(pattern);
210
-
211
- for (const path of paths) {
212
- const name = /** @type {AST.Identifier} */ (path.node).name;
213
- const binding = state.scope.get(name);
214
-
215
- if (binding !== null) {
216
- const has_fallback = path.has_default_value;
217
- binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
218
-
219
- binding.transform = {
220
- read: (_) => {
221
- return path.expression(source_id);
222
- },
223
- };
224
-
225
- if (writable) {
226
- binding.transform.assign = (node, value) => {
227
- return b.assignment(
228
- '=',
229
- /** @type {AST.MemberExpression} */ (path.update_expression(source_id)),
230
- value,
231
- );
232
- };
233
-
234
- if (has_fallback) {
235
- // For bindings with default values, generate proper fallback-aware update
236
- // e.g., count++ with default 0 becomes:
237
- // (() => { var _v = _$_.fallback(obj.count, 0); obj.count = _v + 1; return _v; })() for postfix
238
- // (obj.count = _$_.fallback(obj.count, 0) + 1) for prefix
239
- binding.transform.update = (node) => {
240
- const member = path.update_expression(source_id);
241
- const fallback_read = path.expression(source_id);
242
- const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
243
-
244
- if (node.prefix) {
245
- // ++count: return new value
246
- return b.assignment(
247
- '=',
248
- /** @type {AST.Pattern} */ (member),
249
- b.binary('+', fallback_read, delta),
250
- );
251
- } else {
252
- // count++: return old value, write new value
253
- // Use IIFE to declare temp variable
254
- const temp = b.id('_v');
255
- return b.call(
256
- b.arrow(
257
- [],
258
- b.block([
259
- b.var(temp, fallback_read),
260
- b.stmt(
261
- b.assignment(
262
- '=',
263
- /** @type {AST.Pattern} */ (member),
264
- b.binary('+', temp, delta),
265
- ),
266
- ),
267
- b.return(temp),
268
- ]),
269
- ),
270
- );
271
- }
272
- };
273
- } else {
274
- binding.transform.update = (node) =>
275
- b.update(node.operator, path.update_expression(source_id), node.prefix);
276
- }
277
- }
278
- }
279
- }
280
- }
281
-
282
- /**
283
- * Set up fast-path transforms for lazy array destructuring of tracked values.
284
- * For index 0 (the value): uses _$_.get/set/update directly instead of source[0] getters.
285
- * For index 1 (the tracked ref): returns source directly instead of source[1].
286
- * @param {AST.ArrayPattern} pattern - The array destructuring pattern
287
- * @param {AST.Identifier} source_id - The identifier for the tracked value
288
- * @param {AnalysisState} state - The analysis state
289
- * @param {boolean} writable - Whether assignments/updates should be supported
290
- */
291
- function setup_lazy_array_transforms(pattern, source_id, state, writable) {
292
- for (let i = 0; i < pattern.elements.length; i++) {
293
- const element = pattern.elements[i];
294
- if (!element) continue;
295
-
296
- // Rest elements — fall back to generic source.slice(i)
297
- if (element.type === 'RestElement') {
298
- const rest_paths = extract_paths(pattern);
299
- for (const path of rest_paths) {
300
- if (!path.is_rest) continue;
301
- const name = /** @type {AST.Identifier} */ (path.node).name;
302
- const binding = state.scope.get(name);
303
- if (binding !== null) {
304
- binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
305
- binding.transform = {
306
- read: (_) => path.expression(source_id),
307
- };
308
- }
309
- }
310
- continue;
311
- }
312
-
313
- const actual = element.type === 'AssignmentPattern' ? element.left : element;
314
- const has_fallback = element.type === 'AssignmentPattern';
315
- const fallback_value = has_fallback
316
- ? /** @type {AST.AssignmentPattern} */ (element).right
317
- : null;
318
-
319
- if (actual.type === 'Identifier' && i <= 1) {
320
- const name = actual.name;
321
- const binding = state.scope.get(name);
322
- if (binding === null) continue;
323
-
324
- binding.kind = has_fallback ? 'lazy_fallback' : 'lazy';
325
-
326
- if (i === 0) {
327
- // Fast path for index 0: use _$_.get(source) instead of source[0]
328
- const read_expr = has_fallback
329
- ? () =>
330
- b.call(
331
- '_$_.fallback',
332
- b.call('_$_.get', source_id),
333
- /** @type {AST.Expression} */ (fallback_value),
334
- )
335
- : () => b.call('_$_.get', source_id);
336
-
337
- // Signal that read already produces an unwrapped value (calls _$_.get internally)
338
- binding.read_unwraps = true;
339
-
340
- binding.transform = {
341
- read: (_) => read_expr(),
342
- };
343
-
344
- if (writable) {
345
- binding.transform.assign = (_, value) => {
346
- return b.call('_$_.set', source_id, value);
347
- };
348
-
349
- if (has_fallback) {
350
- binding.transform.update = (node) => {
351
- const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
352
- const temp = b.id('_v');
353
-
354
- if (node.prefix) {
355
- // ++count: compute new value and set it, return new value
356
- return b.call(
357
- b.arrow(
358
- [],
359
- b.block([
360
- b.var(temp, b.binary('+', read_expr(), delta)),
361
- b.stmt(b.call('_$_.set', source_id, temp)),
362
- b.return(temp),
363
- ]),
364
- ),
365
- );
366
- } else {
367
- // count++: read old value, set new value, return old value
368
- return b.call(
369
- b.arrow(
370
- [],
371
- b.block([
372
- b.var(temp, read_expr()),
373
- b.stmt(b.call('_$_.set', source_id, b.binary('+', temp, delta))),
374
- b.return(temp),
375
- ]),
376
- ),
377
- );
378
- }
379
- };
380
- } else {
381
- binding.transform.update = (node) => {
382
- const fn_name = node.prefix ? '_$_.update_pre' : '_$_.update';
383
- /** @type {AST.Expression[]} */
384
- const args = [source_id];
385
- if (node.operator === '--') {
386
- args.push(b.literal(-1));
387
- }
388
- return b.call(fn_name, ...args);
389
- };
390
- }
391
- }
392
- } else {
393
- // Fast path for index 1: source itself is the tracked ref
394
- binding.transform = {
395
- read: (_) => source_id,
396
- };
397
- }
398
- } else {
399
- // Nested patterns or indices > 1: fall back to generic source[i] access via extract_paths
400
- /** @type {(object: AST.Expression) => AST.Expression} */
401
- const base_expression =
402
- i === 0
403
- ? (object) => b.call('_$_.get', object)
404
- : i === 1
405
- ? (object) => object
406
- : (object) => b.member(object, b.literal(i), true);
407
-
408
- const inner_paths = extract_paths(element);
409
- for (const path of inner_paths) {
410
- const name = /** @type {AST.Identifier} */ (path.node).name;
411
- const binding = state.scope.get(name);
412
- if (binding === null) continue;
413
-
414
- binding.kind = path.has_default_value ? 'lazy_fallback' : 'lazy';
415
-
416
- binding.transform = {
417
- read: (_) =>
418
- path.expression(
419
- /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
420
- ),
421
- };
422
-
423
- if (writable) {
424
- binding.transform.assign = (node, value) => {
425
- return b.assignment(
426
- '=',
427
- /** @type {AST.MemberExpression} */ (
428
- path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id)))
429
- ),
430
- value,
431
- );
432
- };
433
-
434
- if (path.has_default_value) {
435
- binding.transform.update = (node) => {
436
- const member = path.update_expression(
437
- /** @type {AST.Identifier} */ (base_expression(source_id)),
438
- );
439
- const fallback_read = path.expression(
440
- /** @type {AST.Identifier | AST.CallExpression} */ (base_expression(source_id)),
441
- );
442
- const delta = node.operator === '++' ? b.literal(1) : b.literal(-1);
443
-
444
- if (node.prefix) {
445
- return b.assignment(
446
- '=',
447
- /** @type {AST.Pattern} */ (member),
448
- b.binary('+', fallback_read, delta),
449
- );
450
- } else {
451
- const temp = b.id('_v');
452
- return b.call(
453
- b.arrow(
454
- [],
455
- b.block([
456
- b.var(temp, fallback_read),
457
- b.stmt(
458
- b.assignment(
459
- '=',
460
- /** @type {AST.Pattern} */ (member),
461
- b.binary('+', temp, delta),
462
- ),
463
- ),
464
- b.return(temp),
465
- ]),
466
- ),
467
- );
468
- }
469
- };
470
- } else {
471
- binding.transform.update = (node) =>
472
- b.update(
473
- node.operator,
474
- path.update_expression(/** @type {AST.Identifier} */ (base_expression(source_id))),
475
- node.prefix,
476
- );
477
- }
478
- }
479
- }
480
- }
481
- }
482
- }
483
-
484
- /**
485
- * @param {AST.Pattern} pattern
486
- * @returns {AST.TypeNode | undefined}
487
- */
488
- function get_pattern_type_annotation(pattern) {
489
- return pattern.typeAnnotation?.typeAnnotation;
490
- }
491
-
492
- /**
493
- * @param {AST.TypeNode | undefined} type_annotation
494
- * @returns {AST.TypeNode | undefined}
495
- */
496
- function unwrap_type_annotation(type_annotation) {
497
- /** @type {AST.TypeNode | undefined} */
498
- let annotation = type_annotation;
499
-
500
- while (annotation) {
501
- if (annotation.type === 'TSParenthesizedType') {
502
- annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
503
- continue;
504
- }
505
- if (annotation.type === 'TSOptionalType') {
506
- annotation = /** @type {AST.TypeNode | undefined} */ (annotation.typeAnnotation);
507
- continue;
508
- }
509
- break;
510
- }
511
-
512
- return annotation;
513
- }
514
-
515
- /**
516
- * @param {AST.TypeNode} type_annotation
517
- * @returns {AST.TypeNode}
518
- */
519
- function normalize_tuple_element_type(type_annotation) {
520
- /** @type {AST.TypeNode} */
521
- let annotation = type_annotation;
522
-
523
- while (true) {
524
- if (annotation.type === 'TSNamedTupleMember') {
525
- annotation = annotation.elementType;
526
- continue;
527
- }
528
- if (annotation.type === 'TSParenthesizedType') {
529
- annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
530
- continue;
531
- }
532
- if (annotation.type === 'TSOptionalType') {
533
- annotation = /** @type {AST.TypeNode} */ (annotation.typeAnnotation);
534
- continue;
535
- }
536
- break;
537
- }
538
-
539
- return annotation;
540
- }
541
-
542
- /**
543
- * @param {AST.Expression} key
544
- * @returns {string | null}
545
- */
546
- function get_object_pattern_key_name(key) {
547
- if (key.type === 'Identifier') {
548
- return key.name;
549
- }
550
- if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
551
- return String(key.value);
552
- }
553
- return null;
554
- }
555
-
556
- /**
557
- * @param {AST.PropertyNameNonComputed} key
558
- * @returns {string | null}
559
- */
560
- function get_type_property_key_name(key) {
561
- if (key.type === 'Identifier') {
562
- return key.name;
563
- }
564
- if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
565
- return String(key.value);
566
- }
567
- return null;
568
- }
569
-
570
- /**
571
- * @param {AST.TypeNode | undefined} type_annotation
572
- * @param {AST.Property | AST.RestElement} property
573
- * @returns {AST.TypeNode | undefined}
574
- */
575
- function get_object_property_type_annotation(type_annotation, property) {
576
- if (property.type === 'RestElement' || property.computed) {
577
- return undefined;
578
- }
579
-
580
- const object_type_annotation = unwrap_type_annotation(type_annotation);
581
- if (object_type_annotation?.type !== 'TSTypeLiteral') {
582
- return undefined;
583
- }
584
-
585
- const key_name = get_object_pattern_key_name(/** @type {AST.Expression} */ (property.key));
586
- if (key_name === null) {
587
- return undefined;
588
- }
589
-
590
- for (const member of object_type_annotation.members) {
591
- if (member.type !== 'TSPropertySignature' || member.computed) {
592
- continue;
593
- }
594
- const member_key_name = get_type_property_key_name(member.key);
595
- if (member_key_name === key_name) {
596
- return member.typeAnnotation?.typeAnnotation;
597
- }
598
- }
599
-
600
- return undefined;
601
- }
602
-
603
- /**
604
- * @param {AST.TypeNode | undefined} type_annotation
605
- * @param {number} index
606
- * @param {boolean} is_rest
607
- * @returns {AST.TypeNode | undefined}
608
- */
609
- function get_array_element_type_annotation(type_annotation, index, is_rest) {
610
- const array_type_annotation = unwrap_type_annotation(type_annotation);
611
-
612
- if (array_type_annotation?.type === 'TSArrayType') {
613
- return array_type_annotation.elementType;
614
- }
615
- if (array_type_annotation?.type !== 'TSTupleType') {
616
- return undefined;
617
- }
618
-
619
- if (is_rest) {
620
- for (let i = array_type_annotation.elementTypes.length - 1; i >= 0; i -= 1) {
621
- const element_type = normalize_tuple_element_type(array_type_annotation.elementTypes[i]);
622
- if (element_type.type === 'TSRestType') {
623
- return element_type.typeAnnotation;
624
- }
625
- }
626
- return undefined;
627
- }
628
-
629
- if (index < array_type_annotation.elementTypes.length) {
630
- const element_type = normalize_tuple_element_type(array_type_annotation.elementTypes[index]);
631
- if (element_type.type === 'TSRestType') {
632
- const rest_type_annotation = unwrap_type_annotation(element_type.typeAnnotation);
633
- return rest_type_annotation?.type === 'TSArrayType'
634
- ? rest_type_annotation.elementType
635
- : element_type.typeAnnotation;
636
- }
637
- return element_type;
638
- }
639
-
640
- const last_element = array_type_annotation.elementTypes.at(-1);
641
- if (!last_element) {
642
- return undefined;
643
- }
644
- const normalized_last_element = normalize_tuple_element_type(last_element);
645
- if (normalized_last_element.type === 'TSRestType') {
646
- const rest_type_annotation = unwrap_type_annotation(normalized_last_element.typeAnnotation);
647
- return rest_type_annotation?.type === 'TSArrayType'
648
- ? rest_type_annotation.elementType
649
- : normalized_last_element.typeAnnotation;
650
- }
651
-
652
- return undefined;
653
- }
654
-
655
- /**
656
- * Checks if a parameter source has a Tracked<T> type annotation imported from ripple.
657
- * This is used to determine if lazy array destructuring should use the track tuple fast path.
658
- * @param {AST.TypeNode | undefined} type_annotation - The source type annotation
659
- * @param {AnalysisContext} context - The analysis context
660
- * @returns {boolean}
661
- */
662
- function is_param_tracked_type(type_annotation, context) {
663
- const annotation = unwrap_type_annotation(type_annotation);
664
-
665
- if (
666
- annotation?.type === 'TSTypeReference' &&
667
- annotation.typeName?.type === 'Identifier' &&
668
- annotation.typeName.name === 'Tracked'
669
- ) {
670
- const binding = context.state.scope.get('Tracked');
671
-
672
- return (
673
- binding?.declaration_kind === 'import' &&
674
- binding.initial !== null &&
675
- binding.initial.type === 'ImportDeclaration' &&
676
- binding.initial.source.type === 'Literal' &&
677
- binding.initial.source.value === 'ripple'
678
- );
679
- }
680
-
681
- return false;
682
- }
683
-
684
- /**
685
- * Sets up lazy transforms for any lazy subpatterns nested inside a function or component param.
686
- * @param {AST.Pattern} pattern
687
- * @param {AnalysisContext} context
688
- * @param {AST.TypeNode | undefined} [type_annotation]
689
- */
690
- function setup_nested_lazy_param_transforms(pattern, context, type_annotation = undefined) {
691
- const pattern_type_annotation = get_pattern_type_annotation(pattern) ?? type_annotation;
692
-
693
- switch (pattern.type) {
694
- case 'AssignmentPattern':
695
- setup_nested_lazy_param_transforms(pattern.left, context, pattern_type_annotation);
696
- return;
697
-
698
- case 'RestElement':
699
- setup_nested_lazy_param_transforms(pattern.argument, context, pattern_type_annotation);
700
- return;
701
-
702
- case 'ObjectPattern':
703
- case 'ArrayPattern': {
704
- if (pattern.lazy) {
705
- const param_id = b.id(context.state.scope.generate('lazy'));
706
- const is_tracked_type =
707
- pattern.type === 'ArrayPattern' &&
708
- is_param_tracked_type(pattern_type_annotation, context);
709
-
710
- setup_lazy_transforms(pattern, param_id, context.state, true, is_tracked_type);
711
- pattern.metadata = { ...pattern.metadata, lazy_id: param_id.name };
712
- return;
713
- }
714
-
715
- if (pattern.type === 'ObjectPattern') {
716
- for (const property of pattern.properties) {
717
- const property_type_annotation = get_object_property_type_annotation(
718
- pattern_type_annotation,
719
- property,
720
- );
721
- if (property.type === 'RestElement') {
722
- setup_nested_lazy_param_transforms(
723
- property.argument,
724
- context,
725
- property_type_annotation,
726
- );
727
- } else {
728
- setup_nested_lazy_param_transforms(property.value, context, property_type_annotation);
729
- }
730
- }
731
- } else {
732
- for (let i = 0; i < pattern.elements.length; i += 1) {
733
- const element = pattern.elements[i];
734
- if (element !== null) {
735
- setup_nested_lazy_param_transforms(
736
- element,
737
- context,
738
- get_array_element_type_annotation(
739
- pattern_type_annotation,
740
- i,
741
- element.type === 'RestElement',
742
- ),
743
- );
744
- }
745
- }
746
- }
747
-
748
- return;
749
- }
750
- }
751
- }
752
-
753
- /**
754
- * @param {AST.Function} node
755
- * @param {AnalysisContext} context
756
- */
757
- function visit_function(node, context) {
758
- node.metadata = {
759
- tracked: false,
760
- path: [...context.path],
761
- };
762
-
763
- // Set up lazy transforms for any lazy destructured parameters
764
- for (let i = 0; i < node.params.length; i++) {
765
- const param_node = node.params[i];
766
- const param = param_node.type === 'AssignmentPattern' ? param_node.left : param_node;
767
- const param_type_annotation =
768
- get_pattern_type_annotation(param) ?? param_node.typeAnnotation?.typeAnnotation;
769
-
770
- if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
771
- setup_nested_lazy_param_transforms(param, context, param_type_annotation);
772
- }
773
- }
774
-
775
- context.next({
776
- ...context.state,
777
- function_depth: (context.state.function_depth ?? 0) + 1,
778
- });
779
-
780
- if (node.metadata.tracked) {
781
- mark_as_tracked(context.path);
782
- }
783
- }
784
-
785
- /**
786
- * @param {AnalysisContext['path']} path
787
- */
788
- function mark_as_tracked(path) {
789
- for (let i = path.length - 1; i >= 0; i -= 1) {
790
- const node = path[i];
791
-
792
- if (node.type === 'Component') {
793
- break;
794
- }
795
- if (
796
- node.type === 'FunctionExpression' ||
797
- node.type === 'ArrowFunctionExpression' ||
798
- node.type === 'FunctionDeclaration'
799
- ) {
800
- node.metadata.tracked = true;
801
- break;
802
- }
803
- }
804
- }
805
-
806
- /**
807
- * @param {AST.ReturnStatement} node
808
- * @returns {AST.ReturnStatement}
809
- */
810
- function get_return_keyword_node(node) {
811
- const return_keyword_length = 'return'.length;
812
- return /** @type {AST.ReturnStatement} */ ({
813
- ...node,
814
- end: /** @type {AST.NodeWithLocation} */ (node).start + return_keyword_length,
815
- loc: {
816
- start: /** @type {AST.NodeWithLocation} */ (node).loc.start,
817
- end: {
818
- line: /** @type {AST.NodeWithLocation} */ (node).loc.start.line,
819
- column: /** @type {AST.NodeWithLocation} */ (node).loc.start.column + return_keyword_length,
820
- },
821
- },
822
- });
823
- }
824
-
825
- /**
826
- * @param {AST.ReturnStatement} node
827
- * @param {AnalysisContext} context
828
- * @param {string} message
829
- */
830
- function error_return_keyword(node, context, message) {
831
- const return_keyword_node = get_return_keyword_node(node);
832
-
833
- error(
834
- message,
835
- context.state.analysis.module.filename,
836
- return_keyword_node,
837
- context.state.loose ? context.state.analysis.errors : undefined,
838
- context.state.analysis.comments,
839
- );
840
- }
841
-
842
- /**
843
- * @param {AST.Expression} expression
844
- * @param {Context<AST.Node, AnalysisState>} context
845
- * @returns {boolean}
846
- */
847
- function is_children_template_expression(expression, context) {
848
- const component = context.path.findLast((node) => node.type === 'Component');
849
- const component_scope = component ? context.state.scopes.get(component) : null;
850
- return is_children_template_expression_in_scope(expression, context.state.scope, component_scope);
851
- }
852
-
853
- /** @type {Visitors<AST.Node, AnalysisState>} */
854
- const visitors = {
855
- _(node, { state, next, path }) {
856
- // Set up metadata.path for each node (needed for CSS pruning)
857
- if (!node.metadata) {
858
- node.metadata = { path: [...path] };
859
- } else {
860
- node.metadata.path = [...path];
861
- }
862
-
863
- const scope = state.scopes.get(node);
864
- next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
865
- },
866
-
867
- Program(_, context) {
868
- return context.next({ ...context.state, function_depth: 0 });
869
- },
870
-
871
- ServerBlock(node, context) {
872
- if (context.path.at(-1)?.type !== 'Program') {
873
- // fatal since we don't have a transformation defined for this case
874
- error(
875
- '`#server` block can only be declared at the module level.',
876
- context.state.analysis.module.filename,
877
- node,
878
- );
879
- }
880
- node.metadata = {
881
- ...node.metadata,
882
- exports: new Set(),
883
- };
884
- context.visit(node.body, {
885
- ...context.state,
886
- ancestor_server_block: node,
887
- });
888
- },
889
-
890
- Identifier(node, context) {
891
- const binding = context.state.scope.get(node.name);
892
- const parent = context.path.at(-1);
893
-
894
- if (
895
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
896
- binding &&
897
- context.state.ancestor_server_block &&
898
- binding.node !== node // Don't check the declaration itself
899
- ) {
900
- /** @type {ScopeInterface | null} */
901
- let current_scope = binding.scope;
902
- let found_server_block = false;
903
-
904
- while (current_scope !== null) {
905
- if (current_scope.server_block) {
906
- found_server_block = true;
907
- break;
908
- }
909
- current_scope = current_scope.parent;
910
- }
911
-
912
- if (!found_server_block) {
913
- error(
914
- `Cannot reference client-side "${node.name}" from a server block. Server blocks can only access variables and imports declared inside them.`,
915
- context.state.analysis.module.filename,
916
- node,
917
- context.state.loose ? context.state.analysis.errors : undefined,
918
- context.state.analysis.comments,
919
- );
920
- }
921
- }
922
-
923
- if (node.tracked && binding) {
924
- if (
925
- binding.kind === 'prop' ||
926
- binding.kind === 'prop_fallback' ||
927
- binding.kind === 'lazy' ||
928
- binding.kind === 'lazy_fallback' ||
929
- binding.kind === 'for_pattern' ||
930
- (is_reference(node, /** @type {AST.Node} */ (parent)) &&
931
- node.tracked &&
932
- binding.node !== node)
933
- ) {
934
- mark_as_tracked(context.path);
935
- if (context.state.metadata?.tracking === false) {
936
- context.state.metadata.tracking = true;
937
- }
938
- }
939
- }
940
-
941
- // Lazy bindings from track() calls (read_unwraps) are inherently reactive —
942
- // propagate tracking so that control flow (if/for/switch)
943
- // and early returns create reactive blocks
944
- if (
945
- !node.tracked &&
946
- binding?.read_unwraps &&
947
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
948
- binding.node !== node
949
- ) {
950
- mark_as_tracked(context.path);
951
- if (context.state.metadata?.tracking === false) {
952
- context.state.metadata.tracking = true;
953
- }
954
- }
955
-
956
- context.next();
957
- },
958
-
959
- MemberExpression(node, context) {
960
- const parent = context.path.at(-1);
961
-
962
- // Track #style.className or #style['className'] references
963
- if (node.object.type === 'StyleIdentifier') {
964
- const component = is_inside_component(context, true);
965
-
966
- if (!component) {
967
- error(
968
- '`#style` can only be used within a component',
969
- context.state.analysis.module.filename,
970
- node,
971
- context.state.loose ? context.state.analysis.errors : undefined,
972
- context.state.analysis.comments,
973
- );
974
- } else {
975
- component.metadata.styleIdentifierPresent = true;
976
- }
977
-
978
- /** @type {string | null} */
979
- let className = null;
980
-
981
- if (!node.computed && node.property.type === 'Identifier') {
982
- // #style.test
983
- className = node.property.name;
984
- } else if (
985
- node.computed &&
986
- node.property.type === 'Literal' &&
987
- typeof node.property.value === 'string'
988
- ) {
989
- // #style['test']
990
- className = node.property.value;
991
- } else {
992
- // #style[expression] - dynamic, not allowed
993
- error(
994
- '`#style` property access must use a dot property or static string for css class name, not a dynamic expression',
995
- context.state.analysis.module.filename,
996
- node.property,
997
- context.state.loose ? context.state.analysis.errors : undefined,
998
- context.state.analysis.comments,
999
- );
1000
- }
1001
-
1002
- if (className !== null) {
1003
- context.state.metadata.styleClasses?.set(className, node.property);
1004
- }
1005
-
1006
- return context.next();
1007
- } else if (node.object.type === 'ServerIdentifier') {
1008
- context.state.analysis.metadata.serverIdentifierPresent = true;
1009
- }
1010
-
1011
- if (node.object.type === 'Identifier' && !node.object.tracked) {
1012
- const binding = context.state.scope.get(node.object.name);
1013
-
1014
- if (binding && binding.metadata?.is_ripple_object) {
1015
- const internalProperties = new Set(['__v', 'a', 'b', 'c', 'f']);
1016
-
1017
- let propertyName = null;
1018
- if (node.property.type === 'Identifier' && !node.computed) {
1019
- propertyName = node.property.name;
1020
- } else if (node.property.type === 'Literal' && typeof node.property.value === 'string') {
1021
- propertyName = node.property.value;
1022
- }
1023
-
1024
- if (propertyName && internalProperties.has(propertyName)) {
1025
- error(
1026
- `Directly accessing internal property "${propertyName}" of a tracked object is not allowed. Use \`${node.object.name}.value\` or \`&[]\` lazy destructuring instead.`,
1027
- context.state.analysis.module.filename,
1028
- node.property,
1029
- context.state.loose ? context.state.analysis.errors : undefined,
1030
- context.state.analysis.comments,
1031
- );
1032
- }
1033
- }
1034
-
1035
- if (
1036
- binding !== null &&
1037
- binding.kind !== 'lazy' &&
1038
- binding.kind !== 'lazy_fallback' &&
1039
- binding.initial?.type === 'CallExpression' &&
1040
- is_ripple_track_call(binding.initial.callee, context)
1041
- ) {
1042
- const is_allowed_tracked_access =
1043
- // Allow [0] and [1] indexed access on tracked objects.
1044
- (node.computed &&
1045
- node.property.type === 'Literal' &&
1046
- (node.property.value === 0 || node.property.value === 1)) ||
1047
- // Allow .value and .length property access on tracked objects.
1048
- (!node.computed &&
1049
- node.property.type === 'Identifier' &&
1050
- (node.property.name === 'value' || node.property.name === 'length'));
1051
-
1052
- if (is_allowed_tracked_access) {
1053
- // pass through
1054
- } else {
1055
- error(
1056
- `Accessing a tracked object directly is not allowed, use \`.value\` or \`&[]\` lazy destructuring to read the value inside a tracked object - for example \`${node.object.name}.value\``,
1057
- context.state.analysis.module.filename,
1058
- node.object,
1059
- context.state.loose ? context.state.analysis.errors : undefined,
1060
- context.state.analysis.comments,
1061
- );
1062
- }
1063
- }
1064
- }
1065
-
1066
- context.next();
1067
- },
1068
-
1069
- CallExpression(node, context) {
1070
- // bug in our acorn [parser]: it uses typeParameters instead of typeArguments
1071
- // @ts-expect-error
1072
- if (node.typeParameters) {
1073
- // @ts-expect-error
1074
- node.typeArguments = node.typeParameters;
1075
- // @ts-expect-error
1076
- delete node.typeParameters;
1077
- }
1078
-
1079
- const callee = node.callee;
1080
-
1081
- if (
1082
- !context.path.some(
1083
- (path_node) => path_node.type === 'TsxCompat' || path_node.type === 'Tsx',
1084
- ) &&
1085
- is_children_template_expression(/** @type {AST.Expression} */ (callee), context)
1086
- ) {
1087
- error(
1088
- '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
1089
- context.state.analysis.module.filename,
1090
- callee,
1091
- context.state.loose ? context.state.analysis.errors : undefined,
1092
- context.state.analysis.comments,
1093
- );
1094
- }
1095
-
1096
- if (context.state.function_depth === 0 && is_ripple_track_call(callee, context)) {
1097
- error(
1098
- '`track` can only be used within a reactive context, such as a component, function or class that is used or created from a component',
1099
- context.state.analysis.module.filename,
1100
- node.callee,
1101
- context.state.loose ? context.state.analysis.errors : undefined,
1102
- context.state.analysis.comments,
1103
- );
1104
- }
1105
-
1106
- if (!is_inside_component(context, true)) {
1107
- mark_as_tracked(context.path);
1108
- }
1109
-
1110
- context.next();
1111
- },
1112
-
1113
- NewExpression(node, context) {
1114
- context.next();
1115
- },
1116
-
1117
- VariableDeclaration(node, context) {
1118
- const { state, visit } = context;
1119
-
1120
- for (const declarator of node.declarations) {
1121
- if (is_inside_component(context) && node.kind === 'var') {
1122
- error(
1123
- '`var` declarations are not allowed in components, use let or const instead',
1124
- state.analysis.module.filename,
1125
- declarator.id,
1126
- context.state.loose ? context.state.analysis.errors : undefined,
1127
- context.state.analysis.comments,
1128
- );
1129
- }
1130
- const metadata = { tracking: false, await: false };
1131
-
1132
- if (declarator.id.type === 'Identifier') {
1133
- const binding = state.scope.get(declarator.id.name);
1134
- if (binding && declarator.init && declarator.init.type === 'CallExpression') {
1135
- const callee = declarator.init.callee;
1136
- // Check if it's a call to `track` or `tracked`
1137
- if (
1138
- (callee.type === 'Identifier' &&
1139
- (callee.name === 'track' || callee.name === 'tracked')) ||
1140
- (callee.type === 'MemberExpression' &&
1141
- callee.property.type === 'Identifier' &&
1142
- (callee.property.name === 'track' || callee.property.name === 'tracked'))
1143
- ) {
1144
- binding.metadata = { ...binding.metadata, is_ripple_object: true };
1145
- }
1146
- }
1147
- visit(declarator, state);
1148
- } else {
1149
- // Handle lazy destructuring patterns
1150
- if (
1151
- (declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') &&
1152
- declarator.id.lazy
1153
- ) {
1154
- const lazy_id = b.id(state.scope.generate('lazy'));
1155
- const writable = node.kind !== 'const';
1156
- const init_is_track =
1157
- declarator.init?.type === 'CallExpression' &&
1158
- is_ripple_track_call(declarator.init.callee, context) === 'track';
1159
- setup_lazy_transforms(declarator.id, lazy_id, state, writable, !!init_is_track);
1160
- // Store the generated identifier name on the pattern for the transform phase
1161
- declarator.id.metadata = { ...declarator.id.metadata, lazy_id: lazy_id.name };
1162
- }
1163
-
1164
- visit(declarator, state);
1165
- }
1166
-
1167
- declarator.metadata = { ...metadata, path: [...context.path] };
1168
- }
1169
- },
1170
-
1171
- ExpressionStatement(node, context) {
1172
- const { state, visit } = context;
1173
-
1174
- // Handle standalone lazy destructuring assignment: &[data] = track(0);
1175
- if (
1176
- node.expression.type === 'AssignmentExpression' &&
1177
- node.expression.operator === '=' &&
1178
- (node.expression.left.type === 'ObjectPattern' ||
1179
- node.expression.left.type === 'ArrayPattern') &&
1180
- node.expression.left.lazy
1181
- ) {
1182
- const pattern = /** @type {AST.ObjectPattern | AST.ArrayPattern} */ (node.expression.left);
1183
- const lazy_id = b.id(state.scope.generate('lazy'));
1184
- const init = /** @type {AST.Expression} */ (node.expression.right);
1185
- const init_is_track =
1186
- init?.type === 'CallExpression' && is_ripple_track_call(init.callee, context) === 'track';
1187
- setup_lazy_transforms(pattern, lazy_id, state, true, !!init_is_track);
1188
- // Store the generated identifier name on the pattern for the transform phase
1189
- pattern.metadata = { ...pattern.metadata, lazy_id: lazy_id.name };
1190
- }
1191
-
1192
- context.next();
1193
- },
1194
-
1195
- StyleIdentifier(node, context) {
1196
- const component = is_inside_component(context, true);
1197
- const parent = context.path.at(-1);
1198
-
1199
- if (component) {
1200
- component.metadata.styleIdentifierPresent = true;
1201
- }
1202
-
1203
- // #style must only be used for property access (e.g., #style.className)
1204
- if (!parent || parent.type !== 'MemberExpression' || parent.object !== node) {
1205
- error(
1206
- '`#style` can only be used for property access, e.g., `#style.className`.',
1207
- context.state.analysis.module.filename,
1208
- node,
1209
- context.state.loose ? context.state.analysis.errors : undefined,
1210
- context.state.analysis.comments,
1211
- );
1212
- }
1213
- context.next();
1214
- },
1215
-
1216
- ServerIdentifier(node, context) {
1217
- const parent = context.path.at(-1);
1218
-
1219
- // #server must only be used for member access (e.g., #server.functionName(...))
1220
- if (!parent || parent.type !== 'MemberExpression' || parent.object !== node) {
1221
- error(
1222
- '`#server` can only be used for member access, e.g., `#server.functionName(...)`.',
1223
- context.state.analysis.module.filename,
1224
- node,
1225
- context.state.loose ? context.state.analysis.errors : undefined,
1226
- context.state.analysis.comments,
1227
- );
1228
- }
1229
- context.next();
1230
- },
1231
-
1232
- ArrowFunctionExpression(node, context) {
1233
- visit_function(node, context);
1234
- },
1235
- FunctionExpression(node, context) {
1236
- visit_function(node, context);
1237
- },
1238
- FunctionDeclaration(node, context) {
1239
- visit_function(node, context);
1240
- },
1241
-
1242
- Component(node, context) {
1243
- context.state.component = node;
1244
-
1245
- if (node.params.length > 0) {
1246
- const props = node.params[0];
1247
-
1248
- if (props.type === 'ObjectPattern' || props.type === 'ArrayPattern') {
1249
- // Lazy destructuring: &{...} or &[...] — set up lazy transforms
1250
- if (props.lazy) {
1251
- setup_lazy_transforms(props, b.id('__props'), context.state, true, false);
1252
- } else {
1253
- setup_nested_lazy_param_transforms(props, context, get_pattern_type_annotation(props));
1254
- }
1255
- } else if (props.type === 'AssignmentPattern') {
1256
- error(
1257
- 'Props are always an object, use destructured props with default values instead',
1258
- context.state.analysis.module.filename,
1259
- props,
1260
- context.state.loose ? context.state.analysis.errors : undefined,
1261
- context.state.analysis.comments,
1262
- );
1263
- }
1264
- }
1265
- /** @type {AST.Element[]} */
1266
- const elements = [];
1267
-
1268
- // Track metadata for this component
1269
- const metadata = {
1270
- await: false,
1271
- styleClasses: /** @type {StyleClasses} */ (new Map()),
1272
- };
1273
-
1274
- /** @type {TopScopedClasses} */
1275
- const topScopedClasses = new Map();
1276
-
1277
- context.next({
1278
- ...context.state,
1279
- elements,
1280
- function_depth: (context.state.function_depth ?? 0) + 1,
1281
- metadata,
1282
- });
1283
-
1284
- const css = node.css;
1285
-
1286
- if (css !== null) {
1287
- // Analyze CSS to set global selector metadata
1288
- analyze_css(css);
1289
-
1290
- for (const node of elements) {
1291
- prune_css(css, node, metadata.styleClasses, topScopedClasses);
1292
- }
1293
-
1294
- if (topScopedClasses.size > 0) {
1295
- node.metadata.topScopedClasses = topScopedClasses;
1296
- }
1297
- }
1298
-
1299
- if (metadata.styleClasses.size > 0) {
1300
- node.metadata.styleClasses = metadata.styleClasses;
1301
-
1302
- for (const [className, property] of metadata.styleClasses) {
1303
- if (!topScopedClasses?.has(className)) {
1304
- error(
1305
- `CSS class ".${className}" does not exist as a stand-alone class in ${node.id?.name ? node.id.name : "this component's"} <style> block`,
1306
- context.state.analysis.module.filename,
1307
- property,
1308
- context.state.loose ? context.state.analysis.errors : undefined,
1309
- context.state.analysis.comments,
1310
- );
1311
- }
1312
- }
1313
- }
1314
-
1315
- // Store component metadata in analysis
1316
- // Only add metadata if component has a name (not anonymous)
1317
- if (node.id) {
1318
- context.state.analysis.component_metadata.push({
1319
- id: node.id.name,
1320
- async: metadata.await,
1321
- });
1322
- }
1323
- },
1324
-
1325
- ForStatement(node, context) {
1326
- if (is_inside_component(context)) {
1327
- // TODO: it's a fatal error for now but
1328
- // we could implement the for loop for the ts mode only
1329
- error(
1330
- 'For loops are not supported in components. Use for...of instead.',
1331
- context.state.analysis.module.filename,
1332
- node,
1333
- );
1334
- }
1335
-
1336
- context.next();
1337
- },
1338
-
1339
- SwitchStatement(node, context) {
1340
- if (!is_inside_component(context)) {
1341
- return context.next();
1342
- }
1343
-
1344
- context.visit(node.discriminant, context.state);
1345
-
1346
- for (const switch_case of node.cases) {
1347
- // Skip empty cases
1348
- if (switch_case.consequent.length === 0) {
1349
- continue;
1350
- }
1351
-
1352
- node.metadata = {
1353
- ...node.metadata,
1354
- has_template: false,
1355
- has_await: false,
1356
- };
1357
-
1358
- context.visit(switch_case, context.state);
1359
-
1360
- if (!node.metadata.has_template && !node.metadata.has_await) {
1361
- error(
1362
- 'Component switch statements must contain a template or an await expression in each of their cases. Move the switch statement into an effect if it does not render anything.',
1363
- context.state.analysis.module.filename,
1364
- switch_case,
1365
- context.state.loose ? context.state.analysis.errors : undefined,
1366
- context.state.analysis.comments,
1367
- );
1368
- }
1369
- }
1370
- },
1371
-
1372
- ForOfStatement(node, context) {
1373
- if (!is_inside_component(context)) {
1374
- return context.next();
1375
- }
1376
-
1377
- if (node.index) {
1378
- const state = context.state;
1379
- const scope = /** @type {ScopeInterface} */ (state.scopes.get(node));
1380
- const binding = scope.get(/** @type {AST.Identifier} */ (node.index).name);
1381
-
1382
- if (binding !== null) {
1383
- binding.kind = 'index';
1384
- binding.transform = {
1385
- read: (node) => {
1386
- return b.call('_$_.get', node);
1387
- },
1388
- };
1389
- }
1390
- }
1391
-
1392
- if (node.key) {
1393
- const state = context.state;
1394
- const pattern = /** @type {AST.VariableDeclaration} */ (node.left).declarations[0].id;
1395
- const paths = extract_paths(pattern);
1396
- const scope = /** @type {ScopeInterface} */ (state.scopes.get(node));
1397
- /** @type {AST.Identifier | AST.Pattern} */
1398
- let pattern_id;
1399
- if (state.to_ts || state.mode === 'server') {
1400
- pattern_id = pattern;
1401
- } else {
1402
- pattern_id = b.id(scope.generate('pattern'));
1403
- /** @type {AST.VariableDeclaration} */ (node.left).declarations[0].id = pattern_id;
1404
- }
1405
-
1406
- for (const path of paths) {
1407
- const name = /** @type {AST.Identifier} */ (path.node).name;
1408
- const binding = context.state.scope.get(name);
1409
-
1410
- if (binding !== null) {
1411
- binding.kind = 'for_pattern';
1412
- if (!binding.metadata) {
1413
- binding.metadata = {
1414
- pattern: /** @type {AST.Identifier} */ (pattern_id),
1415
- };
1416
- }
1417
-
1418
- binding.transform = {
1419
- read: () => {
1420
- return path.expression(b.call('_$_.get', /** @type {AST.Identifier} */ (pattern_id)));
1421
- },
1422
- };
1423
- }
1424
- }
1425
- }
1426
-
1427
- node.metadata = {
1428
- ...node.metadata,
1429
- has_template: false,
1430
- has_await: false,
1431
- };
1432
- context.next();
1433
-
1434
- if (!node.metadata.has_template && !node.metadata.has_await) {
1435
- error(
1436
- 'Component for...of loops must contain a template or an await expression in their body. Move the for loop into an effect if it does not render anything.',
1437
- context.state.analysis.module.filename,
1438
- node.body,
1439
- context.state.loose ? context.state.analysis.errors : undefined,
1440
- context.state.analysis.comments,
1441
- );
1442
- }
1443
- },
1444
-
1445
- ExportNamedDeclaration(node, context) {
1446
- const server_block = context.state.ancestor_server_block;
1447
-
1448
- if (!server_block) {
1449
- return context.next();
1450
- }
1451
-
1452
- const exports = server_block.metadata.exports;
1453
- const declaration = /** @type {AST.RippleExportNamedDeclaration} */ (node).declaration;
1454
-
1455
- if (declaration && declaration.type === 'FunctionDeclaration') {
1456
- exports.add(declaration.id.name);
1457
- } else if (declaration && declaration.type === 'Component') {
1458
- error(
1459
- 'Not implemented: Exported component declaration not supported in server blocks.',
1460
- context.state.analysis.module.filename,
1461
- /** @type {AST.Identifier} */ (declaration.id),
1462
- context.state.loose ? context.state.analysis.errors : undefined,
1463
- context.state.analysis.comments,
1464
- );
1465
- // TODO: the client and server rendering doesn't currently support components
1466
- // If we're going to support this, we need to account also for anonymous object declaration
1467
- // and specifiers
1468
- // exports.add(/** @type {AST.Identifier} */ (declaration.id).name);
1469
- } else if (declaration && declaration.type === 'VariableDeclaration') {
1470
- for (const decl of declaration.declarations) {
1471
- if (decl.init !== undefined && decl.init !== null) {
1472
- if (decl.id.type === 'Identifier') {
1473
- if (
1474
- decl.init.type === 'FunctionExpression' ||
1475
- decl.init.type === 'ArrowFunctionExpression'
1476
- ) {
1477
- exports.add(decl.id.name);
1478
- continue;
1479
- } else if (decl.init.type === 'Identifier') {
1480
- const name = decl.init.name;
1481
- const binding = context.state.scope.get(name);
1482
- if (binding && is_binding_function(binding, context.state.scope)) {
1483
- exports.add(decl.id.name);
1484
- continue;
1485
- }
1486
- } else if (decl.init.type === 'MemberExpression') {
1487
- error(
1488
- 'Not implemented: Exported member expressions are not supported in server blocks.',
1489
- context.state.analysis.module.filename,
1490
- decl.init,
1491
- context.state.loose ? context.state.analysis.errors : undefined,
1492
- context.state.analysis.comments,
1493
- );
1494
- continue;
1495
- }
1496
- } else if (decl.id.type === 'ObjectPattern' || decl.id.type === 'ArrayPattern') {
1497
- const paths = extract_paths(decl.id);
1498
- for (const path of paths) {
1499
- error(
1500
- 'Not implemented: Exported object or array patterns are not supported in server blocks.',
1501
- context.state.analysis.module.filename,
1502
- path.node,
1503
- context.state.loose ? context.state.analysis.errors : undefined,
1504
- context.state.analysis.comments,
1505
- );
1506
- }
1507
- }
1508
- }
1509
- // TODO: allow exporting consts when hydration is supported
1510
- error(
1511
- `Not implemented: Exported '${decl.id.type}' type is not supported in server blocks.`,
1512
- context.state.analysis.module.filename,
1513
- decl,
1514
- context.state.loose ? context.state.analysis.errors : undefined,
1515
- context.state.analysis.comments,
1516
- );
1517
- }
1518
- } else if (node.specifiers) {
1519
- for (const specifier of node.specifiers) {
1520
- const name = /** @type {AST.Identifier} */ (specifier.local).name;
1521
- const binding = context.state.scope.get(name);
1522
- const is_function = binding && is_binding_function(binding, context.state.scope);
1523
-
1524
- if (is_function) {
1525
- exports.add(name);
1526
- continue;
1527
- }
1528
-
1529
- error(
1530
- `Not implemented: Exported specifier type not supported in server blocks.`,
1531
- context.state.analysis.module.filename,
1532
- specifier,
1533
- context.state.loose ? context.state.analysis.errors : undefined,
1534
- context.state.analysis.comments,
1535
- );
1536
- }
1537
- } else {
1538
- error(
1539
- 'Not implemented: Exported declaration type not supported in server blocks.',
1540
- context.state.analysis.module.filename,
1541
- node,
1542
- context.state.loose ? context.state.analysis.errors : undefined,
1543
- context.state.analysis.comments,
1544
- );
1545
- }
1546
-
1547
- return context.next();
1548
- },
1549
-
1550
- TSTypeReference(node, context) {
1551
- // bug in our acorn parser: it uses typeParameters instead of typeArguments
1552
- // @ts-expect-error
1553
- if (node.typeParameters) {
1554
- // @ts-expect-error
1555
- node.typeArguments = node.typeParameters;
1556
- // @ts-expect-error
1557
- delete node.typeParameters;
1558
- }
1559
- context.next();
1560
- },
1561
-
1562
- IfStatement(node, context) {
1563
- if (!is_inside_component(context)) {
1564
- return context.next();
1565
- }
1566
-
1567
- node.metadata = {
1568
- ...node.metadata,
1569
- has_template: false,
1570
- has_await: false,
1571
- has_throw: false,
1572
- };
1573
-
1574
- const test_metadata = { tracking: false };
1575
- context.visit(node.test, { ...context.state, metadata: test_metadata });
1576
- if (test_metadata.tracking) {
1577
- /** @type {AST.TrackedNode} */ (node.test).tracked = true;
1578
- }
1579
-
1580
- context.visit(node.consequent, context.state);
1581
-
1582
- const consequent_body =
1583
- node.consequent.type === 'BlockStatement' ? node.consequent.body : [node.consequent];
1584
-
1585
- if (
1586
- consequent_body.length === 1 &&
1587
- consequent_body[0].type === 'ReturnStatement' &&
1588
- !node.alternate
1589
- ) {
1590
- node.metadata.lone_return = true;
1591
- }
1592
-
1593
- if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1594
- error(
1595
- 'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
1596
- context.state.analysis.module.filename,
1597
- node.consequent,
1598
- context.state.loose ? context.state.analysis.errors : undefined,
1599
- context.state.analysis.comments,
1600
- );
1601
- }
1602
-
1603
- if (node.alternate) {
1604
- const saved_has_return = node.metadata.has_return;
1605
- const saved_returns = node.metadata.returns;
1606
- node.metadata.has_template = false;
1607
- node.metadata.has_await = false;
1608
- node.metadata.has_throw = false;
1609
- context.visit(node.alternate, context.state);
1610
-
1611
- if (!node.metadata.has_template && !node.metadata.has_return && !node.metadata.has_throw) {
1612
- error(
1613
- 'Component if statements must contain a template in their "else" body. Move the if statement into an effect if it does not render anything.',
1614
- context.state.analysis.module.filename,
1615
- node.alternate,
1616
- context.state.loose ? context.state.analysis.errors : undefined,
1617
- context.state.analysis.comments,
1618
- );
1619
- }
1620
-
1621
- if (saved_has_return) {
1622
- node.metadata.has_return = true;
1623
- if (saved_returns) {
1624
- node.metadata.returns = [...saved_returns, ...(node.metadata.returns || [])];
1625
- }
1626
- }
1627
- }
1628
- },
1629
-
1630
- ReturnStatement(node, context) {
1631
- const parent = context.path.at(-1);
1632
-
1633
- if (!is_inside_component(context)) {
1634
- if (parent?.type === 'Program') {
1635
- error_return_keyword(
1636
- node,
1637
- context,
1638
- 'Return statements are not allowed at the top level of a module.',
1639
- );
1640
- }
1641
-
1642
- return context.next();
1643
- }
1644
-
1645
- if (node.argument !== null) {
1646
- error_return_keyword(
1647
- node,
1648
- context,
1649
- 'Return statements inside components cannot have a return value.',
1650
- );
1651
- }
1652
-
1653
- for (let i = context.path.length - 1; i >= 0; i--) {
1654
- const ancestor = context.path[i];
1655
-
1656
- if (
1657
- ancestor.type === 'Component' ||
1658
- ancestor.type === 'FunctionExpression' ||
1659
- ancestor.type === 'ArrowFunctionExpression' ||
1660
- ancestor.type === 'FunctionDeclaration'
1661
- ) {
1662
- break;
1663
- }
1664
-
1665
- if (
1666
- ancestor.type === 'IfStatement' &&
1667
- /** @type {AST.TrackedNode} */ (ancestor.test).tracked
1668
- ) {
1669
- node.metadata.is_reactive = true;
1670
- }
1671
-
1672
- if (!ancestor.metadata.returns) {
1673
- ancestor.metadata.returns = [];
1674
- }
1675
- ancestor.metadata.returns.push(node);
1676
- ancestor.metadata.has_return = true;
1677
- }
1678
- },
1679
-
1680
- ThrowStatement(node, context) {
1681
- if (!is_inside_component(context)) {
1682
- return context.next();
1683
- }
1684
-
1685
- for (let i = context.path.length - 1; i >= 0; i--) {
1686
- const ancestor = context.path[i];
1687
-
1688
- if (
1689
- ancestor.type === 'Component' ||
1690
- ancestor.type === 'FunctionExpression' ||
1691
- ancestor.type === 'ArrowFunctionExpression' ||
1692
- ancestor.type === 'FunctionDeclaration'
1693
- ) {
1694
- break;
1695
- }
1696
-
1697
- if (ancestor.type === 'IfStatement') {
1698
- if (!ancestor.metadata.has_throw) {
1699
- ancestor.metadata.has_throw = true;
1700
- }
1701
- }
1702
- }
1703
-
1704
- context.next();
1705
- },
1706
-
1707
- TryStatement(node, context) {
1708
- const { state } = context;
1709
- if (!is_inside_component(context)) {
1710
- return context.next();
1711
- }
1712
-
1713
- if (node.pending) {
1714
- // Try/pending blocks indicate async operations
1715
- if (state.metadata?.await === false) {
1716
- state.metadata.await = true;
1717
- }
1718
-
1719
- node.metadata = {
1720
- ...node.metadata,
1721
- has_template: false,
1722
- };
1723
-
1724
- context.visit(node.block, state);
1725
-
1726
- if (!node.metadata.has_template) {
1727
- error(
1728
- 'Component try statements must contain a template in their main body. Move the try statement into an effect if it does not render anything.',
1729
- state.analysis.module.filename,
1730
- node.block,
1731
- context.state.loose ? context.state.analysis.errors : undefined,
1732
- context.state.analysis.comments,
1733
- );
1734
- }
1735
-
1736
- node.metadata = {
1737
- ...node.metadata,
1738
- has_template: false,
1739
- };
1740
-
1741
- context.visit(node.pending, state);
1742
-
1743
- if (!node.metadata.has_template) {
1744
- error(
1745
- 'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
1746
- state.analysis.module.filename,
1747
- node.pending,
1748
- context.state.loose ? context.state.analysis.errors : undefined,
1749
- context.state.analysis.comments,
1750
- );
1751
- }
1752
- }
1753
-
1754
- if (node.handler) {
1755
- context.visit(node.handler, state);
1756
- }
1757
-
1758
- if (node.finalizer) {
1759
- context.visit(node.finalizer, state);
1760
- }
1761
- },
1762
-
1763
- ForInStatement(node, context) {
1764
- if (is_inside_component(context)) {
1765
- // TODO: it's a fatal error for now but
1766
- // we could implement the for in loop for the ts mode only to make it a usage error
1767
- error(
1768
- 'For...in loops are not supported in components. Use for...of instead.',
1769
- context.state.analysis.module.filename,
1770
- node,
1771
- );
1772
- }
1773
-
1774
- context.next();
1775
- },
1776
-
1777
- WhileStatement(node, context) {
1778
- if (is_inside_component(context)) {
1779
- error(
1780
- 'While loops are not supported in components. Move the while loop into a function.',
1781
- context.state.analysis.module.filename,
1782
- node,
1783
- );
1784
- }
1785
-
1786
- context.next();
1787
- },
1788
-
1789
- DoWhileStatement(node, context) {
1790
- if (is_inside_component(context)) {
1791
- error(
1792
- 'Do...while loops are not supported in components. Move the do...while loop into a function.',
1793
- context.state.analysis.module.filename,
1794
- node,
1795
- );
1796
- }
1797
-
1798
- context.next();
1799
- },
1800
-
1801
- JSXElement(node, context) {
1802
- const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1803
-
1804
- if (inside_tsx_compat) {
1805
- return context.next();
1806
- }
1807
- // TODO: could compile it as something to avoid a fatal error
1808
- error(
1809
- 'Elements cannot be used as generic expressions, only as statements within a component',
1810
- context.state.analysis.module.filename,
1811
- node,
1812
- );
1813
- },
1814
-
1815
- Tsx(_, context) {
1816
- mark_control_flow_has_template(context.path);
1817
- return context.next();
1818
- },
1819
-
1820
- TsxCompat(node, context) {
1821
- mark_control_flow_has_template(context.path);
1822
-
1823
- const configured_compat_kinds = context.state.configured_compat_kinds;
1824
- if (configured_compat_kinds !== undefined && !configured_compat_kinds.has(node.kind)) {
1825
- error(
1826
- `<tsx:${node.kind}> requires "${node.kind}" compat to be configured in ripple.config.ts.`,
1827
- context.state.analysis.module.filename,
1828
- node,
1829
- context.state.loose ? context.state.analysis.errors : undefined,
1830
- context.state.analysis.comments,
1831
- );
1832
- }
1833
-
1834
- return context.next();
1835
- },
1836
-
1837
- Element(node, context) {
1838
- if (!is_inside_component(context)) {
1839
- error(
1840
- 'Elements cannot be used outside of components',
1841
- context.state.analysis.module.filename,
1842
- node,
1843
- );
1844
- }
1845
-
1846
- const { state, visit, path } = context;
1847
- const is_dom_element = is_element_dom_element(node);
1848
- /** @type {Set<AST.Identifier>} */
1849
- const attribute_names = new Set();
1850
-
1851
- mark_control_flow_has_template(path);
1852
-
1853
- if (
1854
- !is_dom_element &&
1855
- is_children_template_expression(/** @type {AST.Expression} */ (node.id), context)
1856
- ) {
1857
- error(
1858
- '`children` cannot be rendered as a component. Render it with `{children}` or `{props.children}` instead.',
1859
- state.analysis.module.filename,
1860
- node.id,
1861
- context.state.loose ? context.state.analysis.errors : undefined,
1862
- context.state.analysis.comments,
1863
- );
1864
- }
1865
-
1866
- validate_nesting(node, context);
1867
-
1868
- // Store capitalized name for dynamic components/elements
1869
- // TODO: this is not quite right as the node.id could be a member expression
1870
- // so, we'd need to identify dynamic based on that too
1871
- // However, we're going to get rid of capitalization in favor of jsx()
1872
- // so, this will be need to be redone.
1873
- if (node.id.type === 'Identifier' && node.id.tracked) {
1874
- const source_name = node.id.name;
1875
- const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
1876
- node.metadata.ts_name = capitalized_name;
1877
- node.metadata.source_name = source_name;
1878
-
1879
- // Mark the binding as a dynamic component so we can capitalize it everywhere
1880
- const binding = context.state.scope.get(source_name);
1881
- if (binding) {
1882
- if (!binding.metadata) {
1883
- binding.metadata = {};
1884
- }
1885
- binding.metadata.is_dynamic_component = true;
1886
- }
1887
-
1888
- if (!is_dom_element && state.elements) {
1889
- state.elements.push(node);
1890
- // Mark dynamic elements as scoped by default since we can't match CSS at compile time
1891
- if (state.component?.css) {
1892
- node.metadata.scoped = true;
1893
- }
1894
- }
1895
- }
1896
-
1897
- if (is_dom_element) {
1898
- if (/** @type {AST.Identifier} */ (node.id).name === 'head') {
1899
- // head validation
1900
- if (node.attributes.length > 0) {
1901
- // TODO: could transform attributes as something, e.g. Text Node, and avoid a fatal error
1902
- error('<head> cannot have any attributes', state.analysis.module.filename, node);
1903
- }
1904
- if (node.children.length === 0) {
1905
- // TODO: could transform children as something, e.g. Text Node, and avoid a fatal error
1906
- error('<head> must have children', state.analysis.module.filename, node);
1907
- }
1908
-
1909
- for (const child of node.children) {
1910
- context.visit(child, { ...state, inside_head: true });
1911
- }
1912
-
1913
- return;
1914
- }
1915
- if (state.inside_head) {
1916
- if (/** @type {AST.Identifier} */ (node.id).name === 'title') {
1917
- const children = normalize_children(node.children, context);
1918
-
1919
- if (
1920
- children.length !== 1 ||
1921
- (children[0].type !== 'RippleExpression' && children[0].type !== 'Text')
1922
- ) {
1923
- // TODO: could transform children as something, e.g. Text Node, and avoid a fatal error
1924
- error(
1925
- '<title> must have only contain text nodes',
1926
- state.analysis.module.filename,
1927
- node,
1928
- );
1929
- }
1930
- }
1931
-
1932
- // check for invalid elements in head
1933
- if (!valid_in_head.has(/** @type {AST.Identifier} */ (node.id).name)) {
1934
- // TODO: could transform invalid elements as something, e.g. Text Node, and avoid a fatal error
1935
- error(
1936
- `<${/** @type {AST.Identifier} */ (node.id).name}> cannot be used in <head>`,
1937
- state.analysis.module.filename,
1938
- node,
1939
- );
1940
- }
1941
- } else {
1942
- if (/** @type {AST.Identifier} */ (node.id).name === 'script') {
1943
- const err_msg = '<script> cannot be used outside of <head>.';
1944
- error(
1945
- err_msg,
1946
- state.analysis.module.filename,
1947
- node.openingElement,
1948
- state.loose ? state.analysis.errors : undefined,
1949
- );
1950
-
1951
- if (node.closingElement) {
1952
- error(
1953
- err_msg,
1954
- state.analysis.module.filename,
1955
- node.closingElement,
1956
- state.loose ? state.analysis.errors : undefined,
1957
- );
1958
- }
1959
- }
1960
- }
1961
-
1962
- const is_void = is_void_element(/** @type {AST.Identifier} */ (node.id).name);
1963
-
1964
- if (state.elements) {
1965
- state.elements.push(node);
1966
- }
1967
-
1968
- for (const attr of node.attributes) {
1969
- if (attr.type === 'Attribute') {
1970
- if (attr.value && attr.value.type === 'JSXEmptyExpression') {
1971
- const value = /** @type {ESTreeJSX.JSXEmptyExpression & AST.NodeWithLocation} */ (
1972
- attr.value
1973
- );
1974
- error(
1975
- 'attributes must only be assigned a non-empty expression',
1976
- state.analysis.module.filename,
1977
- {
1978
- ...value,
1979
- start: value.start - 1,
1980
- end: value.end + 1,
1981
- loc: {
1982
- start: {
1983
- line: value.loc.start.line,
1984
- column: value.loc.start.column - 1,
1985
- },
1986
- end: {
1987
- line: value.loc.end.line,
1988
- column: value.loc.end.column + 1,
1989
- },
1990
- },
1991
- },
1992
- context.state.loose ? context.state.analysis.errors : undefined,
1993
- context.state.analysis.comments,
1994
- );
1995
- }
1996
- if (attr.name.type === 'Identifier') {
1997
- attribute_names.add(attr.name);
1998
-
1999
- if (attr.name.name === 'key') {
2000
- error(
2001
- 'The `key` attribute is not a thing in Ripple, and cannot be used on DOM elements. If you are using a for loop, then use the `for (let item of items; key item.id)` syntax.',
2002
- state.analysis.module.filename,
2003
- attr,
2004
- context.state.loose ? context.state.analysis.errors : undefined,
2005
- context.state.analysis.comments,
2006
- );
2007
- }
2008
-
2009
- if (
2010
- attr.value &&
2011
- attr.value.type === 'MemberExpression' &&
2012
- attr.value.object.type === 'StyleIdentifier'
2013
- ) {
2014
- error(
2015
- '`#style` cannot be used directly on DOM elements. Pass the class to a child component instead.',
2016
- state.analysis.module.filename,
2017
- attr.value.object,
2018
- context.state.loose ? context.state.analysis.errors : undefined,
2019
- context.state.analysis.comments,
2020
- );
2021
- }
2022
-
2023
- if (is_event_attribute(attr.name.name)) {
2024
- const handler = visit(/** @type {AST.Expression} */ (attr.value), state);
2025
- const is_delegated = is_delegated_event(attr.name.name, handler, context);
2026
-
2027
- if (is_delegated) {
2028
- if (attr.metadata === undefined) {
2029
- attr.metadata = { path: [...path] };
2030
- }
2031
-
2032
- attr.metadata.delegated = is_delegated;
2033
- }
2034
- } else if (attr.value !== null) {
2035
- visit(attr.value, state);
2036
- }
2037
- }
2038
- }
2039
- }
2040
-
2041
- if (is_void && node.children.length > 0) {
2042
- error(
2043
- `The <${/** @type {AST.Identifier} */ (node.id).name}> element is a void element and cannot have children`,
2044
- state.analysis.module.filename,
2045
- node,
2046
- context.state.loose ? context.state.analysis.errors : undefined,
2047
- context.state.analysis.comments,
2048
- );
2049
- }
2050
- } else {
2051
- for (const attr of node.attributes) {
2052
- if (attr.type === 'Attribute') {
2053
- if (attr.name.type === 'Identifier') {
2054
- attribute_names.add(attr.name);
2055
- }
2056
- if (attr.value !== null) {
2057
- visit(attr.value, state);
2058
- }
2059
- } else if (attr.type === 'SpreadAttribute') {
2060
- visit(attr.argument, state);
2061
- } else if (attr.type === 'RefAttribute') {
2062
- visit(attr.argument, state);
2063
- }
2064
- }
2065
- /** @type {(AST.Node | AST.Expression)[]} */
2066
- let implicit_children = [];
2067
-
2068
- for (const child of node.children) {
2069
- if (child.type === 'Component') {
2070
- error(
2071
- 'Component declarations cannot be used inside composite component children. Pass them as explicit props on the template element instead.',
2072
- state.analysis.module.filename,
2073
- child.id || child,
2074
- context.state.loose ? context.state.analysis.errors : undefined,
2075
- context.state.analysis.comments,
2076
- );
2077
- } else if (child.type !== 'EmptyStatement') {
2078
- implicit_children.push(
2079
- child.type === 'RippleExpression' || child.type === 'Text' || child.type === 'Html'
2080
- ? child.expression
2081
- : child,
2082
- );
2083
- }
2084
- }
2085
- }
2086
-
2087
- // Validation
2088
- for (const attribute of attribute_names) {
2089
- const name = attribute.name;
2090
- if (name === 'children') {
2091
- if (is_dom_element) {
2092
- error(
2093
- 'Cannot have a `children` prop on an element',
2094
- state.analysis.module.filename,
2095
- attribute,
2096
- context.state.loose ? context.state.analysis.errors : undefined,
2097
- context.state.analysis.comments,
2098
- );
2099
- }
2100
- }
2101
- }
2102
-
2103
- return {
2104
- ...node,
2105
- children: node.children.map((child) => visit(child)),
2106
- };
2107
- },
2108
-
2109
- RippleExpression(node, context) {
2110
- mark_control_flow_has_template(context.path);
2111
-
2112
- if (expression_has_side_effects(node.expression)) {
2113
- error(
2114
- 'Template expressions must not contain side effects.',
2115
- context.state.analysis.module.filename,
2116
- node.expression,
2117
- context.state.loose ? context.state.analysis.errors : undefined,
2118
- context.state.analysis.comments,
2119
- );
2120
- }
2121
-
2122
- context.next();
2123
- },
2124
-
2125
- Text(node, context) {
2126
- mark_control_flow_has_template(context.path);
2127
-
2128
- if (is_children_template_expression(/** @type {AST.Expression} */ (node.expression), context)) {
2129
- error(
2130
- '`children` cannot be rendered using explicit text interpolation. Use `{children}` or `{props.children}` instead.',
2131
- context.state.analysis.module.filename,
2132
- node.expression,
2133
- context.state.loose ? context.state.analysis.errors : undefined,
2134
- context.state.analysis.comments,
2135
- );
2136
- }
2137
-
2138
- context.next();
2139
- },
2140
-
2141
- AwaitExpression(node, context) {
2142
- const parent_block = get_parent_block_node(context);
2143
-
2144
- if (is_inside_component(context)) {
2145
- if (context.state.metadata?.await === false) {
2146
- context.state.metadata.await = true;
2147
- }
2148
-
2149
- if (
2150
- parent_block !== null &&
2151
- parent_block?.type !== 'Component' &&
2152
- !context.state.ancestor_server_block &&
2153
- !(
2154
- parent_block.type === 'TryStatement' &&
2155
- parent_block.pending &&
2156
- is_inside_try_block(parent_block, context)
2157
- )
2158
- ) {
2159
- // we want the error to live on the `await` keyword vs the whole expression
2160
- const adjusted_node /** @type {AST.AwaitExpression} */ = {
2161
- ...node,
2162
- end: /** @type {AST.NodeWithLocation} */ (node).start + 'await'.length,
2163
- };
2164
- error(
2165
- '`await` is not allowed in client-side control-flow statements',
2166
- context.state.analysis.module.filename,
2167
- adjusted_node,
2168
- context.state.loose ? context.state.analysis.errors : undefined,
2169
- context.state.analysis.comments,
2170
- );
2171
- }
2172
- }
2173
-
2174
- if (parent_block) {
2175
- if (!parent_block.metadata) {
2176
- parent_block.metadata = { path: [...context.path] };
2177
- }
2178
- parent_block.metadata.has_await = true;
2179
- }
2180
-
2181
- context.next();
2182
- },
2183
- };
2184
-
2185
- /**
2186
- *
2187
- * @param {AST.Program} ast
2188
- * @param {string} filename
2189
- * @param {AnalyzeOptions} options
2190
- * @returns {AnalysisResult}
2191
- */
2192
- export function analyze(ast, filename, options = {}) {
2193
- const scope_root = new ScopeRoot();
2194
- const errors = options.errors ?? [];
2195
- const comments = options.comments ?? [];
2196
- const loose = options.loose ?? false;
2197
-
2198
- const { scope, scopes } = create_scopes(ast, scope_root, null, {
2199
- loose,
2200
- errors,
2201
- filename,
2202
- comments,
2203
- });
2204
-
2205
- const analysis = /** @type {AnalysisResult} */ ({
2206
- module: { ast, scope, scopes, filename },
2207
- ast,
2208
- scope,
2209
- scopes,
2210
- component_metadata: [],
2211
- metadata: {
2212
- serverIdentifierPresent: false,
2213
- },
2214
- errors,
2215
- comments,
2216
- });
2217
-
2218
- walk(
2219
- ast,
2220
- /** @type {AnalysisState} */
2221
- {
2222
- scope,
2223
- scopes,
2224
- analysis,
2225
- inside_head: false,
2226
- ancestor_server_block: undefined,
2227
- to_ts: options.to_ts ?? false,
2228
- loose,
2229
- configured_compat_kinds:
2230
- options.compat_kinds === undefined ? undefined : new Set(options.compat_kinds),
2231
- metadata: {},
2232
- mode: options.mode,
2233
- },
2234
- visitors,
2235
- );
2236
-
2237
- return analysis;
2238
- }