@tsrx/core 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.3",
6
+ "version": "0.1.4",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/plugin.js CHANGED
@@ -216,6 +216,8 @@ 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} */
@@ -279,6 +281,204 @@ export function TSRXPlugin(config) {
279
281
  return this.#isInsideComponent() && this.#functionBodyDepth === 0;
280
282
  }
281
283
 
284
+ /**
285
+ * Component bodies and native TSRX element bodies share the same grammar.
286
+ * This helper keeps the parser-state setup in one place while callers keep
287
+ * ownership of their distinct closing delimiter handling (`}` vs `</tag>`).
288
+ *
289
+ * @param {AST.Node} node
290
+ * @param {AST.Node[]} body
291
+ * @param {{
292
+ * enterScope?: boolean,
293
+ * pushPath?: boolean,
294
+ * trackComponentDepth?: boolean,
295
+ * resetFunctionBodyDepth?: boolean,
296
+ * }} [options]
297
+ */
298
+ #parseNativeTemplateBody(
299
+ node,
300
+ body,
301
+ {
302
+ enterScope = false,
303
+ pushPath = false,
304
+ trackComponentDepth = false,
305
+ resetFunctionBodyDepth = false,
306
+ } = {},
307
+ ) {
308
+ const parent_function_body_depth = this.#functionBodyDepth;
309
+
310
+ if (resetFunctionBodyDepth) {
311
+ this.#functionBodyDepth = 0;
312
+ }
313
+ if (enterScope) {
314
+ this.enterScope(0);
315
+ }
316
+ if (pushPath) {
317
+ this.#path.push(node);
318
+ }
319
+ if (trackComponentDepth) {
320
+ this.#componentDepth++;
321
+ }
322
+
323
+ try {
324
+ this.parseTemplateBody(body);
325
+ } finally {
326
+ if (trackComponentDepth) {
327
+ this.#componentDepth--;
328
+ }
329
+ if (pushPath) {
330
+ this.#path.pop();
331
+ }
332
+ if (enterScope) {
333
+ this.exitScope();
334
+ }
335
+ if (resetFunctionBodyDepth) {
336
+ this.#functionBodyDepth = parent_function_body_depth;
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * @param {AST.Node | undefined} node
343
+ */
344
+ #isNativeTemplateNode(node) {
345
+ return (
346
+ node?.type === 'Component' ||
347
+ node?.type === 'Element' ||
348
+ node?.type === 'Tsx' ||
349
+ node?.type === 'Tsrx' ||
350
+ node?.type === 'TsxCompat'
351
+ );
352
+ }
353
+
354
+ #parseNativeTemplateExpressionContainer() {
355
+ const node = this.jsx_parseExpressionContainer();
356
+ // Keep JSXEmptyExpression as-is (for prettier to handle comments)
357
+ // but convert other expressions to native TSRX child nodes.
358
+ if (node.expression.type !== 'JSXEmptyExpression') {
359
+ /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
360
+ /** @type {unknown} */ (node)
361
+ ).type = node.html
362
+ ? 'Html'
363
+ : node.text
364
+ ? 'Text'
365
+ : node.style
366
+ ? 'Style'
367
+ : 'TSRXExpression';
368
+ if (node.style) {
369
+ /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
370
+ /** @type {AST.Literal} */ (node.expression);
371
+ delete (/** @type {any} */ (node).expression);
372
+ }
373
+ delete node.html;
374
+ delete node.text;
375
+ delete node.style;
376
+ }
377
+
378
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style | ESTreeJSX.JSXExpressionContainer} */ (
379
+ /** @type {unknown} */ (node)
380
+ );
381
+ }
382
+
383
+ /**
384
+ * @param {AST.Tsx | AST.TsxCompat} island
385
+ * @param {AST.Node[]} body
386
+ */
387
+ #parseTsxIslandBody(island, body) {
388
+ const tagName =
389
+ island.type === 'TsxCompat'
390
+ ? `tsx:${island.kind}`
391
+ : island.openingElement.name
392
+ ? 'tsx'
393
+ : '';
394
+
395
+ this.exprAllowed = true;
396
+
397
+ while (true) {
398
+ if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
399
+ const displayTag = tagName || '';
400
+ this.#report_broken_markup_error(
401
+ this.start,
402
+ `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of component.`,
403
+ );
404
+ island.unclosed = true;
405
+ /** @type {AST.NodeWithLocation} */ (island).loc.end = {
406
+ .../** @type {AST.SourceLocation} */ (island.openingElement.loc).end,
407
+ };
408
+ island.end = island.openingElement.end;
409
+ return;
410
+ }
411
+
412
+ if (this.#isAtTsxIslandClosing(island)) {
413
+ this.exprAllowed = false;
414
+ return;
415
+ }
416
+
417
+ if (this.type === tt.braceL) {
418
+ body.push(this.jsx_parseExpressionContainer());
419
+ } else if (this.type === tstt.jsxTagStart) {
420
+ body.push(super.jsx_parseElement());
421
+ } else {
422
+ const node = this.#parseTsxIslandText();
423
+ if (node) {
424
+ body.push(node);
425
+ }
426
+ this.#popTemplateLiteralTokenContext();
427
+ this.next();
428
+ }
429
+ }
430
+ }
431
+
432
+ /**
433
+ * @param {AST.Tsx | AST.TsxCompat} island
434
+ */
435
+ #isAtTsxIslandClosing(island) {
436
+ if (island.type === 'TsxCompat') {
437
+ return this.input.slice(this.pos, this.pos + 5) === '/tsx:';
438
+ }
439
+
440
+ if (!island.openingElement.name) {
441
+ return this.input.slice(this.pos, this.pos + 2) === '/>';
442
+ }
443
+
444
+ if (this.input.slice(this.pos, this.pos + 4) !== '/tsx') {
445
+ return false;
446
+ }
447
+
448
+ const after = this.input.charCodeAt(this.pos + 4);
449
+ return after === 62 /* > */;
450
+ }
451
+
452
+ #parseTsxIslandText() {
453
+ const start = this.start;
454
+ this.pos = start;
455
+ let text = '';
456
+
457
+ while (this.pos < this.input.length) {
458
+ const ch = this.input.charCodeAt(this.pos);
459
+
460
+ // Stop at opening tag, expression, or the component-closing brace
461
+ if (ch === 60 || ch === 123 || ch === 125) {
462
+ break;
463
+ }
464
+
465
+ text += this.input[this.pos];
466
+ this.pos++;
467
+ }
468
+
469
+ if (!text) {
470
+ return null;
471
+ }
472
+
473
+ return /** @type {ESTreeJSX.JSXText} */ ({
474
+ type: 'JSXText',
475
+ value: text,
476
+ raw: text,
477
+ start,
478
+ end: this.pos,
479
+ });
480
+ }
481
+
282
482
  #popTsxTokenContextBeforeTemplateExpressionChild() {
283
483
  let index = this.pos;
284
484
  let has_newline = false;
@@ -1151,6 +1351,11 @@ export function TSRXPlugin(config) {
1151
1351
  skipName = false,
1152
1352
  } = {}) {
1153
1353
  const node = /** @type {AST.Component} */ (this.startNode());
1354
+ const parent_context = [...this.context];
1355
+ const restore_parent_context =
1356
+ !requireName &&
1357
+ this.#isInsideComponent() &&
1358
+ this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag);
1154
1359
  node.type = 'Component';
1155
1360
  node.css = null;
1156
1361
  node.default = isDefault;
@@ -1201,32 +1406,24 @@ export function TSRXPlugin(config) {
1201
1406
  this.next();
1202
1407
  }
1203
1408
 
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
1409
  if (this.type === tt.braceL) {
1212
1410
  this.#allowDoubleQuotedTextChildAfterBrace = true;
1213
1411
  }
1214
1412
  this.eat(tt.braceL);
1215
1413
  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();
1414
+ this.#parseNativeTemplateBody(node, node.body, {
1415
+ pushPath: true,
1416
+ trackComponentDepth: true,
1417
+ resetFunctionBodyDepth: true,
1418
+ });
1226
1419
  this.exitScope();
1227
1420
 
1228
1421
  this.next();
1229
1422
  skipWhitespace(this);
1423
+ if (restore_parent_context) {
1424
+ this.context = this.type === tt.braceR ? parent_context.slice(0, -1) : parent_context;
1425
+ this.exprAllowed = false;
1426
+ }
1230
1427
  this.finishNode(node, 'Component');
1231
1428
  this.awaitPos = 0;
1232
1429
 
@@ -1444,6 +1641,14 @@ export function TSRXPlugin(config) {
1444
1641
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1445
1642
  this.#functionBodyDepth++;
1446
1643
  this.#functionStack.push(node);
1644
+ const context_restore = {
1645
+ parentContext: [...this.context],
1646
+ canRestore:
1647
+ this.#isInsideComponent() &&
1648
+ this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag),
1649
+ restore: false,
1650
+ };
1651
+ this.#functionBodyContextRestoreStack.push(context_restore);
1447
1652
  // Inside a component, nested JS function bodies should parse like
1448
1653
  // ordinary functions, not component template bodies.
1449
1654
  if (
@@ -1452,9 +1657,9 @@ export function TSRXPlugin(config) {
1452
1657
  // A stale JSX expression context means the surrounding template
1453
1658
  // tokenizer can still treat `<` as template markup.
1454
1659
  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) &&
1660
+ // Keep callback props on their surrounding JSX attribute path until
1661
+ // statement-position TSRX needs to suspend it.
1662
+ !context_restore.canRestore &&
1458
1663
  // Only reset statement-level function bodies, not expression
1459
1664
  // contexts that are actively parsing JSX.
1460
1665
  this.curContext() === b_stat
@@ -1465,6 +1670,11 @@ export function TSRXPlugin(config) {
1465
1670
  try {
1466
1671
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
1467
1672
  } finally {
1673
+ if (context_restore.restore) {
1674
+ this.context = context_restore.parentContext.slice(0, -1);
1675
+ this.exprAllowed = false;
1676
+ }
1677
+ this.#functionBodyContextRestoreStack.pop();
1468
1678
  this.#functionStack.pop();
1469
1679
  this.#functionBodyDepth--;
1470
1680
  }
@@ -2226,9 +2436,9 @@ export function TSRXPlugin(config) {
2226
2436
  this.next();
2227
2437
  }
2228
2438
  } else if (is_fragment) {
2229
- this.enterScope(0);
2230
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
2231
- this.exitScope();
2439
+ this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2440
+ enterScope: true,
2441
+ });
2232
2442
 
2233
2443
  if (element.type === 'Tsx') {
2234
2444
  this.#path.pop();
@@ -2395,12 +2605,7 @@ export function TSRXPlugin(config) {
2395
2605
  // Ensure we escape JSX <tag></tag> context
2396
2606
  const curContext = this.curContext();
2397
2607
  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';
2608
+ const insideTemplate = this.#isNativeTemplateNode(parent);
2404
2609
 
2405
2610
  if (curContext === tstc.tc_expr && !insideTemplate) {
2406
2611
  this.context.pop();
@@ -2408,9 +2613,9 @@ export function TSRXPlugin(config) {
2408
2613
 
2409
2614
  /** @type {AST.Element} */ (element).css = content;
2410
2615
  } else {
2411
- this.enterScope(0);
2412
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
2413
- this.exitScope();
2616
+ this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2617
+ enterScope: true,
2618
+ });
2414
2619
 
2415
2620
  if (element.type === 'Tsx') {
2416
2621
  this.#path.pop();
@@ -2501,12 +2706,7 @@ export function TSRXPlugin(config) {
2501
2706
  // Ensure we escape JSX <tag></tag> context
2502
2707
  const curContext = this.curContext();
2503
2708
  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';
2709
+ const insideTemplate = this.#isNativeTemplateNode(parent);
2510
2710
 
2511
2711
  if (curContext === tstc.tc_expr && !insideTemplate) {
2512
2712
  this.context.pop();
@@ -2535,8 +2735,9 @@ export function TSRXPlugin(config) {
2535
2735
  parseTemplateBody(body) {
2536
2736
  const inside_func =
2537
2737
  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');
2738
+ const inside_tsx_island = this.#path.findLast(
2739
+ (n) => n.type === 'Tsx' || n.type === 'TsxCompat',
2740
+ );
2540
2741
 
2541
2742
  if (!inside_func) {
2542
2743
  if (this.type.label === 'continue') {
@@ -2547,168 +2748,15 @@ export function TSRXPlugin(config) {
2547
2748
  }
2548
2749
  }
2549
2750
 
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
- }
2751
+ if (inside_tsx_island) {
2752
+ this.#parseTsxIslandBody(
2753
+ /** @type {AST.Tsx | AST.TsxCompat} */ (inside_tsx_island),
2754
+ /** @type {AST.Node[]} */ (/** @type {unknown} */ (body)),
2755
+ );
2756
+ return;
2687
2757
  }
2688
2758
  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);
2759
+ body.push(this.#parseNativeTemplateExpressionContainer());
2712
2760
  } else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
2713
2761
  body.push(this.parseDoubleQuotedTextChild());
2714
2762
  } else if (this.type === tt.braceR) {
@@ -2957,30 +3005,8 @@ export function TSRXPlugin(config) {
2957
3005
  this.type === tt.braceL &&
2958
3006
  this.context.some((c) => c === tstc.tc_expr)
2959
3007
  ) {
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
3008
  return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2983
- /** @type {unknown} */ (node)
3009
+ /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
2984
3010
  );
2985
3011
  }
2986
3012
 
@@ -3008,6 +3034,18 @@ export function TSRXPlugin(config) {
3008
3034
  this.context.pop();
3009
3035
  }
3010
3036
  }
3037
+ const context_restore = this.#functionBodyContextRestoreStack.at(-1);
3038
+ if (
3039
+ this.#functionBodyDepth > 0 &&
3040
+ node.type === 'Tsrx' &&
3041
+ context_restore?.canRestore &&
3042
+ this.type !== tt.braceR &&
3043
+ this.type !== tt.comma
3044
+ ) {
3045
+ context_restore.restore = true;
3046
+ this.context = [b_stat];
3047
+ this.exprAllowed = true;
3048
+ }
3011
3049
  return node;
3012
3050
  }
3013
3051
 
@@ -3077,10 +3115,9 @@ export function TSRXPlugin(config) {
3077
3115
  node.body = [];
3078
3116
  this.#allowDoubleQuotedTextChildAfterBrace = true;
3079
3117
  this.expect(tt.braceL);
3080
- if (createNewLexicalScope) {
3081
- this.enterScope(0);
3082
- }
3083
- this.parseTemplateBody(node.body);
3118
+ this.#parseNativeTemplateBody(node, node.body, {
3119
+ enterScope: createNewLexicalScope,
3120
+ });
3084
3121
 
3085
3122
  if (exitStrict) {
3086
3123
  this.strict = false;
@@ -3088,9 +3125,6 @@ export function TSRXPlugin(config) {
3088
3125
  this.exprAllowed = true;
3089
3126
 
3090
3127
  this.next();
3091
- if (createNewLexicalScope) {
3092
- this.exitScope();
3093
- }
3094
3128
  return this.finishNode(node, 'BlockStatement');
3095
3129
  }
3096
3130