@tsrx/core 0.1.50 → 0.1.52
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 +230 -95
- 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
|
}
|
|
@@ -3544,8 +3612,8 @@ export function TSRXPlugin(config) {
|
|
|
3544
3612
|
acorn.getLineInfo(this.input, brace_start + 1),
|
|
3545
3613
|
);
|
|
3546
3614
|
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
|
|
3547
|
-
/** @type {
|
|
3548
|
-
/** @type {
|
|
3615
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).value = expression;
|
|
3616
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).shorthand = true;
|
|
3549
3617
|
|
|
3550
3618
|
const end = brace_start + 1;
|
|
3551
3619
|
const endLoc = acorn.getLineInfo(this.input, end);
|
|
@@ -3617,13 +3685,13 @@ export function TSRXPlugin(config) {
|
|
|
3617
3685
|
);
|
|
3618
3686
|
expression.expression = name;
|
|
3619
3687
|
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
|
|
3620
|
-
/** @type {
|
|
3688
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).value = this.finishNodeAt(
|
|
3621
3689
|
expression,
|
|
3622
3690
|
'JSXExpressionContainer',
|
|
3623
3691
|
this.end + 1,
|
|
3624
3692
|
this.endLoc,
|
|
3625
3693
|
);
|
|
3626
|
-
/** @type {
|
|
3694
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).shorthand = true;
|
|
3627
3695
|
this.next();
|
|
3628
3696
|
this.expect(tt.braceR);
|
|
3629
3697
|
return this.finishNode(node, 'JSXAttribute');
|
|
@@ -3731,13 +3799,23 @@ export function TSRXPlugin(config) {
|
|
|
3731
3799
|
/** @type {Parse.Parser['jsx_parseAttributeValue']} */
|
|
3732
3800
|
jsx_parseAttributeValue() {
|
|
3733
3801
|
switch (this.type) {
|
|
3734
|
-
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;
|
|
3735
3811
|
this.#jsxAttributeValueExpressionDepth++;
|
|
3736
3812
|
try {
|
|
3737
3813
|
return this.jsx_parseExpressionContainer();
|
|
3738
3814
|
} finally {
|
|
3739
3815
|
this.#jsxAttributeValueExpressionDepth--;
|
|
3816
|
+
this.#openingNativeTemplateNode = opening_node;
|
|
3740
3817
|
}
|
|
3818
|
+
}
|
|
3741
3819
|
case tstt.jsxTagStart:
|
|
3742
3820
|
case tt.string:
|
|
3743
3821
|
return this.parseExprAtom();
|
|
@@ -3996,18 +4074,27 @@ export function TSRXPlugin(config) {
|
|
|
3996
4074
|
|
|
3997
4075
|
switch (ch) {
|
|
3998
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.
|
|
3999
4080
|
if (
|
|
4000
|
-
!this.#shouldReadTemplateRawTextToken() &&
|
|
4081
|
+
!this.#shouldReadTemplateRawTextToken(true) &&
|
|
4001
4082
|
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
4002
4083
|
) {
|
|
4003
4084
|
this.#resetTokenStartToCurrentPosition();
|
|
4004
4085
|
this.pos += 2;
|
|
4005
4086
|
return this.finishToken(tt.arrow);
|
|
4006
4087
|
}
|
|
4007
|
-
if (this.#shouldReadTemplateRawTextToken()) {
|
|
4088
|
+
if (this.#shouldReadTemplateRawTextToken(true)) {
|
|
4008
4089
|
++this.pos;
|
|
4009
4090
|
break;
|
|
4010
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
|
+
}
|
|
4011
4098
|
this.#resetTokenStartToCurrentPosition();
|
|
4012
4099
|
this.context.push(b_stat);
|
|
4013
4100
|
this.exprAllowed = true;
|
|
@@ -4186,6 +4273,16 @@ export function TSRXPlugin(config) {
|
|
|
4186
4273
|
++this.pos;
|
|
4187
4274
|
break;
|
|
4188
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
|
+
}
|
|
4189
4286
|
this.#resetTokenStartToCurrentPosition();
|
|
4190
4287
|
this.context.push(b_stat);
|
|
4191
4288
|
this.exprAllowed = true;
|
|
@@ -4229,18 +4326,18 @@ export function TSRXPlugin(config) {
|
|
|
4229
4326
|
* @type {Parse.Parser['jsx_parseOpeningElementAt']}
|
|
4230
4327
|
*/
|
|
4231
4328
|
jsx_parseOpeningElementAt(startPos, startLoc) {
|
|
4232
|
-
const node = /** @type {ESTreeJSX.
|
|
4329
|
+
const node = /** @type {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} */ (
|
|
4233
4330
|
this.startNodeAt(/** @type {number} */ (startPos), /** @type {AST.Position} */ (startLoc))
|
|
4234
4331
|
);
|
|
4235
4332
|
node.attributes = [];
|
|
4236
4333
|
const nodeName = this.jsx_parseElementName();
|
|
4237
|
-
if (nodeName) node.name =
|
|
4334
|
+
if (nodeName) node.name = nodeName;
|
|
4238
4335
|
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4239
|
-
|
|
4336
|
+
node.isDynamic = true;
|
|
4240
4337
|
}
|
|
4241
4338
|
if (this.match(tt.relational) || this.match(tt.bitShift)) {
|
|
4242
|
-
const typeArguments =
|
|
4243
|
-
|
|
4339
|
+
const typeArguments = this.tsTryParseAndCatch(() =>
|
|
4340
|
+
this.tsParseTypeArgumentsInExpression(),
|
|
4244
4341
|
);
|
|
4245
4342
|
if (typeArguments) node.typeArguments = typeArguments;
|
|
4246
4343
|
}
|
|
@@ -4252,19 +4349,23 @@ export function TSRXPlugin(config) {
|
|
|
4252
4349
|
const opening_template_node = this.#openingNativeTemplateNode;
|
|
4253
4350
|
let pushed_opening_template_node = false;
|
|
4254
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
|
+
);
|
|
4255
4357
|
if (nodeName) {
|
|
4256
|
-
|
|
4358
|
+
template_node.type =
|
|
4257
4359
|
this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
|
|
4258
|
-
|
|
4259
|
-
|
|
4360
|
+
template_node.openingElement = node;
|
|
4361
|
+
template_node.closingElement = null;
|
|
4260
4362
|
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4261
|
-
|
|
4363
|
+
template_node.isDynamic = true;
|
|
4262
4364
|
}
|
|
4263
4365
|
} else {
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
/** @type {any} */ (opening_template_node).closingFragment = null;
|
|
4366
|
+
template_node.type = 'JSXFragment';
|
|
4367
|
+
template_node.openingFragment = this.#toOpeningFragment(node);
|
|
4368
|
+
template_node.closingFragment = null;
|
|
4268
4369
|
}
|
|
4269
4370
|
this.#path.push(opening_template_node);
|
|
4270
4371
|
pushed_opening_template_node = true;
|
|
@@ -4280,8 +4381,10 @@ export function TSRXPlugin(config) {
|
|
|
4280
4381
|
if (nodeName) {
|
|
4281
4382
|
return this.finishNode(node, 'JSXOpeningElement');
|
|
4282
4383
|
}
|
|
4283
|
-
|
|
4284
|
-
|
|
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',
|
|
4285
4388
|
);
|
|
4286
4389
|
}
|
|
4287
4390
|
|
|
@@ -4326,7 +4429,7 @@ export function TSRXPlugin(config) {
|
|
|
4326
4429
|
this.#openingNativeTemplateNode = node;
|
|
4327
4430
|
let open;
|
|
4328
4431
|
try {
|
|
4329
|
-
open = /** @type {ESTreeJSX.
|
|
4432
|
+
open = /** @type {ESTreeJSX.TSRXJSXOpeningElement & AST.NodeWithLocation} */ (
|
|
4330
4433
|
this.jsx_parseOpeningElementAt(start, position)
|
|
4331
4434
|
);
|
|
4332
4435
|
} finally {
|
|
@@ -4354,24 +4457,24 @@ export function TSRXPlugin(config) {
|
|
|
4354
4457
|
);
|
|
4355
4458
|
}
|
|
4356
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);
|
|
4357
4463
|
if (is_fragment) {
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
/** @type {any} */ (node).closingFragment = null;
|
|
4464
|
+
slots.type = 'JSXFragment';
|
|
4465
|
+
slots.openingFragment = this.#toOpeningFragment(open);
|
|
4466
|
+
slots.closingFragment = null;
|
|
4362
4467
|
} else {
|
|
4363
4468
|
if (is_style) {
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
/** @type {AST.JSXStyleElement} */ (node).closingElement =
|
|
4368
|
-
/** @type {AST.JSXStyleElement['closingElement']} */ (null);
|
|
4469
|
+
slots.type = 'JSXStyleElement';
|
|
4470
|
+
slots.openingElement = open;
|
|
4471
|
+
slots.closingElement = null;
|
|
4369
4472
|
} else {
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4473
|
+
slots.type = 'JSXElement';
|
|
4474
|
+
slots.openingElement = open;
|
|
4475
|
+
slots.closingElement = null;
|
|
4373
4476
|
if (is_dynamic) {
|
|
4374
|
-
|
|
4477
|
+
slots.isDynamic = true;
|
|
4375
4478
|
}
|
|
4376
4479
|
}
|
|
4377
4480
|
}
|
|
@@ -4407,7 +4510,7 @@ export function TSRXPlugin(config) {
|
|
|
4407
4510
|
this.start,
|
|
4408
4511
|
`Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
|
|
4409
4512
|
);
|
|
4410
|
-
|
|
4513
|
+
slots.unclosed = true;
|
|
4411
4514
|
/** @type {AST.SourceLocation} */ (node.loc).end = {
|
|
4412
4515
|
.../** @type {AST.SourceLocation} */ (
|
|
4413
4516
|
is_fragment
|
|
@@ -4478,7 +4581,7 @@ export function TSRXPlugin(config) {
|
|
|
4478
4581
|
this.curLine = loc.line;
|
|
4479
4582
|
this.lineStart = at_index - loc.column;
|
|
4480
4583
|
}
|
|
4481
|
-
body.push(
|
|
4584
|
+
body.push(this.#parseCodeBlock());
|
|
4482
4585
|
this.parseTemplateBody(body);
|
|
4483
4586
|
return;
|
|
4484
4587
|
}
|
|
@@ -4492,7 +4595,34 @@ export function TSRXPlugin(config) {
|
|
|
4492
4595
|
// text never starts at `<`, so drop the leaked context and re-read the
|
|
4493
4596
|
// tag instead of emitting an empty node.
|
|
4494
4597
|
if (this.input.charCodeAt(this.start) === CharCode.lessThan) {
|
|
4495
|
-
if (this
|
|
4598
|
+
if (this.#jsxExpressionContainerDepth > 0) {
|
|
4599
|
+
// Inside a `{ … }` container the whole-stack counts below are
|
|
4600
|
+
// blind: the enclosing template's `tc_expr` contexts sit on the
|
|
4601
|
+
// stack but their elements are not on the container-scoped
|
|
4602
|
+
// `#path`. Scope both counts to the container instead — each
|
|
4603
|
+
// element opened inside it (all on `#path` above the container's
|
|
4604
|
+
// baseline) still owns one `tc_expr` that its closing tag's
|
|
4605
|
+
// `jsxTagEnd` pops itself, so only the run's excess above that
|
|
4606
|
+
// quota is leaked. Popping deeper would make a re-read closing
|
|
4607
|
+
// tag pop the container's brace or the enclosing tag context.
|
|
4608
|
+
const path_baseline = this.#expressionContainerPathBaselines.at(-1) ?? 0;
|
|
4609
|
+
let open_elements = 0;
|
|
4610
|
+
for (let i = path_baseline; i < this.#path.length; i++) {
|
|
4611
|
+
if (this.#isNativeTemplateNode(this.#path[i])) open_elements++;
|
|
4612
|
+
}
|
|
4613
|
+
let run = 0;
|
|
4614
|
+
for (
|
|
4615
|
+
let i = this.context.length - 1;
|
|
4616
|
+
i >= 0 && this.context[i] === tstc.tc_expr;
|
|
4617
|
+
i--
|
|
4618
|
+
) {
|
|
4619
|
+
run++;
|
|
4620
|
+
}
|
|
4621
|
+
while (run > open_elements && this.curContext() === tstc.tc_expr) {
|
|
4622
|
+
this.context.pop();
|
|
4623
|
+
run--;
|
|
4624
|
+
}
|
|
4625
|
+
} else if (this.input.charCodeAt(this.start + 1) === CharCode.slash) {
|
|
4496
4626
|
while (this.curContext() === tstc.tc_expr) {
|
|
4497
4627
|
this.context.pop();
|
|
4498
4628
|
}
|
|
@@ -4582,8 +4712,7 @@ export function TSRXPlugin(config) {
|
|
|
4582
4712
|
this.#closingNativeTemplateNode = false;
|
|
4583
4713
|
}
|
|
4584
4714
|
if (closingName) {
|
|
4585
|
-
/** @type {ESTreeJSX.
|
|
4586
|
-
/** @type {ESTreeJSX.JSXIdentifier} */ (closingName);
|
|
4715
|
+
/** @type {ESTreeJSX.TSRXJSXClosingElement} */ (closingNode).name = closingName;
|
|
4587
4716
|
}
|
|
4588
4717
|
const current = /** @type {ESTreeJSX.JSXFragment | ESTreeJSX.JSXElement} */ (
|
|
4589
4718
|
this.#path[this.#path.length - 1]
|
|
@@ -4606,19 +4735,21 @@ export function TSRXPlugin(config) {
|
|
|
4606
4735
|
this.expect(tstt.jsxTagEnd);
|
|
4607
4736
|
this.#closingNativeTemplateNode = false;
|
|
4608
4737
|
const closingElement =
|
|
4609
|
-
/** @type {ESTreeJSX.
|
|
4738
|
+
/** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
|
|
4610
4739
|
this.finishNode(
|
|
4611
4740
|
closingNode,
|
|
4612
4741
|
closingName ? 'JSXClosingElement' : 'JSXClosingFragment',
|
|
4613
4742
|
)
|
|
4614
4743
|
);
|
|
4615
4744
|
if (this.#isDynamicJSXElementName(closingElement.name)) {
|
|
4616
|
-
|
|
4745
|
+
closingElement.isDynamic = true;
|
|
4617
4746
|
}
|
|
4618
4747
|
this.exprAllowed = false;
|
|
4619
4748
|
|
|
4620
4749
|
// Validate that the closing tag matches the opening tag
|
|
4621
|
-
const currentElement = /** @type {
|
|
4750
|
+
const currentElement = /** @type {AST.NativeTSRXTemplateNode} */ (
|
|
4751
|
+
this.#path[this.#path.length - 1]
|
|
4752
|
+
);
|
|
4622
4753
|
if (!this.#isNativeTemplateNode(currentElement)) {
|
|
4623
4754
|
this.raise(this.start, 'Unexpected closing tag');
|
|
4624
4755
|
}
|
|
@@ -4652,7 +4783,7 @@ export function TSRXPlugin(config) {
|
|
|
4652
4783
|
// simply an unexpected closing tag (e.g. `<div></span>`).
|
|
4653
4784
|
const normalized_closing_name = closingTagName ?? '';
|
|
4654
4785
|
const matches_open_element = this.#path.some((node) => {
|
|
4655
|
-
const elem = /** @type {
|
|
4786
|
+
const elem = /** @type {AST.NativeTSRXTemplateNode} */ (node);
|
|
4656
4787
|
if (!this.#isNativeTemplateNode(elem)) return false;
|
|
4657
4788
|
const elemName =
|
|
4658
4789
|
elem.type === 'JSXFragment'
|
|
@@ -4673,7 +4804,9 @@ export function TSRXPlugin(config) {
|
|
|
4673
4804
|
);
|
|
4674
4805
|
// Loop through all unclosed elements on the stack
|
|
4675
4806
|
while (this.#path.length > 0) {
|
|
4676
|
-
const elem = /** @type {
|
|
4807
|
+
const elem = /** @type {AST.NativeTSRXTemplateNode} */ (
|
|
4808
|
+
this.#path[this.#path.length - 1]
|
|
4809
|
+
);
|
|
4677
4810
|
|
|
4678
4811
|
// Stop at non-template boundaries.
|
|
4679
4812
|
if (!this.#isNativeTemplateNode(elem)) {
|
|
@@ -4706,7 +4839,9 @@ export function TSRXPlugin(config) {
|
|
|
4706
4839
|
}
|
|
4707
4840
|
}
|
|
4708
4841
|
|
|
4709
|
-
const elementToClose = /** @type {
|
|
4842
|
+
const elementToClose = /** @type {AST.NativeTSRXTemplateNode} */ (
|
|
4843
|
+
this.#path[this.#path.length - 1]
|
|
4844
|
+
);
|
|
4710
4845
|
if (this.#isNativeTemplateNode(elementToClose)) {
|
|
4711
4846
|
const elementToCloseName =
|
|
4712
4847
|
elementToClose.type === 'JSXFragment'
|