Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: Private internal exports subpaths #33780

Closed
Closed
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
16 changes: 12 additions & 4 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_PRIVATE_PACKAGE_PATH"></a>
### `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][].

<a id="ERR_PROTO_ACCESS"></a>
### `ERR_PROTO_ACCESS`

Expand Down Expand Up @@ -2059,9 +2067,9 @@ signal (such as [`subprocess.kill()`][]).
<a id="ERR_UNSUPPORTED_DIR_IMPORT"></a>
### `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.

<!-- eslint-skip -->
```js
Expand Down Expand Up @@ -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
10 changes: 6 additions & 4 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_.
Expand All @@ -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_)
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
10 changes: 10 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,7 +122,7 @@ function getPackageConfig(path) {
main,
name,
type,
exports
exports: exports === null ? undefined : exports
};
packageJSONCache.set(path, packageConfig);
return packageConfig;
Expand Down Expand Up @@ -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);
}
Expand Down
9 changes: 9 additions & 0 deletions test/es-module/test-esm-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/node_modules/pkgexports/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions test/fixtures/node_modules/pkgexports/private-cjs.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/node_modules/pkgexports/private-esm.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions test/fixtures/node_modules/pkgexports/private-importer.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.