@tsrx/core 0.0.20 → 0.0.22
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
|
@@ -250,17 +250,46 @@ export function flatten_switch_consequent(consequent) {
|
|
|
250
250
|
return result;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* @param {AST.Expression | null | undefined} expression
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
function is_static_string_expression(expression) {
|
|
258
|
+
if (!expression) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (expression.type === 'Literal') {
|
|
262
|
+
return typeof expression.value === 'string';
|
|
263
|
+
}
|
|
264
|
+
if (expression.type === 'TemplateLiteral') {
|
|
265
|
+
return expression.expressions.length === 0;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
253
270
|
/**
|
|
254
271
|
* Build `expr == null ? '' : expr + ''` — the text-coerce form used when a
|
|
255
272
|
* Ripple `{expr}` child must render as a string in JSX (React/Preact drop
|
|
256
273
|
* booleans; Solid's default child semantics don't either). Solid uses this
|
|
257
274
|
* via `to_jsx_child`; React/Preact wrap it in a JSXExpressionContainer.
|
|
258
275
|
*
|
|
276
|
+
* When the expression is statically a non-null string at the AST level —
|
|
277
|
+
* a string `Literal` (`"hello"`, `'hello'`) or a `TemplateLiteral` with no
|
|
278
|
+
* interpolations (`` `hello` ``) — the coercion is provably a no-op and
|
|
279
|
+
* the literal is emitted as-is. This covers both direct double-quoted
|
|
280
|
+
* children (`<b>"hello"</b>`) and inline literal arguments to the explicit
|
|
281
|
+
* `{text ...}` intrinsic (`<b>{text 'hello'}</b>`). Identifiers and any
|
|
282
|
+
* other expression type still get the ternary because the AST alone can't
|
|
283
|
+
* prove they're non-null strings.
|
|
284
|
+
*
|
|
259
285
|
* @param {AST.Expression} expression
|
|
260
286
|
* @param {any} [source_node]
|
|
261
287
|
* @returns {AST.Expression}
|
|
262
288
|
*/
|
|
263
289
|
export function to_text_expression(expression, source_node = expression) {
|
|
290
|
+
if (is_static_string_expression(expression)) {
|
|
291
|
+
return set_loc(clone_expression_node(expression), source_node);
|
|
292
|
+
}
|
|
264
293
|
return set_loc(
|
|
265
294
|
/** @type {AST.Expression} */ ({
|
|
266
295
|
type: 'ConditionalExpression',
|
|
@@ -186,7 +186,31 @@ export function tsx_with_ts_locations() {
|
|
|
186
186
|
context.visit(body);
|
|
187
187
|
}
|
|
188
188
|
},
|
|
189
|
+
|
|
190
|
+
// esrap's JSXOpeningElement printer doesn't emit `typeArguments`, so generic
|
|
191
|
+
// component tags like `<RenderProp<User>>` lose the `<User>` in the output.
|
|
192
|
+
JSXOpeningElement: (node, context) => {
|
|
193
|
+
context.write('<');
|
|
194
|
+
context.visit(node.name);
|
|
195
|
+
if (node.typeArguments) {
|
|
196
|
+
context.visit(node.typeArguments);
|
|
197
|
+
}
|
|
198
|
+
for (const attribute of node.attributes) {
|
|
199
|
+
context.write(' ');
|
|
200
|
+
context.visit(attribute);
|
|
201
|
+
}
|
|
202
|
+
if (node.selfClosing) {
|
|
203
|
+
context.write(' /');
|
|
204
|
+
}
|
|
205
|
+
context.write('>');
|
|
206
|
+
},
|
|
189
207
|
};
|
|
208
|
+
|
|
209
|
+
// Be careful when duplicating visitors that are already defined
|
|
210
|
+
// above in the `wrappers`
|
|
211
|
+
// if there is already a visitor but you still need a mapping
|
|
212
|
+
// on the whole node, only then duplicate it here
|
|
213
|
+
// e.g. JSXOpeningElement is such a case
|
|
190
214
|
for (const type of [
|
|
191
215
|
// JS nodes whose esrap printer emits no location marker, causing
|
|
192
216
|
// segments.js get_mapping_from_node() to throw when it asks for the
|
|
@@ -214,7 +238,9 @@ export function tsx_with_ts_locations() {
|
|
|
214
238
|
'TSTypeParameterDeclaration',
|
|
215
239
|
'TSTypeParameter',
|
|
216
240
|
]) {
|
|
217
|
-
|
|
241
|
+
const visitor = wrappers[type];
|
|
242
|
+
|
|
243
|
+
wrappers[type] = (node, context) => wrap_with_locations(node, context, visitor ?? base[type]);
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
return { ...base, ...wrappers };
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
jsx_attribute as build_jsx_attribute,
|
|
32
32
|
jsx_id as build_jsx_id,
|
|
33
33
|
} from '../../utils/builders.js';
|
|
34
|
+
import * as b from '../../utils/builders.js';
|
|
34
35
|
import {
|
|
35
36
|
apply_lazy_transforms,
|
|
36
37
|
collect_lazy_bindings_from_component,
|
|
@@ -589,6 +590,105 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
589
590
|
continue;
|
|
590
591
|
}
|
|
591
592
|
|
|
593
|
+
if (
|
|
594
|
+
child.type === 'IfStatement' &&
|
|
595
|
+
!child.alternate &&
|
|
596
|
+
!is_returning_if_statement(child) &&
|
|
597
|
+
!transform_context.platform.hooks?.isTopLevelSetupCall &&
|
|
598
|
+
body_contains_top_level_hook_call([child], transform_context, true) &&
|
|
599
|
+
i + 1 < body_nodes.length
|
|
600
|
+
) {
|
|
601
|
+
statements.push(
|
|
602
|
+
...create_continuation_lift_if_statement(
|
|
603
|
+
child,
|
|
604
|
+
body_nodes.slice(i + 1),
|
|
605
|
+
render_nodes,
|
|
606
|
+
transform_context,
|
|
607
|
+
),
|
|
608
|
+
);
|
|
609
|
+
transform_context.available_bindings = saved_bindings;
|
|
610
|
+
return statements;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
child.type === 'ForOfStatement' &&
|
|
615
|
+
!child.await &&
|
|
616
|
+
!transform_context.platform.hooks?.isTopLevelSetupCall &&
|
|
617
|
+
!transform_context.platform.hooks?.controlFlow?.forOf &&
|
|
618
|
+
body_contains_top_level_hook_call(
|
|
619
|
+
child.body.type === 'BlockStatement' ? child.body.body : [child.body],
|
|
620
|
+
transform_context,
|
|
621
|
+
true,
|
|
622
|
+
)
|
|
623
|
+
) {
|
|
624
|
+
const for_of_continuation = body_nodes.slice(i + 1);
|
|
625
|
+
const hoisted = build_hoisted_for_of_with_hooks(
|
|
626
|
+
child,
|
|
627
|
+
for_of_continuation,
|
|
628
|
+
transform_context,
|
|
629
|
+
);
|
|
630
|
+
if (hoisted) {
|
|
631
|
+
statements.push(...hoisted.hoist_statements);
|
|
632
|
+
if (for_of_continuation.length > 0) {
|
|
633
|
+
// Tail was lifted into the helper; everything after the for-of
|
|
634
|
+
// now lives there. Combine prior render_nodes with the iteration
|
|
635
|
+
// JSX and return.
|
|
636
|
+
statements.push({
|
|
637
|
+
type: 'ReturnStatement',
|
|
638
|
+
argument: combine_render_return_argument(render_nodes, hoisted.jsx_child),
|
|
639
|
+
metadata: { path: [] },
|
|
640
|
+
});
|
|
641
|
+
transform_context.available_bindings = saved_bindings;
|
|
642
|
+
return statements;
|
|
643
|
+
}
|
|
644
|
+
if (interleaved && is_capturable_jsx_child(hoisted.jsx_child)) {
|
|
645
|
+
const { declaration, reference } = captureJsxChild(hoisted.jsx_child, capture_index++);
|
|
646
|
+
statements.push(declaration);
|
|
647
|
+
render_nodes.push(reference);
|
|
648
|
+
} else {
|
|
649
|
+
render_nodes.push(hoisted.jsx_child);
|
|
650
|
+
}
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (
|
|
656
|
+
child.type === 'TryStatement' &&
|
|
657
|
+
!child.finalizer &&
|
|
658
|
+
!transform_context.platform.hooks?.isTopLevelSetupCall &&
|
|
659
|
+
try_statement_contains_hooks(child, transform_context) &&
|
|
660
|
+
i + 1 < body_nodes.length
|
|
661
|
+
) {
|
|
662
|
+
statements.push(
|
|
663
|
+
...create_continuation_lift_try_statement(
|
|
664
|
+
child,
|
|
665
|
+
body_nodes.slice(i + 1),
|
|
666
|
+
render_nodes,
|
|
667
|
+
transform_context,
|
|
668
|
+
),
|
|
669
|
+
);
|
|
670
|
+
transform_context.available_bindings = saved_bindings;
|
|
671
|
+
return statements;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (
|
|
675
|
+
child.type === 'SwitchStatement' &&
|
|
676
|
+
!transform_context.platform.hooks?.isTopLevelSetupCall &&
|
|
677
|
+
body_contains_top_level_hook_call([child], transform_context, true) &&
|
|
678
|
+
i + 1 < body_nodes.length
|
|
679
|
+
) {
|
|
680
|
+
statements.push(
|
|
681
|
+
...create_continuation_lift_switch_statement(
|
|
682
|
+
child,
|
|
683
|
+
body_nodes.slice(i + 1),
|
|
684
|
+
render_nodes,
|
|
685
|
+
transform_context,
|
|
686
|
+
),
|
|
687
|
+
);
|
|
688
|
+
transform_context.available_bindings = saved_bindings;
|
|
689
|
+
return statements;
|
|
690
|
+
}
|
|
691
|
+
|
|
592
692
|
if (is_jsx_child(child)) {
|
|
593
693
|
const jsx = to_jsx_child(child, transform_context);
|
|
594
694
|
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
@@ -838,35 +938,26 @@ function create_helper_props_property(binding) {
|
|
|
838
938
|
*/
|
|
839
939
|
function create_helper_component_element(helper_id, bindings, source_node, mapping = {}) {
|
|
840
940
|
const { mapWrapper = true, mapBindingNames = true, mapBindingValues = true } = mapping;
|
|
841
|
-
const attributes = bindings.map(
|
|
842
|
-
(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
),
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
),
|
|
852
|
-
metadata: { path: [] },
|
|
853
|
-
}),
|
|
941
|
+
const attributes = bindings.map((binding) =>
|
|
942
|
+
b.jsx_attribute(
|
|
943
|
+
identifier_to_jsx_name(
|
|
944
|
+
mapBindingNames ? clone_identifier(binding) : create_generated_identifier(binding.name),
|
|
945
|
+
),
|
|
946
|
+
to_jsx_expression_container(
|
|
947
|
+
mapBindingValues ? clone_identifier(binding) : create_generated_identifier(binding.name),
|
|
948
|
+
binding,
|
|
949
|
+
),
|
|
950
|
+
),
|
|
854
951
|
);
|
|
855
952
|
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
name: identifier_to_jsx_name(clone_identifier(helper_id)),
|
|
953
|
+
const opening_element = b.jsx_opening_element(
|
|
954
|
+
identifier_to_jsx_name(clone_identifier(helper_id)),
|
|
859
955
|
attributes,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
openingElement: mapWrapper ? set_loc(openingElement, source_node) : openingElement,
|
|
866
|
-
closingElement: null,
|
|
867
|
-
children: [],
|
|
868
|
-
metadata: { path: [] },
|
|
869
|
-
});
|
|
956
|
+
true,
|
|
957
|
+
);
|
|
958
|
+
const element = b.jsx_element_fresh(
|
|
959
|
+
mapWrapper ? set_loc(opening_element, source_node) : opening_element,
|
|
960
|
+
);
|
|
870
961
|
|
|
871
962
|
return mapWrapper ? set_loc(element, source_node) : element;
|
|
872
963
|
}
|
|
@@ -1056,21 +1147,7 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
|
1056
1147
|
const name = create_helper_name(transform_context.helper_state, 'static');
|
|
1057
1148
|
const id = create_generated_identifier(name);
|
|
1058
1149
|
|
|
1059
|
-
transform_context.helper_state.statics.push(
|
|
1060
|
-
/** @type {any} */ ({
|
|
1061
|
-
type: 'VariableDeclaration',
|
|
1062
|
-
kind: 'const',
|
|
1063
|
-
declarations: [
|
|
1064
|
-
{
|
|
1065
|
-
type: 'VariableDeclarator',
|
|
1066
|
-
id,
|
|
1067
|
-
init: node,
|
|
1068
|
-
metadata: { path: [] },
|
|
1069
|
-
},
|
|
1070
|
-
],
|
|
1071
|
-
metadata: { path: [] },
|
|
1072
|
-
}),
|
|
1073
|
-
);
|
|
1150
|
+
transform_context.helper_state.statics.push(b.const(id, node));
|
|
1074
1151
|
|
|
1075
1152
|
render_nodes[i] = to_jsx_expression_container(clone_identifier(id), node);
|
|
1076
1153
|
}
|
|
@@ -1176,25 +1253,13 @@ function create_component_return_statement(
|
|
|
1176
1253
|
source_node,
|
|
1177
1254
|
map_render_node_locations = true,
|
|
1178
1255
|
) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
render_nodes.map((node) =>
|
|
1184
|
-
map_render_node_locations
|
|
1185
|
-
? clone_expression_node(node)
|
|
1186
|
-
: clone_expression_node_without_locations(node),
|
|
1187
|
-
),
|
|
1188
|
-
) || {
|
|
1189
|
-
type: 'Literal',
|
|
1190
|
-
value: null,
|
|
1191
|
-
raw: 'null',
|
|
1192
|
-
metadata: { path: [] },
|
|
1193
|
-
},
|
|
1194
|
-
metadata: { path: [] },
|
|
1195
|
-
}),
|
|
1196
|
-
source_node,
|
|
1256
|
+
const cloned = render_nodes.map((node) =>
|
|
1257
|
+
map_render_node_locations
|
|
1258
|
+
? clone_expression_node(node)
|
|
1259
|
+
: clone_expression_node_without_locations(node),
|
|
1197
1260
|
);
|
|
1261
|
+
|
|
1262
|
+
return set_loc(b.return(build_return_expression(cloned) || create_null_literal()), source_node);
|
|
1198
1263
|
}
|
|
1199
1264
|
|
|
1200
1265
|
/**
|
|
@@ -1206,20 +1271,14 @@ function create_component_lone_return_if_statement(node, render_nodes) {
|
|
|
1206
1271
|
const consequent_body = get_if_consequent_body(node);
|
|
1207
1272
|
|
|
1208
1273
|
return set_loc(
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
/** @type {any} */ ({
|
|
1214
|
-
type: 'BlockStatement',
|
|
1215
|
-
body: [create_component_return_statement(render_nodes, consequent_body[0], false)],
|
|
1216
|
-
metadata: { path: [] },
|
|
1217
|
-
}),
|
|
1274
|
+
b.if(
|
|
1275
|
+
node.test,
|
|
1276
|
+
set_loc(
|
|
1277
|
+
b.block([create_component_return_statement(render_nodes, consequent_body[0], false)]),
|
|
1218
1278
|
node.consequent,
|
|
1219
1279
|
),
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}),
|
|
1280
|
+
null,
|
|
1281
|
+
),
|
|
1223
1282
|
node,
|
|
1224
1283
|
);
|
|
1225
1284
|
}
|
|
@@ -1235,23 +1294,84 @@ function create_component_returning_if_statement(node, render_nodes, transform_c
|
|
|
1235
1294
|
const branch_statements = build_render_statements(consequent_body, true, transform_context);
|
|
1236
1295
|
prepend_render_nodes_to_return_statements(branch_statements, render_nodes);
|
|
1237
1296
|
|
|
1238
|
-
return set_loc(
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1297
|
+
return set_loc(b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null), node);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/* ---------------------------------------------------------------------- *
|
|
1301
|
+
* Continuation-lift primitives shared across if / switch / try / for-of *
|
|
1302
|
+
* ---------------------------------------------------------------------- */
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Build the helper component that owns the post-control-flow continuation.
|
|
1306
|
+
* Same shape as `create_hook_safe_helper`; named for intent at lift call sites.
|
|
1307
|
+
*
|
|
1308
|
+
* @param {any[]} continuation_body
|
|
1309
|
+
* @param {any} source_node
|
|
1310
|
+
* @param {TransformContext} transform_context
|
|
1311
|
+
* @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
|
|
1312
|
+
*/
|
|
1313
|
+
function build_tail_helper(continuation_body, source_node, transform_context) {
|
|
1314
|
+
return create_hook_safe_helper(continuation_body, undefined, source_node, transform_context);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Clone the tail helper's component element for embedding inside another
|
|
1319
|
+
* branch's body. Loses location info because the same element appears in
|
|
1320
|
+
* multiple positions and downstream tooling treats AST nodes as identity-keyed.
|
|
1321
|
+
*
|
|
1322
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1323
|
+
* @returns {any}
|
|
1324
|
+
*/
|
|
1325
|
+
function clone_tail_invocation(tail_helper) {
|
|
1326
|
+
return clone_expression_node_without_locations(tail_helper.component_element);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Return `[...body, <TailHelper x={x} />]` so the branch's render output
|
|
1331
|
+
* includes the tail invocation and the post-hook locals flow forward.
|
|
1332
|
+
* Used by if / switch / try (unconditional append). For-of uses a different
|
|
1333
|
+
* shape — gating on `_tsrx_isLast_<n>` — so it constructs its own.
|
|
1334
|
+
*
|
|
1335
|
+
* @param {any[]} body
|
|
1336
|
+
* @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
|
|
1337
|
+
* @returns {any[]}
|
|
1338
|
+
*/
|
|
1339
|
+
function append_tail_invocation(body, tail_helper) {
|
|
1340
|
+
return [...body, clone_tail_invocation(tail_helper)];
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Build a `return <combined-render-fragment>;` statement, prepending any
|
|
1345
|
+
* `render_nodes` collected before the control-flow construct so they don't
|
|
1346
|
+
* get dropped on the lift path.
|
|
1347
|
+
*
|
|
1348
|
+
* @param {any[]} render_nodes
|
|
1349
|
+
* @param {any} jsx_child
|
|
1350
|
+
* @returns {any}
|
|
1351
|
+
*/
|
|
1352
|
+
function combined_return_statement(render_nodes, jsx_child) {
|
|
1353
|
+
return b.return(combine_render_return_argument(render_nodes, jsx_child));
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Hoist a for-of iteration source into a generated `let` and add a
|
|
1358
|
+
* normalization assignment via `Array.isArray(src) ? src : Array.from(src)`.
|
|
1359
|
+
* Always emits both — even when the source is already a simple identifier —
|
|
1360
|
+
* so the loop-scoped TS type aliases have a stable name to reference and the
|
|
1361
|
+
* runtime check skips the copy when the value is already an array.
|
|
1362
|
+
*
|
|
1363
|
+
* @param {AST.Identifier} source_id
|
|
1364
|
+
* @param {any} source_expr
|
|
1365
|
+
* @returns {{ source_decl: any, source_normalize_decl: any }}
|
|
1366
|
+
*/
|
|
1367
|
+
function build_array_normalization_decls(source_id, source_expr) {
|
|
1368
|
+
const source_decl = b.let(clone_identifier(source_id), clone_expression_node(source_expr));
|
|
1369
|
+
const is_array_call = b.call(b.member(b.id('Array'), 'isArray'), clone_identifier(source_id));
|
|
1370
|
+
const from_call = b.call(b.member(b.id('Array'), 'from'), clone_identifier(source_id));
|
|
1371
|
+
const normalized = b.conditional(is_array_call, clone_identifier(source_id), from_call);
|
|
1372
|
+
const source_normalize_decl = b.stmt(b.assignment('=', clone_identifier(source_id), normalized));
|
|
1373
|
+
|
|
1374
|
+
return { source_decl, source_normalize_decl };
|
|
1255
1375
|
}
|
|
1256
1376
|
|
|
1257
1377
|
/**
|
|
@@ -1283,43 +1403,582 @@ function create_component_helper_split_returning_if_statements(
|
|
|
1283
1403
|
node,
|
|
1284
1404
|
transform_context,
|
|
1285
1405
|
);
|
|
1406
|
+
|
|
1407
|
+
const branch_block = set_loc(
|
|
1408
|
+
b.block([
|
|
1409
|
+
...branch_helper.setup_statements,
|
|
1410
|
+
combined_return_statement(render_nodes, branch_helper.component_element),
|
|
1411
|
+
]),
|
|
1412
|
+
node.consequent,
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1286
1415
|
return [
|
|
1287
|
-
set_loc(
|
|
1288
|
-
/** @type {any} */ ({
|
|
1289
|
-
type: 'IfStatement',
|
|
1290
|
-
test: node.test,
|
|
1291
|
-
consequent: set_loc(
|
|
1292
|
-
/** @type {any} */ ({
|
|
1293
|
-
type: 'BlockStatement',
|
|
1294
|
-
body: [
|
|
1295
|
-
...branch_helper.setup_statements,
|
|
1296
|
-
{
|
|
1297
|
-
type: 'ReturnStatement',
|
|
1298
|
-
argument: combine_render_return_argument(
|
|
1299
|
-
render_nodes,
|
|
1300
|
-
branch_helper.component_element,
|
|
1301
|
-
),
|
|
1302
|
-
metadata: { path: [] },
|
|
1303
|
-
},
|
|
1304
|
-
],
|
|
1305
|
-
metadata: { path: [] },
|
|
1306
|
-
}),
|
|
1307
|
-
node.consequent,
|
|
1308
|
-
),
|
|
1309
|
-
alternate: null,
|
|
1310
|
-
metadata: { path: [] },
|
|
1311
|
-
}),
|
|
1312
|
-
node,
|
|
1313
|
-
),
|
|
1416
|
+
set_loc(b.if(node.test, branch_block, null), node),
|
|
1314
1417
|
...continuation_helper.setup_statements,
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1418
|
+
combined_return_statement(render_nodes, continuation_helper.component_element),
|
|
1419
|
+
];
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Lift a non-returning `if` whose consequent contains hook calls plus the
|
|
1424
|
+
* statements that follow it into helper components.
|
|
1425
|
+
*
|
|
1426
|
+
* Without this, the consequent's hook would be wrapped into a child component
|
|
1427
|
+
* (StatementBodyHook) but any code after the `if` that reads bindings the hook
|
|
1428
|
+
* mutates would observe the pre-hook value, because React commits children
|
|
1429
|
+
* after their parent has finished rendering. The fix mirrors the early-return
|
|
1430
|
+
* splitter: emit a tail helper that owns the post-`if` statements, append a
|
|
1431
|
+
* call to it inside the branch helper so the post-hook bindings flow forward,
|
|
1432
|
+
* and render the tail helper directly when the `if` is false.
|
|
1433
|
+
*
|
|
1434
|
+
* @param {any} if_node
|
|
1435
|
+
* @param {any[]} continuation_body
|
|
1436
|
+
* @param {any[]} render_nodes
|
|
1437
|
+
* @param {TransformContext} transform_context
|
|
1438
|
+
* @returns {any[]}
|
|
1439
|
+
*/
|
|
1440
|
+
function create_continuation_lift_if_statement(
|
|
1441
|
+
if_node,
|
|
1442
|
+
continuation_body,
|
|
1443
|
+
render_nodes,
|
|
1444
|
+
transform_context,
|
|
1445
|
+
) {
|
|
1446
|
+
const consequent_body = get_if_consequent_body(if_node);
|
|
1447
|
+
const tail_helper = build_tail_helper(continuation_body, if_node, transform_context);
|
|
1448
|
+
const branch_helper = create_hook_safe_helper(
|
|
1449
|
+
append_tail_invocation(consequent_body, tail_helper),
|
|
1450
|
+
undefined,
|
|
1451
|
+
if_node.consequent,
|
|
1452
|
+
transform_context,
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
const branch_block = set_loc(
|
|
1456
|
+
b.block([
|
|
1457
|
+
...branch_helper.setup_statements,
|
|
1458
|
+
combined_return_statement(render_nodes, branch_helper.component_element),
|
|
1459
|
+
]),
|
|
1460
|
+
if_node.consequent,
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1463
|
+
return [
|
|
1464
|
+
...tail_helper.setup_statements,
|
|
1465
|
+
set_loc(b.if(if_node.test, branch_block, null), if_node),
|
|
1466
|
+
combined_return_statement(render_nodes, tail_helper.component_element),
|
|
1467
|
+
];
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Continuation lift for `try` / `try / pending / catch` statements. Same
|
|
1472
|
+
* shape as if/switch: build a tail helper from the post-`try` statements, and
|
|
1473
|
+
* append a clone of its invocation to the try body and the catch body so the
|
|
1474
|
+
* post-hook locals inside each branch flow forward into the tail. The pending
|
|
1475
|
+
* body is left untouched — when Suspense renders the pending fallback the
|
|
1476
|
+
* parent's render is unwound, so the tail wouldn't run in source semantics
|
|
1477
|
+
* either. Once augmented, the existing try transform builds the
|
|
1478
|
+
* Suspense / TsrxErrorBoundary wrapper as usual.
|
|
1479
|
+
*
|
|
1480
|
+
* @param {any} node - TryStatement
|
|
1481
|
+
* @param {any[]} continuation_body
|
|
1482
|
+
* @param {any[]} render_nodes
|
|
1483
|
+
* @param {TransformContext} transform_context
|
|
1484
|
+
* @returns {any[]}
|
|
1485
|
+
*/
|
|
1486
|
+
function create_continuation_lift_try_statement(
|
|
1487
|
+
node,
|
|
1488
|
+
continuation_body,
|
|
1489
|
+
render_nodes,
|
|
1490
|
+
transform_context,
|
|
1491
|
+
) {
|
|
1492
|
+
const tail_helper = build_tail_helper(continuation_body, node, transform_context);
|
|
1493
|
+
|
|
1494
|
+
const augmented_block = {
|
|
1495
|
+
...node.block,
|
|
1496
|
+
body: append_tail_invocation(node.block.body || [], tail_helper),
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
let augmented_handler = node.handler;
|
|
1500
|
+
if (node.handler) {
|
|
1501
|
+
augmented_handler = {
|
|
1502
|
+
...node.handler,
|
|
1503
|
+
body: {
|
|
1504
|
+
...node.handler.body,
|
|
1505
|
+
body: append_tail_invocation(node.handler.body.body || [], tail_helper),
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const augmented_try = {
|
|
1511
|
+
...node,
|
|
1512
|
+
block: augmented_block,
|
|
1513
|
+
handler: augmented_handler,
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
const try_jsx_child = (
|
|
1517
|
+
transform_context.platform.hooks?.controlFlow?.tryStatement ?? try_statement_to_jsx_child
|
|
1518
|
+
)(augmented_try, transform_context);
|
|
1519
|
+
|
|
1520
|
+
return [...tail_helper.setup_statements, combined_return_statement(render_nodes, try_jsx_child)];
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* @param {any} node - TryStatement
|
|
1525
|
+
* @param {TransformContext} transform_context
|
|
1526
|
+
* @returns {boolean}
|
|
1527
|
+
*/
|
|
1528
|
+
function try_statement_contains_hooks(node, transform_context) {
|
|
1529
|
+
if (body_contains_top_level_hook_call(node.block?.body || [], transform_context, true)) {
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
if (
|
|
1533
|
+
node.handler &&
|
|
1534
|
+
body_contains_top_level_hook_call(node.handler.body?.body || [], transform_context, true)
|
|
1535
|
+
) {
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
if (
|
|
1539
|
+
node.pending &&
|
|
1540
|
+
body_contains_top_level_hook_call(node.pending.body || [], transform_context, true)
|
|
1541
|
+
) {
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Continuation lift for `switch` statements. Same shape as the if-version:
|
|
1549
|
+
* each case body is wrapped in its own helper component that ends with a
|
|
1550
|
+
* call to a shared tail helper, so post-hook bindings inside any case flow
|
|
1551
|
+
* forward to the statements after the switch. The fall-through return at
|
|
1552
|
+
* the end renders the tail helper directly, covering the case where no
|
|
1553
|
+
* `case` (and no `default`) matched.
|
|
1554
|
+
*
|
|
1555
|
+
* Empty fall-through cases (`case 'a':` with no body, falling through to
|
|
1556
|
+
* the next case) are preserved as-is — they must not get their own helper
|
|
1557
|
+
* because that would convert fall-through into early-return.
|
|
1558
|
+
*
|
|
1559
|
+
* @param {any} switch_node
|
|
1560
|
+
* @param {any[]} continuation_body
|
|
1561
|
+
* @param {any[]} render_nodes
|
|
1562
|
+
* @param {TransformContext} transform_context
|
|
1563
|
+
* @returns {any[]}
|
|
1564
|
+
*/
|
|
1565
|
+
function create_continuation_lift_switch_statement(
|
|
1566
|
+
switch_node,
|
|
1567
|
+
continuation_body,
|
|
1568
|
+
render_nodes,
|
|
1569
|
+
transform_context,
|
|
1570
|
+
) {
|
|
1571
|
+
const tail_helper = build_tail_helper(continuation_body, switch_node, transform_context);
|
|
1572
|
+
|
|
1573
|
+
// Per-case info computed once: own body (statements before any
|
|
1574
|
+
// terminator) and whether the case has a `break` / `return`.
|
|
1575
|
+
const case_info = switch_node.cases.map((/** @type {any} */ c) => {
|
|
1576
|
+
const consequent = flatten_switch_consequent(c.consequent || []);
|
|
1577
|
+
const own_body = [];
|
|
1578
|
+
let own_has_terminator = false;
|
|
1579
|
+
for (const node of consequent) {
|
|
1580
|
+
if (node.type === 'BreakStatement' || node.type === 'ReturnStatement') {
|
|
1581
|
+
own_has_terminator = true;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
own_body.push(node);
|
|
1585
|
+
}
|
|
1586
|
+
return { own_body, own_has_terminator };
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
// Allocate helper ids in source order (forward pass) so the snapshot's
|
|
1590
|
+
// `StatementBodyHook<N>` numbering reads top-to-bottom by case position.
|
|
1591
|
+
/** @type {Array<AST.Identifier | null>} */
|
|
1592
|
+
const helper_ids = case_info.map(
|
|
1593
|
+
(/** @type {{ own_body: any[], own_has_terminator: boolean }} */ info) =>
|
|
1594
|
+
info.own_body.length === 0
|
|
1595
|
+
? null
|
|
1596
|
+
: create_generated_identifier(create_local_statement_component_name(transform_context)),
|
|
1597
|
+
);
|
|
1598
|
+
|
|
1599
|
+
// Build helpers in reverse order: each fall-through case's helper body
|
|
1600
|
+
// invokes the *next* case's helper, so the chain forwards post-mutation
|
|
1601
|
+
// locals through the switch. Reverse iteration ensures the next helper's
|
|
1602
|
+
// component_element is already constructed when we need to embed it.
|
|
1603
|
+
/** @type {Array<{ setup_statements: any[], component_element: any } | null>} */
|
|
1604
|
+
const case_helper_by_index = new Array(switch_node.cases.length).fill(null);
|
|
1605
|
+
for (let i = switch_node.cases.length - 1; i >= 0; i--) {
|
|
1606
|
+
const { own_body, own_has_terminator } = case_info[i];
|
|
1607
|
+
if (own_body.length === 0) continue;
|
|
1608
|
+
|
|
1609
|
+
// Determine the downstream helper this case invokes after its own body.
|
|
1610
|
+
// - With a terminator: invoke the tail helper directly (case exits switch).
|
|
1611
|
+
// - Otherwise (fall-through): invoke the next non-empty case's helper,
|
|
1612
|
+
// or the tail if nothing else follows.
|
|
1613
|
+
let downstream;
|
|
1614
|
+
if (own_has_terminator) {
|
|
1615
|
+
downstream = tail_helper;
|
|
1616
|
+
} else {
|
|
1617
|
+
let next_helper = null;
|
|
1618
|
+
for (let j = i + 1; j < switch_node.cases.length; j++) {
|
|
1619
|
+
if (case_helper_by_index[j]) {
|
|
1620
|
+
next_helper = case_helper_by_index[j];
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
downstream = next_helper ?? tail_helper;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
case_helper_by_index[i] = create_hook_safe_helper(
|
|
1628
|
+
append_tail_invocation(own_body, downstream),
|
|
1629
|
+
undefined,
|
|
1630
|
+
switch_node.cases[i],
|
|
1631
|
+
transform_context,
|
|
1632
|
+
/** @type {any} */ (helper_ids[i]),
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const new_cases = switch_node.cases.map(
|
|
1637
|
+
(/** @type {any} */ original_case, /** @type {number} */ i) => {
|
|
1638
|
+
const helper = case_helper_by_index[i];
|
|
1639
|
+
if (helper) {
|
|
1640
|
+
return b.switch_case(original_case.test, [
|
|
1641
|
+
combined_return_statement(render_nodes, helper.component_element),
|
|
1642
|
+
]);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const { own_body, own_has_terminator } = case_info[i];
|
|
1646
|
+
if (own_body.length === 0 && own_has_terminator) {
|
|
1647
|
+
// `case 'a': break;` — exits the switch, then runs the tail.
|
|
1648
|
+
return b.switch_case(original_case.test, [
|
|
1649
|
+
combined_return_statement(render_nodes, tail_helper.component_element),
|
|
1650
|
+
]);
|
|
1651
|
+
}
|
|
1652
|
+
// Genuine empty fall-through (`case 'a': case 'b': ...`).
|
|
1653
|
+
return b.switch_case(original_case.test, []);
|
|
1319
1654
|
},
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1657
|
+
// Hoist all case helpers' setup statements above the switch in source
|
|
1658
|
+
// order so the switch body is purely a dispatcher.
|
|
1659
|
+
const case_helper_setup_statements = [];
|
|
1660
|
+
for (const helper of case_helper_by_index) {
|
|
1661
|
+
if (helper) case_helper_setup_statements.push(...helper.setup_statements);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
return [
|
|
1665
|
+
...tail_helper.setup_statements,
|
|
1666
|
+
...case_helper_setup_statements,
|
|
1667
|
+
set_loc(b.switch(switch_node.discriminant, new_cases), switch_node),
|
|
1668
|
+
combined_return_statement(render_nodes, tail_helper.component_element),
|
|
1320
1669
|
];
|
|
1321
1670
|
}
|
|
1322
1671
|
|
|
1672
|
+
/**
|
|
1673
|
+
* Hoist the helper for a hook-bearing for-of body out of the iteration
|
|
1674
|
+
* callback so the helper is declared once per render rather than re-bound on
|
|
1675
|
+
* every iteration. Loop-scoped param types are derived from the iteration
|
|
1676
|
+
* source via a TS `type` alias (rather than the const+typeof pattern used
|
|
1677
|
+
* for outer bindings, which would require the loop var to be in scope).
|
|
1678
|
+
*
|
|
1679
|
+
* The iteration source is hoisted into a generated `let` and normalized via
|
|
1680
|
+
* `Array.isArray(src) ? src : Array.from(src)` so any Iterable / ArrayLike
|
|
1681
|
+
* works while skipping the copy when the source is already an array. The
|
|
1682
|
+
* iteration itself is emitted as `source.map((item, i) => ...)`.
|
|
1683
|
+
*
|
|
1684
|
+
* If `continuation_body` is non-empty (the for-of has a tail) we also lift
|
|
1685
|
+
* the tail into a TailHelper and call it conditionally on the last iteration
|
|
1686
|
+
* via an `isLast={i === source.length - 1}` prop on the loop helper. The
|
|
1687
|
+
* loop helper's mutated locals (post-`useState`) flow into the TailHelper as
|
|
1688
|
+
* its props. When the source is empty, `.map` returns `[]` and the TailHelper
|
|
1689
|
+
* never renders — we add a sibling fallback so the source's tail still runs
|
|
1690
|
+
* with the original outer values in that case.
|
|
1691
|
+
*
|
|
1692
|
+
* Bails out (returns null) when the loop pattern is destructured — deriving
|
|
1693
|
+
* element types from a tuple/object pattern is more involved and deferred.
|
|
1694
|
+
*
|
|
1695
|
+
* @param {any} node - ForOfStatement
|
|
1696
|
+
* @param {any[]} continuation_body
|
|
1697
|
+
* @param {TransformContext} transform_context
|
|
1698
|
+
* @returns {{ hoist_statements: any[], jsx_child: any } | null}
|
|
1699
|
+
*/
|
|
1700
|
+
function build_hoisted_for_of_with_hooks(node, continuation_body, transform_context) {
|
|
1701
|
+
const loop_params = get_for_of_iteration_params(node.left, node.index);
|
|
1702
|
+
for (const param of loop_params) {
|
|
1703
|
+
if (param.type !== 'Identifier') return null;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const has_tail = continuation_body.length > 0;
|
|
1707
|
+
const original_loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
|
|
1708
|
+
|
|
1709
|
+
// When there's a tail, build TailHelper first so its component_element can
|
|
1710
|
+
// be embedded inside the loop helper's body (gated on isLast). The
|
|
1711
|
+
// synthetic isLast prop uses the loop helper's index (which will be the
|
|
1712
|
+
// next one assigned, since `create_hook_safe_helper` for the tail just
|
|
1713
|
+
// consumed one) so it lines up with `StatementBodyHook<N>` in the output.
|
|
1714
|
+
let tail_helper = null;
|
|
1715
|
+
/** @type {AST.Identifier} */ let tail_synthetic_id;
|
|
1716
|
+
if (has_tail) {
|
|
1717
|
+
tail_helper = build_tail_helper(continuation_body, node, transform_context);
|
|
1718
|
+
tail_synthetic_id = create_generated_identifier(
|
|
1719
|
+
`_tsrx_isLast_${transform_context.local_statement_component_index + 1}`,
|
|
1720
|
+
);
|
|
1721
|
+
} else {
|
|
1722
|
+
tail_synthetic_id = /** @type {any} */ (null);
|
|
1723
|
+
}
|
|
1724
|
+
const loop_body = has_tail
|
|
1725
|
+
? [
|
|
1726
|
+
...original_loop_body,
|
|
1727
|
+
b.jsx_expression_container(
|
|
1728
|
+
b.logical(
|
|
1729
|
+
'&&',
|
|
1730
|
+
clone_identifier(tail_synthetic_id),
|
|
1731
|
+
clone_tail_invocation(/** @type {any} */ (tail_helper)),
|
|
1732
|
+
),
|
|
1733
|
+
),
|
|
1734
|
+
]
|
|
1735
|
+
: original_loop_body;
|
|
1736
|
+
|
|
1737
|
+
const source_id = create_generated_identifier(
|
|
1738
|
+
`_tsrx_iteration_items_${transform_context.local_statement_component_index + 1}`,
|
|
1739
|
+
);
|
|
1740
|
+
const { source_decl, source_normalize_decl } = build_array_normalization_decls(
|
|
1741
|
+
source_id,
|
|
1742
|
+
node.right,
|
|
1743
|
+
);
|
|
1744
|
+
|
|
1745
|
+
const saved_bindings = transform_context.available_bindings;
|
|
1746
|
+
transform_context.available_bindings = new Map(saved_bindings);
|
|
1747
|
+
for (const param of loop_params) {
|
|
1748
|
+
collect_pattern_bindings(param, transform_context.available_bindings);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const all_helper_bindings = get_referenced_helper_bindings(
|
|
1752
|
+
loop_body,
|
|
1753
|
+
transform_context.available_bindings,
|
|
1754
|
+
);
|
|
1755
|
+
const loop_scoped_names = new Set(loop_params.map((/** @type {any} */ p) => p.name));
|
|
1756
|
+
const outer_bindings = all_helper_bindings.filter((b) => !loop_scoped_names.has(b.name));
|
|
1757
|
+
const loop_bindings = all_helper_bindings.filter((b) => loop_scoped_names.has(b.name));
|
|
1758
|
+
|
|
1759
|
+
const helper_id = create_generated_identifier(
|
|
1760
|
+
create_local_statement_component_name(transform_context),
|
|
1761
|
+
);
|
|
1762
|
+
|
|
1763
|
+
const outer_aliases = outer_bindings.map((binding) =>
|
|
1764
|
+
create_helper_type_alias_declaration(helper_id, binding),
|
|
1765
|
+
);
|
|
1766
|
+
const loop_aliases = loop_bindings.map((binding) =>
|
|
1767
|
+
create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params),
|
|
1768
|
+
);
|
|
1769
|
+
|
|
1770
|
+
// Synthetic `isLast` prop on the loop helper when there's a tail. It's
|
|
1771
|
+
// passed from the .map callback as `i === source.length - 1` so the loop
|
|
1772
|
+
// helper renders the tail helper only on the last iteration. We do not
|
|
1773
|
+
// gate on this prop's value here — the JSXLogicalExpression appended to
|
|
1774
|
+
// `loop_body` does the gating at render time.
|
|
1775
|
+
const tail_isLast_alias = has_tail
|
|
1776
|
+
? {
|
|
1777
|
+
id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
|
|
1778
|
+
declaration: b.ts_type_alias(
|
|
1779
|
+
create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
|
|
1780
|
+
b.ts_keyword_type('boolean'),
|
|
1781
|
+
),
|
|
1782
|
+
}
|
|
1783
|
+
: null;
|
|
1784
|
+
|
|
1785
|
+
const ordered_bindings = [...outer_bindings, ...loop_bindings];
|
|
1786
|
+
const ordered_aliases = [...outer_aliases, ...loop_aliases];
|
|
1787
|
+
const ordered_use_typeof = [...outer_bindings.map(() => true), ...loop_bindings.map(() => false)];
|
|
1788
|
+
|
|
1789
|
+
const signature_bindings = has_tail ? [...ordered_bindings, tail_synthetic_id] : ordered_bindings;
|
|
1790
|
+
const signature_aliases = has_tail
|
|
1791
|
+
? [...ordered_aliases, /** @type {any} */ (tail_isLast_alias)]
|
|
1792
|
+
: ordered_aliases;
|
|
1793
|
+
const signature_use_typeof = has_tail ? [...ordered_use_typeof, false] : ordered_use_typeof;
|
|
1794
|
+
|
|
1795
|
+
const props_type =
|
|
1796
|
+
signature_bindings.length > 0
|
|
1797
|
+
? create_helper_props_type_literal_with_typeof_flags(
|
|
1798
|
+
signature_bindings,
|
|
1799
|
+
signature_aliases,
|
|
1800
|
+
signature_use_typeof,
|
|
1801
|
+
)
|
|
1802
|
+
: null;
|
|
1803
|
+
const params =
|
|
1804
|
+
props_type !== null ? [create_typed_helper_props_pattern(signature_bindings, props_type)] : [];
|
|
1805
|
+
|
|
1806
|
+
const fn_saved_bindings = transform_context.available_bindings;
|
|
1807
|
+
transform_context.available_bindings = new Map(fn_saved_bindings);
|
|
1808
|
+
if (has_tail) {
|
|
1809
|
+
transform_context.available_bindings.set(tail_synthetic_id.name, tail_synthetic_id);
|
|
1810
|
+
}
|
|
1811
|
+
const fn_body_statements = build_render_statements(loop_body, true, transform_context);
|
|
1812
|
+
transform_context.available_bindings = fn_saved_bindings;
|
|
1813
|
+
|
|
1814
|
+
const helper_fn = /** @type {any} */ (
|
|
1815
|
+
b.function(clone_identifier(helper_id), params, b.block(fn_body_statements))
|
|
1816
|
+
);
|
|
1817
|
+
helper_fn.metadata = { path: [], is_component: true, is_method: false };
|
|
1818
|
+
|
|
1819
|
+
let helper_decl;
|
|
1820
|
+
if (transform_context.helper_state) {
|
|
1821
|
+
const cache_id = create_generated_identifier(
|
|
1822
|
+
`${transform_context.helper_state.base_name}__${helper_id.name}`,
|
|
1823
|
+
);
|
|
1824
|
+
transform_context.helper_state.helpers.push(create_helper_cache_declaration(cache_id));
|
|
1825
|
+
helper_decl = create_cached_helper_declaration(
|
|
1826
|
+
helper_id,
|
|
1827
|
+
cache_id,
|
|
1828
|
+
create_helper_init_expression(helper_id, helper_fn, node, transform_context),
|
|
1829
|
+
);
|
|
1830
|
+
} else {
|
|
1831
|
+
helper_decl = create_helper_declaration(helper_id, helper_fn, node, transform_context);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
transform_context.available_bindings = saved_bindings;
|
|
1835
|
+
|
|
1836
|
+
const callback_invocation_element = create_helper_component_element(
|
|
1837
|
+
helper_id,
|
|
1838
|
+
ordered_bindings,
|
|
1839
|
+
node,
|
|
1840
|
+
{ mapWrapper: false, mapBindingNames: false, mapBindingValues: false },
|
|
1841
|
+
);
|
|
1842
|
+
|
|
1843
|
+
// When there's a tail, the .map callback always needs an index to compute
|
|
1844
|
+
// `isLast`. If the user didn't write `index i`, synthesize one. The same
|
|
1845
|
+
// identifier is also used as the implicit key fallback below.
|
|
1846
|
+
let index_identifier;
|
|
1847
|
+
if (loop_params.length >= 2) {
|
|
1848
|
+
index_identifier = clone_identifier(loop_params[1]);
|
|
1849
|
+
} else if (has_tail) {
|
|
1850
|
+
index_identifier = create_generated_identifier('i');
|
|
1851
|
+
} else {
|
|
1852
|
+
index_identifier = null;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const body_key_expression = find_key_expression_in_body(loop_body);
|
|
1856
|
+
const explicit_key_expression =
|
|
1857
|
+
body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
|
|
1858
|
+
const key_expression =
|
|
1859
|
+
explicit_key_expression ??
|
|
1860
|
+
(loop_params.length >= 2 ? clone_identifier(loop_params[1]) : undefined);
|
|
1861
|
+
if (key_expression) {
|
|
1862
|
+
callback_invocation_element.openingElement.attributes.push(
|
|
1863
|
+
b.jsx_attribute(b.jsx_id('key'), to_jsx_expression_container(key_expression, key_expression)),
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (has_tail && index_identifier) {
|
|
1868
|
+
const length_minus_one = b.binary(
|
|
1869
|
+
'-',
|
|
1870
|
+
b.member(clone_identifier(source_id), 'length'),
|
|
1871
|
+
b.literal(1),
|
|
1872
|
+
);
|
|
1873
|
+
callback_invocation_element.openingElement.attributes.push(
|
|
1874
|
+
b.jsx_attribute(
|
|
1875
|
+
b.jsx_id(tail_synthetic_id.name),
|
|
1876
|
+
to_jsx_expression_container(
|
|
1877
|
+
b.binary('===', clone_identifier(index_identifier), length_minus_one),
|
|
1878
|
+
),
|
|
1879
|
+
),
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const callback_params =
|
|
1884
|
+
has_tail && loop_params.length < 2 && index_identifier
|
|
1885
|
+
? [
|
|
1886
|
+
...loop_params.map((/** @type {any} */ p) => clone_identifier(p)),
|
|
1887
|
+
clone_identifier(index_identifier),
|
|
1888
|
+
]
|
|
1889
|
+
: loop_params.map((/** @type {any} */ p) => clone_identifier(p));
|
|
1890
|
+
|
|
1891
|
+
const iter_callback = b.arrow(callback_params, callback_invocation_element);
|
|
1892
|
+
|
|
1893
|
+
const map_call = b.call(b.member(clone_identifier(source_id), 'map'), iter_callback);
|
|
1894
|
+
|
|
1895
|
+
// jsx_child for the iteration. When there's a tail, also render the tail
|
|
1896
|
+
// helper directly when the source is empty (no iterations means the loop
|
|
1897
|
+
// helper never fires, so the tail wouldn't run otherwise).
|
|
1898
|
+
const jsx_child = has_tail
|
|
1899
|
+
? to_jsx_expression_container(
|
|
1900
|
+
b.conditional(
|
|
1901
|
+
b.binary('===', b.member(clone_identifier(source_id), 'length'), b.literal(0)),
|
|
1902
|
+
clone_tail_invocation(/** @type {any} */ (tail_helper)),
|
|
1903
|
+
map_call,
|
|
1904
|
+
),
|
|
1905
|
+
node,
|
|
1906
|
+
)
|
|
1907
|
+
: to_jsx_expression_container(map_call, node);
|
|
1908
|
+
|
|
1909
|
+
const hoist_statements = [source_decl, source_normalize_decl];
|
|
1910
|
+
if (has_tail) {
|
|
1911
|
+
// TailHelper's setup statements (its alias consts and cache decl).
|
|
1912
|
+
hoist_statements.push(.../** @type {any} */ (tail_helper).setup_statements);
|
|
1913
|
+
}
|
|
1914
|
+
for (const alias of ordered_aliases) hoist_statements.push(alias.declaration);
|
|
1915
|
+
if (has_tail && tail_isLast_alias) {
|
|
1916
|
+
hoist_statements.push(tail_isLast_alias.declaration);
|
|
1917
|
+
}
|
|
1918
|
+
hoist_statements.push(helper_decl);
|
|
1919
|
+
|
|
1920
|
+
return {
|
|
1921
|
+
hoist_statements,
|
|
1922
|
+
jsx_child,
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Build a TS `type` alias for a loop-scoped binding, deriving the type
|
|
1928
|
+
* from the iteration source. For the value param we use
|
|
1929
|
+
* `(typeof source)[number]`, which gives the right element type for arrays
|
|
1930
|
+
* and tuples (the common case in JSX templates). For the index param,
|
|
1931
|
+
* the type is always `number`.
|
|
1932
|
+
*
|
|
1933
|
+
* @param {AST.Identifier} helper_id
|
|
1934
|
+
* @param {AST.Identifier} binding
|
|
1935
|
+
* @param {AST.Identifier} source_id
|
|
1936
|
+
* @param {any[]} loop_params
|
|
1937
|
+
* @returns {{ id: AST.Identifier, declaration: any }}
|
|
1938
|
+
*/
|
|
1939
|
+
function create_loop_scoped_type_alias_declaration(helper_id, binding, source_id, loop_params) {
|
|
1940
|
+
const alias_id = create_generated_identifier(`_tsrx_${helper_id.name}_${binding.name}`);
|
|
1941
|
+
const is_index = loop_params.length > 1 && binding.name === loop_params[1].name;
|
|
1942
|
+
const type_annotation = is_index
|
|
1943
|
+
? b.ts_keyword_type('number')
|
|
1944
|
+
: /** @type {any} */ ({
|
|
1945
|
+
type: 'TSIndexedAccessType',
|
|
1946
|
+
objectType: b.ts_type_query(clone_identifier(source_id)),
|
|
1947
|
+
indexType: b.ts_keyword_type('number'),
|
|
1948
|
+
metadata: { path: [] },
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
return {
|
|
1952
|
+
id: alias_id,
|
|
1953
|
+
declaration: b.ts_type_alias(clone_identifier(alias_id), type_annotation),
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Variant of {@link create_helper_props_type_literal} that lets each
|
|
1959
|
+
* binding's type reference the alias either via `typeof <alias>` (for
|
|
1960
|
+
* outer-scope const aliases) or directly as `<alias>` (for TS `type`
|
|
1961
|
+
* aliases derived from a loop source).
|
|
1962
|
+
*
|
|
1963
|
+
* @param {AST.Identifier[]} bindings
|
|
1964
|
+
* @param {{ id: AST.Identifier }[]} aliases
|
|
1965
|
+
* @param {boolean[]} use_typeof
|
|
1966
|
+
* @returns {any}
|
|
1967
|
+
*/
|
|
1968
|
+
function create_helper_props_type_literal_with_typeof_flags(bindings, aliases, use_typeof) {
|
|
1969
|
+
return b.ts_type_literal(
|
|
1970
|
+
bindings.map((binding, i) => {
|
|
1971
|
+
const alias_ref = use_typeof[i]
|
|
1972
|
+
? b.ts_type_query(clone_identifier(aliases[i].id))
|
|
1973
|
+
: b.ts_type_reference(clone_identifier(aliases[i].id));
|
|
1974
|
+
return b.ts_property_signature(
|
|
1975
|
+
create_generated_identifier(binding.name),
|
|
1976
|
+
b.ts_type_annotation(alias_ref),
|
|
1977
|
+
);
|
|
1978
|
+
}),
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1323
1982
|
/**
|
|
1324
1983
|
* @param {any} node
|
|
1325
1984
|
* @param {any[]} continuation_body
|
|
@@ -1550,47 +2209,26 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
|
|
|
1550
2209
|
(/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
|
|
1551
2210
|
);
|
|
1552
2211
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
name,
|
|
1559
|
-
attributes,
|
|
1560
|
-
selfClosing,
|
|
1561
|
-
metadata: { path: [] },
|
|
1562
|
-
}
|
|
1563
|
-
: set_loc(
|
|
1564
|
-
/** @type {any} */ ({
|
|
1565
|
-
type: 'JSXOpeningElement',
|
|
1566
|
-
name,
|
|
1567
|
-
attributes,
|
|
1568
|
-
selfClosing,
|
|
1569
|
-
}),
|
|
1570
|
-
node.openingElement || node,
|
|
1571
|
-
)
|
|
2212
|
+
const opening_element_node = b.jsx_opening_element(
|
|
2213
|
+
name,
|
|
2214
|
+
attributes,
|
|
2215
|
+
selfClosing,
|
|
2216
|
+
node.openingElement?.typeArguments,
|
|
1572
2217
|
);
|
|
2218
|
+
const openingElement = has_unmappable_attribute
|
|
2219
|
+
? opening_element_node
|
|
2220
|
+
: set_loc(opening_element_node, node.openingElement || node);
|
|
1573
2221
|
|
|
1574
|
-
/** @type {ESTreeJSX.JSXClosingElement | null} */
|
|
1575
2222
|
const closingElement = selfClosing
|
|
1576
2223
|
? null
|
|
1577
2224
|
: set_loc(
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}),
|
|
2225
|
+
b.jsx_closing_element(
|
|
2226
|
+
clone_jsx_name(name, node.closingElement?.name || node.closingElement || node),
|
|
2227
|
+
),
|
|
1582
2228
|
node.closingElement || node,
|
|
1583
2229
|
);
|
|
1584
2230
|
|
|
1585
|
-
return set_loc(
|
|
1586
|
-
/** @type {any} */ ({
|
|
1587
|
-
type: 'JSXElement',
|
|
1588
|
-
openingElement,
|
|
1589
|
-
closingElement,
|
|
1590
|
-
children,
|
|
1591
|
-
}),
|
|
1592
|
-
node,
|
|
1593
|
-
);
|
|
2231
|
+
return set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
|
|
1594
2232
|
}
|
|
1595
2233
|
|
|
1596
2234
|
/**
|
|
@@ -1795,12 +2433,22 @@ function get_referenced_helper_bindings(body_nodes, available_bindings) {
|
|
|
1795
2433
|
* @param {any} key_expression
|
|
1796
2434
|
* @param {any} source_node
|
|
1797
2435
|
* @param {TransformContext} transform_context
|
|
2436
|
+
* @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
|
|
2437
|
+
* Used by the switch lift's chained-call build, which allocates ids in
|
|
2438
|
+
* source order in a forward pass and then constructs helpers in reverse so
|
|
2439
|
+
* each fall-through case can reference the next case's component element.
|
|
1798
2440
|
* @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
|
|
1799
2441
|
*/
|
|
1800
|
-
function create_hook_safe_helper(
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2442
|
+
function create_hook_safe_helper(
|
|
2443
|
+
body_nodes,
|
|
2444
|
+
key_expression,
|
|
2445
|
+
source_node,
|
|
2446
|
+
transform_context,
|
|
2447
|
+
preallocated_helper_id,
|
|
2448
|
+
) {
|
|
2449
|
+
const helper_id =
|
|
2450
|
+
preallocated_helper_id ??
|
|
2451
|
+
create_generated_identifier(create_local_statement_component_name(transform_context));
|
|
1804
2452
|
const helper_bindings = get_referenced_helper_bindings(
|
|
1805
2453
|
body_nodes,
|
|
1806
2454
|
transform_context.available_bindings,
|
|
@@ -2701,27 +3349,10 @@ function try_statement_to_jsx_child(node, transform_context) {
|
|
|
2701
3349
|
* @returns {any}
|
|
2702
3350
|
*/
|
|
2703
3351
|
function create_jsx_element(tag_name, attributes, children) {
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
type: 'JSXOpeningElement',
|
|
2709
|
-
name,
|
|
2710
|
-
attributes,
|
|
2711
|
-
selfClosing: children.length === 0,
|
|
2712
|
-
metadata: { path: [] },
|
|
2713
|
-
},
|
|
2714
|
-
closingElement:
|
|
2715
|
-
children.length > 0
|
|
2716
|
-
? {
|
|
2717
|
-
type: 'JSXClosingElement',
|
|
2718
|
-
name: { type: 'JSXIdentifier', name: tag_name, metadata: { path: [] } },
|
|
2719
|
-
metadata: { path: [] },
|
|
2720
|
-
}
|
|
2721
|
-
: null,
|
|
2722
|
-
children,
|
|
2723
|
-
metadata: { path: [] },
|
|
2724
|
-
};
|
|
3352
|
+
const self_closing = children.length === 0;
|
|
3353
|
+
const opening_element = b.jsx_opening_element(b.jsx_id(tag_name), attributes, self_closing);
|
|
3354
|
+
const closing_element = self_closing ? null : b.jsx_closing_element(b.jsx_id(tag_name));
|
|
3355
|
+
return b.jsx_element_fresh(opening_element, closing_element, children);
|
|
2725
3356
|
}
|
|
2726
3357
|
|
|
2727
3358
|
/**
|
|
@@ -3365,25 +3996,11 @@ function create_dynamic_jsx_element(dynamic_id, node, transform_context) {
|
|
|
3365
3996
|
const children = create_element_children(node.children || [], transform_context);
|
|
3366
3997
|
const name = identifier_to_jsx_name(clone_identifier(dynamic_id));
|
|
3367
3998
|
|
|
3368
|
-
return
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
type: 'JSXOpeningElement',
|
|
3372
|
-
name,
|
|
3373
|
-
attributes,
|
|
3374
|
-
selfClosing,
|
|
3375
|
-
metadata: { path: [] },
|
|
3376
|
-
},
|
|
3377
|
-
closingElement: selfClosing
|
|
3378
|
-
? null
|
|
3379
|
-
: {
|
|
3380
|
-
type: 'JSXClosingElement',
|
|
3381
|
-
name: clone_jsx_name(name),
|
|
3382
|
-
metadata: { path: [] },
|
|
3383
|
-
},
|
|
3999
|
+
return b.jsx_element_fresh(
|
|
4000
|
+
b.jsx_opening_element(name, attributes, selfClosing),
|
|
4001
|
+
selfClosing ? null : b.jsx_closing_element(clone_jsx_name(name)),
|
|
3384
4002
|
children,
|
|
3385
|
-
|
|
3386
|
-
});
|
|
4003
|
+
);
|
|
3387
4004
|
}
|
|
3388
4005
|
|
|
3389
4006
|
/**
|
|
@@ -653,8 +653,11 @@ export function convert_source_map_to_mappings(
|
|
|
653
653
|
// Nothing to visit (just source string)
|
|
654
654
|
return;
|
|
655
655
|
} else if (node.type === 'JSXOpeningElement') {
|
|
656
|
-
// Visit name and attributes in source order
|
|
656
|
+
// Visit name, type arguments, and attributes in source order
|
|
657
657
|
visit(node.name);
|
|
658
|
+
if (node.typeArguments) {
|
|
659
|
+
visit(node.typeArguments);
|
|
660
|
+
}
|
|
658
661
|
for (const attr of node.attributes) {
|
|
659
662
|
visit(attr);
|
|
660
663
|
}
|
package/src/utils/builders.js
CHANGED
|
@@ -1100,6 +1100,82 @@ export function jsx_attribute(name, value = null, shorthand = false, loc_info) {
|
|
|
1100
1100
|
return set_location(node, loc_info);
|
|
1101
1101
|
}
|
|
1102
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Build a fresh `JSXOpeningElement`. For elements derived from an existing
|
|
1105
|
+
* Element node, prefer `jsx_element` which spreads from the source.
|
|
1106
|
+
*
|
|
1107
|
+
* @param {ESTreeJSX.JSXOpeningElement['name']} name
|
|
1108
|
+
* @param {ESTreeJSX.JSXOpeningElement['attributes']} [attributes]
|
|
1109
|
+
* @param {boolean} [self_closing]
|
|
1110
|
+
* @param {ESTreeJSX.JSXOpeningElement['typeArguments']} [type_arguments]
|
|
1111
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
1112
|
+
* @returns {ESTreeJSX.JSXOpeningElement}
|
|
1113
|
+
*/
|
|
1114
|
+
export function jsx_opening_element(
|
|
1115
|
+
name,
|
|
1116
|
+
attributes = [],
|
|
1117
|
+
self_closing = false,
|
|
1118
|
+
type_arguments = undefined,
|
|
1119
|
+
loc_info,
|
|
1120
|
+
) {
|
|
1121
|
+
const node = /** @type {ESTreeJSX.JSXOpeningElement} */ ({
|
|
1122
|
+
type: 'JSXOpeningElement',
|
|
1123
|
+
name,
|
|
1124
|
+
attributes,
|
|
1125
|
+
selfClosing: self_closing,
|
|
1126
|
+
typeArguments: type_arguments,
|
|
1127
|
+
metadata: { path: [] },
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
return set_location(node, loc_info);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Build a fresh `JSXClosingElement`.
|
|
1135
|
+
*
|
|
1136
|
+
* @param {ESTreeJSX.JSXClosingElement['name']} name
|
|
1137
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
1138
|
+
* @returns {ESTreeJSX.JSXClosingElement}
|
|
1139
|
+
*/
|
|
1140
|
+
export function jsx_closing_element(name, loc_info) {
|
|
1141
|
+
const node = /** @type {ESTreeJSX.JSXClosingElement} */ ({
|
|
1142
|
+
type: 'JSXClosingElement',
|
|
1143
|
+
name,
|
|
1144
|
+
metadata: { path: [] },
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
return set_location(node, loc_info);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Build a fresh `JSXElement` from explicit opening / closing / children.
|
|
1152
|
+
* Companion to `jsx_opening_element` / `jsx_closing_element`. For elements
|
|
1153
|
+
* derived from an existing source node, use `jsx_element` (which spreads
|
|
1154
|
+
* the source's name and metadata).
|
|
1155
|
+
*
|
|
1156
|
+
* @param {ESTreeJSX.JSXOpeningElement} opening_element
|
|
1157
|
+
* @param {ESTreeJSX.JSXClosingElement | null} [closing_element]
|
|
1158
|
+
* @param {ESTreeJSX.JSXElement['children']} [children]
|
|
1159
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
1160
|
+
* @returns {ESTreeJSX.JSXElement}
|
|
1161
|
+
*/
|
|
1162
|
+
export function jsx_element_fresh(
|
|
1163
|
+
opening_element,
|
|
1164
|
+
closing_element = null,
|
|
1165
|
+
children = [],
|
|
1166
|
+
loc_info,
|
|
1167
|
+
) {
|
|
1168
|
+
const node = /** @type {ESTreeJSX.JSXElement} */ ({
|
|
1169
|
+
type: 'JSXElement',
|
|
1170
|
+
openingElement: opening_element,
|
|
1171
|
+
closingElement: closing_element,
|
|
1172
|
+
children,
|
|
1173
|
+
metadata: { path: [] },
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
return set_location(node, loc_info);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1103
1179
|
/**
|
|
1104
1180
|
* @param {AST.Element} node
|
|
1105
1181
|
* @param {ESTreeJSX.JSXOpeningElement['attributes']} attributes
|