Bug 1637529 - Rewrite the shell module loader in C++ r=jandem
☠☠ backed out by 1acf50c37541 ☠ ☠
authorJon Coppeard <jcoppeard@mozilla.com>
Fri, 15 May 2020 15:16:12 +0000
changeset 530297 bd25dceab6202a317782fd1dcc87c85567b53d67
parent 530296 170cb6b6d8301e2a4834a54fa00a47c13e6e296a
child 530298 e38a8629520c12289c410189b37c7222372d5fb6
push id37420
push usernerli@mozilla.com
push dateFri, 15 May 2020 21:52:36 +0000
treeherdermozilla-central@f340bbb582d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem
bugs1637529
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1637529 - Rewrite the shell module loader in C++ r=jandem Sorry for the big patch. This is a straight rewrite of shell/ModuleLoader.js in C++. It's mostly straigtforward but there were a couple of clunky parts: using promises/closures from C++ was rather verbose and I had to write some string utilities. Differential Revision: https://phabricator.services.mozilla.com/D75271
js/src/shell/ModuleLoader.cpp
js/src/shell/ModuleLoader.h
js/src/shell/ModuleLoader.js
js/src/shell/OSObject.cpp
js/src/shell/OSObject.h
js/src/shell/StringUtils.h
js/src/shell/js.cpp
js/src/shell/jsshell.h
js/src/shell/moz.build
new file mode 100644
--- /dev/null
+++ b/js/src/shell/ModuleLoader.cpp
@@ -0,0 +1,541 @@
+/* -*- 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;
+
+/*
+#ifdef XP_WIN
+static const char16_t PathSeparator = u'\\';
+#else
+static const char16_t PathSeparator = u'/';
+#endif
+*/
+
+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<CanGC>(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<bool> 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<char16_t> 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<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,
+                                            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<GCVector<JSLinearString*>> 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);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/shell/ModuleLoader.h
@@ -0,0 +1,68 @@
+/* -*- 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<JSLinearString*> pathOut);
+  JSLinearString* normalizePath(JSContext* cx, HandleLinearString path);
+  JSObject* getOrCreateModuleRegistry(JSContext* cx);
+  JSString* fetchSource(JSContext* cx, HandleLinearString path);
+
+  JSAtom* loadPathStr = nullptr;
+  JSAtom* pathSeparatorStr = nullptr;
+};
+
+}  // namespace shell
+}  // namespace js
+
+#endif  // shell_ModuleLoader_h
deleted file mode 100644
--- 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);
-});
-}
--- a/js/src/shell/OSObject.cpp
+++ b/js/src/shell/OSObject.cpp
@@ -30,16 +30,17 @@
 #include "jsfriendapi.h"
 
 #include "gc/FreeOp.h"
 #include "js/CharacterEncoding.h"
 #include "js/Conversions.h"
 #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"
 #include "vm/JSObject.h"
 #include "vm/TypedArrayObject.h"
 
 #include "vm/JSObject-inl.h"
 
@@ -52,48 +53,38 @@
 #  include <libgen.h>
 #endif
 
 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
   // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
   // for details):
   //
   //   "\..."
   //   "\\..."
   //   "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;
 }
 
 /*
  * Resolve a (possibly) relative filename to an absolute path. If
  * |scriptRelative| is true, then the result will be relative to the directory
  * containing the currently-running script, or the current working directory if
  * the currently-running script is "-e" (namely, you're using it from the
  * command line.) Otherwise, it will be relative to the current working
@@ -104,23 +95,28 @@ JSString* ResolvePath(JSContext* cx, Han
   if (!filenameStr) {
 #ifdef XP_WIN
     return JS_NewStringCopyZ(cx, "nul");
 #else
     return JS_NewStringCopyZ(cx, "/dev/null");
 #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;
   if (resolveMode == ScriptRelative) {
     // Get the currently executing script's name.
     if (!DescribeScriptedCaller(cx, &scriptFilename)) {
       return nullptr;
     }
@@ -752,22 +748,22 @@ static bool ospath_isAbsolute(JSContext*
   CallArgs args = CallArgsFromVp(argc, vp);
 
   if (args.length() != 1 || !args[0].isString()) {
     JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
                               JSSMSG_INVALID_ARGS, "isAbsolute");
     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;
 }
 
 static bool ospath_join(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   if (args.length() < 1) {
     JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
@@ -781,24 +777,29 @@ static bool ospath_join(JSContext* cx, u
   JSStringBuilder buffer(cx);
 
   for (unsigned i = 0; i < args.length(); i++) {
     if (!args[i].isString()) {
       JS_ReportErrorASCII(cx, "join expects string arguments only");
       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;
       }
     }
 
     if (!buffer.append(args[i].toString())) {
       return false;
     }
--- a/js/src/shell/OSObject.h
+++ b/js/src/shell/OSObject.h
@@ -9,24 +9,32 @@
 #ifndef shell_OSObject_h
 #define shell_OSObject_h
 
 #include "jsapi.h"
 
 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. */
 bool DefineOS(JSContext* cx, JS::HandleObject global, bool fuzzingSafe,
               RCFile** shellOut, RCFile** shellErr);
 
 enum PathResolutionMode { RootRelative, ScriptRelative };
 
+bool IsAbsolutePath(JSLinearString* filename);
+
 JSString* ResolvePath(JSContext* cx, JS::HandleString filenameStr,
                       PathResolutionMode resolveMode);
 
 JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr);
 
 JS::UniqueChars GetCWD();
 
 }  // namespace shell
new file mode 100644
--- /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 <size_t NullTerminatedLength>
+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 <size_t NullTerminatedLength>
+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<char16_t> 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<GCVector<JSLinearString*>> 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
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -59,17 +59,16 @@
 
 #include "jsapi.h"
 #include "jsfriendapi.h"
 #include "jstypes.h"
 #ifndef JS_WITHOUT_NSPR
 #  include "prerror.h"
 #  include "prlink.h"
 #endif
-#include "shellmoduleloader.out.h"
 
 #include "builtin/Array.h"
 #include "builtin/MapObject.h"
 #include "builtin/ModuleObject.h"
 #include "builtin/RegExp.h"
 #include "builtin/TestingFunctions.h"
 #include "debugger/DebugAPI.h"
 #if defined(JS_BUILD_BINAST)
@@ -188,27 +187,16 @@ using mozilla::Variant;
 
 enum JSShellExitCode {
   EXITCODE_RUNTIME_ERROR = 3,
   EXITCODE_FILE_NOT_FOUND = 4,
   EXITCODE_OUT_OF_MEMORY = 5,
   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
  */
 #if defined(MOZ_ASAN) || (defined(DEBUG) && !defined(XP_WIN))
 static const size_t gMaxStackSize = 2 * 128 * sizeof(size_t) * 1024;
 #else
 static const size_t gMaxStackSize = 128 * sizeof(size_t) * 1024;
@@ -856,42 +844,47 @@ void EnvironmentPreparer::invoke(HandleO
 
   AutoRealm ar(cx, global);
   AutoReportException are(cx);
   if (!closure(cx)) {
     return;
   }
 }
 
+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) {
   // Set the private value associated with a script to a object containing the
   // script's filename so that the module loader can use it to resolve
   // relative imports.
 
   RootedString path(cx, JS_NewStringCopyZ(cx, filename));
   if (!path) {
     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;
   }
 
   JS::SetScriptPrivate(script, ObjectValue(*infoObject));
   return true;
 }
 
 enum class CompileUtf8 {
@@ -997,89 +990,31 @@ static MOZ_MUST_USE bool RunBinAST(JSCon
     return true;
   }
 
   return JS_ExecuteScript(cx, script);
 }
 
 #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<char>(srcLen);
-  if (!src ||
-      !DecompressString(moduleloader::compressedSources,
-                        moduleloader::GetCompressedSize(),
-                        reinterpret_cast<unsigned char*>(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<Utf8Unit> 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<GlobalObject*> 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<JSFunction>()) {
-    JS_ReportErrorASCII(cx, "Module load hook is not a function");
-    return false;
-  }
-
-  resultOut.set(&hookValue.toObject().as<JSFunction>());
-  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) {
     return false;
   }
 
   path = ResolvePath(cx, path, RootRelative);
   if (!path) {
     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,
                                                      void* data) {
   // In the browser this queues a task. Shell jobs correspond to microtasks so
   // we arrange for cleanup to happen after all jobs/microtasks have run.
   auto sc = static_cast<ShellContext*>(data);
   AutoEnterOOMUnsafeRegion oomUnsafe;
@@ -1579,17 +1514,17 @@ static MOZ_MUST_USE bool Process(JSConte
         break;
       case FileScriptUtf16:
         if (!RunFile(cx, filename, file, CompileUtf8::InflateToUtf16,
                      compileOnly)) {
           return false;
         }
         break;
       case FileModule:
-        if (!RunModule(cx, filename, file, compileOnly)) {
+        if (!RunModule(cx, filename, compileOnly)) {
           return false;
         }
         break;
 #if defined(JS_BUILD_BINAST)
       case FileBinASTMultipart:
         if (!RunBinAST(cx, filename, file, compileOnly,
                        JS::BinASTFormat::Multipart)) {
           return false;
@@ -1926,17 +1861,17 @@ static bool ParseCompileOptions(JSContex
   if (!v.isUndefined()) {
     options.setSkipFilenameValidation(ToBoolean(v));
   }
 
   if (!JS_GetProperty(cx, opts, "element", &v)) {
     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;
     }
     if (!JS_DefineProperty(cx, infoObject, "element", elementValue, 0)) {
       return false;
     }
     options.setPrivateValue(ObjectValue(*infoObject));
@@ -5117,54 +5052,16 @@ static bool DecodeModule(JSContext* cx, 
   if (!ModuleObject::Freeze(cx, modObject)) {
     return false;
   }
 
   args.rval().setObject(*modObject);
   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<JSFunction>()) {
-    const char* typeName = InformalValueTypeName(args[0]);
-    JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName);
-    return false;
-  }
-
-  Handle<GlobalObject*> 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<JSFunction>()) {
-    const char* typeName = InformalValueTypeName(args[0]);
-    JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName);
-    return false;
-  }
-
-  Handle<GlobalObject*> global = cx->global();
-  global->setReservedSlot(GlobalAppSlotModuleResolveHook, args[0]);
-
-  args.rval().setUndefined();
-  return true;
-}
-
 static JSObject* ShellModuleResolveHook(JSContext* cx,
                                         HandleValue referencingPrivate,
                                         HandleString specifier) {
   Handle<GlobalObject*> global = cx->global();
   RootedValue hookValue(
       cx, global->getReservedSlot(GlobalAppSlotModuleResolveHook));
   if (hookValue.isUndefined()) {
     JS_ReportErrorASCII(cx, "Module resolve hook not set");
@@ -5184,192 +5081,37 @@ static JSObject* ShellModuleResolveHook(
   if (!result.isObject() || !result.toObject().is<ModuleObject>()) {
     JS_ReportErrorASCII(cx, "Module resolve hook did not return Module object");
     return nullptr;
   }
 
   return &result.toObject();
 }
 
-static bool SetModuleMetadataHook(JSContext* cx, unsigned argc, Value* vp) {
-  CallArgs args = CallArgsFromVp(argc, vp);
-  if (!args.requireAtLeast(cx, "setModuleMetadataHook", 1)) {
+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<JSFunction>()) {
     const char* typeName = InformalValueTypeName(args[0]);
     JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName);
     return false;
   }
 
   Handle<GlobalObject*> global = cx->global();
-  global->setReservedSlot(GlobalAppSlotModuleMetadataHook, args[0]);
-
-  args.rval().setUndefined();
-  return true;
-}
-
-static bool CallModuleMetadataHook(JSContext* cx, HandleValue modulePrivate,
-                                   HandleObject metaObject) {
-  Handle<GlobalObject*> 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<JSFunction>());
-
-  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<ModuleObject>()) {
-    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<ModuleObject>()) {
-    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<JSFunction>()) {
-    const char* typeName = InformalValueTypeName(args[0]);
-    JS_ReportErrorASCII(cx, "expected hook function, got %s", typeName);
-    return false;
-  }
-
-  Handle<GlobalObject*> global = cx->global();
-  global->setReservedSlot(GlobalAppSlotModuleDynamicImportHook, args[0]);
+  global->setReservedSlot(GlobalAppSlotModuleResolveHook, args[0]);
+
+  JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook);
 
   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<PromiseObject>()) {
-    return ReportArgumentTypeError(cx, args[2], "PromiseObject");
-  }
-
-  RootedString specifier(cx, args[1].toString());
-  Rooted<PromiseObject*> promise(cx, &args[2].toObject().as<PromiseObject>());
-
-  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<PromiseObject>()) {
-    return ReportArgumentTypeError(cx, args[2], "PromiseObject");
-  }
-
-  RootedString specifier(cx, args[1].toString());
-  Rooted<PromiseObject*> promise(cx, &args[2].toObject().as<PromiseObject>());
-
-  cx->setPendingExceptionAndCaptureStack(args[3]);
-  return js::FinishDynamicModuleImport(cx, args[0], specifier, promise);
-}
-
-static bool ShellModuleDynamicImportHook(JSContext* cx,
-                                         HandleValue referencingPrivate,
-                                         HandleString specifier,
-                                         HandleObject promise) {
-  Handle<GlobalObject*> 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<JSFunction>());
-
-  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 <typename Tok>
 static bool ParseBinASTData(JSContext* cx, uint8_t* buf_data,
                             uint32_t buf_length,
                             js::frontend::GlobalSharedContext* globalsc,
                             js::frontend::CompilationInfo& compilationInfo,
                             const JS::ReadOnlyCompileOptions& options) {
@@ -8919,62 +8661,21 @@ static const JSFunctionSpecWithHelp shel
     JS_FN_HELP("codeModule", CodeModule, 1, 0,
 "codeModule(module)",
 "   Takes an uninstantiated ModuleObject and returns a XDR bytecode representation of that ModuleObject."),
 
     JS_FN_HELP("decodeModule", DecodeModule, 1, 0,
 "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)
 
 JS_FN_HELP("parseBin", BinParse, 1, 0,
 "parseBin(arraybuffer, [options])",
 "  Parses a Binary AST, potentially throwing. If present, |options| may\n"
 "  have properties saying how the passed |arraybuffer| should be handled:\n"
 "      format: the format of the BinAST file\n"
@@ -10362,37 +10063,35 @@ static MOZ_MUST_USE bool ProcessArgs(JSC
       modulePaths.empty() && binASTPaths.empty() &&
       !op->getStringArg("script")) {
     // Always use the interactive shell when -i is used. Without -i we let
     // Process figure it out based on isatty.
     bool forceTTY = op->getBoolOption('i');
     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();
-  }
-
-  if (!sc->moduleLoadPath) {
-    return false;
-  }
-
-  if (!InitModuleLoader(cx)) {
+    UniqueChars cwd = js::shell::GetCWD();
+    moduleLoadPath = JS_NewStringCopyZ(cx, cwd.get());
+  }
+
+  if (!moduleLoadPath) {
+    return false;
+  }
+
+  sc->moduleLoader = js::MakeUnique<ModuleLoader>();
+  if (!sc->moduleLoader || !sc->moduleLoader->init(cx, moduleLoadPath)) {
     return false;
   }
 
   while (!filePaths.empty() || !utf16FilePaths.empty() || !codeChunks.empty() ||
          !modulePaths.empty() || !binASTPaths.empty()) {
     size_t fpArgno = filePaths.empty() ? SIZE_MAX : filePaths.argno();
     size_t ufpArgno =
         utf16FilePaths.empty() ? SIZE_MAX : utf16FilePaths.argno();
@@ -11861,20 +11560,16 @@ int main(int argc, char** argv, char** e
     JS_SetGCParameter(cx, JSGC_MODE, JSGC_MODE_ZONE_INCREMENTAL);
     JS_SetGCParameter(cx, JSGC_SLICE_TIME_BUDGET_MS, 10);
   }
 
   JS::SetProcessLargeAllocationFailureCallback(my_LargeAllocFailCallback);
 
   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;
     }
     bool disabledHugeMemory = JS::DisableWasmHugeMemory();
     MOZ_RELEASE_ASSERT(disabledHugeMemory);
   }
 
--- a/js/src/shell/jsshell.h
+++ b/js/src/shell/jsshell.h
@@ -11,32 +11,42 @@
 #include "mozilla/Maybe.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Variant.h"
 
 #include "jsapi.h"
 
 #include "builtin/MapObject.h"
 #include "js/GCVector.h"
+#include "shell/ModuleLoader.h"
 #include "threading/ConditionVariable.h"
 #include "threading/LockGuard.h"
 #include "threading/Mutex.h"
 #include "threading/Thread.h"
 #include "vm/GeckoProfiler.h"
 #include "vm/Monitor.h"
 
 // Some platform hooks must be implemented for single-step profiling.
 #if defined(JS_SIMULATOR_ARM) || defined(JS_SIMULATOR_MIPS64) || \
     defined(JS_SIMULATOR_MIPS32)
 #  define SINGLESTEP_PROFILING
 #endif
 
 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"
 #undef MSG_DEF
   JSShellErr_Limit
 };
 
 const JSErrorFormatString* my_GetErrorMessage(void* userRef,
@@ -227,17 +237,17 @@ struct ShellContext {
   JS::UniqueChars readLineBuf;
   size_t readLineBufPos;
 
   js::shell::RCFile** errFilePtr;
   js::shell::RCFile** outFilePtr;
 
   UniquePtr<ProfilingStack> geckoProfilingStack;
 
-  JS::UniqueChars moduleLoadPath;
+  UniquePtr<ModuleLoader> moduleLoader;
 
   UniquePtr<MarkBitObservers> markObservers;
 
   // Off-thread parse state.
   js::Monitor offThreadMonitor;
   Vector<OffThreadJob*, 0, SystemAllocPolicy> offThreadJobs;
 
   // Queued finalization registry cleanup jobs.
@@ -245,12 +255,15 @@ struct ShellContext {
   JS::PersistentRooted<ObjectVector> finalizationRegistriesToCleanUp;
 };
 
 extern ShellContext* GetShellContext(JSContext* cx);
 
 extern MOZ_MUST_USE bool PrintStackTrace(JSContext* cx,
                                          JS::Handle<JSObject*> stackObj);
 
+extern JSObject* CreateScriptPrivate(JSContext* cx,
+                                     HandleString path = nullptr);
+
 } /* namespace shell */
 } /* namespace js */
 
 #endif
--- a/js/src/shell/moz.build
+++ b/js/src/shell/moz.build
@@ -14,16 +14,17 @@ if CONFIG['JS_SHELL_NAME']:
 include('../js-config.mozbuild')
 include('../js-cxxflags.mozbuild')
 include('../js-standalone.mozbuild')
 
 UNIFIED_SOURCES += [
     'js.cpp',
     'jsoptparse.cpp',
     'jsshell.cpp',
+    'ModuleLoader.cpp',
     'OSObject.cpp',
     'WasmTesting.cpp'
 ]
 
 if CONFIG['FUZZING_INTERFACES']:
     UNIFIED_SOURCES += ['jsrtfuzzing/jsrtfuzzing.cpp']
     USE_LIBS += [
         'static:fuzzer',
@@ -33,25 +34,16 @@ DEFINES['EXPORT_JS_API'] = True
 
 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
 FINAL_TARGET_PP_FILES += ['js-gdb.py.in']
 OBJDIR_FILES.js.src.shell += ['!/dist/bin/js-gdb.py']
 
 # People expect the js shell to wind up in the top-level JS dir.
 OBJDIR_FILES.js.src += ['!/dist/bin/js%s' % CONFIG['BIN_SUFFIX']]