@tsrx/core 0.1.25 → 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 +112 -1
- 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 +4 -2
- 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
|
@@ -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
|
|
|
@@ -4315,6 +4423,9 @@ export function TSRXPlugin(config) {
|
|
|
4315
4423
|
} finally {
|
|
4316
4424
|
this.#closingNativeTemplateNode = false;
|
|
4317
4425
|
}
|
|
4426
|
+
if (this.#isDynamicJSXElementName(closingElement.name)) {
|
|
4427
|
+
/** @type {any} */ (closingElement).isDynamic = true;
|
|
4428
|
+
}
|
|
4318
4429
|
this.exprAllowed = false;
|
|
4319
4430
|
|
|
4320
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;
|
|
@@ -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';
|
|
@@ -299,13 +299,14 @@ declare module 'estree' {
|
|
|
299
299
|
|
|
300
300
|
interface Element extends AST.BaseExpression {
|
|
301
301
|
type: 'Element';
|
|
302
|
-
id: AST.
|
|
302
|
+
id: AST.Expression;
|
|
303
303
|
attributes: Array<Attribute | SpreadAttribute>;
|
|
304
304
|
children: AST.Node[];
|
|
305
305
|
openingElement: ESTreeJSX.JSXOpeningElement;
|
|
306
306
|
closingElement: ESTreeJSX.JSXClosingElement | null;
|
|
307
307
|
selfClosing?: boolean;
|
|
308
308
|
unclosed?: boolean;
|
|
309
|
+
isDynamic?: boolean;
|
|
309
310
|
css?: string;
|
|
310
311
|
metadata: BaseNodeMetaData;
|
|
311
312
|
start: number;
|
|
@@ -727,6 +728,7 @@ declare module 'estree-jsx' {
|
|
|
727
728
|
interface JSXExpressionContainer {
|
|
728
729
|
text?: boolean;
|
|
729
730
|
style?: boolean;
|
|
731
|
+
isDynamic?: boolean;
|
|
730
732
|
}
|
|
731
733
|
|
|
732
734
|
interface JSXMemberExpression {
|
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
|
/**
|