UNPKG

electron-compile

Version:

Electron supporting package to compile JS and CSS in Electron applications

295 lines (252 loc) 10.8 kB
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')('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. * * @param {boolean} readOnlyMode Don't attempt to create the cache directory. * * @return {CompileCache} A configured CompileCache instance. */ static createFromCompiler(cachePath, compiler, fileChangeCache, readOnlyMode=false) { 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)}`); if (!readOnlyMode) 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; } }