hard-source-webpack-plugin
Version:
Hard cache the source of modules in webpack.
164 lines (138 loc) • 4.35 kB
JavaScript
const { readdir: _readdir, stat: _stat } = require('graceful-fs');
const { basename, join } = require('path');
const _rimraf = require('rimraf');
const logMessages = require('./util/log-messages');
const pluginCompat = require('./util/plugin-compat');
const promisify = require('./util/promisify');
const readdir = promisify(_readdir);
const rimraf = promisify(_rimraf);
const stat = promisify(_stat);
const directorySize = async dir => {
const _stat = await stat(dir);
if (_stat.isFile()) {
return _stat.size;
}
if (_stat.isDirectory()) {
const names = await readdir(dir);
let size = 0;
for (const name of names) {
size += await directorySize(join(dir, name));
}
return size;
}
return 0;
};
class CacheInfo {
constructor(id = '') {
this.id = id;
this.lastModified = 0;
this.size = 0;
}
static async fromDirectory(dir) {
const info = new CacheInfo(basename(dir));
info.lastModified = new Date(
(await stat(join(dir, 'stamp'))).mtime,
).getTime();
info.size = await directorySize(dir);
return info;
}
static async fromDirectoryChildren(dir) {
const children = [];
const names = await readdir(dir);
for (const name of names) {
children.push(await CacheInfo.fromDirectory(join(dir, name)));
}
return children;
}
}
// Compilers for webpack with multiple parallel configurations might try to
// delete caches at the same time. Mutex lock the process of pruning to keep
// from multiple pruning runs from colliding with each other.
let deleteLock = null;
class PruneCachesSystem {
constructor(cacheRoot, options = {}) {
this.cacheRoot = cacheRoot;
this.options = Object.assign(
{
// Caches younger than `maxAge` are not considered for deletion. They
// must be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 50 * 1024 * 1024,
},
options,
);
}
apply(compiler) {
const compilerHooks = pluginCompat.hooks(compiler);
const deleteOldCaches = async () => {
while (deleteLock !== null) {
await deleteLock;
}
let resolveLock;
let infos;
try {
deleteLock = new Promise(resolve => {
resolveLock = resolve;
});
infos = await CacheInfo.fromDirectoryChildren(this.cacheRoot);
// Sort lastModified in descending order. More recently modified at the
// beginning of the array.
infos.sort((a, b) => b.lastModified - a.lastModified);
const totalSize = infos.reduce((carry, info) => carry + info.size, 0);
const oldInfos = infos.filter(
info => info.lastModified < Date.now() - this.options.maxAge,
);
const oldTotalSize = oldInfos.reduce(
(carry, info) => carry + info.size,
0,
);
if (oldInfos.length > 0 && totalSize > this.options.sizeThreshold) {
const newInfos = infos.filter(
info => info.lastModified >= Date.now() - this.options.maxAge,
);
for (const info of oldInfos) {
rimraf(join(this.cacheRoot, info.id));
}
const newTotalSize = newInfos.reduce(
(carry, info) => carry + info.size,
0,
);
logMessages.deleteOldCaches(compiler, {
infos,
totalSize,
newInfos,
newTotalSize,
oldInfos,
oldTotalSize,
});
} else {
logMessages.keepCaches(compiler, {
infos,
totalSize,
});
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
} finally {
if (typeof resolveLock === 'function') {
deleteLock = null;
resolveLock();
}
}
};
compilerHooks.watchRun.tapPromise(
'HardSource - PruneCachesSystem',
deleteOldCaches,
);
compilerHooks.run.tapPromise(
'HardSource - PruneCachesSystem',
deleteOldCaches,
);
}
}
module.exports = PruneCachesSystem;