@tsrx/core 0.1.2 → 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/src/plugin.js CHANGED
@@ -216,10 +216,13 @@ 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;
225
+ #componentDepth = 0;
223
226
  #functionBodyDepth = 0;
224
227
 
225
228
  /**
@@ -270,6 +273,212 @@ export function TSRXPlugin(config) {
270
273
  return null;
271
274
  }
272
275
 
276
+ #isInsideComponent() {
277
+ return this.#componentDepth > 0;
278
+ }
279
+
280
+ #isInsideComponentTemplate() {
281
+ return this.#isInsideComponent() && this.#functionBodyDepth === 0;
282
+ }
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
+
273
482
  #popTsxTokenContextBeforeTemplateExpressionChild() {
274
483
  let index = this.pos;
275
484
  let has_newline = false;
@@ -320,17 +529,133 @@ export function TSRXPlugin(config) {
320
529
  }
321
530
  }
322
531
 
323
- #popJsxAttributeExpressionContextAfterTemplateElement() {
324
- if (this.type !== tt.braceR) {
325
- return;
532
+ /**
533
+ * @param {number} index
534
+ * @returns {number}
535
+ */
536
+ #skipWhitespaceAndComments(index) {
537
+ while (index < this.input.length) {
538
+ const ch = this.input.charCodeAt(index);
539
+ if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
540
+ index++;
541
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 42) {
542
+ const end = this.input.indexOf('*/', index + 2);
543
+ index = end === -1 ? this.input.length : end + 2;
544
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 47) {
545
+ index += 2;
546
+ while (index < this.input.length) {
547
+ const comment_ch = this.input.charCodeAt(index);
548
+ if (comment_ch === 10 || comment_ch === 13) break;
549
+ index++;
550
+ }
551
+ } else {
552
+ break;
553
+ }
554
+ }
555
+ return index;
556
+ }
557
+
558
+ /** @returns {number} */
559
+ #countFollowingRightBraces() {
560
+ let index = this.end;
561
+ let count = 0;
562
+ while (index < this.input.length) {
563
+ index = this.#skipWhitespaceAndComments(index);
564
+ if (this.input.charCodeAt(index) !== 125) break;
565
+ count++;
566
+ index++;
326
567
  }
568
+ return count;
569
+ }
327
570
 
328
- const context_index = this.context.length - 1;
571
+ /**
572
+ * @param {AST.Tsx | AST.Tsrx | AST.TsxCompat} node
573
+ * @returns {boolean}
574
+ */
575
+ #hasDirectStatementChild(node) {
576
+ return node.children?.some(
577
+ (child) => child.type.endsWith('Statement') || child.type === 'VariableDeclaration',
578
+ );
579
+ }
580
+
581
+ /**
582
+ * @param {AST.Tsx | AST.Tsrx | AST.TsxCompat} node
583
+ */
584
+ #popTokenContextsAfterTemplateExpressionElement(node) {
585
+ const ctx = this.context;
586
+ const ci = ctx.length - 1;
587
+ const top = ctx[ci];
588
+ const second = ctx[ci - 1];
589
+
590
+ // Expression-bodied templates (no statement child) followed by `,`
591
+ // in an object/array literal need surgical fixups; statement-bodied
592
+ // templates fall through to the JSX-expression-container strip.
593
+ const has_stmt_child = this.#hasDirectStatementChild(node);
594
+ if (this.type === tt.comma && !has_stmt_child) {
595
+ // Tail `..., (b_expr)+, tc_expr, b_stat`: the JSX expression
596
+ // container leaks an extra `tc_expr, b_stat`. Pop them, and if
597
+ // the JSX container also closes immediately (`}}` ahead), drop
598
+ // one of the doubled-up `b_expr` contexts too.
599
+ if (top === b_stat && second === tstc.tc_expr) {
600
+ let expr_count = 0;
601
+ for (let i = ci - 2; ctx[i] === b_expr; i--) expr_count++;
602
+ const following_braces = this.#countFollowingRightBraces();
603
+ if (expr_count === 2 || following_braces > 1) {
604
+ if (following_braces > 1 && expr_count > 1) {
605
+ ctx.splice(ci - 2, expr_count - 1);
606
+ ctx.pop();
607
+ this.exprAllowed = false;
608
+ return;
609
+ }
610
+ if (expr_count === 2 && following_braces === 0) {
611
+ // Fragment expression value followed by another
612
+ // object/array entry inside a JSX expression
613
+ // container (`{ a: <></>, b: ... }` or
614
+ // `[<></>, ...]`): strip both the leaked tc_expr
615
+ // and b_stat so the next entry parses as an
616
+ // expression, and leave exprAllowed alone so a
617
+ // following `<` still tokenizes as jsxTagStart.
618
+ ctx.length = ci - 1;
619
+ return;
620
+ }
621
+ ctx.pop();
622
+ this.exprAllowed = false;
623
+ return;
624
+ }
625
+ }
626
+ // Tail `..., b_expr, b_expr` for fragments-with-children
627
+ // inside an array or object literal: re-arm expression mode
628
+ // so the next item parses as an expression value, not a JSX
629
+ // child. If the surrounding b_expr chain has already been
630
+ // consumed, push one back so the subsequent item still has
631
+ // a literal context. Leave exprAllowed alone so a following
632
+ // `<` still tokenizes as jsxTagStart.
633
+ if (top === b_expr && second === b_expr) {
634
+ if (ctx[ci - 2] !== b_expr && ctx[ci - 2] !== tstc.tc_oTag) {
635
+ ctx.push(b_expr);
636
+ }
637
+ return;
638
+ }
639
+ }
640
+
641
+ // Inside `{<tsrx>...</tsrx>}` JSX expression container — strip
642
+ // both the leaked `b_stat` and the container's `tc_expr`.
643
+ if (top === b_stat && second === tstc.tc_expr) {
644
+ ctx.length = ci - 1;
645
+ return;
646
+ }
647
+ // Closing token after the template at expression position. For `}`
648
+ // only pop if it actually closes this `b_expr` — otherwise the
649
+ // brace targets an inner callback/object body that should pop it
650
+ // naturally on the next token step.
329
651
  if (
330
- this.context[context_index] === b_expr &&
331
- this.context[context_index - 1] === tstc.tc_oTag
652
+ (this.type === tt.braceR &&
653
+ top === b_expr &&
654
+ (this.#countFollowingRightBraces() === 0 || second === b_expr)) ||
655
+ (this.type === tt.parenR && top?.token === '(') ||
656
+ (this.type === tt.bracketR && top?.token === '[')
332
657
  ) {
333
- this.context.pop();
658
+ ctx.pop();
334
659
  this.exprAllowed = false;
335
660
  }
336
661
  }
@@ -733,7 +1058,7 @@ export function TSRXPlugin(config) {
733
1058
 
734
1059
  if (code === 60) {
735
1060
  // < character
736
- const inComponent = this.#path.findLast((n) => n.type === 'Component');
1061
+ const inComponent = this.#isInsideComponentTemplate();
737
1062
  /** @type {number | null} */
738
1063
  let prevNonWhitespaceChar = null;
739
1064
 
@@ -1026,6 +1351,11 @@ export function TSRXPlugin(config) {
1026
1351
  skipName = false,
1027
1352
  } = {}) {
1028
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);
1029
1359
  node.type = 'Component';
1030
1360
  node.css = null;
1031
1361
  node.default = isDefault;
@@ -1076,30 +1406,24 @@ export function TSRXPlugin(config) {
1076
1406
  this.next();
1077
1407
  }
1078
1408
 
1079
- // Reset before `eat(braceL)` so the lookahead `next()` it triggers reads
1080
- // the component body's first token as if we'd entered fresh — no
1081
- // surrounding function body should affect our parseStatement/parseBlock
1082
- // branching while inside the template.
1083
- const parent_function_body_depth = this.#functionBodyDepth;
1084
- this.#functionBodyDepth = 0;
1085
-
1086
1409
  if (this.type === tt.braceL) {
1087
1410
  this.#allowDoubleQuotedTextChildAfterBrace = true;
1088
1411
  }
1089
1412
  this.eat(tt.braceL);
1090
1413
  node.body = [];
1091
- this.#path.push(node);
1092
-
1093
- try {
1094
- this.parseTemplateBody(node.body);
1095
- } finally {
1096
- this.#functionBodyDepth = parent_function_body_depth;
1097
- }
1098
- this.#path.pop();
1414
+ this.#parseNativeTemplateBody(node, node.body, {
1415
+ pushPath: true,
1416
+ trackComponentDepth: true,
1417
+ resetFunctionBodyDepth: true,
1418
+ });
1099
1419
  this.exitScope();
1100
1420
 
1101
1421
  this.next();
1102
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
+ }
1103
1427
  this.finishNode(node, 'Component');
1104
1428
  this.awaitPos = 0;
1105
1429
 
@@ -1317,10 +1641,40 @@ export function TSRXPlugin(config) {
1317
1641
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1318
1642
  this.#functionBodyDepth++;
1319
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);
1652
+ // Inside a component, nested JS function bodies should parse like
1653
+ // ordinary functions, not component template bodies.
1654
+ if (
1655
+ // Only adjust functions declared while parsing a component body.
1656
+ this.#isInsideComponent() &&
1657
+ // A stale JSX expression context means the surrounding template
1658
+ // tokenizer can still treat `<` as template markup.
1659
+ this.context.some((context) => context === tstc.tc_expr) &&
1660
+ // Keep callback props on their surrounding JSX attribute path until
1661
+ // statement-position TSRX needs to suspend it.
1662
+ !context_restore.canRestore &&
1663
+ // Only reset statement-level function bodies, not expression
1664
+ // contexts that are actively parsing JSX.
1665
+ this.curContext() === b_stat
1666
+ ) {
1667
+ this.context = [b_stat];
1668
+ }
1320
1669
 
1321
1670
  try {
1322
1671
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
1323
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();
1324
1678
  this.#functionStack.pop();
1325
1679
  this.#functionBodyDepth--;
1326
1680
  }
@@ -1929,7 +2283,9 @@ export function TSRXPlugin(config) {
1929
2283
  const parsed = /** @type {import('estree-jsx').JSXElement} */ (
1930
2284
  /** @type {unknown} */ (this.parseElement())
1931
2285
  );
1932
- this.#popJsxAttributeExpressionContextAfterTemplateElement();
2286
+ this.#popTokenContextsAfterTemplateExpressionElement(
2287
+ /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2288
+ );
1933
2289
  return parsed;
1934
2290
  }
1935
2291
 
@@ -2080,9 +2436,9 @@ export function TSRXPlugin(config) {
2080
2436
  this.next();
2081
2437
  }
2082
2438
  } else if (is_fragment) {
2083
- this.enterScope(0);
2084
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
2085
- this.exitScope();
2439
+ this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2440
+ enterScope: true,
2441
+ });
2086
2442
 
2087
2443
  if (element.type === 'Tsx') {
2088
2444
  this.#path.pop();
@@ -2249,12 +2605,7 @@ export function TSRXPlugin(config) {
2249
2605
  // Ensure we escape JSX <tag></tag> context
2250
2606
  const curContext = this.curContext();
2251
2607
  const parent = this.#path.at(-1);
2252
- const insideTemplate =
2253
- parent?.type === 'Component' ||
2254
- parent?.type === 'Element' ||
2255
- parent?.type === 'Tsx' ||
2256
- parent?.type === 'Tsrx' ||
2257
- parent?.type === 'TsxCompat';
2608
+ const insideTemplate = this.#isNativeTemplateNode(parent);
2258
2609
 
2259
2610
  if (curContext === tstc.tc_expr && !insideTemplate) {
2260
2611
  this.context.pop();
@@ -2262,9 +2613,9 @@ export function TSRXPlugin(config) {
2262
2613
 
2263
2614
  /** @type {AST.Element} */ (element).css = content;
2264
2615
  } else {
2265
- this.enterScope(0);
2266
- this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
2267
- this.exitScope();
2616
+ this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2617
+ enterScope: true,
2618
+ });
2268
2619
 
2269
2620
  if (element.type === 'Tsx') {
2270
2621
  this.#path.pop();
@@ -2355,12 +2706,7 @@ export function TSRXPlugin(config) {
2355
2706
  // Ensure we escape JSX <tag></tag> context
2356
2707
  const curContext = this.curContext();
2357
2708
  const parent = this.#path.at(-1);
2358
- const insideTemplate =
2359
- parent?.type === 'Component' ||
2360
- parent?.type === 'Element' ||
2361
- parent?.type === 'Tsx' ||
2362
- parent?.type === 'Tsrx' ||
2363
- parent?.type === 'TsxCompat';
2709
+ const insideTemplate = this.#isNativeTemplateNode(parent);
2364
2710
 
2365
2711
  if (curContext === tstc.tc_expr && !insideTemplate) {
2366
2712
  this.context.pop();
@@ -2389,8 +2735,9 @@ export function TSRXPlugin(config) {
2389
2735
  parseTemplateBody(body) {
2390
2736
  const inside_func =
2391
2737
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
2392
- const inside_tsx = this.#path.findLast((n) => n.type === 'Tsx');
2393
- 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
+ );
2394
2741
 
2395
2742
  if (!inside_func) {
2396
2743
  if (this.type.label === 'continue') {
@@ -2401,168 +2748,15 @@ export function TSRXPlugin(config) {
2401
2748
  }
2402
2749
  }
2403
2750
 
2404
- if (inside_tsx) {
2405
- this.exprAllowed = true;
2406
-
2407
- while (true) {
2408
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
2409
- this.#report_broken_markup_error(
2410
- this.start,
2411
- `Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
2412
- );
2413
- inside_tsx.unclosed = true;
2414
- /** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
2415
- .../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
2416
- };
2417
- inside_tsx.end = inside_tsx.openingElement.end;
2418
- return;
2419
- }
2420
-
2421
- if (!inside_tsx.openingElement.name) {
2422
- if (this.input.slice(this.pos, this.pos + 2) === '/>') {
2423
- // Reset exprAllowed so the trailing `/` of `</>` is tokenized
2424
- // as a slash rather than as the start of a regex literal.
2425
- this.exprAllowed = false;
2426
- return;
2427
- }
2428
- } else if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
2429
- const after = this.input.charCodeAt(this.pos + 4);
2430
- // Make sure it's </tsx> and not </tsx:...>
2431
- if (after === 62 /* > */) {
2432
- this.exprAllowed = false;
2433
- return;
2434
- }
2435
- }
2436
-
2437
- if (this.type === tt.braceL) {
2438
- const node = this.jsx_parseExpressionContainer();
2439
- body.push(node);
2440
- } else if (this.type === tstt.jsxTagStart) {
2441
- // Parse JSX element
2442
- const node = super.jsx_parseElement();
2443
- body.push(node);
2444
- } else {
2445
- const start = this.start;
2446
- this.pos = start;
2447
- let text = '';
2448
-
2449
- while (this.pos < this.input.length) {
2450
- const ch = this.input.charCodeAt(this.pos);
2451
-
2452
- // Stop at opening tag, expression, or the component-closing brace
2453
- if (ch === 60 || ch === 123 || ch === 125) {
2454
- // < or { or }
2455
- break;
2456
- }
2457
-
2458
- text += this.input[this.pos];
2459
- this.pos++;
2460
- }
2461
-
2462
- if (text) {
2463
- const node = /** @type {ESTreeJSX.JSXText} */ ({
2464
- type: 'JSXText',
2465
- value: text,
2466
- raw: text,
2467
- start,
2468
- end: this.pos,
2469
- });
2470
- body.push(node);
2471
- }
2472
-
2473
- this.#popTemplateLiteralTokenContext();
2474
- // Always call next() to ensure parser makes progress
2475
- this.next();
2476
- }
2477
- }
2478
- }
2479
- if (inside_tsx_compat) {
2480
- this.exprAllowed = true;
2481
-
2482
- while (true) {
2483
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
2484
- this.#report_broken_markup_error(
2485
- this.start,
2486
- `Unclosed tag '<tsx:${inside_tsx_compat.kind}>'. Expected '</tsx:${inside_tsx_compat.kind}>' before end of component.`,
2487
- );
2488
- inside_tsx_compat.unclosed = true;
2489
- /** @type {AST.NodeWithLocation} */ (inside_tsx_compat).loc.end = {
2490
- .../** @type {AST.SourceLocation} */ (inside_tsx_compat.openingElement.loc).end,
2491
- };
2492
- inside_tsx_compat.end = inside_tsx_compat.openingElement.end;
2493
- return;
2494
- }
2495
-
2496
- if (this.input.slice(this.pos, this.pos + 5) === '/tsx:') {
2497
- this.exprAllowed = false;
2498
- return;
2499
- }
2500
-
2501
- if (this.type === tt.braceL) {
2502
- const node = this.jsx_parseExpressionContainer();
2503
- body.push(node);
2504
- } else if (this.type === tstt.jsxTagStart) {
2505
- // Parse JSX element
2506
- const node = super.jsx_parseElement();
2507
- body.push(node);
2508
- } else {
2509
- const start = this.start;
2510
- this.pos = start;
2511
- let text = '';
2512
-
2513
- while (this.pos < this.input.length) {
2514
- const ch = this.input.charCodeAt(this.pos);
2515
-
2516
- // Stop at opening tag, expression, or the component-closing brace
2517
- if (ch === 60 || ch === 123 || ch === 125) {
2518
- // < or { or }
2519
- break;
2520
- }
2521
-
2522
- text += this.input[this.pos];
2523
- this.pos++;
2524
- }
2525
-
2526
- if (text) {
2527
- const node = /** @type {ESTreeJSX.JSXText} */ ({
2528
- type: 'JSXText',
2529
- value: text,
2530
- raw: text,
2531
- start,
2532
- end: this.pos,
2533
- });
2534
- body.push(node);
2535
- }
2536
-
2537
- this.#popTemplateLiteralTokenContext();
2538
- this.next();
2539
- }
2540
- }
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;
2541
2757
  }
2542
2758
  if (this.type === tt.braceL) {
2543
- const node = this.jsx_parseExpressionContainer();
2544
- // Keep JSXEmptyExpression as-is (for prettier to handle comments)
2545
- // but convert other expressions to Html/TSRXExpression/Text nodes
2546
- if (node.expression.type !== 'JSXEmptyExpression') {
2547
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2548
- /** @type {unknown} */ (node)
2549
- ).type = node.html
2550
- ? 'Html'
2551
- : node.text
2552
- ? 'Text'
2553
- : node.style
2554
- ? 'Style'
2555
- : 'TSRXExpression';
2556
- if (node.style) {
2557
- /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2558
- /** @type {AST.Literal} */ (node.expression);
2559
- delete (/** @type {any} */ (node).expression);
2560
- }
2561
- delete node.html;
2562
- delete node.text;
2563
- delete node.style;
2564
- }
2565
- body.push(node);
2759
+ body.push(this.#parseNativeTemplateExpressionContainer());
2566
2760
  } else if (this.type === tt.string && this.input.charCodeAt(this.start) === 34) {
2567
2761
  body.push(this.parseDoubleQuotedTextChild());
2568
2762
  } else if (this.type === tt.braceR) {
@@ -2811,30 +3005,8 @@ export function TSRXPlugin(config) {
2811
3005
  this.type === tt.braceL &&
2812
3006
  this.context.some((c) => c === tstc.tc_expr)
2813
3007
  ) {
2814
- const node = this.jsx_parseExpressionContainer();
2815
- // Keep JSXEmptyExpression as-is (don't convert to TSRXExpression/Text/Html)
2816
- if (node.expression.type !== 'JSXEmptyExpression') {
2817
- /** @type {AST.TSRXExpression | AST.Html | AST.TextNode | AST.Style} */ (
2818
- /** @type {unknown} */ (node)
2819
- ).type = node.html
2820
- ? 'Html'
2821
- : node.text
2822
- ? 'Text'
2823
- : node.style
2824
- ? 'Style'
2825
- : 'TSRXExpression';
2826
- if (node.style) {
2827
- /** @type {AST.Style} */ (/** @type {unknown} */ (node)).value =
2828
- /** @type {AST.Literal} */ (node.expression);
2829
- delete (/** @type {any} */ (node).expression);
2830
- }
2831
- delete node.html;
2832
- delete node.text;
2833
- delete node.style;
2834
- }
2835
-
2836
3008
  return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.Html | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2837
- /** @type {unknown} */ (node)
3009
+ /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
2838
3010
  );
2839
3011
  }
2840
3012
 
@@ -2853,6 +3025,27 @@ export function TSRXPlugin(config) {
2853
3025
  if (!node) {
2854
3026
  this.unexpected();
2855
3027
  }
3028
+ if (this.#functionBodyDepth > 0 && node.type === 'Tsrx' && this.curContext() === b_stat) {
3029
+ this.context.pop();
3030
+ if (this.curContext() === tstc.tc_expr) {
3031
+ this.context.pop();
3032
+ }
3033
+ if (this.curContext() === b_stat) {
3034
+ this.context.pop();
3035
+ }
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
+ }
2856
3049
  return node;
2857
3050
  }
2858
3051
 
@@ -2922,10 +3115,9 @@ export function TSRXPlugin(config) {
2922
3115
  node.body = [];
2923
3116
  this.#allowDoubleQuotedTextChildAfterBrace = true;
2924
3117
  this.expect(tt.braceL);
2925
- if (createNewLexicalScope) {
2926
- this.enterScope(0);
2927
- }
2928
- this.parseTemplateBody(node.body);
3118
+ this.#parseNativeTemplateBody(node, node.body, {
3119
+ enterScope: createNewLexicalScope,
3120
+ });
2929
3121
 
2930
3122
  if (exitStrict) {
2931
3123
  this.strict = false;
@@ -2933,9 +3125,6 @@ export function TSRXPlugin(config) {
2933
3125
  this.exprAllowed = true;
2934
3126
 
2935
3127
  this.next();
2936
- if (createNewLexicalScope) {
2937
- this.exitScope();
2938
- }
2939
3128
  return this.finishNode(node, 'BlockStatement');
2940
3129
  }
2941
3130