diff --git a/.flowconfig b/.flowconfig index 1f7415a495..28afe4ecff 100644 --- a/.flowconfig +++ b/.flowconfig @@ -9,6 +9,7 @@ packages/*/src lib/file.js lib/parser.js lib/types.js +lib/third-party-libs.js.flow [options] strip_root=true diff --git a/lib/third-party-libs.js.flow b/lib/third-party-libs.js.flow new file mode 100644 index 0000000000..7e6fd06d5d --- /dev/null +++ b/lib/third-party-libs.js.flow @@ -0,0 +1,9 @@ +/** + * Basic declarations for the npm modules we use. + */ + +declare module "micromatch" { + declare function exports(Array, Array, ?{ + nocase: boolean, + }): Array; +} diff --git a/packages/babel-core/src/config/build-config-chain.js b/packages/babel-core/src/config/build-config-chain.js index 68b97b1dda..7c9813f14d 100644 --- a/packages/babel-core/src/config/build-config-chain.js +++ b/packages/babel-core/src/config/build-config-chain.js @@ -1,10 +1,24 @@ -import * as babel from "../index"; +// @flow + +import { getEnv } from "./helpers/environment"; import path from "path"; import micromatch from "micromatch"; 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|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 builder = new ConfigChainBuilder(filename); @@ -17,7 +31,7 @@ export default function buildConfigChain(opts: Object = {}) { }); // resolve all .babelrc files - if (opts.babelrc !== false) { + if (opts.babelrc !== false && filename) { builder.findConfigs(filename); } } catch (e) { @@ -30,6 +44,10 @@ export default function buildConfigChain(opts: Object = {}) { } class ConfigChainBuilder { + filename: string|null; + configs: Array; + possibleDirs: null|Array; + constructor(filename) { this.configs = []; this.filename = filename; @@ -40,59 +58,68 @@ class ConfigChainBuilder { * Tests if a filename should be ignored based on "ignore" and "only" options. */ shouldIgnore( - ignore: Array, - only?: Array, + ignore: mixed, + only: mixed, dirname: string, ): boolean { if (!this.filename) return false; - if (ignore && !Array.isArray(ignore)) { - throw new Error(`.ignore should be an array, ${JSON.stringify(ignore)} given`); + if (ignore) { + 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)) { - throw new Error(`.only should be an array, ${JSON.stringify(only)} given`); + if (only) { + 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)) || - (only && !this.matchesPatterns(only, dirname)); + return false; } /** * Returns result of calling function with filename if pattern is a function. * Otherwise returns result of matching pattern Regex with filename. */ - matchesPatterns(patterns: Array, dirname: string) { + matchesPatterns(patterns: Array, dirname: string) { + const filename = this.filename; + if (!filename) throw new Error("Assertion failure: .filename should always exist here"); + const res = []; const strings = []; const fns = []; patterns.forEach((pattern) => { - const type = typeof pattern; - if (type === "string") strings.push(pattern); - else if (type === "function") fns.push(pattern); - else res.push(pattern); + if (typeof pattern === "string") strings.push(pattern); + else if (typeof pattern === "function") fns.push(pattern); + else if (pattern instanceof RegExp) 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 (fns.some((fn) => fn(this.filename))) return true; + if (res.some((re) => re.test(filename))) return true; + if (fns.some((fn) => fn(filename))) return true; if (strings.length > 0) { + let possibleDirs = this.possibleDirs; // Lazy-init so we don't initialize this for files that have no glob patterns. - if (!this.possibleDirs) { - this.possibleDirs = []; + if (!possibleDirs) { + possibleDirs = this.possibleDirs = []; - if (this.filename) { - this.possibleDirs.push(this.filename); + possibleDirs.push(filename); - let current = this.filename; - while (true) { - const previous = current; - current = path.dirname(current); - if (previous === current) break; + let current = filename; + while (true) { + const previous = current; + current = path.dirname(current); + if (previous === current) break; - this.possibleDirs.push(current); - } + possibleDirs.push(current); } } @@ -104,7 +131,7 @@ class ConfigChainBuilder { 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; } } @@ -113,12 +140,6 @@ class ConfigChainBuilder { } findConfigs(loc: string) { - if (!loc) return; - - if (!path.isAbsolute(loc)) { - loc = path.join(process.cwd(), loc); - } - findConfigs(path.dirname(loc)).forEach(({ filepath, dirname, options }) => { this.mergeConfig({ type: "options", @@ -131,31 +152,33 @@ class ConfigChainBuilder { mergeConfig({ type, - options, + options: rawOpts, alias, - loc, 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. - 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. - 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 - const envKey = babel.getEnv(); - if (options.env) { - const envOpts = options.env[envKey]; - delete options.env; + if (rawOpts.env != null && (typeof rawOpts.env !== "object" || Array.isArray(rawOpts.env))) { + throw new Error(".env block must be an object, null, or undefined"); + } + 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({ type, options: envOpts, @@ -168,13 +191,14 @@ class ConfigChainBuilder { type, options, alias, - loc, + loc: alias, dirname, }); - // add extends clause - if (options.extends) { - const extendsConfig = loadConfig(options.extends, dirname); + if (rawOpts.extends) { + if (typeof rawOpts.extends !== "string") throw new Error(".extends must be a string"); + + const extendsConfig = loadConfig(rawOpts.extends, dirname); const existingConfig = this.configs.some((config) => { return config.alias === extendsConfig.filepath; @@ -187,7 +211,6 @@ class ConfigChainBuilder { dirname: extendsConfig.dirname, }); } - delete options.extends; } } } diff --git a/packages/babel-core/src/config/caching.js b/packages/babel-core/src/config/caching.js index 219cf304d4..7f27735874 100644 --- a/packages/babel-core/src/config/caching.js +++ b/packages/babel-core/src/config/caching.js @@ -31,7 +31,7 @@ export function makeStrongCache( * configures its caching behavior. Cached values are stored weakly and the function argument must be * an object type. */ -export function makeWeakCache( +export function makeWeakCache( handler: (ArgT, CacheConfigurator) => ResultT, autoPermacache?: boolean, ): (ArgT) => ResultT { diff --git a/packages/babel-core/src/config/helpers/environment.js b/packages/babel-core/src/config/helpers/environment.js index c5081fdfe0..eaa95de791 100644 --- a/packages/babel-core/src/config/helpers/environment.js +++ b/packages/babel-core/src/config/helpers/environment.js @@ -1,5 +1,6 @@ -export function getEnv(defaultValue = "development") { +// @flow +export function getEnv(defaultValue: string = "development"): string { return process.env.BABEL_ENV || process.env.NODE_ENV || defaultValue; diff --git a/packages/babel-core/src/config/helpers/merge.js b/packages/babel-core/src/config/helpers/merge.js index 1a01e22889..344f3e181c 100644 --- a/packages/babel-core/src/config/helpers/merge.js +++ b/packages/babel-core/src/config/helpers/merge.js @@ -1,9 +1,11 @@ +// @flow + import mergeWith from "lodash/mergeWith"; -export default function (dest?: Object, src?: Object): ?Object { +export default function(dest?: T, src?: T) { if (!dest || !src) return; - return mergeWith(dest, src, function (a, b) { + mergeWith(dest, src, function (a, b) { if (b && Array.isArray(a)) { const newArray = b.slice(0); diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index a37b7bc55b..d0a6030d75 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -1,14 +1,20 @@ +// @flow + import type Plugin from "./plugin"; import manageOptions from "./option-manager"; export type ResolvedConfig = { options: Object, - passes: Array>, + passes: Array>, }; /** * Standard API for loading Babel configuration data. Not for public consumption. */ -export default function loadConfig(opts: Object): ResolvedConfig|null { - return manageOptions(opts); +export default function loadConfig(opts: mixed): ResolvedConfig|null { + if (opts != null && typeof opts !== "object") { + throw new Error("Babel options must be an object, null, or undefined"); + } + + return manageOptions(opts || {}); } diff --git a/packages/babel-core/src/config/loading/files/configuration.js b/packages/babel-core/src/config/loading/files/configuration.js index 4827f7300d..3d762ff8e3 100644 --- a/packages/babel-core/src/config/loading/files/configuration.js +++ b/packages/babel-core/src/config/loading/files/configuration.js @@ -10,7 +10,7 @@ import { makeStrongCache } from "../../caching"; type ConfigFile = { filepath: string, dirname: string, - options: Object, + options: {}, }; const BABELRC_FILENAME = ".babelrc"; diff --git a/packages/babel-core/src/config/loading/files/index-browser.js b/packages/babel-core/src/config/loading/files/index-browser.js index b07fe3b2de..302c2e9c66 100644 --- a/packages/babel-core/src/config/loading/files/index-browser.js +++ b/packages/babel-core/src/config/loading/files/index-browser.js @@ -3,7 +3,7 @@ type ConfigFile = { filepath: string, dirname: string, - options: Object, + options: {}, }; // eslint-disable-next-line no-unused-vars diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index caf3ead145..456cdc43c1 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -1,3 +1,5 @@ +// @flow + import * as context from "../index"; import Plugin from "./plugin"; import * as messages from "babel-messages"; @@ -12,11 +14,11 @@ import clone from "lodash/clone"; import { loadPlugin, loadPreset, loadParser, loadGenerator } from "./loading/files"; type MergeOptions = { - type: "arguments"|"options"|"preset", - options?: Object, + +type: "arguments"|"options"|"preset", + options: {}, alias: string, - loc?: string, - dirname?: string + loc: string, + dirname: string }; const optionNames = new Set([ @@ -70,7 +72,10 @@ const ALLOWED_PLUGIN_KEYS = new Set([ "inherits", ]); -export default function manageOptions(opts?: Object) { +export default function manageOptions(opts: {}): { + options: Object, + passes: Array>, +}|null { return new OptionManager().init(opts); } @@ -81,7 +86,7 @@ class OptionManager { } options: Object; - passes: Array>; + passes: Array>; /** * 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. */ - mergeOptions(config: MergeOptions, pass?: Array) { + mergeOptions(config: MergeOptions, pass?: Array<[Plugin, ?{}]>) { const result = loadConfig(config); const plugins = result.plugins.map((descriptor) => loadPluginDescriptor(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; pass = pass || this.passes[0]; @@ -124,7 +136,7 @@ class OptionManager { merge(this.options, result.options); } - init(opts: Object = {}): Object { + init(opts: {}) { const configChain = buildConfigChain(opts); 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 // to keep things simple we just bail out if re-wrapping the 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; @@ -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. */ -function loadConfig(config) { +function loadConfig(config): { + options: {}, + plugins: Array, + presets: Array, +} { 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 { filepath, value, options } = normalizePair(plugin, loadPlugin, config.dirname); @@ -202,6 +231,11 @@ function loadConfig(config) { 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 { filepath, value, options } = normalizePair(preset, loadPreset, config.dirname); @@ -259,19 +293,19 @@ function loadPluginDescriptor(descriptor) { return [ result, descriptor.options]; } -function instantiatePlugin({ value: pluginObject, descriptor }) { - Object.keys(pluginObject).forEach((key) => { +function instantiatePlugin({ value: pluginObj, descriptor }) { + Object.keys(pluginObj).forEach((key) => { if (!ALLOWED_PLUGIN_KEYS.has(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. " + "Please target individual nodes."); } - const plugin = Object.assign({}, pluginObject, { - visitor: clone(pluginObject.visitor || {}), + const plugin = Object.assign({}, pluginObj, { + visitor: clone(pluginObj.visitor || {}), }); traverse.explode(plugin.visitor); @@ -284,6 +318,7 @@ function instantiatePlugin({ value: pluginObject, descriptor }) { loc: descriptor.loc, value: plugin.inherits, options: descriptor.options, + dirname: descriptor.dirname, }; 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. */ -function loadPresetDescriptor(descriptor) { +function loadPresetDescriptor(descriptor): MergeOptions { return { type: "preset", options: loadDescriptor(descriptor).value, @@ -375,13 +410,6 @@ function normalizeOptions(config) { 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.plugins; delete options.presets; @@ -392,15 +420,19 @@ function normalizeOptions(config) { /** * 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 value = pair; - if (Array.isArray(value)) { - if (value.length > 2) { - throw new Error(`Unexpected extra options ${JSON.stringify(value.slice(2))}.`); + if (Array.isArray(pair)) { + if (pair.length > 2) { + throw new Error(`Unexpected extra options ${JSON.stringify(pair.slice(2))}.`); } - [value, options] = value; + [value, options] = pair; } let filepath = null; @@ -411,6 +443,10 @@ function normalizePair(pair, resolver, dirname) { } = resolver(value, dirname)); } + if (!value) { + throw new Error(`Unexpected falsy value: ${String(value)}`); + } + if (typeof value === "object" && value.__esModule) { if (value.default) { value = value.default; @@ -419,13 +455,12 @@ function normalizePair(pair, resolver, dirname) { } } - if (!value) { - throw new Error(`Unexpected falsy value: ${value}`); + if (typeof value !== "object" && typeof value !== "function") { + throw new Error(`Unsupported format: ${typeof value}. Expected an object or a function.`); } - const type = typeof value; - if (type !== "object" && type !== "function") { - throw new Error(`Unsupported format: ${type}. Expected an object or a function.`); + if (options != null && typeof options !== "object") { + throw new Error("Plugin/Preset options must be an object, null, or undefined"); } return { filepath, value, options }; diff --git a/packages/babel-core/src/config/plugin.js b/packages/babel-core/src/config/plugin.js index 5db1e1dd00..7e6036ec0f 100644 --- a/packages/babel-core/src/config/plugin.js +++ b/packages/babel-core/src/config/plugin.js @@ -1,5 +1,23 @@ +// @flow + 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.manipulateOptions = plugin.manipulateOptions; @@ -12,5 +30,5 @@ export default class Plugin { manipulateOptions: ?Function; post: ?Function; pre: ?Function; - visitor: Object; + visitor: ?{}; } diff --git a/packages/babel-core/src/config/removed.js b/packages/babel-core/src/config/removed.js index c9ccd740a1..542f7d1533 100644 --- a/packages/babel-core/src/config/removed.js +++ b/packages/babel-core/src/config/removed.js @@ -1,3 +1,4 @@ +// @flow /* eslint max-len: "off" */ export default { diff --git a/packages/babel-plugin-transform-react-jsx/test/fixtures/react/honor-custom-jsx-comment-if-jsx-pragma-option-set/options.json b/packages/babel-plugin-transform-react-jsx/test/fixtures/react/honor-custom-jsx-comment-if-jsx-pragma-option-set/options.json index f668e3e74e..bd858cfcb2 100644 --- a/packages/babel-plugin-transform-react-jsx/test/fixtures/react/honor-custom-jsx-comment-if-jsx-pragma-option-set/options.json +++ b/packages/babel-plugin-transform-react-jsx/test/fixtures/react/honor-custom-jsx-comment-if-jsx-pragma-option-set/options.json @@ -1,3 +1,5 @@ { - "plugins": [["transform-react-jsx", "foo.bar"]] + "plugins": [ + ["transform-react-jsx", {"pragma": "foo.bar"}] + ] }