@tsrx/react 0.0.6 → 0.1.0
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 +177 -40
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.0
|
|
6
|
+
"version": "0.1.0",
|
|
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.6"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": ">=18"
|
package/src/transform.js
CHANGED
|
@@ -14,6 +14,9 @@ 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,
|
|
17
20
|
} from '@tsrx/core';
|
|
18
21
|
|
|
19
22
|
/**
|
|
@@ -51,7 +54,6 @@ import {
|
|
|
51
54
|
export function transform(ast, source, filename) {
|
|
52
55
|
/** @type {any[]} */
|
|
53
56
|
const stylesheets = [];
|
|
54
|
-
const module_uses_server_directive = has_use_server_directive(ast);
|
|
55
57
|
|
|
56
58
|
/** @type {TransformContext} */
|
|
57
59
|
const transform_context = {
|
|
@@ -71,13 +73,6 @@ export function transform(ast, source, filename) {
|
|
|
71
73
|
const as_any = /** @type {any} */ (node);
|
|
72
74
|
const await_expression = find_first_top_level_await_in_component_body(as_any.body || []);
|
|
73
75
|
|
|
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
76
|
if (await_expression) {
|
|
82
77
|
as_any.metadata = /** @type {any} */ ({
|
|
83
78
|
...(as_any.metadata || {}),
|
|
@@ -304,6 +299,10 @@ function build_component_statements(
|
|
|
304
299
|
const render_nodes = [];
|
|
305
300
|
const bindings = new Map(available_bindings);
|
|
306
301
|
|
|
302
|
+
const pre_split_body = body_nodes.slice(0, split_index);
|
|
303
|
+
const interleaved = is_interleaved_body(pre_split_body);
|
|
304
|
+
let capture_index = 0;
|
|
305
|
+
|
|
307
306
|
for (let i = 0; i < split_index; i += 1) {
|
|
308
307
|
const child = body_nodes[i];
|
|
309
308
|
|
|
@@ -318,7 +317,14 @@ function build_component_statements(
|
|
|
318
317
|
}
|
|
319
318
|
|
|
320
319
|
if (is_jsx_child(child)) {
|
|
321
|
-
|
|
320
|
+
const jsx = to_jsx_child(child, transform_context);
|
|
321
|
+
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
322
|
+
const { declaration, reference } = captureJsxChild(jsx, capture_index++);
|
|
323
|
+
statements.push(declaration);
|
|
324
|
+
render_nodes.push(reference);
|
|
325
|
+
} else {
|
|
326
|
+
render_nodes.push(jsx);
|
|
327
|
+
}
|
|
322
328
|
} else {
|
|
323
329
|
statements.push(child);
|
|
324
330
|
collect_statement_bindings(child, bindings);
|
|
@@ -326,7 +332,9 @@ function build_component_statements(
|
|
|
326
332
|
}
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
|
|
335
|
+
if (!interleaved) {
|
|
336
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
337
|
+
}
|
|
330
338
|
|
|
331
339
|
const split_node = body_nodes[split_index];
|
|
332
340
|
const consequent_body =
|
|
@@ -389,6 +397,14 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
389
397
|
const saved_bindings = transform_context.available_bindings;
|
|
390
398
|
transform_context.available_bindings = new Map(saved_bindings);
|
|
391
399
|
|
|
400
|
+
// When non-JSX statements are interleaved with JSX children, we must
|
|
401
|
+
// preserve source order so each JSX expression sees the variable state
|
|
402
|
+
// at its textual position. Otherwise statements would all run before
|
|
403
|
+
// any JSX is constructed, and every JSX child would observe the final
|
|
404
|
+
// state of mutable variables.
|
|
405
|
+
const interleaved = is_interleaved_body(body_nodes);
|
|
406
|
+
let capture_index = 0;
|
|
407
|
+
|
|
392
408
|
for (const child of body_nodes) {
|
|
393
409
|
if (is_bare_return_statement(child)) {
|
|
394
410
|
statements.push(create_component_return_statement(render_nodes, child));
|
|
@@ -402,14 +418,23 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
402
418
|
}
|
|
403
419
|
|
|
404
420
|
if (is_jsx_child(child)) {
|
|
405
|
-
|
|
421
|
+
const jsx = to_jsx_child(child, transform_context);
|
|
422
|
+
if (interleaved && is_capturable_jsx_child(jsx)) {
|
|
423
|
+
const { declaration, reference } = captureJsxChild(jsx, capture_index++);
|
|
424
|
+
statements.push(declaration);
|
|
425
|
+
render_nodes.push(reference);
|
|
426
|
+
} else {
|
|
427
|
+
render_nodes.push(jsx);
|
|
428
|
+
}
|
|
406
429
|
} else {
|
|
407
430
|
statements.push(child);
|
|
408
431
|
collect_statement_bindings(child, transform_context.available_bindings);
|
|
409
432
|
}
|
|
410
433
|
}
|
|
411
434
|
|
|
412
|
-
|
|
435
|
+
if (!interleaved) {
|
|
436
|
+
hoist_static_render_nodes(render_nodes, transform_context);
|
|
437
|
+
}
|
|
413
438
|
|
|
414
439
|
const return_arg = build_return_expression(render_nodes);
|
|
415
440
|
if (return_arg || return_null_when_empty) {
|
|
@@ -423,6 +448,22 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
|
|
|
423
448
|
return statements;
|
|
424
449
|
}
|
|
425
450
|
|
|
451
|
+
/**
|
|
452
|
+
* React-specific wrapper around the core `isInterleavedBody` helper that
|
|
453
|
+
* ignores bare `return` / lone return-if statements. Those are rewriting
|
|
454
|
+
* signals rather than user-visible side effects, so JSX children around
|
|
455
|
+
* them don't need capturing.
|
|
456
|
+
*
|
|
457
|
+
* @param {any[]} body_nodes
|
|
458
|
+
* @returns {boolean}
|
|
459
|
+
*/
|
|
460
|
+
function is_interleaved_body(body_nodes) {
|
|
461
|
+
const filtered = body_nodes.filter(
|
|
462
|
+
(child) => !is_bare_return_statement(child) && !is_lone_return_if_statement(child),
|
|
463
|
+
);
|
|
464
|
+
return is_interleaved_body_core(filtered, is_jsx_child);
|
|
465
|
+
}
|
|
466
|
+
|
|
426
467
|
/**
|
|
427
468
|
* @param {any[]} body_nodes
|
|
428
469
|
* @returns {number}
|
|
@@ -535,34 +576,6 @@ function is_hook_callee(callee) {
|
|
|
535
576
|
return false;
|
|
536
577
|
}
|
|
537
578
|
|
|
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
579
|
/**
|
|
567
580
|
* @param {any[]} body_nodes
|
|
568
581
|
* @param {{ base_name: string, next_id: number, helpers: AST.FunctionDeclaration[], statics: any[] }} helper_state
|
|
@@ -881,6 +894,116 @@ function references_scope_bindings(node, scope_bindings) {
|
|
|
881
894
|
return false;
|
|
882
895
|
}
|
|
883
896
|
|
|
897
|
+
/**
|
|
898
|
+
* @param {AST.Literal} node
|
|
899
|
+
* @returns {boolean}
|
|
900
|
+
*/
|
|
901
|
+
function is_static_literal(node) {
|
|
902
|
+
return (
|
|
903
|
+
node.value === null ||
|
|
904
|
+
typeof node.value === 'string' ||
|
|
905
|
+
typeof node.value === 'number' ||
|
|
906
|
+
typeof node.value === 'boolean' ||
|
|
907
|
+
typeof node.value === 'bigint'
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* @param {any} node
|
|
913
|
+
* @returns {boolean}
|
|
914
|
+
*/
|
|
915
|
+
function is_hoist_safe_expression(node) {
|
|
916
|
+
if (!node || typeof node !== 'object') return false;
|
|
917
|
+
|
|
918
|
+
switch (node.type) {
|
|
919
|
+
case 'Literal':
|
|
920
|
+
return is_static_literal(node);
|
|
921
|
+
case 'TemplateLiteral':
|
|
922
|
+
return node.expressions.length === 0;
|
|
923
|
+
case 'UnaryExpression':
|
|
924
|
+
return node.operator !== 'delete' && is_hoist_safe_expression(node.argument);
|
|
925
|
+
case 'BinaryExpression':
|
|
926
|
+
case 'LogicalExpression':
|
|
927
|
+
return is_hoist_safe_expression(node.left) && is_hoist_safe_expression(node.right);
|
|
928
|
+
case 'ConditionalExpression':
|
|
929
|
+
return (
|
|
930
|
+
is_hoist_safe_expression(node.test) &&
|
|
931
|
+
is_hoist_safe_expression(node.consequent) &&
|
|
932
|
+
is_hoist_safe_expression(node.alternate)
|
|
933
|
+
);
|
|
934
|
+
case 'SequenceExpression':
|
|
935
|
+
return node.expressions.every(is_hoist_safe_expression);
|
|
936
|
+
case 'ParenthesizedExpression':
|
|
937
|
+
return is_hoist_safe_expression(node.expression);
|
|
938
|
+
case 'JSXElement':
|
|
939
|
+
return is_hoist_safe_jsx_node(node);
|
|
940
|
+
case 'JSXFragment':
|
|
941
|
+
return node.children.every(is_hoist_safe_jsx_child);
|
|
942
|
+
default:
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* @param {any} node
|
|
949
|
+
* @returns {boolean}
|
|
950
|
+
*/
|
|
951
|
+
function is_hoist_safe_jsx_child(node) {
|
|
952
|
+
if (!node || typeof node !== 'object') return false;
|
|
953
|
+
|
|
954
|
+
switch (node.type) {
|
|
955
|
+
case 'JSXText':
|
|
956
|
+
return true;
|
|
957
|
+
case 'JSXElement':
|
|
958
|
+
return is_hoist_safe_jsx_node(node);
|
|
959
|
+
case 'JSXFragment':
|
|
960
|
+
return node.children.every(is_hoist_safe_jsx_child);
|
|
961
|
+
case 'JSXExpressionContainer':
|
|
962
|
+
return (
|
|
963
|
+
node.expression.type !== 'JSXEmptyExpression' && is_hoist_safe_expression(node.expression)
|
|
964
|
+
);
|
|
965
|
+
default:
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* @param {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} attribute
|
|
972
|
+
* @returns {boolean}
|
|
973
|
+
*/
|
|
974
|
+
function is_hoist_safe_jsx_attribute(attribute) {
|
|
975
|
+
if (attribute.type === 'JSXSpreadAttribute') return false;
|
|
976
|
+
if (attribute.value == null) return true;
|
|
977
|
+
|
|
978
|
+
if (attribute.value.type === 'Literal') {
|
|
979
|
+
return is_static_literal(attribute.value);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (attribute.value.type === 'JSXExpressionContainer') {
|
|
983
|
+
return (
|
|
984
|
+
attribute.value.expression.type !== 'JSXEmptyExpression' &&
|
|
985
|
+
is_hoist_safe_expression(attribute.value.expression)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
|
|
994
|
+
* @returns {boolean}
|
|
995
|
+
*/
|
|
996
|
+
function is_hoist_safe_jsx_node(node) {
|
|
997
|
+
if (node.type === 'JSXFragment') {
|
|
998
|
+
return node.children.every(is_hoist_safe_jsx_child);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return (
|
|
1002
|
+
node.openingElement.attributes.every(is_hoist_safe_jsx_attribute) &&
|
|
1003
|
+
node.children.every(is_hoist_safe_jsx_child)
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
884
1007
|
/**
|
|
885
1008
|
* Hoist static JSX elements from render_nodes to module level.
|
|
886
1009
|
* A JSX element is static if it doesn't reference any component-scope bindings.
|
|
@@ -896,6 +1019,7 @@ function hoist_static_render_nodes(render_nodes, transform_context) {
|
|
|
896
1019
|
for (let i = 0; i < render_nodes.length; i++) {
|
|
897
1020
|
const node = render_nodes[i];
|
|
898
1021
|
if (node.type !== 'JSXElement') continue;
|
|
1022
|
+
if (!is_hoist_safe_jsx_node(node)) continue;
|
|
899
1023
|
if (references_scope_bindings(node, transform_context.available_bindings)) continue;
|
|
900
1024
|
|
|
901
1025
|
const name = create_helper_name(transform_context.helper_state, 'static');
|
|
@@ -1092,6 +1216,19 @@ function to_jsx_element(node, transform_context) {
|
|
|
1092
1216
|
return dynamic_element_to_jsx_child(node, transform_context);
|
|
1093
1217
|
}
|
|
1094
1218
|
|
|
1219
|
+
if (!node.id) {
|
|
1220
|
+
const children = create_element_children(node.children || [], transform_context);
|
|
1221
|
+
return set_loc(
|
|
1222
|
+
/** @type {any} */ ({
|
|
1223
|
+
type: 'JSXFragment',
|
|
1224
|
+
openingFragment: { type: 'JSXOpeningFragment' },
|
|
1225
|
+
closingFragment: { type: 'JSXClosingFragment' },
|
|
1226
|
+
children,
|
|
1227
|
+
}),
|
|
1228
|
+
node,
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1095
1232
|
const name = identifier_to_jsx_name(node.id);
|
|
1096
1233
|
const attributes = (node.attributes || []).map(to_jsx_attribute);
|
|
1097
1234
|
const selfClosing = !!node.selfClosing;
|