use url::ParseError;
use url::Url;
#[derive(
Clone, Debug, Eq, PartialEq, thiserror::Error, deno_error::JsError,
)]
#[class(uri)]
pub enum ModuleResolutionError {
#[error("invalid URL: {0}")]
InvalidUrl(#[source] ParseError),
#[error("invalid base URL for relative import: {0}")]
InvalidBaseUrl(#[source] ParseError),
#[error(
"Relative import path \"{specifier}\" not prefixed with / or ./ or ../{}",
.maybe_referrer.as_ref().map_or(String::new(), |referrer| format!(" from \"{referrer}\""))
)]
ImportPrefixMissing {
specifier: String,
maybe_referrer: Option<String>,
},
}
use ModuleResolutionError::*;
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: specifier.to_string(),
maybe_referrer,
});
}
Err(ParseError::RelativeUrlWithoutBase) => {
let base = 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)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde_json::from_value;
use crate::serde_json::json;
#[test]
fn test_resolve_import() {
let tests = vec![
(
"./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 {
specifier: "awesome.ts".to_string(),
maybe_referrer: Some("<unknown>".to_string()),
},
),
(
"005_more_imports.ts",
"http://deno.land/core/tests/006_url_imports.ts",
ImportPrefixMissing {
specifier: "005_more_imports.ts".to_string(),
maybe_referrer: Some(
"http://deno.land/core/tests/006_url_imports.ts".to_string(),
),
},
),
(
".tomato",
"http://deno.land/core/tests/006_url_imports.ts",
ImportPrefixMissing {
specifier: ".tomato".to_string(),
maybe_referrer: Some(
"http://deno.land/core/tests/006_url_imports.ts".to_string(),
),
},
),
(
"..zucchini.mjs",
"http://deno.land/core/tests/006_url_imports.ts",
ImportPrefixMissing {
specifier: "..zucchini.mjs".to_string(),
maybe_referrer: 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 {
specifier: r".\yam.es".to_string(),
maybe_referrer: 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 {
specifier: r"..\yam.es".to_string(),
maybe_referrer: 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_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);
}
}