diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js index cf0e3eaf01..2ca2a5c3bf 100644 --- a/packages/babel-core/src/config/config-chain.js +++ b/packages/babel-core/src/config/config-chain.js @@ -13,6 +13,7 @@ import { const debug = buildDebug("babel:config:config-chain"); import { + findPackageData, findRelativeConfig, loadConfig, type ConfigFile, @@ -125,15 +126,17 @@ export function buildRootChain( ); if (!programmaticChain) return null; - let ignore, babelrc; + const pkgData = + typeof context.filename === "string" + ? findPackageData(context.filename) + : null; + let ignore, babelrc; const fileChain = emptyChain(); // resolve all .babelrc files - if (opts.babelrc !== false && context.filename !== null) { - const filename = context.filename; - + if (opts.babelrc !== false && pkgData) { ({ ignore, config: babelrc } = findRelativeConfig( - filename, + pkgData, context.envName, )); diff --git a/packages/babel-core/src/config/files/configuration.js b/packages/babel-core/src/config/files/configuration.js index c1a55cc598..ccc300ae48 100644 --- a/packages/babel-core/src/config/files/configuration.js +++ b/packages/babel-core/src/config/files/configuration.js @@ -5,45 +5,33 @@ import path from "path"; import fs from "fs"; import json5 from "json5"; import resolve from "resolve"; -import { makeStrongCache, type CacheConfigurator } from "../caching"; +import { + makeStrongCache, + makeWeakCache, + type CacheConfigurator, +} from "../caching"; import makeAPI from "../helpers/config-api"; +import { makeStaticFileCache } from "./utils"; +import type { FilePackageData, RelativeConfig, ConfigFile } from "./types"; const debug = buildDebug("babel:config:loading:files:configuration"); -export type ConfigFile = { - filepath: string, - dirname: string, - options: {}, -}; - -export type IgnoreFile = { - filepath: string, - dirname: string, - ignore: Array, -}; - -export type RelativeConfig = { - config: ConfigFile | null, - ignore: IgnoreFile | null, -}; - const BABELRC_FILENAME = ".babelrc"; const BABELRC_JS_FILENAME = ".babelrc.js"; -const PACKAGE_FILENAME = "package.json"; const BABELIGNORE_FILENAME = ".babelignore"; export function findRelativeConfig( - filepath: string, + packageData: FilePackageData, envName: string, ): RelativeConfig { let config = null; let ignore = null; - const dirname = path.dirname(filepath); - let loc = dirname; - while (true) { + const dirname = path.dirname(packageData.filepath); + + for (const loc of packageData.directories) { if (!config) { - config = [BABELRC_FILENAME, BABELRC_JS_FILENAME, PACKAGE_FILENAME].reduce( + config = [BABELRC_FILENAME, BABELRC_JS_FILENAME].reduce( (previousConfig: ConfigFile | null, name) => { const filepath = path.join(loc, name); const config = readConfig(filepath, envName); @@ -62,6 +50,23 @@ export function findRelativeConfig( null, ); + const pkgConfig = + packageData.pkg && packageData.pkg.dirname === loc + ? packageToBabelConfig(packageData.pkg) + : 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); } @@ -75,10 +80,6 @@ export function findRelativeConfig( debug("Found ignore %o from %o.", ignore.filepath, dirname); } } - - const nextLoc = path.dirname(loc); - if (loc === nextLoc) break; - loc = nextLoc; } return { config, ignore }; @@ -107,7 +108,7 @@ export function loadConfig( function readConfig(filepath, envName): ConfigFile | null { return path.extname(filepath) === ".js" ? readConfigJS(filepath, { envName }) - : readConfigFile(filepath); + : readConfigJSON5(filepath); } const LOADING_CONFIGS = new Set(); @@ -180,27 +181,34 @@ const readConfigJS = makeStrongCache( }, ); -const readConfigFile = makeStaticFileCache((filepath, content) => { - let options; - if (path.basename(filepath) === PACKAGE_FILENAME) { - try { - options = JSON.parse(content).babel; - } catch (err) { - err.message = `${filepath}: Error while parsing JSON - ${err.message}`; - throw err; - } - if (!options) return null; - } else { - try { - options = json5.parse(content); - } catch (err) { - err.message = `${filepath}: Error while parsing config - ${err.message}`; - throw err; +const packageToBabelConfig = makeWeakCache( + (file: ConfigFile): ConfigFile | null => { + if (typeof file.options.babel === "undefined") return null; + const babel = file.options.babel; + + if (typeof babel !== "object" || Array.isArray(babel) || babel === null) { + throw new Error(`${file.filepath}: .babel property must be an object`); } - if (!options) throw new Error(`${filepath}: No config detected`); + return { + filepath: file.filepath, + dirname: file.dirname, + options: babel, + }; + }, +); + +const readConfigJSON5 = makeStaticFileCache((filepath, content) => { + let options; + try { + options = 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}`); } @@ -228,27 +236,6 @@ const readIgnoreConfig = makeStaticFileCache((filepath, content) => { }; }); -function makeStaticFileCache(fn: (string, string) => T): string => T | null { - return makeStrongCache((filepath, cache) => { - if (cache.invalidate(() => fileMtime(filepath)) === null) { - cache.forever(); - return null; - } - - return fn(filepath, fs.readFileSync(filepath, "utf8")); - }); -} - -function fileMtime(filepath: string): number | null { - try { - return +fs.statSync(filepath).mtime; - } catch (e) { - if (e.code !== "ENOENT") throw e; - } - - return null; -} - function throwConfigError() { throw new Error(`\ Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured diff --git a/packages/babel-core/src/config/files/index-browser.js b/packages/babel-core/src/config/files/index-browser.js index 4e5cb5fe62..9805b9676c 100644 --- a/packages/babel-core/src/config/files/index-browser.js +++ b/packages/babel-core/src/config/files/index-browser.js @@ -1,17 +1,35 @@ // @flow -import type { ConfigFile, IgnoreFile, RelativeConfig } from "./configuration"; +import type { + ConfigFile, + IgnoreFile, + RelativeConfig, + FilePackageData, +} from "./types"; -export type { ConfigFile, IgnoreFile, RelativeConfig }; +export type { ConfigFile, IgnoreFile, RelativeConfig, FilePackageData }; -export function findRelativeConfig( - filepath: string, - envName: string, // eslint-disable-line no-unused-vars -): RelativeConfig { - return { config: null, ignore: null }; +export function findPackageData(filepath: string): FilePackageData { + return { + filepath, + directories: [], + pkg: null, + isPackage: false, + }; } -export function loadConfig(name: string, dirname: string): ConfigFile { +export function findRelativeConfig( + pkgData: FilePackageData, + envName: string, // eslint-disable-line no-unused-vars +): RelativeConfig { + return { pkg: null, config: null, ignore: null }; +} + +export function loadConfig( + name: string, + dirname: string, + envName: string, // eslint-disable-line no-unused-vars +): ConfigFile { throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`); } diff --git a/packages/babel-core/src/config/files/index.js b/packages/babel-core/src/config/files/index.js index c7e61ad469..06e8ce1fb2 100644 --- a/packages/babel-core/src/config/files/index.js +++ b/packages/babel-core/src/config/files/index.js @@ -7,5 +7,18 @@ import typeof * as indexType from "./index"; // exports of index-browser, since this file may be replaced at bundle time with index-browser. ((({}: any): $Exact): $Exact); -export * from "./configuration"; -export * from "./plugins"; +export { findPackageData } from "./package"; + +export { findRelativeConfig, loadConfig } from "./configuration"; +export type { + ConfigFile, + IgnoreFile, + RelativeConfig, + FilePackageData, +} from "./types"; +export { + resolvePlugin, + resolvePreset, + loadPlugin, + loadPreset, +} from "./plugins"; diff --git a/packages/babel-core/src/config/files/package.js b/packages/babel-core/src/config/files/package.js new file mode 100644 index 0000000000..8e370a472d --- /dev/null +++ b/packages/babel-core/src/config/files/package.js @@ -0,0 +1,60 @@ +// @flow + +import path from "path"; +import { makeStaticFileCache } from "./utils"; + +import type { ConfigFile, FilePackageData } from "./types"; + +const PACKAGE_FILENAME = "package.json"; + +/** + * Find metadata about the package that this file is inside of. Resolution + * of Babel's config requires general package information to decide when to + * search for .babelrc files + */ +export function findPackageData(filepath: string): FilePackageData { + let pkg = null; + const directories = []; + let isPackage = true; + + let dirname = path.dirname(filepath); + while (!pkg && path.basename(dirname) !== "node_modules") { + directories.push(dirname); + + pkg = readConfigPackage(path.join(dirname, PACKAGE_FILENAME)); + + const nextLoc = path.dirname(dirname); + if (dirname === nextLoc) { + isPackage = false; + break; + } + dirname = nextLoc; + } + + return { filepath, directories, pkg, isPackage }; +} + +const readConfigPackage = makeStaticFileCache( + (filepath, content): ConfigFile => { + let options; + try { + options = JSON.parse(content); + } catch (err) { + err.message = `${filepath}: Error while parsing JSON - ${err.message}`; + throw err; + } + + 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, + }; + }, +); diff --git a/packages/babel-core/src/config/files/types.js b/packages/babel-core/src/config/files/types.js new file mode 100644 index 0000000000..fa9d96fe5d --- /dev/null +++ b/packages/babel-core/src/config/files/types.js @@ -0,0 +1,38 @@ +// @flow + +export type ConfigFile = { + filepath: string, + dirname: string, + options: {}, +}; + +export type IgnoreFile = { + filepath: string, + dirname: string, + ignore: Array, +}; + +export type RelativeConfig = { + // The actual config, either from package.json#babel, .babelrc, or + // .babelrc.js, if there was one. + config: ConfigFile | null, + + // The .babelignore, if there was one. + ignore: IgnoreFile | null, +}; + +export type FilePackageData = { + // The file in the package. + filepath: string, + + // Any ancestor directories of the file that are within the package. + directories: Array, + + // The contents of the package.json. May not be found if the package just + // terminated at a node_modules folder without finding one. + pkg: ConfigFile | null, + + // True if a package.json or node_modules folder was found while traversing + // the directory structure. + isPackage: boolean, +}; diff --git a/packages/babel-core/src/config/files/utils.js b/packages/babel-core/src/config/files/utils.js new file mode 100644 index 0000000000..db202259d5 --- /dev/null +++ b/packages/babel-core/src/config/files/utils.js @@ -0,0 +1,27 @@ +// @flow + +import fs from "fs"; +import { makeStrongCache } from "../caching"; + +export function makeStaticFileCache( + fn: (string, string) => T, +): string => T | null { + return makeStrongCache((filepath, cache) => { + if (cache.invalidate(() => fileMtime(filepath)) === null) { + cache.forever(); + return null; + } + + return fn(filepath, fs.readFileSync(filepath, "utf8")); + }); +} + +function fileMtime(filepath: string): number | null { + try { + return +fs.statSync(filepath).mtime; + } catch (e) { + if (e.code !== "ENOENT" && e.code !== "ENOTDIR") throw e; + } + + return null; +}