Repository: nodejs/node
Pull request: 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.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: doc/api/module.md
patch: @@ -385,20 +385,33 @@ 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.
 
 To clean up the generated compile cache on disk, simply remove the cache directory. The cache
 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`
 
 <!-- YAML
@@ -494,16 +498,30 @@ The following constants are returned as the `status` field in the object returne
   </tr>
 </table>
 
-### `module.enableCompileCache([cacheDir])`
+### `module.enableCompileCache([options])`
 
 <!-- YAML
 added: v22.8.0
+changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/58797
+    description: Add `portable` option to enable portable compile cache.
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/59931
+    description: Rename the unreleased `path` option to `directory` to maintain consistency.
 -->
 
 > 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

filename: lib/internal/modules/helpers.js
patch: @@ -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: 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.
+ *     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];

filename: test/fixtures/compile-cache-wrapper-options.js
patch: @@ -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());

filename: test/parallel/test-compile-cache-api-options-portable-env.js
patch: @@ -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;
+    }
+  });

filename: test/parallel/test-compile-cache-api-portable.js
patch: @@ -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,
     },