diff --git a/packages/babel-core/src/config/caching.js b/packages/babel-core/src/config/caching.js new file mode 100644 index 0000000000..317e1e3a8a --- /dev/null +++ b/packages/babel-core/src/config/caching.js @@ -0,0 +1,176 @@ +// @flow + +type CacheConfigurator = CacheConfiguratorFn & CacheConfiguratorObj; + +type CacheConfiguratorFn = { + (boolean): void, + (handler: () => T): T, +}; +type CacheConfiguratorObj = { + forever: () => void, + never: () => void, + using: (handler: () => T) => T, + invalidate: (handler: () => T) => T, +}; + +type CacheEntry = Array<[ ResultT, () => boolean ]>; + +/** + * Given a function with a single argument, cache its results based on its argument and how it + * configures its caching behavior. Cached values are stored strongly. + */ +export function makeStrongCache( + handler: (ArgT, CacheConfigurator) => ResultT, + autoPermacache?: boolean, +): (ArgT) => ResultT { + return makeCachedFunction(new Map(), handler, autoPermacache); +} + +/** + * Given a function with a single argument, cache its results based on its argument and how it + * configures its caching behavior. Cached values are stored weakly and the function argument must be + * an object type. + */ +export function makeWeakCache( + handler: (ArgT, CacheConfigurator) => ResultT, + autoPermacache?: boolean, +): (ArgT) => ResultT { + return makeCachedFunction(new WeakMap(), handler, autoPermacache); +} + +type CacheMap = Map>|WeakMap>; + +function makeCachedFunction>( + callCache: Cache, + handler: (ArgT, CacheConfigurator) => ResultT, + autoPermacache: boolean = true, +): (ArgT) => ResultT { + return function cachedFunction(arg) { + let cachedValue: CacheEntry|void = callCache.get(arg); + + if (cachedValue) { + for (const [ value, valid ] of cachedValue) { + if (valid()) return value; + } + } + + const { cache, result } = makeCachePair(); + + const value = handler(arg, cache); + + if (autoPermacache && !result.configured) cache.forever(); + + if (!result.configured) { + // eslint-disable-next-line max-len + throw new Error([ + "Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured", + "for various types of caching, using the first param of their handler functions:", + "", + "module.exports = function(api) {", + " // The API exposes the following:", + "", + " // Cache the returned value forever and don't call this function again.", + " api.cache(true);", + "", + " // Don't cache at all. Not recommended because it will be very slow.", + " api.cache(false);", + "", + " // Cached based on the value of some function. If this function returns a value different from", + " // a previously-encountered value, the plugins will re-evaluate.", + " var env = api.cache(() => process.env.NODE_ENV);", + "", + " // If testing for a specific env, we recommend specifics to avoid instantiating a plugin for", + " // any possible NODE_ENV value that might come up during plugin execution.", + " var isProd = api.cache(() => process.env.NODE_ENV === \"production\");", + "", + " // .cache(fn) will perform a linear search though instances to find the matching plugin based", + " // based on previous instantiated plugins. If you want to recreate the plugin and discard the", + " // previous instance whenever something changes, you may use:", + " var isProd = api.cache.invalidate(() => process.env.NODE_ENV === \"production\");", + "", + " // Note, we also expose the following more-verbose versions of the above examples:", + " api.cache.forever(); // api.cache(true)", + " api.cache.never(); // api.cache(false)", + " api.cache.using(fn); // api.cache(fn)", + "", + " // Return the value that will be cached.", + " return { };", + "};", + ].join("\n")); + } + + if (!result.never) { + if (result.forever) { + cachedValue = [ + [value, () => true], + ]; + } else if (result.invalidate) { + cachedValue = [ + [value, result.valid], + ]; + } else { + cachedValue = cachedValue || []; + cachedValue.push([ value, result.valid ]); + } + callCache.set(arg, cachedValue); + } + + return value; + }; +} + +function makeCachePair(): { cache: CacheConfigurator, result: * } { + const pairs = []; + + const result = { + configured: false, + never: false, + forever: false, + invalidate: false, + valid: () => pairs.every(([key, fn]) => key === fn()), + }; + + const cache: CacheConfigurator = Object.assign((function cacheFn(val) { + if (typeof val === "boolean") { + if (val) cache.forever(); + else cache.never(); + return; + } + + return cache.using(val); + }: any), ({ + forever() { + if (result.never) throw new Error("Caching has already been configured with .never()"); + result.forever = true; + result.configured = true; + }, + never() { + if (result.forever) throw new Error("Caching has already been configured with .forever()"); + result.never = true; + result.configured = true; + }, + using(handler: () => T): T { + if (result.never || result.forever) { + throw new Error("Caching has already been configured with .never or .forever()"); + } + result.configured = true; + + const key = handler(); + pairs.push([ key, handler ]); + return key; + }, + invalidate(handler: () => T): T { + if (result.never || result.forever) { + throw new Error("Caching has already been configured with .never or .forever()"); + } + result.invalidate = true; + result.configured = true; + + const key = handler(); + pairs.push([ key, handler ]); + return key; + }, + }: CacheConfiguratorObj)); + + return { cache, result }; +} diff --git a/packages/babel-core/src/config/loading/files/configuration.js b/packages/babel-core/src/config/loading/files/configuration.js index ba51499ef6..aa47e374f5 100644 --- a/packages/babel-core/src/config/loading/files/configuration.js +++ b/packages/babel-core/src/config/loading/files/configuration.js @@ -4,6 +4,7 @@ import path from "path"; import fs from "fs"; import json5 from "json5"; import resolve from "resolve"; +import { makeStrongCache } from "../../caching"; type ConfigFile = { filepath: string, @@ -11,23 +12,11 @@ type ConfigFile = { options: Object, }; -const existsCache = {}; -const jsonCache = {}; - const BABELRC_FILENAME = ".babelrc"; const BABELRC_JS_FILENAME = ".babelrc.js"; const PACKAGE_FILENAME = "package.json"; const BABELIGNORE_FILENAME = ".babelignore"; -function exists(filename) { - const cached = existsCache[filename]; - if (cached == null) { - return existsCache[filename] = fs.existsSync(filename); - } else { - return cached; - } -} - export function findConfigs(dirname: string): Array { let foundConfig = false; let foundIgnore = false; @@ -96,8 +85,11 @@ function readConfig(filepath) { return (path.extname(filepath) === ".js") ? readConfigJS(filepath) : readConfigFile(filepath); } -function readConfigJS(filepath) { - if (!exists(filepath)) return null; +const readConfigJS = makeStrongCache((filepath, cache) => { + if (!fs.existsSync(filepath)) { + cache.forever(); + return null; + } let options; try { @@ -118,15 +110,13 @@ function readConfigJS(filepath) { dirname: path.dirname(filepath), options, }; -} +}); -const readConfigFile = makeStaticFileHandler((filepath, content) => { +const readConfigFile = makeStaticFileCache((filepath, content) => { let options; if (path.basename(filepath) === PACKAGE_FILENAME) { try { - const json = jsonCache[content] = jsonCache[content] || JSON.parse(content); - - options = json.babel; + options = JSON.parse(content).babel; } catch (err) { err.message = `${filepath}: Error while parsing JSON - ${err.message}`; throw err; @@ -134,7 +124,7 @@ const readConfigFile = makeStaticFileHandler((filepath, content) => { if (!options) return null; } else { try { - options = jsonCache[content] = jsonCache[content] || json5.parse(content); + options = json5.parse(content); } catch (err) { err.message = `${filepath}: Error while parsing config - ${err.message}`; throw err; @@ -153,7 +143,7 @@ const readConfigFile = makeStaticFileHandler((filepath, content) => { }; }); -const readIgnoreConfig = makeStaticFileHandler((filepath, content) => { +const readIgnoreConfig = makeStaticFileCache((filepath, content) => { const ignore = content .split("\n") .map((line) => line.replace(/#(.*?)$/, "").trim()) @@ -166,10 +156,23 @@ const readIgnoreConfig = makeStaticFileHandler((filepath, content) => { }; }); -function makeStaticFileHandler(fn: (string, string) => T): (string) => T|null { - return (filepath) => { - if (!exists(filepath)) return null; +function makeStaticFileCache(fn: (string, string) => T): (string) => T|null { + return makeStrongCache((filepath, cache) => { + if (cache.invalidate(() => fileMtime(filepath)) === null) { + cache.forever(); + return null; + } return fn(filepath, fs.readFileSync(filepath, "utf8")); - }; + }); +} + +function fileMtime(filepath: string): number|null { + try { + return +fs.statSync(filepath).mtime; + } catch (e) { + if (e.code !== "ENOENT") throw e; + } + + return null; }