123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- /* 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";
- // 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.
- 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.
- let [_, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
- let 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 (let [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.
- return url.endsWith(".js") ? 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.
- let prototype = Object.create(globals);
- prototype.Components = {};
- prototype.require = createRequire(module);
- prototype.exports = module.exports;
- prototype.module = module;
- let 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;
- };
- }
- let createSandbox = options.createSandbox;
- let globals = options.globals || Object.create(null);
- let 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.
- let modules = options.modules || {};
- for (let id in modules) {
- let 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]]);
- let resolve = options.resolve || resolveId;
- this.require = createRequire();
- }
- this.WorkerDebuggerLoader = WorkerDebuggerLoader;
- // 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.
- var chrome = {
- CC: undefined,
- Cc: undefined,
- ChromeWorker: undefined,
- Cm: undefined,
- Ci: undefined,
- Cu: undefined,
- Cr: undefined,
- components: undefined
- };
- var loader = {
- lazyGetter: function (object, name, lambda) {
- Object.defineProperty(object, name, {
- get: function () {
- delete object[name];
- return object[name] = lambda.apply(object);
- },
- configurable: true,
- enumerable: true
- });
- },
- lazyImporter: function () {
- throw new Error("Can't import JSM from worker thread!");
- },
- lazyServiceGetter: function () {
- throw new Error("Can't import XPCOM service from worker thread!");
- },
- lazyRequireGetter: function (obj, property, module, destructure) {
- 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.
- var {
- Debugger,
- URL,
- createSandbox,
- dump,
- rpc,
- loadSubScript,
- reportError,
- setImmediate,
- xpcInspector
- } = (function () {
- if (typeof Components === "object") { // Main thread
- let {
- Constructor: CC,
- classes: Cc,
- manager: Cm,
- interfaces: Ci,
- results: Cr,
- utils: Cu
- } = Components;
- let principal = CC("@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.
- let sandbox = Cu.Sandbox(principal, {});
- Cu.evalInSandbox(
- "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
- "addDebuggerToGlobal(this);",
- sandbox
- );
- let Debugger = sandbox.Debugger;
- let createSandbox = function (name, prototype) {
- return Cu.Sandbox(principal, {
- invisibleToDebugger: true,
- sandboxName: name,
- sandboxPrototype: prototype,
- wantComponents: false,
- wantXrays: false
- });
- };
- let rpc = undefined;
- let subScriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
- getService(Ci.mozIJSSubScriptLoader);
- let loadSubScript = function (url, sandbox) {
- subScriptLoader.loadSubScript(url, sandbox, "UTF-8");
- };
- let reportError = Cu.reportError;
- let Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
- let setImmediate = function (callback) {
- Timer.setTimeout(callback, 0);
- };
- let xpcInspector = Cc["@mozilla.org/jsinspector;1"].
- getService(Ci.nsIJSInspector);
- return {
- Debugger,
- URL: this.URL,
- createSandbox,
- dump: this.dump,
- rpc,
- loadSubScript,
- reportError,
- setImmediate,
- xpcInspector
- };
- } else { // Worker thread
- let requestors = [];
- let scope = this;
- let xpcInspector = {
- get eventLoopNestLevel() {
- return requestors.length;
- },
- get lastNestRequestor() {
- return requestors.length === 0 ? null : requestors[requestors.length - 1];
- },
- enterNestedEventLoop: function (requestor) {
- requestors.push(requestor);
- scope.enterEventLoop();
- return requestors.length;
- },
- exitNestedEventLoop: function () {
- 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,
- reportError: this.reportError,
- setImmediate: this.setImmediate,
- xpcInspector: xpcInspector
- };
- }
- }).call(this);
- // Create the default instance of the worker loader, using the APIs we defined
- // above.
- this.worker = new WorkerDebuggerLoader({
- createSandbox: createSandbox,
- globals: {
- "isWorker": true,
- "dump": dump,
- "loader": loader,
- "reportError": reportError,
- "rpc": rpc,
- "setImmediate": setImmediate,
- "URL": URL,
- },
- loadSubScript: loadSubScript,
- modules: {
- "Debugger": Debugger,
- "Services": Object.create(null),
- "chrome": chrome,
- "xpcInspector": xpcInspector
- },
- paths: {
- // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
- "": "resource://gre/modules/commonjs/",
- // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
- // Modules here are intended to have one implementation for
- // chrome, and a separate implementation for content. Here we
- // map the directory to the chrome subdirectory, but the content
- // loader will map to the content subdirectory. See the
- // README.md in devtools/shared/platform.
- "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
- // ⚠ 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 ⚠
- "source-map": "resource://devtools/shared/sourcemap/source-map.js",
- // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
- "xpcshell-test": "resource://test"
- // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
- }
- });
|