@tsrx/core 0.1.16 → 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,7 @@ 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';
24
20
  const DYNAMIC_ELEMENT_IN_TSX_ERROR =
25
21
  'Dynamic element syntax (`<@...>`) is only supported in native TSRX templates.';
26
22
  const DYNAMIC_ATTRIBUTE_NAME_ERROR =
@@ -205,44 +201,9 @@ function looks_like_generic_arrow(input, pos) {
205
201
  );
206
202
  }
207
203
 
208
- /**
209
- * @param {AST.Node | null | undefined} node
210
- * @returns {boolean}
211
- */
212
- function is_pascal_case_function(node) {
213
- if (node && 'id' in node && node.id && node.id.type === 'Identifier') {
214
- return /^[A-Z]/.test(node.id.name);
215
- }
216
- return false;
217
- }
218
-
219
- /**
220
- * @param {string} input
221
- * @param {number} pos
222
- */
223
- function previous_word_before(input, pos) {
224
- let i = pos - 1;
225
- while (i >= 0) {
226
- const ch = input.charCodeAt(i);
227
- if (
228
- ch !== CharCode.space &&
229
- ch !== CharCode.tab &&
230
- ch !== CharCode.lineFeed &&
231
- ch !== CharCode.carriageReturn
232
- )
233
- break;
234
- i--;
235
- }
236
- const end = i + 1;
237
- while (i >= 0 && /[$_\p{ID_Continue}]/u.test(input[i])) {
238
- i--;
239
- }
240
- return input.slice(i + 1, end);
241
- }
242
-
243
204
  /**
244
205
  * Acorn parser plugin for Ripple syntax extensions.
245
- * Adds support for: component declarations, &[]/&{} lazy destructuring,
206
+ * Adds support for: native TSRX templates, &[]/&{} lazy destructuring,
246
207
  * submodule imports, TSRX directives, and enhanced JSX handling.
247
208
  *
248
209
  * @param {import('../types/index').TSRXPluginConfig} [config] - Plugin configuration
@@ -268,18 +229,14 @@ export function TSRXPlugin(config) {
268
229
  #commentContextId = 0;
269
230
  #collect = false;
270
231
  #loose = false;
271
- /** @type {AST.Node[]} */
272
- #functionStack = [];
273
- /** @type {Array<{ parentContext: any[], canRestore: boolean, restore: boolean }>} */
274
- #functionBodyContextRestoreStack = [];
275
232
  /** @type {import('../types/index').CompileError[] | undefined} */
276
233
  #errors = undefined;
277
234
  /** @type {string | null} */
278
235
  #filename = null;
279
- #componentDepth = 0;
280
236
  #functionBodyDepth = 0;
281
237
  #allowExpressionContainerTrailingSemicolon = false;
282
238
  #tsxIslandExpressionDepth = 0;
239
+ #jsxAttributeValueExpressionDepth = 0;
283
240
 
284
241
  /**
285
242
  * @type {Parse.Parser['finishNode']}
@@ -334,16 +291,8 @@ export function TSRXPlugin(config) {
334
291
  return null;
335
292
  }
336
293
 
337
- #isInsideComponent() {
338
- return this.#componentDepth > 0;
339
- }
340
-
341
- #isInsideComponentTemplate() {
342
- return this.#isInsideComponent() && this.#functionBodyDepth === 0;
343
- }
344
-
345
294
  /**
346
- * Component bodies and native TSRX element bodies share the same grammar.
295
+ * Native TSRX template bodies share one grammar across elements and fragments.
347
296
  * This helper keeps the parser-state setup in one place while callers keep
348
297
  * ownership of their distinct closing delimiter handling (`}` vs `</tag>`).
349
298
  *
@@ -352,19 +301,13 @@ export function TSRXPlugin(config) {
352
301
  * @param {{
353
302
  * enterScope?: boolean,
354
303
  * pushPath?: boolean,
355
- * trackComponentDepth?: boolean,
356
304
  * resetFunctionBodyDepth?: boolean,
357
305
  * }} [options]
358
306
  */
359
307
  #parseNativeTemplateBody(
360
308
  node,
361
309
  body,
362
- {
363
- enterScope = false,
364
- pushPath = false,
365
- trackComponentDepth = false,
366
- resetFunctionBodyDepth = false,
367
- } = {},
310
+ { enterScope = false, pushPath = false, resetFunctionBodyDepth = false } = {},
368
311
  ) {
369
312
  const parent_function_body_depth = this.#functionBodyDepth;
370
313
 
@@ -377,16 +320,10 @@ export function TSRXPlugin(config) {
377
320
  if (pushPath) {
378
321
  this.#path.push(node);
379
322
  }
380
- if (trackComponentDepth) {
381
- this.#componentDepth++;
382
- }
383
323
 
384
324
  try {
385
325
  this.parseTemplateBody(body);
386
326
  } finally {
387
- if (trackComponentDepth) {
388
- this.#componentDepth--;
389
- }
390
327
  if (pushPath) {
391
328
  this.#path.pop();
392
329
  }
@@ -404,7 +341,6 @@ export function TSRXPlugin(config) {
404
341
  */
405
342
  #isNativeTemplateNode(node) {
406
343
  return (
407
- node?.type === 'Component' ||
408
344
  node?.type === 'Element' ||
409
345
  node?.type === 'Tsx' ||
410
346
  node?.type === 'Tsrx' ||
@@ -417,14 +353,8 @@ export function TSRXPlugin(config) {
417
353
  */
418
354
  #reportDynamicJsxElementsInTsx(children) {
419
355
  for (const child of children) {
420
- if (child?.type === 'Tsrx') {
421
- continue;
422
- }
423
356
  if (child?.type === 'JSXElement') {
424
357
  const name = child.openingElement?.name;
425
- if (name?.type === 'JSXIdentifier' && name.name === 'tsrx') {
426
- continue;
427
- }
428
358
  const is_dynamic_name =
429
359
  (name?.type === 'JSXIdentifier' && name.tracked) ||
430
360
  (name?.type === 'JSXMemberExpression' &&
@@ -456,26 +386,11 @@ export function TSRXPlugin(config) {
456
386
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
457
387
  // but convert other expressions to native TSRX child nodes.
458
388
  if (node.expression.type !== 'JSXEmptyExpression') {
459
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
460
- /** @type {unknown} */ (node)
461
- ).type = node.html
462
- ? 'Html'
463
- : node.text
464
- ? 'Text'
465
- : node.style
466
- ? 'Style'
467
- : 'TSRXExpression';
468
- if (node.style) {
469
- /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
470
- /** @type {AST.Literal} */ (node.expression);
471
- delete (/** @type {any} */ (node).expression);
472
- }
473
- delete node.html;
474
- delete node.text;
475
- delete node.style;
476
- }
477
-
478
- 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} */ (
479
394
  /** @type {unknown} */ (node)
480
395
  );
481
396
  }
@@ -499,7 +414,7 @@ export function TSRXPlugin(config) {
499
414
  const displayTag = tagName || '';
500
415
  this.#report_broken_markup_error(
501
416
  this.start,
502
- `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of component.`,
417
+ `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
503
418
  );
504
419
  island.unclosed = true;
505
420
  /** @type {AST.NodeWithLocation} */ (island).loc.end = {
@@ -585,25 +500,16 @@ export function TSRXPlugin(config) {
585
500
  */
586
501
  #isReservedTemplateTagNameStart(index) {
587
502
  const char_after_tsx = this.input.charCodeAt(index + 3);
588
- const char_after_tsrx = this.input.charCodeAt(index + 4);
589
503
  return (
590
- (this.input.startsWith('tsx', index) &&
591
- (index + 3 >= this.input.length ||
592
- char_after_tsx === CharCode.greaterThan ||
593
- char_after_tsx === CharCode.slash ||
594
- char_after_tsx === CharCode.space ||
595
- char_after_tsx === CharCode.tab ||
596
- char_after_tsx === CharCode.lineFeed ||
597
- char_after_tsx === CharCode.carriageReturn ||
598
- char_after_tsx === CharCode.colon)) ||
599
- (this.input.startsWith('tsrx', index) &&
600
- (index + 4 >= this.input.length ||
601
- char_after_tsrx === CharCode.greaterThan ||
602
- char_after_tsrx === CharCode.slash ||
603
- char_after_tsrx === CharCode.space ||
604
- char_after_tsrx === CharCode.tab ||
605
- char_after_tsrx === CharCode.lineFeed ||
606
- 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)
607
513
  );
608
514
  }
609
515
 
@@ -635,7 +541,7 @@ export function TSRXPlugin(config) {
635
541
  while (this.pos < this.input.length) {
636
542
  const ch = this.input.charCodeAt(this.pos);
637
543
 
638
- // Stop at opening tag, expression, or the component-closing brace
544
+ // Stop at opening tag, expression, or the template-closing brace
639
545
  if (ch === CharCode.lessThan || ch === CharCode.openBrace || ch === CharCode.closeBrace) {
640
546
  break;
641
547
  }
@@ -827,13 +733,13 @@ export function TSRXPlugin(config) {
827
733
  }
828
734
  }
829
735
 
830
- // Inside `{<tsrx>...</tsrx>}` JSX expression container — strip
736
+ // Inside a native template JSX expression container — strip
831
737
  // both the leaked `b_stat` and the container's `tc_expr`.
832
738
  if (top === b_stat && second === tstc.tc_expr) {
833
739
  ctx.length = ci - 1;
834
740
  return;
835
741
  }
836
- // Statement-bodied `<tsrx>` attributes can leave the attribute's
742
+ // Statement-bodied native template attributes can leave the attribute's
837
743
  // expression contexts above the still-open JSX tag context. Strip
838
744
  // those so a following `/>` stays in JSX opening-tag mode.
839
745
  if (
@@ -871,10 +777,7 @@ export function TSRXPlugin(config) {
871
777
  }
872
778
 
873
779
  const parent = this.#path.at(-1);
874
- if (
875
- !parent ||
876
- (parent.type !== 'Component' && parent.type !== 'Element' && parent.type !== 'Tsrx')
877
- ) {
780
+ if (!parent || (parent.type !== 'Element' && parent.type !== 'Tsrx')) {
878
781
  return false;
879
782
  }
880
783
 
@@ -981,6 +884,75 @@ export function TSRXPlugin(config) {
981
884
  this.raise(position, message);
982
885
  }
983
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
+
984
956
  /**
985
957
  * When collecting, keep parsing after duplicate declaration diagnostics so
986
958
  * editor tooling can continue producing AST and mappings.
@@ -1050,64 +1022,6 @@ export function TSRXPlugin(config) {
1050
1022
  }
1051
1023
  }
1052
1024
 
1053
- /**
1054
- * Override parseProperty to support component methods in object literals.
1055
- * Handles syntax like `{ component something() { <div /> } }`
1056
- * Also supports computed names: `{ component ['something']() { <div /> } }`
1057
- * @type {Parse.Parser['parseProperty']}
1058
- */
1059
- parseProperty(isPattern, refDestructuringErrors) {
1060
- // Check if this is a component method: component name( ... ) { ... }
1061
- if (!isPattern && this.type === tt.name && this.value === 'component') {
1062
- // Look ahead to see if this is "component identifier(", "component identifier<", "component [", or "component 'string'"
1063
- const lookahead = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
1064
- if (lookahead) {
1065
- // This is a component method definition
1066
- const prop = /** @type {AST.Property} */ (this.startNode());
1067
- const isComputed = lookahead[0].trim().startsWith('[');
1068
- const isStringLiteral = /^['"]/.test(lookahead[0].trim());
1069
-
1070
- if (isComputed) {
1071
- // For computed names, consume 'component'
1072
- // parse the key, then parse component without name
1073
- this.next(); // consume 'component'
1074
- this.next(); // consume '['
1075
- prop.key = this.parseExpression();
1076
- this.expect(tt.bracketR);
1077
- prop.computed = true;
1078
-
1079
- // Parse component without name (skipName: true)
1080
- const component_node = this.parseComponent({ skipName: true });
1081
- /** @type {AST.TSRXProperty} */ (prop).value = component_node;
1082
- } else if (isStringLiteral) {
1083
- // For string literal names, consume 'component'
1084
- // parse the string key, then parse component without name
1085
- this.next(); // consume 'component'
1086
- prop.key = /** @type {AST.Literal} */ (this.parseExprAtom());
1087
- prop.computed = false;
1088
-
1089
- // Parse component without name (skipName: true)
1090
- const component_node = this.parseComponent({ skipName: true });
1091
- /** @type {AST.TSRXProperty} */ (prop).value = component_node;
1092
- } else {
1093
- const component_node = this.parseComponent({ requireName: true });
1094
-
1095
- prop.key = /** @type {AST.Identifier} */ (component_node.id);
1096
- /** @type {AST.TSRXProperty} */ (prop).value = component_node;
1097
- prop.computed = false;
1098
- }
1099
-
1100
- prop.shorthand = false;
1101
- prop.method = true;
1102
- prop.kind = 'init';
1103
-
1104
- return this.finishNode(prop, 'Property');
1105
- }
1106
- }
1107
-
1108
- return super.parseProperty(isPattern, refDestructuringErrors);
1109
- }
1110
-
1111
1025
  /**
1112
1026
  * Override parsePropertyValue to support TypeScript generic methods in object literals.
1113
1027
  * By default, acorn-typescript doesn't handle `{ method<T>() {} }` syntax.
@@ -1247,16 +1161,17 @@ export function TSRXPlugin(config) {
1247
1161
  * @type {Parse.Parser['getTokenFromCode']}
1248
1162
  */
1249
1163
  getTokenFromCode(code) {
1250
- // Callback props that return `<tsrx>...</tsrx>` without a semicolon can
1164
+ // Callback props that return native templates without a semicolon can
1251
1165
  // leave the attribute expression context above the still-open tag. Drop
1252
1166
  // it before tokenizing `/>`, otherwise Acorn treats `/` as a regexp.
1253
1167
  if (
1254
1168
  code === CharCode.slash &&
1255
1169
  this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan &&
1256
- this.curContext() === b_expr &&
1257
- this.context[this.context.length - 2] === tstc.tc_oTag
1170
+ this.context.includes(tstc.tc_oTag)
1258
1171
  ) {
1259
- this.context.pop();
1172
+ while (this.context.length > 0 && this.curContext() !== tstc.tc_oTag) {
1173
+ this.context.pop();
1174
+ }
1260
1175
  this.exprAllowed = false;
1261
1176
  }
1262
1177
  if (code === CharCode.doubleQuote) {
@@ -1275,7 +1190,10 @@ export function TSRXPlugin(config) {
1275
1190
 
1276
1191
  if (code === CharCode.lessThan) {
1277
1192
  // < character
1278
- 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');
1279
1197
  /** @type {number | null} */
1280
1198
  let prevNonWhitespaceChar = null;
1281
1199
 
@@ -1314,7 +1232,7 @@ export function TSRXPlugin(config) {
1314
1232
  }
1315
1233
  }
1316
1234
 
1317
- // Support parsing standalone template markup at the top-level (outside `component`)
1235
+ // Support parsing standalone template markup at the top-level
1318
1236
  // for tooling like Prettier, e.g.:
1319
1237
  // <Something>...</Something>\n\n<Child />
1320
1238
  // <head><style>...</style></head>
@@ -1343,13 +1261,13 @@ export function TSRXPlugin(config) {
1343
1261
  prevNonWhitespaceChar === CharCode.closeBrace ||
1344
1262
  prevNonWhitespaceChar === CharCode.greaterThan;
1345
1263
 
1346
- if (!inComponent && prevAllowsTagStart && isTagLikeAfterLt) {
1264
+ if (!inNativeTemplate && prevAllowsTagStart && isTagLikeAfterLt) {
1347
1265
  ++this.pos;
1348
1266
  return this.finishToken(tstt.jsxTagStart);
1349
1267
  }
1350
1268
 
1351
- if (inComponent) {
1352
- // Inside component template bodies, allow adjacent tags without requiring
1269
+ if (inNativeTemplate) {
1270
+ // Inside native template bodies, allow adjacent tags without requiring
1353
1271
  // a newline/indentation before the next '<'. This is important for inputs
1354
1272
  // like `<div />` and `</div><style>...</style>` which Prettier formats.
1355
1273
  if (
@@ -1488,36 +1406,6 @@ export function TSRXPlugin(config) {
1488
1406
  return super.checkLValSimple(expr, bindingType, checkClashes);
1489
1407
  }
1490
1408
 
1491
- /**
1492
- * Components do not use Acorn's normal function-body parser, but they
1493
- * should still report duplicate parameter names like functions do. Keep
1494
- * this validation on `BIND_OUTSIDE` so params are checked without being
1495
- * declared in the component template scope, preserving existing shadowing
1496
- * behavior.
1497
- *
1498
- * @param {AST.Pattern[]} params
1499
- */
1500
- checkComponentParams(params) {
1501
- /** @type {Record<string, boolean>} */
1502
- const name_hash = Object.create(null);
1503
- for (const param of params || []) {
1504
- this.checkLValInnerPattern(param, BINDING_TYPES.BIND_OUTSIDE, name_hash);
1505
- }
1506
- }
1507
-
1508
- /**
1509
- * Parse expression atom - handles RippleArray and RippleObject literals
1510
- * @type {Parse.Parser['parseExprAtom']}
1511
- */
1512
- parseExprAtom(refDestructuringErrors, forNew, forInit) {
1513
- // Check if this is a component expression (e.g., in object literal values)
1514
- if (this.type === tt.name && this.value === 'component') {
1515
- return this.parseComponent();
1516
- }
1517
-
1518
- return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
1519
- }
1520
-
1521
1409
  /**
1522
1410
  * Override to track parenthesized expressions in metadata
1523
1411
  * This allows the prettier plugin to preserve parentheses where they existed
@@ -1560,108 +1448,6 @@ export function TSRXPlugin(config) {
1560
1448
  this.undefinedExports[name] = id;
1561
1449
  }
1562
1450
 
1563
- /**
1564
- * Parse a component - common implementation used by statements, expressions, and export defaults
1565
- * @type {Parse.Parser['parseComponent']}
1566
- */
1567
- parseComponent({
1568
- requireName = false,
1569
- isDefault = false,
1570
- declareName = false,
1571
- skipName = false,
1572
- } = {}) {
1573
- const node = /** @type {AST.Component} */ (this.startNode());
1574
- const parent_context = [...this.context];
1575
- const restore_parent_context =
1576
- !requireName &&
1577
- this.#isInsideComponent() &&
1578
- this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag);
1579
- node.type = 'Component';
1580
- node.css = null;
1581
- node.default = isDefault;
1582
-
1583
- // skipName is used for computed property names where 'component' and the key
1584
- // have already been consumed before calling parseComponent
1585
- if (!skipName) {
1586
- this.next(); // consume 'component'
1587
- }
1588
- this.enterScope(0);
1589
-
1590
- if (skipName) {
1591
- // For computed names, the key is parsed separately, so id is null
1592
- node.id = null;
1593
- } else if (requireName) {
1594
- node.id = this.parseIdent();
1595
- if (declareName) {
1596
- this.declareName(
1597
- node.id.name,
1598
- BINDING_TYPES.BIND_FUNCTION,
1599
- /** @type {AST.NodeWithLocation} */ (node.id).start,
1600
- );
1601
- }
1602
- } else {
1603
- node.id = this.type.label === 'name' ? this.parseIdent() : null;
1604
- if (declareName && node.id) {
1605
- this.declareName(
1606
- node.id.name,
1607
- BINDING_TYPES.BIND_FUNCTION,
1608
- /** @type {AST.NodeWithLocation} */ (node.id).start,
1609
- );
1610
- }
1611
- }
1612
-
1613
- this.parseFunctionParams(node);
1614
- this.checkComponentParams(node.params);
1615
-
1616
- const is_arrow_component = this.type === tt.arrow;
1617
- if (is_arrow_component) {
1618
- if (node.id || requireName || skipName) {
1619
- this.raise(
1620
- this.start,
1621
- 'Arrow component syntax is only supported for anonymous component expressions.',
1622
- );
1623
- }
1624
- node.metadata ??= { path: [] };
1625
- node.metadata.arrow = true;
1626
- this.next();
1627
- }
1628
-
1629
- if (this.type === tt.braceL) {
1630
- this.#allowDoubleQuotedTextChildAfterBrace = true;
1631
- }
1632
- this.eat(tt.braceL);
1633
- node.body = [];
1634
- this.#parseNativeTemplateBody(node, node.body, {
1635
- pushPath: true,
1636
- trackComponentDepth: true,
1637
- resetFunctionBodyDepth: true,
1638
- });
1639
- this.exitScope();
1640
-
1641
- this.next();
1642
- skipWhitespace(this);
1643
- if (restore_parent_context) {
1644
- this.context = this.type === tt.braceR ? parent_context.slice(0, -1) : parent_context;
1645
- this.exprAllowed = false;
1646
- }
1647
- this.finishNode(node, 'Component');
1648
- this.awaitPos = 0;
1649
-
1650
- return node;
1651
- }
1652
-
1653
- /**
1654
- * @type {Parse.Parser['parseExportDefaultDeclaration']}
1655
- */
1656
- parseExportDefaultDeclaration() {
1657
- // Check if this is "export default component"
1658
- if (this.value === 'component') {
1659
- return this.parseComponent({ isDefault: true });
1660
- }
1661
-
1662
- return super.parseExportDefaultDeclaration();
1663
- }
1664
-
1665
1451
  /** @type {Parse.Parser['parseForStatement']} */
1666
1452
  parseForStatement(node) {
1667
1453
  this.next();
@@ -1860,77 +1646,13 @@ export function TSRXPlugin(config) {
1860
1646
  */
1861
1647
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1862
1648
  this.#functionBodyDepth++;
1863
- this.#functionStack.push(node);
1864
- const context_restore = {
1865
- parentContext: [...this.context],
1866
- canRestore:
1867
- this.#isInsideComponent() &&
1868
- this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag),
1869
- restore: false,
1870
- };
1871
- this.#functionBodyContextRestoreStack.push(context_restore);
1872
- // Inside a component, nested JS function bodies should parse like
1873
- // ordinary functions, not component template bodies.
1874
- if (
1875
- // Only adjust functions declared while parsing a component body.
1876
- this.#isInsideComponent() &&
1877
- // A stale JSX expression context means the surrounding template
1878
- // tokenizer can still treat `<` as template markup.
1879
- this.context.some((context) => context === tstc.tc_expr) &&
1880
- // Keep callback props on their surrounding JSX attribute path until
1881
- // statement-position TSRX needs to suspend it.
1882
- !context_restore.canRestore &&
1883
- // Only reset statement-level function bodies, not expression
1884
- // contexts that are actively parsing JSX.
1885
- this.curContext() === b_stat
1886
- ) {
1887
- this.context = [b_stat];
1888
- }
1889
-
1890
1649
  try {
1891
1650
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
1892
1651
  } finally {
1893
- if (context_restore.restore) {
1894
- this.context = context_restore.parentContext.slice(0, -1);
1895
- this.exprAllowed = false;
1896
- }
1897
- this.#functionBodyContextRestoreStack.pop();
1898
- this.#functionStack.pop();
1899
1652
  this.#functionBodyDepth--;
1900
1653
  }
1901
1654
  }
1902
1655
 
1903
- /**
1904
- * @type {Parse.Parser['checkUnreserved']}
1905
- */
1906
- checkUnreserved(ref) {
1907
- if (ref.name === 'component') {
1908
- // Allow 'component' when it's followed by an identifier and '(' or '<' (component method in object literal)
1909
- // e.g., { component something() { ... } }
1910
- // Also allow computed names: { component ['name']() { ... } }
1911
- // Also allow string literal names: { component 'name'() { ... } }
1912
- const nextChars = this.input.slice(this.pos).match(/^\s*(?:(\w+)\s*[(<]|\[|['"])/);
1913
- if (!nextChars) {
1914
- this.raise(
1915
- ref.start,
1916
- '"component" is a TSRX keyword and cannot be used as an identifier',
1917
- );
1918
- }
1919
- }
1920
- return super.checkUnreserved(ref);
1921
- }
1922
-
1923
- /** @type {Parse.Parser['shouldParseExportStatement']} */
1924
- shouldParseExportStatement() {
1925
- if (super.shouldParseExportStatement()) {
1926
- return true;
1927
- }
1928
- if (this.value === 'component') {
1929
- return true;
1930
- }
1931
- return this.type.keyword === 'var';
1932
- }
1933
-
1934
1656
  /**
1935
1657
  * @return {ESTreeJSX.JSXExpressionContainer}
1936
1658
  */
@@ -1938,59 +1660,8 @@ export function TSRXPlugin(config) {
1938
1660
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1939
1661
  this.next();
1940
1662
 
1941
- if (this.type === tt.name && this.value === 'ref') {
1942
- const ref_node = /** @type {AST.RefExpression} */ (this.startNode());
1943
- this.next();
1944
- if (this.type === tt.braceR) {
1945
- this.raise(
1946
- this.start,
1947
- '"ref" is a TSRX keyword and must be used in the form {ref item}',
1948
- );
1949
- }
1950
- ref_node.argument = this.parseMaybeAssign();
1951
- node.expression = /** @type {any} */ (this.finishNode(ref_node, 'RefExpression'));
1952
- this.expect(tt.braceR);
1953
- return this.finishNode(node, 'JSXExpressionContainer');
1954
- }
1955
-
1956
- if (this.type === tt.name && this.value === 'html') {
1957
- node.html = true;
1958
- this.next();
1959
- if (this.type === tt.braceR) {
1960
- this.raise(
1961
- this.start,
1962
- '"html" is a TSRX keyword and must be used in the form {html some_content}',
1963
- );
1964
- }
1965
- } else if (this.type === tt.name && this.value === 'text') {
1966
- node.text = true;
1967
- this.next();
1968
- if (this.type === tt.braceR) {
1969
- this.raise(
1970
- this.start,
1971
- '"text" is a TSRX keyword and must be used in the form {text some_value}',
1972
- );
1973
- }
1974
- } else if (
1975
- this.type === tt.name &&
1976
- this.value === 'style' &&
1977
- this.lookahead().type === tt.string
1978
- ) {
1979
- node.style = true;
1980
- this.next();
1981
- }
1982
-
1983
1663
  node.expression =
1984
1664
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1985
- if (
1986
- node.style &&
1987
- (node.expression.type !== 'Literal' || typeof node.expression.value !== 'string')
1988
- ) {
1989
- this.raise(
1990
- /** @type {number} */ (node.expression.start),
1991
- '"style" is a TSRX keyword and must be used in the form {style "class_name"}',
1992
- );
1993
- }
1994
1665
  if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
1995
1666
  if (this.#collect) {
1996
1667
  this.#report_recoverable_error(
@@ -2088,39 +1759,7 @@ export function TSRXPlugin(config) {
2088
1759
  this.unexpected();
2089
1760
  }
2090
1761
 
2091
- if (this.value === 'ref') {
2092
- this.next();
2093
- if (this.type === tt.braceR) {
2094
- this.raise(
2095
- this.start,
2096
- '"ref" is a Ripple keyword and must be used in the form {ref fn}',
2097
- );
2098
- }
2099
- /** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
2100
- this.expect(tt.braceR);
2101
- return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
2102
- } else if (this.type === tt.name && this.value === 'html') {
2103
- // {html ...}
2104
- // The support is purely for better error messages to avoid
2105
- // the parser throw an unexpected token error
2106
- const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
2107
- id.tracked = false;
2108
- this.finishNode(id, 'Identifier');
2109
- this.next();
2110
- const value = this.type === tt.braceR ? id : this.parseMaybeAssign();
2111
- const report_end = this.type === tt.braceR ? this.end : (value.end ?? this.end);
2112
- this.#report_recoverable_error_range(
2113
- node.start ?? id.start ?? this.start,
2114
- report_end,
2115
- HTML_ATTRIBUTE_VALUE_ERROR,
2116
- DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
2117
- );
2118
- /** @type {AST.Attribute} */ (node).name = id;
2119
- /** @type {AST.Attribute} */ (node).value = value;
2120
- /** @type {AST.Attribute} */ (node).shorthand = false;
2121
- this.expect(tt.braceR);
2122
- return this.finishNode(node, 'Attribute');
2123
- } else if (this.type === tt.ellipsis) {
1762
+ if (this.type === tt.ellipsis) {
2124
1763
  this.expect(tt.ellipsis);
2125
1764
  /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
2126
1765
  this.expect(tt.braceR);
@@ -2160,14 +1799,6 @@ export function TSRXPlugin(config) {
2160
1799
  const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
2161
1800
  this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
2162
1801
  );
2163
- if (value?.type === 'JSXExpressionContainer' && value.html) {
2164
- this.#report_recoverable_error_range(
2165
- value.start ?? node.start ?? this.start,
2166
- value.end ?? node.end ?? this.end,
2167
- HTML_ATTRIBUTE_VALUE_ERROR,
2168
- DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
2169
- );
2170
- }
2171
1802
  /** @type {ESTreeJSX.JSXAttribute} */ (node).value = value;
2172
1803
  return this.finishNode(node, 'JSXAttribute');
2173
1804
  }
@@ -2263,7 +1894,12 @@ export function TSRXPlugin(config) {
2263
1894
  jsx_parseAttributeValue() {
2264
1895
  switch (this.type) {
2265
1896
  case tt.braceL:
2266
- return this.jsx_parseExpressionContainer();
1897
+ this.#jsxAttributeValueExpressionDepth++;
1898
+ try {
1899
+ return this.jsx_parseExpressionContainer();
1900
+ } finally {
1901
+ this.#jsxAttributeValueExpressionDepth--;
1902
+ }
2267
1903
  case tstt.jsxTagStart:
2268
1904
  case tt.string:
2269
1905
  return this.parseExprAtom();
@@ -2480,7 +2116,6 @@ export function TSRXPlugin(config) {
2480
2116
  if (
2481
2117
  ch === CharCode.closeBrace &&
2482
2118
  (this.#path.length === 0 ||
2483
- this.#path.at(-1)?.type === 'Component' ||
2484
2119
  this.#path.at(-1)?.type === 'Element' ||
2485
2120
  this.#path.at(-1)?.type === 'Tsrx')
2486
2121
  ) {
@@ -2519,49 +2154,21 @@ export function TSRXPlugin(config) {
2519
2154
  }
2520
2155
 
2521
2156
  /**
2522
- * Override jsx_parseElement to intercept expression-level JSX.
2523
- * This is called by acorn-jsx's parseExprAtom when it encounters <
2524
- * in expression position. Bare fragments are treated as shorthand
2525
- * for <tsx>...</tsx>. <tsrx>...</tsrx> admits native TSRX
2526
- * template syntax as an expression value. Other tags must still use
2527
- * <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.
2528
2160
  * @type {Parse.Parser['jsx_parseElement']}
2529
2161
  */
2530
2162
  jsx_parseElement() {
2531
- // Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
2532
2163
  // Current token is jsxTagStart, this.end is position after '<'
2533
2164
  const tag_name_start = this.end;
2534
- const is_fragment_tag = this.input.charCodeAt(tag_name_start) === CharCode.greaterThan;
2535
- const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
2536
- const char_after_tsrx = this.input.charCodeAt(tag_name_start + 4);
2537
- const is_tsx_tag =
2538
- this.input.startsWith('tsx', tag_name_start) &&
2539
- (tag_name_start + 3 >= this.input.length ||
2540
- char_after_tsx === CharCode.greaterThan ||
2541
- char_after_tsx === CharCode.slash ||
2542
- char_after_tsx === CharCode.space ||
2543
- char_after_tsx === CharCode.tab ||
2544
- char_after_tsx === CharCode.lineFeed ||
2545
- char_after_tsx === CharCode.carriageReturn ||
2546
- char_after_tsx === CharCode.colon);
2547
- const is_tsrx_tag =
2548
- this.input.startsWith('tsrx', tag_name_start) &&
2549
- (tag_name_start + 4 >= this.input.length ||
2550
- char_after_tsrx === CharCode.greaterThan ||
2551
- char_after_tsrx === CharCode.slash ||
2552
- char_after_tsrx === CharCode.space ||
2553
- char_after_tsrx === CharCode.tab ||
2554
- char_after_tsrx === CharCode.lineFeed ||
2555
- char_after_tsrx === CharCode.carriageReturn);
2556
-
2557
2165
  const current_template_node = this.#path.findLast(
2558
2166
  (n) =>
2559
2167
  n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2560
2168
  );
2561
- if (
2562
- (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') &&
2563
- !is_tsrx_tag
2564
- ) {
2169
+ const inside_tsx_island =
2170
+ current_template_node?.type === 'Tsx' || current_template_node?.type === 'TsxCompat';
2171
+ if (inside_tsx_island) {
2565
2172
  if (this.input.charCodeAt(tag_name_start) === CharCode.at) {
2566
2173
  this.#report_recoverable_error_range(
2567
2174
  this.start,
@@ -2570,53 +2177,26 @@ export function TSRXPlugin(config) {
2570
2177
  );
2571
2178
  }
2572
2179
  // Inside tsx/tsx:*, let acorn-jsx handle regular TSX tags normally.
2573
- // Nested <tsrx> still needs Ripple's native template parser so it
2574
- // can lower through the same path as <tsrx> in component bodies.
2575
2180
  return super.jsx_parseElement();
2576
2181
  }
2577
2182
 
2578
- if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
2579
- // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
2580
- // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
2581
- this.next();
2582
- const parsed = /** @type {import('estree-jsx').JSXElement} */ (
2583
- /** @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)),
2584
2190
  );
2585
- if (
2586
- current_template_node?.type !== 'Tsx' &&
2587
- current_template_node?.type !== 'TsxCompat'
2588
- ) {
2589
- this.#popTokenContextsAfterTemplateExpressionElement(
2590
- /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2591
- );
2592
- } else if (this.type === tt.braceR && this.curContext() === tstc.tc_expr) {
2593
- if (this.#tsxIslandExpressionDepth === 0) {
2594
- // Acorn still owns the surrounding JSX expression container.
2595
- // Keep a block-expression context for its closing `}` so the
2596
- // parent TSX tag continues tokenizing as JSX afterward.
2597
- this.context.push(b_expr);
2598
- }
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);
2599
2197
  }
2600
- return parsed;
2601
2198
  }
2602
-
2603
- if (
2604
- !this.#path.findLast((node) => node.type === 'Component') &&
2605
- !this.#functionStack.findLast(is_pascal_case_function)
2606
- ) {
2607
- return super.jsx_parseElement();
2608
- }
2609
-
2610
- const code = this.#functionStack.findLast(is_pascal_case_function)
2611
- ? DIAGNOSTIC_CODES.FUNCTION_COMPONENT_SYNTAX
2612
- : this.#path.findLast((node) => node.type === 'Component') &&
2613
- this.#functionStack.length === 0 &&
2614
- previous_word_before(this.input, this.start) === 'return'
2615
- ? DIAGNOSTIC_CODES.JSX_RETURN_IN_COMPONENT
2616
- : DIAGNOSTIC_CODES.JSX_EXPRESSION_VALUE;
2617
-
2618
- this.#report_recoverable_error(this.start, JSX_EXPRESSION_VALUE_ERROR, code);
2619
- return super.jsx_parseElement();
2199
+ return parsed;
2620
2200
  }
2621
2201
 
2622
2202
  /**
@@ -2655,11 +2235,6 @@ export function TSRXPlugin(config) {
2655
2235
  !is_tsx_compat &&
2656
2236
  open.name.type === 'JSXIdentifier' &&
2657
2237
  open.name.name === 'tsx';
2658
- const is_tsrx =
2659
- !is_fragment &&
2660
- !is_tsx_compat &&
2661
- open.name.type === 'JSXIdentifier' &&
2662
- open.name.name === 'tsrx';
2663
2238
  const is_dynamic_name =
2664
2239
  !is_fragment &&
2665
2240
  ((open.name.type === 'JSXIdentifier' && open.name.tracked) ||
@@ -2688,22 +2263,13 @@ export function TSRXPlugin(config) {
2688
2263
  `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
2689
2264
  );
2690
2265
  }
2691
- } else if (is_tsrx) {
2692
- /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
2693
-
2694
- if (open.selfClosing) {
2695
- this.raise(
2696
- open.start,
2697
- `TSRX elements cannot be self-closing. '<tsrx />' must have a closing tag '</tsrx>'.`,
2698
- );
2699
- }
2700
2266
  } else if (is_fragment) {
2701
- /** @type {AST.Tsx} */ (element).type = 'Tsx';
2267
+ /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
2702
2268
  } else {
2703
2269
  element.type = 'Element';
2704
2270
  }
2705
2271
 
2706
- if ((is_tsx || is_fragment) && is_dynamic_name) {
2272
+ if (is_tsx && is_dynamic_name) {
2707
2273
  this.#report_recoverable_error_range(
2708
2274
  open.name.start ?? open.start,
2709
2275
  open.name.end ?? open.end,
@@ -2721,14 +2287,6 @@ export function TSRXPlugin(config) {
2721
2287
  if (attr.value !== null) {
2722
2288
  if (attr.value.type === 'JSXExpressionContainer') {
2723
2289
  const expression = attr.value.expression;
2724
- if (attr.value.style) {
2725
- /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).type = 'Style';
2726
- /** @type {AST.Style} */ (/** @type {unknown} */ (attr.value)).value =
2727
- /** @type {AST.Literal} */ (expression);
2728
- delete (/** @type {any} */ (attr.value).expression);
2729
- delete (/** @type {any} */ (attr.value).style);
2730
- continue;
2731
- }
2732
2290
  if (expression.type === 'Literal') {
2733
2291
  expression.was_expression = true;
2734
2292
  }
@@ -2740,7 +2298,7 @@ export function TSRXPlugin(config) {
2740
2298
  }
2741
2299
  }
2742
2300
 
2743
- if (!is_tsx_compat && !is_tsx && !is_tsrx && !is_fragment) {
2301
+ if (!is_tsx_compat && !is_tsx && !is_fragment) {
2744
2302
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
2745
2303
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
2746
2304
  );
@@ -2768,28 +2326,26 @@ export function TSRXPlugin(config) {
2768
2326
  } else if (is_fragment) {
2769
2327
  this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2770
2328
  enterScope: true,
2329
+ resetFunctionBodyDepth: true,
2771
2330
  });
2772
- this.#reportDynamicJsxElementsInTsx(/** @type {AST.Element} */ (element).children);
2773
2331
 
2774
- if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2775
- this.#path.pop();
2332
+ this.#path.pop();
2776
2333
 
2777
- if (!element.unclosed) {
2778
- const raise_error = () => {
2779
- this.raise(this.start, `Expected closing tag '</>'`);
2780
- };
2334
+ if (!element.unclosed) {
2335
+ const raise_error = () => {
2336
+ this.raise(this.start, `Expected closing tag '</>'`);
2337
+ };
2781
2338
 
2782
- this.next();
2783
- if (this.value !== '/') {
2784
- raise_error();
2785
- }
2786
- this.next();
2787
- if (this.type !== tstt.jsxTagEnd) {
2788
- raise_error();
2789
- }
2790
- this.#popTsxTokenContextBeforeTemplateExpressionChild();
2791
- 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();
2792
2346
  }
2347
+ this.#popTsxTokenContextBeforeTemplateExpressionChild();
2348
+ this.next();
2793
2349
  }
2794
2350
  } else {
2795
2351
  if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
@@ -2862,7 +2418,7 @@ export function TSRXPlugin(config) {
2862
2418
  // No closing tag
2863
2419
  this.#report_broken_markup_error(
2864
2420
  open.end,
2865
- "Unclosed tag '<script>'. Expected '</script>' before end of component.",
2421
+ "Unclosed tag '<script>'. Expected '</script>' before end of template.",
2866
2422
  );
2867
2423
  /** @type {AST.Element} */ (element).unclosed = true;
2868
2424
  this.#path.pop();
@@ -2875,16 +2431,8 @@ export function TSRXPlugin(config) {
2875
2431
  const end = input.indexOf('</style>');
2876
2432
  const content = end === -1 ? input : input.slice(0, end);
2877
2433
 
2878
- const component = /** @type {AST.Component} */ (
2879
- this.#path.findLast((n) => n.type === 'Component')
2880
- );
2881
2434
  const parsed_css = parse_style(content, { loose: this.#loose });
2882
-
2883
2435
  if (!inside_head) {
2884
- if (component.css !== null) {
2885
- throw new Error('Components can only have one style tag');
2886
- }
2887
- component.css = parsed_css;
2888
2436
  /** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
2889
2437
  }
2890
2438
 
@@ -2922,7 +2470,7 @@ export function TSRXPlugin(config) {
2922
2470
  } else {
2923
2471
  this.#report_broken_markup_error(
2924
2472
  open.end,
2925
- "Unclosed tag '<style>'. Expected '</style>' before end of component.",
2473
+ "Unclosed tag '<style>'. Expected '</style>' before end of template.",
2926
2474
  );
2927
2475
  /** @type {AST.Element} */ (element).unclosed = true;
2928
2476
  this.#path.pop();
@@ -2946,6 +2494,7 @@ export function TSRXPlugin(config) {
2946
2494
  } else {
2947
2495
  this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2948
2496
  enterScope: true,
2497
+ resetFunctionBodyDepth: true,
2949
2498
  });
2950
2499
  if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2951
2500
  this.#reportDynamicJsxElementsInTsx(/** @type {AST.Element} */ (element).children);
@@ -3014,9 +2563,10 @@ export function TSRXPlugin(config) {
3014
2563
  /** @type {AST.Tsrx} */ (element).type === 'Tsrx' &&
3015
2564
  this.#path[this.#path.length - 1] === element
3016
2565
  ) {
2566
+ const displayTag = element.openingElement.name ? 'tsrx' : '';
3017
2567
  this.#report_broken_markup_error(
3018
2568
  this.start,
3019
- "Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
2569
+ `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
3020
2570
  );
3021
2571
  element.unclosed = true;
3022
2572
  /** @type {AST.SourceLocation} */ (element.loc).end = {
@@ -3032,7 +2582,7 @@ export function TSRXPlugin(config) {
3032
2582
  const tagName = this.getElementName(element.id);
3033
2583
  this.#report_broken_markup_error(
3034
2584
  this.start,
3035
- `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
2585
+ `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of template.`,
3036
2586
  );
3037
2587
  element.unclosed = true;
3038
2588
  /** @type {AST.SourceLocation} */ (element.loc).end = {
@@ -3053,13 +2603,7 @@ export function TSRXPlugin(config) {
3053
2603
  }
3054
2604
  }
3055
2605
 
3056
- if (
3057
- element.closingElement &&
3058
- !is_tsx_compat &&
3059
- !is_tsx &&
3060
- !is_tsrx &&
3061
- element.closingElement.name
3062
- ) {
2606
+ if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
3063
2607
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
3064
2608
  element.closingElement.name,
3065
2609
  );
@@ -3096,10 +2640,10 @@ export function TSRXPlugin(config) {
3096
2640
 
3097
2641
  if (!inside_func) {
3098
2642
  if (this.type.label === 'continue') {
3099
- throw new Error('`continue` statements are not allowed in components');
2643
+ throw new Error('`continue` statements are not allowed in native templates');
3100
2644
  }
3101
2645
  if (this.type.label === 'break') {
3102
- throw new Error('`break` statements are not allowed in components');
2646
+ throw new Error('`break` statements are not allowed in native templates');
3103
2647
  }
3104
2648
  }
3105
2649
 
@@ -3110,6 +2654,16 @@ export function TSRXPlugin(config) {
3110
2654
  );
3111
2655
  return;
3112
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
+ }
3113
2667
  if (this.type === tt.braceL) {
3114
2668
  body.push(this.#parseNativeTemplateExpressionContainer());
3115
2669
  } else if (
@@ -3118,7 +2672,7 @@ export function TSRXPlugin(config) {
3118
2672
  ) {
3119
2673
  body.push(this.parseDoubleQuotedTextChild());
3120
2674
  } else if (this.type === tt.braceR) {
3121
- // 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
3122
2676
  // context (e.g. after parsing markup), but the closing `}` is a JS token.
3123
2677
  // If we don't reset this here, the following `next()` can read EOF using
3124
2678
  // `jsx_readToken()` and throw "Unterminated JSX contents".
@@ -3136,8 +2690,8 @@ export function TSRXPlugin(config) {
3136
2690
  if (this.type === tstt.jsxTagStart) {
3137
2691
  this.next();
3138
2692
  } else {
3139
- // A control-flow block inside <tsrx> can leave the tokenizer
3140
- // 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
3141
2695
  // `<` token. Re-enter JSX closing-tag parsing manually.
3142
2696
  this.pos = startPos + 1;
3143
2697
  this.type = tstt.jsxTagStart;
@@ -3186,7 +2740,7 @@ export function TSRXPlugin(config) {
3186
2740
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
3187
2741
  : this.getElementName(closingElement.name);
3188
2742
  } else if (currentElement.type === 'Tsrx') {
3189
- openingTagName = 'tsrx';
2743
+ openingTagName = '';
3190
2744
  closingTagName =
3191
2745
  closingElement.name?.type === 'JSXNamespacedName'
3192
2746
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
@@ -3212,7 +2766,7 @@ export function TSRXPlugin(config) {
3212
2766
  while (this.#path.length > 0) {
3213
2767
  const elem = this.#path[this.#path.length - 1];
3214
2768
 
3215
- // Stop at non-Element boundaries (Component, etc.)
2769
+ // Stop at non-template boundaries.
3216
2770
  if (
3217
2771
  elem.type !== 'Element' &&
3218
2772
  elem.type !== 'Tsx' &&
@@ -3230,7 +2784,7 @@ export function TSRXPlugin(config) {
3230
2784
  ? 'tsx'
3231
2785
  : null
3232
2786
  : elem.type === 'Tsrx'
3233
- ? 'tsrx'
2787
+ ? ''
3234
2788
  : elem.id
3235
2789
  ? this.getElementName(elem.id)
3236
2790
  : null;
@@ -3258,7 +2812,7 @@ export function TSRXPlugin(config) {
3258
2812
  ) {
3259
2813
  const elementToCloseName =
3260
2814
  elementToClose.type === 'Tsrx'
3261
- ? 'tsrx'
2815
+ ? ''
3262
2816
  : /** @type {AST.Element} */ (elementToClose).id
3263
2817
  ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
3264
2818
  : null;
@@ -3279,6 +2833,7 @@ export function TSRXPlugin(config) {
3279
2833
  } else {
3280
2834
  skipWhitespace(this);
3281
2835
  const node = this.parseStatement(null);
2836
+ this.#report_invalid_template_return_statements(node);
3282
2837
  body.push(node);
3283
2838
 
3284
2839
  // Ensure we're not in JSX context before recursing
@@ -3363,16 +2918,11 @@ export function TSRXPlugin(config) {
3363
2918
  this.type === tt.braceL &&
3364
2919
  this.context.some((c) => c === tstc.tc_expr)
3365
2920
  ) {
3366
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2921
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
3367
2922
  /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
3368
2923
  );
3369
2924
  }
3370
2925
 
3371
- if (this.value === 'component') {
3372
- this.awaitPos = 0;
3373
- return this.parseComponent({ requireName: true, declareName: true });
3374
- }
3375
-
3376
2926
  if (this.type === tstt.jsxTagStart) {
3377
2927
  this.next();
3378
2928
  if (this.value === '/') {
@@ -3392,18 +2942,6 @@ export function TSRXPlugin(config) {
3392
2942
  this.context.pop();
3393
2943
  }
3394
2944
  }
3395
- const context_restore = this.#functionBodyContextRestoreStack.at(-1);
3396
- if (
3397
- this.#functionBodyDepth > 0 &&
3398
- node.type === 'Tsrx' &&
3399
- context_restore?.canRestore &&
3400
- this.type !== tt.braceR &&
3401
- this.type !== tt.comma
3402
- ) {
3403
- context_restore.restore = true;
3404
- this.context = [b_stat];
3405
- this.exprAllowed = true;
3406
- }
3407
2945
  return node;
3408
2946
  }
3409
2947
 
@@ -3411,7 +2949,7 @@ export function TSRXPlugin(config) {
3411
2949
  this.#functionBodyDepth === 0 &&
3412
2950
  this.type === tt.string &&
3413
2951
  this.input.charCodeAt(this.start) === CharCode.doubleQuote &&
3414
- (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')
3415
2953
  ) {
3416
2954
  this.pos = this.start;
3417
2955
  this.#readDoubleQuotedTextChildToken();
@@ -3461,11 +2999,11 @@ export function TSRXPlugin(config) {
3461
2999
  const parent = this.#path.at(-1);
3462
3000
 
3463
3001
  // Inside a JS function body, parse `{...}` as a regular block statement,
3464
- // 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
3465
3003
  // nested function callable, not in a template.
3466
3004
  if (
3467
3005
  this.#functionBodyDepth === 0 &&
3468
- (parent?.type === 'Component' || parent?.type === 'Element')
3006
+ (parent?.type === 'Element' || parent?.type === 'Tsrx')
3469
3007
  ) {
3470
3008
  if (createNewLexicalScope === void 0) createNewLexicalScope = true;
3471
3009
  if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());