@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/package.json +10 -1
- package/src/analyze/prune.js +62 -77
- package/src/analyze/validation.js +7 -7
- package/src/parse/index.js +59 -80
- package/src/plugin.js +219 -107
- package/src/runtime/language-helpers.js +34 -25
- package/src/runtime/ref.js +57 -36
- package/src/scope.js +4 -5
- package/src/source-map-utils.js +8 -22
- package/src/transform/imports.js +8 -14
- package/src/transform/jsx/ast-builders.js +4 -1
- package/src/transform/jsx/index.js +1 -13
- package/src/transform/jsx/server-module.js +102 -96
- package/src/transform/jsx-hoist.js +4 -4
- package/src/transform/lazy.js +196 -137
- package/src/transform/scoping.js +108 -100
- package/src/transform/segments.js +24 -49
- package/src/transform/style-ref.js +92 -106
- package/src/transform/stylesheet.js +5 -21
- package/src/utils/ast.js +25 -20
- package/src/utils/builders.js +66 -2
- package/src/vite/dep-scan.js +135 -0
- package/types/index.d.ts +317 -17
- package/types/parse.d.ts +98 -13
- package/types/runtime/language-helpers.d.ts +10 -5
- package/types/runtime/ref.d.ts +63 -14
- package/types/vite/dep-scan.d.ts +26 -0
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.
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1323
|
+
return this.#parseCodeBlock();
|
|
1280
1324
|
}
|
|
1281
1325
|
|
|
1282
1326
|
if (this.#isJSXControlFlowDirectiveAt(at_index)) {
|
|
1283
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
1488
|
+
return this.#parseCodeBlock();
|
|
1444
1489
|
}
|
|
1445
1490
|
if (this.#isJSXControlFlowDirectiveAt(this.start)) {
|
|
1446
|
-
return
|
|
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 {
|
|
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 {
|
|
1463
|
-
|
|
1464
|
-
|
|
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(
|
|
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(
|
|
1966
|
+
consequent.push(node);
|
|
1912
1967
|
return;
|
|
1913
1968
|
}
|
|
1914
1969
|
|
|
1915
1970
|
if (this.#isJSXControlFlowDirectiveStart()) {
|
|
1916
|
-
consequent.push(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2684
|
-
|
|
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
|
-
(
|
|
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 =
|
|
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 {
|
|
2729
|
-
* @returns {
|
|
2787
|
+
* @param {AST.Node | '' | null | undefined} name
|
|
2788
|
+
* @returns {name is ESTreeJSX.JSXExpressionContainer}
|
|
2730
2789
|
*/
|
|
2731
2790
|
#isDynamicJSXElementName(name) {
|
|
2732
|
-
return
|
|
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 {
|
|
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 &&
|
|
2810
|
+
while (node && is_dynamic_tag_wrapper(node)) {
|
|
2747
2811
|
node = node.expression;
|
|
2748
2812
|
}
|
|
2749
|
-
if (!node || node.type
|
|
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
|
-
*
|
|
2758
|
-
*
|
|
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(
|
|
2769
|
-
(
|
|
2770
|
-
(
|
|
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(
|
|
2841
|
+
for (const key of Object.keys(ast_node)) {
|
|
2775
2842
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
2776
|
-
|
|
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
|
-
|
|
3415
|
-
|
|
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
|
|
3420
|
-
//
|
|
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
|
-
|
|
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
|
|
3446
|
-
//
|
|
3447
|
-
//
|
|
3448
|
-
//
|
|
3449
|
-
//
|
|
3450
|
-
|
|
3451
|
-
|
|
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 {
|
|
3544
|
-
/** @type {
|
|
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 {
|
|
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 {
|
|
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.
|
|
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 =
|
|
4334
|
+
if (nodeName) node.name = nodeName;
|
|
4234
4335
|
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4235
|
-
|
|
4336
|
+
node.isDynamic = true;
|
|
4236
4337
|
}
|
|
4237
4338
|
if (this.match(tt.relational) || this.match(tt.bitShift)) {
|
|
4238
|
-
const typeArguments =
|
|
4239
|
-
|
|
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
|
-
|
|
4358
|
+
template_node.type =
|
|
4253
4359
|
this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
|
|
4254
|
-
|
|
4255
|
-
|
|
4360
|
+
template_node.openingElement = node;
|
|
4361
|
+
template_node.closingElement = null;
|
|
4256
4362
|
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4257
|
-
|
|
4363
|
+
template_node.isDynamic = true;
|
|
4258
4364
|
}
|
|
4259
4365
|
} else {
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
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
|
-
|
|
4280
|
-
|
|
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.
|
|
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
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
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
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
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
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4473
|
+
slots.type = 'JSXElement';
|
|
4474
|
+
slots.openingElement = open;
|
|
4475
|
+
slots.closingElement = null;
|
|
4369
4476
|
if (is_dynamic) {
|
|
4370
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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'
|