@tsrx/core 0.1.41 → 0.1.42

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.41",
6
+ "version": "0.1.42",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -63,11 +63,11 @@
63
63
  "dependencies": {
64
64
  "@jridgewell/sourcemap-codec": "^1.5.5",
65
65
  "@noble/hashes": "^2.2.0",
66
- "@sveltejs/acorn-typescript": "^1.0.10",
66
+ "@sveltejs/acorn-typescript": "^1.0.11",
67
67
  "@types/estree-jsx": "^1.0.5",
68
68
  "@types/estree": "^1.0.8",
69
- "acorn": "^8.16.0",
70
- "esrap": "^2.2.8",
69
+ "acorn": "^8.17.0",
70
+ "esrap": "^2.3.0",
71
71
  "is-reference": "^3.0.3",
72
72
  "magic-string": "^0.30.18",
73
73
  "zimmerframe": "^1.1.2"
@@ -219,12 +219,38 @@ export function createParser(...plugins) {
219
219
  /** @type {AST.Program} */
220
220
  let ast;
221
221
 
222
+ // Lexer-authoritative keyword positions (volar opt-in): the mapping
223
+ // collector needs the SOURCE spans of `async`/`function`, which no AST
224
+ // node records. The tokenizer is the only correct source — offset
225
+ // arithmetic breaks on extra whitespace, and text search breaks on
226
+ // comments (`async /* function */ function`).
227
+ /** @type {Array<{ value: string, start: number, end: number, loc: AST.SourceLocation }> | undefined} */
228
+ const keyword_tokens = options?.keywordTokens ? [] : undefined;
229
+ /** @type {Parse.Options['onToken'] | undefined} */
230
+ const onToken = keyword_tokens
231
+ ? (token) => {
232
+ const t = /** @type {any} */ (token);
233
+ const is_function_keyword = t.type?.keyword === 'function';
234
+ const is_async_name = t.type?.label === 'name' && t.value === 'async';
235
+ if (is_function_keyword || is_async_name) {
236
+ keyword_tokens.push({
237
+ value: is_function_keyword ? 'function' : 'async',
238
+ start: t.start,
239
+ end: t.end,
240
+ loc: t.loc,
241
+ });
242
+ }
243
+ }
244
+ : undefined;
245
+
222
246
  try {
223
247
  ast = parser.parse(source, {
224
248
  sourceType: 'module',
225
249
  ecmaVersion: 13,
226
250
  allowReturnOutsideFunction: true,
227
251
  locations: true,
252
+ onToken,
253
+ preserveParens: !!options?.preserveParens,
228
254
  onComment,
229
255
  tsrxOptions: {
230
256
  filename,
@@ -245,6 +271,10 @@ export function createParser(...plugins) {
245
271
 
246
272
  add_comments(ast);
247
273
 
274
+ if (keyword_tokens) {
275
+ /** @type {any} */ (ast).tsrx_keyword_tokens = keyword_tokens;
276
+ }
277
+
248
278
  return ast;
249
279
  };
250
280
  }
@@ -51,18 +51,26 @@ export function set_node_path_metadata(node, path) {
51
51
  }
52
52
 
53
53
  /**
54
- * Wrap esrap's `tsx()` printer with location markers for nodes whose spans
55
- * (e.g. the leading `new ` of a NewExpression or the angle-bracket delimiters
56
- * around generic arguments) are otherwise invisible to the source map.
57
- * Without these markers, Volar mapping collection in `segments.js` throws
58
- * when looking up the node's start/end positions.
54
+ * Wrap esrap's `tsx()` printer with location markers for the remaining nodes
55
+ * whose spans are invisible to the source map (e.g. `class`, template-literal
56
+ * backticks, JSX angle brackets, generic-argument delimiters). Without these
57
+ * markers, Volar mapping collection in `segments.js` throws when looking up
58
+ * the node's start/end positions. esrap ≥2.3.0 (keyword writes +
59
+ * `boundaryTokens`) covers most starts, but not statement ends — see the
60
+ * list below for what each entry still compensates.
59
61
  *
60
62
  * Shared across all JSX-producing targets (React, Preact, Solid).
61
63
  *
62
64
  * @returns {any}
63
65
  */
64
- export function tsx_with_ts_locations() {
65
- const base = /** @type {any} */ (tsx());
66
+ /**
67
+ * @param {boolean} [boundary_tokens] Enable esrap's `boundaryTokens` anchors
68
+ * (structural tokens carry one-character source locations). typeOnly/volar
69
+ * prints opt in — their maps are consumed positionally by the language
70
+ * tooling and never shipped; build prints stay sparse.
71
+ */
72
+ export function tsx_with_ts_locations(boundary_tokens = false) {
73
+ const base = /** @type {any} */ (tsx({ boundaryTokens: boundary_tokens }));
66
74
 
67
75
  /**
68
76
  * @param {any} node
@@ -87,15 +95,6 @@ export function tsx_with_ts_locations() {
87
95
  context.visit(node.typeAnnotation);
88
96
  }
89
97
  },
90
- Identifier: (node, context) => {
91
- context.write(node.name, node);
92
- if (node.optional) {
93
- context.write('?');
94
- }
95
- if (node.typeAnnotation) {
96
- context.visit(node.typeAnnotation);
97
- }
98
- },
99
98
  TSNamedTupleMember: (node, context) => {
100
99
  context.visit(node.label);
101
100
  if (node.optional) {
@@ -134,39 +133,6 @@ export function tsx_with_ts_locations() {
134
133
  context.write(' ');
135
134
  context.visit(value.body);
136
135
  },
137
- // esrap's ArrowFunctionExpression printer ignores `typeParameters` and
138
- // `returnType`, so an annotated arrow like `(): Record<...> => ...`
139
- // prints as `() => ...` and segments.js can't resolve the return-type
140
- // nodes' positions in the generated output.
141
- ArrowFunctionExpression: (node, context) => {
142
- if (node.async) context.write('async ');
143
- if (node.typeParameters) {
144
- context.visit(node.typeParameters);
145
- }
146
- context.write('(');
147
- for (let i = 0; i < node.params.length; i++) {
148
- if (i > 0) context.write(', ');
149
- context.visit(node.params[i]);
150
- }
151
- context.write(')');
152
- if (node.returnType) {
153
- context.visit(node.returnType);
154
- }
155
- context.write(' => ');
156
- const body = node.body;
157
- const wrap_body =
158
- body.type === 'ObjectExpression' ||
159
- (body.type === 'AssignmentExpression' && body.left.type === 'ObjectPattern') ||
160
- (body.type === 'LogicalExpression' && body.left.type === 'ObjectExpression') ||
161
- (body.type === 'ConditionalExpression' && body.test.type === 'ObjectExpression');
162
- if (wrap_body) {
163
- context.write('(');
164
- context.visit(body);
165
- context.write(')');
166
- } else {
167
- context.visit(body);
168
- }
169
- },
170
136
 
171
137
  // esrap's JSXOpeningElement printer doesn't emit `typeArguments`, so generic
172
138
  // component tags like `<RenderProp<User>>` lose the `<User>` in the output.
@@ -196,6 +162,11 @@ export function tsx_with_ts_locations() {
196
162
  }
197
163
  },
198
164
  TSModuleDeclaration: (node, context) => {
165
+ // Ambient `declare module '…' { … }` must keep its `declare` — the
166
+ // typeOnly/volar output is real TS and `module '…' { … }` alone is a
167
+ // syntax error (TS1035). Non-ambient `module name { }` blocks have no
168
+ // `declare` and print unchanged.
169
+ if (node.declare) context.write('declare ');
199
170
  context.write(node.metadata?.module_keyword ?? 'module');
200
171
  context.write(' ');
201
172
  context.visit(node.id);
@@ -209,9 +180,14 @@ export function tsx_with_ts_locations() {
209
180
  // on the whole node, only then duplicate it here
210
181
  // e.g. JSXOpeningElement is such a case
211
182
  for (const type of [
212
- // JS nodes whose esrap printer emits no location marker, causing
213
- // segments.js get_mapping_from_node() to throw when it asks for the
214
- // generated position of the node's start (or end).
183
+ // JS nodes with boundary positions esrap still cannot map. Keyword
184
+ // writes (if/new/return/for/switch/await) and `boundaryTokens`
185
+ // anchors (brackets, braces, parens, computed/call closers) cover
186
+ // many STARTS, but statement ENDS land on unanchored characters
187
+ // (`;`, a block's `}`), `class` is not a keyword-write, template
188
+ // literals' backticks carry no location, and an arrow's span can
189
+ // start at a bare `(` — so these node-level markers remain the
190
+ // source of both boundaries until esrap can anchor them.
215
191
  'ClassDeclaration',
216
192
  'ClassExpression',
217
193
  'IfStatement',
@@ -214,32 +214,53 @@ function lower_code_block_child(block) {
214
214
  * `lower_code_block_child`). This is the element-scoped equivalent of
215
215
  * `transform_function`'s body lowering — function and arrow bodies are never
216
216
  * element children, so they are untouched here.
217
+ *
218
+ * The input tree is never mutated: replacements land on a shallow copy of the
219
+ * owning node (or array), so the return value must be used in place of the
220
+ * argument. Untouched subtrees are shared by reference with the input.
217
221
  * @param {any} node
218
222
  * @param {Set<any>} [seen]
219
- * @returns {void}
223
+ * @returns {any}
220
224
  */
221
225
  function expand_child_code_blocks(node, seen = new Set()) {
222
- if (!node || typeof node !== 'object' || seen.has(node)) return;
226
+ if (!node || typeof node !== 'object' || seen.has(node)) return node;
223
227
  seen.add(node);
224
228
 
225
229
  if (Array.isArray(node)) {
226
- for (const item of node) expand_child_code_blocks(item, seen);
227
- return;
230
+ let changed = false;
231
+ const result = node.map((item) => {
232
+ const walked = expand_child_code_blocks(item, seen);
233
+ if (walked !== item) changed = true;
234
+ return walked;
235
+ });
236
+ return changed ? result : node;
228
237
  }
229
238
 
239
+ let out = node;
240
+ const set = (/** @type {string} */ key, /** @type {any} */ value) => {
241
+ if (out[key] === value) return;
242
+ if (out === node) out = { ...node };
243
+ out[key] = value;
244
+ };
245
+
230
246
  if (
231
247
  Array.isArray(node.children) &&
232
248
  node.children.some((/** @type {any} */ c) => c?.type === 'JSXCodeBlock')
233
249
  ) {
234
- node.children = node.children.flatMap((/** @type {any} */ child) =>
235
- child?.type === 'JSXCodeBlock' ? lower_code_block_child(child) : [child],
250
+ set(
251
+ 'children',
252
+ node.children.flatMap((/** @type {any} */ child) =>
253
+ child?.type === 'JSXCodeBlock' ? lower_code_block_child(child) : [child],
254
+ ),
236
255
  );
237
256
  }
238
257
 
239
- for (const key of Object.keys(node)) {
258
+ for (const key of Object.keys(out)) {
240
259
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
241
- expand_child_code_blocks(node[key], seen);
260
+ set(key, expand_child_code_blocks(out[key], seen));
242
261
  }
262
+
263
+ return out;
243
264
  }
244
265
 
245
266
  /**
@@ -418,13 +439,17 @@ function wrap_lowered_value_in_fragment(expression, source) {
418
439
  * its rendered value. Either way nothing leaks to the printer as a raw
419
440
  * `JSX…Expression`.
420
441
  *
442
+ * The input tree is never mutated: every replacement lands on a shallow copy of
443
+ * the owning node (or array), so the return value must be used in place of the
444
+ * argument. Untouched subtrees are shared by reference with the input.
445
+ *
421
446
  * @param {any} node
422
447
  * @param {TransformContext} transform_context
423
448
  * @param {Set<any>} [seen]
424
- * @returns {void}
449
+ * @returns {any}
425
450
  */
426
451
  function wrap_control_flow_expression_values(node, transform_context, seen = new Set()) {
427
- if (!node || typeof node !== 'object' || seen.has(node)) return;
452
+ if (!node || typeof node !== 'object' || seen.has(node)) return node;
428
453
  seen.add(node);
429
454
 
430
455
  // Dynamic tags on factory platforms must lower before control-flow
@@ -454,11 +479,17 @@ function wrap_control_flow_expression_values(node, transform_context, seen = new
454
479
  : value;
455
480
 
456
481
  if (Array.isArray(node)) {
457
- for (let i = 0; i < node.length; i++) {
458
- node[i] = lower_child(node[i]);
459
- wrap_control_flow_expression_values(node[i], transform_context, seen);
460
- }
461
- return;
482
+ let changed = false;
483
+ const result = node.map((entry) => {
484
+ const walked = wrap_control_flow_expression_values(
485
+ lower_child(entry),
486
+ transform_context,
487
+ seen,
488
+ );
489
+ if (walked !== entry) changed = true;
490
+ return walked;
491
+ });
492
+ return changed ? result : node;
462
493
  }
463
494
 
464
495
  // Wrap a bare control-flow directive that is the sole value of a render-output
@@ -469,50 +500,72 @@ function wrap_control_flow_expression_values(node, transform_context, seen = new
469
500
  const wrap_value = (/** @type {any} */ value) =>
470
501
  is_jsx_control_flow_expression(value) ? wrap_in_native_tsrx_fragment(value) : value;
471
502
 
503
+ // All replacements land on `out`, a shallow copy made on first write; the
504
+ // input node's fields are never reassigned.
505
+ let out = node;
506
+ const set = (/** @type {string} */ key, /** @type {any} */ value) => {
507
+ if (out[key] === value) return;
508
+ if (out === node) out = { ...node };
509
+ out[key] = value;
510
+ };
511
+
472
512
  if (
473
513
  node.type === 'ArrowFunctionExpression' &&
474
514
  node.body?.type !== 'BlockStatement' &&
475
515
  is_jsx_control_flow_expression(node.body)
476
516
  ) {
477
- node.body = wrap_in_native_tsrx_fragment(node.body);
517
+ set('body', wrap_in_native_tsrx_fragment(node.body));
478
518
  } else if (node.type === 'ReturnStatement' && is_jsx_control_flow_expression(node.argument)) {
479
- node.argument = wrap_in_native_tsrx_fragment(node.argument);
519
+ set('argument', wrap_in_native_tsrx_fragment(node.argument));
480
520
  } else if (
481
521
  node.type === 'ExpressionStatement' &&
482
522
  is_jsx_control_flow_expression(node.expression)
483
523
  ) {
484
- node.expression = wrap_in_native_tsrx_fragment(node.expression);
524
+ set('expression', wrap_in_native_tsrx_fragment(node.expression));
485
525
  } else if (node.type === 'VariableDeclarator' && is_jsx_control_flow_expression(node.init)) {
486
- node.init = wrap_in_native_tsrx_fragment(node.init);
526
+ set('init', wrap_in_native_tsrx_fragment(node.init));
487
527
  } else if (node.type === 'AssignmentExpression' && is_jsx_control_flow_expression(node.right)) {
488
- node.right = wrap_in_native_tsrx_fragment(node.right);
528
+ set('right', wrap_in_native_tsrx_fragment(node.right));
489
529
  } else if (
490
530
  (node.type === 'CallExpression' || node.type === 'NewExpression') &&
491
531
  Array.isArray(node.arguments)
492
532
  ) {
493
- node.arguments = node.arguments.map(wrap_value);
533
+ const wrapped = node.arguments.map(wrap_value);
534
+ if (
535
+ wrapped.some(
536
+ (/** @type {any} */ argument, /** @type {number} */ i) => argument !== node.arguments[i],
537
+ )
538
+ ) {
539
+ set('arguments', wrapped);
540
+ }
494
541
  }
495
542
 
496
- for (const key of Object.keys(node)) {
543
+ for (const key of Object.keys(out)) {
497
544
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
498
545
  // A directive is allowed as a render child/statement, and as the sole value
499
546
  // of a render-output slot (handled above for control flow; `@{ … }` blocks
500
547
  // self-lower). Everywhere else it is combined into an expression — wrap it.
501
548
  const allowed_slot =
502
549
  is_statement_or_template_slot(node, key) || is_render_output_value_slot(node, key);
503
- const value = node[key];
550
+ const value = out[key];
504
551
  if (Array.isArray(value)) {
505
- for (let i = 0; i < value.length; i++) {
506
- value[i] = lower_child(value[i]);
507
- if (!allowed_slot) value[i] = wrap_directive_in_expression(value[i]);
508
- wrap_control_flow_expression_values(value[i], transform_context, seen);
509
- }
552
+ let changed = false;
553
+ const result = value.map((entry) => {
554
+ let next = lower_child(entry);
555
+ if (!allowed_slot) next = wrap_directive_in_expression(next);
556
+ next = wrap_control_flow_expression_values(next, transform_context, seen);
557
+ if (next !== entry) changed = true;
558
+ return next;
559
+ });
560
+ if (changed) set(key, result);
510
561
  } else {
511
- node[key] = lower_child(node[key]);
512
- if (!allowed_slot) node[key] = wrap_directive_in_expression(node[key]);
513
- wrap_control_flow_expression_values(node[key], transform_context, seen);
562
+ let next = lower_child(value);
563
+ if (!allowed_slot) next = wrap_directive_in_expression(next);
564
+ set(key, wrap_control_flow_expression_values(next, transform_context, seen));
514
565
  }
515
566
  }
567
+
568
+ return out;
516
569
  }
517
570
 
518
571
  /**
@@ -578,8 +631,8 @@ export function createJsxTransform(platform) {
578
631
  ...(platform.hooks?.initialState?.() ?? {}),
579
632
  };
580
633
 
581
- expand_child_code_blocks(/** @type {any} */ (ast));
582
- wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
634
+ ast = expand_child_code_blocks(/** @type {any} */ (ast));
635
+ ast = wrap_control_flow_expression_values(/** @type {any} */ (ast), transform_context);
583
636
 
584
637
  if (!transform_context.typeOnly) {
585
638
  preallocate_lazy_ids(/** @type {any} */ (ast), transform_context);
@@ -735,7 +788,15 @@ export function createJsxTransform(platform) {
735
788
  },
736
789
  });
737
790
 
738
- const transformed_program = /** @type {AST.Program} */ (transformed);
791
+ let transformed_program = /** @type {AST.Program} */ (transformed);
792
+ // The walk returns the input program unchanged when no visitor replaced
793
+ // anything beneath it. The post-passes below (style anchors, helper
794
+ // expansion, import injection) write into the program's `body`, so
795
+ // detach from the caller's AST first; a changed program is already a
796
+ // fresh walk-owned node with a fresh `body` array.
797
+ if (/** @type {any} */ (transformed_program) === /** @type {any} */ (ast)) {
798
+ transformed_program = { ...transformed_program, body: [...transformed_program.body] };
799
+ }
739
800
  if (type_only_style_anchors.length > 0) {
740
801
  transformed_program.body.unshift(...type_only_style_anchors);
741
802
  }
@@ -751,7 +812,7 @@ export function createJsxTransform(platform) {
751
812
  // lazy transform runs, so every `@{ … }` block / `@`-directive has already
752
813
  // been lowered to its final closure / block shape. The lazy transform can
753
814
  // then walk the complete function structure in one pass.
754
- lower_remaining_jsx_code_blocks(expanded, transform_context);
815
+ const lowered_program = lower_remaining_jsx_code_blocks(expanded, transform_context);
755
816
 
756
817
  // Apply lazy destructuring transforms to module-level code (top-level function
757
818
  // declarations, arrow functions, etc.).
@@ -772,15 +833,15 @@ export function createJsxTransform(platform) {
772
833
  // so lazy bindings declared inside a nested block or directive body are
773
834
  // rewritten just like a flat function body.
774
835
  if (!transform_context.typeOnly) {
775
- preallocate_lazy_ids(/** @type {any} */ (expanded), transform_context);
836
+ preallocate_lazy_ids(/** @type {any} */ (lowered_program), transform_context);
776
837
  }
777
838
  const final_program = /** @type {any} */ (
778
839
  transform_context.typeOnly
779
- ? expanded
780
- : apply_lazy_transforms(/** @type {any} */ (expanded), new Map())
840
+ ? lowered_program
841
+ : apply_lazy_transforms(/** @type {any} */ (lowered_program), new Map())
781
842
  );
782
843
 
783
- const result = print(/** @type {any} */ (final_program), tsx_with_ts_locations(), {
844
+ const result = print(final_program, tsx_with_ts_locations(transform_context.typeOnly), {
784
845
  sourceMapSource: filename,
785
846
  sourceMapContent: source,
786
847
  });
@@ -1707,7 +1768,7 @@ function get_active_native_tsrx_function(path) {
1707
1768
 
1708
1769
  /**
1709
1770
  * @param {any} node
1710
- * @param {{ next: () => any, state: TransformContext, path: AST.Node[] }} context
1771
+ * @param {{ next: () => any, visit: (node: any, state?: TransformContext) => any, state: TransformContext, path: AST.Node[] }} context
1711
1772
  * @returns {any}
1712
1773
  */
1713
1774
  function transform_function(node, context) {
@@ -1717,15 +1778,26 @@ function transform_function(node, context) {
1717
1778
  // from here it flows through the existing native-component machinery exactly
1718
1779
  // like the older fenced `{ return <> … </> }` shape.
1719
1780
  const has_jsx_code_block_body = node.body?.type === 'JSXCodeBlock';
1720
- lower_jsx_code_block_function_body(node);
1781
+ const lowered = lower_jsx_code_block_function_body(node);
1782
+ if (lowered !== node) {
1783
+ // The lowering produced a COPY; carry the native-body fact through the
1784
+ // sanctioned metadata channel and re-dispatch so the walker transforms
1785
+ // the lowered tree (terminates: the copy's body is a BlockStatement).
1786
+ lowered.metadata = { ...(lowered.metadata || {}), native_tsrx_body: true };
1787
+ return context.visit(lowered);
1788
+ }
1721
1789
 
1722
1790
  if (
1723
1791
  has_jsx_code_block_body ||
1724
1792
  node.metadata?.native_tsrx_function ||
1793
+ node.metadata?.native_tsrx_body ||
1725
1794
  function_has_native_tsrx_return(node)
1726
1795
  ) {
1727
1796
  return transform_native_tsrx_function(node, context, {
1728
- nativeBody: has_jsx_code_block_body || !!node.metadata?.native_tsrx_function,
1797
+ nativeBody:
1798
+ has_jsx_code_block_body ||
1799
+ !!node.metadata?.native_tsrx_function ||
1800
+ !!node.metadata?.native_tsrx_body,
1729
1801
  });
1730
1802
  }
1731
1803
 
@@ -1733,11 +1805,14 @@ function transform_function(node, context) {
1733
1805
  }
1734
1806
 
1735
1807
  /**
1808
+ * Lower a `@{ … }` body (JSXCodeBlock) to an ordinary block on a COPY built
1809
+ * with the AST builders — the source function node is never mutated. Returns
1810
+ * the input node unchanged when there is nothing to lower.
1736
1811
  * @param {any} node
1737
- * @returns {void}
1812
+ * @returns {any}
1738
1813
  */
1739
1814
  function lower_jsx_code_block_function_body(node) {
1740
- if (node.body?.type !== 'JSXCodeBlock') return;
1815
+ if (node.body?.type !== 'JSXCodeBlock') return node;
1741
1816
 
1742
1817
  const code_block = node.body;
1743
1818
  const statements = [...code_block.body];
@@ -1758,10 +1833,11 @@ function lower_jsx_code_block_function_body(node) {
1758
1833
  }
1759
1834
  statements.push(b.return(render, code_block.render));
1760
1835
  }
1761
- node.body = b.block(statements, code_block);
1762
- if (node.type === 'ArrowFunctionExpression') {
1763
- node.expression = false;
1764
- }
1836
+ return {
1837
+ ...node,
1838
+ body: b.block(statements, code_block),
1839
+ ...(node.type === 'ArrowFunctionExpression' ? { expression: false } : null),
1840
+ };
1765
1841
  }
1766
1842
 
1767
1843
  /**
@@ -3039,46 +3115,62 @@ function expand_component_helpers(program) {
3039
3115
  * If one of those helpers contains a statement-container body, lower it before
3040
3116
  * the printer sees the helper subtree.
3041
3117
  *
3118
+ * The tree is never mutated: replacements land on a shallow copy of the owning
3119
+ * node (or array), so the return value must be used in place of the argument.
3120
+ * Untouched subtrees are shared by reference with the input.
3121
+ *
3042
3122
  * @param {any} node
3043
3123
  * @param {TransformContext} transform_context
3044
3124
  * @param {Set<any>} [seen]
3045
- * @returns {void}
3125
+ * @returns {any}
3046
3126
  */
3047
3127
  function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set()) {
3048
- if (!node || typeof node !== 'object' || seen.has(node)) return;
3128
+ if (!node || typeof node !== 'object' || seen.has(node)) return node;
3049
3129
  seen.add(node);
3050
3130
 
3051
- if (is_function_or_class_boundary(node)) {
3052
- lower_jsx_code_block_function_body(node);
3053
- }
3131
+ // A code-block function body lowers to a fresh copy of the function node;
3132
+ // its children are then walked below like any other node's.
3133
+ let out = is_function_or_class_boundary(node) ? lower_jsx_code_block_function_body(node) : node;
3134
+ const set = (/** @type {string} */ key, /** @type {any} */ value) => {
3135
+ if (out[key] === value) return;
3136
+ if (out === node) out = { ...node };
3137
+ out[key] = value;
3138
+ };
3054
3139
 
3055
- for (const key of Object.keys(node)) {
3140
+ for (const key of Object.keys(out)) {
3056
3141
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
3057
- let value = node[key];
3142
+ const value = out[key];
3058
3143
  if (!value || typeof value !== 'object') continue;
3059
3144
 
3060
3145
  if (Array.isArray(value)) {
3061
- if (key === 'body') {
3062
- value = node[key] = value.flatMap((child) => {
3063
- if (child?.type !== 'JSXCodeBlock') return [child];
3064
- const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
3065
- return mark_native_pretransformed_jsx(
3066
- build_render_statements(
3067
- body_nodes,
3068
- true,
3069
- transform_context,
3070
- is_authored_native_fragment(child.render) ? child.render : null,
3071
- ),
3072
- );
3073
- });
3074
- }
3075
- for (const child of value) {
3076
- lower_remaining_jsx_code_blocks(child, transform_context, seen);
3077
- }
3146
+ const expanded =
3147
+ key === 'body' && value.some((child) => child?.type === 'JSXCodeBlock')
3148
+ ? value.flatMap((child) => {
3149
+ if (child?.type !== 'JSXCodeBlock') return [child];
3150
+ const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
3151
+ return mark_native_pretransformed_jsx(
3152
+ build_render_statements(
3153
+ body_nodes,
3154
+ true,
3155
+ transform_context,
3156
+ is_authored_native_fragment(child.render) ? child.render : null,
3157
+ ),
3158
+ );
3159
+ })
3160
+ : value;
3161
+ let changed = expanded !== value;
3162
+ const result = expanded.map((child) => {
3163
+ const walked = lower_remaining_jsx_code_blocks(child, transform_context, seen);
3164
+ if (walked !== child) changed = true;
3165
+ return walked;
3166
+ });
3167
+ if (changed) set(key, result);
3078
3168
  } else {
3079
- lower_remaining_jsx_code_blocks(value, transform_context, seen);
3169
+ set(key, lower_remaining_jsx_code_blocks(value, transform_context, seen));
3080
3170
  }
3081
3171
  }
3172
+
3173
+ return out;
3082
3174
  }
3083
3175
 
3084
3176
  /**
@@ -3202,9 +3294,8 @@ function get_loop_skip_if_consequent_body(node) {
3202
3294
  */
3203
3295
  function create_component_loop_skip_if_statement(node, render_nodes, transform_context) {
3204
3296
  const consequent_body = /** @type {any[]} */ (get_loop_skip_if_consequent_body(node));
3205
- const branch_statements = build_render_statements(consequent_body, true, transform_context);
3206
- prepend_render_nodes_to_return_statements(
3207
- branch_statements,
3297
+ const branch_statements = prepend_render_nodes_to_return_statements(
3298
+ build_render_statements(consequent_body, true, transform_context),
3208
3299
  render_nodes,
3209
3300
  transform_context.typeOnly,
3210
3301
  );
@@ -3221,19 +3312,23 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
3221
3312
  }
3222
3313
 
3223
3314
  /**
3315
+ * Statements can be passed through `build_render_statements` by reference, so
3316
+ * rewritten returns land on shallow copies; the returned array must be used in
3317
+ * place of the argument.
3318
+ *
3224
3319
  * @param {any[]} statements
3225
3320
  * @param {any[]} render_nodes
3226
3321
  * @param {boolean} [type_only]
3227
- * @returns {void}
3322
+ * @returns {any[]}
3228
3323
  */
3229
3324
  function prepend_render_nodes_to_return_statements(statements, render_nodes, type_only = false) {
3230
3325
  if (render_nodes.length === 0) {
3231
- return;
3326
+ return statements;
3232
3327
  }
3233
3328
 
3234
- for (const statement of statements) {
3235
- prepend_render_nodes_to_return_statement(statement, render_nodes, false, type_only);
3236
- }
3329
+ return /** @type {any[]} */ (
3330
+ prepend_render_nodes_to_return_statement(statements, render_nodes, false, type_only)
3331
+ );
3237
3332
  }
3238
3333
 
3239
3334
  /**
@@ -3241,7 +3336,7 @@ function prepend_render_nodes_to_return_statements(statements, render_nodes, typ
3241
3336
  * @param {any[]} render_nodes
3242
3337
  * @param {boolean} inside_nested_function
3243
3338
  * @param {boolean} [type_only]
3244
- * @returns {void}
3339
+ * @returns {any}
3245
3340
  */
3246
3341
  function prepend_render_nodes_to_return_statement(
3247
3342
  node,
@@ -3250,7 +3345,7 @@ function prepend_render_nodes_to_return_statement(
3250
3345
  type_only = false,
3251
3346
  ) {
3252
3347
  if (!node || typeof node !== 'object') {
3253
- return;
3348
+ return node;
3254
3349
  }
3255
3350
 
3256
3351
  if (
@@ -3262,33 +3357,44 @@ function prepend_render_nodes_to_return_statement(
3262
3357
  }
3263
3358
 
3264
3359
  if (!inside_nested_function && node.type === 'ReturnStatement') {
3265
- node.argument = combine_render_return_argument(render_nodes, node.argument, type_only);
3266
- return;
3360
+ return {
3361
+ ...node,
3362
+ argument: combine_render_return_argument(render_nodes, node.argument, type_only),
3363
+ };
3267
3364
  }
3268
3365
 
3269
3366
  if (Array.isArray(node)) {
3270
- for (const child of node) {
3271
- prepend_render_nodes_to_return_statement(
3367
+ let changed = false;
3368
+ const result = node.map((child) => {
3369
+ const walked = prepend_render_nodes_to_return_statement(
3272
3370
  child,
3273
3371
  render_nodes,
3274
3372
  inside_nested_function,
3275
3373
  type_only,
3276
3374
  );
3277
- }
3278
- return;
3375
+ if (walked !== child) changed = true;
3376
+ return walked;
3377
+ });
3378
+ return changed ? result : node;
3279
3379
  }
3280
3380
 
3381
+ let out = node;
3281
3382
  for (const key of Object.keys(node)) {
3282
3383
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3283
3384
  continue;
3284
3385
  }
3285
- prepend_render_nodes_to_return_statement(
3386
+ const walked = prepend_render_nodes_to_return_statement(
3286
3387
  node[key],
3287
3388
  render_nodes,
3288
3389
  inside_nested_function,
3289
3390
  type_only,
3290
3391
  );
3392
+ if (walked !== node[key]) {
3393
+ if (out === node) out = { ...node };
3394
+ out[key] = walked;
3395
+ }
3291
3396
  }
3397
+ return out;
3292
3398
  }
3293
3399
 
3294
3400
  /**
@@ -4733,9 +4839,16 @@ function continue_to_bare_return(source_node) {
4733
4839
  */
4734
4840
  export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4735
4841
  if (Array.isArray(node)) {
4736
- return node.map((child) =>
4737
- rewrite_loop_continues_to_bare_returns(child, is_root && !is_loop_statement(child)),
4738
- );
4842
+ let changed = false;
4843
+ const result = node.map((child) => {
4844
+ const walked = rewrite_loop_continues_to_bare_returns(
4845
+ child,
4846
+ is_root && !is_loop_statement(child),
4847
+ );
4848
+ if (walked !== child) changed = true;
4849
+ return walked;
4850
+ });
4851
+ return changed ? result : node;
4739
4852
  }
4740
4853
 
4741
4854
  if (!node || typeof node !== 'object') {
@@ -4750,14 +4863,19 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4750
4863
  return node;
4751
4864
  }
4752
4865
 
4866
+ let out = node;
4753
4867
  for (const key of Object.keys(node)) {
4754
4868
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
4755
4869
  continue;
4756
4870
  }
4757
- node[key] = rewrite_loop_continues_to_bare_returns(node[key], false);
4871
+ const walked = rewrite_loop_continues_to_bare_returns(node[key], false);
4872
+ if (walked !== node[key]) {
4873
+ if (out === node) out = { ...node };
4874
+ out[key] = walked;
4875
+ }
4758
4876
  }
4759
4877
 
4760
- return node;
4878
+ return out;
4761
4879
  }
4762
4880
 
4763
4881
  /**
@@ -4921,7 +5039,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4921
5039
  }
4922
5040
 
4923
5041
  const loop_params = get_for_of_iteration_params(node.left, node.index);
4924
- const loop_body = /** @type {any[]} */ (
5042
+ let loop_body = /** @type {any[]} */ (
4925
5043
  node.body.type === 'BlockStatement' ? node.body.body : [node.body]
4926
5044
  );
4927
5045
  validate_for_body_control_flow(loop_body, transform_context);
@@ -4952,10 +5070,10 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4952
5070
  }
4953
5071
 
4954
5072
  if (implicit_non_hook_key_expression && should_apply_key_to_loop_body(loop_body)) {
4955
- apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
5073
+ loop_body = apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
4956
5074
  }
4957
5075
 
4958
- const body_statements = has_hooks
5076
+ let body_statements = has_hooks
4959
5077
  ? hook_safe_render_statements(loop_body, key_expression, transform_context)
4960
5078
  : build_render_statements(loop_body, true, transform_context);
4961
5079
 
@@ -4972,7 +5090,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4972
5090
 
4973
5091
  const non_hook_key_expression = key_expression ?? implicit_non_hook_key_expression;
4974
5092
  if (!has_hooks && non_hook_key_expression) {
4975
- apply_key_to_render_statements(body_statements, non_hook_key_expression, transform_context);
5093
+ body_statements = apply_key_to_render_statements(
5094
+ body_statements,
5095
+ non_hook_key_expression,
5096
+ transform_context,
5097
+ );
4976
5098
  }
4977
5099
 
4978
5100
  // Restore bindings
@@ -5025,32 +5147,40 @@ function for_of_statement_to_jsx_child(node, transform_context) {
5025
5147
  }
5026
5148
 
5027
5149
  /**
5150
+ * Returns a copy of `body_nodes` where the first keyable element carries the
5151
+ * key attribute on a rebuilt opening element — the source nodes are never
5152
+ * mutated (they may belong to the caller's parsed AST).
5028
5153
  * @param {any[]} body_nodes
5029
5154
  * @param {any} key_expression
5030
- * @returns {void}
5155
+ * @returns {any[]}
5031
5156
  */
5032
5157
  function apply_key_to_loop_body(body_nodes, key_expression) {
5033
- for (const node of body_nodes) {
5034
- if (node.type === 'JSXElement') {
5035
- const attributes = node.openingElement?.attributes || [];
5036
- const has_key = attributes.some(
5037
- (/** @type {any} */ attr) =>
5038
- attr.type === 'JSXAttribute' &&
5039
- attr.name?.type === 'JSXIdentifier' &&
5040
- attr.name.name === 'key',
5041
- );
5042
-
5043
- if (!has_key) {
5044
- attributes.push(
5158
+ let applied = false;
5159
+ return body_nodes.map((node) => {
5160
+ if (applied || node.type !== 'JSXElement') return node;
5161
+ applied = true;
5162
+ const attributes = node.openingElement?.attributes || [];
5163
+ const has_key = attributes.some(
5164
+ (/** @type {any} */ attr) =>
5165
+ attr.type === 'JSXAttribute' &&
5166
+ attr.name?.type === 'JSXIdentifier' &&
5167
+ attr.name.name === 'key',
5168
+ );
5169
+ if (has_key) return node;
5170
+ return {
5171
+ ...node,
5172
+ openingElement: {
5173
+ ...node.openingElement,
5174
+ attributes: [
5175
+ ...attributes,
5045
5176
  b.jsx_attribute(
5046
5177
  b.jsx_id('key'),
5047
5178
  to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
5048
5179
  ),
5049
- );
5050
- }
5051
- return;
5052
- }
5053
- }
5180
+ ],
5181
+ },
5182
+ };
5183
+ });
5054
5184
  }
5055
5185
 
5056
5186
  /**
@@ -5068,10 +5198,14 @@ function should_apply_key_to_loop_body(body_nodes) {
5068
5198
  }
5069
5199
 
5070
5200
  /**
5201
+ * Statement entries can be shared with the source tree, so the keyed return
5202
+ * lands on shallow copies; the returned array must be used in place of the
5203
+ * argument.
5204
+ *
5071
5205
  * @param {any[]} statements
5072
5206
  * @param {any} key_expression
5073
5207
  * @param {TransformContext} transform_context
5074
- * @returns {void}
5208
+ * @returns {any[]}
5075
5209
  */
5076
5210
  function apply_key_to_render_statements(statements, key_expression, transform_context) {
5077
5211
  for (let i = statements.length - 1; i >= 0; i -= 1) {
@@ -5080,21 +5214,29 @@ function apply_key_to_render_statements(statements, key_expression, transform_co
5080
5214
  continue;
5081
5215
  }
5082
5216
 
5083
- if (statement.argument.type === 'JSXElement') {
5084
- apply_key_to_jsx_element(statement.argument, key_expression);
5085
- } else if (statement.argument.type === 'JSXFragment') {
5217
+ let argument = statement.argument;
5218
+ if (argument.type === 'JSXElement') {
5219
+ argument = apply_key_to_jsx_element(argument, key_expression);
5220
+ } else if (argument.type === 'JSXFragment') {
5086
5221
  transform_context.needs_fragment = true;
5087
- statement.argument = keyed_fragment_to_jsx_element(statement.argument, key_expression);
5222
+ argument = keyed_fragment_to_jsx_element(argument, key_expression);
5088
5223
  }
5089
5224
 
5090
- return;
5225
+ if (argument === statement.argument) {
5226
+ return statements;
5227
+ }
5228
+ const result = [...statements];
5229
+ result[i] = { ...statement, argument };
5230
+ return result;
5091
5231
  }
5232
+ return statements;
5092
5233
  }
5093
5234
 
5094
5235
  /**
5095
5236
  * @param {any} element
5096
5237
  * @param {any} key_expression
5097
- * @returns {void}
5238
+ * @returns {any} the element itself when it already has a `key`, otherwise a
5239
+ * shallow copy with the key attribute appended.
5098
5240
  */
5099
5241
  function apply_key_to_jsx_element(element, key_expression) {
5100
5242
  const attributes = element.openingElement?.attributes || [];
@@ -5104,15 +5246,21 @@ function apply_key_to_jsx_element(element, key_expression) {
5104
5246
  attr.name?.type === 'JSXIdentifier' &&
5105
5247
  attr.name.name === 'key',
5106
5248
  );
5249
+ if (has_key) return element;
5107
5250
 
5108
- if (!has_key) {
5109
- attributes.push(
5110
- b.jsx_attribute(
5111
- b.jsx_id('key'),
5112
- to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
5113
- ),
5114
- );
5115
- }
5251
+ return {
5252
+ ...element,
5253
+ openingElement: {
5254
+ ...element.openingElement,
5255
+ attributes: [
5256
+ ...attributes,
5257
+ b.jsx_attribute(
5258
+ b.jsx_id('key'),
5259
+ to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
5260
+ ),
5261
+ ],
5262
+ },
5263
+ };
5116
5264
  }
5117
5265
 
5118
5266
  /**
@@ -962,19 +962,12 @@ export function convert_source_map_to_mappings(
962
962
  const is_method = node.metadata?.is_method;
963
963
 
964
964
  if (node.type === 'ArrowFunctionExpression' && node.loc) {
965
- const start_key = `${node.loc.start.line}:${node.loc.start.column}`;
966
- const end_key = `${node.loc.end.line}:${node.loc.end.column}`;
967
-
968
- if (src_to_gen_map.has(start_key) && src_to_gen_map.has(end_key)) {
969
- mappings.push(
970
- get_mapping_from_node(
971
- node,
972
- src_to_gen_map,
973
- gen_line_offsets,
974
- mapping_data_verify_only,
975
- ),
976
- );
977
- }
965
+ // The printer emits node-level boundary markers for arrows (their
966
+ // span can start at a bare `(`), so the strict lookup always
967
+ // resolves — no defensive has() guard.
968
+ mappings.push(
969
+ get_mapping_from_node(node, src_to_gen_map, gen_line_offsets, mapping_data_verify_only),
970
+ );
978
971
  }
979
972
 
980
973
  // Add the function keyword token.
@@ -987,41 +980,68 @@ export function convert_source_map_to_mappings(
987
980
  const function_hover = create_function_hover_replacement(
988
981
  /** @type {AST.Parameter[]} */ (node.params),
989
982
  );
990
- let start_col = node_fn.loc.start.column;
991
- let start = node_fn.start;
992
- const async_keyword = 'async';
983
+ // Keyword SOURCE spans come from the LEXER (parse-time
984
+ // `tsrx_keyword_tokens`, opt-in via ParseOptions.keywordTokens):
985
+ // no AST node records them, offset arithmetic breaks on extra
986
+ // whitespace, and text search breaks on comments. Fall back to
987
+ // node-start-anchored arithmetic when tokens were not collected.
988
+ const keyword_bound =
989
+ node_fn.id?.start ?? node_fn.params?.[0]?.start ?? node_fn.body?.start ?? node_fn.end;
990
+ /** @type {Array<{ value: string, start: number, end: number, loc: AST.SourceLocation }>} */
991
+ const lexer_tokens = /** @type {any} */ (ast_from_source).tsrx_keyword_tokens ?? [];
992
+ /**
993
+ * @param {'async' | 'function'} keyword
994
+ * @param {number} from
995
+ * @returns {AST.SourceLocation | null}
996
+ */
997
+ const keyword_loc = (keyword, from) => {
998
+ const token = lexer_tokens.find(
999
+ (candidate) =>
1000
+ candidate.value === keyword &&
1001
+ candidate.start >= from &&
1002
+ candidate.start < keyword_bound,
1003
+ );
1004
+ if (token) return token.loc;
1005
+ if (lexer_tokens.length > 0) return null;
1006
+ // Arithmetic fallback (callers that do not collect tokens):
1007
+ // assumes the historical `async` + one-space + `function`
1008
+ // single-line layout.
1009
+ const offset =
1010
+ keyword === 'function' && node_fn.async
1011
+ ? node_fn.start + 'async '.length
1012
+ : node_fn.start;
1013
+ const start_pos = offset_to_line_col(offset, src_line_offsets);
1014
+ const end_pos = offset_to_line_col(offset + keyword.length, src_line_offsets);
1015
+ return { start: start_pos, end: end_pos };
1016
+ };
993
1017
 
1018
+ let function_from = node_fn.start;
994
1019
  if (node_fn.async) {
995
- // We explicitly mapped async and function in esrap
1020
+ const async_loc = keyword_loc('async', node_fn.start);
1021
+ if (async_loc) {
1022
+ tokens.push({
1023
+ source: 'async',
1024
+ generated: 'async',
1025
+ loc: async_loc,
1026
+ metadata: {},
1027
+ });
1028
+ function_from = loc_to_offset(
1029
+ async_loc.end.line,
1030
+ async_loc.end.column,
1031
+ src_line_offsets,
1032
+ );
1033
+ }
1034
+ }
1035
+
1036
+ const function_loc = keyword_loc('function', function_from);
1037
+ if (function_loc) {
996
1038
  tokens.push({
997
- source: async_keyword,
998
- generated: async_keyword,
999
- loc: {
1000
- start: { line: node_fn.loc.start.line, column: start_col },
1001
- end: {
1002
- line: node_fn.loc.start.line,
1003
- column: start_col + async_keyword.length,
1004
- },
1005
- },
1006
- metadata: {},
1039
+ source: 'function',
1040
+ generated: 'function',
1041
+ loc: function_loc,
1042
+ metadata: function_hover ? { hover: function_hover } : {},
1007
1043
  });
1008
-
1009
- start_col += async_keyword.length + 1; // +1 for space
1010
- start += async_keyword.length + 1;
1011
1044
  }
1012
-
1013
- tokens.push({
1014
- source: 'function',
1015
- generated: 'function',
1016
- loc: {
1017
- start: { line: node_fn.loc.start.line, column: start_col },
1018
- end: {
1019
- line: node_fn.loc.start.line,
1020
- column: start_col + 'function'.length,
1021
- },
1022
- },
1023
- metadata: function_hover ? { hover: function_hover } : {},
1024
- });
1025
1045
  }
1026
1046
 
1027
1047
  // Visit in source order: id, params, body
@@ -251,6 +251,22 @@ export function export_default(declaration) {
251
251
  return { type: 'ExportDefaultDeclaration', declaration, metadata: { path: [] } };
252
252
  }
253
253
 
254
+ /**
255
+ * @param {string | AST.Identifier} local
256
+ * @param {string | AST.Identifier} [exported]
257
+ * @param {AST.ExportSpecifier['exportKind']} [exportKind]
258
+ * @returns {AST.ExportSpecifier}
259
+ */
260
+ export function export_specifier(local, exported = local, exportKind = 'value') {
261
+ return {
262
+ type: 'ExportSpecifier',
263
+ local: typeof local === 'string' ? id(local) : local,
264
+ exported: typeof exported === 'string' ? id(exported) : exported,
265
+ exportKind,
266
+ metadata: { path: [] },
267
+ };
268
+ }
269
+
254
270
  /**
255
271
  * @param {AST.Declaration | null} declaration
256
272
  * @param {AST.ExportSpecifier[]} [specifiers]
package/types/index.d.ts CHANGED
@@ -230,12 +230,14 @@ declare module 'estree' {
230
230
  interface MethodDefinition {
231
231
  typeParameters?: TSTypeParameterDeclaration;
232
232
  accessibility?: Accessibility;
233
+ optional?: boolean;
233
234
  }
234
235
 
235
236
  interface PropertyDefinition {
236
237
  accessibility?: Accessibility;
237
238
  readonly?: boolean;
238
239
  optional?: boolean;
240
+ definite?: boolean;
239
241
  }
240
242
 
241
243
  interface ClassDeclaration {
@@ -484,6 +486,9 @@ declare module 'estree' {
484
486
  interface ExportNamedDeclaration {
485
487
  exportKind: TSESTree.ExportNamedDeclaration['exportKind'];
486
488
  }
489
+ interface ExportSpecifier {
490
+ exportKind: TSESTree.ExportSpecifier['exportKind'];
491
+ }
487
492
 
488
493
  interface BaseNodeWithoutComments {
489
494
  // Adding start, end for now as always there
@@ -1231,6 +1236,13 @@ export interface ParseError {
1231
1236
  export interface ParseOptions {
1232
1237
  collect?: boolean;
1233
1238
  loose?: boolean;
1239
+ preserveParens?: boolean;
1240
+ /**
1241
+ * Collect `async`/`function` keyword tokens from the lexer onto the
1242
+ * returned program (`tsrx_keyword_tokens`) so mapping collection can span
1243
+ * keywords exactly. Volar/typeOnly parses opt in.
1244
+ */
1245
+ keywordTokens?: boolean;
1234
1246
  errors?: CompileError[];
1235
1247
  comments?: AST.CommentWithLocation[];
1236
1248
  }