summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2
-rw-r--r--build_extra/rust/BUILD.gn6
-rw-r--r--cli/BUILD.gn1
-rw-r--r--cli/Cargo.toml3
-rw-r--r--cli/errors.rs14
-rw-r--r--cli/flags.rs57
-rw-r--r--cli/import_map.rs2133
-rw-r--r--cli/main.rs2
-rw-r--r--cli/msg.fbs1
-rw-r--r--cli/ops.rs25
-rw-r--r--cli/state.rs86
-rw-r--r--core/modules.rs30
-rw-r--r--tests/033_import_map.out7
-rw-r--r--tests/033_import_map.test2
-rw-r--r--tests/importmaps/import_map.json14
-rw-r--r--tests/importmaps/lodash/lodash.ts1
-rw-r--r--tests/importmaps/lodash/other_file.ts1
-rw-r--r--tests/importmaps/moment/moment.ts1
-rw-r--r--tests/importmaps/moment/other_file.ts1
-rw-r--r--tests/importmaps/scope/scoped.ts2
-rw-r--r--tests/importmaps/scoped_moment.ts1
-rw-r--r--tests/importmaps/test.ts6
-rw-r--r--tests/importmaps/vue.ts1
-rw-r--r--website/manual.md45
24 files changed, 2406 insertions, 36 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4b51253da..8977d2756 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -221,6 +221,7 @@ dependencies = [
"http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.12.29 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-rustls 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"integer-atomics 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -950,6 +951,7 @@ name = "serde_json"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
+ "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/build_extra/rust/BUILD.gn b/build_extra/rust/BUILD.gn
index 1dcc5dfb7..ab9210788 100644
--- a/build_extra/rust/BUILD.gn
+++ b/build_extra/rust/BUILD.gn
@@ -1301,11 +1301,15 @@ rust_proc_macro("serde_derive") {
rust_rlib("serde_json") {
edition = "2015"
source_root = "$cargo_home/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.39/src/lib.rs"
- features = [ "default" ]
+ features = [
+ "default",
+ "preserve_order",
+ ]
extern_rlib = [
"itoa",
"ryu",
"serde",
+ "indexmap",
]
args = [
"--cap-lints",
diff --git a/cli/BUILD.gn b/cli/BUILD.gn
index 45386f320..484a3f74d 100644
--- a/cli/BUILD.gn
+++ b/cli/BUILD.gn
@@ -29,6 +29,7 @@ main_extern_rlib = [
"http",
"hyper",
"hyper_rustls",
+ "indexmap",
"lazy_static",
"libc",
"log",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index d2481bb6e..9221cd8c4 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -27,6 +27,7 @@ futures = "0.1.27"
http = "0.1.17"
hyper = "0.12.29"
hyper-rustls = "0.16.1"
+indexmap = "1.0.2"
integer-atomics = "1.0.2"
lazy_static = "1.3.0"
libc = "0.2.55"
@@ -38,7 +39,7 @@ ring = "0.14.6"
rustyline = "4.1.0"
serde = "1.0.91"
serde_derive = "1.0.91"
-serde_json = "1.0.39"
+serde_json = { version = "1.0.39", features = [ "preserve_order" ] }
source-map-mappings = "0.5.0"
tempfile = "3.0.8"
tokio = "0.1.20"
diff --git a/cli/errors.rs b/cli/errors.rs
index 8e57fe5f4..eb0fc7d27 100644
--- a/cli/errors.rs
+++ b/cli/errors.rs
@@ -1,4 +1,5 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+use crate::import_map::ImportMapError;
use crate::js_errors::JSErrorColor;
pub use crate::msg::ErrorKind;
use crate::resolve_addr::ResolveAddrError;
@@ -24,6 +25,7 @@ enum Repr {
IoErr(io::Error),
UrlErr(url::ParseError),
HyperErr(hyper::Error),
+ ImportMapErr(ImportMapError),
}
pub fn new(kind: ErrorKind, msg: String) -> DenoError {
@@ -92,6 +94,7 @@ impl DenoError {
ErrorKind::HttpOther
}
}
+ Repr::ImportMapErr(ref _err) => ErrorKind::ImportMapError,
}
}
}
@@ -103,6 +106,7 @@ impl fmt::Display for DenoError {
Repr::IoErr(ref err) => err.fmt(f),
Repr::UrlErr(ref err) => err.fmt(f),
Repr::HyperErr(ref err) => err.fmt(f),
+ Repr::ImportMapErr(ref err) => f.pad(&err.msg),
}
}
}
@@ -114,6 +118,7 @@ impl std::error::Error for DenoError {
Repr::IoErr(ref err) => err.description(),
Repr::UrlErr(ref err) => err.description(),
Repr::HyperErr(ref err) => err.description(),
+ Repr::ImportMapErr(ref err) => &err.msg,
}
}
@@ -123,6 +128,7 @@ impl std::error::Error for DenoError {
Repr::IoErr(ref err) => Some(err),
Repr::UrlErr(ref err) => Some(err),
Repr::HyperErr(ref err) => Some(err),
+ Repr::ImportMapErr(ref _err) => None,
}
}
}
@@ -202,6 +208,14 @@ impl From<UnixError> for DenoError {
}
}
+impl From<ImportMapError> for DenoError {
+ fn from(err: ImportMapError) -> Self {
+ Self {
+ repr: Repr::ImportMapErr(err),
+ }
+ }
+}
+
pub fn bad_resource() -> DenoError {
new(ErrorKind::BadResource, String::from("bad resource id"))
}
diff --git a/cli/flags.rs b/cli/flags.rs
index b9a298d28..b5e759f25 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -15,6 +15,9 @@ pub struct DenoFlags {
/// When the `--config`/`-c` flag is used to pass the name, this will be set
/// the path passed on the command line, otherwise `None`.
pub config_path: Option<String>,
+ /// When the `--importmap` flag is used to pass the name, this will be set
+ /// the path passed on the command line, otherwise `None`.
+ pub import_map_path: Option<String>,
pub allow_read: bool,
pub read_whitelist: Vec<String>,
pub allow_write: bool,
@@ -82,6 +85,16 @@ fn add_run_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
Arg::with_name("no-prompt")
.long("no-prompt")
.help("Do not use prompts"),
+ ).arg(
+ Arg::with_name("importmap")
+ .long("importmap")
+ .value_name("FILE")
+ .help("Load import map file")
+ .long_help(
+ "Load import map file
+Specification: https://wicg.github.io/import-maps/
+Examples: https://github.com/WICG/import-maps#the-import-map",
+ ).takes_value(true),
)
}
@@ -367,10 +380,10 @@ pub fn parse_flags(matches: &ArgMatches) -> DenoFlags {
flags.v8_flags = Some(v8_flags);
}
- flags = parse_permission_args(flags, matches);
+ flags = parse_run_args(flags, matches);
// flags specific to "run" subcommand
if let Some(run_matches) = matches.subcommand_matches("run") {
- flags = parse_permission_args(flags.clone(), run_matches);
+ flags = parse_run_args(flags.clone(), run_matches);
}
flags
@@ -378,10 +391,7 @@ pub fn parse_flags(matches: &ArgMatches) -> DenoFlags {
/// Parse permission specific matches Args and assign to DenoFlags.
/// This method is required because multiple subcommands use permission args.
-fn parse_permission_args(
- mut flags: DenoFlags,
- matches: &ArgMatches,
-) -> DenoFlags {
+fn parse_run_args(mut flags: DenoFlags, matches: &ArgMatches) -> DenoFlags {
if matches.is_present("allow-read") {
if matches.value_of("allow-read").is_some() {
let read_wl = matches.values_of("allow-read").unwrap();
@@ -435,6 +445,7 @@ fn parse_permission_args(
if matches.is_present("no-prompt") {
flags.no_prompts = true;
}
+ flags.import_map_path = matches.value_of("importmap").map(ToOwned::to_owned);
flags
}
@@ -912,6 +923,7 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Xeval);
assert_eq!(argv, svec!["deno", "console.log(val)"]);
}
+
#[test]
fn test_flags_from_vec_19() {
use tempfile::TempDir;
@@ -936,6 +948,7 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Run);
assert_eq!(argv, svec!["deno", "script.ts"]);
}
+
#[test]
fn test_flags_from_vec_20() {
use tempfile::TempDir;
@@ -960,6 +973,7 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Run);
assert_eq!(argv, svec!["deno", "script.ts"]);
}
+
#[test]
fn test_flags_from_vec_21() {
let (flags, subcommand, argv) = flags_from_vec(svec![
@@ -1067,4 +1081,35 @@ mod tests {
assert_eq!(subcommand, DenoSubcommand::Bundle);
assert_eq!(argv, svec!["deno", "source.ts", "bundle.js"])
}
+
+ #[test]
+ fn test_flags_from_vec_27() {
+ let (flags, subcommand, argv) = flags_from_vec(svec![
+ "deno",
+ "run",
+ "--importmap=importmap.json",
+ "script.ts"
+ ]);
+ assert_eq!(
+ flags,
+ DenoFlags {
+ import_map_path: Some("importmap.json".to_owned()),
+ ..DenoFlags::default()
+ }
+ );
+ assert_eq!(subcommand, DenoSubcommand::Run);
+ assert_eq!(argv, svec!["deno", "script.ts"]);
+
+ let (flags, subcommand, argv) =
+ flags_from_vec(svec!["deno", "--importmap=importmap.json", "script.ts"]);
+ assert_eq!(
+ flags,
+ DenoFlags {
+ import_map_path: Some("importmap.json".to_owned()),
+ ..DenoFlags::default()
+ }
+ );
+ assert_eq!(subcommand, DenoSubcommand::Run);
+ assert_eq!(argv, svec!["deno", "script.ts"]);
+ }
}
diff --git a/cli/import_map.rs b/cli/import_map.rs
new file mode 100644
index 000000000..5fe423595
--- /dev/null
+++ b/cli/import_map.rs
@@ -0,0 +1,2133 @@
+use indexmap::IndexMap;
+use serde_json::Map;
+use serde_json::Value;
+use std::cmp::Ordering;
+use std::fs;
+use url::Url;
+
+#[derive(Debug)]
+pub struct ImportMapError {
+ pub msg: String,
+}
+
+impl ImportMapError {
+ pub fn new(msg: &str) -> Self {
+ ImportMapError {
+ msg: msg.to_string(),
+ }
+ }
+}
+
+// NOTE: here is difference between deno and reference implementation - deno currently
+// can't resolve URL with other schemes (eg. data:, about:, blob:)
+const SUPPORTED_FETCH_SCHEMES: [&str; 3] = ["http", "https", "file"];
+
+type SpecifierMap = IndexMap<String, Vec<String>>;
+type ScopesMap = IndexMap<String, SpecifierMap>;
+
+#[derive(Debug)]
+pub struct ImportMap {
+ base_url: String,
+ imports: SpecifierMap,
+ scopes: ScopesMap,
+}
+
+impl ImportMap {
+ pub fn load(base_url: &str, file_name: &str) -> Result<Self, ImportMapError> {
+ let cwd = std::env::current_dir().unwrap();
+ let resolved_path = cwd.join(file_name);
+ debug!(
+ "Attempt to load import map: {}",
+ resolved_path.to_str().unwrap()
+ );
+
+ // Load the contents of import map
+ match fs::read_to_string(&resolved_path) {
+ Ok(json_string) => ImportMap::from_json(base_url, &json_string),
+ _ => panic!(
+ "Error retrieving import map file at \"{}\"",
+ resolved_path.to_str().unwrap()
+ ),
+ }
+ }
+
+ 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,
+ Err(_) => {
+ return Err(ImportMapError::new("Unable to parse import map JSON"));
+ }
+ };
+
+ match v {
+ Value::Object(_) => {}
+ _ => {
+ return Err(ImportMapError::new("Import map JSON must be an object"));
+ }
+ }
+
+ let normalized_imports = match &v.get("imports") {
+ Some(imports_map) => {
+ if !imports_map.is_object() {
+ return Err(ImportMapError::new(
+ "Import map's 'imports' must be an object",
+ ));
+ }
+
+ let imports_map = imports_map.as_object().unwrap();
+ ImportMap::parse_specifier_map(imports_map, base_url)
+ }
+ None => IndexMap::new(),
+ };
+
+ let normalized_scopes = match &v.get("scopes") {
+ Some(scope_map) => {
+ if !scope_map.is_object() {
+ return Err(ImportMapError::new(
+ "Import map's 'scopes' must be an object",
+ ));
+ }
+
+ let scope_map = scope_map.as_object().unwrap();
+ ImportMap::parse_scope_map(scope_map, base_url)?
+ }
+ None => IndexMap::new(),
+ };
+
+ let import_map = ImportMap {
+ base_url: base_url.to_string(),
+ imports: normalized_imports,
+ scopes: normalized_scopes,
+ };
+
+ Ok(import_map)
+ }
+
+ fn try_url_like_specifier(specifier: &str, base: &str) -> Option<Url> {
+ // this should never fail
+ if specifier.starts_with('/')
+ || specifier.starts_with("./")
+ || specifier.starts_with("../")
+ {
+ let base_url = Url::parse(base).unwrap();
+ let url = base_url.join(specifier).unwrap();
+ return Some(url);
+ }
+
+ if let Ok(url) = Url::parse(specifier) {
+ if SUPPORTED_FETCH_SCHEMES.contains(&url.scheme()) {
+ return Some(url);
+ }
+ }
+
+ None
+ }
+
+ /// Parse provided key as import map specifier.
+ ///
+ /// Specifiers must be valid URLs (eg. "https://deno.land/x/std/testing/mod.ts")
+ /// or "bare" specifiers (eg. "moment").
+ // TODO: add proper error handling: https://github.com/WICG/import-maps/issues/100
+ fn normalize_specifier_key(
+ specifier_key: &str,
+ base_url: &str,
+ ) -> Option<String> {
+ // ignore empty keys
+ if specifier_key.is_empty() {
+ 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())
+ }
+
+ /// Parse provided addresses as valid URLs.
+ ///
+ /// Non-valid addresses are skipped.
+ fn normalize_addresses(
+ specifier_key: &str,
+ base_url: &str,
+ potential_addresses: Vec<String>,
+ ) -> Vec<String> {
+ let mut normalized_addresses: Vec<String> = vec![];
+
+ for potential_address in potential_addresses {
+ let url =
+ match ImportMap::try_url_like_specifier(&potential_address, base_url) {
+ Some(url) => url,
+ None => continue,
+ };
+
+ let url_string = url.to_string();
+ if specifier_key.ends_with('/') && !url_string.ends_with('/') {
+ eprintln!(
+ "Invalid target address {:?} for package specifier {:?}.\
+ Package address targets must end with \"/\".",
+ url_string, specifier_key
+ );
+ continue;
+ }
+
+ normalized_addresses.push(url_string);
+ }
+
+ normalized_addresses
+ }
+
+ /// 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,
+ ) -> 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() {
+ let normalized_specifier_key =
+ match ImportMap::normalize_specifier_key(specifier_key, base_url) {
+ Some(s) => s,
+ None => continue,
+ };
+
+ let potential_addresses: Vec<String> = match value {
+ Value::String(address) => vec![address.to_string()],
+ Value::Array(address_array) => {
+ let mut string_addresses: Vec<String> = vec![];
+
+ for address in address_array {
+ match address {
+ Value::String(address) => {
+ string_addresses.push(address.to_string())
+ }
+ _ => continue,
+ }
+ }
+
+ string_addresses
+ }
+ Value::Null => vec![],
+ _ => vec![],
+ };
+
+ let normalized_address_array = ImportMap::normalize_addresses(
+ &normalized_specifier_key,
+ base_url,
+ potential_addresses,
+ );
+
+ debug!(
+ "normalized specifier {:?}; {:?}",
+ normalized_specifier_key, normalized_address_array
+ );
+ normalized_map.insert(normalized_specifier_key, normalized_address_array);
+ }
+
+ // Sort in longest and alphabetical order.
+ normalized_map.sort_by(|k1, _v1, k2, _v2| {
+ if k1.len() > k2.len() {
+ return Ordering::Less;
+ } else if k2.len() > k1.len() {
+ return Ordering::Greater;
+ }
+
+ k2.cmp(k1)
+ });
+
+ 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,
+ ) -> 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() {
+ return Err(ImportMapError::new(&format!(
+ "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) {
+ Ok(url) => {
+ if !SUPPORTED_FETCH_SCHEMES.contains(&url.scheme()) {
+ eprintln!(
+ "Invalid scope {:?}. Scope URLs must have a valid fetch scheme.",
+ url.to_string()
+ );
+ continue;
+ }
+ url.to_string()
+ }
+ _ => continue,
+ };
+
+ let norm_map =
+ ImportMap::parse_specifier_map(potential_specifier_map, base_url);
+
+ normalized_map.insert(scope_prefix_url, norm_map);
+ }
+
+ // Sort in longest and alphabetical order.
+ normalized_map.sort_by(|k1, _v1, k2, _v2| {
+ if k1.len() > k2.len() {
+ return Ordering::Less;
+ } else if k2.len() > k1.len() {
+ return Ordering::Greater;
+ }
+
+ k2.cmp(k1)
+ });
+
+ Ok(normalized_map)
+ }
+
+ pub fn resolve_scopes_match(
+ scopes: &ScopesMap,
+ normalized_specifier: &str,
+ referrer: &str,
+ ) -> Result<Option<String>, ImportMapError> {
+ // exact-match
+ if let Some(scope_imports) = scopes.get(referrer) {
+ if let Ok(scope_match) =
+ ImportMap::resolve_imports_match(scope_imports, normalized_specifier)
+ {
+ // Return only if there was actual match (not None).
+ if scope_match.is_some() {
+ return Ok(scope_match);
+ }
+ }
+ }
+
+ for (normalized_scope_key, scope_imports) in scopes.iter() {
+ if normalized_scope_key.ends_with('/')
+ && referrer.starts_with(normalized_scope_key)
+ {
+ if let Ok(scope_match) =
+ ImportMap::resolve_imports_match(scope_imports, normalized_specifier)
+ {
+ // Return only if there was actual match (not None).
+ if scope_match.is_some() {
+ return Ok(scope_match);
+ }
+ }
+ }
+ }
+
+ Ok(None)
+ }
+
+ // TODO: https://github.com/WICG/import-maps/issues/73#issuecomment-439327758
+ // for some more optimized candidate implementations.
+ pub fn resolve_imports_match(
+ imports: &SpecifierMap,
+ normalized_specifier: &str,
+ ) -> Result<Option<String>, ImportMapError> {
+ // exact-match
+ if let Some(address_vec) = imports.get(normalized_specifier) {
+ if address_vec.is_empty() {
+ return Err(ImportMapError::new(&format!(
+ "Specifier {:?} was mapped to no addresses.",
+ normalized_specifier
+ )));
+ } else if address_vec.len() == 1 {
+ let address = address_vec.first().unwrap();
+ debug!(
+ "Specifier {:?} was mapped to {:?}.",
+ normalized_specifier, address
+ );
+ return Ok(Some(address.to_string()));
+ } else {
+ return Err(ImportMapError::new(
+ "Multi-address mappings are not yet supported",
+ ));
+ }
+ }
+
+ // package-prefix match
+ // "most-specific wins", i.e. when there are multiple matching keys,
+ // choose the longest.
+ // https://github.com/WICG/import-maps/issues/102
+ for (specifier_key, address_vec) in imports.iter() {
+ if specifier_key.ends_with('/')
+ && normalized_specifier.starts_with(specifier_key)
+ {
+ if address_vec.is_empty() {
+ return Err(ImportMapError::new(&format!("Specifier {:?} was mapped to no addresses (via prefix specifier key {:?}).", normalized_specifier, specifier_key)));
+ } else if address_vec.len() == 1 {
+ let address = address_vec.first().unwrap();
+ let after_prefix = &normalized_specifier[specifier_key.len()..];
+
+ if let Ok(base_url) = Url::parse(address) {
+ if let Ok(url) = base_url.join(after_prefix) {
+ let resolved_url = url.to_string();
+ debug!("Specifier {:?} was mapped to {:?} (via prefix specifier key {:?}).", normalized_specifier, resolved_url, address);
+ return Ok(Some(resolved_url));
+ }
+ }
+
+ unreachable!();
+ } else {
+ return Err(ImportMapError::new(
+ "Multi-address mappings are not yet supported",
+ ));
+ }
+ }
+ }
+
+ debug!(
+ "Specifier {:?} was not mapped in import map.",
+ normalized_specifier
+ );
+
+ Ok(None)
+ }
+
+ // TODO: add support for built-in modules
+ /// Currently we support two types of specifiers: URL (http://, https://, file://)
+ /// and "bare" (moment, jquery, lodash)
+ ///
+ /// Scenarios:
+ /// 1. import resolved using import map -> String
+ /// 2. import restricted by import map -> ImportMapError
+ /// 3. import not mapped -> None
+ pub fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ ) -> Result<Option<String>, ImportMapError> {
+ let resolved_url: Option<Url> =
+ ImportMap::try_url_like_specifier(specifier, referrer);
+ let normalized_specifier = match &resolved_url {
+ Some(url) => url.to_string(),
+ None => specifier.to_string(),
+ };
+
+ let scopes_match = ImportMap::resolve_scopes_match(
+ &self.scopes,
+ &normalized_specifier,
+ &referrer.to_string(),
+ )?;
+
+ // match found in scopes map
+ if scopes_match.is_some() {
+ return Ok(scopes_match);
+ }
+
+ let imports_match =
+ ImportMap::resolve_imports_match(&self.imports, &normalized_specifier)?;
+
+ // match found in import map
+ if imports_match.is_some() {
+ return Ok(imports_match);
+ }
+
+ // no match in import map but we got resolvable URL
+ if let Some(resolved_url) = resolved_url {
+ return Ok(Some(resolved_url.to_string()));
+ }
+
+ Err(ImportMapError::new(&format!(
+ "Unmapped bare specifier {:?}",
+ normalized_specifier
+ )))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[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());
+ }
+
+ // invalid schema: 'imports' is non-object
+ for non_object in non_object_strings.to_vec() {
+ assert!(
+ ImportMap::from_json(
+ base_url,
+ &format!("{{\"imports\": {}}}", non_object),
+ ).is_err()
+ );
+ }
+
+ // invalid schema: 'scopes' is non-object
+ for non_object in non_object_strings.to_vec() {
+ assert!(
+ ImportMap::from_json(
+ base_url,
+ &format!("{{\"scopes\": {}}}", non_object),
+ ).is_err()
+ );
+ }
+ }
+
+ #[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());
+ }
+
+ #[test]
+ fn parse_specifier_keys_relative() {
+ // Should absolutize strings prefixed with ./, ../, or / into the corresponding URLs..
+ let json_map = r#"{
+ "imports": {
+ "./foo": "/dotslash",
+ "../foo": "/dotdotslash",
+ "/foo": "/slash"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert_eq!(
+ import_map
+ .imports
+ .get("https://base.example/path1/path2/foo")
+ .unwrap()[0],
+ "https://base.example/dotslash".to_string()
+ );
+ assert_eq!(
+ import_map
+ .imports
+ .get("https://base.example/path1/foo")
+ .unwrap()[0],
+ "https://base.example/dotdotslash".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://base.example/foo").unwrap()[0],
+ "https://base.example/slash".to_string()
+ );
+
+ // Should absolutize the literal strings ./, ../, or / with no suffix..
+ let json_map = r#"{
+ "imports": {
+ "./": "/dotslash/",
+ "../": "/dotdotslash/",
+ "/": "/slash/"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert_eq!(
+ import_map
+ .imports
+ .get("https://base.example/path1/path2/")
+ .unwrap()[0],
+ "https://base.example/dotslash/".to_string()
+ );
+ assert_eq!(
+ import_map
+ .imports
+ .get("https://base.example/path1/")
+ .unwrap()[0],
+ "https://base.example/dotdotslash/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://base.example/").unwrap()[0],
+ "https://base.example/slash/".to_string()
+ );
+
+ // Should treat percent-encoded variants of ./, ../, or / as bare specifiers..
+ let json_map = r#"{
+ "imports": {
+ "%2E/": "/dotSlash1/",
+ "%2E%2E/": "/dotDotSlash1/",
+ ".%2F": "/dotSlash2",
+ "..%2F": "/dotDotSlash2",
+ "%2F": "/slash2",
+ "%2E%2F": "/dotSlash3",
+ "%2E%2E%2F": "/dotDotSlash3"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert_eq!(
+ import_map.imports.get("%2E/").unwrap()[0],
+ "https://base.example/dotSlash1/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("%2E%2E/").unwrap()[0],
+ "https://base.example/dotDotSlash1/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get(".%2F").unwrap()[0],
+ "https://base.example/dotSlash2".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("..%2F").unwrap()[0],
+ "https://base.example/dotDotSlash2".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("%2F").unwrap()[0],
+ "https://base.example/slash2".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("%2E%2F").unwrap()[0],
+ "https://base.example/dotSlash3".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("%2E%2E%2F").unwrap()[0],
+ "https://base.example/dotDotSlash3".to_string()
+ );
+ }
+
+ #[test]
+ fn parse_specifier_keys_absolute() {
+ // Should only accept absolute URL specifier keys with fetch schemes,.
+ // treating others as bare specifiers.
+ let json_map = r#"{
+ "imports": {
+ "file:///good": "/file",
+ "http://good/": "/http/",
+ "https://good/": "/https/",
+ "about:bad": "/about",
+ "blob:bad": "/blob",
+ "data:bad": "/data",
+ "filesystem:bad": "/filesystem",
+ "ftp://bad/": "/ftp/",
+ "import:bad": "/import",
+ "mailto:bad": "/mailto",
+ "javascript:bad": "/javascript",
+ "wss:bad": "/wss"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert_eq!(
+ import_map.imports.get("http://good/").unwrap()[0],
+ "https://base.example/http/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://good/").unwrap()[0],
+ "https://base.example/https/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("file:///good").unwrap()[0],
+ "https://base.example/file".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("http://good/").unwrap()[0],
+ "https://base.example/http/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("import:bad").unwrap()[0],
+ "https://base.example/import".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("mailto:bad").unwrap()[0],
+ "https://base.example/mailto".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("javascript:bad").unwrap()[0],
+ "https://base.example/javascript".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("wss:bad").unwrap()[0],
+ "https://base.example/wss".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("about:bad").unwrap()[0],
+ "https://base.example/about".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("blob:bad").unwrap()[0],
+ "https://base.example/blob".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("data:bad").unwrap()[0],
+ "https://base.example/data".to_string()
+ );
+
+ // Should parse absolute URLs, treating unparseable ones as bare specifiers..
+ let json_map = r#"{
+ "imports": {
+ "https://ex ample.org/": "/unparseable1/",
+ "https://example.com:demo": "/unparseable2",
+ "http://[www.example.com]/": "/unparseable3/",
+ "https:example.org": "/invalidButParseable1/",
+ "https://///example.com///": "/invalidButParseable2/",
+ "https://example.net": "/prettyNormal/",
+ "https://ex%41mple.com/": "/percentDecoding/",
+ "https://example.com/%41": "/noPercentDecoding"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert_eq!(
+ import_map.imports.get("https://ex ample.org/").unwrap()[0],
+ "https://base.example/unparseable1/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://example.com:demo").unwrap()[0],
+ "https://base.example/unparseable2".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("http://[www.example.com]/").unwrap()[0],
+ "https://base.example/unparseable3/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://example.org/").unwrap()[0],
+ "https://base.example/invalidButParseable1/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://example.com///").unwrap()[0],
+ "https://base.example/invalidButParseable2/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://example.net/").unwrap()[0],
+ "https://base.example/prettyNormal/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://example.com/").unwrap()[0],
+ "https://base.example/percentDecoding/".to_string()
+ );
+ assert_eq!(
+ import_map.imports.get("https://example.com/%41").unwrap()[0],
+ "https://base.example/noPercentDecoding".to_string()
+ );
+ }
+
+ #[test]
+ fn parse_scope_keys_relative() {
+ // Should work with no prefix..
+ let json_map = r#"{
+ "scopes": {
+ "foo": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/foo")
+ );
+
+ // Should work with ./, ../, and / prefixes..
+ let json_map = r#"{
+ "scopes": {
+ "./foo": {},
+ "../foo": {},
+ "/foo": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/foo")
+ );
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/foo")
+ );
+ assert!(import_map.scopes.contains_key("https://base.example/foo"));
+
+ // Should work with /s, ?s, and #s..
+ let json_map = r#"{
+ "scopes": {
+ "foo/bar?baz#qux": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/foo/bar?baz#qux")
+ );
+
+ // Should work with an empty string scope key..
+ let json_map = r#"{
+ "scopes": {
+ "": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/path3")
+ );
+
+ // Should work with / suffixes..
+ let json_map = r#"{
+ "scopes": {
+ "foo/": {},
+ "./foo/": {},
+ "../foo/": {},
+ "/foo/": {},
+ "/foo//": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/foo/")
+ );
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/foo/")
+ );
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/foo/")
+ );
+ assert!(import_map.scopes.contains_key("https://base.example/foo/"));
+ assert!(import_map.scopes.contains_key("https://base.example/foo//"));
+
+ // Should deduplicate based on URL parsing rules..
+ let json_map = r#"{
+ "scopes": {
+ "foo/\\": {},
+ "foo//": {},
+ "foo\\\\": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/foo//")
+ );
+ assert_eq!(import_map.scopes.len(), 1);
+ }
+
+ #[test]
+ fn parse_scope_keys_absolute() {
+ // Should only accept absolute URL scope keys with fetch schemes..
+ let json_map = r#"{
+ "scopes": {
+ "http://good/": {},
+ "https://good/": {},
+ "file:///good": {},
+ "about:bad": {},
+ "blob:bad": {},
+ "data:bad": {},
+ "filesystem:bad": {},
+ "ftp://bad/": {},
+ "import:bad": {},
+ "mailto:bad": {},
+ "javascript:bad": {},
+ "wss:bad": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ assert!(import_map.scopes.contains_key("http://good/"));
+ assert!(import_map.scopes.contains_key("https://good/"));
+ assert!(import_map.scopes.contains_key("file:///good"));
+ assert_eq!(import_map.scopes.len(), 3);
+
+ // Should parse absolute URL scope keys, ignoring unparseable ones..
+ let json_map = r#"{
+ "scopes": {
+ "https://ex ample.org/": {},
+ "https://example.com:demo": {},
+ "http://[www.example.com]/": {},
+ "https:example.org": {},
+ "https://///example.com///": {},
+ "https://example.net": {},
+ "https://ex%41mple.com/foo/": {},
+ "https://example.com/%41": {}
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+ // tricky case! remember we have a base URL
+ assert!(
+ import_map
+ .scopes
+ .contains_key("https://base.example/path1/path2/example.org")
+ );
+ assert!(import_map.scopes.contains_key("https://example.com///"));
+ assert!(import_map.scopes.contains_key("https://example.net/"));
+ assert!(import_map.scopes.contains_key("https://example.com/foo/"));
+ assert!(import_map.scopes.contains_key("https://example.com/%41"));
+ assert_eq!(import_map.scopes.len(), 5);
+ }
+
+ #[test]
+ fn parse_addresses_relative_url_like() {
+ // Should accept strings prefixed with ./, ../, or /..
+ let json_map = r#"{
+ "imports": {
+ "dotSlash": "./foo",
+ "dotDotSlash": "../foo",
+ "slash": "/foo"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("dotSlash").unwrap(),
+ &vec!["https://base.example/path1/path2/foo".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("dotDotSlash").unwrap(),
+ &vec!["https://base.example/path1/foo".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("slash").unwrap(),
+ &vec!["https://base.example/foo".to_string()]
+ );
+
+ // Should accept the literal strings ./, ../, or / with no suffix..
+ let json_map = r#"{
+ "imports": {
+ "dotSlash": "./",
+ "dotDotSlash": "../",
+ "slash": "/"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("dotSlash").unwrap(),
+ &vec!["https://base.example/path1/path2/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("dotDotSlash").unwrap(),
+ &vec!["https://base.example/path1/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("slash").unwrap(),
+ &vec!["https://base.example/".to_string()]
+ );
+
+ // Should ignore percent-encoded variants of ./, ../, or /..
+ let json_map = r#"{
+ "imports": {
+ "dotSlash1": "%2E/",
+ "dotDotSlash1": "%2E%2E/",
+ "dotSlash2": ".%2F",
+ "dotDotSlash2": "..%2F",
+ "slash2": "%2F",
+ "dotSlash3": "%2E%2F",
+ "dotDotSlash3": "%2E%2E%2F"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert!(import_map.imports.get("dotSlash1").unwrap().is_empty());
+ assert!(import_map.imports.get("dotDotSlash1").unwrap().is_empty());
+ assert!(import_map.imports.get("dotSlash2").unwrap().is_empty());
+ assert!(import_map.imports.get("dotDotSlash2").unwrap().is_empty());
+ assert!(import_map.imports.get("slash2").unwrap().is_empty());
+ assert!(import_map.imports.get("dotSlash3").unwrap().is_empty());
+ assert!(import_map.imports.get("dotDotSlash3").unwrap().is_empty());
+ }
+
+ #[test]
+ fn parse_addresses_absolute_with_fetch_schemes() {
+ // Should only accept absolute URL addresses with fetch schemes..
+ let json_map = r#"{
+ "imports": {
+ "http": "http://good/",
+ "https": "https://good/",
+ "file": "file:///good",
+ "about": "about:bad",
+ "blob": "blob:bad",
+ "data": "data:bad",
+ "filesystem": "filesystem:bad",
+ "ftp": "ftp://good/",
+ "import": "import:bad",
+ "mailto": "mailto:bad",
+ "javascript": "javascript:bad",
+ "wss": "wss:bad"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("file").unwrap(),
+ &vec!["file:///good".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("http").unwrap(),
+ &vec!["http://good/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("https").unwrap(),
+ &vec!["https://good/".to_string()]
+ );
+
+ assert!(import_map.imports.get("about").unwrap().is_empty());
+ assert!(import_map.imports.get("blob").unwrap().is_empty());
+ assert!(import_map.imports.get("data").unwrap().is_empty());
+ assert!(import_map.imports.get("filesystem").unwrap().is_empty());
+ assert!(import_map.imports.get("ftp").unwrap().is_empty());
+ assert!(import_map.imports.get("import").unwrap().is_empty());
+ assert!(import_map.imports.get("mailto").unwrap().is_empty());
+ assert!(import_map.imports.get("javascript").unwrap().is_empty());
+ assert!(import_map.imports.get("wss").unwrap().is_empty());
+ }
+
+ #[test]
+ fn parse_addresses_absolute_with_fetch_schemes_arrays() {
+ // Should only accept absolute URL addresses with fetch schemes inside arrays..
+ let json_map = r#"{
+ "imports": {
+ "http": ["http://good/"],
+ "https": ["https://good/"],
+ "file": ["file:///good"],
+ "about": ["about:bad"],
+ "blob": ["blob:bad"],
+ "data": ["data:bad"],
+ "filesystem": ["filesystem:bad"],
+ "ftp": ["ftp://good/"],
+ "import": ["import:bad"],
+ "mailto": ["mailto:bad"],
+ "javascript": ["javascript:bad"],
+ "wss": ["wss:bad"]
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("file").unwrap(),
+ &vec!["file:///good".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("http").unwrap(),
+ &vec!["http://good/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("https").unwrap(),
+ &vec!["https://good/".to_string()]
+ );
+
+ assert!(import_map.imports.get("about").unwrap().is_empty());
+ assert!(import_map.imports.get("blob").unwrap().is_empty());
+ assert!(import_map.imports.get("data").unwrap().is_empty());
+ assert!(import_map.imports.get("filesystem").unwrap().is_empty());
+ assert!(import_map.imports.get("ftp").unwrap().is_empty());
+ assert!(import_map.imports.get("import").unwrap().is_empty());
+ assert!(import_map.imports.get("mailto").unwrap().is_empty());
+ assert!(import_map.imports.get("javascript").unwrap().is_empty());
+ assert!(import_map.imports.get("wss").unwrap().is_empty());
+ }
+
+ #[test]
+ fn parse_addresses_unparseable() {
+ // Should parse absolute URLs, ignoring unparseable ones..
+ let json_map = r#"{
+ "imports": {
+ "unparseable1": "https://ex ample.org/",
+ "unparseable2": "https://example.com:demo",
+ "unparseable3": "http://[www.example.com]/",
+ "invalidButParseable1": "https:example.org",
+ "invalidButParseable2": "https://///example.com///",
+ "prettyNormal": "https://example.net",
+ "percentDecoding": "https://ex%41mple.com/",
+ "noPercentDecoding": "https://example.com/%41"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("invalidButParseable1").unwrap(),
+ &vec!["https://example.org/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("invalidButParseable2").unwrap(),
+ &vec!["https://example.com///".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("prettyNormal").unwrap(),
+ &vec!["https://example.net/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("percentDecoding").unwrap(),
+ &vec!["https://example.com/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("noPercentDecoding").unwrap(),
+ &vec!["https://example.com/%41".to_string()]
+ );
+
+ assert!(import_map.imports.get("unparseable1").unwrap().is_empty());
+ assert!(import_map.imports.get("unparseable2").unwrap().is_empty());
+ assert!(import_map.imports.get("unparseable3").unwrap().is_empty());
+ }
+
+ #[test]
+ fn parse_addresses_unparseable_arrays() {
+ // Should parse absolute URLs, ignoring unparseable ones inside arrays..
+ let json_map = r#"{
+ "imports": {
+ "unparseable1": ["https://ex ample.org/"],
+ "unparseable2": ["https://example.com:demo"],
+ "unparseable3": ["http://[www.example.com]/"],
+ "invalidButParseable1": ["https:example.org"],
+ "invalidButParseable2": ["https://///example.com///"],
+ "prettyNormal": ["https://example.net"],
+ "percentDecoding": ["https://ex%41mple.com/"],
+ "noPercentDecoding": ["https://example.com/%41"]
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("invalidButParseable1").unwrap(),
+ &vec!["https://example.org/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("invalidButParseable2").unwrap(),
+ &vec!["https://example.com///".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("prettyNormal").unwrap(),
+ &vec!["https://example.net/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("percentDecoding").unwrap(),
+ &vec!["https://example.com/".to_string()]
+ );
+ assert_eq!(
+ import_map.imports.get("noPercentDecoding").unwrap(),
+ &vec!["https://example.com/%41".to_string()]
+ );
+
+ assert!(import_map.imports.get("unparseable1").unwrap().is_empty());
+ assert!(import_map.imports.get("unparseable2").unwrap().is_empty());
+ assert!(import_map.imports.get("unparseable3").unwrap().is_empty());
+ }
+
+ #[test]
+ fn parse_addresses_mismatched_trailing_slashes() {
+ // Should parse absolute URLs, ignoring unparseable ones inside arrays..
+ let json_map = r#"{
+ "imports": {
+ "trailer/": "/notrailer"
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert!(import_map.imports.get("trailer/").unwrap().is_empty());
+ // TODO: I'd be good to assert that warning was shown
+ }
+
+ #[test]
+ fn parse_addresses_mismatched_trailing_slashes_array() {
+ // Should warn for a mismatch alone in an array..
+ let json_map = r#"{
+ "imports": {
+ "trailer/": ["/notrailer"]
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert!(import_map.imports.get("trailer/").unwrap().is_empty());
+ // TODO: I'd be good to assert that warning was shown
+ }
+
+ #[test]
+ fn parse_addresses_mismatched_trailing_slashes_with_nonmismatched_array() {
+ // Should warn for a mismatch alone in an array..
+ let json_map = r#"{
+ "imports": {
+ "trailer/": ["/atrailer/", "/notrailer"]
+ }
+ }"#;
+ let import_map =
+ ImportMap::from_json("https://base.example/path1/path2/path3", json_map)
+ .unwrap();
+
+ assert_eq!(
+ import_map.imports.get("trailer/").unwrap(),
+ &vec!["https://base.example/atrailer/".to_string()]
+ );
+ // TODO: I'd be good to assert that warning was shown
+ }
+
+ #[test]
+ fn parse_addresses_other_invalid() {
+ // Should ignore unprefixed strings that are not absolute URLs.
+ for bad in &["bar", "\\bar", "~bar", "#bar", "?bar"] {
+ let json_map = json!({
+ "imports": {
+ "foo": bad
+ }
+ });
+ let import_map = ImportMap::from_json(
+ "https://base.example/path1/path2/path3",
+ &json_map.to_string(),
+ ).unwrap();
+
+ assert!(import_map.imports.get("foo").unwrap().is_empty());
+ }
+ }
+
+ fn get_empty_import_map() -> ImportMap {
+ ImportMap {
+ base_url: "https://example.com/app/main.ts".to_string(),
+ imports: IndexMap::new(),
+ scopes: IndexMap::new(),
+ }
+ }
+
+ #[test]
+ fn resolve_unmapped_relative_specifiers() {
+ let referrer_url = "https://example.com/js/script.ts";
+ let import_map = get_empty_import_map();
+
+ // Should resolve ./ specifiers as URLs.
+ assert_eq!(
+ import_map.resolve("./foo", referrer_url).unwrap(),
+ Some("https://example.com/js/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("./foo/bar", referrer_url).unwrap(),
+ Some("https://example.com/js/foo/bar".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("./foo/../bar", referrer_url).unwrap(),
+ Some("https://example.com/js/bar".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("./foo/../../bar", referrer_url).unwrap(),
+ Some("https://example.com/bar".to_string())
+ );
+
+ // Should resolve ../ specifiers as URLs.
+ assert_eq!(
+ import_map.resolve("../foo", referrer_url).unwrap(),
+ Some("https://example.com/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("../foo/bar", referrer_url).unwrap(),
+ Some("https://example.com/foo/bar".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("../../../foo/bar", referrer_url)
+ .unwrap(),
+ Some("https://example.com/foo/bar".to_string())
+ );
+ }
+
+ #[test]
+ fn resolve_unmapped_absolute_specifiers() {
+ let referrer_url = "https://example.com/js/script.ts";
+ let import_map = get_empty_import_map();
+
+ // Should resolve / specifiers as URLs.
+ assert_eq!(
+ import_map.resolve("/foo", referrer_url).unwrap(),
+ Some("https://example.com/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("/foo/bar", referrer_url).unwrap(),
+ Some("https://example.com/foo/bar".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("../../foo/bar", referrer_url).unwrap(),
+ Some("https://example.com/foo/bar".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("/../foo/../bar", referrer_url).unwrap(),
+ Some("https://example.com/bar".to_string())
+ );
+
+ // Should parse absolute fetch-scheme URLs.
+ assert_eq!(
+ import_map
+ .resolve("https://example.net", referrer_url)
+ .unwrap(),
+ Some("https://example.net/".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://ex%41mple.com/", referrer_url)
+ .unwrap(),
+ Some("https://example.com/".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https:example.org", referrer_url)
+ .unwrap(),
+ Some("https://example.org/".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://///example.com///", referrer_url)
+ .unwrap(),
+ Some("https://example.com///".to_string())
+ );
+ }
+
+ #[test]
+ fn resolve_unmapped_bad_specifiers() {
+ let referrer_url = "https://example.com/js/script.ts";
+ let import_map = get_empty_import_map();
+
+ // Should fail for absolute non-fetch-scheme URLs.
+ assert!(import_map.resolve("about:good", referrer_url).is_err());
+ assert!(import_map.resolve("mailto:bad", referrer_url).is_err());
+ assert!(import_map.resolve("import:bad", referrer_url).is_err());
+ assert!(import_map.resolve("javascript:bad", referrer_url).is_err());
+ assert!(import_map.resolve("wss:bad", referrer_url).is_err());
+
+ // Should fail for string not parseable as absolute URLs and not starting with ./, ../ or /.
+ assert!(import_map.resolve("foo", referrer_url).is_err());
+ assert!(import_map.resolve("\\foo", referrer_url).is_err());
+ assert!(import_map.resolve(":foo", referrer_url).is_err());
+ assert!(import_map.resolve("@foo", referrer_url).is_err());
+ assert!(import_map.resolve("%2E/foo", referrer_url).is_err());
+ assert!(import_map.resolve("%2E%2Efoo", referrer_url).is_err());
+ assert!(import_map.resolve(".%2Efoo", referrer_url).is_err());
+ assert!(
+ import_map
+ .resolve("https://ex ample.org", referrer_url)
+ .is_err()
+ );
+ assert!(
+ import_map
+ .resolve("https://example.org:deno", referrer_url)
+ .is_err()
+ );
+ assert!(
+ import_map
+ .resolve("https://[example.org]", referrer_url)
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn resolve_imports_mapped() {
+ let base_url = "https://example.com/app/main.ts";
+ let referrer_url = "https://example.com/js/script.ts";
+
+ // Should fail when mapping is to an empty array.
+ let json_map = r#"{
+ "imports": {
+ "moment": null,
+ "lodash": []
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ assert!(import_map.resolve("moment", referrer_url).is_err());
+ assert!(import_map.resolve("lodash", referrer_url).is_err());
+ }
+
+ #[test]
+ fn resolve_imports_package_like_modules() {
+ let base_url = "https://example.com/app/main.ts";
+ let referrer_url = "https://example.com/js/script.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "moment": "/deps/moment/src/moment.js",
+ "moment/": "/deps/moment/src/",
+ "lodash-dot": "./deps/lodash-es/lodash.js",
+ "lodash-dot/": "./deps/lodash-es/",
+ "lodash-dotdot": "../deps/lodash-es/lodash.js",
+ "lodash-dotdot/": "../deps/lodash-es/",
+ "nowhere/": []
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ // Should work for package main modules.
+ assert_eq!(
+ import_map.resolve("moment", referrer_url).unwrap(),
+ Some("https://example.com/deps/moment/src/moment.js".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dot", referrer_url).unwrap(),
+ Some("https://example.com/app/deps/lodash-es/lodash.js".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dotdot", referrer_url).unwrap(),
+ Some("https://example.com/deps/lodash-es/lodash.js".to_string())
+ );
+
+ // Should work for package submodules.
+ assert_eq!(
+ import_map.resolve("moment/foo", referrer_url).unwrap(),
+ Some("https://example.com/deps/moment/src/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dot/foo", referrer_url).unwrap(),
+ Some("https://example.com/app/deps/lodash-es/foo".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("lodash-dotdot/foo", referrer_url)
+ .unwrap(),
+ Some("https://example.com/deps/lodash-es/foo".to_string())
+ );
+
+ // Should work for package names that end in a slash.
+ assert_eq!(
+ import_map.resolve("moment/", referrer_url).unwrap(),
+ Some("https://example.com/deps/moment/src/".to_string())
+ );
+
+ // Should fail for package modules that are not declared.
+ assert!(import_map.resolve("underscore/", referrer_url).is_err());
+ assert!(import_map.resolve("underscore/foo", referrer_url).is_err());
+
+ // Should fail for package submodules that map to nowhere.
+ assert!(import_map.resolve("nowhere/foo", referrer_url).is_err());
+ }
+
+ #[test]
+ fn resolve_imports_tricky_specifiers() {
+ let base_url = "https://example.com/app/main.ts";
+ let referrer_url = "https://example.com/js/script.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "package/withslash": "/deps/package-with-slash/index.mjs",
+ "not-a-package": "/lib/not-a-package.mjs",
+ ".": "/lib/dot.mjs",
+ "..": "/lib/dotdot.mjs",
+ "..\\\\": "/lib/dotdotbackslash.mjs",
+ "%2E": "/lib/percent2e.mjs",
+ "%2F": "/lib/percent2f.mjs"
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ // Should work for explicitly-mapped specifiers that happen to have a slash.
+ assert_eq!(
+ import_map
+ .resolve("package/withslash", referrer_url)
+ .unwrap(),
+ Some("https://example.com/deps/package-with-slash/index.mjs".to_string())
+ );
+
+ // Should work when the specifier has punctuation.
+ assert_eq!(
+ import_map.resolve(".", referrer_url).unwrap(),
+ Some("https://example.com/lib/dot.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("..", referrer_url).unwrap(),
+ Some("https://example.com/lib/dotdot.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("..\\\\", referrer_url).unwrap(),
+ Some("https://example.com/lib/dotdotbackslash.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("%2E", referrer_url).unwrap(),
+ Some("https://example.com/lib/percent2e.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("%2F", referrer_url).unwrap(),
+ Some("https://example.com/lib/percent2f.mjs".to_string())
+ );
+
+ // Should fail for attempting to get a submodule of something not declared with a trailing slash.
+ assert!(
+ import_map
+ .resolve("not-a-package/foo", referrer_url)
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn resolve_imports_url_like_specifier() {
+ let base_url = "https://example.com/app/main.ts";
+ let referrer_url = "https://example.com/js/script.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "/node_modules/als-polyfill/index.mjs": "std:kv-storage",
+ "/lib/foo.mjs": "./more/bar.mjs",
+ "./dotrelative/foo.mjs": "/lib/dot.mjs",
+ "../dotdotrelative/foo.mjs": "/lib/dotdot.mjs",
+ "/lib/no.mjs": null,
+ "./dotrelative/no.mjs": [],
+ "/": "/lib/slash-only/",
+ "./": "/lib/dotslash-only/",
+ "/test/": "/lib/url-trailing-slash/",
+ "./test/": "/lib/url-trailing-slash-dot/",
+ "/test": "/lib/test1.mjs",
+ "../test": "/lib/test2.mjs"
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ // Should remap to other URLs.
+ assert_eq!(
+ import_map
+ .resolve("https://example.com/lib/foo.mjs", referrer_url)
+ .unwrap(),
+ Some("https://example.com/app/more/bar.mjs".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://///example.com/lib/foo.mjs", referrer_url)
+ .unwrap(),
+ Some("https://example.com/app/more/bar.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("/lib/foo.mjs", referrer_url).unwrap(),
+ Some("https://example.com/app/more/bar.mjs".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://example.com/app/dotrelative/foo.mjs", referrer_url)
+ .unwrap(),
+ Some("https://example.com/lib/dot.mjs".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("../app/dotrelative/foo.mjs", referrer_url)
+ .unwrap(),
+ Some("https://example.com/lib/dot.mjs".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://example.com/dotdotrelative/foo.mjs", referrer_url)
+ .unwrap(),
+ Some("https://example.com/lib/dotdot.mjs".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("../dotdotrelative/foo.mjs", referrer_url)
+ .unwrap(),
+ Some("https://example.com/lib/dotdot.mjs".to_string())
+ );
+
+ // Should fail for URLs that remap to empty arrays.
+ assert!(
+ import_map
+ .resolve("https://example.com/lib/no.mjs", referrer_url)
+ .is_err()
+ );
+ assert!(import_map.resolve("/lib/no.mjs", referrer_url).is_err());
+ assert!(import_map.resolve("../lib/no.mjs", referrer_url).is_err());
+ assert!(
+ import_map
+ .resolve("https://example.com/app/dotrelative/no.mjs", referrer_url)
+ .is_err()
+ );
+ assert!(
+ import_map
+ .resolve("/app/dotrelative/no.mjs", referrer_url)
+ .is_err()
+ );
+ assert!(
+ import_map
+ .resolve("../app/dotrelative/no.mjs", referrer_url)
+ .is_err()
+ );
+
+ // Should remap URLs that are just composed from / and ..
+ assert_eq!(
+ import_map
+ .resolve("https://example.com/", referrer_url)
+ .unwrap(),
+ Some("https://example.com/lib/slash-only/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("/", referrer_url).unwrap(),
+ Some("https://example.com/lib/slash-only/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("../", referrer_url).unwrap(),
+ Some("https://example.com/lib/slash-only/".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://example.com/app/", referrer_url)
+ .unwrap(),
+ Some("https://example.com/lib/dotslash-only/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("/app/", referrer_url).unwrap(),
+ Some("https://example.com/lib/dotslash-only/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("../app/", referrer_url).unwrap(),
+ Some("https://example.com/lib/dotslash-only/".to_string())
+ );
+
+ // Should remap URLs that are prefix-matched by keys with trailing slashes.
+ assert_eq!(
+ import_map.resolve("/test/foo.mjs", referrer_url).unwrap(),
+ Some("https://example.com/lib/url-trailing-slash/foo.mjs".to_string())
+ );
+ assert_eq!(
+ import_map
+ .resolve("https://example.com/app/test/foo.mjs", referrer_url)
+ .unwrap(),
+ Some(
+ "https://example.com/lib/url-trailing-slash-dot/foo.mjs".to_string()
+ )
+ );
+
+ // Should use the last entry's address when URL-like specifiers parse to the same absolute URL.
+ //
+ // NOTE: this works properly because of "preserve_order" feature flag to "serde_json" crate
+ assert_eq!(
+ import_map.resolve("/test", referrer_url).unwrap(),
+ Some("https://example.com/lib/test2.mjs".to_string())
+ );
+ }
+
+ #[test]
+ fn resolve_imports_overlapping_entities_with_trailing_slashes() {
+ let base_url = "https://example.com/app/main.ts";
+ let referrer_url = "https://example.com/js/script.ts";
+
+ // Should favor the most-specific key (no empty arrays).
+ {
+ let json_map = r#"{
+ "imports": {
+ "a": "/1",
+ "a/": "/2/",
+ "a/b": "/3",
+ "a/b/": "/4/"
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ assert_eq!(
+ import_map.resolve("a", referrer_url).unwrap(),
+ Some("https://example.com/1".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a/", referrer_url).unwrap(),
+ Some("https://example.com/2/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a/b", referrer_url).unwrap(),
+ Some("https://example.com/3".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a/b/", referrer_url).unwrap(),
+ Some("https://example.com/4/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a/b/c", referrer_url).unwrap(),
+ Some("https://example.com/4/c".to_string())
+ );
+ }
+
+ // Should favor the most-specific key when empty arrays are involved for less-specific keys.
+ {
+ let json_map = r#"{
+ "imports": {
+ "a": [],
+ "a/": [],
+ "a/b": "/3",
+ "a/b/": "/4/"
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ assert!(import_map.resolve("a", referrer_url).is_err());
+ assert!(import_map.resolve("a/", referrer_url).is_err());
+ assert!(import_map.resolve("a/x", referrer_url).is_err());
+ assert_eq!(
+ import_map.resolve("a/b", referrer_url).unwrap(),
+ Some("https://example.com/3".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a/b/", referrer_url).unwrap(),
+ Some("https://example.com/4/".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a/b/c", referrer_url).unwrap(),
+ Some("https://example.com/4/c".to_string())
+ );
+ assert!(import_map.resolve("a/x/c", referrer_url).is_err());
+ }
+ }
+
+ #[test]
+ fn resolve_scopes_map_to_empty_array() {
+ let base_url = "https://example.com/app/main.ts";
+ let referrer_url = "https://example.com/js";
+
+ let json_map = r#"{
+ "scopes": {
+ "/js/": {
+ "moment": "null",
+ "lodash": []
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ assert!(import_map.resolve("moment", referrer_url).is_err());
+ assert!(import_map.resolve("lodash", referrer_url).is_err());
+ }
+
+ #[test]
+ fn resolve_scopes_exact_vs_prefix_matching() {
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "scopes": {
+ "/js": {
+ "moment": "/only-triggered-by-exact/moment",
+ "moment/": "/only-triggered-by-exact/moment/"
+ },
+ "/js/": {
+ "moment": "/triggered-by-any-subpath/moment",
+ "moment/": "/triggered-by-any-subpath/moment/"
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ let js_non_dir = "https://example.com/js";
+ let js_in_dir = "https://example.com/js/app.mjs";
+ let with_js_prefix = "https://example.com/jsiscool";
+
+ assert_eq!(
+ import_map.resolve("moment", js_non_dir).unwrap(),
+ Some("https://example.com/only-triggered-by-exact/moment".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("moment/foo", js_non_dir).unwrap(),
+ Some(
+ "https://example.com/only-triggered-by-exact/moment/foo".to_string()
+ )
+ );
+ assert_eq!(
+ import_map.resolve("moment", js_in_dir).unwrap(),
+ Some("https://example.com/triggered-by-any-subpath/moment".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("moment/foo", js_in_dir).unwrap(),
+ Some(
+ "https://example.com/triggered-by-any-subpath/moment/foo".to_string()
+ )
+ );
+ assert!(import_map.resolve("moment", with_js_prefix).is_err());
+ assert!(import_map.resolve("moment/foo", with_js_prefix).is_err());
+ }
+
+ #[test]
+ fn resolve_scopes_only_exact_in_map() {
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "scopes": {
+ "/js": {
+ "moment": "/only-triggered-by-exact/moment",
+ "moment/": "/only-triggered-by-exact/moment/"
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ // Should match correctly when only an exact match is in the map.
+ let js_non_dir = "https://example.com/js";
+ let js_in_dir = "https://example.com/js/app.mjs";
+ let with_js_prefix = "https://example.com/jsiscool";
+
+ assert_eq!(
+ import_map.resolve("moment", js_non_dir).unwrap(),
+ Some("https://example.com/only-triggered-by-exact/moment".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("moment/foo", js_non_dir).unwrap(),
+ Some(
+ "https://example.com/only-triggered-by-exact/moment/foo".to_string()
+ )
+ );
+ assert!(import_map.resolve("moment", js_in_dir).is_err());
+ assert!(import_map.resolve("moment/foo", js_in_dir).is_err());
+ assert!(import_map.resolve("moment", with_js_prefix).is_err());
+ assert!(import_map.resolve("moment/foo", with_js_prefix).is_err());
+ }
+
+ #[test]
+ fn resolve_scopes_only_prefix_in_map() {
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "scopes": {
+ "/js/": {
+ "moment": "/triggered-by-any-subpath/moment",
+ "moment/": "/triggered-by-any-subpath/moment/"
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ // Should match correctly when only a prefix match is in the map.
+ let js_non_dir = "https://example.com/js";
+ let js_in_dir = "https://example.com/js/app.mjs";
+ let with_js_prefix = "https://example.com/jsiscool";
+
+ assert!(import_map.resolve("moment", js_non_dir).is_err());
+ assert!(import_map.resolve("moment/foo", js_non_dir).is_err());
+ assert_eq!(
+ import_map.resolve("moment", js_in_dir).unwrap(),
+ Some("https://example.com/triggered-by-any-subpath/moment".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("moment/foo", js_in_dir).unwrap(),
+ Some(
+ "https://example.com/triggered-by-any-subpath/moment/foo".to_string()
+ )
+ );
+ assert!(import_map.resolve("moment", with_js_prefix).is_err());
+ assert!(import_map.resolve("moment/foo", with_js_prefix).is_err());
+ }
+
+ #[test]
+ fn resolve_scopes_package_like() {
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "moment": "/node_modules/moment/src/moment.js",
+ "moment/": "/node_modules/moment/src/",
+ "lodash-dot": "./node_modules/lodash-es/lodash.js",
+ "lodash-dot/": "./node_modules/lodash-es/",
+ "lodash-dotdot": "../node_modules/lodash-es/lodash.js",
+ "lodash-dotdot/": "../node_modules/lodash-es/"
+ },
+ "scopes": {
+ "/": {
+ "moment": "/node_modules_3/moment/src/moment.js",
+ "vue": "/node_modules_3/vue/dist/vue.runtime.esm.js"
+ },
+ "/js/": {
+ "lodash-dot": "./node_modules_2/lodash-es/lodash.js",
+ "lodash-dot/": "./node_modules_2/lodash-es/",
+ "lodash-dotdot": "../node_modules_2/lodash-es/lodash.js",
+ "lodash-dotdot/": "../node_modules_2/lodash-es/"
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ // Should match correctly when only a prefix match is in the map.
+ let js_in_dir = "https://example.com/js/app.mjs";
+ let top_level = "https://example.com/app.mjs";
+
+ // Should resolve scoped.
+ assert_eq!(
+ import_map.resolve("lodash-dot", js_in_dir).unwrap(),
+ Some(
+ "https://example.com/app/node_modules_2/lodash-es/lodash.js"
+ .to_string()
+ )
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dotdot", js_in_dir).unwrap(),
+ Some(
+ "https://example.com/node_modules_2/lodash-es/lodash.js".to_string()
+ )
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dot/foo", js_in_dir).unwrap(),
+ Some("https://example.com/app/node_modules_2/lodash-es/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dotdot/foo", js_in_dir).unwrap(),
+ Some("https://example.com/node_modules_2/lodash-es/foo".to_string())
+ );
+
+ // Should apply best scope match.
+ assert_eq!(
+ import_map.resolve("moment", top_level).unwrap(),
+ Some(
+ "https://example.com/node_modules_3/moment/src/moment.js".to_string()
+ )
+ );
+ assert_eq!(
+ import_map.resolve("moment", js_in_dir).unwrap(),
+ Some(
+ "https://example.com/node_modules_3/moment/src/moment.js".to_string()
+ )
+ );
+ assert_eq!(
+ import_map.resolve("vue", js_in_dir).unwrap(),
+ Some(
+ "https://example.com/node_modules_3/vue/dist/vue.runtime.esm.js"
+ .to_string()
+ )
+ );
+
+ // Should fallback to "imports".
+ assert_eq!(
+ import_map.resolve("moment/foo", top_level).unwrap(),
+ Some("https://example.com/node_modules/moment/src/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("moment/foo", js_in_dir).unwrap(),
+ Some("https://example.com/node_modules/moment/src/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dot", top_level).unwrap(),
+ Some(
+ "https://example.com/app/node_modules/lodash-es/lodash.js".to_string()
+ )
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dotdot", top_level).unwrap(),
+ Some("https://example.com/node_modules/lodash-es/lodash.js".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dot/foo", top_level).unwrap(),
+ Some("https://example.com/app/node_modules/lodash-es/foo".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("lodash-dotdot/foo", top_level).unwrap(),
+ Some("https://example.com/node_modules/lodash-es/foo".to_string())
+ );
+
+ // Should still fail for package-like specifiers that are not declared.
+ assert!(import_map.resolve("underscore/", js_in_dir).is_err());
+ assert!(import_map.resolve("underscore/foo", js_in_dir).is_err());
+ }
+
+ #[test]
+ fn resolve_scopes_inheritance() {
+ // https://github.com/WICG/import-maps#scope-inheritance
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "a": "/a-1.mjs",
+ "b": "/b-1.mjs",
+ "c": "/c-1.mjs"
+ },
+ "scopes": {
+ "/scope2/": {
+ "a": "/a-2.mjs"
+ },
+ "/scope2/scope3/": {
+ "b": "/b-3.mjs"
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ let scope_1_url = "https://example.com/scope1/foo.mjs";
+ let scope_2_url = "https://example.com/scope2/foo.mjs";
+ let scope_3_url = "https://example.com/scope2/scope3/foo.mjs";
+
+ // Should fall back to "imports" when none match.
+ assert_eq!(
+ import_map.resolve("a", scope_1_url).unwrap(),
+ Some("https://example.com/a-1.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("b", scope_1_url).unwrap(),
+ Some("https://example.com/b-1.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("c", scope_1_url).unwrap(),
+ Some("https://example.com/c-1.mjs".to_string())
+ );
+
+ // Should use a direct scope override.
+ assert_eq!(
+ import_map.resolve("a", scope_2_url).unwrap(),
+ Some("https://example.com/a-2.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("b", scope_2_url).unwrap(),
+ Some("https://example.com/b-1.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("c", scope_2_url).unwrap(),
+ Some("https://example.com/c-1.mjs".to_string())
+ );
+
+ // Should use an indirect scope override.
+ assert_eq!(
+ import_map.resolve("a", scope_3_url).unwrap(),
+ Some("https://example.com/a-2.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("b", scope_3_url).unwrap(),
+ Some("https://example.com/b-3.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("c", scope_3_url).unwrap(),
+ Some("https://example.com/c-1.mjs".to_string())
+ );
+ }
+
+ #[test]
+ fn resolve_scopes_relative_url_keys() {
+ // https://github.com/WICG/import-maps#scope-inheritance
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "a": "/a-1.mjs",
+ "b": "/b-1.mjs",
+ "c": "/c-1.mjs"
+ },
+ "scopes": {
+ "": {
+ "a": "/a-empty-string.mjs"
+ },
+ "./": {
+ "b": "/b-dot-slash.mjs"
+ },
+ "../": {
+ "c": "/c-dot-dot-slash.mjs"
+ }
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+ let in_same_dir_as_map = "https://example.com/app/foo.mjs";
+ let in_dir_above_map = "https://example.com/foo.mjs";
+
+ // Should resolve an empty string scope using the import map URL.
+ assert_eq!(
+ import_map.resolve("a", base_url).unwrap(),
+ Some("https://example.com/a-empty-string.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("a", in_same_dir_as_map).unwrap(),
+ Some("https://example.com/a-1.mjs".to_string())
+ );
+
+ // Should resolve a ./ scope using the import map URL's directory.
+ assert_eq!(
+ import_map.resolve("b", base_url).unwrap(),
+ Some("https://example.com/b-dot-slash.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("b", in_same_dir_as_map).unwrap(),
+ Some("https://example.com/b-dot-slash.mjs".to_string())
+ );
+
+ // Should resolve a ../ scope using the import map URL's directory.
+ assert_eq!(
+ import_map.resolve("c", base_url).unwrap(),
+ Some("https://example.com/c-dot-dot-slash.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("c", in_same_dir_as_map).unwrap(),
+ Some("https://example.com/c-dot-dot-slash.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("c", in_dir_above_map).unwrap(),
+ Some("https://example.com/c-dot-dot-slash.mjs".to_string())
+ );
+ }
+
+ #[test]
+ fn cant_resolve_to_built_in() {
+ let base_url = "https://example.com/app/main.ts";
+
+ let import_map = ImportMap::from_json(base_url, "{}").unwrap();
+
+ assert!(import_map.resolve("std:blank", base_url).is_err());
+ }
+
+ #[test]
+ fn resolve_builtins_remap() {
+ let base_url = "https://example.com/app/main.ts";
+
+ let json_map = r#"{
+ "imports": {
+ "std:blank": "./blank.mjs",
+ "std:none": "./none.mjs"
+ }
+ }"#;
+ let import_map = ImportMap::from_json(base_url, json_map).unwrap();
+
+ assert_eq!(
+ import_map.resolve("std:blank", base_url).unwrap(),
+ Some("https://example.com/app/blank.mjs".to_string())
+ );
+ assert_eq!(
+ import_map.resolve("std:none", base_url).unwrap(),
+ Some("https://example.com/app/none.mjs".to_string())
+ );
+ }
+}
diff --git a/cli/main.rs b/cli/main.rs
index ad0374af2..132bd7bc0 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -9,6 +9,7 @@ extern crate futures;
extern crate serde_json;
extern crate clap;
extern crate deno;
+extern crate indexmap;
#[cfg(unix)]
extern crate nix;
extern crate rand;
@@ -24,6 +25,7 @@ mod fs;
mod global_timer;
mod http_body;
mod http_util;
+mod import_map;
pub mod js_errors;
pub mod msg;
pub mod msg_util;
diff --git a/cli/msg.fbs b/cli/msg.fbs
index 7f2db381f..56410097c 100644
--- a/cli/msg.fbs
+++ b/cli/msg.fbs
@@ -136,6 +136,7 @@ enum ErrorKind: byte {
OpNotAvaiable,
WorkerInitFailed,
UnixError,
+ ImportMapError,
}
table Cwd {}
diff --git a/cli/ops.rs b/cli/ops.rs
index c76983c47..f39daaab6 100644
--- a/cli/ops.rs
+++ b/cli/ops.rs
@@ -29,6 +29,7 @@ use crate::worker::Worker;
use deno::js_check;
use deno::Buf;
use deno::JSError;
+//use deno::Loader;
use deno::Op;
use deno::PinnedBuf;
use flatbuffers::FlatBufferBuilder;
@@ -499,10 +500,30 @@ fn op_fetch_module_meta_data(
let use_cache = !state.flags.reload;
let no_fetch = state.flags.no_fetch;
+ // TODO(bartlomieju): I feel this is wrong - specifier is only resolved if there's an
+ // import map - why it is not always resolved? Eg. "bad-module.ts" will return NotFound
+ // error whilst it should return RelativeUrlWithCannotBeABaseBase error
+ let resolved_specifier = match &state.import_map {
+ Some(import_map) => {
+ match import_map.resolve(specifier, referrer) {
+ Ok(result) => match result {
+ Some(url) => url.clone(),
+ None => specifier.to_string(),
+ },
+ Err(err) => panic!("error resolving using import map: {:?}", err), // TODO: this should be coerced to DenoError
+ }
+ }
+ None => specifier.to_string(),
+ };
+
let fut = state
.dir
- .fetch_module_meta_data_async(specifier, referrer, use_cache, no_fetch)
- .and_then(move |out| {
+ .fetch_module_meta_data_async(
+ &resolved_specifier,
+ referrer,
+ use_cache,
+ no_fetch,
+ ).and_then(move |out| {
let builder = &mut FlatBufferBuilder::new();
let data_off = builder.create_vector(out.source_code.as_slice());
let msg_args = msg::FetchModuleMetaDataResArgs {
diff --git a/cli/state.rs b/cli/state.rs
index 9a8b1cab2..4a2db65c2 100644
--- a/cli/state.rs
+++ b/cli/state.rs
@@ -6,6 +6,7 @@ use crate::errors::DenoError;
use crate::errors::DenoResult;
use crate::flags;
use crate::global_timer::GlobalTimer;
+use crate::import_map::ImportMap;
use crate::msg;
use crate::ops;
use crate::permissions::DenoPermissions;
@@ -57,6 +58,7 @@ pub struct ThreadSafeState(Arc<State>);
#[cfg_attr(feature = "cargo-clippy", allow(stutter))]
pub struct State {
+ pub main_module: Option<String>,
pub dir: deno_dir::DenoDir,
pub argv: Vec<String>,
pub permissions: DenoPermissions,
@@ -67,6 +69,9 @@ pub struct State {
/// When flags contains a `.config_path` option, the fully qualified path
/// name of the passed path will be resolved and set.
pub config_path: Option<String>,
+ /// When flags contains a `.import_map_path` option, the content of the
+ /// import map file will be resolved and set.
+ pub import_map: Option<ImportMap>,
pub metrics: Metrics,
pub worker_channels: Mutex<WorkerChannels>,
pub global_timer: Mutex<GlobalTimer>,
@@ -111,9 +116,10 @@ pub fn fetch_module_meta_data_and_maybe_compile_async(
let state_ = state.clone();
let specifier = specifier.to_string();
let referrer = referrer.to_string();
+ let is_root = referrer == ".";
let f =
- futures::future::result(ThreadSafeState::resolve(&specifier, &referrer));
+ futures::future::result(state.resolve(&specifier, &referrer, is_root));
f.and_then(move |module_id| {
let use_cache = !state_.flags.reload || state_.has_compiled(&module_id);
let no_fetch = state_.flags.no_fetch;
@@ -157,7 +163,28 @@ pub fn fetch_module_meta_data_and_maybe_compile(
impl Loader for ThreadSafeState {
type Error = DenoError;
- fn resolve(specifier: &str, referrer: &str) -> Result<String, Self::Error> {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ is_root: bool,
+ ) -> Result<String, Self::Error> {
+ if !is_root {
+ if let Some(import_map) = &self.import_map {
+ match import_map.resolve(specifier, referrer) {
+ Ok(result) => {
+ if result.is_some() {
+ return Ok(result.unwrap());
+ }
+ }
+ Err(err) => {
+ // TODO(bartlomieju): this should be coerced to DenoError
+ panic!("error resolving using import map: {:?}", err);
+ }
+ }
+ }
+ }
+
resolve_module_spec(specifier, referrer).map_err(DenoError::from)
}
@@ -233,14 +260,50 @@ impl ThreadSafeState {
_ => None,
};
+ let dir =
+ deno_dir::DenoDir::new(custom_root, &config, progress.clone()).unwrap();
+
+ let main_module: Option<String> = if argv_rest.len() <= 1 {
+ None
+ } else {
+ let specifier = argv_rest[1].clone();
+ let referrer = ".";
+ // TODO: does this really have to be resolved by DenoDir?
+ // Maybe we can call `resolve_module_spec`
+ match dir.resolve_module_url(&specifier, referrer) {
+ Ok(url) => Some(url.to_string()),
+ Err(e) => {
+ debug!("Potentially swallowed error {}", e);
+ None
+ }
+ }
+ };
+
+ let mut import_map = None;
+ if let Some(file_name) = &flags.import_map_path {
+ let base_url = match &main_module {
+ Some(url) => url,
+ None => unreachable!(),
+ };
+
+ match ImportMap::load(base_url, file_name) {
+ Ok(map) => import_map = Some(map),
+ Err(err) => {
+ println!("{:?}", err);
+ panic!("Error parsing import map");
+ }
+ }
+ }
+
ThreadSafeState(Arc::new(State {
- dir: deno_dir::DenoDir::new(custom_root, &config, progress.clone())
- .unwrap(),
+ main_module,
+ dir,
argv: argv_rest,
permissions: DenoPermissions::from_flags(&flags),
flags,
config,
config_path,
+ import_map,
metrics: Metrics::default(),
worker_channels: Mutex::new(internal_channels),
global_timer: Mutex::new(GlobalTimer::new()),
@@ -255,18 +318,9 @@ impl ThreadSafeState {
/// Read main module from argv
pub fn main_module(&self) -> Option<String> {
- if self.argv.len() <= 1 {
- None
- } else {
- let specifier = self.argv[1].clone();
- let referrer = ".";
- match self.dir.resolve_module_url(&specifier, referrer) {
- Ok(url) => Some(url.to_string()),
- Err(e) => {
- debug!("Potentially swallowed error {}", e);
- None
- }
- }
+ match &self.main_module {
+ Some(url) => Some(url.to_string()),
+ None => None,
}
}
diff --git a/core/modules.rs b/core/modules.rs
index 8a600fd7e..fbfdb0b07 100644
--- a/core/modules.rs
+++ b/core/modules.rs
@@ -43,7 +43,12 @@ pub trait Loader: Send + Sync {
/// When implementing an spec-complaint VM, this should be exactly the
/// algorithm described here:
/// https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier
- fn resolve(specifier: &str, referrer: &str) -> Result<String, Self::Error>;
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ is_root: bool,
+ ) -> Result<String, Self::Error>;
/// Given an absolute url, load its source code.
fn load(&self, url: &str) -> Box<SourceCodeInfoFuture<Self::Error>>;
@@ -98,17 +103,15 @@ impl<L: Loader> RecursiveLoad<L> {
referrer: &str,
parent_id: Option<deno_mod>,
) -> Result<String, L::Error> {
- let url = L::resolve(specifier, referrer)?;
+ let is_root = parent_id.is_none();
+ let url = self.loader.resolve(specifier, referrer, is_root)?;
- let is_root = if let Some(parent_id) = parent_id {
+ if !is_root {
{
let mut m = self.modules.lock().unwrap();
- m.add_child(parent_id, &url);
+ m.add_child(parent_id.unwrap(), &url);
}
- false
- } else {
- true
- };
+ }
{
// #B We only add modules that have not yet been resolved for RecursiveLoad.
@@ -251,7 +254,9 @@ impl<L: Loader> Future for RecursiveLoad<L> {
|specifier: &str, referrer_id: deno_mod| -> deno_mod {
let modules = self.modules.lock().unwrap();
let referrer = modules.get_name(referrer_id).unwrap();
- match L::resolve(specifier, &referrer) {
+ // TODO(bartlomieju): there must be a better way
+ let is_root = referrer == ".";
+ match self.loader.resolve(specifier, &referrer, is_root) {
Ok(url) => match modules.get_id(&url) {
Some(id) => id,
None => 0,
@@ -619,7 +624,12 @@ mod tests {
impl Loader for MockLoader {
type Error = MockError;
- fn resolve(specifier: &str, referrer: &str) -> Result<String, Self::Error> {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ _is_root: bool,
+ ) -> Result<String, Self::Error> {
eprintln!(">> RESOLVING, S: {}, R: {}", specifier, referrer);
let output_specifier =
if specifier.starts_with("./") && referrer.starts_with("./") {
diff --git a/tests/033_import_map.out b/tests/033_import_map.out
new file mode 100644
index 000000000..e9b9160e9
--- /dev/null
+++ b/tests/033_import_map.out
@@ -0,0 +1,7 @@
+Hello from remapped moment!
+Hello from remapped moment dir!
+Hello from remapped lodash!
+Hello from remapped lodash dir!
+Hello from remapped Vue!
+Hello from scoped moment!
+Hello from scoped!
diff --git a/tests/033_import_map.test b/tests/033_import_map.test
new file mode 100644
index 000000000..1633c1807
--- /dev/null
+++ b/tests/033_import_map.test
@@ -0,0 +1,2 @@
+args: run --reload --importmap=tests/importmaps/import_map.json tests/importmaps/test.ts
+output: tests/033_import_map.out
diff --git a/tests/importmaps/import_map.json b/tests/importmaps/import_map.json
new file mode 100644
index 000000000..601874aab
--- /dev/null
+++ b/tests/importmaps/import_map.json
@@ -0,0 +1,14 @@
+{
+ "imports": {
+ "moment": "./moment/moment.ts",
+ "moment/": "./moment/",
+ "lodash": "./lodash/lodash.ts",
+ "lodash/": "./lodash/",
+ "https://www.unpkg.com/vue/dist/vue.runtime.esm.js": "./vue.ts"
+ },
+ "scopes": {
+ "scope/": {
+ "moment": "./scoped_moment.ts"
+ }
+ }
+}
diff --git a/tests/importmaps/lodash/lodash.ts b/tests/importmaps/lodash/lodash.ts
new file mode 100644
index 000000000..2ec04ed3c
--- /dev/null
+++ b/tests/importmaps/lodash/lodash.ts
@@ -0,0 +1 @@
+console.log("Hello from remapped lodash!");
diff --git a/tests/importmaps/lodash/other_file.ts b/tests/importmaps/lodash/other_file.ts
new file mode 100644
index 000000000..714adae3f
--- /dev/null
+++ b/tests/importmaps/lodash/other_file.ts
@@ -0,0 +1 @@
+console.log("Hello from remapped lodash dir!");
diff --git a/tests/importmaps/moment/moment.ts b/tests/importmaps/moment/moment.ts
new file mode 100644
index 000000000..2b54a431e
--- /dev/null
+++ b/tests/importmaps/moment/moment.ts
@@ -0,0 +1 @@
+console.log("Hello from remapped moment!");
diff --git a/tests/importmaps/moment/other_file.ts b/tests/importmaps/moment/other_file.ts
new file mode 100644
index 000000000..24f3a0226
--- /dev/null
+++ b/tests/importmaps/moment/other_file.ts
@@ -0,0 +1 @@
+console.log("Hello from remapped moment dir!");
diff --git a/tests/importmaps/scope/scoped.ts b/tests/importmaps/scope/scoped.ts
new file mode 100644
index 000000000..9a0b5d8e3
--- /dev/null
+++ b/tests/importmaps/scope/scoped.ts
@@ -0,0 +1,2 @@
+import "moment";
+console.log("Hello from scoped!");
diff --git a/tests/importmaps/scoped_moment.ts b/tests/importmaps/scoped_moment.ts
new file mode 100644
index 000000000..9f67f88d4
--- /dev/null
+++ b/tests/importmaps/scoped_moment.ts
@@ -0,0 +1 @@
+console.log("Hello from scoped moment!");
diff --git a/tests/importmaps/test.ts b/tests/importmaps/test.ts
new file mode 100644
index 000000000..9b09e9953
--- /dev/null
+++ b/tests/importmaps/test.ts
@@ -0,0 +1,6 @@
+import "moment";
+import "moment/other_file.ts";
+import "lodash";
+import "lodash/other_file.ts";
+import "https://www.unpkg.com/vue/dist/vue.runtime.esm.js";
+import "./scope/scoped.ts";
diff --git a/tests/importmaps/vue.ts b/tests/importmaps/vue.ts
new file mode 100644
index 000000000..76dbe1917
--- /dev/null
+++ b/tests/importmaps/vue.ts
@@ -0,0 +1 @@
+console.log("Hello from remapped Vue!");
diff --git a/website/manual.md b/website/manual.md
index 08ac60ab7..684d44c2e 100644
--- a/website/manual.md
+++ b/website/manual.md
@@ -634,6 +634,7 @@ OPTIONS:
--allow-read=<allow-read> Allow file system read access
--allow-write=<allow-write> Allow file system write access
-c, --config <FILE> Load compiler configuration file
+ --importmap <FILE> Load import map file
--v8-flags=<v8-flags> Set V8 command line options
SUBCOMMANDS:
@@ -676,6 +677,50 @@ Particularly useful ones:
--async-stack-trace
```
+## Import maps
+
+Deno supports [import maps](https://github.com/WICG/import-maps).
+
+One can use import map with `--importmap=<FILE>` CLI flag.
+
+Current limitations:
+
+- single import map
+- no fallback URLs
+- Deno does not support `std:` namespace
+- Does supports only `file:`, `http:` and `https:` schemes
+
+Example:
+
+```js
+// import_map.json
+
+{
+ "imports": {
+ "http/": "https://deno.land/std/http/"
+ }
+}
+```
+
+```ts
+// hello_server.ts
+
+import { serve } from "http/server.ts";
+
+async function main() {
+ const body = new TextEncoder().encode("Hello World\n");
+ for await (const req of serve(":8000")) {
+ req.respond({ body });
+ }
+}
+
+main();
+```
+
+```bash
+$ deno run --importmap=import_map.json hello_server.ts
+```
+
## Internal details
### Deno and Linux analogy