Merge pull request #7090 from loganfsmyth/chain-processing-rewrite
Refactor config processing more
This commit is contained in:
commit
c9a00fbae8
@ -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"
|
||||
},
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
452
packages/babel-core/src/config/config-chain.js
Normal file
452
packages/babel-core/src/config/config-chain.js
Normal 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;
|
||||
});
|
||||
272
packages/babel-core/src/config/config-descriptors.js
Normal file
272
packages/babel-core/src/config/config-descriptors.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
95
packages/babel-core/src/config/validation/plugins.js
Normal file
95
packages/babel-core/src/config/validation/plugins.js
Normal 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);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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 ` +
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user