// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use deno_core::anyhow::Context; use deno_core::error::uri_error; use deno_core::error::AnyError; pub use deno_core::normalize_path; use deno_core::ModuleSpecifier; use deno_runtime::deno_crypto::rand; use deno_runtime::deno_node::PathClean; use std::borrow::Cow; use std::env::current_dir; use std::fs::OpenOptions; use std::io::Error; use std::io::ErrorKind; use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::time::Duration; use walkdir::WalkDir; pub fn atomic_write_file>( filename: &Path, data: T, mode: u32, ) -> std::io::Result<()> { let rand: String = (0..4) .map(|_| format!("{:02x}", rand::random::())) .collect(); let extension = format!("{}.tmp", rand); let tmp_file = filename.with_extension(extension); write_file(&tmp_file, data, mode)?; std::fs::rename(tmp_file, filename)?; Ok(()) } pub fn write_file>( filename: &Path, data: T, mode: u32, ) -> std::io::Result<()> { write_file_2(filename, data, true, mode, true, false) } pub fn write_file_2>( filename: &Path, data: T, update_mode: bool, mode: u32, is_create: bool, is_append: bool, ) -> std::io::Result<()> { let mut file = OpenOptions::new() .read(false) .write(true) .append(is_append) .truncate(!is_append) .create(is_create) .open(filename)?; if update_mode { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mode = mode & 0o777; let permissions = PermissionsExt::from_mode(mode); file.set_permissions(permissions)?; } #[cfg(not(unix))] let _ = mode; } file.write_all(data.as_ref()) } /// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. pub fn canonicalize_path(path: &Path) -> Result { let path = path.canonicalize()?; #[cfg(windows)] return Ok(strip_unc_prefix(path)); #[cfg(not(windows))] return Ok(path); } /// Canonicalizes a path which might be non-existent by going up the /// ancestors until it finds a directory that exists, canonicalizes /// that path, then adds back the remaining path components. /// /// Note: When using this, you should be aware that a symlink may /// subsequently be created along this path by some other code. pub fn canonicalize_path_maybe_not_exists( path: &Path, ) -> Result { let path = path.to_path_buf().clean(); let mut path = path.as_path(); let mut names_stack = Vec::new(); loop { match canonicalize_path(path) { Ok(mut canonicalized_path) => { for name in names_stack.into_iter().rev() { canonicalized_path = canonicalized_path.join(name); } return Ok(canonicalized_path); } Err(err) if err.kind() == ErrorKind::NotFound => { names_stack.push(path.file_name().unwrap()); path = path.parent().unwrap(); } Err(err) => return Err(err), } } } #[cfg(windows)] fn strip_unc_prefix(path: PathBuf) -> PathBuf { use std::path::Component; use std::path::Prefix; let mut components = path.components(); match components.next() { Some(Component::Prefix(prefix)) => { match prefix.kind() { // \\?\device Prefix::Verbatim(device) => { let mut path = PathBuf::new(); path.push(format!(r"\\{}\", device.to_string_lossy())); path.extend(components.filter(|c| !matches!(c, Component::RootDir))); path } // \\?\c:\path Prefix::VerbatimDisk(_) => { let mut path = PathBuf::new(); path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); path.extend(components); path } // \\?\UNC\hostname\share_name\path Prefix::VerbatimUNC(hostname, share_name) => { let mut path = PathBuf::new(); path.push(format!( r"\\{}\{}\", hostname.to_string_lossy(), share_name.to_string_lossy() )); path.extend(components.filter(|c| !matches!(c, Component::RootDir))); path } _ => path, } } _ => path, } } pub fn resolve_from_cwd(path: &Path) -> Result { let resolved_path = if path.is_absolute() { path.to_owned() } else { let cwd = current_dir().context("Failed to get current working directory")?; cwd.join(path) }; Ok(normalize_path(&resolved_path)) } /// Checks if the path has extension Deno supports. pub fn is_supported_ext(path: &Path) -> bool { if let Some(ext) = get_extension(path) { matches!( ext.as_str(), "ts" | "tsx" | "js" | "jsx" | "mjs" | "mts" | "cjs" | "cts" ) } else { false } } /// Checks if the path has a basename and extension Deno supports for tests. pub fn is_supported_test_path(path: &Path) -> bool { if let Some(name) = path.file_stem() { let basename = name.to_string_lossy(); (basename.ends_with("_test") || basename.ends_with(".test") || basename == "test") && is_supported_ext(path) } else { false } } /// Checks if the path has a basename and extension Deno supports for benches. pub fn is_supported_bench_path(path: &Path) -> bool { if let Some(name) = path.file_stem() { let basename = name.to_string_lossy(); (basename.ends_with("_bench") || basename.ends_with(".bench") || basename == "bench") && is_supported_ext(path) } else { false } } /// Checks if the path has an extension Deno supports for tests. pub fn is_supported_test_ext(path: &Path) -> bool { if let Some(ext) = get_extension(path) { matches!( ext.as_str(), "ts" | "tsx" | "js" | "jsx" | "mjs" | "mts" | "cjs" | "cts" | "md" | "mkd" | "mkdn" | "mdwn" | "mdown" | "markdown" ) } else { false } } /// Get the extension of a file in lowercase. pub fn get_extension(file_path: &Path) -> Option { return file_path .extension() .and_then(|e| e.to_str()) .map(|e| e.to_lowercase()); } /// Collects file paths that satisfy the given predicate, by recursively walking `files`. /// If the walker visits a path that is listed in `ignore`, it skips descending into the directory. pub fn collect_files

( files: &[PathBuf], ignore: &[PathBuf], predicate: P, ) -> Result, AnyError> where P: Fn(&Path) -> bool, { let mut target_files = Vec::new(); // retain only the paths which exist and ignore the rest let canonicalized_ignore: Vec = ignore .iter() .filter_map(|i| canonicalize_path(i).ok()) .collect(); for file in files { for entry in WalkDir::new(file) .into_iter() .filter_entry(|e| { canonicalize_path(e.path()).map_or(false, |c| { !canonicalized_ignore.iter().any(|i| c.starts_with(i)) }) }) .filter_map(|e| match e { Ok(e) if !e.file_type().is_dir() && predicate(e.path()) => Some(e), _ => None, }) { target_files.push(canonicalize_path(entry.path())?) } } Ok(target_files) } /// Collects module specifiers that satisfy the given predicate as a file path, by recursively walking `include`. /// Specifiers that start with http and https are left intact. pub fn collect_specifiers

( include: Vec, ignore: &[PathBuf], predicate: P, ) -> Result, AnyError> where P: Fn(&Path) -> bool, { let mut prepared = vec![]; let root_path = current_dir()?; for path in include { let lowercase_path = path.to_lowercase(); if lowercase_path.starts_with("http://") || lowercase_path.starts_with("https://") { let url = ModuleSpecifier::parse(&path)?; prepared.push(url); continue; } let p = if lowercase_path.starts_with("file://") { specifier_to_file_path(&ModuleSpecifier::parse(&path)?)? } else { root_path.join(path) }; let p = normalize_path(&p); if p.is_dir() { let test_files = collect_files(&[p], ignore, &predicate).unwrap(); let mut test_files_as_urls = test_files .iter() .map(|f| ModuleSpecifier::from_file_path(f).unwrap()) .collect::>(); test_files_as_urls.sort(); prepared.extend(test_files_as_urls); } else { let url = ModuleSpecifier::from_file_path(p).unwrap(); prepared.push(url); } } Ok(prepared) } /// Asynchronously removes a directory and all its descendants, but does not error /// when the directory does not exist. pub async fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> { let result = tokio::fs::remove_dir_all(path).await; match result { Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), _ => result, } } /// Copies a directory to another directory. /// /// Note: Does not handle symlinks. pub fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> { std::fs::create_dir_all(to) .with_context(|| format!("Creating {}", to.display()))?; let read_dir = std::fs::read_dir(from) .with_context(|| format!("Reading {}", from.display()))?; for entry in read_dir { let entry = entry?; let file_type = entry.file_type()?; let new_from = from.join(entry.file_name()); let new_to = to.join(entry.file_name()); if file_type.is_dir() { copy_dir_recursive(&new_from, &new_to).with_context(|| { format!("Dir {} to {}", new_from.display(), new_to.display()) })?; } else if file_type.is_file() { std::fs::copy(&new_from, &new_to).with_context(|| { format!("Copying {} to {}", new_from.display(), new_to.display()) })?; } } Ok(()) } /// Hardlinks the files in one directory to another directory. /// /// Note: Does not handle symlinks. pub fn hard_link_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> { std::fs::create_dir_all(to) .with_context(|| format!("Creating {}", to.display()))?; let read_dir = std::fs::read_dir(from) .with_context(|| format!("Reading {}", from.display()))?; for entry in read_dir { let entry = entry?; let file_type = entry.file_type()?; let new_from = from.join(entry.file_name()); let new_to = to.join(entry.file_name()); if file_type.is_dir() { hard_link_dir_recursive(&new_from, &new_to).with_context(|| { format!("Dir {} to {}", new_from.display(), new_to.display()) })?; } else if file_type.is_file() { // note: chance for race conditions here between attempting to create, // then removing, then attempting to create. There doesn't seem to be // a way to hard link with overwriting in Rust, but maybe there is some // way with platform specific code. The workaround here is to handle // scenarios where something else might create or remove files. if let Err(err) = std::fs::hard_link(&new_from, &new_to) { if err.kind() == ErrorKind::AlreadyExists { if let Err(err) = std::fs::remove_file(&new_to) { if err.kind() == ErrorKind::NotFound { // Assume another process/thread created this hard link to the file we are wanting // to remove then sleep a little bit to let the other process/thread move ahead // faster to reduce contention. std::thread::sleep(Duration::from_millis(10)); } else { return Err(err).with_context(|| { format!( "Removing file to hard link {} to {}", new_from.display(), new_to.display() ) }); } } // Always attempt to recreate the hardlink. In contention scenarios, the other process // might have been killed or exited after removing the file, but before creating the hardlink if let Err(err) = std::fs::hard_link(&new_from, &new_to) { // Assume another process/thread created this hard link to the file we are wanting // to now create then sleep a little bit to let the other process/thread move ahead // faster to reduce contention. if err.kind() == ErrorKind::AlreadyExists { std::thread::sleep(Duration::from_millis(10)); } else { return Err(err).with_context(|| { format!( "Hard linking {} to {}", new_from.display(), new_to.display() ) }); } } } else { return Err(err).with_context(|| { format!( "Hard linking {} to {}", new_from.display(), new_to.display() ) }); } } } } Ok(()) } pub fn symlink_dir(oldpath: &Path, newpath: &Path) -> Result<(), AnyError> { let err_mapper = |err: Error| { Error::new( err.kind(), format!( "{}, symlink '{}' -> '{}'", err, oldpath.display(), newpath.display() ), ) }; #[cfg(unix)] { use std::os::unix::fs::symlink; symlink(oldpath, newpath).map_err(err_mapper)?; } #[cfg(not(unix))] { use std::os::windows::fs::symlink_dir; symlink_dir(oldpath, newpath).map_err(err_mapper)?; } Ok(()) } /// Attempts to convert a specifier to a file path. By default, uses the Url /// crate's `to_file_path()` method, but falls back to try and resolve unix-style /// paths on Windows. pub fn specifier_to_file_path( specifier: &ModuleSpecifier, ) -> Result { let result = if cfg!(windows) { match specifier.to_file_path() { Ok(path) => Ok(path), Err(()) => { // This might be a unix-style path which is used in the tests even on Windows. // Attempt to see if we can convert it to a `PathBuf`. This code should be removed // once/if https://github.com/servo/rust-url/issues/730 is implemented. if specifier.scheme() == "file" && specifier.host().is_none() && specifier.port().is_none() && specifier.path_segments().is_some() { let path_str = specifier.path(); match String::from_utf8( percent_encoding::percent_decode(path_str.as_bytes()).collect(), ) { Ok(path_str) => Ok(PathBuf::from(path_str)), Err(_) => Err(()), } } else { Err(()) } } } } else { specifier.to_file_path() }; match result { Ok(path) => Ok(path), Err(()) => Err(uri_error(format!( "Invalid file path.\n Specifier: {}", specifier ))), } } /// Ensures a specifier that will definitely be a directory has a trailing slash. pub fn ensure_directory_specifier( mut specifier: ModuleSpecifier, ) -> ModuleSpecifier { let path = specifier.path(); if !path.ends_with('/') { let new_path = format!("{}/", path); specifier.set_path(&new_path); } specifier } /// Gets the parent of this module specifier. pub fn specifier_parent(specifier: &ModuleSpecifier) -> ModuleSpecifier { let mut specifier = specifier.clone(); // don't use specifier.segments() because it will strip the leading slash let mut segments = specifier.path().split('/').collect::>(); if segments.iter().all(|s| s.is_empty()) { return specifier; } if let Some(last) = segments.last() { if last.is_empty() { segments.pop(); } segments.pop(); let new_path = format!("{}/", segments.join("/")); specifier.set_path(&new_path); } specifier } /// `from.make_relative(to)` but with fixes. pub fn relative_specifier( from: &ModuleSpecifier, to: &ModuleSpecifier, ) -> Option { let is_dir = to.path().ends_with('/'); if is_dir && from == to { return Some("./".to_string()); } // workaround using parent directory until https://github.com/servo/rust-url/pull/754 is merged let from = if !from.path().ends_with('/') { if let Some(end_slash) = from.path().rfind('/') { let mut new_from = from.clone(); new_from.set_path(&from.path()[..end_slash + 1]); Cow::Owned(new_from) } else { Cow::Borrowed(from) } } else { Cow::Borrowed(from) }; // workaround for url crate not adding a trailing slash for a directory // it seems to be fixed once a version greater than 2.2.2 is released let mut text = from.make_relative(to)?; if is_dir && !text.ends_with('/') && to.query().is_none() { text.push('/'); } Some(if text.starts_with("../") || text.starts_with("./") { text } else { format!("./{}", text) }) } /// This function checks if input path has trailing slash or not. If input path /// has trailing slash it will return true else it will return false. pub fn path_has_trailing_slash(path: &Path) -> bool { if let Some(path_str) = path.to_str() { if cfg!(windows) { path_str.ends_with('\\') } else { path_str.ends_with('/') } } else { false } } /// Gets a path with the specified file stem suffix. /// /// Ex. `file.ts` with suffix `_2` returns `file_2.ts` pub fn path_with_stem_suffix(path: &Path, suffix: &str) -> PathBuf { if let Some(file_name) = path.file_name().map(|f| f.to_string_lossy()) { if let Some(file_stem) = path.file_stem().map(|f| f.to_string_lossy()) { if let Some(ext) = path.extension().map(|f| f.to_string_lossy()) { return if file_stem.to_lowercase().ends_with(".d") { path.with_file_name(format!( "{}{}.{}.{}", &file_stem[..file_stem.len() - ".d".len()], suffix, // maintain casing &file_stem[file_stem.len() - "d".len()..], ext )) } else { path.with_file_name(format!("{}{}.{}", file_stem, suffix, ext)) }; } } path.with_file_name(format!("{}{}", file_name, suffix)) } else { path.with_file_name(suffix) } } /// Gets if the provided character is not supported on all /// kinds of file systems. pub fn is_banned_path_char(c: char) -> bool { matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*') } /// Gets a safe local directory name for the provided url. /// /// For example: /// https://deno.land:8080/path -> deno.land_8080/path pub fn root_url_to_safe_local_dirname(root: &ModuleSpecifier) -> PathBuf { fn sanitize_segment(text: &str) -> String { text .chars() .map(|c| if is_banned_segment_char(c) { '_' } else { c }) .collect() } fn is_banned_segment_char(c: char) -> bool { matches!(c, '/' | '\\') || is_banned_path_char(c) } let mut result = String::new(); if let Some(domain) = root.domain() { result.push_str(&sanitize_segment(domain)); } if let Some(port) = root.port() { if !result.is_empty() { result.push('_'); } result.push_str(&port.to_string()); } let mut result = PathBuf::from(result); if let Some(segments) = root.path_segments() { for segment in segments.filter(|s| !s.is_empty()) { result = result.join(sanitize_segment(segment)); } } result } /// Gets the total size (in bytes) of a directory. pub fn dir_size(path: &Path) -> std::io::Result { let entries = std::fs::read_dir(path)?; let mut total = 0; for entry in entries { let entry = entry?; total += match entry.metadata()? { data if data.is_dir() => dir_size(&entry.path())?, data => data.len(), }; } Ok(total) } #[cfg(test)] mod tests { use super::*; use test_util::TempDir; #[test] fn resolve_from_cwd_child() { let cwd = current_dir().unwrap(); assert_eq!(resolve_from_cwd(Path::new("a")).unwrap(), cwd.join("a")); } #[test] fn resolve_from_cwd_dot() { let cwd = current_dir().unwrap(); assert_eq!(resolve_from_cwd(Path::new(".")).unwrap(), cwd); } #[test] fn resolve_from_cwd_parent() { let cwd = current_dir().unwrap(); assert_eq!(resolve_from_cwd(Path::new("a/..")).unwrap(), cwd); } #[test] fn test_normalize_path() { assert_eq!(normalize_path(Path::new("a/../b")), PathBuf::from("b")); assert_eq!(normalize_path(Path::new("a/./b/")), PathBuf::from("a/b/")); assert_eq!( normalize_path(Path::new("a/./b/../c")), PathBuf::from("a/c") ); if cfg!(windows) { assert_eq!( normalize_path(Path::new("C:\\a\\.\\b\\..\\c")), PathBuf::from("C:\\a\\c") ); } } // TODO: Get a good expected value here for Windows. #[cfg(not(windows))] #[test] fn resolve_from_cwd_absolute() { let expected = Path::new("/a"); assert_eq!(resolve_from_cwd(expected).unwrap(), expected); } #[test] fn test_is_supported_ext() { assert!(!is_supported_ext(Path::new("tests/subdir/redirects"))); assert!(!is_supported_ext(Path::new("README.md"))); assert!(is_supported_ext(Path::new("lib/typescript.d.ts"))); assert!(is_supported_ext(Path::new("testdata/run/001_hello.js"))); assert!(is_supported_ext(Path::new("testdata/run/002_hello.ts"))); assert!(is_supported_ext(Path::new("foo.jsx"))); assert!(is_supported_ext(Path::new("foo.tsx"))); assert!(is_supported_ext(Path::new("foo.TS"))); assert!(is_supported_ext(Path::new("foo.TSX"))); assert!(is_supported_ext(Path::new("foo.JS"))); assert!(is_supported_ext(Path::new("foo.JSX"))); assert!(is_supported_ext(Path::new("foo.mjs"))); assert!(is_supported_ext(Path::new("foo.mts"))); assert!(is_supported_ext(Path::new("foo.cjs"))); assert!(is_supported_ext(Path::new("foo.cts"))); assert!(!is_supported_ext(Path::new("foo.mjsx"))); } #[test] fn test_is_supported_test_ext() { assert!(!is_supported_test_ext(Path::new("tests/subdir/redirects"))); assert!(is_supported_test_ext(Path::new("README.md"))); assert!(is_supported_test_ext(Path::new("readme.MD"))); assert!(is_supported_test_ext(Path::new("lib/typescript.d.ts"))); assert!(is_supported_test_ext(Path::new( "testdata/run/001_hello.js" ))); assert!(is_supported_test_ext(Path::new( "testdata/run/002_hello.ts" ))); assert!(is_supported_test_ext(Path::new("foo.jsx"))); assert!(is_supported_test_ext(Path::new("foo.tsx"))); assert!(is_supported_test_ext(Path::new("foo.TS"))); assert!(is_supported_test_ext(Path::new("foo.TSX"))); assert!(is_supported_test_ext(Path::new("foo.JS"))); assert!(is_supported_test_ext(Path::new("foo.JSX"))); assert!(is_supported_test_ext(Path::new("foo.mjs"))); assert!(is_supported_test_ext(Path::new("foo.mts"))); assert!(is_supported_test_ext(Path::new("foo.cjs"))); assert!(is_supported_test_ext(Path::new("foo.cts"))); assert!(!is_supported_test_ext(Path::new("foo.mjsx"))); assert!(!is_supported_test_ext(Path::new("foo.jsonc"))); assert!(!is_supported_test_ext(Path::new("foo.JSONC"))); assert!(!is_supported_test_ext(Path::new("foo.json"))); assert!(!is_supported_test_ext(Path::new("foo.JsON"))); } #[test] fn test_is_supported_test_path() { assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.ts" ))); assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.tsx" ))); assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.js" ))); assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.jsx" ))); assert!(is_supported_test_path(Path::new("bar/foo.test.ts"))); assert!(is_supported_test_path(Path::new("bar/foo.test.tsx"))); assert!(is_supported_test_path(Path::new("bar/foo.test.js"))); assert!(is_supported_test_path(Path::new("bar/foo.test.jsx"))); assert!(is_supported_test_path(Path::new("foo/bar/test.js"))); assert!(is_supported_test_path(Path::new("foo/bar/test.jsx"))); assert!(is_supported_test_path(Path::new("foo/bar/test.ts"))); assert!(is_supported_test_path(Path::new("foo/bar/test.tsx"))); assert!(!is_supported_test_path(Path::new("README.md"))); assert!(!is_supported_test_path(Path::new("lib/typescript.d.ts"))); assert!(!is_supported_test_path(Path::new("notatest.js"))); assert!(!is_supported_test_path(Path::new("NotAtest.ts"))); } #[test] fn test_collect_files() { fn create_files(dir_path: &Path, files: &[&str]) { std::fs::create_dir(dir_path).expect("Failed to create directory"); for f in files { let path = dir_path.join(f); std::fs::write(path, "").expect("Failed to create file"); } } // dir.ts // ├── a.ts // ├── b.js // ├── child // │ ├── e.mjs // │ ├── f.mjsx // │ ├── .foo.TS // │ └── README.md // ├── c.tsx // ├── d.jsx // └── ignore // ├── g.d.ts // └── .gitignore let t = TempDir::new(); let root_dir_path = t.path().join("dir.ts"); let root_dir_files = ["a.ts", "b.js", "c.tsx", "d.jsx"]; create_files(&root_dir_path, &root_dir_files); let child_dir_path = root_dir_path.join("child"); let child_dir_files = ["e.mjs", "f.mjsx", ".foo.TS", "README.md"]; create_files(&child_dir_path, &child_dir_files); let ignore_dir_path = root_dir_path.join("ignore"); let ignore_dir_files = ["g.d.ts", ".gitignore"]; create_files(&ignore_dir_path, &ignore_dir_files); let result = collect_files(&[root_dir_path], &[ignore_dir_path], |path| { // exclude dotfiles path .file_name() .and_then(|f| f.to_str()) .map_or(false, |f| !f.starts_with('.')) }) .unwrap(); let expected = [ "a.ts", "b.js", "e.mjs", "f.mjsx", "README.md", "c.tsx", "d.jsx", ]; for e in expected.iter() { assert!(result.iter().any(|r| r.ends_with(e))); } assert_eq!(result.len(), expected.len()); } #[test] fn test_collect_specifiers() { fn create_files(dir_path: &Path, files: &[&str]) { std::fs::create_dir(dir_path).expect("Failed to create directory"); for f in files { let path = dir_path.join(f); std::fs::write(path, "").expect("Failed to create file"); } } // dir.ts // ├── a.ts // ├── b.js // ├── child // │ ├── e.mjs // │ ├── f.mjsx // │ ├── .foo.TS // │ └── README.md // ├── c.tsx // ├── d.jsx // └── ignore // ├── g.d.ts // └── .gitignore let t = TempDir::new(); let root_dir_path = t.path().join("dir.ts"); let root_dir_files = ["a.ts", "b.js", "c.tsx", "d.jsx"]; create_files(&root_dir_path, &root_dir_files); let child_dir_path = root_dir_path.join("child"); let child_dir_files = ["e.mjs", "f.mjsx", ".foo.TS", "README.md"]; create_files(&child_dir_path, &child_dir_files); let ignore_dir_path = root_dir_path.join("ignore"); let ignore_dir_files = ["g.d.ts", ".gitignore"]; create_files(&ignore_dir_path, &ignore_dir_files); let predicate = |path: &Path| { // exclude dotfiles path .file_name() .and_then(|f| f.to_str()) .map_or(false, |f| !f.starts_with('.')) }; let result = collect_specifiers( vec![ "http://localhost:8080".to_string(), root_dir_path.to_str().unwrap().to_string(), "https://localhost:8080".to_string(), ], &[ignore_dir_path], predicate, ) .unwrap(); let root_dir_url = ModuleSpecifier::from_file_path( canonicalize_path(&root_dir_path).unwrap(), ) .unwrap() .to_string(); let expected: Vec = [ "http://localhost:8080", &format!("{}/a.ts", root_dir_url), &format!("{}/b.js", root_dir_url), &format!("{}/c.tsx", root_dir_url), &format!("{}/child/README.md", root_dir_url), &format!("{}/child/e.mjs", root_dir_url), &format!("{}/child/f.mjsx", root_dir_url), &format!("{}/d.jsx", root_dir_url), "https://localhost:8080", ] .iter() .map(|f| ModuleSpecifier::parse(f).unwrap()) .collect::>(); assert_eq!(result, expected); let scheme = if cfg!(target_os = "windows") { "file:///" } else { "file://" }; let result = collect_specifiers( vec![format!( "{}{}", scheme, root_dir_path .join("child") .to_str() .unwrap() .replace('\\', "/") )], &[], predicate, ) .unwrap(); let expected: Vec = [ &format!("{}/child/README.md", root_dir_url), &format!("{}/child/e.mjs", root_dir_url), &format!("{}/child/f.mjsx", root_dir_url), ] .iter() .map(|f| ModuleSpecifier::parse(f).unwrap()) .collect::>(); assert_eq!(result, expected); } #[cfg(windows)] #[test] fn test_strip_unc_prefix() { run_test(r"C:\", r"C:\"); run_test(r"C:\test\file.txt", r"C:\test\file.txt"); run_test(r"\\?\C:\", r"C:\"); run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt"); run_test(r"\\.\C:\", r"\\.\C:\"); run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt"); run_test(r"\\?\UNC\localhost\", r"\\localhost"); run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$"); run_test( r"\\?\UNC\localhost\c$\Windows\file.txt", r"\\localhost\c$\Windows\file.txt", ); run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json"); run_test(r"\\?\server1", r"\\server1"); run_test(r"\\?\server1\e$\", r"\\server1\e$\"); run_test( r"\\?\server1\e$\test\file.txt", r"\\server1\e$\test\file.txt", ); fn run_test(input: &str, expected: &str) { assert_eq!( strip_unc_prefix(PathBuf::from(input)), PathBuf::from(expected) ); } } #[test] fn test_specifier_to_file_path() { run_success_test("file:///", "/"); run_success_test("file:///test", "/test"); run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt"); run_success_test( "file:///dir/test%20test/test.txt", "/dir/test test/test.txt", ); fn run_success_test(specifier: &str, expected_path: &str) { let result = specifier_to_file_path(&ModuleSpecifier::parse(specifier).unwrap()) .unwrap(); assert_eq!(result, PathBuf::from(expected_path)); } } #[test] fn test_ensure_directory_specifier() { run_test("file:///", "file:///"); run_test("file:///test", "file:///test/"); run_test("file:///test/", "file:///test/"); run_test("file:///test/other", "file:///test/other/"); run_test("file:///test/other/", "file:///test/other/"); fn run_test(specifier: &str, expected: &str) { let result = ensure_directory_specifier(ModuleSpecifier::parse(specifier).unwrap()); assert_eq!(result.to_string(), expected); } } #[test] fn test_specifier_parent() { run_test("file:///", "file:///"); run_test("file:///test", "file:///"); run_test("file:///test/", "file:///"); run_test("file:///test/other", "file:///test/"); run_test("file:///test/other.txt", "file:///test/"); run_test("file:///test/other/", "file:///test/"); fn run_test(specifier: &str, expected: &str) { let result = specifier_parent(&ModuleSpecifier::parse(specifier).unwrap()); assert_eq!(result.to_string(), expected); } } #[test] fn test_relative_specifier() { run_test("file:///from", "file:///to", Some("./to")); run_test("file:///from", "file:///from/other", Some("./from/other")); run_test("file:///from", "file:///from/other/", Some("./from/other/")); run_test("file:///from", "file:///other/from", Some("./other/from")); run_test("file:///from/", "file:///other/from", Some("../other/from")); run_test("file:///from", "file:///other/from/", Some("./other/from/")); run_test( "file:///from", "file:///to/other.txt", Some("./to/other.txt"), ); run_test( "file:///from/test", "file:///to/other.txt", Some("../to/other.txt"), ); run_test( "file:///from/other.txt", "file:///to/other.txt", Some("../to/other.txt"), ); fn run_test(from: &str, to: &str, expected: Option<&str>) { let result = relative_specifier( &ModuleSpecifier::parse(from).unwrap(), &ModuleSpecifier::parse(to).unwrap(), ); assert_eq!(result.as_deref(), expected); } } #[test] fn test_path_has_trailing_slash() { #[cfg(not(windows))] { run_test("/Users/johndoe/Desktop/deno-project/target/", true); run_test(r"/Users/johndoe/deno-project/target//", true); run_test("/Users/johndoe/Desktop/deno-project", false); run_test(r"/Users/johndoe/deno-project\", false); } #[cfg(windows)] { run_test(r"C:\test\deno-project\", true); run_test(r"C:\test\deno-project\\", true); run_test(r"C:\test\file.txt", false); run_test(r"C:\test\file.txt/", false); } fn run_test(path_str: &str, expected: bool) { let path = Path::new(path_str); let result = path_has_trailing_slash(path); assert_eq!(result, expected); } } #[test] fn test_path_with_stem_suffix() { assert_eq!( path_with_stem_suffix(&PathBuf::from("/"), "_2"), PathBuf::from("/_2") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test"), "_2"), PathBuf::from("/test_2") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test.txt"), "_2"), PathBuf::from("/test_2.txt") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test/subdir"), "_2"), PathBuf::from("/test/subdir_2") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test/subdir.other.txt"), "_2"), PathBuf::from("/test/subdir.other_2.txt") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test.d.ts"), "_2"), PathBuf::from("/test_2.d.ts") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test.D.TS"), "_2"), PathBuf::from("/test_2.D.TS") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test.d.mts"), "_2"), PathBuf::from("/test_2.d.mts") ); assert_eq!( path_with_stem_suffix(&PathBuf::from("/test.d.cts"), "_2"), PathBuf::from("/test_2.d.cts") ); } }