LibWeb: Support "importmap" scripts

This commit is contained in:
Jamie Mansfield 2024-04-13 21:22:05 +01:00 committed by Andreas Kling
parent ccb363c443
commit e487f70bbf
Notes: sideshowbarker 2024-07-18 04:46:35 +09:00
13 changed files with 402 additions and 9 deletions

View file

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

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
println(HTMLScriptElement.supports("importmap"));
});
</script>

View file

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

View file

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

View file

@ -399,6 +399,8 @@ set(SOURCES
HTML/Scripting/EnvironmentSettingsSnapshot.cpp
HTML/Scripting/ExceptionReporter.cpp
HTML/Scripting/Fetching.cpp
HTML/Scripting/ImportMap.cpp
HTML/Scripting/ImportMapParseResult.cpp
HTML/Scripting/ModuleMap.cpp
HTML/Scripting/ModuleScript.cpp
HTML/Scripting/Script.cpp

View file

@ -17,6 +17,8 @@
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Fetching.h>
#include <LibWeb/HTML/Scripting/ImportMapParseResult.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/MimeSniff/MimeType.h>
@ -127,9 +129,11 @@ void HTMLScriptElement::execute_script()
// 2. Run the module script given by el's result.
(void)verify_cast<JavaScriptModuleScript>(*m_result.get<JS::NonnullGCPtr<Script>>()).run();
} else if (m_script_type == ScriptType::ImportMap) {
// FIXME: 1. Register an import map given el's relevant global object and el's result.
dbgln("FIXME: HTMLScriptElement import map support");
}
// -> "importmap"
else if (m_script_type == ScriptType::ImportMap) {
// 1. Register an import map given el's relevant global object and el's result.
m_result.get<JS::NonnullGCPtr<ImportMapParseResult>>()->register_import_map(verify_cast<Window>(relevant_global_object(*this)));
}
// 7. Decrement the ignore-destructive-writes counter of document, if it was incremented in the earlier step.
@ -433,13 +437,26 @@ void HTMLScriptElement::prepare_script()
}
// -> "importmap"
else if (m_script_type == ScriptType::ImportMap) {
// FIXME: 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.
// FIXME: need to check if relevant global object is a Window - is this correct?
auto& global = relevant_global_object(*this);
// FIXME: 2. Set el's relevant global object's import maps allowed to false.
// 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;
}
// FIXME: 3. Let result be the result of creating an import map parse result given source text and base URL.
// 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);
// FIXME: 4. Mark as ready el given result.
// 3. 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);
// 4. Mark as ready el given result.
mark_as_ready(Result(move(result)));
}
}

View file

@ -10,6 +10,7 @@
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
#include <LibWeb/HTML/CORSSettingAttribute.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/Scripting/ImportMapParseResult.h>
#include <LibWeb/HTML/Scripting/Script.h>
#include <LibWeb/ReferrerPolicy/ReferrerPolicy.h>
@ -47,7 +48,7 @@ public:
// https://html.spec.whatwg.org/multipage/scripting.html#dom-script-supports
static bool supports(JS::VM&, StringView type)
{
return type.is_one_of("classic"sv, "module"sv);
return type.is_one_of("classic"sv, "module"sv, "importmap"sv);
}
void set_source_line_number(Badge<HTMLParser>, size_t source_line_number) { m_source_line_number = source_line_number; }
@ -81,7 +82,7 @@ private:
struct Null { };
};
using Result = Variant<ResultState::Uninitialized, ResultState::Null, JS::NonnullGCPtr<HTML::Script>>;
using Result = Variant<ResultState::Uninitialized, ResultState::Null, JS::NonnullGCPtr<HTML::Script>, JS::NonnullGCPtr<HTML::ImportMapParseResult>>;
// https://html.spec.whatwg.org/multipage/scripting.html#mark-as-ready
void mark_as_ready(Result);

View file

@ -0,0 +1,211 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Console.h>
#include <LibJS/Runtime/ConsoleObject.h>
#include <LibWeb/Bindings/HostDefined.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/HTML/Scripting/Fetching.h>
#include <LibWeb/HTML/Scripting/ImportMap.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/Infra/JSON.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string
WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteString const& input, URL::URL base_url)
{
HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm) };
// 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
auto parsed = TRY(Infra::parse_json_string_to_javascript_value(realm, input));
// 2. If parsed is not an ordered map, then throw a TypeError indicating that the top-level value needs to be a JSON object.
if (!parsed.is_object())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
auto& parsed_object = parsed.as_object();
// 3. Let sortedAndNormalizedImports be an empty ordered map.
ModuleSpecifierMap sorted_and_normalised_imports;
// 4. If parsed["imports"] exists, then:
if (TRY(parsed_object.has_property("imports"))) {
auto imports = TRY(parsed_object.get("imports"));
// If parsed["imports"] is not an ordered map, then throw a TypeError indicating that the value for the "imports" top-level key needs to be a JSON object.
if (!imports.is_object())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'imports' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
// Set sortedAndNormalizedImports to the result of sorting and normalizing a module specifier map given parsed["imports"] and baseURL.
sorted_and_normalised_imports = TRY(sort_and_normalise_module_specifier_map(realm, imports.as_object(), base_url));
}
// 5. Let sortedAndNormalizedScopes be an empty ordered map.
HashMap<URL::URL, ModuleSpecifierMap> sorted_and_normalised_scopes;
// 6. If parsed["scopes"] exists, then:
if (TRY(parsed_object.has_property("scopes"))) {
auto scopes = TRY(parsed_object.get("scopes"));
// If parsed["scopes"] is not an ordered map, then throw a TypeError indicating that the value for the "scopes" top-level key needs to be a JSON object.
if (!scopes.is_object())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'scopes' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
// Set sortedAndNormalizedScopes to the result of sorting and normalizing scopes given parsed["scopes"] and baseURL.
sorted_and_normalised_scopes = TRY(sort_and_normalise_scopes(realm, scopes.as_object(), base_url));
}
// 7. If parsed's keys contains any items besides "imports" or "scopes", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
for (auto& key : parsed_object.shape().property_table().keys()) {
if (key.as_string() == "imports" || key.as_string() == "imports")
continue;
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("An invalid top-level key ({}) was present in the import map", key.as_string())));
}
// 8. Return an import map whose imports are sortedAndNormalizedImports and whose scopes are sortedAndNormalizedScopes.
ImportMap import_map;
import_map.set_imports(sorted_and_normalised_imports);
return import_map;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-specifier-key
WebIDL::ExceptionOr<Optional<DeprecatedFlyString>> normalise_specifier_key(JS::Realm& realm, DeprecatedFlyString specifier_key, URL::URL base_url)
{
// 1. If specifierKey is the empty string, then:
if (specifier_key.is_empty()) {
// 1. The user agent may report a warning to the console indicating that specifier keys may not be the empty string.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Specifier keys may not be empty")));
// 2. Return null.
return Optional<DeprecatedFlyString> {};
}
// 2. Let url be the result of resolving a URL-like module specifier, given specifierKey and baseURL.
auto url = resolve_url_like_module_specifier(specifier_key, base_url);
// 3. If url is not null, then return the serialization of url.
if (url.has_value())
return url->serialize();
// 4. Return specifierKey.
return specifier_key;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-a-module-specifier-map
WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
{
// 1. Let normalized be an empty ordered map.
ModuleSpecifierMap normalised;
// 2. For each specifierKey → value of originalMap:
for (auto& specifier_key : original_map.shape().property_table().keys()) {
auto value = TRY(original_map.get(specifier_key.as_string()));
// 1. Let normalizedSpecifierKey be the result of normalizing a specifier key given specifierKey and baseURL.
auto normalised_specifier_key = TRY(normalise_specifier_key(realm, specifier_key.as_string(), base_url));
// 2. If normalizedSpecifierKey is null, then continue.
if (!normalised_specifier_key.has_value())
continue;
// 3. If value is not a string, then:
if (!value.is_string()) {
// 1. The user agent may report a warning to the console indicating that addresses need to be strings.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Addresses need to be strings")));
// 2. Set normalized[normalizedSpecifierKey] to null.
normalised.set(normalised_specifier_key.value(), {});
// 3. Continue.
continue;
}
// 4. Let addressURL be the result of resolving a URL-like module specifier given value and baseURL.
auto address_url = resolve_url_like_module_specifier(value.as_string().byte_string(), base_url);
// 5. If addressURL is null, then:
if (!address_url.has_value()) {
// 1. The user agent may report a warning to the console indicating that the address was invalid.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Address was invalid")));
// 2. Set normalized[normalizedSpecifierKey] to null.
normalised.set(normalised_specifier_key.value(), {});
// 3. Continue.
continue;
}
// 6. If specifierKey ends with U+002F (/), and the serialization of addressURL does not end with U+002F (/), then:
if (specifier_key.as_string().ends_with("/"sv) && !address_url->serialize().ends_with("/"sv)) {
// 1. The user agent may report a warning to the console indicating that an invalid address was given for the specifier key specifierKey; since specifierKey ends with a slash, the address needs to as well.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("An invalid address was given for the specifier key ({}); since specifierKey ends with a slash, the address needs to as well", specifier_key.as_string())));
// 2. Set normalized[normalizedSpecifierKey] to null.
normalised.set(normalised_specifier_key.value(), {});
// 3. Continue.
continue;
}
// 7. Set normalized[normalizedSpecifierKey] to addressURL.
normalised.set(normalised_specifier_key.value(), address_url.value());
}
// 3. Return the result of sorting in descending order normalized, with an entry a being less than an entry b if a's key is code unit less than b's key.
return normalised;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#sorting-and-normalizing-scopes
WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
{
// 1. Let normalized be an empty ordered map.
HashMap<URL::URL, ModuleSpecifierMap> normalised;
// 2. For each scopePrefix → potentialSpecifierMap of originalMap:
for (auto& scope_prefix : original_map.shape().property_table().keys()) {
auto potential_specifier_map = TRY(original_map.get(scope_prefix.as_string()));
// 1. If potentialSpecifierMap is not an ordered map, then throw a TypeError indicating that the value of the scope with prefix scopePrefix needs to be a JSON object.
if (!potential_specifier_map.is_object())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The value of the scope with the prefix '{}' needs to be a JSON object.", scope_prefix.as_string()).release_value_but_fixme_should_propagate_errors() };
// 2. Let scopePrefixURL be the result of URL parsing scopePrefix with baseURL.
auto scope_prefix_url = DOMURL::parse(scope_prefix.as_string(), base_url);
// 3. If scopePrefixURL is failure, then:
if (!scope_prefix_url.is_valid()) {
// 1. The user agent may report a warning to the console that the scope prefix URL was not parseable.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("The scope prefix URL ({}) was not parseable", scope_prefix.as_string())));
// 2. Continue.
continue;
}
// 4. Let normalizedScopePrefix be the serialization of scopePrefixURL.
auto normalised_scope_prefix = scope_prefix_url.serialize();
// 5. Set normalized[normalizedScopePrefix] to the result of sorting and normalizing a module specifier map given potentialSpecifierMap and baseURL.
normalised.set(normalised_scope_prefix, TRY(sort_and_normalise_module_specifier_map(realm, potential_specifier_map.as_object(), base_url)));
}
// 3. Return the result of sorting in descending order normalized, with an entry a being less than an entry b if a's key is code unit less than b's key.
return normalised;
}
}

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022, networkException <networkexception@serenityos.org>
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -7,6 +8,8 @@
#pragma once
#include <AK/HashMap.h>
#include <LibURL/URL.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
@ -19,13 +22,20 @@ public:
ModuleSpecifierMap const& imports() const { return m_imports; }
ModuleSpecifierMap& imports() { return m_imports; }
void set_imports(ModuleSpecifierMap const& imports) { m_imports = imports; }
HashMap<URL::URL, ModuleSpecifierMap> const& scopes() const { return m_scopes; }
HashMap<URL::URL, ModuleSpecifierMap>& scopes() { return m_scopes; }
void set_scopes(HashMap<URL::URL, ModuleSpecifierMap> const& scopes) { m_scopes = scopes; }
private:
ModuleSpecifierMap m_imports;
HashMap<URL::URL, ModuleSpecifierMap> m_scopes;
};
WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteString const& input, URL::URL base_url);
WebIDL::ExceptionOr<Optional<DeprecatedFlyString>> normalise_specifier_key(JS::Realm& realm, DeprecatedFlyString specifier_key, 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);
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/ModuleRequest.h>
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/ImportMapParseResult.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebIDL/DOMException.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(ImportMapParseResult);
ImportMapParseResult::ImportMapParseResult() = default;
ImportMapParseResult::~ImportMapParseResult() = default;
// https://html.spec.whatwg.org/multipage/webappapis.html#create-an-import-map-parse-result
JS::NonnullGCPtr<ImportMapParseResult> ImportMapParseResult::create(JS::Realm& realm, ByteString const& input, URL::URL base_url)
{
// 1. Let result be an import map parse result whose import map is null and whose error to rethrow is null.
auto result = realm.heap().allocate<ImportMapParseResult>(realm);
result->set_error_to_rethrow(JS::js_null());
// 2. Parse an import map string given input and baseURL, catching any exceptions.
auto import_map = parse_import_map_string(realm, input, base_url);
// 2.1. If this threw an exception, then set result's error to rethrow to that exception.
// FIXME: rethrow the original exception
if (import_map.is_exception())
result->set_error_to_rethrow(JS::Error::create(realm, "Failed to parse import map string"sv));
// 2.2. Otherwise, set result's import map to the return value.
else
result->set_import_map(import_map.release_value());
// 3. Return result.
return result;
}
void ImportMapParseResult::visit_host_defined_self(JS::Cell::Visitor& visitor)
{
visitor.visit(*this);
}
void ImportMapParseResult::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_error_to_rethrow);
}
// https://html.spec.whatwg.org/multipage/webappapis.html#register-an-import-map
void ImportMapParseResult::register_import_map(Window& global)
{
// 1. If result's error to rethrow is not null, then report the exception given by result's error to rethrow and return.
if (!m_error_to_rethrow.is_null()) {
HTML::report_exception(m_error_to_rethrow, global.realm());
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.
VERIFY(m_import_map.has_value());
global.set_import_map(m_import_map.value());
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Heap/Cell.h>
#include <LibJS/Script.h>
#include <LibURL/URL.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/Scripting/ImportMap.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/webappapis.html#import-map-parse-result
class ImportMapParseResult
: public JS::Cell
, public JS::Script::HostDefined {
JS_CELL(Script, JS::Cell);
JS_DECLARE_ALLOCATOR(ImportMapParseResult);
public:
virtual ~ImportMapParseResult() override;
static JS::NonnullGCPtr<ImportMapParseResult> create(JS::Realm& realm, ByteString const& input, URL::URL base_url);
[[nodiscard]] Optional<ImportMap> const& import_map() const { return m_import_map; }
void set_import_map(ImportMap const& value) { m_import_map = value; }
[[nodiscard]] JS::Value error_to_rethrow() const { return m_error_to_rethrow; }
void set_error_to_rethrow(JS::Value value) { m_error_to_rethrow = value; }
void register_import_map(Window& global);
protected:
ImportMapParseResult();
virtual void visit_edges(Visitor&) override;
private:
virtual void visit_host_defined_self(JS::Cell::Visitor&) override;
// https://html.spec.whatwg.org/multipage/webappapis.html#impr-import-map
Optional<ImportMap> m_import_map;
// https://html.spec.whatwg.org/multipage/webappapis.html#impr-error-to-rethrow
JS::Value m_error_to_rethrow;
};
}

View file

@ -93,6 +93,7 @@ public:
JS::GCPtr<Navigable> navigable() const;
ImportMap const& import_map() const { return m_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; }