2018-10-09 20:30:59 +08:00
|
|
|
/*
|
|
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
|
|
Author Tobias Koppers @sokra
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const path = require("path");
|
|
|
|
const createHash = require("../util/createHash");
|
|
|
|
const serializer = require("../util/serializer");
|
|
|
|
|
|
|
|
/** @typedef {import("webpack-sources").Source} Source */
|
|
|
|
/** @typedef {import("../../declarations/WebpackOptions").FileCacheOptions} FileCacheOptions */
|
|
|
|
/** @typedef {import("../Compiler")} Compiler */
|
|
|
|
/** @typedef {import("../Module")} Module */
|
|
|
|
|
|
|
|
const memorize = fn => {
|
|
|
|
let result = undefined;
|
|
|
|
return () => {
|
|
|
|
if (result === undefined) result = fn();
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const memoryCache = new Map();
|
|
|
|
|
|
|
|
class FileCachePlugin {
|
|
|
|
/**
|
|
|
|
* @param {FileCacheOptions} options options
|
|
|
|
*/
|
|
|
|
constructor(options) {
|
|
|
|
this.options = options;
|
|
|
|
}
|
|
|
|
|
2018-10-18 04:15:46 +08:00
|
|
|
static purgeMemoryCache() {
|
|
|
|
memoryCache.clear();
|
|
|
|
}
|
|
|
|
|
2018-10-09 20:30:59 +08:00
|
|
|
/**
|
|
|
|
* @param {Compiler} compiler Webpack compiler
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
apply(compiler) {
|
|
|
|
const cacheDirectory = path.resolve(
|
|
|
|
this.options.cacheDirectory || "node_modules/.cache/webpack/",
|
2018-10-10 16:07:51 +08:00
|
|
|
this.options.name || "default"
|
2018-10-09 20:30:59 +08:00
|
|
|
);
|
|
|
|
const hashAlgorithm = this.options.hashAlgorithm || "md4";
|
|
|
|
const version = this.options.version || "";
|
2018-10-10 16:51:08 +08:00
|
|
|
const log = this.options.loglevel
|
|
|
|
? { debug: 3, info: 2, warning: 1 }[this.options.loglevel]
|
|
|
|
: 0;
|
2018-10-09 20:30:59 +08:00
|
|
|
const store = this.options.store || "idle";
|
|
|
|
|
|
|
|
let pendingPromiseFactories = new Map();
|
|
|
|
const toHash = str => {
|
|
|
|
const hash = createHash(hashAlgorithm);
|
|
|
|
hash.update(str);
|
|
|
|
const digest = hash.digest("hex");
|
|
|
|
return `${digest.slice(0, 2)}/${digest.slice(2)}`;
|
|
|
|
};
|
|
|
|
compiler.cache.hooks.store.tapPromise(
|
|
|
|
"FileCachePlugin",
|
|
|
|
(identifier, etag, data) => {
|
|
|
|
const entry = { identifier, data: () => data, etag, version };
|
|
|
|
const filename = path.join(
|
|
|
|
cacheDirectory,
|
|
|
|
toHash(identifier) + ".data"
|
|
|
|
);
|
|
|
|
memoryCache.set(filename, entry);
|
|
|
|
const promiseFactory = () =>
|
|
|
|
serializer
|
|
|
|
.serializeToFile(entry, filename)
|
|
|
|
.then(() => {
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 2) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(`Cached ${identifier} to ${filename}.`);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(err => {
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 1) {
|
2018-10-10 23:19:31 +08:00
|
|
|
console.warn(
|
|
|
|
`Caching failed for ${identifier}: ${
|
|
|
|
log >= 3 ? err.stack : err
|
|
|
|
}`
|
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
if (store === "instant") {
|
|
|
|
return promiseFactory();
|
|
|
|
} else if (store === "idle") {
|
|
|
|
pendingPromiseFactories.set(filename, promiseFactory);
|
|
|
|
return Promise.resolve();
|
|
|
|
} else if (store === "background") {
|
|
|
|
const promise = promiseFactory();
|
|
|
|
pendingPromiseFactories.set(filename, () => promise);
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
compiler.cache.hooks.get.tapPromise(
|
|
|
|
"FileCachePlugin",
|
|
|
|
(identifier, etag) => {
|
|
|
|
const filename = path.join(
|
|
|
|
cacheDirectory,
|
|
|
|
toHash(identifier) + ".data"
|
|
|
|
);
|
|
|
|
const memory = memoryCache.get(filename);
|
|
|
|
if (memory !== undefined) {
|
|
|
|
return Promise.resolve(
|
|
|
|
memory.etag === etag && memory.version === version
|
|
|
|
? memory.data()
|
|
|
|
: undefined
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return serializer.deserializeFromFile(filename).then(
|
|
|
|
cacheEntry => {
|
|
|
|
cacheEntry = {
|
|
|
|
identifier: cacheEntry.identifier,
|
|
|
|
etag: cacheEntry.etag,
|
|
|
|
version: cacheEntry.version,
|
|
|
|
data: memorize(cacheEntry.data)
|
|
|
|
};
|
|
|
|
memoryCache.set(filename, cacheEntry);
|
|
|
|
if (cacheEntry === undefined) return;
|
|
|
|
if (cacheEntry.identifier !== identifier) {
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 2) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(
|
|
|
|
`Restored ${identifier} from ${filename}, but identifier doesn't match.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (cacheEntry.etag !== etag) {
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 2) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(
|
|
|
|
`Restored ${etag} from ${filename}, but etag doesn't match.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (cacheEntry.version !== version) {
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 2) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(
|
|
|
|
`Restored ${version} from ${filename}, but version doesn't match.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 2) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(`Restored ${identifier} from ${filename}.`);
|
|
|
|
}
|
|
|
|
return cacheEntry.data();
|
|
|
|
},
|
|
|
|
err => {
|
2018-10-10 16:51:08 +08:00
|
|
|
if (log >= 1 && err && err.code !== "ENOENT") {
|
2018-10-10 15:51:41 +08:00
|
|
|
console.warn(
|
|
|
|
`Restoring failed for ${identifier} from ${filename}: ${
|
2018-10-10 16:51:08 +08:00
|
|
|
log >= 3 ? err.stack : err
|
2018-10-10 15:51:41 +08:00
|
|
|
}`
|
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
compiler.cache.hooks.shutdown.tapPromise("FileCachePlugin", () => {
|
|
|
|
isIdle = false;
|
|
|
|
const promises = Array.from(pendingPromiseFactories.values()).map(fn =>
|
|
|
|
fn()
|
|
|
|
);
|
|
|
|
pendingPromiseFactories.clear();
|
|
|
|
if (currentIdlePromise !== undefined) promises.push(currentIdlePromise);
|
|
|
|
return Promise.all(promises);
|
|
|
|
});
|
|
|
|
|
|
|
|
let currentIdlePromise;
|
|
|
|
let isIdle = false;
|
|
|
|
const processIdleTasks = () => {
|
|
|
|
if (isIdle && pendingPromiseFactories.size > 0) {
|
|
|
|
const promises = [];
|
|
|
|
const maxTime = Date.now() + 100;
|
|
|
|
let maxCount = 100;
|
|
|
|
for (const [filename, factory] of pendingPromiseFactories) {
|
|
|
|
pendingPromiseFactories.delete(filename);
|
|
|
|
promises.push(factory());
|
|
|
|
if (maxCount-- <= 0 || Date.now() > maxTime) break;
|
|
|
|
}
|
|
|
|
currentIdlePromise = Promise.all(promises).then(() => {
|
|
|
|
currentIdlePromise = undefined;
|
|
|
|
});
|
|
|
|
currentIdlePromise.then(processIdleTasks);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
compiler.cache.hooks.beginIdle.tap("FileCachePlugin", () => {
|
|
|
|
isIdle = true;
|
|
|
|
Promise.resolve().then(processIdleTasks);
|
|
|
|
});
|
|
|
|
compiler.cache.hooks.endIdle.tap("FileCachePlugin", () => {
|
|
|
|
isIdle = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2018-10-18 04:15:46 +08:00
|
|
|
|
2018-10-09 20:30:59 +08:00
|
|
|
module.exports = FileCachePlugin;
|