script: Add support for modulepreload link elements (#42964)

Add support to modulepreload link elements. Currently we only fetch the
root module, as I think it is needs to be discussed whether we should
always fetch module dependencies, or if it should be limited due to
network constraints.
Inside `bind_to_tree` I ended up calling
`fetch_and_process_modulepreload` inside a delayed task, since it would
cause a crash due to DOM not being in a stable state when firing an
event (queueing the event seems to also do the trick).

Remaining tests failures are due to not supporting CSS modules and
performance entries's `transfer_size` being 0.

Testing: Covered by existing tests

---------

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
This commit is contained in:
Gae24
2026-03-17 10:28:05 +01:00
committed by GitHub
parent b491d6ddbb
commit 9a8571efcc
17 changed files with 227 additions and 376 deletions

View File

@@ -74,8 +74,9 @@ use crate::dom::csp::{GlobalCspReporting, Violation};
use crate::dom::document::Document;
use crate::dom::element::Element;
use crate::dom::globalscope::GlobalScope;
use crate::dom::html::htmlscriptelement::{HTMLScriptElement, SCRIPT_JS_MIMES, Script};
use crate::dom::htmlscriptelement::substitute_with_local_script;
use crate::dom::html::htmlscriptelement::{
HTMLScriptElement, SCRIPT_JS_MIMES, Script, substitute_with_local_script,
};
use crate::dom::node::NodeTraits;
use crate::dom::performance::performanceresourcetiming::InitiatorType;
use crate::dom::promise::Promise;
@@ -641,9 +642,10 @@ impl ModuleOwner {
fn notify_owner_to_finish(&self, module_tree: Option<Rc<ModuleTree>>, can_gc: CanGc) {
match &self {
ModuleOwner::Worker(_) => unimplemented!(),
ModuleOwner::DynamicModule(_) => unimplemented!(),
ModuleOwner::DynamicModule(_) => {},
ModuleOwner::Window(script) => {
let document = script.root().owner_document();
let script = script.root();
let document = script.owner_document();
let load = match module_tree {
Some(module_tree) => Ok(Script::Module(module_tree)),
@@ -651,16 +653,15 @@ impl ModuleOwner {
};
let asynch = script
.root()
.upcast::<Element>()
.has_attribute(&local_name!("async"));
if !asynch && (*script.root()).get_parser_inserted() {
document.deferred_script_loaded(&script.root(), load, can_gc);
} else if !asynch && !(*script.root()).get_non_blocking() {
document.asap_in_order_script_loaded(&script.root(), load, can_gc);
if !asynch && script.get_parser_inserted() {
document.deferred_script_loaded(&script, load, can_gc);
} else if !asynch && !script.get_non_blocking() {
document.asap_in_order_script_loaded(&script, load, can_gc);
} else {
document.asap_script_loaded(&script.root(), load, can_gc);
document.asap_script_loaded(&script, load, can_gc);
};
},
}
@@ -738,7 +739,8 @@ impl FetchResponseListener for ModuleContext {
let global = self.owner.global();
let (url, module_type) = &self.module_request;
if let Some(window) = global.downcast::<Window>() {
if let ModuleOwner::Window(_) = self.owner {
let window = global.downcast::<Window>().unwrap();
window
.Document()
.finish_load(LoadType::Script(url.clone()), cx);
@@ -1187,6 +1189,52 @@ pub(crate) fn fetch_an_external_module_script(
);
}
/// <https://html.spec.whatwg.org/multipage/#fetch-a-modulepreload-module-script-graph>
pub(crate) fn fetch_a_modulepreload_module(
url: ServoUrl,
destination: Destination,
global: &GlobalScope,
options: ScriptFetchOptions,
on_complete: impl FnOnce(bool) + 'static,
) {
let referrer = global.get_referrer();
let owner = ModuleOwner::DynamicModule(Trusted::new(global));
// Note: There is a specification inconsistency, `fetch_a_single_module_script` doesn't allow
// fetching top level JSON/CSS module scripts, but should be possible when preloading.
let module_type = if let Destination::Json = destination {
Some(ModuleType::JSON)
} else {
None
};
// Step 1. Fetch a single module script given url, settingsObject, destination, options, settingsObject,
// "client", true, and with the following steps given result:
fetch_a_single_module_script(
url,
owner.clone(),
destination,
options,
referrer,
module_type,
true,
Some(IntroductionType::SRC_SCRIPT),
move |result| {
// Step 1. Run onComplete given result.
on_complete(result.is_none());
// Step 2. Assert: settingsObject's global object implements Window.
assert!(owner.global().is::<Window>());
// Step 3. If result is not null, optionally fetch the descendants of and link result
// given settingsObject, destination, and an empty algorithm.
if let Some(module) = result {
fetch_the_descendants_and_link_module_script(module, destination, owner);
}
},
);
}
/// <https://html.spec.whatwg.org/multipage/#fetch-an-inline-module-script-graph>
pub(crate) fn fetch_inline_module_script(
owner: ModuleOwner,