feat(resolver): implement the basics of ESM (#691)

This commit is contained in:
Boshen 2023-08-05 22:04:57 +08:00 committed by GitHub
parent c5ff534b08
commit 658ef676f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 259 additions and 108 deletions

View file

@ -122,19 +122,14 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
request: &Request,
ctx: ResolveContext,
) -> Result<CacheValue, ResolveError> {
let path = match request.path {
match request.path {
// 1. If X is a core module,
// a. return the core module
// b. STOP
// 2. If X begins with '/'
// a. set Y to be the file system root
RequestPath::Absolute(absolute_path) => {
if !self.options.prefer_relative && self.options.prefer_absolute {
if let Ok(path) = self.package_resolve(cache_value, absolute_path, ctx) {
return Ok(path);
}
}
self.load_roots(cache_value, absolute_path, ctx)
self.require_absolute(cache_value, absolute_path, ctx)
}
// 3. If X begins with './' or '/' or '../'
RequestPath::Relative(relative_path) => {
@ -143,27 +138,42 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// 4. If X begins with '#'
RequestPath::Hash(specifier) => {
// a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
self.package_imports_resolve(cache_value, specifier)
self.require_hash(cache_value, specifier, ctx)
}
// (ESM) 5. Otherwise,
// Note: specifier is now a bare specifier.
// Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
RequestPath::Bare(bare_specifier) => {
if self.options.prefer_relative {
if let Ok(path) = self.require_relative(cache_value, bare_specifier, ctx) {
return Ok(path);
}
}
self.package_resolve(cache_value, bare_specifier, ctx)
self.require_bare(cache_value, bare_specifier, ctx)
}
}?;
if !path.is_file(&self.cache.fs) {
// TODO: Throw a Module Not Found error. Or better error message
return Err(ResolveError::NotFound(path.to_path_buf().into_boxed_path()));
}
}
Ok(path)
fn require_absolute(
&self,
cache_value: &CacheValue,
request: &str,
ctx: ResolveContext,
) -> Result<CacheValue, ResolveError> {
debug_assert!(request.starts_with('/'));
if !self.options.prefer_relative && self.options.prefer_absolute {
if let Ok(path) = self.load_package_self_or_node_modules(cache_value, request, ctx) {
return Ok(path);
}
}
if self.options.roots.is_empty() {
let cache_value = self.cache.value(Path::new("/"));
return self.load_package_self_or_node_modules(&cache_value, request, ctx);
}
for root in &self.options.roots {
let cache_value = self.cache.value(root);
if let Ok(path) =
self.require_relative(&cache_value, request.trim_start_matches('/'), ctx)
{
return Ok(path);
}
}
Err(ResolveError::NotFound(cache_value.to_path_buf().into_boxed_path()))
}
// 3. If X begins with './' or '/' or '../'
@ -189,8 +199,34 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Err(ResolveError::NotFound(path.into_boxed_path()))
}
/// PACKAGE_RESOLVE(packageSpecifier, parentURL)
fn package_resolve(
fn require_hash(
&self,
cache_value: &CacheValue,
request: &str,
ctx: ResolveContext,
) -> Result<CacheValue, ResolveError> {
let cache_value = self.cache.dirname(cache_value);
if let Some(path) = self.load_package_imports(cache_value, request, ctx)? {
return Ok(path);
}
self.load_package_self_or_node_modules(cache_value, request, ctx)
}
fn require_bare(
&self,
cache_value: &CacheValue,
request: &str,
ctx: ResolveContext,
) -> Result<CacheValue, ResolveError> {
if self.options.prefer_relative {
if let Ok(path) = self.require_relative(cache_value, request, ctx) {
return Ok(path);
}
}
self.load_package_self_or_node_modules(cache_value, request, ctx)
}
fn load_package_self_or_node_modules(
&self,
cache_value: &CacheValue,
request: &str,
@ -204,7 +240,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
let dirname = self.cache.dirname(cache_value);
// 5. LOAD_PACKAGE_SELF(X, dirname(Y))
if let Some(path) = self.load_package_self(dirname, request)? {
if let Some(path) = self.load_package_self(dirname, request, ctx)? {
return Ok(path);
}
// 6. LOAD_NODE_MODULES(X, dirname(Y))
@ -215,9 +251,124 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Err(ResolveError::NotFound(cache_value.to_path_buf().into_boxed_path()))
}
/// LOAD_PACKAGE_IMPORTS(X, DIR)
fn load_package_imports(
&self,
cache_value: &CacheValue,
request: &str,
ctx: ResolveContext,
) -> ResolveState {
// 1. Find the closest package scope SCOPE to DIR.
// 2. If no scope was found, return.
let Some(package_json) = cache_value.find_package_json(&self.cache.fs)? else {
return Ok(None);
};
// 3. If the SCOPE/package.json "imports" is null or undefined, return.
if package_json.imports.is_empty() {
return Ok(None);
}
// 4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), ["node", "require"]) defined in the ESM resolver.
let package_url = self.cache.value(package_json.path.parent().unwrap());
let path = self.package_imports_resolve(&package_url, request)?;
// 5. RESOLVE_ESM_MATCH(MATCH).
self.resolve_esm_match(&path, &package_json, ctx)
}
/// PACKAGE_RESOLVE(packageSpecifier, parentURL)
fn package_resolve(&self, cache_value: &CacheValue, request: &str) -> ResolveState {
let (name, subpath) = Self::parse_package_specifier(request);
// 9. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
if let Some(path) = self.package_self_resolve(name, subpath, cache_value)? {
// 10. If selfUrl is not undefined, return selfUrl.
return Ok(Some(path));
}
// 11. While parentURL is not the file system root,
let mut parent_url = cache_value.path().to_path_buf();
loop {
for module_name in &self.options.modules {
// 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
parent_url.push(module_name);
let package_path = parent_url.join(name);
// 2. Set parentURL to the parent folder URL of parentURL.
let cache_value = self.cache.value(&package_path);
// 3. If the folder at packageURL does not exist, then
// 1. Continue the next loop iteration.
if cache_value.is_dir(&self.cache.fs) {
// 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
if let Some(package_json) =
cache_value.package_json(&self.cache.fs).transpose()?
{
// 5. If pjson is not null and pjson.exports is not null or undefined, then
if !package_json.exports.is_none() {
// 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
return self.package_exports_resolve(
cache_value.path(),
subpath,
&package_json.exports,
&self.options.condition_names,
);
}
// 6. Otherwise, if packageSubpath is equal to ".", then
if subpath == "." {
// 1. If pjson.main is a string, then
if let Some(main_field) = &package_json.main {
// 1. Return the URL resolution of main in packageURL.
let path = cache_value.path().normalize_with(main_field);
return Ok(Some(self.cache.value(&path)));
}
}
}
let subpath = format!(".{subpath}");
let request = Request::parse(&subpath).map_err(ResolveError::Request)?;
return self
.require(&cache_value, &request, ResolveContext::default())
.map(Some);
}
parent_url.pop();
}
if !parent_url.pop() {
break;
}
}
Err(ResolveError::NotFound(cache_value.to_path_buf().into_boxed_path()))
}
fn package_self_resolve(
&self,
package_name: &str,
package_subpath: &str,
parent_url: &CacheValue,
) -> ResolveState {
let Some(package_json) = parent_url.find_package_json(&self.cache.fs)? else {
return Ok(None);
};
if package_json.exports.is_none() {
// enhanced_resolve: try browser field
return self.load_browser_field(
parent_url.path(),
Some(package_subpath),
&package_json,
);
}
if package_json
.name
.as_ref()
.is_some_and(|package_json_name| package_json_name == package_name)
{
return self.package_exports_resolve(
&package_json.path,
package_subpath,
&package_json.exports,
&self.options.condition_names,
);
}
Ok(None)
}
fn load_as_file(&self, cache_value: &CacheValue, ctx: ResolveContext) -> ResolveState {
// enhanced-resolve feature: extension_alias
if let Some(path) = self.load_extension_alias(cache_value, ctx)? {
if let Some(path) = self.load_extension_alias(cache_value)? {
return Ok(Some(path));
}
// 1. If X is a file, load X as its file extension format. STOP
@ -337,7 +488,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
for module_name in &self.options.modules {
node_module_path.push(module_name);
// a. LOAD_PACKAGE_EXPORTS(X, DIR)
if let Some(path) = self.load_package_exports(&node_module_path, request)? {
if let Some(path) = self.load_package_exports(&node_module_path, request, ctx)? {
return Ok(Some(path));
}
@ -402,7 +553,12 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
(package_name, package_subpath)
}
fn load_package_exports(&self, path: &Path, request: &str) -> ResolveState {
fn load_package_exports(
&self,
path: &Path,
request: &str,
ctx: ResolveContext,
) -> ResolveState {
// 1. Try to interpret X as a combination of NAME and SUBPATH where the name
// may have a @scope/ prefix and the subpath begins with a slash (`/`).
// 2. If X does not match this pattern or DIR/NAME/package.json is not a file,
@ -420,53 +576,73 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// 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
if let Some(path) = self.package_exports_resolve(
let Some(path) = self.package_exports_resolve(
cache_value.path(),
subpath,
&package_json.exports,
&self.options.condition_names,
)? {
if let Some(path) = self.load_browser_field(path.path(), None, &package_json)? {
return Ok(Some(path));
}
return Ok(Some(path));
}
)? else {
return Ok(None)
};
// 6. RESOLVE_ESM_MATCH(MATCH)
Ok(None)
self.resolve_esm_match(&path, &package_json, ctx)
}
fn load_package_self(&self, cache_value: &CacheValue, request: &str) -> ResolveState {
fn load_package_self(
&self,
cache_value: &CacheValue,
request: &str,
ctx: ResolveContext,
) -> ResolveState {
// 1. Find the closest package scope SCOPE to DIR.
// 2. If no scope was found, return.
let Some(package_json) = cache_value.find_package_json(&self.cache.fs)? else {
return Ok(None);
};
// 3. If the SCOPE/package.json "exports" is null or undefined, return.
if !package_json.exports.is_none() {
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
if let Some(package_name) = &package_json.name {
if let Some(subpath) = package_name.strip_prefix(request) {
// 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.path.parent().unwrap();
// Note: The subpath is not prepended with a dot on purpose
// because `package_exports_resolve` matches subpath without the leading dot.
if let Some(path) = self.package_exports_resolve(
package_url,
subpath,
&package_json.exports,
&self.options.condition_names,
)? {
return Ok(Some(path));
}
}
}
if package_json.exports.is_none() {
return self.load_browser_field(cache_value.path(), Some(request), &package_json);
}
// 4. If the SCOPE/package.json "name" is not the first segment of X, return.
let Some(subpath) = package_json.name.as_ref().and_then(|package_json_name| package_json_name.strip_prefix(request)) else {
return Ok(None);
};
// 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.path.parent().unwrap();
// Note: The subpath is not prepended with a dot on purpose
// because `package_exports_resolve` matches subpath without the leading dot.
let Some(cache_value) = self.package_exports_resolve(
package_url,
subpath,
&package_json.exports,
&self.options.condition_names,
)? else {
return Ok(None);
};
// 6. RESOLVE_ESM_MATCH(MATCH)
self.resolve_esm_match(&cache_value, &package_json, ctx)
}
// Try non-spec-compliant "browser" field since its another form of export
self.load_browser_field(cache_value.path(), Some(request), &package_json)
/// RESOLVE_ESM_MATCH(MATCH)
fn resolve_esm_match(
&self,
cache_value: &CacheValue,
package_json: &PackageJson,
ctx: ResolveContext,
) -> ResolveState {
if let Some(path) = self.load_browser_field(cache_value.path(), None, package_json)? {
return Ok(Some(path));
}
// 1. let RESOLVED_PATH = fileURLToPath(MATCH)
// 2. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
if let Some(path) = self.load_as_file(cache_value, ctx)? {
return Ok(Some(path));
}
// format. STOP
// 3. THROW "not found"
Err(ResolveError::NotFound(cache_value.to_path_buf().into_boxed_path()))
}
fn load_browser_field(
@ -475,17 +651,18 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
request: Option<&str>,
package_json: &PackageJson,
) -> ResolveState {
if self.options.alias_fields.iter().any(|field| field == "browser") {
if let Some(request) = package_json.resolve(path, request)? {
let request = Request::parse(request).map_err(ResolveError::Request)?;
debug_assert!(package_json.path.file_name().is_some_and(|x| x == "package.json"));
// TODO: Do we need to pass query and fragment?
let cache_value = self.cache.value(package_json.path.parent().unwrap());
let ctx = ResolveContext::default();
return self.require(&cache_value, &request, ctx).map(Some);
}
if !self.options.alias_fields.iter().any(|field| field == "browser") {
return Ok(None);
}
Ok(None)
let Some(request) = package_json.resolve(path, request)? else{
return Ok(None);
};
let request = Request::parse(request).map_err(ResolveError::Request)?;
debug_assert!(package_json.path.file_name().is_some_and(|x| x == "package.json"));
// TODO: Do we need to pass query and fragment?
let cache_value = self.cache.value(package_json.path.parent().unwrap());
let ctx = ResolveContext::default();
self.require(&cache_value, &request, ctx).map(Some)
}
fn load_alias(&self, cache_value: &CacheValue, request: &str, alias: &Alias) -> ResolveState {
@ -530,41 +707,21 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
/// # Errors
///
/// * [ResolveError::ExtensionAlias]: When all of the aliased extensions are not found
fn load_extension_alias(&self, cache_value: &CacheValue, ctx: ResolveContext) -> ResolveState {
fn load_extension_alias(&self, cache_value: &CacheValue) -> ResolveState {
let Some(path_extension) = cache_value.path().extension() else { return Ok(None) };
let Some((_, extensions)) =
self.options.extension_alias.iter().find(|(ext, _)| OsStr::new(ext) == path_extension)
else {
return Ok(None);
};
if let Some(path) = self.load_extensions(cache_value, extensions, ctx)? {
if let Some(path) =
self.load_extensions(cache_value, extensions, ResolveContext::default())?
{
return Ok(Some(path));
}
Err(ResolveError::ExtensionAlias)
}
fn load_roots(
&self,
cache_value: &CacheValue,
request: &str,
ctx: ResolveContext,
) -> Result<CacheValue, ResolveError> {
debug_assert!(request.starts_with('/'));
if self.options.roots.is_empty() {
let cache_value = self.cache.value(Path::new("/"));
return self.package_resolve(&cache_value, request, ctx);
}
for root in &self.options.roots {
let cache_value = self.cache.value(root);
if let Ok(path) =
self.require_relative(&cache_value, request.trim_start_matches('/'), ctx)
{
return Ok(path);
}
}
Err(ResolveError::NotFound(cache_value.to_path_buf().into_boxed_path()))
}
/// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
fn package_exports_resolve(
&self,
@ -825,9 +982,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
normalize_string_target(target_key, target, pattern_match, package_url)?;
let package_url = self.cache.value(package_url);
// // 3. Return PACKAGE_RESOLVE(target, packageURL + "/").
return self
.package_resolve(&package_url, &target, ResolveContext::default())
.map(Some);
return self.package_resolve(&package_url, &target);
}
// 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.

View file

@ -109,7 +109,6 @@ fn exports_not_browser_field2() {
}
#[test]
#[ignore = "fully_specified"]
// should resolve extension without fullySpecified
fn extension_without_fully_specified() {
let f2 = super::fixture().join("exports-field2");
@ -125,10 +124,9 @@ fn extension_without_fully_specified() {
assert_eq!(resolved_path, Ok(f2.join("node_modules/exports-field/lib/lib2/main.js")));
}
#[test]
#[ignore = "exports field name"]
// #[test]
// field name path #1 - #5
fn field_name() {}
// fn field_name() {}
#[test]
fn extension_alias_1_2() {
@ -181,7 +179,6 @@ fn extension_alias_3() {
}
#[test]
#[ignore = "fully_specified"]
fn extension_alias_throw_error() {
let f = super::fixture().join("exports-field-and-extension-alias");
@ -195,10 +192,11 @@ fn extension_alias_throw_error() {
#[rustfmt::skip]
let fail = [
// These two test cases are exactly the same in enhanced-resolve
// enhanced-resolve has two test cases that are exactly the same here
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/test/exportsField.test.js#L2976-L3024
("should throw error with the `extensionAlias` option", f.clone(), "pkg/string.js", ResolveError::PackagePathNotExported("node_modules/pkg/dist/string.js".to_string())),
("should throw error with the `extensionAlias` option #2", f, "pkg/string.js", ResolveError::PackagePathNotExported("node_modules/pkg/dist/string.js".to_string())),
("should throw error with the `extensionAlias` option", f, "pkg/string.js", ResolveError::ExtensionAlias),
// TODO: The error is PackagePathNotExported in enhanced_resolve
// ("should throw error with the `extensionAlias` option", f.clone(), "pkg/string.js", ResolveError::PackagePathNotExported("node_modules/pkg/dist/string.ts".to_string())),
];
for (comment, path, request, error) in fail {

View file

@ -50,10 +50,9 @@ fn test() {
}
}
#[test]
#[ignore = "imports field name"]
// #[test]
// field name path #1 - #2
fn field_name() {}
// fn field_name() {}
// Small script for generating the test cases from enhanced_resolve
// for (c of testCases) {

View file

@ -48,9 +48,8 @@ fn resolve() {
}
}
#[test]
#[ignore = "issue238Resolve"]
fn issue238_resolve() {}
// #[test]
// fn issue238_resolve() {}
#[test]
fn prefer_relative() {