mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-30 05:35:31 +00:00
337 lines
12 KiB
JavaScript
337 lines
12 KiB
JavaScript
/* 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 slashes.
|
|
|
|
// Resolve the given relative id to an absolute id.
|
|
function resolveId(id, baseId) {
|
|
return baseId + "/../" + id;
|
|
};
|
|
|
|
// Convert the given absolute id to 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 convert absolute id " + id + " to " +
|
|
"normalized id because it's going past root!");
|
|
} else {
|
|
stack.push("..");
|
|
}
|
|
} else {
|
|
if (stack[stack.length] == "..") {
|
|
stack.push("..");
|
|
} else {
|
|
stack.pop();
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
stack.push(component);
|
|
break;
|
|
}
|
|
});
|
|
|
|
return (root ? root : "") + stack.join("/");
|
|
}
|
|
|
|
// Create a module object 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 takes the name and
|
|
// prototype of the sandbox to be created, and should return the newly
|
|
// created sandbox as result. This option is mandatory.
|
|
// - globals:
|
|
// A map of built-in globals that will be exposed to every module. Defaults
|
|
// to the empty map.
|
|
// - loadInSandbox:
|
|
// A function that will be used to load scripts in sandboxes. It takes the
|
|
// URL from which and the sandbox in which the script is to be loaded, and
|
|
// should not return a result. This option is mandatory.
|
|
// - modules:
|
|
// A map of 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
|
|
// takes the relative id of the module to be required and the normalized id
|
|
// of the requiring module as arguments, and should return the absolute id
|
|
// of the module to be required as result. Defaults to resolveId above.
|
|
function WorkerDebuggerLoader(options) {
|
|
// Resolve the given relative URL to 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 + " to absolute " +
|
|
"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.
|
|
function loadModule(module, url) {
|
|
// CommonJS specifies 3 free variables named require, exports, and module,
|
|
// that 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 {
|
|
loadInSandbox(url, sandbox);
|
|
} catch (error) {
|
|
if (String(error) === "Error opening input stream (invalid filename?)") {
|
|
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 only
|
|
// freeze it 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 requirer. If no requirer is given,
|
|
// create a require function for top-level modules instead.
|
|
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, resolve 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 with 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 loadInSandbox = options.loadInSandbox;
|
|
|
|
// Create the module cache by converting each entry in the map of built-in
|
|
// modules to a module object with its exports property 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 baseURLs into an array for use by resolveURL.
|
|
// The array is sorted from longest to shortest path so the longest path will
|
|
// always be the first to be found.
|
|
let paths = options.paths || {};
|
|
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;
|
|
|
|
const chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined,
|
|
Cm: undefined, Ci: undefined, Cu: undefined,
|
|
Cr: undefined, components: undefined };
|
|
|
|
// The default instance of the worker debugger loader is defined differently
|
|
// depending on whether it is loaded from the main thread or a worker thread.
|
|
if (typeof Components === "object") {
|
|
const { Constructor: CC, classes: Cc, manager: Cm, interfaces: Ci,
|
|
results: Cr, utils: Cu } = Components;
|
|
|
|
const principal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
|
|
|
|
// Create a sandbox with the given name and prototype.
|
|
const createSandbox = function (name, prototype) {
|
|
return Cu.Sandbox(principal, {
|
|
invisibleToDebugger: true,
|
|
sandboxName: name,
|
|
sandboxPrototype: prototype,
|
|
wantComponents: false,
|
|
wantXrays: false
|
|
});
|
|
};
|
|
|
|
const loadSubScript = Cc['@mozilla.org/moz/jssubscript-loader;1'].
|
|
getService(Ci.mozIJSSubScriptLoader).loadSubScript;
|
|
|
|
// Load a script from the given URL in the given sandbox.
|
|
const loadInSandbox = function (url, sandbox) {
|
|
loadSubScript(url, sandbox, "UTF-8");
|
|
};
|
|
|
|
// Define the Debugger object in a sandbox to ensure that the this passed to
|
|
// addDebuggerToGlobal is a global.
|
|
let sandbox = Cu.Sandbox(principal, {});
|
|
Cu.evalInSandbox(
|
|
"Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
|
|
"addDebuggerToGlobal(this);",
|
|
sandbox
|
|
);
|
|
const Debugger = sandbox.Debugger;
|
|
|
|
const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
|
|
const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
|
|
getService(Ci.nsIJSInspector);
|
|
|
|
this.worker = new WorkerDebuggerLoader({
|
|
createSandbox: createSandbox,
|
|
globals: {
|
|
"isWorker": true,
|
|
"Debugger": Debugger,
|
|
"setInterval": Timer.setInterval,
|
|
"setTimeout": Timer.setTimeout,
|
|
"clearInterval": Timer.clearInterval,
|
|
"clearTimeout": Timer.clearTimeout,
|
|
"xpcInspector": xpcInspector,
|
|
"reportError": Cu.reportError,
|
|
},
|
|
loadInSandbox: loadInSandbox,
|
|
modules: {
|
|
"Services": {},
|
|
"chrome": chrome,
|
|
},
|
|
paths: {
|
|
"": "resource://gre/modules/commonjs/",
|
|
"devtools": "resource:///modules/devtools",
|
|
"devtools/server": "resource://gre/modules/devtools/server",
|
|
"devtools/toolkit": "resource://gre/modules/devtools",
|
|
"source-map": "resource://gre/modules/devtools/source-map",
|
|
"xpcshell-test": "resource://test",
|
|
}
|
|
});
|
|
}
|