@tsrx/core 0.1.32 → 0.1.34
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/constants.js +4 -0
- package/src/index.js +3 -1
- package/src/plugin.js +49 -32
- package/src/source-map-utils.js +11 -0
- package/src/transform/jsx/helpers.js +14 -39
- package/src/transform/jsx/index.js +401 -59
- package/src/transform/lazy.js +95 -46
- package/src/transform/segments.js +22 -1
- package/src/utils/builders.js +6 -3
- package/types/index.d.ts +17 -1
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -2,6 +2,10 @@ export const TEMPLATE_FRAGMENT = 1;
|
|
|
2
2
|
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
|
|
3
3
|
export const IS_CONTROLLED = 1 << 2;
|
|
4
4
|
export const IS_INDEXED = 1 << 3;
|
|
5
|
+
// A control-flow block that is its component's sole root: renders before the
|
|
6
|
+
// parent `__anchor` (sibling semantics, no `<!>` wrapper). Must match the value
|
|
7
|
+
// in ripple's runtime constants.
|
|
8
|
+
export const ROOT_CONTROLLED = 1 << 4;
|
|
5
9
|
export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
|
|
6
10
|
export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
|
|
7
11
|
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export {
|
|
|
31
31
|
TEMPLATE_USE_IMPORT_NODE,
|
|
32
32
|
IS_CONTROLLED,
|
|
33
33
|
IS_INDEXED,
|
|
34
|
+
ROOT_CONTROLLED,
|
|
34
35
|
TEMPLATE_SVG_NAMESPACE,
|
|
35
36
|
TEMPLATE_MATHML_NAMESPACE,
|
|
36
37
|
HYDRATION_START,
|
|
@@ -147,6 +148,7 @@ export {
|
|
|
147
148
|
create_hook_safe_helper as createHookSafeHelper,
|
|
148
149
|
create_element_ref_target_type as createElementRefTargetType,
|
|
149
150
|
create_element_ref_target_type_for_name as createElementRefTargetTypeForName,
|
|
151
|
+
build_return_expression as buildReturnExpression,
|
|
150
152
|
createJsxTransform,
|
|
151
153
|
extract_jsx_setup_declarations as extractJsxSetupDeclarations,
|
|
152
154
|
is_component_like_element,
|
|
@@ -163,7 +165,7 @@ export {
|
|
|
163
165
|
} from './transform/jsx/index.js';
|
|
164
166
|
export {
|
|
165
167
|
in_jsx_child_context as inJsxChildContext,
|
|
166
|
-
|
|
168
|
+
is_empty_jsx_fragment as isEmptyJsxFragment,
|
|
167
169
|
tsx_with_ts_locations as tsxWithTsLocations,
|
|
168
170
|
is_template_if_node as isTemplateIfNode,
|
|
169
171
|
is_template_for_of_node as isTemplateForOfNode,
|
package/src/plugin.js
CHANGED
|
@@ -232,6 +232,7 @@ export function TSRXPlugin(config) {
|
|
|
232
232
|
// If we push an undefined context, Acorn's tokenizer will later crash reading `.override`.
|
|
233
233
|
const b_stat = tc.b_stat || acorn.tokContexts.b_stat;
|
|
234
234
|
const b_expr = tc.b_expr || acorn.tokContexts.b_expr;
|
|
235
|
+
const q_tmpl = tc.q_tmpl || acorn.tokContexts.q_tmpl;
|
|
235
236
|
const tstt = Parser.acornTypeScript.tokTypes;
|
|
236
237
|
const tstc = Parser.acornTypeScript.tokContexts;
|
|
237
238
|
|
|
@@ -1272,14 +1273,21 @@ export function TSRXPlugin(config) {
|
|
|
1272
1273
|
*/
|
|
1273
1274
|
#parseCodeBlockSetupStatement() {
|
|
1274
1275
|
const previous_context = this.context;
|
|
1275
|
-
this.
|
|
1276
|
-
(context) =>
|
|
1277
|
-
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1278
|
-
);
|
|
1276
|
+
const at_template_literal = this.type === tt.backQuote;
|
|
1279
1277
|
let pushed_statement_context = false;
|
|
1280
|
-
if (
|
|
1281
|
-
this.
|
|
1282
|
-
|
|
1278
|
+
if (at_template_literal) {
|
|
1279
|
+
if (this.curContext() !== q_tmpl) {
|
|
1280
|
+
this.context.push(q_tmpl);
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
this.context = previous_context.filter(
|
|
1284
|
+
(context) =>
|
|
1285
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1286
|
+
);
|
|
1287
|
+
if (this.curContext() !== b_stat) {
|
|
1288
|
+
this.context.push(b_stat);
|
|
1289
|
+
pushed_statement_context = true;
|
|
1290
|
+
}
|
|
1283
1291
|
}
|
|
1284
1292
|
this.exprAllowed = true;
|
|
1285
1293
|
const previous_path = this.#path;
|
|
@@ -1287,22 +1295,7 @@ export function TSRXPlugin(config) {
|
|
|
1287
1295
|
this.#templateScriptParsingDepth++;
|
|
1288
1296
|
let node;
|
|
1289
1297
|
try {
|
|
1290
|
-
// A code-block/directive body is statements plus at most one render node —
|
|
1291
|
-
// never bare text or markup tokens. If the tokenizer mis-read trailing
|
|
1292
|
-
// code as JSX (raw text or a tag-name token — both can happen for a
|
|
1293
|
-
// statement following the render node, depending on the leftover context),
|
|
1294
|
-
// reposition to the token start and re-read it as code now that the
|
|
1295
|
-
// template path is hidden. It then parses as a statement so the
|
|
1296
|
-
// one-render-node rule reports a clear "statements cannot follow" error
|
|
1297
|
-
// instead of a generic parse fault.
|
|
1298
1298
|
if (this.type === tstt.jsxText || this.type === tstt.jsxName) {
|
|
1299
|
-
// Rewinding `pos` to the mis-read token's start must also rewind the
|
|
1300
|
-
// line counter: a `jsxText` token can span newlines (e.g. the blank
|
|
1301
|
-
// line before a following render node), and reading it already
|
|
1302
|
-
// advanced `curLine`/`lineStart` to its end. Resetting only `pos`
|
|
1303
|
-
// would leave the line counter ahead of `pos`, inflating the `loc`
|
|
1304
|
-
// of this statement and every node after it (which crashes source-map
|
|
1305
|
-
// mapping when the inflated end line runs past the file).
|
|
1306
1299
|
const loc = acorn.getLineInfo(this.input, this.start);
|
|
1307
1300
|
this.pos = this.start;
|
|
1308
1301
|
this.curLine = loc.line;
|
|
@@ -1316,7 +1309,9 @@ export function TSRXPlugin(config) {
|
|
|
1316
1309
|
if (pushed_statement_context && this.curContext() === b_stat) {
|
|
1317
1310
|
this.context.pop();
|
|
1318
1311
|
}
|
|
1319
|
-
|
|
1312
|
+
if (!at_template_literal) {
|
|
1313
|
+
this.context = previous_context;
|
|
1314
|
+
}
|
|
1320
1315
|
}
|
|
1321
1316
|
if (this.curContext() === tstc.tc_expr) {
|
|
1322
1317
|
this.context.pop();
|
|
@@ -4383,22 +4378,29 @@ export function TSRXPlugin(config) {
|
|
|
4383
4378
|
parseTemplateBody(body) {
|
|
4384
4379
|
const current_template_node = this.#currentNativeTemplateNode();
|
|
4385
4380
|
if (!current_template_node) return;
|
|
4386
|
-
// Outside a `@{ … }` block every element/fragment body is plain JSX (§2,
|
|
4387
|
-
// §5). There is no script section and no `---` fence to infer — text is
|
|
4388
|
-
// text, and setup code lives only inside a code block.
|
|
4389
4381
|
current_template_node.metadata ??= { path: [] };
|
|
4390
4382
|
current_template_node.metadata.templateMode = 'template';
|
|
4391
4383
|
|
|
4392
|
-
// `@{ … }` code block as element/fragment content (§2 rule 1). Sibling
|
|
4393
|
-
// code blocks are allowed, so this is not gated on an empty body;
|
|
4394
|
-
// reposition onto the `@` if leading whitespace was tokenized ahead of it.
|
|
4395
4384
|
if (this.#atCodeBlockStart()) {
|
|
4396
4385
|
const at_index = skip_whitespace_from(this.input, this.start);
|
|
4397
4386
|
if (this.start !== at_index) {
|
|
4387
|
+
const ws_start = this.start;
|
|
4388
|
+
const ws_start_loc = this.startLoc;
|
|
4389
|
+
const ws_value = this.input.slice(ws_start, at_index);
|
|
4390
|
+
const text_node = /** @type {ESTreeJSX.JSXText} */ (
|
|
4391
|
+
this.startNodeAt(ws_start, ws_start_loc)
|
|
4392
|
+
);
|
|
4393
|
+
text_node.value = ws_value;
|
|
4394
|
+
text_node.raw = ws_value;
|
|
4398
4395
|
const loc = acorn.getLineInfo(this.input, at_index);
|
|
4396
|
+
const at_position = new acorn.Position(loc.line, loc.column);
|
|
4397
|
+
this.finishNodeAt(text_node, 'JSXText', at_index, at_position);
|
|
4398
|
+
if (this.#shouldKeepTemplateTextNode(text_node)) {
|
|
4399
|
+
body.push(text_node);
|
|
4400
|
+
}
|
|
4399
4401
|
this.pos = at_index;
|
|
4400
4402
|
this.start = at_index;
|
|
4401
|
-
this.startLoc =
|
|
4403
|
+
this.startLoc = at_position;
|
|
4402
4404
|
this.curLine = loc.line;
|
|
4403
4405
|
this.lineStart = at_index - loc.column;
|
|
4404
4406
|
}
|
|
@@ -4416,8 +4418,23 @@ export function TSRXPlugin(config) {
|
|
|
4416
4418
|
// text never starts at `<`, so drop the leaked context and re-read the
|
|
4417
4419
|
// tag instead of emitting an empty node.
|
|
4418
4420
|
if (this.input.charCodeAt(this.start) === CharCode.lessThan) {
|
|
4419
|
-
|
|
4420
|
-
this.
|
|
4421
|
+
if (this.input.charCodeAt(this.start + 1) === CharCode.slash) {
|
|
4422
|
+
while (this.curContext() === tstc.tc_expr) {
|
|
4423
|
+
this.context.pop();
|
|
4424
|
+
}
|
|
4425
|
+
} else {
|
|
4426
|
+
let native_depth = 0;
|
|
4427
|
+
for (const node of this.#path) {
|
|
4428
|
+
if (this.#isNativeTemplateNode(node)) native_depth++;
|
|
4429
|
+
}
|
|
4430
|
+
let tc_expr_depth = 0;
|
|
4431
|
+
for (const context of this.context) {
|
|
4432
|
+
if (context === tstc.tc_expr) tc_expr_depth++;
|
|
4433
|
+
}
|
|
4434
|
+
while (tc_expr_depth > native_depth && this.curContext() === tstc.tc_expr) {
|
|
4435
|
+
this.context.pop();
|
|
4436
|
+
tc_expr_depth--;
|
|
4437
|
+
}
|
|
4421
4438
|
}
|
|
4422
4439
|
this.pos = this.start;
|
|
4423
4440
|
this.exprAllowed = true;
|
package/src/source-map-utils.js
CHANGED
|
@@ -50,6 +50,17 @@ export const mapping_data_verify_complete = {
|
|
|
50
50
|
completion: true,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Completion only — no verification/hover/navigation. Used for positions that
|
|
55
|
+
* should surface completions but must not be type-checked, e.g. a `@`-leading text
|
|
56
|
+
* node that is an in-progress template directive (verifying it as code would raise
|
|
57
|
+
* spurious diagnostics).
|
|
58
|
+
* @type {Partial<VolarCodeMapping['data']>}
|
|
59
|
+
*/
|
|
60
|
+
export const mapping_data_completion_only = {
|
|
61
|
+
completion: true,
|
|
62
|
+
};
|
|
63
|
+
|
|
53
64
|
/**
|
|
54
65
|
* Convert byte offset to line/column
|
|
55
66
|
* @param {number} offset
|
|
@@ -19,6 +19,20 @@ export function in_jsx_child_context(path) {
|
|
|
19
19
|
return !!parent && (parent.type === 'JSXElement' || parent.type === 'JSXFragment');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {any} node
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function is_empty_jsx_fragment(node) {
|
|
27
|
+
return (
|
|
28
|
+
node?.type === 'JSXFragment' &&
|
|
29
|
+
!(node.children || []).some(
|
|
30
|
+
(/** @type {any} */ child) =>
|
|
31
|
+
child && (child.type !== 'JSXText' || child.value.trim() !== ''),
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
/**
|
|
23
37
|
* Match Ripple's transform path metadata shape: every node seen by the walker
|
|
24
38
|
* carries its current ancestor path for downstream CSS pruning and mapping
|
|
@@ -36,45 +50,6 @@ export function set_node_path_metadata(node, path) {
|
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|
|
39
|
-
/**
|
|
40
|
-
* Flatten a JSX-compatible island's children into a single expression. In a
|
|
41
|
-
* JSX-child position, a JSXExpressionContainer `{expr}` is valid and must stay
|
|
42
|
-
* wrapped. In an expression position (e.g. `return ...`), `{expr}` parses as
|
|
43
|
-
* a block/object literal, so unwrap to `expr`.
|
|
44
|
-
*
|
|
45
|
-
* @param {any} node
|
|
46
|
-
* @param {boolean} [in_jsx_child]
|
|
47
|
-
* @returns {any}
|
|
48
|
-
*/
|
|
49
|
-
export function tsx_node_to_jsx_expression(node, in_jsx_child = false) {
|
|
50
|
-
const children = (node.children || []).filter(
|
|
51
|
-
(/** @type {any} */ child) => child.type !== 'JSXText' || child.value.trim() !== '',
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
children.length === 1 &&
|
|
56
|
-
children[0].type !== 'JSXText' &&
|
|
57
|
-
// Reactive-block containers (dynamic tags) must stay expression
|
|
58
|
-
// children so the host JSX compiler wraps them in a render block;
|
|
59
|
-
// unwrapping to a bare call would evaluate them once.
|
|
60
|
-
children[0].metadata?.tsrx_reactive_block !== true
|
|
61
|
-
) {
|
|
62
|
-
const only = children[0];
|
|
63
|
-
if (only.type === 'JSXExpressionContainer' && !in_jsx_child) {
|
|
64
|
-
return only.expression;
|
|
65
|
-
}
|
|
66
|
-
return only;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return /** @type {any} */ ({
|
|
70
|
-
type: 'JSXFragment',
|
|
71
|
-
openingFragment: { type: 'JSXOpeningFragment', metadata: { path: [] } },
|
|
72
|
-
closingFragment: { type: 'JSXClosingFragment', metadata: { path: [] } },
|
|
73
|
-
children,
|
|
74
|
-
metadata: { path: [] },
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
53
|
/**
|
|
79
54
|
* Wrap esrap's `tsx()` printer with location markers for nodes whose spans
|
|
80
55
|
* (e.g. the leading `new ` of a NewExpression or the angle-bracket delimiters
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
import { walk } from 'zimmerframe';
|
|
6
6
|
import { print } from 'esrap';
|
|
7
7
|
import { error } from '../../errors.js';
|
|
8
|
+
import { is_template_value_position } from '../../analyze/validation.js';
|
|
8
9
|
import { analyze_css } from '../../analyze/css-analyze.js';
|
|
9
10
|
import { prune_css } from '../../analyze/prune.js';
|
|
10
11
|
import {
|
|
11
12
|
in_jsx_child_context,
|
|
13
|
+
is_empty_jsx_fragment,
|
|
12
14
|
set_node_path_metadata,
|
|
13
15
|
tsx_with_ts_locations,
|
|
14
16
|
is_template_if_node,
|
|
@@ -258,29 +260,170 @@ function is_jsx_control_flow_expression(node) {
|
|
|
258
260
|
|
|
259
261
|
/**
|
|
260
262
|
* Wrap a render-output node in a native TSRX fragment so it flows through the
|
|
261
|
-
* same single-child render path as a `<> … </>` output.
|
|
263
|
+
* same single-child render path as a `<> … </>` output. This is a compiler
|
|
264
|
+
* GENERATED wrapper (it wraps a control-flow directive / render output so it
|
|
265
|
+
* lowers to a value) — it is marked `tsrx_generated_wrapper` so the single-child
|
|
266
|
+
* collapse keeps unwrapping it, unlike an AUTHORED `<> … </>` which is kept.
|
|
262
267
|
* @param {any} node
|
|
263
268
|
* @returns {any}
|
|
264
269
|
*/
|
|
265
270
|
function wrap_in_native_tsrx_fragment(node) {
|
|
266
271
|
const fragment = b.jsx_fragment([node]);
|
|
267
|
-
fragment.metadata = {
|
|
272
|
+
fragment.metadata = {
|
|
273
|
+
...(fragment.metadata || {}),
|
|
274
|
+
native_tsrx: true,
|
|
275
|
+
tsrx_generated_wrapper: true,
|
|
276
|
+
};
|
|
268
277
|
return fragment;
|
|
269
278
|
}
|
|
270
279
|
|
|
271
280
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
|
|
281
|
+
* An AUTHORED `<> … </>` fragment (not a compiler-generated wrapper, nor a Ripple
|
|
282
|
+
* code-block-chain wrapper). These are kept verbatim in the output instead of
|
|
283
|
+
* being unwrapped to their single child.
|
|
284
|
+
* @param {any} node
|
|
285
|
+
* @returns {boolean}
|
|
286
|
+
*/
|
|
287
|
+
function is_authored_native_fragment(node) {
|
|
288
|
+
return (
|
|
289
|
+
node?.type === 'JSXFragment' &&
|
|
290
|
+
node.metadata?.native_tsrx === true &&
|
|
291
|
+
node.metadata?.tsrx_generated_wrapper !== true
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Slots whose value is a render child / statement, not a JavaScript value
|
|
297
|
+
* expression. A control-flow directive (`@if`/`@for`/`@switch`/`@try`) is
|
|
298
|
+
* legitimate render output in these positions, so it must NOT be treated as a
|
|
299
|
+
* stray "control flow used as a value". Everything else is a value position:
|
|
300
|
+
* an unhandled control-flow directive there is the raw-value error case
|
|
301
|
+
* (a `@for` iterable, an `@if`/`@switch` test, etc.).
|
|
302
|
+
* @param {any} parent
|
|
303
|
+
* @param {string} key
|
|
304
|
+
* @returns {boolean}
|
|
305
|
+
*/
|
|
306
|
+
function is_statement_or_template_slot(parent, key) {
|
|
307
|
+
// JSX children, and the body of any block/program/function/loop.
|
|
308
|
+
if (key === 'children' || key === 'body') return true;
|
|
309
|
+
// A `@{ … }` code block's trailing output (`render`) is render position.
|
|
310
|
+
if (parent?.type === 'JSXCodeBlock' && key === 'render') return true;
|
|
311
|
+
// `{ @if … }` containers lower their expression through the render machinery.
|
|
312
|
+
if (parent?.type === 'JSXExpressionContainer' && key === 'expression') return true;
|
|
313
|
+
// Switch-case statement lists.
|
|
314
|
+
if (parent?.type === 'SwitchCase' && key === 'consequent') return true;
|
|
315
|
+
// An if-node branch is a statement block; its `alternate` is also where the
|
|
316
|
+
// `@else if` chain (another control-flow node) legitimately lives.
|
|
317
|
+
if (is_if_control_node(parent) && (key === 'consequent' || key === 'alternate')) return true;
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Render-output value slots: the only expression positions a directive may be
|
|
323
|
+
* the SOLE value of. A control-flow directive here collapses to its rendered
|
|
324
|
+
* value (wrapped in a native fragment by `wrap_control_flow_expression_values`)
|
|
325
|
+
* and a `@{ … }` code block self-lowers to an IIFE. These are established forms
|
|
326
|
+
* (`const x = @switch …`, `() => @if …`, `return @if …`, `render(@for …)`),
|
|
327
|
+
* distinct from combining a directive INTO an expression (an operator operand, a
|
|
328
|
+
* `@for` iterable, an `@if`/`@switch` test), which is an error.
|
|
329
|
+
* @param {any} parent
|
|
330
|
+
* @param {string} key
|
|
331
|
+
* @returns {boolean}
|
|
332
|
+
*/
|
|
333
|
+
function is_render_output_value_slot(parent, key) {
|
|
334
|
+
switch (parent?.type) {
|
|
335
|
+
case 'ArrowFunctionExpression':
|
|
336
|
+
return key === 'body';
|
|
337
|
+
case 'ReturnStatement':
|
|
338
|
+
return key === 'argument';
|
|
339
|
+
case 'ExpressionStatement':
|
|
340
|
+
return key === 'expression';
|
|
341
|
+
case 'VariableDeclarator':
|
|
342
|
+
return key === 'init';
|
|
343
|
+
case 'AssignmentExpression':
|
|
344
|
+
return key === 'right';
|
|
345
|
+
case 'CallExpression':
|
|
346
|
+
case 'NewExpression':
|
|
347
|
+
return key === 'arguments';
|
|
348
|
+
default:
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* A `<> … </>` is combined INTO a surrounding expression (an operator operand, a
|
|
355
|
+
* conditional branch, an array element, a template-literal hole) — as opposed to
|
|
356
|
+
* being the sole value of a render-output slot, where its single-child collapse
|
|
357
|
+
* is invisible because the value is only rendered. In a combined position the
|
|
358
|
+
* collapse is NOT invisible: a fragment is always a truthy element, but its
|
|
359
|
+
* collapsed content may be falsy, so `<>{0}</> || 'x'` (renders `0`) must not turn
|
|
360
|
+
* into `0 || 'x'` (renders `'x'`). Keep the fragment in these positions.
|
|
361
|
+
* @param {any} parent
|
|
362
|
+
* @param {any} child
|
|
363
|
+
* @returns {boolean}
|
|
364
|
+
*/
|
|
365
|
+
function is_combined_expression_position(parent, child) {
|
|
366
|
+
if (!parent || !is_template_value_position(parent, child)) return false;
|
|
367
|
+
switch (parent.type) {
|
|
368
|
+
// Sole-value render-output slots: the collapse is invisible, keep it.
|
|
369
|
+
case 'VariableDeclarator':
|
|
370
|
+
return parent.init !== child;
|
|
371
|
+
case 'AssignmentExpression':
|
|
372
|
+
return parent.right !== child;
|
|
373
|
+
case 'CallExpression':
|
|
374
|
+
case 'NewExpression':
|
|
375
|
+
return !(Array.isArray(parent.arguments) && parent.arguments.includes(child));
|
|
376
|
+
default:
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Re-wrap an already-lowered render value in a `<> … </>` fragment so a fragment
|
|
383
|
+
* combined into an expression keeps its fragment identity (see
|
|
384
|
+
* `is_combined_expression_position`). A value that is already a fragment is left
|
|
385
|
+
* as-is; a JSX element/text nests directly (`<><span /></>`); any other
|
|
386
|
+
* expression goes in a `{ … }` container (`<>{0}</>`).
|
|
387
|
+
* @param {any} expression
|
|
388
|
+
* @param {any} source
|
|
389
|
+
* @returns {any}
|
|
390
|
+
*/
|
|
391
|
+
function wrap_lowered_value_in_fragment(expression, source) {
|
|
392
|
+
if (expression?.type === 'JSXFragment') return expression;
|
|
393
|
+
const child =
|
|
394
|
+
expression?.type === 'JSXElement' ||
|
|
395
|
+
expression?.type === 'JSXText' ||
|
|
396
|
+
expression?.type === 'JSXExpressionContainer'
|
|
397
|
+
? expression
|
|
398
|
+
: to_jsx_expression_container(expression, source);
|
|
399
|
+
return set_loc(b.jsx_fragment([child]), source?.loc ? source : undefined);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Lower bare JSX control-flow directives that sit as the SOLE value of a
|
|
404
|
+
* render-output slot — an expression-bodied arrow (`() => @switch (…) { … }`), a
|
|
405
|
+
* `return @switch (…) { … }`, an unused expression statement, a variable
|
|
406
|
+
* initializer (`const x = @switch (…) { … }`), an assignment
|
|
407
|
+
* (`x = @switch (…) { … }`), or a call/`new` argument (`render(@if (…) { … })`)
|
|
408
|
+
* — by wrapping them in a native TSRX fragment so they flow through the same
|
|
409
|
+
* render machinery as a `<> … </>` output instead of leaking to the printer as a
|
|
410
|
+
* raw `JSX…Expression`.
|
|
411
|
+
*
|
|
412
|
+
* A control-flow directive or `@{ … }` code block used anywhere ELSE in a value
|
|
413
|
+
* position — COMBINED into an expression (`(@if …) || fallback`, an operator
|
|
414
|
+
* operand, an array element, a template-literal hole, a `@for` iterable, an
|
|
415
|
+
* `@if`/`@switch` test) — is likewise wrapped in a native TSRX fragment. In an
|
|
416
|
+
* operand position the fragment is then KEPT (a fragment is a truthy value, so
|
|
417
|
+
* `<>{…}</> || x` is preserved); in a "raw value" slot the fragment collapses to
|
|
418
|
+
* its rendered value. Either way nothing leaks to the printer as a raw
|
|
419
|
+
* `JSX…Expression`.
|
|
420
|
+
*
|
|
278
421
|
* @param {any} node
|
|
279
|
-
* @param {TransformContext
|
|
422
|
+
* @param {TransformContext} transform_context
|
|
280
423
|
* @param {Set<any>} [seen]
|
|
281
424
|
* @returns {void}
|
|
282
425
|
*/
|
|
283
|
-
function wrap_control_flow_expression_values(node,
|
|
426
|
+
function wrap_control_flow_expression_values(node, transform_context, seen = new Set()) {
|
|
284
427
|
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
285
428
|
seen.add(node);
|
|
286
429
|
|
|
@@ -292,19 +435,40 @@ function wrap_control_flow_expression_values(node, lower_dynamic_context, seen =
|
|
|
292
435
|
// `<{'div'}>`) is hoisted to a module-level static const while still
|
|
293
436
|
// carrying the raw dynamic tag. Alias lowerings return a replacement
|
|
294
437
|
// fragment, which is swapped into the child's position here.
|
|
438
|
+
const lower_dynamic = !!transform_context?.platform?.imports?.dynamicFactory;
|
|
295
439
|
const lower_child = (/** @type {any} */ child) => {
|
|
296
|
-
if (!
|
|
297
|
-
return lower_dynamic_jsx_element(child,
|
|
440
|
+
if (!lower_dynamic || child?.type !== 'JSXElement') return child;
|
|
441
|
+
return lower_dynamic_jsx_element(child, transform_context) ?? child;
|
|
298
442
|
};
|
|
299
443
|
|
|
444
|
+
// A control-flow directive or `@{ … }` code block combined into an expression
|
|
445
|
+
// (an operator operand, a `@for` iterable, an `@if`/`@switch` test, …) is
|
|
446
|
+
// wrapped in a native TSRX fragment so it flows through the render machinery
|
|
447
|
+
// instead of leaking to the printer as a raw `JSX…Expression`. In an operand
|
|
448
|
+
// position the fragment is then KEPT (a fragment is a truthy value); in a
|
|
449
|
+
// "raw value" slot like a `@for` iterable it collapses to its rendered value
|
|
450
|
+
// (see the JSXFragment visitor and `is_combined_expression_position`).
|
|
451
|
+
const wrap_directive_in_expression = (/** @type {any} */ value) =>
|
|
452
|
+
is_jsx_control_flow_expression(value) || value?.type === 'JSXCodeBlock'
|
|
453
|
+
? wrap_in_native_tsrx_fragment(value)
|
|
454
|
+
: value;
|
|
455
|
+
|
|
300
456
|
if (Array.isArray(node)) {
|
|
301
457
|
for (let i = 0; i < node.length; i++) {
|
|
302
458
|
node[i] = lower_child(node[i]);
|
|
303
|
-
wrap_control_flow_expression_values(node[i],
|
|
459
|
+
wrap_control_flow_expression_values(node[i], transform_context, seen);
|
|
304
460
|
}
|
|
305
461
|
return;
|
|
306
462
|
}
|
|
307
463
|
|
|
464
|
+
// Wrap a bare control-flow directive that is the sole value of a render-output
|
|
465
|
+
// slot in a native TSRX fragment, collapsing to its rendered value. (A `@{ … }`
|
|
466
|
+
// code block in the same slots already self-lowers to an IIFE, so it is left
|
|
467
|
+
// as-is.) These render-output slots are the only value positions a directive is
|
|
468
|
+
// allowed in; see `is_render_output_value_slot`.
|
|
469
|
+
const wrap_value = (/** @type {any} */ value) =>
|
|
470
|
+
is_jsx_control_flow_expression(value) ? wrap_in_native_tsrx_fragment(value) : value;
|
|
471
|
+
|
|
308
472
|
if (
|
|
309
473
|
node.type === 'ArrowFunctionExpression' &&
|
|
310
474
|
node.body?.type !== 'BlockStatement' &&
|
|
@@ -326,15 +490,28 @@ function wrap_control_flow_expression_values(node, lower_dynamic_context, seen =
|
|
|
326
490
|
(node.type === 'CallExpression' || node.type === 'NewExpression') &&
|
|
327
491
|
Array.isArray(node.arguments)
|
|
328
492
|
) {
|
|
329
|
-
node.arguments = node.arguments.map(
|
|
330
|
-
is_jsx_control_flow_expression(arg) ? wrap_in_native_tsrx_fragment(arg) : arg,
|
|
331
|
-
);
|
|
493
|
+
node.arguments = node.arguments.map(wrap_value);
|
|
332
494
|
}
|
|
333
495
|
|
|
334
496
|
for (const key of Object.keys(node)) {
|
|
335
497
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
336
|
-
|
|
337
|
-
|
|
498
|
+
// A directive is allowed as a render child/statement, and as the sole value
|
|
499
|
+
// of a render-output slot (handled above for control flow; `@{ … }` blocks
|
|
500
|
+
// self-lower). Everywhere else it is combined into an expression — wrap it.
|
|
501
|
+
const allowed_slot =
|
|
502
|
+
is_statement_or_template_slot(node, key) || is_render_output_value_slot(node, key);
|
|
503
|
+
const value = node[key];
|
|
504
|
+
if (Array.isArray(value)) {
|
|
505
|
+
for (let i = 0; i < value.length; i++) {
|
|
506
|
+
value[i] = lower_child(value[i]);
|
|
507
|
+
if (!allowed_slot) value[i] = wrap_directive_in_expression(value[i]);
|
|
508
|
+
wrap_control_flow_expression_values(value[i], transform_context, seen);
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
node[key] = lower_child(node[key]);
|
|
512
|
+
if (!allowed_slot) node[key] = wrap_directive_in_expression(node[key]);
|
|
513
|
+
wrap_control_flow_expression_values(node[key], transform_context, seen);
|
|
514
|
+
}
|
|
338
515
|
}
|
|
339
516
|
}
|
|
340
517
|
|
|
@@ -402,10 +579,7 @@ export function createJsxTransform(platform) {
|
|
|
402
579
|
};
|
|
403
580
|
|
|
404
581
|
expand_child_code_blocks(/** @type {any} */ (ast));
|
|
405
|
-
wrap_control_flow_expression_values(
|
|
406
|
-
/** @type {any} */ (ast),
|
|
407
|
-
platform.imports.dynamicFactory ? transform_context : null,
|
|
408
|
-
);
|
|
582
|
+
wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
|
|
409
583
|
|
|
410
584
|
if (!transform_context.typeOnly) {
|
|
411
585
|
preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
|
|
@@ -429,8 +603,37 @@ export function createJsxTransform(platform) {
|
|
|
429
603
|
|
|
430
604
|
const style_context = prepare_tsrx_fragment_styles(node, state);
|
|
431
605
|
const target = style_context?.fragment ?? next() ?? node;
|
|
432
|
-
|
|
433
|
-
|
|
606
|
+
// An EMPTY fragment that is the sole expression of a `{ … }` container in a
|
|
607
|
+
// JSX child slot (`<b>{<></>}</b>`) must stay `<></>`: the container already
|
|
608
|
+
// supplies the `{}` wrapper, so lowering it to a bare `null` (the default
|
|
609
|
+
// expression-position behavior) drops the source fragment. This matches how
|
|
610
|
+
// the same fragment is preserved in an attribute value (`a={<></>}`).
|
|
611
|
+
// Non-empty fragments keep their existing lowering.
|
|
612
|
+
const immediate_parent = /** @type {any} */ (path[path.length - 1]);
|
|
613
|
+
const is_empty_container_child =
|
|
614
|
+
immediate_parent?.type === 'JSXExpressionContainer' &&
|
|
615
|
+
in_jsx_child_context(path.slice(0, -1)) &&
|
|
616
|
+
!(target.children || []).some(
|
|
617
|
+
(/** @type {any} */ child) =>
|
|
618
|
+
child &&
|
|
619
|
+
child.type !== 'EmptyStatement' &&
|
|
620
|
+
(child.type !== 'JSXText' || child.value !== ''),
|
|
621
|
+
);
|
|
622
|
+
const in_jsx_child = in_jsx_child_context(path) || is_empty_container_child;
|
|
623
|
+
let expression = tsrx_node_to_jsx_expression(target, state, in_jsx_child);
|
|
624
|
+
// Keep a fragment's `<> … </>` identity in expression position when it is
|
|
625
|
+
// either AUTHORED (the author wrote `<>{1}</>`, so it must not unwrap to a
|
|
626
|
+
// bare `1`) or combined into a surrounding expression (collapsing `<>{0}</>`
|
|
627
|
+
// to `0` would flip `<>{0}</> || 'x'` from rendering `0` to `'x'` — a
|
|
628
|
+
// fragment is always truthy). A compiler-generated wrapper (around a
|
|
629
|
+
// control-flow directive) is NOT authored, so it still collapses.
|
|
630
|
+
if (
|
|
631
|
+
!in_jsx_child &&
|
|
632
|
+
(is_authored_native_fragment(node) ||
|
|
633
|
+
is_combined_expression_position(path[path.length - 1], node))
|
|
634
|
+
) {
|
|
635
|
+
expression = wrap_lowered_value_in_fragment(expression, node);
|
|
636
|
+
}
|
|
434
637
|
for (const statement of create_tsrx_style_ref_setup_statements(
|
|
435
638
|
target,
|
|
436
639
|
style_context,
|
|
@@ -544,6 +747,12 @@ export function createJsxTransform(platform) {
|
|
|
544
747
|
inject_try_imports(expanded, transform_context, platform, suspense_source);
|
|
545
748
|
}
|
|
546
749
|
|
|
750
|
+
// Lower any `@{ … }` code blocks left in generated helper bodies before the
|
|
751
|
+
// lazy transform runs, so every `@{ … }` block / `@`-directive has already
|
|
752
|
+
// been lowered to its final closure / block shape. The lazy transform can
|
|
753
|
+
// then walk the complete function structure in one pass.
|
|
754
|
+
lower_remaining_jsx_code_blocks(expanded, transform_context);
|
|
755
|
+
|
|
547
756
|
// Apply lazy destructuring transforms to module-level code (top-level function
|
|
548
757
|
// declarations, arrow functions, etc.).
|
|
549
758
|
// In type-only mode, the lazy patterns survive untouched: esrap ignores the
|
|
@@ -551,12 +760,25 @@ export function createJsxTransform(platform) {
|
|
|
551
760
|
// = expr` prints as `let [a] = expr`, and the bare statement-level form
|
|
552
761
|
// `&[x] = expr;` (used when `x` is already declared) prints as `[x] =
|
|
553
762
|
// expr;` — a valid destructuring assignment to the existing binding.
|
|
763
|
+
//
|
|
764
|
+
// Re-run `preallocate_lazy_ids` first. The initial pre-walk pass stamps
|
|
765
|
+
// `metadata.has_lazy_descendants` (the fast-path gate that tells
|
|
766
|
+
// `apply_lazy_transforms` a function body is worth walking) on the function
|
|
767
|
+
// boundaries that existed in the source. Lowering `@{ … }` blocks and
|
|
768
|
+
// `@if`/`@for`/`@switch`/`@try` directives introduces NEW function
|
|
769
|
+
// boundaries — scoped IIFEs and `.map(...)` callbacks — that wrap those same
|
|
770
|
+
// lazy patterns but were never stamped. Re-running over the lowered tree
|
|
771
|
+
// stamps them too (it is idempotent: already-allocated `lazy_id`s are kept),
|
|
772
|
+
// so lazy bindings declared inside a nested block or directive body are
|
|
773
|
+
// rewritten just like a flat function body.
|
|
774
|
+
if (!transform_context.typeOnly) {
|
|
775
|
+
preallocate_lazy_ids(/** @type {any} */ (expanded), transform_context);
|
|
776
|
+
}
|
|
554
777
|
const final_program = /** @type {any} */ (
|
|
555
778
|
transform_context.typeOnly
|
|
556
779
|
? expanded
|
|
557
780
|
: apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
|
|
558
781
|
);
|
|
559
|
-
lower_remaining_jsx_code_blocks(final_program, transform_context);
|
|
560
782
|
|
|
561
783
|
const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
|
|
562
784
|
sourceMapSource: filename,
|
|
@@ -932,13 +1154,33 @@ function lower_code_block_stream_node(block, transform_context) {
|
|
|
932
1154
|
* @param {any[]} body_nodes
|
|
933
1155
|
* @param {boolean} return_null_when_empty
|
|
934
1156
|
* @param {TransformContext} transform_context
|
|
1157
|
+
* @param {any} [source_authored_fragment] When the render output is an AUTHORED
|
|
1158
|
+
* `<> … </>` (`is_authored_native_fragment`), the built return value is re-wrapped
|
|
1159
|
+
* in a fragment so the author's fragment is kept verbatim (not collapsed to its
|
|
1160
|
+
* single child), matching value positions. A generated wrapper passes nothing.
|
|
935
1161
|
* @returns {any[]}
|
|
936
1162
|
*/
|
|
937
|
-
function build_render_statements(
|
|
1163
|
+
function build_render_statements(
|
|
1164
|
+
body_nodes,
|
|
1165
|
+
return_null_when_empty,
|
|
1166
|
+
transform_context,
|
|
1167
|
+
source_authored_fragment = null,
|
|
1168
|
+
) {
|
|
938
1169
|
body_nodes = body_nodes.flatMap((node) =>
|
|
939
1170
|
node?.type === 'JSXCodeBlock' ? lower_code_block_stream_node(node, transform_context) : [node],
|
|
940
1171
|
);
|
|
941
1172
|
|
|
1173
|
+
// When a caller (e.g. a directive branch / loop / switch-case body) passes the
|
|
1174
|
+
// authored `<> … </>` as the trailing body node rather than a pre-unwrapped child
|
|
1175
|
+
// list, detect it here so its wrapper is kept too. A generated wrapper carries
|
|
1176
|
+
// `tsrx_generated_wrapper`, so it is excluded and still collapses.
|
|
1177
|
+
if (!source_authored_fragment) {
|
|
1178
|
+
const last_body_node = body_nodes[body_nodes.length - 1];
|
|
1179
|
+
if (is_authored_native_fragment(last_body_node)) {
|
|
1180
|
+
source_authored_fragment = last_body_node;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
942
1184
|
const statements = [];
|
|
943
1185
|
const render_nodes = [];
|
|
944
1186
|
let has_terminal_return = false;
|
|
@@ -960,7 +1202,9 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
960
1202
|
const child = body_nodes[i];
|
|
961
1203
|
|
|
962
1204
|
if (is_loop_skip_return_statement(child)) {
|
|
963
|
-
statements.push(
|
|
1205
|
+
statements.push(
|
|
1206
|
+
create_component_return_statement(render_nodes, child, true, transform_context.typeOnly),
|
|
1207
|
+
);
|
|
964
1208
|
render_nodes.length = 0;
|
|
965
1209
|
has_terminal_return = true;
|
|
966
1210
|
continue;
|
|
@@ -1072,7 +1316,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
1072
1316
|
hoist_static_render_nodes(render_nodes, transform_context);
|
|
1073
1317
|
}
|
|
1074
1318
|
|
|
1075
|
-
|
|
1319
|
+
let return_arg = build_return_expression(render_nodes, false, transform_context.typeOnly);
|
|
1320
|
+
// Keep an authored `<> … </>` render output verbatim instead of collapsing it:
|
|
1321
|
+
// an empty `<></>` stays `<></>` (not `null`), and a single child stays wrapped
|
|
1322
|
+
// (not its bare value). The `!== 'JSXFragment'` guard avoids double-wrapping a
|
|
1323
|
+
// multi-child / nested result already returned as a fragment — matching the value
|
|
1324
|
+
// seam. A generated wrapper is not authored, so it still collapses.
|
|
1325
|
+
if (is_authored_native_fragment(source_authored_fragment)) {
|
|
1326
|
+
if (return_arg === null) {
|
|
1327
|
+
return_arg = set_loc(
|
|
1328
|
+
b.jsx_fragment([]),
|
|
1329
|
+
source_authored_fragment.loc ? source_authored_fragment : undefined,
|
|
1330
|
+
);
|
|
1331
|
+
} else if (return_arg.type !== 'JSXFragment') {
|
|
1332
|
+
return_arg = wrap_lowered_value_in_fragment(return_arg, source_authored_fragment);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1076
1335
|
if (return_arg || (return_null_when_empty && !has_terminal_return)) {
|
|
1077
1336
|
statements.push(b.return(return_arg || b.literal(null)));
|
|
1078
1337
|
}
|
|
@@ -1393,10 +1652,15 @@ function transform_return_statement(node, { next, visit, state, path }) {
|
|
|
1393
1652
|
function transform_jsx_code_block(node, { state, path, visit }) {
|
|
1394
1653
|
const body_nodes = get_jsx_code_block_body_nodes(node, state);
|
|
1395
1654
|
const parent = /** @type {any} */ (path.at(-1));
|
|
1655
|
+
// Keep an authored `<> … </>` trailing render output verbatim (a generated
|
|
1656
|
+
// control-flow wrapper carries `tsrx_generated_wrapper`, so it stays null).
|
|
1657
|
+
const render_authored_fragment = is_authored_native_fragment(node.render) ? node.render : null;
|
|
1396
1658
|
|
|
1397
1659
|
if (parent && parent.body === node && is_function_or_class_boundary(parent)) {
|
|
1398
1660
|
const block = b.block(
|
|
1399
|
-
mark_native_pretransformed_jsx(
|
|
1661
|
+
mark_native_pretransformed_jsx(
|
|
1662
|
+
build_render_statements(body_nodes, true, state, render_authored_fragment),
|
|
1663
|
+
),
|
|
1400
1664
|
node,
|
|
1401
1665
|
);
|
|
1402
1666
|
block.metadata = {
|
|
@@ -1410,7 +1674,9 @@ function transform_jsx_code_block(node, { state, path, visit }) {
|
|
|
1410
1674
|
b.arrow(
|
|
1411
1675
|
[],
|
|
1412
1676
|
b.block(
|
|
1413
|
-
mark_native_pretransformed_jsx(
|
|
1677
|
+
mark_native_pretransformed_jsx(
|
|
1678
|
+
build_render_statements(body_nodes, true, state, render_authored_fragment),
|
|
1679
|
+
),
|
|
1414
1680
|
node,
|
|
1415
1681
|
),
|
|
1416
1682
|
),
|
|
@@ -1483,7 +1749,11 @@ function lower_jsx_code_block_function_body(node) {
|
|
|
1483
1749
|
// component render output. Wrap it in a native fragment so it flows
|
|
1484
1750
|
// through the same children-rendering path as a `<> … </>` render.
|
|
1485
1751
|
const fragment = b.jsx_fragment([render]);
|
|
1486
|
-
fragment.metadata = {
|
|
1752
|
+
fragment.metadata = {
|
|
1753
|
+
...fragment.metadata,
|
|
1754
|
+
native_tsrx: true,
|
|
1755
|
+
tsrx_generated_wrapper: true,
|
|
1756
|
+
};
|
|
1487
1757
|
render = fragment;
|
|
1488
1758
|
}
|
|
1489
1759
|
statements.push(b.return(render, code_block.render));
|
|
@@ -2264,7 +2534,7 @@ function create_native_tsrx_render_statements(fragment, transform_context) {
|
|
|
2264
2534
|
target.type === 'JSXFragment' ? get_tsrx_render_children(target) : [target];
|
|
2265
2535
|
return [
|
|
2266
2536
|
...create_tsrx_style_ref_setup_statements(target, style_context, transform_context),
|
|
2267
|
-
...build_render_statements(render_nodes, true, transform_context),
|
|
2537
|
+
...build_render_statements(render_nodes, true, transform_context, fragment),
|
|
2268
2538
|
];
|
|
2269
2539
|
});
|
|
2270
2540
|
}
|
|
@@ -2793,7 +3063,12 @@ function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set
|
|
|
2793
3063
|
if (child?.type !== 'JSXCodeBlock') return [child];
|
|
2794
3064
|
const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
|
|
2795
3065
|
return mark_native_pretransformed_jsx(
|
|
2796
|
-
build_render_statements(
|
|
3066
|
+
build_render_statements(
|
|
3067
|
+
body_nodes,
|
|
3068
|
+
true,
|
|
3069
|
+
transform_context,
|
|
3070
|
+
is_authored_native_fragment(child.render) ? child.render : null,
|
|
3071
|
+
),
|
|
2797
3072
|
);
|
|
2798
3073
|
});
|
|
2799
3074
|
}
|
|
@@ -2869,18 +3144,23 @@ function get_generated_component_metadata_list(node) {
|
|
|
2869
3144
|
* @param {any[]} render_nodes
|
|
2870
3145
|
* @param {any} source_node
|
|
2871
3146
|
* @param {boolean} [map_render_node_locations]
|
|
3147
|
+
* @param {boolean} [type_only]
|
|
2872
3148
|
* @returns {any}
|
|
2873
3149
|
*/
|
|
2874
3150
|
function create_component_return_statement(
|
|
2875
3151
|
render_nodes,
|
|
2876
3152
|
source_node,
|
|
2877
3153
|
map_render_node_locations = true,
|
|
3154
|
+
type_only = false,
|
|
2878
3155
|
) {
|
|
2879
3156
|
const cloned = render_nodes.map((node) =>
|
|
2880
3157
|
map_render_node_locations ? clone_expression_node(node) : clone_expression_node(node, false),
|
|
2881
3158
|
);
|
|
2882
3159
|
|
|
2883
|
-
return set_loc(
|
|
3160
|
+
return set_loc(
|
|
3161
|
+
b.return(build_return_expression(cloned, false, type_only) || create_null_literal()),
|
|
3162
|
+
source_node,
|
|
3163
|
+
);
|
|
2884
3164
|
}
|
|
2885
3165
|
|
|
2886
3166
|
/**
|
|
@@ -2923,7 +3203,11 @@ function get_loop_skip_if_consequent_body(node) {
|
|
|
2923
3203
|
function create_component_loop_skip_if_statement(node, render_nodes, transform_context) {
|
|
2924
3204
|
const consequent_body = /** @type {any[]} */ (get_loop_skip_if_consequent_body(node));
|
|
2925
3205
|
const branch_statements = build_render_statements(consequent_body, true, transform_context);
|
|
2926
|
-
prepend_render_nodes_to_return_statements(
|
|
3206
|
+
prepend_render_nodes_to_return_statements(
|
|
3207
|
+
branch_statements,
|
|
3208
|
+
render_nodes,
|
|
3209
|
+
transform_context.typeOnly,
|
|
3210
|
+
);
|
|
2927
3211
|
|
|
2928
3212
|
const statement = set_loc(
|
|
2929
3213
|
b.if(node.test, set_loc(b.block(branch_statements), node.consequent), null),
|
|
@@ -2939,15 +3223,16 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
|
|
|
2939
3223
|
/**
|
|
2940
3224
|
* @param {any[]} statements
|
|
2941
3225
|
* @param {any[]} render_nodes
|
|
3226
|
+
* @param {boolean} [type_only]
|
|
2942
3227
|
* @returns {void}
|
|
2943
3228
|
*/
|
|
2944
|
-
function prepend_render_nodes_to_return_statements(statements, render_nodes) {
|
|
3229
|
+
function prepend_render_nodes_to_return_statements(statements, render_nodes, type_only = false) {
|
|
2945
3230
|
if (render_nodes.length === 0) {
|
|
2946
3231
|
return;
|
|
2947
3232
|
}
|
|
2948
3233
|
|
|
2949
3234
|
for (const statement of statements) {
|
|
2950
|
-
prepend_render_nodes_to_return_statement(statement, render_nodes, false);
|
|
3235
|
+
prepend_render_nodes_to_return_statement(statement, render_nodes, false, type_only);
|
|
2951
3236
|
}
|
|
2952
3237
|
}
|
|
2953
3238
|
|
|
@@ -2955,9 +3240,15 @@ function prepend_render_nodes_to_return_statements(statements, render_nodes) {
|
|
|
2955
3240
|
* @param {any} node
|
|
2956
3241
|
* @param {any[]} render_nodes
|
|
2957
3242
|
* @param {boolean} inside_nested_function
|
|
3243
|
+
* @param {boolean} [type_only]
|
|
2958
3244
|
* @returns {void}
|
|
2959
3245
|
*/
|
|
2960
|
-
function prepend_render_nodes_to_return_statement(
|
|
3246
|
+
function prepend_render_nodes_to_return_statement(
|
|
3247
|
+
node,
|
|
3248
|
+
render_nodes,
|
|
3249
|
+
inside_nested_function,
|
|
3250
|
+
type_only = false,
|
|
3251
|
+
) {
|
|
2961
3252
|
if (!node || typeof node !== 'object') {
|
|
2962
3253
|
return;
|
|
2963
3254
|
}
|
|
@@ -2971,13 +3262,18 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
|
|
|
2971
3262
|
}
|
|
2972
3263
|
|
|
2973
3264
|
if (!inside_nested_function && node.type === 'ReturnStatement') {
|
|
2974
|
-
node.argument = combine_render_return_argument(render_nodes, node.argument);
|
|
3265
|
+
node.argument = combine_render_return_argument(render_nodes, node.argument, type_only);
|
|
2975
3266
|
return;
|
|
2976
3267
|
}
|
|
2977
3268
|
|
|
2978
3269
|
if (Array.isArray(node)) {
|
|
2979
3270
|
for (const child of node) {
|
|
2980
|
-
prepend_render_nodes_to_return_statement(
|
|
3271
|
+
prepend_render_nodes_to_return_statement(
|
|
3272
|
+
child,
|
|
3273
|
+
render_nodes,
|
|
3274
|
+
inside_nested_function,
|
|
3275
|
+
type_only,
|
|
3276
|
+
);
|
|
2981
3277
|
}
|
|
2982
3278
|
return;
|
|
2983
3279
|
}
|
|
@@ -2986,23 +3282,29 @@ function prepend_render_nodes_to_return_statement(node, render_nodes, inside_nes
|
|
|
2986
3282
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
|
|
2987
3283
|
continue;
|
|
2988
3284
|
}
|
|
2989
|
-
prepend_render_nodes_to_return_statement(
|
|
3285
|
+
prepend_render_nodes_to_return_statement(
|
|
3286
|
+
node[key],
|
|
3287
|
+
render_nodes,
|
|
3288
|
+
inside_nested_function,
|
|
3289
|
+
type_only,
|
|
3290
|
+
);
|
|
2990
3291
|
}
|
|
2991
3292
|
}
|
|
2992
3293
|
|
|
2993
3294
|
/**
|
|
2994
3295
|
* @param {any[]} render_nodes
|
|
2995
3296
|
* @param {any} return_argument
|
|
3297
|
+
* @param {boolean} [type_only]
|
|
2996
3298
|
* @returns {any}
|
|
2997
3299
|
*/
|
|
2998
|
-
function combine_render_return_argument(render_nodes, return_argument) {
|
|
3300
|
+
function combine_render_return_argument(render_nodes, return_argument, type_only = false) {
|
|
2999
3301
|
const combined = render_nodes.map((node) => clone_expression_node(node, false));
|
|
3000
3302
|
|
|
3001
3303
|
if (return_argument != null && !is_null_literal(return_argument)) {
|
|
3002
3304
|
combined.push(return_argument_to_render_node(return_argument));
|
|
3003
3305
|
}
|
|
3004
3306
|
|
|
3005
|
-
return build_return_expression(combined) || create_null_literal();
|
|
3307
|
+
return build_return_expression(combined, false, type_only) || create_null_literal();
|
|
3006
3308
|
}
|
|
3007
3309
|
|
|
3008
3310
|
/**
|
|
@@ -4011,7 +4313,9 @@ export function wrap_edge_whitespace(nodes) {
|
|
|
4011
4313
|
}
|
|
4012
4314
|
}
|
|
4013
4315
|
if (value !== '') {
|
|
4014
|
-
|
|
4316
|
+
// keep the location as we need it for @ autocomplete
|
|
4317
|
+
// and perhaps other things in the future
|
|
4318
|
+
out.push(b.jsx_text(value, value, node));
|
|
4015
4319
|
}
|
|
4016
4320
|
if (trailing) {
|
|
4017
4321
|
out.push(trailing);
|
|
@@ -4111,9 +4415,22 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
|
|
|
4111
4415
|
/** @type {any} */
|
|
4112
4416
|
let expression;
|
|
4113
4417
|
if (children.length === 0) {
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4418
|
+
// An empty fragment is a real value: keep it as `<></>` in BOTH child and
|
|
4419
|
+
// expression position. Lowering it to a bare `null` in expression position
|
|
4420
|
+
// (e.g. `let b = <></>`) drops the author's fragment and changes its type;
|
|
4421
|
+
// `<></>` is a valid value and keeps the to_ts/runtime view faithful.
|
|
4422
|
+
expression = set_loc(b.jsx_fragment([]), node.loc ? node : undefined);
|
|
4423
|
+
} else if (
|
|
4424
|
+
children.length === 1 &&
|
|
4425
|
+
(is_empty_jsx_fragment(children[0]) ||
|
|
4426
|
+
(children[0]?.type === 'JSXFragment' && is_authored_native_fragment(node)))
|
|
4427
|
+
) {
|
|
4428
|
+
// `<><X></></>` — a fragment whose only child is a fragment. The generic
|
|
4429
|
+
// single-child collapse below would unwrap it to the bare inner fragment,
|
|
4430
|
+
// dropping the outer fragment the author wrote. Keep both levels. (`<><></></>`
|
|
4431
|
+
// is kept regardless; a non-empty inner is only kept for an authored outer, so
|
|
4432
|
+
// a generated wrapper still collapses.)
|
|
4433
|
+
expression = set_loc(b.jsx_fragment(children), node.loc ? node : undefined);
|
|
4117
4434
|
} else {
|
|
4118
4435
|
expression = return_value_body_to_expression(children, node, transform_context);
|
|
4119
4436
|
}
|
|
@@ -4126,7 +4443,9 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
|
|
|
4126
4443
|
const render_nodes = wrap_edge_whitespace(
|
|
4127
4444
|
children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context)),
|
|
4128
4445
|
);
|
|
4129
|
-
expression =
|
|
4446
|
+
expression =
|
|
4447
|
+
build_return_expression(render_nodes, in_jsx_child, transform_context.typeOnly) ||
|
|
4448
|
+
create_null_literal();
|
|
4130
4449
|
} finally {
|
|
4131
4450
|
transform_context.inside_element_child = saved_inside_element_child;
|
|
4132
4451
|
}
|
|
@@ -5319,7 +5638,12 @@ function build_switch_with_lift(switch_node, transform_context) {
|
|
|
5319
5638
|
if (helper) {
|
|
5320
5639
|
return set_loc(
|
|
5321
5640
|
b.switch_case(original_case.test, [
|
|
5322
|
-
create_component_return_statement(
|
|
5641
|
+
create_component_return_statement(
|
|
5642
|
+
[helper.component_element],
|
|
5643
|
+
original_case,
|
|
5644
|
+
true,
|
|
5645
|
+
transform_context.typeOnly,
|
|
5646
|
+
),
|
|
5323
5647
|
]),
|
|
5324
5648
|
original_case,
|
|
5325
5649
|
);
|
|
@@ -5340,7 +5664,14 @@ function build_switch_with_lift(switch_node, transform_context) {
|
|
|
5340
5664
|
|
|
5341
5665
|
for (const child of own_body) {
|
|
5342
5666
|
if (is_loop_skip_return_statement(child)) {
|
|
5343
|
-
case_body.push(
|
|
5667
|
+
case_body.push(
|
|
5668
|
+
create_component_return_statement(
|
|
5669
|
+
render_nodes,
|
|
5670
|
+
child,
|
|
5671
|
+
true,
|
|
5672
|
+
transform_context.typeOnly,
|
|
5673
|
+
),
|
|
5674
|
+
);
|
|
5344
5675
|
has_terminal = true;
|
|
5345
5676
|
break;
|
|
5346
5677
|
}
|
|
@@ -5360,7 +5691,14 @@ function build_switch_with_lift(switch_node, transform_context) {
|
|
|
5360
5691
|
|
|
5361
5692
|
if (!has_terminal) {
|
|
5362
5693
|
if (render_nodes.length > 0) {
|
|
5363
|
-
case_body.push(
|
|
5694
|
+
case_body.push(
|
|
5695
|
+
create_component_return_statement(
|
|
5696
|
+
render_nodes,
|
|
5697
|
+
original_case,
|
|
5698
|
+
true,
|
|
5699
|
+
transform_context.typeOnly,
|
|
5700
|
+
),
|
|
5701
|
+
);
|
|
5364
5702
|
} else if (case_body.length > 0) {
|
|
5365
5703
|
case_body.push(create_null_return_statement());
|
|
5366
5704
|
} else if (has_terminator) {
|
|
@@ -5913,25 +6251,29 @@ function value_has_unmappable_jsx_loc(value) {
|
|
|
5913
6251
|
* @param {boolean} [in_jsx_child]
|
|
5914
6252
|
* @returns {any}
|
|
5915
6253
|
*/
|
|
5916
|
-
function build_return_expression(render_nodes, in_jsx_child = false) {
|
|
6254
|
+
export function build_return_expression(render_nodes, in_jsx_child = false, type_only = false) {
|
|
5917
6255
|
if (render_nodes.length === 0) return null;
|
|
5918
6256
|
if (render_nodes.length === 1) {
|
|
5919
6257
|
const only = render_nodes[0];
|
|
5920
6258
|
if (only.type === 'JSXExpressionContainer') {
|
|
5921
|
-
// Reactive-block containers (dynamic tags) must stay expression
|
|
5922
|
-
// children so the host JSX compiler wraps them in a render block;
|
|
5923
|
-
// returning the bare call would evaluate them once.
|
|
5924
6259
|
if (only.metadata?.tsrx_reactive_block === true) {
|
|
5925
6260
|
return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
|
|
5926
6261
|
}
|
|
6262
|
+
if (only.expression?.type === 'JSXEmptyExpression') {
|
|
6263
|
+
return set_loc(b.jsx_fragment([]), only.loc ? only : undefined);
|
|
6264
|
+
}
|
|
5927
6265
|
return only.expression;
|
|
5928
6266
|
}
|
|
5929
6267
|
if (only.type === 'JSXText') {
|
|
5930
|
-
|
|
5931
|
-
|
|
6268
|
+
// Keep a single text child faithful to the source (e.g. `<>@</>`) — never
|
|
6269
|
+
// promote it to a `{'text'}` string-literal expression, in either the
|
|
6270
|
+
// type-only editor view or runtime codegen. At runtime we additionally drop a
|
|
6271
|
+
// nullish/whitespace-only child so it renders nothing instead of emitting
|
|
6272
|
+
// empty output.
|
|
6273
|
+
if (!type_only && !in_jsx_child && (only.value ?? '').trim() === '') {
|
|
6274
|
+
return null;
|
|
5932
6275
|
}
|
|
5933
|
-
|
|
5934
|
-
return b.literal(value, JSON.stringify(value), only);
|
|
6276
|
+
return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
|
|
5935
6277
|
}
|
|
5936
6278
|
return only;
|
|
5937
6279
|
}
|
package/src/transform/lazy.js
CHANGED
|
@@ -99,38 +99,6 @@ function get_lazy_pattern_mapping_range(pattern) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
* Synthesize an object-shaped annotation for untyped lazy object params so the
|
|
104
|
-
* virtual TSX can expose prop names to TypeScript completions.
|
|
105
|
-
*
|
|
106
|
-
* @param {any} pattern
|
|
107
|
-
* @returns {any | null}
|
|
108
|
-
*/
|
|
109
|
-
function create_lazy_object_type_annotation(pattern) {
|
|
110
|
-
if (pattern.type !== 'ObjectPattern') return null;
|
|
111
|
-
|
|
112
|
-
const members = [];
|
|
113
|
-
for (const prop of pattern.properties || []) {
|
|
114
|
-
if (prop.type === 'RestElement' || prop.computed) continue;
|
|
115
|
-
|
|
116
|
-
const key = prop.key;
|
|
117
|
-
if (key.type !== 'Identifier' && key.type !== 'Literal') continue;
|
|
118
|
-
|
|
119
|
-
const member_key =
|
|
120
|
-
key.type === 'Identifier'
|
|
121
|
-
? create_generated_identifier(key.name, key)
|
|
122
|
-
: set_source_location({ ...key, metadata: { path: [] } }, key);
|
|
123
|
-
|
|
124
|
-
members.push(
|
|
125
|
-
b.ts_property_signature(member_key, b.ts_type_annotation(b.ts_keyword_type('any'))),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (members.length === 0) return null;
|
|
130
|
-
|
|
131
|
-
return b.ts_type_annotation(b.ts_type_literal(members));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
102
|
/**
|
|
135
103
|
* @param {any} node
|
|
136
104
|
* @returns {string | null}
|
|
@@ -348,12 +316,14 @@ function visit_topmost_lazy_patterns(pattern, visit) {
|
|
|
348
316
|
|
|
349
317
|
/**
|
|
350
318
|
* Build the replacement identifier for a lazy pattern. When `is_top` is true
|
|
351
|
-
* (the pattern is itself a function parameter) we
|
|
352
|
-
* `typeAnnotation
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
319
|
+
* (the pattern is itself a function parameter) we carry over the author's
|
|
320
|
+
* `typeAnnotation` if one was written and register source-mapping info. An
|
|
321
|
+
* untyped lazy object param gets NO synthesized annotation: since the source
|
|
322
|
+
* specified no type, the generated param is left implicitly `any` rather than
|
|
323
|
+
* being given a fabricated `{ … : any }` object type. Nested replacements
|
|
324
|
+
* (inside a non-lazy outer destructure) can't carry an inline type annotation —
|
|
325
|
+
* that's not valid syntax — so they get a plain identifier with just source-range
|
|
326
|
+
* info.
|
|
357
327
|
*
|
|
358
328
|
* @param {any} pattern
|
|
359
329
|
* @param {boolean} is_top
|
|
@@ -372,9 +342,6 @@ function build_lazy_id_for_pattern(pattern, is_top) {
|
|
|
372
342
|
if (!is_top) return lazy_id;
|
|
373
343
|
if (pattern.typeAnnotation) {
|
|
374
344
|
lazy_id.typeAnnotation = pattern.typeAnnotation;
|
|
375
|
-
} else {
|
|
376
|
-
const type_annotation = create_lazy_object_type_annotation(pattern);
|
|
377
|
-
if (type_annotation) lazy_id.typeAnnotation = type_annotation;
|
|
378
345
|
}
|
|
379
346
|
set_lazy_param_binding_mappings(lazy_id, pattern);
|
|
380
347
|
return lazy_id;
|
|
@@ -511,6 +478,53 @@ export function preallocate_lazy_ids(root, context) {
|
|
|
511
478
|
visit(root);
|
|
512
479
|
}
|
|
513
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Convert an ESTree member-access chain (`__lazy0.Item`, `__lazy0.a.b`) into the
|
|
483
|
+
* equivalent JSX element-name node (a `JSXIdentifier` or `JSXMemberExpression`),
|
|
484
|
+
* so a lazy binding used as a component/element name (`<Item>`) can be rewritten
|
|
485
|
+
* to `<__lazy0.Item>`. Returns null for a computed access (`__lazy0[0]`, from an
|
|
486
|
+
* array destructure), which has no JSX-name form.
|
|
487
|
+
*
|
|
488
|
+
* @param {any} expr
|
|
489
|
+
* @param {any} [source]
|
|
490
|
+
* @returns {any | null}
|
|
491
|
+
*/
|
|
492
|
+
function estree_member_to_jsx_name(expr, source) {
|
|
493
|
+
if (expr.type === 'Identifier') {
|
|
494
|
+
return set_source_location(b.jsx_id(expr.name), source);
|
|
495
|
+
}
|
|
496
|
+
if (expr.type === 'MemberExpression' && !expr.computed && expr.property?.type === 'Identifier') {
|
|
497
|
+
const object = estree_member_to_jsx_name(expr.object, source);
|
|
498
|
+
if (!object) return null;
|
|
499
|
+
return set_source_location(b.jsx_member(object, b.jsx_id(expr.property.name)), source);
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* If a JSX element/component name references a lazy binding, return the rewritten
|
|
506
|
+
* JSX name (`<Item>` → `<__lazy0.Item>`, `<Item.Sub>` → `<__lazy0.Item.Sub>`).
|
|
507
|
+
* Returns null when the name does not reference a lazy binding (or the access has
|
|
508
|
+
* no JSX-name form), so the caller leaves the original name untouched.
|
|
509
|
+
*
|
|
510
|
+
* @param {any} name
|
|
511
|
+
* @param {Map<string, LazyBinding>} lazy_bindings
|
|
512
|
+
* @returns {any | null}
|
|
513
|
+
*/
|
|
514
|
+
function rewrite_lazy_jsx_name(name, lazy_bindings) {
|
|
515
|
+
if (!name) return null;
|
|
516
|
+
if (name.type === 'JSXIdentifier') {
|
|
517
|
+
const binding = lazy_bindings.get(name.name);
|
|
518
|
+
if (!binding) return null;
|
|
519
|
+
return estree_member_to_jsx_name(binding.read(name), name);
|
|
520
|
+
}
|
|
521
|
+
if (name.type === 'JSXMemberExpression') {
|
|
522
|
+
const new_object = rewrite_lazy_jsx_name(name.object, lazy_bindings);
|
|
523
|
+
return new_object ? { ...name, object: new_object } : null;
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
514
528
|
/**
|
|
515
529
|
* Recursively rewrite lazy-binding references in `node`.
|
|
516
530
|
*
|
|
@@ -669,16 +683,36 @@ export function apply_lazy_transforms(node, lazy_bindings) {
|
|
|
669
683
|
}
|
|
670
684
|
|
|
671
685
|
if (node.type === 'SwitchStatement') {
|
|
686
|
+
// All case consequents share one lexical block scope, and `@switch`
|
|
687
|
+
// case bodies are flattened (the `{ … }` wrapper is dropped), so a
|
|
688
|
+
// `let &[x] = …` / `let &{ … } = …` declared in a case body is scoped to
|
|
689
|
+
// the whole switch block. Collect names and lazy bindings across every
|
|
690
|
+
// consequent — like the BlockStatement handler does for a block body — so
|
|
691
|
+
// references in any case resolve to the generated id, and an inner
|
|
692
|
+
// declaration shadowing an outer lazy name is dropped.
|
|
693
|
+
const all_consequents = node.cases.flatMap(
|
|
694
|
+
(/** @type {any} */ switch_case) => switch_case.consequent,
|
|
695
|
+
);
|
|
696
|
+
const block_shadowed = collect_block_shadowed_names(all_consequents, lazy_bindings);
|
|
697
|
+
const after_shadow =
|
|
698
|
+
block_shadowed.size > 0 ? remove_shadowed(lazy_bindings, block_shadowed) : lazy_bindings;
|
|
699
|
+
|
|
700
|
+
/** @type {Map<string, LazyBinding>} */
|
|
701
|
+
const block_lazy = new Map();
|
|
702
|
+
collect_lazy_bindings_from_statements(all_consequents, block_lazy);
|
|
703
|
+
const effective_bindings =
|
|
704
|
+
block_lazy.size > 0 ? new Map([...after_shadow, ...block_lazy]) : after_shadow;
|
|
705
|
+
|
|
672
706
|
let changed = false;
|
|
673
|
-
|
|
707
|
+
// The discriminant is evaluated before any case body runs, so it sees the
|
|
708
|
+
// bindings visible at the switch (outer, minus inner shadows), not a lazy
|
|
709
|
+
// binding declared inside a case body.
|
|
710
|
+
const new_discriminant = apply_lazy_transforms(node.discriminant, after_shadow);
|
|
674
711
|
if (new_discriminant !== node.discriminant) changed = true;
|
|
675
712
|
const new_cases = node.cases.map((/** @type {any} */ switch_case) => {
|
|
676
|
-
const case_bindings = collect_block_shadowed_names(switch_case.consequent, lazy_bindings);
|
|
677
|
-
const effective_bindings =
|
|
678
|
-
case_bindings.size > 0 ? remove_shadowed(lazy_bindings, case_bindings) : lazy_bindings;
|
|
679
713
|
let case_changed = false;
|
|
680
714
|
const new_test = switch_case.test
|
|
681
|
-
? apply_lazy_transforms(switch_case.test,
|
|
715
|
+
? apply_lazy_transforms(switch_case.test, effective_bindings)
|
|
682
716
|
: null;
|
|
683
717
|
if (new_test !== switch_case.test) case_changed = true;
|
|
684
718
|
const new_consequent = switch_case.consequent.map((/** @type {any} */ stmt) => {
|
|
@@ -823,6 +857,21 @@ export function apply_lazy_transforms(node, lazy_bindings) {
|
|
|
823
857
|
// Skip VariableDeclarator id (already handled above).
|
|
824
858
|
if (key === 'id' && node.type === 'VariableDeclarator') continue;
|
|
825
859
|
|
|
860
|
+
// A JSX element/component name that resolves to a lazy binding becomes a
|
|
861
|
+
// JSX member expression (`<Item>` → `<__lazy0.Item>`): the bound name is no
|
|
862
|
+
// longer a local, it is a property of the synthesized lazy source.
|
|
863
|
+
if (
|
|
864
|
+
key === 'name' &&
|
|
865
|
+
(node.type === 'JSXOpeningElement' || node.type === 'JSXClosingElement')
|
|
866
|
+
) {
|
|
867
|
+
const jsx_name = rewrite_lazy_jsx_name(node[key], lazy_bindings);
|
|
868
|
+
if (jsx_name) {
|
|
869
|
+
clone[key] = jsx_name;
|
|
870
|
+
changed = true;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
826
875
|
const new_value = apply_lazy_transforms(node[key], lazy_bindings);
|
|
827
876
|
if (new_value !== node[key]) {
|
|
828
877
|
clone[key] = new_value;
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
mapping_data,
|
|
54
54
|
mapping_data_verify_only,
|
|
55
55
|
mapping_data_verify_complete,
|
|
56
|
+
mapping_data_completion_only,
|
|
56
57
|
build_line_offsets,
|
|
57
58
|
get_mapping_from_node,
|
|
58
59
|
} from '../source-map-utils.js';
|
|
@@ -806,7 +807,27 @@ export function convert_source_map_to_mappings(
|
|
|
806
807
|
}
|
|
807
808
|
return;
|
|
808
809
|
} else if (node.type === 'JSXText') {
|
|
809
|
-
//
|
|
810
|
+
// A text node whose first non-whitespace char is `@` is an in-progress template
|
|
811
|
+
// directive (`@`, `@i`, `@if …`) the parser recovered as text; emit a completion-only
|
|
812
|
+
// mapping so the editor can still offer `@if`/`@for`/`@switch`/`@try` completions there.
|
|
813
|
+
//
|
|
814
|
+
// Use a token (resolved by matching generated CONTENT) rather than get_mapping_from_node.
|
|
815
|
+
// At a control-flow boundary the text node's source start maps to several generated
|
|
816
|
+
// positions — e.g. a preceding `@switch` value-IIFE's `return null; })()` tail AND the
|
|
817
|
+
// text itself — and get_mapping_from_node just takes the first, so its generated length
|
|
818
|
+
// spans the wrong region and the editor can't map a completion's edit back to source
|
|
819
|
+
// (it then drops the item). The token resolves to the position whose generated text
|
|
820
|
+
// matches the node's value, giving a well-formed same-length mapping. Ripple keeps text
|
|
821
|
+
// verbatim in to_ts, so `source` and `generated` are identical. Other text stays unmapped.
|
|
822
|
+
if (node.loc && typeof node.value === 'string' && node.value.trimStart().startsWith('@')) {
|
|
823
|
+
tokens.push({
|
|
824
|
+
source: node.value,
|
|
825
|
+
generated: node.value,
|
|
826
|
+
loc: node.loc,
|
|
827
|
+
metadata: {},
|
|
828
|
+
mappingData: mapping_data_completion_only,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
810
831
|
return;
|
|
811
832
|
} else if (node.type === 'JSXCodeBlock') {
|
|
812
833
|
for (const statement of node.body) {
|
package/src/utils/builders.js
CHANGED
|
@@ -1369,15 +1369,18 @@ export function jsx_spread_attribute(argument, loc_info) {
|
|
|
1369
1369
|
/**
|
|
1370
1370
|
* @param {string} value
|
|
1371
1371
|
* @param {string} raw
|
|
1372
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
1372
1373
|
* @returns {ESTreeJSX.JSXText}
|
|
1373
1374
|
*/
|
|
1374
|
-
export function jsx_text(value, raw) {
|
|
1375
|
-
|
|
1375
|
+
export function jsx_text(value, raw, loc_info) {
|
|
1376
|
+
const node = /** @type {ESTreeJSX.JSXText} */ ({
|
|
1376
1377
|
type: 'JSXText',
|
|
1377
1378
|
value,
|
|
1378
1379
|
raw,
|
|
1379
1380
|
metadata: { path: [] },
|
|
1380
|
-
};
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
return set_location(node, loc_info);
|
|
1381
1384
|
}
|
|
1382
1385
|
|
|
1383
1386
|
/**
|
package/types/index.d.ts
CHANGED
|
@@ -83,6 +83,7 @@ interface BaseNodeMetaData {
|
|
|
83
83
|
commentContainerId?: number;
|
|
84
84
|
parenthesized?: boolean;
|
|
85
85
|
native_tsrx?: boolean;
|
|
86
|
+
tsrx_generated_wrapper?: boolean;
|
|
86
87
|
native_tsrx_template_block?: boolean;
|
|
87
88
|
dynamicElement?: boolean;
|
|
88
89
|
templateMode?: 'script' | 'template';
|
|
@@ -137,6 +138,7 @@ type AcornTSNode<T> = Omit<T, 'parent' | 'loc' | 'range' | 'expression'> & {
|
|
|
137
138
|
|
|
138
139
|
leadingComments?: AST.Comment[] | undefined;
|
|
139
140
|
trailingComments?: AST.Comment[] | undefined;
|
|
141
|
+
append_into?: AST.Identifier;
|
|
140
142
|
};
|
|
141
143
|
|
|
142
144
|
interface FunctionLikeTS {
|
|
@@ -295,6 +297,8 @@ declare module 'estree' {
|
|
|
295
297
|
TSAsExpression: TSAsExpression;
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
type TraversableAstNode = AST.Node & Record<string, unknown>;
|
|
301
|
+
|
|
298
302
|
// Ripple-normalized template node shapes. The core parser emits JSX-shaped
|
|
299
303
|
// TSRX nodes; @tsrx/ripple creates these during its normalization pass.
|
|
300
304
|
interface Attribute extends AST.BaseNode {
|
|
@@ -516,6 +520,8 @@ declare module 'estree' {
|
|
|
516
520
|
metadata: BaseNodeMetaData;
|
|
517
521
|
|
|
518
522
|
comments?: Comment[];
|
|
523
|
+
|
|
524
|
+
append_into?: AST.Identifier;
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
interface NodeWithLocation {
|
|
@@ -1474,6 +1480,13 @@ export interface TransformServerState extends BaseState {
|
|
|
1474
1480
|
dev?: boolean;
|
|
1475
1481
|
return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
|
|
1476
1482
|
template_child?: boolean;
|
|
1483
|
+
/**
|
|
1484
|
+
* True while transforming the direct body of a control-flow branch
|
|
1485
|
+
* (`@if`/`@else`/`@for`/`@switch`/`@try`). A `<>…</>` in this position is
|
|
1486
|
+
* bracketed with hydration block markers so the client's fragment
|
|
1487
|
+
* `expression()` finds a matching boundary during hydration.
|
|
1488
|
+
*/
|
|
1489
|
+
control_flow_branch_body?: boolean;
|
|
1477
1490
|
skip_regular_blocks?: boolean;
|
|
1478
1491
|
in_regular_block?: boolean;
|
|
1479
1492
|
is_tsrx_element?: boolean;
|
|
@@ -1558,7 +1571,10 @@ export type TransformClientContext = Context<AST.Node, TransformClientState>;
|
|
|
1558
1571
|
export type TransformServerContext = Context<AST.Node, TransformServerState>;
|
|
1559
1572
|
export type AnalysisContext = Context<AST.Node, AnalysisState>;
|
|
1560
1573
|
export type CommonContext = TransformClientContext | TransformServerContext | AnalysisContext;
|
|
1561
|
-
export type VisitorClientContext = TransformClientContext & {
|
|
1574
|
+
export type VisitorClientContext = TransformClientContext & {
|
|
1575
|
+
root?: boolean;
|
|
1576
|
+
value_position?: boolean;
|
|
1577
|
+
};
|
|
1562
1578
|
|
|
1563
1579
|
/**
|
|
1564
1580
|
* Delegated event result
|