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