Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from Jan 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate this, but this is the best way we have so far to test it 😢

: 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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth using path.extname() here?

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why undefined is offered as fallback here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure; I only moved this code here from

const configModule = (require(filepath): mixed);
options =
configModule && configModule.__esModule
? configModule.default || undefined
: configModule;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in case someone exports false as their default config? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, we should investigate this in later PRs, or just remove it in Babel 8 because it looks suspicious.

}

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/,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use toThrowErrorMatchingSnapshot() here and don't have to write the same string again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't use it because the error contains the path of the file in the tmp folder, which might change.

);
});

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;