Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: lazily read package.json.exports for shared resolvers #137

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
#[cfg(test)]
mod tests;

use rustc_hash::FxHashSet;
use std::{
borrow::Cow,
cmp::Ordering,
Expand All @@ -69,6 +68,9 @@
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 @@
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 @@
};
// 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 @@
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 @@
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 @@
&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 @@
}
// 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 @@
Some,
)
}
_ => None,

Check warning on line 1238 in src/lib.rs

View check run for this annotation

Codecov / codecov/patch

src/lib.rs#L1238

Added line #L1238 was not covered by tests
};
// 4. If mainExport is not undefined, then
if let Some(main_export) = main_export {
Expand All @@ -1262,7 +1256,7 @@
}
}
// 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 @@
}
// 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 @@
// 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 @@
// 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 @@
&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 @@
}

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 @@
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 @@
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 @@
// 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