From 3f35bd7475a217ec1b9b36f614ce43302109cd38 Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 18 Mar 2026 14:18:49 +0800 Subject: [PATCH 1/2] fix: align package exports fallback array resolution with Node.js spec The condition `resolved.is_err() && i == targets.len()` was always false (off-by-one) causing InvalidPackageTarget errors to be silently dropped when all targets in a fallback array are invalid. Also implement Debug for CachedPathImpl to display its path. --- src/cache.rs | 8 +++++++- src/lib.rs | 25 ++++++++++++++++--------- src/tests/exports_field.rs | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 8d54f362..06e693a1 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -106,7 +106,7 @@ impl Cache { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CachedPath(Arc); impl Hash for CachedPath { @@ -158,6 +158,12 @@ pub struct CachedPathImpl { package_json: OnceLock>>, } +impl std::fmt::Debug for CachedPathImpl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.path.fmt(f) + } +} + impl CachedPathImpl { fn new(hash: u64, path: Box, parent: Option) -> Self { Self { diff --git a/src/lib.rs b/src/lib.rs index dbbd3d3f..8350eee5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1948,7 +1948,8 @@ impl ResolverGeneric { )); } // 2. For each item targetValue in target, do - for (i, target_value) in targets.iter().enumerate() { + let mut last_error = None; + for target_value in targets.iter() { // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error. let resolved = self .package_target_resolve( @@ -1962,18 +1963,24 @@ impl ResolverGeneric { ) .await; - if resolved.is_err() && i == targets.len() { - return resolved; - } - - // 2. If resolved is undefined, continue the loop. - if let Ok(Some(path)) = resolved { + match resolved { + // Only track InvalidPackageTarget for re-throwing after the loop. + Err(e @ ResolveError::InvalidPackageTarget(..)) => { + last_error = Some(e); + } + Err(_) => {} + // 2. If resolved is undefined, continue the loop. + Ok(None) => { + last_error = None; + } // 3. Return resolved. - return Ok(Some(path)); + Ok(Some(path)) => return Ok(Some(path)), } } // 3. Return or throw the last fallback resolution null return or error. - // Note: see `resolved.is_err() && i == targets.len()` + if let Some(e) = last_error { + return Err(e); + } } JSONValue::Static(_) => {} } diff --git a/src/tests/exports_field.rs b/src/tests/exports_field.rs index cfbbc8b1..9e47397f 100644 --- a/src/tests/exports_field.rs +++ b/src/tests/exports_field.rs @@ -2634,3 +2634,26 @@ async fn test_cases() { } } } + +/// When all targets in a fallback array are invalid, `package_target_resolve` should +/// propagate `InvalidPackageTarget` instead of swallowing it and returning `Ok(None)`. +/// Bug: `resolved.is_err() && i == targets.len()` is always false (off-by-one), +/// causing errors to be silently dropped. +#[tokio::test] +async fn array_fallback_all_invalid_targets_should_return_invalid_package_target() { + let resolved = Resolver::new(ResolveOptions::default()) + .package_exports_resolve( + Path::new(""), + ".", + &exports_field(json!({ + ".": ["invalid_target_1", "invalid_target_2"] + })), + &mut Ctx::default(), + ) + .await; + + assert!( + matches!(resolved, Err(ResolveError::InvalidPackageTarget(..))), + "expected InvalidPackageTarget, got {resolved:?}" + ); +} From a71c4efe83f710b60c098ec641b15a175a82871f Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 18 Mar 2026 14:23:45 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=F0=9F=9A=A8=20lint=20happy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a45e58cf..b287381c 100644 --- a/README.md +++ b/README.md @@ -167,12 +167,12 @@ The options are aligned with [enhanced-resolve](https://github.com/webpack/enhan ### Other Options -| Field | Default | Description | -| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| tsconfig | None | TypeScript related config for resolver | -| tsconfig.configFile | | A relative path to the tsconfig file based on `cwd`, or an absolute path of tsconfig file. | -| tsconfig.references | `[]` | - 'auto': inherits from TypeScript config
- `string []`: relative path (based on directory of the referencing tsconfig file) or absolute path of referenced project's tsconfig | -| enablePnp | false | Enable Yarn Plug'n'Play support | +| Field | Default | Description | +| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| tsconfig | None | TypeScript related config for resolver | +| tsconfig.configFile | | A relative path to the tsconfig file based on `cwd`, or an absolute path of tsconfig file. | +| tsconfig.references | `[]` | - 'auto': inherits from TypeScript config
- `string []`: relative path (based on directory of the referencing tsconfig file) or absolute path of referenced project's tsconfig | +| enablePnp | false | Enable Yarn Plug'n'Play support | In the context of `@rspack/resolver`, the `tsconfig.references` option helps isolate the `paths` configurations of different TypeScript projects. This ensures that path aliases defined in one TypeScript project do not unintentionally affect the resolving behavior of another.