Make invalid 'type' import assertion cause a module to fail to load

Add Modulator::ModuleTypeFromRequest to get the asserted type from
a ModuleRequest's list of import assertions, and use it to make module
creation fail if an invalid type is asserted.

In preparation for subsequent import assertions changes, plumb this
module type through GetFetchedModuleScript, where it will be needed
for module map lookups since module type will be used in the module
map cache key.

Add tests for the invalid module type failure, along with tests
verifying that JS modules can still load with an empty import assertions
clause and an unsupported import assertion.

A subsequent change will require the correct type assertion to be
present to load a JSON or CSS module, and the existing JSON/CSSS module
tests will be updated to reflect the new behavior at that time.

Bug: 1132413
Change-Id: I33e3e6ae68b14d6f6c561a58c0e260a7b129d930
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2603420
Reviewed-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
Reviewed-by: Kouhei Ueno <kouhei@chromium.org>
Commit-Queue: Dan Clark <daniec@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#840714}
diff --git a/third_party/blink/renderer/bindings/bindings.gni b/third_party/blink/renderer/bindings/bindings.gni
index c46ab96..19142adc 100644
--- a/third_party/blink/renderer/bindings/bindings.gni
+++ b/third_party/blink/renderer/bindings/bindings.gni
@@ -54,6 +54,7 @@
                     "core/v8/maplike.h",
                     "core/v8/module_record.cc",
                     "core/v8/module_record.h",
+                    "core/v8/module_request.cc",
                     "core/v8/module_request.h",
                     "core/v8/native_value_traits.h",
                     "core/v8/native_value_traits_impl.cc",
diff --git a/third_party/blink/renderer/bindings/core/v8/module_request.cc b/third_party/blink/renderer/bindings/core/v8/module_request.cc
new file mode 100644
index 0000000..7e394dd
--- /dev/null
+++ b/third_party/blink/renderer/bindings/core/v8/module_request.cc
@@ -0,0 +1,25 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/bindings/core/v8/module_request.h"
+
+namespace blink {
+
+String ModuleRequest::GetModuleTypeString() const {
+  // Currently, Blink will get at most the single "type" assertion because
+  // that's the only one requested from V8 (see
+  // gin::IsoalteHolder::kSupportedImportAssertions). So this doesn't actually
+  // have to be written as a loop at all unless more import assertions are
+  // added. But, it's written as a loop anyway to be more future proof.
+  DCHECK_LE(import_assertions.size(), 1U);
+  for (const ImportAssertion& import_assertion : import_assertions) {
+    if (import_assertion.key == "type") {
+      DCHECK(!import_assertion.value.IsNull());
+      return import_assertion.value;
+    }
+  }
+  return String();
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/bindings/core/v8/module_request.h b/third_party/blink/renderer/bindings/core/v8/module_request.h
index 66d08cc..514ce22 100644
--- a/third_party/blink/renderer/bindings/core/v8/module_request.h
+++ b/third_party/blink/renderer/bindings/core/v8/module_request.h
@@ -36,6 +36,8 @@
       : specifier(specifier),
         position(position),
         import_assertions(import_assertions) {}
+
+  String GetModuleTypeString() const;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h b/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h
index 96f49dd6..b9ecfb8 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h
@@ -19,7 +19,7 @@
 
 namespace blink {
 
-enum class ModuleType { kJavaScript, kJSON, kCSS };
+enum class ModuleType { kInvalid, kJavaScript, kJSON, kCSS };
 
 // ModuleScriptCreationParams contains parameters for creating ModuleScript.
 class ModuleScriptCreationParams {
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc b/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc
index 8ac6ccc..fc4b4db 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc
@@ -269,7 +269,7 @@
       module_script_ = ValueWrapperSyntheticModuleScript::
           CreateCSSWrapperSyntheticModuleScript(params, modulator_);
       break;
-    case ModuleType::kJavaScript: {
+    case ModuleType::kJavaScript:
       // Step 9. "Let source text be the result of UTF-8 decoding response's
       // body." [spec text]
       // Step 10. "Let module script be the result of creating
@@ -277,7 +277,8 @@
       // response's url, and options." [spec text]
       module_script_ = JSModuleScript::Create(params, modulator_, options_);
       break;
-    };
+    case ModuleType::kInvalid:
+      NOTREACHED();
   }
 
   AdvanceState(State::kFinished);
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc
index 17c63ce..3bee303 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc
@@ -7,6 +7,7 @@
 #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
 #include "third_party/blink/renderer/bindings/core/v8/module_record.h"
 #include "third_party/blink/renderer/bindings/core/v8/module_request.h"
+#include "third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h"
 #include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
 #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h"
 #include "third_party/blink/renderer/core/script/module_script.h"
@@ -388,12 +389,16 @@
     // <spec step="5.1">Let url be the result of resolving a module specifier
     // given module script's base URL and requested.</spec>
     KURL url = module_script->ResolveModuleSpecifier(module_request.specifier);
+    ModuleType module_type = modulator_->ModuleTypeFromRequest(module_request);
 
     // <spec step="5.2">Assert: url is never failure, because resolving a module
     // specifier must have been previously successful with these same two
     // arguments.</spec>
     CHECK(url.IsValid()) << "ModuleScript::ResolveModuleSpecifier() impl must "
                             "return a valid url.";
+    CHECK_NE(module_type, ModuleType::kInvalid);
+    // TODO(crbug.com/1132413) Collect module types alongside URLs to include in
+    // visited_set_ and each ModuleScriptFetchRequest.
 
     // <spec step="5.3">If visited set does not contain url, then:</spec>
     if (!visited_set_.Contains(url)) {
@@ -583,6 +588,8 @@
     // moduleScript's base URL and that item. ...</spec>
     KURL child_url =
         module_script->ResolveModuleSpecifier(module_request.specifier);
+    ModuleType child_module_type =
+        modulator_->ModuleTypeFromRequest(module_request);
 
     // <spec step="5.2">... (None of these will ever fail, as otherwise
     // moduleScript would have been marked as itself having a parse
@@ -590,13 +597,14 @@
     CHECK(child_url.IsValid())
         << "ModuleScript::ResolveModuleSpecifier() impl must "
            "return a valid url.";
+    CHECK_NE(child_module_type, ModuleType::kInvalid);
 
     // <spec step="5.3">Let childModules be the list obtained by getting each
     // value in moduleMap whose key is given by an item of childURLs.</spec>
     //
     // <spec step="5.4">For each childModule of childModules:</spec>
     const ModuleScript* child_module =
-        modulator_->GetFetchedModuleScript(child_url);
+        modulator_->GetFetchedModuleScript(child_url, child_module_type);
 
     // <spec step="5.4.1">Assert: childModule is a module script (i.e., it is
     // not "fetching" or null); ...</spec>
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc
index 022f423..96560bd 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc
@@ -139,7 +139,8 @@
     pending_clients_.Set(request.Url(), client);
   }
 
-  ModuleScript* GetFetchedModuleScript(const KURL& url) override {
+  ModuleScript* GetFetchedModuleScript(const KURL& url,
+                                       ModuleType module_type) override {
     const auto& it = module_map_.find(url);
     if (it == module_map_.end())
       return nullptr;
diff --git a/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc b/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc
index 7c9be93..34b6c20 100644
--- a/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc
+++ b/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc
@@ -58,7 +58,8 @@
   // Implements Modulator:
   ScriptState* GetScriptState() final { return script_state_; }
 
-  ModuleScript* GetFetchedModuleScript(const KURL& url) final {
+  ModuleScript* GetFetchedModuleScript(const KURL& url,
+                                       ModuleType module_type) final {
     EXPECT_EQ(TestReferrerURL(), url);
     ModuleScript* module_script =
         JSModuleScript::CreateForTest(this, v8::Local<v8::Module>(), url);
diff --git a/third_party/blink/renderer/core/script/js_module_script.cc b/third_party/blink/renderer/core/script/js_module_script.cc
index 1aeae3a4..ecd5ee7 100644
--- a/third_party/blink/renderer/core/script/js_module_script.cc
+++ b/third_party/blink/renderer/core/script/js_module_script.cc
@@ -82,21 +82,30 @@
     //
     // <spec step="9.2">If url is failure, then:</spec>
     String failure_reason;
-    if (script->ResolveModuleSpecifier(requested.specifier, &failure_reason)
-            .IsValid())
-      continue;
+    bool module_specifier_is_valid =
+        script->ResolveModuleSpecifier(requested.specifier, &failure_reason)
+            .IsValid();
+    ModuleType module_type = modulator->ModuleTypeFromRequest(requested);
 
-    // <spec step="9.2.1">Let error be a new TypeError exception.</spec>
-    String error_message = "Failed to resolve module specifier \"" +
-                           requested.specifier + "\". " + failure_reason;
-    v8::Local<v8::Value> error =
-        V8ThrowException::CreateTypeError(isolate, error_message);
+    if (!module_specifier_is_valid || module_type == ModuleType::kInvalid) {
+      // <spec step="9.2.1">Let error be a new TypeError exception.</spec>
+      String error_message;
+      if (!module_specifier_is_valid) {
+        error_message = "Failed to resolve module specifier \"" +
+                        requested.specifier + "\". " + failure_reason;
+      } else {
+        error_message = "\"" + requested.GetModuleTypeString() +
+                        "\" is not a valid module type.";
+      }
+      v8::Local<v8::Value> error =
+          V8ThrowException::CreateTypeError(isolate, error_message);
 
-    // <spec step="9.2.2">Set script's parse error to error.</spec>
-    script->SetParseErrorAndClearRecord(ScriptValue(isolate, error));
+      // <spec step="9.2.2">Set script's parse error to error.</spec>
+      script->SetParseErrorAndClearRecord(ScriptValue(isolate, error));
 
-    // <spec step="9.2.3">Return script.</spec>
-    return script;
+      // <spec step="9.2.3">Return script.</spec>
+      return script;
+    }
   }
 
   // <spec step="11">Return script.</spec>
diff --git a/third_party/blink/renderer/core/script/modulator.h b/third_party/blink/renderer/core/script/modulator.h
index e10b637..ff74294 100644
--- a/third_party/blink/renderer/core/script/modulator.h
+++ b/third_party/blink/renderer/core/script/modulator.h
@@ -38,6 +38,7 @@
 class ScriptPromiseResolver;
 class ScriptState;
 class ScriptValue;
+enum class ModuleType;
 
 // A SingleModuleClient is notified when single module script node (node as in a
 // module tree graph) load is complete and its corresponding entry is created in
@@ -161,7 +162,11 @@
   // entry.
   // Note: returns nullptr if the module map entry doesn't exist, or
   // is still "fetching".
-  virtual ModuleScript* GetFetchedModuleScript(const KURL&) = 0;
+  // ModuleType indicates the resource type of the module script, e.g.
+  // JavaScript, JSON, or CSS. This is used as part of the module map cache key
+  // alongside the URL, so both are needed to retrieve the correct module. See
+  // https://github.com/whatwg/html/pull/5883
+  virtual ModuleScript* GetFetchedModuleScript(const KURL&, ModuleType) = 0;
 
   // https://html.spec.whatwg.org/C/#resolve-a-module-specifier
   virtual KURL ResolveModuleSpecifier(const String& module_request,
@@ -195,6 +200,9 @@
   virtual Vector<ModuleRequest> ModuleRequestsFromModuleRecord(
       v8::Local<v8::Module>) = 0;
 
+  virtual ModuleType ModuleTypeFromRequest(
+      const ModuleRequest& module_request) const = 0;
+
   virtual ModuleScriptFetcher* CreateModuleScriptFetcher(
       ModuleScriptCustomFetchType,
       base::PassKey<ModuleScriptLoader> pass_key) = 0;
diff --git a/third_party/blink/renderer/core/script/modulator_impl_base.cc b/third_party/blink/renderer/core/script/modulator_impl_base.cc
index 7a62238..6fe852c 100644
--- a/third_party/blink/renderer/core/script/modulator_impl_base.cc
+++ b/third_party/blink/renderer/core/script/modulator_impl_base.cc
@@ -13,6 +13,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/web_feature.h"
+#include "third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h"
 #include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
 #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h"
 #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h"
@@ -100,8 +101,10 @@
                                 level, custom_fetch_type, client);
 }
 
-ModuleScript* ModulatorImplBase::GetFetchedModuleScript(const KURL& url) {
-  return map_->GetFetchedModuleScript(url);
+ModuleScript* ModulatorImplBase::GetFetchedModuleScript(
+    const KURL& url,
+    ModuleType module_type) {
+  return map_->GetFetchedModuleScript(url, module_type);
 }
 
 // <specdef href="https://html.spec.whatwg.org/C/#resolve-a-module-specifier">
@@ -274,6 +277,31 @@
   return ModuleRecord::ModuleRequests(script_state_, module_record);
 }
 
+ModuleType ModulatorImplBase::ModuleTypeFromRequest(
+    const ModuleRequest& module_request) const {
+  String module_type_string = module_request.GetModuleTypeString();
+  if (module_type_string.IsNull()) {
+    // Per https://github.com/whatwg/html/pull/5883, if no type assertion is
+    // provided then the import should be treated as a JavaScript module.
+    return ModuleType::kJavaScript;
+  } else if (base::FeatureList::IsEnabled(blink::features::kJSONModules) &&
+             module_type_string == "json") {
+    // Per https://github.com/whatwg/html/pull/5658, a "json" type assertion
+    // indicates that the import should be treated as a JSON module script.
+    return ModuleType::kJSON;
+  } else if (RuntimeEnabledFeatures::CSSModulesEnabled() &&
+             module_type_string == "css") {
+    // Per https://github.com/whatwg/html/pull/4898, a "css" type assertion
+    // indicates that the import should be treated as a CSS module script.
+    return ModuleType::kCSS;
+  } else {
+    // Per https://github.com/whatwg/html/pull/5883, if an unsupported type
+    // assertion is provided then the import should be treated as an error
+    // similar to an invalid module specifier.
+    return ModuleType::kInvalid;
+  }
+}
+
 void ModulatorImplBase::ProduceCacheModuleTreeTopLevel(
     ModuleScript* module_script) {
   DCHECK(module_script);
@@ -307,11 +335,15 @@
     KURL child_url =
         module_script->ResolveModuleSpecifier(module_request.specifier);
 
+    ModuleType child_module_type = this->ModuleTypeFromRequest(module_request);
+    CHECK_NE(child_module_type, ModuleType::kInvalid);
+
     CHECK(child_url.IsValid())
         << "ModuleScript::ResolveModuleSpecifier() impl must "
            "return a valid url.";
 
-    ModuleScript* child_module = GetFetchedModuleScript(child_url);
+    ModuleScript* child_module =
+        GetFetchedModuleScript(child_url, child_module_type);
     CHECK(child_module);
 
     if (discovered_set->Contains(child_module))
diff --git a/third_party/blink/renderer/core/script/modulator_impl_base.h b/third_party/blink/renderer/core/script/modulator_impl_base.h
index 2e3ac50..717164b 100644
--- a/third_party/blink/renderer/core/script/modulator_impl_base.h
+++ b/third_party/blink/renderer/core/script/modulator_impl_base.h
@@ -71,7 +71,7 @@
                    ModuleGraphLevel,
                    ModuleScriptCustomFetchType,
                    SingleModuleClient*) override;
-  ModuleScript* GetFetchedModuleScript(const KURL&) override;
+  ModuleScript* GetFetchedModuleScript(const KURL&, ModuleType) override;
   bool HasValidContext() override;
   KURL ResolveModuleSpecifier(const String& module_request,
                               const KURL& base_url,
@@ -92,6 +92,8 @@
   ScriptValue InstantiateModule(v8::Local<v8::Module>, const KURL&) override;
   Vector<ModuleRequest> ModuleRequestsFromModuleRecord(
       v8::Local<v8::Module>) override;
+  ModuleType ModuleTypeFromRequest(
+      const ModuleRequest& module_request) const override;
 
   // Populates |reason| and returns true if the dynamic import is disallowed on
   // the associated execution context. In that case, a caller of this function
diff --git a/third_party/blink/renderer/core/script/module_map.cc b/third_party/blink/renderer/core/script/module_map.cc
index 1fe0c286..85dbdf0 100644
--- a/third_party/blink/renderer/core/script/module_map.cc
+++ b/third_party/blink/renderer/core/script/module_map.cc
@@ -142,7 +142,10 @@
     entry->AddClient(client);
 }
 
-ModuleScript* ModuleMap::GetFetchedModuleScript(const KURL& url) const {
+ModuleScript* ModuleMap::GetFetchedModuleScript(const KURL& url,
+                                                ModuleType module_type) const {
+  // TODO(crbug.com/1132413) Make module type part of cache key and use
+  // module_type in this lookup.
   MapImpl::const_iterator it = map_.find(url);
   if (it == map_.end())
     return nullptr;
diff --git a/third_party/blink/renderer/core/script/module_map.h b/third_party/blink/renderer/core/script/module_map.h
index da36d05..8e98235 100644
--- a/third_party/blink/renderer/core/script/module_map.h
+++ b/third_party/blink/renderer/core/script/module_map.h
@@ -22,6 +22,7 @@
 class SingleModuleClient;
 enum class ModuleGraphLevel;
 enum class ModuleScriptCustomFetchType;
+enum class ModuleType;
 
 // A ModuleMap implements "module map" spec.
 // https://html.spec.whatwg.org/C/#module-map
@@ -48,7 +49,7 @@
   // Synchronously get the ModuleScript for a given URL.
   // If the URL wasn't fetched, or is currently being fetched, this returns a
   // nullptr.
-  ModuleScript* GetFetchedModuleScript(const KURL&) const;
+  ModuleScript* GetFetchedModuleScript(const KURL&, ModuleType) const;
 
   Modulator* GetModulator() { return modulator_; }
 
diff --git a/third_party/blink/renderer/core/script/module_record_resolver_impl.cc b/third_party/blink/renderer/core/script/module_record_resolver_impl.cc
index a848218..a615608 100644
--- a/third_party/blink/renderer/core/script/module_record_resolver_impl.cc
+++ b/third_party/blink/renderer/core/script/module_record_resolver_impl.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/script/module_record_resolver_impl.h"
 
 #include "third_party/blink/renderer/bindings/core/v8/module_record.h"
+#include "third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h"
 #include "third_party/blink/renderer/core/script/modulator.h"
 #include "third_party/blink/renderer/core/script/module_script.h"
 
@@ -92,17 +93,19 @@
   // <spec step="5">Let url be the result of resolving a module specifier given
   // base URL and specifier.</spec>
   KURL url = referrer_module->ResolveModuleSpecifier(module_request.specifier);
+  ModuleType child_module_type =
+      modulator_->ModuleTypeFromRequest(module_request);
 
   // <spec step="6">Assert: url is never failure, because resolving a module
   // specifier must have been previously successful with these same two
   // arguments ...</spec>
   DCHECK(url.IsValid());
+  CHECK_NE(child_module_type, ModuleType::kInvalid);
 
   // <spec step="7">Let resolved module script be moduleMap[url]. (This entry
   // must exist for us to have gotten to this point.)</spec>
-  // TODO(crbug.com/1132413): Use import assertions along with URL to get
-  // resolved module script.
-  ModuleScript* module_script = modulator_->GetFetchedModuleScript(url);
+  ModuleScript* module_script =
+      modulator_->GetFetchedModuleScript(url, child_module_type);
 
   // <spec step="8">Assert: resolved module script is a module script (i.e., is
   // not null or "fetching").</spec>
diff --git a/third_party/blink/renderer/core/script/module_record_resolver_impl_test.cc b/third_party/blink/renderer/core/script/module_record_resolver_impl_test.cc
index e768492..000a915 100644
--- a/third_party/blink/renderer/core/script/module_record_resolver_impl_test.cc
+++ b/third_party/blink/renderer/core/script/module_record_resolver_impl_test.cc
@@ -51,7 +51,8 @@
     return KURL(base_url, module_request);
   }
 
-  ModuleScript* GetFetchedModuleScript(const KURL&) override;
+  ModuleScript* GetFetchedModuleScript(const KURL&,
+                                       ModuleType module_type) override;
 
   Member<ScriptState> script_state_;
   int get_fetched_module_script_called_ = 0;
@@ -66,7 +67,8 @@
 }
 
 ModuleScript* ModuleRecordResolverImplTestModulator::GetFetchedModuleScript(
-    const KURL& url) {
+    const KURL& url,
+    ModuleType module_type) {
   get_fetched_module_script_called_++;
   fetched_url_ = url;
   return module_script_.Get();
diff --git a/third_party/blink/renderer/core/testing/dummy_modulator.cc b/third_party/blink/renderer/core/testing/dummy_modulator.cc
index ed65dfe..8f52fc0 100644
--- a/third_party/blink/renderer/core/testing/dummy_modulator.cc
+++ b/third_party/blink/renderer/core/testing/dummy_modulator.cc
@@ -6,6 +6,7 @@
 
 #include "third_party/blink/renderer/bindings/core/v8/module_record.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_value.h"
+#include "third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h"
 #include "third_party/blink/renderer/core/script/module_record_resolver.h"
 
 namespace blink {
@@ -100,7 +101,7 @@
   NOTREACHED();
 }
 
-ModuleScript* DummyModulator::GetFetchedModuleScript(const KURL&) {
+ModuleScript* DummyModulator::GetFetchedModuleScript(const KURL&, ModuleType) {
   NOTREACHED();
   return nullptr;
 }
@@ -169,6 +170,11 @@
   return Vector<ModuleRequest>();
 }
 
+ModuleType DummyModulator::ModuleTypeFromRequest(
+    const ModuleRequest& module_request) const {
+  return ModuleType::kJavaScript;
+}
+
 ModuleScriptFetcher* DummyModulator::CreateModuleScriptFetcher(
     ModuleScriptCustomFetchType,
     base::PassKey<ModuleScriptLoader> pass_key) {
diff --git a/third_party/blink/renderer/core/testing/dummy_modulator.h b/third_party/blink/renderer/core/testing/dummy_modulator.h
index 8ae521d2..4a00d12 100644
--- a/third_party/blink/renderer/core/testing/dummy_modulator.h
+++ b/third_party/blink/renderer/core/testing/dummy_modulator.h
@@ -57,7 +57,7 @@
       mojom::blink::RequestContextType context_type,
       network::mojom::RequestDestination destination,
       ModuleTreeClient*) override;
-  ModuleScript* GetFetchedModuleScript(const KURL&) override;
+  ModuleScript* GetFetchedModuleScript(const KURL&, ModuleType) override;
   KURL ResolveModuleSpecifier(const String&, const KURL&, String*) override;
   bool HasValidContext() override;
   void ResolveDynamically(const String& specifier,
@@ -76,6 +76,8 @@
   ScriptValue InstantiateModule(v8::Local<v8::Module>, const KURL&) override;
   Vector<ModuleRequest> ModuleRequestsFromModuleRecord(
       v8::Local<v8::Module>) override;
+  ModuleType ModuleTypeFromRequest(
+      const ModuleRequest& module_request) const override;
   ModuleScriptFetcher* CreateModuleScriptFetcher(
       ModuleScriptCustomFetchType,
       base::PassKey<ModuleScriptLoader>) override;
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 2942abc..c6e82d32 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -632,6 +632,11 @@
     "args": ["--enable-blink-features=CSSModules"]
   },
   {
+    "prefix": "import-assertions",
+    "bases": ["external/wpt/html/semantics/scripting-1/the-script-element/import-assertions"],
+    "args": ["--js-flags=--harmony-import-assertions"]
+  },
+  {
     "prefix": "import-maps-disabled",
     "bases": ["external/wpt/import-maps/not-as-classic-script.html"],
     "args": ["--disable-blink-features=ImportMaps"]
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause-expected.txt b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause-expected.txt
new file mode 100644
index 0000000..8139c2d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+Harness Error. harness_status.status = 1 , harness_status.message = Uncaught SyntaxError: Unexpected identifier
+FAIL Test that no error occurs when an empty import assertion clause is provided. assert_array_equals: lengths differ, expected array ["hello", "empty-assertion-clause"] length 2, got [object "SyntaxError: Unexpected identifier"] length 1
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause.html b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause.html
new file mode 100644
index 0000000..3a7c371
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Handling of empty import assertion clause</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+    window.log = [];
+
+    window.addEventListener("error", ev => log.push(ev.error));
+
+    const test_load = async_test(
+        "Test that no error occurs when an empty import assertion clause is provided.");
+    window.addEventListener("load", test_load.step_func_done(ev => {
+      assert_array_equals(window.log, ["hello", "empty-assertion-clause"]);
+    }));
+
+    function unreachable() { log.push("unexpected"); }
+</script>
+<script type="module" src="./empty-assertion-clause.js" onerror="unreachable()"></script>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause.js b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause.js
new file mode 100644
index 0000000..6913dd61
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause.js
@@ -0,0 +1,2 @@
+import "./hello.js" assert { };
+log.push("empty-assertion-clause");
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-type-assertion.js b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-type-assertion.js
new file mode 100644
index 0000000..5bb9b1d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-type-assertion.js
@@ -0,0 +1,2 @@
+import "./hello.js#2" assert { type: "" };
+log.push("empty-type-assertion");
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/hello.js b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/hello.js
new file mode 100644
index 0000000..2f34844
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/hello.js
@@ -0,0 +1 @@
+log.push("hello");
\ No newline at end of file
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error-expected.txt b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error-expected.txt
new file mode 100644
index 0000000..e73d069
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+FAIL Test that invalid module type assertion leads to TypeError on window. assert_equals: expected function "function TypeError() { [native code] }" but got function "function SyntaxError() { [native code] }"
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error.html b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error.html
new file mode 100644
index 0000000..d3399f0
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<title>Handling of invalid module type import assertions</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+    setup({allow_uncaught_exception: true});
+
+    window.log = [];
+
+    window.addEventListener("error", ev => log.push(ev.error));
+
+    const test_load = async_test(
+        "Test that invalid module type assertion leads to TypeError on window.");
+    window.addEventListener("load", test_load.step_func_done(ev => {
+      assert_equals(log.length, 2);
+      assert_equals(log[0].constructor, TypeError);
+      assert_equals(log[1].constructor, TypeError);
+    }));
+
+    function unreachable() { log.push("unexpected"); }
+</script>
+<script type="module" src="./invalid-type-assertion.js" onerror="unreachable()"></script>
+<script type="module" src="./empty-type-assertion.js" onerror="unreachable()"></script>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion.js b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion.js
new file mode 100644
index 0000000..e28c017
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion.js
@@ -0,0 +1,2 @@
+import "./hello.js#1" assert { type: "notARealType" };
+log.push("invalid-type-assertion");
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion-expected.txt b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion-expected.txt
new file mode 100644
index 0000000..aa0317a
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+Harness Error. harness_status.status = 1 , harness_status.message = Uncaught SyntaxError: Unexpected identifier
+FAIL Test that no error occurs when an unsupported import assertion is provided. assert_array_equals: lengths differ, expected array ["hello", "unsupported-assertion"] length 2, got [object "SyntaxError: Unexpected identifier"] length 1
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion.html b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion.html
new file mode 100644
index 0000000..edda2d7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<title>Handling of unsupported assertion</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+    window.log = [];
+
+    window.addEventListener("error", ev => log.push(ev.error));
+
+    const test_load = async_test(
+        "Test that no error occurs when an unsupported import assertion is provided.");
+    window.addEventListener("load", test_load.step_func_done(ev => {
+      assert_array_equals(window.log, ["hello", "unsupported-assertion"]);
+    }));
+
+    function unreachable() { log.push("unexpected"); }
+</script>
+<script type="module" src="./unsupported-assertion.js" onerror="unreachable()"></script>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion.js b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion.js
new file mode 100644
index 0000000..45f6d60
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion.js
@@ -0,0 +1,2 @@
+import "./hello.js" assert { unsupportedAssertionKey: "unsupportedAssertionValue" };
+log.push("unsupported-assertion");
diff --git a/third_party/blink/web_tests/virtual/import-assertions/README.md b/third_party/blink/web_tests/virtual/import-assertions/README.md
new file mode 100644
index 0000000..1412037
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/import-assertions/README.md
@@ -0,0 +1,4 @@
+This suite runs the tests with
+--js-flags=--harmony-import-assertions
+
+See https://tc39.es/proposal-import-assertions/
\ No newline at end of file
diff --git a/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause-expected.txt b/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause-expected.txt
new file mode 100644
index 0000000..b59350f
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/empty-assertion-clause-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+PASS Test that no error occurs when an empty import assertion clause is provided.
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error-expected.txt b/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error-expected.txt
new file mode 100644
index 0000000..7908553
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/invalid-type-assertion-error-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+PASS Test that invalid module type assertion leads to TypeError on window.
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion-expected.txt b/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion-expected.txt
new file mode 100644
index 0000000..ece2cbb
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/import-assertions/external/wpt/html/semantics/scripting-1/the-script-element/import-assertions/unsupported-assertion-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+PASS Test that no error occurs when an unsupported import assertion is provided.
+Harness: the test ran to completion.
+