@tsrx/core 0.1.25 → 0.1.27
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 +153 -26
- package/src/transform/jsx/ast-builders.js +27 -0
- package/src/transform/jsx/helpers.js +8 -1
- package/src/transform/jsx/index.js +236 -249
- 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 +16 -10
- package/types/jsx-platform.d.ts +24 -5
- 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
|
@@ -55,25 +55,23 @@ const CharCode = Object.freeze({
|
|
|
55
55
|
closeBrace: 125,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'case',
|
|
76
|
-
'throw',
|
|
58
|
+
// Transparent wrappers to look through when validating a dynamic tag
|
|
59
|
+
// expression (`<{expr}>`), and syntax that disqualifies one outright.
|
|
60
|
+
const DYNAMIC_TAG_WRAPPER_TYPES = new Set([
|
|
61
|
+
'TSAsExpression',
|
|
62
|
+
'TSTypeAssertion',
|
|
63
|
+
'TSNonNullExpression',
|
|
64
|
+
'ParenthesizedExpression',
|
|
65
|
+
'ChainExpression',
|
|
66
|
+
]);
|
|
67
|
+
const DYNAMIC_TAG_DISALLOWED_TYPES = new Set([
|
|
68
|
+
'SpreadElement',
|
|
69
|
+
'ExperimentalSpreadProperty',
|
|
70
|
+
'ObjectExpression',
|
|
71
|
+
'ArrayExpression',
|
|
72
|
+
'CallExpression',
|
|
73
|
+
'NewExpression',
|
|
74
|
+
'TaggedTemplateExpression',
|
|
77
75
|
]);
|
|
78
76
|
|
|
79
77
|
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
@@ -577,6 +575,26 @@ export function TSRXPlugin(config) {
|
|
|
577
575
|
}
|
|
578
576
|
continue;
|
|
579
577
|
}
|
|
578
|
+
if (this.#isTemplateBlockCommentStart(index)) {
|
|
579
|
+
const comment_start = index;
|
|
580
|
+
const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
|
|
581
|
+
const close = this.input.indexOf('*/', index + 2);
|
|
582
|
+
const value_end = close === -1 ? this.input.length : close;
|
|
583
|
+
index = close === -1 ? this.input.length : close + 2;
|
|
584
|
+
if (this.options.onComment && comment_start >= token_end) {
|
|
585
|
+
const comment_end_loc = acorn.getLineInfo(this.input, index);
|
|
586
|
+
this.options.onComment(
|
|
587
|
+
true,
|
|
588
|
+
this.input.slice(comment_start + 2, value_end),
|
|
589
|
+
comment_start,
|
|
590
|
+
index,
|
|
591
|
+
new acorn.Position(comment_start_loc.line, comment_start_loc.column),
|
|
592
|
+
new acorn.Position(comment_end_loc.line, comment_end_loc.column),
|
|
593
|
+
/** @type {any} */ (null),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
580
598
|
const ch = this.input.charCodeAt(index);
|
|
581
599
|
if (
|
|
582
600
|
ch === CharCode.lessThan ||
|
|
@@ -953,6 +971,19 @@ export function TSRXPlugin(config) {
|
|
|
953
971
|
);
|
|
954
972
|
}
|
|
955
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Unlike `//` (which is only a comment at line-start so inline text like
|
|
976
|
+
* `https://…` stays text), `/*` starts a comment anywhere in template
|
|
977
|
+
* text, matching `jsx_readToken`.
|
|
978
|
+
* @param {number} index
|
|
979
|
+
*/
|
|
980
|
+
#isTemplateBlockCommentStart(index) {
|
|
981
|
+
return (
|
|
982
|
+
this.input.charCodeAt(index) === CharCode.slash &&
|
|
983
|
+
this.input.charCodeAt(index + 1) === CharCode.asterisk
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
956
987
|
/**
|
|
957
988
|
* @param {number} start
|
|
958
989
|
*/
|
|
@@ -965,7 +996,8 @@ export function TSRXPlugin(config) {
|
|
|
965
996
|
ch === CharCode.openBrace ||
|
|
966
997
|
ch === CharCode.closeBrace ||
|
|
967
998
|
this.#isJSXControlFlowDirectiveAt(index) ||
|
|
968
|
-
this.#isTemplateLineCommentStart(index)
|
|
999
|
+
this.#isTemplateLineCommentStart(index) ||
|
|
1000
|
+
this.#isTemplateBlockCommentStart(index)
|
|
969
1001
|
) {
|
|
970
1002
|
break;
|
|
971
1003
|
}
|
|
@@ -1163,6 +1195,7 @@ export function TSRXPlugin(config) {
|
|
|
1163
1195
|
if (next === CharCode.slash) return false;
|
|
1164
1196
|
const tagLike =
|
|
1165
1197
|
next === CharCode.greaterThan ||
|
|
1198
|
+
next === CharCode.openBrace ||
|
|
1166
1199
|
next === CharCode.at ||
|
|
1167
1200
|
next === CharCode.dollar ||
|
|
1168
1201
|
next === CharCode.underscore ||
|
|
@@ -1985,9 +2018,10 @@ export function TSRXPlugin(config) {
|
|
|
1985
2018
|
);
|
|
1986
2019
|
const closingEnd = closingStart + '</style>'.length;
|
|
1987
2020
|
const closingEndInfo = acorn.getLineInfo(this.input, closingEnd);
|
|
1988
|
-
const closingElement =
|
|
1989
|
-
|
|
1990
|
-
|
|
2021
|
+
const closingElement =
|
|
2022
|
+
/** @type {ESTreeJSX.TSRXJSXClosingElement & AST.NodeWithLocation} */ (
|
|
2023
|
+
this.startNodeAt(closingStart, closingStartLoc)
|
|
2024
|
+
);
|
|
1991
2025
|
closingElement.name = name;
|
|
1992
2026
|
this.finishNodeAt(
|
|
1993
2027
|
closingElement,
|
|
@@ -2581,10 +2615,70 @@ export function TSRXPlugin(config) {
|
|
|
2581
2615
|
} else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
|
|
2582
2616
|
// For components like <Foo.Bar>, return "Foo.Bar"
|
|
2583
2617
|
return this.getElementName(node.object) + '.' + this.getElementName(node.property);
|
|
2618
|
+
} else if (this.#isDynamicJSXElementName(node)) {
|
|
2619
|
+
// Dynamic tags (`<{Tag}>`) name by expression source. The braces keep
|
|
2620
|
+
// them from colliding with static tag names ('style', 'head', ...) and
|
|
2621
|
+
// read as source syntax in error messages (`</{Tag}>`).
|
|
2622
|
+
const expression = /** @type {AST.Expression} */ (/** @type {any} */ (node).expression);
|
|
2623
|
+
return `{${this.input.slice(expression.start, expression.end).trim()}}`;
|
|
2584
2624
|
}
|
|
2585
2625
|
return null;
|
|
2586
2626
|
}
|
|
2587
2627
|
|
|
2628
|
+
/**
|
|
2629
|
+
* @param {any} name
|
|
2630
|
+
* @returns {boolean}
|
|
2631
|
+
*/
|
|
2632
|
+
#isDynamicJSXElementName(name) {
|
|
2633
|
+
return !!(name && name.type === 'JSXExpressionContainer' && name.isDynamic === true);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* Dynamic tag expressions must be able to resolve to an element name:
|
|
2638
|
+
* an identifier, member access, static string, or a runtime expression
|
|
2639
|
+
* composed of those. Constructed values (calls, spreads, concatenation,
|
|
2640
|
+
* interpolation, object/array literals) and static non-string literals
|
|
2641
|
+
* can never be valid tag names.
|
|
2642
|
+
* @param {any} expression
|
|
2643
|
+
* @returns {boolean}
|
|
2644
|
+
*/
|
|
2645
|
+
#isValidDynamicTagExpression(expression) {
|
|
2646
|
+
let node = expression;
|
|
2647
|
+
while (node && DYNAMIC_TAG_WRAPPER_TYPES.has(node.type)) {
|
|
2648
|
+
node = node.expression;
|
|
2649
|
+
}
|
|
2650
|
+
if (!node || node.type?.startsWith?.('JSX')) return false;
|
|
2651
|
+
if (node.type === 'Identifier') return node.name !== 'undefined';
|
|
2652
|
+
if (node.type === 'Literal') return typeof node.value === 'string';
|
|
2653
|
+
if (node.type === 'UnaryExpression' && node.operator === 'void') return false;
|
|
2654
|
+
return !this.#containsDisallowedDynamicTagSyntax(node);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/**
|
|
2658
|
+
* @param {any} node
|
|
2659
|
+
* @param {Set<any>} [seen]
|
|
2660
|
+
* @returns {boolean}
|
|
2661
|
+
*/
|
|
2662
|
+
#containsDisallowedDynamicTagSyntax(node, seen = new Set()) {
|
|
2663
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return false;
|
|
2664
|
+
seen.add(node);
|
|
2665
|
+
if (Array.isArray(node)) {
|
|
2666
|
+
return node.some((child) => this.#containsDisallowedDynamicTagSyntax(child, seen));
|
|
2667
|
+
}
|
|
2668
|
+
if (
|
|
2669
|
+
DYNAMIC_TAG_DISALLOWED_TYPES.has(node.type) ||
|
|
2670
|
+
(node.type === 'TemplateLiteral' && node.expressions.length > 0) ||
|
|
2671
|
+
(node.type === 'BinaryExpression' && node.operator === '+')
|
|
2672
|
+
) {
|
|
2673
|
+
return true;
|
|
2674
|
+
}
|
|
2675
|
+
for (const key of Object.keys(node)) {
|
|
2676
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
2677
|
+
if (this.#containsDisallowedDynamicTagSyntax(node[key], seen)) return true;
|
|
2678
|
+
}
|
|
2679
|
+
return false;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2588
2682
|
/**
|
|
2589
2683
|
* `<T,>(x: T) => x` and `<T>(x: T): T => x` should parse as generic
|
|
2590
2684
|
* arrow functions, not JSX elements. acorn-typescript's `readToken`
|
|
@@ -2639,6 +2733,7 @@ export function TSRXPlugin(config) {
|
|
|
2639
2733
|
const isTagLikeAfterLt =
|
|
2640
2734
|
next === CharCode.slash ||
|
|
2641
2735
|
next === CharCode.greaterThan ||
|
|
2736
|
+
next === CharCode.openBrace ||
|
|
2642
2737
|
next === CharCode.at ||
|
|
2643
2738
|
next === CharCode.dollar ||
|
|
2644
2739
|
next === CharCode.underscore ||
|
|
@@ -2759,6 +2854,7 @@ export function TSRXPlugin(config) {
|
|
|
2759
2854
|
!isWhitespaceAfterLt &&
|
|
2760
2855
|
(nextChar === CharCode.slash ||
|
|
2761
2856
|
nextChar === CharCode.greaterThan ||
|
|
2857
|
+
nextChar === CharCode.openBrace ||
|
|
2762
2858
|
nextChar === CharCode.at ||
|
|
2763
2859
|
nextChar === CharCode.dollar ||
|
|
2764
2860
|
nextChar === CharCode.underscore ||
|
|
@@ -3444,6 +3540,18 @@ export function TSRXPlugin(config) {
|
|
|
3444
3540
|
return this.finishNode(node, 'JSXIdentifier');
|
|
3445
3541
|
}
|
|
3446
3542
|
|
|
3543
|
+
#parseJSXDynamicElementName() {
|
|
3544
|
+
const container = this.jsx_parseExpressionContainer();
|
|
3545
|
+
container.isDynamic = true;
|
|
3546
|
+
if (!this.#isValidDynamicTagExpression(container.expression)) {
|
|
3547
|
+
this.raise(
|
|
3548
|
+
/** @type {number} */ (container.expression?.start ?? container.start),
|
|
3549
|
+
'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.',
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
return container;
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3447
3555
|
/**
|
|
3448
3556
|
* @type {Parse.Parser['jsx_parseElementName']}
|
|
3449
3557
|
*/
|
|
@@ -3452,6 +3560,10 @@ export function TSRXPlugin(config) {
|
|
|
3452
3560
|
return '';
|
|
3453
3561
|
}
|
|
3454
3562
|
|
|
3563
|
+
if (this.type === tt.braceL) {
|
|
3564
|
+
return this.#parseJSXDynamicElementName();
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3455
3567
|
let node = this.jsx_parseNamespacedName();
|
|
3456
3568
|
|
|
3457
3569
|
if (node.type === 'JSXNamespacedName') {
|
|
@@ -3983,7 +4095,10 @@ export function TSRXPlugin(config) {
|
|
|
3983
4095
|
);
|
|
3984
4096
|
node.attributes = [];
|
|
3985
4097
|
const nodeName = this.jsx_parseElementName();
|
|
3986
|
-
if (nodeName) node.name = nodeName;
|
|
4098
|
+
if (nodeName) node.name = /** @type {any} */ (nodeName);
|
|
4099
|
+
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4100
|
+
/** @type {any} */ (node).isDynamic = true;
|
|
4101
|
+
}
|
|
3987
4102
|
if (this.match(tt.relational) || this.match(tt.bitShift)) {
|
|
3988
4103
|
const typeArguments = /** @type {any} */ (this).tsTryParseAndCatch(() =>
|
|
3989
4104
|
/** @type {any} */ (this).tsParseTypeArgumentsInExpression(),
|
|
@@ -4003,6 +4118,9 @@ export function TSRXPlugin(config) {
|
|
|
4003
4118
|
this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
|
|
4004
4119
|
/** @type {any} */ (opening_template_node).openingElement = node;
|
|
4005
4120
|
/** @type {any} */ (opening_template_node).closingElement = null;
|
|
4121
|
+
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
4122
|
+
/** @type {any} */ (opening_template_node).isDynamic = true;
|
|
4123
|
+
}
|
|
4006
4124
|
} else {
|
|
4007
4125
|
/** @type {any} */ (opening_template_node).type = 'JSXFragment';
|
|
4008
4126
|
/** @type {any} */ (opening_template_node).openingFragment =
|
|
@@ -4076,6 +4194,7 @@ export function TSRXPlugin(config) {
|
|
|
4076
4194
|
this.#openingNativeTemplateNode = previous_opening_native_template_node;
|
|
4077
4195
|
}
|
|
4078
4196
|
const tag_name = open.name ? this.getElementName(open.name) : null;
|
|
4197
|
+
const is_dynamic = this.#isDynamicJSXElementName(open.name);
|
|
4079
4198
|
const is_style = tag_name === 'style';
|
|
4080
4199
|
const inside_head = this.#path.findLast((n) => this.#isNativeElementNamed(n, 'head'));
|
|
4081
4200
|
|
|
@@ -4103,12 +4222,17 @@ export function TSRXPlugin(config) {
|
|
|
4103
4222
|
} else {
|
|
4104
4223
|
if (is_style) {
|
|
4105
4224
|
/** @type {AST.JSXStyleElement} */ (node).type = 'JSXStyleElement';
|
|
4106
|
-
/** @type {AST.JSXStyleElement} */ (node).openingElement =
|
|
4107
|
-
|
|
4225
|
+
/** @type {AST.JSXStyleElement} */ (node).openingElement =
|
|
4226
|
+
/** @type {AST.JSXStyleElement['openingElement']} */ (open);
|
|
4227
|
+
/** @type {AST.JSXStyleElement} */ (node).closingElement =
|
|
4228
|
+
/** @type {AST.JSXStyleElement['closingElement']} */ (null);
|
|
4108
4229
|
} else {
|
|
4109
4230
|
/** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
|
|
4110
4231
|
/** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
|
|
4111
4232
|
/** @type {ESTreeJSX.JSXElement} */ (node).closingElement = null;
|
|
4233
|
+
if (is_dynamic) {
|
|
4234
|
+
/** @type {any} */ (node).isDynamic = true;
|
|
4235
|
+
}
|
|
4112
4236
|
}
|
|
4113
4237
|
}
|
|
4114
4238
|
|
|
@@ -4315,6 +4439,9 @@ export function TSRXPlugin(config) {
|
|
|
4315
4439
|
} finally {
|
|
4316
4440
|
this.#closingNativeTemplateNode = false;
|
|
4317
4441
|
}
|
|
4442
|
+
if (this.#isDynamicJSXElementName(closingElement.name)) {
|
|
4443
|
+
/** @type {any} */ (closingElement).isDynamic = true;
|
|
4444
|
+
}
|
|
4318
4445
|
this.exprAllowed = false;
|
|
4319
4446
|
|
|
4320
4447
|
// 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;
|
|
@@ -7,7 +7,6 @@ import { print } from 'esrap';
|
|
|
7
7
|
import { error } from '../../errors.js';
|
|
8
8
|
import { analyze_css } from '../../analyze/css-analyze.js';
|
|
9
9
|
import { prune_css } from '../../analyze/prune.js';
|
|
10
|
-
import { create_scopes, ScopeRoot } from '../../scope.js';
|
|
11
10
|
import {
|
|
12
11
|
in_jsx_child_context,
|
|
13
12
|
set_node_path_metadata,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
tsx_with_ts_locations,
|
|
16
15
|
} from './helpers.js';
|
|
17
16
|
import {
|
|
17
|
+
add_extra_source_mappings_from_matching_expression,
|
|
18
18
|
clone_expression_node,
|
|
19
19
|
clone_identifier,
|
|
20
20
|
clone_jsx_name,
|
|
@@ -69,6 +69,8 @@ const TSRX_IF_RETURN_ERROR =
|
|
|
69
69
|
const TSRX_IF_BREAK_ERROR = 'Break statements are not allowed inside TSRX template @if blocks.';
|
|
70
70
|
const TSRX_IF_CONTINUE_ERROR =
|
|
71
71
|
'Continue statements are not allowed inside TSRX template @if blocks. Filter before rendering or use conditional output instead.';
|
|
72
|
+
const DYNAMIC_IMPORT_LOCAL = 'TsrxDynamic';
|
|
73
|
+
const DYNAMIC_FACTORY_LOCAL = '_tsrx_dynamic';
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
76
|
* @param {AST.Node} node
|
|
@@ -216,15 +218,32 @@ function wrap_in_native_tsrx_fragment(node) {
|
|
|
216
218
|
* (`const x = @switch (…) { … }`, `x = @switch (…) { … }`), or a call/`new`
|
|
217
219
|
* argument (`render(@if (…) { … })`) — in a native TSRX fragment.
|
|
218
220
|
* @param {any} node
|
|
221
|
+
* @param {TransformContext | null} lower_dynamic_context
|
|
219
222
|
* @param {Set<any>} [seen]
|
|
220
223
|
* @returns {void}
|
|
221
224
|
*/
|
|
222
|
-
function wrap_control_flow_expression_values(node, seen = new Set()) {
|
|
225
|
+
function wrap_control_flow_expression_values(node, lower_dynamic_context, seen = new Set()) {
|
|
223
226
|
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
224
227
|
seen.add(node);
|
|
225
228
|
|
|
229
|
+
// Dynamic tags on factory platforms must lower before control-flow
|
|
230
|
+
// conversion and static hoisting run. Production output needs the scoped
|
|
231
|
+
// `const TsrxDynamic_N = ...` binding declared in the scope that owns the
|
|
232
|
+
// tag expression (e.g. a `@for` loop variable); type-only output needs
|
|
233
|
+
// `<TsrxDynamic is={expr}>` in place before a reference-free tree (e.g.
|
|
234
|
+
// `<{'div'}>`) is hoisted to a module-level static const while still
|
|
235
|
+
// carrying the raw dynamic tag. Alias lowerings return a replacement
|
|
236
|
+
// fragment, which is swapped into the child's position here.
|
|
237
|
+
const lower_child = (/** @type {any} */ child) => {
|
|
238
|
+
if (!lower_dynamic_context || child?.type !== 'JSXElement') return child;
|
|
239
|
+
return lower_dynamic_jsx_element(child, lower_dynamic_context) ?? child;
|
|
240
|
+
};
|
|
241
|
+
|
|
226
242
|
if (Array.isArray(node)) {
|
|
227
|
-
for (
|
|
243
|
+
for (let i = 0; i < node.length; i++) {
|
|
244
|
+
node[i] = lower_child(node[i]);
|
|
245
|
+
wrap_control_flow_expression_values(node[i], lower_dynamic_context, seen);
|
|
246
|
+
}
|
|
228
247
|
return;
|
|
229
248
|
}
|
|
230
249
|
|
|
@@ -256,7 +275,8 @@ function wrap_control_flow_expression_values(node, seen = new Set()) {
|
|
|
256
275
|
|
|
257
276
|
for (const key of Object.keys(node)) {
|
|
258
277
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
259
|
-
|
|
278
|
+
node[key] = lower_child(node[key]);
|
|
279
|
+
wrap_control_flow_expression_values(node[key], lower_dynamic_context, seen);
|
|
260
280
|
}
|
|
261
281
|
}
|
|
262
282
|
|
|
@@ -300,6 +320,8 @@ export function createJsxTransform(platform) {
|
|
|
300
320
|
needs_normalize_spread_props: false,
|
|
301
321
|
needs_normalize_spread_props_for_ref_attr: false,
|
|
302
322
|
needs_fragment: false,
|
|
323
|
+
needs_dynamic_element: false,
|
|
324
|
+
needs_dynamic_factory: false,
|
|
303
325
|
needs_for_of_iterable: false,
|
|
304
326
|
needs_iteration_value_type: false,
|
|
305
327
|
stylesheets,
|
|
@@ -310,7 +332,6 @@ export function createJsxTransform(platform) {
|
|
|
310
332
|
hook_helpers_enabled: false,
|
|
311
333
|
available_bindings: new Map(),
|
|
312
334
|
lazy_next_id: 0,
|
|
313
|
-
runtime_dynamic_scopes: null,
|
|
314
335
|
filename: filename ?? null,
|
|
315
336
|
source,
|
|
316
337
|
collect,
|
|
@@ -323,10 +344,9 @@ export function createJsxTransform(platform) {
|
|
|
323
344
|
};
|
|
324
345
|
|
|
325
346
|
expand_child_code_blocks(/** @type {any} */ (ast));
|
|
326
|
-
wrap_control_flow_expression_values(
|
|
327
|
-
transform_context.runtime_dynamic_scopes = create_runtime_dynamic_scopes(
|
|
347
|
+
wrap_control_flow_expression_values(
|
|
328
348
|
/** @type {any} */ (ast),
|
|
329
|
-
transform_context,
|
|
349
|
+
platform.imports.dynamicFactory ? transform_context : null,
|
|
330
350
|
);
|
|
331
351
|
|
|
332
352
|
if (!transform_context.typeOnly) {
|
|
@@ -363,7 +383,15 @@ export function createJsxTransform(platform) {
|
|
|
363
383
|
return /** @type {any} */ (wrap_jsx_setup_declarations(expression, in_jsx_child));
|
|
364
384
|
},
|
|
365
385
|
|
|
366
|
-
JSXElement(node, { next, path, state }) {
|
|
386
|
+
JSXElement(node, { next, path, state, visit }) {
|
|
387
|
+
const lowered = lower_dynamic_jsx_element(node, state);
|
|
388
|
+
if (lowered) {
|
|
389
|
+
// Alias lowerings replace the element with a fragment; factory
|
|
390
|
+
// platforms normally lower in the pre-walk pass, so this only
|
|
391
|
+
// covers elements introduced after it.
|
|
392
|
+
return /** @type {any} */ (visit(lowered, state));
|
|
393
|
+
}
|
|
394
|
+
|
|
367
395
|
if (!node.metadata?.native_tsrx) {
|
|
368
396
|
return next() ?? node;
|
|
369
397
|
}
|
|
@@ -462,6 +490,7 @@ export function createJsxTransform(platform) {
|
|
|
462
490
|
transformed_program.body.unshift(...type_only_style_anchors);
|
|
463
491
|
}
|
|
464
492
|
const expanded = expand_component_helpers(transformed_program);
|
|
493
|
+
inject_dynamic_import(expanded, transform_context);
|
|
465
494
|
if (platform.hooks?.injectImports) {
|
|
466
495
|
platform.hooks.injectImports(expanded, transform_context, suspense_source);
|
|
467
496
|
} else {
|
|
@@ -497,6 +526,179 @@ export function createJsxTransform(platform) {
|
|
|
497
526
|
return transform;
|
|
498
527
|
}
|
|
499
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Lower a single parser-native dynamic tag (`<{expr}>`) into the target runtime
|
|
531
|
+
* `<Dynamic is={expr}>` helper shape while the existing JSXElement walker is
|
|
532
|
+
* already visiting it. The dynamic name container travels by reference through
|
|
533
|
+
* element rebuilds, so checking it covers rebuilt elements too; once lowered,
|
|
534
|
+
* the name is a plain `JSXIdentifier` and the element is skipped on re-visits.
|
|
535
|
+
*
|
|
536
|
+
* The parsed element is never mutated: every lowering builds a fresh
|
|
537
|
+
* replacement node (an element, or a fragment for the alias lowering) that
|
|
538
|
+
* the caller must put in the original element's position.
|
|
539
|
+
*
|
|
540
|
+
* @param {any} node
|
|
541
|
+
* @param {TransformContext} transform_context
|
|
542
|
+
* @returns {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment | undefined}
|
|
543
|
+
*/
|
|
544
|
+
function lower_dynamic_jsx_element(node, transform_context) {
|
|
545
|
+
const dynamic_name = node.openingElement?.name;
|
|
546
|
+
if (dynamic_name?.type !== 'JSXExpressionContainer' || dynamic_name.isDynamic !== true) return;
|
|
547
|
+
|
|
548
|
+
// Type-only output always uses the `<TsrxDynamic is={expr}>` component
|
|
549
|
+
// shape; production output prefers the platform's runtime factory when one
|
|
550
|
+
// is configured (e.g. Solid's `dynamic`).
|
|
551
|
+
const factory = transform_context.typeOnly
|
|
552
|
+
? undefined
|
|
553
|
+
: transform_context.platform.imports.dynamicFactory;
|
|
554
|
+
if (!factory && !transform_context.platform.imports.dynamic) return;
|
|
555
|
+
|
|
556
|
+
const dynamic_expression = dynamic_name.expression;
|
|
557
|
+
if (!dynamic_expression) return;
|
|
558
|
+
const generated_expression = clone_expression_node(dynamic_expression);
|
|
559
|
+
if (node.closingElement?.name?.expression) {
|
|
560
|
+
// One generated expression stands in for both tags; record the closing
|
|
561
|
+
// tag's positions so editor features keep working on `</{expr}>`.
|
|
562
|
+
add_extra_source_mappings_from_matching_expression(
|
|
563
|
+
generated_expression,
|
|
564
|
+
clone_expression_node(node.closingElement.name.expression),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Rebuild the element as an ordinary component reference named `name_id`,
|
|
570
|
+
* carrying the original attributes (after any `extra_attributes`) and
|
|
571
|
+
* children over by reference.
|
|
572
|
+
*
|
|
573
|
+
* @param {ESTreeJSX.JSXIdentifier} name_id
|
|
574
|
+
* @param {ESTreeJSX.JSXAttribute[]} [extra_attributes]
|
|
575
|
+
* @returns {ESTreeJSX.JSXElement}
|
|
576
|
+
*/
|
|
577
|
+
const rebuild_element = (name_id, extra_attributes = []) => {
|
|
578
|
+
const element = b.jsx_element_fresh(
|
|
579
|
+
b.jsx_opening_element(
|
|
580
|
+
name_id,
|
|
581
|
+
[...extra_attributes, ...(node.openingElement.attributes || [])],
|
|
582
|
+
node.openingElement.selfClosing,
|
|
583
|
+
node.openingElement.typeArguments,
|
|
584
|
+
node.openingElement,
|
|
585
|
+
),
|
|
586
|
+
node.closingElement
|
|
587
|
+
? b.jsx_closing_element(b.jsx_id(name_id.name), node.closingElement)
|
|
588
|
+
: null,
|
|
589
|
+
node.children,
|
|
590
|
+
node,
|
|
591
|
+
);
|
|
592
|
+
element.metadata = { ...(node.metadata || {}), path: [] };
|
|
593
|
+
return element;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Scoped-CSS passes treat lowered dynamic tags like the imported `Dynamic`
|
|
598
|
+
* helper: type selectors survive pruning and the scope hash lands on the
|
|
599
|
+
* element's class.
|
|
600
|
+
*
|
|
601
|
+
* @param {ESTreeJSX.JSXElement} element
|
|
602
|
+
* @returns {ESTreeJSX.JSXElement}
|
|
603
|
+
*/
|
|
604
|
+
const mark_dynamic_element = (element) => {
|
|
605
|
+
element.metadata.dynamicElement = true;
|
|
606
|
+
return element;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
if (factory) {
|
|
610
|
+
// Bind the tag expression to a scoped component const and reference it
|
|
611
|
+
// like an ordinary component.
|
|
612
|
+
transform_context.local_statement_component_index += 1;
|
|
613
|
+
const local = `${DYNAMIC_IMPORT_LOCAL}_${transform_context.local_statement_component_index}`;
|
|
614
|
+
const local_id = b.jsx_id(local);
|
|
615
|
+
transform_context.needs_dynamic_factory = true;
|
|
616
|
+
|
|
617
|
+
if (factory.renderBlock) {
|
|
618
|
+
// Import-free alias inside a reactive render block (Vue): the const
|
|
619
|
+
// is a plain snapshot and Vapor never re-runs setup, so the whole
|
|
620
|
+
// element is rebuilt in a native fragment whose expression-container
|
|
621
|
+
// child holds
|
|
622
|
+
// `(() => { const TsrxDynamic_1 = expr; return <TsrxDynamic_1 ...>; })()`
|
|
623
|
+
// — vue-jsx-vapor compiles expression children into `createNodes(...)`
|
|
624
|
+
// render blocks, which re-run the IIFE when the tag expression
|
|
625
|
+
// changes. The container is marked so downstream lone-child
|
|
626
|
+
// collapsing keeps it in expression-child position instead of
|
|
627
|
+
// unwrapping to a bare call.
|
|
628
|
+
const element = mark_dynamic_element(rebuild_element(local_id));
|
|
629
|
+
const wrapper = b.arrow(
|
|
630
|
+
[],
|
|
631
|
+
b.block([b.const(b.id(local), generated_expression), b.return(element)], node),
|
|
632
|
+
);
|
|
633
|
+
// Lets scoped-CSS collection descend into this generated closure;
|
|
634
|
+
// user function boundaries are otherwise skipped.
|
|
635
|
+
wrapper.metadata = /** @type {any} */ ({
|
|
636
|
+
...(wrapper.metadata || { path: [] }),
|
|
637
|
+
tsrx_dynamic_wrapper: true,
|
|
638
|
+
});
|
|
639
|
+
const container = to_jsx_expression_container(b.call(wrapper), element);
|
|
640
|
+
container.metadata = /** @type {any} */ ({
|
|
641
|
+
...(container.metadata || { path: [] }),
|
|
642
|
+
tsrx_reactive_block: true,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return set_loc(wrap_in_native_tsrx_fragment(container), node);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Statement placement: `const TsrxDynamic_1 = ...;` next to the
|
|
649
|
+
// template. With a factory, the thunk keeps the tag reactive (Solid:
|
|
650
|
+
// `_tsrx_dynamic(() => expr)`); without one, the plain alias is
|
|
651
|
+
// re-evaluated by the host's render cycle (React/Preact re-run the
|
|
652
|
+
// component body). The declaration rides on the name node's metadata:
|
|
653
|
+
// element rebuilds clone names with a shared metadata reference, so
|
|
654
|
+
// setup extraction still finds it afterwards.
|
|
655
|
+
add_jsx_setup_declaration(
|
|
656
|
+
local_id,
|
|
657
|
+
b.const(
|
|
658
|
+
b.id(local),
|
|
659
|
+
factory.name
|
|
660
|
+
? b.call(b.id(DYNAMIC_FACTORY_LOCAL), b.arrow([], generated_expression))
|
|
661
|
+
: generated_expression,
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
return mark_dynamic_element(rebuild_element(local_id));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
transform_context.needs_dynamic_element = true;
|
|
668
|
+
return mark_dynamic_element(
|
|
669
|
+
rebuild_element(b.jsx_id(DYNAMIC_IMPORT_LOCAL), [
|
|
670
|
+
b.jsx_attribute(
|
|
671
|
+
b.jsx_id('is'),
|
|
672
|
+
b.jsx_expression_container(generated_expression, dynamic_name),
|
|
673
|
+
false,
|
|
674
|
+
dynamic_name,
|
|
675
|
+
),
|
|
676
|
+
]),
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* @param {AST.Program} program
|
|
682
|
+
* @param {TransformContext} transform_context
|
|
683
|
+
* @returns {void}
|
|
684
|
+
*/
|
|
685
|
+
function inject_dynamic_import(program, transform_context) {
|
|
686
|
+
const factory = transform_context.platform.imports.dynamicFactory;
|
|
687
|
+
if (transform_context.needs_dynamic_factory && factory?.name && factory.source) {
|
|
688
|
+
program.body.unshift(
|
|
689
|
+
b.import_declaration(
|
|
690
|
+
[b.import_specifier(factory.name, DYNAMIC_FACTORY_LOCAL)],
|
|
691
|
+
factory.source,
|
|
692
|
+
),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const source = transform_context.platform.imports.dynamic;
|
|
696
|
+
if (!transform_context.needs_dynamic_element || !source) return;
|
|
697
|
+
program.body.unshift(
|
|
698
|
+
b.import_declaration([b.import_specifier('Dynamic', DYNAMIC_IMPORT_LOCAL)], source),
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
500
702
|
/**
|
|
501
703
|
* Attach selector-location metadata used by editor definitions/hover before
|
|
502
704
|
* the shared scoping pass mutates class attributes with the component hash.
|
|
@@ -563,15 +765,18 @@ function collect_css_prunable_elements(value, elements = [], transform_context =
|
|
|
563
765
|
}
|
|
564
766
|
|
|
565
767
|
if (
|
|
566
|
-
value.type === 'FunctionDeclaration' ||
|
|
567
|
-
|
|
568
|
-
|
|
768
|
+
(value.type === 'FunctionDeclaration' ||
|
|
769
|
+
value.type === 'FunctionExpression' ||
|
|
770
|
+
value.type === 'ArrowFunctionExpression') &&
|
|
771
|
+
// Generated dynamic-tag wrappers are render-block closures, not user
|
|
772
|
+
// component boundaries — the element inside still belongs to this
|
|
773
|
+
// component's scoped CSS.
|
|
774
|
+
value.metadata?.tsrx_dynamic_wrapper !== true
|
|
569
775
|
) {
|
|
570
776
|
return elements;
|
|
571
777
|
}
|
|
572
778
|
|
|
573
779
|
if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
|
|
574
|
-
mark_runtime_dynamic_element(value, transform_context);
|
|
575
780
|
if (!is_style_element(value)) {
|
|
576
781
|
elements.push(value);
|
|
577
782
|
}
|
|
@@ -587,241 +792,6 @@ function collect_css_prunable_elements(value, elements = [], transform_context =
|
|
|
587
792
|
return elements;
|
|
588
793
|
}
|
|
589
794
|
|
|
590
|
-
/**
|
|
591
|
-
* @param {AST.Program} ast
|
|
592
|
-
* @param {TransformContext} transform_context
|
|
593
|
-
* @returns {Map<any, any> | null}
|
|
594
|
-
*/
|
|
595
|
-
function create_runtime_dynamic_scopes(ast, transform_context) {
|
|
596
|
-
const dynamic_source = transform_context.platform.imports.dynamic;
|
|
597
|
-
if (!dynamic_source) {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
if (!has_runtime_dynamic_import(ast, dynamic_source)) {
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const { scopes } = create_scopes(ast, new ScopeRoot(), null, {
|
|
605
|
-
collect: true,
|
|
606
|
-
errors: [],
|
|
607
|
-
filename: transform_context.filename ?? '',
|
|
608
|
-
comments: transform_context.comments,
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
return scopes;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* @param {any} node
|
|
616
|
-
* @param {TransformContext | null} transform_context
|
|
617
|
-
* @returns {void}
|
|
618
|
-
*/
|
|
619
|
-
function mark_runtime_dynamic_element(node, transform_context) {
|
|
620
|
-
const dynamic_source = transform_context?.platform.imports.dynamic;
|
|
621
|
-
const scopes = transform_context?.runtime_dynamic_scopes;
|
|
622
|
-
if (
|
|
623
|
-
!dynamic_source ||
|
|
624
|
-
!scopes ||
|
|
625
|
-
node.metadata?.runtime_dynamic_element === true ||
|
|
626
|
-
!has_jsx_attribute(node, 'is') ||
|
|
627
|
-
!is_runtime_dynamic_jsx_name(node.openingElement?.name, scopes.get(node), dynamic_source)
|
|
628
|
-
) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
node.metadata.runtime_dynamic_element = true;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* @param {AST.Program} ast
|
|
637
|
-
* @param {string} dynamic_source
|
|
638
|
-
* @returns {boolean}
|
|
639
|
-
*/
|
|
640
|
-
function has_runtime_dynamic_import(ast, dynamic_source) {
|
|
641
|
-
return ast.body.some(
|
|
642
|
-
(/** @type {any} */ node) =>
|
|
643
|
-
node.type === 'ImportDeclaration' &&
|
|
644
|
-
node.importKind !== 'type' &&
|
|
645
|
-
node.source?.type === 'Literal' &&
|
|
646
|
-
node.source.value === dynamic_source &&
|
|
647
|
-
node.specifiers.some(
|
|
648
|
-
(/** @type {any} */ specifier) =>
|
|
649
|
-
is_runtime_dynamic_import_specifier(specifier, 'component') ||
|
|
650
|
-
is_runtime_dynamic_import_specifier(specifier, 'namespace'),
|
|
651
|
-
),
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* @param {any} node
|
|
657
|
-
* @param {string} name
|
|
658
|
-
* @returns {boolean}
|
|
659
|
-
*/
|
|
660
|
-
function has_jsx_attribute(node, name) {
|
|
661
|
-
return (node.openingElement?.attributes ?? []).some(
|
|
662
|
-
(/** @type {any} */ attr) =>
|
|
663
|
-
attr.type === 'JSXAttribute' &&
|
|
664
|
-
attr.name?.type === 'JSXIdentifier' &&
|
|
665
|
-
attr.name.name === name,
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* @param {any} name
|
|
671
|
-
* @param {any} scope
|
|
672
|
-
* @param {string} dynamic_source
|
|
673
|
-
* @returns {boolean}
|
|
674
|
-
*/
|
|
675
|
-
function is_runtime_dynamic_jsx_name(name, scope, dynamic_source) {
|
|
676
|
-
if (!scope || !name) {
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
if (name.type === 'JSXIdentifier') {
|
|
681
|
-
return is_runtime_dynamic_binding(scope.get(name.name), dynamic_source, 'component', new Set());
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
if (
|
|
685
|
-
name.type === 'JSXMemberExpression' &&
|
|
686
|
-
name.object?.type === 'JSXIdentifier' &&
|
|
687
|
-
name.property?.type === 'JSXIdentifier' &&
|
|
688
|
-
name.property.name === 'Dynamic'
|
|
689
|
-
) {
|
|
690
|
-
return is_runtime_dynamic_binding(
|
|
691
|
-
scope.get(name.object.name),
|
|
692
|
-
dynamic_source,
|
|
693
|
-
'namespace',
|
|
694
|
-
new Set(),
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return false;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* @param {any} binding
|
|
703
|
-
* @param {string} dynamic_source
|
|
704
|
-
* @param {'component' | 'namespace'} kind
|
|
705
|
-
* @param {Set<any>} seen
|
|
706
|
-
* @returns {boolean}
|
|
707
|
-
*/
|
|
708
|
-
function is_runtime_dynamic_binding(binding, dynamic_source, kind, seen) {
|
|
709
|
-
if (!binding || seen.has(binding)) {
|
|
710
|
-
return false;
|
|
711
|
-
}
|
|
712
|
-
seen.add(binding);
|
|
713
|
-
|
|
714
|
-
if (is_runtime_dynamic_import_binding(binding, dynamic_source, kind)) {
|
|
715
|
-
return true;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (binding.reassigned) {
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const initial = unwrap_reference_expression(binding.initial);
|
|
723
|
-
if (!initial) {
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (initial.type === 'Identifier') {
|
|
728
|
-
return is_runtime_dynamic_binding(binding.scope.get(initial.name), dynamic_source, kind, seen);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (
|
|
732
|
-
kind === 'component' &&
|
|
733
|
-
initial.type === 'MemberExpression' &&
|
|
734
|
-
!initial.computed &&
|
|
735
|
-
initial.object?.type === 'Identifier' &&
|
|
736
|
-
initial.property?.type === 'Identifier' &&
|
|
737
|
-
initial.property.name === 'Dynamic'
|
|
738
|
-
) {
|
|
739
|
-
return is_runtime_dynamic_binding(
|
|
740
|
-
binding.scope.get(initial.object.name),
|
|
741
|
-
dynamic_source,
|
|
742
|
-
'namespace',
|
|
743
|
-
new Set(),
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* @param {any} binding
|
|
752
|
-
* @param {string} dynamic_source
|
|
753
|
-
* @param {'component' | 'namespace'} kind
|
|
754
|
-
* @returns {boolean}
|
|
755
|
-
*/
|
|
756
|
-
function is_runtime_dynamic_import_binding(binding, dynamic_source, kind) {
|
|
757
|
-
const declaration = binding?.initial;
|
|
758
|
-
if (
|
|
759
|
-
binding?.declaration_kind !== 'import' ||
|
|
760
|
-
declaration?.type !== 'ImportDeclaration' ||
|
|
761
|
-
declaration.importKind === 'type' ||
|
|
762
|
-
declaration.source?.type !== 'Literal' ||
|
|
763
|
-
declaration.source.value !== dynamic_source
|
|
764
|
-
) {
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return declaration.specifiers.some(
|
|
769
|
-
(/** @type {any} */ specifier) =>
|
|
770
|
-
specifier.local?.name === binding.node?.name &&
|
|
771
|
-
is_runtime_dynamic_import_specifier(specifier, kind),
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* @param {any} specifier
|
|
777
|
-
* @param {'component' | 'namespace'} kind
|
|
778
|
-
* @returns {boolean}
|
|
779
|
-
*/
|
|
780
|
-
function is_runtime_dynamic_import_specifier(specifier, kind) {
|
|
781
|
-
if (kind === 'namespace') {
|
|
782
|
-
return specifier.type === 'ImportNamespaceSpecifier';
|
|
783
|
-
}
|
|
784
|
-
return (
|
|
785
|
-
specifier.type === 'ImportSpecifier' &&
|
|
786
|
-
specifier.importKind !== 'type' &&
|
|
787
|
-
get_imported_name(specifier) === 'Dynamic'
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* @param {any} specifier
|
|
793
|
-
* @returns {string | null}
|
|
794
|
-
*/
|
|
795
|
-
function get_imported_name(specifier) {
|
|
796
|
-
const imported = specifier.imported;
|
|
797
|
-
if (imported?.type === 'Identifier') {
|
|
798
|
-
return imported.name;
|
|
799
|
-
}
|
|
800
|
-
if (imported?.type === 'Literal') {
|
|
801
|
-
return String(imported.value);
|
|
802
|
-
}
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* @param {any} expression
|
|
808
|
-
* @returns {any}
|
|
809
|
-
*/
|
|
810
|
-
function unwrap_reference_expression(expression) {
|
|
811
|
-
let node = expression;
|
|
812
|
-
while (
|
|
813
|
-
node &&
|
|
814
|
-
(node.type === 'TSAsExpression' ||
|
|
815
|
-
node.type === 'TSTypeAssertion' ||
|
|
816
|
-
node.type === 'TSNonNullExpression' ||
|
|
817
|
-
node.type === 'ParenthesizedExpression' ||
|
|
818
|
-
node.type === 'ChainExpression')
|
|
819
|
-
) {
|
|
820
|
-
node = node.expression;
|
|
821
|
-
}
|
|
822
|
-
return node;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
795
|
/**
|
|
826
796
|
* @param {any[]} body_nodes
|
|
827
797
|
* @param {TransformContext} transform_context
|
|
@@ -3262,12 +3232,23 @@ function to_jsx_element(
|
|
|
3262
3232
|
? null
|
|
3263
3233
|
: set_loc(
|
|
3264
3234
|
b.jsx_closing_element(
|
|
3265
|
-
|
|
3235
|
+
// Clone from the actual closing name when there is one: a dynamic
|
|
3236
|
+
// tag's closing expression (`</{Tag}>`) has its own source positions,
|
|
3237
|
+
// which editor mappings need.
|
|
3238
|
+
clone_jsx_name(
|
|
3239
|
+
node.closingElement?.name ?? name,
|
|
3240
|
+
node.closingElement?.name || node.closingElement || node,
|
|
3241
|
+
),
|
|
3266
3242
|
),
|
|
3267
3243
|
node.closingElement || node,
|
|
3268
3244
|
);
|
|
3269
3245
|
|
|
3270
3246
|
const element = set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
|
|
3247
|
+
if (node.metadata?.dynamicElement === true) {
|
|
3248
|
+
// Keep lowered dynamic tags recognizable to scoped-CSS passes and the
|
|
3249
|
+
// static-hoist veto after the rebuild.
|
|
3250
|
+
element.metadata.dynamicElement = true;
|
|
3251
|
+
}
|
|
3271
3252
|
if (transform_context.typeOnly && is_style_element(node)) {
|
|
3272
3253
|
disable_style_anchor_verification(element);
|
|
3273
3254
|
}
|
|
@@ -5784,6 +5765,12 @@ function build_return_expression(render_nodes) {
|
|
|
5784
5765
|
if (render_nodes.length === 1) {
|
|
5785
5766
|
const only = render_nodes[0];
|
|
5786
5767
|
if (only.type === 'JSXExpressionContainer') {
|
|
5768
|
+
// Reactive-block containers (dynamic tags) must stay expression
|
|
5769
|
+
// children so the host JSX compiler wraps them in a render block;
|
|
5770
|
+
// returning the bare call would evaluate them once.
|
|
5771
|
+
if (only.metadata?.tsrx_reactive_block === true) {
|
|
5772
|
+
return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
|
|
5773
|
+
}
|
|
5787
5774
|
return only.expression;
|
|
5788
5775
|
}
|
|
5789
5776
|
if (only.type === 'JSXText') {
|
|
@@ -112,6 +112,12 @@ export function is_hoist_safe_jsx_node(node) {
|
|
|
112
112
|
return node.children.every(is_hoist_safe_jsx_child);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Lowered dynamic tags reference a component-scoped const and resolve at
|
|
116
|
+
// runtime — never static, never hoistable.
|
|
117
|
+
if (/** @type {any} */ (node).metadata?.dynamicElement === true) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
return (
|
|
116
122
|
node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
|
|
117
123
|
node.children.every(is_hoist_safe_jsx_child)
|
|
@@ -43,6 +43,10 @@ export function is_interleaved_body(body_nodes, is_jsx_child) {
|
|
|
43
43
|
*/
|
|
44
44
|
export function is_capturable_jsx_child(jsx) {
|
|
45
45
|
if (!jsx) return false;
|
|
46
|
+
// Reactive-block containers (dynamic tags) must stay expression children
|
|
47
|
+
// so the host JSX compiler wraps them in a render block; capturing them
|
|
48
|
+
// into a const would evaluate them once.
|
|
49
|
+
if (jsx.metadata?.tsrx_reactive_block === true) return false;
|
|
46
50
|
const t = jsx.type;
|
|
47
51
|
return t === 'JSXElement' || t === 'JSXFragment' || t === 'JSXExpressionContainer';
|
|
48
52
|
}
|
package/src/transform/scoping.js
CHANGED
|
@@ -75,15 +75,19 @@ export function annotate_with_hash(
|
|
|
75
75
|
) {
|
|
76
76
|
if (!node || typeof node !== 'object') return node;
|
|
77
77
|
if (
|
|
78
|
-
node.type === 'FunctionDeclaration' ||
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
(node.type === 'FunctionDeclaration' ||
|
|
79
|
+
node.type === 'FunctionExpression' ||
|
|
80
|
+
node.type === 'ArrowFunctionExpression') &&
|
|
81
|
+
// Generated dynamic-tag wrappers are render-block closures, not user
|
|
82
|
+
// component boundaries — the element inside still belongs to this
|
|
83
|
+
// component's scoped CSS.
|
|
84
|
+
node.metadata?.tsrx_dynamic_wrapper !== true
|
|
81
85
|
) {
|
|
82
86
|
return node;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
if (node.type === 'JSXElement') {
|
|
86
|
-
if (!is_composite_jsx_element(node) || node.metadata?.
|
|
90
|
+
if (!is_composite_jsx_element(node) || node.metadata?.dynamicElement) {
|
|
87
91
|
add_hash_class_to_jsx_element(node, hash, jsx_class_attr_name);
|
|
88
92
|
}
|
|
89
93
|
if (Array.isArray(node.children)) {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
generated: string;
|
|
28
28
|
loc: AST.SourceLocation;
|
|
29
29
|
metadata: PluginActionOverrides;
|
|
30
|
+
generatedLoc?: AST.SourceLocation;
|
|
30
31
|
end_loc?: AST.SourceLocation;
|
|
31
32
|
sourceLength?: number;
|
|
32
33
|
mappingData?: Partial<VolarCodeMapping['data']>;
|
|
@@ -387,7 +388,8 @@ export function convert_source_map_to_mappings(
|
|
|
387
388
|
* @returns {{ line: number; column: number }}
|
|
388
389
|
*/
|
|
389
390
|
function get_generated_position_for_token(token) {
|
|
390
|
-
const
|
|
391
|
+
const generated_loc = token.generatedLoc ?? token.loc;
|
|
392
|
+
const key = `${generated_loc.start.line}:${generated_loc.start.column}`;
|
|
391
393
|
const positions = src_to_gen_map.get(key);
|
|
392
394
|
if (!positions || positions.length === 0) {
|
|
393
395
|
throw new Error(`No source map entry for position "${key}"`);
|
|
@@ -478,6 +480,32 @@ export function convert_source_map_to_mappings(
|
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
483
|
+
/**
|
|
484
|
+
* @param {any} generated_node
|
|
485
|
+
* @returns {void}
|
|
486
|
+
*/
|
|
487
|
+
function add_extra_source_mapping_tokens(generated_node) {
|
|
488
|
+
if (!generated_node?.loc || !Array.isArray(generated_node.metadata?.extra_source_mappings)) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const source_node of generated_node.metadata.extra_source_mappings) {
|
|
493
|
+
if (!source_node?.loc) continue;
|
|
494
|
+
|
|
495
|
+
tokens.push({
|
|
496
|
+
source: source_node.name ?? generated_node.name,
|
|
497
|
+
generated: generated_node.name,
|
|
498
|
+
loc: source_node.loc,
|
|
499
|
+
generatedLoc: generated_node.loc,
|
|
500
|
+
metadata: {},
|
|
501
|
+
sourceLength:
|
|
502
|
+
typeof source_node.start === 'number' && typeof source_node.end === 'number'
|
|
503
|
+
? source_node.end - source_node.start
|
|
504
|
+
: undefined,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
481
509
|
// We have to visit everything in generated order to maintain correct indices
|
|
482
510
|
|
|
483
511
|
walk(ast, null, {
|
|
@@ -521,6 +549,7 @@ export function convert_source_map_to_mappings(
|
|
|
521
549
|
token.mappingData = { ...mapping_data, verification: false };
|
|
522
550
|
}
|
|
523
551
|
tokens.push(token);
|
|
552
|
+
add_extra_source_mapping_tokens(node);
|
|
524
553
|
|
|
525
554
|
if (Array.isArray(node.metadata?.lazy_param_binding_mappings)) {
|
|
526
555
|
for (const binding_mapping of node.metadata.lazy_param_binding_mappings) {
|
|
@@ -558,6 +587,7 @@ export function convert_source_map_to_mappings(
|
|
|
558
587
|
token.mappingData = { ...mapping_data, verification: false };
|
|
559
588
|
}
|
|
560
589
|
tokens.push(token);
|
|
590
|
+
add_extra_source_mapping_tokens(node);
|
|
561
591
|
}
|
|
562
592
|
return; // Leaf node, don't traverse further
|
|
563
593
|
} else if (node.type === 'Literal') {
|
package/types/index.d.ts
CHANGED
|
@@ -84,7 +84,7 @@ interface BaseNodeMetaData {
|
|
|
84
84
|
parenthesized?: boolean;
|
|
85
85
|
native_tsrx?: boolean;
|
|
86
86
|
native_tsrx_template_block?: boolean;
|
|
87
|
-
|
|
87
|
+
dynamicElement?: boolean;
|
|
88
88
|
templateMode?: 'script' | 'template';
|
|
89
89
|
script_only?: boolean;
|
|
90
90
|
tsrxDirective?: 'if' | 'for' | 'switch' | 'try';
|
|
@@ -253,6 +253,9 @@ declare module 'estree' {
|
|
|
253
253
|
TsrxFragment: TsrxFragment;
|
|
254
254
|
Text: Text;
|
|
255
255
|
TSRXJSXElement: TSRXJSXElement;
|
|
256
|
+
TSRXJSXFragment: TSRXJSXFragment;
|
|
257
|
+
TSRXJSXOpeningElement: ESTreeJSX.TSRXJSXOpeningElement;
|
|
258
|
+
TSRXJSXClosingElement: ESTreeJSX.TSRXJSXClosingElement;
|
|
256
259
|
TSRXExpression: TSRXExpression;
|
|
257
260
|
Attribute: Attribute;
|
|
258
261
|
SpreadAttribute: SpreadAttribute;
|
|
@@ -299,13 +302,14 @@ declare module 'estree' {
|
|
|
299
302
|
|
|
300
303
|
interface Element extends AST.BaseExpression {
|
|
301
304
|
type: 'Element';
|
|
302
|
-
id: AST.
|
|
305
|
+
id: AST.Expression;
|
|
303
306
|
attributes: Array<Attribute | SpreadAttribute>;
|
|
304
307
|
children: AST.Node[];
|
|
305
308
|
openingElement: ESTreeJSX.JSXOpeningElement;
|
|
306
309
|
closingElement: ESTreeJSX.JSXClosingElement | null;
|
|
307
310
|
selfClosing?: boolean;
|
|
308
311
|
unclosed?: boolean;
|
|
312
|
+
isDynamic?: boolean;
|
|
309
313
|
css?: string;
|
|
310
314
|
metadata: BaseNodeMetaData;
|
|
311
315
|
start: number;
|
|
@@ -345,7 +349,11 @@ declare module 'estree' {
|
|
|
345
349
|
| AST.JSXCodeBlock;
|
|
346
350
|
|
|
347
351
|
interface TSRXJSXElement
|
|
348
|
-
extends
|
|
352
|
+
extends
|
|
353
|
+
Omit<ESTreeJSX.JSXElement, 'children' | 'openingElement' | 'closingElement'>,
|
|
354
|
+
AST.NodeWithMaybeComments {
|
|
355
|
+
openingElement: ESTreeJSX.TSRXJSXOpeningElement;
|
|
356
|
+
closingElement: ESTreeJSX.TSRXJSXClosingElement | null;
|
|
349
357
|
children: TSRXJSXChild[];
|
|
350
358
|
metadata: BaseNodeMetaData & {
|
|
351
359
|
ts_name?: string;
|
|
@@ -365,13 +373,10 @@ declare module 'estree' {
|
|
|
365
373
|
innerComments?: AST.Comment[] | undefined;
|
|
366
374
|
}
|
|
367
375
|
|
|
368
|
-
interface JSXStyleElement extends AST.
|
|
376
|
+
interface JSXStyleElement extends Omit<AST.TSRXJSXElement, 'type' | 'children'> {
|
|
369
377
|
type: 'JSXStyleElement';
|
|
370
|
-
openingElement: ESTreeJSX.JSXOpeningElement;
|
|
371
|
-
closingElement: ESTreeJSX.JSXClosingElement | null;
|
|
372
378
|
children: AST.CSS.StyleSheet[];
|
|
373
379
|
css?: string;
|
|
374
|
-
metadata: BaseNodeMetaData;
|
|
375
380
|
unclosed?: boolean;
|
|
376
381
|
}
|
|
377
382
|
|
|
@@ -519,7 +524,7 @@ declare module 'estree' {
|
|
|
519
524
|
|
|
520
525
|
type TSRXStatement = AST.Statement | TSESTree.Statement;
|
|
521
526
|
|
|
522
|
-
type NodeWithChildren = TSRXJSXElement | TSRXJSXFragment | JSXStyleElement;
|
|
527
|
+
type NodeWithChildren = TSRXJSXElement | TSRXJSXFragment | JSXStyleElement | ESTreeJSX.JSXElement;
|
|
523
528
|
|
|
524
529
|
export namespace CSS {
|
|
525
530
|
export interface BaseNode extends AST.NodeWithMaybeComments {
|
|
@@ -727,6 +732,7 @@ declare module 'estree-jsx' {
|
|
|
727
732
|
interface JSXExpressionContainer {
|
|
728
733
|
text?: boolean;
|
|
729
734
|
style?: boolean;
|
|
735
|
+
isDynamic?: boolean;
|
|
730
736
|
}
|
|
731
737
|
|
|
732
738
|
interface JSXMemberExpression {
|
|
@@ -734,11 +740,11 @@ declare module 'estree-jsx' {
|
|
|
734
740
|
}
|
|
735
741
|
|
|
736
742
|
interface TSRXJSXOpeningElement extends Omit<JSXOpeningElement, 'name'> {
|
|
737
|
-
name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName;
|
|
743
|
+
name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName | JSXExpressionContainer;
|
|
738
744
|
}
|
|
739
745
|
|
|
740
746
|
interface TSRXJSXClosingElement extends Omit<JSXClosingElement, 'name'> {
|
|
741
|
-
name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName;
|
|
747
|
+
name: AST.MemberExpression | JSXIdentifier | JSXNamespacedName | JSXExpressionContainer;
|
|
742
748
|
}
|
|
743
749
|
|
|
744
750
|
interface ExpressionMap {
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface JsxTransformContext {
|
|
|
39
39
|
needs_normalize_spread_props: boolean;
|
|
40
40
|
needs_normalize_spread_props_for_ref_attr: boolean;
|
|
41
41
|
needs_fragment: boolean;
|
|
42
|
+
needs_dynamic_element: boolean;
|
|
43
|
+
needs_dynamic_factory: boolean;
|
|
42
44
|
needs_for_of_iterable: boolean;
|
|
43
45
|
needs_iteration_value_type: boolean;
|
|
44
46
|
stylesheets: AST.CSS.StyleSheet[];
|
|
@@ -53,8 +55,6 @@ export interface JsxTransformContext {
|
|
|
53
55
|
hook_helpers_enabled: boolean;
|
|
54
56
|
available_bindings: Map<string, AST.Identifier>;
|
|
55
57
|
lazy_next_id: number;
|
|
56
|
-
/** Scope map used to resolve runtime Dynamic imports for scoped CSS pruning. */
|
|
57
|
-
runtime_dynamic_scopes: Map<any, any> | null;
|
|
58
58
|
inside_element_child?: boolean;
|
|
59
59
|
/** Full source text for source-aware diagnostics. */
|
|
60
60
|
source: string;
|
|
@@ -316,11 +316,30 @@ export interface JsxPlatform {
|
|
|
316
316
|
*/
|
|
317
317
|
suspense: string;
|
|
318
318
|
/**
|
|
319
|
-
* Module
|
|
320
|
-
*
|
|
321
|
-
*
|
|
319
|
+
* Module the type-only transform imports the `Dynamic` component type
|
|
320
|
+
* from when lowering dynamic tags to `<TsrxDynamic is={expr}>`. The
|
|
321
|
+
* module only needs type declarations; production output never imports
|
|
322
|
+
* it.
|
|
322
323
|
*/
|
|
323
324
|
dynamic?: string;
|
|
325
|
+
/**
|
|
326
|
+
* Scoped-binding lowering for dynamic tags (`<{expr}>`) in production
|
|
327
|
+
* output; each tag binds a scoped component const referenced like an
|
|
328
|
+
* ordinary component. With `name`/`source`, the factory is imported as
|
|
329
|
+
* `_tsrx_dynamic` and wraps the tag expression in a reactive thunk:
|
|
330
|
+
* `const TsrxDynamic_N = _tsrx_dynamic(() => expr);` (Solid:
|
|
331
|
+
* `{ name: 'dynamic', source: '@solidjs/web' }`). With an empty object,
|
|
332
|
+
* the const is a plain import-free alias re-evaluated by the host's
|
|
333
|
+
* render cycle: `const TsrxDynamic_N = expr;` (React/Preact). With
|
|
334
|
+
* `renderBlock: true`, the alias and element move inside an
|
|
335
|
+
* expression-child IIFE,
|
|
336
|
+
* `{(() => { const TsrxDynamic_N = expr; return <TsrxDynamic_N ...>; })()}`,
|
|
337
|
+
* relying on the host JSX compiler's reactive render block for
|
|
338
|
+
* expression children (Vue Vapor, which never re-runs setup). In
|
|
339
|
+
* type-only output, dynamic tags always lower to
|
|
340
|
+
* `<TsrxDynamic is={expr}>` imported from `imports.dynamic`.
|
|
341
|
+
*/
|
|
342
|
+
dynamicFactory?: { name?: string; source?: string; renderBlock?: boolean };
|
|
324
343
|
/**
|
|
325
344
|
* Module to import `TsrxErrorBoundary` from when an `@try { ... } @catch (...)`
|
|
326
345
|
* block appears. Usually `'@tsrx/<platform>/error-boundary'`.
|
package/types/parse.d.ts
CHANGED
|
@@ -1621,11 +1621,13 @@ export namespace Parse {
|
|
|
1621
1621
|
| ReturnType<Parser['jsx_parseIdentifier']>;
|
|
1622
1622
|
|
|
1623
1623
|
/**
|
|
1624
|
-
* Parse JSX element name (identifier, member, namespaced
|
|
1624
|
+
* Parse JSX element name (identifier, member, namespaced, or a dynamic
|
|
1625
|
+
* `{expression}` container)
|
|
1625
1626
|
*/
|
|
1626
1627
|
jsx_parseElementName():
|
|
1627
1628
|
| ESTreeJSX.JSXMemberExpression
|
|
1628
1629
|
| ReturnType<Parser['jsx_parseNamespacedName']>
|
|
1630
|
+
| ESTreeJSX.JSXExpressionContainer
|
|
1629
1631
|
| '';
|
|
1630
1632
|
|
|
1631
1633
|
/**
|