mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 20:32:10 +00:00
feat(resolver): accept different file system implementations (#562)
This commit is contained in:
parent
cac4e73461
commit
d410d1a2d7
11 changed files with 234 additions and 76 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -1397,6 +1397,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"static_assertions",
|
||||
"vfs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2265,6 +2266,12 @@ version = "0.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "vfs"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded573c92d7b32013bdab82676be59f56106895e837504568f32804980ec7868"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.3"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ serde_json = { workspace = true }
|
|||
[dev-dependencies]
|
||||
static_assertions = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
vfs = "0.9.0"
|
||||
|
||||
[[bench]]
|
||||
name = "resolver"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
| | exportsFields | ["exports"] | A list of exports fields in description files |
|
||||
| ✅ | extensions | [".js", ".json", ".node"] | A list of extensions which should be tried for files |
|
||||
| | fallback | [] | Same as `alias`, but only used if default resolving fails |
|
||||
| | fileSystem | | The file system which should be used |
|
||||
| ✅ | 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 |
|
||||
|
|
|
|||
56
crates/oxc_resolver/src/cache.rs
Normal file
56
crates/oxc_resolver/src/cache.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
use crate::{package_json::PackageJson, FileMetadata, FileSystem, ResolveError};
|
||||
|
||||
pub struct Cache<Fs> {
|
||||
fs: Fs,
|
||||
cache: DashMap<Box<Path>, Option<FileMetadata>>,
|
||||
package_json_cache: DashMap<Box<Path>, Result<Arc<PackageJson>, ResolveError>>,
|
||||
}
|
||||
|
||||
impl<Fs: FileSystem> Default for Cache<Fs> {
|
||||
fn default() -> Self {
|
||||
Self { fs: Fs::default(), cache: DashMap::new(), package_json_cache: DashMap::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fs: FileSystem> Cache<Fs> {
|
||||
pub fn new(fs: Fs) -> Self {
|
||||
Self { fs, ..Self::default() }
|
||||
}
|
||||
|
||||
fn metadata_cached(&self, path: &Path) -> Option<FileMetadata> {
|
||||
if let Some(result) = self.cache.get(path) {
|
||||
return *result;
|
||||
}
|
||||
let file_metadata = self.fs.metadata(path).ok();
|
||||
self.cache.insert(path.to_path_buf().into_boxed_path(), file_metadata);
|
||||
file_metadata
|
||||
}
|
||||
|
||||
pub fn is_file(&self, path: &Path) -> bool {
|
||||
self.metadata_cached(path).is_some_and(|m| m.is_file)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// * [ResolveError::JSONError]
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// * Failed to read the file (TODO: remove this)
|
||||
pub fn read_package_json(&self, path: &Path) -> Result<Arc<PackageJson>, ResolveError> {
|
||||
if let Some(result) = self.package_json_cache.get(path) {
|
||||
return result.value().clone();
|
||||
}
|
||||
// TODO: handle file read error
|
||||
let package_json_string = self.fs.read_to_string(path).unwrap();
|
||||
let result = PackageJson::parse(path.to_path_buf(), &package_json_string)
|
||||
.map(Arc::new)
|
||||
.map_err(|error| ResolveError::from_serde_json_error(path.to_path_buf(), &error));
|
||||
self.package_json_cache.insert(path.to_path_buf().into_boxed_path(), result.clone());
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,38 @@
|
|||
use std::{fs, path::Path, sync::Arc};
|
||||
use std::{fs, io, path::Path};
|
||||
|
||||
use dashmap::DashMap;
|
||||
pub trait FileSystem: Default + Send + Sync {
|
||||
/// # Errors
|
||||
///
|
||||
/// * Any [io::Error]
|
||||
fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<String>;
|
||||
|
||||
use crate::{package_json::PackageJson, ResolveError};
|
||||
/// # Errors
|
||||
///
|
||||
/// * Any [io::Error]
|
||||
fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FileMetadata {
|
||||
is_file: bool,
|
||||
pub(crate) is_file: bool,
|
||||
}
|
||||
|
||||
/// [File System](https://doc.rust-lang.org/stable/std/fs/) with caching
|
||||
impl FileMetadata {
|
||||
pub fn new(is_file: bool) -> Self {
|
||||
Self { is_file }
|
||||
}
|
||||
}
|
||||
|
||||
/// Operating System
|
||||
#[derive(Default)]
|
||||
pub struct FileSystem {
|
||||
cache: DashMap<Box<Path>, Option<FileMetadata>>,
|
||||
package_json_cache: DashMap<Box<Path>, Result<Arc<PackageJson>, ResolveError>>,
|
||||
}
|
||||
pub struct FileSystemOs;
|
||||
|
||||
impl FileSystem {
|
||||
/// <https://doc.rust-lang.org/stable/std/fs/fn.metadata.html>
|
||||
pub fn metadata(&self, path: &Path) -> Option<FileMetadata> {
|
||||
if let Some(result) = self.cache.get(path) {
|
||||
return *result;
|
||||
}
|
||||
let file_metadata =
|
||||
fs::metadata(path).ok().map(|metadata| FileMetadata { is_file: metadata.is_file() });
|
||||
self.cache.insert(path.to_path_buf().into_boxed_path(), file_metadata);
|
||||
file_metadata
|
||||
impl FileSystem for FileSystemOs {
|
||||
fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
|
||||
fs::read_to_string(path)
|
||||
}
|
||||
|
||||
pub fn is_file(&self, path: &Path) -> bool {
|
||||
self.metadata(path).is_some_and(|m| m.is_file)
|
||||
}
|
||||
|
||||
pub fn read_package_json(&self, path: &Path) -> Result<Arc<PackageJson>, ResolveError> {
|
||||
if let Some(result) = self.package_json_cache.get(path) {
|
||||
return result.value().clone();
|
||||
}
|
||||
let package_json_string = fs::read_to_string(path).unwrap();
|
||||
let result = PackageJson::parse(path.to_path_buf(), &package_json_string)
|
||||
.map(Arc::new)
|
||||
.map_err(|error| ResolveError::from_serde_json_error(path.to_path_buf(), &error));
|
||||
self.package_json_cache.insert(path.to_path_buf().into_boxed_path(), result.clone());
|
||||
result
|
||||
fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata> {
|
||||
fs::metadata(path).map(|metadata| FileMetadata { is_file: metadata.is_file() })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@
|
|||
//! * Algorithm adapted from [Node.js Module Resolution Algorithm](https://nodejs.org/api/modules.html#all-together) and [cjs loader](https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js)
|
||||
//! * Some code adapted from [parcel-resolver](https://github.com/parcel-bundler/parcel/blob/v2/packages/utils/node-resolver-rs)
|
||||
|
||||
mod cache;
|
||||
mod error;
|
||||
mod file_system;
|
||||
mod options;
|
||||
mod package_json;
|
||||
mod path;
|
||||
mod request;
|
||||
mod resolution;
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
|
|
@ -19,11 +21,13 @@ use std::{
|
|||
};
|
||||
|
||||
pub use crate::{
|
||||
cache::Cache,
|
||||
error::{JSONError, ResolveError},
|
||||
file_system::{FileMetadata, FileSystem, FileSystemOs},
|
||||
options::ResolveOptions,
|
||||
resolution::Resolution,
|
||||
};
|
||||
use crate::{
|
||||
file_system::FileSystem,
|
||||
path::PathUtil,
|
||||
request::{Request, RequestPath},
|
||||
};
|
||||
|
|
@ -31,49 +35,27 @@ use crate::{
|
|||
pub type ResolveResult = Result<Resolution, ResolveError>;
|
||||
type ResolveState = Result<Option<PathBuf>, ResolveError>;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Resolution {
|
||||
path: PathBuf,
|
||||
/// Resolver with the current operating system as the file system
|
||||
pub type Resolver = ResolverGeneric<FileSystemOs>;
|
||||
|
||||
/// path query `?query`, contains `?`.
|
||||
query: Option<String>,
|
||||
|
||||
/// path fragment `#query`, contains `#`.
|
||||
fragment: Option<String>,
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn into_path_buf(self) -> PathBuf {
|
||||
self.path
|
||||
}
|
||||
|
||||
pub fn query(&self) -> Option<&str> {
|
||||
self.query.as_deref()
|
||||
}
|
||||
|
||||
pub fn fragment(&self) -> Option<&str> {
|
||||
self.fragment.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Resolver {
|
||||
pub struct ResolverGeneric<Fs> {
|
||||
options: ResolveOptions,
|
||||
fs: FileSystem,
|
||||
cache: Cache<Fs>,
|
||||
}
|
||||
|
||||
impl Default for Resolver {
|
||||
impl<Fs: FileSystem> Default for ResolverGeneric<Fs> {
|
||||
fn default() -> Self {
|
||||
Self::new(ResolveOptions::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
||||
pub fn new(options: ResolveOptions) -> Self {
|
||||
Self { options: options.sanitize(), fs: FileSystem::default() }
|
||||
Self { options: options.sanitize(), cache: Cache::default() }
|
||||
}
|
||||
|
||||
pub fn new_with_file_system(options: ResolveOptions, file_system: Fs) -> Self {
|
||||
Self { cache: Cache::new(file_system), ..Self::new(options) }
|
||||
}
|
||||
|
||||
/// Resolve `request` at `path`
|
||||
|
|
@ -160,7 +142,7 @@ impl Resolver {
|
|||
}
|
||||
|
||||
// 1. If X is a file, load X as its file extension format. STOP
|
||||
if self.fs.is_file(path) {
|
||||
if self.cache.is_file(path) {
|
||||
return Ok(Some(path.to_path_buf()));
|
||||
}
|
||||
// 2. If X.js is a file, load X.js as JavaScript text. STOP
|
||||
|
|
@ -168,7 +150,7 @@ impl Resolver {
|
|||
// 4. If X.node is a file, load X.node as binary addon. STOP
|
||||
for extension in &self.options.extensions {
|
||||
let path_with_extension = path.with_extension(extension);
|
||||
if self.fs.is_file(&path_with_extension) {
|
||||
if self.cache.is_file(&path_with_extension) {
|
||||
return Ok(Some(path_with_extension));
|
||||
}
|
||||
}
|
||||
|
|
@ -182,7 +164,7 @@ impl Resolver {
|
|||
// 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);
|
||||
if self.fs.is_file(&index_path) {
|
||||
if self.cache.is_file(&index_path) {
|
||||
return Ok(Some(index_path));
|
||||
}
|
||||
}
|
||||
|
|
@ -192,9 +174,9 @@ impl Resolver {
|
|||
fn load_as_directory(&self, path: &Path) -> ResolveState {
|
||||
// 1. If X/package.json is a file,
|
||||
let package_json_path = path.join("package.json");
|
||||
if self.fs.is_file(&package_json_path) {
|
||||
if self.cache.is_file(&package_json_path) {
|
||||
// a. Parse X/package.json, and look for "main" field.
|
||||
let package_json = self.fs.read_package_json(&package_json_path)?;
|
||||
let package_json = self.cache.read_package_json(&package_json_path)?;
|
||||
// b. If "main" is a falsy value, GOTO 2.
|
||||
if let Some(main_field) = &package_json.main {
|
||||
// c. let M = X + (json main field)
|
||||
|
|
@ -251,8 +233,8 @@ impl Resolver {
|
|||
fn load_package_self(&self, path: &Path, request_str: &str) -> ResolveState {
|
||||
for dir in path.ancestors() {
|
||||
let package_json_path = dir.join("package.json");
|
||||
if self.fs.is_file(&package_json_path) {
|
||||
let package_json = self.fs.read_package_json(&package_json_path)?;
|
||||
if self.cache.is_file(&package_json_path) {
|
||||
let package_json = self.cache.read_package_json(&package_json_path)?;
|
||||
if let Some(request_str) =
|
||||
package_json.resolve_request(path, request_str, &self.options.extensions)?
|
||||
{
|
||||
|
|
@ -282,7 +264,7 @@ impl Resolver {
|
|||
};
|
||||
for extension in extensions {
|
||||
let path_with_extension = path.with_extension(extension);
|
||||
if self.fs.is_file(&path_with_extension) {
|
||||
if self.cache.is_file(&path_with_extension) {
|
||||
return Ok(Some(path_with_extension));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
crates/oxc_resolver/src/resolution.rs
Normal file
30
crates/oxc_resolver/src/resolution.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Resolution {
|
||||
pub(crate) path: PathBuf,
|
||||
|
||||
/// path query `?query`, contains `?`.
|
||||
pub(crate) query: Option<String>,
|
||||
|
||||
/// path fragment `#query`, contains `#`.
|
||||
pub(crate) fragment: Option<String>,
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn into_path_buf(self) -> PathBuf {
|
||||
self.path
|
||||
}
|
||||
|
||||
pub fn query(&self) -> Option<&str> {
|
||||
self.query.as_deref()
|
||||
}
|
||||
|
||||
pub fn fragment(&self) -> Option<&str> {
|
||||
self.fragment.as_deref()
|
||||
}
|
||||
}
|
||||
27
crates/oxc_resolver/tests/enhanced_resolve/test/alias.rs
Normal file
27
crates/oxc_resolver/tests/enhanced_resolve/test/alias.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/alias.test.js>
|
||||
|
||||
use oxc_resolver::{ResolveOptions, ResolverGeneric};
|
||||
|
||||
use crate::MemoryFS;
|
||||
|
||||
#[test]
|
||||
fn alias() {
|
||||
let file_system = MemoryFS::new(&[
|
||||
("/a/index", ""),
|
||||
("/a/dir/index", ""),
|
||||
("/recursive/index", ""),
|
||||
("/recursive/dir/index", ""),
|
||||
("/b/index", ""),
|
||||
("/b/dir/index", ""),
|
||||
("/c/index", ""),
|
||||
("/c/dir/index", ""),
|
||||
("/d/index.js", ""),
|
||||
("/d/dir/.empty", ""),
|
||||
("/e/index", ""),
|
||||
("/e/anotherDir/index", ""),
|
||||
("/e/dir/file", ""),
|
||||
]);
|
||||
let options = ResolveOptions::default();
|
||||
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(options, file_system);
|
||||
assert!(resolver.resolve("/a/index", ".").is_ok());
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod alias;
|
||||
mod browser_field;
|
||||
mod extension_alias;
|
||||
mod extensions;
|
||||
|
|
|
|||
60
crates/oxc_resolver/tests/memory_fs.rs
Normal file
60
crates/oxc_resolver/tests/memory_fs.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use std::{io, path::Path};
|
||||
|
||||
use oxc_resolver::{FileMetadata, FileSystem};
|
||||
|
||||
pub struct MemoryFS {
|
||||
fs: vfs::MemoryFS,
|
||||
}
|
||||
|
||||
impl Default for MemoryFS {
|
||||
fn default() -> Self {
|
||||
let fs = vfs::MemoryFS::new();
|
||||
Self { fs }
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryFS {
|
||||
/// # Panics
|
||||
///
|
||||
/// * Fails to create directory
|
||||
/// * Fails to write file
|
||||
pub fn new(data: &[(&'static str, &'static str)]) -> Self {
|
||||
use vfs::FileSystem;
|
||||
let fs = vfs::MemoryFS::default();
|
||||
for (path, string) in data {
|
||||
// Create all parent directories
|
||||
for path in Path::new(path).ancestors().collect::<Vec<_>>().iter().rev() {
|
||||
let path = path.to_string_lossy();
|
||||
if !fs.exists(path.as_ref()).unwrap() {
|
||||
fs.create_dir(path.as_ref()).unwrap();
|
||||
}
|
||||
}
|
||||
let mut file = fs.create_file(path).unwrap();
|
||||
file.write_all(string.as_bytes()).unwrap();
|
||||
}
|
||||
Self { fs }
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for MemoryFS {
|
||||
fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
|
||||
use vfs::FileSystem;
|
||||
let mut file = self
|
||||
.fs
|
||||
.open_file(path.as_ref().to_string_lossy().as_ref())
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?;
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer).unwrap();
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata> {
|
||||
use vfs::FileSystem;
|
||||
let metadata = self
|
||||
.fs
|
||||
.metadata(path.as_ref().to_string_lossy().as_ref())
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?;
|
||||
let is_file = metadata.file_type == vfs::VfsFileType::File;
|
||||
Ok(FileMetadata::new(is_file))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
mod enhanced_resolve;
|
||||
mod memory_fs;
|
||||
|
||||
use std::{env, sync::Arc, thread};
|
||||
|
||||
pub(crate) use memory_fs::MemoryFS;
|
||||
use oxc_resolver::Resolver;
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue