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
442 lines
15 KiB
C++
442 lines
15 KiB
C++
/* -*- 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
|