script: Clean up prepare the script element related code and remove ModuleOwner (#44179)

Drop the `ModuleOwner` logic in favour of closures passed down by the
script fetching initiator.
When processing inline module scripts a task in now queued on the
networking task source. Since `Rc<ModuleTree>` is not `Send`, a `result`
field is now introduced to `HTMLScriptElement`, which is initialized
before queueing the task.

This slightly improves `inline-async-inserted-execorder.html`, which now
fails at the fourth assertion instead of stopping at the second one (the
inline module script with no dependencies still resolves after the one
that has a parse error).

Testing: Covered by existing tests.

---------

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
This commit is contained in:
Gae24
2026-04-18 17:58:33 +02:00
committed by GitHub
parent 87819c5f00
commit 2b21c528ea
4 changed files with 189 additions and 266 deletions

View File

@@ -63,7 +63,7 @@ use crate::dom::window::Window;
use crate::fetch::{RequestWithGlobalScope, create_a_potential_cors_request};
use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
use crate::script_module::{
ImportMap, ModuleOwner, ModuleTree, ScriptFetchOptions, fetch_an_external_module_script,
ImportMap, ModuleTree, ScriptFetchOptions, fetch_an_external_module_script,
fetch_inline_module_script, parse_an_import_map_string, register_import_map,
};
use crate::script_runtime::{CanGc, IntroductionType};
@@ -117,6 +117,9 @@ pub(crate) struct HTMLScriptElement {
/// Used to keep track whether we consider this script element render blocking during
/// `prepare`
marked_as_render_blocking: Cell<bool>,
/// <https://html.spec.whatwg.org/multipage/#concept-script-result>
result: DomRefCell<Option<ScriptResult>>,
}
impl HTMLScriptElement {
@@ -140,6 +143,7 @@ impl HTMLScriptElement {
from_an_external_file: Cell::new(false),
blocking: Default::default(),
marked_as_render_blocking: Default::default(),
result: DomRefCell::new(None),
}
}
@@ -167,18 +171,13 @@ impl HTMLScriptElement {
/// Marks that element as delaying the load event or not.
///
/// Nothing happens if the element was already delaying the load event and
/// we pass true to that method again.
///
/// <https://html.spec.whatwg.org/multipage/#concept-script-delay-load>
/// <https://html.spec.whatwg.org/multipage/#delaying-the-load-event-flag>
pub(crate) fn delay_load_event(&self, url: ServoUrl) {
let document = self.get_script_active_document();
fn delay_load_event(&self, document: &Document, url: ServoUrl) {
debug_assert!(self.delaying_the_load_event.borrow().is_none());
let blocker = &self.delaying_the_load_event;
if blocker.borrow().is_none() {
*blocker.borrow_mut() = Some(LoadBlocker::new(&document, LoadType::Script(url)));
}
*self.delaying_the_load_event.borrow_mut() =
Some(LoadBlocker::new(document, LoadType::Script(url)));
}
/// Helper method to determine the script kind based on attributes and insertion context.
@@ -187,39 +186,23 @@ impl HTMLScriptElement {
/// script's active document without full preparation.
///
/// <https://html.spec.whatwg.org/multipage/#prepare-the-script-element>
pub(crate) fn get_script_kind(&self) -> ExternalScriptKind {
fn get_script_kind(&self, script_type: ScriptType) -> ExternalScriptKind {
let element = self.upcast::<Element>();
let was_parser_inserted = self.parser_inserted.get();
let asynch = element.has_attribute(&local_name!("async"));
let mut script_kind = ExternalScriptKind::Asap;
match self.get_script_type() {
Some(ScriptType::Classic) => {
if element.has_attribute(&local_name!("defer")) && was_parser_inserted && !asynch {
script_kind = ExternalScriptKind::Deferred
} else if was_parser_inserted && !asynch {
script_kind = ExternalScriptKind::ParsingBlocking
} else if !asynch && !self.non_blocking.get() {
script_kind = ExternalScriptKind::AsapInOrder
}
},
Some(ScriptType::Module) => {
if !asynch && was_parser_inserted {
script_kind = ExternalScriptKind::Deferred
} else if !asynch && !self.non_blocking.get() {
script_kind = ExternalScriptKind::AsapInOrder
}
},
Some(ScriptType::ImportMap) => (),
None => (),
if element.has_attribute(&local_name!("async")) || self.non_blocking.get() {
ExternalScriptKind::Asap
} else if !self.parser_inserted.get() {
ExternalScriptKind::AsapInOrder
} else if element.has_attribute(&local_name!("defer")) || script_type == ScriptType::Module
{
ExternalScriptKind::Deferred
} else {
ExternalScriptKind::ParsingBlocking
}
script_kind
}
/// <https://html.spec.whatwg.org/multipage/#prepare-the-script-element>
pub(crate) fn get_script_active_document(&self) -> DomRoot<Document> {
let script_kind = self.get_script_kind();
fn get_script_active_document(&self, script_kind: ExternalScriptKind) -> DomRoot<Document> {
match script_kind {
ExternalScriptKind::Asap => self.preparation_time_document.get().unwrap(),
ExternalScriptKind::AsapInOrder => self.preparation_time_document.get().unwrap(),
@@ -329,38 +312,35 @@ impl ScriptOrigin {
}
}
/// Final steps of <https://html.spec.whatwg.org/multipage/#prepare-the-script-element>
pub(crate) fn finish_fetching_a_script(
/// <https://html.spec.whatwg.org/multipage/#steps-to-run-when-the-result-is-ready>
fn finish_fetching_a_script(
elem: &HTMLScriptElement,
script_kind: ExternalScriptKind,
load: ScriptResult,
cx: &mut js::context::JSContext,
cx: &mut JSContext,
) {
// Step 33. The "steps to run when the result is ready" for each type of script in 33.2-33.5.
// of https://html.spec.whatwg.org/multipage/#prepare-the-script-element
let document;
let load = elem.result.take().expect("Result must be ready to proceed");
// Step 2. If el's steps to run when the result is ready are not null, then run them.
match script_kind {
ExternalScriptKind::Asap => {
document = elem.preparation_time_document.get().unwrap();
let document = elem.preparation_time_document.get().unwrap();
document.asap_script_loaded(cx, elem, load)
},
ExternalScriptKind::AsapInOrder => {
document = elem.preparation_time_document.get().unwrap();
let document = elem.preparation_time_document.get().unwrap();
document.asap_in_order_script_loaded(cx, elem, load)
},
ExternalScriptKind::Deferred => {
document = elem.parser_document.as_rooted();
let document = elem.parser_document.as_rooted();
document.deferred_script_loaded(cx, elem, load);
},
ExternalScriptKind::ParsingBlocking => {
document = elem.parser_document.as_rooted();
let document = elem.parser_document.as_rooted();
document.pending_parsing_blocking_script_loaded(elem, load, cx);
},
}
// <https://html.spec.whatwg.org/multipage/#steps-to-run-when-the-result-is-ready>
// Step 4
// Step 4. Set el's delaying the load event to false.
LoadBlocker::terminate(&elem.delaying_the_load_event, cx);
}
@@ -458,11 +438,14 @@ impl FetchResponseListener for ClassicContext {
response: Result<(), NetworkError>,
timing: ResourceFetchTiming,
) {
let elem = self.elem.root();
match (response.as_ref(), self.status.as_ref()) {
(Err(error), _) | (_, Err(error)) => {
error!("Fetching classic script failed {:?} ({})", error, self.url);
// Step 6, response is an error.
finish_fetching_a_script(&self.elem.root(), self.kind, Err(()), cx);
*elem.result.borrow_mut() = Some(Err(()));
finish_fetching_a_script(&elem, self.kind, cx);
// Resource timing is expected to be available before "error" or "load" events are fired.
network_listener::submit_timing(cx, &self, &response, &timing);
@@ -484,7 +467,6 @@ impl FetchResponseListener for ClassicContext {
// Step 5.5. Let sourceText be the result of decoding bodyBytes to Unicode, using encoding as the fallback encoding.
let (mut source_text, _, _) = encoding.decode(&self.data);
let elem = self.elem.root();
let global = elem.global();
if let Some(window) = global.downcast::<Window>() {
@@ -537,8 +519,8 @@ impl FetchResponseListener for ClassicContext {
.is_null());
}
} else {*/
let load = Script::Classic(script);
finish_fetching_a_script(&elem, self.kind, Ok(load), cx);
*elem.result.borrow_mut() = Some(Ok(Script::Classic(script)));
finish_fetching_a_script(&elem, self.kind, cx);
// }
network_listener::submit_timing(cx, &self, &response, &timing);
@@ -825,12 +807,12 @@ impl HTMLScriptElement {
// If the element has a nonce content attribute but is not nonceable strip the nonce to prevent injection attacks.
// Elements without a nonce content attribute (e.g. JS-created with .nonce = "abc")
// use the internal slot directly — the nonceable check only applies to parser-created elements.
let el = self.upcast::<Element>();
let cryptographic_nonce = if el.is_nonceable() || !el.has_attribute(&local_name!("nonce")) {
el.nonce_value().trim().to_owned()
} else {
String::new()
};
let cryptographic_nonce =
if element.is_nonceable() || !element.has_attribute(&local_name!("nonce")) {
element.nonce_value().trim().to_owned()
} else {
String::new()
};
// Step 25. If el has an integrity attribute, then let integrity metadata be that attribute's value.
// Otherwise, let integrity metadata be the empty string.
@@ -842,7 +824,7 @@ impl HTMLScriptElement {
};
// Step 26. Let referrer policy be the current state of el's referrerpolicy content attribute.
let referrer_policy = referrer_policy_for_element(self.upcast::<Element>());
let referrer_policy = referrer_policy_for_element(element);
// TODO: Step 27. Fetch priority.
@@ -865,12 +847,11 @@ impl HTMLScriptElement {
};
// Step 30. Let settings object be el's node document's relevant settings object.
// This is done by passing ModuleOwner in step 31.11 and step 32.2.
// What we actually need is global's import map eventually.
let base_url = doc.base_url();
let kind = self.get_script_kind();
let kind = self.get_script_kind(script_type);
let delayed_document = self.get_script_active_document(kind);
if let Some(src) = element.get_attribute(&local_name!("src")) {
// Step 31. If el has a src content attribute, then:
@@ -912,7 +893,7 @@ impl HTMLScriptElement {
}
// Step 31.8. Set el's delaying the load event to true.
self.delay_load_event(url.clone());
self.delay_load_event(&delayed_document, url.clone());
// Step 31.9. If el is currently render-blocking, then set options's render-blocking to true.
if self.marked_as_render_blocking.get() {
@@ -934,12 +915,20 @@ impl HTMLScriptElement {
.resolve_a_module_integrity_metadata(&url);
}
let script = DomRoot::from_ref(self);
// Step 31.11. Fetch an external module script graph.
fetch_an_external_module_script(
cx,
url,
ModuleOwner::Window(Trusted::new(self)),
global,
options,
move |cx, module_tree| {
let load = module_tree.map(Script::Module).ok_or(());
*script.result.borrow_mut() = Some(load);
finish_fetching_a_script(&script, kind, cx);
},
);
},
ScriptType::ImportMap => (),
@@ -982,11 +971,8 @@ impl HTMLScriptElement {
return;
},
ScriptType::Module => {
// Just to make sure we running in the correct document incase the script has been moved
let doc = self.get_script_active_document();
// Step 32.2.2.1 Set el's delaying the load event to true.
self.delay_load_event(base_url.clone());
self.delay_load_event(&delayed_document, base_url.clone());
// Step 32.2.2.2 If el is potentially render-blocking, then:
if self.potentially_render_blocking() &&
@@ -1000,32 +986,40 @@ impl HTMLScriptElement {
options.render_blocking = true;
}
match kind {
ExternalScriptKind::Deferred => doc.add_deferred_script(self),
ExternalScriptKind::ParsingBlocking => {},
ExternalScriptKind::AsapInOrder => doc.push_asap_in_order_script(self),
ExternalScriptKind::Asap => doc.add_asap_script(self),
}
let script = DomRoot::from_ref(self);
// Step 32.2.2.3 Fetch an inline module script graph, given source text, base
// URL, settings object, options, and with the following steps given result:
fetch_inline_module_script(
cx,
ModuleOwner::Window(Trusted::new(self)),
global,
text_rc,
base_url,
options,
self.line_number as u32,
introduction_type,
move |_, module_tree| {
let load = module_tree.map(Script::Module).ok_or(());
*script.result.borrow_mut() = Some(load);
let trusted = Trusted::new(&*script);
// Queue an element task on the networking task source given el to perform the following steps:
script
.owner_global()
.task_manager()
.networking_task_source()
.queue(task!(terminate_module_fetch: move |cx| {
// Mark as ready el given result.
finish_fetching_a_script(&trusted.root(), kind, cx);
}));
},
);
return;
},
ScriptType::ImportMap => {
// Step 32.1 Let result be the result of creating an import map
// parse result given source text and base URL.
let import_map_result = parse_an_import_map_string(
ModuleOwner::Window(Trusted::new(self)),
Rc::clone(&text_rc),
base_url.clone(),
);
let import_map_result =
parse_an_import_map_string(global, Rc::clone(&text_rc), base_url.clone());
let script = Script::ImportMap(ScriptOrigin::internal(
text_rc,
base_url,
@@ -1042,21 +1036,14 @@ impl HTMLScriptElement {
}
}
// Just to make sure we running in the correct document incase the script has been moved
let doc = self.get_script_active_document();
// Step 33.2/33.3/33.4/33.5, substeps 1-2. Add el to the corresponding script list.
match kind {
ExternalScriptKind::Deferred => doc.add_deferred_script(self),
ExternalScriptKind::Deferred => delayed_document.add_deferred_script(self),
ExternalScriptKind::ParsingBlocking => {
if Some(element.get_attribute(&local_name!("src"))).is_some() &&
script_type == ScriptType::Classic
{
doc.set_pending_parsing_blocking_script(self, None);
}
delayed_document.set_pending_parsing_blocking_script(self, None);
},
ExternalScriptKind::AsapInOrder => doc.push_asap_in_order_script(self),
ExternalScriptKind::Asap => doc.add_asap_script(self),
ExternalScriptKind::AsapInOrder => delayed_document.push_asap_in_order_script(self),
ExternalScriptKind::Asap => delayed_document.add_asap_script(self),
}
}
@@ -1545,7 +1532,7 @@ pub(crate) fn substitute_with_local_script(
}
#[derive(Clone, Copy)]
pub(crate) enum ExternalScriptKind {
enum ExternalScriptKind {
Deferred,
ParsingBlocking,
AsapInOrder,