module: pkg exports validations and fallbacks

PR-URL: https://github.com/nodejs/node/pull/28949
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Jan Krems <jan.krems@gmail.com>
This commit is contained in:
Guy Bedford 2019-08-02 01:30:32 -04:00
parent 15b2d13310
commit 2103ae4835
10 changed files with 421 additions and 128 deletions

View File

@ -242,13 +242,13 @@ throw when an attempt is made to import them:
```js
import submodule from 'es-module-package/private-module.js';
// Throws - Package exports error
// Throws - Module not found
```
> Note: this is not a strong encapsulation as any private modules can still be
> loaded by absolute paths.
Folders can also be mapped with package exports as well:
Folders can also be mapped with package exports:
<!-- eslint-skip -->
```js
@ -268,8 +268,24 @@ import feature from 'es-module-package/features/x.js';
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
exposed.
This is just a convention that works because `false`, just like `{}`, has no
iterable own properties.
Any invalid exports entries will be ignored. This includes exports not
starting with `"./"` or a missing trailing `"/"` for directory exports.
Array fallback support is provided for exports, similarly to import maps
in order to be forward-compatible with fallback workflows in future:
<!-- eslint-skip -->
```js
{
"exports": {
"./submodule": ["not:valid", "./submodule.js"]
}
}
```
Since `"not:valid"` is not a supported target, `"./submodule.js"` is used
instead as the fallback, as if it were the only target.
## <code>import</code> Specifiers
@ -660,7 +676,7 @@ CommonJS loader. Additional formats such as _"addon"_ can be extended in future
updates.
In the following algorithms, all subroutine errors are propagated as errors
of these top-level routines.
of these top-level routines unless stated otherwise.
_isMain_ is **true** when resolving the Node.js application entry point.
@ -681,6 +697,9 @@ _isMain_ is **true** when resolving the Node.js application entry point.
> 1. Note: _specifier_ is now a bare specifier.
> 1. Set _resolvedURL_ the result of
> **PACKAGE_RESOLVE**(_specifier_, _parentURL_).
> 1. If _resolvedURL_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_
> and _"%5C"_ respectively), then
> 1. Throw an _Invalid Specifier_ error.
> 1. If the file at _resolvedURL_ does not exist, then
> 1. Throw a _Module Not Found_ error.
> 1. Set _resolvedURL_ to the real path of _resolvedURL_.
@ -737,7 +756,7 @@ _isMain_ is **true** when resolving the Node.js application entry point.
> 1. If _pjson_ is **null**, then
> 1. Throw a _Module Not Found_ error.
> 1. If _pjson.main_ is a String, then
> 1. Let _resolvedMain_ be the concatenation of _packageURL_, "/", and
> 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and
> _pjson.main_.
> 1. If the file at _resolvedMain_ exists, then
> 1. Return _resolvedMain_.
@ -746,8 +765,6 @@ _isMain_ is **true** when resolving the Node.js application entry point.
> 1. Let _legacyMainURL_ be the result applying the legacy
> **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a
> _Module Not Found_ error for no resolution.
> 1. If _legacyMainURL_ does not end in _".js"_ then,
> 1. Throw an _Unsupported File Extension_ error.
> 1. Return _legacyMainURL_.
**PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _packagePath_, _exports_)
@ -755,19 +772,42 @@ _isMain_ is **true** when resolving the Node.js application entry point.
> 1. Set _packagePath_ to _"./"_ concatenated with _packagePath_.
> 1. If _packagePath_ is a key of _exports_, then
> 1. Let _target_ be the value of _exports[packagePath]_.
> 1. If _target_ is not a String, continue the loop.
> 1. Return the URL resolution of the concatenation of _packageURL_ and
> _target_.
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
> _""_).
> 1. Let _directoryKeys_ be the list of keys of _exports_ ending in
> _"/"_, sorted by length descending.
> 1. For each key _directory_ in _directoryKeys_, do
> 1. If _packagePath_ starts with _directory_, then
> 1. Let _target_ be the value of _exports[directory]_.
> 1. If _target_ is not a String, continue the loop.
> 1. Let _subpath_ be the substring of _target_ starting at the index
> of the length of _directory_.
> 1. Return the URL resolution of the concatenation of _packageURL_,
> _target_ and _subpath_.
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_,
> _subpath_).
> 1. Throw a _Module Not Found_ error.
**PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _target_, _subpath_)
> 1. If _target_ is a String, then
> 1. If _target_ does not start with _"./"_, throw a _Module Not Found_
> error.
> 1. If _subpath_ has non-zero length and _target_ does not end with _"/"_,
> throw a _Module Not Found_ error.
> 1. If _target_ or _subpath_ contain any _"node_modules"_ segments including
> _"node_modules"_ percent-encoding, throw a _Module Not Found_ error.
> 1. Let _resolvedTarget_ be the URL resolution of the concatenation of
> _packageURL_ and _target_.
> 1. If _resolvedTarget_ is contained in _packageURL_, then
> 1. Let _resolved_ be the URL resolution of the concatenation of
> _subpath_ and _resolvedTarget_.
> 1. If _resolved_ is contained in _resolvedTarget_, then
> 1. Return _resolved_.
> 1. Otherwise, if _target_ is an Array, then
> 1. For each item _targetValue_ in _target_, do
> 1. If _targetValue_ is not a String, continue the loop.
> 1. Let _resolved_ be the result of
> **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_, _targetValue_,
> _subpath_), continuing the loop on abrupt completion.
> 1. Assert: _resolved_ is a String.
> 1. Return _resolved_.
> 1. Throw a _Module Not Found_ error.
**ESM_FORMAT**(_url_, _isMain_)
@ -790,6 +830,7 @@ _isMain_ is **true** when resolving the Node.js application entry point.
**READ_PACKAGE_SCOPE**(_url_)
> 1. Let _scopeURL_ be _url_.
> 1. While _scopeURL_ is not the file system root,
> 1. If _scopeURL_ ends in a _"node_modules"_ path segment, return **null**.
> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_scopeURL_).
> 1. If _pjson_ is not **null**, then
> 1. Return _pjson_.

View File

@ -202,11 +202,12 @@ NODE_MODULES_PATHS(START)
5. return DIRS
```
If `--experimental-exports` is enabled,
node allows packages loaded via `LOAD_NODE_MODULES` to explicitly declare
which filepaths to expose and how they should be interpreted.
This expands on the control packages already had using the `main` field.
With this feature enabled, the `LOAD_NODE_MODULES` changes as follows:
If `--experimental-exports` is enabled, Node.js allows packages loaded via
`LOAD_NODE_MODULES` to explicitly declare which file paths to expose and how
they should be interpreted. This expands on the control packages already had
using the `main` field.
With this feature enabled, the `LOAD_NODE_MODULES` changes are:
```txt
LOAD_NODE_MODULES(X, START)
@ -224,10 +225,10 @@ RESOLVE_BARE_SPECIFIER(DIR, X)
b. If "exports" is null or undefined, GOTO 3.
c. Find the longest key in "exports" that the subpath starts with.
d. If no such key can be found, throw "not found".
e. If the key matches the subpath entirely, return DIR/name/${exports[key]}.
f. If either the key or exports[key] do not end with a slash (`/`),
throw "not found".
g. Return DIR/name/${exports[key]}${subpath.slice(key.length)}.
e. let RESOLVED_URL =
PACKAGE_EXPORTS_TARGET_RESOLVE(pathToFileURL(DIR/name), exports[key],
subpath.slice(key.length)), as defined in the esm resolver.
f. return fileURLToPath(RESOLVED_URL)
3. return DIR/X
```

View File

@ -24,6 +24,7 @@
const {
JSON,
Object,
ObjectPrototype,
Reflect,
SafeMap,
StringPrototype,
@ -348,18 +349,18 @@ function resolveExports(nmPath, request, absoluteRequest) {
const basePath = path.resolve(nmPath, name);
const pkgExports = readExports(basePath);
const mappingKey = `.${expansion}`;
if (pkgExports != null) {
const mappingKey = `.${expansion}`;
const mapping = pkgExports[mappingKey];
if (typeof mapping === 'string') {
return fileURLToPath(new URL(mapping, `${pathToFileURL(basePath)}/`));
if (typeof pkgExports === 'object' && pkgExports !== null) {
if (ObjectPrototype.hasOwnProperty(pkgExports, mappingKey)) {
const mapping = pkgExports[mappingKey];
return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping, '',
basePath, mappingKey);
}
let dirMatch = '';
for (const [candidateKey, candidateValue] of Object.entries(pkgExports)) {
for (const candidateKey of Object.keys(pkgExports)) {
if (candidateKey[candidateKey.length - 1] !== '/') continue;
if (candidateValue[candidateValue.length - 1] !== '/') continue;
if (candidateKey.length > dirMatch.length &&
StringPrototype.startsWith(mappingKey, candidateKey)) {
dirMatch = candidateKey;
@ -367,15 +368,13 @@ function resolveExports(nmPath, request, absoluteRequest) {
}
if (dirMatch !== '') {
const dirMapping = pkgExports[dirMatch];
const remainder = StringPrototype.slice(mappingKey, dirMatch.length);
const expectedPrefix =
new URL(dirMapping, `${pathToFileURL(basePath)}/`);
const resolved = new URL(remainder, expectedPrefix).href;
if (StringPrototype.startsWith(resolved, expectedPrefix.href)) {
return fileURLToPath(resolved);
}
const mapping = pkgExports[dirMatch];
const subpath = StringPrototype.slice(mappingKey, dirMatch.length);
return resolveExportsTarget(pathToFileURL(basePath + '/'), mapping,
subpath, basePath, mappingKey);
}
}
if (pkgExports != null) {
// eslint-disable-next-line no-restricted-syntax
const e = new Error(`Package exports for '${basePath}' do not define ` +
`a '${mappingKey}' subpath`);
@ -387,6 +386,43 @@ function resolveExports(nmPath, request, absoluteRequest) {
return path.resolve(nmPath, request);
}
function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) {
if (typeof target === 'string') {
if (target.startsWith('./') &&
(subpath.length === 0 || target.endsWith('/'))) {
const resolvedTarget = new URL(target, pkgPath);
const pkgPathPath = pkgPath.pathname;
const resolvedTargetPath = resolvedTarget.pathname;
if (StringPrototype.startsWith(resolvedTargetPath, pkgPathPath) &&
StringPrototype.indexOf(resolvedTargetPath, '/node_modules/',
pkgPathPath.length - 1) === -1) {
const resolved = new URL(subpath, resolvedTarget);
const resolvedPath = resolved.pathname;
if (StringPrototype.startsWith(resolvedPath, resolvedTargetPath) &&
StringPrototype.indexOf(resolvedPath, '/node_modules/',
pkgPathPath.length - 1) === -1) {
return fileURLToPath(resolved);
}
}
}
} else if (Array.isArray(target)) {
for (const targetValue of target) {
if (typeof targetValue !== 'string') continue;
try {
return resolveExportsTarget(pkgPath, targetValue, subpath, basePath,
mappingKey);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') throw e;
}
}
}
// eslint-disable-next-line no-restricted-syntax
const e = new Error(`Package exports for '${basePath}' do not define a ` +
`valid '${mappingKey}' target${subpath ? 'for ' + subpath : ''}`);
e.code = 'MODULE_NOT_FOUND';
throw e;
}
Module._findPath = function(request, paths, isMain) {
const absoluteRequest = path.isAbsolute(request);
if (absoluteRequest) {

View File

@ -545,8 +545,8 @@ Maybe<const PackageConfig*> GetPackageConfig(Environment* env,
if (existing != env->package_json_cache.end()) {
const PackageConfig* pcfg = &existing->second;
if (pcfg->is_valid == IsValid::No) {
std::string msg = "Invalid JSON in '" + path +
"' imported from " + base.ToFilePath();
std::string msg = "Invalid JSON in " + path +
" imported from " + base.ToFilePath();
node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str());
return Nothing<const PackageConfig*>();
}
@ -579,8 +579,8 @@ Maybe<const PackageConfig*> GetPackageConfig(Environment* env,
env->package_json_cache.emplace(path,
PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "",
PackageType::None, Global<Value>() });
std::string msg = "Invalid JSON in '" + path +
"' imported from " + base.ToFilePath();
std::string msg = "Invalid JSON in " + path +
" imported from " + base.ToFilePath();
node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str());
return Nothing<const PackageConfig*>();
}
@ -633,6 +633,12 @@ Maybe<const PackageConfig*> GetPackageScopeConfig(Environment* env,
const URL& base) {
URL pjson_url("./package.json", &resolved);
while (true) {
std::string pjson_url_path = pjson_url.path();
if (pjson_url_path.length() > 25 &&
pjson_url_path.substr(pjson_url_path.length() - 25, 25) ==
"node_modules/package.json") {
break;
}
Maybe<const PackageConfig*> pkg_cfg =
GetPackageConfig(env, pjson_url.ToFilePath(), base);
if (pkg_cfg.IsNothing()) return pkg_cfg;
@ -643,14 +649,13 @@ Maybe<const PackageConfig*> GetPackageScopeConfig(Environment* env,
// Terminates at root where ../package.json equals ../../package.json
// (can't just check "/package.json" for Windows support).
if (pjson_url.path() == last_pjson_url.path()) {
auto entry = env->package_json_cache.emplace(pjson_url.ToFilePath(),
PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "",
PackageType::None, Global<Value>() });
const PackageConfig* pcfg = &entry.first->second;
return Just(pcfg);
}
if (pjson_url.path() == last_pjson_url.path()) break;
}
auto entry = env->package_json_cache.emplace(pjson_url.ToFilePath(),
PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "",
PackageType::None, Global<Value>() });
const PackageConfig* pcfg = &entry.first->second;
return Just(pcfg);
}
/*
@ -750,16 +755,17 @@ Maybe<URL> FinalizeResolution(Environment* env,
if (!file.IsNothing()) {
return file;
}
std::string msg = "Cannot find module '" + resolved.path() +
"' imported from " + base.ToFilePath();
std::string msg = "Cannot find module " + resolved.path() +
" imported from " + base.ToFilePath();
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
return Nothing<URL>();
}
const std::string& path = resolved.ToFilePath();
if (CheckDescriptorAtPath(path) != FILE) {
std::string msg = "Cannot find module '" + path +
"' imported from " + base.ToFilePath();
std::string msg = "Cannot find module " +
(path.length() != 0 ? path : resolved.path()) +
" imported from " + base.ToFilePath();
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
return Nothing<URL>();
}
@ -793,13 +799,94 @@ Maybe<URL> PackageMainResolve(Environment* env,
}
}
}
std::string msg = "Cannot find main entry point for '" +
URL(".", pjson_url).ToFilePath() + "' imported from " +
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,
const std::string& subpath,
const URL& pjson_url,
const URL& base) {
const std::string msg = "Package exports for " +
pjson_url.ToFilePath() + " do not define a '" + subpath +
"' subpath, imported from " + base.ToFilePath();
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
}
void ThrowExportsInvalid(Environment* env,
const std::string& subpath,
const std::string& target,
const URL& pjson_url,
const URL& base) {
const std::string msg = "Cannot resolve package exports target '" + target +
"' matched for '" + subpath + "' in " + pjson_url.ToFilePath() +
", imported from " + base.ToFilePath();
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
}
void ThrowExportsInvalid(Environment* env,
const std::string& subpath,
Local<Value> target,
const URL& pjson_url,
const URL& base) {
Local<String> target_string;
if (target->ToString(env->context()).ToLocal(&target_string)) {
Utf8Value target_utf8(env->isolate(), target_string);
std::string target_str(*target_utf8, target_utf8.length());
if (target->IsArray()) {
target_str = '[' + target_str + ']';
}
ThrowExportsInvalid(env, subpath, target_str, pjson_url, base);
}
}
Maybe<URL> ResolveExportsTarget(Environment* env,
const std::string& target,
const std::string& subpath,
const std::string& match,
const URL& pjson_url,
const URL& base,
bool throw_invalid = true) {
if (target.substr(0, 2) != "./") {
if (throw_invalid) {
ThrowExportsInvalid(env, match, target, pjson_url, base);
}
return Nothing<URL>();
}
if (subpath.length() > 0 && target.back() != '/') {
if (throw_invalid) {
ThrowExportsInvalid(env, match, target, pjson_url, base);
}
return Nothing<URL>();
}
URL resolved(target, pjson_url);
std::string resolved_path = resolved.path();
std::string pkg_path = URL(".", pjson_url).path();
if (resolved_path.find(pkg_path) != 0 ||
resolved_path.find("/node_modules/", pkg_path.length() - 1) !=
std::string::npos) {
if (throw_invalid) {
ThrowExportsInvalid(env, match, target, pjson_url, base);
}
return Nothing<URL>();
}
if (subpath.length() == 0) return Just(resolved);
URL subpath_resolved(subpath, resolved);
std::string subpath_resolved_path = subpath_resolved.path();
if (subpath_resolved_path.find(resolved_path) != 0 ||
subpath_resolved_path.find("/node_modules/", pkg_path.length() - 1)
!= std::string::npos) {
if (throw_invalid) {
ThrowExportsInvalid(env, match, target + subpath, pjson_url, base);
}
return Nothing<URL>();
}
return Just(subpath_resolved);
}
Maybe<URL> PackageExportsResolve(Environment* env,
const URL& pjson_url,
const std::string& pkg_subpath,
@ -809,57 +896,126 @@ Maybe<URL> PackageExportsResolve(Environment* env,
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
Local<Value> exports = pcfg.exports.Get(isolate);
if (exports->IsObject()) {
Local<Object> exports_obj = exports.As<Object>();
Local<String> subpath = String::NewFromUtf8(isolate,
pkg_subpath.c_str(), v8::NewStringType::kNormal).ToLocalChecked();
if (!exports->IsObject()) {
ThrowExportsNotFound(env, pkg_subpath, pjson_url, base);
return Nothing<URL>();
}
Local<Object> exports_obj = exports.As<Object>();
Local<String> subpath = String::NewFromUtf8(isolate,
pkg_subpath.c_str(), v8::NewStringType::kNormal).ToLocalChecked();
auto target = exports_obj->Get(context, subpath).ToLocalChecked();
if (exports_obj->HasOwnProperty(context, subpath).FromJust()) {
Local<Value> target = exports_obj->Get(context, subpath).ToLocalChecked();
if (target->IsString()) {
Utf8Value target_utf8(isolate, target.As<v8::String>());
std::string target_str(*target_utf8, target_utf8.length());
Maybe<URL> resolved = ResolveExportsTarget(env, target_str, "",
pkg_subpath, pjson_url, base);
if (resolved.IsNothing()) {
ThrowExportsInvalid(env, pkg_subpath, 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, pkg_subpath, 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(*target_utf8, target_utf8.length());
Maybe<URL> resolved = ResolveExportsTarget(env, target, "",
pkg_subpath, 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, pkg_subpath, 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, "",
pkg_subpath, pjson_url, base);
CHECK(resolved.IsNothing());
return Nothing<URL>();
} else {
ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
return Nothing<URL>();
}
}
Local<String> best_match;
std::string best_match_str = "";
Local<Array> keys =
exports_obj->GetOwnPropertyNames(context).ToLocalChecked();
for (uint32_t i = 0; i < keys->Length(); ++i) {
Local<String> key = keys->Get(context, i).ToLocalChecked().As<String>();
Utf8Value key_utf8(isolate, key);
std::string key_str(*key_utf8, key_utf8.length());
if (key_str.back() != '/') continue;
if (pkg_subpath.substr(0, key_str.length()) == key_str &&
key_str.length() > best_match_str.length()) {
best_match = key;
best_match_str = key_str;
}
}
if (best_match_str.length() > 0) {
auto target = exports_obj->Get(context, best_match).ToLocalChecked();
std::string subpath = pkg_subpath.substr(best_match_str.length());
if (target->IsString()) {
Utf8Value target_utf8(isolate, target.As<v8::String>());
std::string target(*target_utf8, target_utf8.length());
if (target.substr(0, 2) == "./") {
URL target_url(target, pjson_url);
return FinalizeResolution(env, target_url, base);
Maybe<URL> resolved = ResolveExportsTarget(env, target, subpath,
pkg_subpath, pjson_url, base);
if (resolved.IsNothing()) {
ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
return Nothing<URL>();
}
}
Local<String> best_match;
std::string best_match_str = "";
Local<Array> keys =
exports_obj->GetOwnPropertyNames(context).ToLocalChecked();
for (uint32_t i = 0; i < keys->Length(); ++i) {
Local<String> key = keys->Get(context, i).ToLocalChecked().As<String>();
Utf8Value key_utf8(isolate, key);
std::string key_str(*key_utf8, key_utf8.length());
if (key_str.back() != '/') continue;
if (pkg_subpath.substr(0, key_str.length()) == key_str &&
key_str.length() > best_match_str.length()) {
best_match = key;
best_match_str = key_str;
return FinalizeResolution(env, URL(subpath, 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, pkg_subpath, target, pjson_url, base);
return Nothing<URL>();
}
}
if (best_match_str.length() > 0) {
auto target = exports_obj->Get(context, best_match).ToLocalChecked();
if (target->IsString()) {
Utf8Value target_utf8(isolate, target.As<v8::String>());
std::string target(*target_utf8, target_utf8.length());
if (target.back() == '/' && target.substr(0, 2) == "./") {
std::string subpath = pkg_subpath.substr(best_match_str.length());
URL url_prefix(target, pjson_url);
URL target_url(subpath, url_prefix);
if (target_url.path().find(url_prefix.path()) == 0) {
return FinalizeResolution(env, target_url, base);
}
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, subpath,
pkg_subpath, 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, pkg_subpath, 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, subpath,
pkg_subpath, pjson_url, base);
CHECK(resolved.IsNothing());
return Nothing<URL>();
} else {
ThrowExportsInvalid(env, pkg_subpath, target, pjson_url, base);
return Nothing<URL>();
}
}
std::string msg = "Package exports for '" +
URL(".", pjson_url).ToFilePath() + "' do not define a '" + pkg_subpath +
"' subpath, imported from " + base.ToFilePath();
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
ThrowExportsNotFound(env, pkg_subpath, pjson_url, base);
return Nothing<URL>();
}

View File

@ -17,6 +17,9 @@ import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
['pkgexports/space', { default: 'encoded path' }],
// Verifying that normal packages still work with exports turned on.
isRequire ? ['baz/index', { default: 'eye catcher' }] : [null],
// Fallbacks
['pkgexports/fallbackdir/asdf.js', { default: 'asdf' }],
['pkgexports/fallbackfile', { default: 'asdf' }],
]);
for (const [validSpecifier, expected] of validSpecifiers) {
if (validSpecifier === null) continue;
@ -27,20 +30,56 @@ import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
}));
}
// There's no such export - so there's nothing to do.
loadFixture('pkgexports/missing').catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
assertStartsWith(err.message, 'Package exports');
assertIncludes(err.message, 'do not define a \'./missing\' subpath');
}));
const undefinedExports = new Map([
// There's no such export - so there's nothing to do.
['pkgexports/missing', './missing'],
// The file exists but isn't exported. The exports is a number which counts
// as a non-null value without any properties, just like `{}`.
['pkgexports-number/hidden.js', './hidden.js'],
]);
// The file exists but isn't exported. The exports is a number which counts
// as a non-null value without any properties, just like `{}`.
loadFixture('pkgexports-number/hidden.js').catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
assertStartsWith(err.message, 'Package exports');
assertIncludes(err.message, 'do not define a \'./hidden.js\' subpath');
}));
const invalidExports = new Map([
// Even though 'pkgexports/sub/asdf.js' works, alternate "path-like"
// variants do not to prevent confusion and accidental loopholes.
['pkgexports/sub/./../asdf.js', './sub/./../asdf.js'],
// This path steps back inside the package but goes through an exports
// target that escapes the package, so we still catch that as invalid
['pkgexports/belowdir/pkgexports/asdf.js', './belowdir/pkgexports/asdf.js'],
// This target file steps below the package
['pkgexports/belowfile', './belowfile'],
// Directory mappings require a trailing / to work
['pkgexports/missingtrailer/x', './missingtrailer/x'],
// Invalid target handling
['pkgexports/null', './null'],
['pkgexports/invalid1', './invalid1'],
['pkgexports/invalid2', './invalid2'],
['pkgexports/invalid3', './invalid3'],
['pkgexports/invalid4', './invalid4'],
// Missing / invalid fallbacks
['pkgexports/nofallback1', './nofallback1'],
['pkgexports/nofallback2', './nofallback2'],
// Reaching into nested node_modules
['pkgexports/nodemodules', './nodemodules'],
]);
for (const [specifier, subpath] of undefinedExports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
assertStartsWith(err.message, 'Package exports');
assertIncludes(err.message, `do not define a '${subpath}' subpath`);
}));
}
for (const [specifier, subpath] of invalidExports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
assertStartsWith(err.message, (isRequire ? 'Package exports' :
'Cannot resolve'));
assertIncludes(err.message, isRequire ?
`do not define a valid '${subpath}' subpath` :
`matched for '${subpath}'`);
}));
}
// 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.
@ -54,26 +93,19 @@ import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
}
}));
// Even though 'pkgexports/sub/asdf.js' works, alternate "path-like" variants
// do not to prevent confusion and accidental loopholes.
loadFixture('pkgexports/sub/./../asdf.js').catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
assertStartsWith(err.message, 'Package exports');
assertIncludes(err.message,
'do not define a \'./sub/./../asdf.js\' subpath');
}));
// Covering out bases - not a file is still not a file after dir mapping.
loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => {
if (isRequire) {
strictEqual(err.code, 'MODULE_NOT_FOUND');
assertStartsWith(err.message,
'Cannot find module \'pkgexports/sub/not-a-file.js\'');
} else {
strictEqual(err.code, 'ERR_MODULE_NOT_FOUND');
// ESM currently returns a full file path
assertStartsWith(err.message, 'Cannot find module');
}
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
// ESM returns a full file path
assertStartsWith(err.message, isRequire ?
'Cannot find module \'pkgexports/sub/not-a-file.js\'' :
'Cannot find module');
}));
// THe use of %2F escapes in paths fails loading
loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => {
strictEqual(err.code, isRequire ? 'ERR_INVALID_FILE_URL_PATH' :
'ERR_MODULE_NOT_FOUND');
}));
});

View File

@ -0,0 +1,10 @@
// Flags: --experimental-modules
import '../common/index.mjs';
import cjs from '../fixtures/baz.js';
import { message } from '../fixtures/es-modules/message.mjs';
import assert from 'assert';
// Assert we loaded esm dependency as ".js" in this mode
assert.strictEqual(message, 'A message');
// Assert we loaded CommonJS dependency
assert.strictEqual(cjs, 'perhaps I work');

View File

@ -1,3 +1,4 @@
import 'dep/dep.js';
const identifier = 'package-type-module';
console.log(identifier);
export default identifier;

View File

@ -0,0 +1,2 @@
// No package.json -> should still be CommonJS as it is in node_modules
module.exports = 42;

View File

@ -0,0 +1 @@
module.exports = 42;

View File

@ -3,6 +3,19 @@
".": "./asdf.js",
"./space": "./sp%20ce.js",
"./valid-cjs": "./asdf.js",
"./sub/": "./"
"./sub/": "./",
"./belowdir/": "../belowdir/",
"./belowfile": "../belowfile",
"./missingtrailer/": ".",
"./null": null,
"./invalid1": {},
"./invalid2": 1234,
"./invalid3": "",
"./invalid4": {},
"./fallbackdir/": [[], null, {}, "builtin:x/", "./fallbackfile", "./"],
"./fallbackfile": [[], null, {}, "builtin:x", "./asdf.js"],
"./nofallback1": [],
"./nofallback2": [null, {}, "builtin:x"],
"./nodemodules": "./node_modules/internalpkg/x.js"
}
}