feat(resolver): implement symlinks (#582)

This commit is contained in:
Boshen 2023-07-21 19:10:59 +08:00 committed by GitHub
parent 3c5333c828
commit 585e48fe9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 191 additions and 10 deletions

1
Cargo.lock generated
View file

@ -1490,6 +1490,7 @@ version = "0.0.0"
dependencies = [
"criterion",
"dashmap",
"dunce",
"jemallocator",
"mimalloc",
"nodejs-resolver",

View file

@ -15,6 +15,7 @@ dashmap = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
rustc-hash = { workspace = true }
dunce = "1.0.4"
[dev-dependencies]
static_assertions = { workspace = true }

View file

@ -32,7 +32,7 @@
| ✅ | preferAbsolute | false | Prefer to resolve server-relative urls as absolute paths before falling back to resolve in roots |
| | restrictions | [] | A list of resolve restrictions |
| ✅ | roots | [] | A list of root paths |
| | symlinks | true | Whether to resolve symlinks to their symlinked location |
| | symlinks | true | Whether to resolve symlinks to their symlinked location |
| | unsafeCache | false | Use this cache object to unsafely cache the successful requests
## Test
@ -64,6 +64,6 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve
- [x] roots.test.js (need to add resolveToContext)
- [x] scoped-packages.test.js
- [x] simple.test.js
- [ ] symlink.test.js
- [x] symlink.test.js
- [ ] unsafe-cache.test.js
- [ ] yield.test.js

View file

@ -1,4 +1,8 @@
use std::{hash::BuildHasherDefault, path::Path, sync::Arc};
use std::{
hash::BuildHasherDefault,
path::{Path, PathBuf},
sync::Arc,
};
use dashmap::DashMap;
use rustc_hash::FxHasher;
@ -36,7 +40,7 @@ impl<Fs: FileSystem> Cache<Fs> {
if let Some(result) = self.cache.get(path) {
return *result;
}
let file_metadata = self.fs.symlink_metadata(path).ok();
let file_metadata = self.fs.metadata(path).ok();
self.cache.insert(path.to_path_buf().into_boxed_path(), file_metadata);
file_metadata
}
@ -45,6 +49,10 @@ impl<Fs: FileSystem> Cache<Fs> {
self.metadata_cached(path).is_some_and(|m| m.is_file)
}
pub fn canonicalize(&self, path: PathBuf) -> PathBuf {
self.fs.canonicalize(&path).unwrap_or(path)
}
/// # Errors
///
/// * [ResolveError::JSONError]

View file

@ -1,11 +1,18 @@
use std::{fs, io, path::Path};
use std::{
fs, io,
path::{Path, PathBuf},
};
pub trait FileSystem: Default + Send + Sync {
/// See [std::fs::read_to_string]
///
/// # Errors
///
/// * Any [io::Error]
fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<String>;
/// See [std::fs::metadata]
///
/// # Errors
///
/// This function will return an error in the following situations, but is not
@ -13,7 +20,18 @@ pub trait FileSystem: Default + Send + Sync {
///
/// * The user lacks permissions to perform `metadata` call on `path`.
/// * `path` does not exist.
fn symlink_metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata>;
fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata>;
/// See [std::fs::canonicalize]
///
/// # Errors
///
/// This function will return an error in the following situations, but is not
/// limited to just these cases:
///
/// * `path` does not exist.
/// * A non-final component in path is not a directory.
fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf>;
}
#[derive(Debug, Clone, Copy)]
@ -36,7 +54,11 @@ impl FileSystem for FileSystemOs {
fs::read_to_string(path)
}
fn symlink_metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata> {
fs::symlink_metadata(path).map(|metadata| FileMetadata { is_file: metadata.is_file() })
fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata> {
fs::metadata(path).map(|metadata| FileMetadata { is_file: metadata.is_file() })
}
fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
dunce::canonicalize(path)
}
}

View file

@ -89,6 +89,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
result?
}
};
let path = self.load_symlink(path);
Ok(Resolution {
path,
query: request.query.map(ToString::to_string),
@ -182,6 +183,10 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Ok(None)
}
fn load_symlink(&self, path: PathBuf) -> PathBuf {
if self.options.symlinks { self.cache.canonicalize(path) } else { path }
}
#[allow(clippy::unnecessary_wraps)]
fn load_index(&self, path: &Path) -> ResolveState {
for main_field in &self.options.main_files {

View file

@ -76,6 +76,13 @@ pub struct ResolveOptions {
///
/// Default `[]`
pub roots: Vec<PathBuf>,
/// Whether to resolve symlinks to their symlinked location.
/// When enabled, symlinked resources are resolved to their real path, not their symlinked location.
/// Note that this may cause module resolution to fail when using tools that symlink packages (like npm link).
///
/// Default `true`
pub symlinks: bool,
}
impl Default for ResolveOptions {
@ -93,6 +100,7 @@ impl Default for ResolveOptions {
prefer_relative: false,
prefer_absolute: false,
roots: vec![],
symlinks: true,
}
}
}

View file

@ -0,0 +1,2 @@
# created by symlink.rs
/temp

View file

@ -8,6 +8,7 @@ mod resolve;
mod roots;
mod scoped_packages;
mod simple;
mod symlink;
use std::{env, path::PathBuf};

View file

@ -0,0 +1,126 @@
use std::{env, fs, io, path::Path};
use oxc_resolver::{Resolution, ResolveOptions, Resolver};
#[derive(Debug, Clone, Copy)]
enum FileType {
File,
Dir,
}
#[allow(unused_variables)]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(
original: P,
link: Q,
file_type: FileType,
) -> io::Result<()> {
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(original, link)
}
#[cfg(target_family = "windows")]
match file_type {
FileType::File => std::os::windows::fs::symlink_file(original, link),
FileType::Dir => std::os::windows::fs::symlink_dir(original, link),
}
}
fn init(dirname: &Path, temp_path: &Path) -> io::Result<()> {
if temp_path.exists() {
_ = fs::remove_dir_all(temp_path);
}
fs::create_dir(temp_path)?;
symlink(dirname.join("../lib/index.js"), temp_path.join("test"), FileType::File)?;
symlink(dirname.join("../lib"), temp_path.join("test2"), FileType::Dir)?;
fs::remove_file(temp_path.join("test"))?;
fs::remove_file(temp_path.join("test2"))?;
fs::remove_dir(temp_path)
}
fn create_symlinks(dirname: &Path, temp_path: &Path) -> io::Result<()> {
fs::create_dir(temp_path).unwrap();
symlink(
dirname.join("../lib/index.js").canonicalize().unwrap(),
temp_path.join("index.js"),
FileType::File,
)?;
symlink(dirname.join("../lib").canonicalize().unwrap(), temp_path.join("lib"), FileType::Dir)?;
symlink(dirname.join("..").canonicalize().unwrap(), temp_path.join("this"), FileType::Dir)?;
symlink(temp_path.join("this"), temp_path.join("that"), FileType::Dir)?;
symlink(Path::new("../../lib/index.js"), temp_path.join("node.relative.js"), FileType::File)?;
symlink(
Path::new("./node.relative.js"),
temp_path.join("node.relative.sym.js"),
FileType::File,
)?;
Ok(())
}
fn cleanup_symlinks(temp_path: &Path) {
_ = fs::remove_dir_all(temp_path);
}
#[test]
fn test() -> io::Result<()> {
let root = env::current_dir().unwrap().join("tests/enhanced_resolve");
let dirname = root.join("test");
let temp_path = dirname.join("temp");
if !temp_path.exists() {
let is_admin = init(&dirname, &temp_path).is_ok();
if !is_admin {
return Ok(());
}
if let Err(err) = create_symlinks(&dirname, &temp_path) {
cleanup_symlinks(&temp_path);
return Err(err);
}
}
let resolver_without_symlinks =
Resolver::new(ResolveOptions { symlinks: false, ..ResolveOptions::default() });
let resolver_with_symlinks = Resolver::default();
#[rustfmt::skip]
let pass = [
("with a symlink to a file", temp_path.clone(), "./index.js"),
("with a relative symlink to a file", temp_path.clone(), "./node.relative.js"),
("with a relative symlink to a symlink to a file", temp_path.clone(), "./node.relative.sym.js"),
("with a symlink to a directory 1", temp_path.clone(), "./lib/index.js"),
("with a symlink to a directory 2", temp_path.clone(), "./this/lib/index.js"),
("with multiple symlinks in the path 1", temp_path.clone(), "./this/test/temp/index.js"),
("with multiple symlinks in the path 2", temp_path.clone(), "./this/test/temp/lib/index.js"),
("with multiple symlinks in the path 3", temp_path.clone(), "./this/test/temp/this/lib/index.js"),
("with a symlink to a directory 2 (chained)", temp_path.clone(), "./that/lib/index.js"),
("with multiple symlinks in the path 1 (chained)", temp_path.clone(), "./that/test/temp/index.js"),
("with multiple symlinks in the path 2 (chained)", temp_path.clone(), "./that/test/temp/lib/index.js"),
("with multiple symlinks in the path 3 (chained)", temp_path.clone(), "./that/test/temp/that/lib/index.js"),
("with symlinked directory as context 1", temp_path.join( "lib"), "./index.js"),
("with symlinked directory as context 2", temp_path.join( "this"), "./lib/index.js"),
("with symlinked directory as context and in path", temp_path.join( "this"), "./test/temp/lib/index.js"),
("with symlinked directory in context path", temp_path.join( "this/lib"), "./index.js"),
("with symlinked directory in context path and symlinked file", temp_path.join( "this/test"), "./temp/index.js"),
("with symlinked directory in context path and symlinked directory", temp_path.join( "this/test"), "./temp/lib/index.js"),
("with symlinked directory as context 2 (chained)", temp_path.join( "that"), "./lib/index.js"),
("with symlinked directory as context and in path (chained)", temp_path.join( "that"), "./test/temp/lib/index.js"),
("with symlinked directory in context path (chained)", temp_path.join( "that/lib"), "./index.js"),
("with symlinked directory in context path and symlinked file (chained)", temp_path.join( "that/test"), "./temp/index.js"),
("with symlinked directory in context path and symlinked directory (chained)", temp_path.join( "that/test"), "./temp/lib/index.js")
];
for (comment, path, request) in pass {
let filename = resolver_with_symlinks.resolve(&path, request).map_or_else(
|err| {
panic!("{err:?} {comment} {path:?} {request}");
},
Resolution::full_path,
);
assert_eq!(filename, root.join("lib/index.js"));
let resolved_path =
resolver_without_symlinks.resolve(&path, request).map(Resolution::full_path);
assert_eq!(resolved_path, Ok(path.join(request)));
}
Ok(())
}

View file

@ -1,4 +1,7 @@
use std::{io, path::Path};
use std::{
io,
path::{Path, PathBuf},
};
use oxc_resolver::{FileMetadata, FileSystem};
@ -48,7 +51,7 @@ impl FileSystem for MemoryFS {
Ok(buffer)
}
fn symlink_metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata> {
fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<FileMetadata> {
use vfs::FileSystem;
let metadata = self
.fs
@ -57,4 +60,8 @@ impl FileSystem for MemoryFS {
let is_file = metadata.file_type == vfs::VfsFileType::File;
Ok(FileMetadata::new(is_file))
}
fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
Ok(path.as_ref().to_path_buf())
}
}