src/compile-cache.js
import fs from 'fs';
import path from 'path';
import zlib from 'zlib';
import createDigestForObject from './digest-for-object';
import {pfs, pzlib} from './promise';
import mkdirp from 'mkdirp';
const d = require('debug')('electron-compile:compile-cache');
/**
* CompileCache manages getting and setting entries for a single compiler; each
* in-use compiler will have an instance of this class, usually created via
* {@link createFromCompiler}.
*
* You usually will not use this class directly, it is an implementation class
* for {@link CompileHost}.
*/
export default class CompileCache {
/**
* Creates an instance, usually used for testing only.
*
* @param {string} cachePath The root directory to use as a cache path
*
* @param {FileChangedCache} fileChangeCache A file-change cache that is
* optionally pre-loaded.
*/
constructor(cachePath, fileChangeCache) {
this.cachePath = cachePath;
this.fileChangeCache = fileChangeCache;
}
/**
* Creates a CompileCache from a class compatible with the CompilerBase
* interface. This method uses the compiler name / version / options to
* generate a unique directory name for cached results
*
* @param {string} cachePath The root path to use for the cache, a directory
* representing the hash of the compiler parameters
* will be created here.
*
* @param {CompilerBase} compiler The compiler to use for version / option
* information.
*
* @param {FileChangedCache} fileChangeCache A file-change cache that is
* optionally pre-loaded.
*
* @return {CompileCache} A configured CompileCache instance.
*/
static createFromCompiler(cachePath, compiler, fileChangeCache) {
let newCachePath = null;
let getCachePath = () => {
if (newCachePath) return newCachePath;
const digestObj = {
name: compiler.name || Object.getPrototypeOf(compiler).constructor.name,
version: compiler.getCompilerVersion(),
options: compiler.compilerOptions
};
newCachePath = path.join(cachePath, createDigestForObject(digestObj));
d(`Path for ${digestObj.name}: ${newCachePath}`);
d(`Set up with parameters: ${JSON.stringify(digestObj)}`);
mkdirp.sync(newCachePath);
return newCachePath;
};
let ret = new CompileCache('', fileChangeCache);
ret.getCachePath = getCachePath;
return ret;
}
/**
* Returns a file's compiled contents from the cache.
*
* @param {string} filePath The path to the file. FileChangedCache will look
* up the hash and use that as the key in the cache.
*
* @return {Promise<Object>} An object with all kinds of information
*
* @property {Object} hashInfo The hash information returned from getHashForPath
* @property {string} code The source code if the file was a text file
* @property {Buffer} binaryData The file if it was a binary file
* @property {string} mimeType The MIME type saved in the cache.
* @property {string[]} dependentFiles The dependent files returned from
* compiling the file, if any.
*/
async get(filePath) {
d(`Fetching ${filePath} from cache`);
let hashInfo = await this.fileChangeCache.getHashForPath(path.resolve(filePath));
let code = null;
let mimeType = null;
let binaryData = null;
let dependentFiles = null;
let cacheFile = null;
try {
cacheFile = path.join(this.getCachePath(), hashInfo.hash);
let result = null;
if (hashInfo.isFileBinary) {
d("File is binary, reading out info");
let info = JSON.parse(await pfs.readFile(cacheFile + '.info'));
mimeType = info.mimeType;
dependentFiles = info.dependentFiles;
binaryData = hashInfo.binaryData;
if (!binaryData) {
binaryData = await pfs.readFile(cacheFile);
binaryData = await pzlib.gunzip(binaryData);
}
} else {
let buf = await pfs.readFile(cacheFile);
let str = (await pzlib.gunzip(buf)).toString('utf8');
result = JSON.parse(str);
code = result.code;
mimeType = result.mimeType;
dependentFiles = result.dependentFiles;
}
} catch (e) {
d(`Failed to read cache for ${filePath}, looked in ${cacheFile}: ${e.message}`);
}
return { hashInfo, code, mimeType, binaryData, dependentFiles };
}
/**
* Saves a compiled result to cache
*
* @param {Object} hashInfo The hash information returned from getHashForPath
*
* @param {string / Buffer} codeOrBinaryData The file's contents, either as
* a string or a Buffer.
* @param {string} mimeType The MIME type returned by the compiler.
*
* @param {string[]} dependentFiles The list of dependent files returned by
* the compiler.
* @return {Promise} Completion.
*/
async save(hashInfo, codeOrBinaryData, mimeType, dependentFiles) {
let buf = null;
let target = path.join(this.getCachePath(), hashInfo.hash);
d(`Saving to ${target}`);
if (hashInfo.isFileBinary) {
buf = await pzlib.gzip(codeOrBinaryData);
await pfs.writeFile(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8');
} else {
buf = await pzlib.gzip(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles})));
}
await pfs.writeFile(target, buf);
}
/**
* Attempts to first get a key via {@link get}, then if it fails, call a method
* to retrieve the contents, then save the result to cache.
*
* The fetcher parameter is expected to have the signature:
*
* Promise<Object> fetcher(filePath : string, hashInfo : Object);
*
* hashInfo is a value returned from getHashForPath
* The return value of fetcher must be an Object with the properties:
*
* mimeType - the MIME type of the data to save
* code (optional) - the source code as a string, if file is text
* binaryData (optional) - the file contents as a Buffer, if file is binary
* dependentFiles - the dependent files returned by the compiler.
*
* @param {string} filePath The path to the file. FileChangedCache will look
* up the hash and use that as the key in the cache.
*
* @param {Function} fetcher A method which conforms to the description above.
*
* @return {Promise<Object>} An Object which has the same fields as the
* {@link get} method return result.
*/
async getOrFetch(filePath, fetcher) {
let cacheResult = await this.get(filePath);
if (cacheResult.code || cacheResult.binaryData) return cacheResult;
let result = await fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo };
if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) {
d(`Cache miss: saving out info for ${filePath}`);
await this.save(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles);
}
result.hashInfo = cacheResult.hashInfo;
return result;
}
getSync(filePath) {
d(`Fetching ${filePath} from cache`);
let hashInfo = this.fileChangeCache.getHashForPathSync(path.resolve(filePath));
let code = null;
let mimeType = null;
let binaryData = null;
let dependentFiles = null;
try {
let cacheFile = path.join(this.getCachePath(), hashInfo.hash);
let result = null;
if (hashInfo.isFileBinary) {
d("File is binary, reading out info");
let info = JSON.parse(fs.readFileSync(cacheFile + '.info'));
mimeType = info.mimeType;
dependentFiles = info.dependentFiles;
binaryData = hashInfo.binaryData;
if (!binaryData) {
binaryData = fs.readFileSync(cacheFile);
binaryData = zlib.gunzipSync(binaryData);
}
} else {
let buf = fs.readFileSync(cacheFile);
let str = (zlib.gunzipSync(buf)).toString('utf8');
result = JSON.parse(str);
code = result.code;
mimeType = result.mimeType;
dependentFiles = result.dependentFiles;
}
} catch (e) {
d(`Failed to read cache for ${filePath}`);
}
return { hashInfo, code, mimeType, binaryData, dependentFiles };
}
saveSync(hashInfo, codeOrBinaryData, mimeType, dependentFiles) {
let buf = null;
let target = path.join(this.getCachePath(), hashInfo.hash);
d(`Saving to ${target}`);
if (hashInfo.isFileBinary) {
buf = zlib.gzipSync(codeOrBinaryData);
fs.writeFileSync(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8');
} else {
buf = zlib.gzipSync(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles})));
}
fs.writeFileSync(target, buf);
}
getOrFetchSync(filePath, fetcher) {
let cacheResult = this.getSync(filePath);
if (cacheResult.code || cacheResult.binaryData) return cacheResult;
let result = fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo };
if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) {
d(`Cache miss: saving out info for ${filePath}`);
this.saveSync(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles);
}
result.hashInfo = cacheResult.hashInfo;
return result;
}
/**
* @private
*/
getCachePath() {
// NB: This is an evil hack so that createFromCompiler can stomp it
// at will
return this.cachePath;
}
/**
* Returns whether a file should not be compiled. Note that this doesn't
* necessarily mean it won't end up in the cache, only that its contents are
* saved verbatim instead of trying to find an appropriate compiler.
*
* @param {Object} hashInfo The hash information returned from getHashForPath
*
* @return {boolean} True if a file should be ignored
*/
static shouldPassthrough(hashInfo) {
return hashInfo.isMinified || hashInfo.isInNodeModules || hashInfo.hasSourceMap || hashInfo.isFileBinary;
}
}