Home Reference Source Test Repository

src/compile-cache.js

  1. import fs from 'fs';
  2. import path from 'path';
  3. import zlib from 'zlib';
  4. import createDigestForObject from './digest-for-object';
  5. import {pfs, pzlib} from './promise';
  6. import mkdirp from 'mkdirp';
  7.  
  8. const d = require('debug')('electron-compile:compile-cache');
  9.  
  10. /**
  11. * CompileCache manages getting and setting entries for a single compiler; each
  12. * in-use compiler will have an instance of this class, usually created via
  13. * {@link createFromCompiler}.
  14. *
  15. * You usually will not use this class directly, it is an implementation class
  16. * for {@link CompileHost}.
  17. */
  18. export default class CompileCache {
  19. /**
  20. * Creates an instance, usually used for testing only.
  21. *
  22. * @param {string} cachePath The root directory to use as a cache path
  23. *
  24. * @param {FileChangedCache} fileChangeCache A file-change cache that is
  25. * optionally pre-loaded.
  26. */
  27. constructor(cachePath, fileChangeCache) {
  28. this.cachePath = cachePath;
  29. this.fileChangeCache = fileChangeCache;
  30. }
  31. /**
  32. * Creates a CompileCache from a class compatible with the CompilerBase
  33. * interface. This method uses the compiler name / version / options to
  34. * generate a unique directory name for cached results
  35. *
  36. * @param {string} cachePath The root path to use for the cache, a directory
  37. * representing the hash of the compiler parameters
  38. * will be created here.
  39. *
  40. * @param {CompilerBase} compiler The compiler to use for version / option
  41. * information.
  42. *
  43. * @param {FileChangedCache} fileChangeCache A file-change cache that is
  44. * optionally pre-loaded.
  45. *
  46. * @return {CompileCache} A configured CompileCache instance.
  47. */
  48. static createFromCompiler(cachePath, compiler, fileChangeCache) {
  49. let newCachePath = null;
  50. let getCachePath = () => {
  51. if (newCachePath) return newCachePath;
  52.  
  53. const digestObj = {
  54. name: compiler.name || Object.getPrototypeOf(compiler).constructor.name,
  55. version: compiler.getCompilerVersion(),
  56. options: compiler.compilerOptions
  57. };
  58.  
  59. newCachePath = path.join(cachePath, createDigestForObject(digestObj));
  60.  
  61. d(`Path for ${digestObj.name}: ${newCachePath}`);
  62. d(`Set up with parameters: ${JSON.stringify(digestObj)}`);
  63. mkdirp.sync(newCachePath);
  64. return newCachePath;
  65. };
  66. let ret = new CompileCache('', fileChangeCache);
  67. ret.getCachePath = getCachePath;
  68. return ret;
  69. }
  70. /**
  71. * Returns a file's compiled contents from the cache.
  72. *
  73. * @param {string} filePath The path to the file. FileChangedCache will look
  74. * up the hash and use that as the key in the cache.
  75. *
  76. * @return {Promise<Object>} An object with all kinds of information
  77. *
  78. * @property {Object} hashInfo The hash information returned from getHashForPath
  79. * @property {string} code The source code if the file was a text file
  80. * @property {Buffer} binaryData The file if it was a binary file
  81. * @property {string} mimeType The MIME type saved in the cache.
  82. * @property {string[]} dependentFiles The dependent files returned from
  83. * compiling the file, if any.
  84. */
  85. async get(filePath) {
  86. d(`Fetching ${filePath} from cache`);
  87. let hashInfo = await this.fileChangeCache.getHashForPath(path.resolve(filePath));
  88. let code = null;
  89. let mimeType = null;
  90. let binaryData = null;
  91. let dependentFiles = null;
  92. let cacheFile = null;
  93. try {
  94. cacheFile = path.join(this.getCachePath(), hashInfo.hash);
  95. let result = null;
  96.  
  97. if (hashInfo.isFileBinary) {
  98. d("File is binary, reading out info");
  99. let info = JSON.parse(await pfs.readFile(cacheFile + '.info'));
  100. mimeType = info.mimeType;
  101. dependentFiles = info.dependentFiles;
  102. binaryData = hashInfo.binaryData;
  103. if (!binaryData) {
  104. binaryData = await pfs.readFile(cacheFile);
  105. binaryData = await pzlib.gunzip(binaryData);
  106. }
  107. } else {
  108. let buf = await pfs.readFile(cacheFile);
  109. let str = (await pzlib.gunzip(buf)).toString('utf8');
  110.  
  111. result = JSON.parse(str);
  112. code = result.code;
  113. mimeType = result.mimeType;
  114. dependentFiles = result.dependentFiles;
  115. }
  116. } catch (e) {
  117. d(`Failed to read cache for ${filePath}, looked in ${cacheFile}: ${e.message}`);
  118. }
  119. return { hashInfo, code, mimeType, binaryData, dependentFiles };
  120. }
  121.  
  122. /**
  123. * Saves a compiled result to cache
  124. *
  125. * @param {Object} hashInfo The hash information returned from getHashForPath
  126. *
  127. * @param {string / Buffer} codeOrBinaryData The file's contents, either as
  128. * a string or a Buffer.
  129. * @param {string} mimeType The MIME type returned by the compiler.
  130. *
  131. * @param {string[]} dependentFiles The list of dependent files returned by
  132. * the compiler.
  133. * @return {Promise} Completion.
  134. */
  135. async save(hashInfo, codeOrBinaryData, mimeType, dependentFiles) {
  136. let buf = null;
  137. let target = path.join(this.getCachePath(), hashInfo.hash);
  138. d(`Saving to ${target}`);
  139. if (hashInfo.isFileBinary) {
  140. buf = await pzlib.gzip(codeOrBinaryData);
  141. await pfs.writeFile(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8');
  142. } else {
  143. buf = await pzlib.gzip(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles})));
  144. }
  145. await pfs.writeFile(target, buf);
  146. }
  147. /**
  148. * Attempts to first get a key via {@link get}, then if it fails, call a method
  149. * to retrieve the contents, then save the result to cache.
  150. *
  151. * The fetcher parameter is expected to have the signature:
  152. *
  153. * Promise<Object> fetcher(filePath : string, hashInfo : Object);
  154. *
  155. * hashInfo is a value returned from getHashForPath
  156. * The return value of fetcher must be an Object with the properties:
  157. *
  158. * mimeType - the MIME type of the data to save
  159. * code (optional) - the source code as a string, if file is text
  160. * binaryData (optional) - the file contents as a Buffer, if file is binary
  161. * dependentFiles - the dependent files returned by the compiler.
  162. *
  163. * @param {string} filePath The path to the file. FileChangedCache will look
  164. * up the hash and use that as the key in the cache.
  165. *
  166. * @param {Function} fetcher A method which conforms to the description above.
  167. *
  168. * @return {Promise<Object>} An Object which has the same fields as the
  169. * {@link get} method return result.
  170. */
  171. async getOrFetch(filePath, fetcher) {
  172. let cacheResult = await this.get(filePath);
  173. if (cacheResult.code || cacheResult.binaryData) return cacheResult;
  174. let result = await fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo };
  175. if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) {
  176. d(`Cache miss: saving out info for ${filePath}`);
  177. await this.save(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles);
  178. }
  179. result.hashInfo = cacheResult.hashInfo;
  180. return result;
  181. }
  182. getSync(filePath) {
  183. d(`Fetching ${filePath} from cache`);
  184. let hashInfo = this.fileChangeCache.getHashForPathSync(path.resolve(filePath));
  185. let code = null;
  186. let mimeType = null;
  187. let binaryData = null;
  188. let dependentFiles = null;
  189. try {
  190. let cacheFile = path.join(this.getCachePath(), hashInfo.hash);
  191. let result = null;
  192. if (hashInfo.isFileBinary) {
  193. d("File is binary, reading out info");
  194. let info = JSON.parse(fs.readFileSync(cacheFile + '.info'));
  195. mimeType = info.mimeType;
  196. dependentFiles = info.dependentFiles;
  197. binaryData = hashInfo.binaryData;
  198. if (!binaryData) {
  199. binaryData = fs.readFileSync(cacheFile);
  200. binaryData = zlib.gunzipSync(binaryData);
  201. }
  202. } else {
  203. let buf = fs.readFileSync(cacheFile);
  204. let str = (zlib.gunzipSync(buf)).toString('utf8');
  205.  
  206. result = JSON.parse(str);
  207. code = result.code;
  208. mimeType = result.mimeType;
  209. dependentFiles = result.dependentFiles;
  210. }
  211. } catch (e) {
  212. d(`Failed to read cache for ${filePath}`);
  213. }
  214. return { hashInfo, code, mimeType, binaryData, dependentFiles };
  215. }
  216.  
  217. saveSync(hashInfo, codeOrBinaryData, mimeType, dependentFiles) {
  218. let buf = null;
  219. let target = path.join(this.getCachePath(), hashInfo.hash);
  220. d(`Saving to ${target}`);
  221. if (hashInfo.isFileBinary) {
  222. buf = zlib.gzipSync(codeOrBinaryData);
  223. fs.writeFileSync(target + '.info', JSON.stringify({mimeType, dependentFiles}), 'utf8');
  224. } else {
  225. buf = zlib.gzipSync(new Buffer(JSON.stringify({code: codeOrBinaryData, mimeType, dependentFiles})));
  226. }
  227. fs.writeFileSync(target, buf);
  228. }
  229. getOrFetchSync(filePath, fetcher) {
  230. let cacheResult = this.getSync(filePath);
  231. if (cacheResult.code || cacheResult.binaryData) return cacheResult;
  232. let result = fetcher(filePath, cacheResult.hashInfo) || { hashInfo: cacheResult.hashInfo };
  233. if (result.mimeType && !cacheResult.hashInfo.isInNodeModules) {
  234. d(`Cache miss: saving out info for ${filePath}`);
  235. this.saveSync(cacheResult.hashInfo, result.code || result.binaryData, result.mimeType, result.dependentFiles);
  236. }
  237. result.hashInfo = cacheResult.hashInfo;
  238. return result;
  239. }
  240. /**
  241. * @private
  242. */
  243. getCachePath() {
  244. // NB: This is an evil hack so that createFromCompiler can stomp it
  245. // at will
  246. return this.cachePath;
  247. }
  248. /**
  249. * Returns whether a file should not be compiled. Note that this doesn't
  250. * necessarily mean it won't end up in the cache, only that its contents are
  251. * saved verbatim instead of trying to find an appropriate compiler.
  252. *
  253. * @param {Object} hashInfo The hash information returned from getHashForPath
  254. *
  255. * @return {boolean} True if a file should be ignored
  256. */
  257. static shouldPassthrough(hashInfo) {
  258. return hashInfo.isMinified || hashInfo.isInNodeModules || hashInfo.hasSourceMap || hashInfo.isFileBinary;
  259. }
  260. }