diff --git a/packages/babel-core/src/config/config-descriptors.js b/packages/babel-core/src/config/config-descriptors.js index cee11ad620..e17cca93c6 100644 --- a/packages/babel-core/src/config/config-descriptors.js +++ b/packages/babel-core/src/config/config-descriptors.js @@ -41,6 +41,22 @@ export type UnloadedDescriptor = { } | void, }; +function isEqualDescriptor( + a: UnloadedDescriptor, + b: UnloadedDescriptor, +): boolean { + return ( + a.name === b.name && + a.value === b.value && + a.options === b.options && + a.dirname === b.dirname && + a.alias === b.alias && + a.ownPass === b.ownPass && + (a.file && a.file.request) === (b.file && b.file.request) && + (a.file && a.file.resolved) === (b.file && b.file.resolved) + ); +} + export type ValidatedFile = { filepath: string, dirname: string, @@ -50,7 +66,7 @@ export type ValidatedFile = { /** * Create a set of descriptors from a given options object, preserving * descriptor identity based on the identity of the plugin/preset arrays - * themselves. + * themselves, and potentially on the identity of the plugins/presets + options. */ export function createCachedDescriptors( dirname: string, @@ -113,26 +129,82 @@ export function createUncachedDescriptors( }; } +const PRESET_DESCRIPTOR_CACHE = new WeakMap(); const createCachedPresetDescriptors = makeWeakCache( (items: PluginList, cache: CacheConfigurator) => { const dirname = cache.using(dir => dir); return makeStrongCache((alias: string) => makeStrongCache((passPerPreset: boolean) => - createPresetDescriptors(items, dirname, alias, passPerPreset), + createPresetDescriptors(items, dirname, alias, passPerPreset).map( + // Items are cached using the overall preset array identity when + // possibly, but individual descriptors are also cached if a match + // can be found in the previously-used descriptor lists. + desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc), + ), ), ); }, ); +const PLUGIN_DESCRIPTOR_CACHE = new WeakMap(); const createCachedPluginDescriptors = makeWeakCache( (items: PluginList, cache: CacheConfigurator) => { const dirname = cache.using(dir => dir); return makeStrongCache((alias: string) => - createPluginDescriptors(items, dirname, alias), + createPluginDescriptors(items, dirname, alias).map( + // Items are cached using the overall plugin array identity when + // possibly, but individual descriptors are also cached if a match + // can be found in the previously-used descriptor lists. + desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc), + ), ); }, ); +/** + * When no options object is given in a descriptor, this object is used + * as a WeakMap key in order to have consistent identity. + */ +const DEFAULT_OPTIONS = {}; + +/** + * Given the cache and a descriptor, returns a matching descriptor from the + * cache, or else returns the input descriptor and adds it to the cache for + * next time. + */ +function loadCachedDescriptor( + cache: WeakMap<{} | Function, WeakMap<{}, Array>>, + desc: UnloadedDescriptor, +) { + const { value, options = DEFAULT_OPTIONS } = desc; + if (options === false) return desc; + + let cacheByOptions = cache.get(value); + if (!cacheByOptions) { + cacheByOptions = new WeakMap(); + cache.set(value, cacheByOptions); + } + + let possibilities = cacheByOptions.get(options); + if (!possibilities) { + possibilities = []; + cacheByOptions.set(options, possibilities); + } + + if (possibilities.indexOf(desc) === -1) { + const matches = possibilities.filter(possibility => + isEqualDescriptor(possibility, desc), + ); + if (matches.length > 0) { + return matches[0]; + } + + possibilities.push(desc); + } + + return desc; +} + function createPresetDescriptors( items: PluginList, dirname: string, diff --git a/packages/babel-core/test/config-loading.js b/packages/babel-core/test/config-loading.js index d53b746b2a..7b8d116234 100644 --- a/packages/babel-core/test/config-loading.js +++ b/packages/babel-core/test/config-loading.js @@ -29,10 +29,10 @@ describe("@babel/core config loading", () => { filename: FILEPATH, presets: skipProgrammatic ? null - : [require("./fixtures/config-loading/preset3")], + : [[require("./fixtures/config-loading/preset3"), {}]], plugins: skipProgrammatic ? null - : [require("./fixtures/config-loading/plugin6")], + : [[require("./fixtures/config-loading/plugin6"), {}]], }; } @@ -213,7 +213,7 @@ describe("@babel/core config loading", () => { } }); - it("should invalidate the plugins when given a fresh arrays", () => { + it("should not invalidate the plugins when given a fresh arrays", () => { const opts = makeOpts(); const options1 = loadConfig(opts).options; @@ -224,6 +224,38 @@ describe("@babel/core config loading", () => { }).options; expect(options2.plugins.length).toBe(options1.plugins.length); + for (let i = 0; i < options2.plugins.length; i++) { + expect(options2.plugins[i]).toBe(options1.plugins[i]); + } + }); + + it("should not 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).toBe(options1.plugins.length); + + for (let i = 0; i < options2.plugins.length; i++) { + expect(options2.plugins[i]).toBe(options1.plugins[i]); + } + }); + + it("should invalidate the plugins when given a fresh options", () => { + const opts = makeOpts(); + + const options1 = loadConfig(opts).options; + + const options2 = loadConfig({ + ...opts, + plugins: opts.plugins.map(([plg, opt]) => [plg, { ...opt }]), + }).options; + expect(options2.plugins.length).toBe(options1.plugins.length); + for (let i = 0; i < options2.plugins.length; i++) { if (i === 2) { expect(options2.plugins[i]).not.toBe(options1.plugins[i]); @@ -233,14 +265,14 @@ describe("@babel/core config loading", () => { } }); - it("should invalidate the presets when given a fresh arrays", () => { + it("should invalidate the presets when given a fresh options", () => { const opts = makeOpts(); const options1 = loadConfig(opts).options; const options2 = loadConfig({ ...opts, - presets: opts.presets.slice(), + presets: opts.presets.map(([plg, opt]) => [plg, { ...opt }]), }).options; expect(options2.plugins.length).toBe(options1.plugins.length);