loader.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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/. */
  4. "use strict";
  5. // A CommonJS module loader that is designed to run inside a worker debugger.
  6. // We can't simply use the SDK module loader, because it relies heavily on
  7. // Components, which isn't available in workers.
  8. //
  9. // In principle, the standard instance of the worker loader should provide the
  10. // same built-in modules as its devtools counterpart, so that both loaders are
  11. // interchangable on the main thread, making them easier to test.
  12. //
  13. // On the worker thread, some of these modules, in particular those that rely on
  14. // the use of Components, and for which the worker debugger doesn't provide an
  15. // alternative API, will be replaced by vacuous objects. Consequently, they can
  16. // still be required, but any attempts to use them will lead to an exception.
  17. this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];
  18. // Some notes on module ids and URLs:
  19. //
  20. // An id is either a relative id or an absolute id. An id is relative if and
  21. // only if it starts with a dot. An absolute id is a normalized id if and only
  22. // if it contains no redundant components.
  23. //
  24. // Every normalized id is a URL. A URL is either an absolute URL or a relative
  25. // URL. A URL is absolute if and only if it starts with a scheme name followed
  26. // by a colon and 2 or 3 slashes.
  27. /**
  28. * Convert the given relative id to an absolute id.
  29. *
  30. * @param String id
  31. * The relative id to be resolved.
  32. * @param String baseId
  33. * The absolute base id to resolve the relative id against.
  34. *
  35. * @return String
  36. * An absolute id
  37. */
  38. function resolveId(id, baseId) {
  39. return baseId + "/../" + id;
  40. }
  41. /**
  42. * Convert the given absolute id to a normalized id.
  43. *
  44. * @param String id
  45. * The absolute id to be normalized.
  46. *
  47. * @return String
  48. * A normalized id.
  49. */
  50. function normalizeId(id) {
  51. // An id consists of an optional root and a path. A root consists of either
  52. // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
  53. // root are not used as separators, so only normalize the path.
  54. let [_, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);
  55. let stack = [];
  56. path.split("/").forEach(function (component) {
  57. switch (component) {
  58. case "":
  59. case ".":
  60. break;
  61. case "..":
  62. if (stack.length === 0) {
  63. if (root !== undefined) {
  64. throw new Error("Can't normalize absolute id '" + id + "'!");
  65. } else {
  66. stack.push("..");
  67. }
  68. } else {
  69. if (stack[stack.length - 1] == "..") {
  70. stack.push("..");
  71. } else {
  72. stack.pop();
  73. }
  74. }
  75. break;
  76. default:
  77. stack.push(component);
  78. break;
  79. }
  80. });
  81. return (root ? root : "") + stack.join("/");
  82. }
  83. /**
  84. * Create a module object with the given normalized id.
  85. *
  86. * @param String
  87. * The normalized id of the module to be created.
  88. *
  89. * @return Object
  90. * A module with the given id.
  91. */
  92. function createModule(id) {
  93. return Object.create(null, {
  94. // CommonJS specifies the id property to be non-configurable and
  95. // non-writable.
  96. id: {
  97. configurable: false,
  98. enumerable: true,
  99. value: id,
  100. writable: false
  101. },
  102. // CommonJS does not specify an exports property, so follow the NodeJS
  103. // convention, which is to make it non-configurable and writable.
  104. exports: {
  105. configurable: false,
  106. enumerable: true,
  107. value: Object.create(null),
  108. writable: true
  109. }
  110. });
  111. }
  112. /**
  113. * Create a CommonJS loader with the following options:
  114. * - createSandbox:
  115. * A function that will be used to create sandboxes. It should take the name
  116. * and prototype of the sandbox to be created, and return the newly created
  117. * sandbox as result. This option is required.
  118. * - globals:
  119. * A map of names to built-in globals that will be exposed to every module.
  120. * Defaults to the empty map.
  121. * - loadSubScript:
  122. * A function that will be used to load scripts in sandboxes. It should take
  123. * the URL from and the sandbox in which the script is to be loaded, and not
  124. * return a result. This option is required.
  125. * - modules:
  126. * A map from normalized ids to built-in modules that will be added to the
  127. * module cache. Defaults to the empty map.
  128. * - paths:
  129. * A map of paths to base URLs that will be used to resolve relative URLs to
  130. * absolute URLS. Defaults to the empty map.
  131. * - resolve:
  132. * A function that will be used to resolve relative ids to absolute ids. It
  133. * should take the relative id of a module to be required and the absolute
  134. * id of the requiring module as arguments, and return the absolute id of
  135. * the module to be required as result. Defaults to resolveId above.
  136. */
  137. function WorkerDebuggerLoader(options) {
  138. /**
  139. * Convert the given relative URL to an absolute URL, using the map of paths
  140. * given below.
  141. *
  142. * @param String url
  143. * The relative URL to be resolved.
  144. *
  145. * @return String
  146. * An absolute URL.
  147. */
  148. function resolveURL(url) {
  149. let found = false;
  150. for (let [path, baseURL] of paths) {
  151. if (url.startsWith(path)) {
  152. found = true;
  153. url = url.replace(path, baseURL);
  154. break;
  155. }
  156. }
  157. if (!found) {
  158. throw new Error("Can't resolve relative URL '" + url + "'!");
  159. }
  160. // If the url has no extension, use ".js" by default.
  161. return url.endsWith(".js") ? url : url + ".js";
  162. }
  163. /**
  164. * Load the given module with the given url.
  165. *
  166. * @param Object module
  167. * The module object to be loaded.
  168. * @param String url
  169. * The URL to load the module from.
  170. */
  171. function loadModule(module, url) {
  172. // CommonJS specifies 3 free variables: require, exports, and module. These
  173. // must be exposed to every module, so define these as properties on the
  174. // sandbox prototype. Additional built-in globals are exposed by making
  175. // the map of built-in globals the prototype of the sandbox prototype.
  176. let prototype = Object.create(globals);
  177. prototype.Components = {};
  178. prototype.require = createRequire(module);
  179. prototype.exports = module.exports;
  180. prototype.module = module;
  181. let sandbox = createSandbox(url, prototype);
  182. try {
  183. loadSubScript(url, sandbox);
  184. } catch (error) {
  185. if (/^Error opening input stream/.test(String(error))) {
  186. throw new Error("Can't load module '" + module.id + "' with url '" +
  187. url + "'!");
  188. }
  189. throw error;
  190. }
  191. // The value of exports may have been changed by the module script, so
  192. // freeze it if and only if it is still an object.
  193. if (typeof module.exports === "object" && module.exports !== null) {
  194. Object.freeze(module.exports);
  195. }
  196. }
  197. /**
  198. * Create a require function for the given module. If no module is given,
  199. * create a require function for the top-level module instead.
  200. *
  201. * @param Object requirer
  202. * The module for which the require function is to be created.
  203. *
  204. * @return Function
  205. * A require function for the given module.
  206. */
  207. function createRequire(requirer) {
  208. return function require(id) {
  209. // Make sure an id was passed.
  210. if (id === undefined) {
  211. throw new Error("Can't require module without id!");
  212. }
  213. // Built-in modules are cached by id rather than URL, so try to find the
  214. // module to be required by id first.
  215. let module = modules[id];
  216. if (module === undefined) {
  217. // Failed to find the module to be required by id, so convert the id to
  218. // a URL and try again.
  219. // If the id is relative, convert it to an absolute id.
  220. if (id.startsWith(".")) {
  221. if (requirer === undefined) {
  222. throw new Error("Can't require top-level module with relative id " +
  223. "'" + id + "'!");
  224. }
  225. id = resolve(id, requirer.id);
  226. }
  227. // Convert the absolute id to a normalized id.
  228. id = normalizeId(id);
  229. // Convert the normalized id to a URL.
  230. let url = id;
  231. // If the URL is relative, resolve it to an absolute URL.
  232. if (url.match(/^\w+:\/\//) === null) {
  233. url = resolveURL(id);
  234. }
  235. // Try to find the module to be required by URL.
  236. module = modules[url];
  237. if (module === undefined) {
  238. // Failed to find the module to be required in the cache, so create
  239. // a new module, load it from the given URL, and add it to the cache.
  240. // Add modules to the cache early so that any recursive calls to
  241. // require for the same module will return the partially-loaded module
  242. // from the cache instead of triggering a new load.
  243. module = modules[url] = createModule(id);
  244. try {
  245. loadModule(module, url);
  246. } catch (error) {
  247. // If the module failed to load, remove it from the cache so that
  248. // subsequent calls to require for the same module will trigger a
  249. // new load, instead of returning a partially-loaded module from
  250. // the cache.
  251. delete modules[url];
  252. throw error;
  253. }
  254. Object.freeze(module);
  255. }
  256. }
  257. return module.exports;
  258. };
  259. }
  260. let createSandbox = options.createSandbox;
  261. let globals = options.globals || Object.create(null);
  262. let loadSubScript = options.loadSubScript;
  263. // Create the module cache, by converting each entry in the map from
  264. // normalized ids to built-in modules to a module object, with the exports
  265. // property of each module set to a frozen version of the original entry.
  266. let modules = options.modules || {};
  267. for (let id in modules) {
  268. let module = createModule(id);
  269. module.exports = Object.freeze(modules[id]);
  270. modules[id] = module;
  271. }
  272. // Convert the map of paths to base URLs into an array for use by resolveURL.
  273. // The array is sorted from longest to shortest path to ensure that the
  274. // longest path is always the first to be found.
  275. let paths = options.paths || Object.create(null);
  276. paths = Object.keys(paths)
  277. .sort((a, b) => b.length - a.length)
  278. .map(path => [path, paths[path]]);
  279. let resolve = options.resolve || resolveId;
  280. this.require = createRequire();
  281. }
  282. this.WorkerDebuggerLoader = WorkerDebuggerLoader;
  283. // The following APIs rely on the use of Components, and the worker debugger
  284. // does not provide alternative definitions for them. Consequently, they are
  285. // stubbed out both on the main thread and worker threads.
  286. var chrome = {
  287. CC: undefined,
  288. Cc: undefined,
  289. ChromeWorker: undefined,
  290. Cm: undefined,
  291. Ci: undefined,
  292. Cu: undefined,
  293. Cr: undefined,
  294. components: undefined
  295. };
  296. var loader = {
  297. lazyGetter: function (object, name, lambda) {
  298. Object.defineProperty(object, name, {
  299. get: function () {
  300. delete object[name];
  301. return object[name] = lambda.apply(object);
  302. },
  303. configurable: true,
  304. enumerable: true
  305. });
  306. },
  307. lazyImporter: function () {
  308. throw new Error("Can't import JSM from worker thread!");
  309. },
  310. lazyServiceGetter: function () {
  311. throw new Error("Can't import XPCOM service from worker thread!");
  312. },
  313. lazyRequireGetter: function (obj, property, module, destructure) {
  314. Object.defineProperty(obj, property, {
  315. get: () => destructure ? worker.require(module)[property]
  316. : worker.require(module || property)
  317. });
  318. }
  319. };
  320. // The following APIs are defined differently depending on whether we are on the
  321. // main thread or a worker thread. On the main thread, we use the Components
  322. // object to implement them. On worker threads, we use the APIs provided by
  323. // the worker debugger.
  324. var {
  325. Debugger,
  326. URL,
  327. createSandbox,
  328. dump,
  329. rpc,
  330. loadSubScript,
  331. reportError,
  332. setImmediate,
  333. xpcInspector
  334. } = (function () {
  335. if (typeof Components === "object") { // Main thread
  336. let {
  337. Constructor: CC,
  338. classes: Cc,
  339. manager: Cm,
  340. interfaces: Ci,
  341. results: Cr,
  342. utils: Cu
  343. } = Components;
  344. let principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
  345. // To ensure that the this passed to addDebuggerToGlobal is a global, the
  346. // Debugger object needs to be defined in a sandbox.
  347. let sandbox = Cu.Sandbox(principal, {});
  348. Cu.evalInSandbox(
  349. "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
  350. "addDebuggerToGlobal(this);",
  351. sandbox
  352. );
  353. let Debugger = sandbox.Debugger;
  354. let createSandbox = function (name, prototype) {
  355. return Cu.Sandbox(principal, {
  356. invisibleToDebugger: true,
  357. sandboxName: name,
  358. sandboxPrototype: prototype,
  359. wantComponents: false,
  360. wantXrays: false
  361. });
  362. };
  363. let rpc = undefined;
  364. let subScriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
  365. getService(Ci.mozIJSSubScriptLoader);
  366. let loadSubScript = function (url, sandbox) {
  367. subScriptLoader.loadSubScript(url, sandbox, "UTF-8");
  368. };
  369. let reportError = Cu.reportError;
  370. let Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
  371. let setImmediate = function (callback) {
  372. Timer.setTimeout(callback, 0);
  373. };
  374. let xpcInspector = Cc["@mozilla.org/jsinspector;1"].
  375. getService(Ci.nsIJSInspector);
  376. return {
  377. Debugger,
  378. URL: this.URL,
  379. createSandbox,
  380. dump: this.dump,
  381. rpc,
  382. loadSubScript,
  383. reportError,
  384. setImmediate,
  385. xpcInspector
  386. };
  387. } else { // Worker thread
  388. let requestors = [];
  389. let scope = this;
  390. let xpcInspector = {
  391. get eventLoopNestLevel() {
  392. return requestors.length;
  393. },
  394. get lastNestRequestor() {
  395. return requestors.length === 0 ? null : requestors[requestors.length - 1];
  396. },
  397. enterNestedEventLoop: function (requestor) {
  398. requestors.push(requestor);
  399. scope.enterEventLoop();
  400. return requestors.length;
  401. },
  402. exitNestedEventLoop: function () {
  403. requestors.pop();
  404. scope.leaveEventLoop();
  405. return requestors.length;
  406. }
  407. };
  408. return {
  409. Debugger: this.Debugger,
  410. URL: this.URL,
  411. createSandbox: this.createSandbox,
  412. dump: this.dump,
  413. rpc: this.rpc,
  414. loadSubScript: this.loadSubScript,
  415. reportError: this.reportError,
  416. setImmediate: this.setImmediate,
  417. xpcInspector: xpcInspector
  418. };
  419. }
  420. }).call(this);
  421. // Create the default instance of the worker loader, using the APIs we defined
  422. // above.
  423. this.worker = new WorkerDebuggerLoader({
  424. createSandbox: createSandbox,
  425. globals: {
  426. "isWorker": true,
  427. "dump": dump,
  428. "loader": loader,
  429. "reportError": reportError,
  430. "rpc": rpc,
  431. "setImmediate": setImmediate,
  432. "URL": URL,
  433. },
  434. loadSubScript: loadSubScript,
  435. modules: {
  436. "Debugger": Debugger,
  437. "Services": Object.create(null),
  438. "chrome": chrome,
  439. "xpcInspector": xpcInspector
  440. },
  441. paths: {
  442. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  443. "": "resource://gre/modules/commonjs/",
  444. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  445. // Modules here are intended to have one implementation for
  446. // chrome, and a separate implementation for content. Here we
  447. // map the directory to the chrome subdirectory, but the content
  448. // loader will map to the content subdirectory. See the
  449. // README.md in devtools/shared/platform.
  450. "devtools/shared/platform": "resource://devtools/shared/platform/chrome",
  451. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  452. "devtools": "resource://devtools",
  453. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  454. "promise": "resource://gre/modules/Promise-backend.js",
  455. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  456. "source-map": "resource://devtools/shared/sourcemap/source-map.js",
  457. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  458. "xpcshell-test": "resource://test"
  459. // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
  460. }
  461. });