fix(preload): preserve modulepreload scanner order

This commit is contained in:
ldm0
2026-09-27 19:23:03 +08:00
parent 5f1bc268e3
commit 1e9b820ebf
4 changed files with 122 additions and 22 deletions
+12 -2
View File
@@ -64,7 +64,13 @@ pub(crate) fn modulepreload_href(element: &Element) -> Option<&str> {
}
pub(crate) fn modulepreload_as_state(element: &Element) -> ModulepreloadAsState {
match link_as_destination(element.attribute("as")) {
modulepreload_as_state_from_attribute(element.attribute("as"))
}
pub(crate) fn modulepreload_as_state_from_attribute(
as_attribute: Option<&str>,
) -> ModulepreloadAsState {
match link_as_destination(as_attribute) {
LinkAsDestination::None
| LinkAsDestination::Script
| LinkAsDestination::AudioWorklet
@@ -126,7 +132,11 @@ fn modulepreload_attributes_for_state(state: ModulepreloadAsState) -> Option<Mod
}
pub(crate) fn modulepreload_media_matches(element: &Element) -> bool {
let Some(media) = element.attribute("media").map(str::trim) else {
modulepreload_media_attribute_matches(element.attribute("media"))
}
pub(crate) fn modulepreload_media_attribute_matches(media: Option<&str>) -> bool {
let Some(media) = media.map(str::trim) else {
return true;
};
if media.is_empty() {
@@ -134,6 +134,7 @@ fn html_preload_scanner_stops_module_preloads_after_import_map() {
r#"
<script type="module" src="/before.mjs"></script>
<script type="importmap">{"integrity": {}}</script>
<link rel="modulepreload" href="/after-preload.mjs">
<script type="module" src="/after.mjs"></script>
<script src="/classic.js"></script>
"#,
@@ -1975,3 +1976,23 @@ fn buffered_preload_scanner_clears_nonnonceable_script_nonces() {
"speculative requests must use the same nonceability gate as parser execution"
);
}
#[test]
fn html_preload_scanner_preserves_modulepreload_before_module_script() {
let final_url = Url::parse("https://example.test/docs/page.html").expect("test url");
let requests = collect_preloadable_external_script_requests_from_html(
&final_url,
r#"
<link rel="modulepreload" href="/entry.mjs" integrity="sha384-invalid">
<script type="module" src="/entry.mjs"></script>
"#,
);
assert_eq!(requests.len(), 2);
assert_eq!(requests[0].url, requests[1].url);
assert_eq!(
requests[0].fetch_metadata.integrity.as_deref(),
Some("sha384-invalid")
);
assert_eq!(requests[1].fetch_metadata.integrity, None);
}
@@ -28,27 +28,7 @@ fn buffered_html_preload_scan_collects_future_external_scripts_without_importmap
]
);
}
#[test]
fn buffered_html_preload_scan_leaves_modulepreload_to_native_module_map() {
let final_url = Url::parse("https://example.test/docs/page.html").expect("test url");
let requests = collect_preloadable_external_script_requests_from_html(
&final_url,
r#"
<link rel="dns-prefetch modulepreload" href="/entry.mjs">
<link rel="MODULEPRELOAD" href="/entry.mjs">
<link rel="preload" as="script" href="/classic.js">
<link rel="modulepreload" as="style" href="/theme.css">
<link rel="modulepreload" href="/theme.css?version=1">
<link rel="modulepreload" href="data:text/javascript,export%20default%201">
"#,
);
assert_eq!(
requests,
Vec::new(),
"modulepreload must not enter the legacy script-text preload cache; the parser publishes exact link candidates to the native module map"
);
}
#[test]
fn incremental_html_preload_scanner_handles_split_script_tag_boundaries() {
let final_url = Url::parse("https://example.test/docs/page.html").expect("test url");
@@ -432,3 +412,35 @@ fn full_body_phase_one_parks_async_subresource_terminal_for_page_owner() {
full_body_phase_one_parks_async_subresource_terminal_for_page_owner_inner,
);
}
#[test]
fn buffered_html_preload_scan_routes_script_like_modulepreloads_to_native_module_map() {
let final_url = Url::parse("https://example.test/docs/page.html").expect("test url");
let requests = collect_preloadable_external_script_requests_from_html(
&final_url,
r#"
<link rel="dns-prefetch modulepreload" href="/entry.mjs">
<link rel="MODULEPRELOAD" href="/entry.mjs">
<link rel="preload" as="script" href="/classic.js">
<link rel="modulepreload" as="style" href="/theme.css">
<link rel="modulepreload" href="/theme.css?version=1">
<link rel="modulepreload" href="data:text/javascript,export%20default%201">
"#,
);
assert!(
requests
.iter()
.all(|request| request.kind_hint == crate::types::ScriptKind::Module),
"scanner modulepreloads must take the native module-map admission path"
);
assert_eq!(
preload_request_urls(requests),
vec![
Url::parse("https://example.test/entry.mjs").expect("entry module URL"),
Url::parse("https://example.test/theme.css?version=1")
.expect("default script-like module URL"),
],
"the scanner should dedupe script-like modulepreloads while leaving typed and data candidates to the parser"
);
}
@@ -15,6 +15,10 @@ use url::Url;
use moli_script::script_element_nonce_is_nonceable;
use parking_lot::Mutex;
use crate::modulepreload::{
ModulepreloadAsState, modulepreload_as_state_from_attribute,
modulepreload_media_attribute_matches, resolve_parser_network_resource_url,
};
use crate::network::ResourceRequestClient;
use crate::page_task_queue::RendererOwnerWakeSender;
use crate::parser::{PreparedScript, ScriptSource};
@@ -1410,6 +1414,58 @@ impl HtmlPreloadScannerSink {
}
}
fn maybe_collect_modulepreload(&self, tag: &Tag) {
if self.seen_import_map.get()
|| (matches!(self.meta_csp_mode, MetaCspScannerMode::StopAfterMeta)
&& self.seen_meta_csp_count.get() != 0)
{
return;
}
let Some(rel) = html_attr_value(&tag.attrs, "rel") else {
return;
};
if !link_rel_includes_token(&rel, "modulepreload")
|| modulepreload_as_state_from_attribute(html_attr_value(&tag.attrs, "as").as_deref())
!= ModulepreloadAsState::ScriptLike
|| !modulepreload_media_attribute_matches(
html_attr_value(&tag.attrs, "media").as_deref(),
)
{
return;
}
let Some(href) = html_attr_value(&tag.attrs, "href") else {
return;
};
let Some(url) = resolve_parser_network_resource_url(&self.base_url.borrow(), &href) else {
return;
};
let kind_hint = crate::types::ScriptKind::Module;
let mode_hint = crate::types::ScriptMode::ModuleInOrder;
let fetch_metadata = crate::planning::ScriptFetchMetadata::from_script_attributes(
html_attr_value(&tag.attrs, "crossorigin").as_deref(),
html_attr_value(&tag.attrs, "referrerpolicy").as_deref(),
None,
html_attr_value(&tag.attrs, "integrity").as_deref(),
html_attr_value(&tag.attrs, "nonce").as_deref(),
html_attr_value(&tag.attrs, "fetchpriority").as_deref(),
);
let Some(key) = BufferedScriptPreloadKey::new(url.clone(), kind_hint, &fetch_metadata)
else {
return;
};
let mut requests = self.requests.borrow_mut();
if requests.seen.insert(BufferedPreloadKey::Script(key)) {
requests.script_requests.push(BufferedScriptPreloadRequest {
url,
initiator_url: self.final_url.clone(),
kind_hint,
mode_hint,
resource_type_hint: moli_fetch::RequestResourceType::Script,
fetch_metadata,
});
}
}
fn maybe_update_base_url(&self, tag: &Tag) {
if self.has_valid_base.get() {
return;
@@ -1630,6 +1686,7 @@ impl TokenSink for HtmlPreloadScannerSink {
TokenSinkResult::RawData(ScriptData)
}
"link" => {
self.maybe_collect_modulepreload(&tag);
self.maybe_collect_stylesheet_preload(&tag);
TokenSinkResult::Continue
}