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;
|