fix(import-maps): merge maps after module loading

This commit is contained in:
ldm0
2026-09-09 06:38:21 +08:00
parent 73ba82ecdd
commit bb0d047cab
18 changed files with 181 additions and 56 deletions
@@ -3704,8 +3704,6 @@ trusted-types/eval-function-constructor-untrusted-arguments-and-applying-default
trusted-types/inheriting-csp-for-local-schemes.html
trusted-types/modify-attributes-in-callback.html
trusted-types/require-trusted-types-for-TypeError-belongs-to-the-global-object-realm.html
trusted-types/script-enforcement-006.html
trusted-types/script-enforcement-007.html
trusted-types/script-enforcement-010.html
trusted-types/script-enforcement-011.html
trusted-types/set-attributes-require-trusted-types-default-policy.html
@@ -7869,6 +7869,8 @@ trusted-types/script-enforcement-002.html
trusted-types/script-enforcement-003.html
trusted-types/script-enforcement-004.html
trusted-types/script-enforcement-005.html
trusted-types/script-enforcement-006.html
trusted-types/script-enforcement-007.html
trusted-types/script-enforcement-012.html
trusted-types/script-enforcement-013.html
trusted-types/script-enforcement-014.html
@@ -2,9 +2,9 @@
<html>
<head>
<script>
window.dynamicAsyncImportMapErrors = 0;
window.addEventListener("error", () => {
window.dynamicAsyncImportMapErrors =
(window.dynamicAsyncImportMapErrors || 0) + 1;
window.dynamicAsyncImportMapErrors += 1;
});
const firstModule = document.createElement("script");
@@ -2,9 +2,9 @@
<html>
<body>
<script>
window.parserImportMapAfterDynamicModuleErrors = 0;
window.addEventListener("error", () => {
window.parserImportMapAfterDynamicModuleErrors =
(window.parserImportMapAfterDynamicModuleErrors || 0) + 1;
window.parserImportMapAfterDynamicModuleErrors += 1;
});
const dynamicModule = document.createElement('script');
@@ -13,9 +13,9 @@
window.parserModuleAcquisitionFirstGreeting = greeting;
</script>
<script>
window.parserModuleAcquisitionLateErrors = 0;
window.addEventListener("error", () => {
window.parserModuleAcquisitionLateErrors =
(window.parserModuleAcquisitionLateErrors || 0) + 1;
window.parserModuleAcquisitionLateErrors += 1;
});
const lateMap = document.createElement("script");
+9 -12
View File
@@ -3330,12 +3330,12 @@ async fn importmap_registered_after_module_load_adds_unresolved_mapping() -> Res
}
#[tokio::test(flavor = "multi_thread")]
async fn dynamic_async_module_does_not_close_multiple_import_map_registration() -> Result<()> {
async fn dynamic_async_module_allows_late_importmap() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let page = browser
.fetch(&server.url("/compat/dynamic-async-module-closes-importmap-acquisition"))
.fetch(&server.url("/compat/dynamic-async-module-allows-late-importmap"))
.await?;
assert_eq!(diagnostic_global(&page, "dynamicAsyncImportMapLoads"), None);
assert_eq!(
@@ -3344,7 +3344,7 @@ async fn dynamic_async_module_does_not_close_multiple_import_map_registration()
);
assert_eq!(
diagnostic_global(&page, "dynamicAsyncImportMapErrors"),
None
Some(&JsValueSnapshot::Number(0.0))
);
assert_eq!(
diagnostic_global(&page, "dynamicAsyncModuleReady"),
@@ -3357,14 +3357,12 @@ async fn dynamic_async_module_does_not_close_multiple_import_map_registration()
}
#[tokio::test(flavor = "multi_thread")]
async fn parser_owned_module_allows_late_dynamic_map_to_add_unresolved_mapping() -> Result<()> {
async fn parser_owned_module_allows_late_dynamic_importmap() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let page = browser
.fetch(
&server.url("/compat/importmap-closed-by-parser-owned-module-before-late-dynamic-map"),
)
.fetch(&server.url("/compat/parser-owned-module-allows-late-dynamic-importmap"))
.await?;
assert_eq!(
diagnostic_global(&page, "parserModuleAcquisitionFirstGreeting"),
@@ -3376,7 +3374,7 @@ async fn parser_owned_module_allows_late_dynamic_map_to_add_unresolved_mapping()
);
assert_eq!(
diagnostic_global(&page, "parserModuleAcquisitionLateErrors"),
None
Some(&JsValueSnapshot::Number(0.0))
);
assert_eq!(
diagnostic_global(&page, "parserModuleAcquisitionMutationDone"),
@@ -3388,13 +3386,12 @@ async fn parser_owned_module_allows_late_dynamic_map_to_add_unresolved_mapping()
}
#[tokio::test(flavor = "multi_thread")]
async fn parser_owned_importmap_after_dynamic_module_prepare_adds_unresolved_mapping() -> Result<()>
{
async fn parser_owned_importmap_applies_after_dynamic_module_prepare() -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let page = browser
.fetch(&server.url("/compat/parser-owned-importmap-blocked-after-dynamic-module-prepare"))
.fetch(&server.url("/compat/parser-owned-importmap-applies-after-dynamic-module-prepare"))
.await?;
assert_eq!(
diagnostic_global(&page, "dynamicModulePrepareBarrierInstalled"),
@@ -3410,7 +3407,7 @@ async fn parser_owned_importmap_after_dynamic_module_prepare_adds_unresolved_map
);
assert_eq!(
diagnostic_global(&page, "parserImportMapAfterDynamicModuleErrors"),
None
Some(&JsValueSnapshot::Number(0.0))
);
assert_eq!(
diagnostic_global(&page, "parserImportMapAfterDynamicModuleDone"),
+11 -1
View File
@@ -63,13 +63,23 @@ mod tests {
assert_eq!(first.as_str(), "https://example.test/app/mod.mjs");
registry
.register_import_map(r#"{"imports":{"./mod.mjs":"/replacement.mjs"}}"#, &base_url)
.register_import_map(
r#"{"imports":{"./mod.mjs":"/replacement.mjs","new":"/new.mjs"}}"#,
&base_url,
)
.expect("later import map should merge");
let second = registry
.resolve_module_specifier("./mod.mjs", &base_url)
.expect("previously resolved module should keep original resolution");
assert_eq!(second.as_str(), "https://example.test/app/mod.mjs");
assert_eq!(
registry
.resolve_module_specifier("new", &base_url)
.expect("late import map should add unresolved specifiers")
.as_str(),
"https://example.test/new.mjs"
);
}
#[test]
@@ -2057,7 +2057,7 @@ pub(super) fn finish_runtime_script_start_candidate(
let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else {
return;
};
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) {
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(scope, runtime, plan) {
Ok(Some(committed)) => {
execute_committed_inline_classic_script(runtime, scope, host_ptr, committed);
}
@@ -78,6 +78,16 @@ impl DocumentRuntime {
.register_import_map(source, base_url)
}
pub(crate) fn register_runtime_owned_import_map_source(
&mut self,
source: &str,
base_url: &Url,
) {
self.script_lifecycle
.scripts_mut()
.register_dynamic_import_map(base_url, source);
}
pub(crate) fn resolve_module_specifier(
&mut self,
specifier: &str,
+3 -7
View File
@@ -1456,15 +1456,11 @@ impl HostScriptScheduler {
)
}
fn register_dynamic_import_map(
&mut self,
preparation: &RuntimeScriptPreparationContext,
source: &str,
) {
if let Err(message) = self.register_import_map(source, &preparation.base_url) {
pub(crate) fn register_dynamic_import_map(&mut self, base_url: &Url, source: &str) {
if let Err(message) = self.register_import_map(source, base_url) {
let work = self.plan_window_script_failure_report_lifecycle_work(
&message,
Some(preparation.base_url.as_str()),
Some(base_url.as_str()),
None,
);
self.enqueue_post_parse_lifecycle_work(work);
+13 -4
View File
@@ -70,6 +70,11 @@ pub(crate) enum PreparedRuntimeScriptStartCommit {
host_script_handle: String,
source: String,
},
InlineImportMap {
node: NativeNodeId,
base_url: url::Url,
source: String,
},
Admission {
reservation: RuntimeScriptStartReservation,
payload: Box<RuntimeScriptAdmissionPayload>,
@@ -210,7 +215,7 @@ impl PreparedRuntimeScriptStart {
scripts.cancel_script_start(host_script_handle, node);
return Ok(None);
}
scripts.register_dynamic_import_map(&preparation, &source);
scripts.register_dynamic_import_map(&preparation.base_url, &source);
Ok(None)
}
RuntimeScriptStartDecision::RejectExternalImportMap => {
@@ -402,16 +407,20 @@ pub(crate) fn prepare_runtime_script_start_commit(
})
}
RuntimeScriptStartDecision::RegisterImportMap { source } => {
if finish_local_runtime_script_start(
if !finish_local_runtime_script_start(
dom_host,
scripts,
node,
&host_script_handle,
ScriptStartCommitKind::RegisterImportMap,
) {
scripts.register_dynamic_import_map(&preparation, &source);
return Ok(PreparedRuntimeScriptStartCommit::Noop);
}
Ok(PreparedRuntimeScriptStartCommit::Noop)
Ok(PreparedRuntimeScriptStartCommit::InlineImportMap {
node,
base_url: preparation.base_url,
source,
})
}
RuntimeScriptStartDecision::RejectExternalImportMap => {
if finish_local_runtime_script_start(
+8 -2
View File
@@ -202,7 +202,10 @@ fn draining_dynamic_scripts_keeps_async_and_in_order_lanes_separate() {
fn dynamic_importmap_registration_does_not_enter_script_batch() {
let preparation = preparation("https://example.test/", NodeId::new(0));
let mut scheduler = HostScriptScheduler::default();
scheduler.register_dynamic_import_map(&preparation, "{\"imports\":{\"fixture\":\"/mod.js\"}}");
scheduler.register_dynamic_import_map(
&preparation.base_url,
"{\"imports\":{\"fixture\":\"/mod.js\"}}",
);
let batch = scheduler.drain_dynamic_scripts();
assert!(batch.in_order.is_empty());
@@ -251,7 +254,10 @@ fn queueing_dynamic_module_does_not_block_later_import_map_merge() {
ScriptMode::Async,
)
.expect("module script should queue");
scheduler.register_dynamic_import_map(&preparation, "{\"imports\":{\"late\":\"/late.mjs\"}}");
scheduler.register_dynamic_import_map(
&preparation.base_url,
"{\"imports\":{\"late\":\"/late.mjs\"}}",
);
let batch = scheduler.drain_dynamic_scripts();
assert_eq!(batch.async_scripts.len(), 1);
+1 -1
View File
@@ -444,7 +444,7 @@ fn host_prepare_script_start_callback(
rv.set_undefined();
return;
};
match host.plan_and_commit_current_main_runtime_script_start(node, &host_script_handle) {
match host.plan_and_commit_current_main_runtime_script_start(scope, node, &host_script_handle) {
Ok(Some(committed)) => {
let (_, _, source) = committed.into_parts();
if let Some(source) = v8_string(scope, &source) {
@@ -20,6 +20,7 @@ use crate::{
impl JsContextHost {
pub(crate) fn plan_and_commit_current_main_runtime_script_start(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
node: crate::document_runtime::DomHandle,
host_script_handle: &str,
) -> std::result::Result<Option<CommittedInlineClassicScript>, String> {
@@ -27,11 +28,12 @@ impl JsContextHost {
let Some(plan) = runtime.host_plan_script_start(node, host_script_handle) else {
return Ok(None);
};
self.commit_current_main_runtime_script_start(runtime, plan)
self.commit_current_main_runtime_script_start(scope, runtime, plan)
}
pub(crate) fn commit_current_main_runtime_script_start(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
runtime: &mut DocumentRuntime,
plan: RuntimeScriptStartPlan,
) -> std::result::Result<Option<CommittedInlineClassicScript>, String> {
@@ -82,6 +84,38 @@ impl JsContextHost {
source,
)))
}
PreparedRuntimeScriptStartCommit::InlineImportMap {
node,
base_url,
source,
} => {
debug_assert!(load_delay_binding.is_none());
let nonce = runtime
.dom_host()
.node(node)
.and_then(crate::dom::native::Node::as_element)
.and_then(|element| {
element
.cryptographic_nonce()
.or_else(|| element.attribute("nonce"))
})
.map(str::to_owned);
let request =
crate::content_security_policy::ContentSecurityPolicyScriptElementRequest {
nonce: nonce.as_deref(),
integrity: None,
parser_inserted: false,
};
let host_ptr: *mut JsContextHost = self;
if let Some(source) =
crate::native_bridge::element::inline_script_source_for_execution(
scope, host_ptr, node, &source, request,
)
{
runtime.register_runtime_owned_import_map_source(&source, &base_url);
}
Ok(None)
}
PreparedRuntimeScriptStartCommit::Admission {
reservation,
payload,
@@ -1031,6 +1031,70 @@ fn trusted_types_default_policy_can_make_changed_empty_script_sources_executable
);
}
#[test]
fn trusted_types_default_policy_prepares_runtime_import_maps_before_registration() {
let mut vm = new_storage_test_vm("https://runtime-import-map-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const root = document.body ||
(document.documentElement || document.appendChild(document.createElement("html")))
.appendChild(document.createElement("body"));
const calls = [];
trustedTypes.createPolicy("default", {
createScript(value, type, sink) {
calls.push([value, type, sink]);
const specifier = sink === "SVGScriptElement text" ? "svg-mapped" : "html-mapped";
return JSON.stringify({ imports: { [specifier]: `/${specifier}.mjs` } });
}
});
const htmlScript = document.createElement("script");
htmlScript.type = "importmap";
htmlScript.appendChild(document.createTextNode("html-map"));
root.appendChild(htmlScript);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
root.appendChild(svg);
const svgScript = document.createElementNS("http://www.w3.org/2000/svg", "script");
svgScript.setAttribute("type", "importmap");
svgScript.appendChild(document.createTextNode("svg-map"));
svg.appendChild(svgScript);
return JSON.stringify(calls);
})()
"#,
)
.expect("runtime import maps should pass through the Trusted Types source gate");
assert_eq!(
result,
r#"[["html-map","TrustedScript","HTMLScriptElement text"],["svg-map","TrustedScript","SVGScriptElement text"]]"#
);
let base_url = vm.document_runtime.document_url().clone();
for (specifier, expected) in [
(
"html-mapped",
"https://runtime-import-map-trusted-types.test/html-mapped.mjs",
),
(
"svg-mapped",
"https://runtime-import-map-trusted-types.test/svg-mapped.mjs",
),
] {
assert_eq!(
vm.document_runtime
.resolve_module_specifier(specifier, &base_url)
.expect("default-policy import map entry should resolve")
.as_str(),
expected
);
}
}
#[test]
fn inline_module_graph_roots_use_trusted_types_compliant_source() {
let mut vm = new_storage_test_vm("https://module-source-trusted-types.test/");
+6 -6
View File
@@ -973,8 +973,8 @@ const DYNAMIC_SCRIPT_PREPARATION_CONTEXT_STAYS_IN_OLD_DOCUMENT_HTML: &str = incl
);
const DYNAMIC_IMPORTMAP_BEFORE_MODULE_HTML: &str =
include_str!("../../moli-core/tests/fixtures/runtime/dynamic_importmap_before_module.html");
const DYNAMIC_ASYNC_MODULE_CLOSES_IMPORTMAP_ACQUISITION_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/dynamic_async_module_closes_importmap_acquisition.html"
const DYNAMIC_ASYNC_MODULE_ALLOWS_LATE_IMPORTMAP_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/dynamic_async_module_allows_late_importmap.html"
);
const DYNAMIC_EXTERNAL_IMPORTMAP_ERROR_BEFORE_MODULE_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/dynamic_external_importmap_error_before_module.html"
@@ -997,11 +997,11 @@ const IMPORTMAP_URL_LIKE_NORMALIZATION_HTML: &str =
include_str!("../../moli-core/tests/fixtures/runtime/importmap_url_like_normalization.html");
const IMPORTMAP_AFTER_MODULE_LOAD_HTML: &str =
include_str!("../../moli-core/tests/fixtures/runtime/importmap_after_module_load.html");
const IMPORTMAP_CLOSED_BY_PARSER_OWNED_MODULE_BEFORE_LATE_DYNAMIC_MAP_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/importmap_closed_by_parser_owned_module_before_late_dynamic_map.html"
const PARSER_OWNED_MODULE_ALLOWS_LATE_DYNAMIC_IMPORTMAP_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/parser_owned_module_allows_late_dynamic_importmap.html"
);
const PARSER_OWNED_IMPORTMAP_BLOCKED_AFTER_DYNAMIC_MODULE_PREPARE_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/parser_owned_importmap_blocked_after_dynamic_module_prepare.html"
const PARSER_OWNED_IMPORTMAP_APPLIES_AFTER_DYNAMIC_MODULE_PREPARE_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/parser_owned_importmap_applies_after_dynamic_module_prepare.html"
);
const IMPORTMAP_NULL_BLOCKS_DYNAMIC_IMPORT_HTML: &str = include_str!(
"../../moli-core/tests/fixtures/runtime/importmap_null_blocks_dynamic_import.html"
+6 -7
View File
@@ -2700,8 +2700,8 @@ pub(super) async fn dynamic_importmap_before_module_page() -> Html<&'static str>
Html(DYNAMIC_IMPORTMAP_BEFORE_MODULE_HTML)
}
pub(super) async fn dynamic_async_module_closes_importmap_acquisition_page() -> Html<&'static str> {
Html(DYNAMIC_ASYNC_MODULE_CLOSES_IMPORTMAP_ACQUISITION_HTML)
pub(super) async fn dynamic_async_module_allows_late_importmap_page() -> Html<&'static str> {
Html(DYNAMIC_ASYNC_MODULE_ALLOWS_LATE_IMPORTMAP_HTML)
}
pub(super) async fn dynamic_external_importmap_error_before_module_page() -> Html<&'static str> {
@@ -2748,14 +2748,13 @@ pub(super) async fn importmap_after_module_load_page() -> Html<&'static str> {
Html(IMPORTMAP_AFTER_MODULE_LOAD_HTML)
}
pub(super) async fn importmap_closed_by_parser_owned_module_before_late_dynamic_map_page()
-> Html<&'static str> {
Html(IMPORTMAP_CLOSED_BY_PARSER_OWNED_MODULE_BEFORE_LATE_DYNAMIC_MAP_HTML)
pub(super) async fn parser_owned_module_allows_late_dynamic_importmap_page() -> Html<&'static str> {
Html(PARSER_OWNED_MODULE_ALLOWS_LATE_DYNAMIC_IMPORTMAP_HTML)
}
pub(super) async fn parser_owned_importmap_blocked_after_dynamic_module_prepare_page()
pub(super) async fn parser_owned_importmap_applies_after_dynamic_module_prepare_page()
-> Html<&'static str> {
Html(PARSER_OWNED_IMPORTMAP_BLOCKED_AFTER_DYNAMIC_MODULE_PREPARE_HTML)
Html(PARSER_OWNED_IMPORTMAP_APPLIES_AFTER_DYNAMIC_MODULE_PREPARE_HTML)
}
pub(super) async fn importmap_null_blocks_dynamic_import_page() -> Html<&'static str> {
+6 -6
View File
@@ -1521,8 +1521,8 @@ pub(super) fn build_router() -> Router {
get(dynamic_importmap_before_module_page),
)
.route(
"/compat/dynamic-async-module-closes-importmap-acquisition",
get(dynamic_async_module_closes_importmap_acquisition_page),
"/compat/dynamic-async-module-allows-late-importmap",
get(dynamic_async_module_allows_late_importmap_page),
)
.route(
"/compat/dynamic-external-importmap-error-before-module",
@@ -1581,12 +1581,12 @@ pub(super) fn build_router() -> Router {
get(importmap_after_module_load_page),
)
.route(
"/compat/importmap-closed-by-parser-owned-module-before-late-dynamic-map",
get(importmap_closed_by_parser_owned_module_before_late_dynamic_map_page),
"/compat/parser-owned-module-allows-late-dynamic-importmap",
get(parser_owned_module_allows_late_dynamic_importmap_page),
)
.route(
"/compat/parser-owned-importmap-blocked-after-dynamic-module-prepare",
get(parser_owned_importmap_blocked_after_dynamic_module_prepare_page),
"/compat/parser-owned-importmap-applies-after-dynamic-module-prepare",
get(parser_owned_importmap_applies_after_dynamic_module_prepare_page),
)
.route(
"/compat/importmap-null-blocks-dynamic-import",