@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/package.json +1 -1
- package/src/analyze/prune.js +12 -23
- package/src/analyze/validation.js +1 -1
- package/src/diagnostics.js +1 -0
- package/src/index.js +0 -2
- package/src/plugin.js +225 -125
- package/src/runtime/html.js +1 -1
- package/src/runtime/language-helpers.js +39 -0
- package/src/transform/jsx/ast-builders.js +2 -62
- package/src/transform/jsx/index.js +703 -125
- package/src/transform/scoping.js +1 -1
- package/src/transform/segments.js +13 -43
- package/types/index.d.ts +2 -5
- package/types/jsx-platform.d.ts +8 -0
- package/types/runtime/html.d.ts +1 -0
- package/types/runtime/language-helpers.d.ts +4 -0
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
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
1640
|
+
#isUnprefixedDirectiveClauseContinuation(keyword, continuations) {
|
|
1534
1641
|
const keywordStart = skip_whitespace_from(this.input, this.start);
|
|
1535
|
-
|
|
1536
|
-
this.input.slice(keywordStart, keywordStart + keyword.length)
|
|
1537
|
-
|
|
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.#
|
|
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
|
|
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
|
|
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
|
|
2058
|
-
// tag (`<List/>`), so restore expression
|
|
2059
|
-
|
|
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
|
|
3042
|
-
//
|
|
3043
|
-
// `{ Render() @{ … } }
|
|
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 (
|
|
3055
|
-
node.body = this
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.#
|
|
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
|
|
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.#
|
|
3609
|
+
} else if (this.#isUnprefixedDirectiveClauseContinuation('catch', ['{', '('])) {
|
|
3499
3610
|
this.raise(this.start, 'Expected `@catch` after `@try` block.');
|
|
3500
3611
|
}
|
|
3501
|
-
node.finalizer =
|
|
3502
|
-
? this.#parseTemplateControlFlowReturnBlock()
|
|
3503
|
-
: null;
|
|
3612
|
+
node.finalizer = null;
|
|
3504
3613
|
|
|
3505
|
-
if (!node.handler && !node.
|
|
3614
|
+
if (!node.handler && !node.pending) {
|
|
3506
3615
|
this.raise(
|
|
3507
3616
|
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
3508
|
-
'Missing catch or
|
|
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
|
-
|
|
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) {
|
package/src/runtime/html.js
CHANGED
|
@@ -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
|
+
}
|