| '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 = this._ownModuleCompile = _ownModuleCompile; |
| self.enabled = true; |
| function _ownModuleCompile(content, filename) { |
| if(!self.enabled) return this._previousModuleCompile.apply(this, arguments); |
| 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() { |
| this.enabled = false; |
| // If something else has since been installed on top of us, we cannot overwrite it. |
| if(Module.prototype._compile === this._ownModuleCompile) { |
| 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'; |
| 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, 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; |
| } |
| |
| function install(opts) { |
| if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) { |
| if(typeof opts === 'undefined') opts = {} |
| let cacheDir = opts.cacheDir |
| if(typeof cacheDir === 'undefined') cacheDir = getCacheDir(); |
| let prefix = opts.prefix |
| if(typeof prefix === 'undefined') prefix = getMainName(); |
| const blobStore = new FileSystemBlobStore(cacheDir, prefix); |
| |
| const nativeCompileCache = new NativeCompileCache(); |
| nativeCompileCache.setCacheStore(blobStore); |
| nativeCompileCache.install(); |
| |
| let uninstalled = false; |
| const uninstall = () => { |
| if(uninstalled) return; |
| uninstalled = true; |
| process.removeListener('exit', uninstall); |
| if (blobStore.isDirty()) { |
| blobStore.save(); |
| } |
| nativeCompileCache.uninstall(); |
| } |
| process.once('exit', uninstall); |
| return {uninstall}; |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| // main |
| //------------------------------------------------------------------------------ |
| |
| module.exports.install = install; |
| |
| module.exports.__TEST__ = { |
| FileSystemBlobStore, |
| NativeCompileCache, |
| mkdirpSync, |
| slashEscape, |
| supportsCachedData, |
| getCacheDir, |
| getMainName, |
| }; |