Separate config/plugin loading from config processing. (#5563)

This commit is contained in:
Logan Smyth
2017-04-04 14:48:48 -07:00
committed by GitHub
parent 9155c613cb
commit d39400c9d5
18 changed files with 381 additions and 244 deletions

View File

@@ -21,6 +21,9 @@
"babel-core",
"compiler"
],
"browser": {
"./lib/config/loading/files/index.js": "./lib/config/loading/files/index-browser.js"
},
"dependencies": {
"babel-code-frame": "7.0.0-alpha.3",
"babel-generator": "7.0.0-alpha.3",

View File

@@ -1,26 +1,8 @@
import * as babel from "../index";
import resolve from "./helpers/resolve";
import json5 from "json5";
import path from "path";
import fs from "fs";
import micromatch from "micromatch";
const existsCache = {};
const jsonCache = {};
const BABELRC_FILENAME = ".babelrc";
const BABELRC_JS_FILENAME = ".babelrc.js";
const PACKAGE_FILENAME = "package.json";
const BABELIGNORE_FILENAME = ".babelignore";
function exists(filename) {
const cached = existsCache[filename];
if (cached == null) {
return existsCache[filename] = fs.existsSync(filename);
} else {
return cached;
}
}
import { findConfigs, loadConfig } from "./loading/files";
export default function buildConfigChain(opts: Object = {}) {
const filename = opts.filename ? path.resolve(opts.filename) : null;
@@ -49,7 +31,6 @@ export default function buildConfigChain(opts: Object = {}) {
class ConfigChainBuilder {
constructor(filename) {
this.resolvedConfigs = [];
this.configs = [];
this.filename = filename;
this.possibleDirs = null;
@@ -124,11 +105,6 @@ class ConfigChainBuilder {
}
}
errorMultipleConfigs(loc1: string, loc2: string) {
throw new Error(`Multiple configuration files found. Please remove one:\n- ${
loc1}\n- ${loc2}`);
}
findConfigs(loc: string) {
if (!loc) return;
@@ -136,110 +112,14 @@ class ConfigChainBuilder {
loc = path.join(process.cwd(), loc);
}
let foundConfig = false;
let foundIgnore = false;
while (loc !== (loc = path.dirname(loc))) {
if (!foundIgnore) {
const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME);
if (exists(ignoreLoc)) {
this.addIgnoreConfig(ignoreLoc);
foundIgnore = true;
}
}
if (!foundConfig) {
const configLoc = path.join(loc, BABELRC_FILENAME);
const configJSLoc = path.join(loc, BABELRC_JS_FILENAME);
const pkgLoc = path.join(loc, PACKAGE_FILENAME);
const configLocs = [configLoc, configJSLoc, pkgLoc];
const foundConfigs = configLocs.reduce((arr, config) => {
if (exists(config)) {
const configAdded = config === pkgLoc
? this.addConfig(config, "babel", JSON)
: this.addConfig(config);
if (configAdded && arr.length) {
this.errorMultipleConfigs(arr.pop(), config);
}
if (configAdded) arr.push(config);
}
return arr;
}, []);
foundConfig = !!foundConfigs.length;
}
if (foundIgnore && foundConfig) return;
}
}
addIgnoreConfig(loc: string) {
const file = fs.readFileSync(loc, "utf8");
let lines = file.split("\n");
lines = lines
.map((line) => line.replace(/#(.*?)$/, "").trim())
.filter((line) => !!line);
if (lines.length) {
findConfigs(path.dirname(loc)).forEach(({ filepath, dirname, options }) => {
this.mergeConfig({
type: "options",
options: { ignore: lines },
alias: loc,
dirname: path.dirname(loc),
options,
alias: filepath,
dirname,
});
}
}
addConfig(loc: string, key?: string, json = json5): boolean {
if (this.resolvedConfigs.indexOf(loc) >= 0) {
return false;
}
this.resolvedConfigs.push(loc);
let options;
if (path.extname(loc) === ".js") {
try {
const configModule = require(loc);
options = configModule && configModule.__esModule ? configModule.default : configModule;
} catch (err) {
err.message = `${loc}: Error while loading config - ${err.message}`;
throw err;
}
if (!options || typeof options !== "object") {
throw new Error("Configuration should be an exported JavaScript object.");
}
} else {
const content = fs.readFileSync(loc, "utf8");
try {
options = jsonCache[content] = jsonCache[content] || json.parse(content);
} catch (err) {
err.message = `${loc}: Error while parsing JSON - ${err.message}`;
throw err;
}
}
if (key) {
if (!options[key]) {
return false;
}
options = options[key];
}
this.mergeConfig({
type: "options",
options,
alias: loc,
dirname: path.dirname(loc),
});
return !!options;
}
mergeConfig({
@@ -287,11 +167,18 @@ class ConfigChainBuilder {
// add extends clause
if (options.extends) {
const extendsLoc = resolve(options.extends, dirname);
if (extendsLoc) {
this.addConfig(extendsLoc);
} else {
throw new Error(`Couldn't resolve extends clause of ${options.extends} in ${alias}`);
const extendsConfig = loadConfig(options.extends, dirname);
const existingConfig = this.configs.some((config) => {
return config.alias === extendsConfig.filepath;
});
if (!existingConfig) {
this.mergeConfig({
type: "options",
alias: extendsConfig.filepath,
options: extendsConfig.options,
dirname: extendsConfig.dirname,
});
}
delete options.extends;
}

View File

@@ -1,3 +0,0 @@
export default function getPossiblePluginNames(pluginName: string): Array<string> {
return [`babel-plugin-${pluginName}`, pluginName];
}

View File

@@ -1,13 +0,0 @@
export default function getPossiblePresetNames(presetName: string): Array<string> {
const possibleNames = [`babel-preset-${presetName}`, presetName];
// trying to resolve @organization shortcat
// @foo/es2015 -> @foo/babel-preset-es2015
const matches = presetName.match(/^(@[^/]+)\/(.+)$/);
if (matches) {
const [, orgName, presetPath] = matches;
possibleNames.push(`${orgName}/babel-preset-${presetPath}`);
}
return possibleNames;
}

View File

@@ -1,5 +0,0 @@
import resolve from "./resolve";
export default function resolveFromPossibleNames(possibleNames: Array<string>, dirname: string): ?string {
return possibleNames.reduce((accum, curr) => accum || resolve(curr, dirname), null);
}

View File

@@ -1,6 +0,0 @@
import resolveFromPossibleNames from "./resolve-from-possible-names";
import getPossiblePluginNames from "./get-possible-plugin-names";
export default function resolvePlugin(pluginName: string, dirname: string = process.cwd()): ?string {
return resolveFromPossibleNames(getPossiblePluginNames(pluginName), dirname);
}

View File

@@ -1,6 +0,0 @@
import resolveFromPossibleNames from "./resolve-from-possible-names";
import getPossiblePresetNames from "./get-possible-preset-names";
export default function resolvePreset(presetName: string, dirname: string = process.cwd()): ?string {
return resolveFromPossibleNames(getPossiblePresetNames(presetName), dirname);
}

View File

@@ -1,9 +0,0 @@
import resolve from "resolve";
export default function (loc: string, relative: string = process.cwd()): ?string {
try {
return resolve.sync(loc, { basedir: relative });
} catch (err) {
return null;
}
}

View File

@@ -0,0 +1,175 @@
// @flow
import path from "path";
import fs from "fs";
import json5 from "json5";
import resolve from "resolve";
type ConfigFile = {
filepath: string,
dirname: string,
options: Object,
};
const existsCache = {};
const jsonCache = {};
const BABELRC_FILENAME = ".babelrc";
const BABELRC_JS_FILENAME = ".babelrc.js";
const PACKAGE_FILENAME = "package.json";
const BABELIGNORE_FILENAME = ".babelignore";
function exists(filename) {
const cached = existsCache[filename];
if (cached == null) {
return existsCache[filename] = fs.existsSync(filename);
} else {
return cached;
}
}
export function findConfigs(dirname: string): Array<ConfigFile> {
let foundConfig = false;
let foundIgnore = false;
const confs = [];
let loc = dirname;
while (true) {
if (!foundIgnore) {
const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME);
const ignore = readIgnoreConfig(ignoreLoc);
if (ignore) {
confs.push(ignore);
foundIgnore = true;
}
}
if (!foundConfig) {
const conf = [
BABELRC_FILENAME,
BABELRC_JS_FILENAME,
PACKAGE_FILENAME,
].reduce((previousConfig: ConfigFile|null, name) => {
const filepath = path.join(loc, name);
const config = readConfig(filepath);
if (config && previousConfig) {
throw new Error(`Multiple configuration files found. Please remove one:\n- ${
path.basename(previousConfig.filepath)}\n- ${name}\nfrom ${loc}`);
}
return config || previousConfig;
}, null);
if (conf) {
confs.push(conf);
foundConfig = true;
}
}
if (foundIgnore && foundConfig) break;
if (loc === path.dirname(loc)) break;
loc = path.dirname(loc);
}
return confs;
}
export function loadConfig(name: string, dirname: string): ConfigFile {
const filepath = resolve.sync(name, { basedir: dirname });
const conf = readConfig(filepath);
if (!conf) throw new Error(`Config file ${filepath} contains no configuration data`);
return conf;
}
/**
* Read the given config file, returning the result. Returns null if no config was found, but will
* throw if there are parsing errors while loading a config.
*/
function readConfig(filepath) {
return (path.extname(filepath) === ".js") ? readConfigJS(filepath) : readConfigFile(filepath);
}
function readIgnoreConfig(filepath) {
if (!exists(filepath)) return null;
const file = fs.readFileSync(filepath, "utf8");
let lines = file.split("\n");
lines = lines
.map((line) => line.replace(/#(.*?)$/, "").trim())
.filter((line) => !!line);
return {
filepath,
dirname: path.dirname(filepath),
options: { ignore: lines },
};
}
function readConfigJS(filepath) {
if (!exists(filepath)) return null;
let options;
try {
// $FlowIssue
const configModule = (require(filepath): mixed);
options = configModule && configModule.__esModule ? (configModule.default || undefined) : configModule;
} catch (err) {
err.message = `${filepath}: Error while loading config - ${err.message}`;
throw err;
}
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new Error(`${filepath}: Configuration should be an exported JavaScript object.`);
}
return {
filepath,
dirname: path.dirname(filepath),
options,
};
}
function readConfigFile(filepath) {
if (!exists(filepath)) return null;
const content = fs.readFileSync(filepath, "utf8");
let options;
if (path.basename(filepath) === PACKAGE_FILENAME) {
try {
const json = jsonCache[content] = jsonCache[content] || JSON.parse(content);
options = json.babel;
} catch (err) {
err.message = `${filepath}: Error while parsing JSON - ${err.message}`;
throw err;
}
if (!options) return null;
} else {
try {
options = jsonCache[content] = jsonCache[content] || json5.parse(content);
} catch (err) {
err.message = `${filepath}: Error while parsing config - ${err.message}`;
throw err;
}
if (!options) throw new Error(`${filepath}: No config detected`);
}
if (typeof options !== "object") throw new Error(`${filepath}: Config returned typeof ${typeof options}`);
if (Array.isArray(options)) throw new Error(`${filepath}: Expected config object but found array`);
return {
filepath,
dirname: path.dirname(filepath),
options,
};
}

View File

@@ -0,0 +1,42 @@
// @flow
type ConfigFile = {
filepath: string,
dirname: string,
options: Object,
};
// eslint-disable-next-line no-unused-vars
export function findConfigs(dirname: string): Array<ConfigFile> {
return [];
}
export function loadConfig(name: string, dirname: string): ConfigFile {
throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`);
}
// eslint-disable-next-line no-unused-vars
export function resolvePlugin(name: string, dirname: string): string|null {
return null;
}
// eslint-disable-next-line no-unused-vars
export function resolvePreset(name: string, dirname: string): string|null {
return null;
}
export function loadPlugin(name: string, dirname: string): { filepath: string, plugin: mixed } {
throw new Error(`Cannot load plugin ${name} relative to ${dirname} in a browser`);
}
export function loadPreset(name: string, dirname: string): { filepath: string, preset: mixed } {
throw new Error(`Cannot load preset ${name} relative to ${dirname} in a browser`);
}
export function loadParser(name: string, dirname: string): { filepath: string, parser: Function } {
throw new Error(`Cannot load parser ${name} relative to ${dirname} in a browser`);
}
export function loadGenerator(name: string, dirname: string): { filepath: string, generator: Function } {
throw new Error(`Cannot load generator ${name} relative to ${dirname} in a browser`);
}

View File

@@ -0,0 +1,11 @@
// @flow
import typeof * as indexBrowserType from "./index-browser";
import typeof * as indexType from "./index";
// Kind of gross, but essentially asserting that the exports of this module are the same as the
// exports of index-browser, since this file may be replaced at bundle time with index-browser.
((({}: any) : $Exact<indexBrowserType>): $Exact<indexType>);
export * from "./configuration";
export * from "./plugins";

View File

@@ -0,0 +1,111 @@
// @flow
/**
* This file handles all logic for converting string-based configuration references into loaded objects.
*/
import resolve from "resolve";
export function resolvePlugin(pluginName: string, dirname: string): string|null {
const possibleNames = [`babel-plugin-${pluginName}`, pluginName];
return resolveFromPossibleNames(possibleNames, dirname);
}
export function resolvePreset(presetName: string, dirname: string): string|null {
const possibleNames = [`babel-preset-${presetName}`, presetName];
// trying to resolve @organization shortcat
// @foo/es2015 -> @foo/babel-preset-es2015
const matches = presetName.match(/^(@[^/]+)\/(.+)$/);
if (matches) {
const [, orgName, presetPath] = matches;
possibleNames.push(`${orgName}/babel-preset-${presetPath}`);
}
return resolveFromPossibleNames(possibleNames, dirname);
}
export function loadPlugin(name: string, dirname: string): { filepath: string, plugin: mixed } {
const filepath = resolvePlugin(name, dirname);
if (!filepath) throw new Error(`Plugin ${name} not found relative to ${dirname}`);
return {
filepath,
plugin: requireModule(filepath),
};
}
export function loadPreset(name: string, dirname: string): { filepath: string, preset: mixed } {
const filepath = resolvePreset(name, dirname);
if (!filepath) throw new Error(`Preset ${name} not found relative to ${dirname}`);
return {
filepath,
preset: requireModule(filepath),
};
}
export function loadParser(name: string, dirname: string): { filepath: string, parser: Function } {
const filepath = resolveQuiet(name, dirname);
if (!filepath) throw new Error(`Parser ${name} not found relative to ${dirname}`);
const mod = requireModule(filepath);
if (!mod) {
throw new Error(`Parser ${name} relative to ${dirname} does not export an object`);
}
if (typeof mod.parse !== "function") {
throw new Error(`Parser ${name} relative to ${dirname} does not export a .parse function`);
}
return {
filepath,
parser: mod.parse,
};
}
export function loadGenerator(name: string, dirname: string): { filepath: string, generator: Function } {
const filepath = resolveQuiet(name, dirname);
if (!filepath) throw new Error(`Generator ${name} not found relative to ${dirname}`);
const mod = requireModule(filepath);
if (!mod) {
throw new Error(`Generator ${name} relative to ${dirname} does not export an object`);
}
if (typeof mod.print !== "function") {
throw new Error(`Generator ${name} relative to ${dirname} does not export a .print function`);
}
return {
filepath,
generator: mod.print,
};
}
function resolveQuiet(name: string, dirname: string): string|null {
try {
return resolve.sync(name, { basedir: dirname });
} catch (e) {
// The 'resolve' module can currently throw ENOTDIR
// https://github.com/substack/node-resolve/issues/121
if (e.code !== "MODULE_NOT_FOUND" && e.code !== "ENOTDIR") throw e;
// Silently fail and move to the next item.
}
return null;
}
function resolveFromPossibleNames(possibleNames: Array<string>, dirname: string): string|null {
for (const name of possibleNames) {
const result = resolveQuiet(name, dirname);
if (result !== null) return result;
}
return null;
}
function requireModule(name: string): mixed {
// $FlowIssue
return require(name);
}

View File

@@ -1,9 +1,6 @@
import * as context from "../index";
import Plugin from "./plugin";
import * as messages from "babel-messages";
import resolve from "./helpers/resolve";
import resolvePlugin from "./helpers/resolve-plugin";
import resolvePreset from "./helpers/resolve-preset";
import defaults from "lodash/defaults";
import cloneDeepWith from "lodash/cloneDeepWith";
import merge from "./helpers/merge";
@@ -11,6 +8,8 @@ import removed from "./removed";
import buildConfigChain from "./build-config-chain";
import path from "path";
import { loadPlugin, loadPreset, loadParser, loadGenerator } from "./loading/files";
type PluginObject = {
pre?: Function;
post?: Function;
@@ -77,11 +76,9 @@ const optionNames = new Set([
export default class OptionManager {
constructor() {
this.resolvedConfigs = [];
this.options = OptionManager.createBareOptions();
}
resolvedConfigs: Array<string>;
options: Object;
static memoisedPlugins: Array<{
@@ -164,12 +161,7 @@ export default class OptionManager {
// allow plugins to be specified as strings
if (typeof plugin === "string") {
const pluginLoc = resolvePlugin(plugin, dirname);
if (pluginLoc) {
plugin = require(pluginLoc);
} else {
throw new ReferenceError(messages.get("pluginUnknown", plugin, loc, i, dirname));
}
plugin = loadPlugin(plugin, dirname).plugin;
}
plugin = OptionManager.normalisePlugin(plugin, loc, i, alias);
@@ -256,23 +248,11 @@ export default class OptionManager {
}
if (opts.parserOpts && typeof opts.parserOpts.parser === "string") {
const parser = resolve(opts.parserOpts.parser, dirname);
if (parser) {
opts.parserOpts.parser = require(parser).parse;
} else {
throw new Error(`Couldn't find parser ${opts.parserOpts.parser} with "parse" method ` +
`relative to directory ${dirname}`);
}
opts.parserOpts.parser = loadParser(opts.parserOpts.parser, dirname).parser;
}
if (opts.generatorOpts && typeof opts.generatorOpts.generator === "string") {
const generator = resolve(opts.generatorOpts.generator, dirname);
if (generator) {
opts.generatorOpts.generator = require(generator).print;
} else {
throw new Error(`Couldn't find generator ${opts.generatorOpts.generator} with "print" method ` +
`relative to directory ${dirname}`);
}
opts.generatorOpts.generator = loadGenerator(opts.generatorOpts.generator, dirname).generator;
}
// resolve plugins
@@ -332,14 +312,12 @@ export default class OptionManager {
let presetLoc;
try {
if (typeof preset === "string") {
presetLoc = resolvePreset(preset, dirname);
if (!presetLoc) {
throw new Error(`Couldn't find preset ${JSON.stringify(preset)} relative to directory ` +
JSON.stringify(dirname));
}
({
filepath: presetLoc,
preset,
} = loadPreset(preset, dirname));
}
const resolvedPreset = this.loadPreset(presetLoc || preset, options, { dirname });
const resolvedPreset = this.loadPreset(preset, options, { dirname });
if (onResolve) onResolve(resolvedPreset, presetLoc);
@@ -359,9 +337,6 @@ export default class OptionManager {
*/
loadPreset(preset, options, meta) {
let presetFactory = preset;
if (typeof presetFactory === "string") {
presetFactory = require(presetFactory);
}
if (typeof presetFactory === "object" && presetFactory.__esModule) {
if (presetFactory.default) {

View File

@@ -1,7 +1,6 @@
export File from "./transformation/file";
export buildExternalHelpers from "./tools/build-external-helpers";
export resolvePlugin from "./config/helpers/resolve-plugin";
export resolvePreset from "./config/helpers/resolve-preset";
export { resolvePlugin, resolvePreset } from "./config/loading/files";
export { version } from "../package";
export { getEnv } from "./config/helpers/environment";

View File

@@ -27,10 +27,7 @@ export function debug(opts: Object, msg: string) {
const shebangRegex = /^#!.*/;
const INTERNAL_PLUGINS = loadConfig({
babelrc: false,
plugins: [ blockHoistPlugin, shadowFunctionsPlugin ],
}).passes[0];
let INTERNAL_PLUGINS;
const errorVisitor = {
enter(path, state) {
@@ -44,6 +41,15 @@ const errorVisitor = {
export default class File extends Store {
constructor({ options, passes }: ResolvedConfig) {
if (!INTERNAL_PLUGINS) {
// Lazy-init the internal plugins to remove the init-time circular dependency between plugins being
// passed babel-core's export object, which loads this file, and this 'loadConfig' loading plugins.
INTERNAL_PLUGINS = loadConfig({
babelrc: false,
plugins: [ blockHoistPlugin, shadowFunctionsPlugin ],
}).passes[0];
}
super();
this.pluginPasses = passes;

View File

@@ -633,7 +633,7 @@ describe("buildConfigChain", function () {
filename: fixture("json-config-error", "src.js"),
});
},
/Error while parsing JSON/
/Error while parsing config/
);
});
});

View File

@@ -1,8 +0,0 @@
import assert from "assert";
import getPossiblePluginNames from "../lib/config/helpers/get-possible-plugin-names";
describe("getPossiblePluginNames", function () {
it("adds the babel-plugin prefix", function() {
assert.deepEqual(getPossiblePluginNames("foobar"), ["babel-plugin-foobar", "foobar"]);
});
});

View File

@@ -1,22 +0,0 @@
import assert from "assert";
import getPossiblePresetNames from "../lib/config/helpers/get-possible-preset-names";
describe("getPossiblePresetNames", function () {
it("adds the babel-preset prefix", function() {
assert.deepEqual(getPossiblePresetNames("foobar"), ["babel-preset-foobar", "foobar"]);
});
it("inserts babel-preset after @org/", function() {
assert.deepEqual(getPossiblePresetNames("@babel/es2015"), [
"babel-preset-@babel/es2015",
"@babel/es2015",
"@babel/babel-preset-es2015",
]);
assert.deepEqual(getPossiblePresetNames("@babel/react/optimizations"), [
"babel-preset-@babel/react/optimizations",
"@babel/react/optimizations",
"@babel/babel-preset-react/optimizations",
]);
});
});