Merge pull request #7090 from loganfsmyth/chain-processing-rewrite

Refactor config processing more
This commit is contained in:
Logan Smyth 2017-12-21 19:57:07 -08:00 committed by GitHub
commit c9a00fbae8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1221 additions and 1140 deletions

View File

@ -23,7 +23,7 @@
"compiler"
],
"browser": {
"./lib/config/loading/files/index.js": "./lib/config/loading/files/index-browser.js",
"./lib/config/files/index.js": "./lib/config/files/index-browser.js",
"./lib/transform-file.js": "./lib/transform-file-browser.js",
"./lib/transform-file-sync.js": "./lib/transform-file-sync-browser.js"
},

View File

@ -1,687 +0,0 @@
// @flow
import path from "path";
import micromatch from "micromatch";
import buildDebug from "debug";
import {
validate,
type ValidatedOptions,
type PluginItem,
type PluginList,
type IgnoreList,
} from "./options";
const debug = buildDebug("babel:config:config-chain");
import {
loadPlugin,
loadPreset,
findConfigs,
loadConfig,
type ConfigFile,
} from "./loading/files";
import { makeWeakCache, makeStrongCache } from "./caching";
type ConfigItem = {
type: "arguments" | "env" | "file",
options: ValidatedOptions,
alias: string,
dirname: string,
};
export type ConfigChain = {
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
options: Array<ValidatedOptions>,
};
export type BasicDescriptor = {
name: string | void,
value: {} | Function,
options: {} | void | false,
dirname: string,
alias: string,
ownPass?: boolean,
};
export type PresetInstance = SimpleConfig;
type LoadedConfig = {
options: ValidatedOptions,
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
};
type ConfigPart =
| {
part: "config",
config: ConfigItem,
ignore: ?IgnoreList,
only: ?IgnoreList,
activeEnv: string | null,
}
| {
part: "extends",
path: string,
dirname: string,
activeEnv: string | null,
};
type SimpleConfig = {
options: ValidatedOptions,
alias: string,
dirname: string,
};
export const buildPresetChain = makeWeakCache(
(preset: PresetInstance): ConfigChain => {
const loaded = processConfig(preset);
return {
plugins: loaded.plugins,
presets: loaded.presets,
options: [loaded.options],
};
},
);
export function buildRootChain(
cwd: string,
opts: ValidatedOptions,
envName: string,
): ConfigChain | null {
const filename = opts.filename ? path.resolve(cwd, opts.filename) : null;
const builder = new ConfigChainBuilder(
filename ? new LoadedFile(filename) : null,
);
try {
builder.mergeConfigArguments(opts, cwd, envName);
// resolve all .babelrc files
if (opts.babelrc !== false && filename) {
findConfigs(path.dirname(filename), envName).forEach(configFile =>
builder.mergeConfigFile(configFile, envName),
);
}
} catch (e) {
if (e.code !== "BABEL_IGNORED_FILE") throw e;
return null;
}
return dedupLoadedConfigs(
builder.configs.reverse().map(config => processConfig(config)),
);
}
class ConfigChainBuilder {
file: LoadedFile | null;
configs: Array<ConfigItem> = [];
seenFiles: Set<ConfigFile> = new Set();
constructor(file: LoadedFile | null) {
this.file = file;
}
mergeConfigArguments(
opts: ValidatedOptions,
dirname: string,
envKey: string,
) {
flattenArgumentsOptionsParts(opts, dirname, envKey).forEach(part =>
this._processConfigPart(part, envKey),
);
}
mergeConfigFile(file: ConfigFile, envName: string) {
if (this.seenFiles.has(file)) {
throw new Error(
`Cycle detected in Babel configuration file through "${
file.filepath
}".`,
);
}
const parts = flattenFileOptionsParts(file)(envName);
this.seenFiles.add(file);
parts.forEach(part => this._processConfigPart(part, envName));
this.seenFiles.delete(file);
}
_processConfigPart(part: ConfigPart, envName: string) {
if (part.part === "config") {
const { ignore, only } = part;
// Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files.
if (
this.file &&
this.file.shouldIgnore(ignore, only, part.config.dirname)
) {
// TODO(logan): This is a really gross way to bail out. Avoid this in rewrite.
throw Object.assign((new Error("This file has been ignored."): any), {
code: "BABEL_IGNORED_FILE",
});
}
this.configs.push(part.config);
} else {
this.mergeConfigFile(
loadConfig(part.path, part.dirname, envName),
envName,
);
}
}
}
/**
* Given a plugin/preset item, resolve it into a standard format.
*/
function createDescriptor(
pair: PluginItem,
resolver,
dirname,
{
index,
alias,
ownPass,
}: {
index: number,
alias: string,
ownPass?: boolean,
},
): BasicDescriptor {
let name;
let options;
let value = pair;
if (Array.isArray(value)) {
if (value.length === 3) {
// $FlowIgnore - Flow doesn't like the multiple tuple types.
[value, options, name] = value;
} else {
[value, options] = value;
}
}
let filepath = null;
if (typeof value === "string") {
({ filepath, value } = 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;
} else {
throw new Error("Must export a default export when using ES6 modules.");
}
}
if (typeof value !== "object" && typeof value !== "function") {
throw new Error(
`Unsupported format: ${typeof value}. Expected an object or a function.`,
);
}
if (filepath !== null && typeof value === "object" && value) {
// We allow object values for plugins/presets nested directly within a
// config object, because it can be useful to define them in nested
// configuration contexts.
throw new Error(
"Plugin/Preset files are not allowed to export objects, only functions.",
);
}
return {
name,
alias: filepath || `${alias}$${index}`,
value,
options,
dirname,
ownPass,
};
}
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;
}
/**
* Load and validate the given config into a set of options, plugins, and presets.
*/
const processConfig = makeWeakCache((config: SimpleConfig): LoadedConfig => {
const plugins = (config.options.plugins || []).map((plugin, index) =>
createDescriptor(plugin, loadPlugin, config.dirname, {
index,
alias: config.alias,
}),
);
assertNoDuplicates(plugins);
const presets = (config.options.presets || []).map((preset, index) =>
createDescriptor(preset, loadPreset, config.dirname, {
index,
alias: config.alias,
ownPass: config.options.passPerPreset,
}),
);
assertNoDuplicates(presets);
return {
options: normalizeOptions(config.options),
plugins,
presets,
};
});
function assertNoDuplicates(items: Array<BasicDescriptor>): void {
const map = new Map();
for (const item of items) {
if (typeof item.value !== "function") continue;
let nameMap = map.get(item.value);
if (!nameMap) {
nameMap = new Set();
map.set(item.value, nameMap);
}
if (nameMap.has(item.name)) {
throw new Error(
[
`Duplicate plugin/preset detected.`,
`If you'd like to use two separate instances of a plugin,`,
`they neen separate names, e.g.`,
``,
` plugins: [`,
` ['some-plugin', {}],`,
` ['some-plugin', {}, 'some unique name'],`,
` ]`,
].join("\n"),
);
}
nameMap.add(item.name);
}
}
function dedupLoadedConfigs(items: Array<LoadedConfig>): ConfigChain {
const options = [];
const plugins = [];
const presets = [];
for (const item of items) {
plugins.push(...item.plugins);
presets.push(...item.presets);
options.push(item.options);
}
return {
options,
plugins: dedupDescriptors(plugins),
presets: dedupDescriptors(presets),
};
}
function dedupDescriptors(
items: Array<BasicDescriptor>,
): Array<BasicDescriptor> {
const map: Map<
Function,
Map<string | void, { value: BasicDescriptor | 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;
}, []);
}
/**
* Given the root config object passed to Babel, split it into the separate
* config parts. The resulting config objects in the 'ConfigPart' have their
* object identity preserved between calls so that they can be used for caching.
*/
function flattenArgumentsOptionsParts(
opts: ValidatedOptions,
dirname: string,
envName: string,
): Array<ConfigPart> {
const {
env,
plugins,
presets,
passPerPreset,
extends: extendsPath,
...options
} = opts;
const raw = [];
if (env) {
raw.push(...flattenArgumentsEnvOptionsParts(env)(dirname)(envName));
}
if (Object.keys(options).length > 0) {
raw.push(...flattenOptionsParts(buildArgumentsItem(options, dirname)));
}
if (plugins) {
raw.push(...flattenArgumentsPluginsOptionsParts(plugins)(dirname));
}
if (presets) {
raw.push(
...flattenArgumentsPresetsOptionsParts(presets)(!!passPerPreset)(dirname),
);
}
if (extendsPath != null) {
raw.push(
...flattenOptionsParts(
buildArgumentsItem({ extends: extendsPath }, dirname),
),
);
}
return raw;
}
/**
* For the top-level 'options' object, we cache the env list based on
* the object identity of the 'env' object.
*/
const flattenArgumentsEnvOptionsParts = makeWeakCache((env: {}) => {
const options: ValidatedOptions = { env };
return makeStrongCache((dirname: string) =>
flattenOptionsPartsLookup(buildArgumentsItem(options, dirname)),
);
});
/**
* For the top-level 'options' object, we cache the plugin list based on
* the object identity of the 'plugins' object.
*/
const flattenArgumentsPluginsOptionsParts = makeWeakCache(
(plugins: PluginList) => {
const options: ValidatedOptions = { plugins };
return makeStrongCache((dirname: string) =>
flattenOptionsParts(buildArgumentsItem(options, dirname)),
);
},
);
/**
* For the top-level 'options' object, we cache the preset list based on
* the object identity of the 'presets' object.
*/
const flattenArgumentsPresetsOptionsParts = makeWeakCache(
(presets: PluginList) =>
makeStrongCache((passPerPreset: boolean) => {
// The concept of passPerPreset is integrally tied to the preset list
// so unfortunately we need to copy both values here, adding an extra
// layer of caching functions.
const options = { presets, passPerPreset };
return makeStrongCache((dirname: string) =>
flattenOptionsParts(buildArgumentsItem(options, dirname)),
);
}),
);
function buildArgumentsItem(
options: ValidatedOptions,
dirname: string,
): ConfigItem {
return {
type: "arguments",
options,
alias: "base",
dirname,
};
}
/**
* Given a config from a specific file, return a list of ConfigPart objects
* with object identity preserved for all 'config' part objects for use
* with caching later in config processing.
*/
const flattenFileOptionsParts = makeWeakCache((file: ConfigFile) => {
return flattenOptionsPartsLookup({
type: "file",
options: validate("file", file.options),
alias: file.filepath,
dirname: file.dirname,
});
});
/**
* Given a config, create a function that will return the config parts for
* the environment passed as the first argument.
*/
function flattenOptionsPartsLookup(
config: ConfigItem,
): (string | null) => Array<ConfigPart> {
const parts = flattenOptionsParts(config);
const def = parts.filter(part => part.activeEnv === null);
const lookup = new Map();
parts.forEach(part => {
if (part.activeEnv !== null) lookup.set(part.activeEnv, []);
});
for (const [activeEnv, values] of lookup) {
parts.forEach(part => {
if (part.activeEnv === null || part.activeEnv === activeEnv) {
values.push(part);
}
});
}
return envName => lookup.get(envName) || def;
}
/**
* Given a generic config object, flatten it into its various parts so that
* then can be cached and processed later.
*/
function flattenOptionsParts(
config: ConfigItem,
activeEnv: string | null = null,
): Array<ConfigPart> {
const { options: rawOpts, alias, dirname } = config;
const parts = [];
if (rawOpts.env) {
for (const envKey of Object.keys(rawOpts.env)) {
if (rawOpts.env[envKey]) {
parts.push(
...flattenOptionsParts(
{
type: "env",
options: rawOpts.env[envKey],
alias: alias + `.env.${envKey}`,
dirname,
},
envKey,
),
);
}
}
}
parts.push({
part: "config",
config,
ignore: rawOpts.ignore,
only: rawOpts.only,
activeEnv,
});
if (rawOpts.extends != null) {
parts.push({
part: "extends",
path: rawOpts.extends,
dirname,
activeEnv,
});
}
return parts;
}
/**
* Track a given file and expose function to check if it should be ignored.
*/
class LoadedFile {
filename: string;
possibleDirs: null | Array<string> = null;
constructor(filename) {
this.filename = filename;
}
/**
* Tests if a filename should be ignored based on "ignore" and "only" options.
*/
shouldIgnore(
ignore: ?IgnoreList,
only: ?IgnoreList,
dirname: string,
): boolean {
if (ignore) {
if (this._matchesPatterns(ignore, dirname)) {
debug(
"Ignored %o because it matched one of %O from %o",
this.filename,
ignore,
dirname,
);
return true;
}
}
if (only) {
if (!this._matchesPatterns(only, dirname)) {
debug(
"Ignored %o because it failed to match one of %O from %o",
this.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.
*/
_matchesPatterns(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 = this.filename;
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 (!possibleDirs) {
possibleDirs = this.possibleDirs = [];
possibleDirs.push(filename);
let current = filename;
while (true) {
const previous = current;
current = path.dirname(current);
if (previous === current) break;
possibleDirs.push(current);
}
}
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;
}
}

View File

@ -14,9 +14,10 @@ type SimpleCacheConfiguratorObj = {
invalidate: <T>(handler: () => T) => T,
};
type CacheEntry<ResultT, SideChannel> = Array<
[ResultT, (SideChannel) => boolean],
>;
type CacheEntry<ResultT, SideChannel> = Array<{
value: ResultT,
valid: SideChannel => boolean,
}>;
export type { CacheConfigurator };
@ -64,7 +65,7 @@ function makeCachedFunction<
);
if (cachedValue) {
for (const [value, valid] of cachedValue) {
for (const { value, valid } of cachedValue) {
if (valid(data)) return value;
}
}
@ -79,18 +80,18 @@ function makeCachedFunction<
switch (cache.mode()) {
case "forever":
cachedValue = [[value, () => true]];
cachedValue = [{ value, valid: () => true }];
callCache.set(arg, cachedValue);
break;
case "invalidate":
cachedValue = [[value, cache.validator()]];
cachedValue = [{ value, valid: cache.validator() }];
callCache.set(arg, cachedValue);
break;
case "valid":
if (cachedValue) {
cachedValue.push([value, cache.validator()]);
cachedValue.push({ value, valid: cache.validator() });
} else {
cachedValue = [[value, cache.validator()]];
cachedValue = [{ value, valid: cache.validator() }];
callCache.set(arg, cachedValue);
}
}

View File

@ -0,0 +1,452 @@
// @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,
};
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 = makeWeakCache(
({ dirname, options, alias }: PresetInstance): ConfigChain => {
const result = createUncachedDescriptors(dirname, options, alias);
const { plugins, presets } = result;
return {
plugins: plugins(),
presets: presets(),
options: [normalizeOptions(result.options)],
};
},
);
/**
* Build a config chain for Babel's full root configuration.
*/
export function buildRootChain(
cwd: string,
opts: ValidatedOptions,
envName: string,
): ConfigChain | null {
const context = {
filename: opts.filename ? path.resolve(cwd, opts.filename) : null,
cwd,
envName,
};
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;
});

View File

@ -0,0 +1,272 @@
// @flow
import { loadPlugin, loadPreset } from "./files";
import {
makeWeakCache,
makeStrongCache,
type CacheConfigurator,
} from "./caching";
import type {
ValidatedOptions,
PluginList,
PluginItem,
} from "./validation/options";
// Represents a config object and functions to lazily load the descriptors
// for the plugins and presets so we don't load the plugins/presets unless
// the options object actually ends up being applicable.
export type OptionsAndDescriptors = {
options: ValidatedOptions,
plugins: () => Array<UnloadedDescriptor>,
presets: () => Array<UnloadedDescriptor>,
};
// Represents a plugin or presets at a given location in a config object.
// At this point these have been resolved to a specific object or function,
// but have not yet been executed to call functions with options.
export type UnloadedDescriptor = {
name: string | void,
value: {} | Function,
options: {} | void | false,
dirname: string,
alias: string,
ownPass?: boolean,
};
export type ValidatedFile = {
filepath: string,
dirname: string,
options: ValidatedOptions,
};
/**
* Create a set of descriptors from a given options object, preserving
* descriptor identity based on the identity of the plugin/preset arrays
* themselves.
*/
export function createCachedDescriptors(
dirname: string,
options: ValidatedOptions,
alias: string,
): OptionsAndDescriptors {
const { plugins, presets, passPerPreset } = options;
return {
options,
plugins: plugins
? () => createCachedPluginDescriptors(plugins, dirname)(alias)
: () => [],
presets: presets
? () =>
createCachedPresetDescriptors(presets, dirname)(alias)(
!!passPerPreset,
)
: () => [],
};
}
/**
* Create a set of descriptors from a given options object, with consistent
* identity for the descriptors, but not caching based on any specific identity.
*/
export function createUncachedDescriptors(
dirname: string,
options: ValidatedOptions,
alias: string,
): OptionsAndDescriptors {
// The returned result here is cached to represent a config object in
// memory, so we build and memoize the descriptors to ensure the same
// values are returned consistently.
let plugins;
let presets;
return {
options,
plugins: () => {
if (!plugins) {
plugins = createPluginDescriptors(
options.plugins || [],
dirname,
alias,
);
}
return plugins;
},
presets: () => {
if (!presets) {
presets = createPresetDescriptors(
options.presets || [],
dirname,
alias,
!!options.passPerPreset,
);
}
return presets;
},
};
}
const createCachedPresetDescriptors = makeWeakCache(
(items: PluginList, cache: CacheConfigurator<string>) => {
const dirname = cache.using(dir => dir);
return makeStrongCache((alias: string) =>
makeStrongCache((passPerPreset: boolean) =>
createPresetDescriptors(items, dirname, alias, passPerPreset),
),
);
},
);
const createCachedPluginDescriptors = makeWeakCache(
(items: PluginList, cache: CacheConfigurator<string>) => {
const dirname = cache.using(dir => dir);
return makeStrongCache((alias: string) =>
createPluginDescriptors(items, dirname, alias),
);
},
);
function createPresetDescriptors(
items: PluginList,
dirname: string,
alias: string,
passPerPreset: boolean,
): Array<UnloadedDescriptor> {
return createDescriptors("preset", items, dirname, alias, passPerPreset);
}
function createPluginDescriptors(
items: PluginList,
dirname: string,
alias: string,
): Array<UnloadedDescriptor> {
return createDescriptors("plugin", items, dirname, alias);
}
function createDescriptors(
type: "plugin" | "preset",
items: PluginList,
dirname: string,
alias: string,
ownPass?: boolean,
): Array<UnloadedDescriptor> {
const descriptors = items.map((item, index) =>
createDescriptor(
item,
type === "plugin" ? loadPlugin : loadPreset,
dirname,
{
index,
alias,
ownPass: !!ownPass,
},
),
);
assertNoDuplicates(descriptors);
return descriptors;
}
/**
* Given a plugin/preset item, resolve it into a standard format.
*/
function createDescriptor(
pair: PluginItem,
resolver,
dirname,
{
index,
alias,
ownPass,
}: {
index: number,
alias: string,
ownPass?: boolean,
},
): UnloadedDescriptor {
let name;
let options;
let value = pair;
if (Array.isArray(value)) {
if (value.length === 3) {
// $FlowIgnore - Flow doesn't like the multiple tuple types.
[value, options, name] = value;
} else {
[value, options] = value;
}
}
let filepath = null;
if (typeof value === "string") {
({ filepath, value } = 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;
} else {
throw new Error("Must export a default export when using ES6 modules.");
}
}
if (typeof value !== "object" && typeof value !== "function") {
throw new Error(
`Unsupported format: ${typeof value}. Expected an object or a function.`,
);
}
if (filepath !== null && typeof value === "object" && value) {
// We allow object values for plugins/presets nested directly within a
// config object, because it can be useful to define them in nested
// configuration contexts.
throw new Error(
"Plugin/Preset files are not allowed to export objects, only functions.",
);
}
return {
name,
alias: filepath || `${alias}$${index}`,
value,
options,
dirname,
ownPass,
};
}
function assertNoDuplicates(items: Array<UnloadedDescriptor>): void {
const map = new Map();
for (const item of items) {
if (typeof item.value !== "function") continue;
let nameMap = map.get(item.value);
if (!nameMap) {
nameMap = new Set();
map.set(item.value, nameMap);
}
if (nameMap.has(item.name)) {
throw new Error(
[
`Duplicate plugin/preset detected.`,
`If you'd like to use two separate instances of a plugin,`,
`they neen separate names, e.g.`,
``,
` plugins: [`,
` ['some-plugin', {}],`,
` ['some-plugin', {}, 'some unique name'],`,
` ]`,
].join("\n"),
);
}
nameMap.add(item.name);
}
}

View File

@ -5,7 +5,7 @@ import path from "path";
import fs from "fs";
import json5 from "json5";
import resolve from "resolve";
import { makeStrongCache, type CacheConfigurator } from "../../caching";
import { makeStrongCache, type CacheConfigurator } from "../caching";
const debug = buildDebug("babel:config:loading:files:configuration");
@ -15,68 +15,75 @@ export type ConfigFile = {
options: {},
};
export type IgnoreFile = {
filepath: string,
dirname: string,
ignore: Array<string>,
};
const BABELRC_FILENAME = ".babelrc";
const BABELRC_JS_FILENAME = ".babelrc.js";
const PACKAGE_FILENAME = "package.json";
const BABELIGNORE_FILENAME = ".babelignore";
export function findConfigs(
dirname: string,
export function findBabelrc(
filepath: string,
envName: string,
): Array<ConfigFile> {
let foundConfig = false;
let foundIgnore = false;
const confs = [];
): ConfigFile | null {
const dirname = path.dirname(filepath);
let loc = dirname;
while (true) {
if (!foundIgnore) {
const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME);
const ignore = readIgnoreConfig(ignoreLoc);
const conf = [
BABELRC_FILENAME,
BABELRC_JS_FILENAME,
PACKAGE_FILENAME,
].reduce((previousConfig: ConfigFile | null, name) => {
const filepath = path.join(loc, name);
const config = readConfig(filepath, envName);
if (ignore) {
debug("Found ignore %o from %o.", ignore.filepath, dirname);
confs.push(ignore);
foundIgnore = true;
if (config && previousConfig) {
throw new Error(
`Multiple configuration files found. Please remove one:\n` +
` - ${path.basename(previousConfig.filepath)}\n` +
` - ${name}\n` +
`from ${loc}`,
);
}
return config || previousConfig;
}, null);
if (conf) {
debug("Found configuration %o from %o.", conf.filepath, dirname);
return conf;
}
if (!foundConfig) {
const conf = [
BABELRC_FILENAME,
BABELRC_JS_FILENAME,
PACKAGE_FILENAME,
].reduce((previousConfig: ConfigFile | null, name) => {
const filepath = path.join(loc, name);
const config = readConfig(filepath, envName);
if (config && previousConfig) {
throw new Error(
`Multiple configuration files found. Please remove one:\n- ${path.basename(
previousConfig.filepath,
)}\n- ${name}\nfrom ${loc}`,
);
}
return config || previousConfig;
}, null);
if (conf) {
debug("Found configuration %o from %o.", conf.filepath, dirname);
confs.push(conf);
foundConfig = true;
}
}
if (foundIgnore && foundConfig) break;
if (loc === path.dirname(loc)) break;
loc = path.dirname(loc);
const nextLoc = path.dirname(loc);
if (loc === nextLoc) break;
loc = nextLoc;
}
return confs;
return null;
}
export function findBabelignore(filepath: string): IgnoreFile | null {
const dirname = path.dirname(filepath);
let loc = dirname;
while (true) {
const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME);
const ignore = readIgnoreConfig(ignoreLoc);
if (ignore) {
debug("Found ignore %o from %o.", ignore.filepath, dirname);
return ignore;
}
const nextLoc = path.dirname(loc);
if (loc === nextLoc) break;
loc = nextLoc;
}
return null;
}
export function loadConfig(
@ -224,7 +231,7 @@ const readIgnoreConfig = makeStaticFileCache((filepath, content) => {
return {
filepath,
dirname: path.dirname(filepath),
options: { ignore },
ignore,
};
});

View File

@ -6,9 +6,22 @@ export type ConfigFile = {
options: {},
};
export type IgnoreFile = {
filepath: string,
dirname: string,
ignore: Array<string>,
};
export function findBabelrc(
filepath: string,
envName: string, // eslint-disable-line no-unused-vars
): ConfigFile | null {
return null;
}
// eslint-disable-next-line no-unused-vars
export function findConfigs(dirname: string): Array<ConfigFile> {
return [];
export function findBabelignore(filepath: string): IgnoreFile | null {
return null;
}
export function loadConfig(name: string, dirname: string): ConfigFile {

View File

@ -1,9 +1,31 @@
// @flow
import type Plugin from "./plugin";
import manageOptions from "./option-manager";
import path from "path";
import * as context from "../index";
import Plugin from "./plugin";
import merge from "lodash/merge";
import {
buildRootChain,
buildPresetChain,
type ConfigChain,
type PresetInstance,
} from "./config-chain";
import type { UnloadedDescriptor } from "./config-descriptors";
import traverse from "@babel/traverse";
import clone from "lodash/clone";
import { makeWeakCache, type CacheConfigurator } from "./caching";
import { getEnv } from "./helpers/environment";
import { validate } from "./validation/options";
import { validatePluginObject } from "./validation/plugins";
export type { InputOptions } from "./options";
type LoadedDescriptor = {
value: {},
options: {},
dirname: string,
alias: string,
};
export type { InputOptions } from "./validation/options";
export type ResolvedConfig = {
options: Object,
@ -14,13 +36,257 @@ export type { Plugin };
export type PluginPassList = Array<Plugin>;
export type PluginPasses = Array<PluginPassList>;
/**
* Standard API for loading Babel configuration data. Not for public consumption.
*/
export default function loadConfig(opts: mixed): ResolvedConfig | null {
if (opts != null && (typeof opts !== "object" || Array.isArray(opts))) {
export default function loadConfig(inputOpts: mixed): ResolvedConfig | null {
if (
inputOpts != null &&
(typeof inputOpts !== "object" || Array.isArray(inputOpts))
) {
throw new Error("Babel options must be an object, null, or undefined");
}
return manageOptions(opts || {});
const args = inputOpts ? validate("arguments", inputOpts) : {};
const { envName = getEnv(), cwd = "." } = args;
const absoluteCwd = path.resolve(cwd);
const configChain = buildRootChain(absoluteCwd, args, envName);
if (!configChain) return null;
const optionDefaults = {};
const options = {};
const passes = [[]];
try {
(function recurseDescriptors(
config: {
plugins: Array<UnloadedDescriptor>,
presets: Array<UnloadedDescriptor>,
},
pass: Array<Plugin>,
envName: string,
) {
const plugins = config.plugins.map(descriptor =>
loadPluginDescriptor(descriptor, envName),
);
const presets = config.presets.map(descriptor => {
return {
preset: loadPresetDescriptor(descriptor, envName),
pass: descriptor.ownPass ? [] : pass,
};
});
// resolve presets
if (presets.length > 0) {
// The passes are created in the same order as the preset list, but are inserted before any
// existing additional passes.
passes.splice(
1,
0,
...presets.map(o => o.pass).filter(p => p !== pass),
);
for (const { preset, pass } of presets) {
recurseDescriptors(
{
plugins: preset.plugins,
presets: preset.presets,
},
pass,
envName,
);
preset.options.forEach(opts => {
merge(optionDefaults, opts);
});
}
}
// resolve plugins
if (plugins.length > 0) {
pass.unshift(...plugins);
}
})(
{
plugins: configChain.plugins,
presets: configChain.presets,
},
passes[0],
envName,
);
configChain.options.forEach(opts => {
merge(options, opts);
});
} catch (e) {
// 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] ${args.filename || "unknown"}: ${e.message}`;
}
throw e;
}
const opts: Object = merge(optionDefaults, options);
// Tack the passes onto the object itself so that, if this object is passed back to Babel a second time,
// it will be in the right structure to not change behavior.
opts.babelrc = false;
opts.plugins = passes[0];
opts.presets = passes
.slice(1)
.filter(plugins => plugins.length > 0)
.map(plugins => ({ plugins }));
opts.passPerPreset = opts.presets.length > 0;
opts.envName = envName;
opts.cwd = absoluteCwd;
return {
options: opts,
passes: passes,
};
}
/**
* Load a generic plugin/preset from the given descriptor loaded from the config object.
*/
const loadDescriptor = makeWeakCache(
(
{ value, options, dirname, alias }: UnloadedDescriptor,
cache: CacheConfigurator<{ envName: string }>,
): LoadedDescriptor => {
// Disabled presets should already have been filtered out
if (options === false) throw new Error("Assertion failure");
options = options || {};
let item = value;
if (typeof value === "function") {
const api = Object.assign(Object.create(context), {
cache: cache.simple(),
env: () => cache.using(data => data.envName),
async: () => false,
});
try {
item = value(api, options, dirname);
} catch (e) {
if (alias) {
e.message += ` (While processing: ${JSON.stringify(alias)})`;
}
throw e;
}
}
if (!item || typeof item !== "object") {
throw new Error("Plugin/Preset did not return an object.");
}
if (typeof item.then === "function") {
throw new Error(
`You appear to be using an async plugin, ` +
`which your current version of Babel does not support.` +
`If you're using a published plugin, ` +
`you may need to upgrade your @babel/core version.`,
);
}
return { value: item, options, dirname, alias };
},
);
/**
* Instantiate a plugin for the given descriptor, returning the plugin/options pair.
*/
function loadPluginDescriptor(
descriptor: UnloadedDescriptor,
envName: string,
): Plugin {
if (descriptor.value instanceof Plugin) {
if (descriptor.options) {
throw new Error(
"Passed options to an existing Plugin instance will not work.",
);
}
return descriptor.value;
}
return instantiatePlugin(loadDescriptor(descriptor, { envName }), {
envName,
});
}
const instantiatePlugin = makeWeakCache(
(
{ value, options, dirname, alias }: LoadedDescriptor,
cache: CacheConfigurator<{ envName: string }>,
): Plugin => {
const pluginObj = validatePluginObject(value);
const plugin = Object.assign({}, pluginObj);
if (plugin.visitor) {
plugin.visitor = traverse.explode(clone(plugin.visitor));
}
if (plugin.inherits) {
const inheritsDescriptor = {
name: undefined,
alias: `${alias}$inherits`,
value: plugin.inherits,
options,
dirname,
};
// If the inherited plugin changes, reinstantiate this plugin.
const inherits = cache.invalidate(data =>
loadPluginDescriptor(inheritsDescriptor, data.envName),
);
plugin.pre = chain(inherits.pre, plugin.pre);
plugin.post = chain(inherits.post, plugin.post);
plugin.manipulateOptions = chain(
inherits.manipulateOptions,
plugin.manipulateOptions,
);
plugin.visitor = traverse.visitors.merge([
inherits.visitor || {},
plugin.visitor || {},
]);
}
return new Plugin(plugin, options, alias);
},
);
/**
* Generate a config object that will act as the root of a new nested config.
*/
const loadPresetDescriptor = (
descriptor: UnloadedDescriptor,
envName: string,
): ConfigChain => {
return buildPresetChain(
instantiatePreset(loadDescriptor(descriptor, { envName })),
);
};
const instantiatePreset = makeWeakCache(
({ value, dirname, alias }: LoadedDescriptor): PresetInstance => {
return {
options: validate("preset", value),
alias,
dirname,
};
},
);
function chain(a, b) {
const fns = [a, b].filter(Boolean);
if (fns.length <= 1) return fns[0];
return function(...args) {
for (const fn of fns) {
fn.apply(this, args);
}
};
}

View File

@ -1,277 +0,0 @@
// @flow
import path from "path";
import * as context from "../index";
import Plugin, { validatePluginObject } from "./plugin";
import merge from "lodash/merge";
import {
buildRootChain,
buildPresetChain,
type ConfigChain,
type BasicDescriptor,
type PresetInstance,
} from "./build-config-chain";
import traverse from "@babel/traverse";
import clone from "lodash/clone";
import { makeWeakCache, type CacheConfigurator } from "./caching";
import { getEnv } from "./helpers/environment";
import { validate } from "./options";
export default function manageOptions(inputOpts: {}): {
options: Object,
passes: Array<Array<Plugin>>,
} | null {
const args = validate("arguments", inputOpts);
const { envName = getEnv(), cwd = "." } = args;
const absoluteCwd = path.resolve(cwd);
const configChain = buildRootChain(absoluteCwd, args, envName);
if (!configChain) return null;
const optionDefaults = {};
const options = {};
const passes = [[]];
try {
(function recurseDescriptors(
config: {
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
},
pass: Array<Plugin>,
envName: string,
) {
const plugins = config.plugins.map(descriptor =>
loadPluginDescriptor(descriptor, envName),
);
const presets = config.presets.map(descriptor => {
return {
preset: loadPresetDescriptor(descriptor, envName),
pass: descriptor.ownPass ? [] : pass,
};
});
// resolve presets
if (presets.length > 0) {
// The passes are created in the same order as the preset list, but are inserted before any
// existing additional passes.
passes.splice(
1,
0,
...presets.map(o => o.pass).filter(p => p !== pass),
);
for (const { preset, pass } of presets) {
recurseDescriptors(
{
plugins: preset.plugins,
presets: preset.presets,
},
pass,
envName,
);
preset.options.forEach(opts => {
merge(optionDefaults, opts);
});
}
}
// resolve plugins
if (plugins.length > 0) {
pass.unshift(...plugins);
}
})(
{
plugins: configChain.plugins,
presets: configChain.presets,
},
passes[0],
envName,
);
configChain.options.forEach(opts => {
merge(options, opts);
});
} catch (e) {
// 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] ${args.filename || "unknown"}: ${e.message}`;
}
throw e;
}
const opts: Object = merge(optionDefaults, options);
// Tack the passes onto the object itself so that, if this object is passed back to Babel a second time,
// it will be in the right structure to not change behavior.
opts.babelrc = false;
opts.plugins = passes[0];
opts.presets = passes
.slice(1)
.filter(plugins => plugins.length > 0)
.map(plugins => ({ plugins }));
opts.passPerPreset = opts.presets.length > 0;
opts.envName = envName;
opts.cwd = absoluteCwd;
return {
options: opts,
passes: passes,
};
}
type LoadedDescriptor = {
value: {},
options: {},
dirname: string,
alias: string,
};
/**
* Load a generic plugin/preset from the given descriptor loaded from the config object.
*/
const loadDescriptor = makeWeakCache(
(
{ value, options, dirname, alias }: BasicDescriptor,
cache: CacheConfigurator<{ envName: string }>,
): LoadedDescriptor => {
// Disabled presets should already have been filtered out
if (options === false) throw new Error("Assertion failure");
options = options || {};
let item = value;
if (typeof value === "function") {
const api = Object.assign(Object.create(context), {
cache: cache.simple(),
env: () => cache.using(data => data.envName),
async: () => false,
});
try {
item = value(api, options, dirname);
} catch (e) {
if (alias) {
e.message += ` (While processing: ${JSON.stringify(alias)})`;
}
throw e;
}
}
if (!item || typeof item !== "object") {
throw new Error("Plugin/Preset did not return an object.");
}
if (typeof item.then === "function") {
throw new Error(
`You appear to be using an async plugin, ` +
`which your current version of Babel does not support.` +
`If you're using a published plugin, ` +
`you may need to upgrade your @babel/core version.`,
);
}
return { value: item, options, dirname, alias };
},
);
/**
* Instantiate a plugin for the given descriptor, returning the plugin/options pair.
*/
function loadPluginDescriptor(
descriptor: BasicDescriptor,
envName: string,
): Plugin {
if (descriptor.value instanceof Plugin) {
if (descriptor.options) {
throw new Error(
"Passed options to an existing Plugin instance will not work.",
);
}
return descriptor.value;
}
return instantiatePlugin(loadDescriptor(descriptor, { envName }), {
envName,
});
}
const instantiatePlugin = makeWeakCache(
(
{ value, options, dirname, alias }: LoadedDescriptor,
cache: CacheConfigurator<{ envName: string }>,
): Plugin => {
const pluginObj = validatePluginObject(value);
const plugin = Object.assign({}, pluginObj);
if (plugin.visitor) {
plugin.visitor = traverse.explode(clone(plugin.visitor));
}
if (plugin.inherits) {
const inheritsDescriptor = {
name: undefined,
alias: `${alias}$inherits`,
value: plugin.inherits,
options,
dirname,
};
// If the inherited plugin changes, reinstantiate this plugin.
const inherits = cache.invalidate(data =>
loadPluginDescriptor(inheritsDescriptor, data.envName),
);
plugin.pre = chain(inherits.pre, plugin.pre);
plugin.post = chain(inherits.post, plugin.post);
plugin.manipulateOptions = chain(
inherits.manipulateOptions,
plugin.manipulateOptions,
);
plugin.visitor = traverse.visitors.merge([
inherits.visitor || {},
plugin.visitor || {},
]);
}
return new Plugin(plugin, options, alias);
},
);
/**
* Generate a config object that will act as the root of a new nested config.
*/
const loadPresetDescriptor = (
descriptor: BasicDescriptor,
envName: string,
): ConfigChain => {
return buildPresetChain(
instantiatePreset(loadDescriptor(descriptor, { envName })),
);
};
const instantiatePreset = makeWeakCache(
({ value, dirname, alias }: LoadedDescriptor): PresetInstance => {
return {
type: "preset",
options: validate("preset", value),
alias,
dirname,
};
},
);
function chain(a, b) {
const fns = [a, b].filter(Boolean);
if (fns.length <= 1) return fns[0];
return function(...args) {
for (const fn of fns) {
fn.apply(this, args);
}
};
}

View File

@ -1,100 +1,6 @@
// @flow
import {
assertString,
assertFunction,
assertObject,
type ValidatorSet,
type Validator,
} from "./option-assertions";
// Note: The casts here are just meant to be static assertions to make sure
// that the assertion functions actually assert that the value's type matches
// the declared types.
const VALIDATORS: ValidatorSet = {
name: (assertString: Validator<$PropertyType<PluginObject, "name">>),
manipulateOptions: (assertFunction: Validator<
$PropertyType<PluginObject, "manipulateOptions">,
>),
pre: (assertFunction: Validator<$PropertyType<PluginObject, "pre">>),
post: (assertFunction: Validator<$PropertyType<PluginObject, "post">>),
inherits: (assertFunction: Validator<
$PropertyType<PluginObject, "inherits">,
>),
visitor: (assertVisitorMap: Validator<
$PropertyType<PluginObject, "visitor">,
>),
parserOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "parserOverride">,
>),
generatorOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "generatorOverride">,
>),
};
function assertVisitorMap(key: string, value: mixed): VisitorMap {
const obj = assertObject(key, value);
if (obj) {
Object.keys(obj).forEach(prop => assertVisitorHandler(prop, obj[prop]));
if (obj.enter || obj.exit) {
throw new Error(
`.${key} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`,
);
}
}
return (obj: any);
}
function assertVisitorHandler(
key: string,
value: mixed,
): VisitorHandler | void {
if (value && typeof value === "object") {
Object.keys(value).forEach(handler => {
if (handler !== "enter" && handler !== "exit") {
throw new Error(
`.visitor["${key}"] may only have .enter and/or .exit handlers.`,
);
}
});
} else if (typeof value !== "function") {
throw new Error(`.visitor["${key}"] must be a function`);
}
return (value: any);
}
type VisitorHandler = Function | { enter?: Function, exit?: Function };
export type VisitorMap = {
[string]: VisitorHandler,
};
export type PluginObject = {
name?: string,
manipulateOptions?: Function,
pre?: Function,
post?: Function,
inherits?: Function,
visitor?: VisitorMap,
parserOverride?: Function,
generatorOverride?: Function,
};
export function validatePluginObject(obj: {}): PluginObject {
Object.keys(obj).forEach(key => {
const validator = VALIDATORS[key];
if (validator) validator(key, obj[key]);
else throw new Error(`.${key} is not a valid Plugin property`);
});
return (obj: any);
}
import type { PluginObject } from "./validation/plugins";
export default class Plugin {
key: ?string;

View File

@ -0,0 +1,95 @@
import {
assertString,
assertFunction,
assertObject,
type ValidatorSet,
type Validator,
} from "./option-assertions";
// Note: The casts here are just meant to be static assertions to make sure
// that the assertion functions actually assert that the value's type matches
// the declared types.
const VALIDATORS: ValidatorSet = {
name: (assertString: Validator<$PropertyType<PluginObject, "name">>),
manipulateOptions: (assertFunction: Validator<
$PropertyType<PluginObject, "manipulateOptions">,
>),
pre: (assertFunction: Validator<$PropertyType<PluginObject, "pre">>),
post: (assertFunction: Validator<$PropertyType<PluginObject, "post">>),
inherits: (assertFunction: Validator<
$PropertyType<PluginObject, "inherits">,
>),
visitor: (assertVisitorMap: Validator<
$PropertyType<PluginObject, "visitor">,
>),
parserOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "parserOverride">,
>),
generatorOverride: (assertFunction: Validator<
$PropertyType<PluginObject, "generatorOverride">,
>),
};
function assertVisitorMap(key: string, value: mixed): VisitorMap {
const obj = assertObject(key, value);
if (obj) {
Object.keys(obj).forEach(prop => assertVisitorHandler(prop, obj[prop]));
if (obj.enter || obj.exit) {
throw new Error(
`.${key} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`,
);
}
}
return (obj: any);
}
function assertVisitorHandler(
key: string,
value: mixed,
): VisitorHandler | void {
if (value && typeof value === "object") {
Object.keys(value).forEach(handler => {
if (handler !== "enter" && handler !== "exit") {
throw new Error(
`.visitor["${key}"] may only have .enter and/or .exit handlers.`,
);
}
});
} else if (typeof value !== "function") {
throw new Error(`.visitor["${key}"] must be a function`);
}
return (value: any);
}
type VisitorHandler = Function | { enter?: Function, exit?: Function };
export type VisitorMap = {
[string]: VisitorHandler,
};
export type PluginObject = {
name?: string,
manipulateOptions?: Function,
pre?: Function,
post?: Function,
inherits?: Function,
visitor?: VisitorMap,
parserOverride?: Function,
generatorOverride?: Function,
};
export function validatePluginObject(obj: {}): PluginObject {
Object.keys(obj).forEach(key => {
const validator = VALIDATORS[key];
if (validator) validator(key, obj[key]);
else throw new Error(`.${key} is not a valid Plugin property`);
});
return (obj: any);
}

View File

@ -4,7 +4,7 @@ export { default as File } from "./transformation/file/file";
export {
default as buildExternalHelpers,
} from "./tools/build-external-helpers";
export { resolvePlugin, resolvePreset } from "./config/loading/files";
export { resolvePlugin, resolvePreset } from "./config/files";
export { version } from "../package.json";
export { getEnv } from "./config/helpers/environment";

View File

@ -208,8 +208,8 @@ to enable [parsing|transformation].
*/
export default function generateMissingPluginMessage(
missingPluginName: string,
loc,
codeFrame,
loc: { line: number, column: number },
codeFrame: string,
): string {
let helpMessage =
`Support for the experimental syntax '${missingPluginName}' isn't currently enabled ` +

View File

@ -194,17 +194,50 @@ describe("buildConfigChain", function() {
assert.notEqual(opts1.plugins[0], opts2.plugins[1]);
});
it("should cache the env options by identity", () => {
const env = {
foo: {
plugins: plugins1,
it("should cache the env plugins by identity", () => {
const plugins = [() => ({})];
const opts1 = loadOptions({
envName: "foo",
env: {
foo: {
plugins,
},
},
};
});
const opts2 = loadOptions({
envName: "foo",
env: {
foo: {
plugins,
},
},
});
const opts1 = loadOptions({ envName: "foo", env });
assert.equal(opts1.plugins.length, 1);
assert.equal(opts2.plugins.length, 1);
assert.equal(opts1.plugins[0], opts2.plugins[0]);
});
env.foo.plugins = plugins2;
const opts2 = loadOptions({ envName: "foo", env });
it("should cache the env presets by identity", () => {
const presets = [() => ({ plugins: [() => ({})] })];
const opts1 = loadOptions({
envName: "foo",
env: {
foo: {
presets,
},
},
});
const opts2 = loadOptions({
envName: "foo",
env: {
foo: {
presets,
},
},
});
assert.equal(opts1.plugins.length, 1);
assert.equal(opts2.plugins.length, 1);