diff --git a/js/src/shell/ModuleLoader.cpp b/js/src/shell/ModuleLoader.cpp new file mode 100644 index 000000000000..d7546278b11e --- /dev/null +++ b/js/src/shell/ModuleLoader.cpp @@ -0,0 +1,533 @@ +/* -*- 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/ArrayUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/TextUtils.h" + +#include "NamespaceImports.h" + +#include "js/Modules.h" +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "shell/jsshell.h" +#include "shell/OSObject.h" +#include "shell/StringUtils.h" +#include "vm/JSAtom.h" +#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(HandleLinearString path) { + return StringStartsWith(path, JavaScriptScheme); +} + +static JSString* ExtractJavaScriptURLSource(JSContext* cx, + HandleLinearString path) { + MOZ_ASSERT(IsJavaScriptURL(path)); + + const size_t schemeLength = mozilla::ArrayLength(JavaScriptScheme) - 1; + return SubString(cx, path, schemeLength); +} + +bool ModuleLoader::init(JSContext* cx, HandleString loadPath) { + loadPathStr = AtomizeString(cx, loadPath, PinAtom); + if (!loadPathStr) { + return false; + } + + MOZ_ASSERT(IsAbsolutePath(loadPathStr)); + + char16_t sep = PathSeparator; + pathSeparatorStr = AtomizeChars(cx, &sep, 1); + if (!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::HandleString specifier) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->resolveImportedModule(cx, referencingPrivate, + specifier); +} + +// static +bool ModuleLoader::GetImportMetaProperties(JSContext* cx, + JS::HandleValue privateValue, + JS::HandleObject metaObject) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->populateImportMeta(cx, privateValue, metaObject); +} + +// static +bool ModuleLoader::ImportModuleDynamically(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + JS::HandleObject promise) { + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->dynamicImport(cx, referencingPrivate, specifier, + promise); +} + +bool ModuleLoader::loadRootModule(JSContext* cx, HandleString path) { + return loadAndExecute(cx, path); +} + +bool ModuleLoader::loadAndExecute(JSContext* cx, HandleString path) { + RootedObject module(cx, loadAndParse(cx, path)); + if (!module) { + return false; + } + + return JS::ModuleInstantiate(cx, module) && JS::ModuleEvaluate(cx, module); +} + +JSObject* ModuleLoader::resolveImportedModule( + JSContext* cx, JS::HandleValue referencingPrivate, + JS::HandleString specifier) { + RootedLinearString path(cx, resolve(cx, specifier, referencingPrivate)); + if (!path) { + return nullptr; + } + + return loadAndParse(cx, path); +} + +bool ModuleLoader::populateImportMeta(JSContext* cx, + JS::HandleValue privateValue, + JS::HandleObject metaObject) { + RootedLinearString path(cx); + if (!privateValue.isUndefined()) { + if (!getScriptPath(cx, privateValue, &path)) { + return false; + } + } + + if (!path) { + path = NewStringCopyZ(cx, "(unknown)"); + if (!path) { + return false; + } + } + + RootedValue pathValue(cx, StringValue(path)); + return JS_DefineProperty(cx, metaObject, "url", pathValue, JSPROP_ENUMERATE); +} + +bool ModuleLoader::dynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + 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 specifierValue(cx, StringValue(specifier)); + RootedValue promiseValue(cx, ObjectValue(*promise)); + RootedObject closure(cx, JS_NewPlainObject(cx)); + if (!closure || + !JS_DefineProperty(cx, closure, "referencingPrivate", referencingPrivate, + JSPROP_ENUMERATE) || + !JS_DefineProperty(cx, closure, "specifier", specifierValue, + 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 specifierValue(cx); + RootedValue promiseValue(cx); + if (!JS_GetProperty(cx, closure, "referencingPrivate", &referencingPrivate) || + !JS_GetProperty(cx, closure, "specifier", &specifierValue) || + !JS_GetProperty(cx, closure, "promise", &promiseValue)) { + return false; + } + + RootedString specifier(cx, specifierValue.toString()); + RootedObject promise(cx, &promiseValue.toObject()); + + ShellContext* scx = GetShellContext(cx); + return scx->moduleLoader->doDynamicImport(cx, referencingPrivate, specifier, + 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::HandleString specifier, + JS::HandleObject promise) { + // Exceptions during dynamic import are handled by calling + // FinishDynamicModuleImport with a pending exception on the context. + mozilla::DebugOnly ok = + tryDynamicImport(cx, referencingPrivate, specifier, promise); + MOZ_ASSERT_IF(!ok, JS_IsExceptionPending(cx)); + return JS::FinishDynamicModuleImport(cx, referencingPrivate, specifier, + promise); +} + +bool ModuleLoader::tryDynamicImport(JSContext* cx, + JS::HandleValue referencingPrivate, + JS::HandleString specifier, + JS::HandleObject promise) { + RootedLinearString path(cx, resolve(cx, specifier, referencingPrivate)); + if (!path) { + return false; + } + + return loadAndExecute(cx, path); +} + +JSLinearString* ModuleLoader::resolve(JSContext* cx, HandleString nameArg, + HandleValue referencingInfo) { + if (nameArg->length() == 0) { + JS_ReportErrorASCII(cx, "Invalid module specifier"); + return nullptr; + } + + RootedLinearString name(cx, JS_EnsureLinearString(cx, nameArg)); + 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; + } + + RootedLinearString 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; + } + + return JS_EnsureLinearString(cx, result); +} + +JSObject* ModuleLoader::loadAndParse(JSContext* cx, HandleString pathArg) { + RootedLinearString 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_EncodeStringToLatin1(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 stableChars(cx); + if (!stableChars.initTwoByte(cx, source)) { + return nullptr; + } + + const char16_t* chars = stableChars.twoByteRange().begin().get(); + JS::SourceText srcBuf; + if (!srcBuf.init(cx, chars, source->length(), + JS::SourceOwnership::Borrowed)) { + return nullptr; + } + + module = JS::CompileModule(cx, options, srcBuf); + if (!module) { + return nullptr; + } + + RootedObject info(cx, CreateScriptPrivate(cx, path)); + if (!info) { + return nullptr; + } + + JS::SetModulePrivate(module, ObjectValue(*info)); + + 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 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 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, + HandleLinearString pathArg) { + RootedLinearString 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. + RootedLinearString 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> components(cx); + size_t lastSep = 0; + while (lastSep < path->length()) { + int32_t i = IndexOf(path, PathSeparator, lastSep); + if (i < 0) { + i = path->length(); + } + + RootedLinearString 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; + } + } + + RootedLinearString 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, HandleLinearString path) { + if (IsJavaScriptURL(path)) { + return ExtractJavaScriptURLSource(cx, path); + } + + RootedString resolvedPath(cx, ResolvePath(cx, path, RootRelative)); + if (!resolvedPath) { + return nullptr; + } + + return FileAsString(cx, resolvedPath); +} diff --git a/js/src/shell/ModuleLoader.h b/js/src/shell/ModuleLoader.h new file mode 100644 index 000000000000..98348e75f3d3 --- /dev/null +++ b/js/src/shell/ModuleLoader.h @@ -0,0 +1,69 @@ +/* -*- 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 shell_ModuleLoader_h +#define shell_ModuleLoader_h + +#include "gc/Rooting.h" +#include "js/RootingAPI.h" + +namespace js { +namespace shell { + +class ModuleLoader { + public: + bool init(JSContext* cx, HandleString loadPath); + bool loadRootModule(JSContext* cx, HandleString path); + + private: + static JSObject* ResolveImportedModule(JSContext* cx, + HandleValue referencingPrivate, + HandleString specifier); + static bool GetImportMetaProperties(JSContext* cx, HandleValue privateValue, + HandleObject metaObject); + static bool ImportModuleDynamically(JSContext* cx, + HandleValue referencingPrivate, + HandleString specifier, + HandleObject promise); + + static bool DynamicImportDelayFulfilled(JSContext* cx, unsigned argc, + Value* vp); + static bool DynamicImportDelayRejected(JSContext* cx, unsigned argc, + Value* vp); + + bool loadAndExecute(JSContext* cx, HandleString path); + JSObject* resolveImportedModule(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier); + bool populateImportMeta(JSContext* cx, HandleValue privateValue, + HandleObject metaObject); + bool dynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier, HandleObject promise); + bool doDynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier, HandleObject promise); + bool tryDynamicImport(JSContext* cx, HandleValue referencingPrivate, + HandleString specifier, HandleObject promise); + JSObject* loadAndParse(JSContext* cx, HandleString path); + bool lookupModuleInRegistry(JSContext* cx, HandleString path, + MutableHandleObject moduleOut); + bool addModuleToRegistry(JSContext* cx, HandleString path, + HandleObject module); + JSLinearString* resolve(JSContext* cx, HandleString name, + HandleValue referencingInfo); + bool getScriptPath(JSContext* cx, HandleValue privateValue, + MutableHandle pathOut); + JSLinearString* normalizePath(JSContext* cx, HandleLinearString path); + JSObject* getOrCreateModuleRegistry(JSContext* cx); + JSString* fetchSource(JSContext* cx, HandleLinearString path); + + // The following are used for pinned atoms which do not need rooting. + JSAtom* loadPathStr = nullptr; + JSAtom* pathSeparatorStr = nullptr; +} JS_HAZ_NON_GC_POINTER; + +} // namespace shell +} // namespace js + +#endif // shell_ModuleLoader_h diff --git a/js/src/shell/ModuleLoader.js b/js/src/shell/ModuleLoader.js deleted file mode 100644 index e2bd344d507f..000000000000 --- a/js/src/shell/ModuleLoader.js +++ /dev/null @@ -1,246 +0,0 @@ -/* -*- 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/. */ - -/* global getModuleLoadPath setModuleLoadHook setModuleResolveHook setModuleMetadataHook */ -/* global getModulePrivate setModulePrivate parseModule os */ -/* global setModuleDynamicImportHook finishDynamicModuleImport abortDynamicModuleImport */ - -// A basic synchronous module loader for testing the shell. -// -// Supports loading files and 'javascript:' URLs that embed JS source text. - -{ -// Save standard built-ins before scripts can modify them. -const ArrayPrototypeJoin = Array.prototype.join; -const MapPrototypeGet = Map.prototype.get; -const MapPrototypeHas = Map.prototype.has; -const MapPrototypeSet = Map.prototype.set; -const ObjectDefineProperty = Object.defineProperty; -const ReflectApply = Reflect.apply; -const StringPrototypeIndexOf = String.prototype.indexOf; -const StringPrototypeLastIndexOf = String.prototype.lastIndexOf; -const StringPrototypeStartsWith = String.prototype.startsWith; -const StringPrototypeSubstring = String.prototype.substring; -const ErrorClass = Error; -const PromiseClass = Promise; -const PromiseResolve = Promise.resolve; - -const JAVASCRIPT_SCHEME = "javascript:"; - -const ReflectLoader = new class { - constructor() { - this.registry = new Map(); - this.loadPath = getModuleLoadPath(); - } - - isJavascriptURL(name) { - return ReflectApply(StringPrototypeStartsWith, name, [JAVASCRIPT_SCHEME]); - } - - resolve(name, referencingInfo) { - if (name === "") { - throw new ErrorClass("Invalid module specifier"); - } - - if (this.isJavascriptURL(name) || os.path.isAbsolute(name)) { - return name; - } - - let loadPath = this.loadPath; - - // Treat |name| as a relative path if it starts with either "./" - // or "../". - let isRelative = ReflectApply(StringPrototypeStartsWith, name, ["./"]) - || ReflectApply(StringPrototypeStartsWith, name, ["../"]) -#ifdef XP_WIN - || ReflectApply(StringPrototypeStartsWith, name, [".\\"]) - || ReflectApply(StringPrototypeStartsWith, name, ["..\\"]) -#endif - ; - - // If |name| is a relative path and the referencing module's path is - // available, load |name| relative to the that path. - if (isRelative) { - if (!referencingInfo) { - throw new ErrorClass("No referencing module for relative import"); - } - - let path = referencingInfo.path; - - let sepIndex = ReflectApply(StringPrototypeLastIndexOf, path, ["/"]); -#ifdef XP_WIN - let otherSepIndex = ReflectApply(StringPrototypeLastIndexOf, path, ["\\"]); - if (otherSepIndex > sepIndex) - sepIndex = otherSepIndex; -#endif - if (sepIndex >= 0) - loadPath = ReflectApply(StringPrototypeSubstring, path, [0, sepIndex]); - } - - return os.path.join(loadPath, name); - } - - normalize(path) { - if (this.isJavascriptURL(path)) { - return path; - } - -#ifdef XP_WIN - // Replace all forward slashes with backward slashes. - // NB: It may be tempting to replace this loop with a call to - // String.prototype.replace, but user scripts may have modified - // String.prototype or RegExp.prototype built-in functions, which makes - // it unsafe to call String.prototype.replace. - let newPath = ""; - let lastSlash = 0; - while (true) { - let i = ReflectApply(StringPrototypeIndexOf, path, ["/", lastSlash]); - if (i < 0) { - newPath += ReflectApply(StringPrototypeSubstring, path, [lastSlash]); - break; - } - newPath += ReflectApply(StringPrototypeSubstring, path, [lastSlash, i]) + "\\"; - lastSlash = i + 1; - } - path = newPath; - - // Remove the drive letter, if present. - let isalpha = c => ("A" <= c && c <= "Z") || ("a" <= c && c <= "z"); - let drive = ""; - if (path.length > 2 && isalpha(path[0]) && path[1] === ":" && path[2] === "\\") { - drive = ReflectApply(StringPrototypeSubstring, path, [0, 2]); - path = ReflectApply(StringPrototypeSubstring, path, [2]); - } -#endif - - const pathsep = -#ifdef XP_WIN - "\\"; -#else - "/"; -#endif - - let n = 0; - let components = []; - - // Normalize the path by removing redundant path components. - // NB: See above for why we don't call String.prototype.split here. - let lastSep = 0; - while (lastSep < path.length) { - let i = ReflectApply(StringPrototypeIndexOf, path, [pathsep, lastSep]); - if (i < 0) - i = path.length; - let part = ReflectApply(StringPrototypeSubstring, path, [lastSep, i]); - lastSep = i + 1; - - // Remove "." when preceded by a path component. - if (part === "." && n > 0) - continue; - - if (part === ".." && n > 0) { - // Replace "./.." with "..". - if (components[n - 1] === ".") { - components[n - 1] = ".."; - continue; - } - - // When preceded by a non-empty path component, remove ".." and - // the preceding component, unless the preceding component is also - // "..". - if (components[n - 1] !== "" && components[n - 1] !== "..") { - components.length = --n; - continue; - } - } - - ObjectDefineProperty(components, n++, { - __proto__: null, - value: part, - writable: true, enumerable: true, configurable: true, - }); - } - - let normalized = ReflectApply(ArrayPrototypeJoin, components, [pathsep]); -#ifdef XP_WIN - normalized = drive + normalized; -#endif - return normalized; - } - - fetch(path) { - if (this.isJavascriptURL(path)) { - return ReflectApply(StringPrototypeSubstring, path, [JAVASCRIPT_SCHEME.length]); - } - - return os.file.readFile(path); - } - - loadAndParse(path) { - let normalized = this.normalize(path); - if (ReflectApply(MapPrototypeHas, this.registry, [normalized])) - return ReflectApply(MapPrototypeGet, this.registry, [normalized]); - - let source = this.fetch(path); - let module = parseModule(source, path); - let moduleInfo = { path: normalized }; - setModulePrivate(module, moduleInfo); - ReflectApply(MapPrototypeSet, this.registry, [normalized, module]); - return module; - } - - loadAndExecute(path) { - let module = this.loadAndParse(path); - module.declarationInstantiation(); - return module.evaluation(); - } - - importRoot(path) { - return this.loadAndExecute(path); - } - - ["import"](name, referencingInfo) { - let path = this.resolve(name, null); - return this.loadAndExecute(path); - } - - populateImportMeta(moduleInfo, metaObject) { - // For the shell, use the module's normalized path as the base URL. - - let path; - if (moduleInfo) { - path = moduleInfo.path; - } else { - path = "(unknown)"; - } - metaObject.url = path; - } - - dynamicImport(referencingInfo, specifier, promise) { - ReflectApply(PromiseResolve, PromiseClass, []) - .then(_ => { - let path = ReflectLoader.resolve(specifier, referencingInfo); - ReflectLoader.loadAndExecute(path); - finishDynamicModuleImport(referencingInfo, specifier, promise); - }).catch(err => { - abortDynamicModuleImport(referencingInfo, specifier, promise, err); - }); - } -}; - -setModuleLoadHook((path) => ReflectLoader.importRoot(path)); - -setModuleResolveHook((referencingInfo, requestName) => { - let path = ReflectLoader.resolve(requestName, referencingInfo); - return ReflectLoader.loadAndParse(path); -}); - -setModuleMetadataHook((module, metaObject) => { - ReflectLoader.populateImportMeta(module, metaObject); -}); - -setModuleDynamicImportHook((referencingInfo, specifier, promise) => { - ReflectLoader.dynamicImport(referencingInfo, specifier, promise); -}); -} diff --git a/js/src/shell/OSObject.cpp b/js/src/shell/OSObject.cpp index fc52922aacbb..7afadc0f4201 100644 --- a/js/src/shell/OSObject.cpp +++ b/js/src/shell/OSObject.cpp @@ -35,6 +35,7 @@ #include "js/PropertySpec.h" #include "js/Wrapper.h" #include "shell/jsshell.h" +#include "shell/StringUtils.h" #include "util/StringBuffer.h" #include "util/Text.h" #include "util/Windows.h" @@ -57,18 +58,8 @@ using js::shell::RCFile; namespace js { namespace shell { -#ifdef XP_WIN -const char PathSeparator = '\\'; -#else -const char PathSeparator = '/'; -#endif - -static bool IsAbsolutePath(const UniqueChars& filename) { - const char* pathname = filename.get(); - - if (pathname[0] == PathSeparator) { - return true; - } +bool IsAbsolutePath(JSLinearString* filename) { + size_t length = filename->length(); #ifdef XP_WIN // On Windows there are various forms of absolute paths (see @@ -79,16 +70,16 @@ static bool IsAbsolutePath(const UniqueChars& filename) { // "\\..." // "C:\..." // - // The first two cases are handled by the test above so we only need a test - // for the last one here. + // The first two cases are handled by the common test below so we only need a + // specific test for the last one here. - if ((strlen(pathname) > 3 && mozilla::IsAsciiAlpha(pathname[0]) && - pathname[1] == ':' && pathname[2] == '\\')) { + if (length > 3 && mozilla::IsAsciiAlpha(CharAt(filename, 0)) && + CharAt(filename, 1) == u':' && CharAt(filename, 2) == u'\\') { return true; } #endif - return false; + return length > 0 && CharAt(filename, 0) == PathSeparator; } /* @@ -109,13 +100,18 @@ JSString* ResolvePath(JSContext* cx, HandleString filenameStr, #endif } - UniqueChars filename = JS_EncodeStringToLatin1(cx, filenameStr); - if (!filename) { + RootedLinearString str(cx, JS_EnsureLinearString(cx, filenameStr)); + if (!str) { return nullptr; } - if (IsAbsolutePath(filename)) { - return filenameStr; + if (IsAbsolutePath(str)) { + return str; + } + + UniqueChars filename = JS_EncodeStringToLatin1(cx, str); + if (!filename) { + return nullptr; } JS::AutoFilename scriptFilename; @@ -757,12 +753,12 @@ static bool ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp) { return false; } - UniqueChars path = JS_EncodeStringToLatin1(cx, args[0].toString()); - if (!path) { + RootedLinearString str(cx, JS_EnsureLinearString(cx, args[0].toString())); + if (!str) { return false; } - args.rval().setBoolean(IsAbsolutePath(path)); + args.rval().setBoolean(IsAbsolutePath(str)); return true; } @@ -786,14 +782,19 @@ static bool ospath_join(JSContext* cx, unsigned argc, Value* vp) { return false; } - UniqueChars path = JS_EncodeStringToLatin1(cx, args[i].toString()); - if (!path) { + RootedLinearString str(cx, JS_EnsureLinearString(cx, args[i].toString())); + if (!str) { return false; } - if (IsAbsolutePath(path)) { + if (IsAbsolutePath(str)) { MOZ_ALWAYS_TRUE(buffer.resize(0)); } else if (i != 0) { + UniqueChars path = JS_EncodeStringToLatin1(cx, str); + if (!path) { + return false; + } + if (!buffer.append(PathSeparator)) { return false; } diff --git a/js/src/shell/OSObject.h b/js/src/shell/OSObject.h index f5b680283542..1ae23eae8e0b 100644 --- a/js/src/shell/OSObject.h +++ b/js/src/shell/OSObject.h @@ -14,6 +14,12 @@ namespace js { namespace shell { +#ifdef XP_WIN +constexpr char PathSeparator = '\\'; +#else +constexpr char PathSeparator = '/'; +#endif + struct RCFile; /* Define an os object on the given global object. */ @@ -22,6 +28,8 @@ bool DefineOS(JSContext* cx, JS::HandleObject global, bool fuzzingSafe, enum PathResolutionMode { RootRelative, ScriptRelative }; +bool IsAbsolutePath(JSLinearString* filename); + JSString* ResolvePath(JSContext* cx, JS::HandleString filenameStr, PathResolutionMode resolveMode); diff --git a/js/src/shell/StringUtils.h b/js/src/shell/StringUtils.h new file mode 100644 index 000000000000..be604eddee86 --- /dev/null +++ b/js/src/shell/StringUtils.h @@ -0,0 +1,147 @@ +/* -*- 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/. */ + +/* String utility functions used by the module loader. */ + +#ifndef shell_StringUtils_h +#define shell_StringUtils_h + +#include "jsapi.h" + +#include "js/StableStringChars.h" + +namespace js { +namespace shell { + +inline char16_t CharAt(JSLinearString* str, size_t index) { + return str->latin1OrTwoByteChar(index); +} + +inline JSLinearString* SubString(JSContext* cx, JSLinearString* str, + size_t start, size_t end) { + MOZ_ASSERT(start <= str->length()); + MOZ_ASSERT(end >= start && end <= str->length()); + return NewDependentString(cx, str, start, end - start); +} + +inline JSLinearString* SubString(JSContext* cx, JSLinearString* str, + size_t start) { + return SubString(cx, str, start, str->length()); +} + +template +bool StringStartsWith(JSLinearString* str, + const char16_t (&chars)[NullTerminatedLength]) { + MOZ_ASSERT(NullTerminatedLength > 0); + const size_t length = NullTerminatedLength - 1; + MOZ_ASSERT(chars[length] == '\0'); + + if (str->length() < length) { + return false; + } + + for (size_t i = 0; i < length; i++) { + if (CharAt(str, i) != chars[i]) { + return false; + } + } + + return true; +} + +template +bool StringEquals(JSLinearString* str, + const char16_t (&chars)[NullTerminatedLength]) { + MOZ_ASSERT(NullTerminatedLength > 0); + const size_t length = NullTerminatedLength - 1; + MOZ_ASSERT(chars[length] == '\0'); + + return str->length() == length && StringStartsWith(str, chars); +} + +inline int32_t IndexOf(HandleLinearString str, char16_t target, + size_t start = 0) { + int32_t length = str->length(); + for (int32_t i = start; i < length; i++) { + if (CharAt(str, i) == target) { + return i; + } + } + + return -1; +} + +inline int32_t LastIndexOf(HandleLinearString str, char16_t target) { + int32_t length = str->length(); + for (int32_t i = length - 1; i >= 0; i--) { + if (CharAt(str, i) == target) { + return i; + } + } + + return -1; +} + +inline JSLinearString* ReplaceCharGlobally(JSContext* cx, + HandleLinearString str, + char16_t target, + char16_t replacement) { + int32_t i = IndexOf(str, target); + if (i == -1) { + return str; + } + + JS::AutoStableStringChars chars(cx); + if (!chars.initTwoByte(cx, str)) { + return nullptr; + } + + Vector buf(cx); + if (!buf.append(chars.twoByteChars(), str->length())) { + return nullptr; + } + + for (; i < int32_t(buf.length()); i++) { + if (buf[i] == target) { + buf[i] = replacement; + } + } + + RootedString result(cx, JS_NewUCStringCopyN(cx, buf.begin(), buf.length())); + if (!result) { + return nullptr; + } + + return JS_EnsureLinearString(cx, result); +} + +inline JSString* JoinStrings(JSContext* cx, + Handle> strings, + HandleLinearString separator) { + RootedString result(cx, JS_GetEmptyString(cx)); + + for (size_t i = 0; i < strings.length(); i++) { + HandleString str = strings[i]; + if (i != 0) { + result = JS_ConcatStrings(cx, result, separator); + if (!result) { + return nullptr; + } + } + + result = JS_ConcatStrings(cx, result, str); + if (!result) { + return nullptr; + } + } + + return result; +} + +} // namespace shell +} // namespace js + +#endif // shell_StringUtils_h diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 250f587beebe..5288aae4e27d 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -64,7 +64,6 @@ # include "prerror.h" # include "prlink.h" #endif -#include "shellmoduleloader.out.h" #include "builtin/Array.h" #include "builtin/MapObject.h" @@ -193,17 +192,6 @@ enum JSShellExitCode { EXITCODE_TIMEOUT = 6 }; -// Define use of application-specific slots on the shell's global object. -enum GlobalAppSlot { - GlobalAppSlotModuleLoadHook, // Shell-specific; load a module graph - GlobalAppSlotModuleResolveHook, // HostResolveImportedModule - GlobalAppSlotModuleMetadataHook, // HostPopulateImportMeta - GlobalAppSlotModuleDynamicImportHook, // HostImportModuleDynamically - GlobalAppSlotCount -}; -static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS, - "Too many applications slots defined for shell global"); - /* * Note: This limit should match the stack limit set by the browser in * js/xpconnect/src/XPCJSContext.cpp @@ -861,6 +849,22 @@ void EnvironmentPreparer::invoke(HandleObject global, Closure& closure) { } } +JSObject* js::shell::CreateScriptPrivate(JSContext* cx, HandleString path) { + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return nullptr; + } + + if (path) { + RootedValue pathValue(cx, StringValue(path)); + if (!JS_DefineProperty(cx, info, "path", pathValue, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + return info; +} + static bool RegisterScriptPathWithModuleLoader(JSContext* cx, HandleScript script, const char* filename) { @@ -873,20 +877,9 @@ static bool RegisterScriptPathWithModuleLoader(JSContext* cx, return false; } - RootedObject infoObject(cx); - - Value val = JS::GetScriptPrivate(script); - if (val.isUndefined()) { - infoObject = JS_NewPlainObject(cx); - if (!infoObject) { - return false; - } - } else { - infoObject = val.toObjectOrNull(); - } - - RootedValue pathValue(cx, StringValue(path)); - if (!JS_DefineProperty(cx, infoObject, "path", pathValue, 0)) { + MOZ_ASSERT(JS::GetScriptPrivate(script).isUndefined()); + RootedObject infoObject(cx, CreateScriptPrivate(cx, path)); + if (!infoObject) { return false; } @@ -1002,63 +995,9 @@ static MOZ_MUST_USE bool RunBinAST(JSContext* cx, const char* filename, #endif // JS_BUILD_BINAST -static bool InitModuleLoader(JSContext* cx) { - // Decompress and evaluate the embedded module loader source to initialize - // the module loader for the current compartment. - - uint32_t srcLen = moduleloader::GetRawScriptsSize(); - auto src = cx->make_pod_array(srcLen); - if (!src || - !DecompressString(moduleloader::compressedSources, - moduleloader::GetCompressedSize(), - reinterpret_cast(src.get()), srcLen)) { - return false; - } - - CompileOptions options(cx); - options.setIntroductionType("shell module loader"); - options.setFileAndLine("shell/ModuleLoader.js", 1); - options.setSelfHostingMode(false); - options.setForceFullParse(); - options.setForceStrictMode(); - - JS::SourceText srcBuf; - if (!srcBuf.init(cx, std::move(src), srcLen)) { - return false; - } - - RootedValue rv(cx); - return JS::Evaluate(cx, options, srcBuf, &rv); -} - -static bool GetModuleImportHook(JSContext* cx, - MutableHandleFunction resultOut) { - Handle global = cx->global(); - RootedValue hookValue(cx, - global->getReservedSlot(GlobalAppSlotModuleLoadHook)); - if (hookValue.isUndefined()) { - JS_ReportErrorASCII(cx, "Module load hook not set"); - return false; - } - - if (!hookValue.isObject() || !hookValue.toObject().is()) { - JS_ReportErrorASCII(cx, "Module load hook is not a function"); - return false; - } - - resultOut.set(&hookValue.toObject().as()); - return true; -} - static MOZ_MUST_USE bool RunModule(JSContext* cx, const char* filename, - FILE* file, bool compileOnly) { - // Execute a module by calling the module loader's import hook on the - // resolved filename. - - RootedFunction importFun(cx); - if (!GetModuleImportHook(cx, &importFun)) { - return false; - } + bool compileOnly) { + ShellContext* sc = GetShellContext(cx); RootedString path(cx, JS_NewStringCopyZ(cx, filename)); if (!path) { @@ -1070,11 +1009,7 @@ static MOZ_MUST_USE bool RunModule(JSContext* cx, const char* filename, return false; } - JS::RootedValueArray<1> args(cx); - args[0].setString(path); - - RootedValue value(cx); - return JS_CallFunction(cx, nullptr, importFun, args, &value); + return sc->moduleLoader->loadRootModule(cx, path); } static void ShellCleanupFinalizationRegistryCallback(JSObject* registry, @@ -1584,7 +1519,7 @@ static MOZ_MUST_USE bool Process(JSContext* cx, const char* filename, } break; case FileModule: - if (!RunModule(cx, filename, file, compileOnly)) { + if (!RunModule(cx, filename, compileOnly)) { return false; } break; @@ -1931,7 +1866,7 @@ static bool ParseCompileOptions(JSContext* cx, CompileOptions& options, return false; } if (v.isObject()) { - RootedObject infoObject(cx, JS_NewPlainObject(cx)); + RootedObject infoObject(cx, CreateScriptPrivate(cx)); RootedValue elementValue(cx, v); if (!JS_WrapValue(cx, &elementValue)) { return false; @@ -5122,44 +5057,6 @@ static bool DecodeModule(JSContext* cx, unsigned argc, Value* vp) { return true; } -static bool SetModuleLoadHook(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "setModuleLoadHook", 1)) { - return false; - } - - if (!args[0].isObject() || !args[0].toObject().is()) { - const char* typeName = InformalValueTypeName(args[0]); - JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName); - return false; - } - - Handle global = cx->global(); - global->setReservedSlot(GlobalAppSlotModuleLoadHook, args[0]); - - args.rval().setUndefined(); - return true; -} - -static bool SetModuleResolveHook(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "setModuleResolveHook", 1)) { - return false; - } - - if (!args[0].isObject() || !args[0].toObject().is()) { - const char* typeName = InformalValueTypeName(args[0]); - JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName); - return false; - } - - Handle global = cx->global(); - global->setReservedSlot(GlobalAppSlotModuleResolveHook, args[0]); - - args.rval().setUndefined(); - return true; -} - static JSObject* ShellModuleResolveHook(JSContext* cx, HandleValue referencingPrivate, HandleString specifier) { @@ -5189,9 +5086,9 @@ static JSObject* ShellModuleResolveHook(JSContext* cx, return &result.toObject(); } -static bool SetModuleMetadataHook(JSContext* cx, unsigned argc, Value* vp) { +static bool SetModuleResolveHook(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "setModuleMetadataHook", 1)) { + if (!args.requireAtLeast(cx, "setModuleResolveHook", 1)) { return false; } @@ -5202,169 +5099,14 @@ static bool SetModuleMetadataHook(JSContext* cx, unsigned argc, Value* vp) { } Handle global = cx->global(); - global->setReservedSlot(GlobalAppSlotModuleMetadataHook, args[0]); + global->setReservedSlot(GlobalAppSlotModuleResolveHook, args[0]); + + JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook); args.rval().setUndefined(); return true; } -static bool CallModuleMetadataHook(JSContext* cx, HandleValue modulePrivate, - HandleObject metaObject) { - Handle global = cx->global(); - RootedValue hookValue( - cx, global->getReservedSlot(GlobalAppSlotModuleMetadataHook)); - if (hookValue.isUndefined()) { - JS_ReportErrorASCII(cx, "Module metadata hook not set"); - return false; - } - MOZ_ASSERT(hookValue.toObject().is()); - - JS::RootedValueArray<2> args(cx); - args[0].set(modulePrivate); - args[1].setObject(*metaObject); - - RootedValue dummy(cx); - return JS_CallFunctionValue(cx, nullptr, hookValue, args, &dummy); -} - -static bool ReportArgumentTypeError(JSContext* cx, HandleValue value, - const char* expected) { - const char* typeName = InformalValueTypeName(value); - JS_ReportErrorASCII(cx, "Expected %s, got %s", expected, typeName); - return false; -} - -static bool ShellSetModulePrivate(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - - if (!args.requireAtLeast(cx, "setModulePrivate", 2)) { - return false; - } - - if (!args[0].isObject() || !args[0].toObject().is()) { - return ReportArgumentTypeError(cx, args[0], "module object"); - } - - JS::SetModulePrivate(&args[0].toObject(), args[1]); - args.rval().setUndefined(); - return true; -} - -static bool ShellGetModulePrivate(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - - if (!args.requireAtLeast(cx, "getModulePrivate", 1)) { - return false; - } - - if (!args[0].isObject() || !args[0].toObject().is()) { - return ReportArgumentTypeError(cx, args[0], "module object"); - } - - args.rval().set(JS::GetModulePrivate(&args[0].toObject())); - return true; -} - -static bool SetModuleDynamicImportHook(JSContext* cx, unsigned argc, - Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "setModuleDynamicImportHook", 1)) { - return false; - } - - if (!args[0].isObject() || !args[0].toObject().is()) { - const char* typeName = InformalValueTypeName(args[0]); - JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName); - return false; - } - - Handle global = cx->global(); - global->setReservedSlot(GlobalAppSlotModuleDynamicImportHook, args[0]); - - args.rval().setUndefined(); - return true; -} - -static bool FinishDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "finishDynamicModuleImport", 3)) { - return false; - } - - if (!args[1].isString()) { - return ReportArgumentTypeError(cx, args[1], "String"); - } - - if (!args[2].isObject() || !args[2].toObject().is()) { - return ReportArgumentTypeError(cx, args[2], "PromiseObject"); - } - - RootedString specifier(cx, args[1].toString()); - Rooted promise(cx, &args[2].toObject().as()); - - return js::FinishDynamicModuleImport(cx, args[0], specifier, promise); -} - -static bool AbortDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "abortDynamicModuleImport", 4)) { - return false; - } - - if (!args[1].isString()) { - return ReportArgumentTypeError(cx, args[1], "String"); - } - - if (!args[2].isObject() || !args[2].toObject().is()) { - return ReportArgumentTypeError(cx, args[2], "PromiseObject"); - } - - RootedString specifier(cx, args[1].toString()); - Rooted promise(cx, &args[2].toObject().as()); - - cx->setPendingExceptionAndCaptureStack(args[3]); - return js::FinishDynamicModuleImport(cx, args[0], specifier, promise); -} - -static bool ShellModuleDynamicImportHook(JSContext* cx, - HandleValue referencingPrivate, - HandleString specifier, - HandleObject promise) { - Handle global = cx->global(); - RootedValue hookValue( - cx, global->getReservedSlot(GlobalAppSlotModuleDynamicImportHook)); - if (hookValue.isUndefined()) { - JS_ReportErrorASCII(cx, "Module resolve hook not set"); - return false; - } - MOZ_ASSERT(hookValue.toObject().is()); - - JS::RootedValueArray<3> args(cx); - args[0].set(referencingPrivate); - args[1].setString(specifier); - args[2].setObject(*promise); - - RootedValue result(cx); - return JS_CallFunctionValue(cx, nullptr, hookValue, args, &result); -} - -static bool GetModuleLoadPath(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - - ShellContext* sc = GetShellContext(cx); - if (sc->moduleLoadPath) { - JSString* str = JS_NewStringCopyZ(cx, sc->moduleLoadPath.get()); - if (!str) { - return false; - } - - args.rval().setString(str); - } else { - args.rval().setNull(); - } - return true; -} - #if defined(JS_BUILD_BINAST) template @@ -8924,52 +8666,11 @@ static const JSFunctionSpecWithHelp shell_functions[] = { "decodeModule(code)", " Takes a XDR bytecode representation of an uninstantiated ModuleObject and returns a ModuleObject."), - JS_FN_HELP("setModuleLoadHook", SetModuleLoadHook, 1, 0, -"setModuleLoadHook(function(path))", -" Set the shell specific module load hook to |function|.\n" -" This hook is used to load a module graph. It should be implemented by the\n" -" module loader."), - JS_FN_HELP("setModuleResolveHook", SetModuleResolveHook, 1, 0, "setModuleResolveHook(function(referrer, specifier))", -" Set the HostResolveImportedModule hook to |function|.\n" -" This hook is used to look up a previously loaded module object. It should\n" -" be implemented by the module loader."), - - JS_FN_HELP("setModuleMetadataHook", SetModuleMetadataHook, 1, 0, -"setModuleMetadataHook(function(module) {})", -" Set the HostPopulateImportMeta hook to |function|.\n" -" This hook is used to create the metadata object returned by import.meta for\n" -" a module. It should be implemented by the module loader."), - - JS_FN_HELP("setModuleDynamicImportHook", SetModuleDynamicImportHook, 1, 0, -"setModuleDynamicImportHook(function(referrer, specifier, promise))", -" Set the HostImportModuleDynamically hook to |function|.\n" -" This hook is used to dynamically import a module. It should\n" -" be implemented by the module loader."), - - JS_FN_HELP("finishDynamicModuleImport", FinishDynamicModuleImport, 3, 0, -"finishDynamicModuleImport(referrer, specifier, promise)", -" The module loader's dynamic import hook should call this when the module has" -" been loaded successfully."), - - JS_FN_HELP("abortDynamicModuleImport", AbortDynamicModuleImport, 4, 0, -"abortDynamicModuleImport(referrer, specifier, promise, error)", -" The module loader's dynamic import hook should call this when the module " -" import has failed."), - - JS_FN_HELP("setModulePrivate", ShellSetModulePrivate, 2, 0, -"setModulePrivate(scriptObject, privateValue)", -" Associate a private value with a module object.\n"), - - JS_FN_HELP("getModulePrivate", ShellGetModulePrivate, 2, 0, -"getModulePrivate(scriptObject)", -" Get the private value associated with a module object.\n"), - - JS_FN_HELP("getModuleLoadPath", GetModuleLoadPath, 0, 0, -"getModuleLoadPath()", -" Return any --module-load-path argument passed to the shell. Used by the\n" -" module loader.\n"), +" Set the HostResolveImportedModule hook to |function|, overriding the shell\n" +" module loader for testing purposes. This hook is used to look up a\n" +" previously loaded module object."), #if defined(JS_BUILD_BINAST) @@ -10367,27 +10068,25 @@ static MOZ_MUST_USE bool ProcessArgs(JSContext* cx, OptionParser* op) { return Process(cx, nullptr, forceTTY, FileScript); } - if (const char* path = op->getStringOption("module-load-path")) { - RootedString jspath(cx, JS_NewStringCopyZ(cx, path)); + RootedString moduleLoadPath(cx); + if (const char* option = op->getStringOption("module-load-path")) { + RootedString jspath(cx, JS_NewStringCopyZ(cx, option)); if (!jspath) { return false; } - JSString* absolutePath = js::shell::ResolvePath(cx, jspath, RootRelative); - if (!absolutePath) { - return false; - } - - sc->moduleLoadPath = JS_EncodeStringToLatin1(cx, absolutePath); + moduleLoadPath = js::shell::ResolvePath(cx, jspath, RootRelative); } else { - sc->moduleLoadPath = js::shell::GetCWD(); + UniqueChars cwd = js::shell::GetCWD(); + moduleLoadPath = JS_NewStringCopyZ(cx, cwd.get()); } - if (!sc->moduleLoadPath) { + if (!moduleLoadPath) { return false; } - if (!InitModuleLoader(cx)) { + sc->moduleLoader = js::MakeUnique(); + if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) { return false; } @@ -11866,10 +11565,6 @@ int main(int argc, char** argv, char** envp) { js::SetPreserveWrapperCallback(cx, DummyPreserveWrapperCallback); - JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook); - JS::SetModuleDynamicImportHook(cx->runtime(), ShellModuleDynamicImportHook); - JS::SetModuleMetadataHook(cx->runtime(), CallModuleMetadataHook); - if (op.getBoolOption("disable-wasm-huge-memory")) { if (!sCompilerProcessFlags.append("--disable-wasm-huge-memory")) { return EXIT_FAILURE; diff --git a/js/src/shell/jsshell.h b/js/src/shell/jsshell.h index 9150e09b5cbe..dc410a7f17bb 100644 --- a/js/src/shell/jsshell.h +++ b/js/src/shell/jsshell.h @@ -16,6 +16,7 @@ #include "builtin/MapObject.h" #include "js/GCVector.h" +#include "shell/ModuleLoader.h" #include "threading/ConditionVariable.h" #include "threading/LockGuard.h" #include "threading/Mutex.h" @@ -32,6 +33,15 @@ namespace js { namespace shell { +// Define use of application-specific slots on the shell's global object. +enum GlobalAppSlot { + GlobalAppSlotModuleRegistry, + GlobalAppSlotModuleResolveHook, // HostResolveImportedModule + GlobalAppSlotCount +}; +static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS, + "Too many applications slots defined for shell global"); + enum JSShellErrNum { #define MSG_DEF(name, count, exception, format) name, #include "jsshell.msg" @@ -232,7 +242,7 @@ struct ShellContext { UniquePtr geckoProfilingStack; - JS::UniqueChars moduleLoadPath; + UniquePtr moduleLoader; UniquePtr markObservers; @@ -250,6 +260,9 @@ extern ShellContext* GetShellContext(JSContext* cx); extern MOZ_MUST_USE bool PrintStackTrace(JSContext* cx, JS::Handle stackObj); +extern JSObject* CreateScriptPrivate(JSContext* cx, + HandleString path = nullptr); + } /* namespace shell */ } /* namespace js */ diff --git a/js/src/shell/moz.build b/js/src/shell/moz.build index 794092487674..bfcfedb222fc 100644 --- a/js/src/shell/moz.build +++ b/js/src/shell/moz.build @@ -19,6 +19,7 @@ UNIFIED_SOURCES += [ 'js.cpp', 'jsoptparse.cpp', 'jsshell.cpp', + 'ModuleLoader.cpp', 'OSObject.cpp', 'WasmTesting.cpp' ] @@ -38,15 +39,6 @@ LOCAL_INCLUDES += [ OS_LIBS += CONFIG['EDITLINE_LIBS'] -# Prepare module loader JS code for embedding -GeneratedFile('shellmoduleloader.out.h', 'shellmoduleloader.js', - script='../builtin/embedjs.py', - entry_point='generate_shellmoduleloader', - inputs=[ - '../js.msg', - 'ModuleLoader.js', - ]) - # Place a GDB Python auto-load file next to the shell executable, both in # the build directory and in the dist/bin directory. DEFINES['topsrcdir'] = '%s/js/src' % TOPSRCDIR