1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // A CommonJS module loader that is designed to run inside a worker debugger.
8 // We can't simply use the SDK module loader, because it relies heavily on
9 // Components, which isn't available in workers.
11 // In principle, the standard instance of the worker loader should provide the
12 // same built-in modules as its devtools counterpart, so that both loaders are
13 // interchangable on the main thread, making them easier to test.
15 // On the worker thread, some of these modules, in particular those that rely on
16 // the use of Components and for which the worker debugger doesn't provide an
17 // alternative API, will be replaced by vacuous objects. Consequently, they can
18 // still be required, but any attempts to use them will lead to an exception.
20 this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];
22 // Some notes on module ids and URLs:
24 // An id is either a relative id or an absolute id. An id is relative if and
25 // only if it starts with a dot. An absolute id is a normalized id if and only if
26 // it contains no redundant components. Every normalized id is a URL.
28 // A URL is either an absolute URL or a relative URL. A URL is absolute if and
29 // only if it starts with a scheme name followed by a colon and 2 slashes.
31 // Resolve the given relative id to an absolute id.
32 function resolveId(id, baseId) {
33 return baseId + "/../" + id;
36 // Convert the given absolute id to a normalized id.
37 function normalizeId(id) {
38 // An id consists of an optional root and a path. A root consists of either
39 // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
40 // root are not used as separators, so only normalize the path.
41 let [_, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
44 path.split("/").forEach(function (component) {
50 if (stack.length === 0) {
51 if (root !== undefined) {
52 throw new Error("can't convert absolute id " + id + " to " +
53 "normalized id because it's going past root!");
58 if (stack[stack.length] == "..") {
66 stack.push(component);
71 return (root ? root : "") + stack.join("/");
74 // Create a module object with the given id.
75 function createModule(id) {
76 return Object.create(null, {
77 // CommonJS specifies the id property to be non-configurable and
86 // CommonJS does not specify an exports property, so follow the NodeJS
87 // convention, which is to make it non-configurable and writable.
91 value: Object.create(null),
97 // Create a CommonJS loader with the following options:
99 // A function that will be used to create sandboxes. It takes the name and
100 // prototype of the sandbox to be created, and should return the newly
101 // created sandbox as result. This option is mandatory.
103 // A map of built-in globals that will be exposed to every module. Defaults
106 // A function that will be used to load scripts in sandboxes. It takes the
107 // URL from which and the sandbox in which the script is to be loaded, and
108 // should not return a result. This option is mandatory.
110 // A map of built-in modules that will be added to the module cache.
111 // Defaults to the empty map.
113 // A map of paths to base URLs that will be used to resolve relative URLs to
114 // absolute URLS. Defaults to the empty map.
116 // A function that will be used to resolve relative ids to absolute ids. It
117 // takes the relative id of the module to be required and the normalized id
118 // of the requiring module as arguments, and should return the absolute id
119 // of the module to be required as result. Defaults to resolveId above.
120 function WorkerDebuggerLoader(options) {
121 // Resolve the given relative URL to an absolute URL.
122 function resolveURL(url) {
124 for (let [path, baseURL] of paths) {
125 if (url.startsWith(path)) {
127 url = url.replace(path, baseURL);
132 throw new Error("can't resolve relative URL " + url + " to absolute " +
136 // If the url has no extension, use ".js" by default.
137 return url.endsWith(".js") ? url : url + ".js";
140 // Load the given module with the given url.
141 function loadModule(module, url) {
142 // CommonJS specifies 3 free variables named require, exports, and module,
143 // that must be exposed to every module, so define these as properties on
144 // the sandbox prototype. Additional built-in globals are exposed by making
145 // the map of built-in globals the prototype of the sandbox prototype.
146 let prototype = Object.create(globals);
147 prototype.Components = {};
148 prototype.require = createRequire(module);
149 prototype.exports = module.exports;
150 prototype.module = module;
152 let sandbox = createSandbox(url, prototype);
154 loadInSandbox(url, sandbox);
156 if (String(error) === "Error opening input stream (invalid filename?)") {
157 throw new Error("can't load module " + module.id + " with url " + url +
163 // The value of exports may have been changed by the module script, so only
164 // freeze it if it is still an object.
165 if (typeof module.exports === "object" && module.exports !== null) {
166 Object.freeze(module.exports);
170 // Create a require function for the given requirer. If no requirer is given,
171 // create a require function for top-level modules instead.
172 function createRequire(requirer) {
173 return function require(id) {
174 // Make sure an id was passed.
175 if (id === undefined) {
176 throw new Error("can't require module without id!");
179 // Built-in modules are cached by id rather than URL, so try to find the
180 // module to be required by id first.
181 let module = modules[id];
182 if (module === undefined) {
183 // Failed to find the module to be required by id, so convert the id to
184 // a URL and try again.
186 // If the id is relative, resolve it to an absolute id.
187 if (id.startsWith(".")) {
188 if (requirer === undefined) {
189 throw new Error("can't require top-level module with relative id " +
192 id = resolve(id, requirer.id);
195 // Convert the absolute id to a normalized id.
196 id = normalizeId(id);
198 // Convert the normalized id to a URL.
201 // If the URL is relative, resolve it to an absolute URL.
202 if (url.match(/^\w+:\/\//) === null) {
203 url = resolveURL(id);
206 // Try to find the module to be required by URL.
207 module = modules[url];
208 if (module === undefined) {
209 // Failed to find the module to be required in the cache, so create
210 // a new module, load it with the given URL, and add it to the cache.
212 // Add modules to the cache early so that any recursive calls to
213 // require for the same module will return the partially-loaded module
214 // from the cache instead of triggering a new load.
215 module = modules[url] = createModule(id);
218 loadModule(module, url);
220 // If the module failed to load, remove it from the cache so that
221 // subsequent calls to require for the same module will trigger a
222 // new load instead of returning a partially-loaded module from
228 Object.freeze(module);
232 return module.exports;
236 let createSandbox = options.createSandbox;
237 let globals = options.globals || Object.create(null);
238 let loadInSandbox = options.loadInSandbox;
240 // Create the module cache by converting each entry in the map of built-in
241 // modules to a module object with its exports property set to a frozen
242 // version of the original entry.
243 let modules = options.modules || {};
244 for (let id in modules) {
245 let module = createModule(id);
246 module.exports = Object.freeze(modules[id]);
247 modules[id] = module;
250 // Convert the map of paths to baseURLs into an array for use by resolveURL.
251 // The array is sorted from longest to shortest path so the longest path will
252 // always be the first to be found.
253 let paths = options.paths || {};
254 paths = Object.keys(paths)
255 .sort((a, b) => b.length - a.length)
256 .map(path => [path, paths[path]]);
258 let resolve = options.resolve || resolveId;
260 this.require = createRequire();
263 this.WorkerDebuggerLoader = WorkerDebuggerLoader;
265 const chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined,
266 Cm: undefined, Ci: undefined, Cu: undefined,
267 Cr: undefined, components: undefined };
269 // The default instance of the worker debugger loader is defined differently
270 // depending on whether it is loaded from the main thread or a worker thread.
271 if (typeof Components === "object") {
272 const { Constructor: CC, classes: Cc, manager: Cm, interfaces: Ci,
273 results: Cr, utils: Cu } = Components;
275 const principal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
277 // Create a sandbox with the given name and prototype.
278 const createSandbox = function (name, prototype) {
279 return Cu.Sandbox(principal, {
280 invisibleToDebugger: true,
282 sandboxPrototype: prototype,
283 wantComponents: false,
288 const loadSubScript = Cc['@mozilla.org/moz/jssubscript-loader;1'].
289 getService(Ci.mozIJSSubScriptLoader).loadSubScript;
291 // Load a script from the given URL in the given sandbox.
292 const loadInSandbox = function (url, sandbox) {
293 loadSubScript(url, sandbox, "UTF-8");
296 // Define the Debugger object in a sandbox to ensure that the this passed to
297 // addDebuggerToGlobal is a global.
298 let sandbox = Cu.Sandbox(principal, {});
300 "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
301 "addDebuggerToGlobal(this);",
304 const Debugger = sandbox.Debugger;
306 const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
307 const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
308 getService(Ci.nsIJSInspector);
310 this.worker = new WorkerDebuggerLoader({
311 createSandbox: createSandbox,
314 "reportError": Cu.reportError,
316 loadInSandbox: loadInSandbox,
321 "Debugger": Debugger,
322 "xpcInspector": xpcInspector,
323 "Timer": Object.create(Timer)
326 "": "resource://gre/modules/commonjs/",
327 "devtools": "resource:///modules/devtools",
328 "devtools/server": "resource://gre/modules/devtools/server",
329 "devtools/toolkit": "resource://gre/modules/devtools",
330 "source-map": "resource://gre/modules/devtools/source-map",
331 "xpcshell-test": "resource://test",