Cache config files based on mtime rather than file content.
This commit is contained in:
176
packages/babel-core/src/config/caching.js
Normal file
176
packages/babel-core/src/config/caching.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// @flow
|
||||
|
||||
type CacheConfigurator = CacheConfiguratorFn & CacheConfiguratorObj;
|
||||
|
||||
type CacheConfiguratorFn = {
|
||||
(boolean): void,
|
||||
<T>(handler: () => T): T,
|
||||
};
|
||||
type CacheConfiguratorObj = {
|
||||
forever: () => void,
|
||||
never: () => void,
|
||||
using: <T>(handler: () => T) => T,
|
||||
invalidate: <T>(handler: () => T) => T,
|
||||
};
|
||||
|
||||
type CacheEntry<ResultT> = 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<ArgT, ResultT>(
|
||||
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<ArgT: Object, ResultT>(
|
||||
handler: (ArgT, CacheConfigurator) => ResultT,
|
||||
autoPermacache?: boolean,
|
||||
): (ArgT) => ResultT {
|
||||
return makeCachedFunction(new WeakMap(), handler, autoPermacache);
|
||||
}
|
||||
|
||||
type CacheMap<ArgT, ResultT> = Map<ArgT, CacheEntry<ResultT>>|WeakMap<ArgT, CacheEntry<ResultT>>;
|
||||
|
||||
function makeCachedFunction<ArgT, ResultT, Cache: CacheMap<ArgT, ResultT>>(
|
||||
callCache: Cache,
|
||||
handler: (ArgT, CacheConfigurator) => ResultT,
|
||||
autoPermacache: boolean = true,
|
||||
): (ArgT) => ResultT {
|
||||
return function cachedFunction(arg) {
|
||||
let cachedValue: CacheEntry<ResultT>|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<T>(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<T>(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 };
|
||||
}
|
||||
@@ -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<ConfigFile> {
|
||||
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<T>(fn: (string, string) => T): (string) => T|null {
|
||||
return (filepath) => {
|
||||
if (!exists(filepath)) return null;
|
||||
function makeStaticFileCache<T>(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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user