@tsrx/core 0.1.31 → 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 +4 -1
- package/src/plugin.js +152 -88
- package/src/transform/jsx/helpers.js +14 -39
- package/src/transform/jsx/index.js +408 -47
- package/src/transform/lazy.js +95 -46
- package/src/utils/builders.js +20 -0
- package/types/index.d.ts +6 -0
|
@@ -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,
|
|
@@ -74,6 +76,16 @@ const TSRX_IF_CONTINUE_ERROR =
|
|
|
74
76
|
'Continue statements are not allowed inside TSRX template @if blocks. Filter before rendering or use conditional output instead.';
|
|
75
77
|
const DYNAMIC_IMPORT_LOCAL = 'TsrxDynamic';
|
|
76
78
|
const DYNAMIC_FACTORY_LOCAL = '_tsrx_dynamic';
|
|
79
|
+
const LEADING_INLINE_WHITESPACE = /^[ \t]+/;
|
|
80
|
+
const TRAILING_INLINE_WHITESPACE = /[ \t]+$/;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string | undefined} ch
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function is_newline_char(ch) {
|
|
87
|
+
return ch === '\n' || ch === '\r';
|
|
88
|
+
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
79
91
|
* @param {AST.Node} node
|
|
@@ -248,29 +260,170 @@ function is_jsx_control_flow_expression(node) {
|
|
|
248
260
|
|
|
249
261
|
/**
|
|
250
262
|
* Wrap a render-output node in a native TSRX fragment so it flows through the
|
|
251
|
-
* 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.
|
|
252
267
|
* @param {any} node
|
|
253
268
|
* @returns {any}
|
|
254
269
|
*/
|
|
255
270
|
function wrap_in_native_tsrx_fragment(node) {
|
|
256
271
|
const fragment = b.jsx_fragment([node]);
|
|
257
|
-
fragment.metadata = {
|
|
272
|
+
fragment.metadata = {
|
|
273
|
+
...(fragment.metadata || {}),
|
|
274
|
+
native_tsrx: true,
|
|
275
|
+
tsrx_generated_wrapper: true,
|
|
276
|
+
};
|
|
258
277
|
return fragment;
|
|
259
278
|
}
|
|
260
279
|
|
|
261
280
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
|
|
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
|
+
*
|
|
268
421
|
* @param {any} node
|
|
269
|
-
* @param {TransformContext
|
|
422
|
+
* @param {TransformContext} transform_context
|
|
270
423
|
* @param {Set<any>} [seen]
|
|
271
424
|
* @returns {void}
|
|
272
425
|
*/
|
|
273
|
-
function wrap_control_flow_expression_values(node,
|
|
426
|
+
function wrap_control_flow_expression_values(node, transform_context, seen = new Set()) {
|
|
274
427
|
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
275
428
|
seen.add(node);
|
|
276
429
|
|
|
@@ -282,19 +435,40 @@ function wrap_control_flow_expression_values(node, lower_dynamic_context, seen =
|
|
|
282
435
|
// `<{'div'}>`) is hoisted to a module-level static const while still
|
|
283
436
|
// carrying the raw dynamic tag. Alias lowerings return a replacement
|
|
284
437
|
// fragment, which is swapped into the child's position here.
|
|
438
|
+
const lower_dynamic = !!transform_context?.platform?.imports?.dynamicFactory;
|
|
285
439
|
const lower_child = (/** @type {any} */ child) => {
|
|
286
|
-
if (!
|
|
287
|
-
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;
|
|
288
442
|
};
|
|
289
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
|
+
|
|
290
456
|
if (Array.isArray(node)) {
|
|
291
457
|
for (let i = 0; i < node.length; i++) {
|
|
292
458
|
node[i] = lower_child(node[i]);
|
|
293
|
-
wrap_control_flow_expression_values(node[i],
|
|
459
|
+
wrap_control_flow_expression_values(node[i], transform_context, seen);
|
|
294
460
|
}
|
|
295
461
|
return;
|
|
296
462
|
}
|
|
297
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
|
+
|
|
298
472
|
if (
|
|
299
473
|
node.type === 'ArrowFunctionExpression' &&
|
|
300
474
|
node.body?.type !== 'BlockStatement' &&
|
|
@@ -316,15 +490,28 @@ function wrap_control_flow_expression_values(node, lower_dynamic_context, seen =
|
|
|
316
490
|
(node.type === 'CallExpression' || node.type === 'NewExpression') &&
|
|
317
491
|
Array.isArray(node.arguments)
|
|
318
492
|
) {
|
|
319
|
-
node.arguments = node.arguments.map(
|
|
320
|
-
is_jsx_control_flow_expression(arg) ? wrap_in_native_tsrx_fragment(arg) : arg,
|
|
321
|
-
);
|
|
493
|
+
node.arguments = node.arguments.map(wrap_value);
|
|
322
494
|
}
|
|
323
495
|
|
|
324
496
|
for (const key of Object.keys(node)) {
|
|
325
497
|
if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
515
|
}
|
|
329
516
|
}
|
|
330
517
|
|
|
@@ -392,10 +579,7 @@ export function createJsxTransform(platform) {
|
|
|
392
579
|
};
|
|
393
580
|
|
|
394
581
|
expand_child_code_blocks(/** @type {any} */ (ast));
|
|
395
|
-
wrap_control_flow_expression_values(
|
|
396
|
-
/** @type {any} */ (ast),
|
|
397
|
-
platform.imports.dynamicFactory ? transform_context : null,
|
|
398
|
-
);
|
|
582
|
+
wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
|
|
399
583
|
|
|
400
584
|
if (!transform_context.typeOnly) {
|
|
401
585
|
preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
|
|
@@ -419,8 +603,37 @@ export function createJsxTransform(platform) {
|
|
|
419
603
|
|
|
420
604
|
const style_context = prepare_tsrx_fragment_styles(node, state);
|
|
421
605
|
const target = style_context?.fragment ?? next() ?? node;
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
|
424
637
|
for (const statement of create_tsrx_style_ref_setup_statements(
|
|
425
638
|
target,
|
|
426
639
|
style_context,
|
|
@@ -534,6 +747,12 @@ export function createJsxTransform(platform) {
|
|
|
534
747
|
inject_try_imports(expanded, transform_context, platform, suspense_source);
|
|
535
748
|
}
|
|
536
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
|
+
|
|
537
756
|
// Apply lazy destructuring transforms to module-level code (top-level function
|
|
538
757
|
// declarations, arrow functions, etc.).
|
|
539
758
|
// In type-only mode, the lazy patterns survive untouched: esrap ignores the
|
|
@@ -541,12 +760,25 @@ export function createJsxTransform(platform) {
|
|
|
541
760
|
// = expr` prints as `let [a] = expr`, and the bare statement-level form
|
|
542
761
|
// `&[x] = expr;` (used when `x` is already declared) prints as `[x] =
|
|
543
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
|
+
}
|
|
544
777
|
const final_program = /** @type {any} */ (
|
|
545
778
|
transform_context.typeOnly
|
|
546
779
|
? expanded
|
|
547
780
|
: apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
|
|
548
781
|
);
|
|
549
|
-
lower_remaining_jsx_code_blocks(final_program, transform_context);
|
|
550
782
|
|
|
551
783
|
const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
|
|
552
784
|
sourceMapSource: filename,
|
|
@@ -922,13 +1154,33 @@ function lower_code_block_stream_node(block, transform_context) {
|
|
|
922
1154
|
* @param {any[]} body_nodes
|
|
923
1155
|
* @param {boolean} return_null_when_empty
|
|
924
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.
|
|
925
1161
|
* @returns {any[]}
|
|
926
1162
|
*/
|
|
927
|
-
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
|
+
) {
|
|
928
1169
|
body_nodes = body_nodes.flatMap((node) =>
|
|
929
1170
|
node?.type === 'JSXCodeBlock' ? lower_code_block_stream_node(node, transform_context) : [node],
|
|
930
1171
|
);
|
|
931
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
|
+
|
|
932
1184
|
const statements = [];
|
|
933
1185
|
const render_nodes = [];
|
|
934
1186
|
let has_terminal_return = false;
|
|
@@ -1062,7 +1314,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
1062
1314
|
hoist_static_render_nodes(render_nodes, transform_context);
|
|
1063
1315
|
}
|
|
1064
1316
|
|
|
1065
|
-
|
|
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
|
+
}
|
|
1066
1333
|
if (return_arg || (return_null_when_empty && !has_terminal_return)) {
|
|
1067
1334
|
statements.push(b.return(return_arg || b.literal(null)));
|
|
1068
1335
|
}
|
|
@@ -1383,10 +1650,15 @@ function transform_return_statement(node, { next, visit, state, path }) {
|
|
|
1383
1650
|
function transform_jsx_code_block(node, { state, path, visit }) {
|
|
1384
1651
|
const body_nodes = get_jsx_code_block_body_nodes(node, state);
|
|
1385
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;
|
|
1386
1656
|
|
|
1387
1657
|
if (parent && parent.body === node && is_function_or_class_boundary(parent)) {
|
|
1388
1658
|
const block = b.block(
|
|
1389
|
-
mark_native_pretransformed_jsx(
|
|
1659
|
+
mark_native_pretransformed_jsx(
|
|
1660
|
+
build_render_statements(body_nodes, true, state, render_authored_fragment),
|
|
1661
|
+
),
|
|
1390
1662
|
node,
|
|
1391
1663
|
);
|
|
1392
1664
|
block.metadata = {
|
|
@@ -1400,7 +1672,9 @@ function transform_jsx_code_block(node, { state, path, visit }) {
|
|
|
1400
1672
|
b.arrow(
|
|
1401
1673
|
[],
|
|
1402
1674
|
b.block(
|
|
1403
|
-
mark_native_pretransformed_jsx(
|
|
1675
|
+
mark_native_pretransformed_jsx(
|
|
1676
|
+
build_render_statements(body_nodes, true, state, render_authored_fragment),
|
|
1677
|
+
),
|
|
1404
1678
|
node,
|
|
1405
1679
|
),
|
|
1406
1680
|
),
|
|
@@ -1473,7 +1747,11 @@ function lower_jsx_code_block_function_body(node) {
|
|
|
1473
1747
|
// component render output. Wrap it in a native fragment so it flows
|
|
1474
1748
|
// through the same children-rendering path as a `<> … </>` render.
|
|
1475
1749
|
const fragment = b.jsx_fragment([render]);
|
|
1476
|
-
fragment.metadata = {
|
|
1750
|
+
fragment.metadata = {
|
|
1751
|
+
...fragment.metadata,
|
|
1752
|
+
native_tsrx: true,
|
|
1753
|
+
tsrx_generated_wrapper: true,
|
|
1754
|
+
};
|
|
1477
1755
|
render = fragment;
|
|
1478
1756
|
}
|
|
1479
1757
|
statements.push(b.return(render, code_block.render));
|
|
@@ -2254,7 +2532,7 @@ function create_native_tsrx_render_statements(fragment, transform_context) {
|
|
|
2254
2532
|
target.type === 'JSXFragment' ? get_tsrx_render_children(target) : [target];
|
|
2255
2533
|
return [
|
|
2256
2534
|
...create_tsrx_style_ref_setup_statements(target, style_context, transform_context),
|
|
2257
|
-
...build_render_statements(render_nodes, true, transform_context),
|
|
2535
|
+
...build_render_statements(render_nodes, true, transform_context, fragment),
|
|
2258
2536
|
];
|
|
2259
2537
|
});
|
|
2260
2538
|
}
|
|
@@ -2413,9 +2691,7 @@ function mark_native_pretransformed_jsx(node, seen = new Set()) {
|
|
|
2413
2691
|
function get_tsrx_render_children(node) {
|
|
2414
2692
|
return (node.children || []).filter(
|
|
2415
2693
|
(/** @type {any} */ child) =>
|
|
2416
|
-
child &&
|
|
2417
|
-
child.type !== 'EmptyStatement' &&
|
|
2418
|
-
(child.type !== 'JSXText' || child.value.trim() !== ''),
|
|
2694
|
+
child && child.type !== 'EmptyStatement' && (child.type !== 'JSXText' || child.value !== ''),
|
|
2419
2695
|
);
|
|
2420
2696
|
}
|
|
2421
2697
|
|
|
@@ -2785,7 +3061,12 @@ function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set
|
|
|
2785
3061
|
if (child?.type !== 'JSXCodeBlock') return [child];
|
|
2786
3062
|
const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
|
|
2787
3063
|
return mark_native_pretransformed_jsx(
|
|
2788
|
-
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
|
+
),
|
|
2789
3070
|
);
|
|
2790
3071
|
});
|
|
2791
3072
|
}
|
|
@@ -3399,7 +3680,9 @@ function create_element_children(children, transform_context) {
|
|
|
3399
3680
|
const saved_inside_element_child = transform_context.inside_element_child;
|
|
3400
3681
|
transform_context.inside_element_child = true;
|
|
3401
3682
|
try {
|
|
3402
|
-
return
|
|
3683
|
+
return wrap_edge_whitespace(
|
|
3684
|
+
children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context)),
|
|
3685
|
+
);
|
|
3403
3686
|
} finally {
|
|
3404
3687
|
transform_context.inside_element_child = saved_inside_element_child;
|
|
3405
3688
|
}
|
|
@@ -3949,6 +4232,67 @@ function is_try_control_node(node) {
|
|
|
3949
4232
|
return node?.type === 'TryStatement' || node?.type === 'JSXTryExpression';
|
|
3950
4233
|
}
|
|
3951
4234
|
|
|
4235
|
+
/**
|
|
4236
|
+
* Wrap the inline whitespace at a fragment/element's content edges in `{' '}`
|
|
4237
|
+
* containers. A bare leading/trailing space is fragile: once the output is
|
|
4238
|
+
* line-wrapped (by prettier or the host JSX compiler) it becomes newline-adjacent
|
|
4239
|
+
* and is trimmed away, dropping a significant space. Whitespace BETWEEN siblings
|
|
4240
|
+
* stays bare text — it is not at an edge and is preserved as-is. Only spaces/tabs
|
|
4241
|
+
* are pulled out; whitespace runs containing a newline are layout indentation and
|
|
4242
|
+
* are left for the host compiler to collapse.
|
|
4243
|
+
*
|
|
4244
|
+
* @param {any[]} nodes
|
|
4245
|
+
* @returns {any[]}
|
|
4246
|
+
*/
|
|
4247
|
+
export function wrap_edge_whitespace(nodes) {
|
|
4248
|
+
const length = nodes.length;
|
|
4249
|
+
if (length === 0) {
|
|
4250
|
+
return nodes;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
const first = nodes[0];
|
|
4254
|
+
const last = nodes[length - 1];
|
|
4255
|
+
if (first?.type !== 'JSXText' && last?.type !== 'JSXText') {
|
|
4256
|
+
return nodes;
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
/** @type {(ESTreeJSX.JSXExpressionContainer | ESTreeJSX.JSXText)[]} */
|
|
4260
|
+
const out = [];
|
|
4261
|
+
for (let i = 0; i < length; i++) {
|
|
4262
|
+
const node = nodes[i];
|
|
4263
|
+
const at_start = i === 0;
|
|
4264
|
+
const at_end = i === length - 1;
|
|
4265
|
+
if (!node || node.type !== 'JSXText' || (!at_start && !at_end)) {
|
|
4266
|
+
out.push(node);
|
|
4267
|
+
continue;
|
|
4268
|
+
}
|
|
4269
|
+
let value = /** @type {string} */ (node.value);
|
|
4270
|
+
if (at_start) {
|
|
4271
|
+
const lead = LEADING_INLINE_WHITESPACE.exec(value);
|
|
4272
|
+
if (lead && !is_newline_char(value[lead[0].length])) {
|
|
4273
|
+
out.push(to_jsx_expression_container(b.literal(lead[0]), node));
|
|
4274
|
+
value = value.slice(lead[0].length);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
/** @type {ESTreeJSX.JSXExpressionContainer | null} */
|
|
4278
|
+
let trailing = null;
|
|
4279
|
+
if (at_end) {
|
|
4280
|
+
const trail = TRAILING_INLINE_WHITESPACE.exec(value);
|
|
4281
|
+
if (trail && !is_newline_char(value[value.length - trail[0].length - 1])) {
|
|
4282
|
+
trailing = to_jsx_expression_container(b.literal(trail[0]), node);
|
|
4283
|
+
value = value.slice(0, value.length - trail[0].length);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
if (value !== '') {
|
|
4287
|
+
out.push(b.jsx_text(value, value));
|
|
4288
|
+
}
|
|
4289
|
+
if (trailing) {
|
|
4290
|
+
out.push(trailing);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return out;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
3952
4296
|
/**
|
|
3953
4297
|
* @param {any} node
|
|
3954
4298
|
* @param {TransformContext} transform_context
|
|
@@ -4034,15 +4378,28 @@ function to_jsx_child(node, transform_context) {
|
|
|
4034
4378
|
function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = false) {
|
|
4035
4379
|
const children = (node.children || []).filter(
|
|
4036
4380
|
(/** @type {any} */ child) =>
|
|
4037
|
-
child &&
|
|
4038
|
-
child.type !== 'EmptyStatement' &&
|
|
4039
|
-
(child.type !== 'JSXText' || child.value.trim() !== ''),
|
|
4381
|
+
child && child.type !== 'EmptyStatement' && (child.type !== 'JSXText' || child.value !== ''),
|
|
4040
4382
|
);
|
|
4041
4383
|
|
|
4042
4384
|
/** @type {any} */
|
|
4043
4385
|
let expression;
|
|
4044
4386
|
if (children.length === 0) {
|
|
4045
|
-
|
|
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);
|
|
4046
4403
|
} else {
|
|
4047
4404
|
expression = return_value_body_to_expression(children, node, transform_context);
|
|
4048
4405
|
}
|
|
@@ -4052,10 +4409,10 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
|
|
|
4052
4409
|
const saved_inside_element_child = transform_context.inside_element_child;
|
|
4053
4410
|
transform_context.inside_element_child = true;
|
|
4054
4411
|
try {
|
|
4055
|
-
const render_nodes =
|
|
4056
|
-
to_jsx_child(child, transform_context),
|
|
4412
|
+
const render_nodes = wrap_edge_whitespace(
|
|
4413
|
+
children.map((/** @type {any} */ child) => to_jsx_child(child, transform_context)),
|
|
4057
4414
|
);
|
|
4058
|
-
expression = build_return_expression(render_nodes) || create_null_literal();
|
|
4415
|
+
expression = build_return_expression(render_nodes, in_jsx_child) || create_null_literal();
|
|
4059
4416
|
} finally {
|
|
4060
4417
|
transform_context.inside_element_child = saved_inside_element_child;
|
|
4061
4418
|
}
|
|
@@ -5839,22 +6196,26 @@ function value_has_unmappable_jsx_loc(value) {
|
|
|
5839
6196
|
|
|
5840
6197
|
/**
|
|
5841
6198
|
* @param {any[]} render_nodes
|
|
6199
|
+
* @param {boolean} [in_jsx_child]
|
|
5842
6200
|
* @returns {any}
|
|
5843
6201
|
*/
|
|
5844
|
-
function build_return_expression(render_nodes) {
|
|
6202
|
+
export function build_return_expression(render_nodes, in_jsx_child = false) {
|
|
5845
6203
|
if (render_nodes.length === 0) return null;
|
|
5846
6204
|
if (render_nodes.length === 1) {
|
|
5847
6205
|
const only = render_nodes[0];
|
|
5848
6206
|
if (only.type === 'JSXExpressionContainer') {
|
|
5849
|
-
// Reactive-block containers (dynamic tags) must stay expression
|
|
5850
|
-
// children so the host JSX compiler wraps them in a render block;
|
|
5851
|
-
// returning the bare call would evaluate them once.
|
|
5852
6207
|
if (only.metadata?.tsrx_reactive_block === true) {
|
|
5853
6208
|
return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
|
|
5854
6209
|
}
|
|
6210
|
+
if (only.expression?.type === 'JSXEmptyExpression') {
|
|
6211
|
+
return set_loc(b.jsx_fragment([]), only.loc ? only : undefined);
|
|
6212
|
+
}
|
|
5855
6213
|
return only.expression;
|
|
5856
6214
|
}
|
|
5857
6215
|
if (only.type === 'JSXText') {
|
|
6216
|
+
if (in_jsx_child) {
|
|
6217
|
+
return set_loc(b.jsx_fragment([only]), only.loc ? only : undefined);
|
|
6218
|
+
}
|
|
5858
6219
|
const value = (only.value ?? '').trim();
|
|
5859
6220
|
return b.literal(value, JSON.stringify(value), only);
|
|
5860
6221
|
}
|