Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions benchmark/module/package-scope-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const common = require('../common');
const fs = require('fs');
const { pathToFileURL } = require('url');
const tmpdir = require('../../test/common/tmpdir');

const bench = common.createBenchmark(main, {
entries: [0, 20, 200, 1000],
n: [1e4],
}, { flags: ['--expose-internals'] });

function main({ entries, n }) {
const { getPackageScopeConfig } = require('internal/modules/package_json_reader');
tmpdir.refresh();
const imports = {};
const exports = {};
for (let i = 0; i < entries; i++) {
imports[`#entry${i}`] = `./entry${i}.js`;
exports[`./entry${i}`] = `./entry${i}.js`;
}
fs.writeFileSync(tmpdir.resolve('package.json'), JSON.stringify({
type: 'module', imports, exports,
}));
const urls = Array.from({ length: 100 }, (_, i) =>
pathToFileURL(tmpdir.resolve(`entry${i}.js`)).href);
// Warm the package cache while looking up distinct modules in one scope.
for (const url of urls) getPackageScopeConfig(url);

bench.start();
for (let i = 0; i < n; i++) {
getPackageScopeConfig(urls[i % urls.length]);
}
bench.end(n);
}
22 changes: 21 additions & 1 deletion lib/internal/modules/package_json_reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ const moduleToParentPackageJSONCache = new SafeMap();
*/
const deserializedPackageJSONCache = new SafeMap();

// Keep the serialized fields as well: unlike the native package cache, VFS
// lookups can return updated contents for the same package.json path.
const packageScopeConfigCache = new SafeMap();

/**
* The directory the native nearest-parent traversal starts from for `checkPath`
* (see BindingData::NormalizePath/TraverseParent): the path itself when it has
Expand Down Expand Up @@ -226,7 +230,22 @@ function getPackageScopeConfig(resolved) {
const result = loaderMethods.getPackageScopeConfig(`${resolved}`);

if (ArrayIsArray(result)) {
const { data, exists, path } = deserializePackageJSON(`${resolved}`, result);
const pjsonPath = result[5];
let cached = packageScopeConfigCache.get(pjsonPath);
if (cached === undefined ||
cached.serialized[0] !== result[0] ||
cached.serialized[1] !== result[1] ||
cached.serialized[2] !== result[2] ||
cached.serialized[3] !== result[3] ||
cached.serialized[4] !== result[4]) {
cached = {
__proto__: null,
serialized: result,
deserialized: deserializePackageJSON(`${resolved}`, result),
};
packageScopeConfigCache.set(pjsonPath, cached);
}
const { data, exists, path } = cached.deserialized;

return {
__proto__: null,
Expand Down Expand Up @@ -407,6 +426,7 @@ function purgePackageJSONCacheForPrefix(mountPoint) {
cleanForVfsPrefix(directoryToParentPackageJSONPathCache, mountPoint);
cleanForVfsPrefix(moduleToParentPackageJSONCache, mountPoint);
cleanForVfsPrefix(deserializedPackageJSONCache, mountPoint);
cleanForVfsPrefix(packageScopeConfigCache, mountPoint);
}

module.exports = {
Expand Down
56 changes: 56 additions & 0 deletions test/parallel/test-package-scope-config-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Flags: --expose-internals
'use strict';

require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const tmpdir = require('../common/tmpdir');
const { getPackageScopeConfig } = require('internal/modules/package_json_reader');

tmpdir.refresh();
const packageConfig = {
name: 'scope-cache',
type: 'module',
exports: { '.': './index.js' },
imports: { '#dep': './dep.js' },
};
fs.writeFileSync(tmpdir.resolve('package.json'), JSON.stringify(packageConfig));
fs.mkdirSync(tmpdir.resolve('nested'));
fs.writeFileSync(tmpdir.resolve('nested', 'package.json'), JSON.stringify({
type: 'commonjs',
exports: ['./other.js'],
}));

const first = getPackageScopeConfig(pathToFileURL(tmpdir.resolve('index.js')));
const second = getPackageScopeConfig(pathToFileURL(tmpdir.resolve('dep.js')).href);
assert.strictEqual(first.pjsonPath,
path.toNamespacedPath(tmpdir.resolve('package.json')));
assert.strictEqual(first.type, 'module');
assert.deepStrictEqual(first.exports, packageConfig.exports);
assert.deepStrictEqual(first.imports, packageConfig.imports);
assert.strictEqual(second.exports, first.exports);
assert.strictEqual(second.imports, first.imports);

// Each caller still receives its own configuration wrapper.
first.type = 'commonjs';
assert.strictEqual(second.type, 'module');
assert.strictEqual(getPackageScopeConfig(
pathToFileURL(tmpdir.resolve('third.js'))).type, 'module');

const nested = getPackageScopeConfig(pathToFileURL(tmpdir.resolve('nested', 'index.js')));
assert.strictEqual(nested.pjsonPath,
path.toNamespacedPath(tmpdir.resolve('nested', 'package.json')));
assert.strictEqual(nested.type, 'commonjs');
assert.deepStrictEqual(nested.exports, ['./other.js']);
assert.strictEqual(nested.imports, undefined);

// Package scope traversal must still stop at node_modules.
fs.mkdirSync(tmpdir.resolve('node_modules', 'bare'), { recursive: true });
const missing = getPackageScopeConfig(
pathToFileURL(tmpdir.resolve('node_modules', 'bare', 'index.js')));
assert.strictEqual(missing.exists, false);
assert.strictEqual(missing.type, 'none');
assert.strictEqual(missing.pjsonPath,
path.join(tmpdir.path, 'node_modules', 'package.json'));
73 changes: 73 additions & 0 deletions test/parallel/test-vfs-package-scope-config-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Flags: --experimental-vfs --expose-internals
'use strict';

require('../common');
const assert = require('assert');
const path = require('path');
const { pathToFileURL } = require('url');
const vfs = require('node:vfs');
const { getPackageScopeConfig } = require('internal/modules/package_json_reader');

const volume = vfs.create();
volume.mkdirSync('/pkg');
const mountPoint = volume.mount();
const packagePath = path.join(mountPoint, 'pkg', 'package.json');
const url = pathToFileURL(path.join(mountPoint, 'pkg', 'index.js'));
const config = {
name: 'before',
main: './index.js',
type: 'module',
imports: { '#dep': './dep.js' },
exports: { '.': './index.js' },
};

function readConfig() {
volume.writeFileSync(packagePath, JSON.stringify(config));
const result = getPackageScopeConfig(url);
for (const key of Object.keys(config)) {
assert.deepStrictEqual(result[key], config[key]);
}
assert.strictEqual(result.exists, true);
assert.strictEqual(result.pjsonPath, path.join(mountPoint, 'pkg', 'package.json'));
return result;
}

try {
const first = readConfig();
assert.strictEqual(getPackageScopeConfig(url).exports, first.exports);

// Invalidate on each serialized field independently, including fields that
// are not used to resolve package imports.
for (const [key, value] of Object.entries({
name: 'after',
main: './other.js',
type: 'commonjs',
imports: { '#dep': './other.js' },
exports: ['./other.js'],
})) {
config[key] = value;
readConfig();
}

const beforePurge = getPackageScopeConfig(url);
volume.unmount();
assert.strictEqual(volume.mount(), mountPoint);
const afterPurge = getPackageScopeConfig(url);
assert.deepStrictEqual(afterPurge, beforePurge);
assert.notStrictEqual(afterPurge.exports, beforePurge.exports);

config.exports = './direct.js';
readConfig();
delete config.imports;
assert.strictEqual(readConfig().imports, undefined);
delete config.exports;
assert.strictEqual(readConfig().exports, undefined);

// A warm cache must not hide parse errors or a removed package.json.
volume.writeFileSync(packagePath, '{');
assert.throws(() => getPackageScopeConfig(url), { code: 'ERR_INVALID_PACKAGE_CONFIG' });
volume.unlinkSync(packagePath);
assert.strictEqual(getPackageScopeConfig(url).exists, false);
} finally {
volume.unmount();
}
Loading