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 => format!(""),
}
),
}
}
}
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);
}
}