Procházet zdrojové kódy

LibWeb: Implement multiple import map support

Shannon Booth před 7 měsíci
rodič
revize
ac6fe2e211

+ 8 - 11
Libraries/LibWeb/Bindings/MainThreadVM.cpp

@@ -532,14 +532,11 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
             }
             }
         }
         }
 
 
-        // 8. Disallow further import maps given moduleMapRealm.
-        HTML::disallow_further_import_maps(*module_map_realm);
-
-        // 9. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]],
+        // 8. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]],
         //    catching any exceptions. If they throw an exception, let resolutionError be the thrown exception.
         //    catching any exceptions. If they throw an exception, let resolutionError be the thrown exception.
         auto url = HTML::resolve_module_specifier(referencing_script, module_request.module_specifier);
         auto url = HTML::resolve_module_specifier(referencing_script, module_request.module_specifier);
 
 
-        // 10. If the previous step threw an exception, then:
+        // 9. If the previous step threw an exception, then:
         if (url.is_exception()) {
         if (url.is_exception()) {
             // 1. Let completion be Completion Record { [[Type]]: throw, [[Value]]: resolutionError, [[Target]]: empty }.
             // 1. Let completion be Completion Record { [[Type]]: throw, [[Value]]: resolutionError, [[Target]]: empty }.
             auto completion = exception_to_throw_completion(main_thread_vm(), url.exception());
             auto completion = exception_to_throw_completion(main_thread_vm(), url.exception());
@@ -552,19 +549,19 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
             return;
             return;
         }
         }
 
 
-        // 11. Let settingsObject be moduleMapRealm's principal realm's settings object.
+        // 10. Let settingsObject be moduleMapRealm's principal realm's settings object.
         auto& settings_object = HTML::principal_realm_settings_object(HTML::principal_realm(*module_map_realm));
         auto& settings_object = HTML::principal_realm_settings_object(HTML::principal_realm(*module_map_realm));
 
 
-        // 12. Let fetchOptions be the result of getting the descendant script fetch options given originalFetchOptions, url, and settingsObject.
+        // 11. Let fetchOptions be the result of getting the descendant script fetch options given originalFetchOptions, url, and settingsObject.
         auto fetch_options = HTML::get_descendant_script_fetch_options(original_fetch_options, url.value(), settings_object);
         auto fetch_options = HTML::get_descendant_script_fetch_options(original_fetch_options, url.value(), settings_object);
 
 
-        // 13. Let destination be "script".
+        // 12. Let destination be "script".
         auto destination = Fetch::Infrastructure::Request::Destination::Script;
         auto destination = Fetch::Infrastructure::Request::Destination::Script;
 
 
-        // 14. Let fetchClient be moduleMapRealm's principal realm's settings object.
+        // 13. Let fetchClient be moduleMapRealm's principal realm's settings object.
         GC::Ref fetch_client { HTML::principal_realm_settings_object(HTML::principal_realm(*module_map_realm)) };
         GC::Ref fetch_client { HTML::principal_realm_settings_object(HTML::principal_realm(*module_map_realm)) };
 
 
-        // 14. If loadState is not undefined, then:
+        // 15. If loadState is not undefined, then:
         HTML::PerformTheFetchHook perform_fetch;
         HTML::PerformTheFetchHook perform_fetch;
         if (load_state) {
         if (load_state) {
             auto& fetch_context = static_cast<HTML::FetchContext&>(*load_state);
             auto& fetch_context = static_cast<HTML::FetchContext&>(*load_state);
@@ -633,7 +630,7 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
             vm.pop_execution_context();
             vm.pop_execution_context();
         });
         });
 
 
-        // 15. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, moduleMapRealm, fetchReferrer,
+        // 16. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, moduleMapRealm, fetchReferrer,
         //     moduleRequest, and onSingleFetchComplete as defined below.
         //     moduleRequest, and onSingleFetchComplete as defined below.
         //     If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
         //     If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
         HTML::fetch_single_imported_module_script(*module_map_realm, url.release_value(), *fetch_client, destination, fetch_options, *module_map_realm, fetch_referrer, module_request, perform_fetch, on_single_fetch_complete);
         HTML::fetch_single_imported_module_script(*module_map_realm, url.release_value(), *fetch_client, destination, fetch_options, *module_map_realm, fetch_referrer, module_request, perform_fetch, on_single_fetch_complete);

+ 2 - 17
Libraries/LibWeb/HTML/HTMLScriptElement.cpp

@@ -460,25 +460,10 @@ void HTMLScriptElement::prepare_script()
         }
         }
         // -> "importmap"
         // -> "importmap"
         else if (m_script_type == ScriptType::ImportMap) {
         else if (m_script_type == ScriptType::ImportMap) {
-            // FIXME: need to check if relevant global object is a Window - is this correct?
-            auto& global = relevant_global_object(*this);
-
-            // 1. If el's relevant global object's import maps allowed is false, then queue an element task on the DOM manipulation task source given el to fire an event named error at el, and return.
-            if (is<Window>(global) && !verify_cast<Window>(global).import_maps_allowed()) {
-                queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] {
-                    dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
-                });
-                return;
-            }
-
-            // 2. Set el's relevant global object's import maps allowed to false.
-            if (is<Window>(global))
-                verify_cast<Window>(global).set_import_maps_allowed(false);
-
-            // 3. Let result be the result of creating an import map parse result given source text and base URL.
+            // 1. Let result be the result of creating an import map parse result given source text and base URL.
             auto result = ImportMapParseResult::create(realm(), source_text.to_byte_string(), base_url);
             auto result = ImportMapParseResult::create(realm(), source_text.to_byte_string(), base_url);
 
 
-            // 4. Mark as ready el given result.
+            // 2. Mark as ready el given result.
             mark_as_ready(Result(move(result)));
             mark_as_ready(Result(move(result)));
         }
         }
     }
     }

+ 14 - 5
Libraries/LibWeb/HTML/Scripting/Environments.cpp

@@ -279,9 +279,8 @@ bool module_type_allowed(JS::Realm const&, StringView module_type)
     return true;
     return true;
 }
 }
 
 
-// https://html.spec.whatwg.org/multipage/webappapis.html#disallow-further-import-maps
-// https://whatpr.org/html/9893/webappapis.html#disallow-further-import-maps
-void disallow_further_import_maps(JS::Realm& realm)
+// https://html.spec.whatwg.org/multipage/webappapis.html#add-module-to-resolved-module-set
+void add_module_to_resolved_module_set(JS::Realm& realm, String const& serialized_base_url, String const& normalized_specifier, Optional<URL::URL> const& as_url)
 {
 {
     // 1. Let global be realm's global object.
     // 1. Let global be realm's global object.
     auto& global = realm.global_object();
     auto& global = realm.global_object();
@@ -290,8 +289,18 @@ void disallow_further_import_maps(JS::Realm& realm)
     if (!is<Window>(global))
     if (!is<Window>(global))
         return;
         return;
 
 
-    // 3. Set global's import maps allowed to false.
-    verify_cast<Window>(global).set_import_maps_allowed(false);
+    // 3. Let record be a new specifier resolution record, with serialized base URL set to serializedBaseURL,
+    //    specifier set to normalizedSpecifier, and specifier as a URL set to asURL.
+    //
+    // NOTE: We set 'specifier as a URL set to asURL' as a bool to simplify logic when merging import maps.
+    SpecifierResolution resolution {
+        .serialized_base_url = serialized_base_url,
+        .specifier = normalized_specifier,
+        .specifier_is_null_or_url_like_that_is_special = !as_url.has_value() || as_url->is_special(),
+    };
+
+    // 4. Append record to global's resolved module set.
+    return verify_cast<Window>(global).append_resolved_module(move(resolution));
 }
 }
 
 
 // https://whatpr.org/html/9893/webappapis.html#concept-realm-module-map
 // https://whatpr.org/html/9893/webappapis.html#concept-realm-module-map

+ 2 - 1
Libraries/LibWeb/HTML/Scripting/Environments.h

@@ -139,7 +139,8 @@ void prepare_to_run_callback(JS::Realm&);
 void clean_up_after_running_callback(JS::Realm const&);
 void clean_up_after_running_callback(JS::Realm const&);
 ModuleMap& module_map_of_realm(JS::Realm&);
 ModuleMap& module_map_of_realm(JS::Realm&);
 bool module_type_allowed(JS::Realm const&, StringView module_type);
 bool module_type_allowed(JS::Realm const&, StringView module_type);
-void disallow_further_import_maps(JS::Realm&);
+
+void add_module_to_resolved_module_set(JS::Realm&, String const& serialized_base_url, String const& normalized_specifier, Optional<URL::URL> const& as_url);
 
 
 EnvironmentSettingsObject& incumbent_settings_object();
 EnvironmentSettingsObject& incumbent_settings_object();
 JS::Realm& incumbent_realm();
 JS::Realm& incumbent_realm();

+ 42 - 50
Libraries/LibWeb/HTML/Scripting/Fetching.cpp

@@ -120,8 +120,8 @@ WebIDL::ExceptionOr<URL::URL> resolve_module_specifier(Optional<Script&> referri
     if (is<Window>(realm->global_object()))
     if (is<Window>(realm->global_object()))
         import_map = verify_cast<Window>(realm->global_object()).import_map();
         import_map = verify_cast<Window>(realm->global_object()).import_map();
 
 
-    // 6. Let baseURLString be baseURL, serialized.
-    auto base_url_string = base_url->serialize();
+    // 6. Let serializedBaseURL be baseURL, serialized.
+    auto serialized_base_url = base_url->serialize();
 
 
     // 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL.
     // 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL.
     auto as_url = resolve_url_like_module_specifier(specifier, *base_url);
     auto as_url = resolve_url_like_module_specifier(specifier, *base_url);
@@ -129,37 +129,49 @@ WebIDL::ExceptionOr<URL::URL> resolve_module_specifier(Optional<Script&> referri
     // 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null; otherwise, specifier.
     // 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null; otherwise, specifier.
     auto normalized_specifier = as_url.has_value() ? as_url->serialize().to_byte_string() : specifier;
     auto normalized_specifier = as_url.has_value() ? as_url->serialize().to_byte_string() : specifier;
 
 
-    // 9. For each scopePrefix → scopeImports of importMap's scopes:
+    // 9. Let result be a URL-or-null, initially null.
+    Optional<URL::URL> result;
+
+    // 10. For each scopePrefix → scopeImports of importMap's scopes:
     for (auto const& entry : import_map.scopes()) {
     for (auto const& entry : import_map.scopes()) {
         // FIXME: Clarify if the serialization steps need to be run here. The steps below assume
         // FIXME: Clarify if the serialization steps need to be run here. The steps below assume
         //        scopePrefix to be a string.
         //        scopePrefix to be a string.
         auto const& scope_prefix = entry.key.serialize();
         auto const& scope_prefix = entry.key.serialize();
         auto const& scope_imports = entry.value;
         auto const& scope_imports = entry.value;
 
 
-        // 1. If scopePrefix is baseURLString, or if scopePrefix ends with U+002F (/) and scopePrefix is a code unit prefix of baseURLString, then:
-        if (scope_prefix == base_url_string || (scope_prefix.ends_with('/') && Infra::is_code_unit_prefix(scope_prefix, base_url_string))) {
+        // 1. If scopePrefix is serializedBaseURL, or if scopePrefix ends with U+002F (/) and scopePrefix is a code unit prefix of serializedBaseURL, then:
+        if (scope_prefix == serialized_base_url || (scope_prefix.ends_with('/') && Infra::is_code_unit_prefix(scope_prefix, serialized_base_url))) {
             // 1. Let scopeImportsMatch be the result of resolving an imports match given normalizedSpecifier, asURL, and scopeImports.
             // 1. Let scopeImportsMatch be the result of resolving an imports match given normalizedSpecifier, asURL, and scopeImports.
             auto scope_imports_match = TRY(resolve_imports_match(normalized_specifier, as_url, scope_imports));
             auto scope_imports_match = TRY(resolve_imports_match(normalized_specifier, as_url, scope_imports));
 
 
-            // 2. If scopeImportsMatch is not null, then return scopeImportsMatch.
-            if (scope_imports_match.has_value())
-                return scope_imports_match.release_value();
+            // 2. If scopeImportsMatch is not null, then set result to scopeImportsMatch, and break.
+            if (scope_imports_match.has_value()) {
+                result = scope_imports_match.release_value();
+                break;
+            }
         }
         }
     }
     }
 
 
-    // 10. Let topLevelImportsMatch be the result of resolving an imports match given normalizedSpecifier, asURL, and importMap's imports.
-    auto top_level_imports_match = TRY(resolve_imports_match(normalized_specifier, as_url, import_map.imports()));
+    // 11. If result is null, set result be the result of resolving an imports match given normalizedSpecifier, asURL, and importMap's imports.
+    if (!result.has_value())
+        result = TRY(resolve_imports_match(normalized_specifier, as_url, import_map.imports()));
+
+    // 12. If result is null, set it to asURL.
+    // Spec-Note: By this point, if result was null, specifier wasn't remapped to anything by importMap, but it might have been able to be turned into a URL.
+    if (!result.has_value())
+        result = as_url;
 
 
-    // 11. If topLevelImportsMatch is not null, then return topLevelImportsMatch.
-    if (top_level_imports_match.has_value())
-        return top_level_imports_match.release_value();
+    // 13. If result is not null, then:
+    if (result.has_value()) {
+        // 1. Add module to resolved module set given realm, serializedBaseURL, normalizedSpecifier, and asURL.
+        add_module_to_resolved_module_set(*realm, serialized_base_url, MUST(String::from_byte_string(normalized_specifier)), as_url);
 
 
-    // 12. If asURL is not null, then return asURL.
-    if (as_url.has_value())
-        return as_url.release_value();
+        // 2. Return result.
+        return result.release_value();
+    }
 
 
-    // 13. Throw a TypeError indicating that specifier was a bare specifier, but was not remapped to anything by importMap.
-    return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("Failed to resolve non relative module specifier '{}' from an import map.", specifier).release_value_but_fixme_should_propagate_errors() };
+    // 14. Throw a TypeError indicating that specifier was a bare specifier, but was not remapped to anything by importMap.
+    return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Failed to resolve non relative module specifier '{}' from an import map.", specifier)) };
 }
 }
 
 
 // https://html.spec.whatwg.org/multipage/webappapis.html#resolving-an-imports-match
 // https://html.spec.whatwg.org/multipage/webappapis.html#resolving-an-imports-match
@@ -292,34 +304,27 @@ ScriptFetchOptions get_descendant_script_fetch_options(ScriptFetchOptions const&
     // 1. Let newOptions be a copy of originalOptions.
     // 1. Let newOptions be a copy of originalOptions.
     auto new_options = original_options;
     auto new_options = original_options;
 
 
-    // 2. Let integrity be the empty string.
-    String integrity;
-
-    // 3. If settingsObject's global object is a Window object, then set integrity to the result of resolving a module integrity metadata with url and settingsObject.
-    if (is<Window>(settings_object.global_object()))
-        integrity = resolve_a_module_integrity_metadata(url, settings_object);
+    // 2. Let integrity be the result of resolving a module integrity metadata with url and settingsObject.
+    String integrity = resolve_a_module_integrity_metadata(url, settings_object);
 
 
-    // 4. Set newOptions's integrity metadata to integrity.
+    // 3. Set newOptions's integrity metadata to integrity.
     new_options.integrity_metadata = integrity;
     new_options.integrity_metadata = integrity;
 
 
-    // 5. Set newOptions's fetch priority to "auto".
+    // 4. Set newOptions's fetch priority to "auto".
     new_options.fetch_priority = Fetch::Infrastructure::Request::Priority::Auto;
     new_options.fetch_priority = Fetch::Infrastructure::Request::Priority::Auto;
 
 
-    // 6. Return newOptions.
+    // 5. Return newOptions.
     return new_options;
     return new_options;
 }
 }
 
 
 // https://html.spec.whatwg.org/multipage/webappapis.html#resolving-a-module-integrity-metadata
 // https://html.spec.whatwg.org/multipage/webappapis.html#resolving-a-module-integrity-metadata
 String resolve_a_module_integrity_metadata(const URL::URL& url, EnvironmentSettingsObject& settings_object)
 String resolve_a_module_integrity_metadata(const URL::URL& url, EnvironmentSettingsObject& settings_object)
 {
 {
-    // 1. Assert: settingsObject's global object is a Window object.
-    VERIFY(is<Window>(settings_object.global_object()));
-
-    // 2. Let map be settingsObject's global object's import map.
-    auto map = static_cast<Window const&>(settings_object.global_object()).import_map();
+    // 1. Let map be settingsObject's global object's import map.
+    auto map = verify_cast<Window>(settings_object.global_object()).import_map();
 
 
-    // 3. If map's integrity[url] does not exist, then return the empty string.
-    // 4. Return map's integrity[url].
+    // 2. If map's integrity[url] does not exist, then return the empty string.
+    // 3. Return map's integrity[url].
     return MUST(String::from_byte_string(map.integrity().get(url).value_or("")));
     return MUST(String::from_byte_string(map.integrity().get(url).value_or("")));
 }
 }
 
 
@@ -860,9 +865,6 @@ void fetch_single_module_script(JS::Realm& realm,
 // https://whatpr.org/html/9893/webappapis.html#fetch-a-module-script-tree
 // https://whatpr.org/html/9893/webappapis.html#fetch-a-module-script-tree
 void fetch_external_module_script_graph(JS::Realm& realm, URL::URL const& url, EnvironmentSettingsObject& settings_object, ScriptFetchOptions const& options, OnFetchScriptComplete on_complete)
 void fetch_external_module_script_graph(JS::Realm& realm, URL::URL const& url, EnvironmentSettingsObject& settings_object, ScriptFetchOptions const& options, OnFetchScriptComplete on_complete)
 {
 {
-    // 1. Disallow further import maps given settingsObject's realm.
-    disallow_further_import_maps(settings_object.realm());
-
     auto steps = create_on_fetch_script_complete(realm.heap(), [&realm, &settings_object, on_complete, url](auto result) mutable {
     auto steps = create_on_fetch_script_complete(realm.heap(), [&realm, &settings_object, on_complete, url](auto result) mutable {
         // 1. If result is null, run onComplete given null, and abort these steps.
         // 1. If result is null, run onComplete given null, and abort these steps.
         if (!result) {
         if (!result) {
@@ -875,27 +877,17 @@ void fetch_external_module_script_graph(JS::Realm& realm, URL::URL const& url, E
         fetch_descendants_of_and_link_a_module_script(realm, module_script, settings_object, Fetch::Infrastructure::Request::Destination::Script, nullptr, on_complete);
         fetch_descendants_of_and_link_a_module_script(realm, module_script, settings_object, Fetch::Infrastructure::Request::Destination::Script, nullptr, on_complete);
     });
     });
 
 
-    // 2. Fetch a single module script given url, settingsObject, "script", options, settingsObject's realm, "client", true, and with the following steps given result:
+    // 1. Fetch a single module script given url, settingsObject, "script", options, settingsObject's realm, "client", true, and with the following steps given result:
     fetch_single_module_script(realm, url, settings_object, Fetch::Infrastructure::Request::Destination::Script, options, settings_object.realm(), Web::Fetch::Infrastructure::Request::Referrer::Client, {}, TopLevelModule::Yes, nullptr, steps);
     fetch_single_module_script(realm, url, settings_object, Fetch::Infrastructure::Request::Destination::Script, options, settings_object.realm(), Web::Fetch::Infrastructure::Request::Referrer::Client, {}, TopLevelModule::Yes, nullptr, steps);
 }
 }
 
 
 // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-an-inline-module-script-graph
 // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-an-inline-module-script-graph
-// https://whatpr.org/html/9893/webappapis.html#fetch-an-inline-module-script-graph
 void fetch_inline_module_script_graph(JS::Realm& realm, ByteString const& filename, ByteString const& source_text, URL::URL const& base_url, EnvironmentSettingsObject& settings_object, OnFetchScriptComplete on_complete)
 void fetch_inline_module_script_graph(JS::Realm& realm, ByteString const& filename, ByteString const& source_text, URL::URL const& base_url, EnvironmentSettingsObject& settings_object, OnFetchScriptComplete on_complete)
 {
 {
-    // 1. Disallow further import maps given settingsObject's realm.
-    disallow_further_import_maps(settings_object.realm());
-
-    // 2. Let script be the result of creating a JavaScript module script using sourceText, settingsObject's realm, baseURL, and options.
+    // 1. Let script be the result of creating a JavaScript module script using sourceText, settingsObject's realm, baseURL, and options.
     auto script = JavaScriptModuleScript::create(filename, source_text.view(), settings_object.realm(), base_url).release_value_but_fixme_should_propagate_errors();
     auto script = JavaScriptModuleScript::create(filename, source_text.view(), settings_object.realm(), base_url).release_value_but_fixme_should_propagate_errors();
 
 
-    // 3. If script is null, run onComplete given null, and return.
-    if (!script) {
-        on_complete->function()(nullptr);
-        return;
-    }
-
-    // 5. Fetch the descendants of and link script, given settingsObject, "script", and onComplete.
+    // 2. Fetch the descendants of and link script, given settingsObject, "script", and onComplete.
     fetch_descendants_of_and_link_a_module_script(realm, *script, settings_object, Fetch::Infrastructure::Request::Destination::Script, nullptr, on_complete);
     fetch_descendants_of_and_link_a_module_script(realm, *script, settings_object, Fetch::Infrastructure::Request::Destination::Script, nullptr, on_complete);
 }
 }
 
 

+ 133 - 0
Libraries/LibWeb/HTML/Scripting/ImportMap.cpp

@@ -1,5 +1,6 @@
 /*
 /*
  * Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
  * Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
  *
  *
  * SPDX-License-Identifier: BSD-2-Clause
  * SPDX-License-Identifier: BSD-2-Clause
  */
  */
@@ -10,7 +11,9 @@
 #include <LibWeb/HTML/Scripting/Fetching.h>
 #include <LibWeb/HTML/Scripting/Fetching.h>
 #include <LibWeb/HTML/Scripting/ImportMap.h>
 #include <LibWeb/HTML/Scripting/ImportMap.h>
 #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
 #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
+#include <LibWeb/HTML/Window.h>
 #include <LibWeb/Infra/JSON.h>
 #include <LibWeb/Infra/JSON.h>
+#include <LibWeb/Infra/Strings.h>
 
 
 namespace Web::HTML {
 namespace Web::HTML {
 
 
@@ -263,4 +266,134 @@ WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm
     return normalised;
     return normalised;
 }
 }
 
 
+// https://html.spec.whatwg.org/multipage/webappapis.html#merge-module-specifier-maps
+static ModuleSpecifierMap merge_module_specifier_maps(JS::Realm& realm, ModuleSpecifierMap const& new_map, ModuleSpecifierMap const& old_map)
+{
+    // 1. Let mergedMap be a deep copy of oldMap.
+    ModuleSpecifierMap merged_map = old_map;
+
+    // 2. For each specifier → url of newMap:
+    for (auto const& [specifier, url] : new_map) {
+        // 1. If specifier exists in oldMap, then:
+        if (old_map.contains(specifier)) {
+            // 1. The user agent may report a warning to the console indicating the ignored rule. They may choose to
+            //    avoid reporting if the rule is identical to an existing one.
+            auto& console = realm.intrinsics().console_object()->console();
+            console.output_debug_message(JS::Console::LogLevel::Warn,
+                MUST(String::formatted("An import map rule for specifier '{}' was ignored as one was already present in the existing import map", specifier)));
+
+            // 2. Continue.
+            continue;
+        }
+
+        // 2. Set mergedMap[specifier] to url.
+        merged_map.set(specifier, url);
+    }
+
+    // 3. Return mergedMap.
+    return merged_map;
+}
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#merge-existing-and-new-import-maps
+void merge_existing_and_new_import_maps(Window& global, ImportMap& new_import_map)
+{
+    auto& realm = global.realm();
+
+    // 1. Let newImportMapScopes be a deep copy of newImportMap's scopes.
+    auto new_import_map_scopes = new_import_map.scopes();
+
+    // Spec-Note: We're mutating these copies and removing items from them when they are used to ignore scope-specific
+    //            rules. This is true for newImportMapScopes, as well as to newImportMapImports below.
+
+    // 2. Let oldImportMap be global's import map.
+    auto& old_import_map = global.import_map();
+
+    // 3. Let newImportMapImports be a deep copy of newImportMap's imports.
+    auto new_import_map_imports = new_import_map.imports();
+
+    // 4. For each scopePrefix → scopeImports of newImportMapScopes:
+    for (auto& [scope_prefix, scope_imports] : new_import_map_scopes) {
+        // 1. For each record of global's resolved module set:
+        for (auto const& record : global.resolved_module_set()) {
+            // 1. If scopePrefix is record's serialized base URL, or if scopePrefix ends with U+002F (/) and scopePrefix is a code unit prefix of record's serialized base URL, then:
+            if (scope_prefix == record.serialized_base_url || (scope_prefix.to_string().ends_with('/') && record.serialized_base_url.has_value() && Infra::is_code_unit_prefix(scope_prefix.to_string(), *record.serialized_base_url))) {
+                // 1. For each specifierKey → resolutionResult of scopeImports:
+                scope_imports.remove_all_matching([&](ByteString const& specifier_key, Optional<URL::URL> const&) {
+                    // 1. If specifierKey is record's specifier, or if all of the following conditions are true:
+                    //      * specifierKey ends with U+002F (/);
+                    //      * specifierKey is a code unit prefix of record's specifier;
+                    //      * either record's specifier as a URL is null or is special,
+                    //    then:
+                    if (specifier_key.view() == record.specifier
+                        || (specifier_key.ends_with('/')
+                            && Infra::is_code_unit_prefix(specifier_key, record.specifier)
+                            && record.specifier_is_null_or_url_like_that_is_special)) {
+                        // 1. The user agent may report a warning to the console indicating the ignored rule. They
+                        //    may choose to avoid reporting if the rule is identical to an existing one.
+                        auto& console = realm.intrinsics().console_object()->console();
+                        console.output_debug_message(JS::Console::LogLevel::Warn,
+                            MUST(String::formatted("An import map rule for specifier '{}' was ignored as one was already present in the existing import map", specifier_key)));
+
+                        // 2. Remove scopeImports[specifierKey].
+                        return true;
+                    }
+
+                    return false;
+                });
+            }
+        }
+
+        // 2. If scopePrefix exists in oldImportMap's scopes, then set oldImportMap's scopes[scopePrefix] to the result
+        //    of merging module specifier maps, given scopeImports and oldImportMap's scopes[scopePrefix].
+        if (auto it = old_import_map.scopes().find(scope_prefix); it != old_import_map.scopes().end()) {
+            it->value = merge_module_specifier_maps(realm, scope_imports, it->value);
+        }
+        // 3. Otherwise, set oldImportMap's scopes[scopePrefix] to scopeImports.
+        else {
+            old_import_map.scopes().set(scope_prefix, scope_imports);
+        }
+    }
+
+    // 5. For each url → integrity of newImportMap's integrity:
+    for (auto const& [url, integrity] : new_import_map.integrity()) {
+        // 1. If url exists in oldImportMap's integrity, then:
+        if (old_import_map.integrity().contains(url)) {
+            // 1. The user agent may report a warning to the console indicating the ignored rule. They may choose to
+            //    avoid reporting if the rule is identical to an existing one.
+            auto& console = realm.intrinsics().console_object()->console();
+            console.output_debug_message(JS::Console::LogLevel::Warn,
+                MUST(String::formatted("An import map integrity rule for url '{}' was ignored as one was already present in the existing import map", url)));
+
+            // 2. Continue.
+            continue;
+        }
+
+        // 2. Set oldImportMap's integrity[url] to integrity.
+        old_import_map.integrity().set(url, integrity);
+    }
+
+    // 6. For each record of global's resolved module set:
+    for (auto const& record : global.resolved_module_set()) {
+        // 1. For each specifier → url of newImportMapImports:
+        new_import_map_imports.remove_all_matching([&](ByteString const& specifier, Optional<URL::URL> const&) {
+            // 1. If specifier starts with record's specifier, then:
+            if (specifier.starts_with(record.specifier)) {
+                // 1. The user agent may report a warning to the console indicating the ignored rule. They may choose to
+                //    avoid reporting if the rule is identical to an existing one.
+                auto& console = realm.intrinsics().console_object()->console();
+                console.output_debug_message(JS::Console::LogLevel::Warn,
+                    MUST(String::formatted("An import map rule for specifier '{}' was ignored as one was already present in the existing import map", specifier)));
+
+                // 2. Remove newImportMapImports[specifier].
+                return true;
+            }
+
+            return false;
+        });
+    }
+
+    // 7. Set oldImportMap's imports to the result of merge module specifier maps, given newImportMapImports and oldImportMap's imports.
+    old_import_map.set_imports(merge_module_specifier_maps(realm, new_import_map_imports, old_import_map.imports()));
+}
+
 }
 }

+ 1 - 0
Libraries/LibWeb/HTML/Scripting/ImportMap.h

@@ -44,5 +44,6 @@ Optional<DeprecatedFlyString> normalise_specifier_key(JS::Realm& realm, Deprecat
 WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
 WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
 WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
 WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
 WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
 WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
+void merge_existing_and_new_import_maps(Window&, ImportMap&);
 
 
 }
 }

+ 2 - 5
Libraries/LibWeb/HTML/Scripting/ImportMapParseResult.cpp

@@ -73,12 +73,9 @@ void ImportMapParseResult::register_import_map(Window& global)
         return;
         return;
     }
     }
 
 
-    // 2. Assert: global's import map is an empty import map.
-    VERIFY(global.import_map().imports().is_empty() && global.import_map().scopes().is_empty());
-
-    // 3. Set global's import map to result's import map.
+    // 2. Merge existing and new import maps, given global and result's import map.
     VERIFY(m_import_map.has_value());
     VERIFY(m_import_map.has_value());
-    global.set_import_map(m_import_map.value());
+    merge_existing_and_new_import_maps(global, m_import_map.value());
 }
 }
 
 
 }
 }

+ 1 - 0
Libraries/LibWeb/HTML/UniversalGlobalScope.h

@@ -12,6 +12,7 @@
 #include <AK/String.h>
 #include <AK/String.h>
 #include <LibJS/Runtime/Value.h>
 #include <LibJS/Runtime/Value.h>
 #include <LibWeb/Forward.h>
 #include <LibWeb/Forward.h>
+#include <LibWeb/HTML/Scripting/ImportMap.h>
 #include <LibWeb/WebIDL/ExceptionOr.h>
 #include <LibWeb/WebIDL/ExceptionOr.h>
 
 
 namespace Web::HTML {
 namespace Web::HTML {

+ 32 - 6
Libraries/LibWeb/HTML/Window.h

@@ -21,7 +21,6 @@
 #include <LibWeb/HTML/MimeType.h>
 #include <LibWeb/HTML/MimeType.h>
 #include <LibWeb/HTML/Navigable.h>
 #include <LibWeb/HTML/Navigable.h>
 #include <LibWeb/HTML/Plugin.h>
 #include <LibWeb/HTML/Plugin.h>
-#include <LibWeb/HTML/Scripting/ImportMap.h>
 #include <LibWeb/HTML/ScrollOptions.h>
 #include <LibWeb/HTML/ScrollOptions.h>
 #include <LibWeb/HTML/StructuredSerializeOptions.h>
 #include <LibWeb/HTML/StructuredSerializeOptions.h>
 #include <LibWeb/HTML/UniversalGlobalScope.h>
 #include <LibWeb/HTML/UniversalGlobalScope.h>
@@ -45,6 +44,25 @@ struct WindowPostMessageOptions : public StructuredSerializeOptions {
     String target_origin { "/"_string };
     String target_origin { "/"_string };
 };
 };
 
 
+// https://html.spec.whatwg.org/multipage/webappapis.html#specifier-resolution-record
+// A specifier resolution record is a struct. It has the following items:
+struct SpecifierResolution {
+    // A serialized base URL
+    //    A string-or-null that represents the base URL of the specifier, when one exists.
+    Optional<String> serialized_base_url;
+
+    // A specifier
+    //     A string representing the specifier.
+    String specifier;
+
+    // A specifier as a URL
+    //     A URL-or-null that represents the URL in case of a URL-like module specifier.
+    //
+    // Spec-Note: Implementations can replace specifier as a URL with a boolean that indicates
+    //            that the specifier is either bare or URL-like that is special.
+    bool specifier_is_null_or_url_like_that_is_special { false };
+};
+
 class Window final
 class Window final
     : public DOM::EventTarget
     : public DOM::EventTarget
     , public GlobalEventHandlers
     , public GlobalEventHandlers
@@ -96,11 +114,12 @@ public:
 
 
     GC::Ptr<Navigable> navigable() const;
     GC::Ptr<Navigable> navigable() const;
 
 
+    ImportMap& import_map() { return m_import_map; }
     ImportMap const& import_map() const { return m_import_map; }
     ImportMap const& import_map() const { return m_import_map; }
     void set_import_map(ImportMap const& import_map) { m_import_map = import_map; }
     void set_import_map(ImportMap const& import_map) { m_import_map = import_map; }
 
 
-    bool import_maps_allowed() const { return m_import_maps_allowed; }
-    void set_import_maps_allowed(bool import_maps_allowed) { m_import_maps_allowed = import_maps_allowed; }
+    void append_resolved_module(SpecifierResolution resolution) { m_resolved_module_set.append(move(resolution)); }
+    Vector<SpecifierResolution> const& resolved_module_set() const { return m_resolved_module_set; }
 
 
     WebIDL::ExceptionOr<GC::Ptr<WindowProxy>> window_open_steps(StringView url, StringView target, StringView features);
     WebIDL::ExceptionOr<GC::Ptr<WindowProxy>> window_open_steps(StringView url, StringView target, StringView features);
 
 
@@ -269,11 +288,18 @@ private:
 
 
     GC::Ptr<DOM::Event> m_current_event;
     GC::Ptr<DOM::Event> m_current_event;
 
 
-    // https://html.spec.whatwg.org/multipage/webappapis.html#concept-window-import-map
+    // https://html.spec.whatwg.org/multipage/webappapis.html#concept-global-import-map
+    // A global object has an import map, initially an empty import map.
     ImportMap m_import_map;
     ImportMap m_import_map;
 
 
-    // https://html.spec.whatwg.org/multipage/webappapis.html#import-maps-allowed
-    bool m_import_maps_allowed { true };
+    // https://html.spec.whatwg.org/multipage/webappapis.html#resolved-module-set
+    // A global object has a resolved module set, a set of specifier resolution records, initially empty.
+    //
+    // Spec-Note: The resolved module set ensures that module specifier resolution returns the same result when called
+    //            multiple times with the same (referrer, specifier) pair. It does that by ensuring that import map rules
+    //            that impact the specifier in its referrer's scope cannot be defined after its initial resolution. For
+    //            now, only Window global objects have their module set data structures modified from the initial empty one.
+    Vector<SpecifierResolution> m_resolved_module_set;
 
 
     GC::Ptr<CSS::Screen> m_screen;
     GC::Ptr<CSS::Screen> m_screen;
     GC::Ptr<Navigator> m_navigator;
     GC::Ptr<Navigator> m_navigator;

+ 1 - 1
Tests/LibWeb/Text/expected/HTML/import-maps.txt

@@ -1 +1 @@
-hello, friends!
+(1) hello, friends!

+ 1 - 0
Tests/LibWeb/Text/expected/HTML/multiple-import-maps-confict.txt

@@ -0,0 +1 @@
+(1) hello, friends!

+ 2 - 0
Tests/LibWeb/Text/expected/HTML/multiple-import-maps.txt

@@ -0,0 +1,2 @@
+(1) hello, friends!
+(2) hello, friends!

+ 3 - 0
Tests/LibWeb/Text/input/HTML/import-maps-1.js

@@ -0,0 +1,3 @@
+export function main() {
+    println("(1) hello, friends!");
+}

+ 3 - 0
Tests/LibWeb/Text/input/HTML/import-maps-2.js

@@ -0,0 +1,3 @@
+export function main() {
+    println("(2) hello, friends!");
+}

+ 1 - 1
Tests/LibWeb/Text/input/HTML/import-maps.html

@@ -3,7 +3,7 @@
 <script type="importmap">
 <script type="importmap">
     {
     {
         "imports": {
         "imports": {
-            "application": "./import-maps.js"
+            "application": "./import-maps-1.js"
         }
         }
     }
     }
 </script>
 </script>

+ 0 - 3
Tests/LibWeb/Text/input/HTML/import-maps.js

@@ -1,3 +0,0 @@
-export function main() {
-    println("hello, friends!");
-}

+ 22 - 0
Tests/LibWeb/Text/input/HTML/multiple-import-maps-confict.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="../include.js"></script>
+<script type="importmap">
+    {
+        "imports": {
+            "conflict": "./import-maps-1.js"
+        }
+    }
+</script>
+<script type="importmap">
+    {
+        "imports": {
+            "conflict": "./import-maps-2.js"
+        }
+    }
+</script>
+<script type="module">
+    import * as Conflict from 'conflict';
+    test(() => {
+        Conflict.main();
+    });
+</script>

+ 24 - 0
Tests/LibWeb/Text/input/HTML/multiple-import-maps.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<script src="../include.js"></script>
+<script type="importmap">
+    {
+        "imports": {
+            "application1": "./import-maps-1.js"
+        }
+    }
+</script>
+<script type="importmap">
+    {
+        "imports": {
+            "application2": "./import-maps-2.js"
+        }
+    }
+</script>
+<script type="module">
+    import * as Application1 from 'application1';
+    import * as Application2 from 'application2';
+    test(() => {
+        Application1.main();
+        Application2.main();
+    });
+</script>