@tsrx/react 0.0.7 → 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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/transform.js +66 -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.7",
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.5"
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
- render_nodes.push(to_jsx_child(child, transform_context));
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
- hoist_static_render_nodes(render_nodes, transform_context);
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
- render_nodes.push(to_jsx_child(child, transform_context));
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
- hoist_static_render_nodes(render_nodes, transform_context);
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
@@ -1203,6 +1216,19 @@ function to_jsx_element(node, transform_context) {
1203
1216
  return dynamic_element_to_jsx_child(node, transform_context);
1204
1217
  }
1205
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
+
1206
1232
  const name = identifier_to_jsx_name(node.id);
1207
1233
  const attributes = (node.attributes || []).map(to_jsx_attribute);
1208
1234
  const selfClosing = !!node.selfClosing;