| // Copyright 2017 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/core/script/modulator_impl_base.h" |
| #include "base/feature_list.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h" |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/renderer/bindings/core/v8/module_record.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.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_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" |
| #include "third_party/blink/renderer/core/script/dynamic_module_resolver.h" |
| #include "third_party/blink/renderer/core/script/import_map.h" |
| #include "third_party/blink/renderer/core/script/js_module_script.h" |
| #include "third_party/blink/renderer/core/script/module_map.h" |
| #include "third_party/blink/renderer/core/script/module_record_resolver_impl.h" |
| #include "third_party/blink/renderer/core/script/parsed_specifier.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| |
| namespace blink { |
| |
| ExecutionContext* ModulatorImplBase::GetExecutionContext() const { |
| return ExecutionContext::From(script_state_); |
| } |
| |
| ModulatorImplBase::ModulatorImplBase(ScriptState* script_state) |
| : script_state_(script_state), |
| task_runner_(ExecutionContext::From(script_state_) |
| ->GetTaskRunner(TaskType::kNetworking)), |
| map_(MakeGarbageCollected<ModuleMap>(this)), |
| tree_linker_registry_(MakeGarbageCollected<ModuleTreeLinkerRegistry>()), |
| module_record_resolver_(MakeGarbageCollected<ModuleRecordResolverImpl>( |
| this, |
| ExecutionContext::From(script_state_))), |
| dynamic_module_resolver_( |
| MakeGarbageCollected<DynamicModuleResolver>(this)) { |
| DCHECK(script_state_); |
| DCHECK(task_runner_); |
| } |
| |
| ModulatorImplBase::~ModulatorImplBase() {} |
| |
| bool ModulatorImplBase::IsScriptingDisabled() const { |
| return !GetExecutionContext()->CanExecuteScripts(kAboutToExecuteScript); |
| } |
| |
| bool ModulatorImplBase::ImportMapsEnabled() const { |
| return RuntimeEnabledFeatures::ImportMapsEnabled(GetExecutionContext()); |
| } |
| |
| // <specdef label="fetch-a-module-script-tree" |
| // href="https://html.spec.whatwg.org/C/#fetch-a-module-script-tree"> |
| // <specdef label="fetch-a-module-worker-script-tree" |
| // href="https://html.spec.whatwg.org/C/#fetch-a-module-worker-script-tree"> |
| void ModulatorImplBase::FetchTree( |
| const KURL& url, |
| ResourceFetcher* fetch_client_settings_object_fetcher, |
| mojom::RequestContextType context_type, |
| network::mojom::RequestDestination destination, |
| const ScriptFetchOptions& options, |
| ModuleScriptCustomFetchType custom_fetch_type, |
| ModuleTreeClient* client) { |
| ModuleTreeLinker::Fetch(url, fetch_client_settings_object_fetcher, |
| context_type, destination, options, this, |
| custom_fetch_type, tree_linker_registry_, client); |
| } |
| |
| void ModulatorImplBase::FetchDescendantsForInlineScript( |
| ModuleScript* module_script, |
| ResourceFetcher* fetch_client_settings_object_fetcher, |
| mojom::RequestContextType context_type, |
| network::mojom::RequestDestination destination, |
| ModuleTreeClient* client) { |
| ModuleTreeLinker::FetchDescendantsForInlineScript( |
| module_script, fetch_client_settings_object_fetcher, context_type, |
| destination, this, ModuleScriptCustomFetchType::kNone, |
| tree_linker_registry_, client); |
| } |
| |
| void ModulatorImplBase::FetchSingle( |
| const ModuleScriptFetchRequest& request, |
| ResourceFetcher* fetch_client_settings_object_fetcher, |
| ModuleGraphLevel level, |
| ModuleScriptCustomFetchType custom_fetch_type, |
| SingleModuleClient* client) { |
| map_->FetchSingleModuleScript(request, fetch_client_settings_object_fetcher, |
| level, custom_fetch_type, client); |
| } |
| |
| ModuleScript* ModulatorImplBase::GetFetchedModuleScript(const KURL& url) { |
| return map_->GetFetchedModuleScript(url); |
| } |
| |
| // <specdef href="https://html.spec.whatwg.org/C/#resolve-a-module-specifier"> |
| KURL ModulatorImplBase::ResolveModuleSpecifier(const String& specifier, |
| const KURL& base_url, |
| String* failure_reason) { |
| ParsedSpecifier parsed_specifier = |
| ParsedSpecifier::Create(specifier, base_url); |
| |
| if (!parsed_specifier.IsValid()) { |
| if (failure_reason) { |
| *failure_reason = |
| "Invalid relative url or base scheme isn't hierarchical."; |
| } |
| return KURL(); |
| } |
| |
| // If |logger| is non-null, outputs detailed logs. |
| // The detailed log should be useful for debugging particular import maps |
| // errors, but should be supressed (i.e. |logger| should be null) in normal |
| // cases. |
| |
| base::Optional<KURL> mapped_url; |
| if (import_map_) { |
| String import_map_debug_message; |
| mapped_url = import_map_->Resolve(parsed_specifier, base_url, |
| &import_map_debug_message); |
| |
| // Output the resolution log. This is too verbose to be always shown, but |
| // will be helpful for Web developers (and also Chromium developers) for |
| // debugging import maps. |
| LOG(INFO) << import_map_debug_message; |
| |
| if (mapped_url) { |
| KURL url = *mapped_url; |
| if (!url.IsValid()) { |
| if (failure_reason) |
| *failure_reason = import_map_debug_message; |
| return KURL(); |
| } |
| return url; |
| } |
| } |
| |
| // The specifier is not mapped by import maps, either because |
| // - There are no import maps, or |
| // - The import map doesn't have an entry for |parsed_specifier|. |
| |
| switch (parsed_specifier.GetType()) { |
| case ParsedSpecifier::Type::kInvalid: |
| NOTREACHED(); |
| return KURL(); |
| |
| case ParsedSpecifier::Type::kBare: |
| // Reject bare specifiers as specced by the pre-ImportMap spec. |
| if (failure_reason) { |
| *failure_reason = |
| "Relative references must start with either \"/\", \"./\", or " |
| "\"../\"."; |
| } |
| return KURL(); |
| |
| case ParsedSpecifier::Type::kURL: |
| return parsed_specifier.GetUrl(); |
| } |
| } |
| |
| ScriptValue ModulatorImplBase::CreateTypeError(const String& message) const { |
| ScriptState::Scope scope(script_state_); |
| ScriptValue error( |
| script_state_->GetIsolate(), |
| V8ThrowException::CreateTypeError(script_state_->GetIsolate(), message)); |
| return error; |
| } |
| |
| ScriptValue ModulatorImplBase::CreateSyntaxError(const String& message) const { |
| ScriptState::Scope scope(script_state_); |
| ScriptValue error(script_state_->GetIsolate(), |
| V8ThrowException::CreateSyntaxError( |
| script_state_->GetIsolate(), message)); |
| return error; |
| } |
| |
| // <specdef href="https://wicg.github.io/import-maps/#register-an-import-map"> |
| void ModulatorImplBase::RegisterImportMap(const ImportMap* import_map, |
| ScriptValue error_to_rethrow) { |
| DCHECK(import_map); |
| DCHECK(ImportMapsEnabled()); |
| |
| // <spec step="7">If import map parse result’s error to rethrow is not null, |
| // then:</spec> |
| if (!error_to_rethrow.IsEmpty()) { |
| // <spec step="7.1">Report the exception given import map parse result’s |
| // error to rethrow. ...</spec> |
| if (!IsScriptingDisabled()) { |
| ScriptState::Scope scope(script_state_); |
| ModuleRecord::ReportException(script_state_, error_to_rethrow.V8Value()); |
| } |
| |
| // <spec step="7.2">Return.</spec> |
| return; |
| } |
| |
| // <spec step="8">Update element’s node document's import map with import map |
| // parse result’s import map.</spec> |
| // |
| // TODO(crbug.com/927119): Implement merging. Currently only one import map is |
| // allowed. |
| if (import_map_) { |
| GetExecutionContext()->AddConsoleMessage( |
| mojom::ConsoleMessageSource::kOther, mojom::ConsoleMessageLevel::kError, |
| "Multiple import maps are not yet supported. https://crbug.com/927119"); |
| return; |
| } |
| |
| import_map_ = import_map; |
| } |
| |
| bool ModulatorImplBase::HasValidContext() { |
| return script_state_->ContextIsValid(); |
| } |
| |
| void ModulatorImplBase::ResolveDynamically( |
| const String& specifier, |
| const KURL& referrer_url, |
| const ReferrerScriptInfo& referrer_info, |
| ScriptPromiseResolver* resolver) { |
| String reason; |
| if (IsDynamicImportForbidden(&reason)) { |
| resolver->Reject(V8ThrowException::CreateTypeError( |
| GetScriptState()->GetIsolate(), reason)); |
| return; |
| } |
| UseCounter::Count(GetExecutionContext(), |
| WebFeature::kDynamicImportModuleScript); |
| dynamic_module_resolver_->ResolveDynamically(specifier, referrer_url, |
| referrer_info, resolver); |
| } |
| |
| // <specdef href="https://html.spec.whatwg.org/C/#hostgetimportmetaproperties"> |
| ModuleImportMeta ModulatorImplBase::HostGetImportMetaProperties( |
| v8::Local<v8::Module> record) const { |
| // <spec step="1">Let module script be moduleRecord.[[HostDefined]].</spec> |
| const ModuleScript* module_script = |
| module_record_resolver_->GetModuleScriptFromModuleRecord(record); |
| DCHECK(module_script); |
| |
| // <spec step="3">Let urlString be module script's base URL, |
| // serialized.</spec> |
| String url_string = module_script->BaseURL().GetString(); |
| |
| // <spec step="4">Return « Record { [[Key]]: "url", [[Value]]: urlString } |
| // ».</spec> |
| return ModuleImportMeta(url_string); |
| } |
| |
| ScriptValue ModulatorImplBase::InstantiateModule( |
| v8::Local<v8::Module> module_record, |
| const KURL& source_url) { |
| UseCounter::Count(GetExecutionContext(), |
| WebFeature::kInstantiateModuleScript); |
| |
| ScriptState::Scope scope(script_state_); |
| return ModuleRecord::Instantiate(script_state_, module_record, source_url); |
| } |
| |
| Vector<Modulator::ModuleRequest> |
| ModulatorImplBase::ModuleRequestsFromModuleRecord( |
| v8::Local<v8::Module> module_record) { |
| ScriptState::Scope scope(script_state_); |
| Vector<String> specifiers = |
| ModuleRecord::ModuleRequests(script_state_, module_record); |
| Vector<TextPosition> positions = |
| ModuleRecord::ModuleRequestPositions(script_state_, module_record); |
| DCHECK_EQ(specifiers.size(), positions.size()); |
| Vector<ModuleRequest> requests; |
| requests.ReserveInitialCapacity(specifiers.size()); |
| for (wtf_size_t i = 0; i < specifiers.size(); ++i) { |
| requests.emplace_back(specifiers[i], positions[i]); |
| } |
| return requests; |
| } |
| |
| void ModulatorImplBase::ProduceCacheModuleTreeTopLevel( |
| ModuleScript* module_script) { |
| DCHECK(module_script); |
| // Since we run this asynchronously, context might be gone already, |
| // for example because the frame was detached. |
| if (!script_state_->ContextIsValid()) |
| return; |
| HeapHashSet<Member<const ModuleScript>> discovered_set; |
| ProduceCacheModuleTree(module_script, &discovered_set); |
| } |
| |
| void ModulatorImplBase::ProduceCacheModuleTree( |
| ModuleScript* module_script, |
| HeapHashSet<Member<const ModuleScript>>* discovered_set) { |
| DCHECK(module_script); |
| |
| v8::Isolate* isolate = GetScriptState()->GetIsolate(); |
| v8::HandleScope scope(isolate); |
| |
| discovered_set->insert(module_script); |
| |
| v8::Local<v8::Module> record = module_script->V8Module(); |
| DCHECK(!record.IsEmpty()); |
| |
| module_script->ProduceCache(); |
| |
| Vector<Modulator::ModuleRequest> child_specifiers = |
| ModuleRequestsFromModuleRecord(record); |
| |
| for (const auto& module_request : child_specifiers) { |
| KURL child_url = |
| module_script->ResolveModuleSpecifier(module_request.specifier); |
| |
| CHECK(child_url.IsValid()) |
| << "ModuleScript::ResolveModuleSpecifier() impl must " |
| "return a valid url."; |
| |
| ModuleScript* child_module = GetFetchedModuleScript(child_url); |
| CHECK(child_module); |
| |
| if (discovered_set->Contains(child_module)) |
| continue; |
| |
| ProduceCacheModuleTree(child_module, discovered_set); |
| } |
| } |
| |
| // <specdef href="https://html.spec.whatwg.org/C/#run-a-module-script"> |
| ModuleEvaluationResult ModulatorImplBase::ExecuteModule( |
| ModuleScript* module_script, |
| CaptureEvalErrorFlag capture_error) { |
| // <spec step="1">If rethrow errors is not given, let it be false.</spec> |
| |
| // <spec step="2">Let settings be the settings object of script.</spec> |
| // |
| // The settings object is |this|. |
| |
| // <spec step="3">Check if we can run script with settings. If this returns |
| // "do not run" then return NormalCompletion(empty).</spec> |
| if (IsScriptingDisabled()) |
| return ModuleEvaluationResult::Empty(); |
| |
| // <spec step="4">Prepare to run script given settings.</spec> |
| // |
| // This is placed here to also cover ModuleRecord::ReportException(). |
| ScriptState::EscapableScope scope(script_state_); |
| |
| // <spec step="5">Let evaluationStatus be null.</spec> |
| // |
| // |result| corresponds to "evaluationStatus of [[Type]]: throw". |
| ModuleEvaluationResult result = ModuleEvaluationResult::Empty(); |
| |
| // <spec step="6">If script's error to rethrow is not null, ...</spec> |
| if (module_script->HasErrorToRethrow()) { |
| // <spec step="6">... then set evaluationStatus to Completion { [[Type]]: |
| // throw, [[Value]]: script's error to rethrow, [[Target]]: empty }.</spec> |
| result = ModuleEvaluationResult::FromException( |
| module_script->CreateErrorToRethrow().V8Value()); |
| } else { |
| // <spec step="7">Otherwise:</spec> |
| |
| // <spec step="7.1">Let record be script's record.</spec> |
| v8::Local<v8::Module> record = module_script->V8Module(); |
| CHECK(!record.IsEmpty()); |
| |
| // <spec step="7.2">Set evaluationStatus to record.Evaluate(). ...</spec> |
| result = ModuleRecord::Evaluate(script_state_, record, |
| module_script->SourceURL()); |
| |
| // <spec step="7.2">... If Evaluate fails to complete as a result of the |
| // user agent aborting the running script, then set evaluationStatus to |
| // Completion { [[Type]]: throw, [[Value]]: a new "QuotaExceededError" |
| // DOMException, [[Target]]: empty }.</spec> |
| |
| // [not specced] Store V8 code cache on successful evaluation. |
| if (result.IsSuccess()) { |
| TaskRunner()->PostTask( |
| FROM_HERE, |
| WTF::Bind(&ModulatorImplBase::ProduceCacheModuleTreeTopLevel, |
| WrapWeakPersistent(this), WrapPersistent(module_script))); |
| } |
| } |
| |
| // <spec step="8">If evaluationStatus is an abrupt completion, then:</spec> |
| if (result.IsException()) { |
| // <spec step="8.1">If rethrow errors is true, rethrow the exception given |
| // by evaluationStatus.[[Value]].</spec> |
| if (capture_error == CaptureEvalErrorFlag::kCapture) |
| return result.Escape(&scope); |
| |
| // <spec step="8.2">Otherwise, report the exception given by |
| // evaluationStatus.[[Value]] for script.</spec> |
| ModuleRecord::ReportException(script_state_, result.GetException()); |
| } |
| |
| // <spec step="9">Clean up after running script with settings.</spec> |
| // |
| // Implemented as the ScriptState::Scope destructor. |
| if (base::FeatureList::IsEnabled(features::kTopLevelAwait)) |
| return result.Escape(&scope); |
| else |
| return ModuleEvaluationResult::Empty(); |
| } |
| |
| void ModulatorImplBase::Trace(Visitor* visitor) const { |
| visitor->Trace(script_state_); |
| visitor->Trace(map_); |
| visitor->Trace(tree_linker_registry_); |
| visitor->Trace(module_record_resolver_); |
| visitor->Trace(dynamic_module_resolver_); |
| visitor->Trace(import_map_); |
| |
| Modulator::Trace(visitor); |
| } |
| |
| } // namespace blink |