@tsrx/core 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.23",
6
+ "version": "0.0.25",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -7,6 +7,18 @@ import { error } from '../errors.js';
7
7
 
8
8
  export const COMPONENT_RETURN_VALUE_ERROR =
9
9
  'Return statements inside components cannot have a return value.';
10
+ export const COMPONENT_LOOP_RETURN_ERROR =
11
+ 'Return statements are not allowed inside component for...of loops. Use continue instead.';
12
+ export const COMPONENT_LOOP_BREAK_ERROR =
13
+ 'Break statements are not allowed inside component for...of loops.';
14
+ export const COMPONENT_FOR_STATEMENT_ERROR =
15
+ 'For loops are not supported in components. Use for...of instead.';
16
+ export const COMPONENT_FOR_IN_STATEMENT_ERROR =
17
+ 'For...in loops are not supported in components. Use for...of instead.';
18
+ export const COMPONENT_WHILE_STATEMENT_ERROR =
19
+ 'While loops are not supported in components. Move the while loop into a function.';
20
+ export const COMPONENT_DO_WHILE_STATEMENT_ERROR =
21
+ 'Do...while loops are not supported in components. Move the do...while loop into a function.';
10
22
 
11
23
  const invalid_nestings = {
12
24
  // <p> cannot contain block-level elements
@@ -133,19 +145,29 @@ function get_element_tag(element) {
133
145
  * @returns {AST.ReturnStatement}
134
146
  */
135
147
  export function get_return_keyword_node(node) {
136
- const return_keyword_length = 'return'.length;
148
+ return get_statement_keyword_node(node, 'return');
149
+ }
150
+
151
+ /**
152
+ * @template {AST.Node} T
153
+ * @param {T} node
154
+ * @param {string} keyword
155
+ * @returns {T}
156
+ */
157
+ export function get_statement_keyword_node(node, keyword) {
158
+ const keyword_length = keyword.length;
137
159
  const start = /** @type {AST.NodeWithLocation} */ (node).start ?? 0;
138
160
  const loc = /** @type {AST.NodeWithLocation} */ (node).loc;
139
161
 
140
- return /** @type {AST.ReturnStatement} */ ({
162
+ return /** @type {T} */ ({
141
163
  ...node,
142
- end: start + return_keyword_length,
164
+ end: start + keyword_length,
143
165
  loc: loc
144
166
  ? {
145
167
  start: loc.start,
146
168
  end: {
147
169
  line: loc.start.line,
148
- column: loc.start.column + return_keyword_length,
170
+ column: loc.start.column + keyword_length,
149
171
  },
150
172
  }
151
173
  : undefined,
@@ -172,6 +194,59 @@ export function validate_component_return_statement(node, filename, errors, comm
172
194
  );
173
195
  }
174
196
 
197
+ /**
198
+ * @param {AST.ReturnStatement} node
199
+ * @param {string | null | undefined} filename
200
+ * @param {CompileError[]} [errors]
201
+ * @param {AST.CommentWithLocation[]} [comments]
202
+ */
203
+ export function validate_component_loop_return_statement(node, filename, errors, comments) {
204
+ error(
205
+ COMPONENT_LOOP_RETURN_ERROR,
206
+ filename ?? null,
207
+ get_return_keyword_node(node),
208
+ errors,
209
+ comments,
210
+ );
211
+ }
212
+
213
+ /**
214
+ * @param {AST.BreakStatement} node
215
+ * @param {string | null | undefined} filename
216
+ * @param {CompileError[]} [errors]
217
+ * @param {AST.CommentWithLocation[]} [comments]
218
+ */
219
+ export function validate_component_loop_break_statement(node, filename, errors, comments) {
220
+ error(
221
+ COMPONENT_LOOP_BREAK_ERROR,
222
+ filename ?? null,
223
+ get_statement_keyword_node(node, 'break'),
224
+ errors,
225
+ comments,
226
+ );
227
+ }
228
+
229
+ /**
230
+ * @param {AST.ForStatement | AST.ForInStatement | AST.WhileStatement | AST.DoWhileStatement} node
231
+ * @param {string | null | undefined} filename
232
+ * @param {CompileError[]} [errors]
233
+ * @param {AST.CommentWithLocation[]} [comments]
234
+ */
235
+ export function validate_component_unsupported_loop_statement(node, filename, errors, comments) {
236
+ let message;
237
+ if (node.type === 'ForStatement') {
238
+ message = COMPONENT_FOR_STATEMENT_ERROR;
239
+ } else if (node.type === 'ForInStatement') {
240
+ message = COMPONENT_FOR_IN_STATEMENT_ERROR;
241
+ } else if (node.type === 'WhileStatement') {
242
+ message = COMPONENT_WHILE_STATEMENT_ERROR;
243
+ } else {
244
+ message = COMPONENT_DO_WHILE_STATEMENT_ERROR;
245
+ }
246
+
247
+ error(message, filename ?? null, node, errors, comments);
248
+ }
249
+
175
250
  /**
176
251
  * @param {AST.Element} element
177
252
  * @param {AnalysisContext} context
package/src/index.js CHANGED
@@ -133,13 +133,17 @@ export {
133
133
  // Sanitize
134
134
  export { sanitize_template_string as sanitizeTemplateString } from './utils/sanitize_template_string.js';
135
135
 
136
+ // CSS Property Name
137
+ export { normalize_css_property_name as normalizeCssPropertyName } from './utils/normalize_css_property_name.js';
138
+
136
139
  // Escaping
137
- export { escape } from './utils/escaping.js';
140
+ export { escape, escape_script as escapeScript } from './utils/escaping.js';
138
141
 
139
142
  // Transform
140
143
  export {
141
144
  createJsxTransform,
142
145
  merge_duplicate_refs as mergeDuplicateRefs,
146
+ rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
143
147
  to_jsx_attribute as toJsxAttribute,
144
148
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
145
149
  component_to_function_declaration as componentToFunctionDeclaration,
@@ -209,8 +213,18 @@ export {
209
213
  // Analyze
210
214
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
211
215
  export {
216
+ COMPONENT_DO_WHILE_STATEMENT_ERROR,
217
+ COMPONENT_FOR_IN_STATEMENT_ERROR,
218
+ COMPONENT_FOR_STATEMENT_ERROR,
219
+ COMPONENT_LOOP_BREAK_ERROR,
220
+ COMPONENT_LOOP_RETURN_ERROR,
212
221
  COMPONENT_RETURN_VALUE_ERROR,
222
+ COMPONENT_WHILE_STATEMENT_ERROR,
213
223
  get_return_keyword_node as getReturnKeywordNode,
224
+ get_statement_keyword_node as getStatementKeywordNode,
225
+ validate_component_loop_break_statement as validateComponentLoopBreakStatement,
226
+ validate_component_loop_return_statement as validateComponentLoopReturnStatement,
214
227
  validate_component_return_statement as validateComponentReturnStatement,
228
+ validate_component_unsupported_loop_statement as validateComponentUnsupportedLoopStatement,
215
229
  validate_nesting as validateNesting,
216
230
  } from './analyze/validation.js';
package/src/plugin.js CHANGED
@@ -269,6 +269,56 @@ export function TSRXPlugin(config) {
269
269
  return null;
270
270
  }
271
271
 
272
+ #popTsxTokenContextBeforeTemplateExpressionChild() {
273
+ let index = this.pos;
274
+ let has_newline = false;
275
+
276
+ // Text-only Tsx nodes can leave the tokenizer in JSX text mode.
277
+ // Only unwind it for ASI before a following TSRX `{expr}` child;
278
+ // fragment props like `content={<></>}` still need the JSX context.
279
+ while (index < this.input.length) {
280
+ const ch = this.input.charCodeAt(index);
281
+ if (ch === 32 || ch === 9) {
282
+ index++;
283
+ } else if (ch === 10 || ch === 13) {
284
+ has_newline = true;
285
+ index++;
286
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 42) {
287
+ const end = this.input.indexOf('*/', index + 2);
288
+ const comment_end = end === -1 ? this.input.length : end + 2;
289
+ if (this.input.slice(index, comment_end).match(regex_newline_characters)) {
290
+ has_newline = true;
291
+ }
292
+ index = comment_end;
293
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 47) {
294
+ has_newline = true;
295
+ index += 2;
296
+ while (index < this.input.length) {
297
+ const comment_ch = this.input.charCodeAt(index);
298
+ if (comment_ch === 10 || comment_ch === 13) break;
299
+ index++;
300
+ }
301
+ } else {
302
+ break;
303
+ }
304
+ }
305
+
306
+ if (!has_newline || this.input.charCodeAt(index) !== 123) {
307
+ return;
308
+ }
309
+
310
+ const context_index = this.context.lastIndexOf(tstc.tc_expr);
311
+ if (context_index !== -1) {
312
+ this.context.length = context_index;
313
+ }
314
+ }
315
+
316
+ #popTemplateLiteralTokenContext() {
317
+ while (this.curContext()?.token === '`') {
318
+ this.context.pop();
319
+ }
320
+ }
321
+
272
322
  #isDoubleQuotedTextChildStart() {
273
323
  if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
274
324
  return false;
@@ -1053,6 +1103,19 @@ export function TSRXPlugin(config) {
1053
1103
  this.parseFunctionParams(node);
1054
1104
  this.checkComponentParams(node.params);
1055
1105
 
1106
+ const is_arrow_component = this.type === tt.arrow;
1107
+ if (is_arrow_component) {
1108
+ if (node.id || requireName || skipName) {
1109
+ this.raise(
1110
+ this.start,
1111
+ 'Arrow component syntax is only supported for anonymous component expressions.',
1112
+ );
1113
+ }
1114
+ node.metadata ??= { path: [] };
1115
+ node.metadata.arrow = true;
1116
+ this.next();
1117
+ }
1118
+
1056
1119
  // Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
1057
1120
  // the component body's first token as if we'd entered fresh — no
1058
1121
  // surrounding function body should affect our parseStatement/parseBlock
@@ -2009,6 +2072,7 @@ export function TSRXPlugin(config) {
2009
2072
  if (this.type !== tstt.jsxTagEnd) {
2010
2073
  raise_error();
2011
2074
  }
2075
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2012
2076
  this.next();
2013
2077
  }
2014
2078
  }
@@ -2194,6 +2258,7 @@ export function TSRXPlugin(config) {
2194
2258
  if (this.type !== tstt.jsxTagEnd) {
2195
2259
  raise_error();
2196
2260
  }
2261
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2197
2262
  this.next();
2198
2263
  }
2199
2264
  } else if (element.type === 'TsxCompat') {
@@ -2225,6 +2290,7 @@ export function TSRXPlugin(config) {
2225
2290
  if (this.type !== tstt.jsxTagEnd) {
2226
2291
  raise_error();
2227
2292
  }
2293
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2228
2294
  this.next();
2229
2295
  }
2230
2296
  } else if (this.#path[this.#path.length - 1] === element) {
@@ -2323,7 +2389,7 @@ export function TSRXPlugin(config) {
2323
2389
  body.push(node);
2324
2390
  } else if (this.type === tstt.jsxTagStart) {
2325
2391
  // Parse JSX element
2326
- const node = super.parseExpression();
2392
+ const node = super.jsx_parseElement();
2327
2393
  body.push(node);
2328
2394
  } else {
2329
2395
  const start = this.start;
@@ -2354,6 +2420,7 @@ export function TSRXPlugin(config) {
2354
2420
  body.push(node);
2355
2421
  }
2356
2422
 
2423
+ this.#popTemplateLiteralTokenContext();
2357
2424
  // Always call next() to ensure parser makes progress
2358
2425
  this.next();
2359
2426
  }
@@ -2386,7 +2453,7 @@ export function TSRXPlugin(config) {
2386
2453
  body.push(node);
2387
2454
  } else if (this.type === tstt.jsxTagStart) {
2388
2455
  // Parse JSX element
2389
- const node = super.parseExpression();
2456
+ const node = super.jsx_parseElement();
2390
2457
  body.push(node);
2391
2458
  } else {
2392
2459
  const start = this.start;
@@ -2417,6 +2484,7 @@ export function TSRXPlugin(config) {
2417
2484
  body.push(node);
2418
2485
  }
2419
2486
 
2487
+ this.#popTemplateLiteralTokenContext();
2420
2488
  this.next();
2421
2489
  }
2422
2490
  }
@@ -2701,6 +2769,19 @@ export function TSRXPlugin(config) {
2701
2769
  return node;
2702
2770
  }
2703
2771
 
2772
+ if (
2773
+ this.#functionBodyDepth === 0 &&
2774
+ this.type === tt.string &&
2775
+ this.input.charCodeAt(this.start) === 34 &&
2776
+ (this.#path.at(-1)?.type === 'Component' || this.#path.at(-1)?.type === 'Element')
2777
+ ) {
2778
+ this.pos = this.start;
2779
+ this.#readDoubleQuotedTextChildToken();
2780
+ const node = this.parseDoubleQuotedTextChild();
2781
+ this.semicolon();
2782
+ return node;
2783
+ }
2784
+
2704
2785
  // &[ or &{ at statement level — lazy destructuring assignment
2705
2786
  // e.g., &[data] = track(0); or &{x, y} = obj;
2706
2787
  if (this.type === tt.bitwiseAND) {
@@ -40,7 +40,12 @@ import {
40
40
  } from '../lazy.js';
41
41
  import { find_first_top_level_await_in_component_body } from '../await.js';
42
42
  import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
43
- import { validate_component_return_statement } from '../../analyze/validation.js';
43
+ import {
44
+ validate_component_loop_break_statement,
45
+ validate_component_loop_return_statement,
46
+ validate_component_return_statement,
47
+ validate_component_unsupported_loop_statement,
48
+ } from '../../analyze/validation.js';
44
49
  import { get_component_from_path } from '../../utils/ast.js';
45
50
  import {
46
51
  is_interleaved_body as is_interleaved_body_core,
@@ -61,6 +66,63 @@ import { is_hoist_safe_jsx_node } from '../jsx-hoist.js';
61
66
  * @typedef {{ source_name: string, read: () => any }} LazyBinding
62
67
  */
63
68
 
69
+ /**
70
+ * @param {any} node
71
+ * @returns {boolean}
72
+ */
73
+ function is_function_or_class_boundary(node) {
74
+ return (
75
+ node?.type === 'FunctionDeclaration' ||
76
+ node?.type === 'FunctionExpression' ||
77
+ node?.type === 'ArrowFunctionExpression' ||
78
+ node?.type === 'ClassDeclaration' ||
79
+ node?.type === 'ClassExpression'
80
+ );
81
+ }
82
+
83
+ /**
84
+ * @param {any[]} path
85
+ * @returns {boolean}
86
+ */
87
+ function is_inside_component_for_of(path) {
88
+ for (let i = path.length - 1; i >= 0; i -= 1) {
89
+ const node = path[i];
90
+ if (is_function_or_class_boundary(node) || node?.type === 'Component') {
91
+ return false;
92
+ }
93
+ if (node?.type === 'ForOfStatement') {
94
+ return true;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * @param {any[]} path
102
+ * @returns {boolean}
103
+ */
104
+ function break_targets_component_loop(path) {
105
+ for (let i = path.length - 1; i >= 0; i -= 1) {
106
+ const node = path[i];
107
+ if (is_function_or_class_boundary(node) || node?.type === 'Component') {
108
+ return false;
109
+ }
110
+ if (node?.type === 'SwitchStatement') {
111
+ return false;
112
+ }
113
+ if (
114
+ node?.type === 'ForOfStatement' ||
115
+ node?.type === 'ForStatement' ||
116
+ node?.type === 'ForInStatement' ||
117
+ node?.type === 'WhileStatement' ||
118
+ node?.type === 'DoWhileStatement'
119
+ ) {
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+
64
126
  /**
65
127
  * Build a `transform()` function for a specific JSX platform (React, Preact,
66
128
  * Solid). Given a `JsxPlatform` descriptor, returns a transform that parses
@@ -104,6 +166,7 @@ export function createJsxTransform(platform) {
104
166
  needs_error_boundary: false,
105
167
  needs_suspense: false,
106
168
  needs_merge_refs: false,
169
+ needs_fragment: false,
107
170
  helper_state: null,
108
171
  available_bindings: new Map(),
109
172
  lazy_next_id: 0,
@@ -122,7 +185,81 @@ export function createJsxTransform(platform) {
122
185
  walk(/** @type {any} */ (ast), transform_context, {
123
186
  ReturnStatement(node, { next, path }) {
124
187
  if (get_component_from_path(path)) {
125
- validate_component_return_statement(
188
+ if (is_inside_component_for_of(path)) {
189
+ validate_component_loop_return_statement(
190
+ node,
191
+ filename,
192
+ transform_context.errors,
193
+ transform_context.comments,
194
+ );
195
+ } else {
196
+ validate_component_return_statement(
197
+ node,
198
+ filename,
199
+ transform_context.errors,
200
+ transform_context.comments,
201
+ );
202
+ }
203
+ }
204
+
205
+ return next();
206
+ },
207
+
208
+ BreakStatement(node, { next, path }) {
209
+ if (get_component_from_path(path) && break_targets_component_loop(path)) {
210
+ validate_component_loop_break_statement(
211
+ node,
212
+ filename,
213
+ transform_context.errors,
214
+ transform_context.comments,
215
+ );
216
+ }
217
+
218
+ return next();
219
+ },
220
+
221
+ ForStatement(node, { next, path }) {
222
+ if (get_component_from_path(path)) {
223
+ validate_component_unsupported_loop_statement(
224
+ node,
225
+ filename,
226
+ transform_context.errors,
227
+ transform_context.comments,
228
+ );
229
+ }
230
+
231
+ return next();
232
+ },
233
+
234
+ ForInStatement(node, { next, path }) {
235
+ if (get_component_from_path(path)) {
236
+ validate_component_unsupported_loop_statement(
237
+ node,
238
+ filename,
239
+ transform_context.errors,
240
+ transform_context.comments,
241
+ );
242
+ }
243
+
244
+ return next();
245
+ },
246
+
247
+ WhileStatement(node, { next, path }) {
248
+ if (get_component_from_path(path)) {
249
+ validate_component_unsupported_loop_statement(
250
+ node,
251
+ filename,
252
+ transform_context.errors,
253
+ transform_context.comments,
254
+ );
255
+ }
256
+
257
+ return next();
258
+ },
259
+
260
+ DoWhileStatement(node, { next, path }) {
261
+ if (get_component_from_path(path)) {
262
+ validate_component_unsupported_loop_statement(
126
263
  node,
127
264
  filename,
128
265
  transform_context.errors,
@@ -350,7 +487,7 @@ function has_use_server_directive(program) {
350
487
  * @param {any} component
351
488
  * @param {TransformContext} transform_context
352
489
  * @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} [walk_helper_state]
353
- * @returns {AST.FunctionDeclaration}
490
+ * @returns {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression}
354
491
  */
355
492
  export function component_to_function_declaration(component, transform_context, walk_helper_state) {
356
493
  const helper_state = walk_helper_state || create_helper_state(component.id?.name || 'Component');
@@ -390,28 +527,62 @@ export function component_to_function_declaration(component, transform_context,
390
527
  const final_body =
391
528
  lazy_bindings.size > 0 ? apply_lazy_transforms(body_block, lazy_bindings) : body_block;
392
529
 
393
- const fn = /** @type {any} */ ({
394
- type: 'FunctionDeclaration',
395
- id: component.id,
396
- typeParameters: component.typeParameters,
397
- params: final_params,
398
- body: final_body,
399
- async: is_async_component,
400
- generator: false,
401
- metadata: {
402
- path: [],
403
- is_component: true,
404
- },
405
- });
530
+ /** @type {AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression} */
531
+ let fn;
532
+
533
+ if (component.id) {
534
+ fn = /** @type {any} */ ({
535
+ type: 'FunctionDeclaration',
536
+ id: component.id,
537
+ typeParameters: component.typeParameters,
538
+ params: final_params,
539
+ body: final_body,
540
+ async: is_async_component,
541
+ generator: false,
542
+ metadata: {
543
+ path: [],
544
+ is_component: true,
545
+ },
546
+ });
547
+ } else if (component.metadata?.arrow) {
548
+ fn = /** @type {any} */ ({
549
+ type: 'ArrowFunctionExpression',
550
+ typeParameters: component.typeParameters,
551
+ params: final_params,
552
+ body: final_body,
553
+ async: is_async_component,
554
+ generator: false,
555
+ expression: false,
556
+ metadata: {
557
+ path: [],
558
+ is_component: true,
559
+ },
560
+ });
561
+ } else {
562
+ fn = /** @type {any} */ ({
563
+ type: 'FunctionExpression',
564
+ id: null,
565
+ typeParameters: component.typeParameters,
566
+ params: final_params,
567
+ body: final_body,
568
+ async: is_async_component,
569
+ generator: false,
570
+ metadata: {
571
+ path: [],
572
+ is_component: true,
573
+ },
574
+ });
575
+ }
406
576
 
407
577
  // Restore context
408
578
  transform_context.helper_state = saved_helper_state;
409
579
  transform_context.available_bindings = saved_bindings;
410
580
 
411
- fn.metadata.generated_helpers = helper_state.helpers;
412
- fn.metadata.generated_statics = helper_state.statics;
581
+ const fn_metadata = /** @type {any} */ (fn.metadata);
582
+ fn_metadata.generated_helpers = helper_state.helpers;
583
+ fn_metadata.generated_statics = helper_state.statics;
413
584
 
414
- if (fn.id) {
585
+ if (fn.type === 'FunctionDeclaration' && fn.id) {
415
586
  fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
416
587
  ...fn.id.metadata,
417
588
  is_component: true,
@@ -440,6 +611,7 @@ function build_component_statements(body_nodes, transform_context) {
440
611
  function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
441
612
  const statements = [];
442
613
  const render_nodes = [];
614
+ let has_bare_return = false;
443
615
 
444
616
  // Create a new bindings map so inner-scope bindings from
445
617
  // collect_statement_bindings don't leak to the caller's scope.
@@ -460,6 +632,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
460
632
  if (is_bare_return_statement(child)) {
461
633
  statements.push(create_component_return_statement(render_nodes, child));
462
634
  render_nodes.length = 0;
635
+ has_bare_return = true;
463
636
  continue;
464
637
  }
465
638
 
@@ -708,7 +881,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
708
881
  }
709
882
 
710
883
  const return_arg = build_return_expression(render_nodes);
711
- if (return_arg || return_null_when_empty) {
884
+ if (return_arg || (return_null_when_empty && !has_bare_return)) {
712
885
  statements.push({
713
886
  type: 'ReturnStatement',
714
887
  argument: return_arg || { type: 'Literal', value: null, raw: 'null' },
@@ -1158,9 +1331,9 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
1158
1331
  */
1159
1332
  function expand_component_helpers(program) {
1160
1333
  program.body = program.body.flatMap((statement) => {
1161
- const meta = get_generated_component_metadata(statement);
1162
- const statics = meta?.generated_statics || [];
1163
- const helpers = meta?.generated_helpers || [];
1334
+ const metas = get_generated_component_metadata_list(statement);
1335
+ const statics = metas.flatMap((meta) => meta.generated_statics || []);
1336
+ const helpers = metas.flatMap((meta) => meta.generated_helpers || []);
1164
1337
  if (statics.length || helpers.length) {
1165
1338
  return [...statics, ...helpers, statement];
1166
1339
  }
@@ -1173,30 +1346,63 @@ function expand_component_helpers(program) {
1173
1346
 
1174
1347
  /**
1175
1348
  * Component hooks may replace a `Component` node with a function declaration,
1176
- * variable declaration, or export-safe expression. Generated helper/statics
1177
- * metadata is carried on whichever replacement node the hook returns, so
1178
- * helper expansion must read metadata from that broader set.
1349
+ * variable declaration, object literal member, or export-safe expression.
1350
+ * Generated helper/statics metadata is carried on whichever replacement node
1351
+ * the hook returns, so helper expansion must read metadata from that broader
1352
+ * set.
1179
1353
  *
1180
1354
  * @param {any} node
1181
- * @returns {{ generated_helpers?: any[], generated_statics?: any[] } | null}
1355
+ * @returns {{ generated_helpers?: any[], generated_statics?: any[] }[]}
1182
1356
  */
1183
- function get_generated_component_metadata(node) {
1184
- if (!node || typeof node !== 'object') {
1185
- return null;
1186
- }
1357
+ function get_generated_component_metadata_list(node) {
1358
+ /** @type {{ generated_helpers?: any[], generated_statics?: any[] }[]} */
1359
+ const metas = [];
1360
+ const seen_nodes = new Set();
1361
+ const seen_metas = new Set();
1362
+
1363
+ /** @param {any} current */
1364
+ const visit = (current) => {
1365
+ if (!current || typeof current !== 'object' || seen_nodes.has(current)) {
1366
+ return;
1367
+ }
1187
1368
 
1188
- if (node.metadata?.generated_helpers || node.metadata?.generated_statics) {
1189
- return node.metadata;
1190
- }
1369
+ seen_nodes.add(current);
1191
1370
 
1192
- if (
1193
- (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') &&
1194
- node.declaration?.metadata
1195
- ) {
1196
- return node.declaration.metadata;
1197
- }
1371
+ if (current.metadata?.generated_helpers || current.metadata?.generated_statics) {
1372
+ if (!seen_metas.has(current.metadata)) {
1373
+ seen_metas.add(current.metadata);
1374
+ metas.push(current.metadata);
1375
+ }
1376
+ return;
1377
+ }
1378
+
1379
+ if (
1380
+ current.type === 'FunctionDeclaration' ||
1381
+ current.type === 'FunctionExpression' ||
1382
+ current.type === 'ArrowFunctionExpression'
1383
+ ) {
1384
+ return;
1385
+ }
1386
+
1387
+ for (const key of Object.keys(current)) {
1388
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1389
+ continue;
1390
+ }
1391
+
1392
+ const value = current[key];
1393
+ if (Array.isArray(value)) {
1394
+ for (const child of value) {
1395
+ visit(child);
1396
+ }
1397
+ } else {
1398
+ visit(value);
1399
+ }
1400
+ }
1401
+ };
1402
+
1403
+ visit(node);
1198
1404
 
1199
- return null;
1405
+ return metas;
1200
1406
  }
1201
1407
 
1202
1408
  /**
@@ -1339,6 +1545,126 @@ function append_tail_invocation(body, tail_helper) {
1339
1545
  return [...body, clone_tail_invocation(tail_helper)];
1340
1546
  }
1341
1547
 
1548
+ /**
1549
+ * @param {AST.Identifier} tail_synthetic_id
1550
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1551
+ * @returns {any}
1552
+ */
1553
+ function create_loop_tail_expression(tail_synthetic_id, tail_helper) {
1554
+ return b.logical('&&', clone_identifier(tail_synthetic_id), clone_tail_invocation(tail_helper));
1555
+ }
1556
+
1557
+ /**
1558
+ * @param {AST.Identifier} tail_synthetic_id
1559
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1560
+ * @returns {any}
1561
+ */
1562
+ function create_loop_tail_conditional(tail_synthetic_id, tail_helper) {
1563
+ return b.conditional(
1564
+ clone_identifier(tail_synthetic_id),
1565
+ clone_tail_invocation(tail_helper),
1566
+ create_null_literal(),
1567
+ );
1568
+ }
1569
+
1570
+ /**
1571
+ * @param {any[]} statements
1572
+ * @param {AST.Identifier} tail_synthetic_id
1573
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1574
+ * @returns {void}
1575
+ */
1576
+ function append_loop_tail_to_return_statements(statements, tail_synthetic_id, tail_helper) {
1577
+ for (const statement of statements) {
1578
+ append_loop_tail_to_return_statement(statement, tail_synthetic_id, tail_helper, false);
1579
+ }
1580
+ }
1581
+
1582
+ /**
1583
+ * @param {any} node
1584
+ * @param {AST.Identifier} tail_synthetic_id
1585
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1586
+ * @param {boolean} inside_nested_function
1587
+ * @returns {void}
1588
+ */
1589
+ function append_loop_tail_to_return_statement(
1590
+ node,
1591
+ tail_synthetic_id,
1592
+ tail_helper,
1593
+ inside_nested_function,
1594
+ ) {
1595
+ if (!node || typeof node !== 'object') {
1596
+ return;
1597
+ }
1598
+
1599
+ if (
1600
+ node.type === 'FunctionDeclaration' ||
1601
+ node.type === 'FunctionExpression' ||
1602
+ node.type === 'ArrowFunctionExpression'
1603
+ ) {
1604
+ inside_nested_function = true;
1605
+ }
1606
+
1607
+ if (!inside_nested_function && node.type === 'ReturnStatement') {
1608
+ if (
1609
+ references_scope_bindings(
1610
+ node.argument,
1611
+ new Map([[tail_synthetic_id.name, tail_synthetic_id]]),
1612
+ )
1613
+ ) {
1614
+ return;
1615
+ }
1616
+ node.argument = append_loop_tail_to_return_argument(
1617
+ node.argument,
1618
+ tail_synthetic_id,
1619
+ tail_helper,
1620
+ );
1621
+ return;
1622
+ }
1623
+
1624
+ if (Array.isArray(node)) {
1625
+ for (const child of node) {
1626
+ append_loop_tail_to_return_statement(
1627
+ child,
1628
+ tail_synthetic_id,
1629
+ tail_helper,
1630
+ inside_nested_function,
1631
+ );
1632
+ }
1633
+ return;
1634
+ }
1635
+
1636
+ for (const key of Object.keys(node)) {
1637
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
1638
+ continue;
1639
+ }
1640
+ append_loop_tail_to_return_statement(
1641
+ node[key],
1642
+ tail_synthetic_id,
1643
+ tail_helper,
1644
+ inside_nested_function,
1645
+ );
1646
+ }
1647
+ }
1648
+
1649
+ /**
1650
+ * @param {any} return_argument
1651
+ * @param {AST.Identifier} tail_synthetic_id
1652
+ * @param {{ component_element: ESTreeJSX.JSXElement }} tail_helper
1653
+ * @returns {any}
1654
+ */
1655
+ function append_loop_tail_to_return_argument(return_argument, tail_synthetic_id, tail_helper) {
1656
+ if (return_argument == null || is_null_literal(return_argument)) {
1657
+ return create_loop_tail_conditional(tail_synthetic_id, tail_helper);
1658
+ }
1659
+
1660
+ return (
1661
+ build_return_expression([
1662
+ return_argument_to_render_node(return_argument),
1663
+ to_jsx_expression_container(create_loop_tail_expression(tail_synthetic_id, tail_helper)),
1664
+ ]) || create_null_literal()
1665
+ );
1666
+ }
1667
+
1342
1668
  /**
1343
1669
  * Build a `return <combined-render-fragment>;` statement, prepending any
1344
1670
  * `render_nodes` collected before the control-flow construct so they don't
@@ -1703,7 +2029,11 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
1703
2029
  }
1704
2030
 
1705
2031
  const has_tail = continuation_body.length > 0;
1706
- const original_loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
2032
+ const original_loop_body = /** @type {any[]} */ (
2033
+ rewrite_loop_continues_to_bare_returns(
2034
+ node.body.type === 'BlockStatement' ? node.body.body : [node.body],
2035
+ )
2036
+ );
1707
2037
 
1708
2038
  // When there's a tail, build TailHelper first so its component_element can
1709
2039
  // be embedded inside the loop helper's body (gated on isLast). The
@@ -1720,18 +2050,13 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
1720
2050
  } else {
1721
2051
  tail_synthetic_id = /** @type {any} */ (null);
1722
2052
  }
1723
- const loop_body = has_tail
1724
- ? [
1725
- ...original_loop_body,
1726
- b.jsx_expression_container(
1727
- b.logical(
1728
- '&&',
1729
- clone_identifier(tail_synthetic_id),
1730
- clone_tail_invocation(/** @type {any} */ (tail_helper)),
1731
- ),
1732
- ),
1733
- ]
1734
- : original_loop_body;
2053
+ const loop_tail_expression = has_tail
2054
+ ? create_loop_tail_expression(tail_synthetic_id, /** @type {any} */ (tail_helper))
2055
+ : null;
2056
+ const loop_body =
2057
+ has_tail && loop_tail_expression
2058
+ ? [...original_loop_body, b.jsx_expression_container(loop_tail_expression)]
2059
+ : original_loop_body;
1735
2060
 
1736
2061
  const source_id = create_generated_identifier(
1737
2062
  `_tsrx_iteration_items_${transform_context.local_statement_component_index + 1}`,
@@ -1767,10 +2092,8 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
1767
2092
  );
1768
2093
 
1769
2094
  // Synthetic `isLast` prop on the loop helper when there's a tail. It's
1770
- // passed from the .map callback as `i === source.length - 1` so the loop
1771
- // helper renders the tail helper only on the last iteration. We do not
1772
- // gate on this prop's value here — the JSXLogicalExpression appended to
1773
- // `loop_body` does the gating at render time.
2095
+ // passed from the .map callback as `i === source.length - 1` so every
2096
+ // loop-helper return can append the tail helper on the last iteration.
1774
2097
  const tail_isLast_alias = has_tail
1775
2098
  ? {
1776
2099
  id: create_generated_identifier(`_tsrx_${helper_id.name}_isLast`),
@@ -1808,6 +2131,13 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
1808
2131
  transform_context.available_bindings.set(tail_synthetic_id.name, tail_synthetic_id);
1809
2132
  }
1810
2133
  const fn_body_statements = build_render_statements(loop_body, true, transform_context);
2134
+ if (has_tail) {
2135
+ append_loop_tail_to_return_statements(
2136
+ fn_body_statements,
2137
+ tail_synthetic_id,
2138
+ /** @type {any} */ (tail_helper),
2139
+ );
2140
+ }
1811
2141
  transform_context.available_bindings = fn_saved_bindings;
1812
2142
 
1813
2143
  const helper_fn = /** @type {any} */ (
@@ -1851,7 +2181,7 @@ function build_hoisted_for_of_with_hooks(node, continuation_body, transform_cont
1851
2181
  index_identifier = null;
1852
2182
  }
1853
2183
 
1854
- const body_key_expression = find_key_expression_in_body(loop_body);
2184
+ const body_key_expression = find_key_expression_in_body(original_loop_body);
1855
2185
  const explicit_key_expression =
1856
2186
  body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
1857
2187
  const key_expression =
@@ -2087,7 +2417,7 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
2087
2417
  function combine_render_return_argument(render_nodes, return_argument) {
2088
2418
  const combined = render_nodes.map((node) => clone_expression_node_without_locations(node));
2089
2419
 
2090
- if (!is_null_literal(return_argument)) {
2420
+ if (return_argument != null && !is_null_literal(return_argument)) {
2091
2421
  combined.push(return_argument_to_render_node(return_argument));
2092
2422
  }
2093
2423
 
@@ -3049,6 +3379,71 @@ function find_key_expression_in_body(body_nodes) {
3049
3379
  return undefined;
3050
3380
  }
3051
3381
 
3382
+ /**
3383
+ * @param {any} source_node
3384
+ * @returns {any}
3385
+ */
3386
+ function continue_to_bare_return(source_node) {
3387
+ return set_loc(
3388
+ /** @type {any} */ ({
3389
+ type: 'ReturnStatement',
3390
+ argument: null,
3391
+ metadata: { path: [] },
3392
+ }),
3393
+ source_node,
3394
+ );
3395
+ }
3396
+
3397
+ /**
3398
+ * `continue` in a component `for...of` body means "skip this item". JSX targets
3399
+ * lower `for...of` to callbacks, so a raw ContinueStatement would be invalid JS;
3400
+ * a bare `return` from the callback preserves the item-skip behavior.
3401
+ *
3402
+ * @param {any[] | any} node
3403
+ * @param {boolean} [is_root]
3404
+ * @returns {any[] | any}
3405
+ */
3406
+ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
3407
+ if (Array.isArray(node)) {
3408
+ return node.map((child) => rewrite_loop_continues_to_bare_returns(child, false));
3409
+ }
3410
+
3411
+ if (!node || typeof node !== 'object') {
3412
+ return node;
3413
+ }
3414
+
3415
+ if (node.type === 'ContinueStatement') {
3416
+ return continue_to_bare_return(node);
3417
+ }
3418
+
3419
+ if (is_function_or_class_boundary(node) || (!is_root && is_loop_statement(node))) {
3420
+ return node;
3421
+ }
3422
+
3423
+ for (const key of Object.keys(node)) {
3424
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3425
+ continue;
3426
+ }
3427
+ node[key] = rewrite_loop_continues_to_bare_returns(node[key], false);
3428
+ }
3429
+
3430
+ return node;
3431
+ }
3432
+
3433
+ /**
3434
+ * @param {any} node
3435
+ * @returns {boolean}
3436
+ */
3437
+ function is_loop_statement(node) {
3438
+ return (
3439
+ node?.type === 'ForOfStatement' ||
3440
+ node?.type === 'ForStatement' ||
3441
+ node?.type === 'ForInStatement' ||
3442
+ node?.type === 'WhileStatement' ||
3443
+ node?.type === 'DoWhileStatement'
3444
+ );
3445
+ }
3446
+
3052
3447
  /**
3053
3448
  * @param {any} node
3054
3449
  * @param {TransformContext} transform_context
@@ -3066,7 +3461,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
3066
3461
  }
3067
3462
 
3068
3463
  const loop_params = get_for_of_iteration_params(node.left, node.index);
3069
- const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
3464
+ const loop_body = /** @type {any[]} */ (
3465
+ rewrite_loop_continues_to_bare_returns(
3466
+ node.body.type === 'BlockStatement' ? node.body.body : [node.body],
3467
+ )
3468
+ );
3070
3469
  const has_hooks = body_contains_top_level_hook_call(loop_body, transform_context, true);
3071
3470
  const body_key_expression = find_key_expression_in_body(loop_body);
3072
3471
  const explicit_key_expression =
@@ -3091,14 +3490,14 @@ function for_of_statement_to_jsx_child(node, transform_context) {
3091
3490
  collect_pattern_bindings(param, transform_context.available_bindings);
3092
3491
  }
3093
3492
 
3493
+ if (implicit_non_hook_key_expression && should_apply_key_to_loop_body(loop_body)) {
3494
+ apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
3495
+ }
3496
+
3094
3497
  const body_statements = has_hooks
3095
3498
  ? hook_safe_render_statements(loop_body, key_expression, transform_context)
3096
3499
  : build_render_statements(loop_body, true, transform_context);
3097
3500
 
3098
- if (implicit_non_hook_key_expression) {
3099
- apply_key_to_render_statements(body_statements, implicit_non_hook_key_expression);
3100
- }
3101
-
3102
3501
  const platform_for_of = transform_context.platform.hooks?.renderForOf?.(
3103
3502
  node,
3104
3503
  loop_params,
@@ -3110,6 +3509,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
3110
3509
  return platform_for_of;
3111
3510
  }
3112
3511
 
3512
+ const non_hook_key_expression = key_expression ?? implicit_non_hook_key_expression;
3513
+ if (!has_hooks && non_hook_key_expression) {
3514
+ apply_key_to_render_statements(body_statements, non_hook_key_expression, transform_context);
3515
+ }
3516
+
3113
3517
  // Restore bindings
3114
3518
  transform_context.available_bindings = saved_bindings;
3115
3519
 
@@ -3147,19 +3551,33 @@ function for_of_statement_to_jsx_child(node, transform_context) {
3147
3551
  }
3148
3552
 
3149
3553
  /**
3150
- * @param {any[]} statements
3554
+ * @param {any[]} body_nodes
3151
3555
  * @param {any} key_expression
3152
3556
  * @returns {void}
3153
3557
  */
3154
- function apply_key_to_render_statements(statements, key_expression) {
3155
- for (let i = statements.length - 1; i >= 0; i -= 1) {
3156
- const statement = statements[i];
3157
- if (statement?.type !== 'ReturnStatement' || !statement.argument) {
3158
- continue;
3558
+ function apply_key_to_loop_body(body_nodes, key_expression) {
3559
+ for (const node of body_nodes) {
3560
+ if (node.type === 'Element') {
3561
+ const attributes = node.attributes || (node.attributes = []);
3562
+ const has_key = attributes.some((/** @type {any} */ attr) => {
3563
+ const attr_name = typeof attr.name === 'string' ? attr.name : attr.name?.name;
3564
+ return attr_name === 'key';
3565
+ });
3566
+
3567
+ if (!has_key) {
3568
+ attributes.push({
3569
+ type: 'Attribute',
3570
+ name: { type: 'Identifier', name: 'key', metadata: { path: [] } },
3571
+ value: clone_expression_node(key_expression),
3572
+ shorthand: false,
3573
+ metadata: { path: [] },
3574
+ });
3575
+ }
3576
+ return;
3159
3577
  }
3160
3578
 
3161
- if (statement.argument.type === 'JSXElement') {
3162
- const attributes = statement.argument.openingElement?.attributes || [];
3579
+ if (node.type === 'JSXElement') {
3580
+ const attributes = node.openingElement?.attributes || [];
3163
3581
  const has_key = attributes.some(
3164
3582
  (/** @type {any} */ attr) =>
3165
3583
  attr.type === 'JSXAttribute' &&
@@ -3180,12 +3598,92 @@ function apply_key_to_render_statements(statements, key_expression) {
3180
3598
  }),
3181
3599
  );
3182
3600
  }
3601
+ return;
3602
+ }
3603
+ }
3604
+ }
3605
+
3606
+ /**
3607
+ * @param {any[]} body_nodes
3608
+ * @returns {boolean}
3609
+ */
3610
+ function should_apply_key_to_loop_body(body_nodes) {
3611
+ let keyable_children = 0;
3612
+ for (const node of body_nodes) {
3613
+ if (node.type === 'Element' || node.type === 'JSXElement') {
3614
+ keyable_children += 1;
3615
+ }
3616
+ }
3617
+ return keyable_children === 1;
3618
+ }
3619
+
3620
+ /**
3621
+ * @param {any[]} statements
3622
+ * @param {any} key_expression
3623
+ * @param {TransformContext} transform_context
3624
+ * @returns {void}
3625
+ */
3626
+ function apply_key_to_render_statements(statements, key_expression, transform_context) {
3627
+ for (let i = statements.length - 1; i >= 0; i -= 1) {
3628
+ const statement = statements[i];
3629
+ if (statement?.type !== 'ReturnStatement' || !statement.argument) {
3630
+ continue;
3631
+ }
3632
+
3633
+ if (statement.argument.type === 'JSXElement') {
3634
+ apply_key_to_jsx_element(statement.argument, key_expression);
3635
+ } else if (statement.argument.type === 'JSXFragment') {
3636
+ transform_context.needs_fragment = true;
3637
+ statement.argument = keyed_fragment_to_jsx_element(statement.argument, key_expression);
3183
3638
  }
3184
3639
 
3185
3640
  return;
3186
3641
  }
3187
3642
  }
3188
3643
 
3644
+ /**
3645
+ * @param {any} element
3646
+ * @param {any} key_expression
3647
+ * @returns {void}
3648
+ */
3649
+ function apply_key_to_jsx_element(element, key_expression) {
3650
+ const attributes = element.openingElement?.attributes || [];
3651
+ const has_key = attributes.some(
3652
+ (/** @type {any} */ attr) =>
3653
+ attr.type === 'JSXAttribute' &&
3654
+ attr.name?.type === 'JSXIdentifier' &&
3655
+ attr.name.name === 'key',
3656
+ );
3657
+
3658
+ if (!has_key) {
3659
+ attributes.push(
3660
+ b.jsx_attribute(
3661
+ b.jsx_id('key'),
3662
+ to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
3663
+ ),
3664
+ );
3665
+ }
3666
+ }
3667
+
3668
+ /**
3669
+ * @param {any} fragment
3670
+ * @param {any} key_expression
3671
+ * @returns {any}
3672
+ */
3673
+ function keyed_fragment_to_jsx_element(fragment, key_expression) {
3674
+ const name = b.jsx_id('Fragment');
3675
+ const key_attribute = b.jsx_attribute(
3676
+ b.jsx_id('key'),
3677
+ to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
3678
+ );
3679
+
3680
+ return b.jsx_element_fresh(
3681
+ b.jsx_opening_element(name, [key_attribute]),
3682
+ b.jsx_closing_element(clone_jsx_name(name)),
3683
+ fragment.children,
3684
+ );
3685
+ }
3686
+
3189
3687
  /**
3190
3688
  * @param {any} node
3191
3689
  * @param {TransformContext} transform_context
@@ -3473,6 +3971,27 @@ function inject_try_imports(program, transform_context, platform, suspense_sourc
3473
3971
  /** @type {any[]} */
3474
3972
  const imports = [];
3475
3973
 
3974
+ if (transform_context.needs_fragment && platform.imports.fragment) {
3975
+ const fragment_source = platform.imports.fragment;
3976
+ imports.push({
3977
+ type: 'ImportDeclaration',
3978
+ specifiers: [
3979
+ {
3980
+ type: 'ImportSpecifier',
3981
+ imported: { type: 'Identifier', name: 'Fragment', metadata: { path: [] } },
3982
+ local: { type: 'Identifier', name: 'Fragment', metadata: { path: [] } },
3983
+ metadata: { path: [] },
3984
+ },
3985
+ ],
3986
+ source: {
3987
+ type: 'Literal',
3988
+ value: fragment_source,
3989
+ raw: `'${fragment_source}'`,
3990
+ },
3991
+ metadata: { path: [] },
3992
+ });
3993
+ }
3994
+
3476
3995
  if (transform_context.needs_suspense) {
3477
3996
  imports.push({
3478
3997
  type: 'ImportDeclaration',
@@ -57,18 +57,21 @@ export function assignment_pattern(left, right) {
57
57
  /**
58
58
  * @param {Array<AST.Pattern>} params
59
59
  * @param {AST.BlockStatement | AST.Expression} body
60
+ * @param {AST.NodeWithLocation} [loc_info]
60
61
  * @returns {AST.ArrowFunctionExpression}
61
62
  */
62
- export function arrow(params, body, async = false) {
63
- return {
63
+ export function arrow(params, body, async = false, loc_info) {
64
+ const node = /** @type {AST.ArrowFunctionExpression} */ ({
64
65
  type: 'ArrowFunctionExpression',
65
66
  params,
66
67
  body,
67
68
  expression: body.type !== 'BlockStatement',
68
69
  generator: false,
69
70
  async,
70
- metadata: /** @type {any} */ (null), // should not be used by codegen
71
- };
71
+ metadata: { path: [] },
72
+ });
73
+
74
+ return set_location(node, loc_info);
72
75
  }
73
76
 
74
77
  /**
@@ -1343,6 +1346,15 @@ export const break_statement = {
1343
1346
  metadata: { path: [] },
1344
1347
  };
1345
1348
 
1349
+ /**
1350
+ * @type {AST.ContinueStatement}
1351
+ */
1352
+ export const continue_statement = {
1353
+ type: 'ContinueStatement',
1354
+ label: null,
1355
+ metadata: { path: [] },
1356
+ };
1357
+
1346
1358
  export {
1347
1359
  await_builder as await,
1348
1360
  let_builder as let,
@@ -1352,6 +1364,7 @@ export {
1352
1364
  true_instance as true,
1353
1365
  false_instance as false,
1354
1366
  break_statement as break,
1367
+ continue_statement as continue,
1355
1368
  for_builder as for,
1356
1369
  switch_builder as switch,
1357
1370
  function_builder as function,
@@ -1,5 +1,7 @@
1
1
  const ATTR_REGEX = /[&"<]/g;
2
2
  const CONTENT_REGEX = /[&<]/g;
3
+ const OPEN_TAG_REGEX = /</g;
4
+ const CLOSE_TAG_REGEX = />/g;
3
5
 
4
6
  /**
5
7
  * @template V
@@ -24,3 +26,12 @@ export function escape(value, is_attr) {
24
26
 
25
27
  return escaped + str.substring(last);
26
28
  }
29
+
30
+ /**
31
+ * Escapes characters that can prematurely terminate inline script tags.
32
+ * @param {string} str
33
+ * @returns {string}
34
+ */
35
+ export function escape_script(str) {
36
+ return str.replace(OPEN_TAG_REGEX, '\\u003c').replace(CLOSE_TAG_REGEX, '\\u003e');
37
+ }
@@ -1,4 +1,4 @@
1
- /** @import { AddEventObject } from '../../types/index'*/
1
+ /** @import { AddEventObject } from '../../types/index' */
2
2
 
3
3
  const NON_DELEGATED_EVENTS = new Set([
4
4
  'abort',
package/types/index.d.ts CHANGED
@@ -74,6 +74,7 @@ interface BaseNodeMetaData {
74
74
  returns?: AST.ReturnStatement[];
75
75
  has_return?: boolean;
76
76
  has_throw?: boolean;
77
+ has_continue?: boolean;
77
78
  is_reactive?: boolean;
78
79
  lone_return?: boolean;
79
80
  forceMapping?: boolean;
@@ -319,12 +320,13 @@ declare module 'estree' {
319
320
  */
320
321
  interface Component extends AST.BaseNode {
321
322
  type: 'Component';
322
- // null is for anonymous components {component: () => {}}
323
+ // null is for anonymous components, e.g. `component(props) => {}`
323
324
  id: AST.Identifier | null;
324
325
  params: AST.Pattern[];
325
326
  body: AST.Node[];
326
327
  css: CSS.StyleSheet | null;
327
328
  metadata: BaseNodeMetaData & {
329
+ arrow?: boolean;
328
330
  topScopedClasses?: TopScopedClasses;
329
331
  styleClasses?: StyleClasses;
330
332
  };
@@ -1498,15 +1500,20 @@ export type StyleClasses = Map<string, AST.MemberExpression['property']>;
1498
1500
  /**
1499
1501
  * Event handling types
1500
1502
  */
1501
- export interface AddEventObject {
1503
+ export interface AddEventOptions extends ExtendedEventOptions {
1502
1504
  customName?: string;
1503
- // from AddEventListenerOptions
1505
+ }
1506
+
1507
+ export interface AddEventObject extends AddEventOptions {
1508
+ handleEvent(object: Event): void;
1509
+ }
1510
+
1511
+ export interface ExtendedEventOptions {
1512
+ capture?: boolean;
1504
1513
  once?: boolean;
1505
1514
  passive?: boolean;
1506
1515
  signal?: AbortSignal;
1507
- capture?: boolean;
1508
- // from EventListenerObject
1509
- handleEvent?(object: Event): void;
1516
+ delegated?: boolean;
1510
1517
  }
1511
1518
 
1512
1519
  /**
@@ -29,6 +29,7 @@ export interface JsxTransformContext {
29
29
  needs_error_boundary: boolean;
30
30
  needs_suspense: boolean;
31
31
  needs_merge_refs: boolean;
32
+ needs_fragment: boolean;
32
33
  helper_state: {
33
34
  base_name: string;
34
35
  next_id: number;
@@ -239,6 +240,11 @@ export interface JsxPlatform {
239
240
  name: string;
240
241
 
241
242
  imports: {
243
+ /**
244
+ * Module to import `Fragment` from when a keyed fragment is required
245
+ * for a multi-child loop body. React: `'react'`. Preact: `'preact'`.
246
+ */
247
+ fragment?: string;
242
248
  /**
243
249
  * Module to import `Suspense` from when a `try { ... } pending { ... }`
244
250
  * block appears. React: `'react'`. Preact: `'preact/compat'`.
@@ -345,4 +351,4 @@ export function componentToFunctionDeclaration(
345
351
  component: any,
346
352
  ctx: any,
347
353
  helperState?: any,
348
- ): AST.FunctionDeclaration;
354
+ ): AST.FunctionDeclaration | AST.FunctionExpression | AST.ArrowFunctionExpression;