Skip to main content

script/modules/
import_map.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use indexmap::IndexMap;
6use js::context::JSContext;
7use script_bindings::error::Fallible;
8use serde_json::{Map as JsonMap, Value as JsonValue};
9use servo_url::ServoUrl;
10
11use crate::dom::bindings::error::{Error, report_pending_exception, throw_dom_exception};
12use crate::dom::console::Console;
13use crate::dom::globalscope::GlobalScope;
14use crate::realms::enter_auto_realm;
15
16type ModuleIntegrityMap = IndexMap<ServoUrl, String>;
17pub(crate) type ModuleSpecifierMap = IndexMap<String, Option<ServoUrl>>;
18
19/// <https://html.spec.whatwg.org/multipage/#import-map-processing-model>
20#[derive(Default, JSTraceable, MallocSizeOf)]
21pub(crate) struct ImportMap {
22    #[no_trace]
23    pub(crate) imports: ModuleSpecifierMap,
24    #[no_trace]
25    pub(crate) scopes: IndexMap<ServoUrl, ModuleSpecifierMap>,
26    #[no_trace]
27    integrity: ModuleIntegrityMap,
28}
29
30impl ImportMap {
31    /// <https://html.spec.whatwg.org/multipage/#resolving-a-module-integrity-metadata>
32    pub(crate) fn resolve_a_module_integrity_metadata(&self, url: &ServoUrl) -> String {
33        // Step 1. Let map be settingsObject's global object's import map.
34
35        // Step 2. If map's integrity[url] does not exist, then return the empty string.
36        // Step 3. Return map's integrity[url].
37        self.integrity.get(url).cloned().unwrap_or_default()
38    }
39}
40
41/// <https://html.spec.whatwg.org/multipage/#register-an-import-map>
42pub(crate) fn register_import_map(
43    cx: &mut JSContext,
44    global: &GlobalScope,
45    result: Fallible<ImportMap>,
46) {
47    match result {
48        Ok(new_import_map) => {
49            // Step 2. Merge existing and new import maps, given global and result's import map.
50            merge_existing_and_new_import_maps(cx, global, new_import_map);
51        },
52        Err(exception) => {
53            let mut realm = enter_auto_realm(cx, global);
54            let cx = &mut realm.current_realm();
55
56            // Step 1. If result's error to rethrow is not null, then report
57            // an exception given by result's error to rethrow for global and return.
58            throw_dom_exception(cx, global, exception);
59            report_pending_exception(cx);
60        },
61    }
62}
63
64/// <https://html.spec.whatwg.org/multipage/#merge-existing-and-new-import-maps>
65fn merge_existing_and_new_import_maps(
66    cx: &mut JSContext,
67    global: &GlobalScope,
68    new_import_map: ImportMap,
69) {
70    // Step 1. Let newImportMapScopes be a deep copy of newImportMap's scopes.
71    let new_import_map_scopes = new_import_map.scopes;
72
73    // Step 2. Let oldImportMap be global's import map.
74    let mut old_import_map = global.import_map_mut();
75
76    // Step 3. Let newImportMapImports be a deep copy of newImportMap's imports.
77    let mut new_import_map_imports = new_import_map.imports;
78
79    let resolved_module_set = global.resolved_module_set();
80    // Step 4. For each scopePrefix → scopeImports of newImportMapScopes:
81    for (scope_prefix, mut scope_imports) in new_import_map_scopes {
82        // Step 4.1. For each record of global's resolved module set:
83        for record in resolved_module_set.iter() {
84            // If scopePrefix is record's serialized base URL, or if scopePrefix ends with
85            // U+002F (/) and scopePrefix is a code unit prefix of record's serialized base URL, then:
86            let prefix = scope_prefix.as_str();
87            if prefix == record.base_url ||
88                (record.base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
89            {
90                // For each specifierKey → resolutionResult of scopeImports:
91                scope_imports.retain(|key, val| {
92                    // If specifierKey is record's specifier, or if all of the following conditions are true:
93                    // specifierKey ends with U+002F (/);
94                    // specifierKey is a code unit prefix of record's specifier;
95                    // either record's specifier as a URL is null or is special,
96                    if *key == record.specifier ||
97                        (key.ends_with('\u{002f}') &&
98                            record.specifier.starts_with(key) &&
99                            (record.specifier_url.is_none() ||
100                                record
101                                    .specifier_url
102                                    .as_ref()
103                                    .is_some_and(|u| u.is_special_scheme())))
104                    {
105                        // The user agent may report a warning to the console indicating the ignored rule.
106                        // They may choose to avoid reporting if the rule is identical to an existing one.
107                        Console::internal_warn(
108                            cx,
109                            global,
110                            format!("Ignored rule: {key} -> {val:?}."),
111                        );
112                        // Remove scopeImports[specifierKey].
113                        false
114                    } else {
115                        true
116                    }
117                })
118            }
119        }
120
121        // Step 4.2 If scopePrefix exists in oldImportMap's scopes
122        if old_import_map.scopes.contains_key(&scope_prefix) {
123            // set oldImportMap's scopes[scopePrefix] to the result of
124            // merging module specifier maps, given scopeImports and oldImportMap's scopes[scopePrefix].
125            let merged_module_specifier_map = merge_module_specifier_maps(
126                cx,
127                global,
128                scope_imports,
129                &old_import_map.scopes[&scope_prefix],
130            );
131            old_import_map
132                .scopes
133                .insert(scope_prefix, merged_module_specifier_map);
134        } else {
135            // Step 4.3 Otherwise, set oldImportMap's scopes[scopePrefix] to scopeImports.
136            old_import_map.scopes.insert(scope_prefix, scope_imports);
137        }
138    }
139
140    // Step 5. For each url → integrity of newImportMap's integrity:
141    for (url, integrity) in &new_import_map.integrity {
142        // Step 5.1 If url exists in oldImportMap's integrity, then:
143        if old_import_map.integrity.contains_key(url) {
144            // Step 5.1.1 The user agent may report a warning to the console indicating the ignored rule.
145            // They may choose to avoid reporting if the rule is identical to an existing one.
146            Console::internal_warn(cx, global, format!("Ignored rule: {url} -> {integrity}."));
147            // Step 5.1.2 Continue.
148            continue;
149        }
150
151        // Step 5.2 Set oldImportMap's integrity[url] to integrity.
152        old_import_map
153            .integrity
154            .insert(url.clone(), integrity.clone());
155    }
156
157    // Step 6. For each record of global's resolved module set:
158    for record in resolved_module_set.iter() {
159        // For each specifier → url of newImportMapImports:
160        new_import_map_imports.retain(|specifier, val| {
161            // If specifier starts with record's specifier, then:
162            //
163            // Note: Spec is wrong, we need to check if record's specifier starts with specifier
164            // See: https://github.com/whatwg/html/issues/11875
165            if record.specifier.starts_with(specifier) {
166                // The user agent may report a warning to the console indicating the ignored rule.
167                // They may choose to avoid reporting if the rule is identical to an existing one.
168                Console::internal_warn(
169                    cx,
170                    global,
171                    format!("Ignored rule: {specifier} -> {val:?}."),
172                );
173                // Remove newImportMapImports[specifier].
174                false
175            } else {
176                true
177            }
178        });
179    }
180
181    // Step 7. Set oldImportMap's imports to the result of merge module specifier maps,
182    // given newImportMapImports and oldImportMap's imports.
183    let merged_module_specifier_map =
184        merge_module_specifier_maps(cx, global, new_import_map_imports, &old_import_map.imports);
185    old_import_map.imports = merged_module_specifier_map;
186
187    // https://html.spec.whatwg.org/multipage/#the-resolution-algorithm
188    // Sort scopes to ensure entries are visited from most-specific to least-specific.
189    old_import_map
190        .scopes
191        .sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
192}
193
194/// <https://html.spec.whatwg.org/multipage/#merge-module-specifier-maps>
195fn merge_module_specifier_maps(
196    cx: &mut JSContext,
197    global: &GlobalScope,
198    new_map: ModuleSpecifierMap,
199    old_map: &ModuleSpecifierMap,
200) -> ModuleSpecifierMap {
201    // Step 1. Let mergedMap be a deep copy of oldMap.
202    let mut merged_map = old_map.clone();
203
204    // Step 2. For each specifier → url of newMap:
205    for (specifier, url) in new_map {
206        // Step 2.1 If specifier exists in oldMap, then:
207        if old_map.contains_key(&specifier) {
208            // Step 2.1.1 The user agent may report a warning to the console indicating the ignored rule.
209            // They may choose to avoid reporting if the rule is identical to an existing one.
210            Console::internal_warn(cx, global, format!("Ignored rule: {specifier} -> {url:?}."));
211
212            // Step 2.1.2 Continue.
213            continue;
214        }
215
216        // Step 2.2 Set mergedMap[specifier] to url.
217        merged_map.insert(specifier, url);
218    }
219
220    merged_map
221}
222
223/// <https://html.spec.whatwg.org/multipage/#parse-an-import-map-string>
224pub(crate) fn parse_an_import_map_string(
225    cx: &mut JSContext,
226    global: &GlobalScope,
227    input: &str,
228    base_url: ServoUrl,
229) -> Fallible<ImportMap> {
230    // Step 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
231    let parsed: JsonValue = serde_json::from_str(input)
232        .map_err(|_| Error::Type(c"The value needs to be a JSON object.".to_owned()))?;
233    // Step 2. If parsed is not an ordered map, then throw a TypeError indicating that the
234    // top-level value needs to be a JSON object.
235    let JsonValue::Object(mut parsed) = parsed else {
236        return Err(Error::Type(
237            c"The top-level value needs to be a JSON object.".to_owned(),
238        ));
239    };
240
241    // Step 3. Let sortedAndNormalizedImports be an empty ordered map.
242    let mut sorted_and_normalized_imports = ModuleSpecifierMap::new();
243    // Step 4. If parsed["imports"] exists, then:
244    if let Some(imports) = parsed.get("imports") {
245        // Step 4.1 If parsed["imports"] is not an ordered map, then throw a TypeError
246        // indicating that the value for the "imports" top-level key needs to be a JSON object.
247        let JsonValue::Object(imports) = imports else {
248            return Err(Error::Type(
249                c"The \"imports\" top-level value needs to be a JSON object.".to_owned(),
250            ));
251        };
252        // Step 4.2 Set sortedAndNormalizedImports to the result of sorting and
253        // normalizing a module specifier map given parsed["imports"] and baseURL.
254        sorted_and_normalized_imports =
255            sort_and_normalize_module_specifier_map(cx, global, imports, &base_url);
256    }
257
258    // Step 5. Let sortedAndNormalizedScopes be an empty ordered map.
259    let mut sorted_and_normalized_scopes: IndexMap<ServoUrl, ModuleSpecifierMap> = IndexMap::new();
260    // Step 6. If parsed["scopes"] exists, then:
261    if let Some(scopes) = parsed.get("scopes") {
262        // Step 6.1 If parsed["scopes"] is not an ordered map, then throw a TypeError
263        // indicating that the value for the "scopes" top-level key needs to be a JSON object.
264        let JsonValue::Object(scopes) = scopes else {
265            return Err(Error::Type(
266                c"The \"scopes\" top-level value needs to be a JSON object.".to_owned(),
267            ));
268        };
269        // Step 6.2 Set sortedAndNormalizedScopes to the result of sorting and
270        // normalizing scopes given parsed["scopes"] and baseURL.
271        sorted_and_normalized_scopes = sort_and_normalize_scopes(cx, global, scopes, &base_url)?;
272    }
273
274    // Step 7. Let normalizedIntegrity be an empty ordered map.
275    let mut normalized_integrity = ModuleIntegrityMap::new();
276    // Step 8. If parsed["integrity"] exists, then:
277    if let Some(integrity) = parsed.get("integrity") {
278        // Step 8.1 If parsed["integrity"] is not an ordered map, then throw a TypeError
279        // indicating that the value for the "integrity" top-level key needs to be a JSON object.
280        let JsonValue::Object(integrity) = integrity else {
281            return Err(Error::Type(
282                c"The \"integrity\" top-level value needs to be a JSON object.".to_owned(),
283            ));
284        };
285        // Step 8.2 Set normalizedIntegrity to the result of normalizing
286        // a module integrity map given parsed["integrity"] and baseURL.
287        normalized_integrity = normalize_module_integrity_map(cx, global, integrity, &base_url);
288    }
289
290    // Step 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity",
291    // then the user agent should report a warning to the console indicating that an invalid
292    // top-level key was present in the import map.
293    parsed.retain(|k, _| !matches!(k.as_str(), "imports" | "scopes" | "integrity"));
294    if !parsed.is_empty() {
295        Console::internal_warn(
296            cx,
297            global,
298            "Invalid top-level key was present in the import map.
299                Only \"imports\", \"scopes\", and \"integrity\" are allowed."
300                .to_string(),
301        );
302    }
303
304    // Step 10. Return an import map
305    Ok(ImportMap {
306        imports: sorted_and_normalized_imports,
307        scopes: sorted_and_normalized_scopes,
308        integrity: normalized_integrity,
309    })
310}
311
312/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-a-module-specifier-map>
313fn sort_and_normalize_module_specifier_map(
314    cx: &mut JSContext,
315    global: &GlobalScope,
316    original_map: &JsonMap<String, JsonValue>,
317    base_url: &ServoUrl,
318) -> ModuleSpecifierMap {
319    // Step 1. Let normalized be an empty ordered map.
320    let mut normalized = ModuleSpecifierMap::new();
321
322    // Step 2. For each specifier_key -> value in originalMap
323    for (specifier_key, value) in original_map {
324        // Step 2.1 Let normalized_specifier_key be the result of
325        // normalizing a specifier key given specifier_key and base_url.
326        let Some(normalized_specifier_key) =
327            normalize_specifier_key(cx, global, specifier_key, base_url)
328        else {
329            // Step 2.2 If normalized_specifier_key is null, then continue.
330            continue;
331        };
332
333        // Step 2.3 If value is not a string, then:
334        let JsonValue::String(value) = value else {
335            // Step 2.3.1 The user agent may report a warning to the console
336            // indicating that addresses need to be strings.
337            Console::internal_warn(cx, global, "Addresses need to be strings.".to_string());
338
339            // Step 2.3.2 Set normalized[normalized_specifier_key] to null.
340            normalized.insert(normalized_specifier_key, None);
341            // Step 2.3.3 Continue.
342            continue;
343        };
344
345        // Step 2.4. Let address_url be the result of resolving a URL-like module specifier given value and baseURL.
346        let Some(address_url) = resolve_url_like_module_specifier(value.as_str(), base_url) else {
347            // Step 2.5 If address_url is null, then:
348            // Step 2.5.1. The user agent may report a warning to the console
349            // indicating that the address was invalid.
350            Console::internal_warn(
351                cx,
352                global,
353                format!("Value failed to resolve to module specifier: {value}"),
354            );
355
356            // Step 2.5.2 Set normalized[normalized_specifier_key] to null.
357            normalized.insert(normalized_specifier_key, None);
358            // Step 2.5.3 Continue.
359            continue;
360        };
361
362        // Step 2.6 If specifier_key ends with U+002F (/), and the serialization of
363        // address_url does not end with U+002F (/), then:
364        if specifier_key.ends_with('\u{002f}') && !address_url.as_str().ends_with('\u{002f}') {
365            // step 2.6.1. The user agent may report a warning to the console
366            // indicating that an invalid address was given for the specifier key specifierKey;
367            // since specifierKey ends with a slash, the address needs to as well.
368            Console::internal_warn(
369                cx,
370                global,
371                format!(
372                    "Invalid address for specifier key '{specifier_key}': {address_url}.
373                    Since specifierKey ends with a slash, the address needs to as well."
374                ),
375            );
376
377            // Step 2.6.2 Set normalized[normalized_specifier_key] to null.
378            normalized.insert(normalized_specifier_key, None);
379            // Step 2.6.3 Continue.
380            continue;
381        }
382
383        // Step 2.7 Set normalized[normalized_specifier_key] to address_url.
384        normalized.insert(normalized_specifier_key, Some(address_url));
385    }
386
387    // Step 3. Return the result of sorting in descending order normalized
388    // with an entry a being less than an entry b if a's key is code unit less than b's key.
389    normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
390    normalized
391}
392
393/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-scopes>
394fn sort_and_normalize_scopes(
395    cx: &mut JSContext,
396    global: &GlobalScope,
397    original_map: &JsonMap<String, JsonValue>,
398    base_url: &ServoUrl,
399) -> Fallible<IndexMap<ServoUrl, ModuleSpecifierMap>> {
400    // Step 1. Let normalized be an empty ordered map.
401    let mut normalized: IndexMap<ServoUrl, ModuleSpecifierMap> = IndexMap::new();
402
403    // Step 2. For each scopePrefix → potentialSpecifierMap of originalMap:
404    for (scope_prefix, potential_specifier_map) in original_map {
405        // Step 2.1 If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating
406        // that the value of the scope with prefix scopePrefix needs to be a JSON object.
407        let JsonValue::Object(potential_specifier_map) = potential_specifier_map else {
408            return Err(Error::Type(
409                c"The value of the scope with prefix scopePrefix needs to be a JSON object."
410                    .to_owned(),
411            ));
412        };
413
414        // Step 2.2 Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
415        let Ok(scope_prefix_url) = ServoUrl::parse_with_base(Some(base_url), scope_prefix) else {
416            // Step 2.3 If scopePrefixURL is failure, then:
417            // Step 2.3.1 The user agent may report a warning
418            // to the console that the scope prefix URL was not parseable.
419            Console::internal_warn(
420                cx,
421                global,
422                format!("Scope prefix URL was not parseable: {scope_prefix}"),
423            );
424            // Step 2.3.2 Continue.
425            continue;
426        };
427
428        // Step 2.4 Let normalizedScopePrefix be the serialization of scopePrefixURL.
429        let normalized_scope_prefix = scope_prefix_url;
430
431        // Step 2.5 Set normalized[normalizedScopePrefix] to the result of sorting and
432        // normalizing a module specifier map given potentialSpecifierMap and baseURL.
433        let normalized_specifier_map =
434            sort_and_normalize_module_specifier_map(cx, global, potential_specifier_map, base_url);
435        normalized.insert(normalized_scope_prefix, normalized_specifier_map);
436    }
437
438    // Step 3. Return the result of sorting in descending order normalized,
439    // with an entry a being less than an entry b if a's key is code unit less than b's key.
440    normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
441    Ok(normalized)
442}
443
444/// <https://html.spec.whatwg.org/multipage/#normalizing-a-module-integrity-map>
445fn normalize_module_integrity_map(
446    cx: &mut JSContext,
447    global: &GlobalScope,
448    original_map: &JsonMap<String, JsonValue>,
449    base_url: &ServoUrl,
450) -> ModuleIntegrityMap {
451    // Step 1. Let normalized be an empty ordered map.
452    let mut normalized = ModuleIntegrityMap::new();
453
454    // Step 2. For each key → value of originalMap:
455    for (key, value) in original_map {
456        // Step 2.1 Let resolvedURL be the result of
457        // resolving a URL-like module specifier given key and baseURL.
458        let Some(resolved_url) = resolve_url_like_module_specifier(key.as_str(), base_url) else {
459            // Step 2.2 If resolvedURL is null, then:
460            // Step 2.2.1 The user agent may report a warning
461            // to the console indicating that the key failed to resolve.
462            Console::internal_warn(
463                cx,
464                global,
465                format!("Key failed to resolve to module specifier: {key}"),
466            );
467            // Step 2.2.2 Continue.
468            continue;
469        };
470
471        // Step 2.3 If value is not a string, then:
472        let JsonValue::String(value) = value else {
473            // Step 2.3.1 The user agent may report a warning
474            // to the console indicating that integrity metadata values need to be strings.
475            Console::internal_warn(
476                cx,
477                global,
478                "Integrity metadata values need to be strings.".to_string(),
479            );
480            // Step 2.3.2 Continue.
481            continue;
482        };
483
484        // Step 2.4 Set normalized[resolvedURL] to value.
485        normalized.insert(resolved_url, value.clone());
486    }
487
488    // Step 3. Return normalized.
489    normalized
490}
491
492/// <https://html.spec.whatwg.org/multipage/#normalizing-a-specifier-key>
493fn normalize_specifier_key(
494    cx: &mut JSContext,
495    global: &GlobalScope,
496    specifier_key: &str,
497    base_url: &ServoUrl,
498) -> Option<String> {
499    // step 1. If specifierKey is the empty string, then:
500    if specifier_key.is_empty() {
501        // step 1.1 The user agent may report a warning to the console
502        // indicating that specifier keys may not be the empty string.
503        Console::internal_warn(
504            cx,
505            global,
506            "Specifier keys may not be the empty string.".to_string(),
507        );
508        // step 1.2 Return null.
509        return None;
510    }
511    // step 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
512    let url = resolve_url_like_module_specifier(specifier_key, base_url);
513
514    // step 3. If url is not null, then return the serialization of url.
515    if let Some(url) = url {
516        return Some(url.into_string());
517    }
518
519    // step 4. Return specifierKey.
520    Some(specifier_key.to_string())
521}
522
523/// <https://html.spec.whatwg.org/multipage/#resolving-a-url-like-module-specifier>
524pub(crate) fn resolve_url_like_module_specifier(
525    specifier: &str,
526    base_url: &ServoUrl,
527) -> Option<ServoUrl> {
528    // Step 1. If specifier starts with "/", "./", or "../", then:
529    if specifier.starts_with('/') || specifier.starts_with("./") || specifier.starts_with("../") {
530        // Step 1.1. Let url be the result of URL parsing specifier with baseURL.
531        return ServoUrl::parse_with_base(Some(base_url), specifier).ok();
532    }
533    // Step 2. Let url be the result of URL parsing specifier (with no base URL).
534    ServoUrl::parse(specifier).ok()
535}