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