native-compile-cache.js
1 const Module = require('module'); 2 const path = require('path'); 3 const crypto = require('crypto'); 4 const vm = require('vm'); 5 6 function computeHash(contents) { 7 return crypto 8 .createHash('sha1') 9 .update(contents, 'utf8') 10 .digest('hex'); 11 } 12 13 class NativeCompileCache { 14 constructor() { 15 this.cacheStore = null; 16 this.previousModuleCompile = null; 17 } 18 19 setCacheStore(store) { 20 this.cacheStore = store; 21 } 22 23 setV8Version(v8Version) { 24 this.v8Version = v8Version.toString(); 25 } 26 27 install() { 28 this.savePreviousModuleCompile(); 29 this.overrideModuleCompile(); 30 } 31 32 uninstall() { 33 this.restorePreviousModuleCompile(); 34 } 35 36 savePreviousModuleCompile() { 37 this.previousModuleCompile = Module.prototype._compile; 38 } 39 40 runInThisContext(code, filename) { 41 // TodoElectronIssue: produceCachedData is deprecated after Node 10.6, so we'll 42 // will need to update this for Electron v4 to use script.createCachedData(). 43 const script = new vm.Script(code, { filename, produceCachedData: true }); 44 return { 45 result: script.runInThisContext(), 46 cacheBuffer: script.cachedDataProduced ? script.cachedData : null 47 }; 48 } 49 50 runInThisContextCached(code, filename, cachedData) { 51 const script = new vm.Script(code, { filename, cachedData }); 52 return { 53 result: script.runInThisContext(), 54 wasRejected: script.cachedDataRejected 55 }; 56 } 57 58 overrideModuleCompile() { 59 let self = this; 60 // Here we override Node's module.js 61 // (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing 62 // only the bits that affect compilation in order to use the cached one. 63 Module.prototype._compile = function(content, filename) { 64 let moduleSelf = this; 65 // remove shebang 66 content = content.replace(/^#!.*/, ''); 67 function require(path) { 68 return moduleSelf.require(path); 69 } 70 require.resolve = function(request) { 71 return Module._resolveFilename(request, moduleSelf); 72 }; 73 require.main = process.mainModule; 74 75 // Enable support to add extra extension types 76 require.extensions = Module._extensions; 77 require.cache = Module._cache; 78 79 let dirname = path.dirname(filename); 80 81 // create wrapper function 82 let wrapper = Module.wrap(content); 83 84 let cacheKey = computeHash(wrapper + self.v8Version); 85 let compiledWrapper = null; 86 if (self.cacheStore.has(cacheKey)) { 87 let buffer = self.cacheStore.get(cacheKey); 88 let compilationResult = self.runInThisContextCached( 89 wrapper, 90 filename, 91 buffer 92 ); 93 compiledWrapper = compilationResult.result; 94 if (compilationResult.wasRejected) { 95 self.cacheStore.delete(cacheKey); 96 } 97 } else { 98 let compilationResult; 99 try { 100 compilationResult = self.runInThisContext(wrapper, filename); 101 } catch (err) { 102 console.error(`Error running script ${filename}`); 103 throw err; 104 } 105 if (compilationResult.cacheBuffer) { 106 self.cacheStore.set(cacheKey, compilationResult.cacheBuffer); 107 } 108 compiledWrapper = compilationResult.result; 109 } 110 111 let args = [ 112 moduleSelf.exports, 113 require, 114 moduleSelf, 115 filename, 116 dirname, 117 process, 118 global, 119 Buffer 120 ]; 121 return compiledWrapper.apply(moduleSelf.exports, args); 122 }; 123 } 124 125 restorePreviousModuleCompile() { 126 Module.prototype._compile = this.previousModuleCompile; 127 } 128 } 129 130 module.exports = new NativeCompileCache();