babel/packages/babel-core/src/config/config-chain.js
2018-01-05 12:47:47 -08:00

457 lines
11 KiB
JavaScript

// @flow
import path from "path";
import micromatch from "micromatch";
import buildDebug from "debug";
import {
validate,
type ValidatedOptions,
type IgnoreList,
} from "./validation/options";
const debug = buildDebug("babel:config:config-chain");
import {
findBabelrc,
findBabelignore,
loadConfig,
type ConfigFile,
} from "./files";
import { makeWeakCache, makeStrongCache } from "./caching";
import {
createCachedDescriptors,
createUncachedDescriptors,
type UnloadedDescriptor,
type OptionsAndDescriptors,
type ValidatedFile,
} from "./config-descriptors";
export type ConfigChain = {
plugins: Array<UnloadedDescriptor>,
presets: Array<UnloadedDescriptor>,
options: Array<ValidatedOptions>,
};
export type PresetInstance = {
options: ValidatedOptions,
alias: string,
dirname: string,
};
export type ConfigContext = {
filename: string | null,
cwd: string,
envName: string,
};
type ConfigContextNamed = {
...ConfigContext,
filename: string,
};
/**
* Build a config chain for a given preset.
*/
export const buildPresetChain: (
arg: PresetInstance,
context: *,
) => * = makeChainWalker({
init: arg => arg,
root: preset => loadPresetDescriptors(preset),
env: (preset, envName) => loadPresetEnvDescriptors(preset)(envName),
});
const loadPresetDescriptors = makeWeakCache((preset: PresetInstance) =>
buildRootDescriptors(preset, preset.alias, createUncachedDescriptors),
);
const loadPresetEnvDescriptors = makeWeakCache((preset: PresetInstance) =>
makeStrongCache((envName: string) =>
buildEnvDescriptors(
preset,
preset.alias,
createUncachedDescriptors,
envName,
),
),
);
/**
* Build a config chain for Babel's full root configuration.
*/
export function buildRootChain(
cwd: string,
opts: ValidatedOptions,
context: ConfigContext,
): ConfigChain | null {
const programmaticChain = loadProgrammaticChain(
{
options: opts,
dirname: context.cwd,
},
context,
);
if (!programmaticChain) return null;
const fileChain = emptyChain();
// resolve all .babelrc files
if (opts.babelrc !== false && context.filename !== null) {
const filename = context.filename;
const babelrcFile = findBabelrc(filename, context.envName);
if (babelrcFile) {
const result = loadFileChain(babelrcFile, context);
if (!result) return null;
mergeChain(fileChain, result);
}
const babelignoreFile = findBabelignore(filename);
if (
babelignoreFile &&
shouldIgnore(
context,
babelignoreFile.ignore,
null,
babelignoreFile.dirname,
)
) {
return null;
}
}
// Insert file chain in front so programmatic options have priority
// over configuration file chain items.
const chain = mergeChain(
mergeChain(emptyChain(), fileChain),
programmaticChain,
);
return {
plugins: dedupDescriptors(chain.plugins),
presets: dedupDescriptors(chain.presets),
options: chain.options.map(o => normalizeOptions(o)),
};
}
/**
* Build a config chain for just the programmatic options passed into Babel.
*/
const loadProgrammaticChain = makeChainWalker({
init: arg => arg,
root: input => buildRootDescriptors(input, "base", createCachedDescriptors),
env: (input, envName) =>
buildEnvDescriptors(input, "base", createCachedDescriptors, envName),
});
/**
* Build a config chain for a given file.
*/
const loadFileChain = makeChainWalker({
init: input => validateFile(input),
root: file => loadFileDescriptors(file),
env: (file, envName) => loadFileEnvDescriptors(file)(envName),
});
const validateFile = makeWeakCache((file: ConfigFile): ValidatedFile => ({
filepath: file.filepath,
dirname: file.dirname,
options: validate("file", file.options),
}));
const loadFileDescriptors = makeWeakCache((file: ValidatedFile) =>
buildRootDescriptors(file, file.filepath, createUncachedDescriptors),
);
const loadFileEnvDescriptors = makeWeakCache((file: ValidatedFile) =>
makeStrongCache((envName: string) =>
buildEnvDescriptors(
file,
file.filepath,
createUncachedDescriptors,
envName,
),
),
);
function buildRootDescriptors({ dirname, options }, alias, descriptors) {
return descriptors(dirname, options, alias);
}
function buildEnvDescriptors(
{ dirname, options },
alias,
descriptors,
envName,
) {
const opts = options.env && options.env[envName];
return opts ? descriptors(dirname, opts, `${alias}.env["${envName}"]`) : null;
}
function makeChainWalker<
ArgT,
InnerT: { options: ValidatedOptions, dirname: string },
>({
init,
root,
env,
}: {
init: ArgT => InnerT,
root: InnerT => OptionsAndDescriptors,
env: (InnerT, string) => OptionsAndDescriptors | null,
}): (ArgT, ConfigContext, Set<ConfigFile> | void) => ConfigChain | null {
return (arg, context, files = new Set()) => {
const input = init(arg);
const { dirname } = input;
const flattenedConfigs = [];
const rootOpts = root(input);
flattenedConfigs.push(rootOpts);
const envOpts = env(input, context.envName);
if (envOpts) {
flattenedConfigs.push(envOpts);
}
// Process 'ignore' and 'only' before 'extends' items are processed so
// that we don't do extra work loading extended configs if a file is
// ignored.
if (
flattenedConfigs.some(({ options: { ignore, only } }) =>
shouldIgnore(context, ignore, only, dirname),
)
) {
return null;
}
const chain = emptyChain();
for (const op of flattenedConfigs) {
if (!mergeExtendsChain(chain, op.options, dirname, context, files)) {
return null;
}
mergeChainOpts(chain, op);
}
return chain;
};
}
function mergeExtendsChain(
chain: ConfigChain,
opts: ValidatedOptions,
dirname: string,
context: ConfigContext,
files: Set<ConfigFile>,
): boolean {
if (opts.extends === undefined) return true;
const file = loadConfig(opts.extends, dirname, context.envName);
if (files.has(file)) {
throw new Error(
`Configuration cycle detected loading ${file.filepath}.\n` +
`File already loaded following the config chain:\n` +
Array.from(files, file => ` - ${file.filepath}`).join("\n"),
);
}
files.add(file);
const fileChain = loadFileChain(file, context, files);
files.delete(file);
if (!fileChain) return false;
mergeChain(chain, fileChain);
return true;
}
function mergeChain(target: ConfigChain, source: ConfigChain): ConfigChain {
target.options.push(...source.options);
target.plugins.push(...source.plugins);
target.presets.push(...source.presets);
return target;
}
function mergeChainOpts(
target: ConfigChain,
{ options, plugins, presets }: OptionsAndDescriptors,
): ConfigChain {
target.options.push(options);
target.plugins.push(...plugins());
target.presets.push(...presets());
return target;
}
function emptyChain(): ConfigChain {
return {
options: [],
presets: [],
plugins: [],
};
}
function normalizeOptions(opts: ValidatedOptions): ValidatedOptions {
const options = Object.assign({}, opts);
delete options.extends;
delete options.env;
delete options.plugins;
delete options.presets;
delete options.passPerPreset;
delete options.ignore;
delete options.only;
// "sourceMap" is just aliased to sourceMap, so copy it over as
// we merge the options together.
if (options.sourceMap) {
options.sourceMaps = options.sourceMap;
delete options.sourceMap;
}
return options;
}
function dedupDescriptors(
items: Array<UnloadedDescriptor>,
): Array<UnloadedDescriptor> {
const map: Map<
Function,
Map<string | void, { value: UnloadedDescriptor | null }>,
> = new Map();
const descriptors = [];
for (const item of items) {
if (typeof item.value === "function") {
const fnKey = item.value;
let nameMap = map.get(fnKey);
if (!nameMap) {
nameMap = new Map();
map.set(fnKey, nameMap);
}
let desc = nameMap.get(item.name);
if (!desc) {
desc = { value: null };
descriptors.push(desc);
// Treat passPerPreset presets as unique, skipping them
// in the merge processing steps.
if (!item.ownPass) nameMap.set(item.name, desc);
}
if (item.options === false) {
desc.value = null;
} else {
desc.value = item;
}
} else {
descriptors.push({ value: item });
}
}
return descriptors.reduce((acc, desc) => {
if (desc.value) acc.push(desc.value);
return acc;
}, []);
}
/**
* Tests if a filename should be ignored based on "ignore" and "only" options.
*/
function shouldIgnore(
context: ConfigContext,
ignore: ?IgnoreList,
only: ?IgnoreList,
dirname: string,
): boolean {
if (context.filename === null) return false;
// $FlowIgnore - Flow refinements aren't quite smart enough for this :(
const ctx: ConfigContextNamed = context;
if (ignore) {
if (matchesPatterns(ctx, ignore, dirname)) {
debug(
"Ignored %o because it matched one of %O from %o",
context.filename,
ignore,
dirname,
);
return true;
}
}
if (only) {
if (!matchesPatterns(ctx, only, dirname)) {
debug(
"Ignored %o because it failed to match one of %O from %o",
context.filename,
only,
dirname,
);
return true;
}
}
return false;
}
/**
* Returns result of calling function with filename if pattern is a function.
* Otherwise returns result of matching pattern Regex with filename.
*/
function matchesPatterns(
context: ConfigContextNamed,
patterns: IgnoreList,
dirname: string,
): boolean {
const res = [];
const strings = [];
const fns = [];
patterns.forEach(pattern => {
if (typeof pattern === "string") strings.push(pattern);
else if (typeof pattern === "function") fns.push(pattern);
else res.push(pattern);
});
const filename = context.filename;
if (res.some(re => re.test(context.filename))) return true;
if (fns.some(fn => fn(filename))) return true;
if (strings.length > 0) {
const possibleDirs = getPossibleDirs(context);
const absolutePatterns = strings.map(pattern => {
// Preserve the "!" prefix so that micromatch can use it for negation.
const negate = pattern[0] === "!";
if (negate) pattern = pattern.slice(1);
return (negate ? "!" : "") + path.resolve(dirname, pattern);
});
if (
micromatch(possibleDirs, absolutePatterns, { nocase: true }).length > 0
) {
return true;
}
}
return false;
}
const getPossibleDirs = makeWeakCache((context: ConfigContextNamed) => {
let current = context.filename;
if (current === null) return [];
const possibleDirs = [current];
while (true) {
const previous = current;
current = path.dirname(current);
if (previous === current) break;
possibleDirs.push(current);
}
return possibleDirs;
});