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'; +