GeezerVison is showing github.com — module: logical conditional exports ordering · nodejs/node@f62fb76
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/esm.md +27-21Lines changed: 27 additions & 21 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -343,33 +343,36 @@ Node.js and the browser can be written: | 343 | 343 | When resolving the `"."` export, if no matching target is found, the `"main"` | 344 | 344 | will be used as the final fallback. | 345 | 345 |
| 346 | | -The conditions supported in Node.js are matched in the following order: | | 346 | +The conditions supported in Node.js condition matching: | 347 | 347 |
| 348 | | -1. `"node"` - matched for any Node.js environment. Can be a CommonJS or ES | | 348 | +* `"default"` - the generic fallback that will always match. Can be a CommonJS | | 349 | + or ES module file. | | 350 | +* `"import"` - matched when the package is loaded via `import` or | | 351 | + `import()`. Can be any module format, this field does not set the type | | 352 | + interpretation. _This is currently only supported behind the | | 353 | + `--experimental-conditional-exports` flag._ | | 354 | +* `"node"` - matched for any Node.js environment. Can be a CommonJS or ES | 349 | 355 | module file. _This is currently only supported behind the | 350 | 356 | `--experimental-conditional-exports` flag._ | 351 | | -2. `"require"` - matched when the package is loaded via `require()`. | | 357 | +* `"require"` - matched when the package is loaded via `require()`. | 352 | 358 | _This is currently only supported behind the | 353 | 359 | `--experimental-conditional-exports` flag._ | 354 | | -3. `"import"` - matched when the package is loaded via `import` or | 355 | | - `import()`. Can be any module format, this field does not set the type | 356 | | - interpretation. _This is currently only supported behind the | 357 | | - `--experimental-conditional-exports` flag._ | 358 | | -4. `"default"` - the generic fallback that will always match if no other | 359 | | - more specific condition is matched first. Can be a CommonJS or ES module | 360 | | - file. | 361 | 360 |
| 362 | | -> Setting any of the above flagged conditions for a published package is not | 363 | | -> recommended until they are unflagged to avoid breaking changes to packages in | 364 | | -> future. | | 361 | +Condition matching is applied in object order from first to last within the | | 362 | +`"exports"` object. | | 363 | + | | 364 | +> Setting the above conditions for a published package is not recommended until | | 365 | +> conditional exports have been unflagged to avoid breaking changes to packages. | 365 | 366 |
| 366 | 367 | Using the `"require"` condition it is possible to define a package that will | 367 | 368 | have a different exported value for CommonJS and ES modules, which can be a | 368 | 369 | hazard in that it can result in having two separate instances of the same | 369 | 370 | package in use in an application, which can cause a number of bugs. | 370 | 371 |
| 371 | 372 | Other conditions such as `"browser"`, `"electron"`, `"deno"`, `"react-native"`, | 372 | | -etc. could be defined in other runtimes or tools. | | 373 | +etc. could be defined in other runtimes or tools. Condition names must not start | | 374 | +with `"."` or be numbers. Further restrictions, definitions or guidance on | | 375 | +condition names may be provided in future. | 373 | 376 |
| 374 | 377 | #### Exports Sugar | 375 | 378 |
| @@ -1547,13 +1550,15 @@ _defaultEnv_ is the conditional environment name priority array, | 1547 | 1550 | > 1. If _resolved_ is contained in _resolvedTarget_, then | 1548 | 1551 | > 1. Return _resolved_. | 1549 | 1552 | > 1. Otherwise, if _target_ is a non-null Object, then | 1550 | | -> 1. If _target_ has an object key matching one of the names in _env_, then | 1551 | | -> 1. Let _targetValue_ be the corresponding value of the first object key | 1552 | | -> of _target_ in _env_. | 1553 | | -> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_TARGET_RESOLVE** | 1554 | | -> (_packageURL_, _targetValue_, _subpath_, _env_). | 1555 | | -> 1. Assert: _resolved_ is a String. | 1556 | | -> 1. Return _resolved_. | | 1553 | +> 1. If _exports_ contains any index property keys, as defined in ECMA-262 | | 1554 | +> [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error. | | 1555 | +> 1. For each property _p_ of _target_, in object insertion order as, | | 1556 | +> 1. If _env_ contains an entry for _p_, then | | 1557 | +> 1. Let _targetValue_ be the value of the _p_ property in _target_. | | 1558 | +> 1. Let _resolved_ be the result of **PACKAGE_EXPORTS_TARGET_RESOLVE** | | 1559 | +> (_packageURL_, _targetValue_, _subpath_, _env_). | | 1560 | +> 1. Assert: _resolved_ is a String. | | 1561 | +> 1. Return _resolved_. | 1557 | 1562 | > 1. Otherwise, if _target_ is an Array, then | 1558 | 1563 | > 1. For each item _targetValue_ in _target_, do | 1559 | 1564 | > 1. If _targetValue_ is an Array, continue the loop. | @@ -1649,3 +1654,4 @@ success! | 1649 | 1654 | [special scheme]: https://url.spec.whatwg.org/#special-scheme | 1650 | 1655 | [the official standard format]: https://tc39.github.io/ecma262/#sec-modules | 1651 | 1656 | [transpiler loader example]: #esm_transpiler_loader | | 1657 | +[6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/cjs/loader.js +41-27Lines changed: 41 additions & 27 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -26,13 +26,16 @@ const { | 26 | 26 | Error, | 27 | 27 | JSONParse, | 28 | 28 | Map, | | 29 | + Number, | 29 | 30 | ObjectCreate, | 30 | 31 | ObjectDefineProperty, | 31 | 32 | ObjectFreeze, | | 33 | + ObjectIs, | 32 | 34 | ObjectKeys, | 33 | 35 | ObjectPrototypeHasOwnProperty, | 34 | 36 | ReflectSet, | 35 | 37 | SafeMap, | | 38 | + String, | 36 | 39 | StringPrototypeIndexOf, | 37 | 40 | StringPrototypeMatch, | 38 | 41 | StringPrototypeSlice, | @@ -554,6 +557,18 @@ function resolveExports(nmPath, request, absoluteRequest) { | 554 | 557 | return path.resolve(nmPath, request); | 555 | 558 | } | 556 | 559 |
| | 560 | +function isArrayIndex(p) { | | 561 | + assert(typeof p === 'string'); | | 562 | + const n = Number(p); | | 563 | + if (String(n) !== p) | | 564 | + return false; | | 565 | + if (ObjectIs(n, +0)) | | 566 | + return true; | | 567 | + if (!Number.isInteger(n)) | | 568 | + return false; | | 569 | + return n >= 0 && n < (2 ** 32) - 1; | | 570 | +} | | 571 | + | 557 | 572 | function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) { | 558 | 573 | if (typeof target === 'string') { | 559 | 574 | if (target.startsWith('./') && | @@ -584,34 +599,33 @@ function resolveExportsTarget(pkgPath, target, subpath, basePath, mappingKey) { | 584 | 599 | } | 585 | 600 | } | 586 | 601 | } else if (typeof target === 'object' && target !== null) { | 587 | | - if (experimentalConditionalExports && | 588 | | - ObjectPrototypeHasOwnProperty(target, 'node')) { | 589 | | - try { | 590 | | - const result = resolveExportsTarget(pkgPath, target.node, subpath, | 591 | | - basePath, mappingKey); | 592 | | - emitExperimentalWarning('Conditional exports'); | 593 | | - return result; | 594 | | - } catch (e) { | 595 | | - if (e.code !== 'MODULE_NOT_FOUND') throw e; | 596 | | - } | 597 | | - } | 598 | | - if (experimentalConditionalExports && | 599 | | - ObjectPrototypeHasOwnProperty(target, 'require')) { | 600 | | - try { | 601 | | - const result = resolveExportsTarget(pkgPath, target.require, subpath, | 602 | | - basePath, mappingKey); | 603 | | - emitExperimentalWarning('Conditional exports'); | 604 | | - return result; | 605 | | - } catch (e) { | 606 | | - if (e.code !== 'MODULE_NOT_FOUND') throw e; | 607 | | - } | | 602 | + const keys = ObjectKeys(target); | | 603 | + if (keys.some(isArrayIndex)) { | | 604 | + throw new ERR_INVALID_PACKAGE_CONFIG(basePath, '"exports" cannot ' + | | 605 | + 'contain numeric property keys.'); | 608 | 606 | } | 609 | | - if (ObjectPrototypeHasOwnProperty(target, 'default')) { | 610 | | - try { | 611 | | - return resolveExportsTarget(pkgPath, target.default, subpath, | 612 | | - basePath, mappingKey); | 613 | | - } catch (e) { | 614 | | - if (e.code !== 'MODULE_NOT_FOUND') throw e; | | 607 | + for (const p of keys) { | | 608 | + switch (p) { | | 609 | + case 'node': | | 610 | + case 'require': | | 611 | + if (!experimentalConditionalExports) | | 612 | + continue; | | 613 | + try { | | 614 | + emitExperimentalWarning('Conditional exports'); | | 615 | + const result = resolveExportsTarget(pkgPath, target[p], subpath, | | 616 | + basePath, mappingKey); | | 617 | + return result; | | 618 | + } catch (e) { | | 619 | + if (e.code !== 'MODULE_NOT_FOUND') throw e; | | 620 | + } | | 621 | + break; | | 622 | + case 'default': | | 623 | + try { | | 624 | + return resolveExportsTarget(pkgPath, target.default, subpath, | | 625 | + basePath, mappingKey); | | 626 | + } catch (e) { | | 627 | + if (e.code !== 'MODULE_NOT_FOUND') throw e; | | 628 | + } | 615 | 629 | } | 616 | 630 | } | 617 | 631 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/env.h -2Lines changed: 0 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -202,7 +202,6 @@ constexpr size_t kFsStatsBufferLength = | 202 | 202 | V(crypto_rsa_pss_string, "rsa-pss") \ | 203 | 203 | V(cwd_string, "cwd") \ | 204 | 204 | V(data_string, "data") \ | 205 | | - V(default_string, "default") \ | 206 | 205 | V(dest_string, "dest") \ | 207 | 206 | V(destroyed_string, "destroyed") \ | 208 | 207 | V(detached_string, "detached") \ | @@ -257,7 +256,6 @@ constexpr size_t kFsStatsBufferLength = | 257 | 256 | V(http_1_1_string, "http/1.1") \ | 258 | 257 | V(identity_string, "identity") \ | 259 | 258 | V(ignore_string, "ignore") \ | 260 | | - V(import_string, "import") \ | 261 | 259 | V(infoaccess_string, "infoAccess") \ | 262 | 260 | V(inherit_string, "inherit") \ | 263 | 261 | V(input_string, "input") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.cc +59-35Lines changed: 59 additions & 35 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 12 | 12 | #include <sys/stat.h> // S_IFDIR | 13 | 13 |
| 14 | 14 | #include <algorithm> | 15 | | -#include <climits> // PATH_MAX | 16 | 15 |
| 17 | 16 | namespace node { | 18 | 17 | namespace loader { | @@ -908,6 +907,25 @@ Maybe<URL> ResolveExportsTargetString(Environment* env, | 908 | 907 | return Just(subpath_resolved); | 909 | 908 | } | 910 | 909 |
| | 910 | +bool IsArrayIndex(Environment* env, Local<Value> p) { | | 911 | + Local<Context> context = env->context(); | | 912 | + Local<String> p_str = p->ToString(context).ToLocalChecked(); | | 913 | + double n_dbl = static_cast<double>(p_str->NumberValue(context).FromJust()); | | 914 | + Local<Number> n = Number::New(env->isolate(), n_dbl); | | 915 | + Local<String> cmp_str = n->ToString(context).ToLocalChecked(); | | 916 | + if (!p_str->Equals(context, cmp_str).FromJust()) { | | 917 | + return false; | | 918 | + } | | 919 | + if (n_dbl == 0 && std::signbit(n_dbl) == false) { | | 920 | + return true; | | 921 | + } | | 922 | + Local<Integer> cmp_integer; | | 923 | + if (!n->ToInteger(context).ToLocal(&cmp_integer)) { | | 924 | + return false; | | 925 | + } | | 926 | + return n_dbl > 0 && n_dbl < (2 ^ 32) - 1; | | 927 | +} | | 928 | + | 911 | 929 | Maybe<URL> ResolveExportsTarget(Environment* env, | 912 | 930 | const URL& pjson_url, | 913 | 931 | Local<Value> target, | @@ -953,44 +971,50 @@ Maybe<URL> ResolveExportsTarget(Environment* env, | 953 | 971 | return Nothing<URL>(); | 954 | 972 | } else if (target->IsObject()) { | 955 | 973 | Local<Object> target_obj = target.As<Object>(); | 956 | | - bool matched = false; | | 974 | + Local<Array> target_obj_keys = | | 975 | + target_obj->GetOwnPropertyNames(context).ToLocalChecked(); | 957 | 976 | Local<Value> conditionalTarget; | 958 | | - if (env->options()->experimental_conditional_exports && | 959 | | - target_obj->HasOwnProperty(context, env->node_string()).FromJust()) { | 960 | | - matched = true; | 961 | | - conditionalTarget = | 962 | | - target_obj->Get(context, env->node_string()).ToLocalChecked(); | 963 | | - Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, | 964 | | - conditionalTarget, subpath, pkg_subpath, base, false); | 965 | | - if (!resolved.IsNothing()) { | 966 | | - ProcessEmitExperimentalWarning(env, "Conditional exports"); | 967 | | - return resolved; | | 977 | + bool matched = false; | | 978 | + for (uint32_t i = 0; i < target_obj_keys->Length(); ++i) { | | 979 | + Local<Value> key = | | 980 | + target_obj_keys->Get(context, i).ToLocalChecked(); | | 981 | + if (IsArrayIndex(env, key)) { | | 982 | + const std::string msg = "Invalid package config for " + | | 983 | + pjson_url.ToFilePath() + ", \"exports\" cannot contain numeric " + | | 984 | + "property keys."; | | 985 | + node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); | | 986 | + return Nothing<URL>(); | 968 | 987 | } | 969 | 988 | } | 970 | | - if (env->options()->experimental_conditional_exports && | 971 | | - target_obj->HasOwnProperty(context, env->import_string()).FromJust()) { | 972 | | - matched = true; | 973 | | - conditionalTarget = | 974 | | - target_obj->Get(context, env->import_string()).ToLocalChecked(); | 975 | | - Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, | | 989 | + for (uint32_t i = 0; i < target_obj_keys->Length(); ++i) { | | 990 | + Local<Value> key = target_obj_keys->Get(context, i).ToLocalChecked(); | | 991 | + Utf8Value key_utf8(env->isolate(), | | 992 | + key->ToString(context).ToLocalChecked()); | | 993 | + std::string key_str(*key_utf8, key_utf8.length()); | | 994 | + if (key_str == "node" || key_str == "import") { | | 995 | + if (!env->options()->experimental_conditional_exports) continue; | | 996 | + matched = true; | | 997 | + conditionalTarget = target_obj->Get(context, key).ToLocalChecked(); | | 998 | + Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, | 976 | 999 | conditionalTarget, subpath, pkg_subpath, base, false); | 977 | | - if (!resolved.IsNothing()) { | 978 | | - return resolved; | 979 | | - } | 980 | | - } | 981 | | - if (target_obj->HasOwnProperty(context, env->default_string()).FromJust()) { | 982 | | - matched = true; | 983 | | - conditionalTarget = | 984 | | - target_obj->Get(context, env->default_string()).ToLocalChecked(); | 985 | | - Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, | | 1000 | + if (!resolved.IsNothing()) { | | 1001 | + ProcessEmitExperimentalWarning(env, "Conditional exports"); | | 1002 | + return resolved; | | 1003 | + } | | 1004 | + } else if (key_str == "default") { | | 1005 | + matched = true; | | 1006 | + conditionalTarget = target_obj->Get(context, key).ToLocalChecked(); | | 1007 | + Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, | 986 | 1008 | conditionalTarget, subpath, pkg_subpath, base, false); | 987 | | - if (!resolved.IsNothing()) { | 988 | | - return resolved; | | 1009 | + if (!resolved.IsNothing()) { | | 1010 | + ProcessEmitExperimentalWarning(env, "Conditional exports"); | | 1011 | + return resolved; | | 1012 | + } | 989 | 1013 | } | 990 | 1014 | } | 991 | 1015 | if (matched && throw_invalid) { | 992 | 1016 | Maybe<URL> resolved = ResolveExportsTarget(env, pjson_url, | 993 | | - conditionalTarget, subpath, pkg_subpath, base, true); | | 1017 | + conditionalTarget, subpath, pkg_subpath, base, true); | 994 | 1018 | CHECK(resolved.IsNothing()); | 995 | 1019 | return Nothing<URL>(); | 996 | 1020 | } | @@ -1013,8 +1037,8 @@ Maybe<bool> IsConditionalExportsMainSugar(Environment* env, | 1013 | 1037 | exports_obj->GetOwnPropertyNames(context).ToLocalChecked(); | 1014 | 1038 | bool isConditionalSugar = false; | 1015 | 1039 | for (uint32_t i = 0; i < keys->Length(); ++i) { | 1016 | | - Local<String> key = keys->Get(context, i).ToLocalChecked().As<String>(); | 1017 | | - Utf8Value key_utf8(env->isolate(), key); | | 1040 | + Local<Value> key = keys->Get(context, i).ToLocalChecked(); | | 1041 | + Utf8Value key_utf8(env->isolate(), key->ToString(context).ToLocalChecked()); | 1018 | 1042 | bool curIsConditionalSugar = key_utf8.length() == 0 || key_utf8[0] != '.'; | 1019 | 1043 | if (i == 0) { | 1020 | 1044 | isConditionalSugar = curIsConditionalSugar; | @@ -1122,13 +1146,13 @@ Maybe<URL> PackageExportsResolve(Environment* env, | 1122 | 1146 | Local<Array> keys = | 1123 | 1147 | exports_obj->GetOwnPropertyNames(context).ToLocalChecked(); | 1124 | 1148 | for (uint32_t i = 0; i < keys->Length(); ++i) { | 1125 | | - Local<String> key = keys->Get(context, i).ToLocalChecked().As<String>(); | 1126 | | - Utf8Value key_utf8(isolate, key); | | 1149 | + Local<Value> key = keys->Get(context, i).ToLocalChecked(); | | 1150 | + Utf8Value key_utf8(isolate, key->ToString(context).ToLocalChecked()); | 1127 | 1151 | std::string key_str(*key_utf8, key_utf8.length()); | 1128 | 1152 | if (key_str.back() != '/') continue; | 1129 | 1153 | if (pkg_subpath.substr(0, key_str.length()) == key_str && | 1130 | 1154 | key_str.length() > best_match_str.length()) { | 1131 | | - best_match = key; | | 1155 | + best_match = key->ToString(context).ToLocalChecked(); | 1132 | 1156 | best_match_str = key_str; | 1133 | 1157 | } | 1134 | 1158 | } | |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-esm-exports.mjs +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -124,6 +124,11 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; | 124 | 124 | 'ERR_MODULE_NOT_FOUND'); | 125 | 125 | })); | 126 | 126 |
| | 127 | + // Package export with numeric index properties must throw a validation error | | 128 | + loadFixture('pkgexports-numeric').catch(mustCall((err) => { | | 129 | + strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); | | 130 | + })); | | 131 | + | 127 | 132 | // Sugar conditional exports main mixed failure case | 128 | 133 | loadFixture('pkgexports-sugar-fail').catch(mustCall((err) => { | 129 | 134 | strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); | |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/node_modules/pkgexports-numeric/package.json +6Lines changed: 6 additions & 0 deletions Collapse file Copy file name to clipboardExpand all lines: test/fixtures/node_modules/pkgexports/package.json +14-1Lines changed: 14 additions & 1 deletion
|