├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bench ├── _measure.js ├── require-babel-core.js ├── require-flow-parser.js ├── require-rxjs-bundle.js ├── require-rxjs-module.js ├── require-yarn.js └── run.sh ├── package.json ├── test ├── FileSystemBlobStore-mock.js ├── FileSystemBlobStore-test.js ├── NativeCompileCache-test.js ├── arch │ ├── run.sh │ └── yarn.js ├── fixtures │ ├── file-1.js │ ├── file-2.js │ ├── file-3.js │ ├── file-4.js │ ├── print-main-name.js │ └── require-resolve-paths.js ├── getCacheDir-test.js ├── getMainName-test.js ├── mkdirpSync-test.js ├── module-test.js ├── node-version-test.js └── slashEscape-test.js └── v8-compile-cache.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 6 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "strict": "warn" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | node_js: 5 | - "4" 6 | - "5.6.0" 7 | - "5" # Introduced "cachedData"/"produceCachedData" after 5.7, need 5.10 for Buffer.from() 8 | - "6" 9 | - "8" 10 | - "10" 11 | - "12" 12 | - "14" 13 | - "15" 14 | before_install: 15 | - case "$TRAVIS_NODE_VERSION" in 16 | 5|5.6.0) 17 | npm install -g npm@4; 18 | ;; 19 | esac 20 | script: 21 | - case "$TRAVIS_NODE_VERSION" in 22 | 4|5.6.0) 23 | tap test/node-version-test.js; 24 | ;; 25 | *) 26 | npm run tap; 27 | ;; 28 | esac 29 | - case "$TRAVIS_NODE_VERSION" in 30 | 4|5.6.0|5|6|8) 31 | ;; 32 | *) 33 | npm run eslint; 34 | npm run bench; 35 | ;; 36 | esac 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `v8-module-cache` Changelog 2 | 3 | ## Next 4 | 5 | 6 | 7 | ## 2023-08-14, Version 2.4.0 8 | 9 | * Fix segmentation fault with Rosetta on Apple Silicon [#45](https://github.com/zertosh/v8-compile-cache/pull/45). 10 | 11 | ## 2021-03-05, Version 2.3.0 12 | 13 | * Fix use require.main instead of module.parent [#34](https://github.com/zertosh/v8-compile-cache/pull/34). 14 | 15 | ## 2020-10-28, Version 2.2.0 16 | 17 | * Added `V8_COMPILE_CACHE_CACHE_DIR` option [#23](https://github.com/zertosh/v8-compile-cache/pull/23). 18 | 19 | ## 2020-05-30, Version 2.1.1 20 | 21 | * Stop using process.umask() [#28](https://github.com/zertosh/v8-compile-cache/pull/28). 22 | 23 | ## 2019-08-04, Version 2.1.0 24 | 25 | * Fix Electron by calling the module wrapper with `Buffer` [#10](https://github.com/zertosh/v8-compile-cache/pull/10). 26 | 27 | ## 2019-05-10, Version 2.0.3 28 | 29 | * Add `LICENSE` file [#19](https://github.com/zertosh/v8-compile-cache/pull/19). 30 | * Add "repository" to `package.json` (see [eea336e](https://github.com/zertosh/v8-compile-cache/commit/eea336eaa8360f9ded9342b8aa928e56ac6a7529)). 31 | * Support `require.resolve.paths` (added in Node v8.9.0) [#20](https://github.com/zertosh/v8-compile-cache/pull/20)/[#22](https://github.com/zertosh/v8-compile-cache/pull/22). 32 | 33 | ## 2018-08-06, Version 2.0.2 34 | 35 | * Re-publish. 36 | 37 | ## 2018-08-06, Version 2.0.1 38 | 39 | * Support `require.resolve` options (added in Node v8.9.0). 40 | 41 | ## 2018-04-30, Version 2.0.0 42 | 43 | * Use `Buffer.alloc` instead of `new Buffer()`. 44 | * Drop support for Node 5.x. 45 | 46 | ## 2018-01-23, Version 1.1.2 47 | 48 | * Instead of checking for `process.versions.v8`, check that `script.cachedDataProduced` is `true` (rather than `null`/`undefined`) for support to be considered existent. 49 | 50 | ## 2018-01-23, Version 1.1.1 51 | 52 | * Check for the existence of `process.versions.v8` before attaching hook (see [f8b0388](https://github.com/zertosh/v8-compile-cache/commit/f8b038848be94bc2c905880dd50447c73393f364)). 53 | 54 | ## 2017-03-27, Version 1.1.0 55 | 56 | * Safer cache directory creation (see [bcb3b12](https://github.com/zertosh/v8-compile-cache/commit/bcb3b12c819ab0927ec4408e70f612a6d50a9617)). 57 | - The cache is now suffixed with the user's uid on POSIX systems (i.e. `/path/to/tmp/v8-compile-cache-1234`). 58 | 59 | ## 2017-02-21, Version 1.0.0 60 | 61 | * Initial release. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Andres Suarez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v8-compile-cache 2 | 3 | [![Build Status](https://travis-ci.org/zertosh/v8-compile-cache.svg?branch=master)](https://travis-ci.org/zertosh/v8-compile-cache) 4 | 5 | `v8-compile-cache` attaches a `require` hook to use [V8's code cache](https://v8project.blogspot.com/2015/07/code-caching.html) to speed up instantiation time. The "code cache" is the work of parsing and compiling done by V8. 6 | 7 | The ability to tap into V8 to produce/consume this cache was introduced in [Node v5.7.0](https://nodejs.org/en/blog/release/v5.7.0/). 8 | 9 | ## Usage 10 | 11 | 1. Add the dependency: 12 | 13 | ```sh 14 | $ npm install --save v8-compile-cache 15 | ``` 16 | 17 | 2. Then, in your entry module add: 18 | 19 | ```js 20 | require('v8-compile-cache'); 21 | ``` 22 | 23 | **Requiring `v8-compile-cache` in Node <5.7.0 is a noop – but you need at least Node 4.0.0 to support the ES2015 syntax used by `v8-compile-cache`.** 24 | 25 | ## Options 26 | 27 | Set the environment variable `DISABLE_V8_COMPILE_CACHE=1` to disable the cache. 28 | 29 | Cache directory is defined by environment variable `V8_COMPILE_CACHE_CACHE_DIR` or defaults to `/v8-compile-cache-`. 30 | 31 | ## Internals 32 | 33 | Cache files are suffixed `.BLOB` and `.MAP` corresponding to the entry module that required `v8-compile-cache`. The cache is _entry module specific_ because it is faster to load the entire code cache into memory at once, than it is to read it from disk on a file-by-file basis. 34 | 35 | ## Benchmarks 36 | 37 | See https://github.com/zertosh/v8-compile-cache/tree/master/bench. 38 | 39 | **Load Times:** 40 | 41 | | Module | Without Cache | With Cache | 42 | | ---------------- | -------------:| ----------:| 43 | | `babel-core` | `218ms` | `185ms` | 44 | | `yarn` | `153ms` | `113ms` | 45 | | `yarn` (bundled) | `228ms` | `105ms` | 46 | 47 | _^ Includes the overhead of loading the cache itself._ 48 | 49 | ## Acknowledgements 50 | 51 | * `FileSystemBlobStore` and `NativeCompileCache` are based on Atom's implementation of their v8 compile cache: 52 | - https://github.com/atom/atom/blob/b0d7a8a/src/file-system-blob-store.js 53 | - https://github.com/atom/atom/blob/b0d7a8a/src/native-compile-cache.js 54 | * `mkdirpSync` is based on: 55 | - https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98 56 | -------------------------------------------------------------------------------- /bench/_measure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (name, withCache, callback) => { 4 | let s; 5 | 6 | const logs = []; 7 | logs.push(`node: ${parseInt(process.uptime() * 1000, 10)}ms`); 8 | 9 | // So each test gets its own cache 10 | module.filename = require.main.filename; 11 | s = Date.now(); 12 | if (withCache) require('../v8-compile-cache'); 13 | logs.push(`require-cache: ${Date.now() - s}ms`); 14 | module.filename = __filename; 15 | 16 | s = Date.now(); 17 | callback(); 18 | logs.push(`${name}: ${Date.now() - s}ms`); 19 | 20 | s = Date.now(); 21 | process.on('exit', () => { 22 | logs.push(`exit: ${Date.now() - s}ms`); 23 | logs.push(`total: ${parseInt(process.uptime() * 1000, 10)}ms`); 24 | console.log(logs.join('\t')); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /bench/require-babel-core.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const WITH_CACHE = true; 5 | 6 | require('./_measure.js')('require-babel-core', WITH_CACHE, () => { 7 | process.argv.push('config', 'get', 'init.author.name'); 8 | require('babel-core'); 9 | }); 10 | -------------------------------------------------------------------------------- /bench/require-flow-parser.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const WITH_CACHE = true; 5 | 6 | require('./_measure.js')('require-flow-parser', WITH_CACHE, () => { 7 | require('flow-parser'); 8 | }); 9 | -------------------------------------------------------------------------------- /bench/require-rxjs-bundle.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const WITH_CACHE = true; 5 | 6 | require('./_measure.js')('require-rxjs-bundle', WITH_CACHE, () => { 7 | require('rxjs/bundles/rxjs.umd.js'); 8 | }); 9 | -------------------------------------------------------------------------------- /bench/require-rxjs-module.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const WITH_CACHE = true; 5 | 6 | require('./_measure.js')('require-rxjs-module', WITH_CACHE, () => { 7 | require('rxjs'); 8 | }); 9 | -------------------------------------------------------------------------------- /bench/require-yarn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const WITH_CACHE = true; 5 | 6 | require('./_measure.js')('require-yarn', WITH_CACHE, () => { 7 | process.argv.push('config', 'get', 'init.author.name'); 8 | require('yarn/lib/cli.js'); 9 | }); 10 | -------------------------------------------------------------------------------- /bench/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | V8_COMPILE_CACHE_CACHE_DIR=$(mktemp -d) 6 | export V8_COMPILE_CACHE_CACHE_DIR=$V8_COMPILE_CACHE_CACHE_DIR 7 | trap 'rm -r "$V8_COMPILE_CACHE_CACHE_DIR"' EXIT 8 | 9 | THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 10 | : "${NODE_BIN:=node}" 11 | 12 | # shellcheck disable=SC2016 13 | "$NODE_BIN" -p '`node ${process.versions.node}, v8 ${process.versions.v8}`' 14 | 15 | for f in "$THIS_DIR"/require-*.js; do 16 | printf 'Running "%s"\n' "$(basename "$f")" 17 | for _ in {1..5}; do "$NODE_BIN" "$f"; done 18 | done 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-compile-cache", 3 | "version": "2.4.0", 4 | "description": "Require hook for automatic V8 compile cache persistence", 5 | "main": "v8-compile-cache.js", 6 | "scripts": { 7 | "bench": "bench/run.sh", 8 | "eslint": "eslint --max-warnings=0 .", 9 | "tap": "tap test/*-test.js", 10 | "test": "npm run tap", 11 | "posttest": "npm run eslint" 12 | }, 13 | "author": "Andres Suarez ", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/zertosh/v8-compile-cache.git" 17 | }, 18 | "files": [ 19 | "v8-compile-cache.js" 20 | ], 21 | "license": "MIT", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "babel-core": "6.26.3", 25 | "eslint": "^7.12.1", 26 | "flow-parser": "0.136.0", 27 | "rimraf": "^2.5.4", 28 | "rxjs": "6.6.3", 29 | "semver": "^5.3.0", 30 | "tap": "^9.0.0", 31 | "temp": "^0.8.3", 32 | "yarn": "1.22.10" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/FileSystemBlobStore-mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class FileSystemBlobStoreMock { 4 | constructor() { 5 | this._cachedFiles = []; 6 | } 7 | 8 | has(key, invalidationKey) { 9 | return !!this._cachedFiles.find( 10 | file => file.key === key && file.invalidationKey === invalidationKey 11 | ); 12 | } 13 | 14 | get(key, invalidationKey) { 15 | if (this.has(key, invalidationKey)) { 16 | return this._cachedFiles.find( 17 | file => file.key === key && file.invalidationKey === invalidationKey 18 | ).buffer; 19 | } 20 | } 21 | 22 | set(key, invalidationKey, buffer) { 23 | const entry = this._cachedFiles.find( 24 | file => file.key === key && file.invalidationKey === invalidationKey 25 | ); 26 | if (entry == null) { 27 | this._cachedFiles.push({key, invalidationKey, buffer}); 28 | } else { 29 | entry.buffer = buffer; 30 | } 31 | return buffer; 32 | } 33 | 34 | delete(key) { 35 | const i = this._cachedFiles.findIndex(file => file.key === key); 36 | if (i != null) { 37 | this._cachedFiles.splice(i, 1); 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /test/FileSystemBlobStore-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const rimraf = require('rimraf'); 6 | const tap = require('tap'); 7 | const temp = require('temp'); 8 | 9 | process.env.DISABLE_V8_COMPILE_CACHE = 1; 10 | const FileSystemBlobStore = require('..').__TEST__.FileSystemBlobStore; 11 | 12 | temp.track(); 13 | 14 | let storageDirectory; 15 | let blobStore; 16 | 17 | tap.beforeEach(cb => { 18 | storageDirectory = temp.path('filesystemblobstore'); 19 | blobStore = new FileSystemBlobStore(storageDirectory); 20 | cb(); 21 | }); 22 | 23 | tap.afterEach(cb => { 24 | rimraf.sync(storageDirectory); 25 | cb(); 26 | }); 27 | 28 | tap.test('is empty when the file doesn\'t exist', t => { 29 | t.equal(blobStore.isDirty(), false); 30 | t.type(blobStore.get('foo', 'invalidation-key-1'), 'undefined'); 31 | t.type(blobStore.get('bar', 'invalidation-key-2'), 'undefined'); 32 | t.end(); 33 | }); 34 | 35 | tap.test('allows to read and write buffers from/to memory without persisting them', t => { 36 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 37 | blobStore.set('bar', 'invalidation-key-2', Buffer.from('bar')); 38 | 39 | t.same(blobStore.get('foo', 'invalidation-key-1'), Buffer.from('foo')); 40 | t.same(blobStore.get('bar', 'invalidation-key-2'), Buffer.from('bar')); 41 | 42 | t.type(blobStore.get('foo', 'unexisting-key'), 'undefined'); 43 | t.type(blobStore.get('bar', 'unexisting-key'), 'undefined'); 44 | 45 | t.end(); 46 | }); 47 | 48 | tap.test('persists buffers when saved and retrieves them on load, giving priority to in-memory ones', t => { 49 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 50 | blobStore.set('bar', 'invalidation-key-2', Buffer.from('bar')); 51 | blobStore.save(); 52 | 53 | blobStore = new FileSystemBlobStore(storageDirectory); 54 | 55 | t.same(blobStore.get('foo', 'invalidation-key-1'), Buffer.from('foo')); 56 | t.same(blobStore.get('bar', 'invalidation-key-2'), Buffer.from('bar')); 57 | t.type(blobStore.get('foo', 'unexisting-key'), 'undefined'); 58 | t.type(blobStore.get('bar', 'unexisting-key'), 'undefined'); 59 | 60 | blobStore.set('foo', 'new-key', Buffer.from('changed')); 61 | 62 | t.same(blobStore.get('foo', 'new-key'), Buffer.from('changed')); 63 | t.type(blobStore.get('foo', 'invalidation-key-1'), 'undefined'); 64 | 65 | t.done(); 66 | }); 67 | 68 | tap.test('persists both in-memory and previously stored buffers when saved', t => { 69 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 70 | blobStore.set('bar', 'invalidation-key-2', Buffer.from('bar')); 71 | blobStore.save(); 72 | 73 | blobStore = new FileSystemBlobStore(storageDirectory); 74 | 75 | blobStore.set('bar', 'invalidation-key-3', Buffer.from('changed')); 76 | blobStore.set('qux', 'invalidation-key-4', Buffer.from('qux')); 77 | blobStore.save(); 78 | 79 | blobStore = new FileSystemBlobStore(storageDirectory); 80 | 81 | t.same(blobStore.get('foo', 'invalidation-key-1'), Buffer.from('foo')); 82 | t.same(blobStore.get('bar', 'invalidation-key-3'), Buffer.from('changed')); 83 | t.same(blobStore.get('qux', 'invalidation-key-4'), Buffer.from('qux')); 84 | t.type(blobStore.get('foo', 'unexisting-key'), 'undefined'); 85 | t.type(blobStore.get('bar', 'invalidation-key-2'), 'undefined'); 86 | t.type(blobStore.get('qux', 'unexisting-key'), 'undefined'); 87 | 88 | t.end(); 89 | }); 90 | 91 | tap.test('allows to delete keys from both memory and stored buffers', t => { 92 | blobStore.set('a', 'invalidation-key-1', Buffer.from('a')); 93 | blobStore.set('b', 'invalidation-key-2', Buffer.from('b')); 94 | blobStore.save(); 95 | 96 | blobStore = new FileSystemBlobStore(storageDirectory); 97 | 98 | blobStore.set('b', 'invalidation-key-3', Buffer.from('b')); 99 | blobStore.set('c', 'invalidation-key-4', Buffer.from('c')); 100 | blobStore.delete('b'); 101 | blobStore.delete('c'); 102 | blobStore.save(); 103 | 104 | blobStore = new FileSystemBlobStore(storageDirectory); 105 | 106 | t.same(blobStore.get('a', 'invalidation-key-1'), Buffer.from('a')); 107 | t.type(blobStore.get('b', 'invalidation-key-2'), 'undefined'); 108 | t.type(blobStore.get('b', 'invalidation-key-3'), 'undefined'); 109 | t.type(blobStore.get('c', 'invalidation-key-4'), 'undefined'); 110 | 111 | t.end(); 112 | }); 113 | 114 | tap.test('ignores errors when loading an invalid blob store', t => { 115 | blobStore.set('a', 'invalidation-key-1', Buffer.from('a')); 116 | blobStore.set('b', 'invalidation-key-2', Buffer.from('b')); 117 | blobStore.save(); 118 | 119 | // Simulate corruption 120 | fs.writeFileSync(path.join(storageDirectory, 'MAP'), Buffer.from([0])); 121 | fs.writeFileSync(path.join(storageDirectory, 'BLOB'), Buffer.from([0])); 122 | 123 | blobStore = new FileSystemBlobStore(storageDirectory); 124 | 125 | t.type(blobStore.get('a', 'invalidation-key-1'), 'undefined'); 126 | t.type(blobStore.get('b', 'invalidation-key-2'), 'undefined'); 127 | 128 | blobStore.set('a', 'invalidation-key-1', Buffer.from('x')); 129 | blobStore.set('b', 'invalidation-key-2', Buffer.from('y')); 130 | blobStore.save(); 131 | 132 | blobStore = new FileSystemBlobStore(storageDirectory); 133 | 134 | t.same(blobStore.get('a', 'invalidation-key-1'), Buffer.from('x')); 135 | t.same(blobStore.get('b', 'invalidation-key-2'), Buffer.from('y')); 136 | 137 | t.end(); 138 | }); 139 | 140 | tap.test('object hash collision', t => { 141 | t.type(blobStore.get('constructor', 'invalidation-key-1'), 'undefined'); 142 | blobStore.delete('constructor'); 143 | t.type(blobStore.get('constructor', 'invalidation-key-1'), 'undefined'); 144 | 145 | blobStore.set('constructor', 'invalidation-key-1', Buffer.from('proto')); 146 | t.same(blobStore.get('constructor', 'invalidation-key-1'), Buffer.from('proto')); 147 | blobStore.save(); 148 | 149 | blobStore = new FileSystemBlobStore(storageDirectory); 150 | t.same(blobStore.get('constructor', 'invalidation-key-1'), Buffer.from('proto')); 151 | t.type(blobStore.get('hasOwnProperty', 'invalidation-key-2'), 'undefined'); 152 | 153 | t.end(); 154 | }); 155 | 156 | tap.test('dirty state (set)', t => { 157 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 158 | t.equal(blobStore.isDirty(), true); 159 | blobStore.save(); 160 | 161 | blobStore = new FileSystemBlobStore(storageDirectory); 162 | 163 | t.equal(blobStore.isDirty(), false); 164 | blobStore.set('foo', 'invalidation-key-2', Buffer.from('bar')); 165 | t.equal(blobStore.isDirty(), true); 166 | 167 | t.end(); 168 | }); 169 | 170 | tap.test('dirty state (delete memory)', t => { 171 | blobStore.delete('foo'); 172 | t.equal(blobStore.isDirty(), false); 173 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 174 | blobStore.delete('foo'); 175 | t.equal(blobStore.isDirty(), true); 176 | blobStore.save(); 177 | 178 | blobStore = new FileSystemBlobStore(storageDirectory); 179 | 180 | t.equal(blobStore.isDirty(), false); 181 | blobStore.set('foo', 'invalidation-key-2', Buffer.from('bar')); 182 | t.equal(blobStore.isDirty(), true); 183 | 184 | t.end(); 185 | }); 186 | 187 | tap.test('dirty state (delete stored)', t => { 188 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 189 | blobStore.save(); 190 | 191 | blobStore = new FileSystemBlobStore(storageDirectory); 192 | 193 | blobStore.delete('foo'); 194 | t.equal(blobStore.isDirty(), true); 195 | 196 | t.end(); 197 | }); 198 | 199 | tap.test('prefix', t => { 200 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 201 | blobStore.save(); 202 | 203 | t.ok(fs.existsSync(path.join(storageDirectory, 'MAP'))); 204 | t.ok(fs.existsSync(path.join(storageDirectory, 'BLOB'))); 205 | 206 | storageDirectory = temp.path('filesystemblobstore'); 207 | blobStore = new FileSystemBlobStore(storageDirectory, 'prefix'); 208 | blobStore.set('foo', 'invalidation-key-1', Buffer.from('foo')); 209 | blobStore.save(); 210 | 211 | t.ok(fs.existsSync(path.join(storageDirectory, 'prefix.MAP'))); 212 | t.ok(fs.existsSync(path.join(storageDirectory, 'prefix.BLOB'))); 213 | 214 | t.end(); 215 | }); 216 | -------------------------------------------------------------------------------- /test/NativeCompileCache-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Module = require('module'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const tap = require('tap'); 7 | const temp = require('temp'); 8 | 9 | temp.track(); 10 | 11 | const FileSystemBlobStore_mock = require('./FileSystemBlobStore-mock'); 12 | 13 | process.env.DISABLE_V8_COMPILE_CACHE = 1; 14 | const NativeCompileCache = require('..').__TEST__.NativeCompileCache; 15 | 16 | let cachedFiles; 17 | let fakeCacheStore; 18 | let nativeCompileCache; 19 | 20 | tap.beforeEach(cb => { 21 | fakeCacheStore = new FileSystemBlobStore_mock(); 22 | cachedFiles = fakeCacheStore._cachedFiles; 23 | nativeCompileCache = new NativeCompileCache(); 24 | nativeCompileCache.setCacheStore(fakeCacheStore); 25 | nativeCompileCache.install(); 26 | cb(); 27 | }); 28 | 29 | tap.afterEach(cb => { 30 | nativeCompileCache.uninstall(); 31 | cb(); 32 | }); 33 | 34 | tap.test('writes and reads from the cache storage when requiring files', t => { 35 | let fn1 = require('./fixtures/file-1'); 36 | const fn2 = require('./fixtures/file-2'); 37 | 38 | t.equal(cachedFiles.length, 2); 39 | 40 | t.equal(cachedFiles[0].key, require.resolve('./fixtures/file-1')); 41 | t.type(cachedFiles[0].buffer, Uint8Array); 42 | t.ok(cachedFiles[0].buffer.length > 0); 43 | t.equal(fn1(), 1); 44 | 45 | t.equal(cachedFiles[1].key, require.resolve('./fixtures/file-2')); 46 | t.type(cachedFiles[1].buffer, Uint8Array); 47 | t.ok(cachedFiles[1].buffer.length > 0); 48 | t.equal(fn2(), 2); 49 | 50 | delete Module._cache[require.resolve('./fixtures/file-1')]; 51 | fn1 = require('./fixtures/file-1'); 52 | t.equal(cachedFiles.length, 2); 53 | t.equal(fn1(), 1); 54 | 55 | t.end(); 56 | }); 57 | 58 | tap.test('when the cache changes it updates the new cache', t => { 59 | let fn4 = require('./fixtures/file-4'); 60 | 61 | t.equal(cachedFiles.length, 1); 62 | t.equal(cachedFiles[0].key, require.resolve('./fixtures/file-4')); 63 | t.type(cachedFiles[0].buffer, Uint8Array); 64 | t.ok(cachedFiles[0].buffer.length > 0); 65 | t.equal(fn4(), 'file-4'); 66 | 67 | const fakeCacheStore2 = new FileSystemBlobStore_mock(); 68 | const cachedFiles2 = fakeCacheStore._cachedFiles; 69 | nativeCompileCache.setCacheStore(fakeCacheStore2); 70 | 71 | delete Module._cache[require.resolve('./fixtures/file-4')]; 72 | fn4 = require('./fixtures/file-4'); 73 | 74 | t.equal(cachedFiles.length, 1); 75 | t.equal(cachedFiles2.length, 1); 76 | t.equal(cachedFiles[0].key, require.resolve('./fixtures/file-4')); 77 | t.equal(cachedFiles2[0].key, require.resolve('./fixtures/file-4')); 78 | t.equal(cachedFiles[0].invalidationKey, cachedFiles2[0].invalidationKey); 79 | t.type(cachedFiles[0].buffer, Uint8Array); 80 | t.type(cachedFiles2[0].buffer, Uint8Array); 81 | t.ok(cachedFiles[0].buffer.length > 0); 82 | t.ok(cachedFiles2[0].buffer.length > 0); 83 | 84 | t.end(); 85 | }); 86 | 87 | tap.test('replaces previously cached code when the cache is an invalid file', t => { 88 | fakeCacheStore.has = () => true; 89 | fakeCacheStore.get = () => Buffer.from('an invalid cache'); 90 | // NOTE: When `script.cachedDataProduced` is true, `_cacheStore` is updated 91 | // using `set` instead of `delete`. 92 | let setWasCalledWith = null; 93 | fakeCacheStore.set = arg => { setWasCalledWith = arg; }; 94 | 95 | const fn3 = require('./fixtures/file-3'); 96 | 97 | t.equal(setWasCalledWith, require.resolve('./fixtures/file-3')); 98 | t.equal(fn3(), 3); 99 | 100 | t.end(); 101 | }); 102 | 103 | tap.test('when a previously required and cached file changes removes it from the store and re-inserts it with the new cache', t => { 104 | const tmpDir = temp.mkdirSync('native-compile-cache-test'); 105 | const tmpFile = path.join(tmpDir, 'file-5.js'); 106 | fs.writeFileSync(tmpFile, 'module.exports = () => `file-5`;'); 107 | 108 | let fn5 = require(tmpFile); 109 | 110 | t.equal(cachedFiles.length, 1); 111 | t.equal(cachedFiles[0].key, require.resolve(tmpFile)); 112 | t.type(cachedFiles[0].buffer, Uint8Array); 113 | t.ok(cachedFiles[0].buffer.length > 0); 114 | t.equal(fn5(), 'file-5'); 115 | 116 | delete Module._cache[require.resolve(tmpFile)]; 117 | fs.appendFileSync(tmpFile, '\n\n'); 118 | fn5 = require(tmpFile); 119 | 120 | t.equal(cachedFiles.length, 2); 121 | t.equal(cachedFiles[1].key, require.resolve(tmpFile)); 122 | t.notEqual(cachedFiles[1].invalidationKey, cachedFiles[0].invalidationKey); 123 | t.type(cachedFiles[1].buffer, Uint8Array); 124 | t.ok(cachedFiles[1].buffer.length > 0); 125 | 126 | t.end(); 127 | }); 128 | -------------------------------------------------------------------------------- /test/arch/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | : "${NODE_BIN:=node}" 7 | 8 | if [ "$(/usr/bin/arch)" != "arm64" ]; then 9 | echo "arch/run.sh: This must be run on Apple Silicon." >&2 10 | exit 11 | fi 12 | 13 | # shellcheck disable=SC2016 14 | "$NODE_BIN" "$THIS_DIR/yarn.js" config get init.author.name >/dev/null 15 | /usr/bin/arch -x86_64 "$NODE_BIN" "$THIS_DIR/yarn.js" config get init.author.name >/dev/null 16 | 17 | echo "Success!" 18 | -------------------------------------------------------------------------------- /test/arch/yarn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require ('../../v8-compile-cache.js'); 4 | require('yarn/lib/cli.js'); 5 | -------------------------------------------------------------------------------- /test/fixtures/file-1.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { return 1; } 2 | -------------------------------------------------------------------------------- /test/fixtures/file-2.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { return 2; } 2 | -------------------------------------------------------------------------------- /test/fixtures/file-3.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { return 3; } 2 | -------------------------------------------------------------------------------- /test/fixtures/file-4.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { return "file-4" } 2 | -------------------------------------------------------------------------------- /test/fixtures/print-main-name.js: -------------------------------------------------------------------------------- 1 | console.log(require('../..').__TEST__.getMainName()); 2 | -------------------------------------------------------------------------------- /test/fixtures/require-resolve-paths.js: -------------------------------------------------------------------------------- 1 | console.log( 2 | '%j', 3 | { 4 | hasRequireResolve: typeof require.resolve.paths === 'function', 5 | value: typeof require.resolve.paths === 'function' 6 | ? require.resolve.paths(process.argv[2]) 7 | : undefined 8 | } 9 | ); 10 | -------------------------------------------------------------------------------- /test/getCacheDir-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const tap = require('tap'); 6 | const vm = require('vm'); 7 | 8 | process.env.DISABLE_V8_COMPILE_CACHE = 1; 9 | const getCacheDir = require('..').__TEST__.getCacheDir; 10 | 11 | tap.test('getCacheDir (v8)', t => { 12 | const cacheDir = getCacheDir(); 13 | const parts = cacheDir.split(os.tmpdir()); 14 | const nameParts = parts[1].split(path.sep); 15 | 16 | t.match(nameParts[1], /^v8-compile-cache(-\d+)?$/); 17 | t.equal(nameParts[2], process.arch); 18 | t.equal(nameParts[3], process.versions.v8); 19 | 20 | t.done(); 21 | }); 22 | 23 | tap.test('getCacheDir (chakracore)', t => { 24 | const cacheDir = vm.runInNewContext( 25 | '(' + getCacheDir.toString() + ')();', 26 | { 27 | process: { 28 | arch: process.arch, 29 | getuid: process.getuid, 30 | versions: {chakracore: '1.2.3'}, 31 | env: {}, 32 | }, 33 | path, 34 | os, 35 | } 36 | ); 37 | 38 | const parts = cacheDir.split(os.tmpdir()); 39 | const nameParts = parts[1].split(path.sep); 40 | 41 | t.match(nameParts[1], /^v8-compile-cache(-\d+)?$/); 42 | t.equal(nameParts[3], 'chakracore-1.2.3'); 43 | 44 | t.done(); 45 | }); 46 | 47 | tap.test('getCacheDir (unknown)', t => { 48 | const cacheDir = vm.runInNewContext( 49 | '(' + getCacheDir.toString() + ')();', 50 | { 51 | process: { 52 | arch: process.arch, 53 | getuid: process.getuid, 54 | version: '1.2.3', 55 | versions: {}, 56 | env: {}, 57 | }, 58 | path, 59 | os, 60 | } 61 | ); 62 | 63 | const parts = cacheDir.split(os.tmpdir()); 64 | const nameParts = parts[1].split(path.sep); 65 | t.match(nameParts[1], /^v8-compile-cache(-\d+)?$/); 66 | t.equal(nameParts[3], 'node-1.2.3'); 67 | 68 | t.done(); 69 | }); 70 | 71 | tap.test('getCacheDir (env)', t => { 72 | const cacheDir = vm.runInNewContext( 73 | '(' + getCacheDir.toString() + ')();', 74 | { 75 | process: { 76 | arch: process.arch, 77 | getuid: process.getuid, 78 | versions: {}, 79 | env: { 80 | V8_COMPILE_CACHE_CACHE_DIR: 'from env', 81 | }, 82 | }, 83 | path, 84 | os, 85 | } 86 | ); 87 | 88 | t.equal(cacheDir, 'from env'); 89 | 90 | t.done(); 91 | }); 92 | -------------------------------------------------------------------------------- /test/getMainName-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const child_process = require('child_process'); 5 | 6 | tap.beforeEach(cb => { 7 | delete process.env.DISABLE_V8_COMPILE_CACHE; 8 | cb(); 9 | }); 10 | 11 | tap.test('handles --require', t => { 12 | const ps = child_process.spawnSync( 13 | process.execPath, 14 | ['--require', '..', require.resolve('./fixtures/print-main-name')], 15 | {cwd: __dirname} 16 | ); 17 | t.equal(ps.status, 0); 18 | t.equal(String(ps.stdout).trim(), __dirname); 19 | 20 | t.end(); 21 | }); 22 | 23 | tap.test('bad require.main.filename', t => { 24 | const ps = child_process.spawnSync( 25 | process.execPath, 26 | ['--eval', ` 27 | module.filename = null; 28 | console.log(require('..').__TEST__.getMainName()); 29 | `], 30 | {cwd: __dirname} 31 | ); 32 | t.equal(ps.status, 0); 33 | t.equal(String(ps.stdout).trim(), __dirname); 34 | 35 | t.end(); 36 | }); 37 | 38 | tap.test('require.main.filename works with --eval', t => { 39 | const ps = child_process.spawnSync( 40 | process.execPath, 41 | ['--eval', 'require("..")'], 42 | {cwd: __dirname} 43 | ); 44 | t.equal(ps.status, 0); 45 | 46 | t.end(); 47 | }); 48 | 49 | tap.test('require.main.filename works with --require', t => { 50 | const ps = child_process.spawnSync( 51 | process.execPath, 52 | ['--require', '..'], 53 | {cwd: __dirname} 54 | ); 55 | t.equal(ps.status, 0); 56 | 57 | t.end(); 58 | }); 59 | 60 | tap.test('require.main.filename works with as arg script', t => { 61 | const ps = child_process.spawnSync( 62 | process.execPath, 63 | [require.resolve('..')], 64 | {cwd: __dirname} 65 | ); 66 | t.equal(ps.status, 0); 67 | 68 | t.end(); 69 | }); 70 | -------------------------------------------------------------------------------- /test/mkdirpSync-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const tap = require('tap'); 6 | const temp = require('temp'); 7 | 8 | temp.track(); 9 | 10 | process.env.DISABLE_V8_COMPILE_CACHE = 1; 11 | const mkdirpSync = require('..').__TEST__.mkdirpSync; 12 | 13 | tap.test('creates nested dirs', t => { 14 | const dirname = path.join(temp.path('mkdirpSync-test'), 'a/b/c'); 15 | 16 | t.notOk(fs.existsSync(dirname)); 17 | mkdirpSync(dirname); 18 | t.ok(fs.existsSync(dirname)); 19 | 20 | t.doesNotThrow(() => { 21 | t.ok(fs.existsSync(dirname)); 22 | mkdirpSync(dirname); 23 | t.ok(fs.existsSync(dirname)); 24 | }); 25 | 26 | t.end(); 27 | }); 28 | 29 | tap.test('throws if trying to write over a file', t => { 30 | const dirname = path.join(temp.path('mkdirpSync-test'), 'a'); 31 | const filename = path.join(dirname, 'b'); 32 | 33 | t.notOk(fs.existsSync(dirname)); 34 | mkdirpSync(dirname); 35 | t.ok(fs.existsSync(dirname)); 36 | 37 | fs.writeFileSync(filename, '\n'); 38 | t.ok(fs.existsSync(dirname)); 39 | 40 | t.throws(() => { 41 | mkdirpSync(filename); 42 | }, /EEXIST: file already exists/); 43 | 44 | t.end(); 45 | }); 46 | -------------------------------------------------------------------------------- /test/module-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const child_process = require('child_process'); 4 | const tap = require('tap'); 5 | const semver = require('semver'); 6 | 7 | tap.beforeEach(cb => { 8 | delete process.env.DISABLE_V8_COMPILE_CACHE; 9 | cb(); 10 | }); 11 | 12 | tap.test('require.resolve.paths module', t => { 13 | const psWithoutCache = child_process.spawnSync( 14 | process.execPath, 15 | [require.resolve('./fixtures/require-resolve-paths'), 'tap'], 16 | {cwd: __dirname} 17 | ); 18 | 19 | const psWithCache = child_process.spawnSync( 20 | process.execPath, 21 | [ 22 | '--require', 23 | '..', 24 | require.resolve('./fixtures/require-resolve-paths'), 25 | 'tap', 26 | ], 27 | {cwd: __dirname} 28 | ); 29 | 30 | const actual = JSON.parse(psWithCache.stdout); 31 | const expected = JSON.parse(psWithoutCache.stdout); 32 | 33 | t.same(actual, expected); 34 | t.equal(psWithCache.stderr.toString(), ''); 35 | t.equal(psWithCache.status, 0); 36 | 37 | t.equal( 38 | actual.hasRequireResolve, 39 | semver.satisfies(process.versions.node, '>=8.9.0') 40 | ); 41 | 42 | t.end(); 43 | }); 44 | 45 | tap.test('require.resolve.paths relative', t => { 46 | const psWithoutCache = child_process.spawnSync( 47 | process.execPath, 48 | [require.resolve('./fixtures/require-resolve-paths'), './foo'], 49 | {cwd: __dirname} 50 | ); 51 | 52 | const psWithCache = child_process.spawnSync( 53 | process.execPath, 54 | [ 55 | '--require', 56 | '..', 57 | require.resolve('./fixtures/require-resolve-paths'), 58 | './foo', 59 | ], 60 | {cwd: __dirname} 61 | ); 62 | 63 | t.same(JSON.parse(psWithCache.stdout), JSON.parse(psWithoutCache.stdout)); 64 | t.equal(psWithCache.stderr.toString(), ''); 65 | t.equal(psWithCache.status, 0); 66 | 67 | t.end(); 68 | }); 69 | -------------------------------------------------------------------------------- /test/node-version-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This test is to make sure that v8-compile-cache.js can at least load in 4 | // Node 4/5. 5 | 6 | const tap = require('tap'); 7 | const semver = require('semver'); 8 | 9 | process.env.DISABLE_V8_COMPILE_CACHE = 1; 10 | 11 | tap.test('loads without throwing', t => { 12 | t.doesNotThrow(() => { 13 | require('..'); 14 | }); 15 | 16 | t.end(); 17 | }); 18 | 19 | tap.test('supportsCachedData', t => { 20 | const hasV8WithCache = semver.satisfies(process.versions.node, '>=5.7.0'); 21 | const supportsCachedData = require('..').__TEST__.supportsCachedData; 22 | t.equal(supportsCachedData(), hasV8WithCache); 23 | 24 | t.end(); 25 | }); 26 | -------------------------------------------------------------------------------- /test/slashEscape-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | 5 | process.env.DISABLE_V8_COMPILE_CACHE = 1; 6 | const slashEscape = require('..').__TEST__.slashEscape; 7 | 8 | var escapes = { 9 | '/a/b/c/d': 'zSazSbzSczSd', 10 | '/z/zZ/a/': 'zSzZzSzZZzSazS', 11 | 'z:\\a/b': 'zZzCzBazSb', 12 | '\x00abc': 'z0abc', 13 | }; 14 | 15 | tap.test('escape', t => { 16 | for (const key of Object.keys(escapes)) { 17 | t.equal( 18 | slashEscape(key), 19 | escapes[key] 20 | ); 21 | } 22 | t.done(); 23 | }); 24 | -------------------------------------------------------------------------------- /v8-compile-cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Module = require('module'); 4 | const crypto = require('crypto'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const vm = require('vm'); 8 | const os = require('os'); 9 | 10 | const hasOwnProperty = Object.prototype.hasOwnProperty; 11 | 12 | //------------------------------------------------------------------------------ 13 | // FileSystemBlobStore 14 | //------------------------------------------------------------------------------ 15 | 16 | class FileSystemBlobStore { 17 | constructor(directory, prefix) { 18 | const name = prefix ? slashEscape(prefix + '.') : ''; 19 | this._blobFilename = path.join(directory, name + 'BLOB'); 20 | this._mapFilename = path.join(directory, name + 'MAP'); 21 | this._lockFilename = path.join(directory, name + 'LOCK'); 22 | this._directory = directory; 23 | this._load(); 24 | } 25 | 26 | has(key, invalidationKey) { 27 | if (hasOwnProperty.call(this._memoryBlobs, key)) { 28 | return this._invalidationKeys[key] === invalidationKey; 29 | } else if (hasOwnProperty.call(this._storedMap, key)) { 30 | return this._storedMap[key][0] === invalidationKey; 31 | } 32 | return false; 33 | } 34 | 35 | get(key, invalidationKey) { 36 | if (hasOwnProperty.call(this._memoryBlobs, key)) { 37 | if (this._invalidationKeys[key] === invalidationKey) { 38 | return this._memoryBlobs[key]; 39 | } 40 | } else if (hasOwnProperty.call(this._storedMap, key)) { 41 | const mapping = this._storedMap[key]; 42 | if (mapping[0] === invalidationKey) { 43 | return this._storedBlob.slice(mapping[1], mapping[2]); 44 | } 45 | } 46 | } 47 | 48 | set(key, invalidationKey, buffer) { 49 | this._invalidationKeys[key] = invalidationKey; 50 | this._memoryBlobs[key] = buffer; 51 | this._dirty = true; 52 | } 53 | 54 | delete(key) { 55 | if (hasOwnProperty.call(this._memoryBlobs, key)) { 56 | this._dirty = true; 57 | delete this._memoryBlobs[key]; 58 | } 59 | if (hasOwnProperty.call(this._invalidationKeys, key)) { 60 | this._dirty = true; 61 | delete this._invalidationKeys[key]; 62 | } 63 | if (hasOwnProperty.call(this._storedMap, key)) { 64 | this._dirty = true; 65 | delete this._storedMap[key]; 66 | } 67 | } 68 | 69 | isDirty() { 70 | return this._dirty; 71 | } 72 | 73 | save() { 74 | const dump = this._getDump(); 75 | const blobToStore = Buffer.concat(dump[0]); 76 | const mapToStore = JSON.stringify(dump[1]); 77 | 78 | try { 79 | mkdirpSync(this._directory); 80 | fs.writeFileSync(this._lockFilename, 'LOCK', {flag: 'wx'}); 81 | } catch (error) { 82 | // Swallow the exception if we fail to acquire the lock. 83 | return false; 84 | } 85 | 86 | try { 87 | fs.writeFileSync(this._blobFilename, blobToStore); 88 | fs.writeFileSync(this._mapFilename, mapToStore); 89 | } finally { 90 | fs.unlinkSync(this._lockFilename); 91 | } 92 | 93 | return true; 94 | } 95 | 96 | _load() { 97 | try { 98 | this._storedBlob = fs.readFileSync(this._blobFilename); 99 | this._storedMap = JSON.parse(fs.readFileSync(this._mapFilename)); 100 | } catch (e) { 101 | this._storedBlob = Buffer.alloc(0); 102 | this._storedMap = {}; 103 | } 104 | this._dirty = false; 105 | this._memoryBlobs = {}; 106 | this._invalidationKeys = {}; 107 | } 108 | 109 | _getDump() { 110 | const buffers = []; 111 | const newMap = {}; 112 | let offset = 0; 113 | 114 | function push(key, invalidationKey, buffer) { 115 | buffers.push(buffer); 116 | newMap[key] = [invalidationKey, offset, offset + buffer.length]; 117 | offset += buffer.length; 118 | } 119 | 120 | for (const key of Object.keys(this._memoryBlobs)) { 121 | const buffer = this._memoryBlobs[key]; 122 | const invalidationKey = this._invalidationKeys[key]; 123 | push(key, invalidationKey, buffer); 124 | } 125 | 126 | for (const key of Object.keys(this._storedMap)) { 127 | if (hasOwnProperty.call(newMap, key)) continue; 128 | const mapping = this._storedMap[key]; 129 | const buffer = this._storedBlob.slice(mapping[1], mapping[2]); 130 | push(key, mapping[0], buffer); 131 | } 132 | 133 | return [buffers, newMap]; 134 | } 135 | } 136 | 137 | //------------------------------------------------------------------------------ 138 | // NativeCompileCache 139 | //------------------------------------------------------------------------------ 140 | 141 | class NativeCompileCache { 142 | constructor() { 143 | this._cacheStore = null; 144 | this._previousModuleCompile = null; 145 | } 146 | 147 | setCacheStore(cacheStore) { 148 | this._cacheStore = cacheStore; 149 | } 150 | 151 | install() { 152 | const self = this; 153 | const hasRequireResolvePaths = typeof require.resolve.paths === 'function'; 154 | this._previousModuleCompile = Module.prototype._compile; 155 | Module.prototype._compile = function(content, filename) { 156 | const mod = this; 157 | 158 | function require(id) { 159 | return mod.require(id); 160 | } 161 | 162 | // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28 163 | function resolve(request, options) { 164 | return Module._resolveFilename(request, mod, false, options); 165 | } 166 | require.resolve = resolve; 167 | 168 | // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L37 169 | // resolve.resolve.paths was added in v8.9.0 170 | if (hasRequireResolvePaths) { 171 | resolve.paths = function paths(request) { 172 | return Module._resolveLookupPaths(request, mod, true); 173 | }; 174 | } 175 | 176 | require.main = process.mainModule; 177 | 178 | // Enable support to add extra extension types 179 | require.extensions = Module._extensions; 180 | require.cache = Module._cache; 181 | 182 | const dirname = path.dirname(filename); 183 | 184 | const compiledWrapper = self._moduleCompile(filename, content); 185 | 186 | // We skip the debugger setup because by the time we run, node has already 187 | // done that itself. 188 | 189 | // `Buffer` is included for Electron. 190 | // See https://github.com/zertosh/v8-compile-cache/pull/10#issuecomment-518042543 191 | const args = [mod.exports, require, mod, filename, dirname, process, global, Buffer]; 192 | return compiledWrapper.apply(mod.exports, args); 193 | }; 194 | } 195 | 196 | uninstall() { 197 | Module.prototype._compile = this._previousModuleCompile; 198 | } 199 | 200 | _moduleCompile(filename, content) { 201 | // https://github.com/nodejs/node/blob/v7.5.0/lib/module.js#L511 202 | 203 | // Remove shebang 204 | var contLen = content.length; 205 | if (contLen >= 2) { 206 | if (content.charCodeAt(0) === 35/*#*/ && 207 | content.charCodeAt(1) === 33/*!*/) { 208 | if (contLen === 2) { 209 | // Exact match 210 | content = ''; 211 | } else { 212 | // Find end of shebang line and slice it off 213 | var i = 2; 214 | for (; i < contLen; ++i) { 215 | var code = content.charCodeAt(i); 216 | if (code === 10/*\n*/ || code === 13/*\r*/) break; 217 | } 218 | if (i === contLen) { 219 | content = ''; 220 | } else { 221 | // Note that this actually includes the newline character(s) in the 222 | // new output. This duplicates the behavior of the regular 223 | // expression that was previously used to replace the shebang line 224 | content = content.slice(i); 225 | } 226 | } 227 | } 228 | } 229 | 230 | // create wrapper function 231 | var wrapper = Module.wrap(content); 232 | 233 | var invalidationKey = crypto 234 | .createHash('sha1') 235 | .update(content, 'utf8') 236 | .digest('hex'); 237 | 238 | var buffer = this._cacheStore.get(filename, invalidationKey); 239 | 240 | var script = new vm.Script(wrapper, { 241 | filename: filename, 242 | lineOffset: 0, 243 | displayErrors: true, 244 | cachedData: buffer, 245 | produceCachedData: true, 246 | }); 247 | 248 | if (script.cachedDataProduced) { 249 | this._cacheStore.set(filename, invalidationKey, script.cachedData); 250 | } else if (script.cachedDataRejected) { 251 | this._cacheStore.delete(filename); 252 | } 253 | 254 | var compiledWrapper = script.runInThisContext({ 255 | filename: filename, 256 | lineOffset: 0, 257 | columnOffset: 0, 258 | displayErrors: true, 259 | }); 260 | 261 | return compiledWrapper; 262 | } 263 | } 264 | 265 | //------------------------------------------------------------------------------ 266 | // utilities 267 | // 268 | // https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98 269 | // https://github.com/zertosh/slash-escape/blob/e7ebb99/slash-escape.js 270 | //------------------------------------------------------------------------------ 271 | 272 | function mkdirpSync(p_) { 273 | _mkdirpSync(path.resolve(p_), 0o777); 274 | } 275 | 276 | function _mkdirpSync(p, mode) { 277 | try { 278 | fs.mkdirSync(p, mode); 279 | } catch (err0) { 280 | if (err0.code === 'ENOENT') { 281 | _mkdirpSync(path.dirname(p)); 282 | _mkdirpSync(p); 283 | } else { 284 | try { 285 | const stat = fs.statSync(p); 286 | if (!stat.isDirectory()) { throw err0; } 287 | } catch (err1) { 288 | throw err0; 289 | } 290 | } 291 | } 292 | } 293 | 294 | function slashEscape(str) { 295 | const ESCAPE_LOOKUP = { 296 | '\\': 'zB', 297 | ':': 'zC', 298 | '/': 'zS', 299 | '\x00': 'z0', 300 | 'z': 'zZ', 301 | }; 302 | const ESCAPE_REGEX = /[\\:/\x00z]/g; // eslint-disable-line no-control-regex 303 | return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); 304 | } 305 | 306 | function supportsCachedData() { 307 | const script = new vm.Script('""', {produceCachedData: true}); 308 | // chakracore, as of v1.7.1.0, returns `false`. 309 | return script.cachedDataProduced === true; 310 | } 311 | 312 | function getCacheDir() { 313 | const v8_compile_cache_cache_dir = process.env.V8_COMPILE_CACHE_CACHE_DIR; 314 | if (v8_compile_cache_cache_dir) { 315 | return v8_compile_cache_cache_dir; 316 | } 317 | 318 | // Avoid cache ownership issues on POSIX systems. 319 | const dirname = typeof process.getuid === 'function' 320 | ? 'v8-compile-cache-' + process.getuid() 321 | : 'v8-compile-cache'; 322 | // Avoid cache incompatibility issues with Rosetta on Apple Silicon. 323 | const arch = process.arch; 324 | const version = typeof process.versions.v8 === 'string' 325 | ? process.versions.v8 326 | : typeof process.versions.chakracore === 'string' 327 | ? 'chakracore-' + process.versions.chakracore 328 | : 'node-' + process.version; 329 | const cacheDir = path.join(os.tmpdir(), dirname, arch, version); 330 | return cacheDir; 331 | } 332 | 333 | function getMainName() { 334 | // `require.main.filename` is undefined or null when: 335 | // * node -e 'require("v8-compile-cache")' 336 | // * node -r 'v8-compile-cache' 337 | // * Or, requiring from the REPL. 338 | const mainName = require.main && typeof require.main.filename === 'string' 339 | ? require.main.filename 340 | : process.cwd(); 341 | return mainName; 342 | } 343 | 344 | //------------------------------------------------------------------------------ 345 | // main 346 | //------------------------------------------------------------------------------ 347 | 348 | if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) { 349 | const cacheDir = getCacheDir(); 350 | const prefix = getMainName(); 351 | const blobStore = new FileSystemBlobStore(cacheDir, prefix); 352 | 353 | const nativeCompileCache = new NativeCompileCache(); 354 | nativeCompileCache.setCacheStore(blobStore); 355 | nativeCompileCache.install(); 356 | 357 | process.once('exit', () => { 358 | if (blobStore.isDirty()) { 359 | blobStore.save(); 360 | } 361 | nativeCompileCache.uninstall(); 362 | }); 363 | } 364 | 365 | module.exports.__TEST__ = { 366 | FileSystemBlobStore, 367 | NativeCompileCache, 368 | mkdirpSync, 369 | slashEscape, 370 | supportsCachedData, 371 | getCacheDir, 372 | getMainName, 373 | }; 374 | --------------------------------------------------------------------------------