@tsrx/core 0.1.32 → 0.1.33
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/transform/jsx/helpers.js +14 -39
- package/src/transform/jsx/index.js +325 -39
- package/src/transform/lazy.js +95 -46
- package/types/index.d.ts +6 -0
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;
|
|
@@ -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;
|
|
@@ -1072,7 +1314,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
1072
1314
|
hoist_static_render_nodes(render_nodes, transform_context);
|
|
1073
1315
|
}
|
|
1074
1316
|
|
|
1075
|
-
|
|
1317
|
+
let return_arg = build_return_expression(render_nodes);
|
|
1318
|
+
// Keep an authored `<> … </>` render output verbatim instead of collapsing it:
|
|
1319
|
+
// an empty `<></>` stays `<></>` (not `null`), and a single child stays wrapped
|
|
1320
|
+
// (not its bare value). The `!== 'JSXFragment'` guard avoids double-wrapping a
|
|
1321
|
+
// multi-child / nested result already returned as a fragment — matching the value
|
|
1322
|
+
// seam. A generated wrapper is not authored, so it still collapses.
|
|
1323
|
+
if (is_authored_native_fragment(source_authored_fragment)) {
|
|
1324
|
+
if (return_arg === null) {
|
|
1325
|
+
return_arg = set_loc(
|
|
1326
|
+
b.jsx_fragment([]),
|
|
1327
|
+
source_authored_fragment.loc ? source_authored_fragment : undefined,
|
|
1328
|
+
);
|
|
1329
|
+
} else if (return_arg.type !== 'JSXFragment') {
|
|
1330
|
+
return_arg = wrap_lowered_value_in_fragment(return_arg, source_authored_fragment);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1076
1333
|
if (return_arg || (return_null_when_empty && !has_terminal_return)) {
|
|
1077
1334
|
statements.push(b.return(return_arg || b.literal(null)));
|
|
1078
1335
|
}
|
|
@@ -1393,10 +1650,15 @@ function transform_return_statement(node, { next, visit, state, path }) {
|
|
|
1393
1650
|
function transform_jsx_code_block(node, { state, path, visit }) {
|
|
1394
1651
|
const body_nodes = get_jsx_code_block_body_nodes(node, state);
|
|
1395
1652
|
const parent = /** @type {any} */ (path.at(-1));
|
|
1653
|
+
// Keep an authored `<> … </>` trailing render output verbatim (a generated
|
|
1654
|
+
// control-flow wrapper carries `tsrx_generated_wrapper`, so it stays null).
|
|
1655
|
+
const render_authored_fragment = is_authored_native_fragment(node.render) ? node.render : null;
|
|
1396
1656
|
|
|
1397
1657
|
if (parent && parent.body === node && is_function_or_class_boundary(parent)) {
|
|
1398
1658
|
const block = b.block(
|
|
1399
|
-
mark_native_pretransformed_jsx(
|
|
1659
|
+
mark_native_pretransformed_jsx(
|
|
1660
|
+
build_render_statements(body_nodes, true, state, render_authored_fragment),
|
|
1661
|
+
),
|
|
1400
1662
|
node,
|
|
1401
1663
|
);
|
|
1402
1664
|
block.metadata = {
|
|
@@ -1410,7 +1672,9 @@ function transform_jsx_code_block(node, { state, path, visit }) {
|
|
|
1410
1672
|
b.arrow(
|
|
1411
1673
|
[],
|
|
1412
1674
|
b.block(
|
|
1413
|
-
mark_native_pretransformed_jsx(
|
|
1675
|
+
mark_native_pretransformed_jsx(
|
|
1676
|
+
build_render_statements(body_nodes, true, state, render_authored_fragment),
|
|
1677
|
+
),
|
|
1414
1678
|
node,
|
|
1415
1679
|
),
|
|
1416
1680
|
),
|
|
@@ -1483,7 +1747,11 @@ function lower_jsx_code_block_function_body(node) {
|
|
|
1483
1747
|
// component render output. Wrap it in a native fragment so it flows
|
|
1484
1748
|
// through the same children-rendering path as a `<> … </>` render.
|
|
1485
1749
|
const fragment = b.jsx_fragment([render]);
|
|
1486
|
-
fragment.metadata = {
|
|
1750
|
+
fragment.metadata = {
|
|
1751
|
+
...fragment.metadata,
|
|
1752
|
+
native_tsrx: true,
|
|
1753
|
+
tsrx_generated_wrapper: true,
|
|
1754
|
+
};
|
|
1487
1755
|
render = fragment;
|
|
1488
1756
|
}
|
|
1489
1757
|
statements.push(b.return(render, code_block.render));
|
|
@@ -2264,7 +2532,7 @@ function create_native_tsrx_render_statements(fragment, transform_context) {
|
|
|
2264
2532
|
target.type === 'JSXFragment' ? get_tsrx_render_children(target) : [target];
|
|
2265
2533
|
return [
|
|
2266
2534
|
...create_tsrx_style_ref_setup_statements(target, style_context, transform_context),
|
|
2267
|
-
...build_render_statements(render_nodes, true, transform_context),
|
|
2535
|
+
...build_render_statements(render_nodes, true, transform_context, fragment),
|
|
2268
2536
|
];
|
|
2269
2537
|
});
|
|
2270
2538
|
}
|
|
@@ -2793,7 +3061,12 @@ function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set
|
|
|
2793
3061
|
if (child?.type !== 'JSXCodeBlock') return [child];
|
|
2794
3062
|
const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
|
|
2795
3063
|
return mark_native_pretransformed_jsx(
|
|
2796
|
-
build_render_statements(
|
|
3064
|
+
build_render_statements(
|
|
3065
|
+
body_nodes,
|
|
3066
|
+
true,
|
|
3067
|
+
transform_context,
|
|
3068
|
+
is_authored_native_fragment(child.render) ? child.render : null,
|
|
3069
|
+
),
|
|
2797
3070
|
);
|
|
2798
3071
|
});
|
|
2799
3072
|
}
|
|
@@ -4111,9 +4384,22 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
|
|
|
4111
4384
|
/** @type {any} */
|
|
4112
4385
|
let expression;
|
|
4113
4386
|
if (children.length === 0) {
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4387
|
+
// An empty fragment is a real value: keep it as `<></>` in BOTH child and
|
|
4388
|
+
// expression position. Lowering it to a bare `null` in expression position
|
|
4389
|
+
// (e.g. `let b = <></>`) drops the author's fragment and changes its type;
|
|
4390
|
+
// `<></>` is a valid value and keeps the to_ts/runtime view faithful.
|
|
4391
|
+
expression = set_loc(b.jsx_fragment([]), node.loc ? node : undefined);
|
|
4392
|
+
} else if (
|
|
4393
|
+
children.length === 1 &&
|
|
4394
|
+
(is_empty_jsx_fragment(children[0]) ||
|
|
4395
|
+
(children[0]?.type === 'JSXFragment' && is_authored_native_fragment(node)))
|
|
4396
|
+
) {
|
|
4397
|
+
// `<><X></></>` — a fragment whose only child is a fragment. The generic
|
|
4398
|
+
// single-child collapse below would unwrap it to the bare inner fragment,
|
|
4399
|
+
// dropping the outer fragment the author wrote. Keep both levels. (`<><></></>`
|
|
4400
|
+
// is kept regardless; a non-empty inner is only kept for an authored outer, so
|
|
4401
|
+
// a generated wrapper still collapses.)
|
|
4402
|
+
expression = set_loc(b.jsx_fragment(children), node.loc ? node : undefined);
|
|
4117
4403
|
} else {
|
|
4118
4404
|
expression = return_value_body_to_expression(children, node, transform_context);
|
|
4119
4405
|
}
|
|
@@ -5913,17 +6199,17 @@ function value_has_unmappable_jsx_loc(value) {
|
|
|
5913
6199
|
* @param {boolean} [in_jsx_child]
|
|
5914
6200
|
* @returns {any}
|
|
5915
6201
|
*/
|
|
5916
|
-
function build_return_expression(render_nodes, in_jsx_child = false) {
|
|
6202
|
+
export function build_return_expression(render_nodes, in_jsx_child = false) {
|
|
5917
6203
|
if (render_nodes.length === 0) return null;
|
|
5918
6204
|
if (render_nodes.length === 1) {
|
|
5919
6205
|
const only = render_nodes[0];
|
|
5920
6206
|
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
6207
|
if (only.metadata?.tsrx_reactive_block === true) {
|
|
5925
6208
|
return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
|
|
5926
6209
|
}
|
|
6210
|
+
if (only.expression?.type === 'JSXEmptyExpression') {
|
|
6211
|
+
return set_loc(b.jsx_fragment([]), only.loc ? only : undefined);
|
|
6212
|
+
}
|
|
5927
6213
|
return only.expression;
|
|
5928
6214
|
}
|
|
5929
6215
|
if (only.type === 'JSXText') {
|
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;
|
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 {
|