import_map/
lib.rs

1// Copyright 2018-2025 the Deno authors. All rights reserved. MIT license.
2
3use indexmap::IndexMap;
4use serde_json::Map;
5use serde_json::Value;
6use std::cmp::Ordering;
7use std::collections::HashSet;
8use std::fmt;
9use std::fmt::Debug;
10use thiserror::Error;
11use url::Url;
12
13#[cfg(feature = "ext")]
14pub mod ext;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ImportMapDiagnostic {
18  EmptySpecifier,
19  InvalidScope(String, String),
20  InvalidTargetAddress(String, String),
21  InvalidAddress(String, String),
22  InvalidAddressNotString(String, String),
23  InvalidTopLevelKey(String),
24}
25
26impl fmt::Display for ImportMapDiagnostic {
27  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28    match self {
29      ImportMapDiagnostic::EmptySpecifier => {
30        write!(f, "Invalid empty string specifier.")
31      }
32      ImportMapDiagnostic::InvalidScope(scope, base_url) => {
33        write!(
34          f,
35          "Invalid scope \"{}\" (parsed against base URL \"{}\").",
36          scope, base_url
37        )
38      }
39      ImportMapDiagnostic::InvalidTargetAddress(address, key) => {
40        write!(
41          f,
42          "Invalid target address {:?} for package specifier {:?}. \
43        Package address targets must end with \"/\".",
44          address, key
45        )
46      }
47      ImportMapDiagnostic::InvalidAddress(value, key) => {
48        write!(
49          f,
50          "Invalid address {:#?} for the specifier key \"{}\".",
51          value, key
52        )
53      }
54      ImportMapDiagnostic::InvalidAddressNotString(value, key) => {
55        write!(f, "Invalid address {:#?} for the specifier key \"{}\". Addresses must be strings.", value, key)
56      }
57      ImportMapDiagnostic::InvalidTopLevelKey(key) => {
58        write!(f, "Invalid top-level key \"{}\". Only \"imports\" and \"scopes\" can be present.", key)
59      }
60    }
61  }
62}
63
64#[derive(Debug, boxed_error::Boxed, deno_error::JsError)]
65pub struct ImportMapError(pub Box<ImportMapErrorKind>);
66
67#[derive(Error, Debug, deno_error::JsError)]
68#[class(type)]
69pub enum ImportMapErrorKind {
70  #[error(
71    "Import \"{}\" not a dependency and not in import map{}",
72    .0,
73    .1.as_ref().map(|referrer| format!(" from \"{}\"", referrer)).unwrap_or_default(),
74  )]
75  UnmappedBareSpecifier(String, Option<String>),
76  #[class(inherit)]
77  #[error("Unable to parse import map JSON: {0}")]
78  JsonParse(serde_json::Error),
79  #[error("Import map JSON must be an object")]
80  ImportMapNotObject,
81  #[error("Import map's 'imports' must be an object")]
82  ImportsFieldNotObject,
83  #[error("Import map's 'scopes' must be an object")]
84  ScopesFieldNotObject,
85  #[error("The value for the {0} scope prefix must be an object")]
86  ScopePrefixNotObject(String),
87  #[error("Blocked by null entry for \"{0:?}\"")]
88  BlockedByNullEntry(String),
89  #[error("Failed to resolve the specifier \"{specifier:?}\" as its after-prefix portion \"{after_prefix:?}\" could not be URL-parsed relative to the URL prefix \"{resolution_result}\" mapped to by the prefix \"{specifier_key}\"")]
90  SpecifierResolutionFailure {
91    specifier: String,
92    after_prefix: String,
93    resolution_result: Url,
94    specifier_key: String,
95  },
96  #[error("The specifier \"{specifier:?}\" backtracks above its prefix \"{specifier_key:?}\"")]
97  SpecifierBacktracksAbovePrefix {
98    specifier: String,
99    specifier_key: String,
100  },
101}
102
103// https://url.spec.whatwg.org/#special-scheme
104const SPECIAL_PROTOCOLS: &[&str] =
105  &["ftp", "file", "http", "https", "ws", "wss"];
106fn is_special(url: &Url) -> bool {
107  SPECIAL_PROTOCOLS.contains(&url.scheme())
108}
109
110/// A key value entry in an import map's "imports", or imports of a scope.
111#[derive(Debug)]
112pub struct SpecifierMapEntry<'a> {
113  /// Resolved key.
114  pub key: &'a str,
115  /// Text of the key in the import map file.
116  pub raw_key: &'a str,
117  /// Resolved value.
118  pub value: Option<&'a Url>,
119  /// Text of the value found in the import map file.
120  pub raw_value: Option<&'a str>,
121}
122
123#[derive(Debug, Clone)]
124struct SpecifierMapValue {
125  /// The original index in the file. Used to determine the order
126  /// when writing out the import map.
127  index: usize,
128  /// The raw key if it differs from the actual key.
129  raw_key: Option<String>,
130  /// The raw value if it differs from the actual value.
131  raw_value: Option<String>,
132  maybe_address: Option<Url>,
133}
134
135struct RawKeyValue {
136  key: String,
137  value: Option<String>,
138}
139
140impl SpecifierMapValue {
141  pub fn new(
142    index: usize,
143    raw: &RawKeyValue,
144    normalized_key: &str,
145    value: Option<Url>,
146  ) -> Self {
147    Self {
148      index,
149      // we don't store these to reduce memory usage
150      raw_key: if raw.key == normalized_key {
151        None
152      } else {
153        Some(raw.key.to_string())
154      },
155      raw_value: if value.as_ref().map(|v| v.as_str()) == raw.value.as_deref() {
156        None
157      } else {
158        raw.value.as_ref().map(|v| v.to_string())
159      },
160      maybe_address: value,
161    }
162  }
163}
164
165impl serde::Serialize for SpecifierMapValue {
166  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
167  where
168    S: serde::Serializer,
169  {
170    if let Some(value) = &self.maybe_address {
171      value.serialize(serializer)
172    } else {
173      serializer.serialize_none()
174    }
175  }
176}
177
178#[derive(Debug, Clone)]
179struct ScopesMapValue {
180  /// The original index in the file. Used to determine the order
181  /// when writing out the import map.
182  index: usize,
183  /// The raw key if it differs from the actual key.
184  raw_key: Option<String>,
185  imports: SpecifierMap,
186}
187
188impl serde::Serialize for ScopesMapValue {
189  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
190  where
191    S: serde::Serializer,
192  {
193    self.imports.serialize(serializer)
194  }
195}
196
197type SpecifierMapInner = IndexMap<String, SpecifierMapValue>;
198
199#[derive(Debug, Clone)]
200pub struct SpecifierMap {
201  base_url: Url,
202  inner: SpecifierMapInner,
203}
204
205impl SpecifierMap {
206  pub fn keys(&self) -> impl Iterator<Item = &str> {
207    self.inner.keys().map(|k| k.as_str())
208  }
209
210  /// Gets the raw key values.
211  pub fn entries(&self) -> impl Iterator<Item = SpecifierMapEntry<'_>> {
212    self.inner.iter().map(|(k, v)| SpecifierMapEntry {
213      key: k.as_str(),
214      raw_key: v.raw_key.as_deref().unwrap_or(k.as_str()),
215      value: v.maybe_address.as_ref(),
216      raw_value: v
217        .raw_value
218        .as_deref()
219        .or_else(|| v.maybe_address.as_ref().map(|a| a.as_str())),
220    })
221  }
222
223  pub fn contains(&self, key: &str) -> bool {
224    if let Ok(key) = normalize_specifier_key(key, &self.base_url) {
225      self.inner.contains_key(&key)
226    } else {
227      false
228    }
229  }
230
231  pub fn append(&mut self, key: String, value: String) -> Result<(), String> {
232    let start_index = self
233      .inner
234      .values()
235      .map(|v| v.index)
236      .max()
237      .map(|index| index + 1)
238      .unwrap_or(0);
239
240    let raw = RawKeyValue {
241      key,
242      value: Some(value),
243    };
244    let key = normalize_specifier_key(&raw.key, &self.base_url)
245      .map_err(|e| e.to_string())?;
246    if let Some(import_value) = self.inner.get(&key) {
247      let import_val = if let Some(value) = &import_value.maybe_address {
248        value.as_str()
249      } else {
250        "<invalid value>"
251      };
252      return Err(format!(
253        "\"{}\" already exists and is mapped to \"{}\"",
254        key, import_val
255      ));
256    }
257
258    let address_url =
259      match try_url_like_specifier(raw.value.as_ref().unwrap(), &self.base_url)
260      {
261        Some(url) => url,
262        None => {
263          return Err(format!(
264            "Invalid address \"{}\" for the specifier key {:?}.",
265            raw.value.unwrap(),
266            key
267          ));
268        }
269      };
270
271    self.inner.insert(
272      key.to_string(),
273      SpecifierMapValue::new(start_index, &raw, &key, Some(address_url)),
274    );
275    self.sort();
276
277    Ok(())
278  }
279
280  fn sort(&mut self) {
281    // Sort in longest and alphabetical order.
282    self.inner.sort_by(|k1, _v1, k2, _v2| match k1.cmp(k2) {
283      Ordering::Greater => Ordering::Less,
284      Ordering::Less => Ordering::Greater,
285      // index map guarantees that there can't be duplicate keys
286      Ordering::Equal => unreachable!(),
287    });
288  }
289}
290
291impl serde::Serialize for SpecifierMap {
292  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
293  where
294    S: serde::Serializer,
295  {
296    self.inner.serialize(serializer)
297  }
298}
299
300type ScopesMap = IndexMap<String, ScopesMapValue>;
301type UnresolvedSpecifierMap = IndexMap<String, Option<String>>;
302type UnresolvedScopesMap = IndexMap<String, UnresolvedSpecifierMap>;
303
304/// A key value entry of a scope.
305pub struct ScopeEntry<'a> {
306  /// Resolved key.
307  pub key: &'a str,
308  /// Text of the key in the import map file.
309  pub raw_key: &'a str,
310  /// Specifier map contained in the scope.
311  pub imports: &'a SpecifierMap,
312}
313
314#[derive(Debug, Clone)]
315pub struct ImportMapWithDiagnostics {
316  pub import_map: ImportMap,
317  pub diagnostics: Vec<ImportMapDiagnostic>,
318}
319
320#[derive(Default)]
321pub struct ImportMapOptions {
322  /// `(parsed_address, key, maybe_scope) -> new_address`
323  #[allow(clippy::type_complexity)]
324  pub address_hook: Option<Box<dyn Fn(&str, &str, Option<&str>) -> String>>,
325  /// Whether to expand imports in the import map.
326  ///
327  /// This functionality can be used to modify the import map
328  /// during parsing, by changing the `imports` mapping to expand
329  /// bare specifier imports to provide "directory" imports, eg.:
330  /// - `"express": "npm:express@4` -> `"express/": "npm:/express@4/`
331  /// - `"@std": "jsr:@std` -> `"std@/": "jsr:/@std/`
332  ///
333  /// Only `npm:` and `jsr:` schemes are expanded and if there's already a
334  /// "directory" import, it is not overwritten.
335  ///
336  /// This requires enabling the "ext" cargo feature.
337  pub expand_imports: bool,
338}
339
340impl Debug for ImportMapOptions {
341  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342    f.debug_struct("ImportMapOptions")
343      .field("address_hook", &self.address_hook.as_ref().map(|_| ()))
344      .finish()
345  }
346}
347
348#[derive(Debug, Clone, serde::Serialize)]
349pub struct ImportMap {
350  #[serde(skip)]
351  base_url: Url,
352
353  imports: SpecifierMap,
354  scopes: ScopesMap,
355}
356
357impl ImportMap {
358  pub fn new(base_url: Url) -> Self {
359    Self {
360      base_url: base_url.clone(),
361      imports: SpecifierMap {
362        base_url,
363        inner: Default::default(),
364      },
365      scopes: Default::default(),
366    }
367  }
368
369  pub fn base_url(&self) -> &Url {
370    &self.base_url
371  }
372
373  /// Given a specifier and a referring specifier, determine if a value in the
374  /// import map could be used as an import specifier that resolves using the
375  /// import map.
376  pub fn lookup(&self, specifier: &Url, referrer: &Url) -> Option<String> {
377    let specifier_str = specifier.as_str();
378    for entry in self.entries_for_referrer(referrer) {
379      if let Some(address) = entry.value {
380        let address_str = address.as_str();
381        if address_str == specifier_str {
382          return Some(entry.raw_key.to_string());
383        }
384        if address_str.ends_with('/') && specifier_str.starts_with(address_str)
385        {
386          return Some(specifier_str.replace(address_str, entry.raw_key));
387        }
388      }
389    }
390    None
391  }
392
393  /// Iterates over the import map entries for the specified referrer in the
394  /// order that should be tested for.
395  pub fn entries_for_referrer(
396    &self,
397    referrer: &Url,
398  ) -> impl Iterator<Item = SpecifierMapEntry<'_>> {
399    let referrer = referrer.as_str();
400    let mut imports = Vec::with_capacity(1 + self.scopes.len());
401    if let Some(scopes_map) = self.scopes.get(referrer) {
402      imports.push(&scopes_map.imports);
403    }
404
405    for (normalized_scope_key, scopes_map) in self.scopes.iter() {
406      if normalized_scope_key.ends_with('/')
407        && referrer.starts_with(normalized_scope_key)
408        // already checked above
409        && normalized_scope_key != referrer
410      {
411        imports.push(&scopes_map.imports);
412      }
413    }
414
415    imports.push(&self.imports);
416    imports.into_iter().flat_map(|i| i.entries())
417  }
418
419  pub fn resolve(
420    &self,
421    specifier: &str,
422    referrer: &Url,
423  ) -> Result<Url, ImportMapError> {
424    let as_url: Option<Url> = try_url_like_specifier(specifier, referrer);
425    let normalized_specifier = if let Some(url) = as_url.as_ref() {
426      url.to_string()
427    } else {
428      specifier.to_string()
429    };
430
431    let scopes_match = resolve_scopes_match(
432      &self.scopes,
433      &normalized_specifier,
434      as_url.as_ref(),
435      referrer.as_ref(),
436    )?;
437
438    // match found in scopes map
439    if let Some(scopes_match) = scopes_match {
440      return Ok(scopes_match);
441    }
442
443    let imports_match = resolve_imports_match(
444      &self.imports,
445      &normalized_specifier,
446      as_url.as_ref(),
447    )?;
448
449    // match found in import map
450    if let Some(imports_match) = imports_match {
451      return Ok(imports_match);
452    }
453
454    // The specifier was able to be turned into a URL, but wasn't remapped into anything.
455    if let Some(as_url) = as_url {
456      return Ok(as_url);
457    }
458
459    Err(
460      ImportMapErrorKind::UnmappedBareSpecifier(
461        specifier.to_string(),
462        Some(referrer.to_string()),
463      )
464      .into_box(),
465    )
466  }
467
468  pub fn imports(&self) -> &SpecifierMap {
469    &self.imports
470  }
471
472  pub fn imports_mut(&mut self) -> &mut SpecifierMap {
473    &mut self.imports
474  }
475
476  pub fn scopes(&self) -> impl Iterator<Item = ScopeEntry<'_>> {
477    self.scopes.iter().map(|scope| ScopeEntry {
478      key: scope.0.as_str(),
479      raw_key: scope.1.raw_key.as_deref().unwrap_or(scope.0.as_str()),
480      imports: &scope.1.imports,
481    })
482  }
483
484  pub fn get_or_append_scope_mut(
485    &mut self,
486    key: &str,
487  ) -> Result<&mut SpecifierMap, ImportMapDiagnostic> {
488    let scope_prefix_url = match self.base_url.join(key) {
489      Ok(url) => url.to_string(),
490      _ => {
491        return Err(ImportMapDiagnostic::InvalidScope(
492          key.to_string(),
493          self.base_url.to_string(),
494        ));
495      }
496    };
497
498    // todo(dsherret): was fighting the borrow checker here... these should be
499    // created lazily
500    let base_url = self.base_url.clone();
501    let index = self.scopes.values().map(|s| s.index + 1).max().unwrap_or(0);
502    Ok(
503      &mut self
504        .scopes
505        .entry(scope_prefix_url.clone())
506        .or_insert_with(|| {
507          ScopesMapValue {
508            index,
509            raw_key: if scope_prefix_url == key {
510              None
511            } else {
512              // only store this if they differ to save memory
513              Some(key.to_string())
514            },
515            imports: SpecifierMap {
516              base_url,
517              inner: Default::default(),
518            },
519          }
520        })
521        .imports,
522    )
523  }
524
525  /// Gets the import map as JSON text.
526  pub fn to_json(&self) -> String {
527    let mut w = String::new();
528
529    w.push('{');
530
531    if !self.imports.inner.is_empty() {
532      w.push('\n');
533      w.push_str(r#"  "imports": {"#);
534      write_imports(&mut w, &self.imports, 2);
535      w.push_str("\n  }");
536    }
537
538    if !self.scopes.is_empty() {
539      if !self.imports.inner.is_empty() {
540        w.push(',');
541      }
542      w.push('\n');
543      w.push_str(r#"  "scopes": {"#);
544      write_scopes(&mut w, &self.scopes);
545      w.push_str("\n  }");
546    }
547
548    w.push_str("\n}\n");
549
550    return w;
551
552    fn write_imports(
553      w: &mut String,
554      imports: &SpecifierMap,
555      indent_level: usize,
556    ) {
557      // sort based on how it originally appeared in the file
558      let mut imports = imports.inner.iter().collect::<Vec<_>>();
559      imports.sort_by_key(|v| v.1.index);
560
561      for (i, (key, value)) in imports.into_iter().enumerate() {
562        w.push_str(if i > 0 { ",\n" } else { "\n" });
563        let raw_key = value.raw_key.as_ref().unwrap_or(key);
564        let raw_value = value
565          .raw_value
566          .as_deref()
567          .or_else(|| value.maybe_address.as_ref().map(|a| a.as_str()));
568
569        w.push_str(&"  ".repeat(indent_level));
570        w.push_str(&format!(r#""{}": "#, escape_string(raw_key)));
571        if let Some(value) = raw_value {
572          w.push_str(&format!(r#""{}""#, escape_string(value)));
573        } else {
574          w.push_str("null");
575        }
576      }
577    }
578
579    fn write_scopes(w: &mut String, scopes: &ScopesMap) {
580      // sort based on how the it originally appeared in the file
581      let mut scopes = scopes.iter().collect::<Vec<_>>();
582      scopes.sort_by_key(|v| v.1.index);
583
584      for (i, (key, value)) in scopes.into_iter().enumerate() {
585        w.push_str(if i > 0 { ",\n" } else { "\n" });
586        let raw_key = value.raw_key.as_ref().unwrap_or(key);
587
588        w.push_str(&format!(r#"    "{}": {{"#, escape_string(raw_key)));
589        write_imports(w, &value.imports, 3);
590        w.push_str("\n    }");
591      }
592    }
593
594    fn escape_string(text: &str) -> String {
595      text.replace('"', "\\\"")
596    }
597  }
598}
599
600pub fn parse_from_json(
601  base_url: Url,
602  json_string: &str,
603) -> Result<ImportMapWithDiagnostics, ImportMapError> {
604  parse_from_json_with_options(base_url, json_string, Default::default())
605}
606
607pub fn parse_from_json_with_options(
608  base_url: Url,
609  json_string: &str,
610  options: ImportMapOptions,
611) -> Result<ImportMapWithDiagnostics, ImportMapError> {
612  let mut diagnostics = vec![];
613  let (unresolved_imports, unresolved_scopes) =
614    parse_json(json_string, &options, &mut diagnostics)?;
615  let imports =
616    parse_specifier_map(unresolved_imports, &base_url, &mut diagnostics);
617  let scopes = parse_scope_map(unresolved_scopes, &base_url, &mut diagnostics)?;
618
619  Ok(ImportMapWithDiagnostics {
620    diagnostics,
621    import_map: ImportMap {
622      base_url,
623      imports,
624      scopes,
625    },
626  })
627}
628
629pub fn parse_from_value(
630  base_url: Url,
631  json_value: Value,
632) -> Result<ImportMapWithDiagnostics, ImportMapError> {
633  parse_from_value_with_options(base_url, json_value, Default::default())
634}
635
636pub fn parse_from_value_with_options(
637  base_url: Url,
638  json_value: Value,
639  options: ImportMapOptions,
640) -> Result<ImportMapWithDiagnostics, ImportMapError> {
641  let mut diagnostics = vec![];
642  let (unresolved_imports, unresolved_scopes) =
643    parse_value(json_value, &options, &mut diagnostics)?;
644  let imports =
645    parse_specifier_map(unresolved_imports, &base_url, &mut diagnostics);
646  let scopes = parse_scope_map(unresolved_scopes, &base_url, &mut diagnostics)?;
647
648  Ok(ImportMapWithDiagnostics {
649    diagnostics,
650    import_map: ImportMap {
651      base_url,
652      imports,
653      scopes,
654    },
655  })
656}
657
658fn parse_json(
659  json_string: &str,
660  options: &ImportMapOptions,
661  diagnostics: &mut Vec<ImportMapDiagnostic>,
662) -> Result<(UnresolvedSpecifierMap, UnresolvedScopesMap), ImportMapError> {
663  let v: Value =
664    serde_json::from_str(json_string).map_err(ImportMapErrorKind::JsonParse)?;
665  parse_value(v, options, diagnostics)
666}
667
668fn parse_value(
669  v: Value,
670  options: &ImportMapOptions,
671  diagnostics: &mut Vec<ImportMapDiagnostic>,
672) -> Result<(UnresolvedSpecifierMap, UnresolvedScopesMap), ImportMapError> {
673  fn maybe_expand_imports(value: Value, options: &ImportMapOptions) -> Value {
674    if options.expand_imports {
675      #[cfg(feature = "ext")]
676      {
677        ext::expand_import_map_value(value)
678      }
679      #[cfg(not(feature = "ext"))]
680      {
681        debug_assert!(
682          false,
683          "expand_imports was true, but the \"ext\" feature was not enabled"
684        );
685        value
686      }
687    } else {
688      value
689    }
690  }
691
692  let mut v = maybe_expand_imports(v, options);
693  match v {
694    Value::Object(_) => {}
695    _ => {
696      return Err(ImportMapErrorKind::ImportMapNotObject.into_box());
697    }
698  }
699
700  let imports = if v.get("imports").is_some() {
701    match v["imports"].take() {
702      Value::Object(imports_map) => {
703        parse_specifier_map_json(imports_map, None, options, diagnostics)
704      }
705      _ => {
706        return Err(ImportMapErrorKind::ImportsFieldNotObject.into_box());
707      }
708    }
709  } else {
710    IndexMap::new()
711  };
712
713  let scopes = if v.get("scopes").is_some() {
714    match v["scopes"].take() {
715      Value::Object(scopes_map) => {
716        parse_scopes_map_json(scopes_map, options, diagnostics)?
717      }
718      _ => {
719        return Err(ImportMapErrorKind::ScopesFieldNotObject.into_box());
720      }
721    }
722  } else {
723    IndexMap::new()
724  };
725
726  let mut keys: HashSet<String> = v
727    .as_object()
728    .unwrap()
729    .keys()
730    .map(|k| k.to_string())
731    .collect();
732  keys.remove("imports");
733  keys.remove("scopes");
734  for key in keys {
735    diagnostics.push(ImportMapDiagnostic::InvalidTopLevelKey(key));
736  }
737
738  Ok((imports, scopes))
739}
740
741/// Convert provided JSON map to key values
742fn parse_specifier_map_json(
743  json_map: Map<String, Value>,
744  scope: Option<&str>,
745  options: &ImportMapOptions,
746  diagnostics: &mut Vec<ImportMapDiagnostic>,
747) -> UnresolvedSpecifierMap {
748  let mut map: IndexMap<String, Option<String>> = IndexMap::new();
749
750  // Order is preserved because of "preserve_order" feature of "serde_json".
751  for (specifier_key, value) in json_map.into_iter() {
752    map.insert(
753      specifier_key.clone(),
754      match value {
755        Value::String(mut address) => {
756          if let Some(address_hook) = &options.address_hook {
757            address = address_hook(&address, &specifier_key, scope);
758          }
759          Some(address)
760        }
761        _ => {
762          diagnostics.push(ImportMapDiagnostic::InvalidAddressNotString(
763            value.to_string(),
764            specifier_key,
765          ));
766          None
767        }
768      },
769    );
770  }
771
772  map
773}
774
775/// Convert provided JSON map to key value strings.
776fn parse_scopes_map_json(
777  scopes_map: Map<String, Value>,
778  options: &ImportMapOptions,
779  diagnostics: &mut Vec<ImportMapDiagnostic>,
780) -> Result<UnresolvedScopesMap, ImportMapError> {
781  let mut map = UnresolvedScopesMap::new();
782
783  // Order is preserved because of "preserve_order" feature of "serde_json".
784  for (scope_prefix, mut potential_specifier_map) in scopes_map.into_iter() {
785    let potential_specifier_map = match potential_specifier_map.take() {
786      Value::Object(obj) => obj,
787      _ => {
788        return Err(
789          ImportMapErrorKind::ScopePrefixNotObject(scope_prefix).into_box(),
790        );
791      }
792    };
793
794    let specifier_map = parse_specifier_map_json(
795      potential_specifier_map,
796      Some(&scope_prefix),
797      options,
798      diagnostics,
799    );
800
801    map.insert(scope_prefix.to_string(), specifier_map);
802  }
803
804  Ok(map)
805}
806
807/// Convert provided key value string imports to valid SpecifierMap.
808///
809/// From specification:
810/// - order of iteration must be retained
811/// - SpecifierMap's keys are sorted in longest and alphabetic order
812fn parse_specifier_map(
813  imports: UnresolvedSpecifierMap,
814  base_url: &Url,
815  diagnostics: &mut Vec<ImportMapDiagnostic>,
816) -> SpecifierMap {
817  let mut normalized_map: SpecifierMapInner = SpecifierMapInner::new();
818
819  for (i, (key, value)) in imports.into_iter().enumerate() {
820    let raw = RawKeyValue { key, value };
821    let normalized_key = match normalize_specifier_key(&raw.key, base_url) {
822      Ok(s) => s,
823      Err(err) => {
824        diagnostics.push(err);
825        continue;
826      }
827    };
828    let potential_address = match &raw.value {
829      Some(address) => address,
830      None => {
831        let value = SpecifierMapValue::new(i, &raw, &normalized_key, None);
832        normalized_map.insert(normalized_key, value);
833        continue;
834      }
835    };
836
837    let address_url = match try_url_like_specifier(potential_address, base_url)
838    {
839      Some(url) => url,
840      None => {
841        diagnostics.push(ImportMapDiagnostic::InvalidAddress(
842          potential_address.to_string(),
843          raw.key.to_string(),
844        ));
845        let value = SpecifierMapValue::new(i, &raw, &normalized_key, None);
846        normalized_map.insert(normalized_key, value);
847        continue;
848      }
849    };
850
851    let address_url_string = address_url.to_string();
852    if raw.key.ends_with('/') && !address_url_string.ends_with('/') {
853      diagnostics.push(ImportMapDiagnostic::InvalidTargetAddress(
854        address_url_string,
855        raw.key.to_string(),
856      ));
857      let value = SpecifierMapValue::new(i, &raw, &normalized_key, None);
858      normalized_map.insert(normalized_key, value);
859      continue;
860    }
861
862    let value =
863      SpecifierMapValue::new(i, &raw, &normalized_key, Some(address_url));
864    normalized_map.insert(normalized_key, value);
865  }
866
867  // Sort in longest and alphabetical order.
868  normalized_map.sort_by(|k1, _v1, k2, _v2| match k1.cmp(k2) {
869    Ordering::Greater => Ordering::Less,
870    Ordering::Less => Ordering::Greater,
871    // JSON guarantees that there can't be duplicate keys
872    Ordering::Equal => unreachable!(),
873  });
874
875  SpecifierMap {
876    inner: normalized_map,
877    base_url: base_url.clone(),
878  }
879}
880
881/// Convert provided key value string scopes to valid ScopeMap.
882///
883/// From specification:
884/// - order of iteration must be retained
885/// - ScopeMap's keys are sorted in longest and alphabetic order
886fn parse_scope_map(
887  scope_map: UnresolvedScopesMap,
888  base_url: &Url,
889  diagnostics: &mut Vec<ImportMapDiagnostic>,
890) -> Result<ScopesMap, ImportMapError> {
891  let mut normalized_map: ScopesMap = ScopesMap::new();
892
893  // Order is preserved because of "preserve_order" feature of "serde_json".
894  for (i, (raw_scope_prefix, potential_specifier_map)) in
895    scope_map.into_iter().enumerate()
896  {
897    let scope_prefix_url = match base_url.join(&raw_scope_prefix) {
898      Ok(url) => url.to_string(),
899      _ => {
900        diagnostics.push(ImportMapDiagnostic::InvalidScope(
901          raw_scope_prefix,
902          base_url.to_string(),
903        ));
904        continue;
905      }
906    };
907
908    let norm_map =
909      parse_specifier_map(potential_specifier_map, base_url, diagnostics);
910
911    let value = ScopesMapValue {
912      index: i,
913      raw_key: if scope_prefix_url == raw_scope_prefix {
914        None
915      } else {
916        // only store this if they differ to save memory
917        Some(raw_scope_prefix)
918      },
919      imports: norm_map,
920    };
921    normalized_map.insert(scope_prefix_url, value);
922  }
923
924  // Sort in longest and alphabetical order.
925  normalized_map.sort_by(|k1, _v1, k2, _v2| match k1.cmp(k2) {
926    Ordering::Greater => Ordering::Less,
927    Ordering::Less => Ordering::Greater,
928    // JSON guarantees that there can't be duplicate keys
929    Ordering::Equal => unreachable!(),
930  });
931
932  Ok(normalized_map)
933}
934
935fn try_url_like_specifier(specifier: &str, base: &Url) -> Option<Url> {
936  if specifier.starts_with('/')
937    || specifier.starts_with("./")
938    || specifier.starts_with("../")
939  {
940    if let Ok(url) = base.join(specifier) {
941      return Some(url);
942    }
943  }
944
945  if let Ok(url) = Url::parse(specifier) {
946    return Some(url);
947  }
948
949  None
950}
951
952/// Parse provided key as import map specifier.
953///
954/// Specifiers must be valid URLs (eg. "`https://deno.land/x/std/testing/asserts.ts`")
955/// or "bare" specifiers (eg. "moment").
956fn normalize_specifier_key(
957  specifier_key: &str,
958  base_url: &Url,
959) -> Result<String, ImportMapDiagnostic> {
960  // ignore empty keys
961  if specifier_key.is_empty() {
962    Err(ImportMapDiagnostic::EmptySpecifier)
963  } else if let Some(url) = try_url_like_specifier(specifier_key, base_url) {
964    Ok(url.to_string())
965  } else {
966    // "bare" specifier
967    Ok(specifier_key.to_string())
968  }
969}
970
971fn append_specifier_to_base(
972  base: &Url,
973  specifier: &str,
974) -> Result<Url, url::ParseError> {
975  // Percent-decode first. Specifier might be pre-encoded and could get encoded
976  // again.
977  let specifier =
978    percent_encoding::percent_decode_str(specifier).decode_utf8_lossy();
979  let mut base = base.clone();
980  let is_relative_or_absolute_specifier = specifier.starts_with("../")
981    || specifier.starts_with("./")
982    || specifier.starts_with('/');
983
984  // The specifier could be a windows path such as "C:/a/test.ts" in which
985  // case we don't want to use `join` because it will make the specifier
986  // the url since it contains what looks to be a uri scheme. To work around
987  // this, we append the specifier to the path segments of the base url when
988  // the specifier is not relative or absolute.
989  let mut maybe_query_string_and_fragment = None;
990  if !is_relative_or_absolute_specifier && base.path_segments_mut().is_ok() {
991    {
992      let mut segments = base.path_segments_mut().unwrap();
993      segments.pop_if_empty();
994
995      // Handle query-string and fragment first, otherwise they would be percent-encoded
996      // by `extend()`
997      let prefix = match specifier.find(&['?', '#'][..]) {
998        Some(idx) => {
999          maybe_query_string_and_fragment = Some(&specifier[idx..]);
1000          &specifier[..idx]
1001        }
1002        None => &specifier,
1003      };
1004      segments.extend(prefix.split('/'));
1005    }
1006
1007    if let Some(query_string_and_fragment) = maybe_query_string_and_fragment {
1008      Ok(base.join(query_string_and_fragment)?)
1009    } else {
1010      Ok(base)
1011    }
1012  } else {
1013    Ok(base.join(&specifier)?)
1014  }
1015}
1016
1017fn resolve_scopes_match(
1018  scopes: &ScopesMap,
1019  normalized_specifier: &str,
1020  as_url: Option<&Url>,
1021  referrer: &str,
1022) -> Result<Option<Url>, ImportMapError> {
1023  // exact-match
1024  if let Some(scope_imports) = scopes.get(referrer) {
1025    let scope_match = resolve_imports_match(
1026      &scope_imports.imports,
1027      normalized_specifier,
1028      as_url,
1029    )?;
1030    // Return only if there was actual match (not None).
1031    if scope_match.is_some() {
1032      return Ok(scope_match);
1033    }
1034  }
1035
1036  for (normalized_scope_key, scope_imports) in scopes.iter() {
1037    if normalized_scope_key.ends_with('/')
1038      && referrer.starts_with(normalized_scope_key)
1039    {
1040      let scope_match = resolve_imports_match(
1041        &scope_imports.imports,
1042        normalized_specifier,
1043        as_url,
1044      )?;
1045      // Return only if there was actual match (not None).
1046      if scope_match.is_some() {
1047        return Ok(scope_match);
1048      }
1049    }
1050  }
1051
1052  Ok(None)
1053}
1054
1055fn resolve_imports_match(
1056  specifier_map: &SpecifierMap,
1057  normalized_specifier: &str,
1058  as_url: Option<&Url>,
1059) -> Result<Option<Url>, ImportMapError> {
1060  // exact-match
1061  if let Some(value) = specifier_map.inner.get(normalized_specifier) {
1062    if let Some(address) = &value.maybe_address {
1063      return Ok(Some(address.clone()));
1064    } else {
1065      return Err(
1066        ImportMapErrorKind::BlockedByNullEntry(
1067          normalized_specifier.to_string(),
1068        )
1069        .into_box(),
1070      );
1071    }
1072  }
1073
1074  // Package-prefix match
1075  // "most-specific wins", i.e. when there are multiple matching keys,
1076  // choose the longest.
1077  for (specifier_key, value) in specifier_map.inner.iter() {
1078    if !specifier_key.ends_with('/') {
1079      continue;
1080    }
1081
1082    if !normalized_specifier.starts_with(specifier_key) {
1083      continue;
1084    }
1085
1086    if let Some(url) = as_url {
1087      if !is_special(url) {
1088        continue;
1089      }
1090    }
1091
1092    let resolution_result = value.maybe_address.as_ref().ok_or_else(|| {
1093      ImportMapErrorKind::BlockedByNullEntry(specifier_key.clone())
1094    })?;
1095
1096    // Enforced by parsing.
1097    assert!(resolution_result.to_string().ends_with('/'));
1098
1099    let after_prefix = &normalized_specifier[specifier_key.len()..];
1100
1101    let url = match append_specifier_to_base(resolution_result, after_prefix) {
1102      Ok(url) => url,
1103      Err(_) => {
1104        return Err(
1105          ImportMapErrorKind::SpecifierResolutionFailure {
1106            specifier: normalized_specifier.to_string(),
1107            after_prefix: after_prefix.to_string(),
1108            resolution_result: resolution_result.clone(),
1109            specifier_key: specifier_key.clone(),
1110          }
1111          .into_box(),
1112        );
1113      }
1114    };
1115
1116    if !url.as_str().starts_with(resolution_result.as_str()) {
1117      return Err(
1118        ImportMapErrorKind::SpecifierBacktracksAbovePrefix {
1119          specifier: normalized_specifier.to_string(),
1120          specifier_key: specifier_key.clone(),
1121        }
1122        .into_box(),
1123      );
1124    }
1125
1126    return Ok(Some(url));
1127  }
1128
1129  #[cfg(feature = "logging")]
1130  log::debug!(
1131    "Specifier {:?} was not mapped in import map.",
1132    normalized_specifier
1133  );
1134
1135  Ok(None)
1136}
1137
1138#[cfg(test)]
1139mod test {
1140  use super::*;
1141  use pretty_assertions::assert_eq;
1142
1143  #[test]
1144  fn npm_specifiers() {
1145    let mut specifiers = SpecifierMapInner::new();
1146    specifiers.insert(
1147      "aws-sdk/".to_string(),
1148      SpecifierMapValue {
1149        index: 0,
1150        raw_key: None,
1151        raw_value: None,
1152        maybe_address: Some(Url::parse("npm:aws-sdk/").unwrap()),
1153      },
1154    );
1155    let specifiers = SpecifierMap {
1156      base_url: Url::parse("file:///").unwrap(),
1157      inner: specifiers,
1158    };
1159
1160    assert!(
1161      resolve_imports_match(&specifiers, "aws-sdk/clients/S3", None).is_err()
1162    );
1163
1164    let mut specifiers = SpecifierMapInner::new();
1165    specifiers.insert(
1166      "aws-sdk/".to_string(),
1167      SpecifierMapValue {
1168        index: 0,
1169        raw_key: None,
1170        raw_value: None,
1171        maybe_address: Some(Url::parse("npm:/aws-sdk/").unwrap()),
1172      },
1173    );
1174    let specifiers = SpecifierMap {
1175      base_url: Url::parse("file:///").unwrap(),
1176      inner: specifiers,
1177    };
1178
1179    let resolved_specifier =
1180      resolve_imports_match(&specifiers, "aws-sdk/clients/S3", None)
1181        .unwrap()
1182        .unwrap();
1183
1184    assert_eq!(resolved_specifier.as_str(), "npm:/aws-sdk/clients/S3");
1185  }
1186
1187  #[test]
1188  fn mapped_windows_file_specifier() {
1189    // from issue #11530
1190    let mut specifiers = SpecifierMapInner::new();
1191    specifiers.insert(
1192      "file:///".to_string(),
1193      SpecifierMapValue {
1194        index: 0,
1195        raw_key: None,
1196        raw_value: None,
1197        maybe_address: Some(Url::parse("http://localhost/").unwrap()),
1198      },
1199    );
1200    let specifiers = SpecifierMap {
1201      base_url: Url::parse("file:///").unwrap(),
1202      inner: specifiers,
1203    };
1204
1205    let resolved_specifier =
1206      resolve_imports_match(&specifiers, "file:///C:/folder/file.ts", None)
1207        .unwrap()
1208        .unwrap();
1209
1210    assert_eq!(
1211      resolved_specifier.as_str(),
1212      "http://localhost/C:/folder/file.ts"
1213    );
1214  }
1215
1216  #[test]
1217  #[cfg(feature = "ext")]
1218  fn ext_expand_imports() {
1219    let url = Url::parse("file:///deno.json").unwrap();
1220    let json_string = r#"{
1221  "imports": {
1222    "@std": "jsr:/@std",
1223    "@foo": "jsr:@foo",
1224    "express": "npm:express@4",
1225    "foo": "https://example.com/foo/bar"
1226  },
1227  "scopes": {
1228    "./folder/": {
1229      "@std": "jsr:/@std",
1230      "@foo": "jsr:@foo",
1231      "express": "npm:express@4",
1232      "foo": "https://example.com/foo/bar"
1233    }
1234  }
1235}"#;
1236    let im = parse_from_json_with_options(
1237      url,
1238      json_string,
1239      ImportMapOptions {
1240        address_hook: None,
1241        expand_imports: true,
1242      },
1243    )
1244    .unwrap();
1245    assert_eq!(
1246      im.import_map.to_json(),
1247      r#"{
1248  "imports": {
1249    "@std": "jsr:/@std",
1250    "@std/": "jsr:/@std/",
1251    "@foo": "jsr:@foo",
1252    "@foo/": "jsr:/@foo/",
1253    "express": "npm:express@4",
1254    "express/": "npm:/express@4/",
1255    "foo": "https://example.com/foo/bar"
1256  },
1257  "scopes": {
1258    "./folder/": {
1259      "@std": "jsr:/@std",
1260      "@std/": "jsr:/@std/",
1261      "@foo": "jsr:@foo",
1262      "@foo/": "jsr:/@foo/",
1263      "express": "npm:express@4",
1264      "express/": "npm:/express@4/",
1265      "foo": "https://example.com/foo/bar"
1266    }
1267  }
1268}
1269"#,
1270    );
1271  }
1272
1273  #[test]
1274  fn iterate_applicable_entries() {
1275    let url = Url::parse("file:///deno.json").unwrap();
1276    let json_string = r#"{
1277  "imports": {
1278    "foo": "./main.ts"
1279  },
1280  "scopes": {
1281    "./folder/": {
1282      "bar": "./main.ts"
1283    },
1284    "./folder/file.ts": {
1285      "baz": "./other.ts"
1286    }
1287  }
1288}"#;
1289    let im = parse_from_json(url, json_string).unwrap();
1290    let im = im.import_map;
1291    let keys = im
1292      .entries_for_referrer(&Url::parse("file:///folder/main.ts").unwrap())
1293      .map(|e| e.raw_key)
1294      .collect::<Vec<_>>();
1295    assert_eq!(keys, ["bar", "foo"]);
1296    let keys = im
1297      .entries_for_referrer(&Url::parse("file:///folder/file.ts").unwrap())
1298      .map(|e| e.raw_key)
1299      .collect::<Vec<_>>();
1300    assert_eq!(keys, ["baz", "bar", "foo"]);
1301    let keys = im
1302      .entries_for_referrer(&Url::parse("file:///other/file.ts").unwrap())
1303      .map(|e| e.raw_key)
1304      .collect::<Vec<_>>();
1305    assert_eq!(keys, ["foo"]);
1306  }
1307}