Move descriptor merging into config chain processing.

This commit is contained in:
Logan Smyth
2017-12-18 10:54:30 -08:00
parent 5f8a1a2613
commit 4afbc02476
2 changed files with 270 additions and 245 deletions

View File

@@ -6,23 +6,53 @@ import buildDebug from "debug";
import {
validate,
type ValidatedOptions,
type PluginItem,
type PluginList,
type IgnoreList,
} from "./options";
const debug = buildDebug("babel:config:config-chain");
import { findConfigs, loadConfig, type ConfigFile } from "./loading/files";
import {
loadPlugin,
loadPreset,
findConfigs,
loadConfig,
type ConfigFile,
} from "./loading/files";
import { makeWeakCache, makeStrongCache } from "./caching";
export type ConfigItem = {
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",
@@ -38,11 +68,29 @@ type ConfigPart =
activeEnv: string | null,
};
export default function buildConfigChain(
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,
): Array<ConfigItem> | null {
): ConfigChain | null {
const filename = opts.filename ? path.resolve(cwd, opts.filename) : null;
const builder = new ConfigChainBuilder(
filename ? new LoadedFile(filename) : null,
@@ -63,7 +111,9 @@ export default function buildConfigChain(
return null;
}
return builder.configs.reverse();
return dedupLoadedConfigs(
builder.configs.reverse().map(config => processConfig(config)),
);
}
class ConfigChainBuilder {
@@ -126,6 +176,198 @@ class ConfigChainBuilder {
}
}
/**
* 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,
};
}
/**
* Load and validate the given config into a set of options, plugins, and presets.
*/
const processConfig = makeWeakCache((config: SimpleConfig): LoadedConfig => {
const options = config.options;
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: options.passPerPreset,
}),
);
assertNoDuplicates(presets);
return { 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

View File

@@ -4,23 +4,18 @@ import path from "path";
import * as context from "../index";
import Plugin, { validatePluginObject } from "./plugin";
import merge from "lodash/merge";
import buildConfigChain, { type ConfigItem } from "./build-config-chain";
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, type ValidatedOptions, type PluginItem } from "./options";
import { loadPlugin, loadPreset } from "./loading/files";
type MergeOptions =
| ConfigItem
| {
type: "preset",
options: ValidatedOptions,
alias: string,
dirname: string,
};
import { validate, type ValidatedOptions } from "./options";
export default function manageOptions(opts: {}): {
options: Object,
@@ -71,18 +66,19 @@ class OptionManager {
);
presets.forEach(({ preset, pass }) => {
const loadedConfig = loadConfig(preset);
this.mergeOptions(
{
// Call dedupDescriptors() to remove 'false' descriptors.
plugins: dedupDescriptors(loadedConfig.plugins),
presets: dedupDescriptors(loadedConfig.presets),
plugins: preset.plugins,
presets: preset.presets,
},
pass,
envName,
);
merge(this.optionDefaults, normalizeOptions(loadedConfig.options));
preset.options.forEach(opts => {
merge(this.optionDefaults, normalizeOptions(opts));
});
});
}
@@ -92,19 +88,17 @@ class OptionManager {
}
}
mergeConfigChain(chain: $ReadOnlyArray<MergeOptions>, envName: string) {
const config = dedupLoadedConfigs(chain.map(config => loadConfig(config)));
mergeConfigChain(chain: ConfigChain, envName: string) {
this.mergeOptions(
{
plugins: config.plugins,
presets: config.presets,
plugins: chain.plugins,
presets: chain.presets,
},
this.passes[0],
envName,
);
config.options.forEach(opts => {
chain.options.forEach(opts => {
merge(this.options, normalizeOptions(opts));
});
}
@@ -115,7 +109,7 @@ class OptionManager {
const { envName = getEnv(), cwd = "." } = args;
const absoluteCwd = path.resolve(cwd);
const configChain = buildConfigChain(absoluteCwd, args, envName);
const configChain = buildRootChain(absoluteCwd, args, envName);
if (!configChain) return null;
try {
@@ -170,15 +164,6 @@ function normalizeOptions(opts: ValidatedOptions): ValidatedOptions {
return options;
}
type BasicDescriptor = {
name: string | void,
value: {} | Function,
options: {} | void | false,
dirname: string,
alias: string,
ownPass?: boolean,
};
type LoadedDescriptor = {
value: {},
options: {},
@@ -186,139 +171,6 @@ type LoadedDescriptor = {
alias: string,
};
type LoadedConfig = {
options: ValidatedOptions,
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
};
/**
* Load and validate the given config into a set of options, plugins, and presets.
*/
const loadConfig = makeWeakCache((config: MergeOptions): LoadedConfig => {
const options = config.options;
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: options.passPerPreset,
}),
);
assertNoDuplicates(presets);
return { 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>,
): {
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
options: Array<ValidatedOptions>,
} {
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;
}, []);
}
/**
* Load a generic plugin/preset from the given descriptor loaded from the config object.
*/
@@ -437,12 +289,14 @@ const instantiatePlugin = makeWeakCache(
const loadPresetDescriptor = (
descriptor: BasicDescriptor,
envName: string,
): MergeOptions => {
return instantiatePreset(loadDescriptor(descriptor, { envName }));
): ConfigChain => {
return buildPresetChain(
instantiatePreset(loadDescriptor(descriptor, { envName })),
);
};
const instantiatePreset = makeWeakCache(
({ value, dirname, alias }: LoadedDescriptor): MergeOptions => {
({ value, dirname, alias }: LoadedDescriptor): PresetInstance => {
return {
type: "preset",
options: validate("preset", value),
@@ -452,77 +306,6 @@ const instantiatePreset = makeWeakCache(
},
);
/**
* 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 chain(a, b) {
const fns = [a, b].filter(Boolean);
if (fns.length <= 1) return fns[0];