esrap 1.2.3 → 1.3.1

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/README.md CHANGED
@@ -44,6 +44,10 @@ const { code, map } = print(ast, {
44
44
  });
45
45
  ```
46
46
 
47
+ ## TypeScript
48
+
49
+ `esrap` can also print TypeScript nodes, assuming they match the ESTree-like [`@typescript-eslint/types`](https://www.npmjs.com/package/@typescript-eslint/types).
50
+
47
51
  ## Why not just use Prettier?
48
52
 
49
53
  Because it's ginormous.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esrap",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "Parse in reverse",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,24 +19,26 @@
19
19
  },
20
20
  "types": "./types/index.d.ts",
21
21
  "devDependencies": {
22
- "@vitest/ui": "^2.0.5",
23
- "acorn": "^8.10.0",
24
- "dts-buddy": "^0.2.4",
22
+ "@vitest/ui": "^2.1.1",
23
+ "acorn": "^8.11.3",
24
+ "acorn-typescript": "^1.4.13",
25
+ "dts-buddy": "^0.5.4",
25
26
  "prettier": "^3.0.3",
26
- "typescript": "^5.2.2",
27
- "vitest": "^2.0.5",
27
+ "typescript": "^5.7.2",
28
+ "vitest": "^2.1.1",
28
29
  "zimmerframe": "^1.0.0"
29
30
  },
30
31
  "scripts": {
31
32
  "check": "tsc",
32
- "prepublishOnly": "pnpm test && dts-buddy",
33
+ "prepublishOnly": "pnpm test && dts-buddy -m esrap:./src/public.d.ts",
33
34
  "sandbox": "node test/sandbox/index.js",
34
- "test": "vitest --run"
35
+ "test": "vitest --run",
36
+ "test:ui": "vitest --ui"
35
37
  },
36
38
  "license": "MIT",
37
39
  "dependencies": {
38
40
  "@jridgewell/sourcemap-codec": "^1.4.15",
39
- "@types/estree": "^1.0.1"
41
+ "@typescript-eslint/types": "^8.2.0"
40
42
  },
41
43
  "packageManager": "pnpm@9.8.0"
42
44
  }
package/src/handlers.js CHANGED
@@ -1,15 +1,18 @@
1
- /** @type {import('./types').Newline} */
1
+ /** @import { TSESTree } from '@typescript-eslint/types' */
2
+ /** @import { Chunk, Command, Dedent, Handlers, Indent, Newline, NodeWithComments, Sequence, State, TypeAnnotationNodes } from './types' */
3
+
4
+ /** @type {Newline} */
2
5
  const newline = { type: 'Newline' };
3
6
 
4
- /** @type {import('./types').Indent} */
7
+ /** @type {Indent} */
5
8
  const indent = { type: 'Indent' };
6
9
 
7
- /** @type {import('./types').Dedent} */
10
+ /** @type {Dedent} */
8
11
  const dedent = { type: 'Dedent' };
9
12
 
10
13
  /**
11
- * @param {import('./types').Command[]} children
12
- * @returns {import('./types').Sequence}
14
+ * @param {Command[]} children
15
+ * @returns {Sequence}
13
16
  */
14
17
  function create_sequence(...children) {
15
18
  return { type: 'Sequence', children };
@@ -17,7 +20,7 @@ function create_sequence(...children) {
17
20
 
18
21
  /**
19
22
  * Rough estimate of the combined width of a group of commands
20
- * @param {import('./types').Command[]} commands
23
+ * @param {Command[]} commands
21
24
  * @param {number} from
22
25
  * @param {number} to
23
26
  */
@@ -39,32 +42,34 @@ function measure(commands, from, to = commands.length) {
39
42
  }
40
43
 
41
44
  /**
42
- * @param {import('estree').Node} node
43
- * @param {import('./types').State} state
45
+ * @param {TSESTree.Node} node
46
+ * @param {State} state
44
47
  */
45
48
  export function handle(node, state) {
49
+ const node_with_comments = /** @type {NodeWithComments} */ (node);
50
+
46
51
  const handler = handlers[node.type];
47
52
 
48
53
  if (!handler) {
49
54
  throw new Error(`Not implemented ${node.type}`);
50
55
  }
51
56
 
52
- if (node.leadingComments) {
53
- prepend_comments(node.leadingComments, state, false);
57
+ if (node_with_comments.leadingComments) {
58
+ prepend_comments(node_with_comments.leadingComments, state, false);
54
59
  }
55
60
 
56
61
  // @ts-expect-error
57
62
  handler(node, state);
58
63
 
59
- if (node.trailingComments) {
60
- state.comments.push(node.trailingComments[0]); // there is only ever one
64
+ if (node_with_comments.trailingComments) {
65
+ state.comments.push(node_with_comments.trailingComments[0]); // there is only ever one
61
66
  }
62
67
  }
63
68
 
64
69
  /**
65
70
  * @param {string} content
66
- * @param {import('estree').Node} node
67
- * @returns {import('./types').Chunk}
71
+ * @param {TSESTree.Node} node
72
+ * @returns {Chunk}
68
73
  */
69
74
  function c(content, node) {
70
75
  return {
@@ -75,8 +80,8 @@ function c(content, node) {
75
80
  }
76
81
 
77
82
  /**
78
- * @param {import('estree').Comment[]} comments
79
- * @param {import('./types').State} state
83
+ * @param {TSESTree.Comment[]} comments
84
+ * @param {State} state
80
85
  * @param {boolean} newlines
81
86
  */
82
87
  function prepend_comments(comments, state, newlines) {
@@ -119,13 +124,16 @@ const OPERATOR_PRECEDENCE = {
119
124
  '**': 13
120
125
  };
121
126
 
122
- /** @type {Record<import('estree').Expression['type'] | 'Super' | 'RestElement', number>} */
127
+ /** @type {Record<TSESTree.Expression['type'] | 'Super' | 'RestElement', number>} */
123
128
  const EXPRESSIONS_PRECEDENCE = {
129
+ JSXFragment: 20,
130
+ JSXElement: 20,
131
+ ArrayPattern: 20,
132
+ ObjectPattern: 20,
124
133
  ArrayExpression: 20,
125
134
  TaggedTemplateExpression: 20,
126
135
  ThisExpression: 20,
127
136
  Identifier: 20,
128
- Literal: 18,
129
137
  TemplateLiteral: 20,
130
138
  Super: 20,
131
139
  SequenceExpression: 20,
@@ -135,10 +143,16 @@ const EXPRESSIONS_PRECEDENCE = {
135
143
  ChainExpression: 19,
136
144
  ImportExpression: 19,
137
145
  NewExpression: 19,
146
+ Literal: 18,
147
+ TSSatisfiesExpression: 18,
148
+ TSInstantiationExpression: 18,
149
+ TSNonNullExpression: 18,
150
+ TSTypeAssertion: 18,
138
151
  AwaitExpression: 17,
139
152
  ClassExpression: 17,
140
153
  FunctionExpression: 17,
141
154
  ObjectExpression: 17,
155
+ TSAsExpression: 16,
142
156
  UpdateExpression: 16,
143
157
  UnaryExpression: 15,
144
158
  BinaryExpression: 14,
@@ -152,12 +166,14 @@ const EXPRESSIONS_PRECEDENCE = {
152
166
 
153
167
  /**
154
168
  *
155
- * @param {import('estree').Expression} node
156
- * @param {import('estree').BinaryExpression | import('estree').LogicalExpression} parent
169
+ * @param {TSESTree.Expression | TSESTree.PrivateIdentifier} node
170
+ * @param {TSESTree.BinaryExpression | TSESTree.LogicalExpression} parent
157
171
  * @param {boolean} is_right
158
172
  * @returns
159
173
  */
160
174
  function needs_parens(node, parent, is_right) {
175
+ if (node.type === 'PrivateIdentifier') return false;
176
+
161
177
  // special case where logical expressions and coalesce expressions cannot be mixed,
162
178
  // either of them need to be wrapped with parentheses
163
179
  if (
@@ -186,7 +202,7 @@ function needs_parens(node, parent, is_right) {
186
202
  }
187
203
 
188
204
  if (
189
- /** @type {import('estree').BinaryExpression} */ (node).operator === '**' &&
205
+ /** @type {TSESTree.BinaryExpression} */ (node).operator === '**' &&
190
206
  parent.operator === '**'
191
207
  ) {
192
208
  // Exponentiation operator has right-to-left associativity
@@ -196,18 +212,18 @@ function needs_parens(node, parent, is_right) {
196
212
  if (is_right) {
197
213
  // Parenthesis are used if both operators have the same precedence
198
214
  return (
199
- OPERATOR_PRECEDENCE[/** @type {import('estree').BinaryExpression} */ (node).operator] <=
215
+ OPERATOR_PRECEDENCE[/** @type {TSESTree.BinaryExpression} */ (node).operator] <=
200
216
  OPERATOR_PRECEDENCE[parent.operator]
201
217
  );
202
218
  }
203
219
 
204
220
  return (
205
- OPERATOR_PRECEDENCE[/** @type {import('estree').BinaryExpression} */ (node).operator] <
221
+ OPERATOR_PRECEDENCE[/** @type {TSESTree.BinaryExpression} */ (node).operator] <
206
222
  OPERATOR_PRECEDENCE[parent.operator]
207
223
  );
208
224
  }
209
225
 
210
- /** @param {import('estree').Node} node */
226
+ /** @param {TSESTree.Node} node */
211
227
  function has_call_expression(node) {
212
228
  while (node) {
213
229
  if (node.type === 'CallExpression') {
@@ -228,11 +244,13 @@ const grouped_expression_types = [
228
244
  ];
229
245
 
230
246
  /**
231
- * @param {import('estree').Node[]} nodes
232
- * @param {import('./types').State} state
247
+ * @param {TSESTree.Node[]} nodes
248
+ * @param {State} state
233
249
  */
234
250
  const handle_body = (nodes, state) => {
235
- let last_statement = /** @type {import('estree').Node} */ ({ type: 'EmptyStatement' });
251
+ let last_statement = /** @type {TSESTree.Node} */ ({
252
+ type: 'EmptyStatement'
253
+ });
236
254
  let first = true;
237
255
  let needs_margin = false;
238
256
 
@@ -244,11 +262,12 @@ const handle_body = (nodes, state) => {
244
262
  if (!first) state.commands.push(margin, newline);
245
263
  first = false;
246
264
 
247
- const leadingComments = statement.leadingComments;
248
- delete statement.leadingComments;
265
+ const statement_with_comments = /** @type {NodeWithComments} */ (statement);
266
+ const leading_comments = statement_with_comments.leadingComments;
267
+ delete statement_with_comments.leadingComments;
249
268
 
250
- if (leadingComments && leadingComments.length > 0) {
251
- prepend_comments(leadingComments, state, true);
269
+ if (leading_comments && leading_comments.length > 0) {
270
+ prepend_comments(leading_comments, state, true);
252
271
  }
253
272
 
254
273
  const child_state = { ...state, multiline: false };
@@ -267,7 +286,7 @@ const handle_body = (nodes, state) => {
267
286
  let add_newline = false;
268
287
 
269
288
  while (state.comments.length) {
270
- const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
289
+ const comment = /** @type {TSESTree.Comment} */ (state.comments.shift());
271
290
 
272
291
  state.commands.push(add_newline ? newline : ' ', { type: 'Comment', comment });
273
292
  add_newline = comment.type === 'Line';
@@ -279,8 +298,8 @@ const handle_body = (nodes, state) => {
279
298
  };
280
299
 
281
300
  /**
282
- * @param {import('estree').VariableDeclaration} node
283
- * @param {import('./types').State} state
301
+ * @param {TSESTree.VariableDeclaration} node
302
+ * @param {State} state
284
303
  */
285
304
  const handle_var_declaration = (node, state) => {
286
305
  const index = state.commands.length;
@@ -314,13 +333,13 @@ const handle_var_declaration = (node, state) => {
314
333
  };
315
334
 
316
335
  /**
317
- * @template {import('estree').Node} T
336
+ * @template {TSESTree.Node} T
318
337
  * @param {Array<T | null>} nodes
319
- * @param {import('./types').State} state
338
+ * @param {State} state
320
339
  * @param {boolean} spaces
321
- * @param {(node: T, state: import('./types').State) => void} fn
340
+ * @param {(node: T, state: State) => void} fn
322
341
  */
323
- function sequence(nodes, state, spaces, fn) {
342
+ function sequence(nodes, state, spaces, fn, separator = ',') {
324
343
  if (nodes.length === 0) return;
325
344
 
326
345
  const index = state.commands.length;
@@ -348,14 +367,14 @@ function sequence(nodes, state, spaces, fn) {
348
367
  fn(node, child_state);
349
368
 
350
369
  if (!is_last) {
351
- state.commands.push(',');
370
+ state.commands.push(separator);
352
371
  }
353
372
 
354
373
  if (state.comments.length > 0) {
355
374
  state.commands.push(' ');
356
375
 
357
376
  while (state.comments.length) {
358
- const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
377
+ const comment = /** @type {TSESTree.Comment} */ (state.comments.shift());
359
378
  state.commands.push({ type: 'Comment', comment });
360
379
  if (!is_last) state.commands.push(join);
361
380
  }
@@ -368,7 +387,7 @@ function sequence(nodes, state, spaces, fn) {
368
387
  // This is only used for ArrayPattern and ArrayExpression, but
369
388
  // it makes more sense to have the logic here than there, because
370
389
  // otherwise we'd duplicate a lot more stuff
371
- state.commands.push(',');
390
+ state.commands.push(separator);
372
391
  }
373
392
 
374
393
  prev = node;
@@ -391,21 +410,172 @@ function sequence(nodes, state, spaces, fn) {
391
410
  }
392
411
  }
393
412
 
394
- /** @satisfies {Record<string, (node: any, state: import('./types').State) => undefined>} */
413
+ /**
414
+ * @param {TypeAnnotationNodes} node
415
+ * @param {State} state
416
+ */
417
+ function handle_type_annotation(node, state) {
418
+ switch (node.type) {
419
+ case 'TSNumberKeyword':
420
+ state.commands.push('number');
421
+ break;
422
+ case 'TSStringKeyword':
423
+ state.commands.push('string');
424
+ break;
425
+ case 'TSBooleanKeyword':
426
+ state.commands.push('boolean');
427
+ break;
428
+ case 'TSAnyKeyword':
429
+ state.commands.push('any');
430
+ break;
431
+ case 'TSVoidKeyword':
432
+ state.commands.push('void');
433
+ break;
434
+ case 'TSUnknownKeyword':
435
+ state.commands.push('unknown');
436
+ break;
437
+ case 'TSNeverKeyword':
438
+ state.commands.push('never');
439
+ break;
440
+ case 'TSArrayType':
441
+ handle_type_annotation(node.elementType, state);
442
+ state.commands.push('[]');
443
+ break;
444
+ case 'TSTypeAnnotation':
445
+ state.commands.push(': ');
446
+ handle_type_annotation(node.typeAnnotation, state);
447
+ break;
448
+ case 'TSTypeLiteral':
449
+ state.commands.push('{ ');
450
+ sequence(node.members, state, false, handle_type_annotation, ';');
451
+ state.commands.push(' }');
452
+ break;
453
+ case 'TSPropertySignature':
454
+ handle(node.key, state);
455
+ if (node.optional) state.commands.push('?');
456
+ if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
457
+ break;
458
+ case 'TSTypeReference':
459
+ handle(node.typeName, state);
460
+
461
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
462
+ if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
463
+ break;
464
+ case 'TSTypeParameterInstantiation':
465
+ case 'TSTypeParameterDeclaration':
466
+ state.commands.push('<');
467
+ for (let i = 0; i < node.params.length; i++) {
468
+ handle_type_annotation(node.params[i], state);
469
+ if (i != node.params.length - 1) state.commands.push(', ');
470
+ }
471
+ state.commands.push('>');
472
+ break;
473
+ case 'TSTypeParameter':
474
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
475
+ state.commands.push(node.name);
476
+
477
+ if (node.constraint) {
478
+ state.commands.push(' extends ');
479
+ handle_type_annotation(node.constraint, state);
480
+ }
481
+ break;
482
+ case 'TSTypeQuery':
483
+ state.commands.push('typeof ');
484
+ handle(node.exprName, state);
485
+ break;
486
+ case 'TSEnumMember':
487
+ handle(node.id, state);
488
+ if (node.initializer) {
489
+ state.commands.push(' = ');
490
+ handle(node.initializer, state);
491
+ }
492
+ break;
493
+ case 'TSFunctionType':
494
+ if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
495
+
496
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
497
+ const parameters = node.parameters;
498
+ state.commands.push('(');
499
+ sequence(parameters, state, false, handle);
500
+
501
+ state.commands.push(') => ');
502
+
503
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
504
+ handle_type_annotation(node.typeAnnotation.typeAnnotation, state);
505
+ break;
506
+ case 'TSIndexSignature':
507
+ const indexParameters = node.parameters;
508
+ state.commands.push('[');
509
+ sequence(indexParameters, state, false, handle);
510
+ state.commands.push(']');
511
+
512
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
513
+ handle_type_annotation(node.typeAnnotation, state);
514
+ break;
515
+ case 'TSMethodSignature':
516
+ handle(node.key, state);
517
+
518
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
519
+ const parametersSignature = node.parameters;
520
+ state.commands.push('(');
521
+ sequence(parametersSignature, state, false, handle);
522
+ state.commands.push(')');
523
+
524
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
525
+ handle_type_annotation(node.typeAnnotation, state);
526
+ break;
527
+ case 'TSExpressionWithTypeArguments':
528
+ handle(node.expression, state);
529
+ break;
530
+ case 'TSTupleType':
531
+ state.commands.push('[');
532
+ sequence(node.elementTypes, state, false, handle_type_annotation);
533
+ state.commands.push(']');
534
+ break;
535
+ case 'TSNamedTupleMember':
536
+ handle(node.label, state);
537
+ state.commands.push(': ');
538
+ handle_type_annotation(node.elementType, state);
539
+
540
+ break;
541
+ case 'TSUnionType':
542
+ sequence(node.types, state, false, handle_type_annotation, ' |');
543
+ break;
544
+ case 'TSIntersectionType':
545
+ sequence(node.types, state, false, handle_type_annotation, ' &');
546
+ break;
547
+ case 'TSLiteralType':
548
+ handle(node.literal, state);
549
+ break;
550
+ case 'TSConditionalType':
551
+ handle_type_annotation(node.checkType, state);
552
+ state.commands.push(' extends ');
553
+ handle_type_annotation(node.extendsType, state);
554
+ state.commands.push(' ? ');
555
+ handle_type_annotation(node.trueType, state);
556
+ state.commands.push(' : ');
557
+ handle_type_annotation(node.falseType, state);
558
+ break;
559
+ default:
560
+ throw new Error(`Not implemented type annotation ${node.type}`);
561
+ }
562
+ }
563
+
564
+ /** @satisfies {Record<string, (node: any, state: State) => undefined>} */
395
565
  const shared = {
396
566
  /**
397
- * @param {import('estree').ArrayExpression | import('estree').ArrayPattern} node
398
- * @param {import('./types').State} state
567
+ * @param {TSESTree.ArrayExpression | TSESTree.ArrayPattern} node
568
+ * @param {State} state
399
569
  */
400
570
  'ArrayExpression|ArrayPattern': (node, state) => {
401
571
  state.commands.push('[');
402
- sequence(/** @type {import('estree').Node[]} */ (node.elements), state, false, handle);
572
+ sequence(/** @type {TSESTree.Node[]} */ (node.elements), state, false, handle);
403
573
  state.commands.push(']');
404
574
  },
405
575
 
406
576
  /**
407
- * @param {import('estree').BinaryExpression | import('estree').LogicalExpression} node
408
- * @param {import('./types').State} state
577
+ * @param {TSESTree.BinaryExpression | TSESTree.LogicalExpression} node
578
+ * @param {State} state
409
579
  */
410
580
  'BinaryExpression|LogicalExpression': (node, state) => {
411
581
  // TODO
@@ -435,8 +605,8 @@ const shared = {
435
605
  },
436
606
 
437
607
  /**
438
- * @param {import('estree').BlockStatement | import('estree').ClassBody} node
439
- * @param {import('./types').State} state
608
+ * @param {TSESTree.BlockStatement | TSESTree.ClassBody} node
609
+ * @param {State} state
440
610
  */
441
611
  'BlockStatement|ClassBody': (node, state) => {
442
612
  if (node.body.length === 0) {
@@ -452,12 +622,10 @@ const shared = {
452
622
  },
453
623
 
454
624
  /**
455
- * @param {import('estree').CallExpression | import('estree').NewExpression} node
456
- * @param {import('./types').State} state
625
+ * @param {TSESTree.CallExpression | TSESTree.NewExpression} node
626
+ * @param {State} state
457
627
  */
458
628
  'CallExpression|NewExpression': (node, state) => {
459
- const index = state.commands.length;
460
-
461
629
  if (node.type === 'NewExpression') {
462
630
  state.commands.push('new ');
463
631
  }
@@ -474,10 +642,13 @@ const shared = {
474
642
  handle(node.callee, state);
475
643
  }
476
644
 
477
- if (/** @type {import('estree').SimpleCallExpression} */ (node).optional) {
645
+ if (/** @type {TSESTree.CallExpression} */ (node).optional) {
478
646
  state.commands.push('?.');
479
647
  }
480
648
 
649
+ // @ts-expect-error
650
+ if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
651
+
481
652
  const open = create_sequence();
482
653
  const join = create_sequence();
483
654
  const close = create_sequence();
@@ -495,7 +666,7 @@ const shared = {
495
666
  state.commands.push(', ');
496
667
 
497
668
  while (state.comments.length) {
498
- const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
669
+ const comment = /** @type {TSESTree.Comment} */ (state.comments.shift());
499
670
 
500
671
  state.commands.push({ type: 'Comment', comment });
501
672
 
@@ -534,8 +705,8 @@ const shared = {
534
705
  },
535
706
 
536
707
  /**
537
- * @param {import('estree').ClassDeclaration | import('estree').ClassExpression} node
538
- * @param {import('./types').State} state
708
+ * @param {TSESTree.ClassDeclaration | TSESTree.ClassExpression} node
709
+ * @param {State} state
539
710
  */
540
711
  'ClassDeclaration|ClassExpression': (node, state) => {
541
712
  state.commands.push('class ');
@@ -551,12 +722,17 @@ const shared = {
551
722
  state.commands.push(' ');
552
723
  }
553
724
 
725
+ if (node.implements) {
726
+ state.commands.push('implements ');
727
+ sequence(node.implements, state, false, handle_type_annotation);
728
+ }
729
+
554
730
  handle(node.body, state);
555
731
  },
556
732
 
557
733
  /**
558
- * @param {import('estree').ForInStatement | import('estree').ForOfStatement} node
559
- * @param {import('./types').State} state
734
+ * @param {TSESTree.ForInStatement | TSESTree.ForOfStatement} node
735
+ * @param {State} state
560
736
  */
561
737
  'ForInStatement|ForOfStatement': (node, state) => {
562
738
  state.commands.push('for ');
@@ -576,32 +752,43 @@ const shared = {
576
752
  },
577
753
 
578
754
  /**
579
- * @param {import('estree').FunctionDeclaration | import('estree').FunctionExpression} node
580
- * @param {import('./types').State} state
755
+ * @param {TSESTree.FunctionDeclaration | TSESTree.FunctionExpression} node
756
+ * @param {State} state
581
757
  */
582
758
  'FunctionDeclaration|FunctionExpression': (node, state) => {
583
759
  if (node.async) state.commands.push('async ');
584
760
  state.commands.push(node.generator ? 'function* ' : 'function ');
585
761
  if (node.id) handle(node.id, state);
586
762
 
763
+ if (node.typeParameters) {
764
+ handle_type_annotation(node.typeParameters, state);
765
+ }
766
+
587
767
  state.commands.push('(');
588
768
  sequence(node.params, state, false, handle);
589
- state.commands.push(') ');
769
+ state.commands.push(')');
770
+
771
+ if (node.returnType) handle_type_annotation(node.returnType, state);
772
+
773
+ state.commands.push(' ');
590
774
 
591
775
  handle(node.body, state);
592
776
  },
593
777
 
594
778
  /**
595
- * @param {import('estree').RestElement | import('estree').SpreadElement} node
596
- * @param {import('./types').State} state
779
+ * @param {TSESTree.RestElement | TSESTree.SpreadElement} node
780
+ * @param {State} state
597
781
  */
598
782
  'RestElement|SpreadElement': (node, state) => {
599
783
  state.commands.push('...');
600
784
  handle(node.argument, state);
785
+
786
+ // @ts-expect-error `acorn-typescript` and `@typescript-esling/types` have slightly different type definitions
787
+ if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
601
788
  }
602
789
  };
603
790
 
604
- /** @type {import('./types').Handlers} */
791
+ /** @type {Handlers} */
605
792
  const handlers = {
606
793
  ArrayExpression: shared['ArrayExpression|ArrayPattern'],
607
794
 
@@ -728,6 +915,12 @@ const handlers = {
728
915
  state.commands.push(c('debugger', node), ';');
729
916
  },
730
917
 
918
+ Decorator(node, state) {
919
+ state.commands.push('@');
920
+ handle(node.expression, state);
921
+ state.commands.push(newline);
922
+ },
923
+
731
924
  DoWhileStatement(node, state) {
732
925
  state.commands.push('do ');
733
926
  handle(node.body, state);
@@ -831,6 +1024,8 @@ const handlers = {
831
1024
  Identifier(node, state) {
832
1025
  let name = node.name;
833
1026
  state.commands.push(c(name, node));
1027
+
1028
+ if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
834
1029
  },
835
1030
 
836
1031
  IfStatement(node, state) {
@@ -853,13 +1048,13 @@ const handlers = {
853
1048
  return;
854
1049
  }
855
1050
 
856
- /** @type {import('estree').ImportNamespaceSpecifier | null} */
1051
+ /** @type {TSESTree.ImportNamespaceSpecifier | null} */
857
1052
  let namespace_specifier = null;
858
1053
 
859
- /** @type {import('estree').ImportDefaultSpecifier | null} */
1054
+ /** @type {TSESTree.ImportDefaultSpecifier | null} */
860
1055
  let default_specifier = null;
861
1056
 
862
- /** @type {import('estree').ImportSpecifier[]} */
1057
+ /** @type {TSESTree.ImportSpecifier[]} */
863
1058
  const named_specifiers = [];
864
1059
 
865
1060
  for (const s of node.specifiers) {
@@ -873,6 +1068,7 @@ const handlers = {
873
1068
  }
874
1069
 
875
1070
  state.commands.push('import ');
1071
+ if (node.importKind == 'type') state.commands.push('type ');
876
1072
 
877
1073
  if (default_specifier) {
878
1074
  state.commands.push(c(default_specifier.local.name, default_specifier));
@@ -891,6 +1087,7 @@ const handlers = {
891
1087
  state.commands.push(' as ');
892
1088
  }
893
1089
 
1090
+ if (s.importKind == 'type') state.commands.push('type ');
894
1091
  handle(s.local, state);
895
1092
  });
896
1093
  state.commands.push('}');
@@ -916,9 +1113,10 @@ const handlers = {
916
1113
  Literal(node, state) {
917
1114
  // TODO do we need to handle weird unicode characters somehow?
918
1115
  // str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n))
919
- const value =
920
- node.raw ??
921
- (typeof node.value === 'string' ? JSON.stringify(node.value) : String(node.value));
1116
+
1117
+ let value = node.raw;
1118
+ if (!value)
1119
+ value = typeof node.value === 'string' ? JSON.stringify(node.value) : String(node.value);
922
1120
 
923
1121
  state.commands.push(c(value, node));
924
1122
  },
@@ -954,6 +1152,12 @@ const handlers = {
954
1152
  },
955
1153
 
956
1154
  MethodDefinition(node, state) {
1155
+ if (node.decorators) {
1156
+ for (const decorator of node.decorators) {
1157
+ handle(decorator, state);
1158
+ }
1159
+ }
1160
+
957
1161
  if (node.static) {
958
1162
  state.commands.push('static ');
959
1163
  }
@@ -979,7 +1183,7 @@ const handlers = {
979
1183
  sequence(node.value.params, state, false, handle);
980
1184
  state.commands.push(') ');
981
1185
 
982
- handle(node.value.body, state);
1186
+ if (node.value.body) handle(node.value.body, state);
983
1187
  },
984
1188
 
985
1189
  NewExpression: shared['CallExpression|NewExpression'],
@@ -988,7 +1192,7 @@ const handlers = {
988
1192
  state.commands.push('{');
989
1193
  sequence(node.properties, state, true, (p, state) => {
990
1194
  if (p.type === 'Property' && p.value.type === 'FunctionExpression') {
991
- const fn = /** @type {import('estree').FunctionExpression} */ (p.value);
1195
+ const fn = /** @type {TSESTree.FunctionExpression} */ (p.value);
992
1196
 
993
1197
  if (p.kind === 'get' || p.kind === 'set') {
994
1198
  state.commands.push(p.kind + ' ');
@@ -1017,6 +1221,8 @@ const handlers = {
1017
1221
  state.commands.push('{');
1018
1222
  sequence(node.properties, state, true, handle);
1019
1223
  state.commands.push('}');
1224
+
1225
+ if (node.typeAnnotation) handle_type_annotation(node.typeAnnotation, state);
1020
1226
  },
1021
1227
 
1022
1228
  // @ts-expect-error this isn't a real node type, but Acorn produces it
@@ -1054,6 +1260,10 @@ const handlers = {
1054
1260
  },
1055
1261
 
1056
1262
  PropertyDefinition(node, state) {
1263
+ if (node.accessibility) {
1264
+ state.commands.push(node.accessibility, ' ');
1265
+ }
1266
+
1057
1267
  if (node.static) {
1058
1268
  state.commands.push('static ');
1059
1269
  }
@@ -1066,6 +1276,11 @@ const handlers = {
1066
1276
  handle(node.key, state);
1067
1277
  }
1068
1278
 
1279
+ if (node.typeAnnotation) {
1280
+ state.commands.push(': ');
1281
+ handle_type_annotation(node.typeAnnotation.typeAnnotation, state);
1282
+ }
1283
+
1069
1284
  if (node.value) {
1070
1285
  state.commands.push(' = ');
1071
1286
 
@@ -1079,9 +1294,10 @@ const handlers = {
1079
1294
 
1080
1295
  ReturnStatement(node, state) {
1081
1296
  if (node.argument) {
1297
+ const argumentWithComment = /** @type {NodeWithComments} */ (node.argument);
1082
1298
  const contains_comment =
1083
- node.argument.leadingComments &&
1084
- node.argument.leadingComments.some((comment) => comment.type === 'Line');
1299
+ argumentWithComment.leadingComments &&
1300
+ argumentWithComment.leadingComments.some((comment) => comment.type === 'Line');
1085
1301
 
1086
1302
  state.commands.push(contains_comment ? 'return (' : 'return ');
1087
1303
  handle(node.argument, state);
@@ -1175,7 +1391,7 @@ const handlers = {
1175
1391
 
1176
1392
  ThrowStatement(node, state) {
1177
1393
  state.commands.push('throw ');
1178
- handle(node.argument, state);
1394
+ if (node.argument) handle(node.argument, state);
1179
1395
  state.commands.push(';');
1180
1396
  },
1181
1397
 
@@ -1201,6 +1417,85 @@ const handlers = {
1201
1417
  }
1202
1418
  },
1203
1419
 
1420
+ TSAsExpression(node, state) {
1421
+ if (node.expression) {
1422
+ const needs_parens =
1423
+ EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.TSAsExpression;
1424
+
1425
+ if (needs_parens) {
1426
+ state.commands.push('(');
1427
+ handle(node.expression, state);
1428
+ state.commands.push(')');
1429
+ } else {
1430
+ handle(node.expression, state);
1431
+ }
1432
+ }
1433
+ state.commands.push(' as ');
1434
+ handle_type_annotation(node.typeAnnotation, state);
1435
+ },
1436
+
1437
+ TSEnumDeclaration(node, state) {
1438
+ state.commands.push('enum ');
1439
+ handle(node.id, state);
1440
+ state.commands.push(' {', indent, newline);
1441
+ sequence(node.members, state, false, handle_type_annotation);
1442
+ state.commands.push(dedent, newline, '}', newline);
1443
+ },
1444
+
1445
+ TSNonNullExpression(node, state) {
1446
+ handle(node.expression, state);
1447
+ state.commands.push('!');
1448
+ },
1449
+
1450
+ TSInterfaceBody(node, state) {
1451
+ sequence(node.body, state, false, handle_type_annotation, ';');
1452
+ },
1453
+
1454
+ TSInterfaceDeclaration(node, state) {
1455
+ state.commands.push('interface ');
1456
+ handle(node.id, state);
1457
+ if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
1458
+ if (node.extends) {
1459
+ state.commands.push(' extends ');
1460
+ sequence(node.extends, state, false, handle_type_annotation);
1461
+ }
1462
+ state.commands.push(' {');
1463
+ handle(node.body, state);
1464
+ state.commands.push('}');
1465
+ },
1466
+
1467
+ TSSatisfiesExpression(node, state) {
1468
+ if (node.expression) {
1469
+ const needs_parens =
1470
+ EXPRESSIONS_PRECEDENCE[node.expression.type] < EXPRESSIONS_PRECEDENCE.TSSatisfiesExpression;
1471
+
1472
+ if (needs_parens) {
1473
+ state.commands.push('(');
1474
+ handle(node.expression, state);
1475
+ state.commands.push(')');
1476
+ } else {
1477
+ handle(node.expression, state);
1478
+ }
1479
+ }
1480
+ state.commands.push(' satisfies ');
1481
+ handle_type_annotation(node.typeAnnotation, state);
1482
+ },
1483
+
1484
+ TSTypeAliasDeclaration(node, state) {
1485
+ state.commands.push('type ');
1486
+ handle(node.id, state);
1487
+ if (node.typeParameters) handle_type_annotation(node.typeParameters, state);
1488
+ state.commands.push(' = ');
1489
+ handle_type_annotation(node.typeAnnotation, state);
1490
+ state.commands.push(';');
1491
+ },
1492
+
1493
+ TSQualifiedName(node, state) {
1494
+ handle(node.left, state);
1495
+ state.commands.push('.');
1496
+ handle(node.right, state);
1497
+ },
1498
+
1204
1499
  UnaryExpression(node, state) {
1205
1500
  state.commands.push(node.operator);
1206
1501
 
package/src/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** @import { TSESTree } from '@typescript-eslint/types' */
2
+ /** @import { Command, PrintOptions, State } from './types' */
1
3
  import { handle } from './handlers.js';
2
4
  import { encode } from '@jridgewell/sourcemap-codec';
3
5
 
@@ -15,15 +17,7 @@ if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
15
17
  }
16
18
 
17
19
  /**
18
- * @typedef {{
19
- * sourceMapSource?: string;
20
- * sourceMapContent?: string;
21
- * sourceMapEncodeMappings?: boolean; // default true
22
- * }} PrintOptions
23
- */
24
-
25
- /**
26
- * @param {import('estree').Node} node
20
+ * @param {TSESTree.Node} node
27
21
  * @param {PrintOptions} opts
28
22
  * @returns {{ code: string, map: any }} // TODO
29
23
  */
@@ -31,6 +25,7 @@ export function print(node, opts = {}) {
31
25
  if (Array.isArray(node)) {
32
26
  return print(
33
27
  {
28
+ //@ts-expect-error
34
29
  type: 'Program',
35
30
  body: node,
36
31
  sourceType: 'module'
@@ -39,7 +34,7 @@ export function print(node, opts = {}) {
39
34
  );
40
35
  }
41
36
 
42
- /** @type {import('./types').State} */
37
+ /** @type {State} */
43
38
  const state = {
44
39
  commands: [],
45
40
  comments: [],
@@ -76,7 +71,7 @@ export function print(node, opts = {}) {
76
71
 
77
72
  let newline = '\n';
78
73
 
79
- /** @param {import('./types').Command} command */
74
+ /** @param {Command} command */
80
75
  function run(command) {
81
76
  if (typeof command === 'string') {
82
77
  append(command);
@@ -0,0 +1,2 @@
1
+ export { PrintOptions } from './types';
2
+ export * from './index';
package/src/types.d.ts CHANGED
@@ -1,16 +1,39 @@
1
- import { Comment, Node } from 'estree';
2
-
3
- type NodeOf<T extends string, X> = X extends { type: T } ? X : never;
1
+ import { TSESTree } from '@typescript-eslint/types';
4
2
 
5
3
  type Handler<T> = (node: T, state: State) => undefined;
6
4
 
7
5
  export type Handlers = {
8
- [K in Node['type']]: Handler<NodeOf<K, Node>>;
6
+ [T in TSESTree.Node['type']]: Handler<Extract<TSESTree.Node, { type: T }>>;
7
+ };
8
+
9
+ export type TypeAnnotationNodes =
10
+ | TSESTree.TypeNode
11
+ | TSESTree.TypeElement
12
+ | TSESTree.TSTypeAnnotation
13
+ | TSESTree.TSPropertySignature
14
+ | TSESTree.TSTypeParameter
15
+ | TSESTree.TSTypeParameterDeclaration
16
+ | TSESTree.TSTypeParameterInstantiation
17
+ | TSESTree.TSEnumMember
18
+ | TSESTree.TSInterfaceHeritage
19
+ | TSESTree.TSClassImplements
20
+ | TSExpressionWithTypeArguments;
21
+
22
+ type TSExpressionWithTypeArguments = {
23
+ type: 'TSExpressionWithTypeArguments';
24
+ expression: any;
9
25
  };
10
26
 
27
+ // `@typescript-eslint/types` differs from the official `estree` spec by handling
28
+ // comments differently. This is a node which we can use to ensure type saftey.
29
+ export type NodeWithComments = {
30
+ leadingComments?: TSESTree.Comment[] | undefined;
31
+ trailingComments?: TSESTree.Comment[] | undefined;
32
+ } & TSESTree.Node;
33
+
11
34
  export interface State {
12
35
  commands: Command[];
13
- comments: Comment[];
36
+ comments: TSESTree.Comment[];
14
37
  multiline: boolean;
15
38
  }
16
39
 
@@ -47,7 +70,13 @@ export interface Sequence {
47
70
 
48
71
  export interface CommentChunk {
49
72
  type: 'Comment';
50
- comment: Comment;
73
+ comment: TSESTree.Comment;
51
74
  }
52
75
 
53
76
  export type Command = string | Chunk | Newline | Indent | Dedent | Sequence | CommentChunk;
77
+
78
+ export interface PrintOptions {
79
+ sourceMapSource?: string;
80
+ sourceMapContent?: string;
81
+ sourceMapEncodeMappings?: boolean; // default true
82
+ }
package/types/index.d.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  declare module 'esrap' {
2
+ import type { TSESTree } from '@typescript-eslint/types';
3
+ export interface PrintOptions {
4
+ sourceMapSource?: string;
5
+ sourceMapContent?: string;
6
+ sourceMapEncodeMappings?: boolean; // default true
7
+ }
2
8
  /**
3
9
  * @returns // TODO
4
10
  */
5
- export function print(node: import('estree').Node, opts?: PrintOptions): {
11
+ export function print(node: TSESTree.Node, opts?: PrintOptions): {
6
12
  code: string;
7
13
  map: any;
8
14
  };
9
- export type PrintOptions = {
10
- sourceMapSource?: string;
11
- sourceMapContent?: string;
12
- sourceMapEncodeMappings?: boolean;
13
- };
15
+
16
+ export {};
14
17
  }
15
18
 
16
19
  //# sourceMappingURL=index.d.ts.map
@@ -2,14 +2,17 @@
2
2
  "version": 3,
3
3
  "file": "index.d.ts",
4
4
  "names": [
5
+ "PrintOptions",
5
6
  "print"
6
7
  ],
7
8
  "sources": [
9
+ "../src/types.d.ts",
8
10
  "../src/index.js"
9
11
  ],
10
12
  "sourcesContent": [
13
+ null,
11
14
  null
12
15
  ],
13
- "mappings": ";;;;iBA6BgBA,KAAKA",
16
+ "mappings": ";;kBA6EiBA,YAAYA;;;;;;;;iBCtDbC,KAAKA",
14
17
  "ignoreList": []
15
18
  }