Bumping manifests a=b2g-bump
[gecko.git] / toolkit / devtools / worker-loader.js
blobe4832349d9cd48103393d272311f27e923d681ed
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/. */
5 "use strict";
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+:\/\/\/?|\/)?(.*)/);
43   let stack = [];
44   path.split("/").forEach(function (component) {
45     switch (component) {
46     case "":
47     case ".":
48       break;
49     case "..":
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!");
54         } else {
55           stack.push("..");
56         }
57       } else {
58         if (stack[stack.length] == "..") {
59           stack.push("..");
60         } else {
61           stack.pop();
62         }
63       }
64       break;
65     default:
66       stack.push(component);
67       break;
68     }
69   });
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
78     // non-writable.
79     id: {
80       configurable: false,
81       enumerable: true,
82       value: id,
83       writable: false
84     },
86     // CommonJS does not specify an exports property, so follow the NodeJS
87     // convention, which is to make it non-configurable and writable.
88     exports: {
89       configurable: false,
90       enumerable: true,
91       value: Object.create(null),
92       writable: true
93     }
94   });
97 // Create a CommonJS loader with the following options:
98 // - createSandbox:
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.
102 // - globals:
103 //     A map of built-in globals that will be exposed to every module. Defaults
104 //     to the empty map.
105 // - loadInSandbox:
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.
109 // - modules:
110 //     A map of built-in modules that will be added to the module cache.
111 //     Defaults to the empty map.
112 // - paths:
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.
115 // - resolve:
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) {
123     let found = false;
124     for (let [path, baseURL] of paths) {
125       if (url.startsWith(path)) {
126         found = true;
127         url = url.replace(path, baseURL);
128         break;
129       }
130     }
131     if (!found) {
132       throw new Error("can't resolve relative URL " + url + " to absolute " +
133                       "URL!");
134     }
136     // If the url has no extension, use ".js" by default.
137     return url.endsWith(".js") ? url : url + ".js";
138   }
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);
153     try {
154       loadInSandbox(url, sandbox);
155     } catch (error) {
156       if (String(error) === "Error opening input stream (invalid filename?)") {
157         throw new Error("can't load module " + module.id + " with url " + url +
158                         "!");
159       }
160       throw error;
161     }
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);
167     }
168   };
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!");
177       }
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 " +
190                             id + "!");
191           }
192           id = resolve(id, requirer.id);
193         }
195         // Convert the absolute id to a normalized id.
196         id = normalizeId(id);
198         // Convert the normalized id to a URL.
199         let url = id;
201         // If the URL is relative, resolve it to an absolute URL.
202         if (url.match(/^\w+:\/\//) === null) {
203           url = resolveURL(id);
204         }
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);
217           try {
218             loadModule(module, url);
219           } catch (error) {
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
223             // the cache.
224             delete modules[url];
225             throw error;
226           }
228           Object.freeze(module);
229         }
230       }
232       return module.exports;
233     };
234   }
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;
248   }
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,
281       sandboxName: name,
282       sandboxPrototype: prototype,
283       wantComponents: false,
284       wantXrays: false
285     });
286   };
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");
294   };
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, {});
299   Cu.evalInSandbox(
300     "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
301     "addDebuggerToGlobal(this);",
302     sandbox
303   );
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,
312     globals: {
313       "isWorker": true,
314       "reportError": Cu.reportError,
315     },
316     loadInSandbox: loadInSandbox,
317     modules: {
318       "Services": {},
319       "chrome": chrome,
320       "promise": Promise,
321       "Debugger": Debugger,
322       "xpcInspector": xpcInspector,
323       "Timer": Object.create(Timer)
324     },
325     paths: {
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",
332     }
333   });