summaryrefslogtreecommitdiffstats
path: root/devtools/shared/loader
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/shared/loader
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/loader')
-rw-r--r--devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs45
-rw-r--r--devtools/shared/loader/Loader.sys.mjs209
-rw-r--r--devtools/shared/loader/base-loader.sys.mjs640
-rw-r--r--devtools/shared/loader/browser-loader-mocks.js72
-rw-r--r--devtools/shared/loader/browser-loader.js239
-rw-r--r--devtools/shared/loader/builtin-modules.js203
-rw-r--r--devtools/shared/loader/loader-plugin-raw.sys.mjs39
-rw-r--r--devtools/shared/loader/moz.build21
-rw-r--r--devtools/shared/loader/worker-loader.js536
9 files changed, 2004 insertions, 0 deletions
diff --git a/devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs b/devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs
new file mode 100644
index 0000000000..06c33b8891
--- /dev/null
+++ b/devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+const { DevToolsLoader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs",
+ {
+ // `loadInDevToolsLoader` will import the loader in a special priviledged
+ // global created for DevTools, which will be reused as the shared global
+ // to load additional modules for the "DistinctSystemPrincipalLoader".
+ loadInDevToolsLoader: true,
+ }
+);
+
+// When debugging system principal resources (JSMs, chrome documents, ...)
+// We have to load DevTools actors in another system principal global.
+// That's mostly because of spidermonkey's Debugger API which requires
+// debuggee and debugger to be in distinct principals.
+//
+// We try to hold a single instance of this special loader via this API.
+//
+// @param requester object
+// Object/instance which is using the loader.
+// The same requester object should be passed to release method.
+let systemLoader = null;
+const systemLoaderRequesters = new Set();
+
+export function useDistinctSystemPrincipalLoader(requester) {
+ if (!systemLoader) {
+ systemLoader = new DevToolsLoader({
+ useDevToolsLoaderGlobal: true,
+ });
+ systemLoaderRequesters.clear();
+ }
+ systemLoaderRequesters.add(requester);
+ return systemLoader;
+}
+
+export function releaseDistinctSystemPrincipalLoader(requester) {
+ systemLoaderRequesters.delete(requester);
+ if (systemLoaderRequesters.size == 0) {
+ systemLoader.destroy();
+ systemLoader = null;
+ }
+}
diff --git a/devtools/shared/loader/Loader.sys.mjs b/devtools/shared/loader/Loader.sys.mjs
new file mode 100644
index 0000000000..e34810dde8
--- /dev/null
+++ b/devtools/shared/loader/Loader.sys.mjs
@@ -0,0 +1,209 @@
+/* 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/. */
+
+/**
+ * Manages the base loader (base-loader.sys.mjs) instance used to load the developer tools.
+ */
+
+import {
+ Loader,
+ Require,
+ resolveURI,
+ unload,
+} from "resource://devtools/shared/loader/base-loader.sys.mjs";
+import { requireRawId } from "resource://devtools/shared/loader/loader-plugin-raw.sys.mjs";
+
+export const DEFAULT_SANDBOX_NAME = "DevTools (Module loader)";
+
+var gNextLoaderID = 0;
+
+/**
+ * The main devtools API. The standard instance of this loader is exported as
+ * |loader| below, but if a fresh copy of the loader is needed, then a new
+ * one can also be created.
+ *
+ * The two following boolean flags are used to control the sandboxes into
+ * which the modules are loaded.
+ * @param invisibleToDebugger boolean
+ * If true, the modules won't be visible by the Debugger API.
+ * This typically allows to hide server modules from the debugger panel.
+ * @param freshCompartment boolean
+ * If true, the modules will be forced to be loaded in a distinct
+ * compartment. It is typically used to load the modules in a distinct
+ * system compartment, different from the main one, which is shared by
+ * all JSMs, XPCOMs and modules loaded with this flag set to true.
+ * We use this in order to debug modules loaded in this shared system
+ * compartment. The debugger actor has to be running in a distinct
+ * compartment than the context it is debugging.
+ * @param useDevToolsLoaderGlobal boolean
+ * If true, the loader will reuse the current global to load other
+ * modules instead of creating a sandbox with custom options. Cannot be
+ * used with invisibleToDebugger and/or freshCompartment.
+ * TODO: This should ultimately replace invisibleToDebugger.
+ */
+export function DevToolsLoader({
+ invisibleToDebugger = false,
+ freshCompartment = false,
+ useDevToolsLoaderGlobal = false,
+} = {}) {
+ if (useDevToolsLoaderGlobal && (invisibleToDebugger || freshCompartment)) {
+ throw new Error(
+ "Loader cannot use invisibleToDebugger or freshCompartment if useDevToolsLoaderGlobal is true"
+ );
+ }
+
+ const paths = {
+ // This resource:// URI is only registered when running DAMP tests.
+ // This is done by: testing/talos/talos/tests/devtools/addon/api.js
+ "damp-test": "resource://damp-test/content",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ devtools: "resource://devtools",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Allow access to xpcshell test items from the loader.
+ "xpcshell-test": "resource://test",
+
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ // Allow access to locale data using paths closer to what is
+ // used in the source tree.
+ "devtools/client/locales": "chrome://devtools/locale",
+ "devtools/shared/locales": "chrome://devtools-shared/locale",
+ "devtools/startup/locales": "chrome://devtools-startup/locale",
+ "toolkit/locales": "chrome://global/locale",
+ };
+
+ const sharedGlobal = useDevToolsLoaderGlobal
+ ? Cu.getGlobalForObject({})
+ : undefined;
+ this.loader = new Loader({
+ paths,
+ sharedGlobal,
+ invisibleToDebugger,
+ freshCompartment,
+ sandboxName: useDevToolsLoaderGlobal
+ ? "DevTools (Server Module Loader)"
+ : DEFAULT_SANDBOX_NAME,
+ // Make sure `define` function exists. JSON Viewer needs modules in AMD
+ // format, as it currently uses RequireJS from a content document and
+ // can't access our usual loaders. So, any modules shared with the JSON
+ // Viewer should include a define wrapper:
+ //
+ // // Make this available to both AMD and CJS environments
+ // define(function(require, exports, module) {
+ // ... code ...
+ // });
+ //
+ // Bug 1248830 will work out a better plan here for our content module
+ // loading needs, especially as we head towards devtools.html.
+ supportAMDModules: true,
+ requireHook: (id, require) => {
+ if (id.startsWith("raw!") || id.startsWith("theme-loader!")) {
+ return requireRawId(id, require);
+ }
+ return require(id);
+ },
+ });
+
+ this.require = Require(this.loader, { id: "devtools" });
+
+ // Various globals are available from ESM, but not from sandboxes,
+ // inject them into the globals list.
+ // Changes here should be mirrored to devtools/.eslintrc.
+ const injectedGlobals = {
+ CanonicalBrowsingContext,
+ console,
+ BrowsingContext,
+ ChromeWorker,
+ DebuggerNotificationObserver,
+ DOMPoint,
+ DOMQuad,
+ DOMRect,
+ fetch,
+ HeapSnapshot,
+ IOUtils,
+ L10nRegistry,
+ Localization,
+ NamedNodeMap,
+ NodeFilter,
+ PathUtils,
+ Services,
+ StructuredCloneHolder,
+ TelemetryStopwatch,
+ WebExtensionPolicy,
+ WindowGlobalParent,
+ WindowGlobalChild,
+ };
+ for (const name in injectedGlobals) {
+ this.loader.globals[name] = injectedGlobals[name];
+ }
+
+ // Fetch custom pseudo modules and globals
+ const { modules, globals } = this.require(
+ "resource://devtools/shared/loader/builtin-modules.js"
+ );
+
+ // Register custom pseudo modules to the current loader instance
+ for (const id in modules) {
+ const uri = resolveURI(id, this.loader.mapping);
+ this.loader.modules[uri] = {
+ get exports() {
+ return modules[id];
+ },
+ };
+ }
+
+ // Register custom globals to the current loader instance
+ Object.defineProperties(
+ this.loader.sharedGlobal,
+ Object.getOwnPropertyDescriptors(globals)
+ );
+
+ // Define the loader id for these two usecases:
+ // * access via the JSM (this.id)
+ // let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ // loader.id
+ this.id = gNextLoaderID++;
+ // * access via module's `loader` global
+ // loader.id
+ globals.loader.id = this.id;
+ globals.loader.invisibleToDebugger = invisibleToDebugger;
+
+ // Expose lazy helpers on `loader`
+ // ie. when you use it like that from a JSM:
+ // let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+ // loader.lazyGetter(...);
+ this.lazyGetter = globals.loader.lazyGetter;
+ this.lazyServiceGetter = globals.loader.lazyServiceGetter;
+ this.lazyRequireGetter = globals.loader.lazyRequireGetter;
+}
+
+DevToolsLoader.prototype = {
+ destroy(reason = "shutdown") {
+ unload(this.loader, reason);
+ delete this.loader;
+ },
+
+ /**
+ * Return true if |id| refers to something requiring help from a
+ * loader plugin.
+ */
+ isLoaderPluginId(id) {
+ return id.startsWith("raw!");
+ },
+};
+
+// Export the standard instance of DevToolsLoader used by the tools.
+export var loader = new DevToolsLoader({
+ /**
+ * Sets whether the compartments loaded by this instance should be invisible
+ * to the debugger. Invisibility is needed for loaders that support debugging
+ * of chrome code. This is true of remote target environments, like Fennec or
+ * B2G. It is not the default case for desktop Firefox because we offer the
+ * Browser Toolbox for chrome debugging there, which uses its own, separate
+ * loader instance.
+ * @see devtools/client/framework/browser-toolbox/Launcher.sys.mjs
+ */
+ invisibleToDebugger: Services.appinfo.name !== "Firefox",
+});
+
+export var require = loader.require;
diff --git a/devtools/shared/loader/base-loader.sys.mjs b/devtools/shared/loader/base-loader.sys.mjs
new file mode 100644
index 0000000000..78998d9b77
--- /dev/null
+++ b/devtools/shared/loader/base-loader.sys.mjs
@@ -0,0 +1,640 @@
+/* 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/. */
+
+/* exported Loader, resolveURI, Module, Require, unload */
+
+const systemPrincipal = Components.Constructor(
+ "@mozilla.org/systemprincipal;1",
+ "nsIPrincipal"
+)();
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "resProto",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsIResProtocolHandler"
+);
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+
+// Define some shortcuts.
+function* getOwnIdentifiers(x) {
+ yield* Object.getOwnPropertyNames(x);
+ yield* Object.getOwnPropertySymbols(x);
+}
+
+function isJSONURI(uri) {
+ return uri.endsWith(".json");
+}
+function isJSMURI(uri) {
+ return uri.endsWith(".jsm");
+}
+function isSYSMJSURI(uri) {
+ return uri.endsWith(".sys.mjs");
+}
+function isJSURI(uri) {
+ return uri.endsWith(".js");
+}
+const AbsoluteRegExp = /^(resource|chrome|file|jar):/;
+function isAbsoluteURI(uri) {
+ return AbsoluteRegExp.test(uri);
+}
+function isRelative(id) {
+ return id.startsWith(".");
+}
+
+function readURI(uri) {
+ const nsURI = lazy.NetUtil.newURI(uri);
+ if (nsURI.scheme == "resource") {
+ // Resolve to a real URI, this will catch any obvious bad paths without
+ // logging assertions in debug builds, see bug 1135219
+ uri = lazy.resProto.resolveURI(nsURI);
+ }
+
+ const stream = lazy.NetUtil.newChannel({
+ uri: lazy.NetUtil.newURI(uri, "UTF-8"),
+ loadUsingSystemPrincipal: true,
+ }).open();
+ const count = stream.available();
+ const data = lazy.NetUtil.readInputStreamToString(stream, count, {
+ charset: "UTF-8",
+ });
+
+ stream.close();
+
+ return data;
+}
+
+// Combines all arguments into a resolved, normalized path
+function join(base, ...paths) {
+ // If this is an absolute URL, we need to normalize only the path portion,
+ // or we wind up stripping too many slashes and producing invalid URLs.
+ const match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec(
+ base
+ );
+ if (match) {
+ return match[1] + normalize([match[2], ...paths].join("/"));
+ }
+
+ return normalize([base, ...paths].join("/"));
+}
+
+// Function takes set of options and returns a JS sandbox. Function may be
+// passed set of options:
+// - `name`: A string value which identifies the sandbox in about:memory. Will
+// throw exception if omitted.
+// - `prototype`: Ancestor for the sandbox that will be created. Defaults to
+// `{}`.
+// - `invisibleToDebugger`: True, if the sandbox is part of the debugger
+// implementation and should not be tracked by debugger API.
+// For more details see:
+// @see https://searchfox.org/mozilla-central/rev/0948667bc62415d48abff27e1405fb4ab4d65d75/js/xpconnect/idl/xpccomponents.idl#127-245
+function Sandbox(options) {
+ // Normalize options and rename to match `Cu.Sandbox` expectations.
+ const sandboxOptions = {
+ // This will allow exposing Components as well as Cu, Ci and Cr.
+ wantComponents: true,
+
+ // By default, Sandbox come with a very limited set of global.
+ // The list of all available symbol names is available over there:
+ // https://searchfox.org/mozilla-central/rev/31368c7795f44b7a15531d6c5e52dc97f82cf2d5/js/xpconnect/src/Sandbox.cpp#905-997
+ // Request to expose all meaningful global here:
+ wantGlobalProperties: [
+ "AbortController",
+ "atob",
+ "btoa",
+ "Blob",
+ "crypto",
+ "ChromeUtils",
+ "CSS",
+ "CSSRule",
+ "DOMParser",
+ "Element",
+ "Event",
+ "FileReader",
+ "FormData",
+ "Headers",
+ "InspectorUtils",
+ "MIDIInputMap",
+ "MIDIOutputMap",
+ "Node",
+ "TextDecoder",
+ "TextEncoder",
+ "URL",
+ "URLSearchParams",
+ "Window",
+ "XMLHttpRequest",
+ ],
+
+ sandboxName: options.name,
+ sandboxPrototype: "prototype" in options ? options.prototype : {},
+ invisibleToDebugger:
+ "invisibleToDebugger" in options ? options.invisibleToDebugger : false,
+ freshCompartment: options.freshCompartment || false,
+ };
+
+ return Cu.Sandbox(systemPrincipal, sandboxOptions);
+}
+
+// This allows defining some modules in AMD format while retaining CommonJS
+// compatibility with this loader by allowing the factory function to have
+// access to general CommonJS functions, e.g.
+//
+// define(function(require, exports, module) {
+// ... code ...
+// });
+function define(factory) {
+ factory(this.require, this.exports, this.module);
+}
+
+// Populates `exports` of the given CommonJS `module` object, in the context
+// of the given `loader` by evaluating code associated with it.
+function load(loader, module) {
+ const require = Require(loader, module);
+
+ // We expose set of properties defined by `CommonJS` specification via
+ // prototype of the sandbox. Also globals are deeper in the prototype
+ // chain so that each module has access to them as well.
+ const properties = {
+ require,
+ module,
+ exports: module.exports,
+ };
+ if (loader.supportAMDModules) {
+ properties.define = define;
+ }
+
+ // Create a new object in the shared global of the loader, that will be used
+ // as the scope object for this particular module.
+ const scopeFromSharedGlobal = new loader.sharedGlobal.Object();
+ Object.assign(scopeFromSharedGlobal, properties);
+
+ const originalExports = module.exports;
+ try {
+ Services.scriptloader.loadSubScript(module.uri, scopeFromSharedGlobal);
+ } catch (error) {
+ // loadSubScript sometime throws string errors, which includes no stack.
+ // At least provide the current stack by re-throwing a real Error object.
+ if (typeof error == "string") {
+ if (
+ error.startsWith("Error creating URI") ||
+ error.startsWith("Error opening input stream (invalid filename?)")
+ ) {
+ throw new Error(
+ `Module \`${module.id}\` is not found at ${module.uri}`
+ );
+ }
+ throw new Error(
+ `Error while loading module \`${module.id}\` at ${module.uri}:` +
+ "\n" +
+ error
+ );
+ }
+ // Otherwise just re-throw everything else which should have a stack
+ throw error;
+ }
+
+ // Only freeze the exports object if we created it ourselves. Modules
+ // which completely replace the exports object and still want it
+ // frozen need to freeze it themselves.
+ if (module.exports === originalExports) {
+ Object.freeze(module.exports);
+ }
+
+ return module;
+}
+
+// Utility function to normalize module `uri`s so they have `.js` extension.
+function normalizeExt(uri) {
+ if (isJSURI(uri) || isJSONURI(uri) || isJSMURI(uri) || isSYSMJSURI(uri)) {
+ return uri;
+ }
+ return uri + ".js";
+}
+
+// Utility function to join paths. In common case `base` is a
+// `requirer.uri` but in some cases it may be `baseURI`. In order to
+// avoid complexity we require `baseURI` with a trailing `/`.
+function resolve(id, base) {
+ if (!isRelative(id)) {
+ return id;
+ }
+
+ const baseDir = dirname(base);
+
+ let resolved;
+ if (baseDir.includes(":")) {
+ resolved = join(baseDir, id);
+ } else {
+ resolved = normalize(`${baseDir}/${id}`);
+ }
+
+ // Joining and normalizing removes the "./" from relative files.
+ // We need to ensure the resolution still has the root
+ if (base.startsWith("./")) {
+ resolved = "./" + resolved;
+ }
+
+ return resolved;
+}
+
+function compileMapping(paths) {
+ // Make mapping array that is sorted from longest path to shortest path.
+ const mapping = Object.keys(paths)
+ .sort((a, b) => b.length - a.length)
+ .map(path => [path, paths[path]]);
+
+ const PATTERN = /([.\\?+*(){}[\]^$])/g;
+ const escapeMeta = str => str.replace(PATTERN, "\\$1");
+
+ const patterns = [];
+ paths = {};
+
+ for (let [path, uri] of mapping) {
+ // Strip off any trailing slashes to make comparisons simpler
+ if (path.endsWith("/")) {
+ path = path.slice(0, -1);
+ uri = uri.replace(/\/+$/, "");
+ }
+
+ paths[path] = uri;
+
+ // We only want to match path segments explicitly. Examples:
+ // * "foo/bar" matches for "foo/bar"
+ // * "foo/bar" matches for "foo/bar/baz"
+ // * "foo/bar" does not match for "foo/bar-1"
+ // * "foo/bar/" does not match for "foo/bar"
+ // * "foo/bar/" matches for "foo/bar/baz"
+ //
+ // Check for an empty path, an exact match, or a substring match
+ // with the next character being a forward slash.
+ if (path == "") {
+ patterns.push("");
+ } else {
+ patterns.push(`${escapeMeta(path)}(?=$|/)`);
+ }
+ }
+
+ const pattern = new RegExp(`^(${patterns.join("|")})`);
+
+ // This will replace the longest matching path mapping at the start of
+ // the ID string with its mapped value.
+ return id => {
+ return id.replace(pattern, (m0, m1) => paths[m1]);
+ };
+}
+
+export function resolveURI(id, mapping) {
+ // Do not resolve if already a resource URI
+ if (isAbsoluteURI(id)) {
+ return normalizeExt(id);
+ }
+
+ return normalizeExt(mapping(id));
+}
+
+// Creates version of `require` that will be exposed to the given `module`
+// in the context of the given `loader`. Each module gets own limited copy
+// of `require` that is allowed to load only a modules that are associated
+// with it during link time.
+export function Require(loader, requirer) {
+ const { modules, mapping, mappingCache, requireHook } = loader;
+
+ function require(id) {
+ if (!id) {
+ // Throw if `id` is not passed.
+ throw Error(
+ "You must provide a module name when calling require() from " +
+ requirer.id,
+ requirer.uri
+ );
+ }
+
+ if (requireHook) {
+ return requireHook(id, _require);
+ }
+
+ return _require(id);
+ }
+
+ function _require(id) {
+ let { uri, requirement } = getRequirements(id);
+
+ let module = null;
+ // If module is already cached by loader then just use it.
+ if (uri in modules) {
+ module = modules[uri];
+ } else if (isJSMURI(uri)) {
+ module = modules[uri] = Module(requirement, uri);
+ module.exports = ChromeUtils.import(uri);
+ } else if (isSYSMJSURI(uri)) {
+ module = modules[uri] = Module(requirement, uri);
+ module.exports = ChromeUtils.importESModule(uri);
+ } else if (isJSONURI(uri)) {
+ let data;
+
+ // First attempt to load and parse json uri
+ // ex: `test.json`
+ // If that doesn"t exist, check for `test.json.js`
+ // for node parity
+ try {
+ data = JSON.parse(readURI(uri));
+ module = modules[uri] = Module(requirement, uri);
+ module.exports = data;
+ } catch (err) {
+ // If error thrown from JSON parsing, throw that, do not
+ // attempt to find .json.js file
+ if (err && /JSON\.parse/.test(err.message)) {
+ throw err;
+ }
+ uri = uri + ".js";
+ }
+ }
+
+ // If not yet cached, load and cache it.
+ // We also freeze module to prevent it from further changes
+ // at runtime.
+ if (!(uri in modules)) {
+ // Many of the loader's functionalities are dependent
+ // on modules[uri] being set before loading, so we set it and
+ // remove it if we have any errors.
+ module = modules[uri] = Module(requirement, uri);
+ try {
+ Object.freeze(load(loader, module));
+ } catch (e) {
+ // Clear out modules cache so we can throw on a second invalid require
+ delete modules[uri];
+ throw e;
+ }
+ }
+
+ return module.exports;
+ }
+
+ // Resolution function taking a module name/path and
+ // returning a resourceURI and a `requirement` used by the loader.
+ // Used by both `require` and `require.resolve`.
+ function getRequirements(id) {
+ if (!id) {
+ // Throw if `id` is not passed.
+ throw Error(
+ "you must provide a module name when calling require() from " +
+ requirer.id,
+ requirer.uri
+ );
+ }
+
+ let requirement, uri;
+
+ if (modules[id]) {
+ uri = requirement = id;
+ } else if (requirer) {
+ // Resolve `id` to its requirer if it's relative.
+ requirement = resolve(id, requirer.id);
+ } else {
+ requirement = id;
+ }
+
+ // Resolves `uri` of module using loaders resolve function.
+ if (!uri) {
+ if (mappingCache.has(requirement)) {
+ uri = mappingCache.get(requirement);
+ } else {
+ uri = resolveURI(requirement, mapping);
+ mappingCache.set(requirement, uri);
+ }
+ }
+
+ // Throw if `uri` can not be resolved.
+ if (!uri) {
+ throw Error(
+ "Module: Can not resolve '" +
+ id +
+ "' module required by " +
+ requirer.id +
+ " located at " +
+ requirer.uri,
+ requirer.uri
+ );
+ }
+
+ return { uri, requirement };
+ }
+
+ // Expose the `resolve` function for this `Require` instance
+ require.resolve = _require.resolve = function (id) {
+ const { uri } = getRequirements(id);
+ return uri;
+ };
+
+ // This is like webpack's require.context. It returns a new require
+ // function that prepends the prefix to any requests.
+ require.context = prefix => {
+ return id => {
+ return require(prefix + id);
+ };
+ };
+
+ return require;
+}
+
+// Makes module object that is made available to CommonJS modules when they
+// are evaluated, along with `exports` and `require`.
+export function Module(id, uri) {
+ return Object.create(null, {
+ id: { enumerable: true, value: id },
+ exports: {
+ enumerable: true,
+ writable: true,
+ value: Object.create(null),
+ configurable: true,
+ },
+ uri: { value: uri },
+ });
+}
+
+// Takes `loader`, and unload `reason` string and notifies all observers that
+// they should cleanup after them-self.
+export function unload(loader, reason) {
+ // subject is a unique object created per loader instance.
+ // This allows any code to cleanup on loader unload regardless of how
+ // it was loaded. To handle unload for specific loader subject may be
+ // asserted against loader.destructor or require("@loader/unload")
+ // Note: We don not destroy loader's module cache or sandboxes map as
+ // some modules may do cleanup in subsequent turns of event loop. Destroying
+ // cache may cause module identity problems in such cases.
+ const subject = { wrappedJSObject: loader.destructor };
+ Services.obs.notifyObservers(subject, "devtools:loader:destroy", reason);
+}
+
+// Function makes new loader that can be used to load CommonJS modules.
+// Loader takes following options:
+// - `paths`: Mandatory dictionary of require path mapped to absolute URIs.
+// Object keys are path prefix used in require(), values are URIs where each
+// prefix should be mapped to.
+// - `globals`: Optional map of globals, that all module scopes will inherit
+// from. Map is also exposed under `globals` property of the returned loader
+// so it can be extended further later. Defaults to `{}`.
+// - `sandboxName`: String, name of the sandbox displayed in about:memory.
+// - `invisibleToDebugger`: Boolean. Should be true when loading debugger
+// modules, in order to ignore them from the Debugger API.
+// - `sandboxPrototype`: Object used to define globals on all module's
+// sandboxes.
+// - `requireHook`: Optional function used to replace native require function
+// from loader. This function receive the module path as first argument,
+// and native require method as second argument.
+export function Loader(options) {
+ let { paths, globals } = options;
+ if (!globals) {
+ globals = {};
+ }
+
+ // We create an identity object that will be dispatched on an unload
+ // event as subject. This way unload listeners will be able to assert
+ // which loader is unloaded. Please note that we intentionally don"t
+ // use `loader` as subject to prevent a loader access leakage through
+ // observer notifications.
+ const destructor = Object.create(null);
+
+ const mapping = compileMapping(paths);
+
+ // Define pseudo modules.
+ const builtinModuleExports = {
+ "@loader/unload": destructor,
+ "@loader/options": options,
+ };
+
+ const modules = {};
+ for (const id of Object.keys(builtinModuleExports)) {
+ // We resolve `uri` from `id` since modules are cached by `uri`.
+ const uri = resolveURI(id, mapping);
+ const module = Module(id, uri);
+
+ // Lazily expose built-in modules in order to
+ // allow them to be loaded lazily.
+ Object.defineProperty(module, "exports", {
+ enumerable: true,
+ get() {
+ return builtinModuleExports[id];
+ },
+ });
+
+ modules[uri] = module;
+ }
+
+ let sharedGlobal;
+ if (options.sharedGlobal) {
+ sharedGlobal = options.sharedGlobal;
+ } else {
+ // Create the unique sandbox we will be using for all modules,
+ // so that we prevent creating a new compartment per module.
+ // The side effect is that all modules will share the same
+ // global objects.
+ sharedGlobal = Sandbox({
+ name: options.sandboxName || "DevTools",
+ invisibleToDebugger: options.invisibleToDebugger || false,
+ prototype: options.sandboxPrototype || globals,
+ freshCompartment: options.freshCompartment,
+ });
+ }
+
+ if (options.sharedGlobal || options.sandboxPrototype) {
+ // If we were given a sharedGlobal or a sandboxPrototype, we have to define
+ // the globals on the shared global directly. Note that this will not work
+ // for callers who depend on being able to add globals after the loader was
+ // created.
+ for (const name of getOwnIdentifiers(globals)) {
+ Object.defineProperty(
+ sharedGlobal,
+ name,
+ Object.getOwnPropertyDescriptor(globals, name)
+ );
+ }
+ }
+
+ // Loader object is just a representation of a environment
+ // state. We mark its properties non-enumerable
+ // as they are pure implementation detail that no one should rely upon.
+ const returnObj = {
+ destructor: { enumerable: false, value: destructor },
+ globals: { enumerable: false, value: globals },
+ mapping: { enumerable: false, value: mapping },
+ mappingCache: { enumerable: false, value: new Map() },
+ // Map of module objects indexed by module URIs.
+ modules: { enumerable: false, value: modules },
+ sharedGlobal: { enumerable: false, value: sharedGlobal },
+ supportAMDModules: {
+ enumerable: false,
+ value: options.supportAMDModules || false,
+ },
+ // Whether the modules loaded should be ignored by the debugger
+ invisibleToDebugger: {
+ enumerable: false,
+ value: options.invisibleToDebugger || false,
+ },
+ requireHook: {
+ enumerable: false,
+ writable: true,
+ value: options.requireHook,
+ },
+ };
+
+ return Object.create(null, returnObj);
+}
+
+// NB: These methods are from the UNIX implementation of OS.Path. Refactoring
+// this module to not use path methods on stringly-typed URIs is
+// non-trivial.
+function dirname(path) {
+ let index = path.lastIndexOf("/");
+ if (index == -1) {
+ return ".";
+ }
+ while (index >= 0 && path[index] == "/") {
+ --index;
+ }
+ return path.slice(0, index + 1);
+}
+
+function normalize(path) {
+ const stack = [];
+ let absolute;
+ if (path.length >= 0 && path[0] == "/") {
+ absolute = true;
+ } else {
+ absolute = false;
+ }
+ path.split("/").forEach(function (v) {
+ switch (v) {
+ case "":
+ case ".": // fallthrough
+ break;
+ case "..":
+ if (!stack.length) {
+ if (absolute) {
+ throw new Error("Path is ill-formed: attempting to go past root");
+ } else {
+ stack.push("..");
+ }
+ } else if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ break;
+ default:
+ stack.push(v);
+ }
+ });
+ const string = stack.join("/");
+ return absolute ? "/" + string : string;
+}
diff --git a/devtools/shared/loader/browser-loader-mocks.js b/devtools/shared/loader/browser-loader-mocks.js
new file mode 100644
index 0000000000..c8fe528ee5
--- /dev/null
+++ b/devtools/shared/loader/browser-loader-mocks.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+"use strict";
+
+// Map of mocked modules, keys are absolute URIs for devtools modules such as
+// "resource://devtools/path/to/mod.js, values are objects (anything passed to
+// setMockedModule technically).
+const _mocks = {};
+
+/**
+ * Retrieve a mocked module matching the provided uri, eg "resource://path/to/file.js".
+ */
+function getMockedModule(uri) {
+ return _mocks[uri];
+}
+exports.getMockedModule = getMockedModule;
+
+/**
+ * Module paths are transparently provided with or without ".js" when using the loader,
+ * normalize the user-provided module paths to always have modules ending with ".js".
+ */
+function _getUriForModulePath(modulePath) {
+ // Assume js modules and add the .js extension if missing.
+ if (!modulePath.endsWith(".js")) {
+ modulePath = modulePath + ".js";
+ }
+
+ // Add resource:// scheme if no scheme is specified.
+ if (!modulePath.includes("://")) {
+ modulePath = "resource://" + modulePath;
+ }
+
+ return modulePath;
+}
+
+/**
+ * Assign a mock object to the provided module path.
+ * @param mock
+ * Plain JavaScript object that will implement the expected API for the mocked
+ * module.
+ * @param modulePath
+ * The module path should be the absolute module path, starting with `devtools`:
+ * "devtools/client/some-panel/some-module"
+ */
+function setMockedModule(mock, modulePath) {
+ const uri = _getUriForModulePath(modulePath);
+ _mocks[uri] = new Proxy(mock, {
+ get(target, key) {
+ if (typeof target[key] === "function") {
+ // Functions are wrapped to be able to update the methods during the test, even if
+ // the methods were imported with destructuring. For instance:
+ // `const { someMethod } = require("devtools/client/shared/my-module");`
+ return function () {
+ return target[key].apply(target, arguments);
+ };
+ }
+ return target[key];
+ },
+ });
+}
+exports.setMockedModule = setMockedModule;
+
+/**
+ * Remove any mock object defined for the provided absolute module path.
+ */
+function removeMockedModule(modulePath) {
+ const uri = _getUriForModulePath(modulePath);
+ delete _mocks[uri];
+}
+exports.removeMockedModule = removeMockedModule;
diff --git a/devtools/shared/loader/browser-loader.js b/devtools/shared/loader/browser-loader.js
new file mode 100644
index 0000000000..f42b009e17
--- /dev/null
+++ b/devtools/shared/loader/browser-loader.js
@@ -0,0 +1,239 @@
+/* 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/. */
+"use strict";
+
+const BaseLoader = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/base-loader.sys.mjs"
+);
+const { require: devtoolsRequire, loader } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const flags = devtoolsRequire("devtools/shared/flags");
+const { joinURI } = devtoolsRequire("devtools/shared/path");
+const { assert } = devtoolsRequire("devtools/shared/DevToolsUtils");
+
+const lazy = {};
+
+loader.lazyRequireGetter(
+ lazy,
+ "getMockedModule",
+ "resource://devtools/shared/loader/browser-loader-mocks.js",
+ {}
+);
+
+const BROWSER_BASED_DIRS = [
+ "resource://devtools/client/inspector/boxmodel",
+ "resource://devtools/client/inspector/changes",
+ "resource://devtools/client/inspector/computed",
+ "resource://devtools/client/inspector/events",
+ "resource://devtools/client/inspector/flexbox",
+ "resource://devtools/client/inspector/fonts",
+ "resource://devtools/client/inspector/grids",
+ "resource://devtools/client/inspector/layout",
+ "resource://devtools/client/inspector/markup",
+ "resource://devtools/client/jsonview",
+ "resource://devtools/client/netmonitor/src/utils",
+ "resource://devtools/client/shared/fluent-l10n",
+ "resource://devtools/client/shared/redux",
+ "resource://devtools/client/shared/vendor",
+];
+
+const COMMON_LIBRARY_DIRS = ["resource://devtools/client/shared/vendor"];
+
+// Any directory that matches the following regular expression
+// is also considered as browser based module directory.
+// ('resource://devtools/client/.*/components/')
+//
+// An example:
+// * `resource://devtools/client/inspector/components`
+// * `resource://devtools/client/inspector/shared/components`
+const browserBasedDirsRegExp =
+ /^resource\:\/\/devtools\/client\/\S*\/components\//;
+
+/*
+ * Create a loader to be used in a browser environment. This evaluates
+ * modules in their own environment, but sets window (the normal
+ * global object) as the sandbox prototype, so when a variable is not
+ * defined it checks `window` before throwing an error. This makes all
+ * browser APIs available to modules by default, like a normal browser
+ * environment, but modules are still evaluated in their own scope.
+ *
+ * Another very important feature of this loader is that it *only*
+ * deals with modules loaded from under `baseURI`. Anything loaded
+ * outside of that path will still be loaded from the devtools loader,
+ * so all system modules are still shared and cached across instances.
+ * An exception to this is anything under
+ * `devtools/client/shared/{vendor/components}`, which is where shared libraries
+ * and React components live that should be evaluated in a browser environment.
+ *
+ * @param string baseURI
+ * Base path to load modules from. If null or undefined, only
+ * the shared vendor/components modules are loaded with the browser
+ * loader.
+ * @param Object window
+ * The window instance to evaluate modules within
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ * @return Object
+ * An object with two properties:
+ * - loader: the Loader instance
+ * - require: a function to require modules with
+ */
+function BrowserLoader(options) {
+ const browserLoaderBuilder = new BrowserLoaderBuilder(options);
+ return {
+ loader: browserLoaderBuilder.loader,
+ require: browserLoaderBuilder.require,
+ };
+}
+
+/**
+ * Private class used to build the Loader instance and require method returned
+ * by BrowserLoader(baseURI, window).
+ *
+ * @param string baseURI
+ * Base path to load modules from.
+ * @param Function commonLibRequire
+ * Require function that should be used to load common libraries, like React.
+ * Allows for sharing common modules between tools, instead of loading a new
+ * instance into each tool. For example, pass "toolbox.browserRequire" here.
+ * @param Boolean useOnlyShared
+ * If true, ignores `baseURI` and only loads the shared
+ * BROWSER_BASED_DIRS via BrowserLoader.
+ * @param Object window
+ * The window instance to evaluate modules within
+ */
+function BrowserLoaderBuilder({
+ baseURI,
+ commonLibRequire,
+ useOnlyShared,
+ window,
+}) {
+ assert(
+ !!baseURI !== !!useOnlyShared,
+ "Cannot use both `baseURI` and `useOnlyShared`."
+ );
+
+ const loaderOptions = devtoolsRequire("@loader/options");
+
+ const opts = {
+ sandboxPrototype: window,
+ sandboxName: "DevTools (UI loader)",
+ paths: loaderOptions.paths,
+ invisibleToDebugger: loaderOptions.invisibleToDebugger,
+ // Make sure `define` function exists. This allows defining some modules
+ // in AMD format while retaining CommonJS compatibility through this hook.
+ // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
+ // from a content document and can't access our usual loaders. So, any
+ // modules shared with the JSON Viewer should include a define wrapper:
+ //
+ // // Make this available to both AMD and CJS environments
+ // define(function(require, exports, module) {
+ // ... code ...
+ // });
+ //
+ // Bug 1248830 will work out a better plan here for our content module
+ // loading needs, especially as we head towards devtools.html.
+ supportAMDModules: true,
+ requireHook: (id, require) => {
+ // If |id| requires special handling, simply defer to devtools
+ // immediately.
+ if (loader.isLoaderPluginId(id)) {
+ return devtoolsRequire(id);
+ }
+
+ const uri = require.resolve(id);
+
+ // The mocks can be set from tests using browser-loader-mocks.js setMockedModule().
+ // If there is an entry for a given uri in the `mocks` object, return it instead of
+ // requiring the module.
+ if (flags.testing && lazy.getMockedModule(uri)) {
+ return lazy.getMockedModule(uri);
+ }
+
+ if (
+ commonLibRequire &&
+ COMMON_LIBRARY_DIRS.some(dir => uri.startsWith(dir))
+ ) {
+ return commonLibRequire(uri);
+ }
+
+ // Check if the URI matches one of hardcoded paths or a regexp.
+ const isBrowserDir =
+ BROWSER_BASED_DIRS.some(dir => uri.startsWith(dir)) ||
+ uri.match(browserBasedDirsRegExp) != null;
+
+ if ((useOnlyShared || !uri.startsWith(baseURI)) && !isBrowserDir) {
+ return devtoolsRequire(uri);
+ }
+
+ return require(uri);
+ },
+ globals: {
+ // Allow modules to use the window's console to ensure logs appear in a
+ // tab toolbox, if one exists, instead of just the browser console.
+ console: window.console,
+ // Allow modules to use the DevToolsLoader lazy loading helpers.
+ loader: {
+ lazyGetter: loader.lazyGetter,
+ lazyServiceGetter: loader.lazyServiceGetter,
+ lazyRequireGetter: this.lazyRequireGetter.bind(this),
+ },
+ },
+ };
+
+ const mainModule = BaseLoader.Module(baseURI, joinURI(baseURI, "main.js"));
+ this.loader = BaseLoader.Loader(opts);
+ // When running tests, expose the BrowserLoader instance for metrics tests.
+ if (flags.testing) {
+ window.getBrowserLoaderForWindow = () => this;
+ }
+ this.require = BaseLoader.Require(this.loader, mainModule);
+}
+
+BrowserLoaderBuilder.prototype = {
+ /**
+ * Define a getter property on the given object that requires the given
+ * module. This enables delaying importing modules until the module is
+ * actually used.
+ *
+ * Several getters can be defined at once by providing an array of
+ * properties and enabling destructuring.
+ *
+ * @param { Object } obj
+ * The object to define the property on.
+ * @param { String | Array<String> } properties
+ * String: Name of the property for the getter.
+ * Array<String>: When destructure is true, properties can be an array of
+ * strings to create several getters at once.
+ * @param { String } module
+ * The module path.
+ * @param { Boolean } destructure
+ * Pass true if the property name is a member of the module's exports.
+ */
+ lazyRequireGetter(obj, properties, module, destructure) {
+ if (Array.isArray(properties) && !destructure) {
+ throw new Error(
+ "Pass destructure=true to call lazyRequireGetter with an array of properties"
+ );
+ }
+
+ if (!Array.isArray(properties)) {
+ properties = [properties];
+ }
+
+ for (const property of properties) {
+ loader.lazyGetter(obj, property, () => {
+ return destructure
+ ? this.require(module)[property]
+ : this.require(module || property);
+ });
+ }
+ },
+};
+
+this.BrowserLoader = BrowserLoader;
+
+this.EXPORTED_SYMBOLS = ["BrowserLoader"];
diff --git a/devtools/shared/loader/builtin-modules.js b/devtools/shared/loader/builtin-modules.js
new file mode 100644
index 0000000000..7dc04e5e98
--- /dev/null
+++ b/devtools/shared/loader/builtin-modules.js
@@ -0,0 +1,203 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * This module defines custom globals injected in all our modules and also
+ * pseudo modules that aren't separate files but just dynamically set values.
+ *
+ * Note that some globals are being defined by base-loader.sys.mjs via wantGlobalProperties property.
+ *
+ * As it does so, the module itself doesn't have access to these globals,
+ * nor the pseudo modules. Be careful to avoid loading any other js module as
+ * they would also miss them.
+ */
+
+const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+/**
+ * Defines a getter on a specified object that will be created upon first use.
+ *
+ * @param object
+ * The object to define the lazy getter on.
+ * @param name
+ * The name of the getter to define on object.
+ * @param lambda
+ * A function that returns what the getter should return. This will
+ * only ever be called once.
+ */
+function defineLazyGetter(object, name, lambda) {
+ Object.defineProperty(object, name, {
+ get() {
+ // Redefine this accessor property as a data property.
+ // Delete it first, to rule out "too much recursion" in case object is
+ // a proxy whose defineProperty handler might unwittingly trigger this
+ // getter again.
+ delete object[name];
+ const value = lambda.apply(object);
+ Object.defineProperty(object, name, {
+ value,
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ });
+ return value;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+}
+
+/**
+ * Defines a getter on a specified object for a service. The service will not
+ * be obtained until first use.
+ *
+ * @param object
+ * The object to define the lazy getter on.
+ * @param name
+ * The name of the getter to define on object for the service.
+ * @param contract
+ * The contract used to obtain the service.
+ * @param interfaceName
+ * The name of the interface to query the service to.
+ */
+function defineLazyServiceGetter(object, name, contract, interfaceName) {
+ defineLazyGetter(object, name, function () {
+ return Cc[contract].getService(Ci[interfaceName]);
+ });
+}
+
+/**
+ * Define a getter property on the given object that requires the given
+ * module. This enables delaying importing modules until the module is
+ * actually used.
+ *
+ * Several getters can be defined at once by providing an array of
+ * properties and enabling destructuring.
+ *
+ * @param { Object } obj
+ * The object to define the property on.
+ * @param { String | Array<String> } properties
+ * String: Name of the property for the getter.
+ * Array<String>: When destructure is true, properties can be an array of
+ * strings to create several getters at once.
+ * @param { String } module
+ * The module path.
+ * @param { Boolean } destructure
+ * Pass true if the property name is a member of the module's exports.
+ */
+function lazyRequireGetter(obj, properties, module, destructure) {
+ if (Array.isArray(properties) && !destructure) {
+ throw new Error(
+ "Pass destructure=true to call lazyRequireGetter with an array of properties"
+ );
+ }
+
+ if (!Array.isArray(properties)) {
+ properties = [properties];
+ }
+
+ for (const property of properties) {
+ defineLazyGetter(obj, property, () => {
+ return destructure
+ ? require(module)[property]
+ : require(module || property);
+ });
+ }
+}
+
+// List of pseudo modules exposed to all devtools modules.
+exports.modules = {
+ HeapSnapshot,
+ // Expose "chrome" Promise, which aren't related to any document
+ // and so are never frozen, even if the browser loader module which
+ // pull it is destroyed. See bug 1402779.
+ Promise,
+ TelemetryStopwatch,
+};
+
+defineLazyGetter(exports.modules, "Debugger", () => {
+ const global = Cu.getGlobalForObject(this);
+ // Debugger may already have been added.
+ if (global.Debugger) {
+ return global.Debugger;
+ }
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(global);
+ return global.Debugger;
+});
+
+defineLazyGetter(exports.modules, "ChromeDebugger", () => {
+ // Sandbox are memory expensive, so we should create as little as possible.
+ const debuggerSandbox = Cu.Sandbox(systemPrincipal, {
+ // This sandbox is used for the ChromeDebugger implementation.
+ // As we want to load the `Debugger` API for debugging chrome contexts,
+ // we have to ensure loading it in a distinct compartment from its debuggee.
+ freshCompartment: true,
+ });
+
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ addDebuggerToGlobal(debuggerSandbox);
+ return debuggerSandbox.Debugger;
+});
+
+defineLazyGetter(exports.modules, "xpcInspector", () => {
+ return Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
+});
+
+// List of all custom globals exposed to devtools modules.
+// Changes here should be mirrored to devtools/.eslintrc.
+exports.globals = {
+ isWorker: false,
+ loader: {
+ lazyGetter: defineLazyGetter,
+ lazyServiceGetter: defineLazyServiceGetter,
+ lazyRequireGetter,
+ // Defined by Loader.sys.mjs
+ id: null,
+ },
+};
+// DevTools loader copy globals property descriptors on each module global
+// object so that we have to memoize them from here in order to instantiate each
+// global only once.
+// `globals` is a cache object on which we put all global values
+// and we set getters on `exports.globals` returning `globals` values.
+const globals = {};
+function lazyGlobal(name, getter) {
+ defineLazyGetter(globals, name, getter);
+ Object.defineProperty(exports.globals, name, {
+ get() {
+ return globals[name];
+ },
+ configurable: true,
+ enumerable: true,
+ });
+}
+
+// Lazily define a few things so that the corresponding modules are only loaded
+// when used.
+lazyGlobal("clearTimeout", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .clearTimeout;
+});
+lazyGlobal("setTimeout", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .setTimeout;
+});
+lazyGlobal("clearInterval", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .clearInterval;
+});
+lazyGlobal("setInterval", () => {
+ return ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs")
+ .setInterval;
+});
+lazyGlobal("WebSocket", () => {
+ return Services.appShell.hiddenDOMWindow.WebSocket;
+});
diff --git a/devtools/shared/loader/loader-plugin-raw.sys.mjs b/devtools/shared/loader/loader-plugin-raw.sys.mjs
new file mode 100644
index 0000000000..a7aa630ce5
--- /dev/null
+++ b/devtools/shared/loader/loader-plugin-raw.sys.mjs
@@ -0,0 +1,39 @@
+/* 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/. */
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * A function that can be used as part of a require hook for a
+ * loader.js Loader.
+ * This function handles "raw!" and "theme-loader!" requires.
+ * See also: https://github.com/webpack/raw-loader.
+ */
+export const requireRawId = function (id, require) {
+ const index = id.indexOf("!");
+ const rawId = id.slice(index + 1);
+ let uri = require.resolve(rawId);
+ // If the original string did not end with ".js", then
+ // require.resolve might have added the suffix. We don't want to
+ // add a suffix for a raw load (if needed the caller can specify it
+ // manually), so remove it here.
+ if (!id.endsWith(".js") && uri.endsWith(".js")) {
+ uri = uri.slice(0, -3);
+ }
+
+ const stream = NetUtil.newChannel({
+ uri: NetUtil.newURI(uri, "UTF-8"),
+ loadUsingSystemPrincipal: true,
+ }).open();
+
+ const count = stream.available();
+ const data = NetUtil.readInputStreamToString(stream, count, {
+ charset: "UTF-8",
+ });
+ stream.close();
+
+ // For the time being it doesn't seem worthwhile to cache the
+ // result here.
+ return data;
+};
diff --git a/devtools/shared/loader/moz.build b/devtools/shared/loader/moz.build
new file mode 100644
index 0000000000..cda2083625
--- /dev/null
+++ b/devtools/shared/loader/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# The browser-loader modules should only be shipped together with the client.
+if CONFIG["MOZ_DEVTOOLS"] == "all":
+ DevToolsModules(
+ "browser-loader-mocks.js",
+ "browser-loader.js",
+ )
+
+DevToolsModules(
+ "base-loader.sys.mjs",
+ "builtin-modules.js",
+ "DistinctSystemPrincipalLoader.sys.mjs",
+ "loader-plugin-raw.sys.mjs",
+ "Loader.sys.mjs",
+ "worker-loader.js",
+)
diff --git a/devtools/shared/loader/worker-loader.js b/devtools/shared/loader/worker-loader.js
new file mode 100644
index 0000000000..4d8ff61bc7
--- /dev/null
+++ b/devtools/shared/loader/worker-loader.js
@@ -0,0 +1,536 @@
+/* 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/. */
+
+"use strict";
+
+/* global worker, DebuggerNotificationObserver */
+
+// A CommonJS module loader that is designed to run inside a worker debugger.
+// We can't simply use the SDK module loader, because it relies heavily on
+// Components, which isn't available in workers.
+//
+// In principle, the standard instance of the worker loader should provide the
+// same built-in modules as its devtools counterpart, so that both loaders are
+// interchangable on the main thread, making them easier to test.
+//
+// On the worker thread, some of these modules, in particular those that rely on
+// the use of Components, and for which the worker debugger doesn't provide an
+// alternative API, will be replaced by vacuous objects. Consequently, they can
+// still be required, but any attempts to use them will lead to an exception.
+//
+// Note: to see dump output when running inside the worker thread, you might
+// need to enable the browser.dom.window.dump.enabled pref.
+
+this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];
+
+// Some notes on module ids and URLs:
+//
+// An id is either a relative id or an absolute id. An id is relative if and
+// only if it starts with a dot. An absolute id is a normalized id if and only
+// if it contains no redundant components.
+//
+// Every normalized id is a URL. A URL is either an absolute URL or a relative
+// URL. A URL is absolute if and only if it starts with a scheme name followed
+// by a colon and 2 or 3 slashes.
+
+/**
+ * Convert the given relative id to an absolute id.
+ *
+ * @param String id
+ * The relative id to be resolved.
+ * @param String baseId
+ * The absolute base id to resolve the relative id against.
+ *
+ * @return String
+ * An absolute id
+ */
+function resolveId(id, baseId) {
+ return baseId + "/../" + id;
+}
+
+/**
+ * Convert the given absolute id to a normalized id.
+ *
+ * @param String id
+ * The absolute id to be normalized.
+ *
+ * @return String
+ * A normalized id.
+ */
+function normalizeId(id) {
+ // An id consists of an optional root and a path. A root consists of either
+ // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
+ // root are not used as separators, so only normalize the path.
+ const [, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
+
+ const stack = [];
+ path.split("/").forEach(function (component) {
+ switch (component) {
+ case "":
+ case ".":
+ break;
+ case "..":
+ if (stack.length === 0) {
+ if (root !== undefined) {
+ throw new Error("Can't normalize absolute id '" + id + "'!");
+ } else {
+ stack.push("..");
+ }
+ } else if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ break;
+ default:
+ stack.push(component);
+ break;
+ }
+ });
+
+ return (root ? root : "") + stack.join("/");
+}
+
+/**
+ * Create a module object with the given normalized id.
+ *
+ * @param String
+ * The normalized id of the module to be created.
+ *
+ * @return Object
+ * A module with the given id.
+ */
+function createModule(id) {
+ return Object.create(null, {
+ // CommonJS specifies the id property to be non-configurable and
+ // non-writable.
+ id: {
+ configurable: false,
+ enumerable: true,
+ value: id,
+ writable: false,
+ },
+
+ // CommonJS does not specify an exports property, so follow the NodeJS
+ // convention, which is to make it non-configurable and writable.
+ exports: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true,
+ },
+ });
+}
+
+/**
+ * Create a CommonJS loader with the following options:
+ * - createSandbox:
+ * A function that will be used to create sandboxes. It should take the name
+ * and prototype of the sandbox to be created, and return the newly created
+ * sandbox as result. This option is required.
+ * - globals:
+ * A map of names to built-in globals that will be exposed to every module.
+ * Defaults to the empty map.
+ * - loadSubScript:
+ * A function that will be used to load scripts in sandboxes. It should take
+ * the URL from and the sandbox in which the script is to be loaded, and not
+ * return a result. This option is required.
+ * - modules:
+ * A map from normalized ids to built-in modules that will be added to the
+ * module cache. Defaults to the empty map.
+ * - paths:
+ * A map of paths to base URLs that will be used to resolve relative URLs to
+ * absolute URLS. Defaults to the empty map.
+ * - resolve:
+ * A function that will be used to resolve relative ids to absolute ids. It
+ * should take the relative id of a module to be required and the absolute
+ * id of the requiring module as arguments, and return the absolute id of
+ * the module to be required as result. Defaults to resolveId above.
+ */
+function WorkerDebuggerLoader(options) {
+ /**
+ * Convert the given relative URL to an absolute URL, using the map of paths
+ * given below.
+ *
+ * @param String url
+ * The relative URL to be resolved.
+ *
+ * @return String
+ * An absolute URL.
+ */
+ function resolveURL(url) {
+ let found = false;
+ for (const [path, baseURL] of paths) {
+ if (url.startsWith(path)) {
+ found = true;
+ url = url.replace(path, baseURL);
+ break;
+ }
+ }
+ if (!found) {
+ throw new Error("Can't resolve relative URL '" + url + "'!");
+ }
+
+ // If the url has no extension, use ".js" by default.
+ // Also allow loading JSMs, but they would need a shim in order to
+ // be loaded as a CommonJS module. (See SessionDataHelpers.jsm)
+ return url.endsWith(".js") || url.endsWith(".jsm") ? url : url + ".js";
+ }
+
+ /**
+ * Load the given module with the given url.
+ *
+ * @param Object module
+ * The module object to be loaded.
+ * @param String url
+ * The URL to load the module from.
+ */
+ function loadModule(module, url) {
+ // CommonJS specifies 3 free variables: require, exports, and module. These
+ // must be exposed to every module, so define these as properties on the
+ // sandbox prototype. Additional built-in globals are exposed by making
+ // the map of built-in globals the prototype of the sandbox prototype.
+ const prototype = Object.create(globals);
+ prototype.Components = {};
+ prototype.require = createRequire(module);
+ prototype.exports = module.exports;
+ prototype.module = module;
+
+ const sandbox = createSandbox(url, prototype);
+ try {
+ loadSubScript(url, sandbox);
+ } catch (error) {
+ if (/^Error opening input stream/.test(String(error))) {
+ throw new Error(
+ "Can't load module '" + module.id + "' with url '" + url + "'!"
+ );
+ }
+ throw error;
+ }
+
+ // The value of exports may have been changed by the module script, so
+ // freeze it if and only if it is still an object.
+ if (typeof module.exports === "object" && module.exports !== null) {
+ Object.freeze(module.exports);
+ }
+ }
+
+ /**
+ * Create a require function for the given module. If no module is given,
+ * create a require function for the top-level module instead.
+ *
+ * @param Object requirer
+ * The module for which the require function is to be created.
+ *
+ * @return Function
+ * A require function for the given module.
+ */
+ function createRequire(requirer) {
+ return function require(id) {
+ // Make sure an id was passed.
+ if (id === undefined) {
+ throw new Error("Can't require module without id!");
+ }
+
+ // Built-in modules are cached by id rather than URL, so try to find the
+ // module to be required by id first.
+ let module = modules[id];
+ if (module === undefined) {
+ // Failed to find the module to be required by id, so convert the id to
+ // a URL and try again.
+
+ // If the id is relative, convert it to an absolute id.
+ if (id.startsWith(".")) {
+ if (requirer === undefined) {
+ throw new Error(
+ "Can't require top-level module with relative id " +
+ "'" +
+ id +
+ "'!"
+ );
+ }
+ id = resolve(id, requirer.id);
+ }
+
+ // Convert the absolute id to a normalized id.
+ id = normalizeId(id);
+
+ // Convert the normalized id to a URL.
+ let url = id;
+
+ // If the URL is relative, resolve it to an absolute URL.
+ if (url.match(/^\w+:\/\//) === null) {
+ url = resolveURL(id);
+ }
+
+ // Try to find the module to be required by URL.
+ module = modules[url];
+ if (module === undefined) {
+ // Failed to find the module to be required in the cache, so create
+ // a new module, load it from the given URL, and add it to the cache.
+
+ // Add modules to the cache early so that any recursive calls to
+ // require for the same module will return the partially-loaded module
+ // from the cache instead of triggering a new load.
+ module = modules[url] = createModule(id);
+
+ try {
+ loadModule(module, url);
+ } catch (error) {
+ // If the module failed to load, remove it from the cache so that
+ // subsequent calls to require for the same module will trigger a
+ // new load, instead of returning a partially-loaded module from
+ // the cache.
+ delete modules[url];
+ throw error;
+ }
+
+ Object.freeze(module);
+ }
+ }
+
+ return module.exports;
+ };
+ }
+
+ const createSandbox = options.createSandbox;
+ const globals = options.globals || Object.create(null);
+ const loadSubScript = options.loadSubScript;
+
+ // Create the module cache, by converting each entry in the map from
+ // normalized ids to built-in modules to a module object, with the exports
+ // property of each module set to a frozen version of the original entry.
+ const modules = options.modules || {};
+ for (const id in modules) {
+ const module = createModule(id);
+ module.exports = Object.freeze(modules[id]);
+ modules[id] = module;
+ }
+
+ // Convert the map of paths to base URLs into an array for use by resolveURL.
+ // The array is sorted from longest to shortest path to ensure that the
+ // longest path is always the first to be found.
+ let paths = options.paths || Object.create(null);
+ paths = Object.keys(paths)
+ .sort((a, b) => b.length - a.length)
+ .map(path => [path, paths[path]]);
+
+ const resolve = options.resolve || resolveId;
+
+ this.require = createRequire();
+}
+
+this.WorkerDebuggerLoader = WorkerDebuggerLoader;
+
+var loader = {
+ lazyGetter(object, name, lambda) {
+ Object.defineProperty(object, name, {
+ get() {
+ delete object[name];
+ object[name] = lambda.apply(object);
+ return object[name];
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ },
+ lazyServiceGetter() {
+ throw new Error("Can't import XPCOM service from worker thread!");
+ },
+ lazyRequireGetter(obj, properties, module, destructure) {
+ if (Array.isArray(properties) && !destructure) {
+ throw new Error(
+ "Pass destructure=true to call lazyRequireGetter with an array of properties"
+ );
+ }
+
+ if (!Array.isArray(properties)) {
+ properties = [properties];
+ }
+
+ for (const property of properties) {
+ Object.defineProperty(obj, property, {
+ get: () =>
+ destructure
+ ? worker.require(module)[property]
+ : worker.require(module || property),
+ });
+ }
+ },
+};
+
+// The following APIs are defined differently depending on whether we are on the
+// main thread or a worker thread. On the main thread, we use the Components
+// object to implement them. On worker threads, we use the APIs provided by
+// the worker debugger.
+
+/* eslint-disable no-shadow */
+var {
+ Debugger,
+ URL,
+ createSandbox,
+ dump,
+ rpc,
+ loadSubScript,
+ setImmediate,
+ xpcInspector,
+} = function () {
+ // Main thread
+ if (typeof Components === "object") {
+ const principal = Components.Constructor(
+ "@mozilla.org/systemprincipal;1",
+ "nsIPrincipal"
+ )();
+
+ // To ensure that the this passed to addDebuggerToGlobal is a global, the
+ // Debugger object needs to be defined in a sandbox.
+ const sandbox = Cu.Sandbox(principal, {
+ wantGlobalProperties: ["ChromeUtils"],
+ });
+ Cu.evalInSandbox(
+ `
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ 'resource://gre/modules/jsdebugger.sys.mjs'
+);
+addDebuggerToGlobal(globalThis);
+`,
+ sandbox
+ );
+ const Debugger = sandbox.Debugger;
+
+ const createSandbox = function (name, prototype) {
+ return Cu.Sandbox(principal, {
+ invisibleToDebugger: true,
+ sandboxName: name,
+ sandboxPrototype: prototype,
+ wantComponents: false,
+ wantXrays: false,
+ });
+ };
+
+ const rpc = undefined;
+
+ // eslint-disable-next-line mozilla/use-services
+ const subScriptLoader = Cc[
+ "@mozilla.org/moz/jssubscript-loader;1"
+ ].getService(Ci.mozIJSSubScriptLoader);
+
+ const loadSubScript = function (url, sandbox) {
+ subScriptLoader.loadSubScript(url, sandbox);
+ };
+
+ const Timer = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+
+ const setImmediate = function (callback) {
+ Timer.setTimeout(callback, 0);
+ };
+
+ const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(
+ Ci.nsIJSInspector
+ );
+
+ const { URL } = Cu.Sandbox(principal, {
+ wantGlobalProperties: ["URL"],
+ });
+
+ return {
+ Debugger,
+ URL,
+ createSandbox,
+ dump: this.dump,
+ rpc,
+ loadSubScript,
+ setImmediate,
+ xpcInspector,
+ };
+ }
+ // Worker thread
+ const requestors = [];
+
+ const scope = this;
+
+ const xpcInspector = {
+ get eventLoopNestLevel() {
+ return requestors.length;
+ },
+
+ get lastNestRequestor() {
+ return requestors.length === 0 ? null : requestors[requestors.length - 1];
+ },
+
+ enterNestedEventLoop(requestor) {
+ requestors.push(requestor);
+ scope.enterEventLoop();
+ return requestors.length;
+ },
+
+ exitNestedEventLoop() {
+ requestors.pop();
+ scope.leaveEventLoop();
+ return requestors.length;
+ },
+ };
+
+ return {
+ Debugger: this.Debugger,
+ URL: this.URL,
+ createSandbox: this.createSandbox,
+ dump: this.dump,
+ rpc: this.rpc,
+ loadSubScript: this.loadSubScript,
+ setImmediate: this.setImmediate,
+ xpcInspector,
+ };
+}.call(this);
+/* eslint-enable no-shadow */
+
+// Create the default instance of the worker loader, using the APIs we defined
+// above.
+
+this.worker = new WorkerDebuggerLoader({
+ createSandbox,
+ globals: {
+ isWorker: true,
+ dump,
+ loader,
+ rpc,
+ URL,
+ setImmediate,
+ retrieveConsoleEvents: this.retrieveConsoleEvents,
+ setConsoleEventHandler: this.setConsoleEventHandler,
+ clearConsoleEvents: this.clearConsoleEvents,
+ console,
+ btoa: this.btoa,
+ atob: this.atob,
+ Services: Object.create(null),
+ ChromeUtils,
+ DebuggerNotificationObserver,
+
+ // The following APIs rely on the use of Components, and the worker debugger
+ // does not provide alternative definitions for them. Consequently, they are
+ // stubbed out both on the main thread and worker threads.
+ Cc: undefined,
+ ChromeWorker: undefined,
+ Ci: undefined,
+ Cu: undefined,
+ Cr: undefined,
+ Components: undefined,
+ },
+ loadSubScript,
+ modules: {
+ Debugger,
+ xpcInspector,
+ },
+ paths: {
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ devtools: "resource://devtools",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ promise: "resource://gre/modules/Promise-backend.js",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ "xpcshell-test": "resource://test",
+ // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
+ },
+});