From 8193ec30618fe7d7831f58f2e483cb5aee16d3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Thu, 10 Dec 2020 00:07:09 +0100 Subject: [PATCH] Support `.mjs` plugins/presets and async factories (#12266) --- packages/babel-core/package.json | 2 +- .../babel-core/src/config/config-chain.js | 16 +- .../src/config/config-descriptors.js | 93 +++++++----- .../src/config/files/index-browser.js | 4 +- .../src/config/files/module-types.js | 14 +- .../babel-core/src/config/files/plugins.js | 30 +++- packages/babel-core/src/config/full.js | 17 ++- packages/babel-core/src/config/item.js | 7 +- packages/babel-core/src/config/printer.js | 19 ++- packages/babel-core/test/api.js | 6 + packages/babel-core/test/async.js | 137 ++++++++++++++---- packages/babel-core/test/config-chain.js | 60 +------- .../async/plugin-mjs-native/babel.config.js | 3 + .../async/plugin-mjs-native/plugin.mjs | 9 ++ .../fixtures/async/plugin-mjs/babel.config.js | 3 + .../test/fixtures/async/plugin-mjs/plugin.mjs | 16 ++ .../babel.config.js | 3 + .../plugin.mjs | 9 ++ .../preset.mjs | 1 + .../preset-mjs-named-exports/babel.config.js | 3 + .../async/preset-mjs-named-exports/plugin.mjs | 16 ++ .../async/preset-mjs-named-exports/preset.mjs | 8 + .../async/preset-mjs-native/babel.config.js | 3 + .../async/preset-mjs-native/plugin.mjs | 9 ++ .../async/preset-mjs-native/preset.mjs | 3 + .../fixtures/async/preset-mjs/babel.config.js | 3 + .../test/fixtures/async/preset-mjs/plugin.mjs | 16 ++ .../test/fixtures/async/preset-mjs/preset.mjs | 10 ++ .../preset-plugin-promise/babel.config.js | 3 + .../async/preset-plugin-promise/plugin.js | 13 ++ .../async/preset-plugin-promise/preset.js | 10 ++ .../fixtures/async/preset/babel.config.js | 3 + .../test/fixtures/async/preset/plugin.js | 13 ++ .../test/fixtures/async/preset/preset.js | 9 ++ .../test/fixtures/babel-compile-async.mjs | 11 ++ .../test/fixtures/babel-compile-sync.mjs | 9 ++ packages/babel-core/test/helpers/esm.js | 74 ++++++++++ yarn.lock | 10 +- 38 files changed, 512 insertions(+), 163 deletions(-) create mode 100644 packages/babel-core/test/fixtures/async/plugin-mjs-native/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-mjs-native/plugin.mjs create mode 100644 packages/babel-core/test/fixtures/async/plugin-mjs/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-mjs/plugin.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/plugin.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/preset.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-named-exports/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-named-exports/plugin.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-named-exports/preset.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-native/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-native/plugin.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs-native/preset.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs/plugin.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-mjs/preset.mjs create mode 100644 packages/babel-core/test/fixtures/async/preset-plugin-promise/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/preset-plugin-promise/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/preset-plugin-promise/preset.js create mode 100644 packages/babel-core/test/fixtures/async/preset/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/preset/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/preset/preset.js create mode 100755 packages/babel-core/test/fixtures/babel-compile-async.mjs create mode 100755 packages/babel-core/test/fixtures/babel-compile-sync.mjs create mode 100644 packages/babel-core/test/helpers/esm.js diff --git a/packages/babel-core/package.json b/packages/babel-core/package.json index 5d141ad1ba0d..4f74090dc16a 100644 --- a/packages/babel-core/package.json +++ b/packages/babel-core/package.json @@ -53,7 +53,7 @@ "@babel/types": "workspace:^7.12.10", "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", + "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", "lodash": "^4.17.19", "semver": "^5.4.1", diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js index 350111a1a41b..79a3d517859c 100644 --- a/packages/babel-core/src/config/config-chain.js +++ b/packages/babel-core/src/config/config-chain.js @@ -154,7 +154,7 @@ export function* buildRootChain( programmaticLogger, ); if (!programmaticChain) return null; - const programmaticReport = programmaticLogger.output(); + const programmaticReport = yield* programmaticLogger.output(); let configFile; if (typeof opts.configFile === "string") { @@ -186,7 +186,7 @@ export function* buildRootChain( configFileLogger, ); if (!result) return null; - configReport = configFileLogger.output(); + configReport = yield* configFileLogger.output(); // Allow config files to toggle `.babelrc` resolution on and off and // specify where the roots are. @@ -244,7 +244,7 @@ export function* buildRootChain( if (!result) { isIgnored = true; } else { - babelRcReport = babelrcLogger.output(); + babelRcReport = yield* babelrcLogger.output(); mergeChain(fileChain, result); } } @@ -599,7 +599,7 @@ function makeChainWalker({ } logger(config, index, envName); - mergeChainOpts(chain, config); + yield* mergeChainOpts(chain, config); } return chain; }; @@ -657,13 +657,13 @@ function mergeChain(target: ConfigChain, source: ConfigChain): ConfigChain { return target; } -function mergeChainOpts( +function* mergeChainOpts( target: ConfigChain, { options, plugins, presets }: OptionsAndDescriptors, -): ConfigChain { +): Handler { target.options.push(options); - target.plugins.push(...plugins()); - target.presets.push(...presets()); + target.plugins.push(...(yield* plugins())); + target.presets.push(...(yield* presets())); return target; } diff --git a/packages/babel-core/src/config/config-descriptors.js b/packages/babel-core/src/config/config-descriptors.js index c203dbc74c46..59f9e462c7fe 100644 --- a/packages/babel-core/src/config/config-descriptors.js +++ b/packages/babel-core/src/config/config-descriptors.js @@ -1,5 +1,7 @@ // @flow +import gensync, { type Handler } from "gensync"; + import { loadPlugin, loadPreset } from "./files"; import { getItemDescriptor } from "./item"; @@ -7,6 +9,7 @@ import { getItemDescriptor } from "./item"; import { makeWeakCacheSync, makeStrongCacheSync, + makeStrongCache, type CacheConfigurator, } from "./caching"; @@ -21,8 +24,8 @@ import type { // the options object actually ends up being applicable. export type OptionsAndDescriptors = { options: ValidatedOptions, - plugins: () => Array, - presets: () => Array, + plugins: () => Handler>, + presets: () => Handler>, }; // Represents a plugin or presets at a given location in a config object. @@ -63,6 +66,11 @@ export type ValidatedFile = { options: ValidatedOptions, }; +// eslint-disable-next-line require-yield +function* handlerOf(value: T): Handler { + return value; +} + /** * Create a set of descriptors from a given options object, preserving * descriptor identity based on the identity of the plugin/preset arrays @@ -78,13 +86,13 @@ export function createCachedDescriptors( options, plugins: plugins ? () => createCachedPluginDescriptors(plugins, dirname)(alias) - : () => [], + : () => handlerOf([]), presets: presets ? () => createCachedPresetDescriptors(presets, dirname)(alias)( !!passPerPreset, ) - : () => [], + : () => handlerOf([]), }; } @@ -105,9 +113,9 @@ export function createUncachedDescriptors( return { options, - plugins: () => { + *plugins() { if (!plugins) { - plugins = createPluginDescriptors( + plugins = yield* createPluginDescriptors( options.plugins || [], dirname, alias, @@ -115,9 +123,9 @@ export function createUncachedDescriptors( } return plugins; }, - presets: () => { + *presets() { if (!presets) { - presets = createPresetDescriptors( + presets = yield* createPresetDescriptors( options.presets || [], dirname, alias, @@ -134,14 +142,22 @@ const createCachedPresetDescriptors = makeWeakCacheSync( (items: PluginList, cache: CacheConfigurator) => { const dirname = cache.using(dir => dir); return makeStrongCacheSync((alias: string) => - makeStrongCacheSync((passPerPreset: boolean) => - createPresetDescriptors(items, dirname, alias, passPerPreset).map( + makeStrongCache(function* ( + passPerPreset: boolean, + ): Handler> { + const descriptors = yield* createPresetDescriptors( + items, + dirname, + alias, + passPerPreset, + ); + return descriptors.map( // Items are cached using the overall preset array identity when // possibly, but individual descriptors are also cached if a match // can be found in the previously-used descriptor lists. desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc), - ), - ), + ); + }), ); }, ); @@ -150,14 +166,17 @@ const PLUGIN_DESCRIPTOR_CACHE = new WeakMap(); const createCachedPluginDescriptors = makeWeakCacheSync( (items: PluginList, cache: CacheConfigurator) => { const dirname = cache.using(dir => dir); - return makeStrongCacheSync((alias: string) => - createPluginDescriptors(items, dirname, alias).map( + return makeStrongCache(function* ( + alias: string, + ): Handler> { + const descriptors = yield* createPluginDescriptors(items, dirname, alias); + return descriptors.map( // Items are cached using the overall plugin array identity when // possibly, but individual descriptors are also cached if a match // can be found in the previously-used descriptor lists. desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc), - ), - ); + ); + }); }, ); @@ -205,36 +224,44 @@ function loadCachedDescriptor( return desc; } -function createPresetDescriptors( +function* createPresetDescriptors( items: PluginList, dirname: string, alias: string, passPerPreset: boolean, -): Array { - return createDescriptors("preset", items, dirname, alias, passPerPreset); +): Handler> { + return yield* createDescriptors( + "preset", + items, + dirname, + alias, + passPerPreset, + ); } -function createPluginDescriptors( +function* createPluginDescriptors( items: PluginList, dirname: string, alias: string, -): Array { - return createDescriptors("plugin", items, dirname, alias); +): Handler> { + return yield* createDescriptors("plugin", items, dirname, alias); } -function createDescriptors( +function* createDescriptors( type: "plugin" | "preset", items: PluginList, dirname: string, alias: string, ownPass?: boolean, -): Array { - const descriptors = items.map((item, index) => - createDescriptor(item, dirname, { - type, - alias: `${alias}$${index}`, - ownPass: !!ownPass, - }), +): Handler> { + const descriptors = yield* gensync.all( + items.map((item, index) => + createDescriptor(item, dirname, { + type, + alias: `${alias}$${index}`, + ownPass: !!ownPass, + }), + ), ); assertNoDuplicates(descriptors); @@ -245,7 +272,7 @@ function createDescriptors( /** * Given a plugin/preset item, resolve it into a standard format. */ -export function createDescriptor( +export function* createDescriptor( pair: PluginItem, dirname: string, { @@ -257,7 +284,7 @@ export function createDescriptor( alias: string, ownPass?: boolean, }, -): UnloadedDescriptor { +): Handler { const desc = getItemDescriptor(pair); if (desc) { return desc; @@ -285,7 +312,7 @@ export function createDescriptor( const resolver = type === "plugin" ? loadPlugin : loadPreset; const request = value; - ({ filepath, value } = resolver(value, dirname)); + ({ filepath, value } = yield* resolver(value, dirname)); file = { request, diff --git a/packages/babel-core/src/config/files/index-browser.js b/packages/babel-core/src/config/files/index-browser.js index 2fb0b6eba1b8..cdac53a626c2 100644 --- a/packages/babel-core/src/config/files/index-browser.js +++ b/packages/babel-core/src/config/files/index-browser.js @@ -80,7 +80,7 @@ export function resolvePreset(name: string, dirname: string): string | null { export function loadPlugin( name: string, dirname: string, -): { filepath: string, value: mixed } { +): Handler<{ filepath: string, value: mixed }> { throw new Error( `Cannot load plugin ${name} relative to ${dirname} in a browser`, ); @@ -89,7 +89,7 @@ export function loadPlugin( export function loadPreset( name: string, dirname: string, -): { filepath: string, value: mixed } { +): Handler<{ filepath: string, value: mixed }> { throw new Error( `Cannot load preset ${name} relative to ${dirname} in a browser`, ); diff --git a/packages/babel-core/src/config/files/module-types.js b/packages/babel-core/src/config/files/module-types.js index 4c761597f9f1..0e6d4a431886 100644 --- a/packages/babel-core/src/config/files/module-types.js +++ b/packages/babel-core/src/config/files/module-types.js @@ -12,13 +12,15 @@ try { export default function* loadCjsOrMjsDefault( filepath: string, asyncError: string, + // TODO(Babel 8): Remove this + fallbackToTranspiledModule: boolean = false, ): Handler { switch (guessJSModuleType(filepath)) { case "cjs": - return loadCjsDefault(filepath); + return loadCjsDefault(filepath, fallbackToTranspiledModule); case "unknown": try { - return loadCjsDefault(filepath); + return loadCjsDefault(filepath, fallbackToTranspiledModule); } catch (e) { if (e.code !== "ERR_REQUIRE_ESM") throw e; } @@ -42,10 +44,12 @@ function guessJSModuleType(filename: string): "cjs" | "mjs" | "unknown" { } } -function loadCjsDefault(filepath: string) { +function loadCjsDefault(filepath: string, fallbackToTranspiledModule: boolean) { const module = (require(filepath): mixed); - // TODO (Babel 8): Remove "undefined" fallback - return module?.__esModule ? module.default || undefined : module; + return module?.__esModule + ? // TODO (Babel 8): Remove "module" and "undefined" fallback + module.default || (fallbackToTranspiledModule ? module : undefined) + : module; } async function loadMjsDefault(filepath: string) { diff --git a/packages/babel-core/src/config/files/plugins.js b/packages/babel-core/src/config/files/plugins.js index 298843e2d609..747592cad9bd 100644 --- a/packages/babel-core/src/config/files/plugins.js +++ b/packages/babel-core/src/config/files/plugins.js @@ -6,6 +6,8 @@ import buildDebug from "debug"; import path from "path"; +import { type Handler } from "gensync"; +import loadCjsOrMjsDefault from "./module-types"; const debug = buildDebug("babel:config:loading:files:plugins"); @@ -26,31 +28,31 @@ export function resolvePreset(name: string, dirname: string): string | null { return resolveStandardizedName("preset", name, dirname); } -export function loadPlugin( +export function* loadPlugin( name: string, dirname: string, -): { filepath: string, value: mixed } { +): Handler<{ filepath: string, value: mixed }> { const filepath = resolvePlugin(name, dirname); if (!filepath) { throw new Error(`Plugin ${name} not found relative to ${dirname}`); } - const value = requireModule("plugin", filepath); + const value = yield* requireModule("plugin", filepath); debug("Loaded plugin %o from %o.", name, dirname); return { filepath, value }; } -export function loadPreset( +export function* loadPreset( name: string, dirname: string, -): { filepath: string, value: mixed } { +): Handler<{ filepath: string, value: mixed }> { const filepath = resolvePreset(name, dirname); if (!filepath) { throw new Error(`Preset ${name} not found relative to ${dirname}`); } - const value = requireModule("preset", filepath); + const value = yield* requireModule("preset", filepath); debug("Loaded preset %o from %o.", name, dirname); @@ -145,7 +147,7 @@ function resolveStandardizedName( } const LOADING_MODULES = new Set(); -function requireModule(type: string, name: string): mixed { +function* requireModule(type: string, name: string): Handler { if (LOADING_MODULES.has(name)) { throw new Error( `Reentrant ${type} detected trying to load "${name}". This module is not ignored ` + @@ -156,7 +158,19 @@ function requireModule(type: string, name: string): mixed { try { LOADING_MODULES.add(name); - return require(name); + return (yield* loadCjsOrMjsDefault( + name, + `You appear to be using a native ECMAScript module ${type}, ` + + "which is only supported when running Babel asynchronously.", + // For backward compatiblity, we need to support malformed presets + // defined as separate named exports rather than a single default + // export. + // See packages/babel-core/test/fixtures/option-manager/presets/es2015_named.js + true, + ): mixed); + } catch (err) { + err.message = `[BABEL]: ${err.message} (While processing: ${name})`; + throw err; } finally { LOADING_MODULES.delete(name); } diff --git a/packages/babel-core/src/config/full.js b/packages/babel-core/src/config/full.js index 75fbe293c7c9..3914f564ec3f 100644 --- a/packages/babel-core/src/config/full.js +++ b/packages/babel-core/src/config/full.js @@ -1,7 +1,7 @@ // @flow import gensync, { type Handler } from "gensync"; -import { forwardAsync } from "../gensync-utils/async"; +import { forwardAsync, maybeAsync, isThenable } from "../gensync-utils/async"; import { mergeOptions } from "./util"; import * as context from "../index"; @@ -228,12 +228,17 @@ const loadDescriptor = makeWeakCache(function* ( let item = value; if (typeof value === "function") { + const factory = maybeAsync( + value, + `You appear to be using an async plugin/preset, but Babel has been called synchronously`, + ); + const api = { ...context, ...makeAPI(cache), }; try { - item = value(api, options, dirname); + item = yield* factory(api, options, dirname); } catch (e) { if (alias) { e.message += ` (While processing: ${JSON.stringify(alias)})`; @@ -246,14 +251,16 @@ const loadDescriptor = makeWeakCache(function* ( throw new Error("Plugin/Preset did not return an object."); } - if (typeof item.then === "function") { + if (isThenable(item)) { yield* []; // if we want to support async plugins throw new Error( - `You appear to be using an async plugin, ` + + `You appear to be using a promise as a plugin, ` + `which your current version of Babel does not support. ` + `If you're using a published plugin, ` + - `you may need to upgrade your @babel/core version.`, + `you may need to upgrade your @babel/core version. ` + + `As an alternative, you can prefix the promise with "await". ` + + `(While processing: ${JSON.stringify(alias)})`, ); } diff --git a/packages/babel-core/src/config/item.js b/packages/babel-core/src/config/item.js index e344732454f5..cf04f1bc40d3 100644 --- a/packages/babel-core/src/config/item.js +++ b/packages/babel-core/src/config/item.js @@ -2,6 +2,7 @@ /*:: declare var invariant; */ +import type { Handler } from "gensync"; import type { PluginTarget, PluginOptions } from "./validation/options"; import path from "path"; @@ -20,7 +21,7 @@ export function createItemFromDescriptor(desc: UnloadedDescriptor): ConfigItem { * ideally, as recreating the config item will mean re-resolving the item * and re-evaluating the plugin/preset function. */ -export function createConfigItem( +export function* createConfigItem( value: | PluginTarget | [PluginTarget, PluginOptions] @@ -32,8 +33,8 @@ export function createConfigItem( dirname?: string, type?: "preset" | "plugin", } = {}, -): ConfigItem { - const descriptor = createDescriptor(value, path.resolve(dirname), { +): Handler { + const descriptor = yield* createDescriptor(value, path.resolve(dirname), { type, alias: "programmatic item", }); diff --git a/packages/babel-core/src/config/printer.js b/packages/babel-core/src/config/printer.js index 9f9d65787694..b1f1072eddda 100644 --- a/packages/babel-core/src/config/printer.js +++ b/packages/babel-core/src/config/printer.js @@ -1,5 +1,7 @@ // @flow +import gensync, { type Handler } from "gensync"; + import type { OptionsAndDescriptors, UnloadedDescriptor, @@ -49,17 +51,17 @@ const Formatter = { return loc; }, - optionsAndDescriptors(opt: OptionsAndDescriptors) { + *optionsAndDescriptors(opt: OptionsAndDescriptors) { const content = { ...opt.options }; // overrides and env will be printed as separated config items delete content.overrides; delete content.env; // resolve to descriptors - const pluginDescriptors = [...opt.plugins()]; + const pluginDescriptors = [...(yield* opt.plugins())]; if (pluginDescriptors.length) { content.plugins = pluginDescriptors.map(d => descriptorToConfig(d)); } - const presetDescriptors = [...opt.presets()]; + const presetDescriptors = [...(yield* opt.presets())]; if (presetDescriptors.length) { content.presets = [...presetDescriptors].map(d => descriptorToConfig(d)); } @@ -114,7 +116,7 @@ export class ConfigPrinter { }); }; } - static format(config: PrintableConfig): string { + static *format(config: PrintableConfig): Handler { let title = Formatter.title( config.type, config.callerName, @@ -122,12 +124,15 @@ export class ConfigPrinter { ); const loc = Formatter.loc(config.index, config.envName); if (loc) title += ` ${loc}`; - const content = Formatter.optionsAndDescriptors(config.content); + const content = yield* Formatter.optionsAndDescriptors(config.content); return `${title}\n${content}`; } - output(): string { + *output(): Handler { if (this._stack.length === 0) return ""; - return this._stack.map(s => ConfigPrinter.format(s)).join("\n\n"); + const configs = yield* gensync.all( + this._stack.map(s => ConfigPrinter.format(s)), + ); + return configs.join("\n\n"); } } diff --git a/packages/babel-core/test/api.js b/packages/babel-core/test/api.js index 9e04d85a623a..d0b7a9ce49ac 100644 --- a/packages/babel-core/test/api.js +++ b/packages/babel-core/test/api.js @@ -15,6 +15,7 @@ function assertNotIgnored(result) { function parse(code, opts) { return babel.parse(code, { cwd: __dirname, + configFile: false, ...opts, }); } @@ -22,6 +23,7 @@ function parse(code, opts) { function transform(code, opts) { return babel.transform(code, { cwd: __dirname, + configFile: false, ...opts, }); } @@ -31,6 +33,7 @@ function transformFile(filename, opts, cb) { filename, { cwd: __dirname, + configFile: false, ...opts, }, cb, @@ -39,6 +42,7 @@ function transformFile(filename, opts, cb) { function transformFileSync(filename, opts) { return babel.transformFileSync(filename, { cwd: __dirname, + configFile: false, ...opts, }); } @@ -46,6 +50,7 @@ function transformFileSync(filename, opts) { function transformAsync(code, opts) { return babel.transformAsync(code, { cwd: __dirname, + configFile: false, ...opts, }); } @@ -53,6 +58,7 @@ function transformAsync(code, opts) { function transformFromAst(ast, code, opts) { return babel.transformFromAst(ast, code, { cwd: __dirname, + configFile: false, ...opts, }); } diff --git a/packages/babel-core/test/async.js b/packages/babel-core/test/async.js index 0e2bbdce8c02..2dfafe1617f2 100644 --- a/packages/babel-core/test/async.js +++ b/packages/babel-core/test/async.js @@ -1,6 +1,12 @@ -import path from "path"; +import { join } from "path"; import * as babel from ".."; +import { + spawnTransformAsync, + spawnTransformSync, + supportsESM, +} from "./helpers/esm"; + const nodeGte8 = (...args) => { // "minNodeVersion": "8.0.0" <-- For Ctrl+F when dropping node 6 const testFn = process.version.slice(0, 3) === "v6." ? it.skip : it; @@ -8,7 +14,7 @@ const nodeGte8 = (...args) => { }; describe("asynchronicity", () => { - const base = path.join(__dirname, "fixtures", "async"); + const base = join(__dirname, "fixtures", "async"); let cwd; beforeEach(function () { @@ -111,25 +117,18 @@ describe("asynchronicity", () => { nodeGte8("called synchronously", () => { process.chdir("plugin"); - expect(() => - babel.transformSync(""), - ).toThrowErrorMatchingInlineSnapshot( - `"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` + - ` does not support. If you're using a published plugin, you may need to upgrade your` + - ` @babel/core version."`, + expect(() => babel.transformSync("")).toThrow( + `[BABEL] unknown: You appear to be using an async plugin/preset, but Babel` + + ` has been called synchronously`, ); }); nodeGte8("called asynchronously", async () => { process.chdir("plugin"); - await expect( - babel.transformAsync(""), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` + - ` does not support. If you're using a published plugin, you may need to upgrade your` + - ` @babel/core version."`, - ); + await expect(babel.transformAsync("")).resolves.toMatchObject({ + code: `"success"`, + }); }); }); @@ -189,24 +188,108 @@ describe("asynchronicity", () => { nodeGte8("called synchronously", () => { process.chdir("plugin-inherits"); - expect(() => - babel.transformSync(""), - ).toThrowErrorMatchingInlineSnapshot( - `"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` + - ` does not support. If you're using a published plugin, you may need to upgrade your` + - ` @babel/core version."`, + expect(() => babel.transformSync("")).toThrow( + `[BABEL] unknown: You appear to be using an async plugin/preset, but Babel has been` + + ` called synchronously`, ); }); nodeGte8("called asynchronously", async () => { process.chdir("plugin-inherits"); - await expect( - babel.transformAsync(""), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` + - ` does not support. If you're using a published plugin, you may need to upgrade your` + - ` @babel/core version."`, + await expect(babel.transformAsync("")).resolves.toMatchObject({ + code: `"success 2"\n"success"`, + }); + }); + }); + + (supportsESM ? describe : describe.skip)(".mjs files", () => { + it("called synchronously", async () => { + process.chdir("plugin-mjs-native"); + + await expect(spawnTransformSync()).rejects.toThrow( + `[BABEL]: You appear to be using a native ECMAScript module plugin, which is` + + ` only supported when running Babel asynchronously.`, + ); + }); + + it("called asynchronously", async () => { + process.chdir("plugin-mjs-native"); + + await expect(spawnTransformAsync()).resolves.toMatchObject({ + code: `"success"`, + }); + }); + }); + }); + + describe("preset", () => { + describe("factory function", () => { + nodeGte8("called synchronously", () => { + process.chdir("preset"); + + expect(() => babel.transformSync("")).toThrow( + `[BABEL] unknown: You appear to be using an async plugin/preset, ` + + `but Babel has been called synchronously`, + ); + }); + + nodeGte8("called asynchronously", async () => { + process.chdir("preset"); + + await expect(babel.transformAsync("")).resolves.toMatchObject({ + code: `"success"`, + }); + }); + }); + + describe("plugins", () => { + nodeGte8("called synchronously", () => { + process.chdir("preset-plugin-promise"); + + expect(() => babel.transformSync("")).toThrow( + `[BABEL] unknown: You appear to be using a promise as a plugin, which your` + + ` current version of Babel does not support. If you're using a published` + + ` plugin, you may need to upgrade your @babel/core version. As an` + + ` alternative, you can prefix the promise with "await".`, + ); + }); + + nodeGte8("called asynchronously", async () => { + process.chdir("preset-plugin-promise"); + + await expect(babel.transformAsync("")).rejects.toThrow( + `[BABEL] unknown: You appear to be using a promise as a plugin, which your` + + ` current version of Babel does not support. If you're using a published` + + ` plugin, you may need to upgrade your @babel/core version. As an` + + ` alternative, you can prefix the promise with "await".`, + ); + }); + }); + + (supportsESM ? describe : describe.skip)(".mjs files", () => { + it("called synchronously", async () => { + process.chdir("preset-mjs-native"); + + await expect(spawnTransformSync()).rejects.toThrow( + `[BABEL]: You appear to be using a native ECMAScript module preset, which is` + + ` only supported when running Babel asynchronously.`, + ); + }); + + it("called asynchronously", async () => { + process.chdir("preset-mjs-native"); + + await expect(spawnTransformAsync()).resolves.toMatchObject({ + code: `"success"`, + }); + }); + + it("must use the 'default' export", async () => { + process.chdir("preset-mjs-named-exports-native"); + + await expect(spawnTransformAsync()).rejects.toThrow( + `Unexpected falsy value: undefined`, ); }); }); diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index 8de2bc99d086..07b0eae64b90 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -1,32 +1,10 @@ -import cp from "child_process"; import fs from "fs"; import os from "os"; import path from "path"; -import util from "util"; import escapeRegExp from "lodash/escapeRegExp"; import * as babel from "../lib"; -// "minNodeVersion": "10.0.0" <-- For Ctrl+F when dropping node 10 -const supportsESM = parseInt(process.versions.node) >= 12; - -const isMJS = file => path.extname(file) === ".mjs"; - -const skipUnsupportedESM = (esm, name) => { - if (esm && !supportsESM) { - console.warn( - `Skipping "${name}" because native ECMAScript modules are not supported.`, - ); - return true; - } - // This can be removed when loadOptionsAsyncInSpawedProcess is removed. - if (esm && process.platform === "win32") { - console.warn( - `Skipping "${name}" because the ESM runner cannot be spawned on Windows.`, - ); - return true; - } - return false; -}; +import { isMJS, loadOptionsAsync, skipUnsupportedESM } from "./helpers/esm"; // TODO: In Babel 8, we can directly uses fs.promises which is supported by // node 8+ @@ -71,42 +49,6 @@ function loadOptions(opts) { return babel.loadOptions({ cwd: __dirname, ...opts }); } -function loadOptionsAsync({ filename, cwd = __dirname }, mjs) { - if (mjs) { - // import() crashes with jest - return loadOptionsAsyncInSpawedProcess({ filename, cwd }); - } - - return babel.loadOptionsAsync({ filename, cwd }); -} - -// !!!! hack is coming !!!! -// Remove this function when https://github.com/nodejs/node/issues/35889 is resolved. -// Jest supports dynamic import(), but Node.js segfaults when using it in our tests. -async function loadOptionsAsyncInSpawedProcess({ filename, cwd }) { - const { stdout, stderr } = await util.promisify(cp.execFile)( - require.resolve("./fixtures/babel-load-options-async.mjs"), - // pass `cwd` as params as `process.cwd()` will normalize `cwd` on macOS - [filename, cwd], - { - cwd, - env: process.env, - }, - ); - - const EXPERIMENTAL_WARNING = /\(node:\d+\) ExperimentalWarning: The ESM module loader is experimental\./; - - if (stderr.replace(EXPERIMENTAL_WARNING, "").trim()) { - throw new Error( - "error is thrown in babel-load-options-async.mjs: stdout\n" + - stdout + - "\nstderr:\n" + - stderr, - ); - } - return JSON.parse(stdout); -} - function pairs(items) { const pairs = []; for (let i = 0; i < items.length - 1; i++) { diff --git a/packages/babel-core/test/fixtures/async/plugin-mjs-native/babel.config.js b/packages/babel-core/test/fixtures/async/plugin-mjs-native/babel.config.js new file mode 100644 index 000000000000..e5e5251080c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-mjs-native/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["./plugin.mjs"], +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-mjs-native/plugin.mjs b/packages/babel-core/test/fixtures/async/plugin-mjs-native/plugin.mjs new file mode 100644 index 000000000000..10195347cc37 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-mjs-native/plugin.mjs @@ -0,0 +1,9 @@ +export default function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +} diff --git a/packages/babel-core/test/fixtures/async/plugin-mjs/babel.config.js b/packages/babel-core/test/fixtures/async/plugin-mjs/babel.config.js new file mode 100644 index 000000000000..e5e5251080c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-mjs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["./plugin.mjs"], +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-mjs/plugin.mjs b/packages/babel-core/test/fixtures/async/plugin-mjs/plugin.mjs new file mode 100644 index 000000000000..e26f86b27ed4 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-mjs/plugin.mjs @@ -0,0 +1,16 @@ +// Until Jest supports native mjs, we must simulate it 🤷 +module.exports = new Promise(resolve => + resolve({ + default: function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; + }, + }) +); + +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/babel.config.js b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/babel.config.js new file mode 100644 index 000000000000..67c90fbdd7c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["./preset.mjs"], +}; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/plugin.mjs b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/plugin.mjs new file mode 100644 index 000000000000..10195347cc37 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/plugin.mjs @@ -0,0 +1,9 @@ +export default function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +} diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/preset.mjs b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/preset.mjs new file mode 100644 index 000000000000..66fe02a6b22b --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports-native/preset.mjs @@ -0,0 +1 @@ +export const plugins = ["./plugin.mjs"]; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/babel.config.js b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/babel.config.js new file mode 100644 index 000000000000..67c90fbdd7c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["./preset.mjs"], +}; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/plugin.mjs b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/plugin.mjs new file mode 100644 index 000000000000..e26f86b27ed4 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/plugin.mjs @@ -0,0 +1,16 @@ +// Until Jest supports native mjs, we must simulate it 🤷 +module.exports = new Promise(resolve => + resolve({ + default: function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; + }, + }) +); + +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/preset.mjs b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/preset.mjs new file mode 100644 index 000000000000..52a1b915e17d --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-named-exports/preset.mjs @@ -0,0 +1,8 @@ +// Until Jest supports native mjs, we must simulate it 🤷 +module.exports = new Promise(resolve => + resolve({ + plugins: ["./plugin.mjs"] + }) +); + +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-native/babel.config.js b/packages/babel-core/test/fixtures/async/preset-mjs-native/babel.config.js new file mode 100644 index 000000000000..67c90fbdd7c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-native/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["./preset.mjs"], +}; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-native/plugin.mjs b/packages/babel-core/test/fixtures/async/preset-mjs-native/plugin.mjs new file mode 100644 index 000000000000..10195347cc37 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-native/plugin.mjs @@ -0,0 +1,9 @@ +export default function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +} diff --git a/packages/babel-core/test/fixtures/async/preset-mjs-native/preset.mjs b/packages/babel-core/test/fixtures/async/preset-mjs-native/preset.mjs new file mode 100644 index 000000000000..25474e1664cc --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs-native/preset.mjs @@ -0,0 +1,3 @@ +export default () => ({ + plugins: ["./plugin.mjs"] +}); diff --git a/packages/babel-core/test/fixtures/async/preset-mjs/babel.config.js b/packages/babel-core/test/fixtures/async/preset-mjs/babel.config.js new file mode 100644 index 000000000000..67c90fbdd7c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["./preset.mjs"], +}; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs/plugin.mjs b/packages/babel-core/test/fixtures/async/preset-mjs/plugin.mjs new file mode 100644 index 000000000000..e26f86b27ed4 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs/plugin.mjs @@ -0,0 +1,16 @@ +// Until Jest supports native mjs, we must simulate it 🤷 +module.exports = new Promise(resolve => + resolve({ + default: function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; + }, + }) +); + +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/async/preset-mjs/preset.mjs b/packages/babel-core/test/fixtures/async/preset-mjs/preset.mjs new file mode 100644 index 000000000000..8779954e9ee4 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-mjs/preset.mjs @@ -0,0 +1,10 @@ +// Until Jest supports native mjs, we must simulate it 🤷 +module.exports = new Promise(resolve => + resolve({ + default: () => ({ + plugins: ["./plugin.mjs"] + }) + }) +); + +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/async/preset-plugin-promise/babel.config.js b/packages/babel-core/test/fixtures/async/preset-plugin-promise/babel.config.js new file mode 100644 index 000000000000..e7104f746f81 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-plugin-promise/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["./preset"], +}; diff --git a/packages/babel-core/test/fixtures/async/preset-plugin-promise/plugin.js b/packages/babel-core/test/fixtures/async/preset-plugin-promise/plugin.js new file mode 100644 index 000000000000..9777dd80ae42 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-plugin-promise/plugin.js @@ -0,0 +1,13 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = async function plugin({ types: t }) { + await wait(50); + + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/preset-plugin-promise/preset.js b/packages/babel-core/test/fixtures/async/preset-plugin-promise/preset.js new file mode 100644 index 000000000000..aceef8a4c242 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset-plugin-promise/preset.js @@ -0,0 +1,10 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +// "Dynamic import" +const import_ = path => Promise.resolve(require(path)); + +module.exports = function preset(api) { + return { + plugins: [import_("./plugin")], + }; +}; diff --git a/packages/babel-core/test/fixtures/async/preset/babel.config.js b/packages/babel-core/test/fixtures/async/preset/babel.config.js new file mode 100644 index 000000000000..e7104f746f81 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ["./preset"], +}; diff --git a/packages/babel-core/test/fixtures/async/preset/plugin.js b/packages/babel-core/test/fixtures/async/preset/plugin.js new file mode 100644 index 000000000000..9777dd80ae42 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset/plugin.js @@ -0,0 +1,13 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = async function plugin({ types: t }) { + await wait(50); + + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/preset/preset.js b/packages/babel-core/test/fixtures/async/preset/preset.js new file mode 100644 index 000000000000..a48e87783ef6 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/preset/preset.js @@ -0,0 +1,9 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = async function preset(api) { + await wait(50); + + return { + plugins: [require("./plugin")], + }; +}; diff --git a/packages/babel-core/test/fixtures/babel-compile-async.mjs b/packages/babel-core/test/fixtures/babel-compile-async.mjs new file mode 100755 index 000000000000..4f67829a74d0 --- /dev/null +++ b/packages/babel-core/test/fixtures/babel-compile-async.mjs @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +// Usage: +// babel-compile-async.js [filename] +import babel from "../../lib/index.js"; + +(async () => { + process.stdout.write( + JSON.stringify(await babel.transformAsync("")) + ); +})(); diff --git a/packages/babel-core/test/fixtures/babel-compile-sync.mjs b/packages/babel-core/test/fixtures/babel-compile-sync.mjs new file mode 100755 index 000000000000..dd4152602f39 --- /dev/null +++ b/packages/babel-core/test/fixtures/babel-compile-sync.mjs @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +// Usage: +// babel-compile-async.js [filename] +import babel from "../../lib/index.js"; + +process.stdout.write( + JSON.stringify(babel.transformSync("")) +); diff --git a/packages/babel-core/test/helpers/esm.js b/packages/babel-core/test/helpers/esm.js new file mode 100644 index 000000000000..23f70eb097ca --- /dev/null +++ b/packages/babel-core/test/helpers/esm.js @@ -0,0 +1,74 @@ +import cp from "child_process"; +import util from "util"; +import path from "path"; +import * as babel from "../../lib"; + +// "minNodeVersion": "10.0.0" <-- For Ctrl+F when dropping node 10 +const nodeSupportsESM = parseInt(process.versions.node) >= 12; +const isWindows = process.platform === "win32"; + +export const supportsESM = nodeSupportsESM && !isWindows; + +export const isMJS = file => path.extname(file) === ".mjs"; + +export const itESM = supportsESM ? it : it.skip; + +export function skipUnsupportedESM(esm, name) { + if (esm && !nodeSupportsESM) { + console.warn( + `Skipping "${name}" because native ECMAScript modules are not supported.`, + ); + return true; + } + // This can be removed when loadOptionsAsyncInSpawedProcess is removed. + if (esm && isWindows) { + console.warn( + `Skipping "${name}" because the ESM runner cannot be spawned on Windows.`, + ); + return true; + } + return false; +} + +export function loadOptionsAsync({ filename, cwd = __dirname }, mjs) { + if (mjs) { + // import() crashes with jest + return spawn("load-options-async", filename, cwd); + } + + return babel.loadOptionsAsync({ filename, cwd }); +} + +export function spawnTransformAsync() { + // import() crashes with jest + return spawn("compile-async"); +} + +export function spawnTransformSync() { + // import() crashes with jest + return spawn("compile-sync"); +} + +// !!!! hack is coming !!!! +// Remove this function when https://github.com/nodejs/node/issues/35889 is resolved. +// Jest supports dynamic import(), but Node.js segfaults when using it in our tests. +async function spawn(runner, filename, cwd = process.cwd()) { + const { stdout, stderr } = await util.promisify(cp.execFile)( + require.resolve(`../fixtures/babel-${runner}.mjs`), + // pass `cwd` as params as `process.cwd()` will normalize `cwd` on macOS + [filename, cwd], + { cwd, env: process.env }, + ); + + const EXPERIMENTAL_WARNING = /\(node:\d+\) ExperimentalWarning: The ESM module loader is experimental\./; + + if (stderr.replace(EXPERIMENTAL_WARNING, "").trim()) { + throw new Error( + `error is thrown in babel-${runner}.mjs: stdout\n` + + stdout + + "\nstderr:\n" + + stderr, + ); + } + return JSON.parse(stdout); +} diff --git a/yarn.lock b/yarn.lock index 6903cf4da5ce..a566ecc10a22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -138,7 +138,7 @@ __metadata: "@babel/types": "workspace:^7.12.10" convert-source-map: ^1.7.0 debug: ^4.1.0 - gensync: ^1.0.0-beta.1 + gensync: ^1.0.0-beta.2 json5: ^2.1.2 lodash: ^4.17.19 semver: ^5.4.1 @@ -7451,10 +7451,10 @@ fsevents@^1.2.7: languageName: node linkType: hard -"gensync@npm:^1.0.0-beta.1": - version: 1.0.0-beta.1 - resolution: "gensync@npm:1.0.0-beta.1" - checksum: 3d14f7c34fc903dd52c36d0879de2c4afde8315edccd630e97919c365819b32c06d98770ef87f7ba45686ee5d2bd5818354920187659b42828319f7cc3352fdb +"gensync@npm:^1.0.0-beta.1, gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: d523437689c97b3aba9c5cdeca4677d5fff9a29d620db693fea40d852bad63563110f16979d0170248439dbcd2ecee0780fb2533d3f0519f019081aa10767c60 languageName: node linkType: hard