@tsrx/solid 0.0.7 → 0.0.9

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/transform.js +136 -439
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.0.7",
6
+ "version": "0.0.9",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "esrap": "^2.1.0",
24
24
  "zimmerframe": "^1.1.2",
25
- "@tsrx/core": "0.0.7"
25
+ "@tsrx/core": "0.0.9"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "solid-js": ">=1.8 || >=2.0.0-beta"
package/src/transform.js CHANGED
@@ -1,22 +1,30 @@
1
1
  /** @import * as AST from 'estree' */
2
2
  /** @import * as ESTreeJSX from 'estree-jsx' */
3
3
 
4
- import { walk } from 'zimmerframe';
5
- import { print } from 'esrap';
6
- import tsx from 'esrap/languages/tsx';
7
4
  import {
8
- renderStylesheets,
5
+ createJsxTransform,
9
6
  setLocation,
10
7
  applyLazyTransforms as apply_lazy_transforms,
11
- findFirstTopLevelAwaitInComponentBody as find_first_top_level_await_in_component_body,
12
8
  collectLazyBindingsFromComponent as collect_lazy_bindings_from_component,
13
- preallocateLazyIds as preallocate_lazy_ids,
14
9
  replaceLazyParams as replace_lazy_params,
15
- prepareStylesheetForRender as prepare_stylesheet_for_render,
16
- annotateComponentWithHash as annotate_component_with_hash,
17
10
  isInterleavedBody as is_interleaved_body_core,
18
11
  isCapturableJsxChild as is_capturable_jsx_child,
19
12
  captureJsxChild,
13
+ tsxNodeToJsxExpression as tsx_node_to_jsx_expression,
14
+ // Shared AST builders (truly platform-agnostic utilities).
15
+ clone_expression_node,
16
+ clone_identifier,
17
+ clone_jsx_name,
18
+ create_compile_error,
19
+ create_generated_identifier,
20
+ create_null_literal,
21
+ flatten_switch_consequent,
22
+ get_for_of_iteration_params,
23
+ identifier_to_jsx_name,
24
+ is_dynamic_element_id,
25
+ is_jsx_child,
26
+ set_loc,
27
+ to_text_expression,
20
28
  } from '@tsrx/core';
21
29
 
22
30
  /**
@@ -37,149 +45,75 @@ import {
37
45
  */
38
46
 
39
47
  /**
40
- * Transform a parsed tsrx-solid AST into a TSX module targeting Solid 2.0.
48
+ * Solid platform descriptor consumed by `createJsxTransform`. Everything
49
+ * that diverges from React/Preact is plugged in via `hooks`:
50
+ * - Component-level `await` is rejected outright (no `"use server"` escape).
51
+ * - Control-flow statements become Solid's `<Show>` / `<For>` /
52
+ * `<Switch>/<Match>` / `<Errored>/<Loading>` instead of inline JSX.
53
+ * - `component` declarations run once at setup, with early-return JSX
54
+ * hoisted into a reactive `<Show when={!cond}>`.
55
+ * - Element attributes support composite elements and lift a lone
56
+ * `{text ...}` child into a `textContent` attribute.
57
+ * - `needs_show` / `needs_for` / etc. flags track which runtime
58
+ * primitives must be imported, injected by `inject_solid_imports`.
41
59
  *
42
- * Each `component` declaration becomes a plain `FunctionDeclaration` that
43
- * returns Solid JSX. Control flow statements are rewritten to Solid's
44
- * built-in components (`<Show>`, `<Switch>/<Match>`, `<For>`, `<Errored>`,
45
- * `<Loading>`) so they remain reactive. Per-component `<style>` blocks are
46
- * collected, rendered via `@tsrx/core`'s stylesheet renderer, and returned
47
- * alongside the JS output so a downstream plugin can inject them.
48
- *
49
- * @param {AST.Program} ast
50
- * @param {string} source
51
- * @param {string} [filename]
52
- * @returns {{ ast: AST.Program, code: string, map: any, css: { code: string, hash: string } | null }}
60
+ * @type {import('@tsrx/core/types').JsxPlatform}
53
61
  */
54
- export function transform(ast, source, filename) {
55
- /** @type {any[]} */
56
- const stylesheets = [];
57
-
58
- /** @type {TransformContext} */
59
- const transform_context = {
60
- needs_show: false,
61
- needs_for: false,
62
- needs_switch: false,
63
- needs_match: false,
64
- needs_errored: false,
65
- needs_loading: false,
66
- lazy_next_id: 0,
67
- current_css_hash: null,
68
- };
69
-
70
- preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
71
-
72
- // First pass: collect stylesheets and annotate elements with the component hash.
73
- walk(/** @type {any} */ (ast), transform_context, {
74
- Component(node, { next, state }) {
75
- const as_any = /** @type {any} */ (node);
76
- const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
77
-
78
- if (await_expression) {
79
- const await_start = get_await_keyword_start(await_expression, source);
80
- const adjusted_node = /** @type {any} */ ({
81
- ...await_expression,
82
- start: await_start,
83
- end: await_start + 'await'.length,
84
- });
85
-
86
- throw create_compile_error(
87
- adjusted_node,
88
- '`await` is not allowed inside Solid components.',
89
- );
90
- }
91
-
92
- const css = as_any.css;
93
- if (css) {
94
- stylesheets.push(css);
95
- annotate_component_with_hash(as_any, css.hash);
96
- }
97
- return next(state);
98
- },
99
- });
100
-
101
- // Second pass: transform Components, Elements, Text nodes, Tsx blocks, etc.
102
- const transformed = walk(/** @type {any} */ (ast), transform_context, {
103
- Component(node, { next, state }) {
104
- const as_any = /** @type {any} */ (node);
105
-
106
- const saved_css_hash = state.current_css_hash;
107
- state.current_css_hash = as_any.css ? as_any.css.hash : null;
108
-
109
- const inner = /** @type {any} */ (next() ?? node);
110
-
111
- state.current_css_hash = saved_css_hash;
112
-
113
- return /** @type {any} */ (component_to_function_declaration(inner, state));
114
- },
115
-
116
- Tsx(node, { next }) {
117
- const inner = /** @type {any} */ (next() ?? node);
118
- return /** @type {any} */ (tsx_node_to_jsx_expression(inner));
119
- },
120
-
121
- TsxCompat(node, { next }) {
122
- const inner = /** @type {any} */ (next() ?? node);
123
- return /** @type {any} */ (tsx_compat_node_to_jsx_expression(inner));
124
- },
125
-
126
- Element(node, { next, state }) {
127
- const inner = /** @type {any} */ (next() ?? node);
128
- return /** @type {any} */ (to_jsx_element(inner, state));
129
- },
130
-
131
- // `Text` nodes are lowered by `to_jsx_child` (and the `textContent`
132
- // optimization in `to_jsx_element`) rather than the walker, so the
133
- // parent element still sees a raw `Text` child when it runs and can
134
- // decide whether to hoist it up to an attribute.
135
- TSRXExpression(node, { next }) {
136
- const inner = /** @type {any} */ (next() ?? node);
137
- return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
62
+ const solid_platform = {
63
+ name: 'Solid',
64
+ imports: {
65
+ // Solid doesn't use the React-style Suspense / ErrorBoundary pair.
66
+ // Both fields are here to satisfy the descriptor shape; actual
67
+ // import injection goes through `hooks.injectImports`.
68
+ suspense: 'solid-js',
69
+ errorBoundary: 'solid-js',
70
+ },
71
+ jsx: {
72
+ rewriteClassAttr: false,
73
+ acceptedTsxKinds: ['solid'],
74
+ },
75
+ validation: {
76
+ requireUseServerForAwait: true,
77
+ // Solid's custom validator always rejects component-level await,
78
+ // so directive scanning is redundant work. Keep the fallback flag
79
+ // above true as a safety net if the custom hook is removed.
80
+ scanUseServerDirectiveForAwaitWithCustomValidator: false,
81
+ },
82
+ hooks: {
83
+ initialState: () => ({
84
+ needs_show: false,
85
+ needs_for: false,
86
+ needs_switch: false,
87
+ needs_match: false,
88
+ needs_errored: false,
89
+ needs_loading: false,
90
+ }),
91
+ validateComponentAwait: (await_expression, _component, _ctx, _requires, source) => {
92
+ const await_start = get_await_keyword_start(await_expression, source);
93
+ const adjusted_node = /** @type {any} */ ({
94
+ ...await_expression,
95
+ start: await_start,
96
+ end: await_start + 'await'.length,
97
+ });
98
+ throw create_compile_error(adjusted_node, '`await` is not allowed inside Solid components.');
138
99
  },
139
-
140
- MemberExpression(node, { next, state }) {
141
- const as_any = /** @type {any} */ (node);
142
- if (as_any.object && as_any.object.type === 'StyleIdentifier' && state.current_css_hash) {
143
- const class_name = as_any.computed ? as_any.property.value : as_any.property.name;
144
- const value = `${state.current_css_hash} ${class_name}`;
145
- return /** @type {any} */ ({ type: 'Literal', value, raw: JSON.stringify(value) });
146
- }
147
- return next();
100
+ controlFlow: {
101
+ ifStatement: if_statement_to_jsx_child,
102
+ forOf: for_of_statement_to_jsx_child,
103
+ switchStatement: switch_statement_to_jsx_child,
104
+ tryStatement: try_statement_to_jsx_child,
148
105
  },
149
- });
150
-
151
- inject_solid_imports(/** @type {AST.Program} */ (transformed), transform_context);
152
-
153
- // Apply lazy destructuring transforms to module-level code (top-level function
154
- // declarations, arrow functions, etc.). Component bodies have already been
155
- // transformed inside component_to_function_declaration; this catches plain
156
- // functions outside components and any lazy patterns in module scope.
157
- const final_program = /** @type {any} */ (
158
- apply_lazy_transforms(/** @type {any} */ (transformed), new Map())
159
- );
160
-
161
- const result = print(/** @type {any} */ (final_program), tsx(), {
162
- sourceMapSource: filename,
163
- sourceMapContent: source,
164
- });
165
-
166
- const css =
167
- stylesheets.length > 0
168
- ? {
169
- code: renderStylesheets(
170
- /** @type {any} */ (stylesheets.map(prepare_stylesheet_for_render)),
171
- ),
172
- hash: stylesheets.map((s) => s.hash).join(' '),
173
- }
174
- : null;
175
-
176
- return {
177
- ast: /** @type {AST.Program} */ (final_program),
178
- code: result.code,
179
- map: result.map,
180
- css,
181
- };
182
- }
106
+ componentToFunction: (component, ctx) =>
107
+ component_to_function_declaration(component, /** @type {any} */ (ctx)),
108
+ injectImports: (program, ctx) => inject_solid_imports(program, /** @type {any} */ (ctx)),
109
+ transformElementAttributes: (attrs, ctx, element) =>
110
+ transform_element_attributes(attrs, is_composite_element(element), /** @type {any} */ (ctx)),
111
+ transformElement: (inner, ctx, raw_children) =>
112
+ to_jsx_element(/** @type {any} */ (inner), /** @type {any} */ (ctx), raw_children),
113
+ },
114
+ };
115
+
116
+ export const transform = createJsxTransform(solid_platform);
183
117
 
184
118
  /**
185
119
  * @param {any} await_node
@@ -373,31 +307,6 @@ function component_to_function_declaration(component, transform_context) {
373
307
  // Control flow → Solid JSX components
374
308
  // =====================================================================
375
309
 
376
- /**
377
- * @param {any} node
378
- * @returns {boolean}
379
- */
380
- function is_jsx_child(node) {
381
- if (!node) return false;
382
- const t = node.type;
383
- return (
384
- t === 'JSXElement' ||
385
- t === 'JSXFragment' ||
386
- t === 'JSXExpressionContainer' ||
387
- t === 'JSXText' ||
388
- t === 'Tsx' ||
389
- t === 'TsxCompat' ||
390
- t === 'Element' ||
391
- t === 'Text' ||
392
- t === 'TSRXExpression' ||
393
- t === 'Html' ||
394
- t === 'IfStatement' ||
395
- t === 'ForOfStatement' ||
396
- t === 'SwitchStatement' ||
397
- t === 'TryStatement'
398
- );
399
- }
400
-
401
310
  /**
402
311
  * @param {any} node
403
312
  * @param {TransformContext} transform_context
@@ -407,9 +316,11 @@ function to_jsx_child(node, transform_context) {
407
316
  if (!node) return node;
408
317
  switch (node.type) {
409
318
  case 'Tsx':
410
- return tsx_node_to_jsx_expression(node);
319
+ // We're inside a JSX child position by construction; keep `{expr}`
320
+ // containers wrapped. See helpers.js.
321
+ return tsx_node_to_jsx_expression(node, true);
411
322
  case 'TsxCompat':
412
- return tsx_compat_node_to_jsx_expression(node);
323
+ return tsx_compat_node_to_jsx_expression(node, true);
413
324
  case 'Element':
414
325
  return to_jsx_element(node, transform_context);
415
326
  case 'Text':
@@ -1059,6 +970,9 @@ function create_jsx_element(tag_name, attributes, children) {
1059
970
  };
1060
971
  }
1061
972
 
973
+ const TEMPLATE_FRAGMENT_ERROR =
974
+ 'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
975
+
1062
976
  /**
1063
977
  * Inject `import { Show, For, Switch, Match, Errored, Loading } from 'solid-js'`
1064
978
  * specifiers for whichever control-flow primitives the transform emitted.
@@ -1099,11 +1013,22 @@ function inject_solid_imports(program, transform_context) {
1099
1013
  // =====================================================================
1100
1014
 
1101
1015
  /**
1102
- * @param {any} node
1016
+ * @param {any} node - walker-transformed Element whose `children` have
1017
+ * already had `StyleIdentifier` / `TSRXExpression` / nested `Element`
1018
+ * walker rewrites applied.
1103
1019
  * @param {TransformContext} transform_context
1020
+ * @param {any[]} [pre_walk_children] - optional pre-walk children list
1021
+ * from the `transformElement` hook. Only used to detect the
1022
+ * "single `Text` child" shape for the `textContent` optimization —
1023
+ * once detected we build the attribute from the original `Text.expression`.
1024
+ * The factory's `Text` walker lowers `Text` → `JSXExpressionContainer`, so
1025
+ * without these we'd miss the optimization. For rendering non-textContent
1026
+ * children we keep using `node.children` (walker-transformed), so
1027
+ * `MemberExpression` rewrites on `StyleIdentifier` refs inside children
1028
+ * are preserved.
1104
1029
  * @returns {any}
1105
1030
  */
1106
- function to_jsx_element(node, transform_context) {
1031
+ function to_jsx_element(node, transform_context, pre_walk_children) {
1107
1032
  if (node.type === 'JSXElement') return node;
1108
1033
 
1109
1034
  // `{html expr}` isn't supported on the Solid target — users should reach
@@ -1112,13 +1037,18 @@ function to_jsx_element(node, transform_context) {
1112
1037
  // explicit in their source. Only Ripple has a `{html ...}` primitive.
1113
1038
  // The check runs before the dynamic-element branch so `<@Dyn>{html x}</@Dyn>`
1114
1039
  // fails with the same diagnostic as the static-element case.
1115
- const raw_children = node.children || [];
1116
- if (raw_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
1040
+ const walked_children = node.children || [];
1041
+ const text_optimization_children = pre_walk_children ?? walked_children;
1042
+ if (walked_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
1117
1043
  throw new Error(
1118
1044
  '`{html ...}` is not supported on the Solid target. Use `innerHTML={...}` as an element attribute instead.',
1119
1045
  );
1120
1046
  }
1121
1047
 
1048
+ if (!node.id) {
1049
+ throw create_compile_error(node, TEMPLATE_FRAGMENT_ERROR);
1050
+ }
1051
+
1122
1052
  if (is_dynamic_element_id(node.id)) {
1123
1053
  return dynamic_element_to_jsx_child(node, transform_context);
1124
1054
  }
@@ -1139,16 +1069,20 @@ function to_jsx_element(node, transform_context) {
1139
1069
  // the parent is a host element (composite components receive
1140
1070
  // `textContent` as an opaque prop with no DOM semantics), and when the
1141
1071
  // user hasn't already set `textContent` themselves.
1072
+ //
1073
+ // We check `text_optimization_children` (pre-walk) rather than
1074
+ // `walked_children` because the factory's `Text` walker has already
1075
+ // lowered `Text` → `JSXExpressionContainer`, which wouldn't match.
1142
1076
  let selfClosing = !!node.selfClosing;
1143
1077
  let children;
1144
1078
  if (
1145
1079
  !is_composite &&
1146
- raw_children.length === 1 &&
1147
- raw_children[0] &&
1148
- raw_children[0].type === 'Text' &&
1080
+ text_optimization_children.length === 1 &&
1081
+ text_optimization_children[0] &&
1082
+ text_optimization_children[0].type === 'Text' &&
1149
1083
  !has_text_content_attribute(attributes)
1150
1084
  ) {
1151
- const text_child = raw_children[0];
1085
+ const text_child = text_optimization_children[0];
1152
1086
  attributes.push(
1153
1087
  set_loc(
1154
1088
  /** @type {any} */ ({
@@ -1158,10 +1092,14 @@ function to_jsx_element(node, transform_context) {
1158
1092
  name: 'textContent',
1159
1093
  metadata: { path: [] },
1160
1094
  },
1161
- value: to_jsx_expression_container(
1162
- to_text_expression(text_child.expression, text_child),
1163
- text_child,
1164
- ),
1095
+ // preserves the walker's rewrites on the Text's inner expression
1096
+ value:
1097
+ walked_children[0] && walked_children[0].type === 'JSXExpressionContainer'
1098
+ ? walked_children[0]
1099
+ : to_jsx_expression_container(
1100
+ to_text_expression(text_child.expression, text_child),
1101
+ text_child,
1102
+ ),
1165
1103
  shorthand: false,
1166
1104
  metadata: { path: [] },
1167
1105
  }),
@@ -1171,7 +1109,10 @@ function to_jsx_element(node, transform_context) {
1171
1109
  children = [];
1172
1110
  selfClosing = true;
1173
1111
  } else {
1174
- children = create_element_children(raw_children, transform_context);
1112
+ // Use walker-transformed children so `MemberExpression` /
1113
+ // `StyleIdentifier` rewrites from the factory walker are preserved
1114
+ // in the emitted JSX.
1115
+ children = create_element_children(walked_children, transform_context);
1175
1116
  }
1176
1117
 
1177
1118
  const openingElement = set_loc(
@@ -1189,7 +1130,11 @@ function to_jsx_element(node, transform_context) {
1189
1130
  : set_loc(
1190
1131
  /** @type {any} */ ({
1191
1132
  type: 'JSXClosingElement',
1192
- name: clone_jsx_name(name, node.closingElement || node),
1133
+ // Forward the source *name* (not the JSXClosingElement wrapper)
1134
+ // so `clone_jsx_name` can propagate member-expression sub-part
1135
+ // locations from the closing tag. See the identical fix in
1136
+ // packages/tsrx/src/transform/jsx/index.js.
1137
+ name: clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
1193
1138
  }),
1194
1139
  node.closingElement || node,
1195
1140
  );
@@ -1269,17 +1214,6 @@ function to_jsx_attribute(attr) {
1269
1214
  );
1270
1215
  }
1271
1216
 
1272
- /**
1273
- * @param {any} id
1274
- * @returns {boolean}
1275
- */
1276
- function is_dynamic_element_id(id) {
1277
- if (!id || typeof id !== 'object') return false;
1278
- if (id.type === 'Identifier') return !!id.tracked;
1279
- if (id.type === 'MemberExpression') return is_dynamic_element_id(id.object);
1280
- return false;
1281
- }
1282
-
1283
1217
  /**
1284
1218
  * Detect whether an `Element` node represents a composite component (tag
1285
1219
  * name starts with an uppercase letter, or is a member expression like
@@ -1531,39 +1465,6 @@ function to_jsx_expression_container(expression, source_node = expression) {
1531
1465
  );
1532
1466
  }
1533
1467
 
1534
- /**
1535
- * `{text expr}` → `expr == null ? '' : expr + ''` — coerce to string,
1536
- * matching React's text semantics so booleans/objects render as text.
1537
- *
1538
- * @param {AST.Expression} expression
1539
- * @param {any} [source_node]
1540
- * @returns {AST.Expression}
1541
- */
1542
- function to_text_expression(expression, source_node = expression) {
1543
- return set_loc(
1544
- /** @type {AST.Expression} */ ({
1545
- type: 'ConditionalExpression',
1546
- test: {
1547
- type: 'BinaryExpression',
1548
- operator: '==',
1549
- left: clone_expression_node(expression),
1550
- right: { type: 'Literal', value: null, raw: 'null', metadata: { path: [] } },
1551
- metadata: { path: [] },
1552
- },
1553
- consequent: { type: 'Literal', value: '', raw: "''", metadata: { path: [] } },
1554
- alternate: {
1555
- type: 'BinaryExpression',
1556
- operator: '+',
1557
- left: clone_expression_node(expression),
1558
- right: { type: 'Literal', value: '', raw: "''", metadata: { path: [] } },
1559
- metadata: { path: [] },
1560
- },
1561
- metadata: { path: [] },
1562
- }),
1563
- source_node,
1564
- );
1565
- }
1566
-
1567
1468
  /**
1568
1469
  * @param {any[]} render_nodes
1569
1470
  * @returns {any}
@@ -1591,221 +1492,17 @@ function build_return_expression(render_nodes) {
1591
1492
  );
1592
1493
  }
1593
1494
 
1594
- /**
1595
- * @param {AST.Identifier | AST.MemberExpression | any} id
1596
- * @returns {any}
1597
- */
1598
- function identifier_to_jsx_name(id) {
1599
- if (id.type === 'Identifier') {
1600
- return set_loc(
1601
- /** @type {any} */ ({
1602
- type: 'JSXIdentifier',
1603
- name: id.name,
1604
- metadata: { path: [], is_component: /^[A-Z]/.test(id.name) },
1605
- }),
1606
- id,
1607
- );
1608
- }
1609
- if (id.type === 'MemberExpression') {
1610
- return set_loc(
1611
- /** @type {any} */ ({
1612
- type: 'JSXMemberExpression',
1613
- object: /** @type {any} */ (identifier_to_jsx_name(id.object)),
1614
- property: /** @type {any} */ (identifier_to_jsx_name(id.property)),
1615
- }),
1616
- id,
1617
- );
1618
- }
1619
- return id;
1620
- }
1621
-
1622
- /**
1623
- * @param {any} name
1624
- * @param {any} [source_node]
1625
- * @returns {any}
1626
- */
1627
- function clone_jsx_name(name, source_node = name) {
1628
- if (name.type === 'JSXIdentifier') {
1629
- return set_loc(
1630
- { type: 'JSXIdentifier', name: name.name, metadata: name.metadata || { path: [] } },
1631
- source_node,
1632
- );
1633
- }
1634
- if (name.type === 'JSXMemberExpression') {
1635
- return set_loc(
1636
- {
1637
- type: 'JSXMemberExpression',
1638
- object: clone_jsx_name(name.object, source_node.object || name.object),
1639
- property: clone_jsx_name(name.property, source_node.property || name.property),
1640
- metadata: name.metadata || { path: [] },
1641
- },
1642
- source_node,
1643
- );
1644
- }
1645
- return name;
1646
- }
1647
-
1648
- /**
1649
- * @param {AST.Identifier} identifier
1650
- * @returns {any}
1651
- */
1652
- function clone_identifier(identifier) {
1653
- return set_loc(
1654
- /** @type {any} */ ({
1655
- type: 'Identifier',
1656
- name: identifier.name,
1657
- metadata: { path: [] },
1658
- }),
1659
- identifier,
1660
- );
1661
- }
1662
-
1663
- /**
1664
- * @param {any} node
1665
- * @returns {any}
1666
- */
1667
- function clone_expression_node(node) {
1668
- if (!node || typeof node !== 'object') return node;
1669
- if (Array.isArray(node)) return node.map(clone_expression_node);
1670
- const clone = { ...node };
1671
- for (const key of Object.keys(clone)) {
1672
- // Positional keys are value-shared across nodes and — more importantly —
1673
- // `loc` objects often contain back-references to sub-objects that would
1674
- // blow the stack without a cycle guard. Every other AST traversal in
1675
- // this file skips these; keep them as shallow-copied references.
1676
- if (key === 'loc' || key === 'start' || key === 'end') continue;
1677
- if (key === 'metadata') {
1678
- clone.metadata = clone.metadata ? { ...clone.metadata } : { path: [] };
1679
- continue;
1680
- }
1681
- clone[key] = clone_expression_node(clone[key]);
1682
- }
1683
- return clone;
1684
- }
1685
-
1686
- /**
1687
- * @returns {AST.Literal}
1688
- */
1689
- function create_null_literal() {
1690
- return /** @type {any} */ ({ type: 'Literal', value: null, raw: 'null', metadata: { path: [] } });
1691
- }
1692
-
1693
- /**
1694
- * @template T
1695
- * @param {T} node
1696
- * @param {any} source_node
1697
- * @returns {T}
1698
- */
1699
- function set_loc(node, source_node) {
1700
- /** @type {any} */ (node).metadata ??= { path: [] };
1701
- if (source_node?.loc) {
1702
- return /** @type {T} */ (setLocation(/** @type {any} */ (node), source_node, true));
1703
- }
1704
- return node;
1705
- }
1706
-
1707
- /**
1708
- * @param {any} left
1709
- * @param {any} index
1710
- * @returns {any[]}
1711
- */
1712
- function get_for_of_iteration_params(left, index) {
1713
- const params = [];
1714
- if (left?.type === 'VariableDeclaration') {
1715
- params.push(left.declarations[0]?.id);
1716
- } else {
1717
- params.push(left);
1718
- }
1719
- if (index) params.push(index);
1720
- return params;
1721
- }
1722
-
1723
- /**
1724
- * @param {string} name
1725
- * @returns {any}
1726
- */
1727
- function create_generated_identifier(name) {
1728
- return /** @type {any} */ ({ type: 'Identifier', name, metadata: { path: [] } });
1729
- }
1730
-
1731
- /**
1732
- * @param {any} node
1733
- * @param {string} message
1734
- * @returns {Error & { pos: number, end: number }}
1735
- */
1736
- function create_compile_error(node, message) {
1737
- const error = /** @type {Error & { pos: number, end: number }} */ (new Error(message));
1738
- error.pos = node.start ?? 0;
1739
- error.end = node.end ?? error.pos + 1;
1740
- return error;
1741
- }
1742
-
1743
- /**
1744
- * @param {any[]} consequent
1745
- * @returns {any[]}
1746
- */
1747
- function flatten_switch_consequent(consequent) {
1748
- const result = [];
1749
- for (const node of consequent) {
1750
- if (node.type === 'BlockStatement') result.push(...node.body);
1751
- else result.push(node);
1752
- }
1753
- return result;
1754
- }
1755
-
1756
1495
  /**
1757
1496
  * @param {any} node
1497
+ * @param {boolean} [in_jsx_child]
1758
1498
  * @returns {any}
1759
1499
  */
1760
- function tsx_compat_node_to_jsx_expression(node) {
1500
+ function tsx_compat_node_to_jsx_expression(node, in_jsx_child = false) {
1761
1501
  if (node.kind !== 'solid') {
1762
1502
  throw create_compile_error(
1763
1503
  node,
1764
1504
  `Solid TSRX does not support <tsx:${node.kind}> blocks. Use <tsx> or <tsx:solid>.`,
1765
1505
  );
1766
1506
  }
1767
- return tsx_node_to_jsx_expression(node);
1768
- }
1769
-
1770
- /**
1771
- * `<tsx>...</tsx>` → Solid JSX fragment (or single child if only one).
1772
- *
1773
- * @param {any} node
1774
- * @returns {any}
1775
- */
1776
- function tsx_node_to_jsx_expression(node) {
1777
- const children = (node.children || []).filter(
1778
- (/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim() !== '',
1779
- );
1780
-
1781
- if (children.length === 1 && children[0].type !== 'JSXText') {
1782
- return strip_locations(children[0]);
1783
- }
1784
-
1785
- return strip_locations(
1786
- /** @type {any} */ ({
1787
- type: 'JSXFragment',
1788
- openingFragment: { type: 'JSXOpeningFragment', metadata: { path: [] } },
1789
- closingFragment: { type: 'JSXClosingFragment', metadata: { path: [] } },
1790
- children,
1791
- metadata: { path: [] },
1792
- }),
1793
- );
1794
- }
1795
-
1796
- /**
1797
- * @param {any} node
1798
- * @returns {any}
1799
- */
1800
- function strip_locations(node) {
1801
- if (!node || typeof node !== 'object') return node;
1802
- if (Array.isArray(node)) return node.map(strip_locations);
1803
- delete node.loc;
1804
- delete node.start;
1805
- delete node.end;
1806
- for (const key of Object.keys(node)) {
1807
- if (key === 'metadata') continue;
1808
- node[key] = strip_locations(node[key]);
1809
- }
1810
- return node;
1507
+ return tsx_node_to_jsx_expression(node, in_jsx_child);
1811
1508
  }