@tsrx/core 0.1.3 → 0.1.6

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
@@ -216,12 +216,15 @@ export function TSRXPlugin(config) {
216
216
  #loose = false;
217
217
  /** @type {AST.Node[]} */
218
218
  #functionStack = [];
219
+ /** @type {Array<{ parentContext: any[], canRestore: boolean, restore: boolean }>} */
220
+ #functionBodyContextRestoreStack = [];
219
221
  /** @type {import('../types/index').CompileError[] | undefined} */
220
222
  #errors = undefined;
221
223
  /** @type {string | null} */
222
224
  #filename = null;
223
225
  #componentDepth = 0;
224
226
  #functionBodyDepth = 0;
227
+ #allowExpressionContainerTrailingSemicolon = false;
225
228
 
226
229
  /**
227
230
  * @type {Parse.Parser['finishNode']}
@@ -279,6 +282,211 @@ export function TSRXPlugin(config) {
279
282
  return this.#isInsideComponent() && this.#functionBodyDepth === 0;
280
283
  }
281
284
 
285
+ /**
286
+ * Component bodies and native TSRX element bodies share the same grammar.
287
+ * This helper keeps the parser-state setup in one place while callers keep
288
+ * ownership of their distinct closing delimiter handling (`}` vs `</tag>`).
289
+ *
290
+ * @param {AST.Node} node
291
+ * @param {AST.Node[]} body
292
+ * @param {{
293
+ * enterScope?: boolean,
294
+ * pushPath?: boolean,
295
+ * trackComponentDepth?: boolean,
296
+ * resetFunctionBodyDepth?: boolean,
297
+ * }} [options]
298
+ */
299
+ #parseNativeTemplateBody(
300
+ node,
301
+ body,
302
+ {
303
+ enterScope = false,
304
+ pushPath = false,
305
+ trackComponentDepth = false,
306
+ resetFunctionBodyDepth = false,
307
+ } = {},
308
+ ) {
309
+ const parent_function_body_depth = this.#functionBodyDepth;
310
+
311
+ if (resetFunctionBodyDepth) {
312
+ this.#functionBodyDepth = 0;
313
+ }
314
+ if (enterScope) {
315
+ this.enterScope(0);
316
+ }
317
+ if (pushPath) {
318
+ this.#path.push(node);
319
+ }
320
+ if (trackComponentDepth) {
321
+ this.#componentDepth++;
322
+ }
323
+
324
+ try {
325
+ this.parseTemplateBody(body);
326
+ } finally {
327
+ if (trackComponentDepth) {
328
+ this.#componentDepth--;
329
+ }
330
+ if (pushPath) {
331
+ this.#path.pop();
332
+ }
333
+ if (enterScope) {
334
+ this.exitScope();
335
+ }
336
+ if (resetFunctionBodyDepth) {
337
+ this.#functionBodyDepth = parent_function_body_depth;
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * @param {AST.Node | undefined} node
344
+ */
345
+ #isNativeTemplateNode(node) {
346
+ return (
347
+ node?.type === 'Component' ||
348
+ node?.type === 'Element' ||
349
+ node?.type === 'Tsx' ||
350
+ node?.type === 'Tsrx' ||
351
+ node?.type === 'TsxCompat'
352
+ );
353
+ }
354
+
355
+ #parseNativeTemplateExpressionContainer() {
356
+ const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
357
+ this.#allowExpressionContainerTrailingSemicolon = true;
358
+ let node;
359
+ try {
360
+ node = this.jsx_parseExpressionContainer();
361
+ } finally {
362
+ this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
363
+ }
364
+ // Keep JSXEmptyExpression as-is (for prettier to handle comments)
365
+ // but convert other expressions to native TSRX child nodes.
366
+ if (node.expression.type !== 'JSXEmptyExpression') {
367
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
368
+ /** @type {unknown} */ (node)
369
+ ).type = node.html
370
+ ? 'Html'
371
+ : node.text
372
+ ? 'Text'
373
+ : node.style
374
+ ? 'Style'
375
+ : 'TSRXExpression';
376
+ if (node.style) {
377
+ /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
378
+ /** @type {AST.Literal} */ (node.expression);
379
+ delete (/** @type {any} */ (node).expression);
380
+ }
381
+ delete node.html;
382
+ delete node.text;
383
+ delete node.style;
384
+ }
385
+
386
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style | ESTreeJSX.JSXExpressionContainer} */ (
387
+ /** @type {unknown} */ (node)
388
+ );
389
+ }
390
+
391
+ /**
392
+ * @param {AST.Tsx | AST.TsxCompat} island
393
+ * @param {AST.Node[]} body
394
+ */
395
+ #parseTsxIslandBody(island, body) {
396
+ const tagName =
397
+ island.type === 'TsxCompat'
398
+ ? `tsx:${island.kind}`
399
+ : island.openingElement.name
400
+ ? 'tsx'
401
+ : '';
402
+
403
+ this.exprAllowed = true;
404
+
405
+ while (true) {
406
+ if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
407
+ const displayTag = tagName || '';
408
+ this.#report_broken_markup_error(
409
+ this.start,
410
+ `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of component.`,
411
+ );
412
+ island.unclosed = true;
413
+ /** @type {AST.NodeWithLocation} */ (island).loc.end = {
414
+ .../** @type {AST.SourceLocation} */ (island.openingElement.loc).end,
415
+ };
416
+ island.end = island.openingElement.end;
417
+ return;
418
+ }
419
+
420
+ if (this.#isAtTsxIslandClosing(island)) {
421
+ this.exprAllowed = false;
422
+ return;
423
+ }
424
+
425
+ if (this.type === tt.braceL) {
426
+ body.push(this.jsx_parseExpressionContainer());
427
+ } else if (this.type === tstt.jsxTagStart) {
428
+ body.push(super.jsx_parseElement());
429
+ } else {
430
+ const node = this.#parseTsxIslandText();
431
+ if (node) {
432
+ body.push(node);
433
+ }
434
+ this.#popTemplateLiteralTokenContext();
435
+ this.next();
436
+ }
437
+ }
438
+ }
439
+
440
+ /**
441
+ * @param {AST.Tsx | AST.TsxCompat} island
442
+ */
443
+ #isAtTsxIslandClosing(island) {
444
+ if (island.type === 'TsxCompat') {
445
+ return this.input.slice(this.pos, this.pos + 5) === '/tsx:';
446
+ }
447
+
448
+ if (!island.openingElement.name) {
449
+ return this.input.slice(this.pos, this.pos + 2) === '/>';
450
+ }
451
+
452
+ if (this.input.slice(this.pos, this.pos + 4) !== '/tsx') {
453
+ return false;
454
+ }
455
+
456
+ const after = this.input.charCodeAt(this.pos + 4);
457
+ return after === 62 /* > */;
458
+ }
459
+
460
+ #parseTsxIslandText() {
461
+ const start = this.start;
462
+ this.pos = start;
463
+ let text = '';
464
+
465
+ while (this.pos < this.input.length) {
466
+ const ch = this.input.charCodeAt(this.pos);
467
+
468
+ // Stop at opening tag, expression, or the component-closing brace
469
+ if (ch === 60 || ch === 123 || ch === 125) {
470
+ break;
471
+ }
472
+
473
+ text += this.input[this.pos];
474
+ this.pos++;
475
+ }
476
+
477
+ if (!text) {
478
+ return null;
479
+ }
480
+
481
+ return /** @type {ESTreeJSX.JSXText} */ ({
482
+ type: 'JSXText',
483
+ value: text,
484
+ raw: text,
485
+ start,
486
+ end: this.pos,
487
+ });
488
+ }
489
+
282
490
  #popTsxTokenContextBeforeTemplateExpressionChild() {
283
491
  let index = this.pos;
284
492
  let has_newline = false;
@@ -444,6 +652,18 @@ export function TSRXPlugin(config) {
444
652
  ctx.length = ci - 1;
445
653
  return;
446
654
  }
655
+ // Statement-bodied `<tsrx>` attributes can leave the attribute's
656
+ // expression contexts above the still-open JSX tag context. Strip
657
+ // those so a following `/>` stays in JSX opening-tag mode.
658
+ if (
659
+ this.type === tt.braceR &&
660
+ top === tstc.tc_expr &&
661
+ second === b_expr &&
662
+ ctx[ci - 2] === tstc.tc_oTag
663
+ ) {
664
+ ctx.length = ci - 1;
665
+ return;
666
+ }
447
667
  // Closing token after the template at expression position. For `}`
448
668
  // only pop if it actually closes this `b_expr` — otherwise the
449
669
  // brace targets an inner callback/object body that should pop it
@@ -1151,6 +1371,11 @@ export function TSRXPlugin(config) {
1151
1371
  skipName = false,
1152
1372
  } = {}) {
1153
1373
  const node = /** @type {AST.Component} */ (this.startNode());
1374
+ const parent_context = [...this.context];
1375
+ const restore_parent_context =
1376
+ !requireName &&
1377
+ this.#isInsideComponent() &&
1378
+ this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag);
1154
1379
  node.type = 'Component';
1155
1380
  node.css = null;
1156
1381
  node.default = isDefault;
@@ -1201,32 +1426,24 @@ export function TSRXPlugin(config) {
1201
1426
  this.next();
1202
1427
  }
1203
1428
 
1204
- // Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
1205
- // the component body's first token as if we'd entered fresh — no
1206
- // surrounding function body should affect our parseStatement/parseBlock
1207
- // branching while inside the template.
1208
- const parent_function_body_depth = this.#functionBodyDepth;
1209
- this.#functionBodyDepth = 0;
1210
-
1211
1429
  if (this.type === tt.braceL) {
1212
1430
  this.#allowDoubleQuotedTextChildAfterBrace = true;
1213
1431
  }
1214
1432
  this.eat(tt.braceL);
1215
1433
  node.body = [];
1216
- this.#path.push(node);
1217
- this.#componentDepth++;
1218
-
1219
- try {
1220
- this.parseTemplateBody(node.body);
1221
- } finally {
1222
- this.#functionBodyDepth = parent_function_body_depth;
1223
- this.#componentDepth--;
1224
- }
1225
- this.#path.pop();
1434
+ this.#parseNativeTemplateBody(node, node.body, {
1435
+ pushPath: true,
1436
+ trackComponentDepth: true,
1437
+ resetFunctionBodyDepth: true,
1438
+ });
1226
1439
  this.exitScope();
1227
1440
 
1228
1441
  this.next();
1229
1442
  skipWhitespace(this);
1443
+ if (restore_parent_context) {
1444
+ this.context = this.type === tt.braceR ? parent_context.slice(0, -1) : parent_context;
1445
+ this.exprAllowed = false;
1446
+ }
1230
1447
  this.finishNode(node, 'Component');
1231
1448
  this.awaitPos = 0;
1232
1449
 
@@ -1444,6 +1661,14 @@ export function TSRXPlugin(config) {
1444
1661
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1445
1662
  this.#functionBodyDepth++;
1446
1663
  this.#functionStack.push(node);
1664
+ const context_restore = {
1665
+ parentContext: [...this.context],
1666
+ canRestore:
1667
+ this.#isInsideComponent() &&
1668
+ this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag),
1669
+ restore: false,
1670
+ };
1671
+ this.#functionBodyContextRestoreStack.push(context_restore);
1447
1672
  // Inside a component, nested JS function bodies should parse like
1448
1673
  // ordinary functions, not component template bodies.
1449
1674
  if (
@@ -1452,9 +1677,9 @@ export function TSRXPlugin(config) {
1452
1677
  // A stale JSX expression context means the surrounding template
1453
1678
  // tokenizer can still treat `<` as template markup.
1454
1679
  this.context.some((context) => context === tstc.tc_expr) &&
1455
- // Keep arrows/functions inside JSX tags, such as event handlers,
1456
- // on the normal JSX attribute parsing path.
1457
- !this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag) &&
1680
+ // Keep callback props on their surrounding JSX attribute path until
1681
+ // statement-position TSRX needs to suspend it.
1682
+ !context_restore.canRestore &&
1458
1683
  // Only reset statement-level function bodies, not expression
1459
1684
  // contexts that are actively parsing JSX.
1460
1685
  this.curContext() === b_stat
@@ -1465,6 +1690,11 @@ export function TSRXPlugin(config) {
1465
1690
  try {
1466
1691
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
1467
1692
  } finally {
1693
+ if (context_restore.restore) {
1694
+ this.context = context_restore.parentContext.slice(0, -1);
1695
+ this.exprAllowed = false;
1696
+ }
1697
+ this.#functionBodyContextRestoreStack.pop();
1468
1698
  this.#functionStack.pop();
1469
1699
  this.#functionBodyDepth--;
1470
1700
  }
@@ -1561,6 +1791,16 @@ export function TSRXPlugin(config) {
1561
1791
  '"style" is a TSRX keyword and must be used in the form {style "class_name"}',
1562
1792
  );
1563
1793
  }
1794
+ if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
1795
+ if (this.#collect) {
1796
+ this.#report_recoverable_error(
1797
+ this.start,
1798
+ 'TSRX expression containers do not use semicolons. Remove this semicolon.',
1799
+ DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
1800
+ );
1801
+ }
1802
+ this.next();
1803
+ }
1564
1804
  this.expect(tt.braceR);
1565
1805
 
1566
1806
  return this.finishNode(node, 'JSXExpressionContainer');
@@ -2226,9 +2466,9 @@ export function TSRXPlugin(config) {
2226
2466
  this.next();
2227
2467
  }
2228
2468
  } else if (is_fragment) {
2229
- this.enterScope(0);
2230
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
2231
- this.exitScope();
2469
+ this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2470
+ enterScope: true,
2471
+ });
2232
2472
 
2233
2473
  if (element.type === 'Tsx') {
2234
2474
  this.#path.pop();
@@ -2395,12 +2635,7 @@ export function TSRXPlugin(config) {
2395
2635
  // Ensure we escape JSX <tag></tag> context
2396
2636
  const curContext = this.curContext();
2397
2637
  const parent = this.#path.at(-1);
2398
- const insideTemplate =
2399
- parent?.type === 'Component' ||
2400
- parent?.type === 'Element' ||
2401
- parent?.type === 'Tsx' ||
2402
- parent?.type === 'Tsrx' ||
2403
- parent?.type === 'TsxCompat';
2638
+ const insideTemplate = this.#isNativeTemplateNode(parent);
2404
2639
 
2405
2640
  if (curContext === tstc.tc_expr && !insideTemplate) {
2406
2641
  this.context.pop();
@@ -2408,9 +2643,9 @@ export function TSRXPlugin(config) {
2408
2643
 
2409
2644
  /** @type {AST.Element} */ (element).css = content;
2410
2645
  } else {
2411
- this.enterScope(0);
2412
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
2413
- this.exitScope();
2646
+ this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2647
+ enterScope: true,
2648
+ });
2414
2649
 
2415
2650
  if (element.type === 'Tsx') {
2416
2651
  this.#path.pop();
@@ -2501,12 +2736,7 @@ export function TSRXPlugin(config) {
2501
2736
  // Ensure we escape JSX <tag></tag> context
2502
2737
  const curContext = this.curContext();
2503
2738
  const parent = this.#path.at(-1);
2504
- const insideTemplate =
2505
- parent?.type === 'Component' ||
2506
- parent?.type === 'Element' ||
2507
- parent?.type === 'Tsx' ||
2508
- parent?.type === 'Tsrx' ||
2509
- parent?.type === 'TsxCompat';
2739
+ const insideTemplate = this.#isNativeTemplateNode(parent);
2510
2740
 
2511
2741
  if (curContext === tstc.tc_expr && !insideTemplate) {
2512
2742
  this.context.pop();
@@ -2535,8 +2765,9 @@ export function TSRXPlugin(config) {
2535
2765
  parseTemplateBody(body) {
2536
2766
  const inside_func =
2537
2767
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
2538
- const inside_tsx = this.#path.findLast((n) => n.type === 'Tsx');
2539
- const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
2768
+ const inside_tsx_island = this.#path.findLast(
2769
+ (n) => n.type === 'Tsx' || n.type === 'TsxCompat',
2770
+ );
2540
2771
 
2541
2772
  if (!inside_func) {
2542
2773
  if (this.type.label === 'continue') {
@@ -2547,168 +2778,15 @@ export function TSRXPlugin(config) {
2547
2778
  }
2548
2779
  }
2549
2780
 
2550
- if (inside_tsx) {
2551
- this.exprAllowed = true;
2552
-
2553
- while (true) {
2554
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
2555
- this.#report_broken_markup_error(
2556
- this.start,
2557
- `Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
2558
- );
2559
- inside_tsx.unclosed = true;
2560
- /** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
2561
- .../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
2562
- };
2563
- inside_tsx.end = inside_tsx.openingElement.end;
2564
- return;
2565
- }
2566
-
2567
- if (!inside_tsx.openingElement.name) {
2568
- if (this.input.slice(this.pos, this.pos + 2) === '/>') {
2569
- // Reset exprAllowed so the trailing `/` of `</>` is tokenized
2570
- // as a slash rather than as the start of a regex literal.
2571
- this.exprAllowed = false;
2572
- return;
2573
- }
2574
- } else if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
2575
- const after = this.input.charCodeAt(this.pos + 4);
2576
- // Make sure it's </tsx> and not </tsx:...>
2577
- if (after === 62 /* > */) {
2578
- this.exprAllowed = false;
2579
- return;
2580
- }
2581
- }
2582
-
2583
- if (this.type === tt.braceL) {
2584
- const node = this.jsx_parseExpressionContainer();
2585
- body.push(node);
2586
- } else if (this.type === tstt.jsxTagStart) {
2587
- // Parse JSX element
2588
- const node = super.jsx_parseElement();
2589
- body.push(node);
2590
- } else {
2591
- const start = this.start;
2592
- this.pos = start;
2593
- let text = '';
2594
-
2595
- while (this.pos < this.input.length) {
2596
- const ch = this.input.charCodeAt(this.pos);
2597
-
2598
- // Stop at opening tag, expression, or the component-closing brace
2599
- if (ch === 60 || ch === 123 || ch === 125) {
2600
- // < or { or }
2601
- break;
2602
- }
2603
-
2604
- text += this.input[this.pos];
2605
- this.pos++;
2606
- }
2607
-
2608
- if (text) {
2609
- const node = /** @type {ESTreeJSX.JSXText} */ ({
2610
- type: 'JSXText',
2611
- value: text,
2612
- raw: text,
2613
- start,
2614
- end: this.pos,
2615
- });
2616
- body.push(node);
2617
- }
2618
-
2619
- this.#popTemplateLiteralTokenContext();
2620
- // Always call next() to ensure parser makes progress
2621
- this.next();
2622
- }
2623
- }
2624
- }
2625
- if (inside_tsx_compat) {
2626
- this.exprAllowed = true;
2627
-
2628
- while (true) {
2629
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
2630
- this.#report_broken_markup_error(
2631
- this.start,
2632
- `Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
2633
- );
2634
- inside_tsx_compat.unclosed = true;
2635
- /** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
2636
- .../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
2637
- };
2638
- inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
2639
- return;
2640
- }
2641
-
2642
- if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
2643
- this.exprAllowed = false;
2644
- return;
2645
- }
2646
-
2647
- if (this.type === tt.braceL) {
2648
- const node = this.jsx_parseExpressionContainer();
2649
- body.push(node);
2650
- } else if (this.type === tstt.jsxTagStart) {
2651
- // Parse JSX element
2652
- const node = super.jsx_parseElement();
2653
- body.push(node);
2654
- } else {
2655
- const start = this.start;
2656
- this.pos = start;
2657
- let text = '';
2658
-
2659
- while (this.pos < this.input.length) {
2660
- const ch = this.input.charCodeAt(this.pos);
2661
-
2662
- // Stop at opening tag, expression, or the component-closing brace
2663
- if (ch === 60 || ch === 123 || ch === 125) {
2664
- // < or { or }
2665
- break;
2666
- }
2667
-
2668
- text += this.input[this.pos];
2669
- this.pos++;
2670
- }
2671
-
2672
- if (text) {
2673
- const node = /** @type {ESTreeJSX.JSXText} */ ({
2674
- type: 'JSXText',
2675
- value: text,
2676
- raw: text,
2677
- start,
2678
- end: this.pos,
2679
- });
2680
- body.push(node);
2681
- }
2682
-
2683
- this.#popTemplateLiteralTokenContext();
2684
- this.next();
2685
- }
2686
- }
2781
+ if (inside_tsx_island) {
2782
+ this.#parseTsxIslandBody(
2783
+ /** @type {AST.Tsx | AST.TsxCompat} */ (inside_tsx_island),
2784
+ /** @type {AST.Node[]} */ (/** @type {unknown} */ (body)),
2785
+ );
2786
+ return;
2687
2787
  }
2688
2788
  if (this.type === tt.braceL) {
2689
- const node = this.jsx_parseExpressionContainer();
2690
- // Keep JSXEmptyExpression as-is (for prettier to handle comments)
2691
- // but convert other expressions to Html/TSRXExpression/Text nodes
2692
- if (node.expression.type !== 'JSXEmptyExpression') {
2693
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2694
- /** @type {unknown} */ (node)
2695
- ).type = node.html
2696
- ? 'Html'
2697
- : node.text
2698
- ? 'Text'
2699
- : node.style
2700
- ? 'Style'
2701
- : 'TSRXExpression';
2702
- if (node.style) {
2703
- /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2704
- /** @type {AST.Literal} */ (node.expression);
2705
- delete (/** @type {any} */ (node).expression);
2706
- }
2707
- delete node.html;
2708
- delete node.text;
2709
- delete node.style;
2710
- }
2711
- body.push(node);
2789
+ body.push(this.#parseNativeTemplateExpressionContainer());
2712
2790
  } else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
2713
2791
  body.push(this.parseDoubleQuotedTextChild());
2714
2792
  } else if (this.type === tt.braceR) {
@@ -2957,30 +3035,8 @@ export function TSRXPlugin(config) {
2957
3035
  this.type === tt.braceL &&
2958
3036
  this.context.some((c) => c === tstc.tc_expr)
2959
3037
  ) {
2960
- const node = this.jsx_parseExpressionContainer();
2961
- // Keep JSXEmptyExpression as-is (don't convert to TSRXExpression/Text/Html)
2962
- if (node.expression.type !== 'JSXEmptyExpression') {
2963
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2964
- /** @type {unknown} */ (node)
2965
- ).type = node.html
2966
- ? 'Html'
2967
- : node.text
2968
- ? 'Text'
2969
- : node.style
2970
- ? 'Style'
2971
- : 'TSRXExpression';
2972
- if (node.style) {
2973
- /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2974
- /** @type {AST.Literal} */ (node.expression);
2975
- delete (/** @type {any} */ (node).expression);
2976
- }
2977
- delete node.html;
2978
- delete node.text;
2979
- delete node.style;
2980
- }
2981
-
2982
3038
  return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2983
- /** @type {unknown} */ (node)
3039
+ /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
2984
3040
  );
2985
3041
  }
2986
3042
 
@@ -3008,6 +3064,18 @@ export function TSRXPlugin(config) {
3008
3064
  this.context.pop();
3009
3065
  }
3010
3066
  }
3067
+ const context_restore = this.#functionBodyContextRestoreStack.at(-1);
3068
+ if (
3069
+ this.#functionBodyDepth > 0 &&
3070
+ node.type === 'Tsrx' &&
3071
+ context_restore?.canRestore &&
3072
+ this.type !== tt.braceR &&
3073
+ this.type !== tt.comma
3074
+ ) {
3075
+ context_restore.restore = true;
3076
+ this.context = [b_stat];
3077
+ this.exprAllowed = true;
3078
+ }
3011
3079
  return node;
3012
3080
  }
3013
3081
 
@@ -3077,10 +3145,9 @@ export function TSRXPlugin(config) {
3077
3145
  node.body = [];
3078
3146
  this.#allowDoubleQuotedTextChildAfterBrace = true;
3079
3147
  this.expect(tt.braceL);
3080
- if (createNewLexicalScope) {
3081
- this.enterScope(0);
3082
- }
3083
- this.parseTemplateBody(node.body);
3148
+ this.#parseNativeTemplateBody(node, node.body, {
3149
+ enterScope: createNewLexicalScope,
3150
+ });
3084
3151
 
3085
3152
  if (exitStrict) {
3086
3153
  this.strict = false;
@@ -3088,9 +3155,6 @@ export function TSRXPlugin(config) {
3088
3155
  this.exprAllowed = true;
3089
3156
 
3090
3157
  this.next();
3091
- if (createNewLexicalScope) {
3092
- this.exitScope();
3093
- }
3094
3158
  return this.finishNode(node, 'BlockStatement');
3095
3159
  }
3096
3160