mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 16:01:31 +00:00
fix(preload): preserve modulepreload scanner order
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user