mirror of
https://github.com/denoland/deno.git
synced 2024-10-29 08:58:01 -04:00
cbb3f85433
This adds support for peer dependencies in npm packages. 1. If not found higher in the tree (ancestor and ancestor siblings), peer dependencies are resolved like a dependency similar to npm 7. 2. Optional peer dependencies are only resolved if found higher in the tree. 3. This creates "copy packages" or duplicates of a package when a package has different resolution due to peer dependency resolution—see https://pnpm.io/how-peers-are-resolved. Unlike pnpm though, duplicates of packages will have `_1`, `_2`, etc. added to the end of the package version in the directory in order to minimize the chance of hitting the max file path limit on Windows. This is done for both the local "node_modules" directory and also the global npm cache. The files are hard linked in this case to reduce hard drive space. This is a first pass and the code is definitely more inefficient than it could be. Closes #15823
1151 lines
34 KiB
Rust
1151 lines
34 KiB
Rust
// 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<T: AsRef<[u8]>>(
|
|
filename: &Path,
|
|
data: T,
|
|
mode: u32,
|
|
) -> std::io::Result<()> {
|
|
let rand: String = (0..4)
|
|
.map(|_| format!("{:02x}", rand::random::<u8>()))
|
|
.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<T: AsRef<[u8]>>(
|
|
filename: &Path,
|
|
data: T,
|
|
mode: u32,
|
|
) -> std::io::Result<()> {
|
|
write_file_2(filename, data, true, mode, true, false)
|
|
}
|
|
|
|
pub fn write_file_2<T: AsRef<[u8]>>(
|
|
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<PathBuf, Error> {
|
|
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<PathBuf, Error> {
|
|
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<PathBuf, AnyError> {
|
|
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<String> {
|
|
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<P>(
|
|
files: &[PathBuf],
|
|
ignore: &[PathBuf],
|
|
predicate: P,
|
|
) -> Result<Vec<PathBuf>, 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<PathBuf> = 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<P>(
|
|
include: Vec<String>,
|
|
ignore: &[PathBuf],
|
|
predicate: P,
|
|
) -> Result<Vec<ModuleSpecifier>, 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::<Vec<ModuleSpecifier>>();
|
|
|
|
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<PathBuf, AnyError> {
|
|
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::<Vec<_>>();
|
|
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<String> {
|
|
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<u64> {
|
|
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<ModuleSpecifier> = [
|
|
"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::<Vec<_>>();
|
|
|
|
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<ModuleSpecifier> = [
|
|
&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::<Vec<_>>();
|
|
|
|
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")
|
|
);
|
|
}
|
|
}
|