From 9fe76e85ecd21505d7866ce55cc8ce0191566589 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 18 Sep 2025 23:39:44 +0200 Subject: [PATCH 1/3] module: fix directory option in the enableCompileCache() API The option name should be `directory` to be consistent with the returned result. It should also allow environment variable overrides. This also adds documentation for the new options and improves it. --- doc/api/module.md | 84 +++++++++-------- lib/internal/modules/helpers.js | 32 ++++--- .../fixtures/compile-cache-wrapper-options.js | 23 +++++ ...-compile-cache-api-options-portable-env.js | 89 +++++++++++++++++++ .../test-compile-cache-api-portable.js | 20 ++--- 5 files changed, 186 insertions(+), 62 deletions(-) create mode 100644 test/fixtures/compile-cache-wrapper-options.js create mode 100644 test/parallel/test-compile-cache-api-options-portable-env.js diff --git a/doc/api/module.md b/doc/api/module.md index a2c7a4be7901c3..25111407b85c2d 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -385,8 +385,8 @@ changes: The module compile cache can be enabled either using the [`module.enableCompileCache()`][] method or the [`NODE_COMPILE_CACHE=dir`][] environment variable. After it is enabled, -whenever Node.js compiles a CommonJS or a ECMAScript Module, it will use on-disk -[V8 code cache][] persisted in the specified directory to speed up the compilation. +whenever Node.js compiles a CommonJS, a ECMAScript Module, or a TypeScript module, it will +use on-disk [V8 code cache][] persisted in the specified directory to speed up the compilation. This may slow down the first load of a module graph, but subsequent loads of the same module graph may get a significant speedup if the contents of the modules do not change. @@ -394,11 +394,24 @@ To clean up the generated compile cache on disk, simply remove the cache directo directory will be recreated the next time the same directory is used for for compile cache storage. To avoid filling up the disk with stale cache, it is recommended to use a directory under the [`os.tmpdir()`][]. If the compile cache is enabled by a call to -[`module.enableCompileCache()`][] without specifying the directory, Node.js will use +[`module.enableCompileCache()`][] without specifying the `directory`, Node.js will use the [`NODE_COMPILE_CACHE=dir`][] environment variable if it's set, or defaults to `path.join(os.tmpdir(), 'node-compile-cache')` otherwise. To locate the compile cache directory used by a running Node.js instance, use [`module.getCompileCacheDir()`][]. +The enabled module compile cache can be disabled by the [`NODE_DISABLE_COMPILE_CACHE=1`][] +environment variable. This can be useful when the compile cache leads to unexpected or +undesired behaviors (e.g. less precise test coverage). + +At the moment, when the compile cache is enabled and a module is loaded afresh, the +code cache is generated from the compiled code immediately, but will only be written +to disk when the Node.js instance is about to exit. This is subject to change. The +[`module.flushCompileCache()`][] method can be used to ensure the accumulated code cache +is flushed to disk in case the application wants to spawn other Node.js instances +and let them share the cache long before the parent exits. + +### Portability of the compile cache + By default, caches are invalidated when the absolute paths of the modules being cached are changed. To keep the cache working after moving the project directory, enable portable compile cache. This allows previously compiled @@ -409,38 +422,29 @@ will not be cached. There are two ways to enable the portable mode: -1. Using the portable option in module.enableCompileCache(): +1. Using the portable option in [`module.enableCompileCache()`][]: ```js // Non-portable cache (default): cache breaks if project is moved - module.enableCompileCache({ path: '/path/to/cache/storage/dir' }); + module.enableCompileCache({ directory: '/path/to/cache/storage/dir' }); // Portable cache: cache works after the project is moved - module.enableCompileCache({ path: '/path/to/cache/storage/dir', portable: true }); + module.enableCompileCache({ directory: '/path/to/cache/storage/dir', portable: true }); ``` 2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][] +### Limitations of the compile cache + Currently when using the compile cache with [V8 JavaScript code coverage][], the coverage being collected by V8 may be less precise in functions that are deserialized from the code cache. It's recommended to turn this off when running tests to generate precise coverage. -The enabled module compile cache can be disabled by the [`NODE_DISABLE_COMPILE_CACHE=1`][] -environment variable. This can be useful when the compile cache leads to unexpected or -undesired behaviors (e.g. less precise test coverage). - Compilation cache generated by one version of Node.js can not be reused by a different version of Node.js. Cache generated by different versions of Node.js will be stored separately if the same base directory is used to persist the cache, so they can co-exist. -At the moment, when the compile cache is enabled and a module is loaded afresh, the -code cache is generated from the compiled code immediately, but will only be written -to disk when the Node.js instance is about to exit. This is subject to change. The -[`module.flushCompileCache()`][] method can be used to ensure the accumulated code cache -is flushed to disk in case the application wants to spawn other Node.js instances -and let them share the cache long before the parent exits. - ### `module.constants.compileCacheStatus` > Stability: 1.1 - Active Development -* `cacheDir` {string|undefined} Optional path to specify the directory where the compile cache - will be stored/retrieved. +* `options` {string|Object} Optional. If a string is passed, it is considered to be `options.directory`. + * `directory` {string} Optional. Directory to store the compile cache. If not specified, + the directory specified by the [`NODE_COMPILE_CACHE=dir`][] environment variable + will be used if it's set, or `path.join(os.tmpdir(), 'node-compile-cache')` + otherwise. + * `portable` {boolean} Optional. If `true`, enables portable compile cache so that + the cache can be reused even if the project directory is moved. This is a best-effort + feature. If not specified, it will depend on whether the environment variable + [`NODE_COMPILE_CACHE_PORTABLE=1`][] is set. * Returns: {Object} * `status` {integer} One of the [`module.constants.compileCacheStatus`][] * `message` {string|undefined} If Node.js cannot enable the compile cache, this contains @@ -515,20 +533,16 @@ added: v22.8.0 Enable [module compile cache][] in the current Node.js instance. -If `cacheDir` is not specified, Node.js will either use the directory specified by the -[`NODE_COMPILE_CACHE=dir`][] environment variable if it's set, or use -`path.join(os.tmpdir(), 'node-compile-cache')` otherwise. For general use cases, it's -recommended to call `module.enableCompileCache()` without specifying the `cacheDir`, -so that the directory can be overridden by the `NODE_COMPILE_CACHE` environment -variable when necessary. - -Since compile cache is supposed to be a quiet optimization that is not required for the -application to be functional, this method is designed to not throw any exception when the -compile cache cannot be enabled. Instead, it will return an object containing an error -message in the `message` field to aid debugging. -If compile cache is enabled successfully, the `directory` field in the returned object -contains the path to the directory where the compile cache is stored. The `status` -field in the returned object would be one of the `module.constants.compileCacheStatus` +For general use cases, it's recommended to call `module.enableCompileCache()` without +specifying the `options.directory`, so that the directory can be overridden by the +`NODE_COMPILE_CACHE` environment variable when necessary. + +Since compile cache is supposed to be a optimization that is not mission critical, this +method is designed to not throw any exception when the compile cache cannot be enabled. +Instead, it will return an object containing an error message in the `message` field to +aid debugging. If compile cache is enabled successfully, the `directory` field in the +returned object contains the path to the directory where the compile cache is stored. The +`status` field in the returned object would be one of the `module.constants.compileCacheStatus` values to indicate the result of the attempt to enable the [module compile cache][]. This method only affects the current Node.js instance. To enable it in child worker threads, @@ -1817,7 +1831,7 @@ returned object contains the following keys: [`SourceMap`]: #class-modulesourcemap [`initialize`]: #initialize [`module.constants.compileCacheStatus`]: #moduleconstantscompilecachestatus -[`module.enableCompileCache()`]: #moduleenablecompilecachecachedir +[`module.enableCompileCache()`]: #moduleenablecompilecacheoptions [`module.flushCompileCache()`]: #moduleflushcompilecache [`module.getCompileCacheDir()`]: #modulegetcompilecachedir [`module.setSourceMapsSupport()`]: #modulesetsourcemapssupportenabled-options diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 1ccae074e0405b..eda9b5c8f08deb 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -405,28 +405,32 @@ function stringify(body) { * Enable on-disk compiled cache for all user modules being compiled in the current Node.js instance * after this method is called. * This method accepts either: - * - A string `cacheDir`: the path to the cache directory. - * - An options object `{path?: string, portable?: boolean}`: - * - `path`: A string path to the cache directory. - * - `portable`: If `portable` is true, the cache directory will be considered relative. Defaults to false. - * If cache path is undefined, it defaults to the NODE_MODULE_CACHE environment variable. - * If `NODE_MODULE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`. - * @param {string | { path?: string, portable?: boolean } | undefined} options + * - A string: the directory to the cache directory. + * - An options object `{directory?: string, portable?: boolean}`: + * - `directory`: A string path to the cache directory. + * - `portable`: If `portable` is true, the cache directory will be considered relative. + * Defaults to `NODE_COMPILE_CACHE_PORTABLE === '1'`. + * If cache directory is undefined, it defaults to the `NODE_COMPILE_CACHE` environment variable. + * If `NODE_COMPILE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`. + * @param {string | { directory?: string, portable?: boolean } | undefined} options * @returns {{status: number, message?: string, directory?: string}} */ function enableCompileCache(options) { - let cacheDir; - let portable = false; + let portable; + let directory; if (typeof options === 'object' && options !== null) { - ({ path: cacheDir, portable = false } = options); + ({ directory, portable } = options); } else { - cacheDir = options; + directory = options; } - if (cacheDir === undefined) { - cacheDir = join(lazyTmpdir(), 'node-compile-cache'); + if (directory === undefined) { + directory = process.env.NODE_COMPILE_CACHE || join(lazyTmpdir(), 'node-compile-cache'); } - const nativeResult = _enableCompileCache(cacheDir, portable); + if (portable === undefined) { + portable = process.env.NODE_COMPILE_CACHE_PORTABLE === '1'; + } + const nativeResult = _enableCompileCache(directory, portable); const result = { status: nativeResult[0] }; if (nativeResult[1]) { result.message = nativeResult[1]; diff --git a/test/fixtures/compile-cache-wrapper-options.js b/test/fixtures/compile-cache-wrapper-options.js new file mode 100644 index 00000000000000..3ab73c9028a04f --- /dev/null +++ b/test/fixtures/compile-cache-wrapper-options.js @@ -0,0 +1,23 @@ +'use strict'; + +const { enableCompileCache, getCompileCacheDir, constants } = require('module'); + +console.log('dir before enableCompileCache:', getCompileCacheDir()); +const options = JSON.parse(process.env.NODE_TEST_COMPILE_CACHE_OPTIONS); +console.log('options:', options); +const result = enableCompileCache(options); +switch (result.status) { + case constants.compileCacheStatus.FAILED: + console.log('Compile cache failed. ' + result.message); + break; + case constants.compileCacheStatus.ENABLED: + console.log('Compile cache enabled. ' + result.directory); + break; + case constants.compileCacheStatus.ALREADY_ENABLED: + console.log('Compile cache already enabled.'); + break; + case constants.compileCacheStatus.DISABLED: + console.log('Compile cache already disabled.'); + break; +} +console.log('dir after enableCompileCache:', getCompileCacheDir()); diff --git a/test/parallel/test-compile-cache-api-options-portable-env.js b/test/parallel/test-compile-cache-api-options-portable-env.js new file mode 100644 index 00000000000000..4e5ca1f5704ff8 --- /dev/null +++ b/test/parallel/test-compile-cache-api-options-portable-env.js @@ -0,0 +1,89 @@ +'use strict'; + +// This tests module.enableCompileCache() with an options object still works with environment +// variable NODE_COMPILE_CACHE overrides. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); +const path = require('path'); + +const wrapper = fixtures.path('compile-cache-wrapper-options.js'); +tmpdir.refresh(); + +// Create a build directory and copy the entry point file. +const buildDir = tmpdir.resolve('build'); +fs.mkdirSync(buildDir); +const entryPoint = path.join(buildDir, 'empty.js'); +fs.copyFileSync(fixtures.path('empty.js'), entryPoint); + +// Check that the portable option can be overridden by NODE_COMPILE_CACHE_PORTABLE. +// We don't override NODE_COMPILE_CACHE because it will enable the cache before +// the wrapper is loaded. +spawnSyncAndAssert( + process.execPath, + ['-r', wrapper, entryPoint], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE_PORTABLE: '1', + NODE_TEST_COMPILE_CACHE_OPTIONS: JSON.stringify({ directory: 'build/.compile_cache' }), + }, + cwd: tmpdir.path + }, + { + stdout(output) { + console.log(output); // Logging for debugging. + assert.match(output, /dir before enableCompileCache: undefined/); + assert.match(output, /Compile cache enabled/); + assert.match(output, /dir after enableCompileCache: .*build\/\.compile_cache/); + return true; + }, + stderr(output) { + console.log(output); // Logging for debugging. + assert.match(output, /reading cache from .*build\/\.compile_cache.* for CommonJS .*empty\.js/); + assert.match(output, /empty\.js was not initialized, initializing the in-memory entry/); + assert.match(output, /writing cache for .*empty\.js.*success/); + return true; + } + }); + +assert(fs.existsSync(tmpdir.resolve('build/.compile_cache'))); + +const movedDir = buildDir + '_moved'; +fs.renameSync(buildDir, movedDir); +const movedEntryPoint = path.join(movedDir, 'empty.js'); + +// When portable is undefined, it should use the env var NODE_COMPILE_CACHE_PORTABLE. +spawnSyncAndAssert( + process.execPath, + ['-r', wrapper, movedEntryPoint], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE_PORTABLE: '1', + NODE_TEST_COMPILE_CACHE_OPTIONS: JSON.stringify({ directory: 'build_moved/.compile_cache' }), + }, + cwd: tmpdir.path + }, + { + stdout(output) { + console.log(output); // Logging for debugging. + assert.match(output, /dir before enableCompileCache: undefined/); + assert.match(output, /Compile cache enabled/); + assert.match(output, /dir after enableCompileCache: .*build_moved\/\.compile_cache/); + return true; + }, + stderr(output) { + console.log(output); // Logging for debugging. + assert.match(output, /reading cache from .*build_moved\/\.compile_cache.* for CommonJS .*empty\.js/); + assert.match(output, /cache for .*empty\.js was accepted, keeping the in-memory entry/); + assert.match(output, /.*skip .*empty\.js because cache was the same/); + return true; + } + }); diff --git a/test/parallel/test-compile-cache-api-portable.js b/test/parallel/test-compile-cache-api-portable.js index d31433e1a6a1f4..3c7c84b32202d1 100644 --- a/test/parallel/test-compile-cache-api-portable.js +++ b/test/parallel/test-compile-cache-api-portable.js @@ -1,6 +1,6 @@ 'use strict'; -// This tests module.enableCompileCache({ path, portable: true }) works +// This tests module.enableCompileCache({ directory, portable: true }) works // and supports portable paths across directory relocations. require('../common'); @@ -9,26 +9,18 @@ const assert = require('assert'); const fs = require('fs'); const tmpdir = require('../common/tmpdir'); const path = require('path'); +const fixtures = require('../common/fixtures'); tmpdir.refresh(); const workDir = path.join(tmpdir.path, 'work'); const cacheRel = '.compile_cache_dir'; fs.mkdirSync(workDir, { recursive: true }); -const wrapper = path.join(workDir, 'wrapper.js'); +const wrapper = fixtures.path('compile-cache-wrapper-options.js'); const target = path.join(workDir, 'target.js'); -fs.writeFileSync( - wrapper, - ` - const { enableCompileCache, getCompileCacheDir } = require('module'); - console.log('dir before enableCompileCache:', getCompileCacheDir()); - enableCompileCache({ path: '${cacheRel}', portable: true }); - console.log('dir after enableCompileCache:', getCompileCacheDir()); -` -); - fs.writeFileSync(target, ''); +const NODE_TEST_COMPILE_CACHE_OPTIONS = JSON.stringify({ directory: cacheRel, portable: true }); // First run { @@ -39,6 +31,7 @@ fs.writeFileSync(target, ''); env: { ...process.env, NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_TEST_COMPILE_CACHE_OPTIONS, }, cwd: workDir, }, @@ -73,13 +66,14 @@ fs.writeFileSync(target, ''); process.execPath, [ '-r', - path.join(movedWorkDir, 'wrapper.js'), + wrapper, path.join(movedWorkDir, 'target.js'), ], { env: { ...process.env, NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_TEST_COMPILE_CACHE_OPTIONS, }, cwd: movedWorkDir, }, From 39325cfce8677493ea84c5472c6f4affa05b7b21 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 22 Sep 2025 16:02:30 +0200 Subject: [PATCH 2/3] fixup! module: fix directory option in the enableCompileCache() API Co-authored-by: Aditi <62544124+Aditi-1400@users.noreply.github.com> --- lib/internal/modules/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index eda9b5c8f08deb..e2cdc0c5bba74b 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -405,7 +405,7 @@ function stringify(body) { * Enable on-disk compiled cache for all user modules being compiled in the current Node.js instance * after this method is called. * This method accepts either: - * - A string: the directory to the cache directory. + * - A string: path to the cache directory. * - An options object `{directory?: string, portable?: boolean}`: * - `directory`: A string path to the cache directory. * - `portable`: If `portable` is true, the cache directory will be considered relative. From 8a72ff2a1ff6e281ec7a44d067c09926c463b7b3 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 29 Sep 2025 16:38:59 +0200 Subject: [PATCH 3/3] fixup! module: fix directory option in the enableCompileCache() API --- .../test-compile-cache-api-options-portable-env.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/parallel/test-compile-cache-api-options-portable-env.js b/test/parallel/test-compile-cache-api-options-portable-env.js index 4e5ca1f5704ff8..5586e3e840467c 100644 --- a/test/parallel/test-compile-cache-api-options-portable-env.js +++ b/test/parallel/test-compile-cache-api-options-portable-env.js @@ -40,12 +40,12 @@ spawnSyncAndAssert( console.log(output); // Logging for debugging. assert.match(output, /dir before enableCompileCache: undefined/); assert.match(output, /Compile cache enabled/); - assert.match(output, /dir after enableCompileCache: .*build\/\.compile_cache/); + assert.match(output, /dir after enableCompileCache: .*build[/\\]\.compile_cache/); return true; }, stderr(output) { console.log(output); // Logging for debugging. - assert.match(output, /reading cache from .*build\/\.compile_cache.* for CommonJS .*empty\.js/); + assert.match(output, /reading cache from .*build[/\\]\.compile_cache.* for CommonJS .*empty\.js/); assert.match(output, /empty\.js was not initialized, initializing the in-memory entry/); assert.match(output, /writing cache for .*empty\.js.*success/); return true; @@ -76,12 +76,12 @@ spawnSyncAndAssert( console.log(output); // Logging for debugging. assert.match(output, /dir before enableCompileCache: undefined/); assert.match(output, /Compile cache enabled/); - assert.match(output, /dir after enableCompileCache: .*build_moved\/\.compile_cache/); + assert.match(output, /dir after enableCompileCache: .*build_moved[/\\]\.compile_cache/); return true; }, stderr(output) { console.log(output); // Logging for debugging. - assert.match(output, /reading cache from .*build_moved\/\.compile_cache.* for CommonJS .*empty\.js/); + assert.match(output, /reading cache from .*build_moved[/\\]\.compile_cache.* for CommonJS .*empty\.js/); assert.match(output, /cache for .*empty\.js was accepted, keeping the in-memory entry/); assert.match(output, /.*skip .*empty\.js because cache was the same/); return true;