@tsrx/react 0.0.7 → 0.1.1
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 +2 -2
- package/src/transform.js +76 -160
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "React compiler built on @tsrx/core",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.1.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"publishConfig": {
|
|
9
9
|
"access": "public"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"esrap": "^2.1.0",
|
|
27
27
|
"zimmerframe": "^1.1.2",
|
|
28
|
-
"@tsrx/core": "0.0.
|
|
28
|
+
"@tsrx/core": "0.0.7"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": ">=18"
|
package/src/transform.js
CHANGED
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
replaceLazyParams as replace_lazy_params,
|
|
15
15
|
prepareStylesheetForRender as prepare_stylesheet_for_render,
|
|
16
16
|
annotateComponentWithHash as annotate_component_with_hash,
|
|
17
|
+
isInterleavedBody as is_interleaved_body_core,
|
|
18
|
+
isCapturableJsxChild as is_capturable_jsx_child,
|
|
19
|
+
captureJsxChild,
|
|
20
|
+
isHoistSafeJsxNode as is_hoist_safe_jsx_node,
|
|
17
21
|
} from '@tsrx/core';
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -51,7 +55,6 @@ import {
|
|
|
51
55
|
export function transform(ast, source, filename) {
|
|
52
56
|
/** @type {any[]} */
|
|
53
57
|
const stylesheets = [];
|
|
54
|
-
const module_uses_server_directive = has_use_server_directive(ast);
|
|
55
58
|
|
|
56
59
|
/** @type {TransformContext} */
|
|
57
60
|
const transform_context = {
|
|
@@ -71,13 +74,6 @@ export function transform(ast, source, filename) {
|
|
|
71
74
|
const as_any = /** @type {any} */ (node);
|
|
72
75
|
const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
|
|
73
76
|
|
|
74
|
-
if (await_expression && !module_uses_server_directive) {
|
|
75
|
-
throw create_compile_error(
|
|
76
|
-
await_expression,
|
|
77
|
-
'React components can only use `await` when the module has a top-level "use server" directive.',
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
77
|
if (await_expression) {
|
|
82
78
|
as_any.metadata = /** @type {any} */ ({
|
|
83
79
|
...(as_any.metadata || {}),
|
|
@@ -304,6 +300,10 @@ function build_component_statements(
|
|
|
304
300
|
const render_nodes = [];
|
|
305
301
|
const bindings = new Map(available_bindings);
|
|
306
302
|
|
|
303
|
+
const pre_split_body = body_nodes.slice(0, split_index);
|
|
304
|
+
const interleaved = is_interleaved_body(pre_split_body);
|
|
305
|
+
let capture_index = 0;
|
|
306
|
+
|
|
307
307
|
for (let i = 0; i < split_index; i += 1) {
|
|
308
308
|
const child = body_nodes[i];
|
|
309
309
|
|
|
@@ -318,7 +318,14 @@ function build_component_statements(
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
if (is_jsx_child(child)) {
|
|
321
|
-
|
|
321
|
+
const jsx = to_jsx_child(child, transform_context);
|
|
322
|
+
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
323
|
+
const { declaration, reference } = captureJsxChild(jsx, capture_index++);
|
|
324
|
+
statements.push(declaration);
|
|
325
|
+
render_nodes.push(reference);
|
|
326
|
+
} else {
|
|
327
|
+
render_nodes.push(jsx);
|
|
328
|
+
}
|
|
322
329
|
} else {
|
|
323
330
|
statements.push(child);
|
|
324
331
|
collect_statement_bindings(child, bindings);
|
|
@@ -326,7 +333,9 @@ function build_component_statements(
|
|
|
326
333
|
}
|
|
327
334
|
}
|
|
328
335
|
|
|
329
|
-
|
|
336
|
+
if (!interleaved) {
|
|
337
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
338
|
+
}
|
|
330
339
|
|
|
331
340
|
const split_node = body_nodes[split_index];
|
|
332
341
|
const consequent_body =
|
|
@@ -389,6 +398,14 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
389
398
|
const saved_bindings = transform_context.available_bindings;
|
|
390
399
|
transform_context.available_bindings = new Map(saved_bindings);
|
|
391
400
|
|
|
401
|
+
// When non-JSX statements are interleaved with JSX children, we must
|
|
402
|
+
// preserve source order so each JSX expression sees the variable state
|
|
403
|
+
// at its textual position. Otherwise statements would all run before
|
|
404
|
+
// any JSX is constructed, and every JSX child would observe the final
|
|
405
|
+
// state of mutable variables.
|
|
406
|
+
const interleaved = is_interleaved_body(body_nodes);
|
|
407
|
+
let capture_index = 0;
|
|
408
|
+
|
|
392
409
|
for (const child of body_nodes) {
|
|
393
410
|
if (is_bare_return_statement(child)) {
|
|
394
411
|
statements.push(create_component_return_statement(render_nodes, child));
|
|
@@ -402,14 +419,23 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
402
419
|
}
|
|
403
420
|
|
|
404
421
|
if (is_jsx_child(child)) {
|
|
405
|
-
|
|
422
|
+
const jsx = to_jsx_child(child, transform_context);
|
|
423
|
+
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
424
|
+
const { declaration, reference } = captureJsxChild(jsx, capture_index++);
|
|
425
|
+
statements.push(declaration);
|
|
426
|
+
render_nodes.push(reference);
|
|
427
|
+
} else {
|
|
428
|
+
render_nodes.push(jsx);
|
|
429
|
+
}
|
|
406
430
|
} else {
|
|
407
431
|
statements.push(child);
|
|
408
432
|
collect_statement_bindings(child, transform_context.available_bindings);
|
|
409
433
|
}
|
|
410
434
|
}
|
|
411
435
|
|
|
412
|
-
|
|
436
|
+
if (!interleaved) {
|
|
437
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
438
|
+
}
|
|
413
439
|
|
|
414
440
|
const return_arg = build_return_expression(render_nodes);
|
|
415
441
|
if (return_arg || return_null_when_empty) {
|
|
@@ -423,6 +449,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
423
449
|
return statements;
|
|
424
450
|
}
|
|
425
451
|
|
|
452
|
+
/**
|
|
453
|
+
* React-specific wrapper around the core `isInterleavedBody` helper that
|
|
454
|
+
* ignores bare `return` / lone return-if statements. Those are rewriting
|
|
455
|
+
* signals rather than user-visible side effects, so JSX children around
|
|
456
|
+
* them don't need capturing.
|
|
457
|
+
*
|
|
458
|
+
* @param {any[]} body_nodes
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
function is_interleaved_body(body_nodes) {
|
|
462
|
+
const filtered = body_nodes.filter(
|
|
463
|
+
(child) => !is_bare_return_statement(child) && !is_lone_return_if_statement(child),
|
|
464
|
+
);
|
|
465
|
+
return is_interleaved_body_core(filtered, is_jsx_child);
|
|
466
|
+
}
|
|
467
|
+
|
|
426
468
|
/**
|
|
427
469
|
* @param {any[]} body_nodes
|
|
428
470
|
* @returns {number}
|
|
@@ -535,34 +577,6 @@ function is_hook_callee(callee) {
|
|
|
535
577
|
return false;
|
|
536
578
|
}
|
|
537
579
|
|
|
538
|
-
/**
|
|
539
|
-
* @param {AST.Program} program
|
|
540
|
-
* @returns {boolean}
|
|
541
|
-
*/
|
|
542
|
-
function has_use_server_directive(program) {
|
|
543
|
-
for (const statement of program.body || []) {
|
|
544
|
-
const directive = /** @type {any} */ (statement).directive;
|
|
545
|
-
|
|
546
|
-
if (directive === 'use server') {
|
|
547
|
-
return true;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (
|
|
551
|
-
statement.type === 'ExpressionStatement' &&
|
|
552
|
-
statement.expression?.type === 'Literal' &&
|
|
553
|
-
statement.expression.value === 'use server'
|
|
554
|
-
) {
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (directive == null) {
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
580
|
/**
|
|
567
581
|
* @param {any[]} body_nodes
|
|
568
582
|
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
@@ -881,116 +895,6 @@ function references_scope_bindings(node, scope_bindings) {
|
|
|
881
895
|
return false;
|
|
882
896
|
}
|
|
883
897
|
|
|
884
|
-
/**
|
|
885
|
-
* @param {AST.Literal} node
|
|
886
|
-
* @returns {boolean}
|
|
887
|
-
*/
|
|
888
|
-
function is_static_literal(node) {
|
|
889
|
-
return (
|
|
890
|
-
node.value === null ||
|
|
891
|
-
typeof node.value === 'string' ||
|
|
892
|
-
typeof node.value === 'number' ||
|
|
893
|
-
typeof node.value === 'boolean' ||
|
|
894
|
-
typeof node.value === 'bigint'
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* @param {any} node
|
|
900
|
-
* @returns {boolean}
|
|
901
|
-
*/
|
|
902
|
-
function is_hoist_safe_expression(node) {
|
|
903
|
-
if (!node || typeof node !== 'object') return false;
|
|
904
|
-
|
|
905
|
-
switch (node.type) {
|
|
906
|
-
case 'Literal':
|
|
907
|
-
return is_static_literal(node);
|
|
908
|
-
case 'TemplateLiteral':
|
|
909
|
-
return node.expressions.length === 0;
|
|
910
|
-
case 'UnaryExpression':
|
|
911
|
-
return node.operator !== 'delete' && is_hoist_safe_expression(node.argument);
|
|
912
|
-
case 'BinaryExpression':
|
|
913
|
-
case 'LogicalExpression':
|
|
914
|
-
return is_hoist_safe_expression(node.left) && is_hoist_safe_expression(node.right);
|
|
915
|
-
case 'ConditionalExpression':
|
|
916
|
-
return (
|
|
917
|
-
is_hoist_safe_expression(node.test) &&
|
|
918
|
-
is_hoist_safe_expression(node.consequent) &&
|
|
919
|
-
is_hoist_safe_expression(node.alternate)
|
|
920
|
-
);
|
|
921
|
-
case 'SequenceExpression':
|
|
922
|
-
return node.expressions.every(is_hoist_safe_expression);
|
|
923
|
-
case 'ParenthesizedExpression':
|
|
924
|
-
return is_hoist_safe_expression(node.expression);
|
|
925
|
-
case 'JSXElement':
|
|
926
|
-
return is_hoist_safe_jsx_node(node);
|
|
927
|
-
case 'JSXFragment':
|
|
928
|
-
return node.children.every(is_hoist_safe_jsx_child);
|
|
929
|
-
default:
|
|
930
|
-
return false;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
* @param {any} node
|
|
936
|
-
* @returns {boolean}
|
|
937
|
-
*/
|
|
938
|
-
function is_hoist_safe_jsx_child(node) {
|
|
939
|
-
if (!node || typeof node !== 'object') return false;
|
|
940
|
-
|
|
941
|
-
switch (node.type) {
|
|
942
|
-
case 'JSXText':
|
|
943
|
-
return true;
|
|
944
|
-
case 'JSXElement':
|
|
945
|
-
return is_hoist_safe_jsx_node(node);
|
|
946
|
-
case 'JSXFragment':
|
|
947
|
-
return node.children.every(is_hoist_safe_jsx_child);
|
|
948
|
-
case 'JSXExpressionContainer':
|
|
949
|
-
return (
|
|
950
|
-
node.expression.type !== 'JSXEmptyExpression' && is_hoist_safe_expression(node.expression)
|
|
951
|
-
);
|
|
952
|
-
default:
|
|
953
|
-
return false;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* @param {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} attribute
|
|
959
|
-
* @returns {boolean}
|
|
960
|
-
*/
|
|
961
|
-
function is_hoist_safe_jsx_attribute(attribute) {
|
|
962
|
-
if (attribute.type === 'JSXSpreadAttribute') return false;
|
|
963
|
-
if (attribute.value == null) return true;
|
|
964
|
-
|
|
965
|
-
if (attribute.value.type === 'Literal') {
|
|
966
|
-
return is_static_literal(attribute.value);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
if (attribute.value.type === 'JSXExpressionContainer') {
|
|
970
|
-
return (
|
|
971
|
-
attribute.value.expression.type !== 'JSXEmptyExpression' &&
|
|
972
|
-
is_hoist_safe_expression(attribute.value.expression)
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return false;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
|
|
981
|
-
* @returns {boolean}
|
|
982
|
-
*/
|
|
983
|
-
function is_hoist_safe_jsx_node(node) {
|
|
984
|
-
if (node.type === 'JSXFragment') {
|
|
985
|
-
return node.children.every(is_hoist_safe_jsx_child);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return (
|
|
989
|
-
node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
|
|
990
|
-
node.children.every(is_hoist_safe_jsx_child)
|
|
991
|
-
);
|
|
992
|
-
}
|
|
993
|
-
|
|
994
898
|
/**
|
|
995
899
|
* Hoist static JSX elements from render_nodes to module level.
|
|
996
900
|
* A JSX element is static if it doesn't reference any component-scope bindings.
|
|
@@ -1203,6 +1107,19 @@ function to_jsx_element(node, transform_context) {
|
|
|
1203
1107
|
return dynamic_element_to_jsx_child(node, transform_context);
|
|
1204
1108
|
}
|
|
1205
1109
|
|
|
1110
|
+
if (!node.id) {
|
|
1111
|
+
const children = create_element_children(node.children || [], transform_context);
|
|
1112
|
+
return set_loc(
|
|
1113
|
+
/** @type {any} */ ({
|
|
1114
|
+
type: 'JSXFragment',
|
|
1115
|
+
openingFragment: { type: 'JSXOpeningFragment' },
|
|
1116
|
+
closingFragment: { type: 'JSXClosingFragment' },
|
|
1117
|
+
children,
|
|
1118
|
+
}),
|
|
1119
|
+
node,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1206
1123
|
const name = identifier_to_jsx_name(node.id);
|
|
1207
1124
|
const attributes = (node.attributes || []).map(to_jsx_attribute);
|
|
1208
1125
|
const selfClosing = !!node.selfClosing;
|
|
@@ -1686,24 +1603,23 @@ function for_of_statement_to_jsx_child(node, transform_context) {
|
|
|
1686
1603
|
);
|
|
1687
1604
|
}
|
|
1688
1605
|
|
|
1689
|
-
if (node.key) {
|
|
1690
|
-
throw create_compile_error(
|
|
1691
|
-
node.key,
|
|
1692
|
-
'React TSRX does not support `key` in `for` control flow. Put the key on the rendered element instead, for example `<div key={i}>...</div>`.',
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
1606
|
const loop_params = get_for_of_iteration_params(node.left, node.index);
|
|
1697
1607
|
const loop_body = node.body.type === 'BlockStatement' ? node.body.body : [node.body];
|
|
1698
1608
|
const has_hooks = body_contains_top_level_hook_call(loop_body);
|
|
1699
|
-
const
|
|
1609
|
+
const body_key_expression = find_key_expression_in_body(loop_body);
|
|
1610
|
+
const explicit_key_expression =
|
|
1611
|
+
body_key_expression ?? (node.key ? clone_expression_node(node.key) : undefined);
|
|
1700
1612
|
const key_expression =
|
|
1701
1613
|
has_hooks && explicit_key_expression == null && node.index
|
|
1702
1614
|
? clone_expression_node(node.index)
|
|
1703
1615
|
: explicit_key_expression;
|
|
1704
1616
|
const implicit_non_hook_key_expression =
|
|
1705
|
-
!has_hooks &&
|
|
1706
|
-
?
|
|
1617
|
+
!has_hooks && body_key_expression == null
|
|
1618
|
+
? node.key
|
|
1619
|
+
? clone_expression_node(node.key)
|
|
1620
|
+
: node.index
|
|
1621
|
+
? clone_expression_node(node.index)
|
|
1622
|
+
: undefined
|
|
1707
1623
|
: undefined;
|
|
1708
1624
|
|
|
1709
1625
|
// Add loop params to available bindings so hoisted helpers receive them as props
|