Skip to content

Commit 20fabd8

Browse files
committed
feat(to-promise-all): new command, close #5
1 parent 99b3c5e commit 20fabd8

File tree

5 files changed

+300
-0
lines changed

5 files changed

+300
-0
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,34 @@ const quxx = `${qux}quxx`
326326
const foo = `foo`; const baz = 'baz'; const qux = `qux`
327327
```
328328

329+
### `to-promise-all`
330+
331+
Convert multiple `await` statements to `await Promise.all()`.
332+
333+
Triggers:
334+
- `/// to-promise-all`
335+
- `/// 2pa
336+
337+
```js
338+
/// to-promise-all
339+
const foo = await getFoo()
340+
const { bar, baz } = await getBar()
341+
```
342+
343+
Will be converted to:
344+
345+
```js
346+
const [
347+
foo,
348+
{ bar, baz },
349+
] = await Promise.all([
350+
getFoo(),
351+
getBar(),
352+
])
353+
```
354+
355+
This command will try to search all continuous declarations with `await` and convert them to a single `await Promise.all()` call.
356+
329357
## Custom Commands
330358

331359
It's also possible to define your custom commands.

src/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { toDynamicImport } from './to-dynamic-import'
77
import { toStringLiteral } from './to-string-literal'
88
import { toTemplateLiteral } from './to-template-literal'
99
import { inlineArrow } from './inline-arrow'
10+
import { toPromiseAll } from './to-promise-all'
1011

1112
// @keep-sorted
1213
export {
@@ -17,6 +18,7 @@ export {
1718
toForEach,
1819
toForOf,
1920
toFunction,
21+
toPromiseAll,
2022
toStringLiteral,
2123
toTemplateLiteral,
2224
}
@@ -30,6 +32,7 @@ export const builtinCommands = [
3032
toForEach,
3133
toForOf,
3234
toFunction,
35+
toPromiseAll,
3336
toStringLiteral,
3437
toTemplateLiteral,
3538
]

src/commands/to-promise-all.test.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { toPromiseAll as command } from './to-promise-all'
2+
import { d, run } from './_test-utils'
3+
4+
run(
5+
command,
6+
// Program level
7+
{
8+
code: d`
9+
/// to-promise-all
10+
const a = await foo()
11+
const b = await bar()
12+
`,
13+
output: d`
14+
const [
15+
a,
16+
b,
17+
] = await Promise.all([
18+
foo(),
19+
bar(),
20+
])
21+
`,
22+
errors: ['command-removal', 'command-fix'],
23+
},
24+
// Function declaration
25+
{
26+
filename: 'index.ts',
27+
code: d`
28+
async function fn() {
29+
/// to-promise-all
30+
const a = await foo()
31+
const b = await bar()
32+
}
33+
`,
34+
output: d`
35+
async function fn() {
36+
const [
37+
a,
38+
b,
39+
] = await Promise.all([
40+
foo(),
41+
bar(),
42+
] as const)
43+
}
44+
`,
45+
errors: ['command-removal', 'command-fix'],
46+
},
47+
// If Statement
48+
{
49+
code: d`
50+
if (true) {
51+
/// to-promise-all
52+
const a = await foo()
53+
.then(() => {})
54+
const b = await import('bar').then(m => m.default)
55+
}
56+
`,
57+
output: d`
58+
if (true) {
59+
const [
60+
a,
61+
b,
62+
] = await Promise.all([
63+
foo()
64+
.then(() => {}),
65+
import('bar').then(m => m.default),
66+
])
67+
}
68+
`,
69+
errors: ['command-removal', 'command-fix'],
70+
},
71+
// Mixed declarations
72+
{
73+
code: d`
74+
on('event', async () => {
75+
/// to-promise-all
76+
let a = await foo()
77+
.then(() => {})
78+
const { foo, bar } = await import('bar').then(m => m.default)
79+
const b = await baz(), c = await qux(), d = foo()
80+
})
81+
`,
82+
output: d`
83+
on('event', async () => {
84+
let [
85+
a,
86+
{ foo, bar },
87+
b,
88+
c,
89+
d,
90+
] = await Promise.all([
91+
foo()
92+
.then(() => {}),
93+
import('bar').then(m => m.default),
94+
baz(),
95+
qux(),
96+
foo(),
97+
])
98+
})
99+
`,
100+
errors: ['command-removal', 'command-fix'],
101+
},
102+
// Await expressions
103+
{
104+
code: d`
105+
/// to-promise-all
106+
const a = await bar()
107+
await foo()
108+
const b = await baz()
109+
doSomething()
110+
const nonTarget = await qux()
111+
`,
112+
output: d`
113+
const [
114+
a,
115+
/* discarded */,
116+
b,
117+
] = await Promise.all([
118+
bar(),
119+
foo(),
120+
baz(),
121+
])
122+
doSomething()
123+
const nonTarget = await qux()
124+
`,
125+
errors: ['command-removal', 'command-fix'],
126+
},
127+
// Should stop on first non-await expression
128+
{
129+
code: d`
130+
/// to-promise-all
131+
const a = await bar()
132+
let b = await foo()
133+
let c = baz()
134+
const d = await qux()
135+
`,
136+
output: d`
137+
let [
138+
a,
139+
b,
140+
] = await Promise.all([
141+
bar(),
142+
foo(),
143+
])
144+
let c = baz()
145+
const d = await qux()
146+
`,
147+
errors: ['command-removal', 'command-fix'],
148+
},
149+
)

src/commands/to-promise-all.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Command, Tree } from '../types'
2+
3+
type TargetNode = Tree.VariableDeclaration | Tree.ExpressionStatement
4+
type TargetDeclarator = Tree.VariableDeclarator | Tree.AwaitExpression
5+
6+
export const toPromiseAll: Command = {
7+
name: 'to-promise-all',
8+
match: /^[\/@:]\s*(?:to-|2)(?:promise-all|pa)$/,
9+
action(ctx) {
10+
const parent = ctx.getParentBlock()
11+
const nodeStart = ctx.findNodeBelow(isTarget) as TargetNode
12+
let nodeEnd: Tree.Node = nodeStart
13+
if (!nodeStart)
14+
return ctx.reportError('Unable to find variable declaration')
15+
if (!parent.body.includes(nodeStart))
16+
return ctx.reportError('Variable declaration is not in the same block')
17+
18+
function isTarget(node: Tree.Node): node is TargetNode {
19+
if (node.type === 'VariableDeclaration')
20+
return node.declarations.some(declarator => declarator.init?.type === 'AwaitExpression')
21+
else if (node.type === 'ExpressionStatement')
22+
return node.expression.type === 'AwaitExpression'
23+
return false
24+
}
25+
26+
function getDeclarators(node: TargetNode): TargetDeclarator[] {
27+
if (node.type === 'VariableDeclaration')
28+
return node.declarations
29+
if (node.expression.type === 'AwaitExpression')
30+
return [node.expression]
31+
return []
32+
}
33+
34+
let declarationType = 'const'
35+
const declarators: TargetDeclarator[] = []
36+
for (let i = parent.body.indexOf(nodeStart); i < parent.body.length; i++) {
37+
const node = parent.body[i]
38+
if (isTarget(node)) {
39+
declarators.push(...getDeclarators(node))
40+
nodeEnd = node
41+
if (node.type === 'VariableDeclaration' && node.kind !== 'const')
42+
declarationType = 'let'
43+
}
44+
else {
45+
break
46+
}
47+
}
48+
49+
ctx.removeComment()
50+
ctx.report({
51+
loc: {
52+
start: nodeStart.loc.start,
53+
end: nodeEnd.loc.end,
54+
},
55+
message: 'Convert to `await Promise.all`',
56+
fix(fixer) {
57+
const lineIndent = ctx.getIndentOfLine(nodeStart.loc.start.line)
58+
const isTs = ctx.context.filename.match(/\.[mc]?tsx?$/)
59+
60+
function unwrapAwait(node: Tree.Node | null) {
61+
if (node?.type === 'AwaitExpression')
62+
return node.argument
63+
return node
64+
}
65+
66+
function getId(declarator: TargetDeclarator) {
67+
if (declarator.type === 'AwaitExpression')
68+
return '/* discarded */'
69+
return ctx.getTextOf(declarator.id)
70+
}
71+
72+
function getInit(declarator: TargetDeclarator) {
73+
if (declarator.type === 'AwaitExpression')
74+
return ctx.getTextOf(declarator.argument)
75+
return ctx.getTextOf(unwrapAwait(declarator.init))
76+
}
77+
78+
const str = [
79+
`${declarationType} [`,
80+
...declarators
81+
.map(declarator => `${getId(declarator)},`),
82+
'] = await Promise.all([',
83+
...declarators
84+
.map(declarator => `${getInit(declarator)},`),
85+
isTs ? '] as const)' : '])',
86+
]
87+
.map((line, idx) => idx ? lineIndent + line : line)
88+
.join('\n')
89+
90+
return fixer.replaceTextRange([
91+
nodeStart.range[0],
92+
nodeEnd.range[1],
93+
], str)
94+
},
95+
})
96+
},
97+
}

src/context.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,27 @@ export class CommandContext {
204204
? result
205205
: result[0]
206206
}
207+
208+
/**
209+
* Get the parent block of the triggering comment
210+
*/
211+
getParentBlock(): Tree.BlockStatement | Tree.Program {
212+
const node = this.source.getNodeByRangeIndex(this.comment.range[0])
213+
if (node?.type === 'BlockStatement') {
214+
if (this.source.getCommentsInside(node).includes(this.comment))
215+
return node
216+
}
217+
if (node)
218+
console.warn(`Expected BlockStatement, got ${node.type}. This is probably an internal bug.`)
219+
return this.source.ast
220+
}
221+
222+
/**
223+
* Get indent string of a specific line
224+
*/
225+
getIndentOfLine(line: number): string {
226+
const lineStr = this.source.getLines()[line - 1] || ''
227+
const match = lineStr.match(/^\s*/)
228+
return match ? match[0] : ''
229+
}
207230
}

0 commit comments

Comments
 (0)