2018-10-09 20:30:59 +08:00
|
|
|
/*
|
|
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
|
|
Author Tobias Koppers @sokra
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2018-10-23 17:31:20 +08:00
|
|
|
const fs = require("fs");
|
2018-10-09 20:30:59 +08:00
|
|
|
const path = require("path");
|
|
|
|
const createHash = require("../util/createHash");
|
2018-10-23 17:31:20 +08:00
|
|
|
const makeSerializable = require("../util/makeSerializable");
|
2018-10-09 20:30:59 +08:00
|
|
|
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 */
|
|
|
|
|
2018-10-23 17:31:20 +08:00
|
|
|
class Pack {
|
|
|
|
constructor(version) {
|
|
|
|
this.version = version;
|
|
|
|
this.content = new Map();
|
|
|
|
this.lastAccess = new Map();
|
|
|
|
this.used = new Set();
|
|
|
|
this.invalid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
get(relativeFilename) {
|
|
|
|
this.used.add(relativeFilename);
|
|
|
|
return this.content.get(relativeFilename);
|
|
|
|
}
|
|
|
|
|
|
|
|
set(relativeFilename, data) {
|
|
|
|
this.used.add(relativeFilename);
|
|
|
|
this.invalid = true;
|
|
|
|
return this.content.set(relativeFilename, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
collectGarbage(maxAge) {
|
|
|
|
this._updateLastAccess();
|
|
|
|
const now = Date.now();
|
|
|
|
for (const [relativeFilename, lastAccess] of this.lastAccess) {
|
|
|
|
if (now - lastAccess > maxAge) {
|
|
|
|
this.lastAccess.delete(relativeFilename);
|
|
|
|
this.content.delete(relativeFilename);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateLastAccess() {
|
|
|
|
const now = Date.now();
|
|
|
|
for (const relativeFilename of this.used) {
|
|
|
|
this.lastAccess.set(relativeFilename, now);
|
|
|
|
}
|
|
|
|
this.used.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
serialize({ write, snapshot, rollback }) {
|
|
|
|
this._updateLastAccess();
|
|
|
|
write(this.version);
|
|
|
|
for (const [relativeFilename, data] of this.content) {
|
|
|
|
const s = snapshot();
|
|
|
|
try {
|
|
|
|
write(relativeFilename);
|
|
|
|
write(data);
|
|
|
|
} catch (err) {
|
|
|
|
rollback(s);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
write(null);
|
|
|
|
write(this.lastAccess);
|
|
|
|
}
|
|
|
|
|
|
|
|
deserialize({ read }) {
|
|
|
|
this.version = read();
|
|
|
|
this.content = new Map();
|
|
|
|
let relativeFilename = read();
|
|
|
|
while (relativeFilename !== null) {
|
|
|
|
this.content.set(relativeFilename, read());
|
|
|
|
relativeFilename = read();
|
|
|
|
}
|
|
|
|
this.lastAccess = read();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
makeSerializable(Pack, "webpack/lib/cache/FileCachePlugin", "Pack");
|
|
|
|
|
2018-10-09 20:30:59 +08:00
|
|
|
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
|
2018-10-23 13:33:45 +08:00
|
|
|
? { debug: 4, verbose: 3, info: 2, warning: 1 }[this.options.loglevel]
|
2018-10-10 16:51:08 +08:00
|
|
|
: 0;
|
2018-10-29 19:06:21 +08:00
|
|
|
const store = this.options.store || "pack";
|
2018-10-09 20:30:59 +08:00
|
|
|
|
|
|
|
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)}`;
|
|
|
|
};
|
2018-10-23 17:31:20 +08:00
|
|
|
let packPromise;
|
|
|
|
if (store === "pack") {
|
|
|
|
packPromise = serializer
|
|
|
|
.deserializeFromFile(`${cacheDirectory}.pack`)
|
|
|
|
.then(cacheEntry => {
|
|
|
|
if (cacheEntry) {
|
|
|
|
if (!(cacheEntry instanceof Pack)) {
|
2018-10-23 13:33:45 +08:00
|
|
|
if (log >= 3) {
|
2018-10-23 17:31:20 +08:00
|
|
|
console.warn(
|
|
|
|
`Restored pack from ${cacheDirectory}.pack, but is not a Pack.`
|
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
2018-10-23 17:31:20 +08:00
|
|
|
return new Pack(version);
|
|
|
|
}
|
|
|
|
if (cacheEntry.version !== version) {
|
2018-10-23 13:33:45 +08:00
|
|
|
if (log >= 3) {
|
2018-10-10 23:19:31 +08:00
|
|
|
console.warn(
|
2018-10-23 17:31:20 +08:00
|
|
|
`Restored pack from ${cacheDirectory}.pack, but version doesn't match.`
|
2018-10-10 23:19:31 +08:00
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
2018-10-23 17:31:20 +08:00
|
|
|
return new Pack(version);
|
|
|
|
}
|
|
|
|
return cacheEntry;
|
|
|
|
}
|
|
|
|
return new Pack(version);
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
if (log >= 1 && err && err.code !== "ENOENT") {
|
|
|
|
console.warn(
|
|
|
|
`Restoring pack failed from ${cacheDirectory}.pack: ${
|
2018-10-23 13:33:45 +08:00
|
|
|
log >= 4 ? err.stack : err
|
2018-10-23 17:31:20 +08:00
|
|
|
}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return new Pack(version);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
compiler.cache.hooks.store.tapPromise(
|
|
|
|
"FileCachePlugin",
|
|
|
|
(identifier, etag, data) => {
|
|
|
|
const entry = {
|
|
|
|
identifier,
|
2018-10-29 19:07:02 +08:00
|
|
|
data: etag ? () => data : data,
|
2018-10-23 17:31:20 +08:00
|
|
|
etag,
|
|
|
|
version
|
|
|
|
};
|
|
|
|
const relativeFilename = toHash(identifier) + ".data";
|
|
|
|
const filename = path.join(cacheDirectory, relativeFilename);
|
|
|
|
memoryCache.set(filename, entry);
|
|
|
|
const promiseFactory =
|
|
|
|
store === "pack"
|
|
|
|
? () =>
|
|
|
|
packPromise.then(pack => {
|
|
|
|
if (log >= 2) {
|
|
|
|
console.warn(`Cached ${identifier} to pack.`);
|
|
|
|
}
|
|
|
|
pack.set(relativeFilename, entry);
|
|
|
|
})
|
|
|
|
: () =>
|
|
|
|
serializer
|
|
|
|
.serializeToFile(entry, filename)
|
|
|
|
.then(() => {
|
|
|
|
if (log >= 2) {
|
|
|
|
console.warn(`Cached ${identifier} to ${filename}.`);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
if (log >= 1) {
|
|
|
|
console.warn(
|
|
|
|
`Caching failed for ${identifier}: ${
|
|
|
|
log >= 3 ? err.stack : err
|
|
|
|
}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (store === "instant" || store === "pack") {
|
2018-10-09 20:30:59 +08:00
|
|
|
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) => {
|
2018-10-23 17:31:20 +08:00
|
|
|
const relativeFilename = toHash(identifier) + ".data";
|
|
|
|
const filename = path.join(cacheDirectory, relativeFilename);
|
|
|
|
const logMessage = store === "pack" ? "pack" : filename;
|
2018-10-09 20:30:59 +08:00
|
|
|
const memory = memoryCache.get(filename);
|
|
|
|
if (memory !== undefined) {
|
|
|
|
return Promise.resolve(
|
2018-10-23 17:31:20 +08:00
|
|
|
memory.etag !== etag || memory.version !== version
|
|
|
|
? undefined
|
|
|
|
: typeof memory.data === "function"
|
|
|
|
? memory.data()
|
|
|
|
: memory.data
|
2018-10-09 20:30:59 +08:00
|
|
|
);
|
|
|
|
}
|
2018-10-23 17:31:20 +08:00
|
|
|
const cacheEntryPromise =
|
|
|
|
store === "pack"
|
|
|
|
? packPromise.then(pack => pack.get(relativeFilename))
|
|
|
|
: serializer.deserializeFromFile(filename);
|
|
|
|
return cacheEntryPromise.then(
|
2018-10-09 20:30:59 +08:00
|
|
|
cacheEntry => {
|
2018-10-23 17:31:20 +08:00
|
|
|
if (cacheEntry === undefined) return;
|
|
|
|
if (typeof cacheEntry.data === "function")
|
|
|
|
cacheEntry.data = memorize(cacheEntry.data);
|
2018-10-09 20:30:59 +08:00
|
|
|
memoryCache.set(filename, cacheEntry);
|
|
|
|
if (cacheEntry === undefined) return;
|
|
|
|
if (cacheEntry.identifier !== identifier) {
|
2018-10-23 13:33:45 +08:00
|
|
|
if (log >= 3) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(
|
2018-10-23 17:31:20 +08:00
|
|
|
`Restored ${identifier} from ${logMessage}, but identifier doesn't match.`
|
2018-10-09 20:30:59 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (cacheEntry.etag !== etag) {
|
2018-10-23 13:33:45 +08:00
|
|
|
if (log >= 3) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(
|
2018-10-23 17:31:20 +08:00
|
|
|
`Restored ${identifier} from ${logMessage}, but etag doesn't match.`
|
2018-10-09 20:30:59 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (cacheEntry.version !== version) {
|
2018-10-23 13:33:45 +08:00
|
|
|
if (log >= 3) {
|
2018-10-09 20:30:59 +08:00
|
|
|
console.warn(
|
2018-10-23 17:31:20 +08:00
|
|
|
`Restored ${identifier} from ${logMessage}, but version doesn't match.`
|
2018-10-09 20:30:59 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2018-10-23 13:33:45 +08:00
|
|
|
if (log >= 3) {
|
2018-10-23 17:31:20 +08:00
|
|
|
console.warn(`Restored ${identifier} from ${logMessage}.`);
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
2018-10-23 17:31:20 +08:00
|
|
|
if (typeof cacheEntry.data === "function") return cacheEntry.data();
|
|
|
|
return cacheEntry.data;
|
2018-10-09 20:30:59 +08:00
|
|
|
},
|
|
|
|
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(
|
2018-10-23 17:31:20 +08:00
|
|
|
`Restoring failed for ${identifier} from ${logMessage}: ${
|
2018-10-23 13:33:45 +08:00
|
|
|
log >= 4 ? err.stack : err
|
2018-10-10 15:51:41 +08:00
|
|
|
}`
|
|
|
|
);
|
2018-10-09 20:30:59 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
2018-10-23 17:31:20 +08:00
|
|
|
const serializePack = () => {
|
|
|
|
packPromise = packPromise.then(pack => {
|
|
|
|
if (!pack.invalid) return pack;
|
|
|
|
if (log >= 3) {
|
|
|
|
console.warn(`Storing pack...`);
|
|
|
|
}
|
|
|
|
pack.collectGarbage(1000 * 60 * 60 * 24 * 2);
|
|
|
|
return serializer
|
|
|
|
.serializeToFile(pack, `${cacheDirectory}.pack~`)
|
|
|
|
.then(
|
|
|
|
result =>
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
if (!result) {
|
|
|
|
if (log >= 1) {
|
|
|
|
console.warn(
|
|
|
|
'Caching failed for pack, because content is flagged as not serializable. Use store: "idle" instead.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
fs.unlink(`${cacheDirectory}.pack`, err => {
|
|
|
|
fs.rename(
|
|
|
|
`${cacheDirectory}.pack~`,
|
|
|
|
`${cacheDirectory}.pack`,
|
|
|
|
err => {
|
|
|
|
if (err) return reject(err);
|
|
|
|
if (log >= 3) {
|
|
|
|
console.warn(`Stored pack`);
|
|
|
|
}
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.then(() => {
|
|
|
|
return serializer.deserializeFromFile(`${cacheDirectory}.pack`);
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
if (log >= 1) {
|
|
|
|
console.warn(
|
2018-10-23 13:33:45 +08:00
|
|
|
`Caching failed for pack: ${log >= 4 ? err.stack : err}`
|
2018-10-23 17:31:20 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return new Pack(version);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return packPromise;
|
|
|
|
};
|
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);
|
2018-10-23 17:31:20 +08:00
|
|
|
const promise = Promise.all(promises);
|
|
|
|
if (store === "pack") {
|
|
|
|
return promise.then(serializePack);
|
|
|
|
}
|
|
|
|
return promise;
|
2018-10-09 20:30:59 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
2018-10-23 17:31:20 +08:00
|
|
|
if (store === "pack") {
|
|
|
|
pendingPromiseFactories.delete("pack");
|
|
|
|
pendingPromiseFactories.set("pack", serializePack);
|
|
|
|
}
|
2018-10-09 20:30:59 +08:00
|
|
|
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;
|