feat(resolver): accept different file system implementations (#562)

This commit is contained in:
Boshen 2023-07-17 18:29:41 +08:00 committed by GitHub
parent cac4e73461
commit d410d1a2d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 234 additions and 76 deletions

7
Cargo.lock generated
View file

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

View file

@ -18,6 +18,7 @@ serde_json = { workspace = true }
[dev-dependencies]
static_assertions = { workspace = true }
criterion = { workspace = true }
vfs = "0.9.0"
[[bench]]
name = "resolver"

View file

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

View 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
}
}

View file

@ -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() })
}
}

View 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));
}
}

View 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()
}
}

View 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());
}

View file

@ -1,3 +1,4 @@
mod alias;
mod browser_field;
mod extension_alias;
mod extensions;

View 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))
}
}

View 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]