feat(script): create import map parse result (#37405)

Implement import map parser
Spec:
https://html.spec.whatwg.org/multipage/#create-an-import-map-parse-result

- Create an import map parse result
- Sorting and normalizing a module specifier map
- Sorting and normalizing scopes
- Normalizing a module integrity map

Testing: should pass current WPT
Fixes: part of https://github.com/servo/servo/issues/37316

---------

Signed-off-by: Jason Tsai <git@pews.dev>
Co-authored-by: Wu Yu Wei <yuweiwu@pm.me>
This commit is contained in:
Jason Tsai 2025-06-12 18:34:31 +09:00 committed by GitHub
parent f242c120b6
commit 41bed9840a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 338 additions and 4 deletions

View file

@ -76,6 +76,7 @@ use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingLi
use crate::realms::enter_realm;
use crate::script_module::{
ModuleOwner, ScriptFetchOptions, fetch_external_module_script, fetch_inline_module_script,
parse_an_import_map_string,
};
use crate::script_runtime::CanGc;
use crate::task_source::{SendableTaskSource, TaskSourceName};
@ -967,8 +968,14 @@ impl HTMLScriptElement {
);
},
ScriptType::ImportMap => {
// TODO: Let result be the result of creating an import map
// Step 32.1 Let result be the result of creating an import map
// parse result given source text and base URL.
let _result = parse_an_import_map_string(
ModuleOwner::Window(Trusted::new(self)),
text_rc,
base_url.clone(),
can_gc,
);
},
}
}

View file

@ -16,7 +16,7 @@ use encoding_rs::UTF_8;
use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
use html5ever::local_name;
use hyper_serde::Serde;
use indexmap::IndexSet;
use indexmap::{IndexMap, IndexSet};
use js::jsapi::{
CompileModule1, ExceptionStackBehavior, FinishDynamicModuleImport, GetModuleRequestSpecifier,
GetModuleResolveHook, GetRequestedModuleSpecifier, GetRequestedModulesCount,
@ -42,6 +42,8 @@ use net_traits::{
FetchMetadata, FetchResponseListener, Metadata, NetworkError, ReferrerPolicy,
ResourceFetchTiming, ResourceTimingType,
};
use script_bindings::error::Fallible;
use serde_json::{Map as JsonMap, Value as JsonValue};
use servo_url::ServoUrl;
use uuid::Uuid;
@ -68,6 +70,7 @@ use crate::dom::node::NodeTraits;
use crate::dom::performanceresourcetiming::InitiatorType;
use crate::dom::promise::Promise;
use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
use crate::dom::types::Console;
use crate::dom::window::Window;
use crate::dom::worker::TrustedWorkerAddress;
use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingListener};
@ -678,9 +681,8 @@ impl ModuleTree {
// Step 1.1. Let url be the result of URL parsing specifier with baseURL.
return ServoUrl::parse_with_base(Some(base_url), specifier).ok();
}
// Step 2. Let url be the result of URL parsing specifier (with no base URL).
ServoUrl::parse_with_base(None, specifier).ok()
ServoUrl::parse(specifier).ok()
}
/// <https://html.spec.whatwg.org/multipage/#finding-the-first-parse-error>
@ -1861,3 +1863,328 @@ pub(crate) fn fetch_inline_module_script(
},
}
}
#[derive(Default, JSTraceable)]
pub(crate) struct ImportMap {
#[no_trace]
imports: IndexMap<String, Option<ServoUrl>>,
#[no_trace]
scopes: IndexMap<String, IndexMap<String, Option<ServoUrl>>>,
#[no_trace]
integrity: IndexMap<String, String>,
}
/// <https://html.spec.whatwg.org/multipage/#parse-an-import-map-string>
pub(crate) fn parse_an_import_map_string(
module_owner: ModuleOwner,
input: Rc<DOMString>,
base_url: ServoUrl,
can_gc: CanGc,
) -> Fallible<ImportMap> {
// Step 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
let parsed: JsonValue = serde_json::from_str(input.str())
.map_err(|_| Error::Type("The value needs to be a JSON object.".to_owned()))?;
// Step 2. If parsed is not an ordered map, then throw a TypeError indicating that the
// top-level value needs to be a JSON object.
let JsonValue::Object(mut parsed) = parsed else {
return Err(Error::Type(
"The top-level value needs to be a JSON object.".to_owned(),
));
};
// Step 3. Let sortedAndNormalizedImports be an empty ordered map.
let mut sorted_and_normalized_imports: IndexMap<String, Option<ServoUrl>> = IndexMap::new();
// Step 4. If parsed["imports"] exists, then:
if let Some(imports) = parsed.get("imports") {
// Step 4.1 If parsed["imports"] is not an ordered map, then throw a TypeError
// indicating that the value for the "imports" top-level key needs to be a JSON object.
let JsonValue::Object(imports) = imports else {
return Err(Error::Type(
"The \"imports\" top-level value needs to be a JSON object.".to_owned(),
));
};
// Step 4.2 Set sortedAndNormalizedImports to the result of sorting and
// normalizing a module specifier map given parsed["imports"] and baseURL.
sorted_and_normalized_imports = sort_and_normalize_module_specifier_map(
&module_owner.global(),
imports,
&base_url,
can_gc,
);
}
// Step 5. Let sortedAndNormalizedScopes be an empty ordered map.
let mut sorted_and_normalized_scopes: IndexMap<String, IndexMap<String, Option<ServoUrl>>> =
IndexMap::new();
// Step 6. If parsed["scopes"] exists, then:
if let Some(scopes) = parsed.get("scopes") {
// Step 6.1 If parsed["scopes"] is not an ordered map, then throw a TypeError
// indicating that the value for the "scopes" top-level key needs to be a JSON object.
let JsonValue::Object(scopes) = scopes else {
return Err(Error::Type(
"The \"scopes\" top-level value needs to be a JSON object.".to_owned(),
));
};
// Step 6.2 Set sortedAndNormalizedScopes to the result of sorting and
// normalizing scopes given parsed["scopes"] and baseURL.
sorted_and_normalized_scopes =
sort_and_normalize_scopes(&module_owner.global(), scopes, &base_url, can_gc)?;
}
// Step 7. Let normalizedIntegrity be an empty ordered map.
let mut normalized_integrity = IndexMap::new();
// Step 8. If parsed["integrity"] exists, then:
if let Some(integrity) = parsed.get("integrity") {
// Step 8.1 If parsed["integrity"] is not an ordered map, then throw a TypeError
// indicating that the value for the "integrity" top-level key needs to be a JSON object.
let JsonValue::Object(integrity) = integrity else {
return Err(Error::Type(
"The \"integrity\" top-level value needs to be a JSON object.".to_owned(),
));
};
// Step 8.2 Set normalizedIntegrity to the result of normalizing
// a module integrity map given parsed["integrity"] and baseURL.
normalized_integrity =
normalize_module_integrity_map(&module_owner.global(), integrity, &base_url, can_gc);
}
// Step 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity",
// then the user agent should report a warning to the console indicating that an invalid
// top-level key was present in the import map.
parsed.retain(|k, _| !matches!(k.as_str(), "imports" | "scopes" | "integrity"));
if !parsed.is_empty() {
Console::internal_warn(
&module_owner.global(),
DOMString::from(
"Invalid top-level key was present in the import map.
Only \"imports\", \"scopes\", and \"integrity\" are allowed.",
),
);
}
// Step 10. Return an import map
Ok(ImportMap {
imports: sorted_and_normalized_imports,
scopes: sorted_and_normalized_scopes,
integrity: normalized_integrity,
})
}
/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-a-module-specifier-map>
#[allow(unsafe_code)]
fn sort_and_normalize_module_specifier_map(
global: &GlobalScope,
original_map: &JsonMap<String, JsonValue>,
base_url: &ServoUrl,
can_gc: CanGc,
) -> IndexMap<String, Option<ServoUrl>> {
// Step 1. Let normalized be an empty ordered map.
let mut normalized: IndexMap<String, Option<ServoUrl>> = IndexMap::new();
// Step 2. For each specifier_key -> value in originalMap
for (specifier_key, value) in original_map {
// Step 2.1 Let normalized_specifier_key be the result of
// normalizing a specifier key given specifier_key and base_url.
let Some(normalized_specifier_key) =
normalize_specifier_key(global, specifier_key, base_url, can_gc)
else {
// Step 2.2 If normalized_specifier_key is null, then continue.
continue;
};
// Step 2.3 If value is not a string, then:
let JsonValue::String(value) = value else {
// Step 2.3.1 The user agent may report a warning to the console
// indicating that addresses need to be strings.
Console::internal_warn(global, DOMString::from("Addresses need to be strings."));
// Step 2.3.2 Set normalized[normalized_specifier_key] to null.
normalized.insert(normalized_specifier_key, None);
// Step 2.3.3 Continue.
continue;
};
// Step 2.4. Let address_url be the result of resolving a URL-like module specifier given value and baseURL.
let value = DOMString::from(value.as_str());
let Some(address_url) = ModuleTree::resolve_url_like_module_specifier(&value, base_url)
else {
// Step 2.5 If address_url is null, then:
// Step 2.5.1. The user agent may report a warning to the console
// indicating that the address was invalid.
Console::internal_warn(
global,
DOMString::from(format!(
"Value failed to resolve to module specifier: {value}"
)),
);
// Step 2.5.2 Set normalized[normalized_specifier_key] to null.
normalized.insert(normalized_specifier_key, None);
// Step 2.5.3 Continue.
continue;
};
// Step 2.6 If specifier_key ends with U+002F (/), and the serialization of
// address_url does not end with U+002F (/), then:
if specifier_key.ends_with('\u{002f}') && !address_url.as_str().ends_with('\u{002f}') {
// step 2.6.1. The user agent may report a warning to the console
// indicating that an invalid address was given for the specifier key specifierKey;
// since specifierKey ends with a slash, the address needs to as well.
Console::internal_warn(
global,
DOMString::from(format!(
"Invalid address for specifier key '{specifier_key}': {address_url}.
Since specifierKey ends with a slash, the address needs to as well."
)),
);
// Step 2.6.2 Set normalized[normalized_specifier_key] to null.
normalized.insert(normalized_specifier_key, None);
// Step 2.6.3 Continue.
continue;
}
// Step 2.7 Set normalized[normalized_specifier_key] to address_url.
normalized.insert(normalized_specifier_key, Some(address_url));
}
// Step 3. Return the result of sorting in descending order normalized
// with an entry a being less than an entry b if a's key is code unit less than b's key.
normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
normalized
}
/// <https://html.spec.whatwg.org/multipage/#sorting-and-normalizing-scopes>
fn sort_and_normalize_scopes(
global: &GlobalScope,
original_map: &JsonMap<String, JsonValue>,
base_url: &ServoUrl,
can_gc: CanGc,
) -> Fallible<IndexMap<String, IndexMap<String, Option<ServoUrl>>>> {
// Step 1. Let normalized be an empty ordered map.
let mut normalized: IndexMap<String, IndexMap<String, Option<ServoUrl>>> = IndexMap::new();
// Step 2. For each scopePrefix → potentialSpecifierMap of originalMap:
for (scope_prefix, potential_specifier_map) in original_map {
// Step 2.1 If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating
// that the value of the scope with prefix scopePrefix needs to be a JSON object.
let JsonValue::Object(potential_specifier_map) = potential_specifier_map else {
return Err(Error::Type(
"The value of the scope with prefix scopePrefix needs to be a JSON object."
.to_owned(),
));
};
// Step 2.2 Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
let Ok(scope_prefix_url) = ServoUrl::parse_with_base(Some(base_url), scope_prefix) else {
// Step 2.3 If scopePrefixURL is failure, then:
// Step 2.3.1 The user agent may report a warning
// to the console that the scope prefix URL was not parseable.
Console::internal_warn(
global,
DOMString::from(format!(
"Scope prefix URL was not parseable: {scope_prefix}"
)),
);
// Step 2.3.2 Continue.
continue;
};
// Step 2.4 Let normalizedScopePrefix be the serialization of scopePrefixURL.
let normalized_scope_prefix = scope_prefix_url.into_string();
// Step 2.5 Set normalized[normalizedScopePrefix] to the result of sorting and
// normalizing a module specifier map given potentialSpecifierMap and baseURL.
let normalized_specifier_map = sort_and_normalize_module_specifier_map(
global,
potential_specifier_map,
base_url,
can_gc,
);
normalized.insert(normalized_scope_prefix, normalized_specifier_map);
}
// Step 3. Return the result of sorting in descending order normalized,
// with an entry a being less than an entry b if a's key is code unit less than b's key.
normalized.sort_by(|a_key, _, b_key, _| b_key.cmp(a_key));
Ok(normalized)
}
/// <https://html.spec.whatwg.org/multipage/#normalizing-a-module-integrity-map>
fn normalize_module_integrity_map(
global: &GlobalScope,
original_map: &JsonMap<String, JsonValue>,
base_url: &ServoUrl,
_can_gc: CanGc,
) -> IndexMap<String, String> {
// Step 1. Let normalized be an empty ordered map.
let mut normalized: IndexMap<String, String> = IndexMap::new();
// Step 2. For each key → value of originalMap:
for (key, value) in original_map {
// Step 2.1 Let resolvedURL be the result of
// resolving a URL-like module specifier given key and baseURL.
let Some(resolved_url) =
ModuleTree::resolve_url_like_module_specifier(&DOMString::from(key.as_str()), base_url)
else {
// Step 2.2 If resolvedURL is null, then:
// Step 2.2.1 The user agent may report a warning
// to the console indicating that the key failed to resolve.
Console::internal_warn(
global,
DOMString::from(format!("Key failed to resolve to module specifier: {key}")),
);
// Step 2.2.2 Continue.
continue;
};
// Step 2.3 If value is not a string, then:
let JsonValue::String(value) = value else {
// Step 2.3.1 The user agent may report a warning
// to the console indicating that integrity metadata values need to be strings.
Console::internal_warn(
global,
DOMString::from("Integrity metadata values need to be strings."),
);
// Step 2.3.2 Continue.
continue;
};
// Step 2.4 Set normalized[resolvedURL] to value.
normalized.insert(resolved_url.into_string(), value.clone());
}
// Step 3. Return normalized.
normalized
}
/// <https://html.spec.whatwg.org/multipage/#normalizing-a-specifier-key>
fn normalize_specifier_key(
global: &GlobalScope,
specifier_key: &str,
base_url: &ServoUrl,
_can_gc: CanGc,
) -> Option<String> {
// step 1. If specifierKey is the empty string, then:
if specifier_key.is_empty() {
// step 1.1 The user agent may report a warning to the console
// indicating that specifier keys may not be the empty string.
Console::internal_warn(
global,
DOMString::from("Specifier keys may not be the empty string."),
);
// step 1.2 Return null.
return None;
}
// step 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
let url =
ModuleTree::resolve_url_like_module_specifier(&DOMString::from(specifier_key), base_url);
// step 3. If url is not null, then return the serialization of url.
if let Some(url) = url {
return Some(url.into_string());
}
// step 4. Return specifierKey.
Some(specifier_key.to_string())
}