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