@tsrx/core 0.1.49 → 0.1.51

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
@@ -7,7 +7,7 @@
7
7
  import * as acorn from 'acorn';
8
8
  import { isWhitespaceTextNode, BINDING_TYPES, DestructuringErrors } from './parse/index.js';
9
9
  import { parse_style } from './parse/style.js';
10
- import { regex_newline_characters } from './utils/patterns.js';
10
+ import { regex_newline_characters, regex_not_whitespace } from './utils/patterns.js';
11
11
  import { error } from './errors.js';
12
12
  import { DIAGNOSTIC_CODES } from './diagnostics.js';
13
13
  import { TSRX_RETURN_STATEMENT_ERROR } from './analyze/validation.js';
@@ -68,6 +68,15 @@ const DYNAMIC_TAG_DISALLOWED_TYPES = new Set([
68
68
  'TaggedTemplateExpression',
69
69
  ]);
70
70
 
71
+ /**
72
+ * The expression wrappers a dynamic tag (`<{expr}>`) may be written through.
73
+ * @param {AST.Node} node
74
+ * @returns {node is AST.TSAsExpression | AST.TSTypeAssertion | AST.TSNonNullExpression | AST.ParenthesizedExpression | AST.ChainExpression}
75
+ */
76
+ function is_dynamic_tag_wrapper(node) {
77
+ return DYNAMIC_TAG_WRAPPER_TYPES.has(node.type);
78
+ }
79
+
71
80
  /** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
72
81
  const argument_clash_first_positions = new WeakMap();
73
82
  /** @type {WeakMap<Record<string, boolean>, Set<string>>} */
@@ -278,7 +287,7 @@ export function TSRXPlugin(config) {
278
287
  #templateControlFlowTryDepth = 0;
279
288
  /** @type {Parse.Parser['context']} */
280
289
  context = [b_stat];
281
- /** @type {AST.Node | null} */
290
+ /** @type {AST.NativeTSRXTemplateNode | null} */
282
291
  #openingNativeTemplateNode = null;
283
292
  #closingNativeTemplateNode = false;
284
293
  #readingJSXControlFlowDirectiveKeyword = false;
@@ -926,14 +935,24 @@ export function TSRXPlugin(config) {
926
935
  * template-mode element whose text children are raw JSX text; the rest of
927
936
  * the directive/comment/boundary checks below still apply, so a directive
928
937
  * body inside an expression container is correctly excluded.
938
+ * @param {boolean} [ignore_directive_start] Skip the directive-start bail —
939
+ * used to ask whether the position is template text APART from starting a
940
+ * directive, so whitespace directly before the directive can be kept. Also
941
+ * skips the value-position (`#templateScriptParsingDepth`) and `@switch`
942
+ * (JS switch label) bails: those describe the surrounding construct, not
943
+ * this position, and significant whitespace between template siblings must
944
+ * not depend on which construct the template sits in.
929
945
  */
930
- #shouldReadTemplateRawTextToken(allow_inside_expression_container = false) {
946
+ #shouldReadTemplateRawTextToken(
947
+ allow_inside_expression_container = false,
948
+ ignore_directive_start = false,
949
+ ) {
931
950
  if (
932
951
  this.#closingNativeTemplateNode ||
933
952
  this.#readingJSXControlFlowDirectiveKeyword ||
934
953
  this.#readingJSXControlFlowHeader ||
935
954
  this.#parsingJSXSwitchCaseScriptStatementDepth > 0 ||
936
- this.#templateScriptParsingDepth > 0 ||
955
+ (!ignore_directive_start && this.#templateScriptParsingDepth > 0) ||
937
956
  (!allow_inside_expression_container && this.#jsxExpressionContainerDepth > 0)
938
957
  ) {
939
958
  return false;
@@ -942,11 +961,14 @@ export function TSRXPlugin(config) {
942
961
  if (current_context_token === '<tag' || current_context_token === '</tag') {
943
962
  return false;
944
963
  }
945
- if (this.labels.some((label) => label.kind === 'switch')) {
964
+ if (!ignore_directive_start && this.labels.some((label) => label.kind === 'switch')) {
946
965
  return false;
947
966
  }
948
967
  const current_template_node = this.#currentNativeTemplateNode();
949
- if (!current_template_node || this.#isJSXControlFlowDirectiveAt(this.pos)) {
968
+ if (
969
+ !current_template_node ||
970
+ (!ignore_directive_start && this.#isJSXControlFlowDirectiveAt(this.pos))
971
+ ) {
950
972
  return false;
951
973
  }
952
974
  // Inside an expression container (only reachable with
@@ -1007,7 +1029,8 @@ export function TSRXPlugin(config) {
1007
1029
  if (
1008
1030
  opening &&
1009
1031
  current_template_node === opening &&
1010
- /** @type {any} */ (opening).openingElement?.selfClosing &&
1032
+ opening.type !== 'JSXFragment' &&
1033
+ opening.openingElement?.selfClosing &&
1011
1034
  this.input.charCodeAt(this.lastTokEnd - 1) === CharCode.greaterThan &&
1012
1035
  this.input.charCodeAt(this.lastTokEnd - 2) === CharCode.slash
1013
1036
  ) {
@@ -1022,6 +1045,26 @@ export function TSRXPlugin(config) {
1022
1045
  return true;
1023
1046
  }
1024
1047
 
1048
+ /**
1049
+ * At a raw-text bail boundary in `jsx_readToken`, decides whether the
1050
+ * accumulated run is template text that must be finished as a jsxText
1051
+ * token rather than silently discarded by the token-start reset. True
1052
+ * when the run contains non-whitespace (whitespace accumulates without
1053
+ * consulting the raw-text gate, so a whitespace-only run can be plain JS
1054
+ * layout — a newline before a `return`, indentation before `: null`), or
1055
+ * when it is significant whitespace directly before a directive in an
1056
+ * open template element (`<><a /> @for (…) { … }</>`).
1057
+ * @param {string} accumulated
1058
+ */
1059
+ #shouldFinishAccumulatedTemplateText(accumulated) {
1060
+ if (!accumulated) return false;
1061
+ if (regex_not_whitespace.test(accumulated)) return true;
1062
+ return (
1063
+ this.#isJSXControlFlowDirectiveAt(this.pos) &&
1064
+ this.#shouldReadTemplateRawTextToken(true, true)
1065
+ );
1066
+ }
1067
+
1025
1068
  #readTemplateRawTextToken() {
1026
1069
  const start = this.pos;
1027
1070
  const index = this.#templateRawTextEnd(start);
@@ -1260,6 +1303,7 @@ export function TSRXPlugin(config) {
1260
1303
  * Parse the single bare render node of a code block — a JSX element/fragment
1261
1304
  * (parsed as a native TSRX element so its own body may again be plain JSX or
1262
1305
  * a nested `@{ … }`) or an `@if`/`@for`/`@switch`/`@try` directive.
1306
+ * @returns {AST.TSRXRenderOutput}
1263
1307
  */
1264
1308
  #parseCodeBlockRenderNode() {
1265
1309
  const at_index = skip_whitespace_from(this.input, this.start);
@@ -1276,13 +1320,11 @@ export function TSRXPlugin(config) {
1276
1320
  }
1277
1321
 
1278
1322
  if (this.#isCodeBlockStart(at_index)) {
1279
- return /** @type {AST.Node} */ (/** @type {unknown} */ (this.#parseCodeBlock()));
1323
+ return this.#parseCodeBlock();
1280
1324
  }
1281
1325
 
1282
1326
  if (this.#isJSXControlFlowDirectiveAt(at_index)) {
1283
- return /** @type {AST.Node} */ (
1284
- /** @type {unknown} */ (this.#parseJSXControlFlowExpression())
1285
- );
1327
+ return this.#parseJSXControlFlowExpression();
1286
1328
  }
1287
1329
 
1288
1330
  // Re-read the `<` so its `jsxTagStart` pushes the opening-tag contexts.
@@ -1303,7 +1345,7 @@ export function TSRXPlugin(config) {
1303
1345
  if (this.curContext() === tstc.tc_expr) {
1304
1346
  this.context.pop();
1305
1347
  }
1306
- return /** @type {AST.Node} */ (/** @type {unknown} */ (node));
1348
+ return node;
1307
1349
  }
1308
1350
 
1309
1351
  /**
@@ -1438,30 +1480,43 @@ export function TSRXPlugin(config) {
1438
1480
  * @type {Parse.Parser['parseExprAtom']}
1439
1481
  */
1440
1482
  parseExprAtom(refDestructuringErrors, forInit, forNew) {
1441
- if (this.input.charCodeAt(this.start) === CharCode.at) {
1483
+ // A token already consumed as JSX text (a script-mode element child) must
1484
+ // stay text even when it happens to begin at an `@` — otherwise whether
1485
+ // `@if` parses as a directive would depend on leading whitespace.
1486
+ if (this.input.charCodeAt(this.start) === CharCode.at && this.type !== tstt.jsxText) {
1442
1487
  if (this.#isCodeBlockStart(this.start)) {
1443
- return /** @type {any} */ (this.#parseCodeBlock());
1488
+ return this.#parseCodeBlock();
1444
1489
  }
1445
1490
  if (this.#isJSXControlFlowDirectiveAt(this.start)) {
1446
- return /** @type {any} */ (this.#parseJSXControlFlowExpression());
1491
+ return this.#parseJSXControlFlowExpression();
1447
1492
  }
1448
1493
  }
1449
1494
  return super.parseExprAtom(refDestructuringErrors, forInit, forNew);
1450
1495
  }
1451
1496
 
1452
1497
  /**
1498
+ * Retype a parsed control-flow statement in place as its directive
1499
+ * expression form (`IfStatement` -> `JSXIfExpression`, …), keeping the
1500
+ * original statement type in `statementType`.
1501
+ *
1453
1502
  * @param {AST.Node} node
1454
- * @param {string} type
1503
+ * @param {AST.JSXTemplateDirective['type']} type
1455
1504
  * @param {number} start
1456
1505
  * @param {AST.Position} startLoc
1506
+ * @returns {AST.JSXTemplateDirective}
1457
1507
  */
1458
1508
  #finishJSXControlFlowExpression(node, type, start, startLoc) {
1459
1509
  node.start = start;
1460
1510
  /** @type {AST.NodeWithLocation} */ (node).loc.start = startLoc;
1461
1511
  node.metadata ??= { path: [] };
1462
- /** @type {any} */ (node).statementType = node.type;
1463
- /** @type {any} */ (node).type = type;
1464
- return node;
1512
+ const directive = /** @type {Parse.JSXControlFlowDirectiveSlots} */ (
1513
+ /** @type {unknown} */ (node)
1514
+ );
1515
+ directive.statementType = /** @type {AST.JSXTemplateDirective['statementType']} */ (
1516
+ node.type
1517
+ );
1518
+ directive.type = type;
1519
+ return /** @type {AST.JSXTemplateDirective} */ (directive);
1465
1520
  }
1466
1521
 
1467
1522
  /**
@@ -1880,7 +1935,7 @@ export function TSRXPlugin(config) {
1880
1935
  if (this.type === tstt.jsxText) {
1881
1936
  const text = this.#parseJSXSwitchCaseRawText();
1882
1937
  if (!isWhitespaceTextNode(text)) {
1883
- consequent.push(/** @type {any} */ (text));
1938
+ consequent.push(text);
1884
1939
  }
1885
1940
  return;
1886
1941
  }
@@ -1908,12 +1963,12 @@ export function TSRXPlugin(config) {
1908
1963
  if (!node) {
1909
1964
  this.unexpected();
1910
1965
  }
1911
- consequent.push(/** @type {any} */ (node));
1966
+ consequent.push(node);
1912
1967
  return;
1913
1968
  }
1914
1969
 
1915
1970
  if (this.#isJSXControlFlowDirectiveStart()) {
1916
- consequent.push(/** @type {any} */ (this.#parseJSXControlFlowExpression()));
1971
+ consequent.push(this.#parseJSXControlFlowExpression());
1917
1972
  return;
1918
1973
  }
1919
1974
 
@@ -1965,30 +2020,31 @@ export function TSRXPlugin(config) {
1965
2020
  }
1966
2021
 
1967
2022
  /**
1968
- * @param {ESTreeJSX.JSXOpeningElement} openingElement
2023
+ * @param {ESTreeJSX.TSRXJSXOpeningElement} openingElement
1969
2024
  * @returns {ESTreeJSX.JSXOpeningFragment}
1970
2025
  */
1971
2026
  #toOpeningFragment(openingElement) {
2027
+ const element = /** @type {Partial<ESTreeJSX.TSRXJSXOpeningElement>} */ (openingElement);
2028
+ delete element.name;
2029
+ delete element.attributes;
2030
+ delete element.selfClosing;
1972
2031
  const openingFragment = /** @type {ESTreeJSX.JSXOpeningFragment} */ (
1973
2032
  /** @type {unknown} */ (openingElement)
1974
2033
  );
1975
2034
  openingFragment.type = 'JSXOpeningFragment';
1976
- delete (/** @type {any} */ (openingFragment).name);
1977
- delete (/** @type {any} */ (openingFragment).attributes);
1978
- delete (/** @type {any} */ (openingFragment).selfClosing);
1979
2035
  return openingFragment;
1980
2036
  }
1981
2037
 
1982
2038
  /**
1983
- * @param {ESTreeJSX.JSXClosingElement} closingElement
2039
+ * @param {ESTreeJSX.TSRXJSXClosingElement} closingElement
1984
2040
  * @returns {ESTreeJSX.JSXClosingFragment}
1985
2041
  */
1986
2042
  #toClosingFragment(closingElement) {
2043
+ delete (/** @type {Partial<ESTreeJSX.TSRXJSXClosingElement>} */ (closingElement).name);
1987
2044
  const closingFragment = /** @type {ESTreeJSX.JSXClosingFragment} */ (
1988
2045
  /** @type {unknown} */ (closingElement)
1989
2046
  );
1990
2047
  closingFragment.type = 'JSXClosingFragment';
1991
- delete (/** @type {any} */ (closingFragment).name);
1992
2048
  return closingFragment;
1993
2049
  }
1994
2050
 
@@ -1998,7 +2054,7 @@ export function TSRXPlugin(config) {
1998
2054
  * synthesize the closing element, and restore the tokenizer state past it.
1999
2055
  * Shared by `<style>` and `<script>`.
2000
2056
  *
2001
- * @param {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} open
2057
+ * @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
2002
2058
  * @param {AST.JSXStyleElement | AST.TSRXJSXElement} node
2003
2059
  * @param {'style' | 'script'} tagName
2004
2060
  * @returns {string} The raw body text
@@ -2092,7 +2148,7 @@ export function TSRXPlugin(config) {
2092
2148
  }
2093
2149
 
2094
2150
  /**
2095
- * @param {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} open
2151
+ * @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
2096
2152
  * @param {AST.JSXStyleElement} node
2097
2153
  * @param {boolean} insideHead
2098
2154
  */
@@ -2135,7 +2191,7 @@ export function TSRXPlugin(config) {
2135
2191
  * `content` directly (the Ripple transforms, the prettier plugin) must skip
2136
2192
  * the children instead of emitting both.
2137
2193
  *
2138
- * @param {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} open
2194
+ * @param {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} open
2139
2195
  * @param {AST.TSRXJSXElement} node
2140
2196
  */
2141
2197
  #parseScriptElement(open, node) {
@@ -2680,11 +2736,14 @@ export function TSRXPlugin(config) {
2680
2736
  return null;
2681
2737
  }
2682
2738
 
2683
- const children = Array.isArray(/** @type {any} */ (container).children)
2684
- ? /** @type {any} */ (container).children
2739
+ // A directive's `{ }` block is a native template node too, and it has no
2740
+ // `children`, so read the slot defensively.
2741
+ const container_children = /** @type {{ children?: unknown }} */ (container).children;
2742
+ const children = Array.isArray(container_children)
2743
+ ? /** @type {AST.Node[]} */ (container_children)
2685
2744
  : [];
2686
2745
  const hasMeaningfulChildren = children.some(
2687
- (/** @type {any} */ child) => child && !isWhitespaceTextNode(child),
2746
+ (child) => child && !isWhitespaceTextNode(child),
2688
2747
  );
2689
2748
 
2690
2749
  if (hasMeaningfulChildren) {
@@ -2718,18 +2777,23 @@ export function TSRXPlugin(config) {
2718
2777
  // Dynamic tags (`<{Tag}>`) name by expression source. The braces keep
2719
2778
  // them from colliding with static tag names ('style', 'head', ...) and
2720
2779
  // read as source syntax in error messages (`</{Tag}>`).
2721
- const expression = /** @type {AST.Expression} */ (/** @type {any} */ (node).expression);
2780
+ const expression = node.expression;
2722
2781
  return `{${this.input.slice(expression.start, expression.end).trim()}}`;
2723
2782
  }
2724
2783
  return null;
2725
2784
  }
2726
2785
 
2727
2786
  /**
2728
- * @param {any} name
2729
- * @returns {boolean}
2787
+ * @param {AST.Node | '' | null | undefined} name
2788
+ * @returns {name is ESTreeJSX.JSXExpressionContainer}
2730
2789
  */
2731
2790
  #isDynamicJSXElementName(name) {
2732
- return !!(name && name.type === 'JSXExpressionContainer' && name.isDynamic === true);
2791
+ return (
2792
+ !!name &&
2793
+ typeof name !== 'string' &&
2794
+ name.type === 'JSXExpressionContainer' &&
2795
+ name.isDynamic === true
2796
+ );
2733
2797
  }
2734
2798
 
2735
2799
  /**
@@ -2738,15 +2802,15 @@ export function TSRXPlugin(config) {
2738
2802
  * composed of those. Constructed values (calls, spreads, concatenation,
2739
2803
  * interpolation, object/array literals) and static non-string literals
2740
2804
  * can never be valid tag names.
2741
- * @param {any} expression
2805
+ * @param {AST.Node | null | undefined} expression
2742
2806
  * @returns {boolean}
2743
2807
  */
2744
2808
  #isValidDynamicTagExpression(expression) {
2745
2809
  let node = expression;
2746
- while (node && DYNAMIC_TAG_WRAPPER_TYPES.has(node.type)) {
2810
+ while (node && is_dynamic_tag_wrapper(node)) {
2747
2811
  node = node.expression;
2748
2812
  }
2749
- if (!node || node.type?.startsWith?.('JSX')) return false;
2813
+ if (!node || node.type.startsWith('JSX')) return false;
2750
2814
  if (node.type === 'Identifier') return node.name !== 'undefined';
2751
2815
  if (node.type === 'Literal') return typeof node.value === 'string';
2752
2816
  if (node.type === 'UnaryExpression' && node.operator === 'void') return false;
@@ -2754,8 +2818,10 @@ export function TSRXPlugin(config) {
2754
2818
  }
2755
2819
 
2756
2820
  /**
2757
- * @param {any} node
2758
- * @param {Set<any>} [seen]
2821
+ * Walks every property of the tag expression, so it receives whatever the
2822
+ * AST holds — nodes, arrays of nodes, and the primitives in between.
2823
+ * @param {unknown} node
2824
+ * @param {Set<unknown>} [seen]
2759
2825
  * @returns {boolean}
2760
2826
  */
2761
2827
  #containsDisallowedDynamicTagSyntax(node, seen = new Set()) {
@@ -2764,16 +2830,18 @@ export function TSRXPlugin(config) {
2764
2830
  if (Array.isArray(node)) {
2765
2831
  return node.some((child) => this.#containsDisallowedDynamicTagSyntax(child, seen));
2766
2832
  }
2833
+ const ast_node = /** @type {AST.Node} */ (node);
2767
2834
  if (
2768
- DYNAMIC_TAG_DISALLOWED_TYPES.has(node.type) ||
2769
- (node.type === 'TemplateLiteral' && node.expressions.length > 0) ||
2770
- (node.type === 'BinaryExpression' && node.operator === '+')
2835
+ DYNAMIC_TAG_DISALLOWED_TYPES.has(ast_node.type) ||
2836
+ (ast_node.type === 'TemplateLiteral' && ast_node.expressions.length > 0) ||
2837
+ (ast_node.type === 'BinaryExpression' && ast_node.operator === '+')
2771
2838
  ) {
2772
2839
  return true;
2773
2840
  }
2774
- for (const key of Object.keys(node)) {
2841
+ for (const key of Object.keys(ast_node)) {
2775
2842
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
2776
- if (this.#containsDisallowedDynamicTagSyntax(node[key], seen)) return true;
2843
+ const value = /** @type {Record<string, unknown>} */ (ast_node)[key];
2844
+ if (this.#containsDisallowedDynamicTagSyntax(value, seen)) return true;
2777
2845
  }
2778
2846
  return false;
2779
2847
  }
@@ -3411,17 +3479,20 @@ export function TSRXPlugin(config) {
3411
3479
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
3412
3480
  this.#jsxExpressionContainerDepth++;
3413
3481
  let pushed_context_baseline = false;
3414
- /** @type {number} */
3415
- let context_baseline;
3482
+ // The stack depth with the container's `{` brace context on top, taken
3483
+ // before `next()` so the first expression token's own context pushes
3484
+ // (e.g. a fragment's `<` pushing its tag contexts) are not counted.
3485
+ // This is the depth the stack must return to when the container's
3486
+ // closing `}` is the current token.
3487
+ const container_context_depth = this.context.length;
3416
3488
  try {
3417
3489
  this.next();
3418
3490
 
3419
- // Record the context-stack depth now that the container's `{` brace
3420
- // context is on the stack. A control-flow directive parsed inside this
3491
+ // Record the context-stack depth now that the first expression token
3492
+ // has been read. A control-flow directive parsed inside this
3421
3493
  // container must not strip anything below this floor (see
3422
3494
  // `#filterTemplateScriptContexts`).
3423
- context_baseline = this.context.length;
3424
- this.#expressionContainerContextBaselines.push(context_baseline);
3495
+ this.#expressionContainerContextBaselines.push(this.context.length);
3425
3496
  this.#expressionContainerPathBaselines.push(this.#path.length);
3426
3497
  pushed_context_baseline = true;
3427
3498
 
@@ -3442,13 +3513,14 @@ export function TSRXPlugin(config) {
3442
3513
  // a snapshot taken inside this container
3443
3514
  // (`#parseTemplateControlFlowBlock`), so the container's closing `}`
3444
3515
  // — read while that stale snapshot was active — pops the wrong entry
3445
- // and leaves stale brace contexts above the enclosing tag's contexts.
3446
- // Once the `}` has been read the stack must be back at one below the
3447
- // baseline (the container's own brace context popped); drop anything
3448
- // above that so the token after `}` (e.g. the `>` finishing the
3449
- // enclosing opening tag) tokenizes in the right context.
3450
- if (this.type === tt.braceR && this.context.length >= context_baseline) {
3451
- this.context.length = context_baseline - 1;
3516
+ // and leaves stale contexts above the enclosing tag's contexts (the
3517
+ // directive's statement brace, or a wrapping fragment's child
3518
+ // context). Once the `}` has been read the stack must be back at one
3519
+ // below the container's depth (its own brace context popped); drop
3520
+ // anything above that so the token after `}` (e.g. the `>` finishing
3521
+ // the enclosing opening tag) tokenizes in the right context.
3522
+ if (this.type === tt.braceR && this.context.length >= container_context_depth) {
3523
+ this.context.length = container_context_depth - 1;
3452
3524
  }
3453
3525
  this.expect(tt.braceR);
3454
3526
  }
@@ -3540,8 +3612,8 @@ export function TSRXPlugin(config) {
3540
3612
  acorn.getLineInfo(this.input, brace_start + 1),
3541
3613
  );
3542
3614
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
3543
- /** @type {any} */ (node).value = expression;
3544
- /** @type {any} */ (node).shorthand = true;
3615
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).value = expression;
3616
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).shorthand = true;
3545
3617
 
3546
3618
  const end = brace_start + 1;
3547
3619
  const endLoc = acorn.getLineInfo(this.input, end);
@@ -3613,13 +3685,13 @@ export function TSRXPlugin(config) {
3613
3685
  );
3614
3686
  expression.expression = name;
3615
3687
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
3616
- /** @type {any} */ (node).value = this.finishNodeAt(
3688
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).value = this.finishNodeAt(
3617
3689
  expression,
3618
3690
  'JSXExpressionContainer',
3619
3691
  this.end + 1,
3620
3692
  this.endLoc,
3621
3693
  );
3622
- /** @type {any} */ (node).shorthand = true;
3694
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).shorthand = true;
3623
3695
  this.next();
3624
3696
  this.expect(tt.braceR);
3625
3697
  return this.finishNode(node, 'JSXAttribute');
@@ -3727,13 +3799,23 @@ export function TSRXPlugin(config) {
3727
3799
  /** @type {Parse.Parser['jsx_parseAttributeValue']} */
3728
3800
  jsx_parseAttributeValue() {
3729
3801
  switch (this.type) {
3730
- case tt.braceL:
3802
+ case tt.braceL: {
3803
+ // The host element's `templateMode` is still `'script'` while its
3804
+ // opening tag parses, which would route JSX inside the value
3805
+ // container to the vanilla (non-TSRX) element parser. Clear the
3806
+ // opening node so the container parses exactly like a child-position
3807
+ // `{ … }` container — an inline template value must behave the same
3808
+ // as one assigned to a variable and passed by name.
3809
+ const opening_node = this.#openingNativeTemplateNode;
3810
+ this.#openingNativeTemplateNode = null;
3731
3811
  this.#jsxAttributeValueExpressionDepth++;
3732
3812
  try {
3733
3813
  return this.jsx_parseExpressionContainer();
3734
3814
  } finally {
3735
3815
  this.#jsxAttributeValueExpressionDepth--;
3816
+ this.#openingNativeTemplateNode = opening_node;
3736
3817
  }
3818
+ }
3737
3819
  case tstt.jsxTagStart:
3738
3820
  case tt.string:
3739
3821
  return this.parseExprAtom();
@@ -3992,18 +4074,27 @@ export function TSRXPlugin(config) {
3992
4074
 
3993
4075
  switch (ch) {
3994
4076
  case CharCode.equals:
4077
+ // The `allow_inside_expression_container` form keeps `=` (and `=>`)
4078
+ // as text inside a container-nested element's children, matching the
4079
+ // bare-template path — see the default case below.
3995
4080
  if (
3996
- !this.#shouldReadTemplateRawTextToken() &&
4081
+ !this.#shouldReadTemplateRawTextToken(true) &&
3997
4082
  this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
3998
4083
  ) {
3999
4084
  this.#resetTokenStartToCurrentPosition();
4000
4085
  this.pos += 2;
4001
4086
  return this.finishToken(tt.arrow);
4002
4087
  }
4003
- if (this.#shouldReadTemplateRawTextToken()) {
4088
+ if (this.#shouldReadTemplateRawTextToken(true)) {
4004
4089
  ++this.pos;
4005
4090
  break;
4006
4091
  }
4092
+ {
4093
+ const accumulated = out + this.input.slice(chunkStart, this.pos);
4094
+ if (this.#shouldFinishAccumulatedTemplateText(accumulated)) {
4095
+ return this.finishToken(tstt.jsxText, accumulated);
4096
+ }
4097
+ }
4007
4098
  this.#resetTokenStartToCurrentPosition();
4008
4099
  this.context.push(b_stat);
4009
4100
  this.exprAllowed = true;
@@ -4182,6 +4273,16 @@ export function TSRXPlugin(config) {
4182
4273
  ++this.pos;
4183
4274
  break;
4184
4275
  }
4276
+ // Like `<` and `{` above, a bail boundary (e.g. a directive's `@`)
4277
+ // must first finish accumulated template text — resetting the token
4278
+ // start here would silently drop it from the template. See
4279
+ // `#shouldFinishAccumulatedTemplateText` for which runs qualify.
4280
+ {
4281
+ const accumulated = out + this.input.slice(chunkStart, this.pos);
4282
+ if (this.#shouldFinishAccumulatedTemplateText(accumulated)) {
4283
+ return this.finishToken(tstt.jsxText, accumulated);
4284
+ }
4285
+ }
4185
4286
  this.#resetTokenStartToCurrentPosition();
4186
4287
  this.context.push(b_stat);
4187
4288
  this.exprAllowed = true;
@@ -4225,18 +4326,18 @@ export function TSRXPlugin(config) {
4225
4326
  * @type {Parse.Parser['jsx_parseOpeningElementAt']}
4226
4327
  */
4227
4328
  jsx_parseOpeningElementAt(startPos, startLoc) {
4228
- const node = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
4329
+ const node = /** @type {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} */ (
4229
4330
  this.startNodeAt(/** @type {number} */ (startPos), /** @type {AST.Position} */ (startLoc))
4230
4331
  );
4231
4332
  node.attributes = [];
4232
4333
  const nodeName = this.jsx_parseElementName();
4233
- if (nodeName) node.name = /** @type {any} */ (nodeName);
4334
+ if (nodeName) node.name = nodeName;
4234
4335
  if (this.#isDynamicJSXElementName(nodeName)) {
4235
- /** @type {any} */ (node).isDynamic = true;
4336
+ node.isDynamic = true;
4236
4337
  }
4237
4338
  if (this.match(tt.relational) || this.match(tt.bitShift)) {
4238
- const typeArguments = /** @type {any} */ (this).tsTryParseAndCatch(() =>
4239
- /** @type {any} */ (this).tsParseTypeArgumentsInExpression(),
4339
+ const typeArguments = this.tsTryParseAndCatch(() =>
4340
+ this.tsParseTypeArgumentsInExpression(),
4240
4341
  );
4241
4342
  if (typeArguments) node.typeArguments = typeArguments;
4242
4343
  }
@@ -4248,19 +4349,23 @@ export function TSRXPlugin(config) {
4248
4349
  const opening_template_node = this.#openingNativeTemplateNode;
4249
4350
  let pushed_opening_template_node = false;
4250
4351
  if (opening_template_node) {
4352
+ // The enclosing `parseElement` started this node before the opening tag
4353
+ // said what it is; stamp its shape now that the tag has been read.
4354
+ const template_node = /** @type {Parse.NativeTemplateNodeSlots} */ (
4355
+ opening_template_node
4356
+ );
4251
4357
  if (nodeName) {
4252
- /** @type {any} */ (opening_template_node).type =
4358
+ template_node.type =
4253
4359
  this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
4254
- /** @type {any} */ (opening_template_node).openingElement = node;
4255
- /** @type {any} */ (opening_template_node).closingElement = null;
4360
+ template_node.openingElement = node;
4361
+ template_node.closingElement = null;
4256
4362
  if (this.#isDynamicJSXElementName(nodeName)) {
4257
- /** @type {any} */ (opening_template_node).isDynamic = true;
4363
+ template_node.isDynamic = true;
4258
4364
  }
4259
4365
  } else {
4260
- /** @type {any} */ (opening_template_node).type = 'JSXFragment';
4261
- /** @type {any} */ (opening_template_node).openingFragment =
4262
- this.#toOpeningFragment(node);
4263
- /** @type {any} */ (opening_template_node).closingFragment = null;
4366
+ template_node.type = 'JSXFragment';
4367
+ template_node.openingFragment = this.#toOpeningFragment(node);
4368
+ template_node.closingFragment = null;
4264
4369
  }
4265
4370
  this.#path.push(opening_template_node);
4266
4371
  pushed_opening_template_node = true;
@@ -4276,8 +4381,10 @@ export function TSRXPlugin(config) {
4276
4381
  if (nodeName) {
4277
4382
  return this.finishNode(node, 'JSXOpeningElement');
4278
4383
  }
4279
- return /** @type {any} */ (
4280
- /** @type {any} */ (this).finishNode(node, 'JSXOpeningFragment')
4384
+ // `<>` reuses the opening-element node, so finishing it also retypes it.
4385
+ return this.finishNode(
4386
+ /** @type {ESTreeJSX.JSXOpeningFragment} */ (/** @type {unknown} */ (node)),
4387
+ 'JSXOpeningFragment',
4281
4388
  );
4282
4389
  }
4283
4390
 
@@ -4322,7 +4429,7 @@ export function TSRXPlugin(config) {
4322
4429
  this.#openingNativeTemplateNode = node;
4323
4430
  let open;
4324
4431
  try {
4325
- open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
4432
+ open = /** @type {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} */ (
4326
4433
  this.jsx_parseOpeningElementAt(start, position)
4327
4434
  );
4328
4435
  } finally {
@@ -4350,24 +4457,24 @@ export function TSRXPlugin(config) {
4350
4457
  );
4351
4458
  }
4352
4459
 
4460
+ // The node was started before its opening tag was read, so its shape is
4461
+ // stamped through the under-construction view.
4462
+ const slots = /** @type {Parse.NativeTemplateNodeSlots} */ (node);
4353
4463
  if (is_fragment) {
4354
- /** @type {ESTreeJSX.JSXFragment} */ (node).type = 'JSXFragment';
4355
- /** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment =
4356
- this.#toOpeningFragment(open);
4357
- /** @type {any} */ (node).closingFragment = null;
4464
+ slots.type = 'JSXFragment';
4465
+ slots.openingFragment = this.#toOpeningFragment(open);
4466
+ slots.closingFragment = null;
4358
4467
  } else {
4359
4468
  if (is_style) {
4360
- /** @type {AST.JSXStyleElement} */ (node).type = 'JSXStyleElement';
4361
- /** @type {AST.JSXStyleElement} */ (node).openingElement =
4362
- /** @type {AST.JSXStyleElement['openingElement']} */ (open);
4363
- /** @type {AST.JSXStyleElement} */ (node).closingElement =
4364
- /** @type {AST.JSXStyleElement['closingElement']} */ (null);
4469
+ slots.type = 'JSXStyleElement';
4470
+ slots.openingElement = open;
4471
+ slots.closingElement = null;
4365
4472
  } else {
4366
- /** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
4367
- /** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
4368
- /** @type {ESTreeJSX.JSXElement} */ (node).closingElement = null;
4473
+ slots.type = 'JSXElement';
4474
+ slots.openingElement = open;
4475
+ slots.closingElement = null;
4369
4476
  if (is_dynamic) {
4370
- /** @type {any} */ (node).isDynamic = true;
4477
+ slots.isDynamic = true;
4371
4478
  }
4372
4479
  }
4373
4480
  }
@@ -4403,7 +4510,7 @@ export function TSRXPlugin(config) {
4403
4510
  this.start,
4404
4511
  `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
4405
4512
  );
4406
- /** @type {any} */ (node).unclosed = true;
4513
+ slots.unclosed = true;
4407
4514
  /** @type {AST.SourceLocation} */ (node.loc).end = {
4408
4515
  .../** @type {AST.SourceLocation} */ (
4409
4516
  is_fragment
@@ -4474,7 +4581,7 @@ export function TSRXPlugin(config) {
4474
4581
  this.curLine = loc.line;
4475
4582
  this.lineStart = at_index - loc.column;
4476
4583
  }
4477
- body.push(/** @type {any} */ (this.#parseCodeBlock()));
4584
+ body.push(this.#parseCodeBlock());
4478
4585
  this.parseTemplateBody(body);
4479
4586
  return;
4480
4587
  }
@@ -4578,8 +4685,7 @@ export function TSRXPlugin(config) {
4578
4685
  this.#closingNativeTemplateNode = false;
4579
4686
  }
4580
4687
  if (closingName) {
4581
- /** @type {ESTreeJSX.JSXClosingElement} */ (closingNode).name =
4582
- /** @type {ESTreeJSX.JSXIdentifier} */ (closingName);
4688
+ /** @type {ESTreeJSX.TSRXJSXClosingElement} */ (closingNode).name = closingName;
4583
4689
  }
4584
4690
  const current = /** @type {ESTreeJSX.JSXFragment | ESTreeJSX.JSXElement} */ (
4585
4691
  this.#path[this.#path.length - 1]
@@ -4602,19 +4708,21 @@ export function TSRXPlugin(config) {
4602
4708
  this.expect(tstt.jsxTagEnd);
4603
4709
  this.#closingNativeTemplateNode = false;
4604
4710
  const closingElement =
4605
- /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
4711
+ /** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
4606
4712
  this.finishNode(
4607
4713
  closingNode,
4608
4714
  closingName ? 'JSXClosingElement' : 'JSXClosingFragment',
4609
4715
  )
4610
4716
  );
4611
4717
  if (this.#isDynamicJSXElementName(closingElement.name)) {
4612
- /** @type {any} */ (closingElement).isDynamic = true;
4718
+ closingElement.isDynamic = true;
4613
4719
  }
4614
4720
  this.exprAllowed = false;
4615
4721
 
4616
4722
  // Validate that the closing tag matches the opening tag
4617
- const currentElement = /** @type {any} */ (this.#path[this.#path.length - 1]);
4723
+ const currentElement = /** @type {AST.NativeTSRXTemplateNode} */ (
4724
+ this.#path[this.#path.length - 1]
4725
+ );
4618
4726
  if (!this.#isNativeTemplateNode(currentElement)) {
4619
4727
  this.raise(this.start, 'Unexpected closing tag');
4620
4728
  }
@@ -4648,7 +4756,7 @@ export function TSRXPlugin(config) {
4648
4756
  // simply an unexpected closing tag (e.g. `<div></span>`).
4649
4757
  const normalized_closing_name = closingTagName ?? '';
4650
4758
  const matches_open_element = this.#path.some((node) => {
4651
- const elem = /** @type {any} */ (node);
4759
+ const elem = /** @type {AST.NativeTSRXTemplateNode} */ (node);
4652
4760
  if (!this.#isNativeTemplateNode(elem)) return false;
4653
4761
  const elemName =
4654
4762
  elem.type === 'JSXFragment'
@@ -4669,7 +4777,9 @@ export function TSRXPlugin(config) {
4669
4777
  );
4670
4778
  // Loop through all unclosed elements on the stack
4671
4779
  while (this.#path.length > 0) {
4672
- const elem = /** @type {any} */ (this.#path[this.#path.length - 1]);
4780
+ const elem = /** @type {AST.NativeTSRXTemplateNode} */ (
4781
+ this.#path[this.#path.length - 1]
4782
+ );
4673
4783
 
4674
4784
  // Stop at non-template boundaries.
4675
4785
  if (!this.#isNativeTemplateNode(elem)) {
@@ -4702,7 +4812,9 @@ export function TSRXPlugin(config) {
4702
4812
  }
4703
4813
  }
4704
4814
 
4705
- const elementToClose = /** @type {any} */ (this.#path[this.#path.length - 1]);
4815
+ const elementToClose = /** @type {AST.NativeTSRXTemplateNode} */ (
4816
+ this.#path[this.#path.length - 1]
4817
+ );
4706
4818
  if (this.#isNativeTemplateNode(elementToClose)) {
4707
4819
  const elementToCloseName =
4708
4820
  elementToClose.type === 'JSXFragment'