Bug 1688879 - Part 3: Parse and register an import map. r=jonco,yulia
Implement https://wicg.github.io/import-maps/#parse-an-import-map-string, and https://wicg.github.io/import-maps/#register-an-import-map Differential Revision: https://phabricator.services.mozilla.com/D142071
This commit is contained in:
@@ -323,6 +323,13 @@ ScriptSourceNotAllowed=<script> source URI is not allowed in this document: “%
|
||||
ModuleSourceNotAllowed=Module source URI is not allowed in this document: “%S”.
|
||||
WebExtContentScriptModuleSourceNotAllowed=WebExtension content scripts may only load modules with moz-extension URLs and not: “%S”.
|
||||
ModuleResolveFailure=Error resolving module specifier “%S”. Relative module specifiers must start with “./”, “../” or “/”.
|
||||
ImportMapInvalidTopLevelKey=An invalid top-level key “%S” was present in the import map.
|
||||
ImportMapEmptySpecifierKeys=Specifier keys cannot be empty strings.
|
||||
ImportMapAddressesNotStrings=Addresses need to be strings.
|
||||
ImportMapInvalidAddress=Address “%S” was invalid.
|
||||
# %1$S is the specifier key, %2$S is the URL.
|
||||
ImportMapAddressNotEndsWithSlash=An invalid address was given for the specifier key “%1$S”; since “%1$S” ended in a slash, the address “%2$S” needs to as well.
|
||||
ImportMapScopePrefixNotParseable=The scope prefix URL “%S” was not parseable.
|
||||
# LOCALIZATION NOTE: %1$S is the invalid property value and %2$S is the property name.
|
||||
InvalidKeyframePropertyValue=Keyframe property value “%1$S” is invalid according to the syntax for “%2$S”.
|
||||
# LOCALIZATION NOTE: Do not translate "ReadableStream".
|
||||
|
||||
@@ -822,7 +822,7 @@ already_AddRefed<ScriptLoadRequest> ScriptLoader::CreateLoadRequest(
|
||||
aCORSMode, aReferrerPolicy, aTriggeringPrincipal, domElement);
|
||||
RefPtr<ScriptLoadContext> context = new ScriptLoadContext();
|
||||
|
||||
if (aKind == ScriptKind::eClassic) {
|
||||
if (aKind == ScriptKind::eClassic || aKind == ScriptKind::eImportMap) {
|
||||
RefPtr<ScriptLoadRequest> aRequest = new ScriptLoadRequest(
|
||||
aKind, aURI, fetchOptions, aIntegrity, referrer, context);
|
||||
|
||||
@@ -1166,6 +1166,29 @@ bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement,
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request->IsImportMapRequest()) {
|
||||
UniquePtr<ImportMap> importMap = mModuleLoader->ParseImportMap(request);
|
||||
|
||||
// https://wicg.github.io/import-maps/#register-an-import-map
|
||||
//
|
||||
// Step 1. If element’s the script’s result is null, then fire an event
|
||||
// named error at element, and return.
|
||||
if (!importMap) {
|
||||
NS_DispatchToCurrentThread(
|
||||
NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement,
|
||||
&nsIScriptElement::FireErrorEvent));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3. Assert: element’s the script’s type is "importmap".
|
||||
MOZ_ASSERT(aElement->GetScriptIsImportMap());
|
||||
|
||||
// Step 4 to step 9 is done in RegisterImportMap.
|
||||
mModuleLoader->RegisterImportMap(std::move(importMap));
|
||||
return false;
|
||||
}
|
||||
|
||||
request->mState = ScriptLoadRequest::State::Ready;
|
||||
if (aElement->GetParserCreated() == FROM_PARSER_XSLT &&
|
||||
(!ReadyToExecuteParserBlockingScripts() || !mXSLTRequests.isEmpty())) {
|
||||
@@ -2994,6 +3017,19 @@ void ScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest,
|
||||
params, nullptr, u""_ns, lineNo, columnNo);
|
||||
}
|
||||
|
||||
void ScriptLoader::ReportWarningToConsole(
|
||||
ScriptLoadRequest* aRequest, const char* aMessageName,
|
||||
const nsTArray<nsString>& aParams) const {
|
||||
nsIScriptElement* element =
|
||||
aRequest->GetScriptLoadContext()->GetScriptElement();
|
||||
uint32_t lineNo = element ? element->GetScriptLineNumber() : 0;
|
||||
uint32_t columnNo = element ? element->GetScriptColumnNumber() : 0;
|
||||
nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
|
||||
"Script Loader"_ns, mDocument,
|
||||
nsContentUtils::eDOM_PROPERTIES, aMessageName,
|
||||
aParams, nullptr, u""_ns, lineNo, columnNo);
|
||||
}
|
||||
|
||||
void ScriptLoader::ReportPreloadErrorsToConsole(ScriptLoadRequest* aRequest) {
|
||||
if (NS_FAILED(aRequest->GetScriptLoadContext()->mUnreportedPreloadError)) {
|
||||
ReportErrorToConsole(
|
||||
|
||||
@@ -551,6 +551,11 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
|
||||
|
||||
void ReportErrorToConsole(ScriptLoadRequest* aRequest,
|
||||
nsresult aResult) const override;
|
||||
|
||||
void ReportWarningToConsole(
|
||||
ScriptLoadRequest* aRequest, const char* aMessageName,
|
||||
const nsTArray<nsString>& aParams = nsTArray<nsString>()) const override;
|
||||
|
||||
void ReportPreloadErrorsToConsole(ScriptLoadRequest* aRequest);
|
||||
|
||||
nsresult AttemptAsyncScriptCompile(ScriptLoadRequest* aRequest,
|
||||
|
||||
441
js/loader/ImportMap.cpp
Normal file
441
js/loader/ImportMap.cpp
Normal file
@@ -0,0 +1,441 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "ImportMap.h"
|
||||
|
||||
#include "js/Array.h" // IsArrayObject
|
||||
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
|
||||
#include "js/JSON.h" // JS_ParseJSON
|
||||
#include "ModuleLoaderBase.h" // ScriptLoaderInterface
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsIScriptElement.h"
|
||||
#include "nsIScriptError.h"
|
||||
#include "nsJSUtils.h" // nsAutoJSString
|
||||
#include "nsNetUtil.h" // NS_NewURI
|
||||
#include "ScriptLoadRequest.h"
|
||||
|
||||
using JS::SourceText;
|
||||
using mozilla::LazyLogModule;
|
||||
using mozilla::MakeUnique;
|
||||
using mozilla::UniquePtr;
|
||||
|
||||
namespace JS::loader {
|
||||
|
||||
LazyLogModule ImportMap::gImportMapLog("ImportMap");
|
||||
|
||||
#undef LOG
|
||||
#define LOG(args) \
|
||||
MOZ_LOG(ImportMap::gImportMapLog, mozilla::LogLevel::Debug, args)
|
||||
|
||||
#define LOG_ENABLED() \
|
||||
MOZ_LOG_TEST(ImportMap::gImportMapLog, mozilla::LogLevel::Debug)
|
||||
|
||||
void ReportWarningHelper::Report(const char* aMessageName,
|
||||
const nsTArray<nsString>& aParams) const {
|
||||
mLoader->ReportWarningToConsole(mRequest, aMessageName, aParams);
|
||||
}
|
||||
|
||||
// https://wicg.github.io/import-maps/#parse-a-url-like-import-specifier
|
||||
static already_AddRefed<nsIURI> ParseURLLikeImportSpecifier(
|
||||
const nsAString& aSpecifier, nsIURI* aBaseURL) {
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
nsresult rv;
|
||||
|
||||
// Step 1. If specifier starts with "/", "./", or "../", then:
|
||||
if (StringBeginsWith(aSpecifier, u"/"_ns) ||
|
||||
StringBeginsWith(aSpecifier, u"./"_ns) ||
|
||||
StringBeginsWith(aSpecifier, u"../"_ns)) {
|
||||
// Step 1.1. Let url be the result of parsing specifier with baseURL as the
|
||||
// base URL.
|
||||
rv = NS_NewURI(getter_AddRefs(uri), aSpecifier, nullptr, aBaseURL);
|
||||
// Step 1.2. If url is failure, then return null.
|
||||
if (NS_FAILED(rv)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 1.3. Return url.
|
||||
return uri.forget();
|
||||
}
|
||||
|
||||
// Step 2. Let url be the result of parsing specifier (with no base URL).
|
||||
rv = NS_NewURI(getter_AddRefs(uri), aSpecifier);
|
||||
// Step 3. If url is failure, then return null.
|
||||
if (NS_FAILED(rv)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 4. Return url.
|
||||
return uri.forget();
|
||||
}
|
||||
|
||||
// https://wicg.github.io/import-maps/#normalize-a-specifier-key
|
||||
static void NormalizeSpecifierKey(const nsAString& aSpecifierKey,
|
||||
nsIURI* aBaseURL,
|
||||
const ReportWarningHelper& aWarning,
|
||||
nsAString& aRetVal) {
|
||||
// Step 1. If specifierKey is the empty string, then:
|
||||
if (aSpecifierKey.IsEmpty()) {
|
||||
// Step 1.1. Report a warning to the console that specifier keys cannot be
|
||||
// the empty string.
|
||||
aWarning.Report("ImportMapEmptySpecifierKeys");
|
||||
|
||||
// Step 1.2. Return null.
|
||||
aRetVal = EmptyString();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2. Let url be the result of parsing a URL-like import specifier, given
|
||||
// specifierKey and baseURL.
|
||||
nsCOMPtr<nsIURI> url = ParseURLLikeImportSpecifier(aSpecifierKey, aBaseURL);
|
||||
|
||||
// Step 3. If url is not null, then return the serialization of url.
|
||||
if (url) {
|
||||
aRetVal = NS_ConvertUTF8toUTF16(url->GetSpecOrDefault());
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4. Return specifierKey.
|
||||
aRetVal = aSpecifierKey;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/import-maps/#sort-and-normalize-a-specifier-map
|
||||
static UniquePtr<SpecifierMap> SortAndNormalizeSpecifierMap(
|
||||
JSContext* aCx, JS::HandleObject aOriginalMap, nsIURI* aBaseURL,
|
||||
const ReportWarningHelper& aWarning) {
|
||||
// Step 1. Let normalized be an empty map.
|
||||
UniquePtr<SpecifierMap> normalized = MakeUnique<SpecifierMap>();
|
||||
|
||||
JS::Rooted<JS::IdVector> specifierKeys(aCx, JS::IdVector(aCx));
|
||||
if (!JS_Enumerate(aCx, aOriginalMap, &specifierKeys)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 2. For each specifierKey → value of originalMap,
|
||||
for (size_t i = 0; i < specifierKeys.length(); i++) {
|
||||
const JS::RootedId specifierId(aCx, specifierKeys[i]);
|
||||
nsAutoJSString specifierKey;
|
||||
NS_ENSURE_TRUE(specifierKey.init(aCx, specifierId), nullptr);
|
||||
|
||||
// Step 2.1. Let normalizedSpecifierKey be the result of normalizing a
|
||||
// specifier key given specifierKey and baseURL.
|
||||
nsString normalizedSpecifierKey;
|
||||
NormalizeSpecifierKey(specifierKey, aBaseURL, aWarning,
|
||||
normalizedSpecifierKey);
|
||||
|
||||
// Step 2.2. If normalizedSpecifierKey is null, then continue.
|
||||
if (normalizedSpecifierKey.IsEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JS::RootedValue idVal(aCx);
|
||||
NS_ENSURE_TRUE(JS_GetPropertyById(aCx, aOriginalMap, specifierId, &idVal),
|
||||
nullptr);
|
||||
// Step 2.3. If value is not a string, then:
|
||||
if (!idVal.isString()) {
|
||||
// Step 2.3.1. Report a warning to the console that addresses need to
|
||||
// be strings.
|
||||
aWarning.Report("ImportMapAddressesNotStrings");
|
||||
|
||||
// Step 2.3.2. Set normalized[normalizedSpecifierKey] to null.
|
||||
normalized->insert_or_assign(normalizedSpecifierKey, nullptr);
|
||||
|
||||
// Step 2.3.3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
nsAutoJSString value;
|
||||
NS_ENSURE_TRUE(value.init(aCx, idVal), nullptr);
|
||||
|
||||
// Step 2.4. Let addressURL be the result of parsing a URL-like import
|
||||
// specifier given value and baseURL.
|
||||
nsCOMPtr<nsIURI> addressURL = ParseURLLikeImportSpecifier(value, aBaseURL);
|
||||
|
||||
// Step 2.5. If addressURL is null, then:
|
||||
if (!addressURL) {
|
||||
// Step 2.5.1. Report a warning to the console that the address was
|
||||
// invalid.
|
||||
AutoTArray<nsString, 1> params;
|
||||
params.AppendElement(value);
|
||||
aWarning.Report("ImportMapInvalidAddress", params);
|
||||
|
||||
// Step 2.5.2. Set normalized[normalizedSpecifierKey] to null.
|
||||
normalized->insert_or_assign(normalizedSpecifierKey, nullptr);
|
||||
|
||||
// Step 2.5.3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
nsCString address = addressURL->GetSpecOrDefault();
|
||||
// Step 2.6. If specifierKey ends with U+002F (/), and the serialization
|
||||
// of addressURL does not end with U+002F (/), then:
|
||||
if (StringEndsWith(specifierKey, u"/"_ns) &&
|
||||
!StringEndsWith(address, "/"_ns)) {
|
||||
// Step 2.6.1. Report a warning to the console that an invalid address
|
||||
// was given for the specifier key specifierKey; since specifierKey
|
||||
// ended in a slash, the address needs to as well.
|
||||
AutoTArray<nsString, 2> params;
|
||||
params.AppendElement(specifierKey);
|
||||
params.AppendElement(NS_ConvertUTF8toUTF16(address));
|
||||
aWarning.Report("ImportMapAddressNotEndsWithSlash", params);
|
||||
|
||||
// Step 2.6.2. Set normalized[normalizedSpecifierKey] to null.
|
||||
normalized->insert_or_assign(normalizedSpecifierKey, nullptr);
|
||||
|
||||
// Step 2.6.3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG(("ImportMap::SortAndNormalizeSpecifierMap {%s, %s}",
|
||||
NS_ConvertUTF16toUTF8(normalizedSpecifierKey).get(),
|
||||
addressURL->GetSpecOrDefault().get()));
|
||||
|
||||
// Step 2.7. Set normalized[normalizedSpecifierKey] to addressURL.
|
||||
normalized->insert_or_assign(normalizedSpecifierKey, addressURL);
|
||||
}
|
||||
|
||||
// Step 3: Return the result of sorting normalized, with an entry a being
|
||||
// less than an entry b if b’s key is code unit less than a’s key.
|
||||
//
|
||||
// Impl note: The sorting is done when inserting the entry.
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Check if it's a map defined in
|
||||
// https://infra.spec.whatwg.org/#ordered-map
|
||||
//
|
||||
// If it is, *aIsMap will be set to true.
|
||||
static bool IsMapObject(JSContext* aCx, JS::HandleValue aMapVal, bool* aIsMap) {
|
||||
MOZ_ASSERT(aIsMap);
|
||||
|
||||
*aIsMap = false;
|
||||
if (!aMapVal.isObject()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isArray;
|
||||
if (!IsArrayObject(aCx, aMapVal, &isArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*aIsMap = !isArray;
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/import-maps/#sort-and-normalize-scopes
|
||||
static UniquePtr<ScopeMap> SortAndNormalizeScopes(
|
||||
JSContext* aCx, JS::HandleObject aOriginalMap, nsIURI* aBaseURL,
|
||||
const ReportWarningHelper& aWarning) {
|
||||
JS::Rooted<JS::IdVector> scopeKeys(aCx, JS::IdVector(aCx));
|
||||
if (!JS_Enumerate(aCx, aOriginalMap, &scopeKeys)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 1. Let normalized be an empty map.
|
||||
UniquePtr<ScopeMap> normalized = MakeUnique<ScopeMap>();
|
||||
|
||||
// Step 2. For each scopePrefix → potentialSpecifierMap of originalMap,
|
||||
for (size_t i = 0; i < scopeKeys.length(); i++) {
|
||||
const JS::RootedId scopeKey(aCx, scopeKeys[i]);
|
||||
nsAutoJSString scopePrefix;
|
||||
NS_ENSURE_TRUE(scopePrefix.init(aCx, scopeKey), nullptr);
|
||||
|
||||
// Step 2.1. If potentialSpecifierMap is not a map, then throw a TypeError
|
||||
// indicating that the value of the scope with prefix scopePrefix needs to
|
||||
// be a JSON object.
|
||||
JS::RootedValue mapVal(aCx);
|
||||
NS_ENSURE_TRUE(JS_GetPropertyById(aCx, aOriginalMap, scopeKey, &mapVal),
|
||||
nullptr);
|
||||
|
||||
bool isMap;
|
||||
if (!IsMapObject(aCx, mapVal, &isMap)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!isMap) {
|
||||
const char16_t* scope = scopePrefix.get();
|
||||
JS_ReportErrorNumberUC(aCx, js::GetErrorMessage, nullptr,
|
||||
JSMSG_IMPORT_MAPS_SCOPE_VALUE_NOT_A_MAP, scope);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 2.2. Let scopePrefixURL be the result of parsing scopePrefix with
|
||||
// baseURL as the base URL.
|
||||
nsCOMPtr<nsIURI> scopePrefixURL;
|
||||
nsresult rv = NS_NewURI(getter_AddRefs(scopePrefixURL), scopePrefix,
|
||||
nullptr, aBaseURL);
|
||||
|
||||
// Step 2.3. If scopePrefixURL is failure, then:
|
||||
if (NS_FAILED(rv)) {
|
||||
// Step 2.3.1. Report a warning to the console that the scope prefix URL
|
||||
// was not parseable.
|
||||
AutoTArray<nsString, 1> params;
|
||||
params.AppendElement(scopePrefix);
|
||||
aWarning.Report("ImportMapScopePrefixNotParseable", params);
|
||||
|
||||
// Step 2.3.2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Step 2.4. Let normalizedScopePrefix be the serialization of
|
||||
// scopePrefixURL.
|
||||
nsCString normalizedScopePrefix = scopePrefixURL->GetSpecOrDefault();
|
||||
|
||||
// Step 2.5. Set normalized[normalizedScopePrefix] to the result of sorting
|
||||
// and normalizing a specifier map given potentialSpecifierMap and baseURL.
|
||||
JS::RootedObject potentialSpecifierMap(aCx, &mapVal.toObject());
|
||||
UniquePtr<SpecifierMap> specifierMap = SortAndNormalizeSpecifierMap(
|
||||
aCx, potentialSpecifierMap, aBaseURL, aWarning);
|
||||
if (!specifierMap) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
normalized->insert_or_assign(normalizedScopePrefix,
|
||||
std::move(specifierMap));
|
||||
}
|
||||
|
||||
// Step 3. Return the result of sorting normalized, with an entry a being less
|
||||
// than an entry b if b’s key is code unit less than a’s key.
|
||||
//
|
||||
// Impl note: The sorting is done when inserting the entry.
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/import-maps/#parse-an-import-map-string
|
||||
// static
|
||||
UniquePtr<ImportMap> ImportMap::ParseString(
|
||||
JSContext* aCx, SourceText<char16_t>& aInput, nsIURI* aBaseURL,
|
||||
const ReportWarningHelper& aWarning) {
|
||||
// Step 1. Let parsed be the result of parsing JSON into Infra values given
|
||||
// input.
|
||||
JS::Rooted<JS::Value> parsedVal(aCx);
|
||||
if (!JS_ParseJSON(aCx, aInput.get(), aInput.length(), &parsedVal)) {
|
||||
// If JS_ParseJSON fail it will throw SyntaxError.
|
||||
NS_WARNING("Parsing Import map string failed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 2. If parsed is not a map, then throw a TypeError indicating that
|
||||
// the top-level value needs to be a JSON object.
|
||||
bool isMap;
|
||||
if (!IsMapObject(aCx, parsedVal, &isMap)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!isMap) {
|
||||
JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr,
|
||||
JSMSG_IMPORT_MAPS_NOT_A_MAP);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JS::RootedObject parsedObj(aCx, &parsedVal.toObject());
|
||||
JS::RootedValue importsVal(aCx);
|
||||
if (!JS_GetProperty(aCx, parsedObj, "imports", &importsVal)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 3. Let sortedAndNormalizedImports be an empty map.
|
||||
//
|
||||
// Impl note: If parsed["imports"] doesn't exist, we will allocate
|
||||
// sortedAndNormalizedImports to an empty map in Step 8 below.
|
||||
UniquePtr<SpecifierMap> sortedAndNormalizedImports = nullptr;
|
||||
|
||||
// Step 4. If parsed["imports"] exists, then:
|
||||
if (!importsVal.isUndefined()) {
|
||||
// Step 4.1. If parsed["imports"] is not a map, then throw a TypeError
|
||||
// indicating that the "imports" top-level key needs to be a JSON object.
|
||||
bool isMap;
|
||||
if (!IsMapObject(aCx, importsVal, &isMap)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!isMap) {
|
||||
JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr,
|
||||
JSMSG_IMPORT_MAPS_IMPORTS_NOT_A_MAP);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 4.2. Set sortedAndNormalizedImports to the result of sorting and
|
||||
// normalizing a specifier map given parsed["imports"] and baseURL.
|
||||
JS::RootedObject importsObj(aCx, &importsVal.toObject());
|
||||
sortedAndNormalizedImports =
|
||||
SortAndNormalizeSpecifierMap(aCx, importsObj, aBaseURL, aWarning);
|
||||
if (!sortedAndNormalizedImports) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
JS::RootedValue scopesVal(aCx);
|
||||
if (!JS_GetProperty(aCx, parsedObj, "scopes", &scopesVal)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 5. Let sortedAndNormalizedScopes be an empty map.
|
||||
//
|
||||
// Impl note: If parsed["scopes"] doesn't exist, we will allocate
|
||||
// sortedAndNormalizedScopes to an empty map in Step 8 below.
|
||||
UniquePtr<ScopeMap> sortedAndNormalizedScopes = nullptr;
|
||||
|
||||
// Step 6. If parsed["scopes"] exists, then:
|
||||
if (!scopesVal.isUndefined()) {
|
||||
// Step 6.1. If parsed["scopes"] is not a map, then throw a TypeError
|
||||
// indicating that the "scopes" top-level key needs to be a JSON object.
|
||||
bool isMap;
|
||||
if (!IsMapObject(aCx, scopesVal, &isMap)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!isMap) {
|
||||
JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr,
|
||||
JSMSG_IMPORT_MAPS_SCOPES_NOT_A_MAP);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Step 6.2. Set sortedAndNormalizedScopes to the result of sorting and
|
||||
// normalizing scopes given parsed["scopes"] and baseURL.
|
||||
JS::RootedObject scopesObj(aCx, &scopesVal.toObject());
|
||||
sortedAndNormalizedScopes =
|
||||
SortAndNormalizeScopes(aCx, scopesObj, aBaseURL, aWarning);
|
||||
if (!sortedAndNormalizedScopes) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7. If parsed’s keys contains any items besides "imports" or
|
||||
// "scopes", report a warning to the console that an invalid top-level key
|
||||
// was present in the import map.
|
||||
JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
|
||||
if (!JS_Enumerate(aCx, parsedObj, &keys)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < keys.length(); i++) {
|
||||
const JS::RootedId key(aCx, keys[i]);
|
||||
nsAutoJSString val;
|
||||
NS_ENSURE_TRUE(val.init(aCx, key), nullptr);
|
||||
if (val.EqualsLiteral("imports") || val.EqualsLiteral("scopes")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AutoTArray<nsString, 1> params;
|
||||
params.AppendElement(val);
|
||||
aWarning.Report("ImportMapInvalidTopLevelKey", params);
|
||||
}
|
||||
|
||||
// Impl note: Create empty maps for sortedAndNormalizedImports and
|
||||
// sortedAndNormalizedImports if they aren't allocated.
|
||||
if (!sortedAndNormalizedImports) {
|
||||
sortedAndNormalizedImports = MakeUnique<SpecifierMap>();
|
||||
}
|
||||
if (!sortedAndNormalizedScopes) {
|
||||
sortedAndNormalizedScopes = MakeUnique<ScopeMap>();
|
||||
}
|
||||
|
||||
// Step 8. Return the import map whose imports are
|
||||
// sortedAndNormalizedImports and whose scopes scopes are
|
||||
// sortedAndNormalizedScopes.
|
||||
return MakeUnique<ImportMap>(std::move(sortedAndNormalizedImports),
|
||||
std::move(sortedAndNormalizedScopes));
|
||||
}
|
||||
|
||||
#undef LOG
|
||||
#undef LOG_ENABLED
|
||||
} // namespace JS::loader
|
||||
95
js/loader/ImportMap.h
Normal file
95
js/loader/ImportMap.h
Normal file
@@ -0,0 +1,95 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef js_loader_ImportMap_h
|
||||
#define js_loader_ImportMap_h
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
|
||||
#include "js/SourceText.h"
|
||||
#include "mozilla/AlreadyAddRefed.h"
|
||||
#include "mozilla/Logging.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/UniquePtr.h"
|
||||
#include "nsStringFwd.h"
|
||||
#include "nsTArray.h"
|
||||
|
||||
struct JSContext;
|
||||
class nsIScriptElement;
|
||||
class nsIURI;
|
||||
|
||||
namespace JS::loader {
|
||||
class LoadedScript;
|
||||
class ScriptLoaderInterface;
|
||||
class ScriptLoadRequest;
|
||||
|
||||
/**
|
||||
* A helper class to report warning to ScriptLoaderInterface.
|
||||
*/
|
||||
class ReportWarningHelper {
|
||||
public:
|
||||
ReportWarningHelper(ScriptLoaderInterface* aLoader,
|
||||
ScriptLoadRequest* aRequest)
|
||||
: mLoader(aLoader), mRequest(aRequest) {}
|
||||
|
||||
void Report(const char* aMessageName,
|
||||
const nsTArray<nsString>& aParams = nsTArray<nsString>()) const;
|
||||
|
||||
private:
|
||||
RefPtr<ScriptLoaderInterface> mLoader;
|
||||
ScriptLoadRequest* mRequest;
|
||||
};
|
||||
|
||||
// Specifier map from import maps.
|
||||
// https://wicg.github.io/import-maps/#specifier-map
|
||||
using SpecifierMap =
|
||||
std::map<nsString, nsCOMPtr<nsIURI>, std::greater<nsString>>;
|
||||
|
||||
// Scope map from import maps.
|
||||
// https://wicg.github.io/import-maps/#import-map-scopes
|
||||
using ScopeMap = std::map<nsCString, mozilla::UniquePtr<SpecifierMap>,
|
||||
std::greater<nsCString>>;
|
||||
|
||||
/**
|
||||
* Implementation of Import maps.
|
||||
* https://wicg.github.io/import-maps
|
||||
*/
|
||||
class ImportMap {
|
||||
public:
|
||||
ImportMap(mozilla::UniquePtr<SpecifierMap> aImports,
|
||||
mozilla::UniquePtr<ScopeMap> aScopes)
|
||||
: mImports(std::move(aImports)), mScopes(std::move(aScopes)) {}
|
||||
|
||||
/**
|
||||
* Parse the JSON string from the Import map script.
|
||||
* This function will throw TypeError if there's any invalid key or value in
|
||||
* the JSON text according to the spec.
|
||||
*
|
||||
* See https://wicg.github.io/import-maps/#parse-an-import-map-string
|
||||
*/
|
||||
static mozilla::UniquePtr<ImportMap> ParseString(
|
||||
JSContext* aCx, JS::SourceText<char16_t>& aInput, nsIURI* aBaseURL,
|
||||
const ReportWarningHelper& aWarning);
|
||||
|
||||
// Logging
|
||||
static mozilla::LazyLogModule gImportMapLog;
|
||||
|
||||
private:
|
||||
/**
|
||||
* https://wicg.github.io/import-maps/#import-map
|
||||
*
|
||||
* A import map is a struct with two items:
|
||||
* 1. imports, a specifier map, and
|
||||
* 2. scopes, an ordered map of URLs to specifier maps.
|
||||
*/
|
||||
mozilla::UniquePtr<SpecifierMap> mImports;
|
||||
mozilla::UniquePtr<ScopeMap> mScopes;
|
||||
};
|
||||
|
||||
} // namespace JS::loader
|
||||
|
||||
#endif // js_loader_ImportMap_h
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "LoadedScript.h"
|
||||
#include "ModuleLoadRequest.h"
|
||||
#include "ScriptLoadRequest.h"
|
||||
#include "mozilla/dom/ScriptSettings.h" // AutoJSAPI
|
||||
#include "mozilla/dom/ScriptTrace.h"
|
||||
|
||||
#include "js/Array.h" // JS::GetArrayLength
|
||||
@@ -31,6 +32,7 @@
|
||||
|
||||
using mozilla::GetMainThreadSerialEventTarget;
|
||||
using mozilla::Preferences;
|
||||
using mozilla::UniquePtr;
|
||||
using mozilla::dom::AutoJSAPI;
|
||||
|
||||
namespace JS::loader {
|
||||
@@ -1099,6 +1101,50 @@ void ModuleLoaderBase::CancelAndClearDynamicImports() {
|
||||
mDynamicImportRequests.CancelRequestsAndClear();
|
||||
}
|
||||
|
||||
UniquePtr<ImportMap> ModuleLoaderBase::ParseImportMap(
|
||||
ScriptLoadRequest* aRequest) {
|
||||
AutoJSAPI jsapi;
|
||||
if (!jsapi.Init(GetGlobalObject())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(aRequest->IsTextSource());
|
||||
MaybeSourceText maybeSource;
|
||||
nsresult rv = aRequest->GetScriptSource(jsapi.cx(), &maybeSource);
|
||||
if (NS_FAILED(rv)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JS::SourceText<char16_t>& text = maybeSource.ref<SourceText<char16_t>>();
|
||||
ReportWarningHelper warning{mLoader, aRequest};
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script
|
||||
// https://wicg.github.io/import-maps/#integration-prepare-a-script
|
||||
// Insert the following case to prepare a script step 25.2:
|
||||
// (Impl Note: the latest html spec is step 27.2)
|
||||
// Switch on the script's type:
|
||||
// "importmap"
|
||||
// Step 1. Let import map parse result be the result of create an import map
|
||||
// parse result, given source text, base URL and settings object.
|
||||
//
|
||||
// Impl note: According to the spec, ImportMap::ParseString will throw a
|
||||
// TypeError if there's any invalid key/value in the text. After the parsing
|
||||
// is done, we should report the error if there's any, this is done in
|
||||
// ~AutoJSAPI.
|
||||
//
|
||||
// See https://wicg.github.io/import-maps/#register-an-import-map, step 7.
|
||||
return ImportMap::ParseString(jsapi.cx(), text, aRequest->mBaseURL, warning);
|
||||
}
|
||||
|
||||
void ModuleLoaderBase::RegisterImportMap(UniquePtr<ImportMap> aImportMap) {
|
||||
// Check for aImportMap is done in ScriptLoader.
|
||||
MOZ_ASSERT(aImportMap);
|
||||
|
||||
// Step 8. Set element’s node document's import map to import map parse
|
||||
// result’s import map.
|
||||
mImportMap = std::move(aImportMap);
|
||||
}
|
||||
|
||||
#undef LOG
|
||||
#undef LOG_ENABLED
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "LoadedScript.h"
|
||||
#include "ScriptLoadRequest.h"
|
||||
|
||||
#include "ImportMap.h"
|
||||
#include "js/TypeDecls.h" // JS::MutableHandle, JS::Handle, JS::Root
|
||||
#include "js/Modules.h"
|
||||
#include "nsRefPtrHashtable.h"
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "mozilla/dom/JSExecutionContext.h"
|
||||
#include "mozilla/MaybeOneOf.h"
|
||||
#include "mozilla/MozPromise.h"
|
||||
#include "mozilla/UniquePtr.h"
|
||||
|
||||
class nsIURI;
|
||||
|
||||
@@ -60,6 +62,10 @@ class ScriptLoaderInterface : public nsISupports {
|
||||
virtual void ReportErrorToConsole(ScriptLoadRequest* aRequest,
|
||||
nsresult aResult) const = 0;
|
||||
|
||||
virtual void ReportWarningToConsole(
|
||||
ScriptLoadRequest* aRequest, const char* aMessageName,
|
||||
const nsTArray<nsString>& aParams = nsTArray<nsString>()) const = 0;
|
||||
|
||||
// Fill in CompileOptions, as well as produce the introducer script for
|
||||
// subsequent calls to UpdateDebuggerMetadata
|
||||
virtual nsresult FillCompileOptionsForRequest(
|
||||
@@ -147,6 +153,8 @@ class ModuleLoaderBase : public nsISupports {
|
||||
protected:
|
||||
RefPtr<ScriptLoaderInterface> mLoader;
|
||||
|
||||
mozilla::UniquePtr<ImportMap> mImportMap;
|
||||
|
||||
virtual ~ModuleLoaderBase();
|
||||
|
||||
public:
|
||||
@@ -227,6 +235,12 @@ class ModuleLoaderBase : public nsISupports {
|
||||
void ProcessDynamicImport(ModuleLoadRequest* aRequest);
|
||||
void CancelAndClearDynamicImports();
|
||||
|
||||
// Process <script type="importmap">
|
||||
mozilla::UniquePtr<ImportMap> ParseImportMap(ScriptLoadRequest* aRequest);
|
||||
|
||||
// Implements https://wicg.github.io/import-maps/#register-an-import-map
|
||||
void RegisterImportMap(mozilla::UniquePtr<ImportMap> aImportMap);
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private:
|
||||
|
||||
@@ -181,6 +181,7 @@ class ScriptLoadRequest
|
||||
mozilla::MaybeOneOf<JS::SourceText<char16_t>, JS::SourceText<Utf8Unit>>;
|
||||
|
||||
bool IsModuleRequest() const { return mKind == ScriptKind::eModule; }
|
||||
bool IsImportMapRequest() const { return mKind == ScriptKind::eImportMap; }
|
||||
|
||||
ModuleLoadRequest* AsModuleRequest();
|
||||
const ModuleLoadRequest* AsModuleRequest() const;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXPORTS.js.loader += [
|
||||
"ImportMap.h",
|
||||
"LoadContextBase.h",
|
||||
"LoadedScript.h",
|
||||
"ModuleLoaderBase.h",
|
||||
@@ -14,6 +15,7 @@ EXPORTS.js.loader += [
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
"ImportMap.cpp",
|
||||
"LoadContextBase.cpp",
|
||||
"LoadedScript.cpp",
|
||||
"ModuleLoaderBase.cpp",
|
||||
|
||||
@@ -727,6 +727,12 @@ MSG_DEF(JSMSG_MISSING_EXPORT, 1, JSEXN_SYNTAXERR, "local binding for
|
||||
MSG_DEF(JSMSG_BAD_MODULE_STATUS, 0, JSEXN_INTERNALERR, "module record has unexpected status")
|
||||
MSG_DEF(JSMSG_DYNAMIC_IMPORT_FAILED, 0, JSEXN_TYPEERR, "error loading dynamically imported module")
|
||||
|
||||
// Import maps
|
||||
MSG_DEF(JSMSG_IMPORT_MAPS_NOT_A_MAP, 0, JSEXN_TYPEERR, "the top-level value needs to be a JSON object")
|
||||
MSG_DEF(JSMSG_IMPORT_MAPS_IMPORTS_NOT_A_MAP, 0, JSEXN_TYPEERR, "the imports top-level key needs to be a JSON object")
|
||||
MSG_DEF(JSMSG_IMPORT_MAPS_SCOPES_NOT_A_MAP, 0, JSEXN_TYPEERR, "the scopes top-level key needs to be a JSON object")
|
||||
MSG_DEF(JSMSG_IMPORT_MAPS_SCOPE_VALUE_NOT_A_MAP, 1, JSEXN_TYPEERR, "the value of the scope with prefix '{0}' needs to be a JSON object")
|
||||
|
||||
// Promise
|
||||
MSG_DEF(JSMSG_CANNOT_RESOLVE_PROMISE_WITH_ITSELF, 0, JSEXN_TYPEERR, "A promise cannot be resolved with itself.")
|
||||
MSG_DEF(JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY, 0, JSEXN_TYPEERR, "GetCapabilitiesExecutor function already invoked with non-undefined values.")
|
||||
|
||||
Reference in New Issue
Block a user