/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/util/path.rs
424 строки
14 KB
em
fix(info): normalize trailing spaces in file path args on Windows (#35008)
10 июн 2026, 05:21
Не верифицирован
10 июн 2026, 05:21
caf69f8
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::path::Path; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_config::glob::PathGlobMatch; use deno_config::glob::PathOrPattern; use deno_config::glob::PathOrPatternSet; /// Checks if the path has an extension Deno supports for script execution. pub fn is_script_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 an extension Deno supports for importing. pub fn is_importable_ext(path: &Path) -> bool { if let Some(ext) = get_extension(path) { matches!( ext.as_str(), "ts" | "tsx" | "js" | "jsx" | "mjs" | "mts" | "cjs" | "cts" | "json" | "wasm" ) } else { false } } /// Get the extension of a file in lowercase. pub fn get_extension(file_path: &Path) -> Option<String> { file_path .extension() .and_then(|e| e.to_str()) .map(|e| e.to_lowercase()) } /// TypeScript figures out the type of file based on the extension, but we take /// other factors into account like the file headers. The hack here is to map the /// specifier passed to TypeScript to a new specifier with the file extension. pub fn mapped_specifier_for_tsc( specifier: &ModuleSpecifier, media_type: MediaType, ) -> Option<String> { let ext_media_type = MediaType::from_specifier(specifier); if media_type != ext_media_type { // we can't just add on the extension because typescript considers // all .d.*.ts files as declaration files in TS 5.0+ if media_type != MediaType::Dts && media_type == MediaType::TypeScript && specifier .path() .split('/') .next_back() .map(|last| last.contains(".d.")) .unwrap_or(false) { let mut path_parts = specifier .path() .split('/') .map(ToOwned::to_owned) .collect::<Vec<_>>(); let last_part = path_parts.last_mut().unwrap(); *last_part = last_part.replace(".d.", "$d$"); let mut specifier = specifier.clone(); specifier.set_path(&path_parts.join("/")); Some(format!("{}{}", specifier, media_type.as_ts_extension())) } else { Some(format!("{}{}", specifier, media_type.as_ts_extension())) } } else { None } } /// `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 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 text = from.make_relative(to)?; let text = if text.starts_with("../") || text.starts_with("./") { text } else { format!("./{text}") }; Some(to_percent_decoded_str(&text)) } pub fn relative_specifier_path_for_display( from: &ModuleSpecifier, to: &ModuleSpecifier, ) -> String { if to.scheme() == "file" && from.scheme() == "file" { let relative_specifier = relative_specifier(from, to) .map(Cow::Owned) .unwrap_or_else(|| Cow::Borrowed(to.as_str())); let relative_specifier = if relative_specifier.starts_with("../../../") { to.as_str() } else { relative_specifier.trim_start_matches("./") }; to_percent_decoded_str(relative_specifier) } else { to_percent_decoded_str(to.as_str()) } } /// Slightly different behaviour than the default matching /// where an exact path needs to be matched to be opted-in /// rather than just a partial directory match. /// /// This is used by the test and bench filtering. pub fn matches_pattern_or_exact_path( path_or_pattern_set: &PathOrPatternSet, path: &Path, ) -> bool { for p in path_or_pattern_set.inner().iter().rev() { match p { PathOrPattern::Path(p) => { if p == path { return true; } } PathOrPattern::NegatedPath(p) => { if path.starts_with(p) { return false; } } PathOrPattern::RemoteUrl(_) => {} PathOrPattern::Pattern(p) => match p.matches_path(path) { PathGlobMatch::Matched => return true, PathGlobMatch::MatchedNegated => return false, PathGlobMatch::NotMatched => {} }, } } false } /// For decoding percent-encodeing string /// could be used for module specifier string literal of local modules, /// or local file path to display `non-ASCII` characters correctly /// # Examples /// ``` /// use crate::util::path::to_percent_decoded_str; /// /// let str = to_percent_decoded_str("file:///Users/path/to/%F0%9F%A6%95.ts"); /// assert_eq!(str, "file:///Users/path/to/🦕.ts"); /// ``` pub fn to_percent_decoded_str(s: &str) -> String { match percent_encoding::percent_decode_str(s).decode_utf8() { Ok(s) => s.to_string(), // when failed to decode, return the original string Err(_) => s.to_string(), } } /// Normalizes a user provided file path the same way the Windows file system /// APIs do when opening a file: trailing spaces and dots are stripped from the /// final path component. /// /// Without this, `deno info ".\foo.tsx "` resolves to a `foo.tsx%20` specifier. /// The file still loads on Windows (the OS trims the trailing space when /// opening it), but the trailing `%20` causes the media type to be misdetected /// (e.g. as TypeScript instead of TSX), leading to a confusing parse error. /// See https://github.com/denoland/deno/issues/30585. /// /// This is a no-op on non-Windows platforms, where trailing whitespace and dots /// in a file name are significant. /// /// `\\?\` extended-length paths *can* preserve trailing spaces/dots, so this /// would diverge there, but such a path as a CLI argument is vanishingly rare /// and intentionally out of scope. #[cfg(windows)] pub fn strip_windows_trailing_chars(path: &str) -> Cow<'_, str> { let (dir, name) = match path.rfind(['/', '\\']) { Some(i) => path.split_at(i + 1), None => ("", path), }; // Leave drive specifiers (e.g. "C:") alone. Relative directory references // ("." / "..") are handled by the `trimmed.is_empty()` check below, since // they trim away entirely. if name.ends_with(':') { return Cow::Borrowed(path); } let trimmed = name.trim_end_matches([' ', '.']); if trimmed.is_empty() || trimmed.len() == name.len() { Cow::Borrowed(path) } else { Cow::Owned(format!("{dir}{trimmed}")) } } #[cfg(not(windows))] pub fn strip_windows_trailing_chars(path: &str) -> Cow<'_, str> { Cow::Borrowed(path) } /// Like [`deno_path_util::resolve_url_or_path`], but first normalizes a file /// path argument the way the Windows file system would (see /// [`strip_windows_trailing_chars`]). URL arguments (anything with a URI /// scheme) are passed through unchanged. pub fn resolve_url_or_path_normalized( specifier: &str, current_dir: &Path, ) -> Result<ModuleSpecifier, deno_path_util::ResolveUrlOrPathError> { if deno_path_util::specifier_has_uri_scheme(specifier) { deno_path_util::resolve_url_or_path(specifier, current_dir) } else { deno_path_util::resolve_url_or_path( &strip_windows_trailing_chars(specifier), current_dir, ) } } #[cfg(test)] mod test { use super::*; #[cfg(windows)] #[test] fn test_strip_windows_trailing_chars() { // trailing space/dot stripped from the final component assert_eq!(strip_windows_trailing_chars(".\\foo.tsx "), ".\\foo.tsx"); assert_eq!(strip_windows_trailing_chars("foo.tsx "), "foo.tsx"); assert_eq!(strip_windows_trailing_chars("foo.tsx."), "foo.tsx"); assert_eq!(strip_windows_trailing_chars("foo.tsx. "), "foo.tsx"); assert_eq!( strip_windows_trailing_chars("C:\\a\\foo.tsx "), "C:\\a\\foo.tsx" ); assert_eq!(strip_windows_trailing_chars("a/b/foo.tsx "), "a/b/foo.tsx"); // nothing to strip -> unchanged assert_eq!(strip_windows_trailing_chars("foo.tsx"), "foo.tsx"); assert_eq!(strip_windows_trailing_chars(".\\foo.tsx"), ".\\foo.tsx"); // intermediate components and trailing space within them are left alone assert_eq!(strip_windows_trailing_chars("a /b/foo.tsx"), "a /b/foo.tsx"); // relative dir references and drive specifiers are left alone assert_eq!(strip_windows_trailing_chars("."), "."); assert_eq!(strip_windows_trailing_chars(".."), ".."); assert_eq!(strip_windows_trailing_chars("C:"), "C:"); assert_eq!(strip_windows_trailing_chars("a\\.."), "a\\.."); // an all-trailing-chars name is left alone (avoid producing an empty name) assert_eq!(strip_windows_trailing_chars("a\\ "), "a\\ "); } #[test] fn test_resolve_url_or_path_normalized_url_passthrough() { // URL arguments must bypass the Windows trailing-char normalization. The // scheme guard is what prevents the strip from mangling URLs (e.g. turning // `.../foo.tsx.` into `.../foo.tsx` on Windows), so lock that in here. The // current dir is unused for URL inputs, so a dummy is fine. let cwd = Path::new("/"); let url = resolve_url_or_path_normalized("http://example.com/foo.tsx.", cwd) .unwrap(); assert_eq!(url.as_str(), "http://example.com/foo.tsx."); } #[test] fn test_is_script_ext() { assert!(!is_script_ext(Path::new("tests/subdir/redirects"))); assert!(!is_script_ext(Path::new("README.md"))); assert!(is_script_ext(Path::new("lib/typescript.d.ts"))); assert!(is_script_ext(Path::new("testdata/run/001_hello.js"))); assert!(is_script_ext(Path::new("testdata/run/002_hello.ts"))); assert!(is_script_ext(Path::new("foo.jsx"))); assert!(is_script_ext(Path::new("foo.tsx"))); assert!(is_script_ext(Path::new("foo.TS"))); assert!(is_script_ext(Path::new("foo.TSX"))); assert!(is_script_ext(Path::new("foo.JS"))); assert!(is_script_ext(Path::new("foo.JSX"))); assert!(is_script_ext(Path::new("foo.mjs"))); assert!(is_script_ext(Path::new("foo.mts"))); assert!(is_script_ext(Path::new("foo.cjs"))); assert!(is_script_ext(Path::new("foo.cts"))); assert!(!is_script_ext(Path::new("foo.json"))); assert!(!is_script_ext(Path::new("foo.wasm"))); assert!(!is_script_ext(Path::new("foo.mjsx"))); } #[test] fn test_is_importable_ext() { assert!(!is_importable_ext(Path::new("tests/subdir/redirects"))); assert!(!is_importable_ext(Path::new("README.md"))); assert!(is_importable_ext(Path::new("lib/typescript.d.ts"))); assert!(is_importable_ext(Path::new("testdata/run/001_hello.js"))); assert!(is_importable_ext(Path::new("testdata/run/002_hello.ts"))); assert!(is_importable_ext(Path::new("foo.jsx"))); assert!(is_importable_ext(Path::new("foo.tsx"))); assert!(is_importable_ext(Path::new("foo.TS"))); assert!(is_importable_ext(Path::new("foo.TSX"))); assert!(is_importable_ext(Path::new("foo.JS"))); assert!(is_importable_ext(Path::new("foo.JSX"))); assert!(is_importable_ext(Path::new("foo.mjs"))); assert!(is_importable_ext(Path::new("foo.mts"))); assert!(is_importable_ext(Path::new("foo.cjs"))); assert!(is_importable_ext(Path::new("foo.cts"))); assert!(is_importable_ext(Path::new("foo.json"))); assert!(is_importable_ext(Path::new("foo.wasm"))); assert!(!is_importable_ext(Path::new("foo.mjsx"))); } #[test] fn test_relative_specifier() { let fixtures: Vec<(&str, &str, Option<&str>)> = vec![ ("file:///from", "file:///to", Some("./to")), ("file:///from", "file:///from/other", Some("./from/other")), ("file:///from", "file:///from/other/", Some("./from/other/")), ("file:///from", "file:///other/from", Some("./other/from")), ("file:///from/", "file:///other/from", Some("../other/from")), ("file:///from", "file:///other/from/", Some("./other/from/")), ( "file:///from", "file:///to/other.txt", Some("./to/other.txt"), ), ( "file:///from/test", "file:///to/other.txt", Some("../to/other.txt"), ), ( "file:///from/other.txt", "file:///to/other.txt", Some("../to/other.txt"), ), ( "https://deno.land/x/a/b/d.ts", "https://deno.land/x/a/b/c.ts", Some("./c.ts"), ), ( "https://deno.land/x/a/b/d.ts", "https://deno.land/x/a/c.ts", Some("../c.ts"), ), ( "https://deno.land/x/a/b/d.ts", "https://deno.land/x/a/b/c/d.ts", Some("./c/d.ts"), ), ( "https://deno.land/x/a/b/c/", "https://deno.land/x/a/b/c/d.ts", Some("./d.ts"), ), ( "https://deno.land/x/a/b/c/", "https://deno.land/x/a/b/c/d/e.ts", Some("./d/e.ts"), ), ( "https://deno.land/x/a/b/c/f.ts", "https://deno.land/x/a/b/c/d/e.ts", Some("./d/e.ts"), ), ( "https://deno.land/x/a/b/d.ts", "https://deno.land/x/a/c.ts?foo=bar", Some("../c.ts?foo=bar"), ), ( "https://deno.land/x/a/b/d.ts?foo=bar", "https://deno.land/x/a/b/c.ts", Some("./c.ts"), ), ("file:///a/b/d.ts", "file:///a/b/c.ts", Some("./c.ts")), ("https://deno.land/x/a/b/c.ts", "file:///a/b/c.ts", None), ( "https://deno.land/", "https://deno.land/x/a/b/c.ts", Some("./x/a/b/c.ts"), ), ( "https://deno.land/x/d/e/f.ts", "https://deno.land/x/a/b/c.ts", Some("../../a/b/c.ts"), ), ]; for (from_str, to_str, expected) in fixtures { let from = ModuleSpecifier::parse(from_str).unwrap(); let to = ModuleSpecifier::parse(to_str).unwrap(); let actual = relative_specifier(&from, &to); assert_eq!( actual.as_deref(), expected, "from: \"{from_str}\" to: \"{to_str}\"" ); } } #[test] fn test_to_percent_decoded_str() { let str = to_percent_decoded_str("%F0%9F%A6%95"); assert_eq!(str, "🦕"); } }