esrap 1.1.0 → 1.2.0

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