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
43 changes: 43 additions & 0 deletions benchmark/module/module-require-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

// require() of a package that is already loaded, from a directory other than
// the one that first loaded it, with and without package.json "exports".

const fs = require('fs');
const path = require('path');
const { createRequire } = require('module');
const common = require('../common.js');
const tmpdir = require('../../test/common/tmpdir');

const bench = common.createBenchmark(main, {
exports: ['true', 'false'],
n: [1e5],
});

function main({ exports, n }) {
tmpdir.refresh();
const root = tmpdir.resolve('nodejs-benchmark-module-exports');
const pkgDir = path.join(root, 'node_modules', 'pkg');
const consumerDir = path.join(root, 'node_modules', 'consumer');
fs.mkdirSync(pkgDir, { recursive: true });
fs.mkdirSync(consumerDir);

const manifest = { name: 'pkg', main: 'index.js' };
if (exports === 'true') {
manifest.exports = './index.js';
}
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify(manifest));
fs.writeFileSync(path.join(pkgDir, 'index.js'), 'module.exports = {};');
fs.writeFileSync(path.join(consumerDir, 'index.js'),
'module.exports = () => require("pkg");');

const rootRequire = createRequire(path.join(root, 'index.js'));
rootRequire('pkg');
const load = rootRequire('consumer');

bench.start();
for (let i = 0; i < n; i++) {
load();
}
bench.end(n);
}
24 changes: 24 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ const { tracingChannel } = require('diagnostics_channel');
const onRequire = getLazy(() => tracingChannel('module.require'));

const relativeResolveCache = new SafeMap();
// Resolutions through package.json "exports" depend on the conditions, which the
// Module._pathCache key leaves out, so they are cached here for the default
// conditions only.
const exportsPathCache = new SafeMap();
let exportsPathCacheOwner = null;

let requireDepth = 0;
let isPreloading = false;
Expand Down Expand Up @@ -299,6 +304,7 @@ function purgeModuleCachesForPrefix(mountPoint) {
cleanForVfsPrefix(Module._cache, mountPoint);
// Module._pathCache keys are `request\0parent` tokens; match on value.
cleanForVfsPrefix(Module._pathCache, mountPoint, true);
cleanForVfsPrefix(exportsPathCache, mountPoint, true);
if (statCache !== null) { cleanForVfsPrefix(statCache, mountPoint); }
}

Expand Down Expand Up @@ -795,6 +801,21 @@ Module._findPath = function(request, paths, isMain, conditions = getCjsCondition
return entry;
}

// Only bare specifiers can resolve through "exports".
const cacheExports = !absoluteRequest && !isRelative(request) &&
conditions === getCjsConditions();
if (cacheExports) {
// Replacing Module._pathCache is how callers force a fresh resolution.
if (exportsPathCacheOwner !== Module._pathCache) {
exportsPathCache.clear();
exportsPathCacheOwner = Module._pathCache;
}
const exportsEntry = exportsPathCache.get(cacheKey);
if (exportsEntry !== undefined) {
return exportsEntry;
}
}

let exts;
const trailingSlash = request.length > 0 &&
(StringPrototypeCharCodeAt(request, request.length - 1) === CHAR_FORWARD_SLASH || (
Expand Down Expand Up @@ -831,6 +852,9 @@ Module._findPath = function(request, paths, isMain, conditions = getCjsCondition
if (!absoluteRequest) {
const exportsResolved = resolveExports(curPath, request, conditions);
if (exportsResolved) {
if (cacheExports) {
exportsPathCache.set(cacheKey, exportsResolved);
}
return exportsResolved;
}
}
Expand Down
60 changes: 60 additions & 0 deletions test/parallel/test-module-exports-resolution-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

// Checks the cache for packages resolved through package.json "exports" in
// Module._findPath(): it must not serve other conditions, and replacing
// Module._pathCache must still force a fresh resolution.

require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const Module = require('module');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

function writePackage(dir, files) {
fs.mkdirSync(dir, { recursive: true });
for (const [name, contents] of Object.entries(files)) {
fs.writeFileSync(path.join(dir, name), contents);
}
}

writePackage(tmpdir.resolve('node_modules', 'pkg'), {
'package.json': JSON.stringify({
name: 'pkg',
exports: { custom: './custom.js', default: './default.js' },
}),
'custom.js': '',
'default.js': '',
});

// Other conditions are resolved, not served the cached default result.
{
const paths = Module._nodeModulePaths(tmpdir.path);
const defaultTarget = tmpdir.resolve('node_modules', 'pkg', 'default.js');
assert.strictEqual(Module._findPath('pkg', paths), defaultTarget);
assert.strictEqual(Module._findPath('pkg', paths), defaultTarget);
assert.strictEqual(
Module._findPath('pkg', paths, false, new Set(['custom', 'require', 'node'])),
tmpdir.resolve('node_modules', 'pkg', 'custom.js'),
);
}

// A package installed closer to the parent is found once Module._pathCache
// has been replaced.
{
const dir = tmpdir.resolve('nested');
fs.mkdirSync(dir);
const paths = Module._nodeModulePaths(dir);
assert.strictEqual(Module._findPath('pkg', paths),
tmpdir.resolve('node_modules', 'pkg', 'default.js'));

writePackage(path.join(dir, 'node_modules', 'pkg'), {
'package.json': JSON.stringify({ name: 'pkg', exports: './nearer.js' }),
'nearer.js': '',
});
Module._pathCache = { __proto__: null };
assert.strictEqual(Module._findPath('pkg', paths),
path.join(dir, 'node_modules', 'pkg', 'nearer.js'));
}
Loading