@tsrx/core 0.1.24 → 0.1.26
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 +11 -4
- package/src/index.js +1 -0
- package/src/plugin.js +154 -2
- package/src/transform/jsx/ast-builders.js +27 -0
- package/src/transform/jsx/helpers.js +8 -1
- package/src/transform/jsx/index.js +381 -1514
- package/src/transform/jsx-hoist.js +6 -0
- package/src/transform/jsx-interleave.js +4 -0
- package/src/transform/scoping.js +8 -4
- package/src/transform/segments.js +31 -1
- package/types/index.d.ts +4 -4
- package/types/jsx-platform.d.ts +27 -13
- package/types/parse.d.ts +3 -1
package/package.json
CHANGED
package/src/analyze/prune.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import * as ESTreeJSX from 'estree-jsx' */
|
|
2
3
|
/** @import { Visitors, TopScopedClasses, StyleClasses } from '../../types/index' */
|
|
3
4
|
/** @typedef {0 | 1} Direction */
|
|
4
5
|
|
|
@@ -66,8 +67,14 @@ function get_attribute_value(attribute) {
|
|
|
66
67
|
* @param {AST.Node} node
|
|
67
68
|
* @returns {boolean}
|
|
68
69
|
*/
|
|
69
|
-
function
|
|
70
|
-
|
|
70
|
+
function is_dynamic_element(node) {
|
|
71
|
+
// `metadata.dynamicElement` marks lowered dynamic tags; `isDynamic` is the
|
|
72
|
+
// parser flag on a not-yet-lowered `<{expr}>` element. Both resolve their
|
|
73
|
+
// tag at runtime, so they can match any type selector.
|
|
74
|
+
return (
|
|
75
|
+
node?.metadata?.dynamicElement === true ||
|
|
76
|
+
/** @type {ESTreeJSX.JSXExpressionContainer} */ (node)?.isDynamic === true
|
|
77
|
+
);
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
/**
|
|
@@ -390,7 +397,7 @@ function get_descendant_elements(node, adjacent_only) {
|
|
|
390
397
|
* @returns {boolean}
|
|
391
398
|
*/
|
|
392
399
|
function can_render_dynamic_content(element, check_classes = false) {
|
|
393
|
-
if (
|
|
400
|
+
if (is_dynamic_element(element)) {
|
|
394
401
|
return true;
|
|
395
402
|
}
|
|
396
403
|
|
|
@@ -1014,7 +1021,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
|
|
|
1014
1021
|
}
|
|
1015
1022
|
|
|
1016
1023
|
case 'TypeSelector': {
|
|
1017
|
-
if (
|
|
1024
|
+
if (is_dynamic_element(element)) {
|
|
1018
1025
|
break;
|
|
1019
1026
|
}
|
|
1020
1027
|
|
package/src/index.js
CHANGED
package/src/plugin.js
CHANGED
|
@@ -76,6 +76,25 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
|
76
76
|
'throw',
|
|
77
77
|
]);
|
|
78
78
|
|
|
79
|
+
// Transparent wrappers to look through when validating a dynamic tag
|
|
80
|
+
// expression (`<{expr}>`), and syntax that disqualifies one outright.
|
|
81
|
+
const DYNAMIC_TAG_WRAPPER_TYPES = new Set([
|
|
82
|
+
'TSAsExpression',
|
|
83
|
+
'TSTypeAssertion',
|
|
84
|
+
'TSNonNullExpression',
|
|
85
|
+
'ParenthesizedExpression',
|
|
86
|
+
'ChainExpression',
|
|
87
|
+
]);
|
|
88
|
+
const DYNAMIC_TAG_DISALLOWED_TYPES = new Set([
|
|
89
|
+
'SpreadElement',
|
|
90
|
+
'ExperimentalSpreadProperty',
|
|
91
|
+
'ObjectExpression',
|
|
92
|
+
'ArrayExpression',
|
|
93
|
+
'CallExpression',
|
|
94
|
+
'NewExpression',
|
|
95
|
+
'TaggedTemplateExpression',
|
|
96
|
+
]);
|
|
97
|
+
|
|
79
98
|
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
80
99
|
const argument_clash_first_positions = new WeakMap();
|
|
81
100
|
/** @type {WeakMap<Record<string, boolean>, Set<string>>} */
|
|
@@ -1163,6 +1182,7 @@ export function TSRXPlugin(config) {
|
|
|
1163
1182
|
if (next === CharCode.slash) return false;
|
|
1164
1183
|
const tagLike =
|
|
1165
1184
|
next === CharCode.greaterThan ||
|
|
1185
|
+
next === CharCode.openBrace ||
|
|
1166
1186
|
next === CharCode.at ||
|
|
1167
1187
|
next === CharCode.dollar ||
|
|
1168
1188
|
next === CharCode.underscore ||
|
|
@@ -2581,10 +2601,70 @@ export function TSRXPlugin(config) {
|
|
|
2581
2601
|
} else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
|
|
2582
2602
|
// For components like <Foo.Bar>, return "Foo.Bar"
|
|
2583
2603
|
return this.getElementName(node.object) + '.' + this.getElementName(node.property);
|
|
2604
|
+
} else if (this.#isDynamicJSXElementName(node)) {
|
|
2605
|
+
// Dynamic tags (`<{Tag}>`) name by expression source. The braces keep
|
|
2606
|
+
// them from colliding with static tag names ('style', 'head', ...) and
|
|
2607
|
+
// read as source syntax in error messages (`</{Tag}>`).
|
|
2608
|
+
const expression = /** @type {AST.Expression} */ (/** @type {any} */ (node).expression);
|
|
2609
|
+
return `{${this.input.slice(expression.start, expression.end).trim()}}`;
|
|
2584
2610
|
}
|
|
2585
2611
|
return null;
|
|
2586
2612
|
}
|
|
2587
2613
|
|
|
2614
|
+
/**
|
|
2615
|
+
* @param {any} name
|
|
2616
|
+
* @returns {boolean}
|
|
2617
|
+
*/
|
|
2618
|
+
#isDynamicJSXElementName(name) {
|
|
2619
|
+
return !!(name && name.type === 'JSXExpressionContainer' && name.isDynamic === true);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
/**
|
|
2623
|
+
* Dynamic tag expressions must be able to resolve to an element name:
|
|
2624
|
+
* an identifier, member access, static string, or a runtime expression
|
|
2625
|
+
* composed of those. Constructed values (calls, spreads, concatenation,
|
|
2626
|
+
* interpolation, object/array literals) and static non-string literals
|
|
2627
|
+
* can never be valid tag names.
|
|
2628
|
+
* @param {any} expression
|
|
2629
|
+
* @returns {boolean}
|
|
2630
|
+
*/
|
|
2631
|
+
#isValidDynamicTagExpression(expression) {
|
|
2632
|
+
let node = expression;
|
|
2633
|
+
while (node && DYNAMIC_TAG_WRAPPER_TYPES.has(node.type)) {
|
|
2634
|
+
node = node.expression;
|
|
2635
|
+
}
|
|
2636
|
+
if (!node || node.type?.startsWith?.('JSX')) return false;
|
|
2637
|
+
if (node.type === 'Identifier') return node.name !== 'undefined';
|
|
2638
|
+
if (node.type === 'Literal') return typeof node.value === 'string';
|
|
2639
|
+
if (node.type === 'UnaryExpression' && node.operator === 'void') return false;
|
|
2640
|
+
return !this.#containsDisallowedDynamicTagSyntax(node);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/**
|
|
2644
|
+
* @param {any} node
|
|
2645
|
+
* @param {Set<any>} [seen]
|
|
2646
|
+
* @returns {boolean}
|
|
2647
|
+
*/
|
|
2648
|
+
#containsDisallowedDynamicTagSyntax(node, seen = new Set()) {
|
|
2649
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return false;
|
|
2650
|
+
seen.add(node);
|
|
2651
|
+
if (Array.isArray(node)) {
|
|
2652
|
+
return node.some((child) => this.#containsDisallowedDynamicTagSyntax(child, seen));
|
|
2653
|
+
}
|
|
2654
|
+
if (
|
|
2655
|
+
DYNAMIC_TAG_DISALLOWED_TYPES.has(node.type) ||
|
|
2656
|
+
(node.type === 'TemplateLiteral' && node.expressions.length > 0) ||
|
|
2657
|
+
(node.type === 'BinaryExpression' && node.operator === '+')
|
|
2658
|
+
) {
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
for (const key of Object.keys(node)) {
|
|
2662
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
2663
|
+
if (this.#containsDisallowedDynamicTagSyntax(node[key], seen)) return true;
|
|
2664
|
+
}
|
|
2665
|
+
return false;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2588
2668
|
/**
|
|
2589
2669
|
* `<T,>(x: T) => x` and `<T>(x: T): T => x` should parse as generic
|
|
2590
2670
|
* arrow functions, not JSX elements. acorn-typescript's `readToken`
|
|
@@ -2639,6 +2719,7 @@ export function TSRXPlugin(config) {
|
|
|
2639
2719
|
const isTagLikeAfterLt =
|
|
2640
2720
|
next === CharCode.slash ||
|
|
2641
2721
|
next === CharCode.greaterThan ||
|
|
2722
|
+
next === CharCode.openBrace ||
|
|
2642
2723
|
next === CharCode.at ||
|
|
2643
2724
|
next === CharCode.dollar ||
|
|
2644
2725
|
next === CharCode.underscore ||
|
|
@@ -2759,6 +2840,7 @@ export function TSRXPlugin(config) {
|
|
|
2759
2840
|
!isWhitespaceAfterLt &&
|
|
2760
2841
|
(nextChar === CharCode.slash ||
|
|
2761
2842
|
nextChar === CharCode.greaterThan ||
|
|
2843
|
+
nextChar === CharCode.openBrace ||
|
|
2762
2844
|
nextChar === CharCode.at ||
|
|
2763
2845
|
nextChar === CharCode.dollar ||
|
|
2764
2846
|
nextChar === CharCode.underscore ||
|
|
@@ -3444,6 +3526,18 @@ export function TSRXPlugin(config) {
|
|
|
3444
3526
|
return this.finishNode(node, 'JSXIdentifier');
|
|
3445
3527
|
}
|
|
3446
3528
|
|
|
3529
|
+
#parseJSXDynamicElementName() {
|
|
3530
|
+
const container = this.jsx_parseExpressionContainer();
|
|
3531
|
+
container.isDynamic = true;
|
|
3532
|
+
if (!this.#isValidDynamicTagExpression(container.expression)) {
|
|
3533
|
+
this.raise(
|
|
3534
|
+
/** @type {number} */ (container.expression?.start ?? container.start),
|
|
3535
|
+
'Dynamic element names must be an identifier, member expression, static string, or runtime expression; calls, spreads, string concatenation, string interpolation, and static null, undefined, boolean, number, object, and array literals are not valid tag names.',
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
return container;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3447
3541
|
/**
|
|
3448
3542
|
* @type {Parse.Parser['jsx_parseElementName']}
|
|
3449
3543
|
*/
|
|
@@ -3452,6 +3546,10 @@ export function TSRXPlugin(config) {
|
|
|
3452
3546
|
return '';
|
|
3453
3547
|
}
|
|
3454
3548
|
|
|
3549
|
+
if (this.type === tt.braceL) {
|
|
3550
|
+
return this.#parseJSXDynamicElementName();
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3455
3553
|
let node = this.jsx_parseNamespacedName();
|
|
3456
3554
|
|
|
3457
3555
|
if (node.type === 'JSXNamespacedName') {
|
|
@@ -3983,7 +4081,10 @@ export function TSRXPlugin(config) {
|
|
|
3983
4081
|
);
|
|
3984
4082
|
node.attributes = [];
|
|
3985
4083
|
const nodeName = this.jsx_parseElementName();
|
|
3986
|
-
if (nodeName) node.name = nodeName;
|
|
4084
|
+
if (nodeName) node.name = /** @type {any} */ (nodeName);
|
|
4085
|
+
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4086
|
+
/** @type {any} */ (node).isDynamic = true;
|
|
4087
|
+
}
|
|
3987
4088
|
if (this.match(tt.relational) || this.match(tt.bitShift)) {
|
|
3988
4089
|
const typeArguments = /** @type {any} */ (this).tsTryParseAndCatch(() =>
|
|
3989
4090
|
/** @type {any} */ (this).tsParseTypeArgumentsInExpression(),
|
|
@@ -4003,6 +4104,9 @@ export function TSRXPlugin(config) {
|
|
|
4003
4104
|
this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
|
|
4004
4105
|
/** @type {any} */ (opening_template_node).openingElement = node;
|
|
4005
4106
|
/** @type {any} */ (opening_template_node).closingElement = null;
|
|
4107
|
+
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4108
|
+
/** @type {any} */ (opening_template_node).isDynamic = true;
|
|
4109
|
+
}
|
|
4006
4110
|
} else {
|
|
4007
4111
|
/** @type {any} */ (opening_template_node).type = 'JSXFragment';
|
|
4008
4112
|
/** @type {any} */ (opening_template_node).openingFragment =
|
|
@@ -4076,6 +4180,7 @@ export function TSRXPlugin(config) {
|
|
|
4076
4180
|
this.#openingNativeTemplateNode = previous_opening_native_template_node;
|
|
4077
4181
|
}
|
|
4078
4182
|
const tag_name = open.name ? this.getElementName(open.name) : null;
|
|
4183
|
+
const is_dynamic = this.#isDynamicJSXElementName(open.name);
|
|
4079
4184
|
const is_style = tag_name === 'style';
|
|
4080
4185
|
const inside_head = this.#path.findLast((n) => this.#isNativeElementNamed(n, 'head'));
|
|
4081
4186
|
|
|
@@ -4109,6 +4214,9 @@ export function TSRXPlugin(config) {
|
|
|
4109
4214
|
/** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
|
|
4110
4215
|
/** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
|
|
4111
4216
|
/** @type {ESTreeJSX.JSXElement} */ (node).closingElement = null;
|
|
4217
|
+
if (is_dynamic) {
|
|
4218
|
+
/** @type {any} */ (node).isDynamic = true;
|
|
4219
|
+
}
|
|
4112
4220
|
}
|
|
4113
4221
|
}
|
|
4114
4222
|
|
|
@@ -4232,7 +4340,36 @@ export function TSRXPlugin(config) {
|
|
|
4232
4340
|
body.push(text);
|
|
4233
4341
|
}
|
|
4234
4342
|
} else if (this.#isJSXControlFlowDirectiveStart()) {
|
|
4235
|
-
|
|
4343
|
+
const directive = this.#parseJSXControlFlowExpression();
|
|
4344
|
+
body.push(directive);
|
|
4345
|
+
// `#parseTemplateControlFlowBlock` reads the token after the block's
|
|
4346
|
+
// closing `}` in a code (b_stat) context, which runs `skipSpace()` and
|
|
4347
|
+
// advances `start` past any whitespace. The following token is therefore a
|
|
4348
|
+
// JS token (e.g. the `else` keyword), and when it is actually sibling
|
|
4349
|
+
// template raw text it reaches `#parseTemplateRawText` having lost the
|
|
4350
|
+
// space(s) between `}` and the text (e.g. `@if (x) { … } else` -> the text
|
|
4351
|
+
// "else" instead of " else"). JSX text after a plain element keeps that
|
|
4352
|
+
// whitespace, so when raw text follows and only whitespace was skipped,
|
|
4353
|
+
// rewind `start` to the block's end to re-include the dropped whitespace.
|
|
4354
|
+
const blockEnd = directive.end;
|
|
4355
|
+
const nextCh = this.input.charCodeAt(this.start);
|
|
4356
|
+
const startsRawText =
|
|
4357
|
+
this.type !== tt.eof &&
|
|
4358
|
+
nextCh !== CharCode.lessThan &&
|
|
4359
|
+
nextCh !== CharCode.openBrace &&
|
|
4360
|
+
nextCh !== CharCode.closeBrace &&
|
|
4361
|
+
!this.#isJSXControlFlowDirectiveStart();
|
|
4362
|
+
if (
|
|
4363
|
+
startsRawText &&
|
|
4364
|
+
typeof blockEnd === 'number' &&
|
|
4365
|
+
this.start > blockEnd &&
|
|
4366
|
+
/^\s*$/.test(this.input.slice(blockEnd, this.start))
|
|
4367
|
+
) {
|
|
4368
|
+
const loc = acorn.getLineInfo(this.input, blockEnd);
|
|
4369
|
+
this.pos = blockEnd;
|
|
4370
|
+
this.start = blockEnd;
|
|
4371
|
+
this.startLoc = new acorn.Position(loc.line, loc.column);
|
|
4372
|
+
}
|
|
4236
4373
|
} else if (this.type === tt.braceR) {
|
|
4237
4374
|
// Leaving a native template body. We may still be in TSX/JSX tokenization
|
|
4238
4375
|
// context (e.g. after parsing markup), but the closing `}` is a JS token.
|
|
@@ -4259,6 +4396,18 @@ export function TSRXPlugin(config) {
|
|
|
4259
4396
|
this.start = startPos;
|
|
4260
4397
|
this.startLoc = startLoc;
|
|
4261
4398
|
this.exprAllowed = false;
|
|
4399
|
+
// A genuine `jsxTagStart` pushes `tc_expr` + `tc_oTag` in its
|
|
4400
|
+
// `updateContext`; faking the token here skips those pushes. That is
|
|
4401
|
+
// harmless for an opening tag (the next token is the tag name), but a
|
|
4402
|
+
// closing tag (`</`) immediately runs `context.length -= 2` in the
|
|
4403
|
+
// slash `updateContext`, which would underflow the context stack and
|
|
4404
|
+
// throw "Invalid array length" (e.g. `<>@if (a) { … } done</>`). Push
|
|
4405
|
+
// the two contexts a real `jsxTagStart` would have added so the closing
|
|
4406
|
+
// tag pops its own contexts instead of the enclosing template's.
|
|
4407
|
+
if (this.input.charCodeAt(this.pos) === CharCode.slash) {
|
|
4408
|
+
this.context.push(tstc.tc_expr);
|
|
4409
|
+
this.context.push(tstc.tc_oTag);
|
|
4410
|
+
}
|
|
4262
4411
|
this.next();
|
|
4263
4412
|
}
|
|
4264
4413
|
if (this.value === '/' || this.type === tt.slash) {
|
|
@@ -4274,6 +4423,9 @@ export function TSRXPlugin(config) {
|
|
|
4274
4423
|
} finally {
|
|
4275
4424
|
this.#closingNativeTemplateNode = false;
|
|
4276
4425
|
}
|
|
4426
|
+
if (this.#isDynamicJSXElementName(closingElement.name)) {
|
|
4427
|
+
/** @type {any} */ (closingElement).isDynamic = true;
|
|
4428
|
+
}
|
|
4277
4429
|
this.exprAllowed = false;
|
|
4278
4430
|
|
|
4279
4431
|
// Validate that the closing tag matches the opening tag
|
|
@@ -89,6 +89,33 @@ export function clone_jsx_name(name, source_node = name) {
|
|
|
89
89
|
return name;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Record extra source positions on a generated expression so one generated
|
|
94
|
+
* range can map back to several source ranges. Used for dynamic tags, where
|
|
95
|
+
* the generated `is={expr}` value stands in for both `<{expr}` and `</{expr}>`;
|
|
96
|
+
* segments.js turns each recorded node into an additional mapping token.
|
|
97
|
+
* @param {any} generated
|
|
98
|
+
* @param {any} source
|
|
99
|
+
* @returns {void}
|
|
100
|
+
*/
|
|
101
|
+
export function add_extra_source_mappings_from_matching_expression(generated, source) {
|
|
102
|
+
if (!generated || !source || generated.type !== source.type) return;
|
|
103
|
+
|
|
104
|
+
if (generated.type === 'Identifier' || generated.type === 'PrivateIdentifier') {
|
|
105
|
+
if (!source.loc) return;
|
|
106
|
+
generated.metadata ??= { path: [] };
|
|
107
|
+
generated.metadata.extra_source_mappings ??= [];
|
|
108
|
+
generated.metadata.extra_source_mappings.push(source);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const key of ['expression', 'object', 'property']) {
|
|
113
|
+
if (generated[key] && source[key]) {
|
|
114
|
+
add_extra_source_mappings_from_matching_expression(generated[key], source[key]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
/**
|
|
93
120
|
* @returns {AST.Literal}
|
|
94
121
|
*/
|
|
@@ -52,7 +52,14 @@ export function tsx_node_to_jsx_expression(node, in_jsx_child = false) {
|
|
|
52
52
|
(/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim() !== '',
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
children.length === 1 &&
|
|
57
|
+
children[0].type !== 'JSXText' &&
|
|
58
|
+
// Reactive-block containers (dynamic tags) must stay expression
|
|
59
|
+
// children so the host JSX compiler wraps them in a render block;
|
|
60
|
+
// unwrapping to a bare call would evaluate them once.
|
|
61
|
+
children[0].metadata?.tsrx_reactive_block !== true
|
|
62
|
+
) {
|
|
56
63
|
const only = children[0];
|
|
57
64
|
if (only.type === 'JSXExpressionContainer' && !in_jsx_child) {
|
|
58
65
|
return only.expression;
|