esrap 1.1.1 → 1.2.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/src/handlers.js CHANGED
@@ -1,23 +1,46 @@
1
- // heavily based on https://github.com/davidbonnet/astring
2
- // released under MIT license https://github.com/davidbonnet/astring/blob/master/LICENSE
1
+ /** @type {import('./types').Newline} */
2
+ const newline = { type: 'Newline' };
3
+
4
+ /** @type {import('./types').Indent} */
5
+ const indent = { type: 'Indent' };
6
+
7
+ /** @type {import('./types').Dedent} */
8
+ const dedent = { type: 'Dedent' };
3
9
 
4
10
  /**
5
- * Does `array.push` for all `items`. Needed because `array.push(...items)` throws
6
- * "Maximum call stack size exceeded" when `items` is too big of an array.
7
- *
8
- * @param {any[]} array
9
- * @param {any[]} items
11
+ * @param {import('./types').Command[]} children
12
+ * @returns {import('./types').Sequence}
13
+ */
14
+ function create_sequence(...children) {
15
+ return { type: 'Sequence', children };
16
+ }
17
+
18
+ /**
19
+ * Rough estimate of the combined width of a group of commands
20
+ * @param {import('./types').Command[]} commands
21
+ * @param {number} from
22
+ * @param {number} to
10
23
  */
11
- function push_array(array, items) {
12
- for (let i = 0; i < items.length; i++) {
13
- array.push(items[i]);
24
+ function measure(commands, from, to = commands.length) {
25
+ let total = 0;
26
+ for (let i = from; i < to; i += 1) {
27
+ const command = commands[i];
28
+ if (typeof command === 'string') {
29
+ total += command.length;
30
+ } else if (command.type === 'Chunk') {
31
+ total += command.content.length;
32
+ } else if (command.type === 'Sequence') {
33
+ // assume this is ', '
34
+ total += 2;
35
+ }
14
36
  }
37
+
38
+ return total;
15
39
  }
16
40
 
17
41
  /**
18
42
  * @param {import('estree').Node} node
19
43
  * @param {import('./types').State} state
20
- * @returns {import('./types').Chunk[]}
21
44
  */
22
45
  export function handle(node, state) {
23
46
  const handler = handlers[node.type];
@@ -26,54 +49,46 @@ export function handle(node, state) {
26
49
  throw new Error(`Not implemented ${node.type}`);
27
50
  }
28
51
 
29
- // @ts-expect-error
30
- const result = handler(node, state);
31
-
32
52
  if (node.leadingComments) {
33
- prepend_comments(result, node.leadingComments, state);
53
+ prepend_comments(node.leadingComments, state, false);
34
54
  }
35
55
 
56
+ // @ts-expect-error
57
+ handler(node, state);
58
+
36
59
  if (node.trailingComments) {
37
60
  state.comments.push(node.trailingComments[0]); // there is only ever one
38
61
  }
39
-
40
- return result;
41
62
  }
42
63
 
43
64
  /**
44
65
  * @param {string} content
45
- * @param {import('estree').Node} [node]
66
+ * @param {import('estree').Node} node
46
67
  * @returns {import('./types').Chunk}
47
68
  */
48
69
  function c(content, node) {
49
70
  return {
71
+ type: 'Chunk',
50
72
  content,
51
- loc: node?.loc ?? null,
52
- has_newline: /\n/.test(content)
73
+ loc: node?.loc ?? null
53
74
  };
54
75
  }
55
76
 
56
77
  /**
57
- * @param {import('./types').Chunk[]} chunks
58
78
  * @param {import('estree').Comment[]} comments
59
79
  * @param {import('./types').State} state
80
+ * @param {boolean} newlines
60
81
  */
61
- function prepend_comments(chunks, comments, state) {
62
- chunks.unshift(
63
- c(
64
- comments
65
- .map((comment) =>
66
- comment.type === 'Block'
67
- ? `/*${comment.value}*/${
68
- /** @type {any} */ (comment).has_trailing_newline ? `\n${state.indent}` : ` `
69
- }`
70
- : `//${comment.value}${
71
- /** @type {any} */ (comment).has_trailing_newline ? `\n${state.indent}` : ` `
72
- }`
73
- )
74
- .join(``)
75
- )
76
- );
82
+ function prepend_comments(comments, state, newlines) {
83
+ for (const comment of comments) {
84
+ state.commands.push({ type: 'Comment', comment });
85
+
86
+ if (newlines || comment.type === 'Line' || /\n/.test(comment.value)) {
87
+ state.commands.push(newline);
88
+ } else {
89
+ state.commands.push(' ');
90
+ }
91
+ }
77
92
  }
78
93
 
79
94
  const OPERATOR_PRECEDENCE = {
@@ -205,45 +220,6 @@ function has_call_expression(node) {
205
220
  }
206
221
  }
207
222
 
208
- /** @param {import('./types').Chunk[]} chunks */
209
- const has_newline = (chunks) => {
210
- for (let i = 0; i < chunks.length; i += 1) {
211
- if (chunks[i].has_newline) return true;
212
- }
213
- return false;
214
- };
215
-
216
- /** @param {import('./types').Chunk[]} chunks */
217
- const get_length = (chunks) => {
218
- let total = 0;
219
- for (let i = 0; i < chunks.length; i += 1) {
220
- total += chunks[i].content.length;
221
- }
222
- return total;
223
- };
224
-
225
- /**
226
- * @param {number} a
227
- * @param {number} b
228
- */
229
- const sum = (a, b) => a + b;
230
-
231
- /**
232
- * @param {import('./types').Chunk[][]} nodes
233
- * @param {import('./types').Chunk} separator
234
- * @returns {import('./types').Chunk[]}
235
- */
236
- const join = (nodes, separator) => {
237
- if (nodes.length === 0) return [];
238
-
239
- const joined = [...nodes[0]];
240
- for (let i = 1; i < nodes.length; i += 1) {
241
- joined.push(separator);
242
- push_array(joined, nodes[i]);
243
- }
244
- return joined;
245
- };
246
-
247
223
  const grouped_expression_types = [
248
224
  'ImportDeclaration',
249
225
  'VariableDeclaration',
@@ -256,174 +232,175 @@ const grouped_expression_types = [
256
232
  * @param {import('./types').State} state
257
233
  */
258
234
  const handle_body = (nodes, state) => {
259
- /** @type {import('./types').Chunk[][][]} */
260
- const groups = [];
261
-
262
- /** @type {import('./types').Chunk[][]} */
263
- let group = [];
264
-
265
235
  let last_statement = /** @type {import('estree').Node} */ ({ type: 'EmptyStatement' });
266
-
267
- function flush() {
268
- if (group.length > 0) {
269
- groups.push(group);
270
- group = [];
271
- }
272
- }
236
+ let first = true;
237
+ let needs_margin = false;
273
238
 
274
239
  for (const statement of nodes) {
275
240
  if (statement.type === 'EmptyStatement') continue;
276
241
 
277
- if (
278
- (grouped_expression_types.includes(statement.type) ||
279
- grouped_expression_types.includes(last_statement.type)) &&
280
- last_statement.type !== statement.type
281
- ) {
282
- flush();
283
- }
242
+ const margin = create_sequence();
243
+
244
+ if (!first) state.commands.push(margin, newline);
245
+ first = false;
284
246
 
285
247
  const leadingComments = statement.leadingComments;
286
248
  delete statement.leadingComments;
287
249
 
288
- const chunks = handle(statement, {
289
- ...state,
290
- indent: state.indent
291
- });
250
+ if (leadingComments && leadingComments.length > 0) {
251
+ prepend_comments(leadingComments, state, true);
252
+ }
292
253
 
293
- // if a statement requires multiple lines, or it has a leading `/**` comment,
294
- // we add blank lines around it
295
- const standalone =
296
- has_newline(chunks) ||
297
- (leadingComments?.[0]?.type === 'Block' && leadingComments[0].value.startsWith('*'));
254
+ const child_state = { ...state, multiline: false };
255
+ handle(statement, child_state);
298
256
 
299
- if (leadingComments && leadingComments.length > 0) {
300
- prepend_comments(chunks, leadingComments, state);
301
- flush();
257
+ if (
258
+ child_state.multiline ||
259
+ needs_margin ||
260
+ ((grouped_expression_types.includes(statement.type) ||
261
+ grouped_expression_types.includes(last_statement.type)) &&
262
+ last_statement.type !== statement.type)
263
+ ) {
264
+ margin.children.push('\n');
302
265
  }
303
266
 
304
267
  let add_newline = false;
305
268
 
306
269
  while (state.comments.length) {
307
270
  const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
308
- const prefix = add_newline ? `\n${state.indent}` : ` `;
309
-
310
- chunks.push(
311
- c(
312
- comment.type === 'Block' ? `${prefix}/*${comment.value}*/` : `${prefix}//${comment.value}`
313
- )
314
- );
315
271
 
272
+ state.commands.push(add_newline ? newline : ' ', { type: 'Comment', comment });
316
273
  add_newline = comment.type === 'Line';
317
274
  }
318
275
 
319
- if (standalone) {
320
- flush();
321
- group.push(chunks);
322
- flush();
323
- } else {
324
- group.push(chunks);
325
- }
326
-
276
+ needs_margin = child_state.multiline;
327
277
  last_statement = statement;
328
278
  }
279
+ };
329
280
 
330
- flush();
281
+ /**
282
+ * @param {import('estree').VariableDeclaration} node
283
+ * @param {import('./types').State} state
284
+ */
285
+ const handle_var_declaration = (node, state) => {
286
+ const index = state.commands.length;
331
287
 
332
- const chunks = [];
288
+ const open = create_sequence();
289
+ const join = create_sequence();
290
+ const child_state = { ...state, multiline: false };
333
291
 
334
- for (let i = 0; i < groups.length; i += 1) {
335
- if (i > 0) {
336
- chunks.push(c(`\n\n${state.indent}`));
337
- }
292
+ state.commands.push(`${node.kind} `, open);
338
293
 
339
- for (let j = 0; j < groups[i].length; j += 1) {
340
- if (j > 0) {
341
- chunks.push(c(`\n${state.indent}`));
342
- }
294
+ let first = true;
343
295
 
344
- push_array(chunks, groups[i][j]);
345
- }
296
+ for (const d of node.declarations) {
297
+ if (!first) state.commands.push(join);
298
+ first = false;
299
+
300
+ handle(d, child_state);
346
301
  }
347
302
 
348
- return chunks;
303
+ const multiline =
304
+ child_state.multiline || (node.declarations.length > 1 && measure(state.commands, index) > 50);
305
+
306
+ if (multiline) {
307
+ state.multiline = true;
308
+ if (node.declarations.length > 1) open.children.push(indent);
309
+ join.children.push(',', newline);
310
+ if (node.declarations.length > 1) state.commands.push(dedent);
311
+ } else {
312
+ join.children.push(', ');
313
+ }
349
314
  };
350
315
 
351
316
  /**
352
- * @param {import('estree').VariableDeclaration} node
317
+ * @template {import('estree').Node} T
318
+ * @param {Array<T | null>} nodes
353
319
  * @param {import('./types').State} state
320
+ * @param {boolean} spaces
321
+ * @param {(node: T, state: import('./types').State) => void} fn
354
322
  */
355
- const handle_var_declaration = (node, state) => {
356
- const chunks = [c(`${node.kind} `)];
323
+ function sequence(nodes, state, spaces, fn) {
324
+ if (nodes.length === 0) return;
357
325
 
358
- const declarators = node.declarations.map((d) =>
359
- handle(d, {
360
- ...state,
361
- indent: state.indent + (node.declarations.length === 1 ? '' : '\t')
362
- })
363
- );
326
+ const index = state.commands.length;
364
327
 
365
- const multiple_lines =
366
- declarators.some(has_newline) ||
367
- declarators.map(get_length).reduce(sum, 0) +
368
- (state.indent.length + declarators.length - 1) * 2 >
369
- 80;
328
+ const open = create_sequence();
329
+ const join = create_sequence();
330
+ const close = create_sequence();
370
331
 
371
- const separator = c(multiple_lines ? `,\n${state.indent}\t` : ', ');
332
+ state.commands.push(open);
372
333
 
373
- push_array(chunks, join(declarators, separator));
334
+ const child_state = { ...state, multiline: false };
374
335
 
375
- return chunks;
376
- };
336
+ let prev;
377
337
 
378
- const shared = {
379
- /**
380
- * @param {import('estree').ArrayExpression | import('estree').ArrayPattern} node
381
- * @param {import('./types').State} state
382
- */
383
- 'ArrayExpression|ArrayPattern': (node, state) => {
384
- const chunks = [c('[')];
385
-
386
- /** @type {import('./types').Chunk[][]} */
387
- const elements = [];
388
-
389
- /** @type {import('./types').Chunk[]} */
390
- let sparse_commas = [];
391
-
392
- for (let i = 0; i < node.elements.length; i += 1) {
393
- // can't use map/forEach because of sparse arrays
394
- const element = node.elements[i];
395
- if (element) {
396
- elements.push([
397
- ...sparse_commas,
398
- ...handle(element, {
399
- ...state,
400
- indent: state.indent + '\t'
401
- })
402
- ]);
403
- sparse_commas = [];
404
- } else {
405
- sparse_commas.push(c(','));
338
+ for (let i = 0; i < nodes.length; i += 1) {
339
+ const node = nodes[i];
340
+ const is_first = i === 0;
341
+ const is_last = i === nodes.length - 1;
342
+
343
+ if (node) {
344
+ if (!is_first && !prev) {
345
+ state.commands.push(join);
406
346
  }
407
- }
408
347
 
409
- const multiple_lines =
410
- elements.some(has_newline) ||
411
- elements.map(get_length).reduce(sum, 0) + (state.indent.length + elements.length - 1) * 2 >
412
- 80;
348
+ fn(node, child_state);
413
349
 
414
- if (multiple_lines) {
415
- chunks.push(c(`\n${state.indent}\t`));
416
- push_array(chunks, join(elements, c(`,\n${state.indent}\t`)));
417
- chunks.push(c(`\n${state.indent}`));
418
- push_array(chunks, sparse_commas);
350
+ if (!is_last) {
351
+ state.commands.push(',');
352
+ }
353
+
354
+ if (state.comments.length > 0) {
355
+ state.commands.push(' ');
356
+
357
+ while (state.comments.length) {
358
+ const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
359
+ state.commands.push({ type: 'Comment', comment });
360
+ if (!is_last) state.commands.push(join);
361
+ }
362
+
363
+ child_state.multiline = true;
364
+ } else {
365
+ if (!is_last) state.commands.push(join);
366
+ }
419
367
  } else {
420
- push_array(chunks, join(elements, c(', ')));
421
- push_array(chunks, sparse_commas);
368
+ // This is only used for ArrayPattern and ArrayExpression, but
369
+ // it makes more sense to have the logic here than there, because
370
+ // otherwise we'd duplicate a lot more stuff
371
+ state.commands.push(',');
422
372
  }
423
373
 
424
- chunks.push(c(']'));
374
+ prev = node;
375
+ }
376
+
377
+ state.commands.push(close);
378
+
379
+ const multiline = child_state.multiline || measure(state.commands, index) > 50;
380
+
381
+ if (multiline) {
382
+ state.multiline = true;
383
+
384
+ open.children.push(indent, newline);
385
+ join.children.push(newline);
386
+ close.children.push(dedent, newline);
387
+ } else {
388
+ if (spaces) open.children.push(' ');
389
+ join.children.push(' ');
390
+ if (spaces) close.children.push(' ');
391
+ }
392
+ }
425
393
 
426
- return chunks;
394
+ /** @satisfies {Record<string, (node: any, state: import('./types').State) => undefined>} */
395
+ const shared = {
396
+ /**
397
+ * @param {import('estree').ArrayExpression | import('estree').ArrayPattern} node
398
+ * @param {import('./types').State} state
399
+ */
400
+ 'ArrayExpression|ArrayPattern': (node, state) => {
401
+ state.commands.push('[');
402
+ sequence(/** @type {import('estree').Node[]} */ (node.elements), state, false, handle);
403
+ state.commands.push(']');
427
404
  },
428
405
 
429
406
  /**
@@ -431,11 +408,6 @@ const shared = {
431
408
  * @param {import('./types').State} state
432
409
  */
433
410
  'BinaryExpression|LogicalExpression': (node, state) => {
434
- /**
435
- * @type any[]
436
- */
437
- const chunks = [];
438
-
439
411
  // TODO
440
412
  // const is_in = node.operator === 'in';
441
413
  // if (is_in) {
@@ -444,24 +416,22 @@ const shared = {
444
416
  // }
445
417
 
446
418
  if (needs_parens(node.left, node, false)) {
447
- chunks.push(c('('));
448
- push_array(chunks, handle(node.left, state));
449
- chunks.push(c(')'));
419
+ state.commands.push('(');
420
+ handle(node.left, state);
421
+ state.commands.push(')');
450
422
  } else {
451
- push_array(chunks, handle(node.left, state));
423
+ handle(node.left, state);
452
424
  }
453
425
 
454
- chunks.push(c(` ${node.operator} `));
426
+ state.commands.push(` ${node.operator} `);
455
427
 
456
428
  if (needs_parens(node.right, node, true)) {
457
- chunks.push(c('('));
458
- push_array(chunks, handle(node.right, state));
459
- chunks.push(c(')'));
429
+ state.commands.push('(');
430
+ handle(node.right, state);
431
+ state.commands.push(')');
460
432
  } else {
461
- push_array(chunks, handle(node.right, state));
433
+ handle(node.right, state);
462
434
  }
463
-
464
- return chunks;
465
435
  },
466
436
 
467
437
  /**
@@ -469,13 +439,98 @@ const shared = {
469
439
  * @param {import('./types').State} state
470
440
  */
471
441
  'BlockStatement|ClassBody': (node, state) => {
472
- if (node.body.length === 0) return [c('{}')];
442
+ if (node.body.length === 0) {
443
+ state.commands.push('{}');
444
+ return;
445
+ }
473
446
 
474
- return [
475
- c(`{\n${state.indent}\t`),
476
- ...handle_body(node.body, { ...state, indent: state.indent + '\t' }),
477
- c(`\n${state.indent}}`)
478
- ];
447
+ state.multiline = true;
448
+
449
+ state.commands.push('{', indent, newline);
450
+ handle_body(node.body, state);
451
+ state.commands.push(dedent, newline, '}');
452
+ },
453
+
454
+ /**
455
+ * @param {import('estree').CallExpression | import('estree').NewExpression} node
456
+ * @param {import('./types').State} state
457
+ */
458
+ 'CallExpression|NewExpression': (node, state) => {
459
+ const index = state.commands.length;
460
+
461
+ if (node.type === 'NewExpression') {
462
+ state.commands.push('new ');
463
+ }
464
+
465
+ const needs_parens =
466
+ EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression ||
467
+ (node.type === 'NewExpression' && has_call_expression(node.callee));
468
+
469
+ if (needs_parens) {
470
+ state.commands.push('(');
471
+ handle(node.callee, state);
472
+ state.commands.push(')');
473
+ } else {
474
+ handle(node.callee, state);
475
+ }
476
+
477
+ if (/** @type {import('estree').SimpleCallExpression} */ (node).optional) {
478
+ state.commands.push('?.');
479
+ }
480
+
481
+ const open = create_sequence();
482
+ const join = create_sequence();
483
+ const close = create_sequence();
484
+
485
+ state.commands.push('(', open);
486
+
487
+ // if the final argument is multiline, it doesn't need to force all the
488
+ // other arguments to also be multiline
489
+ const child_state = { ...state, multiline: false };
490
+ const final_state = { ...state, multiline: false };
491
+
492
+ for (let i = 0; i < node.arguments.length; i += 1) {
493
+ if (i > 0) {
494
+ if (state.comments.length > 0) {
495
+ state.commands.push(', ');
496
+
497
+ while (state.comments.length) {
498
+ const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
499
+
500
+ state.commands.push({ type: 'Comment', comment });
501
+
502
+ if (comment.type === 'Line') {
503
+ child_state.multiline = true;
504
+ state.commands.push(newline);
505
+ } else {
506
+ state.commands.push(' ');
507
+ }
508
+ }
509
+ } else {
510
+ state.commands.push(join);
511
+ }
512
+ }
513
+
514
+ const p = node.arguments[i];
515
+
516
+ handle(p, i === node.arguments.length - 1 ? final_state : child_state);
517
+ }
518
+
519
+ state.commands.push(close, ')');
520
+
521
+ const multiline = child_state.multiline;
522
+
523
+ if (multiline || final_state.multiline) {
524
+ state.multiline = true;
525
+ }
526
+
527
+ if (multiline) {
528
+ open.children.push(indent, newline);
529
+ join.children.push(',', newline);
530
+ close.children.push(dedent, newline);
531
+ } else {
532
+ join.children.push(', ');
533
+ }
479
534
  },
480
535
 
481
536
  /**
@@ -483,22 +538,20 @@ const shared = {
483
538
  * @param {import('./types').State} state
484
539
  */
485
540
  'ClassDeclaration|ClassExpression': (node, state) => {
486
- const chunks = [c('class ')];
541
+ state.commands.push('class ');
487
542
 
488
543
  if (node.id) {
489
- push_array(chunks, handle(node.id, state));
490
- chunks.push(c(' '));
544
+ handle(node.id, state);
545
+ state.commands.push(' ');
491
546
  }
492
547
 
493
548
  if (node.superClass) {
494
- chunks.push(c('extends '));
495
- push_array(chunks, handle(node.superClass, state));
496
- chunks.push(c(' '));
549
+ state.commands.push('extends ');
550
+ handle(node.superClass, state);
551
+ state.commands.push(' ');
497
552
  }
498
553
 
499
- push_array(chunks, handle(node.body, state));
500
-
501
- return chunks;
554
+ handle(node.body, state);
502
555
  },
503
556
 
504
557
  /**
@@ -506,20 +559,20 @@ const shared = {
506
559
  * @param {import('./types').State} state
507
560
  */
508
561
  'ForInStatement|ForOfStatement': (node, state) => {
509
- const chunks = [c(`for ${node.type === 'ForOfStatement' && node.await ? 'await ' : ''}(`)];
562
+ state.commands.push('for ');
563
+ if (node.type === 'ForOfStatement' && node.await) state.commands.push('await ');
564
+ state.commands.push('(');
510
565
 
511
566
  if (node.left.type === 'VariableDeclaration') {
512
- push_array(chunks, handle_var_declaration(node.left, state));
567
+ handle_var_declaration(node.left, state);
513
568
  } else {
514
- push_array(chunks, handle(node.left, state));
569
+ handle(node.left, state);
515
570
  }
516
571
 
517
- chunks.push(c(node.type === 'ForInStatement' ? ` in ` : ` of `));
518
- push_array(chunks, handle(node.right, state));
519
- chunks.push(c(') '));
520
- push_array(chunks, handle(node.body, state));
521
-
522
- return chunks;
572
+ state.commands.push(node.type === 'ForInStatement' ? ` in ` : ` of `);
573
+ handle(node.right, state);
574
+ state.commands.push(') ');
575
+ handle(node.body, state);
523
576
  },
524
577
 
525
578
  /**
@@ -527,38 +580,15 @@ const shared = {
527
580
  * @param {import('./types').State} state
528
581
  */
529
582
  'FunctionDeclaration|FunctionExpression': (node, state) => {
530
- const chunks = [];
531
-
532
- if (node.async) chunks.push(c('async '));
533
- chunks.push(c(node.generator ? 'function* ' : 'function '));
534
- if (node.id) push_array(chunks, handle(node.id, state));
535
- chunks.push(c('('));
536
-
537
- const params = node.params.map((p) =>
538
- handle(p, {
539
- ...state,
540
- indent: state.indent + '\t'
541
- })
542
- );
583
+ if (node.async) state.commands.push('async ');
584
+ state.commands.push(node.generator ? 'function* ' : 'function ');
585
+ if (node.id) handle(node.id, state);
543
586
 
544
- const multiple_lines =
545
- params.some(has_newline) ||
546
- params.map(get_length).reduce(sum, 0) + (state.indent.length + params.length - 1) * 2 > 80;
587
+ state.commands.push('(');
588
+ sequence(node.params, state, false, handle);
589
+ state.commands.push(') ');
547
590
 
548
- const separator = c(multiple_lines ? `,\n${state.indent}` : ', ');
549
-
550
- if (multiple_lines) {
551
- chunks.push(c(`\n${state.indent}\t`));
552
- push_array(chunks, join(params, separator));
553
- chunks.push(c(`\n${state.indent}`));
554
- } else {
555
- push_array(chunks, join(params, separator));
556
- }
557
-
558
- chunks.push(c(') '));
559
- push_array(chunks, handle(node.body, state));
560
-
561
- return chunks;
591
+ handle(node.body, state);
562
592
  },
563
593
 
564
594
  /**
@@ -566,7 +596,8 @@ const shared = {
566
596
  * @param {import('./types').State} state
567
597
  */
568
598
  'RestElement|SpreadElement': (node, state) => {
569
- return [c('...'), ...handle(node.argument, state)];
599
+ state.commands.push('...');
600
+ handle(node.argument, state);
570
601
  }
571
602
  };
572
603
 
@@ -577,47 +608,34 @@ const handlers = {
577
608
  ArrayPattern: shared['ArrayExpression|ArrayPattern'],
578
609
 
579
610
  ArrowFunctionExpression: (node, state) => {
580
- const chunks = [];
611
+ if (node.async) state.commands.push('async ');
581
612
 
582
- if (node.async) chunks.push(c('async '));
583
-
584
- if (node.params.length === 1 && node.params[0].type === 'Identifier') {
585
- push_array(chunks, handle(node.params[0], state));
586
- } else {
587
- const params = node.params.map((param) =>
588
- handle(param, {
589
- ...state,
590
- indent: state.indent + '\t'
591
- })
592
- );
593
-
594
- chunks.push(c('('));
595
- push_array(chunks, join(params, c(', ')));
596
- chunks.push(c(')'));
597
- }
598
-
599
- chunks.push(c(' => '));
613
+ state.commands.push('(');
614
+ sequence(node.params, state, false, handle);
615
+ state.commands.push(') => ');
600
616
 
601
617
  if (
602
618
  node.body.type === 'ObjectExpression' ||
603
619
  (node.body.type === 'AssignmentExpression' && node.body.left.type === 'ObjectPattern')
604
620
  ) {
605
- chunks.push(c('('));
606
- push_array(chunks, handle(node.body, state));
607
- chunks.push(c(')'));
621
+ state.commands.push('(');
622
+ handle(node.body, state);
623
+ state.commands.push(')');
608
624
  } else {
609
- push_array(chunks, handle(node.body, state));
625
+ handle(node.body, state);
610
626
  }
611
-
612
- return chunks;
613
627
  },
614
628
 
615
629
  AssignmentExpression(node, state) {
616
- return [...handle(node.left, state), c(` ${node.operator} `), ...handle(node.right, state)];
630
+ handle(node.left, state);
631
+ state.commands.push(` ${node.operator} `);
632
+ handle(node.right, state);
617
633
  },
618
634
 
619
635
  AssignmentPattern(node, state) {
620
- return [...handle(node.left, state), c(` = `), ...handle(node.right, state)];
636
+ handle(node.left, state);
637
+ state.commands.push(' = ');
638
+ handle(node.right, state);
621
639
  },
622
640
 
623
641
  AwaitExpression(node, state) {
@@ -625,13 +643,16 @@ const handlers = {
625
643
  const precedence = EXPRESSIONS_PRECEDENCE[node.argument.type];
626
644
 
627
645
  if (precedence && precedence < EXPRESSIONS_PRECEDENCE.AwaitExpression) {
628
- return [c('await ('), ...handle(node.argument, state), c(')')];
646
+ state.commands.push('await (');
647
+ handle(node.argument, state);
648
+ state.commands.push(')');
629
649
  } else {
630
- return [c('await '), ...handle(node.argument, state)];
650
+ state.commands.push('await ');
651
+ handle(node.argument, state);
631
652
  }
653
+ } else {
654
+ state.commands.push('await');
632
655
  }
633
-
634
- return [c('await')];
635
656
  },
636
657
 
637
658
  BinaryExpression: shared['BinaryExpression|LogicalExpression'],
@@ -639,75 +660,19 @@ const handlers = {
639
660
  BlockStatement: shared['BlockStatement|ClassBody'],
640
661
 
641
662
  BreakStatement(node, state) {
642
- return node.label ? [c('break '), ...handle(node.label, state), c(';')] : [c('break;')];
643
- },
644
-
645
- CallExpression(node, state) {
646
- /**
647
- * @type any[]
648
- */
649
- const chunks = [];
650
-
651
- if (EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression) {
652
- chunks.push(c('('));
653
- push_array(chunks, handle(node.callee, state));
654
- chunks.push(c(')'));
655
- } else {
656
- push_array(chunks, handle(node.callee, state));
657
- }
658
-
659
- if (/** @type {import('estree').SimpleCallExpression} */ (node).optional) {
660
- chunks.push(c('?.'));
661
- }
662
-
663
- let has_inline_comment = false;
664
- let arg_chunks = [];
665
- outer: for (const arg of node.arguments) {
666
- const chunks = [];
667
- while (state.comments.length) {
668
- const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
669
- if (comment.type === 'Line') {
670
- has_inline_comment = true;
671
- break outer;
672
- }
673
- chunks.push(c(comment.type === 'Block' ? `/*${comment.value}*/ ` : `//${comment.value}`));
674
- }
675
- push_array(chunks, handle(arg, state));
676
- arg_chunks.push(chunks);
677
- }
678
-
679
- const multiple_lines = has_inline_comment || arg_chunks.slice(0, -1).some(has_newline); // TODO or length exceeds 80
680
- if (multiple_lines) {
681
- // need to handle args again. TODO find alternative approach?
682
- const args = node.arguments.map((arg, i) => {
683
- const chunks = handle(arg, {
684
- ...state,
685
- indent: `${state.indent}\t`
686
- });
687
- if (i < node.arguments.length - 1) chunks.push(c(','));
688
- while (state.comments.length) {
689
- const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
690
- chunks.push(
691
- c(comment.type === 'Block' ? ` /*${comment.value}*/ ` : ` //${comment.value}`)
692
- );
693
- }
694
- return chunks;
695
- });
696
-
697
- chunks.push(c(`(\n${state.indent}\t`));
698
- push_array(chunks, join(args, c(`\n${state.indent}\t`)));
699
- chunks.push(c(`\n${state.indent})`));
663
+ if (node.label) {
664
+ state.commands.push('break ');
665
+ handle(node.label, state);
666
+ state.commands.push(';');
700
667
  } else {
701
- chunks.push(c('('));
702
- push_array(chunks, join(arg_chunks, c(', ')));
703
- chunks.push(c(')'));
668
+ state.commands.push('break;');
704
669
  }
705
-
706
- return chunks;
707
670
  },
708
671
 
672
+ CallExpression: shared['CallExpression|NewExpression'],
673
+
709
674
  ChainExpression(node, state) {
710
- return handle(node.expression, state);
675
+ handle(node.expression, state);
711
676
  },
712
677
 
713
678
  ClassBody: shared['BlockStatement|ClassBody'],
@@ -717,118 +682,103 @@ const handlers = {
717
682
  ClassExpression: shared['ClassDeclaration|ClassExpression'],
718
683
 
719
684
  ConditionalExpression(node, state) {
720
- /**
721
- * @type any[]
722
- */
723
- const chunks = [];
724
-
725
685
  if (EXPRESSIONS_PRECEDENCE[node.test.type] > EXPRESSIONS_PRECEDENCE.ConditionalExpression) {
726
- push_array(chunks, handle(node.test, state));
686
+ handle(node.test, state);
727
687
  } else {
728
- chunks.push(c('('));
729
- push_array(chunks, handle(node.test, state));
730
- chunks.push(c(')'));
688
+ state.commands.push('(');
689
+ handle(node.test, state);
690
+ state.commands.push(')');
731
691
  }
732
692
 
733
- const child_state = { ...state, indent: state.indent + '\t' };
693
+ const if_true = create_sequence();
694
+ const if_false = create_sequence();
734
695
 
735
- const consequent = handle(node.consequent, child_state);
736
- const alternate = handle(node.alternate, child_state);
696
+ const child_state = { ...state, multiline: false };
737
697
 
738
- const multiple_lines =
739
- has_newline(consequent) ||
740
- has_newline(alternate) ||
741
- get_length(chunks) + get_length(consequent) + get_length(alternate) > 50;
698
+ state.commands.push(if_true);
699
+ handle(node.consequent, child_state);
700
+ state.commands.push(if_false);
701
+ handle(node.alternate, child_state);
742
702
 
743
- if (multiple_lines) {
744
- chunks.push(c(`\n${state.indent}? `));
745
- push_array(chunks, consequent);
746
- chunks.push(c(`\n${state.indent}: `));
747
- push_array(chunks, alternate);
703
+ const multiline = child_state.multiline;
704
+
705
+ if (multiline) {
706
+ if_true.children.push(indent, newline, '? ');
707
+ if_false.children.push(newline, ': ');
708
+ state.commands.push(dedent);
748
709
  } else {
749
- chunks.push(c(` ? `));
750
- push_array(chunks, consequent);
751
- chunks.push(c(` : `));
752
- push_array(chunks, alternate);
710
+ if_true.children.push(' ? ');
711
+ if_false.children.push(' : ');
753
712
  }
754
-
755
- return chunks;
756
713
  },
757
714
 
758
715
  ContinueStatement(node, state) {
759
- return node.label ? [c('continue '), ...handle(node.label, state), c(';')] : [c('continue;')];
716
+ if (node.label) {
717
+ state.commands.push('continue ');
718
+ handle(node.label, state);
719
+ state.commands.push(';');
720
+ } else {
721
+ state.commands.push('continue;');
722
+ }
760
723
  },
761
724
 
762
725
  DebuggerStatement(node, state) {
763
- return [c('debugger', node), c(';')];
726
+ state.commands.push(c('debugger', node), ';');
764
727
  },
765
728
 
766
729
  DoWhileStatement(node, state) {
767
- return [
768
- c('do '),
769
- ...handle(node.body, state),
770
- c(' while ('),
771
- ...handle(node.test, state),
772
- c(');')
773
- ];
730
+ state.commands.push('do ');
731
+ handle(node.body, state);
732
+ state.commands.push(' while (');
733
+ handle(node.test, state);
734
+ state.commands.push(');');
774
735
  },
775
736
 
776
737
  EmptyStatement(node, state) {
777
- return [c(';')];
738
+ state.commands.push(';');
778
739
  },
779
740
 
780
741
  ExportAllDeclaration(node, state) {
781
- return [c(`export * from `), ...handle(node.source, state), c(`;`)];
742
+ state.commands.push('export * from ');
743
+ handle(node.source, state);
744
+ state.commands.push(';');
782
745
  },
783
746
 
784
747
  ExportDefaultDeclaration(node, state) {
785
- const chunks = [c(`export default `), ...handle(node.declaration, state)];
748
+ state.commands.push('export default ');
749
+
750
+ handle(node.declaration, state);
786
751
 
787
752
  if (node.declaration.type !== 'FunctionDeclaration') {
788
- chunks.push(c(';'));
753
+ state.commands.push(';');
789
754
  }
790
-
791
- return chunks;
792
755
  },
793
756
 
794
757
  ExportNamedDeclaration(node, state) {
795
- const chunks = [c('export ')];
758
+ state.commands.push('export ');
796
759
 
797
760
  if (node.declaration) {
798
- push_array(chunks, handle(node.declaration, state));
799
- } else {
800
- const specifiers = node.specifiers.map((specifier) => {
801
- const name = handle(specifier.local, state)[0];
802
- const as = handle(specifier.exported, state)[0];
803
-
804
- if (name.content === as.content) {
805
- return [name];
806
- }
807
-
808
- return [name, c(' as '), as];
809
- });
761
+ handle(node.declaration, state);
762
+ return;
763
+ }
810
764
 
811
- const width = 7 + specifiers.map(get_length).reduce(sum, 0) + 2 * specifiers.length;
765
+ state.commands.push('{');
766
+ sequence(node.specifiers, state, true, (s, state) => {
767
+ handle(s.local, state);
812
768
 
813
- if (width > 80) {
814
- chunks.push(c('{\n\t'));
815
- push_array(chunks, join(specifiers, c(',\n\t')));
816
- chunks.push(c('\n}'));
817
- } else {
818
- chunks.push(c('{ '));
819
- push_array(chunks, join(specifiers, c(', ')));
820
- chunks.push(c(' }'));
769
+ if (s.local.name !== s.exported.name) {
770
+ state.commands.push(' as ');
771
+ handle(s.exported, state);
821
772
  }
773
+ });
774
+ state.commands.push('}');
822
775
 
823
- if (node.source) {
824
- chunks.push(c(' from '));
825
- push_array(chunks, handle(node.source, state));
826
- }
776
+ if (node.source) {
777
+ state.commands.push(' from ');
778
+ handle(node.source, state);
827
779
  }
828
780
 
829
- chunks.push(c(';'));
830
-
831
- return chunks;
781
+ state.commands.push(';');
832
782
  },
833
783
 
834
784
  ExpressionStatement(node, state) {
@@ -837,32 +787,34 @@ const handlers = {
837
787
  node.expression.left.type === 'ObjectPattern'
838
788
  ) {
839
789
  // is an AssignmentExpression to an ObjectPattern
840
- return [c('('), ...handle(node.expression, state), c(');')];
790
+ state.commands.push('(');
791
+ handle(node.expression, state);
792
+ state.commands.push(');');
793
+ return;
841
794
  }
842
795
 
843
- return [...handle(node.expression, state), c(';')];
796
+ handle(node.expression, state);
797
+ state.commands.push(';');
844
798
  },
845
799
 
846
800
  ForStatement: (node, state) => {
847
- const chunks = [c('for (')];
801
+ state.commands.push('for (');
848
802
 
849
803
  if (node.init) {
850
804
  if (node.init.type === 'VariableDeclaration') {
851
- push_array(chunks, handle_var_declaration(node.init, state));
805
+ handle_var_declaration(node.init, state);
852
806
  } else {
853
- push_array(chunks, handle(node.init, state));
807
+ handle(node.init, state);
854
808
  }
855
809
  }
856
810
 
857
- chunks.push(c('; '));
858
- if (node.test) push_array(chunks, handle(node.test, state));
859
- chunks.push(c('; '));
860
- if (node.update) push_array(chunks, handle(node.update, state));
861
-
862
- chunks.push(c(') '));
863
- push_array(chunks, handle(node.body, state));
811
+ state.commands.push('; ');
812
+ if (node.test) handle(node.test, state);
813
+ state.commands.push('; ');
814
+ if (node.update) handle(node.update, state);
864
815
 
865
- return chunks;
816
+ state.commands.push(') ');
817
+ handle(node.body, state);
866
818
  },
867
819
 
868
820
  ForInStatement: shared['ForInStatement|ForOfStatement'],
@@ -873,297 +825,195 @@ const handlers = {
873
825
 
874
826
  FunctionExpression: shared['FunctionDeclaration|FunctionExpression'],
875
827
 
876
- Identifier(node) {
828
+ Identifier(node, state) {
877
829
  let name = node.name;
878
- return [c(name, node)];
830
+ state.commands.push(c(name, node));
879
831
  },
880
832
 
881
833
  IfStatement(node, state) {
882
- const chunks = [
883
- c('if ('),
884
- ...handle(node.test, state),
885
- c(') '),
886
- ...handle(node.consequent, state)
887
- ];
834
+ state.commands.push('if (');
835
+ handle(node.test, state);
836
+ state.commands.push(') ');
837
+ handle(node.consequent, state);
888
838
 
889
839
  if (node.alternate) {
890
- chunks.push(c(' else '));
891
- push_array(chunks, handle(node.alternate, state));
840
+ state.commands.push(' else ');
841
+ handle(node.alternate, state);
892
842
  }
893
-
894
- return chunks;
895
843
  },
896
844
 
897
845
  ImportDeclaration(node, state) {
898
- const chunks = [c('import ')];
899
-
900
- const { length } = node.specifiers;
901
- const source = handle(node.source, state);
846
+ if (node.specifiers.length === 0) {
847
+ state.commands.push('import ');
848
+ handle(node.source, state);
849
+ state.commands.push(';');
850
+ return;
851
+ }
902
852
 
903
- if (length > 0) {
904
- let i = 0;
853
+ /** @type {import('estree').ImportNamespaceSpecifier | null} */
854
+ let namespace_specifier = null;
905
855
 
906
- while (i < length) {
907
- if (i > 0) {
908
- chunks.push(c(', '));
909
- }
856
+ /** @type {import('estree').ImportDefaultSpecifier | null} */
857
+ let default_specifier = null;
910
858
 
911
- const specifier = node.specifiers[i];
859
+ /** @type {import('estree').ImportSpecifier[]} */
860
+ const named_specifiers = [];
912
861
 
913
- if (specifier.type === 'ImportDefaultSpecifier') {
914
- chunks.push(c(specifier.local.name, specifier));
915
- i += 1;
916
- } else if (specifier.type === 'ImportNamespaceSpecifier') {
917
- chunks.push(c('* as ' + specifier.local.name, specifier));
918
- i += 1;
919
- } else {
920
- break;
921
- }
862
+ for (const s of node.specifiers) {
863
+ if (s.type === 'ImportNamespaceSpecifier') {
864
+ namespace_specifier = s;
865
+ } else if (s.type === 'ImportDefaultSpecifier') {
866
+ default_specifier = s;
867
+ } else {
868
+ named_specifiers.push(s);
922
869
  }
870
+ }
923
871
 
924
- if (i < length) {
925
- // we have named specifiers
926
- const specifiers = /** @type {import('estree').ImportSpecifier[]} */ (node.specifiers)
927
- .slice(i)
928
- .map((specifier) => {
929
- const name = handle(specifier.imported, state)[0];
930
- const as = handle(specifier.local, state)[0];
872
+ state.commands.push('import ');
931
873
 
932
- if (name.content === as.content) {
933
- return [as];
934
- }
935
-
936
- return [name, c(' as '), as];
937
- });
874
+ if (default_specifier) {
875
+ state.commands.push(c(default_specifier.local.name, default_specifier));
876
+ if (namespace_specifier || named_specifiers.length > 0) state.commands.push(', ');
877
+ }
938
878
 
939
- const width =
940
- get_length(chunks) +
941
- specifiers.map(get_length).reduce(sum, 0) +
942
- 2 * specifiers.length +
943
- 6 +
944
- get_length(source);
879
+ if (namespace_specifier) {
880
+ state.commands.push(c('* as ' + namespace_specifier.local.name, namespace_specifier));
881
+ }
945
882
 
946
- if (width > 80) {
947
- chunks.push(c(`{\n\t`));
948
- push_array(chunks, join(specifiers, c(',\n\t')));
949
- chunks.push(c('\n}'));
950
- } else {
951
- chunks.push(c(`{ `));
952
- push_array(chunks, join(specifiers, c(', ')));
953
- chunks.push(c(' }'));
883
+ if (named_specifiers.length > 0) {
884
+ state.commands.push('{');
885
+ sequence(named_specifiers, state, true, (s, state) => {
886
+ if (s.local.name !== s.imported.name) {
887
+ handle(s.imported, state);
888
+ state.commands.push(' as ');
954
889
  }
955
- }
956
890
 
957
- chunks.push(c(' from '));
891
+ handle(s.local, state);
892
+ });
893
+ state.commands.push('}');
958
894
  }
959
895
 
960
- push_array(chunks, source);
961
- chunks.push(c(';'));
962
-
963
- return chunks;
896
+ state.commands.push(' from ');
897
+ handle(node.source, state);
898
+ state.commands.push(';');
964
899
  },
965
900
 
966
901
  ImportExpression(node, state) {
967
- return [c('import('), ...handle(node.source, state), c(')')];
902
+ state.commands.push('import(');
903
+ handle(node.source, state);
904
+ state.commands.push(')');
968
905
  },
969
906
 
970
907
  LabeledStatement(node, state) {
971
- return [...handle(node.label, state), c(': '), ...handle(node.body, state)];
908
+ handle(node.label, state);
909
+ state.commands.push(': ');
910
+ handle(node.body, state);
972
911
  },
973
912
 
974
913
  Literal(node, state) {
975
- if (typeof node.value === 'string') {
976
- return [
977
- // TODO do we need to handle weird unicode characters somehow?
978
- // str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n))
979
- c(node.raw || JSON.stringify(node.value), node)
980
- ];
981
- }
914
+ // TODO do we need to handle weird unicode characters somehow?
915
+ // str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n))
916
+ const value =
917
+ node.raw ??
918
+ (typeof node.value === 'string' ? JSON.stringify(node.value) : String(node.value));
982
919
 
983
- return [c(node.raw || String(node.value), node)];
920
+ state.commands.push(c(value, node));
984
921
  },
985
922
 
986
923
  LogicalExpression: shared['BinaryExpression|LogicalExpression'],
987
924
 
988
925
  MemberExpression(node, state) {
989
- /**
990
- * @type any[]
991
- */
992
- const chunks = [];
993
-
994
926
  if (EXPRESSIONS_PRECEDENCE[node.object.type] < EXPRESSIONS_PRECEDENCE.MemberExpression) {
995
- chunks.push(c('('));
996
- push_array(chunks, handle(node.object, state));
997
- chunks.push(c(')'));
927
+ state.commands.push('(');
928
+ handle(node.object, state);
929
+ state.commands.push(')');
998
930
  } else {
999
- push_array(chunks, handle(node.object, state));
931
+ handle(node.object, state);
1000
932
  }
1001
933
 
1002
934
  if (node.computed) {
1003
935
  if (node.optional) {
1004
- chunks.push(c('?.'));
936
+ state.commands.push('?.');
1005
937
  }
1006
- chunks.push(c('['));
1007
- push_array(chunks, handle(node.property, state));
1008
- chunks.push(c(']'));
938
+ state.commands.push('[');
939
+ handle(node.property, state);
940
+ state.commands.push(']');
1009
941
  } else {
1010
- chunks.push(c(node.optional ? '?.' : '.'));
1011
- push_array(chunks, handle(node.property, state));
942
+ state.commands.push(node.optional ? '?.' : '.');
943
+ handle(node.property, state);
1012
944
  }
1013
-
1014
- return chunks;
1015
945
  },
1016
946
 
1017
947
  MetaProperty(node, state) {
1018
- return [...handle(node.meta, state), c('.'), ...handle(node.property, state)];
948
+ handle(node.meta, state);
949
+ state.commands.push('.');
950
+ handle(node.property, state);
1019
951
  },
1020
952
 
1021
953
  MethodDefinition(node, state) {
1022
- const chunks = [];
1023
-
1024
954
  if (node.static) {
1025
- chunks.push(c('static '));
955
+ state.commands.push('static ');
1026
956
  }
1027
957
 
1028
958
  if (node.kind === 'get' || node.kind === 'set') {
1029
959
  // Getter or setter
1030
- chunks.push(c(node.kind + ' '));
960
+ state.commands.push(node.kind + ' ');
1031
961
  }
1032
962
 
1033
963
  if (node.value.async) {
1034
- chunks.push(c('async '));
964
+ state.commands.push('async ');
1035
965
  }
1036
966
 
1037
967
  if (node.value.generator) {
1038
- chunks.push(c('*'));
968
+ state.commands.push('*');
1039
969
  }
1040
970
 
1041
- if (node.computed) {
1042
- chunks.push(c('['));
1043
- push_array(chunks, handle(node.key, state));
1044
- chunks.push(c(']'));
1045
- } else {
1046
- push_array(chunks, handle(node.key, state));
1047
- }
971
+ if (node.computed) state.commands.push('[');
972
+ handle(node.key, state);
973
+ if (node.computed) state.commands.push(']');
1048
974
 
1049
- chunks.push(c('('));
1050
-
1051
- const { params } = node.value;
1052
- for (let i = 0; i < params.length; i += 1) {
1053
- push_array(chunks, handle(params[i], state));
1054
- if (i < params.length - 1) chunks.push(c(', '));
1055
- }
975
+ state.commands.push('(');
976
+ sequence(node.value.params, state, false, handle);
977
+ state.commands.push(') ');
1056
978
 
1057
- chunks.push(c(') '));
1058
- push_array(chunks, handle(node.value.body, state));
1059
-
1060
- return chunks;
979
+ handle(node.value.body, state);
1061
980
  },
1062
981
 
1063
- NewExpression(node, state) {
1064
- const chunks = [c('new ')];
1065
-
1066
- if (
1067
- EXPRESSIONS_PRECEDENCE[node.callee.type] < EXPRESSIONS_PRECEDENCE.CallExpression ||
1068
- has_call_expression(node.callee)
1069
- ) {
1070
- chunks.push(c('('));
1071
- push_array(chunks, handle(node.callee, state));
1072
- chunks.push(c(')'));
1073
- } else {
1074
- push_array(chunks, handle(node.callee, state));
1075
- }
1076
-
1077
- // TODO this is copied from CallExpression — DRY it out
1078
- const args = node.arguments.map((arg) =>
1079
- handle(arg, {
1080
- ...state,
1081
- indent: state.indent + '\t'
1082
- })
1083
- );
1084
-
1085
- const separator = args.some(has_newline) // TODO or length exceeds 80
1086
- ? c(',\n' + state.indent)
1087
- : c(', ');
1088
-
1089
- chunks.push(c('('));
1090
- push_array(chunks, join(args, separator));
1091
- chunks.push(c(')'));
1092
-
1093
- return chunks;
1094
- },
982
+ NewExpression: shared['CallExpression|NewExpression'],
1095
983
 
1096
984
  ObjectExpression(node, state) {
1097
- if (node.properties.length === 0) {
1098
- return [c('{}')];
1099
- }
1100
-
1101
- let has_inline_comment = false;
985
+ state.commands.push('{');
986
+ sequence(node.properties, state, true, (p, state) => {
987
+ if (p.type === 'Property' && p.value.type === 'FunctionExpression') {
988
+ const fn = /** @type {import('estree').FunctionExpression} */ (p.value);
1102
989
 
1103
- /** @type {import('./types').Chunk[]} */
1104
- const chunks = [];
1105
- const separator = c(', ');
1106
-
1107
- node.properties.forEach((p, i) => {
1108
- push_array(
1109
- chunks,
1110
- handle(p, {
1111
- ...state,
1112
- indent: state.indent + '\t'
1113
- })
1114
- );
1115
-
1116
- if (state.comments.length) {
1117
- // TODO generalise this, so it works with ArrayExpressions and other things.
1118
- // At present, stuff will just get appended to the closest statement/declaration
1119
- chunks.push(c(', '));
990
+ if (p.kind === 'get' || p.kind === 'set') {
991
+ state.commands.push(p.kind + ' ');
992
+ } else {
993
+ if (fn.async) state.commands.push('async ');
994
+ if (fn.generator) state.commands.push('*');
995
+ }
1120
996
 
1121
- while (state.comments.length) {
1122
- const comment = /** @type {import('estree').Comment} */ (state.comments.shift());
997
+ if (p.computed) state.commands.push('[');
998
+ handle(p.key, state);
999
+ if (p.computed) state.commands.push(']');
1123
1000
 
1124
- chunks.push(
1125
- c(
1126
- comment.type === 'Block'
1127
- ? `/*${comment.value}*/\n${state.indent}\t`
1128
- : `//${comment.value}\n${state.indent}\t`
1129
- )
1130
- );
1001
+ state.commands.push('(');
1002
+ sequence(fn.params, state, false, handle);
1003
+ state.commands.push(') ');
1131
1004
 
1132
- if (comment.type === 'Line') {
1133
- has_inline_comment = true;
1134
- }
1135
- }
1005
+ handle(fn.body, state);
1136
1006
  } else {
1137
- if (i < node.properties.length - 1) {
1138
- chunks.push(separator);
1139
- }
1007
+ handle(p, state);
1140
1008
  }
1141
1009
  });
1142
-
1143
- const multiple_lines = has_inline_comment || has_newline(chunks) || get_length(chunks) > 40;
1144
-
1145
- if (multiple_lines) {
1146
- separator.content = `,\n${state.indent}\t`;
1147
- }
1148
-
1149
- return [
1150
- c(multiple_lines ? `{\n${state.indent}\t` : `{ `),
1151
- ...chunks,
1152
- c(multiple_lines ? `\n${state.indent}}` : ` }`)
1153
- ];
1010
+ state.commands.push('}');
1154
1011
  },
1155
1012
 
1156
1013
  ObjectPattern(node, state) {
1157
- const chunks = [c('{ ')];
1158
-
1159
- for (let i = 0; i < node.properties.length; i += 1) {
1160
- push_array(chunks, handle(node.properties[i], state));
1161
- if (i < node.properties.length - 1) chunks.push(c(', '));
1162
- }
1163
-
1164
- chunks.push(c(' }'));
1165
-
1166
- return chunks;
1014
+ state.commands.push('{');
1015
+ sequence(node.properties, state, true, handle);
1016
+ state.commands.push('}');
1167
1017
  },
1168
1018
 
1169
1019
  // @ts-expect-error this isn't a real node type, but Acorn produces it
@@ -1172,100 +1022,54 @@ const handlers = {
1172
1022
  },
1173
1023
 
1174
1024
  PrivateIdentifier(node, state) {
1175
- const chunks = [c('#')];
1176
-
1177
- push_array(chunks, [c(node.name, node)]);
1178
-
1179
- return chunks;
1025
+ state.commands.push('#', c(node.name, node));
1180
1026
  },
1181
1027
 
1182
1028
  Program(node, state) {
1183
- return handle_body(node.body, state);
1029
+ handle_body(node.body, state);
1184
1030
  },
1185
1031
 
1186
1032
  Property(node, state) {
1187
- const value = handle(node.value, state);
1033
+ const value = node.value.type === 'AssignmentPattern' ? node.value.left : node.value;
1188
1034
 
1189
- if (node.key === node.value) {
1190
- return value;
1191
- }
1192
-
1193
- // special case
1194
- if (
1035
+ const shorthand =
1195
1036
  !node.computed &&
1196
- node.value.type === 'AssignmentPattern' &&
1197
- node.value.left.type === 'Identifier' &&
1037
+ node.kind === 'init' &&
1198
1038
  node.key.type === 'Identifier' &&
1199
- node.value.left.name === node.key.name
1200
- ) {
1201
- return value;
1202
- }
1039
+ value.type === 'Identifier' &&
1040
+ node.key.name === value.name;
1203
1041
 
1204
- if (
1205
- !node.computed &&
1206
- node.value.type === 'Identifier' &&
1207
- ((node.key.type === 'Identifier' && node.key.name === value[0].content) ||
1208
- (node.key.type === 'Literal' && node.key.value === value[0].content))
1209
- ) {
1210
- return value;
1042
+ if (shorthand) {
1043
+ handle(node.value, state);
1044
+ return;
1211
1045
  }
1212
1046
 
1213
- const key = handle(node.key, state);
1214
-
1215
- if (node.value.type === 'FunctionExpression' && !node.value.id) {
1216
- const chunks = node.kind !== 'init' ? [c(`${node.kind} `)] : [];
1217
-
1218
- if (node.value.async) {
1219
- chunks.push(c('async '));
1220
- }
1221
- if (node.value.generator) {
1222
- chunks.push(c('*'));
1223
- }
1224
-
1225
- push_array(chunks, node.computed ? [c('['), ...key, c(']')] : key);
1226
- chunks.push(c('('));
1227
- push_array(
1228
- chunks,
1229
- join(
1230
- node.value.params.map((param) => handle(param, state)),
1231
- c(', ')
1232
- )
1233
- );
1234
- chunks.push(c(') '));
1235
- push_array(chunks, handle(node.value.body, state));
1236
-
1237
- return chunks;
1238
- }
1239
-
1240
- if (node.computed) {
1241
- return [c('['), ...key, c(']: '), ...value];
1242
- }
1243
-
1244
- return [...key, c(': '), ...value];
1047
+ if (node.computed) state.commands.push('[');
1048
+ handle(node.key, state);
1049
+ state.commands.push(node.computed ? ']: ' : ': ');
1050
+ handle(node.value, state);
1245
1051
  },
1246
1052
 
1247
1053
  PropertyDefinition(node, state) {
1248
- const chunks = [];
1249
-
1250
1054
  if (node.static) {
1251
- chunks.push(c('static '));
1055
+ state.commands.push('static ');
1252
1056
  }
1253
1057
 
1254
1058
  if (node.computed) {
1255
- chunks.push(c('['), ...handle(node.key, state), c(']'));
1059
+ state.commands.push('[');
1060
+ handle(node.key, state);
1061
+ state.commands.push(']');
1256
1062
  } else {
1257
- chunks.push(...handle(node.key, state));
1063
+ handle(node.key, state);
1258
1064
  }
1259
1065
 
1260
1066
  if (node.value) {
1261
- chunks.push(c(' = '));
1067
+ state.commands.push(' = ');
1262
1068
 
1263
- chunks.push(...handle(node.value, state));
1069
+ handle(node.value, state);
1264
1070
  }
1265
1071
 
1266
- chunks.push(c(';'));
1267
-
1268
- return chunks;
1072
+ state.commands.push(';');
1269
1073
  },
1270
1074
 
1271
1075
  RestElement: shared['RestElement|SpreadElement'],
@@ -1274,163 +1078,186 @@ const handlers = {
1274
1078
  if (node.argument) {
1275
1079
  const contains_comment =
1276
1080
  node.argument.leadingComments &&
1277
- node.argument.leadingComments.some(
1278
- (/** @type {any} */ comment) => comment.has_trailing_newline
1279
- );
1280
- return [
1281
- c(contains_comment ? 'return (' : 'return '),
1282
- ...handle(node.argument, state),
1283
- c(contains_comment ? ');' : ';')
1284
- ];
1081
+ node.argument.leadingComments.some((comment) => comment.type === 'Line');
1082
+
1083
+ state.commands.push(contains_comment ? 'return (' : 'return ');
1084
+ handle(node.argument, state);
1085
+ state.commands.push(contains_comment ? ');' : ';');
1285
1086
  } else {
1286
- return [c('return;')];
1087
+ state.commands.push('return;');
1287
1088
  }
1288
1089
  },
1289
1090
 
1290
1091
  SequenceExpression(node, state) {
1291
- const expressions = node.expressions.map((e) => handle(e, state));
1292
-
1293
- return [c('('), ...join(expressions, c(', ')), c(')')];
1092
+ state.commands.push('(');
1093
+ sequence(node.expressions, state, false, handle);
1094
+ state.commands.push(')');
1294
1095
  },
1295
1096
 
1296
1097
  SpreadElement: shared['RestElement|SpreadElement'],
1297
1098
 
1298
1099
  StaticBlock(node, state) {
1299
- return [
1300
- c('static '),
1301
- c(`{\n${state.indent}\t`),
1302
- ...handle_body(node.body, { ...state, indent: state.indent + '\t' }),
1303
- c(`\n${state.indent}}`)
1304
- ];
1100
+ state.commands.push(indent, 'static {', newline);
1101
+
1102
+ handle_body(node.body, state);
1103
+
1104
+ state.commands.push(dedent, newline, '}');
1305
1105
  },
1306
1106
 
1307
1107
  Super(node, state) {
1308
- return [c('super', node)];
1108
+ state.commands.push(c('super', node));
1309
1109
  },
1310
1110
 
1311
1111
  SwitchStatement(node, state) {
1312
- const chunks = [c('switch ('), ...handle(node.discriminant, state), c(') {')];
1112
+ state.commands.push('switch (');
1113
+ handle(node.discriminant, state);
1114
+ state.commands.push(') {', indent);
1115
+
1116
+ let first = true;
1117
+
1118
+ for (const block of node.cases) {
1119
+ if (!first) state.commands.push('\n');
1120
+ first = false;
1313
1121
 
1314
- node.cases.forEach((block) => {
1315
1122
  if (block.test) {
1316
- chunks.push(c(`\n${state.indent}\tcase `));
1317
- push_array(chunks, handle(block.test, { ...state, indent: `${state.indent}\t` }));
1318
- chunks.push(c(':'));
1123
+ state.commands.push(newline, `case `);
1124
+ handle(block.test, state);
1125
+ state.commands.push(':');
1319
1126
  } else {
1320
- chunks.push(c(`\n${state.indent}\tdefault:`));
1127
+ state.commands.push(newline, `default:`);
1321
1128
  }
1322
1129
 
1323
- block.consequent.forEach((statement) => {
1324
- chunks.push(c(`\n${state.indent}\t\t`));
1325
- push_array(chunks, handle(statement, { ...state, indent: `${state.indent}\t\t` }));
1326
- });
1327
- });
1130
+ state.commands.push(indent);
1328
1131
 
1329
- chunks.push(c(`\n${state.indent}}`));
1132
+ for (const statement of block.consequent) {
1133
+ state.commands.push(newline);
1134
+ handle(statement, state);
1135
+ }
1136
+
1137
+ state.commands.push(dedent);
1138
+ }
1330
1139
 
1331
- return chunks;
1140
+ state.commands.push(dedent, newline, `}`);
1332
1141
  },
1333
1142
 
1334
1143
  TaggedTemplateExpression(node, state) {
1335
- return handle(node.tag, state).concat(handle(node.quasi, state));
1144
+ handle(node.tag, state);
1145
+ handle(node.quasi, state);
1336
1146
  },
1337
1147
 
1338
1148
  TemplateLiteral(node, state) {
1339
- const chunks = [c('`')];
1149
+ state.commands.push('`');
1340
1150
 
1341
1151
  const { quasis, expressions } = node;
1342
1152
 
1343
1153
  for (let i = 0; i < expressions.length; i++) {
1344
- chunks.push(c(quasis[i].value.raw), c('${'));
1345
- push_array(chunks, handle(expressions[i], state));
1346
- chunks.push(c('}'));
1154
+ const raw = quasis[i].value.raw;
1155
+
1156
+ state.commands.push(raw, '${');
1157
+ handle(expressions[i], state);
1158
+ state.commands.push('}');
1159
+
1160
+ if (/\n/.test(raw)) state.multiline = true;
1347
1161
  }
1348
1162
 
1349
- chunks.push(c(quasis[quasis.length - 1].value.raw), c('`'));
1163
+ const raw = quasis[quasis.length - 1].value.raw;
1350
1164
 
1351
- return chunks;
1165
+ state.commands.push(raw, '`');
1166
+ if (/\n/.test(raw)) state.multiline = true;
1352
1167
  },
1353
1168
 
1354
1169
  ThisExpression(node, state) {
1355
- return [c('this', node)];
1170
+ state.commands.push(c('this', node));
1356
1171
  },
1357
1172
 
1358
1173
  ThrowStatement(node, state) {
1359
- return [c('throw '), ...handle(node.argument, state), c(';')];
1174
+ state.commands.push('throw ');
1175
+ handle(node.argument, state);
1176
+ state.commands.push(';');
1360
1177
  },
1361
1178
 
1362
1179
  TryStatement(node, state) {
1363
- const chunks = [c('try '), ...handle(node.block, state)];
1180
+ state.commands.push('try ');
1181
+ handle(node.block, state);
1364
1182
 
1365
1183
  if (node.handler) {
1366
1184
  if (node.handler.param) {
1367
- chunks.push(c(' catch('));
1368
- push_array(chunks, handle(node.handler.param, state));
1369
- chunks.push(c(') '));
1185
+ state.commands.push(' catch(');
1186
+ handle(node.handler.param, state);
1187
+ state.commands.push(') ');
1370
1188
  } else {
1371
- chunks.push(c(' catch '));
1189
+ state.commands.push(' catch ');
1372
1190
  }
1373
1191
 
1374
- push_array(chunks, handle(node.handler.body, state));
1192
+ handle(node.handler.body, state);
1375
1193
  }
1376
1194
 
1377
1195
  if (node.finalizer) {
1378
- chunks.push(c(' finally '));
1379
- push_array(chunks, handle(node.finalizer, state));
1196
+ state.commands.push(' finally ');
1197
+ handle(node.finalizer, state);
1380
1198
  }
1381
-
1382
- return chunks;
1383
1199
  },
1384
1200
 
1385
1201
  UnaryExpression(node, state) {
1386
- const chunks = [c(node.operator)];
1202
+ state.commands.push(node.operator);
1387
1203
 
1388
1204
  if (node.operator.length > 1) {
1389
- chunks.push(c(' '));
1205
+ state.commands.push(' ');
1390
1206
  }
1391
1207
 
1392
1208
  if (EXPRESSIONS_PRECEDENCE[node.argument.type] < EXPRESSIONS_PRECEDENCE.UnaryExpression) {
1393
- chunks.push(c('('));
1394
- push_array(chunks, handle(node.argument, state));
1395
- chunks.push(c(')'));
1209
+ state.commands.push('(');
1210
+ handle(node.argument, state);
1211
+ state.commands.push(')');
1396
1212
  } else {
1397
- push_array(chunks, handle(node.argument, state));
1213
+ handle(node.argument, state);
1398
1214
  }
1399
-
1400
- return chunks;
1401
1215
  },
1402
1216
 
1403
1217
  UpdateExpression(node, state) {
1404
- return node.prefix
1405
- ? [c(node.operator), ...handle(node.argument, state)]
1406
- : [...handle(node.argument, state), c(node.operator)];
1218
+ if (node.prefix) {
1219
+ state.commands.push(node.operator);
1220
+ handle(node.argument, state);
1221
+ } else {
1222
+ handle(node.argument, state);
1223
+ state.commands.push(node.operator);
1224
+ }
1407
1225
  },
1408
1226
 
1409
1227
  VariableDeclaration(node, state) {
1410
- return handle_var_declaration(node, state).concat(c(';'));
1228
+ handle_var_declaration(node, state);
1229
+ state.commands.push(';');
1411
1230
  },
1412
1231
 
1413
1232
  VariableDeclarator(node, state) {
1233
+ handle(node.id, state);
1234
+
1414
1235
  if (node.init) {
1415
- return [...handle(node.id, state), c(' = '), ...handle(node.init, state)];
1416
- } else {
1417
- return handle(node.id, state);
1236
+ state.commands.push(' = ');
1237
+ handle(node.init, state);
1418
1238
  }
1419
1239
  },
1420
1240
 
1421
1241
  WhileStatement(node, state) {
1422
- return [c('while ('), ...handle(node.test, state), c(') '), ...handle(node.body, state)];
1242
+ state.commands.push('while (');
1243
+ handle(node.test, state);
1244
+ state.commands.push(') ');
1245
+ handle(node.body, state);
1423
1246
  },
1424
1247
 
1425
1248
  WithStatement(node, state) {
1426
- return [c('with ('), ...handle(node.object, state), c(') '), ...handle(node.body, state)];
1249
+ state.commands.push('with (');
1250
+ handle(node.object, state);
1251
+ state.commands.push(') ');
1252
+ handle(node.body, state);
1427
1253
  },
1428
1254
 
1429
1255
  YieldExpression(node, state) {
1430
1256
  if (node.argument) {
1431
- return [c(node.delegate ? `yield* ` : `yield `), ...handle(node.argument, state)];
1257
+ state.commands.push(node.delegate ? `yield* ` : `yield `);
1258
+ handle(node.argument, state);
1259
+ } else {
1260
+ state.commands.push(node.delegate ? `yield*` : `yield`);
1432
1261
  }
1433
-
1434
- return [c(node.delegate ? `yield*` : `yield`)];
1435
1262
  }
1436
1263
  };