fix(preload): preserve modulepreload scanner order

This commit is contained in:
ldm0
2026-09-22 21:43:35 +08:00
parent bee651bb9f
commit 3008797eae
3 changed files with 104 additions and 6 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() {
+35 -4
View File
@@ -2662,7 +2662,7 @@ document.body.setAttribute('data-error-state', [
}
#[test]
fn buffered_html_preload_scan_leaves_modulepreload_to_native_module_map() {
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,
@@ -2676,10 +2676,20 @@ document.body.setAttribute('data-error-state', [
"#,
);
assert!(
requests
.iter()
.all(|request| request.kind_hint == crate::types::ScriptKind::Module),
"scanner modulepreloads must take the native module-map admission path"
);
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"
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"
);
}
@@ -2857,6 +2867,7 @@ document.body.setAttribute('data-error-state', [
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>
"#,
@@ -2871,6 +2882,26 @@ document.body.setAttribute('data-error-state', [
);
}
#[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);
}
#[test]
fn html_preload_scanner_remembers_import_map_across_chunks() {
let final_url = Url::parse("https://example.test/docs/page.html").expect("test url");
@@ -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
}