module: reintroduce package exports dot main
This reintroduces the dot main in exports as discussed in the previous Node.js modules meeting. The implementation includes both CommonJS and ES module resolution with the associated documentation and resolver specification changes. In addition to the dot main, "exports" as a string or direct fallback array is supported as well. Co-Authored-By: Geoffrey Booth <GeoffreyBooth@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/29494 Reviewed-By: Jan Krems <jan.krems@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com>
This commit is contained in:
parent
17e420b23f
commit
3f3ad38c87
@ -313,6 +313,33 @@ If a package has no exports, setting `"exports": false` can be used instead of
|
|||||||
`"exports": {}` to indicate the package does not intend for submodules to be
|
`"exports": {}` to indicate the package does not intend for submodules to be
|
||||||
exposed.
|
exposed.
|
||||||
|
|
||||||
|
Exports can also be used to map the main entry point of a package:
|
||||||
|
|
||||||
|
<!-- eslint-skip -->
|
||||||
|
```js
|
||||||
|
// ./node_modules/es-module-package/package.json
|
||||||
|
{
|
||||||
|
"exports": {
|
||||||
|
".": "./main.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where the "." indicates loading the package without any subpath. Exports will
|
||||||
|
always override any existing `"main"` value for both CommonJS and
|
||||||
|
ES module packages.
|
||||||
|
|
||||||
|
For packages with only a main entry point, an `"exports"` value of just
|
||||||
|
a string is also supported:
|
||||||
|
|
||||||
|
<!-- eslint-skip -->
|
||||||
|
```js
|
||||||
|
// ./node_modules/es-module-package/package.json
|
||||||
|
{
|
||||||
|
"exports": "./main.js"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Any invalid exports entries will be ignored. This includes exports not
|
Any invalid exports entries will be ignored. This includes exports not
|
||||||
starting with `"./"` or a missing trailing `"/"` for directory exports.
|
starting with `"./"` or a missing trailing `"/"` for directory exports.
|
||||||
|
|
||||||
@ -841,6 +868,15 @@ _isMain_ is **true** when resolving the Node.js application entry point.
|
|||||||
|
|
||||||
> 1. If _pjson_ is **null**, then
|
> 1. If _pjson_ is **null**, then
|
||||||
> 1. Throw a _Module Not Found_ error.
|
> 1. Throw a _Module Not Found_ error.
|
||||||
|
> 1. If _pjson.exports_ is not **null** or **undefined**, then
|
||||||
|
> 1. If _pjson.exports_ is a String or Array, then
|
||||||
|
> 1. Return _PACKAGE_EXPORTS_TARGET_RESOLVE(packageURL, pjson.exports,
|
||||||
|
> "")_.
|
||||||
|
> 1. If _pjson.exports is an Object, then
|
||||||
|
> 1. If _pjson.exports_ contains a _"."_ property, then
|
||||||
|
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
|
||||||
|
> 1. Return _PACKAGE_EXPORTS_TARGET_RESOLVE(packageURL, mainExport,
|
||||||
|
> "")_.
|
||||||
> 1. If _pjson.main_ is a String, then
|
> 1. If _pjson.main_ is a String, then
|
||||||
> 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and
|
> 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and
|
||||||
> _pjson.main_.
|
> _pjson.main_.
|
||||||
|
@ -360,11 +360,11 @@ function findLongestRegisteredExtension(filename) {
|
|||||||
// This only applies to requests of a specific form:
|
// This only applies to requests of a specific form:
|
||||||
// 1. name/.*
|
// 1. name/.*
|
||||||
// 2. @scope/name/.*
|
// 2. @scope/name/.*
|
||||||
const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)$/;
|
const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
|
||||||
function resolveExports(nmPath, request, absoluteRequest) {
|
function resolveExports(nmPath, request, absoluteRequest) {
|
||||||
// The implementation's behavior is meant to mirror resolution in ESM.
|
// The implementation's behavior is meant to mirror resolution in ESM.
|
||||||
if (experimentalExports && !absoluteRequest) {
|
if (experimentalExports && !absoluteRequest) {
|
||||||
const [, name, expansion] =
|
const [, name, expansion = ''] =
|
||||||
StringPrototype.match(request, EXPORTS_PATTERN) || [];
|
StringPrototype.match(request, EXPORTS_PATTERN) || [];
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return path.resolve(nmPath, request);
|
return path.resolve(nmPath, request);
|
||||||
@ -397,6 +397,10 @@ function resolveExports(nmPath, request, absoluteRequest) {
|
|||||||
subpath, basePath, mappingKey);
|
subpath, basePath, mappingKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mappingKey === '.' && typeof pkgExports === 'string') {
|
||||||
|
return resolveExportsTarget(pathToFileURL(basePath + '/'), pkgExports,
|
||||||
|
'', basePath, mappingKey);
|
||||||
|
}
|
||||||
if (pkgExports != null) {
|
if (pkgExports != null) {
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const e = new Error(`Package exports for '${basePath}' do not define ` +
|
const e = new Error(`Package exports for '${basePath}' do not define ` +
|
||||||
|
@ -773,39 +773,6 @@ Maybe<URL> FinalizeResolution(Environment* env,
|
|||||||
return Just(resolved);
|
return Just(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
Maybe<URL> PackageMainResolve(Environment* env,
|
|
||||||
const URL& pjson_url,
|
|
||||||
const PackageConfig& pcfg,
|
|
||||||
const URL& base) {
|
|
||||||
if (pcfg.exists == Exists::Yes) {
|
|
||||||
if (pcfg.has_main == HasMain::Yes) {
|
|
||||||
URL resolved(pcfg.main, pjson_url);
|
|
||||||
const std::string& path = resolved.ToFilePath();
|
|
||||||
if (CheckDescriptorAtPath(path) == FILE) {
|
|
||||||
return Just(resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (env->options()->es_module_specifier_resolution == "node") {
|
|
||||||
if (pcfg.has_main == HasMain::Yes) {
|
|
||||||
return FinalizeResolution(env, URL(pcfg.main, pjson_url), base);
|
|
||||||
} else {
|
|
||||||
return FinalizeResolution(env, URL("index", pjson_url), base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pcfg.type != PackageType::Module) {
|
|
||||||
Maybe<URL> resolved = LegacyMainResolve(pjson_url, pcfg);
|
|
||||||
if (!resolved.IsNothing()) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::string msg = "Cannot find main entry point for " +
|
|
||||||
URL(".", pjson_url).ToFilePath() + " imported from " +
|
|
||||||
base.ToFilePath();
|
|
||||||
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
|
|
||||||
return Nothing<URL>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ThrowExportsNotFound(Environment* env,
|
void ThrowExportsNotFound(Environment* env,
|
||||||
const std::string& subpath,
|
const std::string& subpath,
|
||||||
const URL& pjson_url,
|
const URL& pjson_url,
|
||||||
@ -887,6 +854,99 @@ Maybe<URL> ResolveExportsTarget(Environment* env,
|
|||||||
return Just(subpath_resolved);
|
return Just(subpath_resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Maybe<URL> PackageMainResolve(Environment* env,
|
||||||
|
const URL& pjson_url,
|
||||||
|
const PackageConfig& pcfg,
|
||||||
|
const URL& base) {
|
||||||
|
if (pcfg.exists == Exists::Yes) {
|
||||||
|
Isolate* isolate = env->isolate();
|
||||||
|
Local<Context> context = env->context();
|
||||||
|
if (!pcfg.exports.IsEmpty()) {
|
||||||
|
Local<Value> exports = pcfg.exports.Get(isolate);
|
||||||
|
if (exports->IsString() || exports->IsObject() || exports->IsArray()) {
|
||||||
|
Local<Value> target;
|
||||||
|
if (!exports->IsObject()) {
|
||||||
|
target = exports;
|
||||||
|
} else {
|
||||||
|
Local<Object> exports_obj = exports.As<Object>();
|
||||||
|
Local<String> dot_string = String::NewFromUtf8(env->isolate(), ".",
|
||||||
|
v8::NewStringType::kNormal).ToLocalChecked();
|
||||||
|
target =
|
||||||
|
exports_obj->Get(env->context(), dot_string).ToLocalChecked();
|
||||||
|
}
|
||||||
|
if (target->IsString()) {
|
||||||
|
Utf8Value target_utf8(isolate, target.As<v8::String>());
|
||||||
|
std::string target(*target_utf8, target_utf8.length());
|
||||||
|
Maybe<URL> resolved = ResolveExportsTarget(env, target, "", ".",
|
||||||
|
pjson_url, base);
|
||||||
|
if (resolved.IsNothing()) {
|
||||||
|
ThrowExportsInvalid(env, ".", target, pjson_url, base);
|
||||||
|
return Nothing<URL>();
|
||||||
|
}
|
||||||
|
return FinalizeResolution(env, resolved.FromJust(), base);
|
||||||
|
} else if (target->IsArray()) {
|
||||||
|
Local<Array> target_arr = target.As<Array>();
|
||||||
|
const uint32_t length = target_arr->Length();
|
||||||
|
if (length == 0) {
|
||||||
|
ThrowExportsInvalid(env, ".", target, pjson_url, base);
|
||||||
|
return Nothing<URL>();
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < length; i++) {
|
||||||
|
auto target_item = target_arr->Get(context, i).ToLocalChecked();
|
||||||
|
if (target_item->IsString()) {
|
||||||
|
Utf8Value target_utf8(isolate, target_item.As<v8::String>());
|
||||||
|
std::string target_str(*target_utf8, target_utf8.length());
|
||||||
|
Maybe<URL> resolved = ResolveExportsTarget(env, target_str, "",
|
||||||
|
".", pjson_url, base, false);
|
||||||
|
if (resolved.IsNothing()) continue;
|
||||||
|
return FinalizeResolution(env, resolved.FromJust(), base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto invalid = target_arr->Get(context, length - 1).ToLocalChecked();
|
||||||
|
if (!invalid->IsString()) {
|
||||||
|
ThrowExportsInvalid(env, ".", invalid, pjson_url, base);
|
||||||
|
return Nothing<URL>();
|
||||||
|
}
|
||||||
|
Utf8Value invalid_utf8(isolate, invalid.As<v8::String>());
|
||||||
|
std::string invalid_str(*invalid_utf8, invalid_utf8.length());
|
||||||
|
Maybe<URL> resolved = ResolveExportsTarget(env, invalid_str, "",
|
||||||
|
".", pjson_url, base);
|
||||||
|
CHECK(resolved.IsNothing());
|
||||||
|
return Nothing<URL>();
|
||||||
|
} else {
|
||||||
|
ThrowExportsInvalid(env, ".", target, pjson_url, base);
|
||||||
|
return Nothing<URL>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pcfg.has_main == HasMain::Yes) {
|
||||||
|
URL resolved(pcfg.main, pjson_url);
|
||||||
|
const std::string& path = resolved.ToFilePath();
|
||||||
|
if (CheckDescriptorAtPath(path) == FILE) {
|
||||||
|
return Just(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (env->options()->es_module_specifier_resolution == "node") {
|
||||||
|
if (pcfg.has_main == HasMain::Yes) {
|
||||||
|
return FinalizeResolution(env, URL(pcfg.main, pjson_url), base);
|
||||||
|
} else {
|
||||||
|
return FinalizeResolution(env, URL("index", pjson_url), base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pcfg.type != PackageType::Module) {
|
||||||
|
Maybe<URL> resolved = LegacyMainResolve(pjson_url, pcfg);
|
||||||
|
if (!resolved.IsNothing()) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string msg = "Cannot find main entry point for " +
|
||||||
|
URL(".", pjson_url).ToFilePath() + " imported from " +
|
||||||
|
base.ToFilePath();
|
||||||
|
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
|
||||||
|
return Nothing<URL>();
|
||||||
|
}
|
||||||
|
|
||||||
Maybe<URL> PackageExportsResolve(Environment* env,
|
Maybe<URL> PackageExportsResolve(Environment* env,
|
||||||
const URL& pjson_url,
|
const URL& pjson_url,
|
||||||
const std::string& pkg_subpath,
|
const std::string& pkg_subpath,
|
||||||
|
@ -20,6 +20,8 @@ import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
|
|||||||
// Fallbacks
|
// Fallbacks
|
||||||
['pkgexports/fallbackdir/asdf.js', { default: 'asdf' }],
|
['pkgexports/fallbackdir/asdf.js', { default: 'asdf' }],
|
||||||
['pkgexports/fallbackfile', { default: 'asdf' }],
|
['pkgexports/fallbackfile', { default: 'asdf' }],
|
||||||
|
// Dot main
|
||||||
|
['pkgexports', { default: 'asdf' }],
|
||||||
]);
|
]);
|
||||||
for (const [validSpecifier, expected] of validSpecifiers) {
|
for (const [validSpecifier, expected] of validSpecifiers) {
|
||||||
if (validSpecifier === null) continue;
|
if (validSpecifier === null) continue;
|
||||||
@ -81,18 +83,6 @@ import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's no main field so we won't find anything when importing the name.
|
|
||||||
// The fact that "." is mapped is ignored, it's not a valid main config.
|
|
||||||
loadFixture('pkgexports').catch(mustCall((err) => {
|
|
||||||
if (isRequire) {
|
|
||||||
strictEqual(err.code, 'MODULE_NOT_FOUND');
|
|
||||||
assertStartsWith(err.message, 'Cannot find module \'pkgexports\'');
|
|
||||||
} else {
|
|
||||||
strictEqual(err.code, 'ERR_MODULE_NOT_FOUND');
|
|
||||||
assertStartsWith(err.message, 'Cannot find main entry point');
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Covering out bases - not a file is still not a file after dir mapping.
|
// Covering out bases - not a file is still not a file after dir mapping.
|
||||||
loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => {
|
loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => {
|
||||||
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
|
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user