prettier/scripts/build/cache.js

132 lines
3.7 KiB
JavaScript

"use strict";
const util = require("util");
const assert = require("assert");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { rollup } = require("rollup");
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const ROOT = path.join(__dirname, "..", "..");
function Cache(cacheDir, version) {
this.cacheDir = path.resolve(cacheDir || required("cacheDir"));
this.manifest = path.join(this.cacheDir, "manifest.json");
this.version = version || required("version");
this.checksums = {};
this.files = {};
this.updated = {
version: this.version,
checksums: {},
files: {}
};
}
// Loads the manifest.json file with the information from the last build
Cache.prototype.load = async function() {
// This should never throw, if it does, let it fail the build
const lockfile = await readFile("yarn.lock", "utf-8");
const lockfileHash = hashString(lockfile);
this.updated.checksums["yarn.lock"] = lockfileHash;
try {
const manifest = await readFile(this.manifest, "utf-8");
const { version, checksums, files } = JSON.parse(manifest);
// Ignore the cache if the version changed
assert.equal(this.version, version);
assert.ok(typeof checksums === "object");
// If yarn.lock changed, rebuild everything
assert.equal(lockfileHash, checksums["yarn.lock"]);
this.checksums = checksums;
assert.ok(typeof files === "object");
this.files = files;
for (const files of Object.values(this.files)) {
assert.ok(Array.isArray(files));
}
} catch (err) {
this.checksums = {};
this.files = {};
}
};
// Run rollup to get the list of files included in the bundle and check if
// any (or the list itself) have changed.
// This takes the same rollup config used for bundling to include files that are
// resolved by specific plugins.
Cache.prototype.checkBundle = async function(id, inputOptions, outputOptions) {
const files = new Set(this.files[id]);
const newFiles = (this.updated.files[id] = []);
let dirty = false;
const bundle = await rollup(getRollupConfig(inputOptions));
const { output } = await bundle.generate(outputOptions);
const modules = output
.filter(mod => !/\0/.test(mod.facadeModuleId))
.map(mod => [path.relative(ROOT, mod.facadeModuleId), mod.code]);
for (const [id, code] of modules) {
newFiles.push(id);
// If we already checked this file for another bundle, reuse the hash
if (!this.updated.checksums[id]) {
this.updated.checksums[id] = hashString(code);
}
const hash = this.updated.checksums[id];
// Check if this file changed
if (!this.checksums[id] || this.checksums[id] !== hash) {
dirty = true;
}
// Check if this file is new
if (!files.delete(id)) {
dirty = true;
}
}
// Final check: if any file was removed, `files` is not empty
return !dirty && files.size === 0;
};
Cache.prototype.save = async function() {
try {
await writeFile(this.manifest, JSON.stringify(this.updated, null, 2));
} catch (err) {
// Don't fail the build
}
};
function required(name) {
throw new Error(name + " is required");
}
function hashString(string) {
return crypto
.createHash("md5")
.update(string)
.digest("hex");
}
function getRollupConfig(rollupConfig) {
return Object.assign({}, rollupConfig, {
onwarn() {},
plugins: rollupConfig.plugins.filter(
plugin =>
// We're not interested in dependencies, we already check `yarn.lock`
plugin.name !== "node-resolve" &&
// This is really slow, we need this "preflight" to be fast
plugin.name !== "babel"
)
});
}
module.exports = Cache;