Add support for .cjs config files (#10599)

* Remove duplicate config loading logic and errors

* Add support for .cjs config files

* Add tests

* [tests] Fallback for fs.promises on node 6
This commit is contained in:
Nicolò Ribaudo 2019-11-04 00:24:44 +01:00 committed by GitHub
parent 58a646be59
commit bea1b0d0af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 204 additions and 220 deletions

View File

@ -18,26 +18,22 @@ import type { CallerMetadata } from "../validation/options";
const debug = buildDebug("babel:config:loading:files:configuration"); const debug = buildDebug("babel:config:loading:files:configuration");
const BABEL_CONFIG_JS_FILENAME = "babel.config.js";
const BABEL_CONFIG_JSON_FILENAME = "babel.config.json";
const ROOT_CONFIG_FILENAMES = [ const ROOT_CONFIG_FILENAMES = [
BABEL_CONFIG_JS_FILENAME, "babel.config.js",
BABEL_CONFIG_JSON_FILENAME, "babel.config.cjs",
"babel.config.json",
]; ];
const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs"];
const BABELRC_FILENAME = ".babelrc";
const BABELRC_JS_FILENAME = ".babelrc.js";
const BABELIGNORE_FILENAME = ".babelignore"; const BABELIGNORE_FILENAME = ".babelignore";
export function findConfigUpwards(rootDir: string): string | null { export function findConfigUpwards(rootDir: string): string | null {
let dirname = rootDir; let dirname = rootDir;
while (true) { while (true) {
if ( const configFileFound = ROOT_CONFIG_FILENAMES.some(filename =>
fs.existsSync(path.join(dirname, BABEL_CONFIG_JS_FILENAME)) || fs.existsSync(path.join(dirname, filename)),
fs.existsSync(path.join(dirname, BABEL_CONFIG_JSON_FILENAME)) );
) { if (configFileFound) return dirname;
return dirname;
}
const nextDir = path.dirname(dirname); const nextDir = path.dirname(dirname);
if (dirname === nextDir) break; if (dirname === nextDir) break;
@ -59,45 +55,15 @@ export function findRelativeConfig(
for (const loc of packageData.directories) { for (const loc of packageData.directories) {
if (!config) { if (!config) {
config = [BABELRC_FILENAME, BABELRC_JS_FILENAME].reduce( config = loadOneConfig(
(previousConfig: ConfigFile | null, name) => { RELATIVE_CONFIG_FILENAMES,
const filepath = path.join(loc, name); loc,
const config = readConfig(filepath, envName, caller); envName,
caller,
if (config && previousConfig) {
throw new Error(
`Multiple configuration files found. Please remove one:\n` +
` - ${path.basename(previousConfig.filepath)}\n` +
` - ${name}\n` +
`from ${loc}`,
);
}
return config || previousConfig;
},
null,
);
const pkgConfig =
packageData.pkg && packageData.pkg.dirname === loc packageData.pkg && packageData.pkg.dirname === loc
? packageToBabelConfig(packageData.pkg) ? packageToBabelConfig(packageData.pkg)
: null; : null,
);
if (pkgConfig) {
if (config) {
throw new Error(
`Multiple configuration files found. Please remove one:\n` +
` - ${path.basename(pkgConfig.filepath)}#babel\n` +
` - ${path.basename(config.filepath)}\n` +
`from ${loc}`,
);
}
config = pkgConfig;
}
if (config) {
debug("Found configuration %o from %o.", config.filepath, dirname);
}
} }
if (!ignore) { if (!ignore) {
@ -118,24 +84,31 @@ export function findRootConfig(
envName: string, envName: string,
caller: CallerMetadata | void, caller: CallerMetadata | void,
): ConfigFile | null { ): ConfigFile | null {
const config = ROOT_CONFIG_FILENAMES.reduce( return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller);
(previousConfig: ConfigFile | null, name) => { }
const filepath = path.resolve(dirname, name);
const config = readConfig(filepath, envName, caller);
if (config && previousConfig) { function loadOneConfig(
throw new Error( names: string[],
`Multiple configuration files found. Please remove one:\n` + dirname: string,
` - ${path.basename(previousConfig.filepath)}\n` + envName: string,
` - ${name}\n` + caller: CallerMetadata | void,
`from ${dirname}`, previousConfig?: ConfigFile | null = null,
); ): ConfigFile | null {
} const config = names.reduce((previousConfig: ConfigFile | null, name) => {
const filepath = path.resolve(dirname, name);
const config = readConfig(filepath, envName, caller);
return config || previousConfig; if (config && previousConfig) {
}, throw new Error(
null, `Multiple configuration files found. Please remove one:\n` +
); ` - ${path.basename(previousConfig.filepath)}\n` +
` - ${name}\n` +
`from ${dirname}`,
);
}
return config || previousConfig;
}, previousConfig);
if (config) { if (config) {
debug("Found configuration %o from %o.", config.filepath, dirname); debug("Found configuration %o from %o.", config.filepath, dirname);
@ -165,7 +138,8 @@ export function loadConfig(
* throw if there are parsing errors while loading a config. * throw if there are parsing errors while loading a config.
*/ */
function readConfig(filepath, envName, caller): ConfigFile | null { function readConfig(filepath, envName, caller): ConfigFile | null {
return path.extname(filepath) === ".js" const ext = path.extname(filepath);
return ext === ".js" || ext === ".cjs"
? readConfigJS(filepath, { envName, caller }) ? readConfigJS(filepath, { envName, caller })
: readConfigJSON5(filepath); : readConfigJSON5(filepath);
} }

View File

@ -1,8 +1,44 @@
import fs from "fs"; import fs from "fs";
import os from "os";
import path from "path"; import path from "path";
import escapeRegExp from "lodash/escapeRegExp"; import escapeRegExp from "lodash/escapeRegExp";
import { loadOptions as loadOptionsOrig } from "../lib"; import { loadOptions as loadOptionsOrig } from "../lib";
// TODO: In Babel 8, we can directly uses fs.promises which is supported by
// node 8+
const pfs =
fs.promises ??
new Proxy(fs, {
get(target, name) {
if (name === "copyFile") {
// fs.copyFile is only supported since node 8.5
// https://stackoverflow.com/a/30405105/2359289
return function copyFile(source, target) {
const rd = fs.createReadStream(source);
const wr = fs.createWriteStream(target);
return new Promise(function(resolve, reject) {
rd.on("error", reject);
wr.on("error", reject);
wr.on("finish", resolve);
rd.pipe(wr);
}).catch(function(error) {
rd.destroy();
wr.end();
throw error;
});
};
}
return (...args) =>
new Promise((resolve, reject) =>
target[name](...args, (error, result) => {
if (error) reject(error);
else resolve(result);
}),
);
},
});
function fixture(...args) { function fixture(...args) {
return path.join(__dirname, "fixtures", "config", ...args); return path.join(__dirname, "fixtures", "config", ...args);
} }
@ -14,6 +50,24 @@ function loadOptions(opts) {
}); });
} }
function pairs(items) {
const pairs = [];
for (let i = 0; i < items.length - 1; i++) {
for (let j = i + 1; j < items.length; j++) {
pairs.push([items[i], items[j]]);
}
}
return pairs;
}
async function getTemp(name) {
const cwd = await pfs.mkdtemp(os.tmpdir() + path.sep + name);
const tmp = name => path.join(cwd, name);
const config = name =>
pfs.copyFile(fixture("config-files-templates", name), tmp(name));
return { cwd, tmp, config };
}
describe("buildConfigChain", function() { describe("buildConfigChain", function() {
describe("test", () => { describe("test", () => {
describe("single", () => { describe("single", () => {
@ -944,159 +998,122 @@ describe("buildConfigChain", function() {
} }
}); });
it("should load babel.config.json", () => { describe("root", () => {
const filename = fixture("config-files", "babel-config-json", "src.js"); test.each(["babel.config.json", "babel.config.js", "babel.config.cjs"])(
"should load %s",
async name => {
const { cwd, tmp, config } = await getTemp(
`babel-test-load-config-${name}`,
);
const filename = tmp("src.js");
expect( await config(name);
loadOptions({
filename,
cwd: path.dirname(filename),
}),
).toEqual({
...getDefaults(),
filename: filename,
cwd: path.dirname(filename),
root: path.dirname(filename),
comments: true,
});
});
it("should load babel.config.js", () => { expect(
const filename = fixture("config-files", "babel-config-js", "src.js"); loadOptions({
filename,
expect( cwd,
loadOptions({ }),
filename, ).toEqual({
cwd: path.dirname(filename), ...getDefaults(),
}), filename,
).toEqual({ cwd,
...getDefaults(), root: cwd,
filename: filename, comments: true,
cwd: path.dirname(filename), });
root: path.dirname(filename), },
comments: true,
});
});
it("should whtow if both babel.config.json and babel.config.js are used", () => {
const filename = fixture(
"config-files",
"babel-config-js-and-json",
"src.js",
); );
expect(() => test.each(
loadOptions({ filename, cwd: path.dirname(filename) }), pairs(["babel.config.json", "babel.config.js", "babel.config.cjs"]),
).toThrow(/Multiple configuration files found/); )("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/,
);
});
}); });
it("should load .babelrc", () => { describe("relative", () => {
const filename = fixture("config-files", "babelrc", "src.js"); test.each(["package.json", ".babelrc", ".babelrc.js", ".babelrc.cjs"])(
"should load %s",
async name => {
const { cwd, tmp, config } = await getTemp(
`babel-test-load-config-${name}`,
);
const filename = tmp("src.js");
expect( await config(name);
loadOptions({
filename, expect(
loadOptions({
filename,
cwd,
}),
).toEqual({
...getDefaults(),
filename,
cwd,
root: cwd,
comments: true,
});
},
);
it("should load .babelignore", () => {
const filename = fixture("config-files", "babelignore", "src.js");
expect(
loadOptions({ filename, cwd: path.dirname(filename) }),
).toBeNull();
});
test.each(
pairs(["package.json", ".babelrc", ".babelrc.js", ".babelrc.cjs"]),
)("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/,
);
});
it("should ignore package.json without a 'babel' property", () => {
const filename = fixture("config-files", "pkg-ignored", "src.js");
expect(loadOptions({ filename, cwd: path.dirname(filename) })).toEqual({
...getDefaults(),
filename: filename,
cwd: path.dirname(filename), cwd: path.dirname(filename),
}), root: path.dirname(filename),
).toEqual({ comments: true,
...getDefaults(), });
filename: filename,
cwd: path.dirname(filename),
root: path.dirname(filename),
comments: true,
}); });
});
it("should load .babelrc.js", () => { test.each`
const filename = fixture("config-files", "babelrc-js", "src.js"); config | dir | error
${".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/}
${"package.json"} | ${"pkg-error"} | ${/Error while parsing JSON - /}
`("should show helpful errors for $config", ({ dir, error }) => {
const filename = fixture("config-files", dir, "src.js");
expect(loadOptions({ filename, cwd: path.dirname(filename) })).toEqual({ expect(() =>
...getDefaults(), loadOptions({ filename, cwd: path.dirname(filename) }),
filename: filename, ).toThrow(error);
cwd: path.dirname(filename),
root: path.dirname(filename),
comments: true,
}); });
}); });
it("should load package.json#babel", () => {
const filename = fixture("config-files", "pkg", "src.js");
expect(loadOptions({ filename, cwd: path.dirname(filename) })).toEqual({
...getDefaults(),
filename: filename,
cwd: path.dirname(filename),
root: path.dirname(filename),
comments: true,
});
});
it("should load .babelignore", () => {
const filename = fixture("config-files", "babelignore", "src.js");
expect(loadOptions({ filename, cwd: path.dirname(filename) })).toBeNull();
});
it("should throw if there are both .babelrc and .babelrc.js", () => {
const filename = fixture("config-files", "both-babelrc", "src.js");
expect(() =>
loadOptions({ filename, cwd: path.dirname(filename) }),
).toThrow(/Multiple configuration files found/);
});
it("should throw if there are both .babelrc and package.json", () => {
const filename = fixture("config-files", "pkg-babelrc", "src.js");
expect(() =>
loadOptions({ filename, cwd: path.dirname(filename) }),
).toThrow(/Multiple configuration files found/);
});
it("should throw if there are both .babelrc.js and package.json", () => {
const filename = fixture("config-files", "pkg-babelrc-js", "src.js");
expect(() =>
loadOptions({ filename, cwd: path.dirname(filename) }),
).toThrow(/Multiple configuration files found/);
});
it("should ignore package.json without a 'babel' property", () => {
const filename = fixture("config-files", "pkg-ignored", "src.js");
expect(loadOptions({ filename, cwd: path.dirname(filename) })).toEqual({
...getDefaults(),
filename: filename,
cwd: path.dirname(filename),
root: path.dirname(filename),
comments: true,
});
});
it("should show helpful errors for .babelrc", () => {
const filename = fixture("config-files", "babelrc-error", "src.js");
expect(() =>
loadOptions({ filename, cwd: path.dirname(filename) }),
).toThrow(/Error while parsing config - /);
});
it("should show helpful errors for .babelrc.js", () => {
const filename = fixture("config-files", "babelrc-js-error", "src.js");
expect(() =>
loadOptions({ filename, cwd: path.dirname(filename) }),
).toThrow(/Babelrc threw an error/);
});
it("should show helpful errors for package.json", () => {
const filename = fixture("config-files", "pkg-error", "src.js");
expect(() =>
loadOptions({ filename, cwd: path.dirname(filename) }),
).toThrow(/Error while parsing JSON - /);
});
it("should throw when `test` presents but `filename` is not passed", () => { it("should throw when `test` presents but `filename` is not passed", () => {
expect(() => loadOptions({ test: /\.ts$/, plugins: [] })).toThrow( expect(() => loadOptions({ test: /\.ts$/, plugins: [] })).toThrow(
/Configuration contains string\/RegExp pattern/, /Configuration contains string\/RegExp pattern/,

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
comments: true, comments: true
}; };

View File

@ -0,0 +1,3 @@
module.exports = {
comments: true
};

View File

@ -0,0 +1,3 @@
module.exports = function() {
throw new Error("Babelrc threw an error");
};

View File

@ -1,3 +0,0 @@
{
comments: true,
}

View File

@ -1 +0,0 @@
module.exports = {};

View File

@ -1 +0,0 @@
module.exports = {};

View File

@ -1,3 +0,0 @@
{
"babel": {}
}

View File

@ -1,3 +0,0 @@
{
"babel": {}
}