Skip to main content

script/scripting/
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::bindings::inheritance::Castable;
13use crate::dom::bindings::root::DomRoot;
14use crate::dom::console::Console;
15use crate::dom::globalscope::GlobalScope;
16use crate::dom::window::Window;
17use crate::realms::enter_auto_realm;
18use crate::scripting::fetching_options::BaseScript;
19
20type ModuleIntegrityMap = IndexMap<ServoUrl, String>;
21type ModuleSpecifierMap = IndexMap<String, Option<ServoUrl>>;
22
23/// <https://html.spec.whatwg.org/multipage/#import-map-processing-model>
24#[derive(Default, JSTraceable, MallocSizeOf)]
25pub(crate) struct ImportMap {
26    #[no_trace]
27    pub(crate) imports: ModuleSpecifierMap,
28    #[no_trace]
29    pub(crate) scopes: IndexMap<ServoUrl, ModuleSpecifierMap>,
30    #[no_trace]
31    integrity: ModuleIntegrityMap,
32}
33
34/// The 'referringScript' argument for the purposes of `resolve_module_specifier`.
35pub(crate) struct ReferringScript<'a> {
36    pub(crate) script: &'a BaseScript,
37    pub(crate) settings_object: DomRoot<GlobalScope>,
38}
39
40impl ImportMap {
41    /// <https://html.spec.whatwg.org/multipage/#resolving-a-module-integrity-metadata>
42    pub(crate) fn resolve_a_module_integrity_metadata(&self, url: &ServoUrl) -> String {
43        // Step 1. Let map be settingsObject's global object's import map.
44
45        // Step 2. If map's integrity[url] does not exist, then return the empty string.
46        // Step 3. Return map's integrity[url].
47        self.integrity.get(url).cloned().unwrap_or_default()
48    }
49
50    /// <https://html.spec.whatwg.org/multipage/#resolve-a-module-specifier>
51    pub(crate) fn resolve_module_specifier(
52        current_settings_object: &GlobalScope,
53        referring_script: Option<&ReferringScript>,
54        specifier: String,
55    ) -> Fallible<ServoUrl> {
56        // Step 1. Let settingsObject and baseURL be null.
57        let (settings_object, base_url) = match referring_script {
58            // Step 2. If referringScript is not null, then:
59            // Set settingsObject to referringScript's settings object.
60            // Set baseURL to referringScript's base URL.
61            Some(referring_script) => (
62                &*referring_script.settings_object,
63                &referring_script.script.base_url,
64            ),
65            // Step 3. Otherwise:
66            // Set settingsObject to the current settings object.
67            // Set baseURL to settingsObject's API base URL.
68            None => (
69                current_settings_object,
70                &current_settings_object.api_base_url(),
71            ),
72        };
73
74        // Step 4. Let importMap be an empty import map.
75        // Step 5. If settingsObject's global object implements Window, then set importMap to settingsObject's
76        // global object's import map.
77        let import_map = if settings_object.is::<Window>() {
78            Some(settings_object.import_map())
79        } else {
80            None
81        };
82
83        // Step 6. Let serializedBaseURL be baseURL, serialized.
84        let serialized_base_url = base_url.as_str();
85        // Step 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL.
86        let as_url = resolve_url_like_module_specifier(&specifier, base_url);
87        // Step 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null;
88        // otherwise, specifier.
89        let normalized_specifier = match &as_url {
90            Some(url) => url.as_str(),
91            None => &specifier,
92        };
93
94        // Step 9. Let result be a URL-or-null, initially null.
95        let mut result = None;
96        if let Some(map) = import_map {
97            // Step 10. For each scopePrefix → scopeImports of importMap's scopes:
98            for (prefix, imports) in &map.scopes {
99                // Step 10.1 If scopePrefix is serializedBaseURL, or if scopePrefix ends with U+002F (/)
100                // and scopePrefix is a code unit prefix of serializedBaseURL, then:
101                let prefix = prefix.as_str();
102                if prefix == serialized_base_url ||
103                    (serialized_base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
104                {
105                    // Step 10.1.1 Let scopeImportsMatch be the result of resolving an imports match
106                    // given normalizedSpecifier, asURL, and scopeImports.
107                    let scope_imports_match =
108                        resolve_imports_match(normalized_specifier, as_url.as_ref(), imports)?;
109
110                    // Step 10.1.2 If scopeImportsMatch is not null, then set result to scopeImportsMatch, and break.
111                    if scope_imports_match.is_some() {
112                        result = scope_imports_match;
113                        break;
114                    }
115                }
116            }
117
118            // Step 11. If result is null, set result to the result of resolving an imports match given
119            // normalizedSpecifier, asURL, and importMap's imports.
120            if result.is_none() {
121                result =
122                    resolve_imports_match(normalized_specifier, as_url.as_ref(), &map.imports)?;
123            }
124        }
125
126        // Step 12. If result is null, set it to asURL.
127        if result.is_none() {
128            result = as_url.clone();
129        }
130
131        // Step 13. If result is not null, then:
132        match result {
133            Some(result) => {
134                // Step 13.1 Add module to resolved module set given settingsObject, serializedBaseURL,
135                // normalizedSpecifier, and asURL.
136                settings_object.add_module_to_resolved_module_set(
137                    serialized_base_url,
138                    normalized_specifier,
139                    as_url.clone(),
140                );
141                // Step 13.2 Return result.
142                Ok(result)
143            },
144            // Step 14. Throw a TypeError indicating that specifier was a bare specifier,
145            // but was not remapped to anything by importMap.
146            None => Err(Error::Type(
147                c"Specifier was a bare specifier, but was not remapped to anything by importMap."
148                    .to_owned(),
149            )),
150        }
151    }
152}
153
154/// <https://html.spec.whatwg.org/multipage/#register-an-import-map>
155pub(crate) fn register_import_map(
156    cx: &mut JSContext,
157    global: &GlobalScope,
158    result: Fallible<ImportMap>,
159) {
160    match result {
161        Ok(new_import_map) => {
162            // Step 2. Merge existing and new import maps, given global and result's import map.
163            merge_existing_and_new_import_maps(cx, global, new_import_map);
164        },
165        Err(exception) => {
166            let mut realm = enter_auto_realm(cx, global);
167            let cx = &mut realm.current_realm();
168
169            // Step 1. If result's error to rethrow is not null, then report
170            // an exception given by result's error to rethrow for global and return.
171            throw_dom_exception(cx, global, exception);
172            report_pending_exception(cx);
173        },
174    }
175}
176
177/// <https://html.spec.whatwg.org/multipage/#merge-existing-and-new-import-maps>
178fn merge_existing_and_new_import_maps(
179    cx: &mut JSContext,
180    global: &GlobalScope,
181    new_import_map: ImportMap,
182) {
183    // Step 1. Let newImportMapScopes be a deep copy of newImportMap's scopes.
184    let new_import_map_scopes = new_import_map.scopes;
185
186    // Step 2. Let oldImportMap be global's import map.
187    let mut old_import_map = global.import_map_mut();
188
189    // Step 3. Let newImportMapImports be a deep copy of newImportMap's imports.
190    let mut new_import_map_imports = new_import_map.imports;
191
192    let resolved_module_set = global.resolved_module_set();
193    // Step 4. For each scopePrefix → scopeImports of newImportMapScopes:
194    for (scope_prefix, mut scope_imports) in new_import_map_scopes {
195        // Step 4.1. For each record of global's resolved module set:
196        for record in resolved_module_set.iter() {
197            // If scopePrefix is record's serialized base URL, or if scopePrefix ends with
198            // U+002F (/) and scopePrefix is a code unit prefix of record's serialized base URL, then:
199            let prefix = scope_prefix.as_str();
200            if prefix == record.base_url ||
201                (record.base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
202            {
203                // For each specifierKey → resolutionResult of scopeImports:
204                scope_imports.retain(|key, val| {
205                    // If specifierKey is record's specifier, or if all of the following conditions are true:
206                    // specifierKey ends with U+002F (/);
207                    // specifierKey is a code unit prefix of record's specifier;
208                    // either record's specifier as a URL is null or is special,
209                    if *key == record.specifier ||
210                        (key.ends_with('\u{002f}') &&
211                            record.specifier.starts_with(key) &&
212                            (record.specifier_url.is_none() ||
213                                record
214                                    .specifier_url
215                                    .as_ref()
216                                    .is_some_and(|u| u.is_special_scheme())))
217                    {
218                        // The user agent may report a warning to the console indicating the ignored rule.
219                        // They may choose to avoid reporting if the rule is identical to an existing one.
220                        Console::internal_warn(
221                            cx,
222                            global,
223                            format!("Ignored rule: {key} -> {val:?}."),
224                        );
225                        // Remove scopeImports[specifierKey].
226                        false
227                    } else {
228                        true
229                    }
230                })
231            }
232        }
233
234        // Step 4.2 If scopePrefix exists in oldImportMap's scopes
235        if old_import_map.scopes.contains_key(&scope_prefix) {
236            // set oldImportMap's scopes[scopePrefix] to the result of
237            // merging module specifier maps, given scopeImports and oldImportMap's scopes[scopePrefix].
238            let merged_module_specifier_map = merge_module_specifier_maps(
239                cx,
240                global,
241                scope_imports,
242                &old_import_map.scopes[&scope_prefix],
243            );
244            old_import_map
245                .scopes
246                .insert(scope_prefix, merged_module_specifier_map);
247        } else {
248            // Step 4.3 Otherwise, set oldImportMap's scopes[scopePrefix] to scopeImports.
249            old_import_map.scopes.insert(scope_prefix, scope_imports);
250        }
251    }
252
253    // Step 5. For each url → integrity of newImportMap's integrity:
254    for (url, integrity) in &new_import_map.integrity {
255        // Step 5.1 If url exists in oldImportMap's integrity, then:
256        if old_import_map.integrity.contains_key(url) {
257            // Step 5.1.1 The user agent may report a warning to the console indicating the ignored rule.
258            // They may choose to avoid reporting if the rule is identical to an existing one.
259            Console::internal_warn(cx, global, format!("Ignored rule: {url} -> {integrity}."));
260            // Step 5.1.2 Continue.
261            continue;
262        }
263
264        // Step 5.2 Set oldImportMap's integrity[url] to integrity.
265        old_import_map
266            .integrity
267            .insert(url.clone(), integrity.clone());
268    }
269
270    // Step 6. For each record of global's resolved module set:
271    for record in resolved_module_set.iter() {
272        // For each specifier → url of newImportMapImports:
273        new_import_map_imports.retain(|specifier, val| {
274            // If specifier starts with record's specifier, then:
275            //
276            // Note: Spec is wrong, we need to check if record's specifier starts with specifier
277            // See: https://github.com/whatwg/html/issues/11875
278            if record.specifier.starts_with(specifier) {
279                // The user agent may report a warning to the console indicating the ignored rule.
280                // They may choose to avoid reporting if the rule is identical to an existing one.
281                Console::internal_warn(
282                    cx,
283                    global,
284                    format!("Ignored rule: {specifier} -> {val:?}."),
285                );
286                // Remove newImportMapImports[specifier].
287                false
288            } else {
289                true
290            }
291        });
292    }
293
294    // Step 7. Set oldImportMap's imports to the result of merge module specifier maps,
295    // given newImportMapImports and oldImportMap's imports.
296    let merged_module_specifier_map =
297        merge_module_specifier_maps(cx, global, new_import_map_imports, &old_import_map.imports);
298    old_import_map.imports = merged_module_specifier_map;
299
300    // https://html.spec.whatwg.org/multipage/#the-resolution-algorithm
301    // Sort scopes to ensure entries are visited from most-specific to least-specific.
302    old_import_map
303        .scopes
304        .sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
305}
306
307/// <https://html.spec.whatwg.org/multipage/#merge-module-specifier-maps>
308fn merge_module_specifier_maps(
309    cx: &mut JSContext,
310    global: &GlobalScope,
311    new_map: ModuleSpecifierMap,
312    old_map: &ModuleSpecifierMap,
313) -> ModuleSpecifierMap {
314    // Step 1. Let mergedMap be a deep copy of oldMap.
315    let mut merged_map = old_map.clone();
316
317    // Step 2. For each specifier → url of newMap:
318    for (specifier, url) in new_map {
319        // Step 2.1 If specifier exists in oldMap, then:
320        if old_map.contains_key(&specifier) {
321            // Step 2.1.1 The user agent may report a warning to the console indicating the ignored rule.
322            // They may choose to avoid reporting if the rule is identical to an existing one.
323            Console::internal_warn(cx, global, format!("Ignored rule: {specifier} -> {url:?}."));
324
325            // Step 2.1.2 Continue.
326            continue;
327        }
328
329        // Step 2.2 Set mergedMap[specifier] to url.
330        merged_map.insert(specifier, url);
331    }
332
333    merged_map
334}
335
336/// <https://html.spec.whatwg.org/multipage/#parse-an-import-map-string>
337pub(crate) fn parse_an_import_map_string(
338    cx: &mut JSContext,
339    global: &GlobalScope,
340    input: &str,
341    base_url: ServoUrl,
342) -> Fallible<ImportMap> {
343    // Step 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
344    let parsed: JsonValue = serde_json::from_str(input)
345        .map_err(|_| Error::Type(c"The value needs to be a JSON object.".to_owned()))?;
346    // Step 2. If parsed is not an ordered map, then throw a TypeError indicating that the
347    // top-level value needs to be a JSON object.
348    let JsonValue::Object(mut parsed) = parsed else {
349        return Err(Error::Type(
350            c"The top-level value needs to be a JSON object.".to_owned(),
351        ));
352    };
353
354    // Step 3. Let sortedAndNormalizedImports be an empty ordered map.
355    let mut sorted_and_normalized_imports = ModuleSpecifierMap::new();
356    // Step 4. If parsed["imports"] exists, then:
357    if let Some(imports) = parsed.get("imports") {
358        // Step 4.1 If parsed["imports"] is not an ordered map, then throw a TypeError
359        // indicating that the value for the "imports" top-level key needs to be a JSON object.
360        let JsonValue::Object(imports) = imports else {
361            return Err(Error::Type(
362                c"The \"imports\" top-level value needs to be a JSON object.".to_owned(),
363            ));
364        };
365        // Step 4.2 Set sortedAndNormalizedImports to the result of sorting and
366        // normalizing a module specifier map given parsed["imports"] and baseURL.
367        sorted_and_normalized_imports =
368            sort_and_normalize_module_specifier_map(cx, global, imports, &base_url);
369    }
370
371    // Step 5. Let sortedAndNormalizedScopes be an empty ordered map.
372    let mut sorted_and_normalized_scopes: IndexMap<ServoUrl, ModuleSpecifierMap> = IndexMap::new();
373    // Step 6. If parsed["scopes"] exists, then:
374    if let Some(scopes) = parsed.get("scopes") {
375        // Step 6.1 If parsed["scopes"] is not an ordered map, then throw a TypeError
376        // indicating that the value for the "scopes" top-level key needs to be a JSON object.
377        let JsonValue::Object(scopes) = scopes else {
378            return Err(Error::Type(
379                c"The \"scopes\" top-level value needs to be a JSON object.".to_owned(),
380            ));
381        };
382        // Step 6.2 Set sortedAndNormalizedScopes to the result of sorting and
383        // normalizing scopes given parsed["scopes"] and baseURL.
384        sorted_and_normalized_scopes = sort_and_normalize_scopes(cx, global, scopes, &base_url)?;
385    }
386
387    // Step 7. Let normalizedIntegrity be an empty ordered map.
388    let mut normalized_integrity = ModuleIntegrityMap::new();
389    // Step 8. If parsed["integrity"] exists, then:
390    if let Some(integrity) = parsed.get("integrity") {
391        // Step 8.1 If parsed["integrity"] is not an ordered map, then throw a TypeError
392        // indicating that the value for the "integrity" top-level key needs to be a JSON object.
393        let JsonValue::Object(integrity) = integrity else {
394            return Err(Error::Type(
395                c"The \"integrity\" top-level value needs to be a JSON object.".to_owned(),
396            ));
397        };
398        // Step 8.2 Set normalizedIntegrity to the result of normalizing
399        // a module integrity map given parsed["integrity"] and baseURL.
400        normalized_integrity = normalize_module_integrity_map(cx, global, integrity, &base_url);
401    }
402
403    // Step 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity",
404    // then the user agent should report a warning to the console indicating that an invalid
405    // top-level key was present in the import map.
406    parsed.retain(|k, _| !matches!(k.as_str(), "imports" | "scopes" | "integrity"));
407    if !parsed.is_empty() {
408        Console::internal_warn(
409            cx,
410            global,
411            "Invalid top-level key was present in the import map.
412                Only \"imports\", \"scopes\", and \"integrity\" are allowed."
413                .to_string(),
414        );
415    }
416
417    // Step 10. Return an import map
418    Ok(ImportMap {
419        imports: sorted_and_normalized_imports,
420        scopes: sorted_and_normalized_scopes,
421        integrity: normalized_integrity,
422    })
423}
424
425/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-a-module-specifier-map>
426fn sort_and_normalize_module_specifier_map(
427    cx: &mut JSContext,
428    global: &GlobalScope,
429    original_map: &JsonMap<String, JsonValue>,
430    base_url: &ServoUrl,
431) -> ModuleSpecifierMap {
432    // Step 1. Let normalized be an empty ordered map.
433    let mut normalized = ModuleSpecifierMap::new();
434
435    // Step 2. For each specifier_key -> value in originalMap
436    for (specifier_key, value) in original_map {
437        // Step 2.1 Let normalized_specifier_key be the result of
438        // normalizing a specifier key given specifier_key and base_url.
439        let Some(normalized_specifier_key) =
440            normalize_specifier_key(cx, global, specifier_key, base_url)
441        else {
442            // Step 2.2 If normalized_specifier_key is null, then continue.
443            continue;
444        };
445
446        // Step 2.3 If value is not a string, then:
447        let JsonValue::String(value) = value else {
448            // Step 2.3.1 The user agent may report a warning to the console
449            // indicating that addresses need to be strings.
450            Console::internal_warn(cx, global, "Addresses need to be strings.".to_string());
451
452            // Step 2.3.2 Set normalized[normalized_specifier_key] to null.
453            normalized.insert(normalized_specifier_key, None);
454            // Step 2.3.3 Continue.
455            continue;
456        };
457
458        // Step 2.4. Let address_url be the result of resolving a URL-like module specifier given value and baseURL.
459        let Some(address_url) = resolve_url_like_module_specifier(value.as_str(), base_url) else {
460            // Step 2.5 If address_url is null, then:
461            // Step 2.5.1. The user agent may report a warning to the console
462            // indicating that the address was invalid.
463            Console::internal_warn(
464                cx,
465                global,
466                format!("Value failed to resolve to module specifier: {value}"),
467            );
468
469            // Step 2.5.2 Set normalized[normalized_specifier_key] to null.
470            normalized.insert(normalized_specifier_key, None);
471            // Step 2.5.3 Continue.
472            continue;
473        };
474
475        // Step 2.6 If specifier_key ends with U+002F (/), and the serialization of
476        // address_url does not end with U+002F (/), then:
477        if specifier_key.ends_with('\u{002f}') && !address_url.as_str().ends_with('\u{002f}') {
478            // step 2.6.1. The user agent may report a warning to the console
479            // indicating that an invalid address was given for the specifier key specifierKey;
480            // since specifierKey ends with a slash, the address needs to as well.
481            Console::internal_warn(
482                cx,
483                global,
484                format!(
485                    "Invalid address for specifier key '{specifier_key}': {address_url}.
486                    Since specifierKey ends with a slash, the address needs to as well."
487                ),
488            );
489
490            // Step 2.6.2 Set normalized[normalized_specifier_key] to null.
491            normalized.insert(normalized_specifier_key, None);
492            // Step 2.6.3 Continue.
493            continue;
494        }
495
496        // Step 2.7 Set normalized[normalized_specifier_key] to address_url.
497        normalized.insert(normalized_specifier_key, Some(address_url));
498    }
499
500    // Step 3. Return the result of sorting in descending order normalized
501    // with an entry a being less than an entry b if a's key is code unit less than b's key.
502    normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
503    normalized
504}
505
506/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-scopes>
507fn sort_and_normalize_scopes(
508    cx: &mut JSContext,
509    global: &GlobalScope,
510    original_map: &JsonMap<String, JsonValue>,
511    base_url: &ServoUrl,
512) -> Fallible<IndexMap<ServoUrl, ModuleSpecifierMap>> {
513    // Step 1. Let normalized be an empty ordered map.
514    let mut normalized: IndexMap<ServoUrl, ModuleSpecifierMap> = IndexMap::new();
515
516    // Step 2. For each scopePrefix → potentialSpecifierMap of originalMap:
517    for (scope_prefix, potential_specifier_map) in original_map {
518        // Step 2.1 If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating
519        // that the value of the scope with prefix scopePrefix needs to be a JSON object.
520        let JsonValue::Object(potential_specifier_map) = potential_specifier_map else {
521            return Err(Error::Type(
522                c"The value of the scope with prefix scopePrefix needs to be a JSON object."
523                    .to_owned(),
524            ));
525        };
526
527        // Step 2.2 Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
528        let Ok(scope_prefix_url) = ServoUrl::parse_with_base(Some(base_url), scope_prefix) else {
529            // Step 2.3 If scopePrefixURL is failure, then:
530            // Step 2.3.1 The user agent may report a warning
531            // to the console that the scope prefix URL was not parseable.
532            Console::internal_warn(
533                cx,
534                global,
535                format!("Scope prefix URL was not parseable: {scope_prefix}"),
536            );
537            // Step 2.3.2 Continue.
538            continue;
539        };
540
541        // Step 2.4 Let normalizedScopePrefix be the serialization of scopePrefixURL.
542        let normalized_scope_prefix = scope_prefix_url;
543
544        // Step 2.5 Set normalized[normalizedScopePrefix] to the result of sorting and
545        // normalizing a module specifier map given potentialSpecifierMap and baseURL.
546        let normalized_specifier_map =
547            sort_and_normalize_module_specifier_map(cx, global, potential_specifier_map, base_url);
548        normalized.insert(normalized_scope_prefix, normalized_specifier_map);
549    }
550
551    // Step 3. Return the result of sorting in descending order normalized,
552    // with an entry a being less than an entry b if a's key is code unit less than b's key.
553    normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
554    Ok(normalized)
555}
556
557/// <https://html.spec.whatwg.org/multipage/#normalizing-a-module-integrity-map>
558fn normalize_module_integrity_map(
559    cx: &mut JSContext,
560    global: &GlobalScope,
561    original_map: &JsonMap<String, JsonValue>,
562    base_url: &ServoUrl,
563) -> ModuleIntegrityMap {
564    // Step 1. Let normalized be an empty ordered map.
565    let mut normalized = ModuleIntegrityMap::new();
566
567    // Step 2. For each key → value of originalMap:
568    for (key, value) in original_map {
569        // Step 2.1 Let resolvedURL be the result of
570        // resolving a URL-like module specifier given key and baseURL.
571        let Some(resolved_url) = resolve_url_like_module_specifier(key.as_str(), base_url) else {
572            // Step 2.2 If resolvedURL is null, then:
573            // Step 2.2.1 The user agent may report a warning
574            // to the console indicating that the key failed to resolve.
575            Console::internal_warn(
576                cx,
577                global,
578                format!("Key failed to resolve to module specifier: {key}"),
579            );
580            // Step 2.2.2 Continue.
581            continue;
582        };
583
584        // Step 2.3 If value is not a string, then:
585        let JsonValue::String(value) = value else {
586            // Step 2.3.1 The user agent may report a warning
587            // to the console indicating that integrity metadata values need to be strings.
588            Console::internal_warn(
589                cx,
590                global,
591                "Integrity metadata values need to be strings.".to_string(),
592            );
593            // Step 2.3.2 Continue.
594            continue;
595        };
596
597        // Step 2.4 Set normalized[resolvedURL] to value.
598        normalized.insert(resolved_url, value.clone());
599    }
600
601    // Step 3. Return normalized.
602    normalized
603}
604
605/// <https://html.spec.whatwg.org/multipage/#normalizing-a-specifier-key>
606fn normalize_specifier_key(
607    cx: &mut JSContext,
608    global: &GlobalScope,
609    specifier_key: &str,
610    base_url: &ServoUrl,
611) -> Option<String> {
612    // step 1. If specifierKey is the empty string, then:
613    if specifier_key.is_empty() {
614        // step 1.1 The user agent may report a warning to the console
615        // indicating that specifier keys may not be the empty string.
616        Console::internal_warn(
617            cx,
618            global,
619            "Specifier keys may not be the empty string.".to_string(),
620        );
621        // step 1.2 Return null.
622        return None;
623    }
624    // step 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
625    let url = resolve_url_like_module_specifier(specifier_key, base_url);
626
627    // step 3. If url is not null, then return the serialization of url.
628    if let Some(url) = url {
629        return Some(url.into_string());
630    }
631
632    // step 4. Return specifierKey.
633    Some(specifier_key.to_string())
634}
635
636/// <https://html.spec.whatwg.org/multipage/#resolving-a-url-like-module-specifier>
637fn resolve_url_like_module_specifier(specifier: &str, base_url: &ServoUrl) -> Option<ServoUrl> {
638    // Step 1. If specifier starts with "/", "./", or "../", then:
639    if specifier.starts_with('/') || specifier.starts_with("./") || specifier.starts_with("../") {
640        // Step 1.1. Let url be the result of URL parsing specifier with baseURL.
641        return ServoUrl::parse_with_base(Some(base_url), specifier).ok();
642    }
643    // Step 2. Let url be the result of URL parsing specifier (with no base URL).
644    ServoUrl::parse(specifier).ok()
645}
646
647/// <https://html.spec.whatwg.org/multipage/#resolving-an-imports-match>
648///
649/// When the error is thrown, it will terminate the entire resolve a module specifier algorithm
650/// without any further fallbacks.
651fn resolve_imports_match(
652    normalized_specifier: &str,
653    as_url: Option<&ServoUrl>,
654    specifier_map: &ModuleSpecifierMap,
655) -> Fallible<Option<ServoUrl>> {
656    // Step 1. For each specifierKey → resolutionResult of specifierMap:
657    for (specifier_key, resolution_result) in specifier_map {
658        // Step 1.1 If specifierKey is normalizedSpecifier, then:
659        if specifier_key == normalized_specifier {
660            if let Some(resolution_result) = resolution_result {
661                // Step 1.1.2 Assert: resolutionResult is a URL.
662                // This is checked by Url type already.
663                // Step 1.1.3 Return resolutionResult.
664                return Ok(Some(resolution_result.clone()));
665            } else {
666                // Step 1.1.1 If resolutionResult is null, then throw a TypeError.
667                return Err(Error::Type(
668                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
669                ));
670            }
671        }
672
673        // Step 1.2 If all of the following are true:
674        // - specifierKey ends with U+002F (/)
675        // - specifierKey is a code unit prefix of normalizedSpecifier
676        // - either asURL is null, or asURL is special, then:
677        if specifier_key.ends_with('\u{002f}') &&
678            normalized_specifier.starts_with(specifier_key) &&
679            (as_url.is_none() || as_url.is_some_and(|u| u.is_special_scheme()))
680        {
681            // Step 1.2.1 If resolutionResult is null, then throw a TypeError.
682            // Step 1.2.2 Assert: resolutionResult is a URL.
683            let Some(resolution_result) = resolution_result else {
684                return Err(Error::Type(
685                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
686                ));
687            };
688
689            // Step 1.2.3 Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix.
690            let after_prefix = normalized_specifier
691                .strip_prefix(specifier_key)
692                .expect("specifier_key should be the prefix of normalized_specifier");
693
694            // Step 1.2.4 Assert: resolutionResult, serialized, ends with U+002F (/), as enforced during parsing.
695            debug_assert!(resolution_result.as_str().ends_with('\u{002f}'));
696
697            // Step 1.2.5 Let url be the result of URL parsing afterPrefix with resolutionResult.
698            let url = ServoUrl::parse_with_base(Some(resolution_result), after_prefix);
699
700            // Step 1.2.6 If url is failure, then throw a TypeError
701            // Step 1.2.7 Assert: url is a URL.
702            let Ok(url) = url else {
703                return Err(Error::Type(
704                    c"Resolution of normalizedSpecifier was blocked since
705                    the afterPrefix portion could not be URL-parsed relative to
706                    the resolutionResult mapped to by the specifierKey prefix."
707                        .to_owned(),
708                ));
709            };
710
711            // Step 1.2.8 If the serialization of resolutionResult is not
712            // a code unit prefix of the serialization of url, then throw a TypeError
713            if !url.as_str().starts_with(resolution_result.as_str()) {
714                return Err(Error::Type(
715                    c"Resolution of normalizedSpecifier was blocked due to
716                    it backtracking above its prefix specifierKey."
717                        .to_owned(),
718                ));
719            }
720
721            // Step 1.2.9 Return url.
722            return Ok(Some(url));
723        }
724    }
725
726    // Step 2. Return null.
727    Ok(None)
728}