diff --git a/doc/api/errors.md b/doc/api/errors.md
index ccbd1bd406c0b4..c0251fcdb94b53 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1697,6 +1697,14 @@ The `package.json` [exports][] field does not export the requested subpath.
Because exports are encapsulated, private internal modules that are not exported
cannot be imported through the package resolution, unless using an absolute URL.
+
+### `ERR_PRIVATE_PACKAGE_PATH`
+
+Thrown when trying to access a private exports subpath from outside a package.
+Private package subpaths starting with `#` defined in the `package.json`
+[exports][] field can only be resolved from within modules of the same package
+using [package internal self-resolution][].
+
### `ERR_PROTO_ACCESS`
@@ -2059,9 +2067,9 @@ signal (such as [`subprocess.kill()`][]).
### `ERR_UNSUPPORTED_DIR_IMPORT`
-`import` a directory URL is unsupported. Instead, you can
-[self-reference a package using its name][] and [define a custom subpath][] in
-the `"exports"` field of the `package.json` file.
+`import` a directory URL is unsupported. Instead use explicit file paths
+or the package author can [define a custom subpath][] in the `"exports"` field
+of the `package.json` file.
```js
@@ -2624,5 +2632,5 @@ such as `process.stdout.on('data')`.
[Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute
[try-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
[vm]: vm.html
-[self-reference a package using its name]: esm.html#esm_self_referencing_a_package_using_its_name
+[package internal self-resolution]: esm.html#esm_self_referencing_a_package_using_its_name
[define a custom subpath]: esm.html#esm_subpath_exports
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 38885d7a0b243e..445a6abd4e0ec9 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -1634,6 +1634,8 @@ The resolver can throw the following errors:
> 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then
> 1. Let _exports_ be _pjson.exports_.
> 1. If _exports_ is not **null** or **undefined**, then
+> 1. If _packageSubpath_ starts with _"#"_, then
+> 1. Throw a _Private Package Path_ error.
> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_,
> _packageSubpath_, _pjson.exports_).
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
@@ -1656,9 +1658,9 @@ The resolver can throw the following errors:
> 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then
> 1. Let _exports_ be _pjson.exports_.
> 1. If _exports_ is not **null** or **undefined**, then
-> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_,
-> _pjson.exports_).
-> 1. Return the URL resolution of _subpath_ in _packageURL_.
+> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_,
+> _packageSubpath_, _pjson.exports_).
+> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
> 1. Otherwise, return **undefined**.
**PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_)
@@ -1682,7 +1684,7 @@ The resolver can throw the following errors:
> _Module Not Found_ error for no resolution.
> 1. Return _legacyMainURL_.
-**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packagePath_, _exports_)
+**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packageSubpath_, _exports_)
> 1. If _exports_ is an Object with both a key starting with _"."_ and a key not
> starting with _"."_, throw an _Invalid Package Configuration_ error.
> 1. If _exports_ is an Object and all keys of _exports_ start with _"."_, then
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index d3ca1ea1a6bdaf..7ebb852b71d227 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1314,6 +1314,10 @@ E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
return `Package subpath '${subpath}' is not defined by "exports" in ${
pkgPath} imported from ${base}`;
}, Error);
+E('ERR_PRIVATE_PACKAGE_PATH', (pkgPath, subpath, base = undefined) => {
+ return `Private package subpath '${subpath}' can only be resolved from within
+ the package ${pkgPath}${base ? `, imported from ${base}` : ''}`;
+}, Error);
E('ERR_REQUIRE_ESM',
(filename, parentPath = null, packageJsonPath = null) => {
let msg = `Must use import to load ES Module: ${filename}`;
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index c2be5ea025a3d1..950d8ae44945fe 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -90,6 +90,7 @@ const {
ERR_INVALID_PACKAGE_TARGET,
ERR_INVALID_MODULE_SPECIFIER,
ERR_PACKAGE_PATH_NOT_EXPORTED,
+ ERR_PRIVATE_PACKAGE_PATH,
ERR_REQUIRE_ESM
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');
@@ -527,6 +528,15 @@ function resolveExports(nmPath, request) {
}
const basePath = path.resolve(nmPath, name);
+
+ if (StringPrototypeStartsWith(expansion, '/#')) {
+ // Only throw private subpath errors when exports field is defined.
+ const pkgExports = readPackageExports(basePath);
+ if (pkgExports === undefined || pkgExports === null)
+ return false;
+ throw new ERR_PRIVATE_PACKAGE_PATH(basePath, '.' + expansion, request);
+ }
+
const fromExports = applyExports(basePath, expansion);
if (fromExports) {
return tryFile(fromExports, false);
diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js
index 987a139c6aae57..38513bab0ac5b4 100644
--- a/lib/internal/modules/esm/resolve.js
+++ b/lib/internal/modules/esm/resolve.js
@@ -45,6 +45,7 @@ const {
ERR_INVALID_PACKAGE_TARGET,
ERR_MODULE_NOT_FOUND,
ERR_PACKAGE_PATH_NOT_EXPORTED,
+ ERR_PRIVATE_PACKAGE_PATH,
ERR_UNSUPPORTED_DIR_IMPORT,
ERR_UNSUPPORTED_ESM_URL_SCHEME,
} = require('internal/errors').codes;
@@ -121,7 +122,7 @@ function getPackageConfig(path) {
main,
name,
type,
- exports
+ exports: exports === null ? undefined : exports
};
packageJSONCache.set(path, packageConfig);
return packageConfig;
@@ -581,6 +582,12 @@ function packageResolve(specifier, base, conditions) {
return packageMainResolve(packageJSONUrl, packageConfig, base,
conditions);
} else if (packageConfig.exports !== undefined) {
+ if (StringPrototypeStartsWith(packageSubpath, './#') &&
+ packageConfig.exports !== undefined) {
+ throw new ERR_PRIVATE_PACKAGE_PATH(
+ removePackageJsonFromPath(fileURLToPath(packageJSONUrl)),
+ packageSubpath, fileURLToPath(base));
+ }
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs
index f9dc6ec472f424..bc1437cb74b6bc 100644
--- a/test/es-module/test-esm-exports.mjs
+++ b/test/es-module/test-esm-exports.mjs
@@ -179,15 +179,24 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
'package subpath keys or an object of main entry condition name keys ' +
'only.');
}));
+
+ // Private mappings should throw a private error when imported externally
+ loadFixture('pkgexports/#private').catch(mustCall((err) => {
+ strictEqual(err.code, 'ERR_PRIVATE_PACKAGE_PATH');
+ assertStartsWith(err.message, 'Private package subpath \'./#private\'');
+ }));
});
const { requireFromInside, importFromInside } = fromInside;
[importFromInside, requireFromInside].forEach((loadFromInside) => {
+ const isRequire = loadFromInside === requireFromInside;
const validSpecifiers = new Map([
// A file not visible from outside of the package
['../not-exported.js', { default: 'not-exported' }],
// Part of the public interface
['pkgexports/valid-cjs', { default: 'asdf' }],
+ // Private mappings
+ ['pkg-exports/#private', { default: isRequire ? 'cjs' : 'esm' }],
]);
for (const [validSpecifier, expected] of validSpecifiers) {
if (validSpecifier === null) continue;
diff --git a/test/fixtures/node_modules/pkgexports/package.json b/test/fixtures/node_modules/pkgexports/package.json
index b99e5c7b79f6a8..7495d14185053e 100644
--- a/test/fixtures/node_modules/pkgexports/package.json
+++ b/test/fixtures/node_modules/pkgexports/package.json
@@ -1,6 +1,10 @@
{
"name": "pkg-exports",
"exports": {
+ "./#private": {
+ "require": "./private-cjs.cjs",
+ "import": "./private-esm.mjs"
+ },
"./hole": "./lib/hole.js",
"./space": "./sp%20ce.js",
"./valid-cjs": "./asdf.js",
diff --git a/test/fixtures/node_modules/pkgexports/private-cjs.cjs b/test/fixtures/node_modules/pkgexports/private-cjs.cjs
new file mode 100644
index 00000000000000..57822c50bd7204
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports/private-cjs.cjs
@@ -0,0 +1,2 @@
+module.exports = 'cjs';
+
diff --git a/test/fixtures/node_modules/pkgexports/private-esm.mjs b/test/fixtures/node_modules/pkgexports/private-esm.mjs
new file mode 100644
index 00000000000000..914e3a97d5dd82
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports/private-esm.mjs
@@ -0,0 +1 @@
+export default 'esm';
diff --git a/test/fixtures/node_modules/pkgexports/private-importer.mjs b/test/fixtures/node_modules/pkgexports/private-importer.mjs
new file mode 100644
index 00000000000000..e6f9eb1494b32e
--- /dev/null
+++ b/test/fixtures/node_modules/pkgexports/private-importer.mjs
@@ -0,0 +1,2 @@
+export { default } from 'pkg-exports/#private';
+