Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4
* -*- */
/* 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 "shell/ModuleLoader.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/TextUtils.h"
#include "jsapi.h"
#include "NamespaceImports.h"
#include "builtin/TestingUtility.h" // js::CreateScriptPrivate
#include "js/Conversions.h"
#include "js/MapAndSet.h"
#include "js/Modules.h"
#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty
#include "js/SourceText.h"
#include "js/StableStringChars.h"
#include "shell/jsshell.h"
#include "shell/OSObject.h"
#include "shell/StringUtils.h"
#include "util/Text.h"
#include "vm/JSAtomUtils.h" // AtomizeString, PinAtom
#include "vm/JSContext.h"
#include "vm/StringType.h"
using namespace js;
using namespace js::shell;
static constexpr char16_t JavaScriptScheme[] = u"javascript:";
static bool IsJavaScriptURL(Handle<JSLinearString*> path) {
return StringStartsWith(path, JavaScriptScheme);
}
static JSString* ExtractJavaScriptURLSource(JSContext* cx,
Handle<JSLinearString*> path) {
MOZ_ASSERT(IsJavaScriptURL(path));
const size_t schemeLength = js_strlen(JavaScriptScheme);
return SubString(cx, path, schemeLength);
}
bool ModuleLoader::init(JSContext* cx, HandleString loadPath) {
loadPathStr = AtomizeString(cx, loadPath);
if (!loadPathStr || !PinAtom(cx, loadPathStr)) {
return false;
}
MOZ_ASSERT(IsAbsolutePath(loadPathStr));
char16_t sep = PathSeparator;
pathSeparatorStr = AtomizeChars(cx, &sep, 1);
if (!pathSeparatorStr || !PinAtom(cx, pathSeparatorStr)) {
return false;
}
JSRuntime* rt = cx->runtime();
JS::SetModuleResolveHook(rt, ModuleLoader::ResolveImportedModule);
JS::SetModuleMetadataHook(rt, ModuleLoader::GetImportMetaProperties);
JS::SetModuleDynamicImportHook(rt, ModuleLoader::ImportModuleDynamically);
return true;
}
// static
JSObject* ModuleLoader::ResolveImportedModule(
JSContext* cx, JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest) {
ShellContext* scx = GetShellContext(cx);
return scx->moduleLoader->resolveImportedModule(cx, referencingPrivate,
moduleRequest);
}
// static
bool ModuleLoader::GetImportMetaProperties(JSContext* cx,
JS::HandleValue privateValue,
JS::HandleObject metaObject) {
ShellContext* scx = GetShellContext(cx);
return scx->moduleLoader->populateImportMeta(cx, privateValue, metaObject);
}
bool ModuleLoader::ImportMetaResolve(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedValue modulePrivate(
cx, js::GetFunctionNativeReserved(&args.callee(), ModulePrivateSlot));
// Step 4.1. Set specifier to ? ToString(specifier).
//
RootedValue v(cx, args.get(ImportMetaResolveSpecifierArg));
RootedString specifier(cx, JS::ToString(cx, v));
if (!specifier) {
return false;
}
// Step 4.2, 4.3 are implemented in importMetaResolve.
ShellContext* scx = GetShellContext(cx);
RootedString url(cx);
if (!scx->moduleLoader->importMetaResolve(cx, modulePrivate, specifier,
&url)) {
return false;
}
// Step 4.4. Return the serialization of url.
args.rval().setString(url);
return true;
}
// static
bool ModuleLoader::ImportModuleDynamically(JSContext* cx,
JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest,
JS::HandleObject promise) {
ShellContext* scx = GetShellContext(cx);
return scx->moduleLoader->dynamicImport(cx, referencingPrivate, moduleRequest,
promise);
}
bool ModuleLoader::loadRootModule(JSContext* cx, HandleString path) {
RootedValue rval(cx);
if (!loadAndExecute(cx, path, nullptr, &rval)) {
return false;
}
RootedObject evaluationPromise(cx, &rval.toObject());
if (evaluationPromise == nullptr) {
return false;
}
return JS::ThrowOnModuleEvaluationFailure(cx, evaluationPromise);
}
bool ModuleLoader::registerTestModule(JSContext* cx, HandleObject moduleRequest,
Handle<ModuleObject*> module) {
Rooted<JSLinearString*> path(
cx, resolve(cx, moduleRequest, UndefinedHandleValue));
if (!path) {
return false;
}
path = normalizePath(cx, path);
if (!path) {
return false;
}
return addModuleToRegistry(cx, path, module);
}
void ModuleLoader::clearModules(JSContext* cx) {
Handle<GlobalObject*> global = cx->global();
global->setReservedSlot(GlobalAppSlotModuleRegistry, UndefinedValue());
}
bool ModuleLoader::loadAndExecute(JSContext* cx, HandleString path,
HandleObject moduleRequestArg,
MutableHandleValue rval) {
RootedObject module(cx, loadAndParse(cx, path, moduleRequestArg));
if (!module) {
return false;
}
if (!JS::ModuleLink(cx, module)) {
return false;
}
return JS::ModuleEvaluate(cx, module, rval);
}
JSObject* ModuleLoader::resolveImportedModule(
JSContext* cx, JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest) {
Rooted<JSLinearString*> path(cx,
resolve(cx, moduleRequest, referencingPrivate));
if (!path) {
return nullptr;
}
return loadAndParse(cx, path, moduleRequest);
}
bool ModuleLoader::populateImportMeta(JSContext* cx,
JS::HandleValue privateValue,
JS::HandleObject metaObject) {
Rooted<JSLinearString*> path(cx);
if (!privateValue.isUndefined()) {
if (!getScriptPath(cx, privateValue, &path)) {
return false;
}
}
if (!path) {
path = NewStringCopyZ<CanGC>(cx, "(unknown)");
if (!path) {
return false;
}
}
RootedValue pathValue(cx, StringValue(path));
if (!JS_DefineProperty(cx, metaObject, "url", pathValue, JSPROP_ENUMERATE)) {
return false;
}
JSFunction* resolveFunc = js::DefineFunctionWithReserved(
cx, metaObject, "resolve", ImportMetaResolve, ImportMetaResolveNumArgs,
JSPROP_ENUMERATE);
if (!resolveFunc) {
return false;
}
RootedObject resolveFuncObj(cx, JS_GetFunctionObject(resolveFunc));
js::SetFunctionNativeReserved(resolveFuncObj, ModulePrivateSlot,
privateValue);
return true;
}
bool ModuleLoader::importMetaResolve(JSContext* cx,
JS::Handle<JS::Value> referencingPrivate,
JS::Handle<JSString*> specifier,
JS::MutableHandle<JSString*> urlOut) {
Rooted<JSLinearString*> path(cx, resolve(cx, specifier, referencingPrivate));
if (!path) {
return false;
}
urlOut.set(path);
return true;
}
bool ModuleLoader::dynamicImport(JSContext* cx,
JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest,
JS::HandleObject promise) {
// To make this more realistic, use a promise to delay the import and make it
// happen asynchronously. This method packages up the arguments and creates a
// resolved promise, which on fullfillment calls doDynamicImport with the
// original arguments.
MOZ_ASSERT(promise);
RootedValue moduleRequestValue(cx, ObjectValue(*moduleRequest));
RootedValue promiseValue(cx, ObjectValue(*promise));
RootedObject closure(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr));
if (!closure ||
!JS_DefineProperty(cx, closure, "referencingPrivate", referencingPrivate,
JSPROP_ENUMERATE) ||
!JS_DefineProperty(cx, closure, "moduleRequest", moduleRequestValue,
JSPROP_ENUMERATE) ||
!JS_DefineProperty(cx, closure, "promise", promiseValue,
JSPROP_ENUMERATE)) {
return false;
}
RootedFunction onResolved(
cx, NewNativeFunction(cx, DynamicImportDelayFulfilled, 1, nullptr));
if (!onResolved) {
return false;
}
RootedFunction onRejected(
cx, NewNativeFunction(cx, DynamicImportDelayRejected, 1, nullptr));
if (!onRejected) {
return false;
}
RootedObject delayPromise(cx);
RootedValue closureValue(cx, ObjectValue(*closure));
delayPromise = PromiseObject::unforgeableResolve(cx, closureValue);
if (!delayPromise) {
return false;
}
return JS::AddPromiseReactions(cx, delayPromise, onResolved, onRejected);
}
bool ModuleLoader::DynamicImportDelayFulfilled(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
RootedObject closure(cx, &args[0].toObject());
RootedValue referencingPrivate(cx);
RootedValue moduleRequestValue(cx);
RootedValue promiseValue(cx);
if (!JS_GetProperty(cx, closure, "referencingPrivate", &referencingPrivate) ||
!JS_GetProperty(cx, closure, "moduleRequest", &moduleRequestValue) ||
!JS_GetProperty(cx, closure, "promise", &promiseValue)) {
return false;
}
RootedObject moduleRequest(cx, &moduleRequestValue.toObject());
RootedObject promise(cx, &promiseValue.toObject());
ShellContext* scx = GetShellContext(cx);
return scx->moduleLoader->doDynamicImport(cx, referencingPrivate,
moduleRequest, promise);
}
bool ModuleLoader::DynamicImportDelayRejected(JSContext* cx, unsigned argc,
Value* vp) {
MOZ_CRASH("This promise should never be rejected");
}
bool ModuleLoader::doDynamicImport(JSContext* cx,
JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest,
JS::HandleObject promise) {
// Exceptions during dynamic import are handled by calling
// FinishDynamicModuleImport with a pending exception on the context.
RootedValue rval(cx);
bool ok =
tryDynamicImport(cx, referencingPrivate, moduleRequest, promise, &rval);
JSObject* evaluationObject = ok ? &rval.toObject() : nullptr;
RootedObject evaluationPromise(cx, evaluationObject);
return JS::FinishDynamicModuleImport(
cx, evaluationPromise, referencingPrivate, moduleRequest, promise);
}
bool ModuleLoader::tryDynamicImport(JSContext* cx,
JS::HandleValue referencingPrivate,
JS::HandleObject moduleRequest,
JS::HandleObject promise,
JS::MutableHandleValue rval) {
Rooted<JSLinearString*> path(cx,
resolve(cx, moduleRequest, referencingPrivate));
if (!path) {
return false;
}
return loadAndExecute(cx, path, moduleRequest, rval);
}
JSLinearString* ModuleLoader::resolve(JSContext* cx,
HandleObject moduleRequestArg,
HandleValue referencingInfo) {
ModuleRequestObject* moduleRequest =
&moduleRequestArg->as<ModuleRequestObject>();
if (moduleRequest->specifier()->length() == 0) {
JS_ReportErrorASCII(cx, "Invalid module specifier");
return nullptr;
}
Rooted<JSLinearString*> name(
cx, JS_EnsureLinearString(cx, moduleRequest->specifier()));
if (!name) {
return nullptr;
}
return resolve(cx, name, referencingInfo);
}
JSLinearString* ModuleLoader::resolve(JSContext* cx, HandleString specifier,
HandleValue referencingInfo) {
Rooted<JSLinearString*> name(cx, JS_EnsureLinearString(cx, specifier));
if (!name) {
return nullptr;
}
if (IsJavaScriptURL(name) || IsAbsolutePath(name)) {
return name;
}
// Treat |name| as a relative path if it starts with either "./" or "../".
bool isRelative =
StringStartsWith(name, u"./") || StringStartsWith(name, u"../")
#ifdef XP_WIN
|| StringStartsWith(name, u".\\") || StringStartsWith(name, u"..\\")
#endif
;
RootedString path(cx, loadPathStr);
if (isRelative) {
if (referencingInfo.isUndefined()) {
JS_ReportErrorASCII(cx, "No referencing module for relative import");
return nullptr;
}
Rooted<JSLinearString*> refPath(cx);
if (!getScriptPath(cx, referencingInfo, &refPath)) {
return nullptr;
}
if (!refPath) {
JS_ReportErrorASCII(cx, "No path set for referencing module");
return nullptr;
}
int32_t sepIndex = LastIndexOf(refPath, u'/');
#ifdef XP_WIN
sepIndex = std::max(sepIndex, LastIndexOf(refPath, u'\\'));
#endif
if (sepIndex >= 0) {
path = SubString(cx, refPath, 0, sepIndex);
if (!path) {
return nullptr;
}
}
}
RootedString result(cx);
RootedString pathSep(cx, pathSeparatorStr);
result = JS_ConcatStrings(cx, path, pathSep);
if (!result) {
return nullptr;
}
result = JS_ConcatStrings(cx, result, name);
if (!result) {
return nullptr;
}
Rooted<JSLinearString*> linear(cx, JS_EnsureLinearString(cx, result));
if (!linear) {
return nullptr;
}
return normalizePath(cx, linear);
}
JSObject* ModuleLoader::loadAndParse(JSContext* cx, HandleString pathArg,
JS::HandleObject moduleRequestArg) {
Rooted<JSLinearString*> path(cx, JS_EnsureLinearString(cx, pathArg));
if (!path) {
return nullptr;
}
path = normalizePath(cx, path);
if (!path) {
return nullptr;
}
RootedObject module(cx);
if (!lookupModuleInRegistry(cx, path, &module)) {
return nullptr;
}
if (module) {
return module;
}
UniqueChars filename = JS_EncodeStringToUTF8(cx, path);
if (!filename) {
return nullptr;
}
JS::CompileOptions options(cx);
options.setFileAndLine(filename.get(), 1);
RootedString source(cx, fetchSource(cx, path));
if (!source) {
return nullptr;
}
JS::AutoStableStringChars linearChars(cx);
if (!linearChars.initTwoByte(cx, source)) {
return nullptr;
}
JS::SourceText<char16_t> srcBuf;
if (!srcBuf.initMaybeBorrowed(cx, linearChars)) {
return nullptr;
}
JS::ModuleType moduleType = JS::ModuleType::JavaScript;
if (moduleRequestArg) {
Rooted<ModuleRequestObject*> moduleRequest(
cx, &moduleRequestArg->as<ModuleRequestObject>());
if (!ModuleRequestObject::getModuleType(cx, moduleRequest, moduleType)) {
return nullptr;
}
}
switch (moduleType) {
case JS::ModuleType::Unknown:
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_BAD_MODULE_TYPE);
return nullptr;
case JS::ModuleType::JavaScript: {
module = JS::CompileModule(cx, options, srcBuf);
if (!module) {
return nullptr;
}
RootedObject info(cx, js::CreateScriptPrivate(cx, path));
if (!info) {
return nullptr;
}
JS::SetModulePrivate(module, ObjectValue(*info));
} break;
case JS::ModuleType::JSON:
module = JS::CompileJsonModule(cx, options, srcBuf);
if (!module) {
return nullptr;
}
break;
}
if (!addModuleToRegistry(cx, path, module)) {
return nullptr;
}
return module;
}
bool ModuleLoader::lookupModuleInRegistry(JSContext* cx, HandleString path,
MutableHandleObject moduleOut) {
moduleOut.set(nullptr);
RootedObject registry(cx, getOrCreateModuleRegistry(cx));
if (!registry) {
return false;
}
RootedValue pathValue(cx, StringValue(path));
RootedValue moduleValue(cx);
if (!JS::MapGet(cx, registry, pathValue, &moduleValue)) {
return false;
}
if (!moduleValue.isUndefined()) {
moduleOut.set(&moduleValue.toObject());
}
return true;
}
bool ModuleLoader::addModuleToRegistry(JSContext* cx, HandleString path,
HandleObject module) {
RootedObject registry(cx, getOrCreateModuleRegistry(cx));
if (!registry) {
return false;
}
RootedValue pathValue(cx, StringValue(path));
RootedValue moduleValue(cx, ObjectValue(*module));
return JS::MapSet(cx, registry, pathValue, moduleValue);
}
JSObject* ModuleLoader::getOrCreateModuleRegistry(JSContext* cx) {
Handle<GlobalObject*> global = cx->global();
RootedValue value(cx, global->getReservedSlot(GlobalAppSlotModuleRegistry));
if (!value.isUndefined()) {
return &value.toObject();
}
JSObject* registry = JS::NewMapObject(cx);
if (!registry) {
return nullptr;
}
global->setReservedSlot(GlobalAppSlotModuleRegistry, ObjectValue(*registry));
return registry;
}
bool ModuleLoader::getScriptPath(JSContext* cx, HandleValue privateValue,
MutableHandle<JSLinearString*> pathOut) {
pathOut.set(nullptr);
RootedObject infoObj(cx, &privateValue.toObject());
RootedValue pathValue(cx);
if (!JS_GetProperty(cx, infoObj, "path", &pathValue)) {
return false;
}
if (pathValue.isUndefined()) {
return true;
}
RootedString path(cx, pathValue.toString());
pathOut.set(JS_EnsureLinearString(cx, path));
return pathOut;
}
JSLinearString* ModuleLoader::normalizePath(JSContext* cx,
Handle<JSLinearString*> pathArg) {
Rooted<JSLinearString*> path(cx, pathArg);
if (IsJavaScriptURL(path)) {
return path;
}
#ifdef XP_WIN
// Replace all forward slashes with backward slashes.
path = ReplaceCharGlobally(cx, path, u'/', PathSeparator);
if (!path) {
return nullptr;
}
// Remove the drive letter, if present.
Rooted<JSLinearString*> drive(cx);
if (path->length() > 2 && mozilla::IsAsciiAlpha(CharAt(path, 0)) &&
CharAt(path, 1) == u':' && CharAt(path, 2) == u'\\') {
drive = SubString(cx, path, 0, 2);
path = SubString(cx, path, 2);
if (!drive || !path) {
return nullptr;
}
}
#endif // XP_WIN
// Normalize the path by removing redundant path components.
Rooted<GCVector<JSLinearString*>> components(cx, cx);
size_t lastSep = 0;
while (lastSep < path->length()) {
int32_t i = IndexOf(path, PathSeparator, lastSep);
if (i < 0) {
i = path->length();
}
Rooted<JSLinearString*> part(cx, SubString(cx, path, lastSep, i));
if (!part) {
return nullptr;
}
lastSep = i + 1;
// Remove "." when preceded by a path component.
if (StringEquals(part, u".") && !components.empty()) {
continue;
}
if (StringEquals(part, u"..") && !components.empty()) {
// Replace "./.." with "..".
if (StringEquals(components.back(), u".")) {
components.back() = part;
continue;
}
// When preceded by a non-empty path component, remove ".." and the
// preceding component, unless the preceding component is also "..".
if (!StringEquals(components.back(), u"") &&
!StringEquals(components.back(), u"..")) {
components.popBack();
continue;
}
}
if (!components.append(part)) {
return nullptr;
}
}
Rooted<JSLinearString*> pathSep(cx, pathSeparatorStr);
RootedString normalized(cx, JoinStrings(cx, components, pathSep));
if (!normalized) {
return nullptr;
}
#ifdef XP_WIN
if (drive) {
normalized = JS_ConcatStrings(cx, drive, normalized);
if (!normalized) {
return nullptr;
}
}
#endif
return JS_EnsureLinearString(cx, normalized);
}
JSString* ModuleLoader::fetchSource(JSContext* cx,
Handle<JSLinearString*> path) {
if (IsJavaScriptURL(path)) {
return ExtractJavaScriptURLSource(cx, path);
}
RootedString resolvedPath(cx, ResolvePath(cx, path, RootRelative));
if (!resolvedPath) {
return nullptr;
}
return FileAsString(cx, resolvedPath);
}