Typecheck much more of the config loading process (#5642)

* Add type declarations for micromatch.

* Enable Flowtype on all config loading.

* Remove unneeded comments.
This commit is contained in:
Logan Smyth 2017-04-18 09:28:18 -07:00 committed by GitHub
parent c46ef658b5
commit 6af8e64711
13 changed files with 199 additions and 101 deletions

View File

@ -9,6 +9,7 @@ packages/*/src
lib/file.js lib/file.js
lib/parser.js lib/parser.js
lib/types.js lib/types.js
lib/third-party-libs.js.flow
[options] [options]
strip_root=true strip_root=true

View File

@ -0,0 +1,9 @@
/**
* Basic declarations for the npm modules we use.
*/
declare module "micromatch" {
declare function exports(Array<string>, Array<string>, ?{
nocase: boolean,
}): Array<string>;
}

View File

@ -1,10 +1,24 @@
import * as babel from "../index"; // @flow
import { getEnv } from "./helpers/environment";
import path from "path"; import path from "path";
import micromatch from "micromatch"; import micromatch from "micromatch";
import { findConfigs, loadConfig } from "./loading/files"; import { findConfigs, loadConfig } from "./loading/files";
export default function buildConfigChain(opts: Object = {}) { type ConfigItem = {
type: "options"|"arguments",
options: {},
dirname: string,
alias: string,
loc: string,
};
export default function buildConfigChain(opts: {}): Array<ConfigItem>|null {
if (typeof opts.filename !== "string" && opts.filename != null) {
throw new Error(".filename must be a string, null, or undefined");
}
const filename = opts.filename ? path.resolve(opts.filename) : null; const filename = opts.filename ? path.resolve(opts.filename) : null;
const builder = new ConfigChainBuilder(filename); const builder = new ConfigChainBuilder(filename);
@ -17,7 +31,7 @@ export default function buildConfigChain(opts: Object = {}) {
}); });
// resolve all .babelrc files // resolve all .babelrc files
if (opts.babelrc !== false) { if (opts.babelrc !== false && filename) {
builder.findConfigs(filename); builder.findConfigs(filename);
} }
} catch (e) { } catch (e) {
@ -30,6 +44,10 @@ export default function buildConfigChain(opts: Object = {}) {
} }
class ConfigChainBuilder { class ConfigChainBuilder {
filename: string|null;
configs: Array<ConfigItem>;
possibleDirs: null|Array<string>;
constructor(filename) { constructor(filename) {
this.configs = []; this.configs = [];
this.filename = filename; this.filename = filename;
@ -40,59 +58,68 @@ class ConfigChainBuilder {
* Tests if a filename should be ignored based on "ignore" and "only" options. * Tests if a filename should be ignored based on "ignore" and "only" options.
*/ */
shouldIgnore( shouldIgnore(
ignore: Array<string | RegExp | Function>, ignore: mixed,
only?: Array<string | RegExp | Function>, only: mixed,
dirname: string, dirname: string,
): boolean { ): boolean {
if (!this.filename) return false; if (!this.filename) return false;
if (ignore && !Array.isArray(ignore)) { if (ignore) {
throw new Error(`.ignore should be an array, ${JSON.stringify(ignore)} given`); if (!Array.isArray(ignore)) {
throw new Error(`.ignore should be an array, ${JSON.stringify(ignore)} given`);
}
if (this.matchesPatterns(ignore, dirname)) return true;
} }
if (only && !Array.isArray(only)) { if (only) {
throw new Error(`.only should be an array, ${JSON.stringify(only)} given`); if (!Array.isArray(only)) {
throw new Error(`.only should be an array, ${JSON.stringify(only)} given`);
}
if (!this.matchesPatterns(only, dirname)) return true;
} }
return (ignore && this.matchesPatterns(ignore, dirname)) || return false;
(only && !this.matchesPatterns(only, dirname));
} }
/** /**
* Returns result of calling function with filename if pattern is a function. * Returns result of calling function with filename if pattern is a function.
* Otherwise returns result of matching pattern Regex with filename. * Otherwise returns result of matching pattern Regex with filename.
*/ */
matchesPatterns(patterns: Array<string | Function | RegExp>, dirname: string) { matchesPatterns(patterns: Array<mixed>, dirname: string) {
const filename = this.filename;
if (!filename) throw new Error("Assertion failure: .filename should always exist here");
const res = []; const res = [];
const strings = []; const strings = [];
const fns = []; const fns = [];
patterns.forEach((pattern) => { patterns.forEach((pattern) => {
const type = typeof pattern; if (typeof pattern === "string") strings.push(pattern);
if (type === "string") strings.push(pattern); else if (typeof pattern === "function") fns.push(pattern);
else if (type === "function") fns.push(pattern); else if (pattern instanceof RegExp) res.push(pattern);
else res.push(pattern); else throw new Error("Patterns must be a string, function, or regular expression");
}); });
if (res.some((re) => re.test(this.filename))) return true; if (res.some((re) => re.test(filename))) return true;
if (fns.some((fn) => fn(this.filename))) return true; if (fns.some((fn) => fn(filename))) return true;
if (strings.length > 0) { if (strings.length > 0) {
let possibleDirs = this.possibleDirs;
// Lazy-init so we don't initialize this for files that have no glob patterns. // Lazy-init so we don't initialize this for files that have no glob patterns.
if (!this.possibleDirs) { if (!possibleDirs) {
this.possibleDirs = []; possibleDirs = this.possibleDirs = [];
if (this.filename) { possibleDirs.push(filename);
this.possibleDirs.push(this.filename);
let current = this.filename; let current = filename;
while (true) { while (true) {
const previous = current; const previous = current;
current = path.dirname(current); current = path.dirname(current);
if (previous === current) break; if (previous === current) break;
this.possibleDirs.push(current); possibleDirs.push(current);
}
} }
} }
@ -104,7 +131,7 @@ class ConfigChainBuilder {
return (negate ? "!" : "") + path.resolve(dirname, pattern); return (negate ? "!" : "") + path.resolve(dirname, pattern);
}); });
if (micromatch(this.possibleDirs, absolutePatterns, { nocase: true }).length > 0) { if (micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0) {
return true; return true;
} }
} }
@ -113,12 +140,6 @@ class ConfigChainBuilder {
} }
findConfigs(loc: string) { findConfigs(loc: string) {
if (!loc) return;
if (!path.isAbsolute(loc)) {
loc = path.join(process.cwd(), loc);
}
findConfigs(path.dirname(loc)).forEach(({ filepath, dirname, options }) => { findConfigs(path.dirname(loc)).forEach(({ filepath, dirname, options }) => {
this.mergeConfig({ this.mergeConfig({
type: "options", type: "options",
@ -131,31 +152,33 @@ class ConfigChainBuilder {
mergeConfig({ mergeConfig({
type, type,
options, options: rawOpts,
alias, alias,
loc,
dirname, dirname,
}) { }) {
if (!options) {
return false;
}
// Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files. // Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files.
if (this.filename && this.shouldIgnore(options.ignore, options.only, dirname)) { if (this.filename && this.shouldIgnore(rawOpts.ignore || null, rawOpts.only || null, dirname)) {
// TODO(logan): This is a really cross way to bail out. Avoid this in rewrite. // TODO(logan): This is a really cross way to bail out. Avoid this in rewrite.
throw Object.assign(new Error("This file has been ignored."), { code: "BABEL_IGNORED_FILE" }); throw Object.assign((new Error("This file has been ignored."): any), { code: "BABEL_IGNORED_FILE" });
} }
options = Object.assign({}, options); const options = Object.assign({}, rawOpts);
delete options.env;
delete options.extends;
loc = loc || alias; const envKey = getEnv();
// env if (rawOpts.env != null && (typeof rawOpts.env !== "object" || Array.isArray(rawOpts.env))) {
const envKey = babel.getEnv(); throw new Error(".env block must be an object, null, or undefined");
if (options.env) { }
const envOpts = options.env[envKey];
delete options.env;
const envOpts = rawOpts.env && rawOpts.env[envKey];
if (envOpts != null && (typeof envOpts !== "object" || Array.isArray(envOpts))) {
throw new Error(".env[...] block must be an object, null, or undefined");
}
if (envOpts) {
this.mergeConfig({ this.mergeConfig({
type, type,
options: envOpts, options: envOpts,
@ -168,13 +191,14 @@ class ConfigChainBuilder {
type, type,
options, options,
alias, alias,
loc, loc: alias,
dirname, dirname,
}); });
// add extends clause if (rawOpts.extends) {
if (options.extends) { if (typeof rawOpts.extends !== "string") throw new Error(".extends must be a string");
const extendsConfig = loadConfig(options.extends, dirname);
const extendsConfig = loadConfig(rawOpts.extends, dirname);
const existingConfig = this.configs.some((config) => { const existingConfig = this.configs.some((config) => {
return config.alias === extendsConfig.filepath; return config.alias === extendsConfig.filepath;
@ -187,7 +211,6 @@ class ConfigChainBuilder {
dirname: extendsConfig.dirname, dirname: extendsConfig.dirname,
}); });
} }
delete options.extends;
} }
} }
} }

View File

@ -31,7 +31,7 @@ export function makeStrongCache<ArgT, ResultT>(
* configures its caching behavior. Cached values are stored weakly and the function argument must be * configures its caching behavior. Cached values are stored weakly and the function argument must be
* an object type. * an object type.
*/ */
export function makeWeakCache<ArgT: Object, ResultT>( export function makeWeakCache<ArgT: {}, ResultT>(
handler: (ArgT, CacheConfigurator) => ResultT, handler: (ArgT, CacheConfigurator) => ResultT,
autoPermacache?: boolean, autoPermacache?: boolean,
): (ArgT) => ResultT { ): (ArgT) => ResultT {

View File

@ -1,5 +1,6 @@
export function getEnv(defaultValue = "development") { // @flow
export function getEnv(defaultValue: string = "development"): string {
return process.env.BABEL_ENV return process.env.BABEL_ENV
|| process.env.NODE_ENV || process.env.NODE_ENV
|| defaultValue; || defaultValue;

View File

@ -1,9 +1,11 @@
// @flow
import mergeWith from "lodash/mergeWith"; import mergeWith from "lodash/mergeWith";
export default function (dest?: Object, src?: Object): ?Object { export default function<T: {}>(dest?: T, src?: T) {
if (!dest || !src) return; if (!dest || !src) return;
return mergeWith(dest, src, function (a, b) { mergeWith(dest, src, function (a, b) {
if (b && Array.isArray(a)) { if (b && Array.isArray(a)) {
const newArray = b.slice(0); const newArray = b.slice(0);

View File

@ -1,14 +1,20 @@
// @flow
import type Plugin from "./plugin"; import type Plugin from "./plugin";
import manageOptions from "./option-manager"; import manageOptions from "./option-manager";
export type ResolvedConfig = { export type ResolvedConfig = {
options: Object, options: Object,
passes: Array<Array<Plugin>>, passes: Array<Array<[ Plugin, ?{} ]>>,
}; };
/** /**
* Standard API for loading Babel configuration data. Not for public consumption. * Standard API for loading Babel configuration data. Not for public consumption.
*/ */
export default function loadConfig(opts: Object): ResolvedConfig|null { export default function loadConfig(opts: mixed): ResolvedConfig|null {
return manageOptions(opts); if (opts != null && typeof opts !== "object") {
throw new Error("Babel options must be an object, null, or undefined");
}
return manageOptions(opts || {});
} }

View File

@ -10,7 +10,7 @@ import { makeStrongCache } from "../../caching";
type ConfigFile = { type ConfigFile = {
filepath: string, filepath: string,
dirname: string, dirname: string,
options: Object, options: {},
}; };
const BABELRC_FILENAME = ".babelrc"; const BABELRC_FILENAME = ".babelrc";

View File

@ -3,7 +3,7 @@
type ConfigFile = { type ConfigFile = {
filepath: string, filepath: string,
dirname: string, dirname: string,
options: Object, options: {},
}; };
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars

View File

@ -1,3 +1,5 @@
// @flow
import * as context from "../index"; import * as context from "../index";
import Plugin from "./plugin"; import Plugin from "./plugin";
import * as messages from "babel-messages"; import * as messages from "babel-messages";
@ -12,11 +14,11 @@ import clone from "lodash/clone";
import { loadPlugin, loadPreset, loadParser, loadGenerator } from "./loading/files"; import { loadPlugin, loadPreset, loadParser, loadGenerator } from "./loading/files";
type MergeOptions = { type MergeOptions = {
type: "arguments"|"options"|"preset", +type: "arguments"|"options"|"preset",
options?: Object, options: {},
alias: string, alias: string,
loc?: string, loc: string,
dirname?: string dirname: string
}; };
const optionNames = new Set([ const optionNames = new Set([
@ -70,7 +72,10 @@ const ALLOWED_PLUGIN_KEYS = new Set([
"inherits", "inherits",
]); ]);
export default function manageOptions(opts?: Object) { export default function manageOptions(opts: {}): {
options: Object,
passes: Array<Array<[ Plugin, ?{} ]>>,
}|null {
return new OptionManager().init(opts); return new OptionManager().init(opts);
} }
@ -81,7 +86,7 @@ class OptionManager {
} }
options: Object; options: Object;
passes: Array<Array<Plugin>>; passes: Array<Array<[Plugin, ?{}]>>;
/** /**
* This is called when we want to merge the input `opts` into the * This is called when we want to merge the input `opts` into the
@ -92,12 +97,19 @@ class OptionManager {
* - `dirname` is used to resolve plugins relative to it. * - `dirname` is used to resolve plugins relative to it.
*/ */
mergeOptions(config: MergeOptions, pass?: Array<Plugin>) { mergeOptions(config: MergeOptions, pass?: Array<[Plugin, ?{}]>) {
const result = loadConfig(config); const result = loadConfig(config);
const plugins = result.plugins.map((descriptor) => loadPluginDescriptor(descriptor)); const plugins = result.plugins.map((descriptor) => loadPluginDescriptor(descriptor));
const presets = result.presets.map((descriptor) => loadPresetDescriptor(descriptor)); const presets = result.presets.map((descriptor) => loadPresetDescriptor(descriptor));
if (
config.options.passPerPreset != null &&
typeof config.options.passPerPreset !== "boolean"
) {
throw new Error(".passPerPreset must be a boolean or undefined");
}
const passPerPreset = config.options.passPerPreset; const passPerPreset = config.options.passPerPreset;
pass = pass || this.passes[0]; pass = pass || this.passes[0];
@ -124,7 +136,7 @@ class OptionManager {
merge(this.options, result.options); merge(this.options, result.options);
} }
init(opts: Object = {}): Object { init(opts: {}) {
const configChain = buildConfigChain(opts); const configChain = buildConfigChain(opts);
if (!configChain) return null; if (!configChain) return null;
@ -136,7 +148,8 @@ class OptionManager {
// There are a few case where thrown errors will try to annotate themselves multiple times, so // There are a few case where thrown errors will try to annotate themselves multiple times, so
// to keep things simple we just bail out if re-wrapping the message. // to keep things simple we just bail out if re-wrapping the message.
if (!/^\[BABEL\]/.test(e.message)) { if (!/^\[BABEL\]/.test(e.message)) {
e.message = `[BABEL] ${opts.filename || "unknown"}: ${e.message}`; const filename = typeof opts.filename === "string" ? opts.filename : null;
e.message = `[BABEL] ${filename || "unknown"}: ${e.message}`;
} }
throw e; throw e;
@ -185,12 +198,28 @@ class OptionManager {
} }
} }
type BasicDescriptor = {
value: {}|Function,
options: ?{},
dirname: string,
alias: string,
loc: string,
};
/** /**
* Load and validate the given config into a set of options, plugins, and presets. * Load and validate the given config into a set of options, plugins, and presets.
*/ */
function loadConfig(config) { function loadConfig(config): {
options: {},
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
} {
const options = normalizeOptions(config); const options = normalizeOptions(config);
if (config.options.plugins != null && !Array.isArray(config.options.plugins)) {
throw new Error(".plugins should be an array, null, or undefined");
}
const plugins = (config.options.plugins || []).map((plugin, index) => { const plugins = (config.options.plugins || []).map((plugin, index) => {
const { filepath, value, options } = normalizePair(plugin, loadPlugin, config.dirname); const { filepath, value, options } = normalizePair(plugin, loadPlugin, config.dirname);
@ -202,6 +231,11 @@ function loadConfig(config) {
dirname: config.dirname, dirname: config.dirname,
}; };
}); });
if (config.options.presets != null && !Array.isArray(config.options.presets)) {
throw new Error(".presets should be an array, null, or undefined");
}
const presets = (config.options.presets || []).map((preset, index) => { const presets = (config.options.presets || []).map((preset, index) => {
const { filepath, value, options } = normalizePair(preset, loadPreset, config.dirname); const { filepath, value, options } = normalizePair(preset, loadPreset, config.dirname);
@ -259,19 +293,19 @@ function loadPluginDescriptor(descriptor) {
return [ result, descriptor.options]; return [ result, descriptor.options];
} }
function instantiatePlugin({ value: pluginObject, descriptor }) { function instantiatePlugin({ value: pluginObj, descriptor }) {
Object.keys(pluginObject).forEach((key) => { Object.keys(pluginObj).forEach((key) => {
if (!ALLOWED_PLUGIN_KEYS.has(key)) { if (!ALLOWED_PLUGIN_KEYS.has(key)) {
throw new Error(messages.get("pluginInvalidProperty", descriptor.alias, key)); throw new Error(messages.get("pluginInvalidProperty", descriptor.alias, key));
} }
}); });
if (pluginObject.visitor && (pluginObject.visitor.enter || pluginObject.visitor.exit)) { if (pluginObj.visitor && (pluginObj.visitor.enter || pluginObj.visitor.exit)) {
throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " + throw new Error("Plugins aren't allowed to specify catch-all enter/exit handlers. " +
"Please target individual nodes."); "Please target individual nodes.");
} }
const plugin = Object.assign({}, pluginObject, { const plugin = Object.assign({}, pluginObj, {
visitor: clone(pluginObject.visitor || {}), visitor: clone(pluginObj.visitor || {}),
}); });
traverse.explode(plugin.visitor); traverse.explode(plugin.visitor);
@ -284,6 +318,7 @@ function instantiatePlugin({ value: pluginObject, descriptor }) {
loc: descriptor.loc, loc: descriptor.loc,
value: plugin.inherits, value: plugin.inherits,
options: descriptor.options, options: descriptor.options,
dirname: descriptor.dirname,
}; };
inherits = loadPluginDescriptor(inheritsDescriptor)[0]; inherits = loadPluginDescriptor(inheritsDescriptor)[0];
@ -300,7 +335,7 @@ function instantiatePlugin({ value: pluginObject, descriptor }) {
/** /**
* Generate a config object that will act as the root of a new nested config. * Generate a config object that will act as the root of a new nested config.
*/ */
function loadPresetDescriptor(descriptor) { function loadPresetDescriptor(descriptor): MergeOptions {
return { return {
type: "preset", type: "preset",
options: loadDescriptor(descriptor).value, options: loadDescriptor(descriptor).value,
@ -375,13 +410,6 @@ function normalizeOptions(config) {
options.generatorOpts.generator = loadGenerator(options.generatorOpts.generator, config.dirname).value; options.generatorOpts.generator = loadGenerator(options.generatorOpts.generator, config.dirname).value;
} }
if (config.options.presets && !Array.isArray(config.options.presets)) {
throw new Error(`${alias}.presets should be an array`);
}
if (config.options.plugins && !Array.isArray(config.options.plugins)) {
throw new Error(`${alias}.plugins should be an array`);
}
delete options.passPerPreset; delete options.passPerPreset;
delete options.plugins; delete options.plugins;
delete options.presets; delete options.presets;
@ -392,15 +420,19 @@ function normalizeOptions(config) {
/** /**
* Given a plugin/preset item, resolve it into a standard format. * Given a plugin/preset item, resolve it into a standard format.
*/ */
function normalizePair(pair, resolver, dirname) { function normalizePair(pair: mixed, resolver, dirname): {
filepath: string|null,
value: {}|Function,
options: ?{},
} {
let options; let options;
let value = pair; let value = pair;
if (Array.isArray(value)) { if (Array.isArray(pair)) {
if (value.length > 2) { if (pair.length > 2) {
throw new Error(`Unexpected extra options ${JSON.stringify(value.slice(2))}.`); throw new Error(`Unexpected extra options ${JSON.stringify(pair.slice(2))}.`);
} }
[value, options] = value; [value, options] = pair;
} }
let filepath = null; let filepath = null;
@ -411,6 +443,10 @@ function normalizePair(pair, resolver, dirname) {
} = resolver(value, dirname)); } = resolver(value, dirname));
} }
if (!value) {
throw new Error(`Unexpected falsy value: ${String(value)}`);
}
if (typeof value === "object" && value.__esModule) { if (typeof value === "object" && value.__esModule) {
if (value.default) { if (value.default) {
value = value.default; value = value.default;
@ -419,13 +455,12 @@ function normalizePair(pair, resolver, dirname) {
} }
} }
if (!value) { if (typeof value !== "object" && typeof value !== "function") {
throw new Error(`Unexpected falsy value: ${value}`); throw new Error(`Unsupported format: ${typeof value}. Expected an object or a function.`);
} }
const type = typeof value; if (options != null && typeof options !== "object") {
if (type !== "object" && type !== "function") { throw new Error("Plugin/Preset options must be an object, null, or undefined");
throw new Error(`Unsupported format: ${type}. Expected an object or a function.`);
} }
return { filepath, value, options }; return { filepath, value, options };

View File

@ -1,5 +1,23 @@
// @flow
export default class Plugin { export default class Plugin {
constructor(plugin: Object, key?: string) { constructor(plugin: {}, key?: string) {
if (plugin.name != null && typeof plugin.name !== "string") {
throw new Error("Plugin .name must be a string, null, or undefined");
}
if (plugin.manipulateOptions != null && typeof plugin.manipulateOptions !== "function") {
throw new Error("Plugin .manipulateOptions must be a function, null, or undefined");
}
if (plugin.post != null && typeof plugin.post !== "function") {
throw new Error("Plugin .post must be a function, null, or undefined");
}
if (plugin.pre != null && typeof plugin.pre !== "function") {
throw new Error("Plugin .pre must be a function, null, or undefined");
}
if (plugin.visitor != null && typeof plugin.visitor !== "object") {
throw new Error("Plugin .visitor must be an object, null, or undefined");
}
this.key = plugin.name || key; this.key = plugin.name || key;
this.manipulateOptions = plugin.manipulateOptions; this.manipulateOptions = plugin.manipulateOptions;
@ -12,5 +30,5 @@ export default class Plugin {
manipulateOptions: ?Function; manipulateOptions: ?Function;
post: ?Function; post: ?Function;
pre: ?Function; pre: ?Function;
visitor: Object; visitor: ?{};
} }

View File

@ -1,3 +1,4 @@
// @flow
/* eslint max-len: "off" */ /* eslint max-len: "off" */
export default { export default {

View File

@ -1,3 +1,5 @@
{ {
"plugins": [["transform-react-jsx", "foo.bar"]] "plugins": [
["transform-react-jsx", {"pragma": "foo.bar"}]
]
} }