use crate::normalize_path;use std::env::current_dir;use std::error::Error;use std::fmt;use std::path::PathBuf;use url::ParseError;use url::Url;
pub const DUMMY_SPECIFIER: &str = "<unknown>";
#[derive(Clone, Debug, Eq, PartialEq)]pub enum ModuleResolutionError { InvalidUrl(ParseError), InvalidBaseUrl(ParseError), InvalidPath(PathBuf), ImportPrefixMissing(String, Option<String>),}use ModuleResolutionError::*;
impl Error for ModuleResolutionError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { InvalidUrl(ref err) | InvalidBaseUrl(ref err) => Some(err), _ => None, } }}
impl fmt::Display for ModuleResolutionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { InvalidUrl(ref err) => write!(f, "invalid URL: {}", err), InvalidBaseUrl(ref err) => { write!(f, "invalid base URL for relative import: {}", err) } InvalidPath(ref path) => write!(f, "invalid module path: {:?}", path), ImportPrefixMissing(ref specifier, ref maybe_referrer) => write!( f, "Relative import path \"{}\" not prefixed with / or ./ or ../{}", specifier, match maybe_referrer { Some(referrer) => format!(" from \"{}\"", referrer), None => String::new(), } ), } }}
pub type ModuleSpecifier = Url;
pub fn resolve_import( specifier: &str, base: &str,) -> Result<ModuleSpecifier, ModuleResolutionError> { let url = match Url::parse(specifier) { Ok(url) => url,
Err(ParseError::RelativeUrlWithoutBase) if !(specifier.starts_with('/') || specifier.starts_with("./") || specifier.starts_with("../")) => { let maybe_referrer = if base.is_empty() { None } else { Some(base.to_string()) }; return Err(ImportPrefixMissing(specifier.to_string(), maybe_referrer)); }
Err(ParseError::RelativeUrlWithoutBase) => { let base = if base == DUMMY_SPECIFIER {
let path = current_dir().unwrap().join(base); Url::from_file_path(path).unwrap() } else { Url::parse(base).map_err(InvalidBaseUrl)? }; base.join(specifier).map_err(InvalidUrl)? }
Err(err) => return Err(InvalidUrl(err)), };
Ok(url)}
pub fn resolve_url( url_str: &str,) -> Result<ModuleSpecifier, ModuleResolutionError> { Url::parse(url_str).map_err(ModuleResolutionError::InvalidUrl)}
pub fn resolve_url_or_path( specifier: &str,) -> Result<ModuleSpecifier, ModuleResolutionError> { if specifier_has_uri_scheme(specifier) { resolve_url(specifier) } else { resolve_path(specifier) }}
pub fn resolve_path( path_str: &str,) -> Result<ModuleSpecifier, ModuleResolutionError> { let path = current_dir().unwrap().join(path_str); let path = normalize_path(&path); Url::from_file_path(path.clone()) .map_err(|()| ModuleResolutionError::InvalidPath(path))}
fn specifier_has_uri_scheme(specifier: &str) -> bool { let mut chars = specifier.chars(); let mut len = 0usize; match chars.next() { Some(c) if c.is_ascii_alphabetic() => len += 1, _ => return false, } loop { match chars.next() { Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1, Some(':') if len >= 2 => return true, _ => return false, } }}
#[cfg(test)]mod tests { use super::*; use crate::serde_json::from_value; use crate::serde_json::json; use std::path::Path;
#[test] fn test_resolve_import() { fn get_path(specifier: &str) -> Url { let base_path = current_dir().unwrap().join("<unknown>"); let base_url = Url::from_file_path(base_path).unwrap(); base_url.join(specifier).unwrap() } let awesome = get_path("/awesome.ts"); let awesome_srv = get_path("/service/awesome.ts"); let tests = vec![ ("/awesome.ts", "<unknown>", awesome.as_str()), ("/service/awesome.ts", "<unknown>", awesome_srv.as_str()), ( "./005_more_imports.ts", "http://deno.land/core/tests/006_url_imports.ts", "http://deno.land/core/tests/005_more_imports.ts", ), ( "../005_more_imports.ts", "http://deno.land/core/tests/006_url_imports.ts", "http://deno.land/core/005_more_imports.ts", ), ( "http://deno.land/core/tests/005_more_imports.ts", "http://deno.land/core/tests/006_url_imports.ts", "http://deno.land/core/tests/005_more_imports.ts", ), ( "data:text/javascript,export default 'grapes';", "http://deno.land/core/tests/006_url_imports.ts", "data:text/javascript,export default 'grapes';", ), ( "blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f", "http://deno.land/core/tests/006_url_imports.ts", "blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f", ), ( "javascript:export default 'artichokes';", "http://deno.land/core/tests/006_url_imports.ts", "javascript:export default 'artichokes';", ), ( "data:text/plain,export default 'kale';", "http://deno.land/core/tests/006_url_imports.ts", "data:text/plain,export default 'kale';", ), ( "/dev/core/tests/005_more_imports.ts", "file:///home/yeti", "file:///dev/core/tests/005_more_imports.ts", ), ( "//zombo.com/1999.ts", "https://cherry.dev/its/a/thing", "https://zombo.com/1999.ts", ), ( "http://deno.land/this/url/is/valid", "base is clearly not a valid url", "http://deno.land/this/url/is/valid", ), ( "//server/some/dir/file", "file:///home/yeti/deno", "file://server/some/dir/file", ), ];
for (specifier, base, expected_url) in tests { let url = resolve_import(specifier, base).unwrap().to_string(); assert_eq!(url, expected_url); } }
#[test] fn test_resolve_import_error() { use url::ParseError::*; use ModuleResolutionError::*;
let tests = vec![ ( "awesome.ts", "<unknown>", ImportPrefixMissing( "awesome.ts".to_string(), Some("<unknown>".to_string()), ), ), ( "005_more_imports.ts", "http://deno.land/core/tests/006_url_imports.ts", ImportPrefixMissing( "005_more_imports.ts".to_string(), Some("http://deno.land/core/tests/006_url_imports.ts".to_string()), ), ), ( ".tomato", "http://deno.land/core/tests/006_url_imports.ts", ImportPrefixMissing( ".tomato".to_string(), Some("http://deno.land/core/tests/006_url_imports.ts".to_string()), ), ), ( "..zucchini.mjs", "http://deno.land/core/tests/006_url_imports.ts", ImportPrefixMissing( "..zucchini.mjs".to_string(), Some("http://deno.land/core/tests/006_url_imports.ts".to_string()), ), ), ( r".\yam.es", "http://deno.land/core/tests/006_url_imports.ts", ImportPrefixMissing( r".\yam.es".to_string(), Some("http://deno.land/core/tests/006_url_imports.ts".to_string()), ), ), ( r"..\yam.es", "http://deno.land/core/tests/006_url_imports.ts", ImportPrefixMissing( r"..\yam.es".to_string(), Some("http://deno.land/core/tests/006_url_imports.ts".to_string()), ), ), ( "https://eggplant:b/c", "http://deno.land/core/tests/006_url_imports.ts", InvalidUrl(InvalidPort), ), ( "https://eggplant@/c", "http://deno.land/core/tests/006_url_imports.ts", InvalidUrl(EmptyHost), ), ( "./foo.ts", "/relative/base/url", InvalidBaseUrl(RelativeUrlWithoutBase), ), ];
for (specifier, base, expected_err) in tests { let err = resolve_import(specifier, base).unwrap_err(); assert_eq!(err, expected_err); } }
#[test] fn test_resolve_url_or_path() { let mut tests: Vec<(&str, String)> = vec![ ( "http://deno.land/core/tests/006_url_imports.ts", "http://deno.land/core/tests/006_url_imports.ts".to_string(), ), ( "https://deno.land/core/tests/006_url_imports.ts", "https://deno.land/core/tests/006_url_imports.ts".to_string(), ), ];
let cwd = current_dir().unwrap(); let cwd_str = cwd.to_str().unwrap();
if cfg!(target_os = "windows") { let expected_url = "file:///C:/deno/tests/006_url_imports.ts"; tests.extend(vec![ ( r"C:/deno/tests/006_url_imports.ts", expected_url.to_string(), ), ( r"C:\deno\tests\006_url_imports.ts", expected_url.to_string(), ), ( r"\\?\C:\deno\tests\006_url_imports.ts", expected_url.to_string(), ), ]);
let expected_url = format!( "file:///{}:/deno/tests/006_url_imports.ts", cwd_str.get(..1).unwrap(), ); tests.extend(vec![ (r"/deno/tests/006_url_imports.ts", expected_url.to_string()), (r"\deno\tests\006_url_imports.ts", expected_url.to_string()), ( r"\deno\..\deno\tests\006_url_imports.ts", expected_url.to_string(), ), (r"\deno\.\tests\006_url_imports.ts", expected_url), ]);
let expected_url = format!( "file:///{}/tests/006_url_imports.ts", cwd_str.replace('\\', "/") ); tests.extend(vec![ (r"tests/006_url_imports.ts", expected_url.to_string()), (r"tests\006_url_imports.ts", expected_url.to_string()), (r"./tests/006_url_imports.ts", (*expected_url).to_string()), (r".\tests\006_url_imports.ts", (*expected_url).to_string()), ]);
let expected_url = "file://server/share/deno/cool"; tests.extend(vec![ (r"\\server\share\deno\cool", expected_url.to_string()), (r"\\server/share/deno/cool", expected_url.to_string()), ]); } else { let expected_url = "file:///deno/tests/006_url_imports.ts"; tests.extend(vec![ ("/deno/tests/006_url_imports.ts", expected_url.to_string()), ("//deno/tests/006_url_imports.ts", expected_url.to_string()), ]);
let expected_url = format!("file://{}/tests/006_url_imports.ts", cwd_str); tests.extend(vec![ ("tests/006_url_imports.ts", expected_url.to_string()), ("./tests/006_url_imports.ts", expected_url.to_string()), ( "tests/../tests/006_url_imports.ts", expected_url.to_string(), ), ("tests/./006_url_imports.ts", expected_url), ]); }
for (specifier, expected_url) in tests { let url = resolve_url_or_path(specifier).unwrap().to_string(); assert_eq!(url, expected_url); } }
#[test] fn test_resolve_url_or_path_error() { use url::ParseError::*; use ModuleResolutionError::*;
let mut tests = vec![ ("https://eggplant:b/c", InvalidUrl(InvalidPort)), ("https://:8080/a/b/c", InvalidUrl(EmptyHost)), ]; if cfg!(target_os = "windows") { let p = r"\\.\c:/stuff/deno/script.ts"; tests.push((p, InvalidPath(PathBuf::from(p)))); }
for (specifier, expected_err) in tests { let err = resolve_url_or_path(specifier).unwrap_err(); assert_eq!(err, expected_err); } }
#[test] fn test_specifier_has_uri_scheme() { let tests = vec![ ("http://foo.bar/etc", true), ("HTTP://foo.bar/etc", true), ("http:ftp:", true), ("http:", true), ("hTtP:", true), ("ftp:", true), ("mailto:spam@please.me", true), ("git+ssh://git@github.com/denoland/deno", true), ("blob:https://whatwg.org/mumbojumbo", true), ("abc.123+DEF-ghi:", true), ("abc.123+def-ghi:@", true), ("", false), (":not", false), ("http", false), ("c:dir", false), ("X:", false), ("./http://not", false), ("1abc://kinda/but/no", false), ("schluẞ://no/more", false), ];
for (specifier, expected) in tests { let result = specifier_has_uri_scheme(specifier); assert_eq!(result, expected); } }
#[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") ); } }
#[test] fn test_deserialize_module_specifier() { let actual: ModuleSpecifier = from_value(json!("http://deno.land/x/mod.ts")).unwrap(); let expected = resolve_url("http://deno.land/x/mod.ts").unwrap(); assert_eq!(actual, expected); }}