summaryrefslogtreecommitdiffstats
path: root/devtools/shared/loader/base-loader.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/loader/base-loader.sys.mjs')
-rw-r--r--devtools/shared/loader/base-loader.sys.mjs638
1 files changed, 638 insertions, 0 deletions
diff --git a/devtools/shared/loader/base-loader.sys.mjs b/devtools/shared/loader/base-loader.sys.mjs
new file mode 100644
index 0000000000..b9d625f3e3
--- /dev/null
+++ b/devtools/shared/loader/base-loader.sys.mjs
@@ -0,0 +1,638 @@
+/* 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.defineESModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+// 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;
+}