Skip to content

Commit 675f5e3

Browse files
authored
fix: lazily read package.json.exports for shared resolvers (#137)
relates #135
1 parent b2e2094 commit 675f5e3

File tree

4 files changed

+119
-111
lines changed

4 files changed

+119
-111
lines changed

src/lib.rs

Lines changed: 61 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ mod tsconfig;
6060
#[cfg(test)]
6161
mod tests;
6262

63-
use rustc_hash::FxHashSet;
6463
use std::{
6564
borrow::Cow,
6665
cmp::Ordering,
@@ -69,6 +68,9 @@ use std::{
6968
path::{Component, Path, PathBuf},
7069
sync::Arc,
7170
};
71+
72+
use rustc_hash::FxHashSet;
73+
use serde_json::Value as JSONValue;
7274
use typescript_tsconfig_json::ExtendsField;
7375

7476
pub use crate::{
@@ -86,11 +88,11 @@ use crate::{
8688
cache::{Cache, CachedPath},
8789
context::ResolveContext as Ctx,
8890
file_system::FileSystemOs,
91+
package_json::ImportExportMap,
8992
path::{PathUtil, SLASH_START},
9093
specifier::Specifier,
9194
tsconfig::{ProjectReference, TsConfig},
9295
};
93-
use nodejs_package_json::{ImportExportField, ImportExportKey, ImportExportMap};
9496

9597
type ResolveResult = Result<Option<CachedPath>, ResolveError>;
9698

@@ -749,16 +751,13 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
749751
};
750752
// 3. Parse DIR/NAME/package.json, and look for "exports" field.
751753
// 4. If "exports" is null or undefined, return.
752-
if package_json.exports.is_empty() {
753-
return Ok(None);
754-
};
755754
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
756755
// `package.json` "exports", ["node", "require"]) defined in the ESM resolver.
757756
// Note: The subpath is not prepended with a dot on purpose
758-
for exports in &package_json.exports {
757+
for exports in package_json.exports_fields(&self.options.exports_fields) {
759758
if let Some(path) = self.package_exports_resolve(
760759
cached_path.path(),
761-
subpath,
760+
&format!(".{subpath}"),
762761
exports,
763762
&self.options.condition_names,
764763
ctx,
@@ -784,30 +783,28 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
784783
return Ok(None);
785784
};
786785
// 3. If the SCOPE/package.json "exports" is null or undefined, return.
787-
if !package_json.exports.is_empty() {
788-
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
789-
if let Some(subpath) = package_json
790-
.name
791-
.as_ref()
792-
.and_then(|package_name| Self::strip_package_name(specifier, package_name))
793-
{
794-
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
795-
// "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
796-
// defined in the ESM resolver.
797-
let package_url = package_json.directory();
798-
// Note: The subpath is not prepended with a dot on purpose
799-
// because `package_exports_resolve` matches subpath without the leading dot.
800-
for exports in &package_json.exports {
801-
if let Some(cached_path) = self.package_exports_resolve(
802-
package_url,
803-
subpath,
804-
exports,
805-
&self.options.condition_names,
806-
ctx,
807-
)? {
808-
// 6. RESOLVE_ESM_MATCH(MATCH)
809-
return self.resolve_esm_match(specifier, &cached_path, &package_json, ctx);
810-
}
786+
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
787+
if let Some(subpath) = package_json
788+
.name
789+
.as_ref()
790+
.and_then(|package_name| Self::strip_package_name(specifier, package_name))
791+
{
792+
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
793+
// "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
794+
// defined in the ESM resolver.
795+
let package_url = package_json.directory();
796+
// Note: The subpath is not prepended with a dot on purpose
797+
// because `package_exports_resolve` matches subpath without the leading dot.
798+
for exports in package_json.exports_fields(&self.options.exports_fields) {
799+
if let Some(cached_path) = self.package_exports_resolve(
800+
package_url,
801+
&format!(".{subpath}"),
802+
exports,
803+
&self.options.condition_names,
804+
ctx,
805+
)? {
806+
// 6. RESOLVE_ESM_MATCH(MATCH)
807+
return self.resolve_esm_match(specifier, &cached_path, &package_json, ctx);
811808
}
812809
}
813810
}
@@ -1143,18 +1140,16 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
11431140
cached_path.package_json(&self.cache.fs, &self.options, ctx)?
11441141
{
11451142
// 5. If pjson is not null and pjson.exports is not null or undefined, then
1146-
if !package_json.exports.is_empty() {
1147-
// 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
1148-
for exports in &package_json.exports {
1149-
if let Some(path) = self.package_exports_resolve(
1150-
cached_path.path(),
1151-
subpath,
1152-
exports,
1153-
&self.options.condition_names,
1154-
ctx,
1155-
)? {
1156-
return Ok(Some(path));
1157-
}
1143+
// 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
1144+
for exports in package_json.exports_fields(&self.options.exports_fields) {
1145+
if let Some(path) = self.package_exports_resolve(
1146+
cached_path.path(),
1147+
&format!(".{subpath}"),
1148+
exports,
1149+
&self.options.condition_names,
1150+
ctx,
1151+
)? {
1152+
return Ok(Some(path));
11581153
}
11591154
}
11601155
// 6. Otherwise, if packageSubpath is equal to ".", then
@@ -1187,18 +1182,17 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
11871182
&self,
11881183
package_url: &Path,
11891184
subpath: &str,
1190-
exports: &ImportExportField,
1185+
exports: &JSONValue,
11911186
conditions: &[String],
11921187
ctx: &mut Ctx,
11931188
) -> ResolveResult {
11941189
// 1. If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
1195-
if let ImportExportField::Map(map) = exports {
1190+
if let JSONValue::Object(map) = exports {
11961191
let mut has_dot = false;
11971192
let mut without_dot = false;
11981193
for key in map.keys() {
1199-
has_dot =
1200-
has_dot || matches!(key, ImportExportKey::Main | ImportExportKey::Pattern(_));
1201-
without_dot = without_dot || matches!(key, ImportExportKey::CustomCondition(_));
1194+
has_dot = has_dot || key.starts_with(|s| s == '.' || s == '#');
1195+
without_dot = without_dot || !key.starts_with(|s| s == '.' || s == '#');
12021196
if has_dot && without_dot {
12031197
return Err(ResolveError::InvalidPackageConfig(
12041198
package_url.join("package.json"),
@@ -1208,32 +1202,31 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12081202
}
12091203
// 2. If subpath is equal to ".", then
12101204
// Note: subpath is not prepended with a dot when passed in.
1211-
if subpath.is_empty() {
1205+
if subpath == "." {
12121206
// enhanced-resolve appends query and fragment when resolving exports field
12131207
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/ExportsFieldPlugin.js#L57-L62
12141208
// This is only need when querying the main export, otherwise ctx is passed through.
12151209
if ctx.query.is_some() || ctx.fragment.is_some() {
12161210
let query = ctx.query.clone().unwrap_or_default();
12171211
let fragment = ctx.fragment.clone().unwrap_or_default();
12181212
return Err(ResolveError::PackagePathNotExported(
1219-
format!("./{subpath}{query}{fragment}"),
1213+
format!("./{}{query}{fragment}", subpath.trim_start_matches('.')),
12201214
package_url.join("package.json"),
12211215
));
12221216
}
12231217
// 1. Let mainExport be undefined.
12241218
let main_export = match exports {
1225-
ImportExportField::None => None,
12261219
// 2. If exports is a String or Array, or an Object containing no keys starting with ".", then
1227-
ImportExportField::String(_) | ImportExportField::Array(_) => {
1220+
JSONValue::String(_) | JSONValue::Array(_) => {
12281221
// 1. Set mainExport to exports.
12291222
Some(exports)
12301223
}
12311224
// 3. Otherwise if exports is an Object containing a "." property, then
1232-
ImportExportField::Map(map) => {
1225+
JSONValue::Object(map) => {
12331226
// 1. Set mainExport to exports["."].
1234-
map.get(&ImportExportKey::Main).map_or_else(
1227+
map.get(".").map_or_else(
12351228
|| {
1236-
if map.keys().any(|key| matches!(key, ImportExportKey::Pattern(_))) {
1229+
if map.keys().any(|key| key.starts_with("./") || key.starts_with('#')) {
12371230
None
12381231
} else {
12391232
Some(exports)
@@ -1242,6 +1235,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12421235
Some,
12431236
)
12441237
}
1238+
_ => None,
12451239
};
12461240
// 4. If mainExport is not undefined, then
12471241
if let Some(main_export) = main_export {
@@ -1262,7 +1256,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12621256
}
12631257
}
12641258
// 3. Otherwise, if exports is an Object and all keys of exports start with ".", then
1265-
if let ImportExportField::Map(exports) = exports {
1259+
if let JSONValue::Object(exports) = exports {
12661260
// 1. Let matchKey be the string "./" concatenated with subpath.
12671261
// Note: `package_imports_exports_resolve` does not require the leading dot.
12681262
let match_key = &subpath;
@@ -1281,7 +1275,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
12811275
}
12821276
// 4. Throw a Package Path Not Exported error.
12831277
Err(ResolveError::PackagePathNotExported(
1284-
format!(".{subpath}"),
1278+
subpath.to_string(),
12851279
package_url.join("package.json"),
12861280
))
12871281
}
@@ -1346,7 +1340,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
13461340
// 1. If matchKey is a key of matchObj and does not contain "*", then
13471341
if !match_key.contains('*') {
13481342
// 1. Let target be the value of matchObj[matchKey].
1349-
if let Some(target) = match_obj.get(&ImportExportKey::Pattern(match_key.to_string())) {
1343+
if let Some(target) = match_obj.get(match_key) {
13501344
// 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
13511345
return self.package_target_resolve(
13521346
package_url,
@@ -1366,7 +1360,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
13661360
// 2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
13671361
// 3. For each key expansionKey in expansionKeys, do
13681362
for (expansion_key, target) in match_obj {
1369-
if let ImportExportKey::Pattern(expansion_key) = expansion_key {
1363+
if expansion_key.starts_with("./") || expansion_key.starts_with('#') {
13701364
// 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character.
13711365
if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') {
13721366
// 2. If matchKey starts with but is not equal to patternBase, then
@@ -1419,7 +1413,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
14191413
&self,
14201414
package_url: &Path,
14211415
target_key: &str,
1422-
target: &ImportExportField,
1416+
target: &JSONValue,
14231417
pattern_match: Option<&str>,
14241418
is_imports: bool,
14251419
conditions: &[String],
@@ -1452,9 +1446,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
14521446
}
14531447

14541448
match target {
1455-
ImportExportField::None => {}
14561449
// 1. If target is a String, then
1457-
ImportExportField::String(target) => {
1450+
JSONValue::String(target) => {
14581451
// 1. If target does not start with "./", then
14591452
if !target.starts_with("./") {
14601453
// 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then
@@ -1495,24 +1488,22 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
14951488
return Ok(Some(value));
14961489
}
14971490
// 2. Otherwise, if target is a non-null Object, then
1498-
ImportExportField::Map(target) => {
1491+
JSONValue::Object(target) => {
14991492
// 1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
15001493
// 2. For each property p of target, in object insertion order as,
15011494
for (i, (key, target_value)) in target.iter().enumerate() {
15021495
// https://nodejs.org/api/packages.html#conditional-exports
15031496
// "default" - the generic fallback that always matches. Can be a CommonJS or ES module file. This condition should always come last.
15041497
// Note: node.js does not throw this but enhanced-resolve does.
1505-
let is_default = matches!(key, ImportExportKey::CustomCondition(condition) if condition == "default");
1498+
let is_default = key == "default";
15061499
if i < target.len() - 1 && is_default {
15071500
return Err(ResolveError::InvalidPackageConfigDefault(
15081501
package_url.join("package.json"),
15091502
));
15101503
}
15111504

15121505
// 1. If p equals "default" or conditions contains an entry for p, then
1513-
if is_default
1514-
|| matches!(key, ImportExportKey::CustomCondition(condition) if conditions.contains(condition))
1515-
{
1506+
if is_default || conditions.contains(key) {
15161507
// 1. Let targetValue be the value of the p property in target.
15171508
// 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
15181509
let resolved = self.package_target_resolve(
@@ -1535,12 +1526,12 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
15351526
return Ok(None);
15361527
}
15371528
// 3. Otherwise, if target is an Array, then
1538-
ImportExportField::Array(targets) => {
1529+
JSONValue::Array(targets) => {
15391530
// 1. If _target.length is zero, return null.
15401531
if targets.is_empty() {
15411532
// Note: return PackagePathNotExported has the same effect as return because there are no matches.
15421533
return Err(ResolveError::PackagePathNotExported(
1543-
format!(".{}", pattern_match.unwrap_or(".")),
1534+
pattern_match.unwrap_or(".").to_string(),
15441535
package_url.join("package.json"),
15451536
));
15461537
}
@@ -1570,6 +1561,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
15701561
// 3. Return or throw the last fallback resolution null return or error.
15711562
// Note: see `resolved.is_err() && i == targets.len()`
15721563
}
1564+
_ => {}
15731565
}
15741566
// 4. Otherwise, if target is null, return null.
15751567
Ok(None)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy