@tsrx/core 0.1.3 → 0.1.4

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.
@@ -57,6 +57,79 @@ import {
57
57
  } from '../jsx-interleave.js';
58
58
  import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
59
59
 
60
+ const HOOK_OUTER_ASSIGNMENT_ERROR =
61
+ 'Hook calls inside conditional or repeated TSRX scopes must keep their results local to the generated hook component.';
62
+ const HOOK_CALLBACK_OUTER_MUTATION_ERROR =
63
+ 'Hook callbacks inside conditional or repeated TSRX scopes must not mutate bindings declared outside the generated hook component.';
64
+ const TEMPLATE_FRAGMENT_ERROR =
65
+ '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>.';
66
+
67
+ /**
68
+ * @param {AST.Node} node
69
+ * @param {TransformContext} transform_context
70
+ */
71
+ function report_html_template_unsupported_error(node, transform_context) {
72
+ // this should be a fatal error so we don't pass the errors collection,
73
+ // since we don't have a transform for the Html node
74
+ error(
75
+ `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
76
+ transform_context.filename,
77
+ node,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * @param {AST.Node} node
83
+ * @param {TransformContext} transform_context
84
+ */
85
+ function report_jsx_fragment_in_tsrx_error(node, transform_context) {
86
+ error(
87
+ TEMPLATE_FRAGMENT_ERROR,
88
+ transform_context.filename,
89
+ node,
90
+ transform_context.errors,
91
+ transform_context.comments,
92
+ );
93
+ }
94
+
95
+ /**
96
+ * @param {AST.Node} node
97
+ * @param {string[]} names
98
+ * @param {string} hook_name
99
+ * @param {TransformContext} transform_context
100
+ * @returns {void}
101
+ */
102
+ function report_hook_outer_assignment_error(node, names, hook_name, transform_context) {
103
+ const target =
104
+ names.length === 1 ? `\`${names[0]}\`` : names.map((name) => `\`${name}\``).join(', ');
105
+ error(
106
+ `${HOOK_OUTER_ASSIGNMENT_ERROR} The ${hook_name} result is assigned to ${target}, which is declared outside that generated component. Declare the hook result inside the TSRX branch, or move the hook into an explicit child component and pass values with props.`,
107
+ transform_context.filename,
108
+ node,
109
+ transform_context.errors,
110
+ transform_context.comments,
111
+ );
112
+ }
113
+
114
+ /**
115
+ * @param {AST.Node} node
116
+ * @param {string[]} names
117
+ * @param {string} hook_name
118
+ * @param {TransformContext} transform_context
119
+ * @returns {void}
120
+ */
121
+ function report_hook_callback_outer_mutation_error(node, names, hook_name, transform_context) {
122
+ const target =
123
+ names.length === 1 ? `\`${names[0]}\`` : names.map((name) => `\`${name}\``).join(', ');
124
+ error(
125
+ `${HOOK_CALLBACK_OUTER_MUTATION_ERROR} The ${hook_name} callback mutates ${target}. Read outer values through props or dependencies, and move mutable state into an explicit child component when it needs to change over time.`,
126
+ transform_context.filename,
127
+ node,
128
+ transform_context.errors,
129
+ transform_context.comments,
130
+ );
131
+ }
132
+
60
133
  /**
61
134
  * Local alias for the shared `JsxTransformContext`. Kept as a typedef so the
62
135
  * rest of this file's `@param {TransformContext}` annotations don't all have
@@ -449,9 +522,12 @@ export function createJsxTransform(platform) {
449
522
  // (e.g. segments.js reading node.value.metadata.is_component on class
450
523
  // methods) don't trip on an undefined metadata object. Ripple's analyze
451
524
  // phase does this via visit_function; tsrx-react has no analyze phase.
452
- FunctionDeclaration: ensure_function_metadata,
453
- FunctionExpression: ensure_function_metadata,
454
- ArrowFunctionExpression: ensure_function_metadata,
525
+ // If a plain JS function contains a hook-bearing <tsrx> expression,
526
+ // give it a temporary helper scope so extracted hook components can
527
+ // be emitted with stable identities just like component-body helpers.
528
+ FunctionDeclaration: transform_function_with_hook_helpers,
529
+ FunctionExpression: transform_function_with_hook_helpers,
530
+ ArrowFunctionExpression: transform_function_with_hook_helpers,
455
531
 
456
532
  RefExpression(node) {
457
533
  return create_ref_prop_call(node, transform_context);
@@ -1250,6 +1326,156 @@ function create_helper_state(base_name) {
1250
1326
  };
1251
1327
  }
1252
1328
 
1329
+ /**
1330
+ * @param {any} node
1331
+ * @param {{ next: () => any, state: TransformContext }} context
1332
+ * @returns {any}
1333
+ */
1334
+ function transform_function_with_hook_helpers(node, { next, state }) {
1335
+ if (state.helper_state || !function_contains_hook_bearing_tsrx(node, state)) {
1336
+ return ensure_function_metadata(node, { next });
1337
+ }
1338
+
1339
+ const helper_state = create_helper_state(get_function_helper_base_name(node));
1340
+ const saved_helper_state = state.helper_state;
1341
+ const saved_bindings = state.available_bindings;
1342
+
1343
+ state.helper_state = helper_state;
1344
+ state.available_bindings = collect_function_scope_bindings(node);
1345
+
1346
+ const inner = /** @type {any} */ (next() ?? node);
1347
+
1348
+ state.helper_state = saved_helper_state;
1349
+ state.available_bindings = saved_bindings;
1350
+
1351
+ ensure_function_metadata(inner, { next: () => inner });
1352
+ if (helper_state.helpers.length || helper_state.statics.length) {
1353
+ inner.metadata = {
1354
+ ...(inner.metadata || {}),
1355
+ generated_helpers: helper_state.helpers,
1356
+ generated_statics: helper_state.statics,
1357
+ };
1358
+ }
1359
+
1360
+ return inner;
1361
+ }
1362
+
1363
+ /**
1364
+ * @param {any} node
1365
+ * @returns {string}
1366
+ */
1367
+ function get_function_helper_base_name(node) {
1368
+ if (node.id?.type === 'Identifier') {
1369
+ return node.id.name;
1370
+ }
1371
+ return 'Tsrx';
1372
+ }
1373
+
1374
+ /**
1375
+ * @param {any} node
1376
+ * @returns {Map<string, AST.Identifier>}
1377
+ */
1378
+ function collect_function_scope_bindings(node) {
1379
+ const bindings = collect_param_bindings(node.params || []);
1380
+ collect_descendant_declaration_bindings(node.body, bindings);
1381
+ return bindings;
1382
+ }
1383
+
1384
+ /**
1385
+ * @param {any} node
1386
+ * @param {Map<string, AST.Identifier>} bindings
1387
+ * @returns {void}
1388
+ */
1389
+ function collect_descendant_declaration_bindings(node, bindings) {
1390
+ if (!node || typeof node !== 'object') {
1391
+ return;
1392
+ }
1393
+
1394
+ if (node.type === 'VariableDeclaration') {
1395
+ for (const declaration of node.declarations || []) {
1396
+ collect_pattern_bindings(declaration.id, bindings);
1397
+ }
1398
+ }
1399
+
1400
+ if (
1401
+ (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') &&
1402
+ node.id?.type === 'Identifier'
1403
+ ) {
1404
+ bindings.set(node.id.name, node.id);
1405
+ }
1406
+
1407
+ if (
1408
+ node.type === 'FunctionDeclaration' ||
1409
+ node.type === 'FunctionExpression' ||
1410
+ node.type === 'ArrowFunctionExpression' ||
1411
+ node.type === 'Component'
1412
+ ) {
1413
+ return;
1414
+ }
1415
+
1416
+ if (Array.isArray(node)) {
1417
+ for (const child of node) {
1418
+ collect_descendant_declaration_bindings(child, bindings);
1419
+ }
1420
+ return;
1421
+ }
1422
+
1423
+ for (const key of Object.keys(node)) {
1424
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1425
+ continue;
1426
+ }
1427
+ collect_descendant_declaration_bindings(node[key], bindings);
1428
+ }
1429
+ }
1430
+
1431
+ /**
1432
+ * @param {any} node
1433
+ * @param {TransformContext} transform_context
1434
+ * @returns {boolean}
1435
+ */
1436
+ function function_contains_hook_bearing_tsrx(node, transform_context) {
1437
+ return node_contains_hook_bearing_tsrx(node.body, transform_context);
1438
+ }
1439
+
1440
+ /**
1441
+ * @param {any} node
1442
+ * @param {TransformContext} transform_context
1443
+ * @returns {boolean}
1444
+ */
1445
+ function node_contains_hook_bearing_tsrx(node, transform_context) {
1446
+ if (!node || typeof node !== 'object') {
1447
+ return false;
1448
+ }
1449
+
1450
+ if (Array.isArray(node)) {
1451
+ return node.some((child) => node_contains_hook_bearing_tsrx(child, transform_context));
1452
+ }
1453
+
1454
+ if (node.type === 'Tsrx') {
1455
+ return body_contains_top_level_hook_call(node.children || [], transform_context, true);
1456
+ }
1457
+
1458
+ if (
1459
+ node.type === 'FunctionDeclaration' ||
1460
+ node.type === 'FunctionExpression' ||
1461
+ node.type === 'ArrowFunctionExpression' ||
1462
+ node.type === 'Component'
1463
+ ) {
1464
+ return false;
1465
+ }
1466
+
1467
+ for (const key of Object.keys(node)) {
1468
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1469
+ continue;
1470
+ }
1471
+ if (node_contains_hook_bearing_tsrx(node[key], transform_context)) {
1472
+ return true;
1473
+ }
1474
+ }
1475
+
1476
+ return false;
1477
+ }
1478
+
1253
1479
  /**
1254
1480
  * @param {TransformContext} transform_context
1255
1481
  * @returns {boolean}
@@ -2231,15 +2457,20 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
2231
2457
 
2232
2458
  const saved_bindings = transform_context.available_bindings;
2233
2459
  transform_context.available_bindings = new Map(saved_bindings);
2460
+ const loop_scoped_names = new Set(loop_params.map((/** @type {any} */ p) => p.name));
2234
2461
  for (const param of loop_params) {
2235
2462
  collect_pattern_bindings(param, transform_context.available_bindings);
2236
2463
  }
2464
+ validate_hook_safe_body_does_not_assign_hook_results_to_outer_bindings(
2465
+ original_loop_body,
2466
+ transform_context,
2467
+ loop_scoped_names,
2468
+ );
2237
2469
 
2238
2470
  const all_helper_bindings = get_referenced_helper_bindings(
2239
2471
  loop_body,
2240
2472
  transform_context.available_bindings,
2241
2473
  );
2242
- const loop_scoped_names = new Set(loop_params.map((/** @type {any} */ p) => p.name));
2243
2474
  const outer_bindings = all_helper_bindings.filter((b) => !loop_scoped_names.has(b.name));
2244
2475
  const loop_bindings = all_helper_bindings.filter((b) => loop_scoped_names.has(b.name));
2245
2476
 
@@ -2632,9 +2863,6 @@ function is_null_literal(node) {
2632
2863
  return node?.type === 'Literal' && node.value == null;
2633
2864
  }
2634
2865
 
2635
- const TEMPLATE_FRAGMENT_ERROR =
2636
- '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>.';
2637
-
2638
2866
  /**
2639
2867
  * @param {any} node
2640
2868
  * @param {TransformContext} transform_context
@@ -2643,13 +2871,7 @@ const TEMPLATE_FRAGMENT_ERROR =
2643
2871
  function to_jsx_element(node, transform_context, raw_children = node.children || []) {
2644
2872
  if (node.type === 'JSXElement') return node;
2645
2873
  if (!node.id) {
2646
- error(
2647
- TEMPLATE_FRAGMENT_ERROR,
2648
- transform_context.filename,
2649
- node,
2650
- transform_context.errors,
2651
- transform_context.comments,
2652
- );
2874
+ report_jsx_fragment_in_tsrx_error(node, transform_context);
2653
2875
  return set_loc(
2654
2876
  /** @type {any} */ ({
2655
2877
  type: 'JSXFragment',
@@ -2688,9 +2910,7 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
2688
2910
  }
2689
2911
  } else {
2690
2912
  if (walked_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
2691
- throw new Error(
2692
- `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
2693
- );
2913
+ return report_html_template_unsupported_error(node, transform_context);
2694
2914
  }
2695
2915
  children = create_element_children(walked_children, transform_context);
2696
2916
  }
@@ -2922,98 +3142,773 @@ function get_referenced_helper_bindings(body_nodes, available_bindings) {
2922
3142
 
2923
3143
  /**
2924
3144
  * @param {any[]} body_nodes
2925
- * @param {any} key_expression
2926
- * @param {any} source_node
2927
3145
  * @param {TransformContext} transform_context
2928
- * @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
2929
- * Used by the switch lift's chained-call build, which allocates ids in
2930
- * source order in a forward pass and then constructs helpers in reverse so
2931
- * each fall-through case can reference the next case's component element.
2932
- * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
3146
+ * @param {Set<string>} [local_binding_names]
3147
+ * @returns {void}
2933
3148
  */
2934
- function create_hook_safe_helper(
3149
+ function validate_hook_safe_body_does_not_assign_hook_results_to_outer_bindings(
2935
3150
  body_nodes,
2936
- key_expression,
2937
- source_node,
2938
3151
  transform_context,
2939
- preallocated_helper_id,
3152
+ local_binding_names,
2940
3153
  ) {
2941
- const helper_id =
2942
- preallocated_helper_id ??
2943
- create_generated_identifier(create_local_statement_component_name(transform_context));
2944
- const use_module_scoped_component = should_use_module_scoped_hook_components(transform_context);
2945
- const component_id = use_module_scoped_component
2946
- ? create_module_scoped_hook_component_id(helper_id, transform_context)
2947
- : helper_id;
2948
- const helper_bindings = get_referenced_helper_bindings(
2949
- body_nodes,
2950
- transform_context.available_bindings,
2951
- );
2952
- const aliases = use_module_scoped_component
2953
- ? []
2954
- : helper_bindings.map((binding) => create_helper_type_alias_declaration(helper_id, binding));
2955
- const props_type =
2956
- helper_bindings.length > 0 && !use_module_scoped_component
2957
- ? create_helper_props_type_literal(helper_bindings, aliases)
2958
- : null;
2959
- const params =
2960
- helper_bindings.length > 0
2961
- ? [
2962
- props_type !== null
2963
- ? create_typed_helper_props_pattern(helper_bindings, props_type)
2964
- : create_helper_props_pattern(helper_bindings),
2965
- ]
2966
- : [];
2967
-
2968
- const saved_bindings = transform_context.available_bindings;
2969
- transform_context.available_bindings = new Map(saved_bindings);
2970
-
2971
- const helper_fn = /** @type {any} */ ({
2972
- type: 'FunctionExpression',
2973
- id: clone_identifier(component_id),
2974
- params,
2975
- body: {
2976
- type: 'BlockStatement',
2977
- body: build_render_statements(body_nodes, true, transform_context),
2978
- metadata: { path: [] },
2979
- },
2980
- async: false,
2981
- generator: false,
2982
- metadata: {
2983
- path: [],
2984
- is_component: true,
2985
- is_method: false,
2986
- },
2987
- });
3154
+ if (!is_react_like_hook_platform(transform_context)) {
3155
+ return;
3156
+ }
3157
+ if (!body_contains_top_level_hook_call(body_nodes, transform_context, true)) {
3158
+ return;
3159
+ }
3160
+ if (!transform_context.available_bindings || transform_context.available_bindings.size === 0) {
3161
+ return;
3162
+ }
2988
3163
 
2989
- transform_context.available_bindings = saved_bindings;
3164
+ const shadowed_names = collect_block_binding_names(body_nodes);
3165
+ for (const name of local_binding_names || []) {
3166
+ shadowed_names.add(name);
3167
+ }
3168
+ validate_hook_outer_assignments_in_node(body_nodes, shadowed_names, transform_context, new Set());
3169
+ }
2990
3170
 
2991
- const component_element = create_helper_component_element(
2992
- component_id,
2993
- helper_bindings,
2994
- source_node,
2995
- {
2996
- mapWrapper: false,
2997
- mapBindingNames: false,
2998
- mapBindingValues: false,
2999
- },
3171
+ /**
3172
+ * @param {TransformContext} transform_context
3173
+ * @returns {boolean}
3174
+ */
3175
+ function is_react_like_hook_platform(transform_context) {
3176
+ return (
3177
+ transform_context.platform.name === 'React' || transform_context.platform.name === 'Preact'
3000
3178
  );
3179
+ }
3001
3180
 
3002
- if (key_expression) {
3003
- component_element.openingElement.attributes.push(
3004
- /** @type {any} */ ({
3005
- type: 'JSXAttribute',
3006
- name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
3007
- value: to_jsx_expression_container(key_expression, key_expression),
3008
- metadata: { path: [] },
3009
- }),
3010
- );
3181
+ /**
3182
+ * @param {any[]} statements
3183
+ * @returns {Set<string>}
3184
+ */
3185
+ function collect_block_binding_names(statements) {
3186
+ const names = new Set();
3187
+ for (const statement of statements || []) {
3188
+ collect_block_binding_names_from_statement(statement, names);
3011
3189
  }
3190
+ return names;
3191
+ }
3012
3192
 
3013
- if (!transform_context.helper_state) {
3014
- return {
3015
- setup_statements: [
3016
- ...aliases.map((alias) => alias.declaration),
3193
+ /**
3194
+ * @param {any} statement
3195
+ * @param {Set<string>} names
3196
+ * @returns {void}
3197
+ */
3198
+ function collect_block_binding_names_from_statement(statement, names) {
3199
+ if (!statement || typeof statement !== 'object') {
3200
+ return;
3201
+ }
3202
+
3203
+ if (statement.type === 'VariableDeclaration') {
3204
+ for (const declaration of statement.declarations || []) {
3205
+ collect_pattern_names(declaration.id, names);
3206
+ }
3207
+ return;
3208
+ }
3209
+
3210
+ if (
3211
+ (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
3212
+ statement.id?.type === 'Identifier'
3213
+ ) {
3214
+ names.add(statement.id.name);
3215
+ return;
3216
+ }
3217
+
3218
+ if (statement.type === 'ForOfStatement' || statement.type === 'ForInStatement') {
3219
+ if (statement.left?.type === 'VariableDeclaration' && statement.left.kind === 'var') {
3220
+ for (const declaration of statement.left.declarations || []) {
3221
+ collect_pattern_names(declaration.id, names);
3222
+ }
3223
+ }
3224
+ return;
3225
+ }
3226
+
3227
+ if (
3228
+ statement.type === 'ForStatement' &&
3229
+ statement.init?.type === 'VariableDeclaration' &&
3230
+ statement.init.kind === 'var'
3231
+ ) {
3232
+ for (const declaration of statement.init.declarations || []) {
3233
+ collect_pattern_names(declaration.id, names);
3234
+ }
3235
+ }
3236
+ }
3237
+
3238
+ /**
3239
+ * @param {any} pattern
3240
+ * @param {Set<string>} names
3241
+ * @returns {void}
3242
+ */
3243
+ function collect_pattern_names(pattern, names) {
3244
+ if (!pattern || typeof pattern !== 'object') {
3245
+ return;
3246
+ }
3247
+
3248
+ if (pattern.type === 'Identifier') {
3249
+ names.add(pattern.name);
3250
+ return;
3251
+ }
3252
+
3253
+ if (pattern.type === 'RestElement') {
3254
+ collect_pattern_names(pattern.argument, names);
3255
+ return;
3256
+ }
3257
+
3258
+ if (pattern.type === 'AssignmentPattern') {
3259
+ collect_pattern_names(pattern.left, names);
3260
+ return;
3261
+ }
3262
+
3263
+ if (pattern.type === 'ArrayPattern') {
3264
+ for (const element of pattern.elements || []) {
3265
+ collect_pattern_names(element, names);
3266
+ }
3267
+ return;
3268
+ }
3269
+
3270
+ if (pattern.type === 'ObjectPattern') {
3271
+ for (const property of pattern.properties || []) {
3272
+ if (property.type === 'RestElement') {
3273
+ collect_pattern_names(property.argument, names);
3274
+ } else {
3275
+ collect_pattern_names(property.value, names);
3276
+ }
3277
+ }
3278
+ }
3279
+ }
3280
+
3281
+ /**
3282
+ * @param {any} node
3283
+ * @param {Set<string>} shadowed_names
3284
+ * @param {TransformContext} transform_context
3285
+ * @param {Set<string>} hook_result_names
3286
+ * @returns {void}
3287
+ */
3288
+ function validate_hook_outer_assignments_in_node(
3289
+ node,
3290
+ shadowed_names,
3291
+ transform_context,
3292
+ hook_result_names,
3293
+ ) {
3294
+ if (!node || typeof node !== 'object') {
3295
+ return;
3296
+ }
3297
+
3298
+ if (Array.isArray(node)) {
3299
+ for (const child of node) {
3300
+ validate_hook_outer_assignments_in_node(
3301
+ child,
3302
+ shadowed_names,
3303
+ transform_context,
3304
+ hook_result_names,
3305
+ );
3306
+ }
3307
+ return;
3308
+ }
3309
+
3310
+ if (is_function_like_node(node)) {
3311
+ return;
3312
+ }
3313
+
3314
+ if (node.type === 'CallExpression' && is_hook_callee(node.callee)) {
3315
+ validate_hook_callback_outer_mutations(node, shadowed_names, transform_context);
3316
+ }
3317
+
3318
+ if (node.type === 'BlockStatement') {
3319
+ const next_shadowed = new Set(shadowed_names);
3320
+ const next_hook_result_names = new Set(hook_result_names);
3321
+ for (const name of collect_block_binding_names(node.body || [])) {
3322
+ next_shadowed.add(name);
3323
+ }
3324
+ for (const child of node.body || []) {
3325
+ validate_hook_outer_assignments_in_node(
3326
+ child,
3327
+ next_shadowed,
3328
+ transform_context,
3329
+ next_hook_result_names,
3330
+ );
3331
+ }
3332
+ return;
3333
+ }
3334
+
3335
+ if (node.type === 'VariableDeclaration') {
3336
+ for (const declaration of node.declarations || []) {
3337
+ if (
3338
+ declaration.init &&
3339
+ expression_contains_hook_derived_value(
3340
+ declaration.init,
3341
+ transform_context,
3342
+ hook_result_names,
3343
+ )
3344
+ ) {
3345
+ collect_pattern_names(declaration.id, hook_result_names);
3346
+ }
3347
+ validate_hook_outer_assignments_in_node(
3348
+ declaration.init,
3349
+ shadowed_names,
3350
+ transform_context,
3351
+ hook_result_names,
3352
+ );
3353
+ }
3354
+ return;
3355
+ }
3356
+
3357
+ if (
3358
+ node.type === 'AssignmentExpression' &&
3359
+ expression_contains_hook_derived_value(node.right, transform_context, hook_result_names)
3360
+ ) {
3361
+ const outer_names = get_referenced_outer_binding_names(
3362
+ node.left,
3363
+ transform_context.available_bindings,
3364
+ shadowed_names,
3365
+ );
3366
+ if (outer_names.length > 0) {
3367
+ report_hook_outer_assignment_error(
3368
+ node,
3369
+ outer_names,
3370
+ find_first_hook_call_name(node.right) || 'hook',
3371
+ transform_context,
3372
+ );
3373
+ }
3374
+ for (const name of get_referenced_local_binding_names(node.left, shadowed_names)) {
3375
+ hook_result_names.add(name);
3376
+ }
3377
+ }
3378
+
3379
+ if (node.type === 'ForOfStatement') {
3380
+ if (
3381
+ node.left &&
3382
+ node.left.type !== 'VariableDeclaration' &&
3383
+ expression_contains_hook_derived_value(node.right, transform_context, hook_result_names)
3384
+ ) {
3385
+ const outer_names = get_referenced_outer_binding_names(
3386
+ node.left,
3387
+ transform_context.available_bindings,
3388
+ shadowed_names,
3389
+ );
3390
+ if (outer_names.length > 0) {
3391
+ report_hook_outer_assignment_error(
3392
+ node,
3393
+ outer_names,
3394
+ find_first_hook_call_name(node.right) || 'hook',
3395
+ transform_context,
3396
+ );
3397
+ }
3398
+ for (const name of get_referenced_local_binding_names(node.left, shadowed_names)) {
3399
+ hook_result_names.add(name);
3400
+ }
3401
+ }
3402
+
3403
+ validate_hook_outer_assignments_in_node(
3404
+ node.right,
3405
+ shadowed_names,
3406
+ transform_context,
3407
+ hook_result_names,
3408
+ );
3409
+
3410
+ // Loop-declared bindings (`for (const x of …)`, `for (let x of …)`) live
3411
+ // only in the body. They are deliberately NOT in the enclosing block's
3412
+ // shadowed set (see collect_block_binding_names_from_statement), so add
3413
+ // them just for the body recursion to keep references to the loop var
3414
+ // from being flagged as outer-binding mutations.
3415
+ const body_shadowed = new Set(shadowed_names);
3416
+ if (node.left && node.left.type === 'VariableDeclaration') {
3417
+ for (const declaration of node.left.declarations || []) {
3418
+ collect_pattern_names(declaration.id, body_shadowed);
3419
+ }
3420
+ }
3421
+ validate_hook_outer_assignments_in_node(
3422
+ node.body,
3423
+ body_shadowed,
3424
+ transform_context,
3425
+ hook_result_names,
3426
+ );
3427
+ return;
3428
+ }
3429
+
3430
+ for (const key of Object.keys(node)) {
3431
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3432
+ continue;
3433
+ }
3434
+ validate_hook_outer_assignments_in_node(
3435
+ node[key],
3436
+ shadowed_names,
3437
+ transform_context,
3438
+ hook_result_names,
3439
+ );
3440
+ }
3441
+ }
3442
+
3443
+ /**
3444
+ * @param {any} call_node
3445
+ * @param {Set<string>} shadowed_names
3446
+ * @param {TransformContext} transform_context
3447
+ * @returns {void}
3448
+ */
3449
+ function validate_hook_callback_outer_mutations(call_node, shadowed_names, transform_context) {
3450
+ const hook_name = get_hook_callee_name(call_node.callee);
3451
+ for (const argument of call_node.arguments || []) {
3452
+ if (!is_function_like_node(argument)) {
3453
+ continue;
3454
+ }
3455
+ const callback_shadowed_names = create_function_like_shadowed_names(argument, shadowed_names);
3456
+ validate_hook_callback_outer_mutations_in_node(
3457
+ argument.body,
3458
+ callback_shadowed_names,
3459
+ transform_context,
3460
+ hook_name,
3461
+ );
3462
+ }
3463
+ }
3464
+
3465
+ /**
3466
+ * @param {any} node
3467
+ * @returns {boolean}
3468
+ */
3469
+ function is_function_like_node(node) {
3470
+ return (
3471
+ node.type === 'FunctionDeclaration' ||
3472
+ node.type === 'FunctionExpression' ||
3473
+ node.type === 'ArrowFunctionExpression' ||
3474
+ // this is just in case but we should already
3475
+ // have a component replaced with a function node
3476
+ node.type === 'Component'
3477
+ );
3478
+ }
3479
+
3480
+ /**
3481
+ * @param {any} node
3482
+ * @param {Set<string>} shadowed_names
3483
+ * @returns {Set<string>}
3484
+ */
3485
+ function create_function_like_shadowed_names(node, shadowed_names) {
3486
+ const next_shadowed_names = new Set(shadowed_names);
3487
+ for (const param of node.params || []) {
3488
+ collect_pattern_names(param, next_shadowed_names);
3489
+ }
3490
+ if (node.body?.type === 'BlockStatement') {
3491
+ for (const name of collect_block_binding_names(node.body.body || [])) {
3492
+ next_shadowed_names.add(name);
3493
+ }
3494
+ }
3495
+ return next_shadowed_names;
3496
+ }
3497
+
3498
+ /**
3499
+ * @param {any} node
3500
+ * @param {Set<string>} shadowed_names
3501
+ * @param {TransformContext} transform_context
3502
+ * @param {string} hook_name
3503
+ * @returns {void}
3504
+ */
3505
+ function validate_hook_callback_outer_mutations_in_node(
3506
+ node,
3507
+ shadowed_names,
3508
+ transform_context,
3509
+ hook_name,
3510
+ ) {
3511
+ if (!node || typeof node !== 'object') {
3512
+ return;
3513
+ }
3514
+
3515
+ if (Array.isArray(node)) {
3516
+ for (const child of node) {
3517
+ validate_hook_callback_outer_mutations_in_node(
3518
+ child,
3519
+ shadowed_names,
3520
+ transform_context,
3521
+ hook_name,
3522
+ );
3523
+ }
3524
+ return;
3525
+ }
3526
+
3527
+ if (is_function_like_node(node)) {
3528
+ validate_hook_callback_outer_mutations_in_node(
3529
+ node.body,
3530
+ create_function_like_shadowed_names(node, shadowed_names),
3531
+ transform_context,
3532
+ hook_name,
3533
+ );
3534
+ return;
3535
+ }
3536
+
3537
+ if (node.type === 'BlockStatement') {
3538
+ const next_shadowed_names = new Set(shadowed_names);
3539
+ for (const name of collect_block_binding_names(node.body || [])) {
3540
+ next_shadowed_names.add(name);
3541
+ }
3542
+ for (const child of node.body || []) {
3543
+ validate_hook_callback_outer_mutations_in_node(
3544
+ child,
3545
+ next_shadowed_names,
3546
+ transform_context,
3547
+ hook_name,
3548
+ );
3549
+ }
3550
+ return;
3551
+ }
3552
+
3553
+ if (node.type === 'AssignmentExpression') {
3554
+ const outer_names = get_referenced_outer_binding_names(
3555
+ node.left,
3556
+ transform_context.available_bindings,
3557
+ shadowed_names,
3558
+ );
3559
+ if (outer_names.length > 0) {
3560
+ report_hook_callback_outer_mutation_error(node, outer_names, hook_name, transform_context);
3561
+ }
3562
+ }
3563
+
3564
+ if (node.type === 'UpdateExpression') {
3565
+ const outer_names = get_referenced_outer_binding_names(
3566
+ node.argument,
3567
+ transform_context.available_bindings,
3568
+ shadowed_names,
3569
+ );
3570
+ if (outer_names.length > 0) {
3571
+ report_hook_callback_outer_mutation_error(node, outer_names, hook_name, transform_context);
3572
+ }
3573
+ }
3574
+
3575
+ for (const key of Object.keys(node)) {
3576
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3577
+ continue;
3578
+ }
3579
+ if (key === 'left' && node.type === 'AssignmentExpression') {
3580
+ continue;
3581
+ }
3582
+ if (key === 'argument' && node.type === 'UpdateExpression') {
3583
+ continue;
3584
+ }
3585
+ validate_hook_callback_outer_mutations_in_node(
3586
+ node[key],
3587
+ shadowed_names,
3588
+ transform_context,
3589
+ hook_name,
3590
+ );
3591
+ }
3592
+ }
3593
+
3594
+ /**
3595
+ * @param {any} node
3596
+ * @param {TransformContext} transform_context
3597
+ * @param {Set<string>} hook_result_names
3598
+ * @returns {boolean}
3599
+ */
3600
+ function expression_contains_hook_derived_value(node, transform_context, hook_result_names) {
3601
+ return (
3602
+ node_contains_top_level_hook_call(node, false, transform_context, true) ||
3603
+ references_name_in_set(node, hook_result_names)
3604
+ );
3605
+ }
3606
+
3607
+ /**
3608
+ * @param {any} node
3609
+ * @param {Set<string>} names
3610
+ * @returns {boolean}
3611
+ */
3612
+ function references_name_in_set(node, names) {
3613
+ if (!node || typeof node !== 'object' || names.size === 0) {
3614
+ return false;
3615
+ }
3616
+
3617
+ if (node.type === 'Identifier') {
3618
+ return names.has(node.name);
3619
+ }
3620
+
3621
+ if (
3622
+ node.type === 'FunctionDeclaration' ||
3623
+ node.type === 'FunctionExpression' ||
3624
+ node.type === 'ArrowFunctionExpression' ||
3625
+ node.type === 'Component'
3626
+ ) {
3627
+ return false;
3628
+ }
3629
+
3630
+ if (Array.isArray(node)) {
3631
+ return node.some((child) => references_name_in_set(child, names));
3632
+ }
3633
+
3634
+ for (const key of Object.keys(node)) {
3635
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3636
+ continue;
3637
+ }
3638
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) {
3639
+ continue;
3640
+ }
3641
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) {
3642
+ continue;
3643
+ }
3644
+ if (references_name_in_set(node[key], names)) {
3645
+ return true;
3646
+ }
3647
+ }
3648
+
3649
+ return false;
3650
+ }
3651
+
3652
+ /**
3653
+ * @param {any} node
3654
+ * @param {Set<string>} shadowed_names
3655
+ * @returns {string[]}
3656
+ */
3657
+ function get_referenced_local_binding_names(node, shadowed_names) {
3658
+ const names = new Set();
3659
+ collect_referenced_local_binding_names(node, shadowed_names, names);
3660
+ return [...names];
3661
+ }
3662
+
3663
+ /**
3664
+ * @param {any} node
3665
+ * @param {Set<string>} shadowed_names
3666
+ * @param {Set<string>} names
3667
+ * @returns {void}
3668
+ */
3669
+ function collect_referenced_local_binding_names(node, shadowed_names, names) {
3670
+ if (!node || typeof node !== 'object') {
3671
+ return;
3672
+ }
3673
+
3674
+ if (node.type === 'Identifier') {
3675
+ if (shadowed_names.has(node.name)) {
3676
+ names.add(node.name);
3677
+ }
3678
+ return;
3679
+ }
3680
+
3681
+ if (Array.isArray(node)) {
3682
+ for (const child of node) {
3683
+ collect_referenced_local_binding_names(child, shadowed_names, names);
3684
+ }
3685
+ return;
3686
+ }
3687
+
3688
+ for (const key of Object.keys(node)) {
3689
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3690
+ continue;
3691
+ }
3692
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) {
3693
+ continue;
3694
+ }
3695
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) {
3696
+ continue;
3697
+ }
3698
+ collect_referenced_local_binding_names(node[key], shadowed_names, names);
3699
+ }
3700
+ }
3701
+
3702
+ /**
3703
+ * @param {any} node
3704
+ * @param {Map<string, AST.Identifier>} available_bindings
3705
+ * @param {Set<string>} shadowed_names
3706
+ * @returns {string[]}
3707
+ */
3708
+ function get_referenced_outer_binding_names(node, available_bindings, shadowed_names) {
3709
+ const names = new Set();
3710
+ collect_referenced_outer_binding_names(node, available_bindings, shadowed_names, names);
3711
+ return [...names];
3712
+ }
3713
+
3714
+ /**
3715
+ * @param {any} node
3716
+ * @param {Map<string, AST.Identifier>} available_bindings
3717
+ * @param {Set<string>} shadowed_names
3718
+ * @param {Set<string>} names
3719
+ * @returns {void}
3720
+ */
3721
+ function collect_referenced_outer_binding_names(node, available_bindings, shadowed_names, names) {
3722
+ if (!node || typeof node !== 'object') {
3723
+ return;
3724
+ }
3725
+
3726
+ if (node.type === 'Identifier') {
3727
+ if (available_bindings.has(node.name) && !shadowed_names.has(node.name)) {
3728
+ names.add(node.name);
3729
+ }
3730
+ return;
3731
+ }
3732
+
3733
+ if (Array.isArray(node)) {
3734
+ for (const child of node) {
3735
+ collect_referenced_outer_binding_names(child, available_bindings, shadowed_names, names);
3736
+ }
3737
+ return;
3738
+ }
3739
+
3740
+ for (const key of Object.keys(node)) {
3741
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3742
+ continue;
3743
+ }
3744
+ if (key === 'property' && node.type === 'MemberExpression' && !node.computed) {
3745
+ continue;
3746
+ }
3747
+ if (key === 'key' && node.type === 'Property' && !node.computed && !node.shorthand) {
3748
+ continue;
3749
+ }
3750
+ collect_referenced_outer_binding_names(node[key], available_bindings, shadowed_names, names);
3751
+ }
3752
+ }
3753
+
3754
+ /**
3755
+ * @param {any} node
3756
+ * @returns {string | null}
3757
+ */
3758
+ function find_first_hook_call_name(node) {
3759
+ if (!node || typeof node !== 'object') {
3760
+ return null;
3761
+ }
3762
+
3763
+ if (node.type === 'CallExpression' && is_hook_callee(node.callee)) {
3764
+ return get_hook_callee_name(node.callee);
3765
+ }
3766
+
3767
+ if (
3768
+ node.type === 'FunctionDeclaration' ||
3769
+ node.type === 'FunctionExpression' ||
3770
+ node.type === 'ArrowFunctionExpression' ||
3771
+ node.type === 'Component'
3772
+ ) {
3773
+ return null;
3774
+ }
3775
+
3776
+ if (Array.isArray(node)) {
3777
+ for (const child of node) {
3778
+ const name = find_first_hook_call_name(child);
3779
+ if (name) return name;
3780
+ }
3781
+ return null;
3782
+ }
3783
+
3784
+ for (const key of Object.keys(node)) {
3785
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3786
+ continue;
3787
+ }
3788
+ const name = find_first_hook_call_name(node[key]);
3789
+ if (name) return name;
3790
+ }
3791
+
3792
+ return null;
3793
+ }
3794
+
3795
+ /**
3796
+ * @param {any} callee
3797
+ * @returns {string}
3798
+ */
3799
+ function get_hook_callee_name(callee) {
3800
+ if (callee?.type === 'Identifier') {
3801
+ return callee.name;
3802
+ }
3803
+ if (
3804
+ callee?.type === 'MemberExpression' &&
3805
+ !callee.computed &&
3806
+ callee.property?.type === 'Identifier'
3807
+ ) {
3808
+ return callee.property.name;
3809
+ }
3810
+ return 'hook';
3811
+ }
3812
+
3813
+ /**
3814
+ * @param {any[]} body_nodes
3815
+ * @param {any} key_expression
3816
+ * @param {any} source_node
3817
+ * @param {TransformContext} transform_context
3818
+ * @param {AST.Identifier} [preallocated_helper_id] - Optional pre-allocated id.
3819
+ * Used by the switch lift's chained-call build, which allocates ids in
3820
+ * source order in a forward pass and then constructs helpers in reverse so
3821
+ * each fall-through case can reference the next case's component element.
3822
+ * @returns {{ setup_statements: any[], component_element: ESTreeJSX.JSXElement }}
3823
+ */
3824
+ function create_hook_safe_helper(
3825
+ body_nodes,
3826
+ key_expression,
3827
+ source_node,
3828
+ transform_context,
3829
+ preallocated_helper_id,
3830
+ ) {
3831
+ validate_hook_safe_body_does_not_assign_hook_results_to_outer_bindings(
3832
+ body_nodes,
3833
+ transform_context,
3834
+ );
3835
+
3836
+ const helper_id =
3837
+ preallocated_helper_id ??
3838
+ create_generated_identifier(create_local_statement_component_name(transform_context));
3839
+ const use_module_scoped_component = should_use_module_scoped_hook_components(transform_context);
3840
+ const component_id = use_module_scoped_component
3841
+ ? create_module_scoped_hook_component_id(helper_id, transform_context)
3842
+ : helper_id;
3843
+ const helper_bindings = get_referenced_helper_bindings(
3844
+ body_nodes,
3845
+ transform_context.available_bindings,
3846
+ );
3847
+ const aliases = use_module_scoped_component
3848
+ ? []
3849
+ : helper_bindings.map((binding) => create_helper_type_alias_declaration(helper_id, binding));
3850
+ const props_type =
3851
+ helper_bindings.length > 0 && !use_module_scoped_component
3852
+ ? create_helper_props_type_literal(helper_bindings, aliases)
3853
+ : null;
3854
+ const params =
3855
+ helper_bindings.length > 0
3856
+ ? [
3857
+ props_type !== null
3858
+ ? create_typed_helper_props_pattern(helper_bindings, props_type)
3859
+ : create_helper_props_pattern(helper_bindings),
3860
+ ]
3861
+ : [];
3862
+
3863
+ const saved_bindings = transform_context.available_bindings;
3864
+ transform_context.available_bindings = new Map(saved_bindings);
3865
+
3866
+ const helper_fn = /** @type {any} */ ({
3867
+ type: 'FunctionExpression',
3868
+ id: clone_identifier(component_id),
3869
+ params,
3870
+ body: {
3871
+ type: 'BlockStatement',
3872
+ body: build_render_statements(body_nodes, true, transform_context),
3873
+ metadata: { path: [] },
3874
+ },
3875
+ async: false,
3876
+ generator: false,
3877
+ metadata: {
3878
+ path: [],
3879
+ is_component: true,
3880
+ is_method: false,
3881
+ },
3882
+ });
3883
+
3884
+ transform_context.available_bindings = saved_bindings;
3885
+
3886
+ const component_element = create_helper_component_element(
3887
+ component_id,
3888
+ helper_bindings,
3889
+ source_node,
3890
+ {
3891
+ mapWrapper: false,
3892
+ mapBindingNames: false,
3893
+ mapBindingValues: false,
3894
+ },
3895
+ );
3896
+
3897
+ if (key_expression) {
3898
+ component_element.openingElement.attributes.push(
3899
+ /** @type {any} */ ({
3900
+ type: 'JSXAttribute',
3901
+ name: { type: 'JSXIdentifier', name: 'key', metadata: { path: [] } },
3902
+ value: to_jsx_expression_container(key_expression, key_expression),
3903
+ metadata: { path: [] },
3904
+ }),
3905
+ );
3906
+ }
3907
+
3908
+ if (!transform_context.helper_state) {
3909
+ return {
3910
+ setup_statements: [
3911
+ ...aliases.map((alias) => alias.declaration),
3017
3912
  create_helper_declaration(helper_id, helper_fn, source_node, transform_context),
3018
3913
  ],
3019
3914
  component_element,
@@ -3410,9 +4305,7 @@ function to_jsx_child(node, transform_context) {
3410
4305
  case 'TSRXExpression':
3411
4306
  return to_jsx_expression_container(node.expression, node);
3412
4307
  case 'Html':
3413
- throw new Error(
3414
- `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
3415
- );
4308
+ return report_html_template_unsupported_error(node, transform_context);
3416
4309
  case 'IfStatement':
3417
4310
  return (
3418
4311
  transform_context.platform.hooks?.controlFlow?.ifStatement ?? if_statement_to_jsx_child
@@ -4227,9 +5120,16 @@ function try_statement_to_jsx_child(node, transform_context) {
4227
5120
  // correctly identifies references to err/reset as non-static
4228
5121
  const saved_catch_bindings = transform_context.available_bindings;
4229
5122
  transform_context.available_bindings = new Map(saved_catch_bindings);
5123
+ const catch_scoped_names = new Set();
4230
5124
  for (const param of catch_params) {
4231
5125
  collect_pattern_bindings(param, transform_context.available_bindings);
5126
+ collect_pattern_names(param, catch_scoped_names);
4232
5127
  }
5128
+ validate_hook_safe_body_does_not_assign_hook_results_to_outer_bindings(
5129
+ catch_body_nodes,
5130
+ transform_context,
5131
+ catch_scoped_names,
5132
+ );
4233
5133
 
4234
5134
  const fallback_fn = {
4235
5135
  type: 'ArrowFunctionExpression',