michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: /** michael@0: * Implementation of a CommonJS module loader for workers. michael@0: * michael@0: * Use: michael@0: * // in the .js file loaded by the constructor of the worker michael@0: * importScripts("resource://gre/modules/workers/require.js"); michael@0: * let module = require("resource://gre/modules/worker/myModule.js"); michael@0: * michael@0: * // in myModule.js michael@0: * // Load dependencies michael@0: * let SimpleTest = require("resource://gre/modules/workers/SimpleTest.js"); michael@0: * let Logger = require("resource://gre/modules/workers/Logger.js"); michael@0: * michael@0: * // Define things that will not be exported michael@0: * let someValue = // ... michael@0: * michael@0: * // Export symbols michael@0: * exports.foo = // ... michael@0: * exports.bar = // ... michael@0: * michael@0: * michael@0: * Note #1: michael@0: * Properties |fileName| and |stack| of errors triggered from a module michael@0: * contain file names that do not correspond to human-readable module paths. michael@0: * Human readers should rather use properties |moduleName| and |moduleStack|. michael@0: * michael@0: * Note #2: michael@0: * The current version of |require()| only accepts absolute URIs. michael@0: * michael@0: * Note #3: michael@0: * By opposition to some other module loader implementations, this module michael@0: * loader does not enforce separation of global objects. Consequently, if michael@0: * a module modifies a global object (e.g. |String.prototype|), all other michael@0: * modules in the same worker may be affected. michael@0: */ michael@0: michael@0: michael@0: (function(exports) { michael@0: "use strict"; michael@0: michael@0: if (exports.require) { michael@0: // Avoid double-imports michael@0: return; michael@0: } michael@0: michael@0: // Simple implementation of |require| michael@0: let require = (function() { michael@0: michael@0: /** michael@0: * Mapping from module paths to module exports. michael@0: * michael@0: * @keys {string} The absolute path to a module. michael@0: * @values {object} The |exports| objects for that module. michael@0: */ michael@0: let modules = new Map(); michael@0: michael@0: /** michael@0: * Mapping from object urls to module paths. michael@0: */ michael@0: let paths = { michael@0: /** michael@0: * @keys {string} The object url holding a module. michael@0: * @values {string} The absolute path to that module. michael@0: */ michael@0: _map: new Map(), michael@0: /** michael@0: * A regexp that may be used to search for all mapped paths. michael@0: */ michael@0: get regexp() { michael@0: if (this._regexp) { michael@0: return this._regexp; michael@0: } michael@0: let objectURLs = []; michael@0: for (let [objectURL, _] of this._map) { michael@0: objectURLs.push(objectURL); michael@0: } michael@0: return this._regexp = new RegExp(objectURLs.join("|"), "g"); michael@0: }, michael@0: _regexp: null, michael@0: /** michael@0: * Add a mapping from an object url to a path. michael@0: */ michael@0: set: function(url, path) { michael@0: this._regexp = null; // invalidate regexp michael@0: this._map.set(url, path); michael@0: }, michael@0: /** michael@0: * Get a mapping from an object url to a path. michael@0: */ michael@0: get: function(url) { michael@0: return this._map.get(url); michael@0: }, michael@0: /** michael@0: * Transform a string by replacing all the instances of objectURLs michael@0: * appearing in that string with the corresponding module path. michael@0: * michael@0: * This is used typically to translate exception stacks. michael@0: * michael@0: * @param {string} source A source string. michael@0: * @return {string} The same string as |source|, in which every occurrence michael@0: * of an objectURL registered in this object has been replaced with the michael@0: * corresponding module path. michael@0: */ michael@0: substitute: function(source) { michael@0: let map = this._map; michael@0: return source.replace(this.regexp, function(url) { michael@0: return map.get(url) || url; michael@0: }, "g"); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A human-readable version of |stack|. michael@0: * michael@0: * @type {string} michael@0: */ michael@0: Object.defineProperty(Error.prototype, "moduleStack", michael@0: { michael@0: get: function() { michael@0: return paths.substitute(this.stack); michael@0: } michael@0: }); michael@0: /** michael@0: * A human-readable version of |fileName|. michael@0: * michael@0: * @type {string} michael@0: */ michael@0: Object.defineProperty(Error.prototype, "moduleName", michael@0: { michael@0: get: function() { michael@0: return paths.substitute(this.fileName); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Import a module michael@0: * michael@0: * @param {string} path The path to the module. michael@0: * @return {*} An object containing the properties exported by the module. michael@0: */ michael@0: return function require(path) { michael@0: if (typeof path != "string" || path.indexOf("://") == -1) { michael@0: throw new TypeError("The argument to require() must be a string uri, got " + path); michael@0: } michael@0: // Automatically add ".js" if there is no extension michael@0: let uri; michael@0: if (path.lastIndexOf(".") <= path.lastIndexOf("/")) { michael@0: uri = path + ".js"; michael@0: } else { michael@0: uri = path; michael@0: } michael@0: michael@0: // Exports provided by the module michael@0: let exports = Object.create(null); michael@0: michael@0: // Identification of the module michael@0: let module = { michael@0: id: path, michael@0: uri: uri, michael@0: exports: exports michael@0: }; michael@0: michael@0: // Make module available immediately michael@0: // (necessary in case of circular dependencies) michael@0: if (modules.has(path)) { michael@0: return modules.get(path).exports; michael@0: } michael@0: modules.set(path, module); michael@0: michael@0: let name = ":" + path; michael@0: let objectURL; michael@0: try { michael@0: // Load source of module, synchronously michael@0: let xhr = new XMLHttpRequest(); michael@0: xhr.open("GET", uri, false); michael@0: xhr.responseType = "text"; michael@0: xhr.send(); michael@0: michael@0: michael@0: let source = xhr.responseText; michael@0: if (source == "") { michael@0: // There doesn't seem to be a better way to detect that the file couldn't be found michael@0: throw new Error("Could not find module " + path); michael@0: } michael@0: // From the source, build a function and an object URL. We michael@0: // avoid any newline at the start of the file to ensure that michael@0: // we do not mess up with line numbers. However, using object URLs michael@0: // messes up with stack traces in instances of Error(). michael@0: source = "require._tmpModules[\"" + name + "\"] = " + michael@0: "function(exports, require, module) {" + michael@0: source + michael@0: "\n}\n"; michael@0: let blob = new Blob( michael@0: [ michael@0: (new TextEncoder()).encode(source) michael@0: ], { michael@0: type: "application/javascript" michael@0: }); michael@0: objectURL = URL.createObjectURL(blob); michael@0: paths.set(objectURL, path); michael@0: importScripts(objectURL); michael@0: require._tmpModules[name].call(null, exports, require, module); michael@0: michael@0: } catch (ex) { michael@0: // Module loading has failed, exports should not be made available michael@0: // after all. michael@0: modules.delete(path); michael@0: throw ex; michael@0: } finally { michael@0: if (objectURL) { michael@0: // Clean up the object url as soon as possible. It will not be needed. michael@0: URL.revokeObjectURL(objectURL); michael@0: } michael@0: delete require._tmpModules[name]; michael@0: } michael@0: michael@0: Object.freeze(module.exports); michael@0: Object.freeze(module); michael@0: return module.exports; michael@0: }; michael@0: })(); michael@0: michael@0: /** michael@0: * An object used to hold temporarily the module constructors michael@0: * while they are being loaded. michael@0: * michael@0: * @keys {string} The path to the module, prefixed with ":". michael@0: * @values {function} A function wrapping the module. michael@0: */ michael@0: require._tmpModules = Object.create(null); michael@0: Object.freeze(require); michael@0: michael@0: Object.defineProperty(exports, "require", { michael@0: value: require, michael@0: enumerable: true, michael@0: configurable: false michael@0: }); michael@0: })(this);