Implement caching of plugins/presets/options

This commit is contained in:
Logan Smyth
2017-04-13 16:41:50 -07:00
parent cc8109cdc3
commit f9bac2a358
13 changed files with 409 additions and 67 deletions

View File

@@ -9,6 +9,8 @@ import buildConfigChain from "./build-config-chain";
import path from "path";
import traverse from "babel-traverse";
import clone from "lodash/clone";
import { makeWeakCache } from "./caching";
import { getEnv } from "./helpers/environment";
import {
loadPlugin,
@@ -221,13 +223,11 @@ type BasicDescriptor = {
/**
* Load and validate the given config into a set of options, plugins, and presets.
*/
function loadConfig(
config,
): {
const loadConfig = makeWeakCache((config): {
options: {},
plugins: Array<BasicDescriptor>,
presets: Array<BasicDescriptor>,
} {
} => {
const options = normalizeOptions(config);
if (
@@ -277,24 +277,25 @@ function loadConfig(
});
return { options, plugins, presets };
}
});
/**
* Load a generic plugin/preset from the given descriptor loaded from the config object.
*/
function loadDescriptor(descriptor, skipOptions) {
const loadDescriptor = makeWeakCache((descriptor, cache) => {
if (typeof descriptor.value !== "function") {
return { value: descriptor.value, descriptor };
}
const { value, options } = descriptor;
const api = Object.assign(Object.create(context), {
cache,
env: () => cache.using(() => getEnv()),
});
let item;
try {
if (skipOptions) {
item = value(context);
} else {
item = value(context, options, { dirname: descriptor.dirname });
}
item = value(api, options, { dirname: descriptor.dirname });
} catch (e) {
if (descriptor.alias) {
e.message += ` (While processing: ${JSON.stringify(descriptor.alias)})`;
@@ -307,92 +308,94 @@ function loadDescriptor(descriptor, skipOptions) {
}
return { value: item, descriptor };
}
});
/**
* Instantiate a plugin for the given descriptor, returning the plugin/options pair.
*/
const PLUGIN_CACHE = new WeakMap();
function loadPluginDescriptor(descriptor) {
function loadPluginDescriptor(descriptor: BasicDescriptor) {
if (descriptor.value instanceof Plugin) {
return [descriptor.value, descriptor.options];
}
let result = PLUGIN_CACHE.get(descriptor.value);
if (!result) {
result = instantiatePlugin(
loadDescriptor(descriptor, true /* skipOptions */),
);
PLUGIN_CACHE.set(descriptor.value, result);
}
const result = instantiatePlugin(loadDescriptor(descriptor));
return [result, descriptor.options];
}
function instantiatePlugin({ value: pluginObj, descriptor }) {
Object.keys(pluginObj).forEach(key => {
if (!ALLOWED_PLUGIN_KEYS.has(key)) {
const instantiatePlugin = makeWeakCache(
({ value: pluginObj, descriptor }, cache) => {
Object.keys(pluginObj).forEach(key => {
if (!ALLOWED_PLUGIN_KEYS.has(key)) {
throw new Error(
`Plugin ${descriptor.alias} provided an invalid property of ${key}`,
);
}
});
if (
pluginObj.visitor &&
(pluginObj.visitor.enter || pluginObj.visitor.exit)
) {
throw new Error(
`Plugin ${descriptor.alias} provided an invalid property of ${key}`,
"Plugins aren't allowed to specify catch-all enter/exit handlers. " +
"Please target individual nodes.",
);
}
});
if (
pluginObj.visitor &&
(pluginObj.visitor.enter || pluginObj.visitor.exit)
) {
throw new Error(
"Plugins aren't allowed to specify catch-all enter/exit handlers. " +
"Please target individual nodes.",
);
}
const plugin = Object.assign({}, pluginObj, {
visitor: clone(pluginObj.visitor || {}),
});
const plugin = Object.assign({}, pluginObj, {
visitor: clone(pluginObj.visitor || {}),
});
traverse.explode(plugin.visitor);
traverse.explode(plugin.visitor);
let inheritsDescriptor;
let inherits;
if (plugin.inherits) {
inheritsDescriptor = {
alias: `${descriptor.loc}$inherits`,
loc: descriptor.loc,
value: plugin.inherits,
options: descriptor.options,
dirname: descriptor.dirname,
};
let inheritsDescriptor;
let inherits;
if (plugin.inherits) {
inheritsDescriptor = {
alias: `${descriptor.loc}$inherits`,
loc: descriptor.loc,
value: plugin.inherits,
options: descriptor.options,
dirname: descriptor.dirname,
};
inherits = loadPluginDescriptor(inheritsDescriptor)[0];
// If the inherited plugin changes, reinstantiate this plugin.
inherits = cache.invalidate(
() => loadPluginDescriptor(inheritsDescriptor)[0],
);
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,
]);
}
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, descriptor.alias);
}
return new Plugin(plugin, descriptor.alias);
},
);
/**
* Generate a config object that will act as the root of a new nested config.
*/
function loadPresetDescriptor(descriptor): MergeOptions {
const loadPresetDescriptor = (descriptor: BasicDescriptor): MergeOptions => {
return instantiatePreset(loadDescriptor(descriptor));
};
const instantiatePreset = makeWeakCache(({ value, descriptor }) => {
return {
type: "preset",
options: loadDescriptor(descriptor).value,
options: value,
alias: descriptor.alias,
loc: descriptor.loc,
dirname: descriptor.dirname,
};
}
});
/**
* Validate and return the options object for the config.

View File

@@ -0,0 +1,254 @@
import loadConfig from "../lib/config";
import path from "path";
import { expect } from "chai";
describe("babel-core config loading", () => {
const FILEPATH = path.join(
__dirname,
"fixtures",
"config-loading",
"folder",
"example.js",
);
afterEach(() => {
delete process.env.INVALIDATE_BABELRC;
delete process.env.INVALIDATE_PRESET1;
delete process.env.INVALIDATE_PRESET2;
delete process.env.INVALIDATE_PRESET3;
delete process.env.INVALIDATE_PLUGIN1;
delete process.env.INVALIDATE_PLUGIN2;
delete process.env.INVALIDATE_PLUGIN3;
delete process.env.INVALIDATE_PLUGIN4;
delete process.env.INVALIDATE_PLUGIN5;
delete process.env.INVALIDATE_PLUGIN6;
});
function makeOpts(skipProgrammatic = false) {
return {
filename: FILEPATH,
presets: skipProgrammatic
? null
: [require("./fixtures/config-loading/preset3")],
plugins: skipProgrammatic
? null
: [require("./fixtures/config-loading/plugin6")],
};
}
describe("config file", () => {
it("should load and cache the config with plugins and presets", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
expect(options1.plugins.map(p => p.key)).to.eql([
"plugin6",
"plugin5",
"plugin1",
"plugin2",
"plugin4",
"plugin3",
]);
const options2 = loadConfig(opts).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options2.plugins.length; i++) {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
});
it("should load and cache the config for unique opts objects", () => {
const options1 = loadConfig(makeOpts(true)).options;
expect(options1.plugins.map(p => p.key)).to.eql([
"plugin1",
"plugin2",
"plugin4",
"plugin3",
]);
const options2 = loadConfig(makeOpts(true)).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options2.plugins.length; i++) {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
});
it("should invalidate config file plugins", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
process.env.INVALIDATE_PLUGIN1 = true;
const options2 = loadConfig(opts).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 2) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
process.env.INVALIDATE_PLUGIN3 = true;
const options3 = loadConfig(opts).options;
expect(options3.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 2 || i === 5) {
expect(options3.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options3.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
it("should invalidate config file presets and their children", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
process.env.INVALIDATE_PRESET1 = true;
const options2 = loadConfig(opts).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 5) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
process.env.INVALIDATE_PRESET2 = true;
const options3 = loadConfig(opts).options;
expect(options3.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 4 || i === 5) {
expect(options3.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options3.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
it("should invalidate the config file and its plugins/presets", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
process.env.INVALIDATE_BABELRC = true;
const options2 = loadConfig(opts).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 2 || i === 3 || i === 4 || i === 5 || i === 6) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
});
describe("programmatic plugins/presets", () => {
it("should not invalidate the plugins when given a fresh object", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
const options2 = loadConfig(Object.assign({}, opts)).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options2.plugins.length; i++) {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
});
it("should invalidate the plugins when given a fresh arrays", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
const options2 = loadConfig({
...opts,
plugins: opts.plugins.slice(),
}).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options2.plugins.length; i++) {
if (i === 0) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
it("should invalidate the presets when given a fresh arrays", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
const options2 = loadConfig({
...opts,
presets: opts.presets.slice(),
}).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options2.plugins.length; i++) {
if (i === 1) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
it("should invalidate the programmatic plugins", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
process.env.INVALIDATE_PLUGIN6 = true;
const options2 = loadConfig(opts).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 0) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
it("should invalidate the programmatic presets and their children", () => {
const opts = makeOpts();
const options1 = loadConfig(opts).options;
process.env.INVALIDATE_PRESET3 = true;
const options2 = loadConfig(opts).options;
expect(options2.plugins.length).to.equal(options1.plugins.length);
for (let i = 0; i < options1.plugins.length; i++) {
if (i === 1) {
expect(options2.plugins[i]).not.to.equal(options1.plugins[i]);
} else {
expect(options2.plugins[i]).to.equal(options1.plugins[i]);
}
}
});
});
});

View File

@@ -0,0 +1,14 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_BABELRC);
return {
plugins: [
"./plugin1",
"./plugin2",
],
presets: [
"./preset1",
"./preset2",
],
}
};

View File

@@ -0,0 +1,2 @@
# No-op .babelignore to stop babel from skipping these, which the root
# .babelignore tells it to do.

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_PLUGIN1);
return {
name: "plugin1",
};
};

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_PLUGIN2);
return {
name: "plugin2",
};
};

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_PLUGIN3);
return {
name: "plugin3",
};
};

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_PLUGIN4);
return {
name: "plugin4",
};
};

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_PLUGIN5);
return {
name: "plugin5",
};
};

View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache.using(() => process.env.INVALIDATE_PLUGIN6);
return {
name: "plugin6",
};
};

View File

@@ -0,0 +1,9 @@
module.exports = function(api, options) {
api.cache.using(() => process.env.INVALIDATE_PRESET1);
return {
plugins: [
[require('./plugin3.js'), options],
],
};
};

View File

@@ -0,0 +1,9 @@
module.exports = function(api, options) {
api.cache.using(() => process.env.INVALIDATE_PRESET2);
return {
plugins: [
[require('./plugin4.js'), options],
],
};
};

View File

@@ -0,0 +1,9 @@
module.exports = function(api, options) {
api.cache.using(() => process.env.INVALIDATE_PRESET3);
return {
plugins: [
[require('./plugin5.js'), options],
],
};
};