Skip to content

Commit

Permalink
Restrict .babelrc resolution to within a given package.
Browse files Browse the repository at this point in the history
  • Loading branch information
loganfsmyth committed Apr 21, 2018
1 parent e45b58d commit f013dab
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 80 deletions.
13 changes: 8 additions & 5 deletions packages/babel-core/src/config/config-chain.js
Expand Up @@ -13,6 +13,7 @@ import {
const debug = buildDebug("babel:config:config-chain");

import {
findPackageData,
findRelativeConfig,
loadConfig,
type ConfigFile,
Expand Down Expand Up @@ -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,
));

Expand Down
121 changes: 54 additions & 67 deletions packages/babel-core/src/config/files/configuration.js
Expand Up @@ -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<string>,
};

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);
Expand All @@ -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);
}
Expand All @@ -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 };
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -228,27 +236,6 @@ const readIgnoreConfig = makeStaticFileCache((filepath, content) => {
};
});

function makeStaticFileCache<T>(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
Expand Down
30 changes: 24 additions & 6 deletions packages/babel-core/src/config/files/index-browser.js
@@ -1,17 +1,35 @@
// @flow

import type { ConfigFile, IgnoreFile, RelativeConfig } from "./configuration";

export type { ConfigFile, IgnoreFile, RelativeConfig };
import type {
ConfigFile,
IgnoreFile,
RelativeConfig,
FilePackageData,
} from "./types";

export type { ConfigFile, IgnoreFile, RelativeConfig, FilePackageData };

export function findPackageData(filepath: string): FilePackageData {
return {
filepath,
directories: [],
pkg: null,
isPackage: false,
};
}

export function findRelativeConfig(
filepath: string,
pkgData: FilePackageData,
envName: string, // eslint-disable-line no-unused-vars
): RelativeConfig {
return { config: null, ignore: null };
return { pkg: null, config: null, ignore: null };
}

export function loadConfig(name: string, dirname: string): ConfigFile {
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`);
}

Expand Down
17 changes: 15 additions & 2 deletions packages/babel-core/src/config/files/index.js
Expand Up @@ -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<indexBrowserType>): $Exact<indexType>);

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";
60 changes: 60 additions & 0 deletions 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,
};
},
);
38 changes: 38 additions & 0 deletions 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<string>,
};

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<string>,

// 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,
};

0 comments on commit f013dab

Please sign in to comment.