@tsrx/core 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.js CHANGED
@@ -16,8 +16,8 @@ import { regex_newline_characters } from './utils/patterns.js';
16
16
  import { error } from './errors.js';
17
17
  import { DIAGNOSTIC_CODES } from './diagnostics.js';
18
18
  import { TSRX_RETURN_STATEMENT_ERROR } from './analyze/validation.js';
19
- const DYNAMIC_ATTRIBUTE_NAME_ERROR =
20
- 'Dynamic component / element syntax (`@`) is only supported on native TSRX element names, not attribute names.';
19
+ const FORGOTTEN_STATEMENT_CONTAINER_ERROR =
20
+ "This function body contains TSRX template output, but it is a normal JavaScript block. Add '@' before the opening brace to use a TSRX statement container.";
21
21
 
22
22
  const CharCode = Object.freeze({
23
23
  tab: 9,
@@ -256,6 +256,15 @@ export function TSRXPlugin(config) {
256
256
  #allowExpressionContainerTrailingSemicolon = false;
257
257
  #jsxAttributeValueExpressionDepth = 0;
258
258
  #jsxExpressionContainerDepth = 0;
259
+ // Context-stack length at the start of each open `{ … }` expression container.
260
+ // A control-flow directive (`@if`/`@for`/…) parsed inside a container strips
261
+ // JSX contexts so its header/body tokenize as JS; without a floor it would also
262
+ // strip the enclosing element's and container's contexts (which nothing rebuilds),
263
+ // underflowing the context stack when the surrounding markup closes. The directive
264
+ // filter preserves everything below the innermost baseline. See
265
+ // `#filterTemplateScriptContexts`.
266
+ /** @type {number[]} */
267
+ #expressionContainerContextBaselines = [];
259
268
  #consumeContainerBraceAfterScope = false;
260
269
  #scriptJSXElementDepth = 0;
261
270
  #forceScriptJSXElementDepth = 0;
@@ -316,7 +325,7 @@ export function TSRXPlugin(config) {
316
325
  * This helper keeps the parser-state setup in one place while callers keep
317
326
  * ownership of their distinct closing delimiter handling (`}` vs `</tag>`).
318
327
  *
319
- * @param {AST.Node} node
328
+ * @param {AST.Node & { body?: AST.Node }} node
320
329
  * @param {AST.Node[]} body
321
330
  * @param {{
322
331
  * enterScope?: boolean,
@@ -450,16 +459,6 @@ export function TSRXPlugin(config) {
450
459
  );
451
460
  }
452
461
 
453
- /**
454
- * @param {any} name
455
- * @returns {boolean}
456
- */
457
- #isDynamicJSXElementName(name) {
458
- if (!name || typeof name !== 'object') return false;
459
- if (name.dynamic === true) return true;
460
- return name.type === 'JSXMemberExpression' && this.#isDynamicJSXElementName(name.object);
461
- }
462
-
463
462
  #isInsideNativeTemplateScriptSection() {
464
463
  const node = this.#currentNativeTemplateNode();
465
464
  return !!node && node.metadata?.templateMode !== 'template';
@@ -836,10 +835,7 @@ export function TSRXPlugin(config) {
836
835
  }
837
836
  this.pos = index;
838
837
  if (found_boundary) {
839
- this.context = this.context.filter(
840
- (context) =>
841
- context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
842
- );
838
+ this.#filterTemplateScriptContexts();
843
839
  if (this.curContext() !== b_stat) {
844
840
  this.context.push(b_stat);
845
841
  }
@@ -851,14 +847,22 @@ export function TSRXPlugin(config) {
851
847
  return this.finishNodeAt(node, 'JSXText', index, endLoc);
852
848
  }
853
849
 
854
- #shouldReadTemplateRawTextToken() {
850
+ /**
851
+ * @param {boolean} [allow_inside_expression_container] When set, do not bail
852
+ * purely because we are inside a `{ … }` expression container. A JSX
853
+ * element nested in a container (e.g. `{<div> a</div>}`) is still a
854
+ * template-mode element whose text children are raw JSX text; the rest of
855
+ * the directive/comment/boundary checks below still apply, so a directive
856
+ * body inside an expression container is correctly excluded.
857
+ */
858
+ #shouldReadTemplateRawTextToken(allow_inside_expression_container = false) {
855
859
  if (
856
860
  this.#closingNativeTemplateNode ||
857
861
  this.#readingJSXControlFlowDirectiveKeyword ||
858
862
  this.#readingJSXControlFlowHeader ||
859
863
  this.#parsingJSXSwitchCaseScriptStatementDepth > 0 ||
860
864
  this.#templateScriptParsingDepth > 0 ||
861
- this.#jsxExpressionContainerDepth > 0
865
+ (!allow_inside_expression_container && this.#jsxExpressionContainerDepth > 0)
862
866
  ) {
863
867
  return false;
864
868
  }
@@ -1055,6 +1059,85 @@ export function TSRXPlugin(config) {
1055
1059
  return false;
1056
1060
  }
1057
1061
 
1062
+ /**
1063
+ * @param {AST.Node | null | undefined} node
1064
+ */
1065
+ #isForgottenStatementContainerOutputNode(node) {
1066
+ return this.#isRenderOutputNode(node) && node?.type !== 'JSXCodeBlock';
1067
+ }
1068
+
1069
+ /**
1070
+ * @param {AST.Node | null | undefined} node
1071
+ */
1072
+ #isIgnoredForgottenStatementContainerStatement(node) {
1073
+ return !node || node.type === 'EmptyStatement';
1074
+ }
1075
+
1076
+ /**
1077
+ * A normal function body that directly contains a bare JSX/control-flow node
1078
+ * almost always means the author wrote `{ ... <div /> }` but intended
1079
+ * `@{ ... <div /> }`. Only report when adding `@` would produce a valid
1080
+ * statement container: setup statements first, followed by one final render
1081
+ * output. Report only direct body children so ordinary nested callbacks/branches
1082
+ * are diagnosed by their own function body, not their parent.
1083
+ * @param {AST.Node} node
1084
+ */
1085
+ #reportForgottenStatementContainerBody(node) {
1086
+ if (!this.#collect) {
1087
+ return;
1088
+ }
1089
+
1090
+ const body = /** @type {{ body?: AST.Node }} */ (node).body;
1091
+ if (body?.type !== 'BlockStatement') {
1092
+ return;
1093
+ }
1094
+
1095
+ const statements = /** @type {AST.BlockStatement} */ (body).body || [];
1096
+ const has_return_type = Boolean(/** @type {{ returnType?: AST.Node }} */ (node).returnType);
1097
+ if (!has_return_type) {
1098
+ return;
1099
+ }
1100
+
1101
+ let target = null;
1102
+ let target_index = -1;
1103
+ for (let index = 0; index < statements.length; index++) {
1104
+ const statement = statements[index];
1105
+ const output =
1106
+ this.#isForgottenStatementContainerOutputNode(statement) ||
1107
+ (statement.type === 'ExpressionStatement' &&
1108
+ this.#isForgottenStatementContainerOutputNode(statement.expression))
1109
+ ? statement
1110
+ : null;
1111
+
1112
+ if (!output) {
1113
+ continue;
1114
+ }
1115
+
1116
+ if (target_index !== -1) {
1117
+ return;
1118
+ }
1119
+ target_index = index;
1120
+ target = output;
1121
+ }
1122
+
1123
+ if (!target) {
1124
+ return;
1125
+ }
1126
+
1127
+ for (const statement of statements.slice(target_index + 1)) {
1128
+ if (!this.#isIgnoredForgottenStatementContainerStatement(statement)) {
1129
+ return;
1130
+ }
1131
+ }
1132
+
1133
+ this.#report_recoverable_error_range(
1134
+ /** @type {number} */ (target.start),
1135
+ /** @type {number} */ (target.end),
1136
+ FORGOTTEN_STATEMENT_CONTAINER_ERROR,
1137
+ DIAGNOSTIC_CODES.FORGOTTEN_STATEMENT_CONTAINER,
1138
+ );
1139
+ }
1140
+
1058
1141
  /**
1059
1142
  * Inside a code block (`@{ … }` or a directive's `{ }`), decides whether the
1060
1143
  * next thing is the single bare render node (`<tag …>`, `<>…</>`, or an
@@ -1255,7 +1338,7 @@ export function TSRXPlugin(config) {
1255
1338
  * `JSXCodeBlock` whose `body` holds the setup statements and `render` the
1256
1339
  * single optional render output (§9).
1257
1340
  */
1258
- #parseCodeBlock() {
1341
+ #parseCodeBlock({ allowReturnStatements = false } = {}) {
1259
1342
  const start = this.start;
1260
1343
  const startLoc = this.startLoc;
1261
1344
  const node = /** @type {AST.JSXCodeBlock} */ (this.startNodeAt(start, startLoc));
@@ -1306,6 +1389,9 @@ export function TSRXPlugin(config) {
1306
1389
  } else {
1307
1390
  node.body = /** @type {AST.Statement[]} */ (flat);
1308
1391
  }
1392
+ if (!allowReturnStatements) {
1393
+ this.#report_invalid_template_return_statements(node.body);
1394
+ }
1309
1395
 
1310
1396
  if (this.type !== tt.braceR) {
1311
1397
  this.unexpected();
@@ -1358,6 +1444,25 @@ export function TSRXPlugin(config) {
1358
1444
  return node;
1359
1445
  }
1360
1446
 
1447
+ /**
1448
+ * Drop the JSX tokenizer contexts (`tc_expr`/`tc_oTag`/`tc_cTag`) so the
1449
+ * directive header/body tokenizes as JavaScript, while preserving every
1450
+ * context below the innermost open `{ … }` expression container. Those lower
1451
+ * contexts belong to the enclosing markup (the container brace, the element
1452
+ * that holds the `{ … }`, any outer fragment); a plain filter would drop them
1453
+ * too and underflow the context stack when that markup later closes. Outside
1454
+ * any expression container the baseline is 0, so this matches the original
1455
+ * "strip everything" behavior the bare-template path relies on.
1456
+ */
1457
+ #filterTemplateScriptContexts() {
1458
+ const baseline = this.#expressionContainerContextBaselines.at(-1) ?? 0;
1459
+ this.context = this.context.filter(
1460
+ (context, index) =>
1461
+ index < baseline ||
1462
+ (context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag),
1463
+ );
1464
+ }
1465
+
1361
1466
  #parseJSXControlFlowExpression() {
1362
1467
  const start = this.start;
1363
1468
  const startLoc = this.startLoc;
@@ -1365,6 +1470,13 @@ export function TSRXPlugin(config) {
1365
1470
  this.pos = keywordStart;
1366
1471
  this.start = keywordStart;
1367
1472
  this.startLoc = acorn.getLineInfo(this.input, keywordStart);
1473
+ this.curLine = this.startLoc.line;
1474
+ this.lineStart = keywordStart - this.startLoc.column;
1475
+ this.#filterTemplateScriptContexts();
1476
+ if (this.curContext() !== b_stat) {
1477
+ this.context.push(b_stat);
1478
+ }
1479
+ this.exprAllowed = true;
1368
1480
  this.#readingJSXControlFlowDirectiveKeyword = true;
1369
1481
  try {
1370
1482
  this.nextToken();
@@ -1421,7 +1533,7 @@ export function TSRXPlugin(config) {
1421
1533
  } finally {
1422
1534
  this.#templateControlFlowBlockDepth--;
1423
1535
  }
1424
- } else if (this.#isUnprefixedDirectiveClauseKeyword('empty')) {
1536
+ } else if (this.#isUnprefixedDirectiveClauseContinuation('empty', ['{'])) {
1425
1537
  this.raise(this.start, 'Expected `@empty` after `@for` block.');
1426
1538
  } else {
1427
1539
  /** @type {any} */ (node).empty = null;
@@ -1471,10 +1583,7 @@ export function TSRXPlugin(config) {
1471
1583
  this.startLoc = acorn.getLineInfo(this.input, wordStart);
1472
1584
  this.curLine = this.startLoc.line;
1473
1585
  this.lineStart = wordStart - this.startLoc.column;
1474
- this.context = this.context.filter(
1475
- (context) =>
1476
- context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
1477
- );
1586
+ this.#filterTemplateScriptContexts();
1478
1587
  if (this.curContext() !== b_stat) {
1479
1588
  this.context.push(b_stat);
1480
1589
  }
@@ -1510,10 +1619,7 @@ export function TSRXPlugin(config) {
1510
1619
  this.startLoc = acorn.getLineInfo(this.input, wordStart);
1511
1620
  this.curLine = this.startLoc.line;
1512
1621
  this.lineStart = wordStart - this.startLoc.column;
1513
- this.context = this.context.filter(
1514
- (context) =>
1515
- context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
1516
- );
1622
+ this.#filterTemplateScriptContexts();
1517
1623
  if (this.curContext() !== b_stat) {
1518
1624
  this.context.push(b_stat);
1519
1625
  }
@@ -1529,13 +1635,31 @@ export function TSRXPlugin(config) {
1529
1635
 
1530
1636
  /**
1531
1637
  * @param {string} keyword
1638
+ * @param {string[]} continuations
1532
1639
  */
1533
- #isUnprefixedDirectiveClauseKeyword(keyword) {
1640
+ #isUnprefixedDirectiveClauseContinuation(keyword, continuations) {
1534
1641
  const keywordStart = skip_whitespace_from(this.input, this.start);
1535
- return (
1536
- this.input.slice(keywordStart, keywordStart + keyword.length) === keyword &&
1537
- !this.#isIdentifierChar(this.input.charCodeAt(keywordStart + keyword.length))
1538
- );
1642
+ if (
1643
+ this.input.slice(keywordStart, keywordStart + keyword.length) !== keyword ||
1644
+ this.#isIdentifierChar(this.input.charCodeAt(keywordStart + keyword.length))
1645
+ ) {
1646
+ return false;
1647
+ }
1648
+
1649
+ const continuationStart = skip_whitespace_from(this.input, keywordStart + keyword.length);
1650
+ for (const continuation of continuations) {
1651
+ if (continuation.length === 1 && this.input[continuationStart] === continuation) {
1652
+ return true;
1653
+ }
1654
+ if (
1655
+ this.input.slice(continuationStart, continuationStart + continuation.length) ===
1656
+ continuation &&
1657
+ !this.#isIdentifierChar(this.input.charCodeAt(continuationStart + continuation.length))
1658
+ ) {
1659
+ return true;
1660
+ }
1661
+ }
1662
+ return false;
1539
1663
  }
1540
1664
 
1541
1665
  /**
@@ -1575,7 +1699,7 @@ export function TSRXPlugin(config) {
1575
1699
  node.alternate = this.#eatJSXDirectiveBareClauseKeyword('if')
1576
1700
  ? this.#parseTemplateIfStatement()
1577
1701
  : /** @type {AST.Statement} */ (this.#parseTemplateControlFlowStatement());
1578
- } else if (this.#isUnprefixedDirectiveClauseKeyword('else')) {
1702
+ } else if (this.#isUnprefixedDirectiveClauseContinuation('else', ['{', 'if'])) {
1579
1703
  this.raise(this.start, 'Expected `@else` after `@if` block.');
1580
1704
  }
1581
1705
 
@@ -1683,10 +1807,7 @@ export function TSRXPlugin(config) {
1683
1807
  if (/^return\b/.test(raw)) {
1684
1808
  this.raise(this.start, '`return` is invalid inside `@switch` cases.');
1685
1809
  }
1686
- this.context = this.context.filter(
1687
- (context) =>
1688
- context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
1689
- );
1810
+ this.#filterTemplateScriptContexts();
1690
1811
  this.pos = this.start;
1691
1812
  this.startLoc = this.curPosition();
1692
1813
  if (this.curContext() !== b_stat) {
@@ -1772,10 +1893,7 @@ export function TSRXPlugin(config) {
1772
1893
  // token contexts so the statement and the following `}`/`case` tokenize as
1773
1894
  // code.
1774
1895
  if (this.type !== tstt.jsxText && this.type !== tt.eof) {
1775
- this.context = this.context.filter(
1776
- (context) =>
1777
- context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
1778
- );
1896
+ this.#filterTemplateScriptContexts();
1779
1897
  if (this.curContext() !== b_stat) {
1780
1898
  this.context.push(b_stat);
1781
1899
  }
@@ -1859,7 +1977,6 @@ export function TSRXPlugin(config) {
1859
1977
  )
1860
1978
  );
1861
1979
  name.name = 'style';
1862
- name.tracked = false;
1863
1980
  this.finishNodeAt(
1864
1981
  name,
1865
1982
  'JSXIdentifier',
@@ -2054,9 +2171,19 @@ export function TSRXPlugin(config) {
2054
2171
  #popTokenContextsAfterTemplateExpressionElement(node) {
2055
2172
  // A fragment in expression position (`() => <>…</>`) leaves the tokenizer
2056
2173
  // at `exprAllowed === false`, unlike a self-closing element. When the next
2057
- // token is a `;`, the following statement may legitimately open with a JSX
2058
- // tag (`<List/>`), so restore expression position to match the element path.
2059
- if (this.type === tt.semi && node.type === 'JSXFragment') {
2174
+ // token is a `;` or ASI can insert one, the following statement may
2175
+ // legitimately open with a JSX tag (`<List/>`), so restore expression
2176
+ // position to match the element path.
2177
+ if ((this.type === tt.semi || this.canInsertSemicolon()) && node.type === 'JSXFragment') {
2178
+ this.exprAllowed = true;
2179
+ }
2180
+ // A JSX element/fragment used as a ternary consequent (`cond ? <a>…</a> : …`)
2181
+ // likewise leaves the tokenizer at `exprAllowed === false`, so the `<` after
2182
+ // the `:` would not start a tag. Restore expression position so the alternate
2183
+ // branch parses as JSX too. This applies to both elements and fragments,
2184
+ // unlike the `;`/ASI case above (a `:` only follows a value, so the next
2185
+ // token always begins the alternate expression).
2186
+ if (this.type === tt.colon) {
2060
2187
  this.exprAllowed = true;
2061
2188
  }
2062
2189
  const ctx = this.context;
@@ -3038,10 +3165,9 @@ export function TSRXPlugin(config) {
3038
3165
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
3039
3166
  this.#functionBodyDepth++;
3040
3167
  try {
3041
- // Allow a `@{ … }` code block as the body of a (non-arrow) function or
3042
- // method, so a component can be written `function Something() @{ … }` or
3043
- // `{ Render() @{ … } }`. Arrow concise bodies (`() => @{ … }`) already
3044
- // route through `parseExprAtom`.
3168
+ // Allow a `@{ … }` code block as the body of a function, method, or
3169
+ // arrow function, so components can be written as `function Something()
3170
+ // @{ … }`, `{ Render() @{ … } }`, or `const Something = () => @{ … }`.
3045
3171
  //
3046
3172
  // A return-type annotation sits between the params and the body
3047
3173
  // (`function f(): T @{ … }`). acorn-typescript parses it inside
@@ -3051,13 +3177,15 @@ export function TSRXPlugin(config) {
3051
3177
  if (!isArrowFunction && this.match(tt.colon)) {
3052
3178
  node.returnType = this.tsParseTypeOrTypePredicateAnnotation(tt.colon);
3053
3179
  }
3054
- if (!isArrowFunction && this.#isCodeBlockStart(this.start)) {
3055
- node.body = this.parseMaybeAssign(forInit);
3180
+ if (this.#isCodeBlockStart(this.start)) {
3181
+ node.body = this.#parseCodeBlock({ allowReturnStatements: true });
3056
3182
  this.checkParams(node, false);
3057
3183
  this.exitScope();
3058
3184
  return node;
3059
3185
  }
3060
- return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
3186
+ const parsed = super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
3187
+ this.#reportForgottenStatementContainerBody(parsed);
3188
+ return parsed;
3061
3189
  } finally {
3062
3190
  this.#functionBodyDepth--;
3063
3191
  }
@@ -3077,9 +3205,17 @@ export function TSRXPlugin(config) {
3077
3205
  this.#consumeContainerBraceAfterScope = false;
3078
3206
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
3079
3207
  this.#jsxExpressionContainerDepth++;
3208
+ let pushed_context_baseline = false;
3080
3209
  try {
3081
3210
  this.next();
3082
3211
 
3212
+ // Record the context-stack depth now that the container's `{` brace
3213
+ // context is on the stack. A control-flow directive parsed inside this
3214
+ // container must not strip anything below this floor (see
3215
+ // `#filterTemplateScriptContexts`).
3216
+ this.#expressionContainerContextBaselines.push(this.context.length);
3217
+ pushed_context_baseline = true;
3218
+
3083
3219
  node.expression =
3084
3220
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
3085
3221
  if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
@@ -3097,6 +3233,9 @@ export function TSRXPlugin(config) {
3097
3233
  }
3098
3234
  } finally {
3099
3235
  this.#jsxExpressionContainerDepth--;
3236
+ if (pushed_context_baseline) {
3237
+ this.#expressionContainerContextBaselines.pop();
3238
+ }
3100
3239
  }
3101
3240
 
3102
3241
  if (consumeBraceAfterScope) {
@@ -3162,7 +3301,6 @@ export function TSRXPlugin(config) {
3162
3301
  this.startNodeAt(name_start, name_start_loc)
3163
3302
  );
3164
3303
  id.name = name_value;
3165
- id.tracked = false;
3166
3304
  this.finishNodeAt(id, 'JSXIdentifier', name_end, name_end_loc);
3167
3305
  const name = /** @type {AST.Identifier} */ (
3168
3306
  this.startNodeAt(name_start, name_start_loc)
@@ -3239,7 +3377,6 @@ export function TSRXPlugin(config) {
3239
3377
  this.startNodeAt(name_start, name_start_loc)
3240
3378
  );
3241
3379
  id.name = name_value;
3242
- id.tracked = false;
3243
3380
  this.finishNodeAt(id, 'JSXIdentifier', name_end, name_end_loc);
3244
3381
  const name = /** @type {AST.Identifier} */ (
3245
3382
  this.startNodeAt(name_start, name_start_loc)
@@ -3267,16 +3404,6 @@ export function TSRXPlugin(config) {
3267
3404
  }
3268
3405
  }
3269
3406
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
3270
- if (this.#isDynamicJSXElementName(/** @type {ESTreeJSX.JSXAttribute} */ (node).name)) {
3271
- this.#report_recoverable_error_range(
3272
- /** @type {AST.NodeWithLocation} */ (node).start,
3273
- /** @type {AST.NodeWithLocation} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
3274
- .end ??
3275
- node.end ??
3276
- node.start,
3277
- DYNAMIC_ATTRIBUTE_NAME_ERROR,
3278
- );
3279
- }
3280
3407
  const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
3281
3408
  this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
3282
3409
  );
@@ -3307,18 +3434,7 @@ export function TSRXPlugin(config) {
3307
3434
  jsx_parseIdentifier() {
3308
3435
  const node = /** @type {ESTreeJSX.JSXIdentifier} */ (this.startNode());
3309
3436
 
3310
- if (this.type.label === '@') {
3311
- this.next(); // consume @
3312
-
3313
- if (this.type === tt.name || this.type === tstt.jsxName) {
3314
- node.name = /** @type {string} */ (this.value);
3315
- /** @type {any} */ (node).dynamic = true;
3316
- this.next();
3317
- } else {
3318
- // Unexpected token after @
3319
- this.unexpected();
3320
- }
3321
- } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
3437
+ if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
3322
3438
  node.name = /** @type {string} */ (this.value);
3323
3439
  this.next();
3324
3440
  } else {
@@ -3389,7 +3505,7 @@ export function TSRXPlugin(config) {
3389
3505
  }
3390
3506
 
3391
3507
  /**
3392
- * `@try`/`@pending`/`@catch`/`finally` blocks lower their direct `return`
3508
+ * `@try`/`@pending`/`@catch` blocks lower their direct `return`
3393
3509
  * values into reactive boundary fallbacks, so unlike `@if`/`@for`/`@switch`
3394
3510
  * blocks they legitimately allow `return <markup>` statements. Set the flag
3395
3511
  * immediately before parsing each such block so its body sees it.
@@ -3413,7 +3529,7 @@ export function TSRXPlugin(config) {
3413
3529
 
3414
3530
  if (this.#eatJSXDirectiveClauseKeyword('pending')) {
3415
3531
  node.pending = this.#parseTemplateControlFlowReturnBlock();
3416
- } else if (this.#isUnprefixedDirectiveClauseKeyword('pending')) {
3532
+ } else if (this.#isUnprefixedDirectiveClauseContinuation('pending', ['{'])) {
3417
3533
  this.raise(this.start, 'Expected `@pending` after `@try` block.');
3418
3534
  } else {
3419
3535
  node.pending = null;
@@ -3432,12 +3548,7 @@ export function TSRXPlugin(config) {
3432
3548
  this.startLoc = acorn.getLineInfo(this.input, paramStart);
3433
3549
  this.curLine = this.startLoc.line;
3434
3550
  this.lineStart = paramStart - this.startLoc.column;
3435
- this.context = this.context.filter(
3436
- (context) =>
3437
- context !== tstc.tc_expr &&
3438
- context !== tstc.tc_oTag &&
3439
- context !== tstc.tc_cTag,
3440
- );
3551
+ this.#filterTemplateScriptContexts();
3441
3552
  if (this.curContext() !== b_stat) {
3442
3553
  this.context.push(b_stat);
3443
3554
  }
@@ -3495,17 +3606,15 @@ export function TSRXPlugin(config) {
3495
3606
  clause.body = this.#parseTemplateControlFlowReturnBlock(false);
3496
3607
  this.exitScope();
3497
3608
  node.handler = this.finishNode(clause, 'CatchClause');
3498
- } else if (this.#isUnprefixedDirectiveClauseKeyword('catch')) {
3609
+ } else if (this.#isUnprefixedDirectiveClauseContinuation('catch', ['{', '('])) {
3499
3610
  this.raise(this.start, 'Expected `@catch` after `@try` block.');
3500
3611
  }
3501
- node.finalizer = this.eat(tt._finally)
3502
- ? this.#parseTemplateControlFlowReturnBlock()
3503
- : null;
3612
+ node.finalizer = null;
3504
3613
 
3505
- if (!node.handler && !node.finalizer && !node.pending) {
3614
+ if (!node.handler && !node.pending) {
3506
3615
  this.raise(
3507
3616
  /** @type {AST.NodeWithLocation} */ (node).start,
3508
- 'Missing catch or finally clause',
3617
+ 'Missing `@catch` or `@pending` after `@try` block.',
3509
3618
  );
3510
3619
  }
3511
3620
  return this.finishNode(node, 'TryStatement');
@@ -3811,7 +3920,18 @@ export function TSRXPlugin(config) {
3811
3920
  } else if (ch === CharCode.space || ch === CharCode.tab) {
3812
3921
  ++this.pos;
3813
3922
  } else {
3814
- if (this.#shouldReadTemplateRawTextToken()) {
3923
+ // A JSX element nested inside a `{ … }` expression container is
3924
+ // still a template-mode element whose text children are raw JSX
3925
+ // text (e.g. `{<div> a</div>}`). The default raw-text check bails
3926
+ // for everything inside an expression container, so without the
3927
+ // `allow_inside_expression_container` form the first non-space char
3928
+ // would re-anchor the token start and drop the leading whitespace
3929
+ // this loop already skipped. Keep scanning so the full run —
3930
+ // leading indentation included — is captured, matching the
3931
+ // bare-template path. Directive bodies (`@if`/`@for`/…) inside the
3932
+ // element still fall through to JS tokenization via the other
3933
+ // checks in `#shouldReadTemplateRawTextToken`.
3934
+ if (this.#shouldReadTemplateRawTextToken(true)) {
3815
3935
  ++this.pos;
3816
3936
  break;
3817
3937
  }
@@ -3854,32 +3974,6 @@ export function TSRXPlugin(config) {
3854
3974
  return parsed;
3855
3975
  }
3856
3976
 
3857
- /**
3858
- * @type {Parse.Parser['jsx_parseElementAt']}
3859
- */
3860
- jsx_parseElementAt(startPos, startLoc) {
3861
- if (this.input.charCodeAt(startPos + 1) === CharCode.at) {
3862
- const previous_script_jsx_element_depth = this.#scriptJSXElementDepth;
3863
- this.#scriptJSXElementDepth = 0;
3864
- try {
3865
- const parsed = /** @type {ESTreeJSX.JSXElement} */ (
3866
- /** @type {unknown} */ (this.parseElement())
3867
- );
3868
- // A dynamic `<@tag>` parsed here goes straight through `parseElement`,
3869
- // bypassing `jsx_parseElement`'s context cleanup. In expression
3870
- // position (e.g. a render-prop arrow body inside object params) its
3871
- // markup contexts must be unwound, or the following JS token (a `,`/`}`)
3872
- // is mis-tokenized as JSX raw text.
3873
- this.#popTokenContextsAfterTemplateExpressionElement(parsed);
3874
- return parsed;
3875
- } finally {
3876
- this.#scriptJSXElementDepth = previous_script_jsx_element_depth;
3877
- }
3878
- }
3879
-
3880
- return super.jsx_parseElementAt(startPos, startLoc);
3881
- }
3882
-
3883
3977
  /**
3884
3978
  * @type {Parse.Parser['jsx_parseOpeningElementAt']}
3885
3979
  */
@@ -3890,9 +3984,6 @@ export function TSRXPlugin(config) {
3890
3984
  node.attributes = [];
3891
3985
  const nodeName = this.jsx_parseElementName();
3892
3986
  if (nodeName) node.name = nodeName;
3893
- if (this.#isDynamicJSXElementName(nodeName)) {
3894
- /** @type {any} */ (node).dynamic = true;
3895
- }
3896
3987
  if (this.match(tt.relational) || this.match(tt.bitShift)) {
3897
3988
  const typeArguments = /** @type {any} */ (this).tsTryParseAndCatch(() =>
3898
3989
  /** @type {any} */ (this).tsParseTypeArgumentsInExpression(),
@@ -4018,9 +4109,6 @@ export function TSRXPlugin(config) {
4018
4109
  /** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
4019
4110
  /** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
4020
4111
  /** @type {ESTreeJSX.JSXElement} */ (node).closingElement = null;
4021
- if (/** @type {any} */ (open).dynamic) {
4022
- /** @type {any} */ (node).dynamic = true;
4023
- }
4024
4112
  }
4025
4113
  }
4026
4114
 
@@ -4421,6 +4509,18 @@ export function TSRXPlugin(config) {
4421
4509
  return node;
4422
4510
  }
4423
4511
 
4512
+ if (
4513
+ this.input.charCodeAt(this.start) === CharCode.at &&
4514
+ (this.#isCodeBlockStart(this.start) || this.#isJSXControlFlowDirectiveStart())
4515
+ ) {
4516
+ const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
4517
+ node.expression = /** @type {AST.Expression} */ (this.parseExpression());
4518
+ this.semicolon();
4519
+ return /** @type {AST.ExpressionStatement} */ (
4520
+ this.finishNode(node, 'ExpressionStatement')
4521
+ );
4522
+ }
4523
+
4424
4524
  // &[ or &{ at statement level — lazy destructuring assignment
4425
4525
  // e.g., &[data] = track(0); or &{x, y} = obj;
4426
4526
  if (this.type === tt.bitwiseAND) {
@@ -1,3 +1,3 @@
1
- export { is_boolean_attribute } from '../utils/dom.js';
1
+ export { is_boolean_attribute, is_void_element } from '../utils/dom.js';
2
2
  export { escape, escape_script } from '../utils/escaping.js';
3
3
  export { normalize_css_property_name } from '../utils/normalize_css_property_name.js';
@@ -89,3 +89,42 @@ export function iterable_array_from(iterable, index = 0) {
89
89
  }
90
90
  return result;
91
91
  }
92
+
93
+ /**
94
+ * Creates a shallow forwarding object without one prop. Values are exposed through
95
+ * getters so compiler-emitted reactive prop accessors are not snapshotted.
96
+ * @param {Record<PropertyKey, any> | null | undefined} props
97
+ * @param {PropertyKey} exclude_prop
98
+ * @returns {Record<PropertyKey, any>}
99
+ */
100
+ export function exclude_prop_from_object(props, exclude_prop) {
101
+ /** @type {Record<PropertyKey, any>} */
102
+ const next = {};
103
+ if (props == null) return next;
104
+
105
+ for (const prop of Reflect.ownKeys(props)) {
106
+ if (prop === exclude_prop) continue;
107
+
108
+ const descriptor = get_descriptor(props, prop);
109
+ if (!descriptor?.enumerable) continue;
110
+
111
+ /** @type {PropertyDescriptor} */
112
+ const forwarding_descriptor = {
113
+ enumerable: true,
114
+ configurable: true,
115
+ get() {
116
+ return props[prop];
117
+ },
118
+ };
119
+
120
+ if (descriptor.writable === true || typeof descriptor.set === 'function') {
121
+ forwarding_descriptor.set = (value) => {
122
+ props[prop] = value;
123
+ };
124
+ }
125
+
126
+ define_property(next, prop, forwarding_descriptor);
127
+ }
128
+
129
+ return next;
130
+ }