Files
node/test/parallel/test-module-nearest-parent-package-json-cache.js
T
Shelley Vohr 9c641c590f module: cache nearest parent package.json per directory
getNearestParentPackageJSON() memoized its answer per file, so every
module loaded still made one native call, and TraverseParent() builds
several std::filesystem::path temporaries per directory level and
serializes the whole package.json, which the JS side then usually
discarded because it already had that package.json deserialized.

The native traversal starts at the directory of the given path, so the
answer only depends on that directory: key the memo by it (following
NormalizePath()'s trailing-separator rule), so that all modules in a
directory share one native call. When the permission model is enabled
the traversal also depends on the read permissions in effect at call
time, so that configuration keeps the per-file cache.

Loading a 1000-module tree spread over ~240 directories goes from 1000
to 236 native calls.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
2026-08-19 13:18:11 +00:00

58 lines
2.4 KiB
JavaScript

'use strict';
// Flags: --expose-internals
// The nearest parent package.json lookup that every CommonJS module load
// performs is answered once per directory, not once per file.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { internalBinding } = require('internal/test/binding');
const packageJsonReader = require('internal/modules/package_json_reader');
tmpdir.refresh();
const root = tmpdir.resolve('pkg');
const sub = path.join(root, 'lib', 'sub');
fs.mkdirSync(sub, { recursive: true });
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'pkg', type: 'commonjs' }));
const files = [];
for (const dir of [path.join(root, 'lib'), sub]) {
for (let i = 0; i < 5; i++) {
const file = path.join(dir, `m${i}.js`);
fs.writeFileSync(file, 'module.exports = __filename;');
files.push(file);
}
}
const modulesBinding = internalBinding('modules');
const original = modulesBinding.getNearestParentPackageJSON;
const calls = [];
modulesBinding.getNearestParentPackageJSON = common.mustCallAtLeast((checkPath) => {
calls.push(checkPath);
return original(checkPath);
}, 1);
for (const file of files) {
assert.strictEqual(require(file), file);
}
// Ten modules in two directories: two lookups reach the binding.
assert.strictEqual(calls.length, 2, `binding called for: ${calls.join(', ')}`);
// Same answer (and the same object) for every file of a directory, and for
// the directory itself when asked with a trailing separator.
const viaFile = packageJsonReader.getNearestParentPackageJSON(files[0]);
assert.strictEqual(viaFile.data.name, 'pkg');
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(files[1]), viaFile);
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(root, 'lib') + path.sep), viaFile);
assert.strictEqual(calls.length, 2);
// A directory that has not been seen yet is looked up once more.
const other = path.join(root, 'other');
fs.mkdirSync(other);
fs.writeFileSync(path.join(other, 'x.js'), '');
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other, 'x.js')).data.name, 'pkg');
assert.strictEqual(packageJsonReader.getNearestParentPackageJSON(path.join(other, 'y.js')).data.name, 'pkg');
assert.strictEqual(calls.length, 3);
modulesBinding.getNearestParentPackageJSON = original;