From 108fb56caf2f7065ee8b0976dc67b86267c8c55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Wed, 18 Dec 2019 15:51:14 +0100 Subject: [PATCH] Add support for babel.config.mjs and .babelrc.mjs --- babel.config.js | 4 + .../src/config/files/configuration.js | 26 ++-- .../babel-core/src/config/files/import.js | 7 + .../src/config/files/module-types.js | 57 ++++++++ packages/babel-core/test/config-chain.js | 138 ++++++++++++++---- .../config-files-templates/.babelrc.mjs | 8 + .../config-files-templates/babel.config.mjs | 8 + .../babelrc-mjs-error/.babelrc.mjs | 8 + 8 files changed, 213 insertions(+), 43 deletions(-) create mode 100644 packages/babel-core/src/config/files/import.js create mode 100644 packages/babel-core/src/config/files/module-types.js create mode 100644 packages/babel-core/test/fixtures/config/config-files-templates/.babelrc.mjs create mode 100644 packages/babel-core/test/fixtures/config/config-files-templates/babel.config.mjs create mode 100644 packages/babel-core/test/fixtures/config/config-files/babelrc-mjs-error/.babelrc.mjs diff --git a/babel.config.js b/babel.config.js index ecdb2f6a92bc..8d2d1b3be87c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -107,6 +107,10 @@ module.exports = function(api) { ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: true }], convertESM ? "@babel/transform-modules-commonjs" : null, + // Until Jest supports native mjs, we must simulate it 🤷 + env === "test" || env === "development" + ? "@babel/plugin-proposal-dynamic-import" + : null, ].filter(Boolean), overrides: [ { diff --git a/packages/babel-core/src/config/files/configuration.js b/packages/babel-core/src/config/files/configuration.js index 035da8eb3681..d9eed80d2960 100644 --- a/packages/babel-core/src/config/files/configuration.js +++ b/packages/babel-core/src/config/files/configuration.js @@ -11,6 +11,7 @@ import { } from "../caching"; import makeAPI, { type PluginAPI } from "../helpers/config-api"; import { makeStaticFileCache } from "./utils"; +import loadCjsOrMjsDefault from "./module-types"; import pathPatternToRegex from "../pattern-to-regex"; import type { FilePackageData, RelativeConfig, ConfigFile } from "./types"; import type { CallerMetadata } from "../validation/options"; @@ -23,9 +24,15 @@ const debug = buildDebug("babel:config:loading:files:configuration"); export const ROOT_CONFIG_FILENAMES = [ "babel.config.js", "babel.config.cjs", + "babel.config.mjs", "babel.config.json", ]; -const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs"]; +const RELATIVE_CONFIG_FILENAMES = [ + ".babelrc", + ".babelrc.js", + ".babelrc.cjs", + ".babelrc.mjs", +]; const BABELIGNORE_FILENAME = ".babelignore"; @@ -144,7 +151,7 @@ export function* loadConfig( */ function readConfig(filepath, envName, caller) { const ext = path.extname(filepath); - return ext === ".js" || ext === ".cjs" + return ext === ".js" || ext === ".cjs" || ext === ".mjs" ? readConfigJS(filepath, { envName, caller }) : readConfigJSON5(filepath); } @@ -177,17 +184,14 @@ const readConfigJS = makeStrongCache(function* readConfigJS( }; } - let options; + let options: mixed; try { LOADING_CONFIGS.add(filepath); - - yield* []; // If we want to allow mjs configs imported using `import()` - // $FlowIssue - const configModule = (require(filepath): mixed); - options = - configModule && configModule.__esModule - ? configModule.default || undefined - : configModule; + options = (yield* loadCjsOrMjsDefault( + filepath, + "You appear to be using a native ECMAScript module configuration " + + "file, which is only supported when running Babel asynchronously.", + ): mixed); } catch (err) { err.message = `${filepath}: Error while loading config - ${err.message}`; throw err; diff --git a/packages/babel-core/src/config/files/import.js b/packages/babel-core/src/config/files/import.js new file mode 100644 index 000000000000..e16bc29752a9 --- /dev/null +++ b/packages/babel-core/src/config/files/import.js @@ -0,0 +1,7 @@ +// We keep this in a seprate file so that in older node versions, where +// import() isn't supported, we can try/catch around the require() call +// when loading this file. + +export default function import_(filepath: string) { + return import(filepath); +} diff --git a/packages/babel-core/src/config/files/module-types.js b/packages/babel-core/src/config/files/module-types.js new file mode 100644 index 000000000000..c915a92e1db3 --- /dev/null +++ b/packages/babel-core/src/config/files/module-types.js @@ -0,0 +1,57 @@ +import { isAsync, waitFor } from "../../gensync-utils/async"; +import type { Handler } from "gensync"; + +let import_; +try { + // Node < 13.3 doesn't support import() syntax. + import_ = require("./import").default; +} catch {} + +export default function* loadCjsOrMjsDefault( + filepath: string, + asyncError: string, +): Handler { + switch (guessJSModuleType(filepath)) { + case "cjs": + return loadCjsDefault(filepath); + case "unknown": + try { + return loadCjsDefault(filepath); + } catch (e) { + if (e.code !== "ERR_REQUIRE_ESM") throw e; + } + case "mjs": + if (yield* isAsync()) { + return yield* waitFor(loadMjsDefault(filepath)); + } + throw new Error(asyncError); + } +} + +function guessJSModuleType(path: string): "cjs" | "mjs" | "unknown" { + switch (path.slice(-4)) { + case ".cjs": + return "cjs"; + case ".mjs": + return "mjs"; + default: + return "unknown"; + } +} + +function loadCjsDefault(filepath: string) { + const module = (require(filepath): mixed); + return module?.__esModule ? module.default || undefined : module; +} + +async function loadMjsDefault(filepath: string) { + if (!import_) { + throw new Error( + "Internal error: Native ECMAScript modules aren't supported" + + " by this platform.\n", + ); + } + + const module = await import_(filepath); + return module.default; +} diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index 078cd91dce97..ecaf066f1656 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -2,7 +2,7 @@ import fs from "fs"; import os from "os"; import path from "path"; import escapeRegExp from "lodash/escapeRegExp"; -import { loadOptions as loadOptionsOrig } from "../lib"; +import * as babel from "../lib"; // TODO: In Babel 8, we can directly uses fs.promises which is supported by // node 8+ @@ -44,10 +44,11 @@ function fixture(...args) { } function loadOptions(opts) { - return loadOptionsOrig({ - cwd: __dirname, - ...opts, - }); + return babel.loadOptions({ cwd: __dirname, ...opts }); +} + +function loadOptionsAsync(opts) { + return babel.loadOptionsAsync({ cwd: __dirname, ...opts }); } function pairs(items) { @@ -1000,21 +1001,16 @@ describe("buildConfigChain", function() { describe("root", () => { test.each(["babel.config.json", "babel.config.js", "babel.config.cjs"])( - "should load %s", + "should load %s synchronously", async name => { const { cwd, tmp, config } = await getTemp( - `babel-test-load-config-${name}`, + `babel-test-load-config-sync-${name}`, ); const filename = tmp("src.js"); await config(name); - expect( - loadOptions({ - filename, - cwd, - }), - ).toEqual({ + expect(loadOptions({ filename, cwd })).toEqual({ ...getDefaults(), filename, cwd, @@ -1024,8 +1020,48 @@ describe("buildConfigChain", function() { }, ); + test("should not load babel.config.mjs synchronously", async () => { + const { cwd, tmp, config } = await getTemp( + "babel-test-load-config-sync-babel.config.mjs", + ); + const filename = tmp("src.js"); + + await config("babel.config.mjs"); + + expect(() => loadOptions({ filename, cwd })).toThrow( + /is only supported when running Babel asynchronously/, + ); + }); + + test.each([ + "babel.config.json", + "babel.config.js", + "babel.config.cjs", + "babel.config.mjs", + ])("should load %s asynchronously", async name => { + const { cwd, tmp, config } = await getTemp( + `babel-test-load-config-async-${name}`, + ); + const filename = tmp("src.js"); + + await config(name); + + expect(await loadOptionsAsync({ filename, cwd })).toEqual({ + ...getDefaults(), + filename, + cwd, + root: cwd, + comments: true, + }); + }); + test.each( - pairs(["babel.config.json", "babel.config.js", "babel.config.cjs"]), + pairs([ + "babel.config.json", + "babel.config.js", + "babel.config.cjs", + "babel.config.mjs", + ]), )("should throw if both %s and %s are used", async (name1, name2) => { const { cwd, tmp, config } = await getTemp( `babel-test-dup-config-${name1}-${name2}`, @@ -1033,15 +1069,15 @@ describe("buildConfigChain", function() { await Promise.all([config(name1), config(name2)]); - expect(() => loadOptions({ filename: tmp("src.js"), cwd })).toThrow( - /Multiple configuration files found/, - ); + await expect( + loadOptionsAsync({ filename: tmp("src.js"), cwd }), + ).rejects.toThrow(/Multiple configuration files found/); }); }); describe("relative", () => { test.each(["package.json", ".babelrc", ".babelrc.js", ".babelrc.cjs"])( - "should load %s", + "should load %s synchronously", async name => { const { cwd, tmp, config } = await getTemp( `babel-test-load-config-${name}`, @@ -1050,12 +1086,7 @@ describe("buildConfigChain", function() { await config(name); - expect( - loadOptions({ - filename, - cwd, - }), - ).toEqual({ + expect(loadOptions({ filename, cwd })).toEqual({ ...getDefaults(), filename, cwd, @@ -1065,6 +1096,42 @@ describe("buildConfigChain", function() { }, ); + test("should not load .babelrc.mjs synchronously", async () => { + const { cwd, tmp, config } = await getTemp( + "babel-test-load-config-sync-.babelrc.mjs", + ); + const filename = tmp("src.js"); + + await config(".babelrc.mjs"); + + expect(() => loadOptions({ filename, cwd })).toThrow( + /is only supported when running Babel asynchronously/, + ); + }); + + test.each([ + "package.json", + ".babelrc", + ".babelrc.js", + ".babelrc.cjs", + ".babelrc.mjs", + ])("should load %s asynchronously", async name => { + const { cwd, tmp, config } = await getTemp( + `babel-test-load-config-${name}`, + ); + const filename = tmp("src.js"); + + await config(name); + + expect(await loadOptionsAsync({ filename, cwd })).toEqual({ + ...getDefaults(), + filename, + cwd, + root: cwd, + comments: true, + }); + }); + it("should load .babelignore", () => { const filename = fixture("config-files", "babelignore", "src.js"); @@ -1074,7 +1141,13 @@ describe("buildConfigChain", function() { }); test.each( - pairs(["package.json", ".babelrc", ".babelrc.js", ".babelrc.cjs"]), + pairs([ + "package.json", + ".babelrc", + ".babelrc.js", + ".babelrc.cjs", + ".babelrc.mjs", + ]), )("should throw if both %s and %s are used", async (name1, name2) => { const { cwd, tmp, config } = await getTemp( `babel-test-dup-config-${name1}-${name2}`, @@ -1082,9 +1155,9 @@ describe("buildConfigChain", function() { await Promise.all([config(name1), config(name2)]); - expect(() => loadOptions({ filename: tmp("src.js"), cwd })).toThrow( - /Multiple configuration files found/, - ); + await expect( + loadOptionsAsync({ filename: tmp("src.js"), cwd }), + ).rejects.toThrow(/Multiple configuration files found/); }); it("should ignore package.json without a 'babel' property", () => { @@ -1104,13 +1177,14 @@ describe("buildConfigChain", function() { ${".babelrc"} | ${"babelrc-error"} | ${/Error while parsing config - /} ${".babelrc.js"} | ${"babelrc-js-error"} | ${/Babelrc threw an error/} ${".babelrc.cjs"} | ${"babelrc-cjs-error"} | ${/Babelrc threw an error/} + ${".babelrc.mjs"} | ${"babelrc-mjs-error"} | ${/Babelrc threw an error/} ${"package.json"} | ${"pkg-error"} | ${/Error while parsing JSON - /} - `("should show helpful errors for $config", ({ dir, error }) => { + `("should show helpful errors for $config", async ({ dir, error }) => { const filename = fixture("config-files", dir, "src.js"); - expect(() => - loadOptions({ filename, cwd: path.dirname(filename) }), - ).toThrow(error); + await expect( + loadOptionsAsync({ filename, cwd: path.dirname(filename) }), + ).rejects.toThrow(error); }); }); diff --git a/packages/babel-core/test/fixtures/config/config-files-templates/.babelrc.mjs b/packages/babel-core/test/fixtures/config/config-files-templates/.babelrc.mjs new file mode 100644 index 000000000000..7fc8bbfea752 --- /dev/null +++ b/packages/babel-core/test/fixtures/config/config-files-templates/.babelrc.mjs @@ -0,0 +1,8 @@ +// Until Jest supports native mjs, we must simulate it 🤷 + +module.exports = new Promise(resolve => resolve({ + default: { + comments: true + } +})); +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/config/config-files-templates/babel.config.mjs b/packages/babel-core/test/fixtures/config/config-files-templates/babel.config.mjs new file mode 100644 index 000000000000..7fc8bbfea752 --- /dev/null +++ b/packages/babel-core/test/fixtures/config/config-files-templates/babel.config.mjs @@ -0,0 +1,8 @@ +// Until Jest supports native mjs, we must simulate it 🤷 + +module.exports = new Promise(resolve => resolve({ + default: { + comments: true + } +})); +module.exports.__esModule = true; diff --git a/packages/babel-core/test/fixtures/config/config-files/babelrc-mjs-error/.babelrc.mjs b/packages/babel-core/test/fixtures/config/config-files/babelrc-mjs-error/.babelrc.mjs new file mode 100644 index 000000000000..5a4fdd06a352 --- /dev/null +++ b/packages/babel-core/test/fixtures/config/config-files/babelrc-mjs-error/.babelrc.mjs @@ -0,0 +1,8 @@ +// Until Jest supports native mjs, we must simulate it 🤷 + +module.exports = new Promise(resolve => resolve({ + default: function () { + throw new Error("Babelrc threw an error"); + } +})); +module.exports.__esModule = true;