mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 20:32:10 +00:00
feat(resolver): implement scoped packages (#558)
This commit is contained in:
parent
e0a17ace8f
commit
b2152ec050
7 changed files with 115 additions and 20 deletions
|
|
@ -1,11 +1,15 @@
|
|||
# Oxc Resolver
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] use `thiserror` for better error messages
|
||||
|
||||
#### Resolver Options
|
||||
|
||||
| Done | Field | Default | Description |
|
||||
|------|------------------|-----------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| | alias | [] | A list of module alias configurations or an object which maps key to value |
|
||||
| | aliasFields | [] | A list of alias fields in description files |
|
||||
| ✅ | aliasFields | [] | A list of alias fields in description files |
|
||||
| ✅ | extensionAlias | {} | An object which maps extension to extension aliases |
|
||||
| | cachePredicate | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with `path` and `request` properties. |
|
||||
| | cacheWithContext | true | If unsafe cache is enabled, includes `request.context` in the cache key |
|
||||
|
|
@ -18,7 +22,7 @@
|
|||
| | fileSystem | | The file system which should be used |
|
||||
| | fullySpecified | false | Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests) |
|
||||
| | mainFields | ["main"] | A list of main fields in description files |
|
||||
| | mainFiles | ["index"] | A list of main files in directories |
|
||||
| ✅ | mainFiles | ["index"] | A list of main files in directories |
|
||||
| | modules | ["node_modules"] | A list of directories to resolve modules from, can be absolute path or folder name |
|
||||
| | plugins | [] | A list of additional resolve plugins which should be applied |
|
||||
| | resolver | undefined | A prepared Resolver to which the plugins are attached |
|
||||
|
|
@ -57,7 +61,7 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve
|
|||
- [x] resolve.test.js (partially done)
|
||||
- [ ] restrictions.test.js
|
||||
- [ ] roots.test.js
|
||||
- [ ] scoped-packages.test.js
|
||||
- [x] scoped-packages.test.js
|
||||
- [x] simple.test.js
|
||||
- [ ] symlink.test.js
|
||||
- [ ] unsafe-cache.test.js
|
||||
|
|
|
|||
|
|
@ -169,13 +169,16 @@ impl Resolver {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn load_index(&self, path: &Path) -> ResolveState {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn load_index(&self, path: &Path, package_json: Option<&PackageJson>) -> ResolveState {
|
||||
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
|
||||
// 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
|
||||
// 3. If X/index.node is a file, load X/index.node as binary addon. STOP
|
||||
for extension in &self.options.extensions {
|
||||
let index_path = path.join("index").with_extension(extension);
|
||||
let mut index_path = path.join("index").with_extension(extension);
|
||||
if let Some(resolved_path) = package_json.and_then(|p| p.resolve(&index_path)) {
|
||||
index_path = resolved_path;
|
||||
}
|
||||
if self.fs.is_file(&index_path) {
|
||||
return Ok(Some(index_path));
|
||||
}
|
||||
|
|
@ -189,7 +192,7 @@ impl Resolver {
|
|||
if self.fs.is_file(&package_json_path) {
|
||||
// a. Parse X/package.json, and look for "main" field.
|
||||
let package_json_string = fs::read_to_string(&package_json_path).unwrap();
|
||||
let package_json = PackageJson::try_from(package_json_string.as_str())
|
||||
let package_json = PackageJson::parse(package_json_path.clone(), &package_json_string)
|
||||
.map_err(|error| ResolveError::from_serde_json_error(package_json_path, &error))?;
|
||||
// b. If "main" is a falsy value, GOTO 2.
|
||||
if let Some(main_field) = &package_json.main {
|
||||
|
|
@ -200,19 +203,22 @@ impl Resolver {
|
|||
return Ok(Some(path));
|
||||
}
|
||||
// e. LOAD_INDEX(M)
|
||||
if let Some(path) = self.load_index(&main_field_path)? {
|
||||
if let Some(path) = self.load_index(&main_field_path, Some(&package_json))? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
// f. LOAD_INDEX(X) DEPRECATED
|
||||
// g. THROW "not found"
|
||||
return Err(ResolveError::NotFound);
|
||||
}
|
||||
// g. THROW "not found"
|
||||
return Err(ResolveError::NotFound);
|
||||
// 2. LOAD_INDEX(X)
|
||||
self.load_index(path, Some(&package_json))
|
||||
} else {
|
||||
// 2. LOAD_INDEX(X)
|
||||
self.load_index(path, None)
|
||||
}
|
||||
// 2. LOAD_INDEX(X)
|
||||
self.load_index(path)
|
||||
}
|
||||
|
||||
fn load_node_modules(&self, start: &Path, module_path: &str) -> ResolveState {
|
||||
fn load_node_modules(&self, start: &Path, request_str: &str) -> ResolveState {
|
||||
const NODE_MODULES: &str = "node_modules";
|
||||
// 1. let DIRS = NODE_MODULES_PATHS(START)
|
||||
let dirs = start
|
||||
|
|
@ -220,10 +226,11 @@ impl Resolver {
|
|||
.filter(|path| path.file_name().is_some_and(|name| name != NODE_MODULES));
|
||||
// 2. for each DIR in DIRS:
|
||||
for dir in dirs {
|
||||
let node_module_path = dir.join(NODE_MODULES);
|
||||
// a. LOAD_PACKAGE_EXPORTS(X, DIR)
|
||||
// b. LOAD_AS_FILE(DIR/X)
|
||||
let node_module_path = dir.join(NODE_MODULES).join(module_path);
|
||||
if !module_path.ends_with('/') {
|
||||
let node_module_path = node_module_path.join(request_str);
|
||||
if !request_str.ends_with('/') {
|
||||
if let Some(path) = self.load_as_file(&node_module_path)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct ResolveOptions {
|
||||
/// A list of alias fields in description files.
|
||||
/// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec).
|
||||
///
|
||||
/// Default `[]`
|
||||
pub alias_fields: Vec<String>,
|
||||
|
||||
/// An object which maps extension to extension aliases
|
||||
///
|
||||
/// Default `{}`
|
||||
|
|
@ -24,6 +30,7 @@ pub struct ResolveOptions {
|
|||
impl Default for ResolveOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
alias_fields: vec![],
|
||||
extension_alias: vec![],
|
||||
enforce_extension: false,
|
||||
extensions: vec![".js".into(), ".json".into(), ".node".into()],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,56 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::path::PathUtil;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PackageJson<'a> {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub main: Option<&'a str>,
|
||||
pub browser: Option<BrowserField<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for PackageJson<'a> {
|
||||
type Error = serde_json::Error;
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum BrowserField<'a> {
|
||||
String(&'a str),
|
||||
Map(HashMap<&'a str, &'a str>),
|
||||
}
|
||||
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
serde_json::from_str(s)
|
||||
impl<'a> PackageJson<'a> {
|
||||
pub fn parse(path: PathBuf, json: &'a str) -> Result<PackageJson<'a>, serde_json::Error> {
|
||||
let mut package_json: PackageJson = serde_json::from_str(json)?;
|
||||
package_json.path = path;
|
||||
Ok(package_json)
|
||||
}
|
||||
|
||||
pub fn resolve(&self, path: &Path) -> Option<PathBuf> {
|
||||
// TODO: return ResolveError if the provided `alias_fields` is not `browser` for future
|
||||
// proof
|
||||
let browser_field = self.browser.as_ref()?;
|
||||
match browser_field {
|
||||
BrowserField::Map(map) => {
|
||||
for (key, value) in map {
|
||||
let resolved_path = self.resolve_browser_field(key, value, path);
|
||||
if resolved_path.is_some() {
|
||||
return resolved_path;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
// TODO: implement <https://github.com/defunctzombie/package-browser-field-spec#alternate-main---basic>
|
||||
BrowserField::String(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_browser_field(&self, key: &str, value: &str, path: &Path) -> Option<PathBuf> {
|
||||
let directory = self.path.parent().unwrap(); // `unwrap`: this is a path to package.json, parent is its containing directory
|
||||
// TODO: cache this join
|
||||
(directory.join(key).normalize() == path).then(|| directory.join(value).normalize())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ pub enum RequestPath<'a> {
|
|||
Absolute(&'a str),
|
||||
/// `./path`, `../path`
|
||||
Relative(&'a str),
|
||||
/// `path`
|
||||
/// `path`, `@scope/path`
|
||||
Module(&'a str),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ mod extension_alias;
|
|||
mod extensions;
|
||||
mod incorrect_description_file;
|
||||
mod resolve;
|
||||
mod scoped_packages;
|
||||
mod simple;
|
||||
|
||||
use std::{env, path::PathBuf};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/scoped-packages.test.js>
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use oxc_resolver::{ResolveError, ResolveOptions, Resolver};
|
||||
|
||||
fn fixture() -> PathBuf {
|
||||
super::fixture().join("scoped")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scoped_packages() -> Result<(), ResolveError> {
|
||||
let f = fixture();
|
||||
|
||||
let options =
|
||||
ResolveOptions { alias_fields: vec!["browser".into()], ..ResolveOptions::default() };
|
||||
|
||||
let resolver = Resolver::new(options);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let pass = [
|
||||
("main field should work", f.clone(), "@scope/pack1", f.join("./node_modules/@scope/pack1/main.js")),
|
||||
("browser field should work", f.clone(), "@scope/pack2", f.join("./node_modules/@scope/pack2/main.js")),
|
||||
("folder request should work", f.clone(), "@scope/pack2/lib", f.join("./node_modules/@scope/pack2/lib/index.js"))
|
||||
];
|
||||
|
||||
for (comment, path, request, expected) in pass {
|
||||
let resolution = resolver.resolve(&f, request)?;
|
||||
let resolved_path = resolution.path();
|
||||
assert_eq!(resolved_path, expected, "{comment} {path:?} {request}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue