Skip to content

Commit 0216853

Browse files
feat: support async plugin's pre/post (#16862)
* feat: support async plugin's pre/post * feat: add isAsync on PluginPass * Update types --------- Co-authored-by: Nicolò Ribaudo <hello@nicr.dev>
1 parent a7f2c6f commit 0216853

13 files changed

Lines changed: 202 additions & 85 deletions

File tree

packages/babel-core/src/config/full.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,9 @@ const instantiatePlugin = makeWeakCache(function* (
363363
return cache.invalidate(data => run(inheritsDescriptor, data));
364364
});
365365

366-
plugin.pre = chain(inherits.pre, plugin.pre);
367-
plugin.post = chain(inherits.post, plugin.post);
368-
plugin.manipulateOptions = chain(
366+
plugin.pre = chainMaybeAsync(inherits.pre, plugin.pre);
367+
plugin.post = chainMaybeAsync(inherits.post, plugin.post);
368+
plugin.manipulateOptions = chainMaybeAsync(
369369
inherits.manipulateOptions,
370370
plugin.manipulateOptions,
371371
);
@@ -488,16 +488,18 @@ function* loadPresetDescriptor(
488488
};
489489
}
490490

491-
function chain<Args extends any[]>(
492-
a: undefined | ((...args: Args) => void),
493-
b: undefined | ((...args: Args) => void),
494-
) {
495-
const fns = [a, b].filter(Boolean);
496-
if (fns.length <= 1) return fns[0];
497-
498-
return function (this: unknown, ...args: unknown[]) {
499-
for (const fn of fns) {
500-
fn.apply(this, args);
491+
function chainMaybeAsync<Args extends any[], R extends void | Promise<void>>(
492+
a: undefined | ((...args: Args) => R),
493+
b: undefined | ((...args: Args) => R),
494+
): (...args: Args) => R {
495+
if (!a) return b;
496+
if (!b) return a;
497+
498+
return function (this: unknown, ...args: Args) {
499+
const res = a.apply(this, args);
500+
if (res && typeof res.then === "function") {
501+
return res.then(() => b.apply(this, args));
501502
}
502-
};
503+
return b.apply(this, args);
504+
} as (...args: Args) => R;
503505
}

packages/babel-core/src/config/validation/plugins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export type PluginObject<S extends PluginPass = PluginPass> = {
8585
options: ValidatedOptions,
8686
parserOpts: ParserOptions,
8787
) => void;
88-
pre?: (this: S, file: File) => void;
89-
post?: (this: S, file: File) => void;
88+
pre?: (this: S, file: File) => void | Promise<void>;
89+
post?: (this: S, file: File) => void | Promise<void>;
9090
inherits?: (
9191
api: PluginAPI,
9292
options: unknown,

packages/babel-core/src/transformation/index.ts

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import generateCode from "./file/generate.ts";
1515
import type File from "./file/file.ts";
1616

1717
import { flattenToSet } from "../config/helpers/deep-array.ts";
18+
import { isAsync, maybeAsync } from "../gensync-utils/async.ts";
1819

1920
export type FileResultCallback = {
2021
(err: Error, file: null): void;
@@ -79,36 +80,30 @@ export function* run(
7980
}
8081

8182
function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
83+
const async = yield* isAsync();
84+
8285
for (const pluginPairs of pluginPasses) {
8386
const passPairs: [Plugin, PluginPass][] = [];
8487
const passes = [];
8588
const visitors = [];
8689

8790
for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
88-
const pass = new PluginPass(file, plugin.key, plugin.options);
91+
const pass = new PluginPass(file, plugin.key, plugin.options, async);
8992

9093
passPairs.push([plugin, pass]);
9194
passes.push(pass);
9295
visitors.push(plugin.visitor);
9396
}
9497

9598
for (const [plugin, pass] of passPairs) {
96-
const fn = plugin.pre;
97-
if (fn) {
98-
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
99-
const result = fn.call(pass, file);
100-
101-
// If we want to support async .pre
102-
yield* [];
103-
104-
if (isThenable(result)) {
105-
throw new Error(
106-
`You appear to be using an plugin with an async .pre, ` +
107-
`which your current version of Babel does not support. ` +
108-
`If you're using a published plugin, you may need to upgrade ` +
109-
`your @babel/core version.`,
110-
);
111-
}
99+
if (plugin.pre) {
100+
const fn = maybeAsync(
101+
plugin.pre,
102+
`You appear to be using an async plugin/preset, but Babel has been called synchronously`,
103+
);
104+
105+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
106+
yield* fn.call(pass, file);
112107
}
113108
}
114109

@@ -125,32 +120,15 @@ function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
125120
}
126121

127122
for (const [plugin, pass] of passPairs) {
128-
const fn = plugin.post;
129-
if (fn) {
130-
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
131-
const result = fn.call(pass, file);
132-
133-
// If we want to support async .post
134-
yield* [];
135-
136-
if (isThenable(result)) {
137-
throw new Error(
138-
`You appear to be using an plugin with an async .post, ` +
139-
`which your current version of Babel does not support. ` +
140-
`If you're using a published plugin, you may need to upgrade ` +
141-
`your @babel/core version.`,
142-
);
143-
}
123+
if (plugin.post) {
124+
const fn = maybeAsync(
125+
plugin.post,
126+
`You appear to be using an async plugin/preset, but Babel has been called synchronously`,
127+
);
128+
129+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
130+
yield* fn.call(pass, file);
144131
}
145132
}
146133
}
147134
}
148-
149-
function isThenable<T extends PromiseLike<any>>(val: any): val is T {
150-
return (
151-
!!val &&
152-
(typeof val === "object" || typeof val === "function") &&
153-
!!val.then &&
154-
typeof val.then === "function"
155-
);
156-
}

packages/babel-core/src/transformation/plugin-pass.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,32 @@ export default class PluginPass<Options = object> {
77
file: File;
88
opts: Partial<Options>;
99

10-
// The working directory that Babel's programmatic options are loaded
11-
// relative to.
10+
/**
11+
* The working directory that Babel's programmatic options are loaded
12+
* relative to.
13+
*/
1214
cwd: string;
1315

14-
// The absolute path of the file being compiled.
16+
/** The absolute path of the file being compiled. */
1517
filename: string | void;
1618

17-
constructor(file: File, key?: string | null, options?: Options) {
19+
/**
20+
* Is Babel executed in async mode or not.
21+
*/
22+
isAsync: boolean;
23+
24+
constructor(
25+
file: File,
26+
key: string | null,
27+
options: Options | undefined,
28+
isAsync: boolean,
29+
) {
1830
this.key = key;
1931
this.file = file;
2032
this.opts = options || {};
2133
this.cwd = file.opts.cwd;
2234
this.filename = file.opts.filename;
35+
this.isAsync = isAsync;
2336
}
2437

2538
set(key: unknown, val: unknown) {

packages/babel-core/test/async.js

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,24 @@ describe("asynchronicity", () => {
157157
expect(() =>
158158
babel.transformSync(""),
159159
).toThrowErrorMatchingInlineSnapshot(
160-
`"unknown file: You appear to be using an plugin with an async .pre, which your current version` +
161-
` of Babel does not support. If you're using a published plugin, you may need to upgrade your` +
162-
` @babel/core version."`,
160+
`"unknown file: You appear to be using an async plugin/preset, but Babel has been called synchronously"`,
163161
);
164162
});
165163

166164
nodeGte8("called asynchronously", async () => {
167165
process.chdir("plugin-pre");
168166

169-
await expect(
170-
babel.transformAsync(""),
171-
).rejects.toThrowErrorMatchingInlineSnapshot(
172-
`"unknown file: You appear to be using an plugin with an async .pre, which your current version` +
173-
` of Babel does not support. If you're using a published plugin, you may need to upgrade your` +
174-
` @babel/core version."`,
175-
);
167+
await expect(babel.transformAsync("")).resolves.toMatchObject({
168+
code: `"success"`,
169+
});
170+
});
171+
172+
nodeGte8("should await inherited .pre", async () => {
173+
process.chdir("plugin-pre-chaining");
174+
175+
await expect(babel.transformAsync("")).resolves.toMatchObject({
176+
code: `"pluginC,pluginB,pluginA"`,
177+
});
176178
});
177179
});
178180

@@ -183,22 +185,50 @@ describe("asynchronicity", () => {
183185
expect(() =>
184186
babel.transformSync(""),
185187
).toThrowErrorMatchingInlineSnapshot(
186-
`"unknown file: You appear to be using an plugin with an async .post, which your current version` +
187-
` of Babel does not support. If you're using a published plugin, you may need to upgrade your` +
188-
` @babel/core version."`,
188+
`"unknown file: You appear to be using an async plugin/preset, but Babel has been called synchronously"`,
189189
);
190190
});
191191

192192
nodeGte8("called asynchronously", async () => {
193193
process.chdir("plugin-post");
194194

195-
await expect(
196-
babel.transformAsync(""),
197-
).rejects.toThrowErrorMatchingInlineSnapshot(
198-
`"unknown file: You appear to be using an plugin with an async .post, which your current version` +
199-
` of Babel does not support. If you're using a published plugin, you may need to upgrade your` +
200-
` @babel/core version."`,
201-
);
195+
await expect(babel.transformAsync("")).resolves.toMatchObject({
196+
code: `"success"`,
197+
});
198+
});
199+
200+
nodeGte8("should await inherited .post", async () => {
201+
process.chdir("plugin-post-chaining");
202+
203+
await expect(babel.transformAsync("")).resolves.toMatchObject({
204+
code: `"pluginC,pluginB,pluginA"`,
205+
});
206+
});
207+
});
208+
209+
describe("PluginPass.isAsync", () => {
210+
nodeGte8("called synchronously", () => {
211+
process.chdir("plugin-pass-is-async");
212+
213+
expect(babel.transformSync("")).toMatchObject({
214+
code: `"sync"`,
215+
});
216+
});
217+
218+
nodeGte8("called asynchronously", async () => {
219+
process.chdir("plugin-pass-is-async");
220+
221+
await expect(babel.transformAsync("")).resolves.toMatchObject({
222+
code: `"async"`,
223+
});
224+
});
225+
226+
nodeGte8("should await inherited .pre", async () => {
227+
process.chdir("plugin-pre-chaining");
228+
229+
await expect(babel.transformAsync("")).resolves.toMatchObject({
230+
code: `"pluginC,pluginB,pluginA"`,
231+
});
202232
});
203233
});
204234

@@ -350,7 +380,7 @@ describe("asynchronicity", () => {
350380
});
351381

352382
describe("misc", () => {
353-
it("unknown preset in config file does not trigget unhandledRejection if caught", async () => {
383+
it("unknown preset in config file does not trigger unhandledRejection if caught", async () => {
354384
process.chdir("unknown-preset");
355385
const handler = jest.fn();
356386

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
plugins: ["./plugin"],
3+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = function pluginA({ types: t }) {
2+
return {
3+
visitor: {
4+
Program(path) {
5+
const label = this.isAsync ? "async" : "sync";
6+
7+
path.pushContainer("body", t.stringLiteral(label));
8+
},
9+
},
10+
};
11+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
plugins: ["./plugin"],
3+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const wait = t => new Promise(r => setTimeout(r, t));
2+
3+
function pluginC({ types: t }) {
4+
return {
5+
async post() {
6+
await wait(50);
7+
this.file.ast.program.body[0].value = 'pluginC'
8+
},
9+
};
10+
}
11+
12+
function pluginB({ types: t }) {
13+
return {
14+
inherits: pluginC,
15+
post() {
16+
this.file.ast.program.body[0].value += ',pluginB';
17+
},
18+
};
19+
}
20+
21+
module.exports = function pluginA({ types: t }) {
22+
return {
23+
inherits: pluginB,
24+
async post() {
25+
await wait(50);
26+
this.file.ast.program.body[0].value += ',pluginA';
27+
},
28+
29+
visitor: {
30+
Program(path) {
31+
path.pushContainer("body", t.stringLiteral('failure'));
32+
},
33+
},
34+
};
35+
};

packages/babel-core/test/fixtures/async/plugin-post/plugin.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ module.exports = function plugin({ types: t }) {
44
return {
55
async post() {
66
await wait(50);
7+
this.file.ast.program.body[0].value = "success"
78
},
89

910
visitor: {
1011
Program(path) {
11-
path.pushContainer("body", t.stringLiteral("success"));
12+
path.pushContainer("body", t.stringLiteral("failure"));
1213
},
1314
},
1415
};

0 commit comments

Comments
 (0)