@tsrx/core 0.1.1 → 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.1",
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
@@ -202,6 +202,7 @@ export function TSRXPlugin(config) {
202
202
  // Some parser constructors (e.g. via TS plugins) expose `tokContexts` without `b_stat`.
203
203
  // If we push an undefined context, Acorn's tokenizer will later crash reading `.override`.
204
204
  const b_stat = tc.b_stat || acorn.tokContexts.b_stat;
205
+ const b_expr = tc.b_expr || acorn.tokContexts.b_expr;
205
206
  const tstt = Parser.acornTypeScript.tokTypes;
206
207
  const tstc = Parser.acornTypeScript.tokContexts;
207
208
 
@@ -219,6 +220,7 @@ export function TSRXPlugin(config) {
219
220
  #errors = undefined;
220
221
  /** @type {string | null} */
221
222
  #filename = null;
223
+ #componentDepth = 0;
222
224
  #functionBodyDepth = 0;
223
225
 
224
226
  /**
@@ -269,6 +271,14 @@ export function TSRXPlugin(config) {
269
271
  return null;
270
272
  }
271
273
 
274
+ #isInsideComponent() {
275
+ return this.#componentDepth > 0;
276
+ }
277
+
278
+ #isInsideComponentTemplate() {
279
+ return this.#isInsideComponent() && this.#functionBodyDepth === 0;
280
+ }
281
+
272
282
  #popTsxTokenContextBeforeTemplateExpressionChild() {
273
283
  let index = this.pos;
274
284
  let has_newline = false;
@@ -319,6 +329,137 @@ export function TSRXPlugin(config) {
319
329
  }
320
330
  }
321
331
 
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
+ }
354
+ }
355
+ return index;
356
+ }
357
+
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.
451
+ if (
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 === '[')
457
+ ) {
458
+ ctx.pop();
459
+ this.exprAllowed = false;
460
+ }
461
+ }
462
+
322
463
  #isDoubleQuotedTextChildStart() {
323
464
  if (this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx')) {
324
465
  return false;
@@ -717,7 +858,7 @@ export function TSRXPlugin(config) {
717
858
 
718
859
  if (code === 60) {
719
860
  // < character
720
- const inComponent = this.#path.findLast((n) => n.type === 'Component');
861
+ const inComponent = this.#isInsideComponentTemplate();
721
862
  /** @type {number | null} */
722
863
  let prevNonWhitespaceChar = null;
723
864
 
@@ -1073,11 +1214,13 @@ export function TSRXPlugin(config) {
1073
1214
  this.eat(tt.braceL);
1074
1215
  node.body = [];
1075
1216
  this.#path.push(node);
1217
+ this.#componentDepth++;
1076
1218
 
1077
1219
  try {
1078
1220
  this.parseTemplateBody(node.body);
1079
1221
  } finally {
1080
1222
  this.#functionBodyDepth = parent_function_body_depth;
1223
+ this.#componentDepth--;
1081
1224
  }
1082
1225
  this.#path.pop();
1083
1226
  this.exitScope();
@@ -1301,6 +1444,23 @@ export function TSRXPlugin(config) {
1301
1444
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1302
1445
  this.#functionBodyDepth++;
1303
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
+ }
1304
1464
 
1305
1465
  try {
1306
1466
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
@@ -1910,9 +2070,20 @@ export function TSRXPlugin(config) {
1910
2070
  // Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
1911
2071
  // Bare fragments (<></>) are shorthand for <tsx>...</tsx>.
1912
2072
  this.next();
1913
- return /** @type {import('estree-jsx').JSXElement} */ (
2073
+ const parsed = /** @type {import('estree-jsx').JSXElement} */ (
1914
2074
  /** @type {unknown} */ (this.parseElement())
1915
2075
  );
2076
+ this.#popTokenContextsAfterTemplateExpressionElement(
2077
+ /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
2078
+ );
2079
+ return parsed;
2080
+ }
2081
+
2082
+ if (
2083
+ !this.#path.findLast((node) => node.type === 'Component') &&
2084
+ !this.#functionStack.findLast(is_pascal_case_function)
2085
+ ) {
2086
+ return super.jsx_parseElement();
1916
2087
  }
1917
2088
 
1918
2089
  const code = this.#functionStack.findLast(is_pascal_case_function)
@@ -2828,6 +2999,15 @@ export function TSRXPlugin(config) {
2828
2999
  if (!node) {
2829
3000
  this.unexpected();
2830
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
+ }
2831
3011
  return node;
2832
3012
  }
2833
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',