diff --git a/Cargo.lock b/Cargo.lock index cc9ac699..41419914 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,12 +340,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.13.0" @@ -688,7 +682,6 @@ dependencies = [ "criterion2", "dashmap", "document-features", - "dunce", "indexmap 2.6.0", "json-strip-comments", "normalize-path", diff --git a/Cargo.toml b/Cargo.toml index 182640a1..c1b1ec07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["node", "resolve", "cjs", "esm", "enhanced-resolve"] license = "MIT" readme = "README.md" repository = "https://github.com/oxc-project/oxc-resolver" -rust-version = "1.70" +rust-version = "1.74" include = ["/src", "/examples", "/benches"] [lib] @@ -70,7 +70,6 @@ serde_json = { version = "1", features = [ "preserve_order", ] } # preserve_order: package_json.exports requires order such as `["require", "import", "default"]` rustc-hash = { version = "2" } -dunce = "1" # Normalize Windows paths to the most compatible format, avoiding UNC where possible once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable. thiserror = "1" json-strip-comments = "1" diff --git a/fixtures/pnpm/longfilename/index.js b/fixtures/pnpm/longfilename/index.js new file mode 100644 index 00000000..7f0f7ce6 --- /dev/null +++ b/fixtures/pnpm/longfilename/index.js @@ -0,0 +1,3 @@ +const test = 'hello world' + +export default test diff --git a/fixtures/pnpm/longfilename/package.json b/fixtures/pnpm/longfilename/package.json new file mode 100644 index 00000000..21b5e76f --- /dev/null +++ b/fixtures/pnpm/longfilename/package.json @@ -0,0 +1,7 @@ +{ + "name": "@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "private": true, + "version": "0.0.0", + "main": "index.js", + "type": "module" +} diff --git a/fixtures/pnpm/package.json b/fixtures/pnpm/package.json index 12fd900b..577d8a3c 100644 --- a/fixtures/pnpm/package.json +++ b/fixtures/pnpm/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "devDependencies": { + "@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "file:./longfilename", "axios": "1.6.2", "ipaddr.js": "2.2.0", "postcss": "8.4.33", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6f5501b..30d0e6f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: fixtures/pnpm: devDependencies: + '@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': + specifier: file:./longfilename + version: file:fixtures/pnpm/longfilename axios: specifier: 1.6.2 version: 1.6.2 @@ -546,6 +549,9 @@ packages: '@octokit/types@13.6.1': resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==} + '@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@file:fixtures/pnpm/longfilename': + resolution: {directory: fixtures/pnpm/longfilename, type: directory} + '@rollup/pluginutils@4.2.1': resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -1907,6 +1913,8 @@ snapshots: dependencies: '@octokit/openapi-types': 22.2.0 + '@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@file:fixtures/pnpm/longfilename': {} + '@rollup/pluginutils@4.2.1': dependencies: estree-walker: 2.0.2 diff --git a/src/file_system.rs b/src/file_system.rs index 0ba7da73..cb1bfdf5 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -7,6 +7,11 @@ use cfg_if::cfg_if; #[cfg(feature = "yarn_pnp")] use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache}; +#[cfg(windows)] +const UNC_PATH_PREFIX: &[u8] = b"\\\\?\\UNC\\"; +#[cfg(windows)] +const LONG_PATH_PREFIX: &[u8] = b"\\\\?\\"; + /// File System abstraction used for `ResolverGeneric` pub trait FileSystem: Send + Sync { /// See [std::fs::read_to_string] @@ -164,55 +169,12 @@ impl FileSystem for FileSystemOs { cfg_if! { if #[cfg(feature = "yarn_pnp")] { match VPath::from(path)? { - VPath::Zip(info) => { - dunce::canonicalize(info.physical_base_path().join(info.zip_path)) - } - VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()), - VPath::Native(path) => dunce::canonicalize(path), + VPath::Zip(info) => fast_canonicalize(info.physical_base_path().join(info.zip_path)), + VPath::Virtual(info) => fast_canonicalize(info.physical_base_path()), + VPath::Native(path) => fast_canonicalize(path), } - } else if #[cfg(windows)] { - dunce::canonicalize(path) } else { - use std::path::Component; - let mut path_buf = path.to_path_buf(); - loop { - let link = fs::read_link(&path_buf)?; - path_buf.pop(); - if fs::symlink_metadata(&path_buf)?.is_symlink() - { - path_buf = self.canonicalize(path_buf.as_path())?; - } - for component in link.components() { - match component { - Component::ParentDir => { - path_buf.pop(); - } - Component::Normal(seg) => { - #[cfg(target_family = "wasm")] - { - // Need to trim the extra \0 introduces by https://github.com/nodejs/uvwasi/issues/262 - path_buf.push(seg.to_string_lossy().trim_end_matches('\0')); - } - #[cfg(not(target_family = "wasm"))] - { - path_buf.push(seg); - } - } - Component::RootDir => { - path_buf = PathBuf::from("/"); - } - Component::CurDir | Component::Prefix(_) => {} - } - if fs::symlink_metadata(&path_buf)?.is_symlink() - { - path_buf = self.canonicalize(path_buf.as_path())?; - } - } - if !fs::symlink_metadata(&path_buf)?.is_symlink() { - break; - } - } - Ok(path_buf) + fast_canonicalize(path) } } } @@ -227,3 +189,77 @@ fn metadata() { ); let _ = meta; } + +#[inline] +fn fast_canonicalize>(path: P) -> io::Result { + #[cfg(windows)] + { + // fs::canonicalize was faster on Windows (https://github.com/oxc-project/oxc-resolver/pull/306) + Ok(node_compatible_raw_canonicalize(fs::canonicalize(path)?)) + } + #[cfg(not(windows))] + { + fast_canonicalize_non_windows(path.as_ref().to_path_buf()) + } +} + +#[inline] +#[cfg(not(windows))] +// This is A faster fs::canonicalize implementation by reducing the number of syscalls +fn fast_canonicalize_non_windows(path: PathBuf) -> io::Result { + use std::path::Component; + let mut path_buf = path; + + loop { + let link = fs::read_link(&path_buf)?; + path_buf.pop(); + if fs::symlink_metadata(&path_buf)?.is_symlink() { + path_buf = fast_canonicalize(path_buf)?; + } + for component in link.components() { + match component { + Component::ParentDir => { + path_buf.pop(); + } + Component::Normal(seg) => { + #[cfg(target_family = "wasm")] + { + // Need to trim the extra \0 introduces by https://github.com/nodejs/uvwasi/issues/262 + path_buf.push(seg.to_string_lossy().trim_end_matches('\0')); + } + #[cfg(not(target_family = "wasm"))] + { + path_buf.push(seg); + } + } + Component::RootDir => { + path_buf = PathBuf::from("/"); + } + Component::CurDir | Component::Prefix(_) => {} + } + + if fs::symlink_metadata(&path_buf)?.is_symlink() { + path_buf = fast_canonicalize(path_buf)?; + } + } + if !fs::symlink_metadata(&path_buf)?.is_symlink() { + break; + } + } + Ok(path_buf) +} + +#[cfg(windows)] +fn node_compatible_raw_canonicalize>(path: P) -> PathBuf { + let path_bytes = path.as_ref().as_os_str().as_encoded_bytes(); + path_bytes + .strip_prefix(UNC_PATH_PREFIX) + .or_else(|| path_bytes.strip_prefix(LONG_PATH_PREFIX)) + .map_or_else( + || path.as_ref().to_path_buf(), + |p| { + // SAFETY: `as_encoded_bytes` ensures `p` is valid path bytes + unsafe { PathBuf::from(std::ffi::OsStr::from_encoded_bytes_unchecked(p)) } + }, + ) +} diff --git a/tests/resolve_test.rs b/tests/resolve_test.rs index 3c250569..8b4341d1 100644 --- a/tests/resolve_test.rs +++ b/tests/resolve_test.rs @@ -205,3 +205,13 @@ fn nested_symlinks() { Ok(dir.join("nm/index.js")) ); } + +#[test] +fn windows_symlinked_longfilename() { + let dir = dir(); + let path = dir.join("fixtures/pnpm"); + let module_path = dir.join("node_modules/.pnpm/@oxc-resolver+test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_m464apeldykmdsyzlfhtrggk24/node_modules/@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/index.js"); + + let resolution = Resolver::new(ResolveOptions::default()).resolve(&path, "@oxc-resolver/test-longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").map(|r| r.full_path()); + assert_eq!(resolution, Ok(module_path)); +}