diff --git a/moli-renderer-v8/src/modulepreload.rs b/moli-renderer-v8/src/modulepreload.rs index 494c7cf1c7..9df16b0de7 100644 --- a/moli-renderer-v8/src/modulepreload.rs +++ b/moli-renderer-v8/src/modulepreload.rs @@ -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 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() { diff --git a/moli-renderer-v8/src/runtime/phase_one/mod.rs b/moli-renderer-v8/src/runtime/phase_one/mod.rs index bfc5f2a0bb..aa1f6e3a25 100644 --- a/moli-renderer-v8/src/runtime/phase_one/mod.rs +++ b/moli-renderer-v8/src/runtime/phase_one/mod.rs @@ -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#" + "#, @@ -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#" + + + "#, + ); + + 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"); diff --git a/moli-renderer-v8/src/runtime/script_preloads.rs b/moli-renderer-v8/src/runtime/script_preloads.rs index b25ca01573..fff96fa2f3 100644 --- a/moli-renderer-v8/src/runtime/script_preloads.rs +++ b/moli-renderer-v8/src/runtime/script_preloads.rs @@ -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 }