@tsrx/core 0.1.41 → 0.1.43

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.43",
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"
@@ -58,6 +58,18 @@ export function is_jsdoc_ts_annotation(comment) {
58
58
  return tsAnnotations.some((annotation) => comment.value.includes(annotation));
59
59
  }
60
60
 
61
+ /**
62
+ * Check if a comment is a TypeScript JSX pragma (`@jsxImportSource`,
63
+ * `@jsxRuntime`, `@jsxFrag`, `@jsx`). TS reads these from a file's LEADING
64
+ * comments to pick that file's JSX type source/factory, overriding tsconfig —
65
+ * dropping one silently retypes every JSX expression in the file.
66
+ * @param {AST.CommentWithLocation} comment
67
+ * @returns {boolean}
68
+ */
69
+ export function is_jsx_pragma(comment) {
70
+ return /@jsx(ImportSource|Runtime|Frag)?\b/.test(comment.value);
71
+ }
72
+
61
73
  /**
62
74
  * Check if a comment should be preserved in to_ts mode
63
75
  * @param {AST.CommentWithLocation} comment
@@ -65,7 +77,10 @@ export function is_jsdoc_ts_annotation(comment) {
65
77
  */
66
78
  export function should_preserve_comment(comment) {
67
79
  return (
68
- is_ts_pragma(comment) || is_triple_slash_directive(comment) || is_jsdoc_ts_annotation(comment)
80
+ is_ts_pragma(comment) ||
81
+ is_triple_slash_directive(comment) ||
82
+ is_jsdoc_ts_annotation(comment) ||
83
+ is_jsx_pragma(comment)
69
84
  );
70
85
  }
71
86
 
@@ -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
  }
@@ -2,6 +2,7 @@
2
2
  /** @import { Visitors } from 'zimmerframe' */
3
3
 
4
4
  import tsx from 'esrap/languages/tsx';
5
+ import { should_preserve_comment, format_comment } from '../../comment-utils.js';
5
6
 
6
7
  /**
7
8
  * Zimmerframe provides `path` as the ancestor chain. A native template node in
@@ -51,18 +52,48 @@ export function set_node_path_metadata(node, path) {
51
52
  }
52
53
 
53
54
  /**
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.
55
+ * Wrap esrap's `tsx()` printer with location markers for the remaining nodes
56
+ * whose spans are invisible to the source map (e.g. `class`, template-literal
57
+ * backticks, JSX angle brackets, generic-argument delimiters). Without these
58
+ * markers, Volar mapping collection in `segments.js` throws when looking up
59
+ * the node's start/end positions. esrap ≥2.3.0 (keyword writes +
60
+ * `boundaryTokens`) covers most starts, but not statement ends — see the
61
+ * list below for what each entry still compensates.
59
62
  *
60
63
  * Shared across all JSX-producing targets (React, Preact, Solid).
61
64
  *
62
65
  * @returns {any}
63
66
  */
64
- export function tsx_with_ts_locations() {
65
- const base = /** @type {any} */ (tsx());
67
+ /**
68
+ * @param {boolean} [boundary_tokens] Enable esrap's `boundaryTokens` anchors
69
+ * (structural tokens carry one-character source locations). typeOnly/volar
70
+ * prints opt in — their maps are consumed positionally by the language
71
+ * tooling and never shipped; build prints stay sparse.
72
+ * @param {AST.CommentWithLocation[]} [comments] Source comments; the ones
73
+ * `should_preserve_comment` classifies as semantic-to-TS (`@ts-nocheck`,
74
+ * `@jsxImportSource`, triple-slash references, …) and that LEAD the program
75
+ * are re-emitted at the top of the printed output. The generated TSX is real
76
+ * TS input — dropping a leading pragma changes how the whole file checks.
77
+ */
78
+ export function tsx_with_ts_locations(boundary_tokens = false, comments = undefined) {
79
+ const base = /** @type {any} */ (tsx({ boundaryTokens: boundary_tokens }));
80
+
81
+ const leading_preserved = (/** @type {any} */ program) => {
82
+ if (!comments?.length) return [];
83
+ // Injected statements (dynamic-import/try-import prepends) carry no
84
+ // loc; anchor "leading" on the first statement that maps to source,
85
+ // else every preserved comment in the file would hoist to the top.
86
+ const first = program.body?.find((/** @type {any} */ node) => node.loc);
87
+ return comments.filter(
88
+ (/** @type {any} */ comment) =>
89
+ should_preserve_comment(comment) &&
90
+ (first?.loc == null ||
91
+ (comment.loc &&
92
+ (comment.loc.end.line < first.loc.start.line ||
93
+ (comment.loc.end.line === first.loc.start.line &&
94
+ comment.loc.end.column <= first.loc.start.column)))),
95
+ );
96
+ };
66
97
 
67
98
  /**
68
99
  * @param {any} node
@@ -81,17 +112,17 @@ export function tsx_with_ts_locations() {
81
112
 
82
113
  /** @type {Record<string, (node: any, context: any) => void>} */
83
114
  const wrappers = {
84
- ArrayPattern: (node, context) => {
85
- base.ArrayPattern(node, context);
86
- if (node.typeAnnotation) {
87
- context.visit(node.typeAnnotation);
115
+ Program: (node, context) => {
116
+ for (const comment of leading_preserved(node)) {
117
+ if (comment.loc) context.location(comment.loc.start.line, comment.loc.start.column);
118
+ context.write(format_comment(comment));
119
+ if (comment.loc) context.location(comment.loc.end.line, comment.loc.end.column);
120
+ context.newline();
88
121
  }
122
+ base.Program(node, context);
89
123
  },
90
- Identifier: (node, context) => {
91
- context.write(node.name, node);
92
- if (node.optional) {
93
- context.write('?');
94
- }
124
+ ArrayPattern: (node, context) => {
125
+ base.ArrayPattern(node, context);
95
126
  if (node.typeAnnotation) {
96
127
  context.visit(node.typeAnnotation);
97
128
  }
@@ -134,39 +165,6 @@ export function tsx_with_ts_locations() {
134
165
  context.write(' ');
135
166
  context.visit(value.body);
136
167
  },
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
168
 
171
169
  // esrap's JSXOpeningElement printer doesn't emit `typeArguments`, so generic
172
170
  // component tags like `<RenderProp<User>>` lose the `<User>` in the output.
@@ -196,6 +194,11 @@ export function tsx_with_ts_locations() {
196
194
  }
197
195
  },
198
196
  TSModuleDeclaration: (node, context) => {
197
+ // Ambient `declare module '…' { … }` must keep its `declare` — the
198
+ // typeOnly/volar output is real TS and `module '…' { … }` alone is a
199
+ // syntax error (TS1035). Non-ambient `module name { }` blocks have no
200
+ // `declare` and print unchanged.
201
+ if (node.declare) context.write('declare ');
199
202
  context.write(node.metadata?.module_keyword ?? 'module');
200
203
  context.write(' ');
201
204
  context.visit(node.id);
@@ -209,9 +212,14 @@ export function tsx_with_ts_locations() {
209
212
  // on the whole node, only then duplicate it here
210
213
  // e.g. JSXOpeningElement is such a case
211
214
  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).
215
+ // JS nodes with boundary positions esrap still cannot map. Keyword
216
+ // writes (if/new/return/for/switch/await) and `boundaryTokens`
217
+ // anchors (brackets, braces, parens, computed/call closers) cover
218
+ // many STARTS, but statement ENDS land on unanchored characters
219
+ // (`;`, a block's `}`), `class` is not a keyword-write, template
220
+ // literals' backticks carry no location, and an arrow's span can
221
+ // start at a bare `(` — so these node-level markers remain the
222
+ // source of both boundaries until esrap can anchor them.
215
223
  'ClassDeclaration',
216
224
  'ClassExpression',
217
225
  '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,18 +833,28 @@ 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(), {
784
- sourceMapSource: filename,
785
- sourceMapContent: source,
786
- });
844
+ const result = print(
845
+ final_program,
846
+ // typeOnly output is real TS input: re-emit preserved leading comments
847
+ // (@jsxImportSource / @ts-nocheck / triple-slash references) so TS
848
+ // semantics survive the comment-stripping print.
849
+ tsx_with_ts_locations(
850
+ transform_context.typeOnly,
851
+ transform_context.typeOnly ? transform_context.comments : undefined,
852
+ ),
853
+ {
854
+ sourceMapSource: filename,
855
+ sourceMapContent: source,
856
+ },
857
+ );
787
858
 
788
859
  const { css, cssHash } = render_css_result(/** @type {any} */ (stylesheets));
789
860
 
@@ -1707,7 +1778,7 @@ function get_active_native_tsrx_function(path) {
1707
1778
 
1708
1779
  /**
1709
1780
  * @param {any} node
1710
- * @param {{ next: () => any, state: TransformContext, path: AST.Node[] }} context
1781
+ * @param {{ next: () => any, visit: (node: any, state?: TransformContext) => any, state: TransformContext, path: AST.Node[] }} context
1711
1782
  * @returns {any}
1712
1783
  */
1713
1784
  function transform_function(node, context) {
@@ -1717,15 +1788,26 @@ function transform_function(node, context) {
1717
1788
  // from here it flows through the existing native-component machinery exactly
1718
1789
  // like the older fenced `{ return <> … </> }` shape.
1719
1790
  const has_jsx_code_block_body = node.body?.type === 'JSXCodeBlock';
1720
- lower_jsx_code_block_function_body(node);
1791
+ const lowered = lower_jsx_code_block_function_body(node);
1792
+ if (lowered !== node) {
1793
+ // The lowering produced a COPY; carry the native-body fact through the
1794
+ // sanctioned metadata channel and re-dispatch so the walker transforms
1795
+ // the lowered tree (terminates: the copy's body is a BlockStatement).
1796
+ lowered.metadata = { ...(lowered.metadata || {}), native_tsrx_body: true };
1797
+ return context.visit(lowered);
1798
+ }
1721
1799
 
1722
1800
  if (
1723
1801
  has_jsx_code_block_body ||
1724
1802
  node.metadata?.native_tsrx_function ||
1803
+ node.metadata?.native_tsrx_body ||
1725
1804
  function_has_native_tsrx_return(node)
1726
1805
  ) {
1727
1806
  return transform_native_tsrx_function(node, context, {
1728
- nativeBody: has_jsx_code_block_body || !!node.metadata?.native_tsrx_function,
1807
+ nativeBody:
1808
+ has_jsx_code_block_body ||
1809
+ !!node.metadata?.native_tsrx_function ||
1810
+ !!node.metadata?.native_tsrx_body,
1729
1811
  });
1730
1812
  }
1731
1813
 
@@ -1733,11 +1815,14 @@ function transform_function(node, context) {
1733
1815
  }
1734
1816
 
1735
1817
  /**
1818
+ * Lower a `@{ … }` body (JSXCodeBlock) to an ordinary block on a COPY built
1819
+ * with the AST builders — the source function node is never mutated. Returns
1820
+ * the input node unchanged when there is nothing to lower.
1736
1821
  * @param {any} node
1737
- * @returns {void}
1822
+ * @returns {any}
1738
1823
  */
1739
1824
  function lower_jsx_code_block_function_body(node) {
1740
- if (node.body?.type !== 'JSXCodeBlock') return;
1825
+ if (node.body?.type !== 'JSXCodeBlock') return node;
1741
1826
 
1742
1827
  const code_block = node.body;
1743
1828
  const statements = [...code_block.body];
@@ -1758,10 +1843,11 @@ function lower_jsx_code_block_function_body(node) {
1758
1843
  }
1759
1844
  statements.push(b.return(render, code_block.render));
1760
1845
  }
1761
- node.body = b.block(statements, code_block);
1762
- if (node.type === 'ArrowFunctionExpression') {
1763
- node.expression = false;
1764
- }
1846
+ return {
1847
+ ...node,
1848
+ body: b.block(statements, code_block),
1849
+ ...(node.type === 'ArrowFunctionExpression' ? { expression: false } : null),
1850
+ };
1765
1851
  }
1766
1852
 
1767
1853
  /**
@@ -3039,46 +3125,62 @@ function expand_component_helpers(program) {
3039
3125
  * If one of those helpers contains a statement-container body, lower it before
3040
3126
  * the printer sees the helper subtree.
3041
3127
  *
3128
+ * The tree is never mutated: replacements land on a shallow copy of the owning
3129
+ * node (or array), so the return value must be used in place of the argument.
3130
+ * Untouched subtrees are shared by reference with the input.
3131
+ *
3042
3132
  * @param {any} node
3043
3133
  * @param {TransformContext} transform_context
3044
3134
  * @param {Set<any>} [seen]
3045
- * @returns {void}
3135
+ * @returns {any}
3046
3136
  */
3047
3137
  function lower_remaining_jsx_code_blocks(node, transform_context, seen = new Set()) {
3048
- if (!node || typeof node !== 'object' || seen.has(node)) return;
3138
+ if (!node || typeof node !== 'object' || seen.has(node)) return node;
3049
3139
  seen.add(node);
3050
3140
 
3051
- if (is_function_or_class_boundary(node)) {
3052
- lower_jsx_code_block_function_body(node);
3053
- }
3141
+ // A code-block function body lowers to a fresh copy of the function node;
3142
+ // its children are then walked below like any other node's.
3143
+ let out = is_function_or_class_boundary(node) ? lower_jsx_code_block_function_body(node) : node;
3144
+ const set = (/** @type {string} */ key, /** @type {any} */ value) => {
3145
+ if (out[key] === value) return;
3146
+ if (out === node) out = { ...node };
3147
+ out[key] = value;
3148
+ };
3054
3149
 
3055
- for (const key of Object.keys(node)) {
3150
+ for (const key of Object.keys(out)) {
3056
3151
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
3057
- let value = node[key];
3152
+ const value = out[key];
3058
3153
  if (!value || typeof value !== 'object') continue;
3059
3154
 
3060
3155
  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
- }
3156
+ const expanded =
3157
+ key === 'body' && value.some((child) => child?.type === 'JSXCodeBlock')
3158
+ ? value.flatMap((child) => {
3159
+ if (child?.type !== 'JSXCodeBlock') return [child];
3160
+ const body_nodes = get_jsx_code_block_body_nodes(child, transform_context);
3161
+ return mark_native_pretransformed_jsx(
3162
+ build_render_statements(
3163
+ body_nodes,
3164
+ true,
3165
+ transform_context,
3166
+ is_authored_native_fragment(child.render) ? child.render : null,
3167
+ ),
3168
+ );
3169
+ })
3170
+ : value;
3171
+ let changed = expanded !== value;
3172
+ const result = expanded.map((child) => {
3173
+ const walked = lower_remaining_jsx_code_blocks(child, transform_context, seen);
3174
+ if (walked !== child) changed = true;
3175
+ return walked;
3176
+ });
3177
+ if (changed) set(key, result);
3078
3178
  } else {
3079
- lower_remaining_jsx_code_blocks(value, transform_context, seen);
3179
+ set(key, lower_remaining_jsx_code_blocks(value, transform_context, seen));
3080
3180
  }
3081
3181
  }
3182
+
3183
+ return out;
3082
3184
  }
3083
3185
 
3084
3186
  /**
@@ -3202,9 +3304,8 @@ function get_loop_skip_if_consequent_body(node) {
3202
3304
  */
3203
3305
  function create_component_loop_skip_if_statement(node, render_nodes, transform_context) {
3204
3306
  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,
3307
+ const branch_statements = prepend_render_nodes_to_return_statements(
3308
+ build_render_statements(consequent_body, true, transform_context),
3208
3309
  render_nodes,
3209
3310
  transform_context.typeOnly,
3210
3311
  );
@@ -3221,19 +3322,23 @@ function create_component_loop_skip_if_statement(node, render_nodes, transform_c
3221
3322
  }
3222
3323
 
3223
3324
  /**
3325
+ * Statements can be passed through `build_render_statements` by reference, so
3326
+ * rewritten returns land on shallow copies; the returned array must be used in
3327
+ * place of the argument.
3328
+ *
3224
3329
  * @param {any[]} statements
3225
3330
  * @param {any[]} render_nodes
3226
3331
  * @param {boolean} [type_only]
3227
- * @returns {void}
3332
+ * @returns {any[]}
3228
3333
  */
3229
3334
  function prepend_render_nodes_to_return_statements(statements, render_nodes, type_only = false) {
3230
3335
  if (render_nodes.length === 0) {
3231
- return;
3336
+ return statements;
3232
3337
  }
3233
3338
 
3234
- for (const statement of statements) {
3235
- prepend_render_nodes_to_return_statement(statement, render_nodes, false, type_only);
3236
- }
3339
+ return /** @type {any[]} */ (
3340
+ prepend_render_nodes_to_return_statement(statements, render_nodes, false, type_only)
3341
+ );
3237
3342
  }
3238
3343
 
3239
3344
  /**
@@ -3241,7 +3346,7 @@ function prepend_render_nodes_to_return_statements(statements, render_nodes, typ
3241
3346
  * @param {any[]} render_nodes
3242
3347
  * @param {boolean} inside_nested_function
3243
3348
  * @param {boolean} [type_only]
3244
- * @returns {void}
3349
+ * @returns {any}
3245
3350
  */
3246
3351
  function prepend_render_nodes_to_return_statement(
3247
3352
  node,
@@ -3250,7 +3355,7 @@ function prepend_render_nodes_to_return_statement(
3250
3355
  type_only = false,
3251
3356
  ) {
3252
3357
  if (!node || typeof node !== 'object') {
3253
- return;
3358
+ return node;
3254
3359
  }
3255
3360
 
3256
3361
  if (
@@ -3262,33 +3367,44 @@ function prepend_render_nodes_to_return_statement(
3262
3367
  }
3263
3368
 
3264
3369
  if (!inside_nested_function && node.type === 'ReturnStatement') {
3265
- node.argument = combine_render_return_argument(render_nodes, node.argument, type_only);
3266
- return;
3370
+ return {
3371
+ ...node,
3372
+ argument: combine_render_return_argument(render_nodes, node.argument, type_only),
3373
+ };
3267
3374
  }
3268
3375
 
3269
3376
  if (Array.isArray(node)) {
3270
- for (const child of node) {
3271
- prepend_render_nodes_to_return_statement(
3377
+ let changed = false;
3378
+ const result = node.map((child) => {
3379
+ const walked = prepend_render_nodes_to_return_statement(
3272
3380
  child,
3273
3381
  render_nodes,
3274
3382
  inside_nested_function,
3275
3383
  type_only,
3276
3384
  );
3277
- }
3278
- return;
3385
+ if (walked !== child) changed = true;
3386
+ return walked;
3387
+ });
3388
+ return changed ? result : node;
3279
3389
  }
3280
3390
 
3391
+ let out = node;
3281
3392
  for (const key of Object.keys(node)) {
3282
3393
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
3283
3394
  continue;
3284
3395
  }
3285
- prepend_render_nodes_to_return_statement(
3396
+ const walked = prepend_render_nodes_to_return_statement(
3286
3397
  node[key],
3287
3398
  render_nodes,
3288
3399
  inside_nested_function,
3289
3400
  type_only,
3290
3401
  );
3402
+ if (walked !== node[key]) {
3403
+ if (out === node) out = { ...node };
3404
+ out[key] = walked;
3405
+ }
3291
3406
  }
3407
+ return out;
3292
3408
  }
3293
3409
 
3294
3410
  /**
@@ -4733,9 +4849,16 @@ function continue_to_bare_return(source_node) {
4733
4849
  */
4734
4850
  export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4735
4851
  if (Array.isArray(node)) {
4736
- return node.map((child) =>
4737
- rewrite_loop_continues_to_bare_returns(child, is_root && !is_loop_statement(child)),
4738
- );
4852
+ let changed = false;
4853
+ const result = node.map((child) => {
4854
+ const walked = rewrite_loop_continues_to_bare_returns(
4855
+ child,
4856
+ is_root && !is_loop_statement(child),
4857
+ );
4858
+ if (walked !== child) changed = true;
4859
+ return walked;
4860
+ });
4861
+ return changed ? result : node;
4739
4862
  }
4740
4863
 
4741
4864
  if (!node || typeof node !== 'object') {
@@ -4750,14 +4873,19 @@ export function rewrite_loop_continues_to_bare_returns(node, is_root = true) {
4750
4873
  return node;
4751
4874
  }
4752
4875
 
4876
+ let out = node;
4753
4877
  for (const key of Object.keys(node)) {
4754
4878
  if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') {
4755
4879
  continue;
4756
4880
  }
4757
- node[key] = rewrite_loop_continues_to_bare_returns(node[key], false);
4881
+ const walked = rewrite_loop_continues_to_bare_returns(node[key], false);
4882
+ if (walked !== node[key]) {
4883
+ if (out === node) out = { ...node };
4884
+ out[key] = walked;
4885
+ }
4758
4886
  }
4759
4887
 
4760
- return node;
4888
+ return out;
4761
4889
  }
4762
4890
 
4763
4891
  /**
@@ -4921,7 +5049,7 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4921
5049
  }
4922
5050
 
4923
5051
  const loop_params = get_for_of_iteration_params(node.left, node.index);
4924
- const loop_body = /** @type {any[]} */ (
5052
+ let loop_body = /** @type {any[]} */ (
4925
5053
  node.body.type === 'BlockStatement' ? node.body.body : [node.body]
4926
5054
  );
4927
5055
  validate_for_body_control_flow(loop_body, transform_context);
@@ -4952,10 +5080,10 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4952
5080
  }
4953
5081
 
4954
5082
  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);
5083
+ loop_body = apply_key_to_loop_body(loop_body, implicit_non_hook_key_expression);
4956
5084
  }
4957
5085
 
4958
- const body_statements = has_hooks
5086
+ let body_statements = has_hooks
4959
5087
  ? hook_safe_render_statements(loop_body, key_expression, transform_context)
4960
5088
  : build_render_statements(loop_body, true, transform_context);
4961
5089
 
@@ -4972,7 +5100,11 @@ function for_of_statement_to_jsx_child(node, transform_context) {
4972
5100
 
4973
5101
  const non_hook_key_expression = key_expression ?? implicit_non_hook_key_expression;
4974
5102
  if (!has_hooks && non_hook_key_expression) {
4975
- apply_key_to_render_statements(body_statements, non_hook_key_expression, transform_context);
5103
+ body_statements = apply_key_to_render_statements(
5104
+ body_statements,
5105
+ non_hook_key_expression,
5106
+ transform_context,
5107
+ );
4976
5108
  }
4977
5109
 
4978
5110
  // Restore bindings
@@ -5025,32 +5157,40 @@ function for_of_statement_to_jsx_child(node, transform_context) {
5025
5157
  }
5026
5158
 
5027
5159
  /**
5160
+ * Returns a copy of `body_nodes` where the first keyable element carries the
5161
+ * key attribute on a rebuilt opening element — the source nodes are never
5162
+ * mutated (they may belong to the caller's parsed AST).
5028
5163
  * @param {any[]} body_nodes
5029
5164
  * @param {any} key_expression
5030
- * @returns {void}
5165
+ * @returns {any[]}
5031
5166
  */
5032
5167
  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(
5168
+ let applied = false;
5169
+ return body_nodes.map((node) => {
5170
+ if (applied || node.type !== 'JSXElement') return node;
5171
+ applied = true;
5172
+ const attributes = node.openingElement?.attributes || [];
5173
+ const has_key = attributes.some(
5174
+ (/** @type {any} */ attr) =>
5175
+ attr.type === 'JSXAttribute' &&
5176
+ attr.name?.type === 'JSXIdentifier' &&
5177
+ attr.name.name === 'key',
5178
+ );
5179
+ if (has_key) return node;
5180
+ return {
5181
+ ...node,
5182
+ openingElement: {
5183
+ ...node.openingElement,
5184
+ attributes: [
5185
+ ...attributes,
5045
5186
  b.jsx_attribute(
5046
5187
  b.jsx_id('key'),
5047
5188
  to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
5048
5189
  ),
5049
- );
5050
- }
5051
- return;
5052
- }
5053
- }
5190
+ ],
5191
+ },
5192
+ };
5193
+ });
5054
5194
  }
5055
5195
 
5056
5196
  /**
@@ -5068,10 +5208,14 @@ function should_apply_key_to_loop_body(body_nodes) {
5068
5208
  }
5069
5209
 
5070
5210
  /**
5211
+ * Statement entries can be shared with the source tree, so the keyed return
5212
+ * lands on shallow copies; the returned array must be used in place of the
5213
+ * argument.
5214
+ *
5071
5215
  * @param {any[]} statements
5072
5216
  * @param {any} key_expression
5073
5217
  * @param {TransformContext} transform_context
5074
- * @returns {void}
5218
+ * @returns {any[]}
5075
5219
  */
5076
5220
  function apply_key_to_render_statements(statements, key_expression, transform_context) {
5077
5221
  for (let i = statements.length - 1; i >= 0; i -= 1) {
@@ -5080,21 +5224,29 @@ function apply_key_to_render_statements(statements, key_expression, transform_co
5080
5224
  continue;
5081
5225
  }
5082
5226
 
5083
- if (statement.argument.type === 'JSXElement') {
5084
- apply_key_to_jsx_element(statement.argument, key_expression);
5085
- } else if (statement.argument.type === 'JSXFragment') {
5227
+ let argument = statement.argument;
5228
+ if (argument.type === 'JSXElement') {
5229
+ argument = apply_key_to_jsx_element(argument, key_expression);
5230
+ } else if (argument.type === 'JSXFragment') {
5086
5231
  transform_context.needs_fragment = true;
5087
- statement.argument = keyed_fragment_to_jsx_element(statement.argument, key_expression);
5232
+ argument = keyed_fragment_to_jsx_element(argument, key_expression);
5088
5233
  }
5089
5234
 
5090
- return;
5235
+ if (argument === statement.argument) {
5236
+ return statements;
5237
+ }
5238
+ const result = [...statements];
5239
+ result[i] = { ...statement, argument };
5240
+ return result;
5091
5241
  }
5242
+ return statements;
5092
5243
  }
5093
5244
 
5094
5245
  /**
5095
5246
  * @param {any} element
5096
5247
  * @param {any} key_expression
5097
- * @returns {void}
5248
+ * @returns {any} the element itself when it already has a `key`, otherwise a
5249
+ * shallow copy with the key attribute appended.
5098
5250
  */
5099
5251
  function apply_key_to_jsx_element(element, key_expression) {
5100
5252
  const attributes = element.openingElement?.attributes || [];
@@ -5104,15 +5256,21 @@ function apply_key_to_jsx_element(element, key_expression) {
5104
5256
  attr.name?.type === 'JSXIdentifier' &&
5105
5257
  attr.name.name === 'key',
5106
5258
  );
5259
+ if (has_key) return element;
5107
5260
 
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
- }
5261
+ return {
5262
+ ...element,
5263
+ openingElement: {
5264
+ ...element.openingElement,
5265
+ attributes: [
5266
+ ...attributes,
5267
+ b.jsx_attribute(
5268
+ b.jsx_id('key'),
5269
+ to_jsx_expression_container(clone_expression_node(key_expression), key_expression),
5270
+ ),
5271
+ ],
5272
+ },
5273
+ };
5116
5274
  }
5117
5275
 
5118
5276
  /**
@@ -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
  }