@tsrx/solid 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Solid compiler built on @tsrx/core",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.4",
6
+ "version": "0.1.7",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -24,9 +24,9 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "esrap": "^2.1.0",
27
+ "esrap": "^2.2.7",
28
28
  "zimmerframe": "^1.1.2",
29
- "@tsrx/core": "0.1.4"
29
+ "@tsrx/core": "0.1.7"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "solid-js": ">=1.8 || >=2.0.0-beta"
package/src/index.js CHANGED
@@ -65,6 +65,7 @@ export function compile_to_volar_mappings(source, filename, options) {
65
65
  const transformed = transform(ast, source, filename, {
66
66
  collect: true,
67
67
  loose: !!options?.loose,
68
+ moduleScopedHookComponents: false,
68
69
  typeOnly: true,
69
70
  errors,
70
71
  comments,
package/src/transform.js CHANGED
@@ -11,9 +11,7 @@ import {
11
11
  setLocation,
12
12
  addJsxSetupDeclaration as add_jsx_setup_declaration,
13
13
  applyLazyTransforms as apply_lazy_transforms,
14
- collectLazyBindingsFromComponent as collect_lazy_bindings_from_component,
15
14
  extractJsxSetupDeclarations as extract_jsx_setup_declarations,
16
- replaceLazyParams as replace_lazy_params,
17
15
  rewriteLoopContinuesToBareReturns as rewrite_loop_continues_to_bare_returns,
18
16
  isRefPropExpression as is_ref_prop_expression,
19
17
  isInterleavedBody as is_interleaved_body_core,
@@ -27,10 +25,14 @@ import {
27
25
  clone_expression_node,
28
26
  clone_identifier,
29
27
  clone_jsx_name,
28
+ cloneSwitchHelperInvocation as clone_switch_helper_invocation,
29
+ collectParamBindings as collect_param_bindings,
30
+ collectStatementBindings as collect_statement_bindings,
31
+ contains_component_jsx,
30
32
  create_generated_identifier,
31
33
  create_null_literal,
32
- flatten_switch_consequent,
33
34
  get_for_of_iteration_params,
35
+ planSwitchLift as plan_switch_lift,
34
36
  identifier_to_jsx_name,
35
37
  is_bare_render_expression,
36
38
  is_dynamic_element_id,
@@ -104,6 +106,12 @@ const solid_platform = {
104
106
  scanUseServerDirectiveForAwaitWithCustomValidator: false,
105
107
  },
106
108
  hooks: {
109
+ // Hoist to module scope in the client transform —
110
+ // same trade-off as React and Vue, where one definition per helper
111
+ // keeps bundles small and source mappings 1:1. The
112
+ // `compile_to_volar_mappings` entry point opts back out so Volar's
113
+ // type-only output keeps helpers inline against the component body.
114
+ moduleScopedHookComponents: true,
107
115
  initialState: () => ({
108
116
  needs_show: false,
109
117
  needs_for: false,
@@ -113,6 +121,17 @@ const solid_platform = {
113
121
  needs_loading: false,
114
122
  needs_normalize_spread_props: false,
115
123
  }),
124
+ canHoistStaticNode(node) {
125
+ // Solid's reactive runtime doesn't reuse JSX-element identity the
126
+ // way React does, so hoisting `<Component />` references to module
127
+ // level pays no runtime cost — it just creates an extra `const`
128
+ // that aliases a helper invocation (e.g. `App__static1 =
129
+ // <App__StatementBodyHook2 />`). Truly-static DOM trees like
130
+ // `<span>'Hello'</span>` still benefit from being hoisted out of
131
+ // the per-render closure, so we only veto hoisting when the
132
+ // subtree contains a *component* JSX element. Same logic Vue uses.
133
+ return !contains_component_jsx(node);
134
+ },
116
135
  validateComponentAwait: (await_expression, _component, ctx, _requires, source) => {
117
136
  const await_start = get_await_keyword_start(await_expression, source);
118
137
  const adjusted_node = /** @type {any} */ ({
@@ -134,8 +153,8 @@ const solid_platform = {
134
153
  switchStatement: switch_statement_to_jsx_child,
135
154
  tryStatement: try_statement_to_jsx_child,
136
155
  },
137
- componentToFunction: (component, ctx) =>
138
- component_to_function_declaration(component, /** @type {any} */ (ctx)),
156
+ componentToFunction: (component, ctx, helper_state) =>
157
+ component_to_function_declaration(component, /** @type {any} */ (ctx), helper_state),
139
158
  injectImports: (program, ctx) => inject_solid_imports(program, /** @type {any} */ (ctx)),
140
159
  // `transformElementAttributes` is intentionally omitted: the
141
160
  // `transformElement` hook below short-circuits core's element walker
@@ -180,17 +199,41 @@ function get_await_keyword_start(await_node, source) {
180
199
  /**
181
200
  * @param {any} component
182
201
  * @param {TransformContext} transform_context
202
+ * @param {any} [walk_helper_state]
203
+ * The helper_state owned by the walker for this component. Solid's local
204
+ * body lowering happens *after* the walker has restored `helper_state` to
205
+ * the outer scope's value, so we re-install the walker's helper_state here
206
+ * for the duration of the body lowering. That way `to_jsx_child` calls into
207
+ * `switch_statement_to_jsx_child` (and any other lift path that goes
208
+ * through `create_hook_safe_helper`) see the same module-scoped helper
209
+ * bucket the React / Vue paths see, and `moduleScopedHookComponents: true`
210
+ * actually hoists Solid's `<StatementBodyHook/>` helpers out of the
211
+ * component body.
183
212
  * @returns {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression}
184
213
  */
185
- function component_to_function_declaration(component, transform_context) {
214
+ function component_to_function_declaration(component, transform_context, walk_helper_state) {
186
215
  const params = component.params || [];
187
216
  const body = /** @type {any[]} */ (component.body || []);
188
217
 
189
- // In type-only mode the lazy rewrite is skipped so destructuring patterns
190
- // survive into the virtual TSX and TypeScript can flow real types.
191
- const lazy_bindings = transform_context.typeOnly
192
- ? new Map()
193
- : collect_lazy_bindings_from_component(params, body, transform_context);
218
+ const saved_helper_state = transform_context.helper_state;
219
+ if (walk_helper_state) {
220
+ transform_context.helper_state = walk_helper_state;
221
+ }
222
+
223
+ // Re-install the component's body bindings before lowering the body. The
224
+ // walker sets `state.available_bindings` to the component-scope bindings
225
+ // during `next()` and restores them before calling the platform's
226
+ // `componentToFunction` hook — but Solid's body lowering happens *here*,
227
+ // after that restore, so `to_jsx_child` (and any `create_hook_safe_helper`
228
+ // it triggers via `switch_statement_to_jsx_child` / the if / try / for-of
229
+ // lifts) wouldn't otherwise see component-scope locals like `const obj =
230
+ // {...}` and would emit helpers that close over undefined references.
231
+ const saved_bindings = transform_context.available_bindings;
232
+ const body_bindings = collect_param_bindings(params);
233
+ for (const node of body) {
234
+ collect_statement_bindings(node, body_bindings);
235
+ }
236
+ transform_context.available_bindings = body_bindings;
194
237
 
195
238
  // Detect top-level early-return patterns such as `if (cond) { return; }`
196
239
  // and `if (cond) { <p />; return; }`.
@@ -314,75 +357,38 @@ function component_to_function_declaration(component, transform_context) {
314
357
  }
315
358
 
316
359
  if (render_nodes.length > 0) {
317
- statements.push(
318
- /** @type {any} */ ({
319
- type: 'ReturnStatement',
320
- argument: build_return_expression(render_nodes) || {
321
- type: 'Literal',
322
- value: null,
323
- raw: 'null',
324
- metadata: { path: [] },
325
- },
326
- metadata: { path: [] },
327
- }),
328
- );
360
+ statements.push(b.return(build_return_expression(render_nodes) || b.literal(null)));
329
361
  }
330
362
 
331
- const final_params = lazy_bindings.size > 0 ? replace_lazy_params(params) : params;
332
-
333
- const body_block = /** @type {any} */ ({
334
- type: 'BlockStatement',
335
- body: statements,
336
- metadata: { path: [] },
337
- });
338
- const final_body =
339
- lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
363
+ const body_block = b.block(statements);
340
364
 
341
365
  /** @type {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression} */
342
366
  let fn;
343
367
 
344
368
  if (component.id) {
345
- fn = /** @type {any} */ ({
346
- type: 'FunctionDeclaration',
347
- id: component.id,
348
- typeParameters: component.typeParameters,
349
- params: final_params,
350
- body: final_body,
351
- async: false,
352
- generator: false,
353
- metadata: {
354
- path: [],
355
- is_component: true,
356
- },
357
- });
369
+ fn = b.function_declaration(component.id, params, body_block, false, component.typeParameters);
358
370
  } else if (component.metadata?.arrow) {
359
- fn = /** @type {any} */ ({
360
- type: 'ArrowFunctionExpression',
361
- typeParameters: component.typeParameters,
362
- params: final_params,
363
- body: final_body,
364
- async: false,
365
- generator: false,
366
- expression: false,
367
- metadata: {
368
- path: [],
369
- is_component: true,
370
- },
371
- });
371
+ fn = b.arrow(params, body_block, false, component.typeParameters);
372
372
  } else {
373
- fn = /** @type {any} */ ({
374
- type: 'FunctionExpression',
375
- id: null,
376
- typeParameters: component.typeParameters,
377
- params: final_params,
378
- body: final_body,
379
- async: false,
380
- generator: false,
381
- metadata: {
382
- path: [],
383
- is_component: true,
384
- },
385
- });
373
+ fn = b.function(null, params, body_block, false, component.typeParameters);
374
+ }
375
+ fn.metadata.is_component = true;
376
+
377
+ // `preallocate_lazy_ids` stamped `has_lazy_descendants` on the source
378
+ // `Component` node; the freshly-built `fn` shares the same params/body
379
+ // subtree, so propagate the flag so the function-handler's early-return
380
+ // path can fire for non-lazy components.
381
+ if (/** @type {any} */ (component).metadata?.has_lazy_descendants) {
382
+ /** @type {any} */ (fn.metadata).has_lazy_descendants = true;
383
+ }
384
+
385
+ // Apply lazy `&{}` / `&[]` rewrites end-to-end via the function-handler in
386
+ // `apply_lazy_transforms`. Constant-time fast-path for functions whose
387
+ // subtrees contain no lazy patterns (flagged ahead of time by
388
+ // `preallocate_lazy_ids`). In type-only mode the rewrite is skipped so
389
+ // destructuring patterns survive into the virtual TSX.
390
+ if (!transform_context.typeOnly) {
391
+ fn = /** @type {typeof fn} */ (apply_lazy_transforms(fn, new Map()));
386
392
  }
387
393
 
388
394
  if (fn.type === 'FunctionDeclaration' && fn.id) {
@@ -393,6 +399,20 @@ function component_to_function_declaration(component, transform_context) {
393
399
  }
394
400
 
395
401
  setLocation(fn, /** @type {any} */ (component), true);
402
+
403
+ // Restore the outer helper_state and bindings, then surface this
404
+ // component's generated helpers / statics so the downstream Program-level
405
+ // lift can hoist them (`moduleScopedHookComponents: true` registers
406
+ // helpers into `helper_state.helpers`; the React/Vue post-pass reads them
407
+ // off `fn.metadata.generated_helpers`).
408
+ transform_context.helper_state = saved_helper_state;
409
+ transform_context.available_bindings = saved_bindings;
410
+ if (walk_helper_state) {
411
+ const fn_metadata = /** @type {any} */ (fn.metadata);
412
+ fn_metadata.generated_helpers = walk_helper_state.helpers;
413
+ fn_metadata.generated_statics = walk_helper_state.statics;
414
+ }
415
+
396
416
  return fn;
397
417
  }
398
418
 
@@ -528,14 +548,12 @@ function body_to_jsx_child(body_nodes, transform_context) {
528
548
  let capture_index = 0;
529
549
  for (const child of body_nodes) {
530
550
  if (is_bare_return_statement(child)) {
531
- statements.push({
532
- type: 'ReturnStatement',
533
- argument: children.length > 0 ? build_return_expression(children) : create_null_literal(),
534
- metadata: { path: [] },
535
- start: child.start,
536
- end: child.end,
537
- loc: child.loc,
538
- });
551
+ statements.push(
552
+ set_loc(
553
+ b.return(children.length > 0 ? build_return_expression(children) : create_null_literal()),
554
+ child,
555
+ ),
556
+ );
539
557
  children.length = 0;
540
558
  has_terminal_return = true;
541
559
  continue;
@@ -580,27 +598,13 @@ function body_to_jsx_child(body_nodes, transform_context) {
580
598
  const block_body = [...statements];
581
599
  if (children.length > 0 || !has_terminal_return) {
582
600
  block_body.push(
583
- /** @type {any} */ ({
584
- type: 'ReturnStatement',
585
- argument: children.length > 0 ? build_return_expression(children) : create_null_literal(),
586
- metadata: { path: [] },
587
- }),
601
+ b.return(children.length > 0 ? build_return_expression(children) : create_null_literal()),
588
602
  );
589
603
  }
590
604
 
591
- return /** @type {any} */ ({
592
- type: 'ArrowFunctionExpression',
593
- params: [],
594
- body: {
595
- type: 'BlockStatement',
596
- body: block_body,
597
- metadata: { path: [] },
598
- },
599
- async: false,
600
- generator: false,
601
- expression: false,
602
- metadata: { path: [], is_branch_arrow: true },
603
- });
605
+ const arrow = b.arrow([], b.block(block_body));
606
+ /** @type {any} */ (arrow.metadata).is_branch_arrow = true;
607
+ return arrow;
604
608
  }
605
609
 
606
610
  /**
@@ -686,14 +690,7 @@ function loop_body_to_callback_statements(body_nodes, transform_context) {
686
690
  const create_return_statement = (source_node, render_nodes) => {
687
691
  const cloned = render_nodes.map((node) => clone_expression_node(node));
688
692
  const argument = cloned.length > 0 ? build_return_expression(cloned) : create_null_literal();
689
- return {
690
- type: 'ReturnStatement',
691
- argument,
692
- metadata: { path: [] },
693
- start: source_node?.start,
694
- end: source_node?.end,
695
- loc: source_node?.loc,
696
- };
693
+ return set_loc(b.return(argument), source_node);
697
694
  };
698
695
 
699
696
  /** @param {any} source_node */
@@ -719,20 +716,7 @@ function loop_body_to_callback_statements(body_nodes, transform_context) {
719
716
  transform_context,
720
717
  );
721
718
  prepend_render_nodes_to_return_statements(branch_statements, children);
722
- statements.push({
723
- type: 'IfStatement',
724
- test: child.test,
725
- consequent: {
726
- type: 'BlockStatement',
727
- body: branch_statements,
728
- metadata: { path: [] },
729
- },
730
- alternate: null,
731
- metadata: { path: [] },
732
- start: child.start,
733
- end: child.end,
734
- loc: child.loc,
735
- });
719
+ statements.push(set_loc(b.if(child.test, b.block(branch_statements), null), child));
736
720
  continue;
737
721
  }
738
722
 
@@ -953,13 +937,7 @@ function negate_expression(expr) {
953
937
  return clone_expression_node(expr.argument);
954
938
  }
955
939
 
956
- return {
957
- type: 'UnaryExpression',
958
- operator: '!',
959
- prefix: true,
960
- argument: clone_expression_node(expr),
961
- metadata: { path: [] },
962
- };
940
+ return b.unary('!', clone_expression_node(expr));
963
941
  }
964
942
 
965
943
  /**
@@ -971,13 +949,7 @@ function negate_expression(expr) {
971
949
  */
972
950
  function iife_if_arrow(node) {
973
951
  if (!is_branch_arrow(node)) return node;
974
- return {
975
- type: 'CallExpression',
976
- callee: node,
977
- arguments: [],
978
- optional: false,
979
- metadata: { path: [] },
980
- };
952
+ return b.call(node);
981
953
  }
982
954
 
983
955
  /**
@@ -1137,54 +1109,26 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1137
1109
  )
1138
1110
  );
1139
1111
 
1140
- let arrow = /** @type {any} */ ({
1141
- type: 'ArrowFunctionExpression',
1142
- params: loop_params,
1143
- body: null,
1144
- async: false,
1145
- generator: false,
1146
- expression: true,
1147
- metadata: { path: [] },
1148
- });
1112
+ let arrow;
1149
1113
 
1150
1114
  if (body_has_loop_skip(loop_body)) {
1151
- arrow.body = {
1152
- type: 'BlockStatement',
1153
- body: loop_body_to_callback_statements(loop_body, transform_context),
1154
- metadata: { path: [] },
1155
- };
1156
- arrow.expression = false;
1115
+ arrow = b.arrow(
1116
+ loop_params,
1117
+ b.block(loop_body_to_callback_statements(loop_body, transform_context)),
1118
+ );
1157
1119
  } else {
1120
+ // Placeholder body — merge_branch_body_into_arrow replaces it below.
1121
+ arrow = b.arrow(loop_params, b.literal(null));
1158
1122
  arrow = merge_branch_body_into_arrow(arrow, body_to_jsx_child(loop_body, transform_context));
1159
1123
  }
1160
1124
 
1161
- const attributes = [
1162
- {
1163
- type: 'JSXAttribute',
1164
- name: { type: 'JSXIdentifier', name: 'each', metadata: { path: [] } },
1165
- value: to_jsx_expression_container(node.right),
1166
- metadata: { path: [] },
1167
- },
1168
- ];
1125
+ const attributes = [b.jsx_attribute(b.jsx_id('each'), to_jsx_expression_container(node.right))];
1169
1126
 
1170
1127
  if (node.key) {
1171
1128
  const item_param = clone_expression_node(loop_params[0]);
1172
- const keyed_arrow = /** @type {any} */ ({
1173
- type: 'ArrowFunctionExpression',
1174
- params: [item_param],
1175
- body: node.key,
1176
- async: false,
1177
- generator: false,
1178
- expression: true,
1179
- metadata: { path: [] },
1180
- });
1129
+ const keyed_arrow = b.arrow([item_param], node.key);
1181
1130
  attributes.push(
1182
- /** @type {any} */ ({
1183
- type: 'JSXAttribute',
1184
- name: { type: 'JSXIdentifier', name: 'keyed', metadata: { path: [] } },
1185
- value: to_jsx_expression_container(keyed_arrow, node.key),
1186
- metadata: { path: [] },
1187
- }),
1131
+ b.jsx_attribute(b.jsx_id('keyed'), to_jsx_expression_container(keyed_arrow, node.key)),
1188
1132
  );
1189
1133
  }
1190
1134
 
@@ -1197,6 +1141,20 @@ function for_of_statement_to_jsx_child(node, transform_context) {
1197
1141
  * statement with a discriminant `d` and cases `[c1, c2, default]` becomes:
1198
1142
  * <Switch fallback={...default}><Match when={d === c1}>...</Match>...</Switch>
1199
1143
  *
1144
+ * Fall-through across cases reuses the shared `plan_switch_lift` pipeline
1145
+ * from `@tsrx/core`: each duplicated case body is hoisted into a
1146
+ * `StatementBodyHook` helper component that chains into the next helper, and
1147
+ * each `<Match>` body just renders the appropriate helper element. The
1148
+ * client transform hoists those helpers to module scope (Solid's platform
1149
+ * sets `moduleScopedHookComponents: true`); `compile_to_volar_mappings` opts
1150
+ * back out and emits the helpers locally inside the component body so Volar
1151
+ * still sees closure-captured bindings against the component scope.
1152
+ *
1153
+ * When any case is lifted in `typeOnly` mode the helper declarations have to
1154
+ * live somewhere local-scoped — we wrap the whole `<Switch>` in an IIFE that
1155
+ * declares them in order and returns the element. The client transform's
1156
+ * module-scoped helpers leave that IIFE empty, so we skip the wrapper.
1157
+ *
1200
1158
  * @param {any} node
1201
1159
  * @param {TransformContext} transform_context
1202
1160
  * @returns {any}
@@ -1205,20 +1163,51 @@ function switch_statement_to_jsx_child(node, transform_context) {
1205
1163
  transform_context.needs_switch = true;
1206
1164
  transform_context.needs_match = true;
1207
1165
 
1166
+ const { case_info, case_helpers, find_next_helper_after, setup_statements } = plan_switch_lift(
1167
+ node,
1168
+ transform_context,
1169
+ );
1170
+
1208
1171
  /** @type {any} */
1209
1172
  let fallback = null;
1210
- const match_children = [];
1211
-
1212
- for (const switch_case of node.cases) {
1213
- const consequent = flatten_switch_consequent(switch_case.consequent || []);
1214
- const body = [];
1215
- for (const child of consequent) {
1216
- if (child.type === 'BreakStatement') break;
1217
- body.push(child);
1173
+ /** @type {Array<{ test: any, body_jsx: any }>} */
1174
+ const match_entries = [];
1175
+
1176
+ for (let i = 0; i < node.cases.length; i++) {
1177
+ const original_case = node.cases[i];
1178
+ const info = case_info[i];
1179
+ const helper = case_helpers[i];
1180
+
1181
+ /** @type {any} */
1182
+ let body_jsx;
1183
+ if (helper) {
1184
+ // Lifted case: render the helper element directly. Use the
1185
+ // original `component_element` (not a clone) for this — its
1186
+ // definition's `loc` is what the case position should map to.
1187
+ body_jsx = helper.component_element;
1188
+ } else if (info.own_body.length === 0) {
1189
+ // Empty case in the source. If a downstream chain exists (alias
1190
+ // pattern like `case 1: case 2: body; break;`), the `<Match>` for
1191
+ // the empty label still has to render that downstream body —
1192
+ // Solid's `<Match>` arms are exclusive, so JS fall-through can't
1193
+ // rescue us here.
1194
+ const next_helper = find_next_helper_after(i);
1195
+ body_jsx = next_helper ? clone_switch_helper_invocation(next_helper) : create_null_literal();
1196
+ } else {
1197
+ // Inline case body: process JSX/non-JSX statements just like Solid
1198
+ // does for any other branch body, then append the chain helper if
1199
+ // this case falls through with no terminator.
1200
+ const inline_body = [...info.own_body];
1201
+ if (!info.has_terminator) {
1202
+ const next_helper = find_next_helper_after(i);
1203
+ if (next_helper) {
1204
+ inline_body.push(clone_switch_helper_invocation(next_helper));
1205
+ }
1206
+ }
1207
+ body_jsx = body_to_jsx_child(inline_body, transform_context);
1218
1208
  }
1219
1209
 
1220
- const body_jsx = body_to_jsx_child(body, transform_context);
1221
- if (switch_case.test === null) {
1210
+ if (original_case.test === null) {
1222
1211
  fallback = body_jsx;
1223
1212
  continue;
1224
1213
  }
@@ -1226,31 +1215,29 @@ function switch_statement_to_jsx_child(node, transform_context) {
1226
1215
  // Clone the discriminant per-case: every generated `<Match when={d === caseN}>`
1227
1216
  // would otherwise share the same AST node reference, so a downstream pass
1228
1217
  // (lazy transforms, printer metadata, source-map annotation) mutating it on
1229
- // one case would corrupt the others.
1230
- const test = /** @type {any} */ ({
1231
- type: 'BinaryExpression',
1232
- operator: '===',
1233
- left: clone_expression_node(node.discriminant),
1234
- right: switch_case.test,
1235
- metadata: { path: [] },
1236
- });
1218
+ // one case would corrupt the others. The right operand (`caseN`) is the
1219
+ // original source `test` node unique per case, so we keep its real loc
1220
+ // for editor IntelliSense and don't clone it.
1221
+ const test = b.binary('===', clone_expression_node(node.discriminant), original_case.test);
1237
1222
 
1238
- match_children.push(
1239
- create_jsx_element(
1240
- 'Match',
1241
- [
1242
- {
1243
- type: 'JSXAttribute',
1244
- name: { type: 'JSXIdentifier', name: 'when', metadata: { path: [] } },
1245
- value: to_jsx_expression_container(test),
1246
- metadata: { path: [] },
1247
- },
1248
- ],
1249
- [jsx_child_wrap(to_function_child(body_jsx))],
1250
- ),
1251
- );
1223
+ match_entries.push({ test, body_jsx });
1252
1224
  }
1253
1225
 
1226
+ const match_children = match_entries.map(({ test, body_jsx }) =>
1227
+ create_jsx_element(
1228
+ 'Match',
1229
+ [
1230
+ {
1231
+ type: 'JSXAttribute',
1232
+ name: { type: 'JSXIdentifier', name: 'when', metadata: { path: [] } },
1233
+ value: to_jsx_expression_container(test),
1234
+ metadata: { path: [] },
1235
+ },
1236
+ ],
1237
+ [jsx_child_wrap(to_function_child(body_jsx))],
1238
+ ),
1239
+ );
1240
+
1254
1241
  const attributes =
1255
1242
  fallback !== null
1256
1243
  ? [
@@ -1263,7 +1250,17 @@ function switch_statement_to_jsx_child(node, transform_context) {
1263
1250
  ]
1264
1251
  : [];
1265
1252
 
1266
- return create_jsx_element('Switch', attributes, match_children);
1253
+ const switch_element = create_jsx_element('Switch', attributes, match_children);
1254
+
1255
+ if (setup_statements.length === 0) {
1256
+ return switch_element;
1257
+ }
1258
+
1259
+ // Local-scoped helpers (typeOnly mode): wrap the <Switch> in an IIFE that
1260
+ // declares the helpers in source order and returns the element.
1261
+ return to_jsx_expression_container(
1262
+ b.call(b.arrow([], b.block([...setup_statements, b.return(switch_element)]))),
1263
+ );
1267
1264
  }
1268
1265
 
1269
1266
  /**
@@ -1336,28 +1333,13 @@ function try_statement_to_jsx_child(node, transform_context) {
1336
1333
  const catch_jsx = body_to_jsx_child(catch_body_nodes, transform_context);
1337
1334
 
1338
1335
  const fallback_fn = merge_branch_body_into_arrow(
1339
- /** @type {any} */ ({
1340
- type: 'ArrowFunctionExpression',
1341
- params: catch_params,
1342
- body: null,
1343
- async: false,
1344
- generator: false,
1345
- expression: true,
1346
- metadata: { path: [] },
1347
- }),
1336
+ b.arrow(catch_params, b.literal(null)),
1348
1337
  catch_jsx,
1349
1338
  );
1350
1339
 
1351
1340
  result = create_jsx_element(
1352
1341
  'Errored',
1353
- [
1354
- {
1355
- type: 'JSXAttribute',
1356
- name: { type: 'JSXIdentifier', name: 'fallback', metadata: { path: [] } },
1357
- value: to_jsx_expression_container(fallback_fn),
1358
- metadata: { path: [] },
1359
- },
1360
- ],
1342
+ [b.jsx_attribute(b.jsx_id('fallback'), to_jsx_expression_container(fallback_fn))],
1361
1343
  [result],
1362
1344
  );
1363
1345
  }
@@ -1424,47 +1406,16 @@ function inject_solid_imports(program, transform_context) {
1424
1406
  const specifiers = [];
1425
1407
 
1426
1408
  if (transform_context.needs_ref_prop) {
1427
- specifiers.push({
1428
- type: 'ImportSpecifier',
1429
- imported: { type: 'Identifier', name: 'create_ref_prop', metadata: { path: [] } },
1430
- local: {
1431
- type: 'Identifier',
1432
- name: CREATE_REF_PROP_INTERNAL_NAME,
1433
- metadata: { path: [] },
1434
- },
1435
- metadata: { path: [] },
1436
- });
1409
+ specifiers.push(b.import_specifier('create_ref_prop', CREATE_REF_PROP_INTERNAL_NAME));
1437
1410
  }
1438
1411
 
1439
1412
  if (transform_context.needs_normalize_spread_props) {
1440
- specifiers.push({
1441
- type: 'ImportSpecifier',
1442
- imported: {
1443
- type: 'Identifier',
1444
- name: 'normalize_spread_props',
1445
- metadata: { path: [] },
1446
- },
1447
- local: {
1448
- type: 'Identifier',
1449
- name: NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
1450
- metadata: { path: [] },
1451
- },
1452
- metadata: { path: [] },
1453
- });
1413
+ specifiers.push(
1414
+ b.import_specifier('normalize_spread_props', NORMALIZE_SPREAD_PROPS_INTERNAL_NAME),
1415
+ );
1454
1416
  }
1455
1417
 
1456
- program.body.unshift(
1457
- /** @type {any} */ ({
1458
- type: 'ImportDeclaration',
1459
- specifiers,
1460
- source: {
1461
- type: 'Literal',
1462
- value: '@tsrx/solid/ref',
1463
- raw: "'@tsrx/solid/ref'",
1464
- },
1465
- metadata: { path: [] },
1466
- }),
1467
- );
1418
+ program.body.unshift(b.import_declaration(specifiers, '@tsrx/solid/ref'));
1468
1419
  }
1469
1420
 
1470
1421
  const needed = [];
@@ -1477,20 +1428,11 @@ function inject_solid_imports(program, transform_context) {
1477
1428
 
1478
1429
  if (needed.length === 0) return;
1479
1430
 
1480
- const specifiers = needed.map((name) => ({
1481
- type: 'ImportSpecifier',
1482
- imported: { type: 'Identifier', name, metadata: { path: [] } },
1483
- local: { type: 'Identifier', name, metadata: { path: [] } },
1484
- metadata: { path: [] },
1485
- }));
1486
-
1487
1431
  program.body.unshift(
1488
- /** @type {any} */ ({
1489
- type: 'ImportDeclaration',
1490
- specifiers,
1491
- source: { type: 'Literal', value: 'solid-js', raw: "'solid-js'" },
1492
- metadata: { path: [] },
1493
- }),
1432
+ b.imports(
1433
+ needed.map((name) => [name, name]),
1434
+ 'solid-js',
1435
+ ),
1494
1436
  );
1495
1437
  }
1496
1438
 
@@ -1876,57 +1818,19 @@ function is_solid_jsx_ref_attribute(attr) {
1876
1818
  */
1877
1819
  function dynamic_element_to_jsx_child(node, transform_context) {
1878
1820
  const dynamic_id = set_loc(create_generated_identifier('DynamicElement'), node.id);
1879
- const alias_declaration = set_loc(
1880
- /** @type {any} */ ({
1881
- type: 'VariableDeclaration',
1882
- kind: 'const',
1883
- declarations: [
1884
- {
1885
- type: 'VariableDeclarator',
1886
- id: dynamic_id,
1887
- init: clone_expression_node(node.id),
1888
- metadata: { path: [] },
1889
- },
1890
- ],
1891
- metadata: { path: [] },
1892
- }),
1893
- node,
1894
- );
1821
+ const alias_declaration = set_loc(b.const(dynamic_id, clone_expression_node(node.id)), node);
1895
1822
  const jsx_element = create_dynamic_jsx_element(dynamic_id, node, transform_context);
1896
1823
 
1897
1824
  return to_jsx_expression_container(
1898
- /** @type {any} */ ({
1899
- type: 'CallExpression',
1900
- callee: {
1901
- type: 'ArrowFunctionExpression',
1902
- params: [],
1903
- body: /** @type {any} */ ({
1904
- type: 'BlockStatement',
1905
- body: [
1906
- alias_declaration,
1907
- {
1908
- type: 'ReturnStatement',
1909
- argument: {
1910
- type: 'ConditionalExpression',
1911
- test: clone_identifier(dynamic_id),
1912
- consequent: jsx_element,
1913
- alternate: create_null_literal(),
1914
- metadata: { path: [] },
1915
- },
1916
- metadata: { path: [] },
1917
- },
1918
- ],
1919
- metadata: { path: [] },
1920
- }),
1921
- async: false,
1922
- generator: false,
1923
- expression: false,
1924
- metadata: { path: [] },
1925
- },
1926
- arguments: [],
1927
- optional: false,
1928
- metadata: { path: [] },
1929
- }),
1825
+ b.call(
1826
+ b.arrow(
1827
+ [],
1828
+ b.block([
1829
+ alias_declaration,
1830
+ b.return(b.conditional(clone_identifier(dynamic_id), jsx_element, create_null_literal())),
1831
+ ]),
1832
+ ),
1833
+ ),
1930
1834
  node,
1931
1835
  );
1932
1836
  }