@tsrx/core 0.1.2 → 0.1.3

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core compiler infrastructure for TSRX syntax",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.1.2",
6
+ "version": "0.1.3",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
package/src/index.js CHANGED
@@ -149,6 +149,7 @@ export {
149
149
  MERGE_REFS_INTERNAL_NAME,
150
150
  merge_duplicate_refs as mergeDuplicateRefs,
151
151
  NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
152
+ return_value_body_to_expression as returnValueBodyToExpression,
152
153
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
153
154
  to_jsx_attribute as toJsxAttribute,
154
155
  validate_at_most_one_ref_attribute as validateAtMostOneRefAttribute,
package/src/plugin.js CHANGED
@@ -220,6 +220,7 @@ export function TSRXPlugin(config) {
220
220
  #errors = undefined;
221
221
  /** @type {string | null} */
222
222
  #filename = null;
223
+ #componentDepth = 0;
223
224
  #functionBodyDepth = 0;
224
225
 
225
226
  /**
@@ -270,6 +271,14 @@ export function TSRXPlugin(config) {
270
271
  return null;
271
272
  }
272
273
 
274
+ #isInsideComponent() {
275
+ return this.#componentDepth > 0;
276
+ }
277
+
278
+ #isInsideComponentTemplate() {
279
+ return this.#isInsideComponent() && this.#functionBodyDepth === 0;
280
+ }
281
+
273
282
  #popTsxTokenContextBeforeTemplateExpressionChild() {
274
283
  let index = this.pos;
275
284
  let has_newline = false;
@@ -320,17 +329,133 @@ export function TSRXPlugin(config) {
320
329
  }
321
330
  }
322
331
 
323
- #popJsxAttributeExpressionContextAfterTemplateElement() {
324
- if (this.type !== tt.braceR) {
325
- return;
332
+ /**
333
+ * @param {number} index
334
+ * @returns {number}
335
+ */
336
+ #skipWhitespaceAndComments(index) {
337
+ while (index < this.input.length) {
338
+ const ch = this.input.charCodeAt(index);
339
+ if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
340
+ index++;
341
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 42) {
342
+ const end = this.input.indexOf('*/', index + 2);
343
+ index = end === -1 ? this.input.length : end + 2;
344
+ } else if (ch === 47 && this.input.charCodeAt(index + 1) === 47) {
345
+ index += 2;
346
+ while (index < this.input.length) {
347
+ const comment_ch = this.input.charCodeAt(index);
348
+ if (comment_ch === 10 || comment_ch === 13) break;
349
+ index++;
350
+ }
351
+ } else {
352
+ break;
353
+ }
326
354
  }
355
+ return index;
356
+ }
327
357
 
328
- const context_index = this.context.length - 1;
358
+ /** @returns {number} */
359
+ #countFollowingRightBraces() {
360
+ let index = this.end;
361
+ let count = 0;
362
+ while (index < this.input.length) {
363
+ index = this.#skipWhitespaceAndComments(index);
364
+ if (this.input.charCodeAt(index) !== 125) break;
365
+ count++;
366
+ index++;
367
+ }
368
+ return count;
369
+ }
370
+
371
+ /**
372
+ * @param {AST.Tsx | AST.Tsrx | AST.TsxCompat} node
373
+ * @returns {boolean}
374
+ */
375
+ #hasDirectStatementChild(node) {
376
+ return node.children?.some(
377
+ (child) => child.type.endsWith('Statement') || child.type === 'VariableDeclaration',
378
+ );
379
+ }
380
+
381
+ /**
382
+ * @param {AST.Tsx | AST.Tsrx | AST.TsxCompat} node
383
+ */
384
+ #popTokenContextsAfterTemplateExpressionElement(node) {
385
+ const ctx = this.context;
386
+ const ci = ctx.length - 1;
387
+ const top = ctx[ci];
388
+ const second = ctx[ci - 1];
389
+
390
+ // Expression-bodied templates (no statement child) followed by `,`
391
+ // in an object/array literal need surgical fixups; statement-bodied
392
+ // templates fall through to the JSX-expression-container strip.
393
+ const has_stmt_child = this.#hasDirectStatementChild(node);
394
+ if (this.type === tt.comma && !has_stmt_child) {
395
+ // Tail `..., (b_expr)+, tc_expr, b_stat`: the JSX expression
396
+ // container leaks an extra `tc_expr, b_stat`. Pop them, and if
397
+ // the JSX container also closes immediately (`}}` ahead), drop
398
+ // one of the doubled-up `b_expr` contexts too.
399
+ if (top === b_stat && second === tstc.tc_expr) {
400
+ let expr_count = 0;
401
+ for (let i = ci - 2; ctx[i] === b_expr; i--) expr_count++;
402
+ const following_braces = this.#countFollowingRightBraces();
403
+ if (expr_count === 2 || following_braces > 1) {
404
+ if (following_braces > 1 && expr_count > 1) {
405
+ ctx.splice(ci - 2, expr_count - 1);
406
+ ctx.pop();
407
+ this.exprAllowed = false;
408
+ return;
409
+ }
410
+ if (expr_count === 2 && following_braces === 0) {
411
+ // Fragment expression value followed by another
412
+ // object/array entry inside a JSX expression
413
+ // container (`{ a: <></>, b: ... }` or
414
+ // `[<></>, ...]`): strip both the leaked tc_expr
415
+ // and b_stat so the next entry parses as an
416
+ // expression, and leave exprAllowed alone so a
417
+ // following `<` still tokenizes as jsxTagStart.
418
+ ctx.length = ci - 1;
419
+ return;
420
+ }
421
+ ctx.pop();
422
+ this.exprAllowed = false;
423
+ return;
424
+ }
425
+ }
426
+ // Tail `..., b_expr, b_expr` for fragments-with-children
427
+ // inside an array or object literal: re-arm expression mode
428
+ // so the next item parses as an expression value, not a JSX
429
+ // child. If the surrounding b_expr chain has already been
430
+ // consumed, push one back so the subsequent item still has
431
+ // a literal context. Leave exprAllowed alone so a following
432
+ // `<` still tokenizes as jsxTagStart.
433
+ if (top === b_expr && second === b_expr) {
434
+ if (ctx[ci - 2] !== b_expr && ctx[ci - 2] !== tstc.tc_oTag) {
435
+ ctx.push(b_expr);
436
+ }
437
+ return;
438
+ }
439
+ }
440
+
441
+ // Inside `{<tsrx>...</tsrx>}` JSX expression container — strip
442
+ // both the leaked `b_stat` and the container's `tc_expr`.
443
+ if (top === b_stat && second === tstc.tc_expr) {
444
+ ctx.length = ci - 1;
445
+ return;
446
+ }
447
+ // Closing token after the template at expression position. For `}`
448
+ // only pop if it actually closes this `b_expr` — otherwise the
449
+ // brace targets an inner callback/object body that should pop it
450
+ // naturally on the next token step.
329
451
  if (
330
- this.context[context_index] === b_expr &&
331
- this.context[context_index - 1] === tstc.tc_oTag
452
+ (this.type === tt.braceR &&
453
+ top === b_expr &&
454
+ (this.#countFollowingRightBraces() === 0 || second === b_expr)) ||
455
+ (this.type === tt.parenR && top?.token === '(') ||
456
+ (this.type === tt.bracketR && top?.token === '[')
332
457
  ) {
333
- this.context.pop();
458
+ ctx.pop();
334
459
  this.exprAllowed = false;
335
460
  }
336
461
  }
@@ -733,7 +858,7 @@ export function TSRXPlugin(config) {
733
858
 
734
859
  if (code === 60) {
735
860
  // < character
736
- const inComponent = this.#path.findLast((n) => n.type === 'Component');
861
+ const inComponent = this.#isInsideComponentTemplate();
737
862
  /** @type {number | null} */
738
863
  let prevNonWhitespaceChar = null;
739
864
 
@@ -1089,11 +1214,13 @@ export function TSRXPlugin(config) {
1089
1214
  this.eat(tt.braceL);
1090
1215
  node.body = [];
1091
1216
  this.#path.push(node);
1217
+ this.#componentDepth++;
1092
1218
 
1093
1219
  try {
1094
1220
  this.parseTemplateBody(node.body);
1095
1221
  } finally {
1096
1222
  this.#functionBodyDepth = parent_function_body_depth;
1223
+ this.#componentDepth--;
1097
1224
  }
1098
1225
  this.#path.pop();
1099
1226
  this.exitScope();
@@ -1317,6 +1444,23 @@ export function TSRXPlugin(config) {
1317
1444
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1318
1445
  this.#functionBodyDepth++;
1319
1446
  this.#functionStack.push(node);
1447
+ // Inside a component, nested JS function bodies should parse like
1448
+ // ordinary functions, not component template bodies.
1449
+ if (
1450
+ // Only adjust functions declared while parsing a component body.
1451
+ this.#isInsideComponent() &&
1452
+ // A stale JSX expression context means the surrounding template
1453
+ // tokenizer can still treat `<` as template markup.
1454
+ this.context.some((context) => context === tstc.tc_expr) &&
1455
+ // Keep arrows/functions inside JSX tags, such as event handlers,
1456
+ // on the normal JSX attribute parsing path.
1457
+ !this.context.some((context) => context === tstc.tc_oTag || context === tstc.tc_cTag) &&
1458
+ // Only reset statement-level function bodies, not expression
1459
+ // contexts that are actively parsing JSX.
1460
+ this.curContext() === b_stat
1461
+ ) {
1462
+ this.context = [b_stat];
1463
+ }
1320
1464
 
1321
1465
  try {
1322
1466
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
@@ -1929,7 +2073,9 @@ export function TSRXPlugin(config) {
1929
2073
  const parsed = /** @type {import('estree-jsx').JSXElement} */ (
1930
2074
  /** @type {unknown} */ (this.parseElement())
1931
2075
  );
1932
- this.#popJsxAttributeExpressionContextAfterTemplateElement();
2076
+ this.#popTokenContextsAfterTemplateExpressionElement(
2077
+ /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2078
+ );
1933
2079
  return parsed;
1934
2080
  }
1935
2081
 
@@ -2853,6 +2999,15 @@ export function TSRXPlugin(config) {
2853
2999
  if (!node) {
2854
3000
  this.unexpected();
2855
3001
  }
3002
+ if (this.#functionBodyDepth > 0 && node.type === 'Tsrx' && this.curContext() === b_stat) {
3003
+ this.context.pop();
3004
+ if (this.curContext() === tstc.tc_expr) {
3005
+ this.context.pop();
3006
+ }
3007
+ if (this.curContext() === b_stat) {
3008
+ this.context.pop();
3009
+ }
3010
+ }
2856
3011
  return node;
2857
3012
  }
2858
3013
 
@@ -682,7 +682,7 @@ function build_component_statements(body_nodes, transform_context) {
682
682
  function build_render_statements(body_nodes, return_null_when_empty, transform_context) {
683
683
  const statements = [];
684
684
  const render_nodes = [];
685
- let has_bare_return = false;
685
+ let has_terminal_return = false;
686
686
 
687
687
  // Create a new bindings map so inner-scope bindings from
688
688
  // collect_statement_bindings don't leak to the caller's scope.
@@ -707,7 +707,13 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
707
707
  if (is_bare_return_statement(child)) {
708
708
  statements.push(create_component_return_statement(render_nodes, child));
709
709
  render_nodes.length = 0;
710
- has_bare_return = true;
710
+ has_terminal_return = true;
711
+ continue;
712
+ }
713
+
714
+ if (child?.type === 'ReturnStatement' && child.argument != null) {
715
+ statements.push(child);
716
+ has_terminal_return = true;
711
717
  continue;
712
718
  }
713
719
 
@@ -968,7 +974,7 @@ function build_render_statements(body_nodes, return_null_when_empty, transform_c
968
974
  }
969
975
 
970
976
  const return_arg = build_return_expression(render_nodes);
971
- if (return_arg || (return_null_when_empty && !has_bare_return)) {
977
+ if (return_arg || (return_null_when_empty && !has_terminal_return)) {
972
978
  statements.push({
973
979
  type: 'ReturnStatement',
974
980
  argument: return_arg || { type: 'Literal', value: null, raw: 'null' },
@@ -2761,7 +2767,10 @@ function child_contains_return_semantics(node) {
2761
2767
  return false;
2762
2768
  }
2763
2769
 
2764
- if (node.type === 'ReturnStatement' || is_lone_return_if_statement(node)) {
2770
+ if (
2771
+ (node.type === 'ReturnStatement' && node.argument == null) ||
2772
+ is_lone_return_if_statement(node)
2773
+ ) {
2765
2774
  return true;
2766
2775
  }
2767
2776
 
@@ -3448,22 +3457,25 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
3448
3457
  let expression;
3449
3458
  if (children.length === 0) {
3450
3459
  expression = create_null_literal();
3451
- } else if (
3452
- children.every(is_inline_element_child) &&
3453
- !children_contain_return_semantics(children)
3454
- ) {
3455
- const saved_inside_element_child = transform_context.inside_element_child;
3456
- transform_context.inside_element_child = true;
3457
- try {
3458
- const render_nodes = children.map((/** @type {any} */ child) =>
3459
- to_jsx_child(child, transform_context),
3460
- );
3461
- expression = build_return_expression(render_nodes) || create_null_literal();
3462
- } finally {
3463
- transform_context.inside_element_child = saved_inside_element_child;
3464
- }
3465
3460
  } else {
3466
- expression = statement_body_to_jsx_child(children, transform_context).expression;
3461
+ expression = return_value_body_to_expression(children, node, transform_context);
3462
+ }
3463
+
3464
+ if (!expression) {
3465
+ if (children.every(is_inline_element_child) && !children_contain_return_semantics(children)) {
3466
+ const saved_inside_element_child = transform_context.inside_element_child;
3467
+ transform_context.inside_element_child = true;
3468
+ try {
3469
+ const render_nodes = children.map((/** @type {any} */ child) =>
3470
+ to_jsx_child(child, transform_context),
3471
+ );
3472
+ expression = build_return_expression(render_nodes) || create_null_literal();
3473
+ } finally {
3474
+ transform_context.inside_element_child = saved_inside_element_child;
3475
+ }
3476
+ } else {
3477
+ expression = statement_body_to_jsx_child(children, transform_context).expression;
3478
+ }
3467
3479
  }
3468
3480
 
3469
3481
  if (
@@ -3479,6 +3491,149 @@ function tsrx_node_to_jsx_expression(node, transform_context, in_jsx_child = fal
3479
3491
  return expression;
3480
3492
  }
3481
3493
 
3494
+ /**
3495
+ * Explicit return values inside expression-position `<tsrx>` templates are JavaScript
3496
+ * values, so keep them out of platform render control flow.
3497
+ *
3498
+ * @param {any[]} body_nodes
3499
+ * @param {any} source_node
3500
+ * @param {TransformContext} [transform_context]
3501
+ * @returns {any | null}
3502
+ */
3503
+ export function return_value_body_to_expression(body_nodes, source_node, transform_context) {
3504
+ if (!body_contains_top_level_return_value(body_nodes)) return null;
3505
+
3506
+ if (body_nodes.length === 1) {
3507
+ const expression = return_value_statement_to_expression(body_nodes[0], transform_context);
3508
+ if (expression) return expression;
3509
+ }
3510
+
3511
+ return create_statement_iife(body_nodes, source_node, transform_context);
3512
+ }
3513
+
3514
+ /**
3515
+ * @param {any} node
3516
+ * @param {TransformContext} [transform_context]
3517
+ * @returns {any | null}
3518
+ */
3519
+ function return_value_statement_to_expression(node, transform_context) {
3520
+ if (node?.type === 'ReturnStatement' && node.argument != null) {
3521
+ return node.argument;
3522
+ }
3523
+
3524
+ if (node?.type === 'IfStatement') {
3525
+ return return_value_if_statement_to_conditional_expression(node, transform_context);
3526
+ }
3527
+
3528
+ return null;
3529
+ }
3530
+
3531
+ /**
3532
+ * @param {any} node
3533
+ * @returns {boolean}
3534
+ */
3535
+ function body_contains_top_level_return_value(node) {
3536
+ if (!node || typeof node !== 'object') return false;
3537
+
3538
+ if (Array.isArray(node)) {
3539
+ return node.some(body_contains_top_level_return_value);
3540
+ }
3541
+
3542
+ if (node.type === 'ReturnStatement') {
3543
+ return node.argument != null;
3544
+ }
3545
+
3546
+ if (
3547
+ node.type === 'FunctionDeclaration' ||
3548
+ node.type === 'FunctionExpression' ||
3549
+ node.type === 'ArrowFunctionExpression' ||
3550
+ node.type === 'ClassDeclaration' ||
3551
+ node.type === 'ClassExpression' ||
3552
+ node.type === 'Component'
3553
+ ) {
3554
+ return false;
3555
+ }
3556
+
3557
+ for (const key of Object.keys(node)) {
3558
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3559
+ continue;
3560
+ }
3561
+ if (body_contains_top_level_return_value(node[key])) {
3562
+ return true;
3563
+ }
3564
+ }
3565
+
3566
+ return false;
3567
+ }
3568
+
3569
+ /**
3570
+ * @param {any[]} body_nodes
3571
+ * @param {any} source_node
3572
+ * @param {TransformContext} [transform_context]
3573
+ * @returns {any}
3574
+ */
3575
+ function create_statement_iife(body_nodes, source_node, transform_context) {
3576
+ return set_generated_expression_loc(
3577
+ b.call(b.arrow([], b.block(body_nodes))),
3578
+ source_node,
3579
+ transform_context,
3580
+ );
3581
+ }
3582
+
3583
+ /**
3584
+ * @param {any} node
3585
+ * @param {any} source_node
3586
+ * @param {TransformContext} [transform_context]
3587
+ * @returns {any}
3588
+ */
3589
+ function set_generated_expression_loc(node, source_node, transform_context) {
3590
+ if (transform_context?.typeOnly || !source_node?.loc) return node;
3591
+ return setLocation(/** @type {any} */ (node), source_node);
3592
+ }
3593
+
3594
+ /**
3595
+ * @returns {any}
3596
+ */
3597
+ function create_undefined_expression() {
3598
+ return b.unary('void', b.literal(0));
3599
+ }
3600
+
3601
+ /**
3602
+ * @param {any} node
3603
+ * @param {TransformContext} [transform_context]
3604
+ * @returns {any | null}
3605
+ */
3606
+ function return_value_block_to_expression(node, transform_context) {
3607
+ const body = node?.type === 'BlockStatement' ? node.body : node ? [node] : [];
3608
+ if (body.length !== 1) return null;
3609
+
3610
+ return return_value_statement_to_expression(body[0], transform_context);
3611
+ }
3612
+
3613
+ /**
3614
+ * @param {any} node
3615
+ * @param {TransformContext} [transform_context]
3616
+ * @returns {any | null}
3617
+ */
3618
+ function return_value_if_statement_to_conditional_expression(node, transform_context) {
3619
+ if (!node || node.type !== 'IfStatement') return null;
3620
+
3621
+ const consequent = return_value_block_to_expression(node.consequent, transform_context);
3622
+ if (!consequent) return null;
3623
+
3624
+ let alternate = create_undefined_expression();
3625
+ if (node.alternate) {
3626
+ alternate = return_value_block_to_expression(node.alternate, transform_context);
3627
+ if (!alternate) return null;
3628
+ }
3629
+
3630
+ return set_generated_expression_loc(
3631
+ b.conditional(node.test, consequent, alternate),
3632
+ node,
3633
+ transform_context,
3634
+ );
3635
+ }
3636
+
3482
3637
  /**
3483
3638
  * @param {any} node
3484
3639
  * @param {TransformContext} transform_context
@@ -4009,7 +4164,7 @@ function try_statement_to_jsx_child(node, transform_context) {
4009
4164
  );
4010
4165
  }
4011
4166
  const pending_body = pending.body || [];
4012
- if (!pending_body.some(is_jsx_child)) {
4167
+ if (pending_body.length > 0 && !pending_body.some(is_jsx_child)) {
4013
4168
  error(
4014
4169
  'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
4015
4170
  transform_context.filename,
@@ -4031,7 +4186,10 @@ function try_statement_to_jsx_child(node, transform_context) {
4031
4186
  if (pending) {
4032
4187
  transform_context.needs_suspense = true;
4033
4188
  const pending_body_nodes = pending.body || [];
4034
- const fallback_content = statement_body_to_jsx_child(pending_body_nodes, transform_context);
4189
+ const fallback_content =
4190
+ pending_body_nodes.length === 0
4191
+ ? to_jsx_expression_container(create_null_literal())
4192
+ : statement_body_to_jsx_child(pending_body_nodes, transform_context);
4035
4193
 
4036
4194
  result = create_jsx_element(
4037
4195
  'Suspense',