Add support for babel.config.mjs and .babelrc.mjs (#10903)

* Add support for babel.config.mjs and .babelrc.mjs

* Use path.extname
This commit is contained in:
Nicolò Ribaudo 2020-01-10 23:14:29 +01:00 committed by GitHub
parent 282f81bd67
commit ae06baf22f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 215 additions and 43 deletions

View File

@ -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: [
{

View File

@ -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;

View File

@ -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);
}

View File

@ -0,0 +1,59 @@
import { isAsync, waitFor } from "../../gensync-utils/async";
import type { Handler } from "gensync";
import path from "path";
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<mixed> {
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(filename: string): "cjs" | "mjs" | "unknown" {
switch (path.extname(filename)) {
case ".cjs":
return "cjs";
case ".mjs":
return "mjs";
default:
return "unknown";
}
}
function loadCjsDefault(filepath: string) {
const module = (require(filepath): mixed);
// TODO (Babel 8): Remove "undefined" fallback
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;
}

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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;

View File

@ -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;