From f9bac2a358514d871632a9c715cd4f9c25474532 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Thu, 13 Apr 2017 16:41:50 -0700 Subject: [PATCH] Implement caching of plugins/presets/options --- .../babel-core/src/config/option-manager.js | 137 +++++----- packages/babel-core/test/config-loading.js | 254 ++++++++++++++++++ .../test/fixtures/config-loading/.babelrc.js | 14 + .../config-loading/folder/.babelignore | 2 + .../test/fixtures/config-loading/plugin1.js | 7 + .../test/fixtures/config-loading/plugin2.js | 7 + .../test/fixtures/config-loading/plugin3.js | 7 + .../test/fixtures/config-loading/plugin4.js | 7 + .../test/fixtures/config-loading/plugin5.js | 7 + .../test/fixtures/config-loading/plugin6.js | 7 + .../test/fixtures/config-loading/preset1.js | 9 + .../test/fixtures/config-loading/preset2.js | 9 + .../test/fixtures/config-loading/preset3.js | 9 + 13 files changed, 409 insertions(+), 67 deletions(-) create mode 100644 packages/babel-core/test/config-loading.js create mode 100644 packages/babel-core/test/fixtures/config-loading/.babelrc.js create mode 100644 packages/babel-core/test/fixtures/config-loading/folder/.babelignore create mode 100644 packages/babel-core/test/fixtures/config-loading/plugin1.js create mode 100644 packages/babel-core/test/fixtures/config-loading/plugin2.js create mode 100644 packages/babel-core/test/fixtures/config-loading/plugin3.js create mode 100644 packages/babel-core/test/fixtures/config-loading/plugin4.js create mode 100644 packages/babel-core/test/fixtures/config-loading/plugin5.js create mode 100644 packages/babel-core/test/fixtures/config-loading/plugin6.js create mode 100644 packages/babel-core/test/fixtures/config-loading/preset1.js create mode 100644 packages/babel-core/test/fixtures/config-loading/preset2.js create mode 100644 packages/babel-core/test/fixtures/config-loading/preset3.js diff --git a/packages/babel-core/src/config/option-manager.js b/packages/babel-core/src/config/option-manager.js index 41610ba722..d1d37579b5 100644 --- a/packages/babel-core/src/config/option-manager.js +++ b/packages/babel-core/src/config/option-manager.js @@ -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, presets: Array, -} { +} => { 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. diff --git a/packages/babel-core/test/config-loading.js b/packages/babel-core/test/config-loading.js new file mode 100644 index 0000000000..6eb06e938f --- /dev/null +++ b/packages/babel-core/test/config-loading.js @@ -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]); + } + } + }); + }); +}); diff --git a/packages/babel-core/test/fixtures/config-loading/.babelrc.js b/packages/babel-core/test/fixtures/config-loading/.babelrc.js new file mode 100644 index 0000000000..2a890d96e7 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/.babelrc.js @@ -0,0 +1,14 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_BABELRC); + + return { + plugins: [ + "./plugin1", + "./plugin2", + ], + presets: [ + "./preset1", + "./preset2", + ], + } +}; diff --git a/packages/babel-core/test/fixtures/config-loading/folder/.babelignore b/packages/babel-core/test/fixtures/config-loading/folder/.babelignore new file mode 100644 index 0000000000..0098e2b67a --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/folder/.babelignore @@ -0,0 +1,2 @@ +# No-op .babelignore to stop babel from skipping these, which the root +# .babelignore tells it to do. diff --git a/packages/babel-core/test/fixtures/config-loading/plugin1.js b/packages/babel-core/test/fixtures/config-loading/plugin1.js new file mode 100644 index 0000000000..83701eba9c --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/plugin1.js @@ -0,0 +1,7 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_PLUGIN1); + + return { + name: "plugin1", + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/plugin2.js b/packages/babel-core/test/fixtures/config-loading/plugin2.js new file mode 100644 index 0000000000..4181e8f5d5 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/plugin2.js @@ -0,0 +1,7 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_PLUGIN2); + + return { + name: "plugin2", + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/plugin3.js b/packages/babel-core/test/fixtures/config-loading/plugin3.js new file mode 100644 index 0000000000..4d84779a69 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/plugin3.js @@ -0,0 +1,7 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_PLUGIN3); + + return { + name: "plugin3", + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/plugin4.js b/packages/babel-core/test/fixtures/config-loading/plugin4.js new file mode 100644 index 0000000000..462b280f86 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/plugin4.js @@ -0,0 +1,7 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_PLUGIN4); + + return { + name: "plugin4", + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/plugin5.js b/packages/babel-core/test/fixtures/config-loading/plugin5.js new file mode 100644 index 0000000000..bb049350a9 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/plugin5.js @@ -0,0 +1,7 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_PLUGIN5); + + return { + name: "plugin5", + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/plugin6.js b/packages/babel-core/test/fixtures/config-loading/plugin6.js new file mode 100644 index 0000000000..577c03c4ae --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/plugin6.js @@ -0,0 +1,7 @@ +module.exports = function(api) { + api.cache.using(() => process.env.INVALIDATE_PLUGIN6); + + return { + name: "plugin6", + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/preset1.js b/packages/babel-core/test/fixtures/config-loading/preset1.js new file mode 100644 index 0000000000..6364f90cb8 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/preset1.js @@ -0,0 +1,9 @@ +module.exports = function(api, options) { + api.cache.using(() => process.env.INVALIDATE_PRESET1); + + return { + plugins: [ + [require('./plugin3.js'), options], + ], + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/preset2.js b/packages/babel-core/test/fixtures/config-loading/preset2.js new file mode 100644 index 0000000000..7a15eb85ec --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/preset2.js @@ -0,0 +1,9 @@ +module.exports = function(api, options) { + api.cache.using(() => process.env.INVALIDATE_PRESET2); + + return { + plugins: [ + [require('./plugin4.js'), options], + ], + }; +}; diff --git a/packages/babel-core/test/fixtures/config-loading/preset3.js b/packages/babel-core/test/fixtures/config-loading/preset3.js new file mode 100644 index 0000000000..e1e7d5ce13 --- /dev/null +++ b/packages/babel-core/test/fixtures/config-loading/preset3.js @@ -0,0 +1,9 @@ +module.exports = function(api, options) { + api.cache.using(() => process.env.INVALIDATE_PRESET3); + + return { + plugins: [ + [require('./plugin5.js'), options], + ], + }; +};