You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							374 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
	
	
							374 lines
						
					
					
						
							11 KiB
						
					
					
				| 'use strict';
 | |
| 
 | |
| const Module = require('module');
 | |
| const crypto = require('crypto');
 | |
| const fs = require('fs');
 | |
| const path = require('path');
 | |
| const vm = require('vm');
 | |
| const os = require('os');
 | |
| 
 | |
| const hasOwnProperty = Object.prototype.hasOwnProperty;
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // FileSystemBlobStore
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| class FileSystemBlobStore {
 | |
|   constructor(directory, prefix) {
 | |
|     const name = prefix ? slashEscape(prefix + '.') : '';
 | |
|     this._blobFilename = path.join(directory, name + 'BLOB');
 | |
|     this._mapFilename = path.join(directory, name + 'MAP');
 | |
|     this._lockFilename = path.join(directory, name + 'LOCK');
 | |
|     this._directory = directory;
 | |
|     this._load();
 | |
|   }
 | |
| 
 | |
|   has(key, invalidationKey) {
 | |
|     if (hasOwnProperty.call(this._memoryBlobs, key)) {
 | |
|       return this._invalidationKeys[key] === invalidationKey;
 | |
|     } else if (hasOwnProperty.call(this._storedMap, key)) {
 | |
|       return this._storedMap[key][0] === invalidationKey;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   get(key, invalidationKey) {
 | |
|     if (hasOwnProperty.call(this._memoryBlobs, key)) {
 | |
|       if (this._invalidationKeys[key] === invalidationKey) {
 | |
|         return this._memoryBlobs[key];
 | |
|       }
 | |
|     } else if (hasOwnProperty.call(this._storedMap, key)) {
 | |
|       const mapping = this._storedMap[key];
 | |
|       if (mapping[0] === invalidationKey) {
 | |
|         return this._storedBlob.slice(mapping[1], mapping[2]);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   set(key, invalidationKey, buffer) {
 | |
|     this._invalidationKeys[key] = invalidationKey;
 | |
|     this._memoryBlobs[key] = buffer;
 | |
|     this._dirty = true;
 | |
|   }
 | |
| 
 | |
|   delete(key) {
 | |
|     if (hasOwnProperty.call(this._memoryBlobs, key)) {
 | |
|       this._dirty = true;
 | |
|       delete this._memoryBlobs[key];
 | |
|     }
 | |
|     if (hasOwnProperty.call(this._invalidationKeys, key)) {
 | |
|       this._dirty = true;
 | |
|       delete this._invalidationKeys[key];
 | |
|     }
 | |
|     if (hasOwnProperty.call(this._storedMap, key)) {
 | |
|       this._dirty = true;
 | |
|       delete this._storedMap[key];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   isDirty() {
 | |
|     return this._dirty;
 | |
|   }
 | |
| 
 | |
|   save() {
 | |
|     const dump = this._getDump();
 | |
|     const blobToStore = Buffer.concat(dump[0]);
 | |
|     const mapToStore = JSON.stringify(dump[1]);
 | |
| 
 | |
|     try {
 | |
|       mkdirpSync(this._directory);
 | |
|       fs.writeFileSync(this._lockFilename, 'LOCK', {flag: 'wx'});
 | |
|     } catch (error) {
 | |
|       // Swallow the exception if we fail to acquire the lock.
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       fs.writeFileSync(this._blobFilename, blobToStore);
 | |
|       fs.writeFileSync(this._mapFilename, mapToStore);
 | |
|     } finally {
 | |
|       fs.unlinkSync(this._lockFilename);
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   _load() {
 | |
|     try {
 | |
|       this._storedBlob = fs.readFileSync(this._blobFilename);
 | |
|       this._storedMap = JSON.parse(fs.readFileSync(this._mapFilename));
 | |
|     } catch (e) {
 | |
|       this._storedBlob = Buffer.alloc(0);
 | |
|       this._storedMap = {};
 | |
|     }
 | |
|     this._dirty = false;
 | |
|     this._memoryBlobs = {};
 | |
|     this._invalidationKeys = {};
 | |
|   }
 | |
| 
 | |
|   _getDump() {
 | |
|     const buffers = [];
 | |
|     const newMap = {};
 | |
|     let offset = 0;
 | |
| 
 | |
|     function push(key, invalidationKey, buffer) {
 | |
|       buffers.push(buffer);
 | |
|       newMap[key] = [invalidationKey, offset, offset + buffer.length];
 | |
|       offset += buffer.length;
 | |
|     }
 | |
| 
 | |
|     for (const key of Object.keys(this._memoryBlobs)) {
 | |
|       const buffer = this._memoryBlobs[key];
 | |
|       const invalidationKey = this._invalidationKeys[key];
 | |
|       push(key, invalidationKey, buffer);
 | |
|     }
 | |
| 
 | |
|     for (const key of Object.keys(this._storedMap)) {
 | |
|       if (hasOwnProperty.call(newMap, key)) continue;
 | |
|       const mapping = this._storedMap[key];
 | |
|       const buffer = this._storedBlob.slice(mapping[1], mapping[2]);
 | |
|       push(key, mapping[0], buffer);
 | |
|     }
 | |
| 
 | |
|     return [buffers, newMap];
 | |
|   }
 | |
| }
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // NativeCompileCache
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| class NativeCompileCache {
 | |
|   constructor() {
 | |
|     this._cacheStore = null;
 | |
|     this._previousModuleCompile = null;
 | |
|   }
 | |
| 
 | |
|   setCacheStore(cacheStore) {
 | |
|     this._cacheStore = cacheStore;
 | |
|   }
 | |
| 
 | |
|   install() {
 | |
|     const self = this;
 | |
|     const hasRequireResolvePaths = typeof require.resolve.paths === 'function';
 | |
|     this._previousModuleCompile = Module.prototype._compile;
 | |
|     Module.prototype._compile = function(content, filename) {
 | |
|       const mod = this;
 | |
| 
 | |
|       function require(id) {
 | |
|         return mod.require(id);
 | |
|       }
 | |
| 
 | |
|       // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28
 | |
|       function resolve(request, options) {
 | |
|         return Module._resolveFilename(request, mod, false, options);
 | |
|       }
 | |
|       require.resolve = resolve;
 | |
| 
 | |
|       // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L37
 | |
|       // resolve.resolve.paths was added in v8.9.0
 | |
|       if (hasRequireResolvePaths) {
 | |
|         resolve.paths = function paths(request) {
 | |
|           return Module._resolveLookupPaths(request, mod, true);
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       require.main = process.mainModule;
 | |
| 
 | |
|       // Enable support to add extra extension types
 | |
|       require.extensions = Module._extensions;
 | |
|       require.cache = Module._cache;
 | |
| 
 | |
|       const dirname = path.dirname(filename);
 | |
| 
 | |
|       const compiledWrapper = self._moduleCompile(filename, content);
 | |
| 
 | |
|       // We skip the debugger setup because by the time we run, node has already
 | |
|       // done that itself.
 | |
| 
 | |
|       // `Buffer` is included for Electron.
 | |
|       // See https://github.com/zertosh/v8-compile-cache/pull/10#issuecomment-518042543
 | |
|       const args = [mod.exports, require, mod, filename, dirname, process, global, Buffer];
 | |
|       return compiledWrapper.apply(mod.exports, args);
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   uninstall() {
 | |
|     Module.prototype._compile = this._previousModuleCompile;
 | |
|   }
 | |
| 
 | |
|   _moduleCompile(filename, content) {
 | |
|     // https://github.com/nodejs/node/blob/v7.5.0/lib/module.js#L511
 | |
| 
 | |
|     // Remove shebang
 | |
|     var contLen = content.length;
 | |
|     if (contLen >= 2) {
 | |
|       if (content.charCodeAt(0) === 35/*#*/ &&
 | |
|           content.charCodeAt(1) === 33/*!*/) {
 | |
|         if (contLen === 2) {
 | |
|           // Exact match
 | |
|           content = '';
 | |
|         } else {
 | |
|           // Find end of shebang line and slice it off
 | |
|           var i = 2;
 | |
|           for (; i < contLen; ++i) {
 | |
|             var code = content.charCodeAt(i);
 | |
|             if (code === 10/*\n*/ || code === 13/*\r*/) break;
 | |
|           }
 | |
|           if (i === contLen) {
 | |
|             content = '';
 | |
|           } else {
 | |
|             // Note that this actually includes the newline character(s) in the
 | |
|             // new output. This duplicates the behavior of the regular
 | |
|             // expression that was previously used to replace the shebang line
 | |
|             content = content.slice(i);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // create wrapper function
 | |
|     var wrapper = Module.wrap(content);
 | |
| 
 | |
|     var invalidationKey = crypto
 | |
|       .createHash('sha1')
 | |
|       .update(content, 'utf8')
 | |
|       .digest('hex');
 | |
| 
 | |
|     var buffer = this._cacheStore.get(filename, invalidationKey);
 | |
| 
 | |
|     var script = new vm.Script(wrapper, {
 | |
|       filename: filename,
 | |
|       lineOffset: 0,
 | |
|       displayErrors: true,
 | |
|       cachedData: buffer,
 | |
|       produceCachedData: true,
 | |
|     });
 | |
| 
 | |
|     if (script.cachedDataProduced) {
 | |
|       this._cacheStore.set(filename, invalidationKey, script.cachedData);
 | |
|     } else if (script.cachedDataRejected) {
 | |
|       this._cacheStore.delete(filename);
 | |
|     }
 | |
| 
 | |
|     var compiledWrapper = script.runInThisContext({
 | |
|       filename: filename,
 | |
|       lineOffset: 0,
 | |
|       columnOffset: 0,
 | |
|       displayErrors: true,
 | |
|     });
 | |
| 
 | |
|     return compiledWrapper;
 | |
|   }
 | |
| }
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // utilities
 | |
| //
 | |
| // https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98
 | |
| // https://github.com/zertosh/slash-escape/blob/e7ebb99/slash-escape.js
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| function mkdirpSync(p_) {
 | |
|   _mkdirpSync(path.resolve(p_), 0o777);
 | |
| }
 | |
| 
 | |
| function _mkdirpSync(p, mode) {
 | |
|   try {
 | |
|     fs.mkdirSync(p, mode);
 | |
|   } catch (err0) {
 | |
|     if (err0.code === 'ENOENT') {
 | |
|       _mkdirpSync(path.dirname(p));
 | |
|       _mkdirpSync(p);
 | |
|     } else {
 | |
|       try {
 | |
|         const stat = fs.statSync(p);
 | |
|         if (!stat.isDirectory()) { throw err0; }
 | |
|       } catch (err1) {
 | |
|         throw err0;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function slashEscape(str) {
 | |
|   const ESCAPE_LOOKUP = {
 | |
|     '\\': 'zB',
 | |
|     ':': 'zC',
 | |
|     '/': 'zS',
 | |
|     '\x00': 'z0',
 | |
|     'z': 'zZ',
 | |
|   };
 | |
|   const ESCAPE_REGEX = /[\\:/\x00z]/g; // eslint-disable-line no-control-regex
 | |
|   return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]);
 | |
| }
 | |
| 
 | |
| function supportsCachedData() {
 | |
|   const script = new vm.Script('""', {produceCachedData: true});
 | |
|   // chakracore, as of v1.7.1.0, returns `false`.
 | |
|   return script.cachedDataProduced === true;
 | |
| }
 | |
| 
 | |
| function getCacheDir() {
 | |
|   const v8_compile_cache_cache_dir = process.env.V8_COMPILE_CACHE_CACHE_DIR;
 | |
|   if (v8_compile_cache_cache_dir) {
 | |
|     return v8_compile_cache_cache_dir;
 | |
|   }
 | |
| 
 | |
|   // Avoid cache ownership issues on POSIX systems.
 | |
|   const dirname = typeof process.getuid === 'function'
 | |
|     ? 'v8-compile-cache-' + process.getuid()
 | |
|     : 'v8-compile-cache';
 | |
|   // Avoid cache incompatibility issues with Rosetta on Apple Silicon.
 | |
|   const arch = process.arch;
 | |
|   const version = typeof process.versions.v8 === 'string'
 | |
|     ? process.versions.v8
 | |
|     : typeof process.versions.chakracore === 'string'
 | |
|       ? 'chakracore-' + process.versions.chakracore
 | |
|       : 'node-' + process.version;
 | |
|   const cacheDir = path.join(os.tmpdir(), dirname, arch, version);
 | |
|   return cacheDir;
 | |
| }
 | |
| 
 | |
| function getMainName() {
 | |
|   // `require.main.filename` is undefined or null when:
 | |
|   //    * node -e 'require("v8-compile-cache")'
 | |
|   //    * node -r 'v8-compile-cache'
 | |
|   //    * Or, requiring from the REPL.
 | |
|   const mainName = require.main && typeof require.main.filename === 'string'
 | |
|     ? require.main.filename
 | |
|     : process.cwd();
 | |
|   return mainName;
 | |
| }
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // main
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) {
 | |
|   const cacheDir = getCacheDir();
 | |
|   const prefix = getMainName();
 | |
|   const blobStore = new FileSystemBlobStore(cacheDir, prefix);
 | |
| 
 | |
|   const nativeCompileCache = new NativeCompileCache();
 | |
|   nativeCompileCache.setCacheStore(blobStore);
 | |
|   nativeCompileCache.install();
 | |
| 
 | |
|   process.once('exit', () => {
 | |
|     if (blobStore.isDirty()) {
 | |
|       blobStore.save();
 | |
|     }
 | |
|     nativeCompileCache.uninstall();
 | |
|   });
 | |
| }
 | |
| 
 | |
| module.exports.__TEST__ = {
 | |
|   FileSystemBlobStore,
 | |
|   NativeCompileCache,
 | |
|   mkdirpSync,
 | |
|   slashEscape,
 | |
|   supportsCachedData,
 | |
|   getCacheDir,
 | |
|   getMainName,
 | |
| };
 |