import_map/
lib.rs

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