LibWeb: Implement multiple import map support

This commit is contained in:
Shannon Booth 2024-12-03 20:31:14 +13:00 committed by Luke Wilde
parent 0a216f9c14
commit ac6fe2e211
Notes: github-actions[bot] 2024-12-10 12:02:45 +00:00
19 changed files with 294 additions and 100 deletions

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -10,7 +11,9 @@
#include <LibWeb/HTML/Scripting/Fetching.h>
#include <LibWeb/HTML/Scripting/ImportMap.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/JSON.h>
#include <LibWeb/Infra/Strings.h>
namespace Web::HTML {
@ -263,4 +266,134 @@ WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm
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()));
}
}