@tsrx/core 0.1.15 → 0.1.17

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/src/plugin.js CHANGED
@@ -16,11 +16,11 @@ import {
16
16
  import { regex_newline_characters } from './utils/patterns.js';
17
17
  import { error } from './errors.js';
18
18
  import { DIAGNOSTIC_CODES } from './diagnostics.js';
19
-
20
- const JSX_EXPRESSION_VALUE_ERROR =
21
- 'JSX elements cannot be used as expressions. Wrap JSX with `<>...</>` or `<tsx>...</tsx>`, wrap TSRX templates with `<tsrx>...</tsrx>`, or use elements as statements within a component.';
22
- const HTML_ATTRIBUTE_VALUE_ERROR =
23
- '`{html ...}` is not supported as an attribute value. Use a string literal or expression without `html`.';
19
+ import { TSRX_RETURN_STATEMENT_ERROR } from './analyze/validation.js';
20
+ const DYNAMIC_ELEMENT_IN_TSX_ERROR =
21
+ 'Dynamic element syntax (`<@...>`) is only supported in native TSRX templates.';
22
+ const DYNAMIC_ATTRIBUTE_NAME_ERROR =
23
+ 'Dynamic component / element syntax (`@`) is only supported on native TSRX element names, not attribute names.';
24
24
 
25
25
  const CharCode = Object.freeze({
26
26
  tab: 9,
@@ -201,44 +201,9 @@ function looks_like_generic_arrow(input, pos) {
201
201
  );
202
202
  }
203
203
 
204
- /**
205
- * @param {AST.Node | null | undefined} node
206
- * @returns {boolean}
207
- */
208
- function is_pascal_case_function(node) {
209
- if (node && 'id' in node && node.id && node.id.type === 'Identifier') {
210
- return /^[A-Z]/.test(node.id.name);
211
- }
212
- return false;
213
- }
214
-
215
- /**
216
- * @param {string} input
217
- * @param {number} pos
218
- */
219
- function previous_word_before(input, pos) {
220
- let i = pos - 1;
221
- while (i >= 0) {
222
- const ch = input.charCodeAt(i);
223
- if (
224
- ch !== CharCode.space &&
225
- ch !== CharCode.tab &&
226
- ch !== CharCode.lineFeed &&
227
- ch !== CharCode.carriageReturn
228
- )
229
- break;
230
- i--;
231
- }
232
- const end = i + 1;
233
- while (i >= 0 && /[$_\p{ID_Continue}]/u.test(input[i])) {
234
- i--;
235
- }
236
- return input.slice(i + 1, end);
237
- }
238
-
239
204
  /**
240
205
  * Acorn parser plugin for Ripple syntax extensions.
241
- * Adds support for: component declarations, &[]/&{} lazy destructuring,
206
+ * Adds support for: native TSRX templates, &[]/&{} lazy destructuring,
242
207
  * submodule imports, TSRX directives, and enhanced JSX handling.
243
208
  *
244
209
  * @param {import('../types/index').TSRXPluginConfig} [config] - Plugin configuration
@@ -264,18 +229,14 @@ export function TSRXPlugin(config) {
264
229
  #commentContextId = 0;
265
230
  #collect = false;
266
231
  #loose = false;
267
- /** @type {AST.Node[]} */
268
- #functionStack = [];
269
- /** @type {Array<{ parentContext: any[], canRestore: boolean, restore: boolean }>} */
270
- #functionBodyContextRestoreStack = [];
271
232
  /** @type {import('../types/index').CompileError[] | undefined} */
272
233
  #errors = undefined;
273
234
  /** @type {string | null} */
274
235
  #filename = null;
275
- #componentDepth = 0;
276
236
  #functionBodyDepth = 0;
277
237
  #allowExpressionContainerTrailingSemicolon = false;
278
238
  #tsxIslandExpressionDepth = 0;
239
+ #jsxAttributeValueExpressionDepth = 0;
279
240
 
280
241
  /**
281
242
  * @type {Parse.Parser['finishNode']}
@@ -330,16 +291,8 @@ export function TSRXPlugin(config) {
330
291
  return null;
331
292
  }
332
293
 
333
- #isInsideComponent() {
334
- return this.#componentDepth > 0;
335
- }
336
-
337
- #isInsideComponentTemplate() {
338
- return this.#isInsideComponent() && this.#functionBodyDepth === 0;
339
- }
340
-
341
294
  /**
342
- * Component bodies and native TSRX element bodies share the same grammar.
295
+ * Native TSRX template bodies share one grammar across elements and fragments.
343
296
  * This helper keeps the parser-state setup in one place while callers keep
344
297
  * ownership of their distinct closing delimiter handling (`}` vs `</tag>`).
345
298
  *
@@ -348,19 +301,13 @@ export function TSRXPlugin(config) {
348
301
  * @param {{
349
302
  * enterScope?: boolean,
350
303
  * pushPath?: boolean,
351
- * trackComponentDepth?: boolean,
352
304
  * resetFunctionBodyDepth?: boolean,
353
305
  * }} [options]
354
306
  */
355
307
  #parseNativeTemplateBody(
356
308
  node,
357
309
  body,
358
- {
359
- enterScope = false,
360
- pushPath = false,
361
- trackComponentDepth = false,
362
- resetFunctionBodyDepth = false,
363
- } = {},
310
+ { enterScope = false, pushPath = false, resetFunctionBodyDepth = false } = {},
364
311
  ) {
365
312
  const parent_function_body_depth = this.#functionBodyDepth;
366
313
 
@@ -373,16 +320,10 @@ export function TSRXPlugin(config) {
373
320
  if (pushPath) {
374
321
  this.#path.push(node);
375
322
  }
376
- if (trackComponentDepth) {
377
- this.#componentDepth++;
378
- }
379
323
 
380
324
  try {
381
325
  this.parseTemplateBody(body);
382
326
  } finally {
383
- if (trackComponentDepth) {
384
- this.#componentDepth--;
385
- }
386
327
  if (pushPath) {
387
328
  this.#path.pop();
388
329
  }
@@ -400,7 +341,6 @@ export function TSRXPlugin(config) {
400
341
  */
401
342
  #isNativeTemplateNode(node) {
402
343
  return (
403
- node?.type === 'Component' ||
404
344
  node?.type === 'Element' ||
405
345
  node?.type === 'Tsx' ||
406
346
  node?.type === 'Tsrx' ||
@@ -408,6 +348,32 @@ export function TSRXPlugin(config) {
408
348
  );
409
349
  }
410
350
 
351
+ /**
352
+ * @param {AST.Node[]} children
353
+ */
354
+ #reportDynamicJsxElementsInTsx(children) {
355
+ for (const child of children) {
356
+ if (child?.type === 'JSXElement') {
357
+ const name = child.openingElement?.name;
358
+ const is_dynamic_name =
359
+ (name?.type === 'JSXIdentifier' && name.tracked) ||
360
+ (name?.type === 'JSXMemberExpression' &&
361
+ name.object.type === 'JSXIdentifier' &&
362
+ name.object.tracked);
363
+ if (is_dynamic_name) {
364
+ this.#report_recoverable_error_range(
365
+ /** @type {AST.NodeWithLocation} */ (name).start ?? child.start,
366
+ /** @type {AST.NodeWithLocation} */ (name).end ?? child.end,
367
+ DYNAMIC_ELEMENT_IN_TSX_ERROR,
368
+ );
369
+ }
370
+ this.#reportDynamicJsxElementsInTsx(/** @type {AST.Node[]} */ (child.children));
371
+ } else if (child?.type === 'Tsx' || child?.type === 'TsxCompat') {
372
+ this.#reportDynamicJsxElementsInTsx(/** @type {AST.Node[]} */ (child.children));
373
+ }
374
+ }
375
+ }
376
+
411
377
  #parseNativeTemplateExpressionContainer() {
412
378
  const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
413
379
  this.#allowExpressionContainerTrailingSemicolon = true;
@@ -420,26 +386,11 @@ export function TSRXPlugin(config) {
420
386
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
421
387
  // but convert other expressions to native TSRX child nodes.
422
388
  if (node.expression.type !== 'JSXEmptyExpression') {
423
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
424
- /** @type {unknown} */ (node)
425
- ).type = node.html
426
- ? 'Html'
427
- : node.text
428
- ? 'Text'
429
- : node.style
430
- ? 'Style'
431
- : 'TSRXExpression';
432
- if (node.style) {
433
- /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
434
- /** @type {AST.Literal} */ (node.expression);
435
- delete (/** @type {any} */ (node).expression);
436
- }
437
- delete node.html;
438
- delete node.text;
439
- delete node.style;
440
- }
441
-
442
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style | ESTreeJSX.JSXExpressionContainer} */ (
389
+ /** @type {AST.TSRXExpression | AST.TextNode} */ (/** @type {unknown} */ (node)).type =
390
+ 'TSRXExpression';
391
+ }
392
+
393
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
443
394
  /** @type {unknown} */ (node)
444
395
  );
445
396
  }
@@ -463,7 +414,7 @@ export function TSRXPlugin(config) {
463
414
  const displayTag = tagName || '';
464
415
  this.#report_broken_markup_error(
465
416
  this.start,
466
- `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of component.`,
417
+ `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
467
418
  );
468
419
  island.unclosed = true;
469
420
  /** @type {AST.NodeWithLocation} */ (island).loc.end = {
@@ -549,25 +500,16 @@ export function TSRXPlugin(config) {
549
500
  */
550
501
  #isReservedTemplateTagNameStart(index) {
551
502
  const char_after_tsx = this.input.charCodeAt(index + 3);
552
- const char_after_tsrx = this.input.charCodeAt(index + 4);
553
503
  return (
554
- (this.input.startsWith('tsx', index) &&
555
- (index + 3 >= this.input.length ||
556
- char_after_tsx === CharCode.greaterThan ||
557
- char_after_tsx === CharCode.slash ||
558
- char_after_tsx === CharCode.space ||
559
- char_after_tsx === CharCode.tab ||
560
- char_after_tsx === CharCode.lineFeed ||
561
- char_after_tsx === CharCode.carriageReturn ||
562
- char_after_tsx === CharCode.colon)) ||
563
- (this.input.startsWith('tsrx', index) &&
564
- (index + 4 >= this.input.length ||
565
- char_after_tsrx === CharCode.greaterThan ||
566
- char_after_tsrx === CharCode.slash ||
567
- char_after_tsrx === CharCode.space ||
568
- char_after_tsrx === CharCode.tab ||
569
- char_after_tsrx === CharCode.lineFeed ||
570
- char_after_tsrx === CharCode.carriageReturn))
504
+ this.input.startsWith('tsx', index) &&
505
+ (index + 3 >= this.input.length ||
506
+ char_after_tsx === CharCode.greaterThan ||
507
+ char_after_tsx === CharCode.slash ||
508
+ char_after_tsx === CharCode.space ||
509
+ char_after_tsx === CharCode.tab ||
510
+ char_after_tsx === CharCode.lineFeed ||
511
+ char_after_tsx === CharCode.carriageReturn ||
512
+ char_after_tsx === CharCode.colon)
571
513
  );
572
514
  }
573
515
 
@@ -599,7 +541,7 @@ export function TSRXPlugin(config) {
599
541
  while (this.pos < this.input.length) {
600
542
  const ch = this.input.charCodeAt(this.pos);
601
543
 
602
- // Stop at opening tag, expression, or the component-closing brace
544
+ // Stop at opening tag, expression, or the template-closing brace
603
545
  if (ch === CharCode.lessThan || ch === CharCode.openBrace || ch === CharCode.closeBrace) {
604
546
  break;
605
547
  }
@@ -791,13 +733,13 @@ export function TSRXPlugin(config) {
791
733
  }
792
734
  }
793
735
 
794
- // Inside `{<tsrx>...</tsrx>}` JSX expression container — strip
736
+ // Inside a native template JSX expression container — strip
795
737
  // both the leaked `b_stat` and the container's `tc_expr`.
796
738
  if (top === b_stat && second === tstc.tc_expr) {
797
739
  ctx.length = ci - 1;
798
740
  return;
799
741
  }
800
- // Statement-bodied `<tsrx>` attributes can leave the attribute's
742
+ // Statement-bodied native template attributes can leave the attribute's
801
743
  // expression contexts above the still-open JSX tag context. Strip
802
744
  // those so a following `/>` stays in JSX opening-tag mode.
803
745
  if (
@@ -835,10 +777,7 @@ export function TSRXPlugin(config) {
835
777
  }
836
778
 
837
779
  const parent = this.#path.at(-1);
838
- if (
839
- !parent ||
840
- (parent.type !== 'Component' && parent.type !== 'Element' && parent.type !== 'Tsrx')
841
- ) {
780
+ if (!parent || (parent.type !== 'Element' && parent.type !== 'Tsrx')) {
842
781
  return false;
843
782
  }
844
783
 
@@ -945,6 +884,75 @@ export function TSRXPlugin(config) {
945
884
  this.raise(position, message);
946
885
  }
947
886
 
887
+ /**
888
+ * @param {AST.Node | AST.Node[] | unknown} maybe_node
889
+ * @param {boolean} [inside_nested_function]
890
+ * @param {boolean} [inside_loop]
891
+ */
892
+ #report_invalid_template_return_statements(
893
+ maybe_node,
894
+ inside_nested_function = false,
895
+ inside_loop = false,
896
+ ) {
897
+ if (!maybe_node || typeof maybe_node !== 'object') {
898
+ return;
899
+ }
900
+
901
+ let node = /** @type {AST.Node} */ (maybe_node);
902
+ if (
903
+ node.type === 'FunctionDeclaration' ||
904
+ node.type === 'FunctionExpression' ||
905
+ node.type === 'ArrowFunctionExpression'
906
+ ) {
907
+ inside_nested_function = true;
908
+ }
909
+
910
+ if (
911
+ node.type === 'ForStatement' ||
912
+ node.type === 'ForInStatement' ||
913
+ node.type === 'ForOfStatement' ||
914
+ node.type === 'WhileStatement' ||
915
+ node.type === 'DoWhileStatement'
916
+ ) {
917
+ inside_loop = true;
918
+ }
919
+
920
+ if (!inside_nested_function && !inside_loop && node.type === 'ReturnStatement') {
921
+ node.metadata = {
922
+ ...node.metadata,
923
+ invalid_tsrx_template_return: true,
924
+ };
925
+ this.#report_recoverable_error(
926
+ /** @type {AST.NodeWithLocation} */ (node).start ?? this.start,
927
+ TSRX_RETURN_STATEMENT_ERROR,
928
+ DIAGNOSTIC_CODES.TEMPLATE_RETURN_STATEMENT,
929
+ );
930
+ return;
931
+ }
932
+
933
+ if (Array.isArray(node)) {
934
+ for (const child of /** @type {AST.Node[]} */ (node)) {
935
+ this.#report_invalid_template_return_statements(
936
+ child,
937
+ inside_nested_function,
938
+ inside_loop,
939
+ );
940
+ }
941
+ return;
942
+ }
943
+
944
+ for (const key of Object.keys(node)) {
945
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
946
+ continue;
947
+ }
948
+ this.#report_invalid_template_return_statements(
949
+ /** @type {Record<string, unknown>} */ (node)[key],
950
+ inside_nested_function,
951
+ inside_loop,
952
+ );
953
+ }
954
+ }
955
+
948
956
  /**
949
957
  * When collecting, keep parsing after duplicate declaration diagnostics so
950
958
  * editor tooling can continue producing AST and mappings.
@@ -1014,64 +1022,6 @@ export function TSRXPlugin(config) {
1014
1022
  }
1015
1023
  }
1016
1024
 
1017
- /**
1018
- * Override parseProperty to support component methods in object literals.
1019
- * Handles syntax like `{ component something() { <div /> } }`
1020
- * Also supports computed names: `{ component ['something']() { <div /> } }`
1021
- * @type {Parse.Parser['parseProperty']}
1022
- */
1023
- parseProperty(isPattern, refDestructuringErrors) {
1024
- // Check if this is a component method: component name( ... ) { ... }
1025
- if (!isPattern && this.type === tt.name && this.value === 'component') {
1026
- // Look ahead to see if this is "component identifier(", "component identifier<", "component [", or "component 'string'"
1027
- const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
1028
- if (lookahead) {
1029
- // This is a component method definition
1030
- const prop = /** @type {AST.Property} */ (this.startNode());
1031
- const isComputed = lookahead[0].trim().startsWith('[');
1032
- const isStringLiteral = /^['"]/.test(lookahead[0].trim());
1033
-
1034
- if (isComputed) {
1035
- // For computed names, consume 'component'
1036
- // parse the key, then parse component without name
1037
- this.next(); // consume 'component'
1038
- this.next(); // consume '['
1039
- prop.key = this.parseExpression();
1040
- this.expect(tt.bracketR);
1041
- prop.computed = true;
1042
-
1043
- // Parse component without name (skipName: true)
1044
- const component_node = this.parseComponent({ skipName: true });
1045
- /** @type {AST.TSRXProperty} */ (prop).value = component_node;
1046
- } else if (isStringLiteral) {
1047
- // For string literal names, consume 'component'
1048
- // parse the string key, then parse component without name
1049
- this.next(); // consume 'component'
1050
- prop.key = /** @type {AST.Literal} */ (this.parseExprAtom());
1051
- prop.computed = false;
1052
-
1053
- // Parse component without name (skipName: true)
1054
- const component_node = this.parseComponent({ skipName: true });
1055
- /** @type {AST.TSRXProperty} */ (prop).value = component_node;
1056
- } else {
1057
- const component_node = this.parseComponent({ requireName: true });
1058
-
1059
- prop.key = /** @type {AST.Identifier} */ (component_node.id);
1060
- /** @type {AST.TSRXProperty} */ (prop).value = component_node;
1061
- prop.computed = false;
1062
- }
1063
-
1064
- prop.shorthand = false;
1065
- prop.method = true;
1066
- prop.kind = 'init';
1067
-
1068
- return this.finishNode(prop, 'Property');
1069
- }
1070
- }
1071
-
1072
- return super.parseProperty(isPattern, refDestructuringErrors);
1073
- }
1074
-
1075
1025
  /**
1076
1026
  * Override parsePropertyValue to support TypeScript generic methods in object literals.
1077
1027
  * By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
@@ -1211,16 +1161,17 @@ export function TSRXPlugin(config) {
1211
1161
  * @type {Parse.Parser['getTokenFromCode']}
1212
1162
  */
1213
1163
  getTokenFromCode(code) {
1214
- // Callback props that return `<tsrx>...</tsrx>` without a semicolon can
1164
+ // Callback props that return native templates without a semicolon can
1215
1165
  // leave the attribute expression context above the still-open tag. Drop
1216
1166
  // it before tokenizing `/>`, otherwise Acorn treats `/` as a regexp.
1217
1167
  if (
1218
1168
  code === CharCode.slash &&
1219
1169
  this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan &&
1220
- this.curContext() === b_expr &&
1221
- this.context[this.context.length - 2] === tstc.tc_oTag
1170
+ this.context.includes(tstc.tc_oTag)
1222
1171
  ) {
1223
- this.context.pop();
1172
+ while (this.context.length > 0 && this.curContext() !== tstc.tc_oTag) {
1173
+ this.context.pop();
1174
+ }
1224
1175
  this.exprAllowed = false;
1225
1176
  }
1226
1177
  if (code === CharCode.doubleQuote) {
@@ -1239,7 +1190,10 @@ export function TSRXPlugin(config) {
1239
1190
 
1240
1191
  if (code === CharCode.lessThan) {
1241
1192
  // < character
1242
- const inComponent = this.#isInsideComponentTemplate();
1193
+ const parent = this.#path.at(-1);
1194
+ const inNativeTemplate =
1195
+ this.#functionBodyDepth === 0 &&
1196
+ (parent?.type === 'Element' || parent?.type === 'Tsrx');
1243
1197
  /** @type {number | null} */
1244
1198
  let prevNonWhitespaceChar = null;
1245
1199
 
@@ -1278,7 +1232,7 @@ export function TSRXPlugin(config) {
1278
1232
  }
1279
1233
  }
1280
1234
 
1281
- // Support parsing standalone template markup at the top-level (outside `component`)
1235
+ // Support parsing standalone template markup at the top-level
1282
1236
  // for tooling like Prettier, e.g.:
1283
1237
  // <Something>...</Something>\n\n<Child />
1284
1238
  // <head><style>...</style></head>
@@ -1307,13 +1261,13 @@ export function TSRXPlugin(config) {
1307
1261
  prevNonWhitespaceChar === CharCode.closeBrace ||
1308
1262
  prevNonWhitespaceChar === CharCode.greaterThan;
1309
1263
 
1310
- if (!inComponent && prevAllowsTagStart && isTagLikeAfterLt) {
1264
+ if (!inNativeTemplate && prevAllowsTagStart && isTagLikeAfterLt) {
1311
1265
  ++this.pos;
1312
1266
  return this.finishToken(tstt.jsxTagStart);
1313
1267
  }
1314
1268
 
1315
- if (inComponent) {
1316
- // Inside component template bodies, allow adjacent tags without requiring
1269
+ if (inNativeTemplate) {
1270
+ // Inside native template bodies, allow adjacent tags without requiring
1317
1271
  // a newline/indentation before the next '<'. This is important for inputs
1318
1272
  // like `<div />` and `</div><style>...</style>` which Prettier formats.
1319
1273
  if (
@@ -1452,36 +1406,6 @@ export function TSRXPlugin(config) {
1452
1406
  return super.checkLValSimple(expr, bindingType, checkClashes);
1453
1407
  }
1454
1408
 
1455
- /**
1456
- * Components do not use Acorn's normal function-body parser, but they
1457
- * should still report duplicate parameter names like functions do. Keep
1458
- * this validation on `BIND_OUTSIDE` so params are checked without being
1459
- * declared in the component template scope, preserving existing shadowing
1460
- * behavior.
1461
- *
1462
- * @param {AST.Pattern[]} params
1463
- */
1464
- checkComponentParams(params) {
1465
- /** @type {Record<string, boolean>} */
1466
- const name_hash = Object.create(null);
1467
- for (const param of params || []) {
1468
- this.checkLValInnerPattern(param, BINDING_TYPES.BIND_OUTSIDE, name_hash);
1469
- }
1470
- }
1471
-
1472
- /**
1473
- * Parse expression atom - handles RippleArray and RippleObject literals
1474
- * @type {Parse.Parser['parseExprAtom']}
1475
- */
1476
- parseExprAtom(refDestructuringErrors, forNew, forInit) {
1477
- // Check if this is a component expression (e.g., in object literal values)
1478
- if (this.type === tt.name && this.value === 'component') {
1479
- return this.parseComponent();
1480
- }
1481
-
1482
- return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
1483
- }
1484
-
1485
1409
  /**
1486
1410
  * Override to track parenthesized expressions in metadata
1487
1411
  * This allows the prettier plugin to preserve parentheses where they existed
@@ -1524,108 +1448,6 @@ export function TSRXPlugin(config) {
1524
1448
  this.undefinedExports[name] = id;
1525
1449
  }
1526
1450
 
1527
- /**
1528
- * Parse a component - common implementation used by statements, expressions, and export defaults
1529
- * @type {Parse.Parser['parseComponent']}
1530
- */
1531
- parseComponent({
1532
- requireName = false,
1533
- isDefault = false,
1534
- declareName = false,
1535
- skipName = false,
1536
- } = {}) {
1537
- const node = /** @type {AST.Component} */ (this.startNode());
1538
- const parent_context = [...this.context];
1539
- const restore_parent_context =
1540
- !requireName &&
1541
- this.#isInsideComponent() &&
1542
- this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag);
1543
- node.type = 'Component';
1544
- node.css = null;
1545
- node.default = isDefault;
1546
-
1547
- // skipName is used for computed property names where 'component' and the key
1548
- // have already been consumed before calling parseComponent
1549
- if (!skipName) {
1550
- this.next(); // consume 'component'
1551
- }
1552
- this.enterScope(0);
1553
-
1554
- if (skipName) {
1555
- // For computed names, the key is parsed separately, so id is null
1556
- node.id = null;
1557
- } else if (requireName) {
1558
- node.id = this.parseIdent();
1559
- if (declareName) {
1560
- this.declareName(
1561
- node.id.name,
1562
- BINDING_TYPES.BIND_FUNCTION,
1563
- /** @type {AST.NodeWithLocation} */ (node.id).start,
1564
- );
1565
- }
1566
- } else {
1567
- node.id = this.type.label === 'name' ? this.parseIdent() : null;
1568
- if (declareName && node.id) {
1569
- this.declareName(
1570
- node.id.name,
1571
- BINDING_TYPES.BIND_FUNCTION,
1572
- /** @type {AST.NodeWithLocation} */ (node.id).start,
1573
- );
1574
- }
1575
- }
1576
-
1577
- this.parseFunctionParams(node);
1578
- this.checkComponentParams(node.params);
1579
-
1580
- const is_arrow_component = this.type === tt.arrow;
1581
- if (is_arrow_component) {
1582
- if (node.id || requireName || skipName) {
1583
- this.raise(
1584
- this.start,
1585
- 'Arrow component syntax is only supported for anonymous component expressions.',
1586
- );
1587
- }
1588
- node.metadata ??= { path: [] };
1589
- node.metadata.arrow = true;
1590
- this.next();
1591
- }
1592
-
1593
- if (this.type === tt.braceL) {
1594
- this.#allowDoubleQuotedTextChildAfterBrace = true;
1595
- }
1596
- this.eat(tt.braceL);
1597
- node.body = [];
1598
- this.#parseNativeTemplateBody(node, node.body, {
1599
- pushPath: true,
1600
- trackComponentDepth: true,
1601
- resetFunctionBodyDepth: true,
1602
- });
1603
- this.exitScope();
1604
-
1605
- this.next();
1606
- skipWhitespace(this);
1607
- if (restore_parent_context) {
1608
- this.context = this.type === tt.braceR ? parent_context.slice(0, -1) : parent_context;
1609
- this.exprAllowed = false;
1610
- }
1611
- this.finishNode(node, 'Component');
1612
- this.awaitPos = 0;
1613
-
1614
- return node;
1615
- }
1616
-
1617
- /**
1618
- * @type {Parse.Parser['parseExportDefaultDeclaration']}
1619
- */
1620
- parseExportDefaultDeclaration() {
1621
- // Check if this is "export default component"
1622
- if (this.value === 'component') {
1623
- return this.parseComponent({ isDefault: true });
1624
- }
1625
-
1626
- return super.parseExportDefaultDeclaration();
1627
- }
1628
-
1629
1451
  /** @type {Parse.Parser['parseForStatement']} */
1630
1452
  parseForStatement(node) {
1631
1453
  this.next();
@@ -1824,77 +1646,13 @@ export function TSRXPlugin(config) {
1824
1646
  */
1825
1647
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1826
1648
  this.#functionBodyDepth++;
1827
- this.#functionStack.push(node);
1828
- const context_restore = {
1829
- parentContext: [...this.context],
1830
- canRestore:
1831
- this.#isInsideComponent() &&
1832
- this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag),
1833
- restore: false,
1834
- };
1835
- this.#functionBodyContextRestoreStack.push(context_restore);
1836
- // Inside a component, nested JS function bodies should parse like
1837
- // ordinary functions, not component template bodies.
1838
- if (
1839
- // Only adjust functions declared while parsing a component body.
1840
- this.#isInsideComponent() &&
1841
- // A stale JSX expression context means the surrounding template
1842
- // tokenizer can still treat `<` as template markup.
1843
- this.context.some((context) => context === tstc.tc_expr) &&
1844
- // Keep callback props on their surrounding JSX attribute path until
1845
- // statement-position TSRX needs to suspend it.
1846
- !context_restore.canRestore &&
1847
- // Only reset statement-level function bodies, not expression
1848
- // contexts that are actively parsing JSX.
1849
- this.curContext() === b_stat
1850
- ) {
1851
- this.context = [b_stat];
1852
- }
1853
-
1854
1649
  try {
1855
1650
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
1856
1651
  } finally {
1857
- if (context_restore.restore) {
1858
- this.context = context_restore.parentContext.slice(0, -1);
1859
- this.exprAllowed = false;
1860
- }
1861
- this.#functionBodyContextRestoreStack.pop();
1862
- this.#functionStack.pop();
1863
1652
  this.#functionBodyDepth--;
1864
1653
  }
1865
1654
  }
1866
1655
 
1867
- /**
1868
- * @type {Parse.Parser['checkUnreserved']}
1869
- */
1870
- checkUnreserved(ref) {
1871
- if (ref.name === 'component') {
1872
- // Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal)
1873
- // e.g., { component something() { ... } }
1874
- // Also allow computed names: { component ['name']() { ... } }
1875
- // Also allow string literal names: { component 'name'() { ... } }
1876
- const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
1877
- if (!nextChars) {
1878
- this.raise(
1879
- ref.start,
1880
- '"component" is a TSRX keyword and cannot be used as an identifier',
1881
- );
1882
- }
1883
- }
1884
- return super.checkUnreserved(ref);
1885
- }
1886
-
1887
- /** @type {Parse.Parser['shouldParseExportStatement']} */
1888
- shouldParseExportStatement() {
1889
- if (super.shouldParseExportStatement()) {
1890
- return true;
1891
- }
1892
- if (this.value === 'component') {
1893
- return true;
1894
- }
1895
- return this.type.keyword === 'var';
1896
- }
1897
-
1898
1656
  /**
1899
1657
  * @return {ESTreeJSX.JSXExpressionContainer}
1900
1658
  */
@@ -1902,59 +1660,8 @@ export function TSRXPlugin(config) {
1902
1660
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1903
1661
  this.next();
1904
1662
 
1905
- if (this.type === tt.name && this.value === 'ref') {
1906
- const ref_node = /** @type {AST.RefExpression} */ (this.startNode());
1907
- this.next();
1908
- if (this.type === tt.braceR) {
1909
- this.raise(
1910
- this.start,
1911
- '"ref" is a TSRX keyword and must be used in the form {ref item}',
1912
- );
1913
- }
1914
- ref_node.argument = this.parseMaybeAssign();
1915
- node.expression = /** @type {any} */ (this.finishNode(ref_node, 'RefExpression'));
1916
- this.expect(tt.braceR);
1917
- return this.finishNode(node, 'JSXExpressionContainer');
1918
- }
1919
-
1920
- if (this.type === tt.name && this.value === 'html') {
1921
- node.html = true;
1922
- this.next();
1923
- if (this.type === tt.braceR) {
1924
- this.raise(
1925
- this.start,
1926
- '"html" is a TSRX keyword and must be used in the form {html some_content}',
1927
- );
1928
- }
1929
- } else if (this.type === tt.name && this.value === 'text') {
1930
- node.text = true;
1931
- this.next();
1932
- if (this.type === tt.braceR) {
1933
- this.raise(
1934
- this.start,
1935
- '"text" is a TSRX keyword and must be used in the form {text some_value}',
1936
- );
1937
- }
1938
- } else if (
1939
- this.type === tt.name &&
1940
- this.value === 'style' &&
1941
- this.lookahead().type === tt.string
1942
- ) {
1943
- node.style = true;
1944
- this.next();
1945
- }
1946
-
1947
1663
  node.expression =
1948
1664
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1949
- if (
1950
- node.style &&
1951
- (node.expression.type !== 'Literal' || typeof node.expression.value !== 'string')
1952
- ) {
1953
- this.raise(
1954
- /** @type {number} */ (node.expression.start),
1955
- '"style" is a TSRX keyword and must be used in the form {style "class_name"}',
1956
- );
1957
- }
1958
1665
  if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
1959
1666
  if (this.#collect) {
1960
1667
  this.#report_recoverable_error(
@@ -2052,39 +1759,7 @@ export function TSRXPlugin(config) {
2052
1759
  this.unexpected();
2053
1760
  }
2054
1761
 
2055
- if (this.value === 'ref') {
2056
- this.next();
2057
- if (this.type === tt.braceR) {
2058
- this.raise(
2059
- this.start,
2060
- '"ref" is a Ripple keyword and must be used in the form {ref fn}',
2061
- );
2062
- }
2063
- /** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
2064
- this.expect(tt.braceR);
2065
- return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
2066
- } else if (this.type === tt.name && this.value === 'html') {
2067
- // {html ...}
2068
- // The support is purely for better error messages to avoid
2069
- // the parser throw an unexpected token error
2070
- const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
2071
- id.tracked = false;
2072
- this.finishNode(id, 'Identifier');
2073
- this.next();
2074
- const value = this.type === tt.braceR ? id : this.parseMaybeAssign();
2075
- const report_end = this.type === tt.braceR ? this.end : (value.end ?? this.end);
2076
- this.#report_recoverable_error_range(
2077
- node.start ?? id.start ?? this.start,
2078
- report_end,
2079
- HTML_ATTRIBUTE_VALUE_ERROR,
2080
- DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
2081
- );
2082
- /** @type {AST.Attribute} */ (node).name = id;
2083
- /** @type {AST.Attribute} */ (node).value = value;
2084
- /** @type {AST.Attribute} */ (node).shorthand = false;
2085
- this.expect(tt.braceR);
2086
- return this.finishNode(node, 'Attribute');
2087
- } else if (this.type === tt.ellipsis) {
1762
+ if (this.type === tt.ellipsis) {
2088
1763
  this.expect(tt.ellipsis);
2089
1764
  /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
2090
1765
  this.expect(tt.braceR);
@@ -2107,17 +1782,23 @@ export function TSRXPlugin(config) {
2107
1782
  }
2108
1783
  }
2109
1784
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
2110
- const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
2111
- this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
2112
- );
2113
- if (value?.type === 'JSXExpressionContainer' && value.html) {
1785
+ if (
1786
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).name.type === 'JSXIdentifier' &&
1787
+ /** @type {ESTreeJSX.JSXIdentifier} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
1788
+ .tracked
1789
+ ) {
2114
1790
  this.#report_recoverable_error_range(
2115
- value.start ?? node.start ?? this.start,
2116
- value.end ?? node.end ?? this.end,
2117
- HTML_ATTRIBUTE_VALUE_ERROR,
2118
- DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
1791
+ /** @type {AST.NodeWithLocation} */ (node).start,
1792
+ /** @type {AST.NodeWithLocation} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
1793
+ .end ??
1794
+ node.end ??
1795
+ node.start,
1796
+ DYNAMIC_ATTRIBUTE_NAME_ERROR,
2119
1797
  );
2120
1798
  }
1799
+ const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
1800
+ this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
1801
+ );
2121
1802
  /** @type {ESTreeJSX.JSXAttribute} */ (node).value = value;
2122
1803
  return this.finishNode(node, 'JSXAttribute');
2123
1804
  }
@@ -2213,7 +1894,12 @@ export function TSRXPlugin(config) {
2213
1894
  jsx_parseAttributeValue() {
2214
1895
  switch (this.type) {
2215
1896
  case tt.braceL:
2216
- return this.jsx_parseExpressionContainer();
1897
+ this.#jsxAttributeValueExpressionDepth++;
1898
+ try {
1899
+ return this.jsx_parseExpressionContainer();
1900
+ } finally {
1901
+ this.#jsxAttributeValueExpressionDepth--;
1902
+ }
2217
1903
  case tstt.jsxTagStart:
2218
1904
  case tt.string:
2219
1905
  return this.parseExprAtom();
@@ -2430,7 +2116,6 @@ export function TSRXPlugin(config) {
2430
2116
  if (
2431
2117
  ch === CharCode.closeBrace &&
2432
2118
  (this.#path.length === 0 ||
2433
- this.#path.at(-1)?.type === 'Component' ||
2434
2119
  this.#path.at(-1)?.type === 'Element' ||
2435
2120
  this.#path.at(-1)?.type === 'Tsrx')
2436
2121
  ) {
@@ -2469,97 +2154,49 @@ export function TSRXPlugin(config) {
2469
2154
  }
2470
2155
 
2471
2156
  /**
2472
- * Override jsx_parseElement to intercept expression-level JSX.
2473
- * This is called by acorn-jsx's parseExprAtom when it encounters <
2474
- * in expression position. Bare fragments are treated as shorthand
2475
- * for <tsx>...</tsx>. <tsrx>...</tsrx> admits native TSRX
2476
- * template syntax as an expression value. Other tags must still use
2477
- * <tsx>, <tsrx>, or <tsx:*>.
2157
+ * Override jsx_parseElement to parse tags and bare fragments as native TSRX
2158
+ * by default. Explicit <tsx> and <tsx:*> islands keep ordinary TSX parsing
2159
+ * for their children.
2478
2160
  * @type {Parse.Parser['jsx_parseElement']}
2479
2161
  */
2480
2162
  jsx_parseElement() {
2481
- // Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
2482
2163
  // Current token is jsxTagStart, this.end is position after '<'
2483
2164
  const tag_name_start = this.end;
2484
- const is_fragment_tag = this.input.charCodeAt(tag_name_start) === CharCode.greaterThan;
2485
- const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
2486
- const char_after_tsrx = this.input.charCodeAt(tag_name_start + 4);
2487
- const is_tsx_tag =
2488
- this.input.startsWith('tsx', tag_name_start) &&
2489
- (tag_name_start + 3 >= this.input.length ||
2490
- char_after_tsx === CharCode.greaterThan ||
2491
- char_after_tsx === CharCode.slash ||
2492
- char_after_tsx === CharCode.space ||
2493
- char_after_tsx === CharCode.tab ||
2494
- char_after_tsx === CharCode.lineFeed ||
2495
- char_after_tsx === CharCode.carriageReturn ||
2496
- char_after_tsx === CharCode.colon);
2497
- const is_tsrx_tag =
2498
- this.input.startsWith('tsrx', tag_name_start) &&
2499
- (tag_name_start + 4 >= this.input.length ||
2500
- char_after_tsrx === CharCode.greaterThan ||
2501
- char_after_tsrx === CharCode.slash ||
2502
- char_after_tsrx === CharCode.space ||
2503
- char_after_tsrx === CharCode.tab ||
2504
- char_after_tsrx === CharCode.lineFeed ||
2505
- char_after_tsrx === CharCode.carriageReturn);
2506
-
2507
2165
  const current_template_node = this.#path.findLast(
2508
2166
  (n) =>
2509
2167
  n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2510
2168
  );
2511
- if (
2512
- (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') &&
2513
- !is_tsrx_tag
2514
- ) {
2169
+ const inside_tsx_island =
2170
+ current_template_node?.type === 'Tsx' || current_template_node?.type === 'TsxCompat';
2171
+ if (inside_tsx_island) {
2172
+ if (this.input.charCodeAt(tag_name_start) === CharCode.at) {
2173
+ this.#report_recoverable_error_range(
2174
+ this.start,
2175
+ tag_name_start + 1,
2176
+ DYNAMIC_ELEMENT_IN_TSX_ERROR,
2177
+ );
2178
+ }
2515
2179
  // Inside tsx/tsx:*, let acorn-jsx handle regular TSX tags normally.
2516
- // Nested <tsrx> still needs Ripple's native template parser so it
2517
- // can lower through the same path as <tsrx> in component bodies.
2518
2180
  return super.jsx_parseElement();
2519
2181
  }
2520
2182
 
2521
- if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
2522
- // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
2523
- // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
2524
- this.next();
2525
- const parsed = /** @type {import('estree-jsx').JSXElement} */ (
2526
- /** @type {unknown} */ (this.parseElement())
2183
+ this.next();
2184
+ const parsed = /** @type {import('estree-jsx').JSXElement} */ (
2185
+ /** @type {unknown} */ (this.parseElement())
2186
+ );
2187
+ if (!inside_tsx_island) {
2188
+ this.#popTokenContextsAfterTemplateExpressionElement(
2189
+ /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2527
2190
  );
2528
- if (
2529
- current_template_node?.type !== 'Tsx' &&
2530
- current_template_node?.type !== 'TsxCompat'
2531
- ) {
2532
- this.#popTokenContextsAfterTemplateExpressionElement(
2533
- /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2534
- );
2535
- } else if (this.type === tt.braceR && this.curContext() === tstc.tc_expr) {
2536
- if (this.#tsxIslandExpressionDepth === 0) {
2537
- // Acorn still owns the surrounding JSX expression container.
2538
- // Keep a block-expression context for its closing `}` so the
2539
- // parent TSX tag continues tokenizing as JSX afterward.
2540
- this.context.push(b_expr);
2541
- }
2191
+ } else if (this.type === tt.braceR && this.curContext() === tstc.tc_expr) {
2192
+ if (this.#tsxIslandExpressionDepth === 0) {
2193
+ // Acorn still owns the surrounding JSX expression container.
2194
+ // Keep a block-expression context for its closing `}` so the
2195
+ // parent TSX tag continues tokenizing as JSX afterward.
2196
+ this.context.push(b_expr);
2542
2197
  }
2543
- return parsed;
2544
2198
  }
2545
-
2546
- if (
2547
- !this.#path.findLast((node) => node.type === 'Component') &&
2548
- !this.#functionStack.findLast(is_pascal_case_function)
2549
- ) {
2550
- return super.jsx_parseElement();
2551
- }
2552
-
2553
- const code = this.#functionStack.findLast(is_pascal_case_function)
2554
- ? DIAGNOSTIC_CODES.FUNCTION_COMPONENT_SYNTAX
2555
- : this.#path.findLast((node) => node.type === 'Component') &&
2556
- this.#functionStack.length === 0 &&
2557
- previous_word_before(this.input, this.start) === 'return'
2558
- ? DIAGNOSTIC_CODES.JSX_RETURN_IN_COMPONENT
2559
- : DIAGNOSTIC_CODES.JSX_EXPRESSION_VALUE;
2560
-
2561
- this.#report_recoverable_error(this.start, JSX_EXPRESSION_VALUE_ERROR, code);
2562
- return super.jsx_parseElement();
2199
+ return parsed;
2563
2200
  }
2564
2201
 
2565
2202
  /**
@@ -2598,11 +2235,12 @@ export function TSRXPlugin(config) {
2598
2235
  !is_tsx_compat &&
2599
2236
  open.name.type === 'JSXIdentifier' &&
2600
2237
  open.name.name === 'tsx';
2601
- const is_tsrx =
2238
+ const is_dynamic_name =
2602
2239
  !is_fragment &&
2603
- !is_tsx_compat &&
2604
- open.name.type === 'JSXIdentifier' &&
2605
- open.name.name === 'tsrx';
2240
+ ((open.name.type === 'JSXIdentifier' && open.name.tracked) ||
2241
+ (open.name.type === 'JSXMemberExpression' &&
2242
+ open.name.object.type === 'JSXIdentifier' &&
2243
+ open.name.object.tracked));
2606
2244
 
2607
2245
  if (is_tsx_compat) {
2608
2246
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
@@ -2625,21 +2263,20 @@ export function TSRXPlugin(config) {
2625
2263
  `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
2626
2264
  );
2627
2265
  }
2628
- } else if (is_tsrx) {
2629
- /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
2630
-
2631
- if (open.selfClosing) {
2632
- this.raise(
2633
- open.start,
2634
- `TSRX elements cannot be self-closing. '<tsrx />' must have a closing tag '</tsrx>'.`,
2635
- );
2636
- }
2637
2266
  } else if (is_fragment) {
2638
- /** @type {AST.Tsx} */ (element).type = 'Tsx';
2267
+ /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
2639
2268
  } else {
2640
2269
  element.type = 'Element';
2641
2270
  }
2642
2271
 
2272
+ if (is_tsx && is_dynamic_name) {
2273
+ this.#report_recoverable_error_range(
2274
+ open.name.start ?? open.start,
2275
+ open.name.end ?? open.end,
2276
+ DYNAMIC_ELEMENT_IN_TSX_ERROR,
2277
+ );
2278
+ }
2279
+
2643
2280
  for (const attr of open.attributes) {
2644
2281
  if (attr.type === 'JSXAttribute') {
2645
2282
  /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
@@ -2650,14 +2287,6 @@ export function TSRXPlugin(config) {
2650
2287
  if (attr.value !== null) {
2651
2288
  if (attr.value.type === 'JSXExpressionContainer') {
2652
2289
  const expression = attr.value.expression;
2653
- if (attr.value.style) {
2654
- /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).type = 'Style';
2655
- /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).value =
2656
- /** @type {AST.Literal} */ (expression);
2657
- delete (/** @type {any} */ (attr.value).expression);
2658
- delete (/** @type {any} */ (attr.value).style);
2659
- continue;
2660
- }
2661
2290
  if (expression.type === 'Literal') {
2662
2291
  expression.was_expression = true;
2663
2292
  }
@@ -2669,7 +2298,7 @@ export function TSRXPlugin(config) {
2669
2298
  }
2670
2299
  }
2671
2300
 
2672
- if (!is_tsx_compat && !is_tsx && !is_tsrx && !is_fragment) {
2301
+ if (!is_tsx_compat && !is_tsx && !is_fragment) {
2673
2302
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
2674
2303
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
2675
2304
  );
@@ -2697,27 +2326,26 @@ export function TSRXPlugin(config) {
2697
2326
  } else if (is_fragment) {
2698
2327
  this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2699
2328
  enterScope: true,
2329
+ resetFunctionBodyDepth: true,
2700
2330
  });
2701
2331
 
2702
- if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2703
- this.#path.pop();
2332
+ this.#path.pop();
2704
2333
 
2705
- if (!element.unclosed) {
2706
- const raise_error = () => {
2707
- this.raise(this.start, `Expected closing tag '</>'`);
2708
- };
2334
+ if (!element.unclosed) {
2335
+ const raise_error = () => {
2336
+ this.raise(this.start, `Expected closing tag '</>'`);
2337
+ };
2709
2338
 
2710
- this.next();
2711
- if (this.value !== '/') {
2712
- raise_error();
2713
- }
2714
- this.next();
2715
- if (this.type !== tstt.jsxTagEnd) {
2716
- raise_error();
2717
- }
2718
- this.#popTsxTokenContextBeforeTemplateExpressionChild();
2719
- this.next();
2339
+ this.next();
2340
+ if (this.value !== '/') {
2341
+ raise_error();
2342
+ }
2343
+ this.next();
2344
+ if (this.type !== tstt.jsxTagEnd) {
2345
+ raise_error();
2720
2346
  }
2347
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2348
+ this.next();
2721
2349
  }
2722
2350
  } else {
2723
2351
  if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
@@ -2790,7 +2418,7 @@ export function TSRXPlugin(config) {
2790
2418
  // No closing tag
2791
2419
  this.#report_broken_markup_error(
2792
2420
  open.end,
2793
- "Unclosed tag '<script>'. Expected '</script>' before end of component.",
2421
+ "Unclosed tag '<script>'. Expected '</script>' before end of template.",
2794
2422
  );
2795
2423
  /** @type {AST.Element} */ (element).unclosed = true;
2796
2424
  this.#path.pop();
@@ -2803,16 +2431,8 @@ export function TSRXPlugin(config) {
2803
2431
  const end = input.indexOf('</style>');
2804
2432
  const content = end === -1 ? input : input.slice(0, end);
2805
2433
 
2806
- const component = /** @type {AST.Component} */ (
2807
- this.#path.findLast((n) => n.type === 'Component')
2808
- );
2809
2434
  const parsed_css = parse_style(content, { loose: this.#loose });
2810
-
2811
2435
  if (!inside_head) {
2812
- if (component.css !== null) {
2813
- throw new Error('Components can only have one style tag');
2814
- }
2815
- component.css = parsed_css;
2816
2436
  /** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
2817
2437
  }
2818
2438
 
@@ -2850,7 +2470,7 @@ export function TSRXPlugin(config) {
2850
2470
  } else {
2851
2471
  this.#report_broken_markup_error(
2852
2472
  open.end,
2853
- "Unclosed tag '<style>'. Expected '</style>' before end of component.",
2473
+ "Unclosed tag '<style>'. Expected '</style>' before end of template.",
2854
2474
  );
2855
2475
  /** @type {AST.Element} */ (element).unclosed = true;
2856
2476
  this.#path.pop();
@@ -2874,7 +2494,11 @@ export function TSRXPlugin(config) {
2874
2494
  } else {
2875
2495
  this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2876
2496
  enterScope: true,
2497
+ resetFunctionBodyDepth: true,
2877
2498
  });
2499
+ if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2500
+ this.#reportDynamicJsxElementsInTsx(/** @type {AST.Element} */ (element).children);
2501
+ }
2878
2502
 
2879
2503
  if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2880
2504
  this.#path.pop();
@@ -2939,9 +2563,10 @@ export function TSRXPlugin(config) {
2939
2563
  /** @type {AST.Tsrx} */ (element).type === 'Tsrx' &&
2940
2564
  this.#path[this.#path.length - 1] === element
2941
2565
  ) {
2566
+ const displayTag = element.openingElement.name ? 'tsrx' : '';
2942
2567
  this.#report_broken_markup_error(
2943
2568
  this.start,
2944
- "Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
2569
+ `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
2945
2570
  );
2946
2571
  element.unclosed = true;
2947
2572
  /** @type {AST.SourceLocation} */ (element.loc).end = {
@@ -2957,7 +2582,7 @@ export function TSRXPlugin(config) {
2957
2582
  const tagName = this.getElementName(element.id);
2958
2583
  this.#report_broken_markup_error(
2959
2584
  this.start,
2960
- `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
2585
+ `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of template.`,
2961
2586
  );
2962
2587
  element.unclosed = true;
2963
2588
  /** @type {AST.SourceLocation} */ (element.loc).end = {
@@ -2978,13 +2603,7 @@ export function TSRXPlugin(config) {
2978
2603
  }
2979
2604
  }
2980
2605
 
2981
- if (
2982
- element.closingElement &&
2983
- !is_tsx_compat &&
2984
- !is_tsx &&
2985
- !is_tsrx &&
2986
- element.closingElement.name
2987
- ) {
2606
+ if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
2988
2607
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
2989
2608
  element.closingElement.name,
2990
2609
  );
@@ -3021,10 +2640,10 @@ export function TSRXPlugin(config) {
3021
2640
 
3022
2641
  if (!inside_func) {
3023
2642
  if (this.type.label === 'continue') {
3024
- throw new Error('`continue` statements are not allowed in components');
2643
+ throw new Error('`continue` statements are not allowed in native templates');
3025
2644
  }
3026
2645
  if (this.type.label === 'break') {
3027
- throw new Error('`break` statements are not allowed in components');
2646
+ throw new Error('`break` statements are not allowed in native templates');
3028
2647
  }
3029
2648
  }
3030
2649
 
@@ -3035,6 +2654,16 @@ export function TSRXPlugin(config) {
3035
2654
  );
3036
2655
  return;
3037
2656
  }
2657
+ if (
2658
+ current_template_node?.type === 'Tsrx' &&
2659
+ !current_template_node.openingElement.name &&
2660
+ ((this.type === tstt.jsxTagStart && this.input.slice(this.pos, this.pos + 2) === '/>') ||
2661
+ (this.input.charCodeAt(this.start) === CharCode.lessThan &&
2662
+ this.input.slice(this.start + 1, this.start + 3) === '/>'))
2663
+ ) {
2664
+ this.exprAllowed = false;
2665
+ return;
2666
+ }
3038
2667
  if (this.type === tt.braceL) {
3039
2668
  body.push(this.#parseNativeTemplateExpressionContainer());
3040
2669
  } else if (
@@ -3043,7 +2672,7 @@ export function TSRXPlugin(config) {
3043
2672
  ) {
3044
2673
  body.push(this.parseDoubleQuotedTextChild());
3045
2674
  } else if (this.type === tt.braceR) {
3046
- // Leaving a component/template body. We may still be in TSX/JSX tokenization
2675
+ // Leaving a native template body. We may still be in TSX/JSX tokenization
3047
2676
  // context (e.g. after parsing markup), but the closing `}` is a JS token.
3048
2677
  // If we don't reset this here, the following `next()` can read EOF using
3049
2678
  // `jsx_readToken()` and throw "Unterminated JSX contents".
@@ -3061,8 +2690,8 @@ export function TSRXPlugin(config) {
3061
2690
  if (this.type === tstt.jsxTagStart) {
3062
2691
  this.next();
3063
2692
  } else {
3064
- // A control-flow block inside <tsrx> can leave the tokenizer
3065
- // in normal JS mode, so `</tsrx>` may arrive as a relational
2693
+ // A control-flow block inside a native template can leave the tokenizer
2694
+ // in normal JS mode, so a closing tag may arrive as a relational
3066
2695
  // `<` token. Re-enter JSX closing-tag parsing manually.
3067
2696
  this.pos = startPos + 1;
3068
2697
  this.type = tstt.jsxTagStart;
@@ -3111,7 +2740,7 @@ export function TSRXPlugin(config) {
3111
2740
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
3112
2741
  : this.getElementName(closingElement.name);
3113
2742
  } else if (currentElement.type === 'Tsrx') {
3114
- openingTagName = 'tsrx';
2743
+ openingTagName = '';
3115
2744
  closingTagName =
3116
2745
  closingElement.name?.type === 'JSXNamespacedName'
3117
2746
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
@@ -3137,7 +2766,7 @@ export function TSRXPlugin(config) {
3137
2766
  while (this.#path.length > 0) {
3138
2767
  const elem = this.#path[this.#path.length - 1];
3139
2768
 
3140
- // Stop at non-Element boundaries (Component, etc.)
2769
+ // Stop at non-template boundaries.
3141
2770
  if (
3142
2771
  elem.type !== 'Element' &&
3143
2772
  elem.type !== 'Tsx' &&
@@ -3155,7 +2784,7 @@ export function TSRXPlugin(config) {
3155
2784
  ? 'tsx'
3156
2785
  : null
3157
2786
  : elem.type === 'Tsrx'
3158
- ? 'tsrx'
2787
+ ? ''
3159
2788
  : elem.id
3160
2789
  ? this.getElementName(elem.id)
3161
2790
  : null;
@@ -3183,7 +2812,7 @@ export function TSRXPlugin(config) {
3183
2812
  ) {
3184
2813
  const elementToCloseName =
3185
2814
  elementToClose.type === 'Tsrx'
3186
- ? 'tsrx'
2815
+ ? ''
3187
2816
  : /** @type {AST.Element} */ (elementToClose).id
3188
2817
  ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
3189
2818
  : null;
@@ -3204,6 +2833,7 @@ export function TSRXPlugin(config) {
3204
2833
  } else {
3205
2834
  skipWhitespace(this);
3206
2835
  const node = this.parseStatement(null);
2836
+ this.#report_invalid_template_return_statements(node);
3207
2837
  body.push(node);
3208
2838
 
3209
2839
  // Ensure we're not in JSX context before recursing
@@ -3288,16 +2918,11 @@ export function TSRXPlugin(config) {
3288
2918
  this.type === tt.braceL &&
3289
2919
  this.context.some((c) => c === tstc.tc_expr)
3290
2920
  ) {
3291
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2921
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
3292
2922
  /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
3293
2923
  );
3294
2924
  }
3295
2925
 
3296
- if (this.value === 'component') {
3297
- this.awaitPos = 0;
3298
- return this.parseComponent({ requireName: true, declareName: true });
3299
- }
3300
-
3301
2926
  if (this.type === tstt.jsxTagStart) {
3302
2927
  this.next();
3303
2928
  if (this.value === '/') {
@@ -3317,18 +2942,6 @@ export function TSRXPlugin(config) {
3317
2942
  this.context.pop();
3318
2943
  }
3319
2944
  }
3320
- const context_restore = this.#functionBodyContextRestoreStack.at(-1);
3321
- if (
3322
- this.#functionBodyDepth > 0 &&
3323
- node.type === 'Tsrx' &&
3324
- context_restore?.canRestore &&
3325
- this.type !== tt.braceR &&
3326
- this.type !== tt.comma
3327
- ) {
3328
- context_restore.restore = true;
3329
- this.context = [b_stat];
3330
- this.exprAllowed = true;
3331
- }
3332
2945
  return node;
3333
2946
  }
3334
2947
 
@@ -3336,7 +2949,7 @@ export function TSRXPlugin(config) {
3336
2949
  this.#functionBodyDepth === 0 &&
3337
2950
  this.type === tt.string &&
3338
2951
  this.input.charCodeAt(this.start) === CharCode.doubleQuote &&
3339
- (this.#path.at(-1)?.type === 'Component' || this.#path.at(-1)?.type === 'Element')
2952
+ (this.#path.at(-1)?.type === 'Element' || this.#path.at(-1)?.type === 'Tsrx')
3340
2953
  ) {
3341
2954
  this.pos = this.start;
3342
2955
  this.#readDoubleQuotedTextChildToken();
@@ -3386,11 +2999,11 @@ export function TSRXPlugin(config) {
3386
2999
  const parent = this.#path.at(-1);
3387
3000
 
3388
3001
  // Inside a JS function body, parse `{...}` as a regular block statement,
3389
- // even if the nearest `#path` entry is a Component/Element — we're in a
3002
+ // even if the nearest `#path` entry is a native template — we're in a
3390
3003
  // nested function callable, not in a template.
3391
3004
  if (
3392
3005
  this.#functionBodyDepth === 0 &&
3393
- (parent?.type === 'Component' || parent?.type === 'Element')
3006
+ (parent?.type === 'Element' || parent?.type === 'Tsrx')
3394
3007
  ) {
3395
3008
  if (createNewLexicalScope === void 0) createNewLexicalScope = true;
3396
3009
  if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());