Skip to content

Commit

Permalink
fix: lazily read package.json.exports for shared resolvers (#137)
Browse files Browse the repository at this point in the history
relates #135
  • Loading branch information
Boshen authored Apr 23, 2024
1 parent b2e2094 commit 675f5e3
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 111 deletions.
130 changes: 61 additions & 69 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ mod tsconfig;
#[cfg(test)]
mod tests;

use rustc_hash::FxHashSet;
use std::{
borrow::Cow,
cmp::Ordering,
Expand All @@ -69,6 +68,9 @@ use std::{
path::{Component, Path, PathBuf},
sync::Arc,
};

use rustc_hash::FxHashSet;
use serde_json::Value as JSONValue;
use typescript_tsconfig_json::ExtendsField;

pub use crate::{
Expand All @@ -86,11 +88,11 @@ use crate::{
cache::{Cache, CachedPath},
context::ResolveContext as Ctx,
file_system::FileSystemOs,
package_json::ImportExportMap,
path::{PathUtil, SLASH_START},
specifier::Specifier,
tsconfig::{ProjectReference, TsConfig},
};
use nodejs_package_json::{ImportExportField, ImportExportKey, ImportExportMap};

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

Expand Down Expand Up @@ -749,16 +751,13 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
};
// 3. Parse DIR/NAME/package.json, and look for "exports" field.
// 4. If "exports" is null or undefined, return.
if package_json.exports.is_empty() {
return Ok(None);
};
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
// `package.json` "exports", ["node", "require"]) defined in the ESM resolver.
// Note: The subpath is not prepended with a dot on purpose
for exports in &package_json.exports {
for exports in package_json.exports_fields(&self.options.exports_fields) {
if let Some(path) = self.package_exports_resolve(
cached_path.path(),
subpath,
&format!(".{subpath}"),
exports,
&self.options.condition_names,
ctx,
Expand All @@ -784,30 +783,28 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
return Ok(None);
};
// 3. If the SCOPE/package.json "exports" is null or undefined, return.
if !package_json.exports.is_empty() {
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
if let Some(subpath) = package_json
.name
.as_ref()
.and_then(|package_name| Self::strip_package_name(specifier, package_name))
{
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
// "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
// defined in the ESM resolver.
let package_url = package_json.directory();
// Note: The subpath is not prepended with a dot on purpose
// because `package_exports_resolve` matches subpath without the leading dot.
for exports in &package_json.exports {
if let Some(cached_path) = self.package_exports_resolve(
package_url,
subpath,
exports,
&self.options.condition_names,
ctx,
)? {
// 6. RESOLVE_ESM_MATCH(MATCH)
return self.resolve_esm_match(specifier, &cached_path, &package_json, ctx);
}
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
if let Some(subpath) = package_json
.name
.as_ref()
.and_then(|package_name| Self::strip_package_name(specifier, package_name))
{
// 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
// "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
// defined in the ESM resolver.
let package_url = package_json.directory();
// Note: The subpath is not prepended with a dot on purpose
// because `package_exports_resolve` matches subpath without the leading dot.
for exports in package_json.exports_fields(&self.options.exports_fields) {
if let Some(cached_path) = self.package_exports_resolve(
package_url,
&format!(".{subpath}"),
exports,
&self.options.condition_names,
ctx,
)? {
// 6. RESOLVE_ESM_MATCH(MATCH)
return self.resolve_esm_match(specifier, &cached_path, &package_json, ctx);
}
}
}
Expand Down Expand Up @@ -1143,18 +1140,16 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
cached_path.package_json(&self.cache.fs, &self.options, ctx)?
{
// 5. If pjson is not null and pjson.exports is not null or undefined, then
if !package_json.exports.is_empty() {
// 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
for exports in &package_json.exports {
if let Some(path) = self.package_exports_resolve(
cached_path.path(),
subpath,
exports,
&self.options.condition_names,
ctx,
)? {
return Ok(Some(path));
}
// 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
for exports in package_json.exports_fields(&self.options.exports_fields) {
if let Some(path) = self.package_exports_resolve(
cached_path.path(),
&format!(".{subpath}"),
exports,
&self.options.condition_names,
ctx,
)? {
return Ok(Some(path));
}
}
// 6. Otherwise, if packageSubpath is equal to ".", then
Expand Down Expand Up @@ -1187,18 +1182,17 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
&self,
package_url: &Path,
subpath: &str,
exports: &ImportExportField,
exports: &JSONValue,
conditions: &[String],
ctx: &mut Ctx,
) -> ResolveResult {
// 1. If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
if let ImportExportField::Map(map) = exports {
if let JSONValue::Object(map) = exports {
let mut has_dot = false;
let mut without_dot = false;
for key in map.keys() {
has_dot =
has_dot || matches!(key, ImportExportKey::Main | ImportExportKey::Pattern(_));
without_dot = without_dot || matches!(key, ImportExportKey::CustomCondition(_));
has_dot = has_dot || key.starts_with(|s| s == '.' || s == '#');
without_dot = without_dot || !key.starts_with(|s| s == '.' || s == '#');
if has_dot && without_dot {
return Err(ResolveError::InvalidPackageConfig(
package_url.join("package.json"),
Expand All @@ -1208,32 +1202,31 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}
// 2. If subpath is equal to ".", then
// Note: subpath is not prepended with a dot when passed in.
if subpath.is_empty() {
if subpath == "." {
// enhanced-resolve appends query and fragment when resolving exports field
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/ExportsFieldPlugin.js#L57-L62
// This is only need when querying the main export, otherwise ctx is passed through.
if ctx.query.is_some() || ctx.fragment.is_some() {
let query = ctx.query.clone().unwrap_or_default();
let fragment = ctx.fragment.clone().unwrap_or_default();
return Err(ResolveError::PackagePathNotExported(
format!("./{subpath}{query}{fragment}"),
format!("./{}{query}{fragment}", subpath.trim_start_matches('.')),
package_url.join("package.json"),
));
}
// 1. Let mainExport be undefined.
let main_export = match exports {
ImportExportField::None => None,
// 2. If exports is a String or Array, or an Object containing no keys starting with ".", then
ImportExportField::String(_) | ImportExportField::Array(_) => {
JSONValue::String(_) | JSONValue::Array(_) => {
// 1. Set mainExport to exports.
Some(exports)
}
// 3. Otherwise if exports is an Object containing a "." property, then
ImportExportField::Map(map) => {
JSONValue::Object(map) => {
// 1. Set mainExport to exports["."].
map.get(&ImportExportKey::Main).map_or_else(
map.get(".").map_or_else(
|| {
if map.keys().any(|key| matches!(key, ImportExportKey::Pattern(_))) {
if map.keys().any(|key| key.starts_with("./") || key.starts_with('#')) {
None
} else {
Some(exports)
Expand All @@ -1242,6 +1235,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Some,
)
}
_ => None,
};
// 4. If mainExport is not undefined, then
if let Some(main_export) = main_export {
Expand All @@ -1262,7 +1256,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}
}
// 3. Otherwise, if exports is an Object and all keys of exports start with ".", then
if let ImportExportField::Map(exports) = exports {
if let JSONValue::Object(exports) = exports {
// 1. Let matchKey be the string "./" concatenated with subpath.
// Note: `package_imports_exports_resolve` does not require the leading dot.
let match_key = &subpath;
Expand All @@ -1281,7 +1275,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}
// 4. Throw a Package Path Not Exported error.
Err(ResolveError::PackagePathNotExported(
format!(".{subpath}"),
subpath.to_string(),
package_url.join("package.json"),
))
}
Expand Down Expand Up @@ -1346,7 +1340,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// 1. If matchKey is a key of matchObj and does not contain "*", then
if !match_key.contains('*') {
// 1. Let target be the value of matchObj[matchKey].
if let Some(target) = match_obj.get(&ImportExportKey::Pattern(match_key.to_string())) {
if let Some(target) = match_obj.get(match_key) {
// 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
return self.package_target_resolve(
package_url,
Expand All @@ -1366,7 +1360,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// 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.
// 3. For each key expansionKey in expansionKeys, do
for (expansion_key, target) in match_obj {
if let ImportExportKey::Pattern(expansion_key) = expansion_key {
if expansion_key.starts_with("./") || expansion_key.starts_with('#') {
// 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character.
if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') {
// 2. If matchKey starts with but is not equal to patternBase, then
Expand Down Expand Up @@ -1419,7 +1413,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
&self,
package_url: &Path,
target_key: &str,
target: &ImportExportField,
target: &JSONValue,
pattern_match: Option<&str>,
is_imports: bool,
conditions: &[String],
Expand Down Expand Up @@ -1452,9 +1446,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}

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

// 1. If p equals "default" or conditions contains an entry for p, then
if is_default
|| matches!(key, ImportExportKey::CustomCondition(condition) if conditions.contains(condition))
{
if is_default || conditions.contains(key) {
// 1. Let targetValue be the value of the p property in target.
// 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
let resolved = self.package_target_resolve(
Expand All @@ -1535,12 +1526,12 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
return Ok(None);
}
// 3. Otherwise, if target is an Array, then
ImportExportField::Array(targets) => {
JSONValue::Array(targets) => {
// 1. If _target.length is zero, return null.
if targets.is_empty() {
// Note: return PackagePathNotExported has the same effect as return because there are no matches.
return Err(ResolveError::PackagePathNotExported(
format!(".{}", pattern_match.unwrap_or(".")),
pattern_match.unwrap_or(".").to_string(),
package_url.join("package.json"),
));
}
Expand Down Expand Up @@ -1570,6 +1561,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// 3. Return or throw the last fallback resolution null return or error.
// Note: see `resolved.is_err() && i == targets.len()`
}
_ => {}
}
// 4. Otherwise, if target is null, return null.
Ok(None)
Expand Down
Loading

0 comments on commit 675f5e3

Please sign in to comment.