fix(cssom): publish parser style imports before scripts

This commit is contained in:
ldm0
2026-09-27 06:43:19 +08:00
parent e768869de5
commit 5b1e92d600
4 changed files with 108 additions and 8 deletions
+15 -1
View File
@@ -18370,7 +18370,7 @@ document.body.setAttribute("data-range", [
let outcome = driver
.advance_parser_step(
&mut page_vm,
"<!doctype html><html><head><style>@import url('/style.css');</style><script>window.afterStyle = true;</script></head></html>",
"<!doctype html><html><head><style id='blocking-style'>@import url('/style.css');</style><script>window.afterStyle = true;</script></head></html>",
None,
)
.await
@@ -18380,6 +18380,20 @@ document.body.setAttribute("data-range", [
matches!(outcome, ParserStepAdvanceOutcome::BlockedOnStylesheet(_)),
"parser-created style import should gate parser-blocking script on live PageVm"
);
let style = page_vm
.vm()
.document_runtime
.dom_host()
.element_handle_by_id("blocking-style")
.expect("parser-created style owner");
assert_eq!(
page_vm
.vm()
.document_runtime
.pending_style_import_binding_for_test(style),
Some((1, true)),
"the parser-discovered import must bind its live stylesheet root before the script gate is released"
);
}));
}
@@ -63,6 +63,22 @@ impl ScriptVm {
.document_runtime
.note_discovered_document_owned_blocking_stylesheet_inputs(inputs.iter());
self.settle_stylesheet_link_clients(completed_stylesheet_clients);
if inputs.iter().any(|input| {
matches!(
input.signature(),
crate::stylesheet_blocking::DocumentBlockingStylesheetSignature::ParserCreatedStyleImport { .. }
)
}) {
// The import graph owns the parser gate, while the connected
// style operation owns its CSSOM installation. Bind both views to
// the same live root before an already-fast graph can release the
// parser-blocking script.
self.sync_live_document_style_sources();
let prepared = self
.document_runtime
.prepare_parser_discovered_style_import_loads(inputs);
self.commit_and_apply_connected_style_loads(prepared);
}
inputs.len()
}
@@ -9,9 +9,9 @@ use crate::module_runtime::{
use crate::planning::{ScriptFetchMetadata, module_script_credentials_mode};
use crate::service_worker_runtime::ServiceWorkerRequestDestination;
use crate::stylesheet_blocking::{
StylesheetFetchOptions, connected_preload_like_link_url,
document_owned_blocking_stylesheet_candidate_for_node, link_rel_includes_token,
preload_like_link_loads_stylesheet, stylesheet_link_disposition,
DocumentOwnedBlockingStylesheetDiscoveryInput, StylesheetFetchOptions,
connected_preload_like_link_url, document_owned_blocking_stylesheet_candidate_for_node,
link_rel_includes_token, preload_like_link_loads_stylesheet, stylesheet_link_disposition,
stylesheet_preload_link_request,
};
use crate::types::{AsyncSubresourceFetchResponseFilter, SubresourceResourceType};
@@ -238,6 +238,24 @@ impl DocumentRuntime {
prepared
}
pub(crate) fn prepare_parser_discovered_style_import_loads(
&mut self,
inputs: &[DocumentOwnedBlockingStylesheetDiscoveryInput],
) -> Vec<PreparedConnectedStyleLoad> {
inputs
.iter()
.filter(|input| {
matches!(
input.signature(),
DocumentBlockingStylesheetSignature::ParserCreatedStyleImport { .. }
)
})
.flat_map(|input| {
self.prepare_connected_style_loads(DomHandle::new(input.node_id().index()), true)
})
.collect()
}
#[cfg(test)]
pub(crate) fn queue_initial_connected_style_loads(&mut self) {
let prepared = self.prepare_initial_connected_style_loads();
@@ -580,6 +598,21 @@ impl DocumentRuntime {
self.connected_style_load_is_queued(handle)
}
#[cfg(test)]
pub(crate) fn pending_style_import_binding_for_test(
&self,
handle: DomHandle,
) -> Option<(usize, bool)> {
let operation = self
.stylesheet_lifecycle
.owner_states
.pending_operation(handle)?;
let ConnectedLoadParameters::StyleImports { roots, .. } = &operation.parameters else {
return None;
};
Some((roots.len(), operation.blocking_operation.is_some()))
}
pub(super) fn push_ready_connected_style_load(&mut self, ready: ReadyConnectedStyleLoad) {
let producer = self
.stylesheet_lifecycle
@@ -671,11 +704,17 @@ impl DocumentRuntime {
if !node.flags().parser_created() || !node.is_html_element_named("style") {
return false;
}
document_owned_blocking_stylesheet_candidate_for_node(
let Some(candidate) = document_owned_blocking_stylesheet_candidate_for_node(
&self.dom_host,
NodeId::new(handle.index()),
)
.is_some()
) else {
return false;
};
let signature = DocumentBlockingStylesheetSignature::from_candidate(&candidate);
self.stylesheet_lifecycle
.fetches
.blocking_operation(NodeId::new(handle.index()), &signature)
.is_none()
}
pub(crate) fn has_pending_style_loads(&self) -> bool {
+32 -1
View File
@@ -691,7 +691,7 @@ fn parser_created_style_import_urls(
if !element.is_html_element("style") || !element.parser_blocking_eligible {
return None;
}
if element.disabled || !media_blocks_scripts(element.media.as_deref()) {
if !media_blocks_scripts(element.media.as_deref()) {
return None;
}
let css_text = document.text_content(native_node_id)?;
@@ -777,6 +777,37 @@ mod tests {
);
}
#[test]
fn style_disabled_content_attribute_does_not_suppress_parser_import_blocking() {
let document_url = url::Url::parse("https://example.com/path/page.html").unwrap();
let mut host = DomHost::from_dom(NativeDom::new_html(document_url));
let style = host.create_parser_element_without_attributes(
"style".to_owned(),
"http://www.w3.org/1999/xhtml".to_owned(),
None,
);
let text = host.create_text_node("@import url('theme.css');");
assert!(host.set_attribute(style, "disabled", ""));
assert!(host.append_child(style, text));
assert!(host.append_child(host.document_handle(), style));
let candidate = document_owned_blocking_stylesheet_candidate_for_node(
&host,
moli_dom::NodeId::new(style.index()),
)
.expect("the unsupported style content attribute must not disable import blocking");
let super::DocumentOwnedBlockingStylesheetCandidate::ParserCreatedStyleImport {
urls, ..
} = candidate
else {
panic!("expected a parser-created style import candidate");
};
assert_eq!(
urls,
[url::Url::parse("https://example.com/path/theme.css").unwrap()]
);
}
#[test]
fn hidden_nonce_remains_part_of_stylesheet_request_identity() {
let document_url = url::Url::parse("https://example.com/page.html").unwrap();