summaryrefslogtreecommitdiffstats
path: root/toolkit/components/workerloader/require.js
blob: 246c4a18842aa32a7c42061f8b7692cf0325430f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/* 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/. */

/**
 * Implementation of a CommonJS module loader for workers.
 *
 * Use:
 * // in the .js file loaded by the constructor of the worker
 * importScripts("resource://gre/modules/workers/require.js");
 * let module = require("resource://gre/modules/worker/myModule.js");
 *
 * // in myModule.js
 * // Load dependencies
 * let SimpleTest = require("resource://gre/modules/workers/SimpleTest.js");
 * let Logger = require("resource://gre/modules/workers/Logger.js");
 *
 * // Define things that will not be exported
 * let someValue = // ...
 *
 * // Export symbols
 * exports.foo = // ...
 * exports.bar = // ...
 *
 *
 * Note #1:
 * Properties |fileName| and |stack| of errors triggered from a module
 * contain file names that do not correspond to human-readable module paths.
 * Human readers should rather use properties |moduleName| and |moduleStack|.
 *
 * Note #2:
 * By opposition to some other module loader implementations, this module
 * loader does not enforce separation of global objects. Consequently, if
 * a module modifies a global object (e.g. |String.prototype|), all other
 * modules in the same worker may be affected.
 */

/* global require */
/* exported require */

(function (exports) {
  "use strict";

  if (exports.require) {
    // Avoid double-imports
    return;
  }

  // Simple implementation of |require|
  let require = (function () {
    /**
     * Mapping from module URI to module exports.
     *
     * @keys {string} The absolute URI to a module.
     * @values {object} The |exports| objects for that module.
     */
    let modules = new Map();

    /**
     * A human-readable version of |stack|.
     *
     * @type {string}
     */
    Object.defineProperty(Error.prototype, "moduleStack", {
      get() {
        return this.stack;
      },
    });
    /**
     * A human-readable version of |fileName|.
     *
     * @type {string}
     */
    Object.defineProperty(Error.prototype, "moduleName", {
      get() {
        let match = this.stack.match(/\@(.*):.*:/);
        if (match) {
          return match[1];
        }
        return "(unknown module)";
      },
    });

    /**
     * Import a module
     *
     * @param {string} baseURL The URL of the modules from which we load a new module.
     *        This will be null for the first loaded module and so expect an absolute URI in path.
     *        Note that this first parameter is bound before `require` method is passed outside
     *        of this module. So that typical callsites will only path the second `path` parameter.
     * @param {string} path The path to the module.
     * @return {*} An object containing the properties exported by the module.
     */
    return function require(baseURL, path) {
      let startTime = performance.now();
      if (typeof path != "string") {
        throw new TypeError(
          "The argument to require() must be a string got " + path
        );
      }

      // Resolve relative paths
      if ((path.startsWith("./") || path.startsWith("../")) && baseURL) {
        path = new URL(path, baseURL).href;
      }

      if (!path.includes("://")) {
        throw new TypeError(
          "The argument to require() must be a string uri, got " + path
        );
      }
      // Automatically add ".js" if there is no extension
      let uri;
      if (path.lastIndexOf(".") <= path.lastIndexOf("/")) {
        uri = path + ".js";
      } else {
        uri = path;
      }

      // Exports provided by the module
      let exports = Object.create(null);

      // Identification of the module
      let module = {
        id: path,
        uri,
        exports,
      };

      // Make module available immediately
      // (necessary in case of circular dependencies)
      if (modules.has(uri)) {
        return modules.get(uri).exports;
      }
      modules.set(uri, module);

      try {
        // Load source of module, synchronously
        let xhr = new XMLHttpRequest();
        xhr.open("GET", uri, false);
        xhr.responseType = "text";
        xhr.send();

        let source = xhr.responseText;
        if (source == "") {
          // There doesn't seem to be a better way to detect that the file couldn't be found
          throw new Error("Could not find module " + path);
        }
        // Use `Function` to leave this scope, use `eval` to start the line
        // number from 1 that is observed by `source` and the error message
        // thrown from the module, and also use `arguments` for accessing
        // `source` and `uri` to avoid polluting the module's environment.
        let code = new Function(
          "exports",
          "require",
          "module",
          `eval(arguments[3] + "\\n//# sourceURL=" + arguments[4] + "\\n")`
        );
        code(exports, require.bind(null, path), module, source, uri);
      } catch (ex) {
        // Module loading has failed, exports should not be made available
        // after all.
        modules.delete(uri);
        throw ex;
      } finally {
        ChromeUtils.addProfilerMarker("require", startTime, path);
      }

      Object.freeze(module.exports);
      Object.freeze(module);
      return module.exports;
    };
  })();

  Object.freeze(require);

  Object.defineProperty(exports, "require", {
    value: require.bind(null, null),
    enumerable: true,
    configurable: false,
  });
})(this);