├── .eslintignore ├── .gitignore ├── test ├── fixtures │ ├── file-1.js │ ├── file-2.js │ ├── file-3.js │ ├── file-4.js │ ├── print-main-name.js │ └── require-resolve-paths.js ├── slashEscape-test.js ├── node-version-test.js ├── FileSystemBlobStore-mock.js ├── mkdirpSync-test.js ├── getMainName-test.js ├── module-test.js ├── getCacheDir-test.js ├── NativeCompileCache-test.js └── FileSystemBlobStore-test.js ├── v8-compile-cache.d.ts ├── bench ├── require-rxjs-module.js ├── require-flow-parser.js ├── require-rxjs-bundle.js ├── require-yarn.js ├── require-babel-core.js ├── run.sh └── _measure.js ├── .eslintrc.json ├── package.json ├── LICENSE ├── .github └── workflows │ └── continuous-integration.yml ├── CHANGELOG.md ├── README.md └── v8-compile-cache.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /v8-compile-cache.d.ts: -------------------------------------------------------------------------------- 1 | export function install(opts?: { 2 | cacheDir?: string; 3 | prefix?: string; 4 | }): { 5 | uninstall(): void; 6 | } | undefined; 7 | x -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | this._cachedFiles.push({key, invalidationKey, buffer}); 24 | return buffer; 25 | } 26 | 27 | delete(key) { 28 | const i = this._cachedFiles.findIndex(file => file.key === key); 29 | if (i != null) { 30 | this._cachedFiles.splice(i, 1); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-compile-cache-lib", 3 | "version": "3.0.1", 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": "Andrew Bradley ", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/cspotcode/v8-compile-cache-lib.git" 17 | }, 18 | "files": [ 19 | "v8-compile-cache.d.ts", 20 | "v8-compile-cache.js" 21 | ], 22 | "license": "MIT", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "babel-core": "6.26.3", 26 | "eslint": "^7.12.1", 27 | "flow-parser": "0.136.0", 28 | "rimraf": "^2.5.4", 29 | "rxjs": "6.6.3", 30 | "semver": "^5.3.0", 31 | "tap": "^9.0.0", 32 | "temp": "^0.8.3", 33 | "yarn": "1.22.10" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | # branches pushed by collaborators 4 | push: 5 | branches: 6 | - master 7 | # pull request from non-collaborators 8 | pull_request: {} 9 | # nightly 10 | # schedule: 11 | # - cron: '0 0 * * *' 12 | jobs: 13 | test: 14 | name: "Test: ${{ matrix.os }}, node ${{ matrix.node }}" 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-20.04] 20 | # Don't forget to add all new flavors to this list! 21 | node: 22 | - "12" 23 | - "14" 24 | - "15" 25 | - "16" 26 | - "17" 27 | steps: 28 | # checkout code 29 | - uses: actions/checkout@v2 30 | # install node 31 | - name: Use Node.js ${{ matrix.node }} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{ matrix.node }} 35 | # Downgrade npm as necessary 36 | - run: | 37 | case "${{ matrix.node }}" in 38 | 5|5.6.0) 39 | npm install -g npm@4; 40 | ;; 41 | esac 42 | - run: npm install 43 | - run: | 44 | case "${{ matrix.node }}" in 45 | 4|5.6.0) 46 | ./node_modules/.bin/tap test/node-version-test.js; 47 | ;; 48 | *) 49 | npm run tap; 50 | ;; 51 | esac 52 | case "${{ matrix.node }}" in 53 | 4|5.6.0|5|6|8) 54 | ;; 55 | *) 56 | npm run eslint; 57 | npm run bench; 58 | ;; 59 | esac 60 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `v8-module-cache` Changelog 2 | 3 | ## 2021-03-05, Version 2.3.0 4 | 5 | * Fix use require.main instead of module.parent [#34](https://github.com/zertosh/v8-compile-cache/pull/34). 6 | 7 | ## 2020-10-28, Version 2.2.0 8 | 9 | * Added `V8_COMPILE_CACHE_CACHE_DIR` option [#23](https://github.com/zertosh/v8-compile-cache/pull/23). 10 | 11 | ## 2020-05-30, Version 2.1.1 12 | 13 | * Stop using process.umask() [#28](https://github.com/zertosh/v8-compile-cache/pull/28). 14 | 15 | ## 2019-08-04, Version 2.1.0 16 | 17 | * Fix Electron by calling the module wrapper with `Buffer` [#10](https://github.com/zertosh/v8-compile-cache/pull/10). 18 | 19 | ## 2019-05-10, Version 2.0.3 20 | 21 | * Add `LICENSE` file [#19](https://github.com/zertosh/v8-compile-cache/pull/19). 22 | * Add "repository" to `package.json` (see [eea336e](https://github.com/zertosh/v8-compile-cache/commit/eea336eaa8360f9ded9342b8aa928e56ac6a7529)). 23 | * 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). 24 | 25 | ## 2018-08-06, Version 2.0.2 26 | 27 | * Re-publish. 28 | 29 | ## 2018-08-06, Version 2.0.1 30 | 31 | * Support `require.resolve` options (added in Node v8.9.0). 32 | 33 | ## 2018-04-30, Version 2.0.0 34 | 35 | * Use `Buffer.alloc` instead of `new Buffer()`. 36 | * Drop support for Node 5.x. 37 | 38 | ## 2018-01-23, Version 1.1.2 39 | 40 | * Instead of checking for `process.versions.v8`, check that `script.cachedDataProduced` is `true` (rather than `null`/`undefined`) for support to be considered existent. 41 | 42 | ## 2018-01-23, Version 1.1.1 43 | 44 | * Check for the existence of `process.versions.v8` before attaching hook (see [f8b0388](https://github.com/zertosh/v8-compile-cache/commit/f8b038848be94bc2c905880dd50447c73393f364)). 45 | 46 | ## 2017-03-27, Version 1.1.0 47 | 48 | * Safer cache directory creation (see [bcb3b12](https://github.com/zertosh/v8-compile-cache/commit/bcb3b12c819ab0927ec4408e70f612a6d50a9617)). 49 | - The cache is now suffixed with the user's uid on POSIX systems (i.e. `/path/to/tmp/v8-compile-cache-1234`). 50 | 51 | ## 2017-02-21, Version 1.0.0 52 | 53 | * Initial release. 54 | -------------------------------------------------------------------------------- /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.versions.v8); 18 | 19 | t.done(); 20 | }); 21 | 22 | tap.test('getCacheDir (chakracore)', t => { 23 | const cacheDir = vm.runInNewContext( 24 | '(' + getCacheDir.toString() + ')();', 25 | { 26 | process: { 27 | getuid: process.getuid, 28 | versions: {chakracore: '1.2.3'}, 29 | env: {}, 30 | }, 31 | path, 32 | os, 33 | } 34 | ); 35 | 36 | const parts = cacheDir.split(os.tmpdir()); 37 | const nameParts = parts[1].split(path.sep); 38 | 39 | t.match(nameParts[1], /^v8-compile-cache(-\d+)?$/); 40 | t.equal(nameParts[2], 'chakracore-1.2.3'); 41 | 42 | t.done(); 43 | }); 44 | 45 | tap.test('getCacheDir (unknown)', t => { 46 | const cacheDir = vm.runInNewContext( 47 | '(' + getCacheDir.toString() + ')();', 48 | { 49 | process: { 50 | getuid: process.getuid, 51 | version: '1.2.3', 52 | versions: {}, 53 | env: {}, 54 | }, 55 | path, 56 | os, 57 | } 58 | ); 59 | 60 | const parts = cacheDir.split(os.tmpdir()); 61 | const nameParts = parts[1].split(path.sep); 62 | t.match(nameParts[1], /^v8-compile-cache(-\d+)?$/); 63 | t.equal(nameParts[2], 'node-1.2.3'); 64 | 65 | t.done(); 66 | }); 67 | 68 | tap.test('getCacheDir (env)', t => { 69 | const cacheDir = vm.runInNewContext( 70 | '(' + getCacheDir.toString() + ')();', 71 | { 72 | process: { 73 | getuid: process.getuid, 74 | versions: {}, 75 | env: { 76 | V8_COMPILE_CACHE_CACHE_DIR: 'from env', 77 | }, 78 | }, 79 | path, 80 | os, 81 | } 82 | ); 83 | 84 | t.equal(cacheDir, 'from env'); 85 | 86 | t.done(); 87 | }); 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v8-compile-cache-lib 2 | 3 | > Fork of [`v8-compile-cache`](https://www.npmjs.com/package/v8-compile-cache) exposed as an API for programmatic usage in other libraries and tools. 4 | 5 | --- 6 | 7 | [![Build status](https://img.shields.io/github/workflow/status/cspotcode/v8-compile-cache-lib/Continuous%20Integration)](https://github.com/cspotcode/v8-compile-cache-lib/actions?query=workflow%3A%22Continuous+Integration%22) 8 | 9 | `v8-compile-cache-lib` 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. 10 | 11 | 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/). 12 | 13 | ## Usage 14 | 15 | 1. Add the dependency: 16 | 17 | ```sh 18 | $ npm install --save v8-compile-cache-lib 19 | ``` 20 | 21 | 2. Then, in your entry module add: 22 | 23 | ```js 24 | require('v8-compile-cache-lib').install(); 25 | ``` 26 | 27 | **`.install()` 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-lib`.** 28 | 29 | ## Options 30 | 31 | Set the environment variable `DISABLE_V8_COMPILE_CACHE=1` to disable the cache. 32 | 33 | Cache directory is defined by environment variable `V8_COMPILE_CACHE_CACHE_DIR` or defaults to `/v8-compile-cache-`. 34 | 35 | ## Internals 36 | 37 | Cache files are suffixed `.BLOB` and `.MAP` corresponding to the entry module that required `v8-compile-cache-lib`. 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. 38 | 39 | ## Benchmarks 40 | 41 | See https://github.com/cspotcode/v8-compile-cache-lib/tree/master/bench. 42 | 43 | **Load Times:** 44 | 45 | | Module | Without Cache | With Cache | 46 | | ---------------- | -------------:| ----------:| 47 | | `babel-core` | `218ms` | `185ms` | 48 | | `yarn` | `153ms` | `113ms` | 49 | | `yarn` (bundled) | `228ms` | `105ms` | 50 | 51 | _^ Includes the overhead of loading the cache itself._ 52 | 53 | ## Acknowledgements 54 | 55 | * The original [`v8-compile-cache`](https://github.com/zertosh/v8-compile-cache) from which this library is forked. 56 | * `FileSystemBlobStore` and `NativeCompileCache` are based on Atom's implementation of their v8 compile cache: 57 | - https://github.com/atom/atom/blob/b0d7a8a/src/file-system-blob-store.js 58 | - https://github.com/atom/atom/blob/b0d7a8a/src/native-compile-cache.js 59 | * `mkdirpSync` is based on: 60 | - https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98 61 | -------------------------------------------------------------------------------- /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('deletes previously cached code when the cache is an invalid file', t => { 88 | fakeCacheStore.has = () => true; 89 | fakeCacheStore.get = () => Buffer.from('an invalid cache'); 90 | let deleteWasCalledWith = null; 91 | fakeCacheStore.delete = arg => { deleteWasCalledWith = arg; }; 92 | 93 | const fn3 = require('./fixtures/file-3'); 94 | 95 | t.equal(deleteWasCalledWith, require.resolve('./fixtures/file-3')); 96 | t.equal(fn3(), 3); 97 | 98 | t.end(); 99 | }); 100 | 101 | tap.test('when a previously required and cached file changes removes it from the store and re-inserts it with the new cache', t => { 102 | const tmpDir = temp.mkdirSync('native-compile-cache-test'); 103 | const tmpFile = path.join(tmpDir, 'file-5.js'); 104 | fs.writeFileSync(tmpFile, 'module.exports = () => `file-5`;'); 105 | 106 | let fn5 = require(tmpFile); 107 | 108 | t.equal(cachedFiles.length, 1); 109 | t.equal(cachedFiles[0].key, require.resolve(tmpFile)); 110 | t.type(cachedFiles[0].buffer, Uint8Array); 111 | t.ok(cachedFiles[0].buffer.length > 0); 112 | t.equal(fn5(), 'file-5'); 113 | 114 | delete Module._cache[require.resolve(tmpFile)]; 115 | fs.appendFileSync(tmpFile, '\n\n'); 116 | fn5 = require(tmpFile); 117 | 118 | t.equal(cachedFiles.length, 2); 119 | t.equal(cachedFiles[1].key, require.resolve(tmpFile)); 120 | t.notEqual(cachedFiles[1].invalidationKey, cachedFiles[0].invalidationKey); 121 | t.type(cachedFiles[1].buffer, Uint8Array); 122 | t.ok(cachedFiles[1].buffer.length > 0); 123 | 124 | t.end(); 125 | }); 126 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = this._ownModuleCompile = _ownModuleCompile; 156 | self.enabled = true; 157 | function _ownModuleCompile(content, filename) { 158 | if(!self.enabled) return this._previousModuleCompile.apply(this, arguments); 159 | const mod = this; 160 | 161 | function require(id) { 162 | return mod.require(id); 163 | } 164 | 165 | // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28 166 | function resolve(request, options) { 167 | return Module._resolveFilename(request, mod, false, options); 168 | } 169 | require.resolve = resolve; 170 | 171 | // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L37 172 | // resolve.resolve.paths was added in v8.9.0 173 | if (hasRequireResolvePaths) { 174 | resolve.paths = function paths(request) { 175 | return Module._resolveLookupPaths(request, mod, true); 176 | }; 177 | } 178 | 179 | require.main = process.mainModule; 180 | 181 | // Enable support to add extra extension types 182 | require.extensions = Module._extensions; 183 | require.cache = Module._cache; 184 | 185 | const dirname = path.dirname(filename); 186 | 187 | const compiledWrapper = self._moduleCompile(filename, content); 188 | 189 | // We skip the debugger setup because by the time we run, node has already 190 | // done that itself. 191 | 192 | // `Buffer` is included for Electron. 193 | // See https://github.com/zertosh/v8-compile-cache/pull/10#issuecomment-518042543 194 | const args = [mod.exports, require, mod, filename, dirname, process, global, Buffer]; 195 | return compiledWrapper.apply(mod.exports, args); 196 | } 197 | } 198 | 199 | uninstall() { 200 | this.enabled = false; 201 | // If something else has since been installed on top of us, we cannot overwrite it. 202 | if(Module.prototype._compile === this._ownModuleCompile) { 203 | Module.prototype._compile = this._previousModuleCompile; 204 | } 205 | } 206 | 207 | _moduleCompile(filename, content) { 208 | // https://github.com/nodejs/node/blob/v7.5.0/lib/module.js#L511 209 | 210 | // Remove shebang 211 | var contLen = content.length; 212 | if (contLen >= 2) { 213 | if (content.charCodeAt(0) === 35/*#*/ && 214 | content.charCodeAt(1) === 33/*!*/) { 215 | if (contLen === 2) { 216 | // Exact match 217 | content = ''; 218 | } else { 219 | // Find end of shebang line and slice it off 220 | var i = 2; 221 | for (; i < contLen; ++i) { 222 | var code = content.charCodeAt(i); 223 | if (code === 10/*\n*/ || code === 13/*\r*/) break; 224 | } 225 | if (i === contLen) { 226 | content = ''; 227 | } else { 228 | // Note that this actually includes the newline character(s) in the 229 | // new output. This duplicates the behavior of the regular 230 | // expression that was previously used to replace the shebang line 231 | content = content.slice(i); 232 | } 233 | } 234 | } 235 | } 236 | 237 | // create wrapper function 238 | var wrapper = Module.wrap(content); 239 | 240 | var invalidationKey = crypto 241 | .createHash('sha1') 242 | .update(content, 'utf8') 243 | .digest('hex'); 244 | 245 | var buffer = this._cacheStore.get(filename, invalidationKey); 246 | 247 | var script = new vm.Script(wrapper, { 248 | filename: filename, 249 | lineOffset: 0, 250 | displayErrors: true, 251 | cachedData: buffer, 252 | produceCachedData: true, 253 | }); 254 | 255 | if (script.cachedDataProduced) { 256 | this._cacheStore.set(filename, invalidationKey, script.cachedData); 257 | } else if (script.cachedDataRejected) { 258 | this._cacheStore.delete(filename); 259 | } 260 | 261 | var compiledWrapper = script.runInThisContext({ 262 | filename: filename, 263 | lineOffset: 0, 264 | columnOffset: 0, 265 | displayErrors: true, 266 | }); 267 | 268 | return compiledWrapper; 269 | } 270 | } 271 | 272 | //------------------------------------------------------------------------------ 273 | // utilities 274 | // 275 | // https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98 276 | // https://github.com/zertosh/slash-escape/blob/e7ebb99/slash-escape.js 277 | //------------------------------------------------------------------------------ 278 | 279 | function mkdirpSync(p_) { 280 | _mkdirpSync(path.resolve(p_), 0o777); 281 | } 282 | 283 | function _mkdirpSync(p, mode) { 284 | try { 285 | fs.mkdirSync(p, mode); 286 | } catch (err0) { 287 | if (err0.code === 'ENOENT') { 288 | _mkdirpSync(path.dirname(p)); 289 | _mkdirpSync(p); 290 | } else { 291 | try { 292 | const stat = fs.statSync(p); 293 | if (!stat.isDirectory()) { throw err0; } 294 | } catch (err1) { 295 | throw err0; 296 | } 297 | } 298 | } 299 | } 300 | 301 | function slashEscape(str) { 302 | const ESCAPE_LOOKUP = { 303 | '\\': 'zB', 304 | ':': 'zC', 305 | '/': 'zS', 306 | '\x00': 'z0', 307 | 'z': 'zZ', 308 | }; 309 | const ESCAPE_REGEX = /[\\:/\x00z]/g; // eslint-disable-line no-control-regex 310 | return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); 311 | } 312 | 313 | function supportsCachedData() { 314 | const script = new vm.Script('""', {produceCachedData: true}); 315 | // chakracore, as of v1.7.1.0, returns `false`. 316 | return script.cachedDataProduced === true; 317 | } 318 | 319 | function getCacheDir() { 320 | const v8_compile_cache_cache_dir = process.env.V8_COMPILE_CACHE_CACHE_DIR; 321 | if (v8_compile_cache_cache_dir) { 322 | return v8_compile_cache_cache_dir; 323 | } 324 | 325 | // Avoid cache ownership issues on POSIX systems. 326 | const dirname = typeof process.getuid === 'function' 327 | ? 'v8-compile-cache-' + process.getuid() 328 | : 'v8-compile-cache'; 329 | const version = typeof process.versions.v8 === 'string' 330 | ? process.versions.v8 331 | : typeof process.versions.chakracore === 'string' 332 | ? 'chakracore-' + process.versions.chakracore 333 | : 'node-' + process.version; 334 | const cacheDir = path.join(os.tmpdir(), dirname, version); 335 | return cacheDir; 336 | } 337 | 338 | function getMainName() { 339 | // `require.main.filename` is undefined or null when: 340 | // * node -e 'require("v8-compile-cache")' 341 | // * node -r 'v8-compile-cache' 342 | // * Or, requiring from the REPL. 343 | const mainName = require.main && typeof require.main.filename === 'string' 344 | ? require.main.filename 345 | : process.cwd(); 346 | return mainName; 347 | } 348 | 349 | function install(opts) { 350 | if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) { 351 | if(typeof opts === 'undefined') opts = {} 352 | let cacheDir = opts.cacheDir 353 | if(typeof cacheDir === 'undefined') cacheDir = getCacheDir(); 354 | let prefix = opts.prefix 355 | if(typeof prefix === 'undefined') prefix = getMainName(); 356 | const blobStore = new FileSystemBlobStore(cacheDir, prefix); 357 | 358 | const nativeCompileCache = new NativeCompileCache(); 359 | nativeCompileCache.setCacheStore(blobStore); 360 | nativeCompileCache.install(); 361 | 362 | let uninstalled = false; 363 | const uninstall = () => { 364 | if(uninstalled) return; 365 | uninstalled = true; 366 | process.removeListener('exit', uninstall); 367 | if (blobStore.isDirty()) { 368 | blobStore.save(); 369 | } 370 | nativeCompileCache.uninstall(); 371 | } 372 | process.once('exit', uninstall); 373 | return {uninstall}; 374 | } 375 | } 376 | 377 | //------------------------------------------------------------------------------ 378 | // main 379 | //------------------------------------------------------------------------------ 380 | 381 | module.exports.install = install; 382 | 383 | module.exports.__TEST__ = { 384 | FileSystemBlobStore, 385 | NativeCompileCache, 386 | mkdirpSync, 387 | slashEscape, 388 | supportsCachedData, 389 | getCacheDir, 390 | getMainName, 391 | }; 392 | --------------------------------------------------------------------------------