2021-01-11 12:13:41 -05:00
|
|
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
2020-09-21 08:26:41 -04:00
|
|
|
|
2021-07-29 15:20:34 -04:00
|
|
|
use deno_core::error::AnyError;
|
2021-02-28 19:49:08 -05:00
|
|
|
use deno_core::serde::Serialize;
|
2020-09-21 12:36:37 -04:00
|
|
|
use deno_core::serde_json;
|
|
|
|
use deno_core::serde_json::Map;
|
|
|
|
use deno_core::serde_json::Value;
|
2020-09-16 14:28:07 -04:00
|
|
|
use deno_core::url::Url;
|
2019-06-09 09:08:20 -04:00
|
|
|
use indexmap::IndexMap;
|
2021-03-26 12:34:25 -04:00
|
|
|
use log::debug;
|
|
|
|
use log::info;
|
2019-06-09 09:08:20 -04:00
|
|
|
use std::cmp::Ordering;
|
2021-02-28 19:49:08 -05:00
|
|
|
use std::collections::HashSet;
|
2019-07-10 18:53:48 -04:00
|
|
|
use std::error::Error;
|
|
|
|
use std::fmt;
|
2019-06-09 09:08:20 -04:00
|
|
|
|
|
|
|
#[derive(Debug)]
|
2021-05-30 20:20:34 -04:00
|
|
|
pub enum ImportMapError {
|
|
|
|
UnmappedBareSpecifier(String, Option<String>),
|
|
|
|
Other(String),
|
|
|
|
}
|
2019-06-09 09:08:20 -04:00
|
|
|
|
2019-07-10 18:53:48 -04:00
|
|
|
impl fmt::Display for ImportMapError {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
2021-05-30 20:20:34 -04:00
|
|
|
match self {
|
|
|
|
ImportMapError::UnmappedBareSpecifier(specifier, maybe_referrer) => write!(
|
|
|
|
f,
|
|
|
|
"Relative import path \"{}\" not prefixed with / or ./ or ../ and not in import map{}",
|
|
|
|
specifier,
|
|
|
|
match maybe_referrer {
|
|
|
|
Some(referrer) => format!(" from \"{}\"", referrer),
|
|
|
|
None => format!(""),
|
|
|
|
}
|
|
|
|
),
|
|
|
|
ImportMapError::Other(message) => f.pad(message),
|
|
|
|
}
|
2019-07-10 18:53:48 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Error for ImportMapError {}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
// https://url.spec.whatwg.org/#special-scheme
|
|
|
|
const SPECIAL_PROTOCOLS: &[&str] =
|
|
|
|
&["ftp", "file", "http", "https", "ws", "wss"];
|
|
|
|
fn is_special(url: &Url) -> bool {
|
|
|
|
SPECIAL_PROTOCOLS.contains(&url.scheme())
|
|
|
|
}
|
2019-06-09 09:08:20 -04:00
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
type SpecifierMap = IndexMap<String, Option<Url>>;
|
2019-06-09 09:08:20 -04:00
|
|
|
type ScopesMap = IndexMap<String, SpecifierMap>;
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
#[derive(Debug, Clone, Serialize)]
|
2019-06-09 09:08:20 -04:00
|
|
|
pub struct ImportMap {
|
2021-02-28 19:49:08 -05:00
|
|
|
#[serde(skip)]
|
2019-06-09 09:08:20 -04:00
|
|
|
base_url: String,
|
2021-02-28 19:49:08 -05:00
|
|
|
|
2019-06-09 09:08:20 -04:00
|
|
|
imports: SpecifierMap,
|
|
|
|
scopes: ScopesMap,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ImportMap {
|
|
|
|
pub fn from_json(
|
|
|
|
base_url: &str,
|
|
|
|
json_string: &str,
|
|
|
|
) -> Result<Self, ImportMapError> {
|
|
|
|
let v: Value = match serde_json::from_str(json_string) {
|
|
|
|
Ok(v) => v,
|
2021-08-03 15:29:12 -04:00
|
|
|
Err(err) => {
|
|
|
|
return Err(ImportMapError::Other(format!(
|
|
|
|
"Unable to parse import map JSON: {}",
|
|
|
|
err.to_string()
|
|
|
|
)));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
match v {
|
|
|
|
Value::Object(_) => {}
|
|
|
|
_ => {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(
|
2021-02-28 19:49:08 -05:00
|
|
|
"Import map JSON must be an object".to_string(),
|
|
|
|
));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let mut diagnostics = vec![];
|
2019-06-09 09:08:20 -04:00
|
|
|
let normalized_imports = match &v.get("imports") {
|
|
|
|
Some(imports_map) => {
|
|
|
|
if !imports_map.is_object() {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(
|
2021-02-28 19:49:08 -05:00
|
|
|
"Import map's 'imports' must be an object".to_string(),
|
2019-06-09 09:08:20 -04:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
let imports_map = imports_map.as_object().unwrap();
|
2021-02-28 19:49:08 -05:00
|
|
|
ImportMap::parse_specifier_map(imports_map, base_url, &mut diagnostics)
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
None => IndexMap::new(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let normalized_scopes = match &v.get("scopes") {
|
|
|
|
Some(scope_map) => {
|
|
|
|
if !scope_map.is_object() {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(
|
2021-02-28 19:49:08 -05:00
|
|
|
"Import map's 'scopes' must be an object".to_string(),
|
2019-06-09 09:08:20 -04:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
let scope_map = scope_map.as_object().unwrap();
|
2021-02-28 19:49:08 -05:00
|
|
|
ImportMap::parse_scope_map(scope_map, base_url, &mut diagnostics)?
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
None => IndexMap::new(),
|
|
|
|
};
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let mut keys: HashSet<String> = v
|
|
|
|
.as_object()
|
|
|
|
.unwrap()
|
|
|
|
.keys()
|
|
|
|
.map(|k| k.to_string())
|
|
|
|
.collect();
|
|
|
|
keys.remove("imports");
|
|
|
|
keys.remove("scopes");
|
|
|
|
for key in keys {
|
|
|
|
diagnostics.push(format!("Invalid top-level key \"{}\". Only \"imports\" and \"scopes\" can be present.", key));
|
|
|
|
}
|
|
|
|
|
2019-06-09 09:08:20 -04:00
|
|
|
let import_map = ImportMap {
|
|
|
|
base_url: base_url.to_string(),
|
|
|
|
imports: normalized_imports,
|
|
|
|
scopes: normalized_scopes,
|
|
|
|
};
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
if !diagnostics.is_empty() {
|
|
|
|
info!("Import map diagnostics:");
|
|
|
|
for diagnotic in diagnostics {
|
|
|
|
info!(" - {}", diagnotic);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 09:08:20 -04:00
|
|
|
Ok(import_map)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn try_url_like_specifier(specifier: &str, base: &str) -> Option<Url> {
|
|
|
|
if specifier.starts_with('/')
|
|
|
|
|| specifier.starts_with("./")
|
|
|
|
|| specifier.starts_with("../")
|
|
|
|
{
|
2021-02-28 19:49:08 -05:00
|
|
|
if let Ok(base_url) = Url::parse(base) {
|
|
|
|
if let Ok(url) = base_url.join(specifier) {
|
|
|
|
return Some(url);
|
|
|
|
}
|
|
|
|
}
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(url) = Url::parse(specifier) {
|
2021-02-28 19:49:08 -05:00
|
|
|
return Some(url);
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parse provided key as import map specifier.
|
|
|
|
///
|
2021-09-05 10:22:45 -04:00
|
|
|
/// Specifiers must be valid URLs (eg. "`https://deno.land/x/std/testing/asserts.ts`")
|
2019-06-09 09:08:20 -04:00
|
|
|
/// or "bare" specifiers (eg. "moment").
|
|
|
|
fn normalize_specifier_key(
|
|
|
|
specifier_key: &str,
|
|
|
|
base_url: &str,
|
2021-02-28 19:49:08 -05:00
|
|
|
diagnostics: &mut Vec<String>,
|
2019-06-09 09:08:20 -04:00
|
|
|
) -> Option<String> {
|
|
|
|
// ignore empty keys
|
|
|
|
if specifier_key.is_empty() {
|
2021-02-28 19:49:08 -05:00
|
|
|
diagnostics.push("Invalid empty string specifier.".to_string());
|
2019-06-09 09:08:20 -04:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(url) =
|
|
|
|
ImportMap::try_url_like_specifier(specifier_key, base_url)
|
|
|
|
{
|
|
|
|
return Some(url.to_string());
|
|
|
|
}
|
|
|
|
|
|
|
|
// "bare" specifier
|
|
|
|
Some(specifier_key.to_string())
|
|
|
|
}
|
|
|
|
|
2021-07-29 15:20:34 -04:00
|
|
|
fn append_specifier_to_base(
|
|
|
|
base: &Url,
|
|
|
|
specifier: &str,
|
|
|
|
) -> Result<Url, AnyError> {
|
|
|
|
let mut base = base.clone();
|
|
|
|
let is_relative_or_absolute_specifier = specifier.starts_with("../")
|
|
|
|
|| specifier.starts_with("./")
|
|
|
|
|| specifier.starts_with('/');
|
|
|
|
|
|
|
|
// The specifier could be a windows path such as "C:/a/test.ts" in which
|
|
|
|
// case we don't want to use `join` because it will make the specifier
|
|
|
|
// the url since it contains what looks to be a uri scheme. To work around
|
|
|
|
// this, we append the specifier to the path segments of the base url when
|
|
|
|
// the specifier is not relative or absolute.
|
|
|
|
if !is_relative_or_absolute_specifier && base.path_segments_mut().is_ok() {
|
|
|
|
{
|
|
|
|
let mut segments = base.path_segments_mut().unwrap();
|
|
|
|
segments.pop_if_empty();
|
|
|
|
segments.extend(specifier.split('/'));
|
|
|
|
}
|
|
|
|
Ok(base)
|
|
|
|
} else {
|
|
|
|
Ok(base.join(specifier)?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 09:08:20 -04:00
|
|
|
/// Convert provided JSON map to valid SpecifierMap.
|
|
|
|
///
|
|
|
|
/// From specification:
|
|
|
|
/// - order of iteration must be retained
|
|
|
|
/// - SpecifierMap's keys are sorted in longest and alphabetic order
|
|
|
|
fn parse_specifier_map(
|
|
|
|
json_map: &Map<String, Value>,
|
|
|
|
base_url: &str,
|
2021-02-28 19:49:08 -05:00
|
|
|
diagnostics: &mut Vec<String>,
|
2019-06-09 09:08:20 -04:00
|
|
|
) -> SpecifierMap {
|
|
|
|
let mut normalized_map: SpecifierMap = SpecifierMap::new();
|
|
|
|
|
|
|
|
// Order is preserved because of "preserve_order" feature of "serde_json".
|
|
|
|
for (specifier_key, value) in json_map.iter() {
|
2021-02-28 19:49:08 -05:00
|
|
|
let normalized_specifier_key = match ImportMap::normalize_specifier_key(
|
|
|
|
specifier_key,
|
|
|
|
base_url,
|
|
|
|
diagnostics,
|
|
|
|
) {
|
|
|
|
Some(s) => s,
|
|
|
|
None => continue,
|
|
|
|
};
|
2019-06-09 09:08:20 -04:00
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let potential_address = match value {
|
|
|
|
Value::String(address) => address.to_string(),
|
|
|
|
_ => {
|
|
|
|
diagnostics.push(format!("Invalid address {:#?} for the specifier key \"{}\". Addresses must be strings.", value, specifier_key));
|
|
|
|
normalized_map.insert(normalized_specifier_key, None);
|
|
|
|
continue;
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let address_url =
|
|
|
|
match ImportMap::try_url_like_specifier(&potential_address, base_url) {
|
|
|
|
Some(url) => url,
|
|
|
|
None => {
|
|
|
|
diagnostics.push(format!(
|
|
|
|
"Invalid address \"{}\" for the specifier key \"{}\".",
|
|
|
|
potential_address, specifier_key
|
|
|
|
));
|
|
|
|
normalized_map.insert(normalized_specifier_key, None);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
2019-06-09 09:08:20 -04:00
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let address_url_string = address_url.to_string();
|
|
|
|
if specifier_key.ends_with('/') && !address_url_string.ends_with('/') {
|
|
|
|
diagnostics.push(format!(
|
|
|
|
"Invalid target address {:?} for package specifier {:?}. \
|
|
|
|
Package address targets must end with \"/\".",
|
|
|
|
address_url_string, specifier_key
|
|
|
|
));
|
|
|
|
normalized_map.insert(normalized_specifier_key, None);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
normalized_map.insert(normalized_specifier_key, Some(address_url));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sort in longest and alphabetical order.
|
2021-07-30 09:03:41 -04:00
|
|
|
normalized_map.sort_by(|k1, _v1, k2, _v2| match k1.cmp(k2) {
|
2019-12-23 09:59:44 -05:00
|
|
|
Ordering::Greater => Ordering::Less,
|
|
|
|
Ordering::Less => Ordering::Greater,
|
2021-02-28 19:49:08 -05:00
|
|
|
// JSON guarantees that there can't be duplicate keys
|
|
|
|
Ordering::Equal => unreachable!(),
|
2019-06-09 09:08:20 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
normalized_map
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Convert provided JSON map to valid ScopeMap.
|
|
|
|
///
|
|
|
|
/// From specification:
|
|
|
|
/// - order of iteration must be retained
|
|
|
|
/// - ScopeMap's keys are sorted in longest and alphabetic order
|
|
|
|
fn parse_scope_map(
|
|
|
|
scope_map: &Map<String, Value>,
|
|
|
|
base_url: &str,
|
2021-02-28 19:49:08 -05:00
|
|
|
diagnostics: &mut Vec<String>,
|
2019-06-09 09:08:20 -04:00
|
|
|
) -> Result<ScopesMap, ImportMapError> {
|
|
|
|
let mut normalized_map: ScopesMap = ScopesMap::new();
|
|
|
|
|
|
|
|
// Order is preserved because of "preserve_order" feature of "serde_json".
|
|
|
|
for (scope_prefix, potential_specifier_map) in scope_map.iter() {
|
|
|
|
if !potential_specifier_map.is_object() {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(format!(
|
2019-06-09 09:08:20 -04:00
|
|
|
"The value for the {:?} scope prefix must be an object",
|
|
|
|
scope_prefix
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
|
|
|
|
let potential_specifier_map =
|
|
|
|
potential_specifier_map.as_object().unwrap();
|
|
|
|
|
|
|
|
let scope_prefix_url =
|
|
|
|
match Url::parse(base_url).unwrap().join(scope_prefix) {
|
2021-02-28 19:49:08 -05:00
|
|
|
Ok(url) => url.to_string(),
|
|
|
|
_ => {
|
|
|
|
diagnostics.push(format!(
|
|
|
|
"Invalid scope \"{}\" (parsed against base URL \"{}\").",
|
|
|
|
scope_prefix, base_url
|
|
|
|
));
|
|
|
|
continue;
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let norm_map = ImportMap::parse_specifier_map(
|
|
|
|
potential_specifier_map,
|
|
|
|
base_url,
|
|
|
|
diagnostics,
|
|
|
|
);
|
2019-06-09 09:08:20 -04:00
|
|
|
|
|
|
|
normalized_map.insert(scope_prefix_url, norm_map);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort in longest and alphabetical order.
|
2021-07-30 09:03:41 -04:00
|
|
|
normalized_map.sort_by(|k1, _v1, k2, _v2| match k1.cmp(k2) {
|
2019-12-23 09:59:44 -05:00
|
|
|
Ordering::Greater => Ordering::Less,
|
|
|
|
Ordering::Less => Ordering::Greater,
|
2021-02-28 19:49:08 -05:00
|
|
|
// JSON guarantees that there can't be duplicate keys
|
|
|
|
Ordering::Equal => unreachable!(),
|
2019-06-09 09:08:20 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
Ok(normalized_map)
|
|
|
|
}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
fn resolve_scopes_match(
|
2019-06-09 09:08:20 -04:00
|
|
|
scopes: &ScopesMap,
|
|
|
|
normalized_specifier: &str,
|
2021-02-28 19:49:08 -05:00
|
|
|
as_url: Option<&Url>,
|
2019-06-09 09:08:20 -04:00
|
|
|
referrer: &str,
|
2021-02-28 19:49:08 -05:00
|
|
|
) -> Result<Option<Url>, ImportMapError> {
|
2019-06-09 09:08:20 -04:00
|
|
|
// exact-match
|
|
|
|
if let Some(scope_imports) = scopes.get(referrer) {
|
2021-02-28 19:49:08 -05:00
|
|
|
let scope_match = ImportMap::resolve_imports_match(
|
|
|
|
scope_imports,
|
|
|
|
normalized_specifier,
|
|
|
|
as_url,
|
|
|
|
)?;
|
|
|
|
// Return only if there was actual match (not None).
|
|
|
|
if scope_match.is_some() {
|
|
|
|
return Ok(scope_match);
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (normalized_scope_key, scope_imports) in scopes.iter() {
|
|
|
|
if normalized_scope_key.ends_with('/')
|
|
|
|
&& referrer.starts_with(normalized_scope_key)
|
|
|
|
{
|
2021-02-28 19:49:08 -05:00
|
|
|
let scope_match = ImportMap::resolve_imports_match(
|
|
|
|
scope_imports,
|
|
|
|
normalized_specifier,
|
|
|
|
as_url,
|
|
|
|
)?;
|
|
|
|
// Return only if there was actual match (not None).
|
|
|
|
if scope_match.is_some() {
|
|
|
|
return Ok(scope_match);
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
fn resolve_imports_match(
|
|
|
|
specifier_map: &SpecifierMap,
|
2019-06-09 09:08:20 -04:00
|
|
|
normalized_specifier: &str,
|
2021-02-28 19:49:08 -05:00
|
|
|
as_url: Option<&Url>,
|
|
|
|
) -> Result<Option<Url>, ImportMapError> {
|
2019-06-09 09:08:20 -04:00
|
|
|
// exact-match
|
2021-02-28 19:49:08 -05:00
|
|
|
if let Some(maybe_address) = specifier_map.get(normalized_specifier) {
|
|
|
|
if let Some(address) = maybe_address {
|
2019-06-12 15:00:08 -04:00
|
|
|
return Ok(Some(address.clone()));
|
2019-06-09 09:08:20 -04:00
|
|
|
} else {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(format!(
|
2021-02-28 19:49:08 -05:00
|
|
|
"Blocked by null entry for \"{:?}\"",
|
|
|
|
normalized_specifier
|
|
|
|
)));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
// Package-prefix match
|
2019-06-09 09:08:20 -04:00
|
|
|
// "most-specific wins", i.e. when there are multiple matching keys,
|
|
|
|
// choose the longest.
|
2021-02-28 19:49:08 -05:00
|
|
|
for (specifier_key, maybe_address) in specifier_map.iter() {
|
|
|
|
if !specifier_key.ends_with('/') {
|
|
|
|
continue;
|
|
|
|
}
|
2019-06-09 09:08:20 -04:00
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
if !normalized_specifier.starts_with(specifier_key) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(url) = as_url {
|
|
|
|
if !is_special(url) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-29 15:20:34 -04:00
|
|
|
let resolution_result = maybe_address.as_ref().ok_or_else(|| {
|
|
|
|
ImportMapError::Other(format!(
|
2021-02-28 19:49:08 -05:00
|
|
|
"Blocked by null entry for \"{:?}\"",
|
|
|
|
specifier_key
|
2021-07-29 15:20:34 -04:00
|
|
|
))
|
|
|
|
})?;
|
2021-02-28 19:49:08 -05:00
|
|
|
|
|
|
|
// Enforced by parsing.
|
|
|
|
assert!(resolution_result.to_string().ends_with('/'));
|
|
|
|
|
|
|
|
let after_prefix = &normalized_specifier[specifier_key.len()..];
|
|
|
|
|
2021-07-29 15:20:34 -04:00
|
|
|
let url = match ImportMap::append_specifier_to_base(
|
|
|
|
resolution_result,
|
|
|
|
after_prefix,
|
|
|
|
) {
|
2021-02-28 19:49:08 -05:00
|
|
|
Ok(url) => url,
|
|
|
|
Err(_) => {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(format!(
|
2021-02-28 19:49:08 -05:00
|
|
|
"Failed to resolve the specifier \"{:?}\" as its after-prefix
|
|
|
|
portion \"{:?}\" could not be URL-parsed relative to the URL prefix
|
|
|
|
\"{:?}\" mapped to by the prefix \"{:?}\"",
|
|
|
|
normalized_specifier,
|
|
|
|
after_prefix,
|
|
|
|
resolution_result,
|
|
|
|
specifier_key
|
|
|
|
)));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
2021-02-28 19:49:08 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
if !url.as_str().starts_with(resolution_result.as_str()) {
|
2021-05-30 20:20:34 -04:00
|
|
|
return Err(ImportMapError::Other(format!(
|
2021-02-28 19:49:08 -05:00
|
|
|
"The specifier \"{:?}\" backtracks above its prefix \"{:?}\"",
|
|
|
|
normalized_specifier, specifier_key
|
|
|
|
)));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
2021-02-28 19:49:08 -05:00
|
|
|
|
|
|
|
return Ok(Some(url));
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
debug!(
|
|
|
|
"Specifier {:?} was not mapped in import map.",
|
|
|
|
normalized_specifier
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn resolve(
|
|
|
|
&self,
|
|
|
|
specifier: &str,
|
|
|
|
referrer: &str,
|
2021-05-30 20:20:34 -04:00
|
|
|
) -> Result<Url, ImportMapError> {
|
2021-02-28 19:49:08 -05:00
|
|
|
let as_url: Option<Url> =
|
2019-06-09 09:08:20 -04:00
|
|
|
ImportMap::try_url_like_specifier(specifier, referrer);
|
2021-02-28 19:49:08 -05:00
|
|
|
let normalized_specifier = if let Some(url) = as_url.as_ref() {
|
|
|
|
url.to_string()
|
|
|
|
} else {
|
|
|
|
specifier.to_string()
|
2019-06-09 09:08:20 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
let scopes_match = ImportMap::resolve_scopes_match(
|
|
|
|
&self.scopes,
|
|
|
|
&normalized_specifier,
|
2021-02-28 19:49:08 -05:00
|
|
|
as_url.as_ref(),
|
2019-06-09 09:08:20 -04:00
|
|
|
&referrer.to_string(),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
// match found in scopes map
|
2021-05-30 20:20:34 -04:00
|
|
|
if let Some(scopes_match) = scopes_match {
|
2019-06-09 09:08:20 -04:00
|
|
|
return Ok(scopes_match);
|
|
|
|
}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
let imports_match = ImportMap::resolve_imports_match(
|
|
|
|
&self.imports,
|
|
|
|
&normalized_specifier,
|
|
|
|
as_url.as_ref(),
|
|
|
|
)?;
|
2019-06-09 09:08:20 -04:00
|
|
|
|
|
|
|
// match found in import map
|
2021-05-30 20:20:34 -04:00
|
|
|
if let Some(imports_match) = imports_match {
|
2019-06-09 09:08:20 -04:00
|
|
|
return Ok(imports_match);
|
|
|
|
}
|
|
|
|
|
2021-02-28 19:49:08 -05:00
|
|
|
// The specifier was able to be turned into a URL, but wasn't remapped into anything.
|
2021-05-30 20:20:34 -04:00
|
|
|
if let Some(as_url) = as_url {
|
2021-02-28 19:49:08 -05:00
|
|
|
return Ok(as_url);
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
|
2021-05-30 20:20:34 -04:00
|
|
|
Err(ImportMapError::UnmappedBareSpecifier(
|
|
|
|
specifier.to_string(),
|
|
|
|
Some(referrer.to_string()),
|
|
|
|
))
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-02-28 19:49:08 -05:00
|
|
|
|
2019-06-09 09:08:20 -04:00
|
|
|
use super::*;
|
2021-02-28 19:49:08 -05:00
|
|
|
use std::path::Path;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum TestKind {
|
|
|
|
Resolution {
|
|
|
|
given_specifier: String,
|
|
|
|
expected_specifier: Option<String>,
|
|
|
|
base_url: String,
|
|
|
|
},
|
|
|
|
Parse {
|
|
|
|
expected_import_map: Value,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct ImportMapTestCase {
|
|
|
|
name: String,
|
|
|
|
import_map: String,
|
|
|
|
import_map_base_url: String,
|
|
|
|
kind: TestKind,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_import_map_wpt_tests() -> Vec<String> {
|
|
|
|
let mut found_test_files = vec![];
|
|
|
|
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
|
|
|
let import_map_wpt_path =
|
|
|
|
repo_root.join("test_util/wpt/import-maps/data-driven/resources");
|
|
|
|
eprintln!("import map wpt path {:#?}", import_map_wpt_path);
|
|
|
|
for entry in WalkDir::new(import_map_wpt_path)
|
|
|
|
.contents_first(true)
|
|
|
|
.into_iter()
|
|
|
|
.filter_entry(|e| {
|
|
|
|
eprintln!("entry {:#?}", e);
|
|
|
|
if let Some(ext) = e.path().extension() {
|
|
|
|
return ext.to_string_lossy() == "json";
|
|
|
|
}
|
|
|
|
false
|
|
|
|
})
|
|
|
|
.filter_map(|e| match e {
|
|
|
|
Ok(e) => Some(e),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|e| PathBuf::from(e.path()))
|
|
|
|
{
|
|
|
|
found_test_files.push(entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut file_contents = vec![];
|
|
|
|
|
|
|
|
for file in found_test_files {
|
|
|
|
let content = std::fs::read_to_string(file).unwrap();
|
|
|
|
file_contents.push(content);
|
|
|
|
}
|
|
|
|
|
|
|
|
file_contents
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_import_map_tests(test_str: &str) -> Vec<ImportMapTestCase> {
|
|
|
|
let json_file: serde_json::Value = serde_json::from_str(test_str).unwrap();
|
|
|
|
let maybe_name = json_file
|
|
|
|
.get("name")
|
|
|
|
.map(|s| s.as_str().unwrap().to_string());
|
|
|
|
return parse_test_object(&json_file, maybe_name, None, None, None, None);
|
|
|
|
|
|
|
|
fn parse_test_object(
|
|
|
|
test_obj: &Value,
|
|
|
|
maybe_name_prefix: Option<String>,
|
|
|
|
maybe_import_map: Option<String>,
|
|
|
|
maybe_base_url: Option<String>,
|
|
|
|
maybe_import_map_base_url: Option<String>,
|
|
|
|
maybe_expected_import_map: Option<Value>,
|
|
|
|
) -> Vec<ImportMapTestCase> {
|
|
|
|
let maybe_import_map_base_url =
|
|
|
|
if let Some(base_url) = test_obj.get("importMapBaseURL") {
|
|
|
|
Some(base_url.as_str().unwrap().to_string())
|
|
|
|
} else {
|
|
|
|
maybe_import_map_base_url
|
|
|
|
};
|
|
|
|
|
|
|
|
let maybe_base_url = if let Some(base_url) = test_obj.get("baseURL") {
|
|
|
|
Some(base_url.as_str().unwrap().to_string())
|
|
|
|
} else {
|
|
|
|
maybe_base_url
|
|
|
|
};
|
|
|
|
|
|
|
|
let maybe_expected_import_map =
|
|
|
|
if let Some(im) = test_obj.get("expectedParsedImportMap") {
|
|
|
|
Some(im.to_owned())
|
|
|
|
} else {
|
|
|
|
maybe_expected_import_map
|
|
|
|
};
|
|
|
|
|
|
|
|
let maybe_import_map = if let Some(import_map) = test_obj.get("importMap")
|
|
|
|
{
|
|
|
|
Some(if import_map.is_string() {
|
|
|
|
import_map.as_str().unwrap().to_string()
|
|
|
|
} else {
|
|
|
|
serde_json::to_string(import_map).unwrap()
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
maybe_import_map
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(nested_tests) = test_obj.get("tests") {
|
|
|
|
let nested_tests_obj = nested_tests.as_object().unwrap();
|
|
|
|
let mut collected = vec![];
|
|
|
|
for (name, test_obj) in nested_tests_obj {
|
|
|
|
let nested_name = if let Some(ref name_prefix) = maybe_name_prefix {
|
|
|
|
format!("{}: {}", name_prefix, name)
|
|
|
|
} else {
|
|
|
|
name.to_string()
|
|
|
|
};
|
|
|
|
let parsed_nested_tests = parse_test_object(
|
|
|
|
test_obj,
|
|
|
|
Some(nested_name),
|
|
|
|
maybe_import_map.clone(),
|
|
|
|
maybe_base_url.clone(),
|
|
|
|
maybe_import_map_base_url.clone(),
|
|
|
|
maybe_expected_import_map.clone(),
|
|
|
|
);
|
|
|
|
collected.extend(parsed_nested_tests)
|
|
|
|
}
|
|
|
|
return collected;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut collected_cases = vec![];
|
|
|
|
if let Some(results) = test_obj.get("expectedResults") {
|
|
|
|
let expected_results = results.as_object().unwrap();
|
|
|
|
for (given, expected) in expected_results {
|
|
|
|
let name = if let Some(ref name_prefix) = maybe_name_prefix {
|
|
|
|
format!("{}: {}", name_prefix, given)
|
|
|
|
} else {
|
|
|
|
given.to_string()
|
|
|
|
};
|
|
|
|
let given_specifier = given.to_string();
|
|
|
|
let expected_specifier = expected.as_str().map(|str| str.to_string());
|
|
|
|
|
|
|
|
let test_case = ImportMapTestCase {
|
|
|
|
name,
|
|
|
|
import_map_base_url: maybe_import_map_base_url.clone().unwrap(),
|
|
|
|
import_map: maybe_import_map.clone().unwrap(),
|
|
|
|
kind: TestKind::Resolution {
|
|
|
|
given_specifier,
|
|
|
|
expected_specifier,
|
|
|
|
base_url: maybe_base_url.clone().unwrap(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
collected_cases.push(test_case);
|
|
|
|
}
|
|
|
|
} else if let Some(expected_import_map) = maybe_expected_import_map {
|
|
|
|
let test_case = ImportMapTestCase {
|
|
|
|
name: maybe_name_prefix.unwrap(),
|
|
|
|
import_map_base_url: maybe_import_map_base_url.unwrap(),
|
|
|
|
import_map: maybe_import_map.unwrap(),
|
|
|
|
kind: TestKind::Parse {
|
|
|
|
expected_import_map,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
collected_cases.push(test_case);
|
|
|
|
} else {
|
|
|
|
eprintln!("unreachable {:#?}", test_obj);
|
|
|
|
unreachable!();
|
|
|
|
}
|
|
|
|
|
|
|
|
collected_cases
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn run_import_map_test_cases(tests: Vec<ImportMapTestCase>) {
|
|
|
|
for test in tests {
|
|
|
|
match &test.kind {
|
|
|
|
TestKind::Resolution {
|
|
|
|
given_specifier,
|
|
|
|
expected_specifier,
|
|
|
|
base_url,
|
|
|
|
} => {
|
|
|
|
let import_map =
|
|
|
|
ImportMap::from_json(&test.import_map_base_url, &test.import_map)
|
|
|
|
.unwrap();
|
|
|
|
let maybe_resolved = import_map
|
2021-07-30 09:03:41 -04:00
|
|
|
.resolve(given_specifier, base_url)
|
2021-02-28 19:49:08 -05:00
|
|
|
.ok()
|
2021-05-30 20:20:34 -04:00
|
|
|
.map(|url| url.to_string());
|
2021-02-28 19:49:08 -05:00
|
|
|
assert_eq!(expected_specifier, &maybe_resolved, "{}", test.name);
|
|
|
|
}
|
|
|
|
TestKind::Parse {
|
|
|
|
expected_import_map,
|
|
|
|
} => {
|
|
|
|
if matches!(expected_import_map, Value::Null) {
|
|
|
|
assert!(ImportMap::from_json(
|
|
|
|
&test.import_map_base_url,
|
|
|
|
&test.import_map
|
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
} else {
|
|
|
|
let import_map =
|
|
|
|
ImportMap::from_json(&test.import_map_base_url, &test.import_map)
|
|
|
|
.unwrap();
|
|
|
|
let import_map_value = serde_json::to_value(import_map).unwrap();
|
|
|
|
assert_eq!(expected_import_map, &import_map_value, "{}", test.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn wpt() {
|
|
|
|
let test_file_contents = load_import_map_wpt_tests();
|
|
|
|
eprintln!("Found test files {}", test_file_contents.len());
|
|
|
|
|
|
|
|
for test_file in test_file_contents {
|
|
|
|
let tests = parse_import_map_tests(&test_file);
|
|
|
|
run_import_map_test_cases(tests);
|
|
|
|
}
|
|
|
|
}
|
2019-06-09 09:08:20 -04:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn from_json_1() {
|
|
|
|
let base_url = "https://deno.land";
|
|
|
|
|
|
|
|
// empty JSON
|
|
|
|
assert!(ImportMap::from_json(base_url, "{}").is_ok());
|
|
|
|
|
|
|
|
let non_object_strings = vec!["null", "true", "1", "\"foo\"", "[]"];
|
|
|
|
|
|
|
|
// invalid JSON
|
|
|
|
for non_object in non_object_strings.to_vec() {
|
|
|
|
assert!(ImportMap::from_json(base_url, non_object).is_err());
|
|
|
|
}
|
|
|
|
|
2021-08-03 15:29:12 -04:00
|
|
|
// invalid JSON message test
|
|
|
|
assert_eq!(
|
|
|
|
ImportMap::from_json(base_url, "{\"a\":1,}")
|
|
|
|
.unwrap_err()
|
|
|
|
.to_string(),
|
|
|
|
"Unable to parse import map JSON: trailing comma at line 1 column 8",
|
|
|
|
);
|
|
|
|
|
2019-06-09 09:08:20 -04:00
|
|
|
// invalid schema: 'imports' is non-object
|
|
|
|
for non_object in non_object_strings.to_vec() {
|
2019-07-31 17:11:37 -04:00
|
|
|
assert!(ImportMap::from_json(
|
|
|
|
base_url,
|
|
|
|
&format!("{{\"imports\": {}}}", non_object),
|
|
|
|
)
|
|
|
|
.is_err());
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// invalid schema: 'scopes' is non-object
|
|
|
|
for non_object in non_object_strings.to_vec() {
|
2019-07-31 17:11:37 -04:00
|
|
|
assert!(ImportMap::from_json(
|
|
|
|
base_url,
|
|
|
|
&format!("{{\"scopes\": {}}}", non_object),
|
|
|
|
)
|
|
|
|
.is_err());
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn from_json_2() {
|
|
|
|
let json_map = r#"{
|
|
|
|
"imports": {
|
|
|
|
"foo": "https://example.com/1",
|
|
|
|
"bar": ["https://example.com/2"],
|
|
|
|
"fizz": null
|
|
|
|
}
|
|
|
|
}"#;
|
|
|
|
let result = ImportMap::from_json("https://deno.land", json_map);
|
|
|
|
assert!(result.is_ok());
|
|
|
|
}
|
2021-07-29 15:20:34 -04:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn mapped_windows_file_specifier() {
|
|
|
|
// from issue #11530
|
|
|
|
let mut specifiers = SpecifierMap::new();
|
|
|
|
specifiers.insert(
|
|
|
|
"file:///".to_string(),
|
|
|
|
Some(Url::parse("http://localhost/").unwrap()),
|
|
|
|
);
|
|
|
|
|
|
|
|
let resolved_specifier = ImportMap::resolve_imports_match(
|
|
|
|
&specifiers,
|
|
|
|
"file:///C:/folder/file.ts",
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
.unwrap()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
resolved_specifier.as_str(),
|
|
|
|
"http://localhost/C:/folder/file.ts"
|
|
|
|
);
|
|
|
|
}
|
2019-06-09 09:08:20 -04:00
|
|
|
}
|