Skip to content

Commit

Permalink
Add support for babel.config.mjs and .babelrc.mjs
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Dec 27, 2019
1 parent f71a3d6 commit f4b9d50
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 43 deletions.
4 changes: 4 additions & 0 deletions babel.config.js
Expand Up @@ -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: [
{
Expand Down
26 changes: 15 additions & 11 deletions packages/babel-core/src/config/files/configuration.js
Expand Up @@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions 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);
}
57 changes: 57 additions & 0 deletions 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<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(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;
}
138 changes: 106 additions & 32 deletions packages/babel-core/test/config-chain.js
Expand Up @@ -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+
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -1024,24 +1020,64 @@ 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}`,
);

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}`,
Expand All @@ -1050,12 +1086,7 @@ describe("buildConfigChain", function() {

await config(name);

expect(
loadOptions({
filename,
cwd,
}),
).toEqual({
expect(loadOptions({ filename, cwd })).toEqual({
...getDefaults(),
filename,
cwd,
Expand All @@ -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");

Expand All @@ -1074,17 +1141,23 @@ 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}`,
);

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", () => {
Expand All @@ -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);
});
});

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

0 comments on commit f4b9d50

Please sign in to comment.