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}