addon-sdk/source/lib/toolkit/loader.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 ;(function(id, factory) { // Module boilerplate :(
michael@0 6 if (typeof(define) === 'function') { // RequireJS
michael@0 7 define(factory);
michael@0 8 } else if (typeof(require) === 'function') { // CommonJS
michael@0 9 factory.call(this, require, exports, module);
michael@0 10 } else if (~String(this).indexOf('BackstagePass')) { // JSM
michael@0 11 this[factory.name] = {};
michael@0 12 factory(function require(uri) {
michael@0 13 var imports = {};
michael@0 14 this['Components'].utils.import(uri, imports);
michael@0 15 return imports;
michael@0 16 }, this[factory.name], { uri: __URI__, id: id });
michael@0 17 this.EXPORTED_SYMBOLS = [factory.name];
michael@0 18 } else if (~String(this).indexOf('Sandbox')) { // Sandbox
michael@0 19 factory(function require(uri) {}, this, { uri: __URI__, id: id });
michael@0 20 } else { // Browser or alike
michael@0 21 var globals = this
michael@0 22 factory(function require(id) {
michael@0 23 return globals[id];
michael@0 24 }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id });
michael@0 25 }
michael@0 26 }).call(this, 'loader', function Loader(require, exports, module) {
michael@0 27
michael@0 28 'use strict';
michael@0 29
michael@0 30 module.metadata = {
michael@0 31 "stability": "unstable"
michael@0 32 };
michael@0 33
michael@0 34 const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
michael@0 35 results: Cr, manager: Cm } = Components;
michael@0 36 const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
michael@0 37 const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1'].
michael@0 38 getService(Ci.mozIJSSubScriptLoader);
michael@0 39 const { notifyObservers } = Cc['@mozilla.org/observer-service;1'].
michael@0 40 getService(Ci.nsIObserverService);
michael@0 41 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
michael@0 42 const { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {});
michael@0 43 const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm");
michael@0 44 const { join: pathJoin, normalize, dirname } = Cu.import("resource://gre/modules/osfile/ospath_unix.jsm");
michael@0 45
michael@0 46 // Define some shortcuts.
michael@0 47 const bind = Function.call.bind(Function.bind);
michael@0 48 const getOwnPropertyNames = Object.getOwnPropertyNames;
michael@0 49 const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
michael@0 50 const define = Object.defineProperties;
michael@0 51 const prototypeOf = Object.getPrototypeOf;
michael@0 52 const create = Object.create;
michael@0 53 const keys = Object.keys;
michael@0 54
michael@0 55 const NODE_MODULES = ["assert", "buffer_ieee754", "buffer", "child_process", "cluster", "console", "constants", "crypto", "_debugger", "dgram", "dns", "domain", "events", "freelist", "fs", "http", "https", "_linklist", "module", "net", "os", "path", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "sys", "timers", "tls", "tty", "url", "util", "vm", "zlib"];
michael@0 56
michael@0 57 const COMPONENT_ERROR = '`Components` is not available in this context.\n' +
michael@0 58 'Functionality provided by Components may be available in an SDK\n' +
michael@0 59 'module: https://jetpack.mozillalabs.com/sdk/latest/docs/ \n\n' +
michael@0 60 'However, if you still need to import Components, you may use the\n' +
michael@0 61 '`chrome` module\'s properties for shortcuts to Component properties:\n\n' +
michael@0 62 'Shortcuts: \n' +
michael@0 63 ' Cc = Components' + '.classes \n' +
michael@0 64 ' Ci = Components' + '.interfaces \n' +
michael@0 65 ' Cu = Components' + '.utils \n' +
michael@0 66 ' CC = Components' + '.Constructor \n' +
michael@0 67 'Example: \n' +
michael@0 68 ' let { Cc, Ci } = require(\'chrome\');\n';
michael@0 69
michael@0 70 // Workaround for bug 674195. Freezing objects from other compartments fail,
michael@0 71 // so we use `Object.freeze` from the same component instead.
michael@0 72 function freeze(object) {
michael@0 73 if (prototypeOf(object) === null) {
michael@0 74 Object.freeze(object);
michael@0 75 }
michael@0 76 else {
michael@0 77 prototypeOf(prototypeOf(object.isPrototypeOf)).
michael@0 78 constructor. // `Object` from the owner compartment.
michael@0 79 freeze(object);
michael@0 80 }
michael@0 81 return object;
michael@0 82 }
michael@0 83
michael@0 84 // Returns map of given `object`-s own property descriptors.
michael@0 85 const descriptor = iced(function descriptor(object) {
michael@0 86 let value = {};
michael@0 87 getOwnPropertyNames(object).forEach(function(name) {
michael@0 88 value[name] = getOwnPropertyDescriptor(object, name)
michael@0 89 });
michael@0 90 return value;
michael@0 91 });
michael@0 92 exports.descriptor = descriptor;
michael@0 93
michael@0 94 // Freeze important built-ins so they can't be used by untrusted code as a
michael@0 95 // message passing channel.
michael@0 96 freeze(Object);
michael@0 97 freeze(Object.prototype);
michael@0 98 freeze(Function);
michael@0 99 freeze(Function.prototype);
michael@0 100 freeze(Array);
michael@0 101 freeze(Array.prototype);
michael@0 102 freeze(String);
michael@0 103 freeze(String.prototype);
michael@0 104
michael@0 105 // This function takes `f` function sets it's `prototype` to undefined and
michael@0 106 // freezes it. We need to do this kind of deep freeze with all the exposed
michael@0 107 // functions so that untrusted code won't be able to use them a message
michael@0 108 // passing channel.
michael@0 109 function iced(f) {
michael@0 110 if (!Object.isFrozen(f)) {
michael@0 111 f.prototype = undefined;
michael@0 112 }
michael@0 113 return freeze(f);
michael@0 114 }
michael@0 115
michael@0 116 // Defines own properties of given `properties` object on the given
michael@0 117 // target object overriding any existing property with a conflicting name.
michael@0 118 // Returns `target` object. Note we only export this function because it's
michael@0 119 // useful during loader bootstrap when other util modules can't be used &
michael@0 120 // thats only case where this export should be used.
michael@0 121 const override = iced(function override(target, source) {
michael@0 122 let properties = descriptor(target)
michael@0 123 let extension = descriptor(source || {})
michael@0 124 getOwnPropertyNames(extension).forEach(function(name) {
michael@0 125 properties[name] = extension[name];
michael@0 126 });
michael@0 127 return define({}, properties);
michael@0 128 });
michael@0 129 exports.override = override;
michael@0 130
michael@0 131 function sourceURI(uri) { return String(uri).split(" -> ").pop(); }
michael@0 132 exports.sourceURI = iced(sourceURI);
michael@0 133
michael@0 134 function isntLoaderFrame(frame) { return frame.fileName !== module.uri }
michael@0 135
michael@0 136 function parseURI(uri) { return String(uri).split(" -> ").pop(); }
michael@0 137 exports.parseURI = parseURI;
michael@0 138
michael@0 139 function parseStack(stack) {
michael@0 140 let lines = String(stack).split("\n");
michael@0 141 return lines.reduce(function(frames, line) {
michael@0 142 if (line) {
michael@0 143 let atIndex = line.indexOf("@");
michael@0 144 let columnIndex = line.lastIndexOf(":");
michael@0 145 let lineIndex = line.lastIndexOf(":", columnIndex - 1);
michael@0 146 let fileName = parseURI(line.slice(atIndex + 1, lineIndex));
michael@0 147 let lineNumber = parseInt(line.slice(lineIndex + 1, columnIndex));
michael@0 148 let columnNumber = parseInt(line.slice(columnIndex + 1));
michael@0 149 let name = line.slice(0, atIndex).split("(").shift();
michael@0 150 frames.unshift({
michael@0 151 fileName: fileName,
michael@0 152 name: name,
michael@0 153 lineNumber: lineNumber,
michael@0 154 columnNumber: columnNumber
michael@0 155 });
michael@0 156 }
michael@0 157 return frames;
michael@0 158 }, []);
michael@0 159 }
michael@0 160 exports.parseStack = parseStack;
michael@0 161
michael@0 162 function serializeStack(frames) {
michael@0 163 return frames.reduce(function(stack, frame) {
michael@0 164 return frame.name + "@" +
michael@0 165 frame.fileName + ":" +
michael@0 166 frame.lineNumber + ":" +
michael@0 167 frame.columnNumber + "\n" +
michael@0 168 stack;
michael@0 169 }, "");
michael@0 170 }
michael@0 171 exports.serializeStack = serializeStack;
michael@0 172
michael@0 173 function readURI(uri) {
michael@0 174 let stream = NetUtil.newChannel(uri, 'UTF-8', null).open();
michael@0 175 let count = stream.available();
michael@0 176 let data = NetUtil.readInputStreamToString(stream, count, {
michael@0 177 charset: 'UTF-8'
michael@0 178 });
michael@0 179
michael@0 180 stream.close();
michael@0 181
michael@0 182 return data;
michael@0 183 }
michael@0 184
michael@0 185 // Combines all arguments into a resolved, normalized path
michael@0 186 function join (...paths) {
michael@0 187 let resolved = normalize(pathJoin(...paths))
michael@0 188 // OS.File `normalize` strips out the second slash in
michael@0 189 // `resource://` or `chrome://`, and third slash in
michael@0 190 // `file:///`, so we work around this
michael@0 191 resolved = resolved.replace(/^resource\:\/([^\/])/, 'resource://$1');
michael@0 192 resolved = resolved.replace(/^file\:\/([^\/])/, 'file:///$1');
michael@0 193 resolved = resolved.replace(/^chrome\:\/([^\/])/, 'chrome://$1');
michael@0 194 return resolved;
michael@0 195 }
michael@0 196 exports.join = join;
michael@0 197
michael@0 198 // Function takes set of options and returns a JS sandbox. Function may be
michael@0 199 // passed set of options:
michael@0 200 // - `name`: A string value which identifies the sandbox in about:memory. Will
michael@0 201 // throw exception if omitted.
michael@0 202 // - `principal`: String URI or `nsIPrincipal` for the sandbox. Defaults to
michael@0 203 // system principal.
michael@0 204 // - `prototype`: Ancestor for the sandbox that will be created. Defaults to
michael@0 205 // `{}`.
michael@0 206 // - `wantXrays`: A Boolean value indicating whether code outside the sandbox
michael@0 207 // wants X-ray vision with respect to objects inside the sandbox. Defaults
michael@0 208 // to `true`.
michael@0 209 // - `sandbox`: A sandbox to share JS compartment with. If omitted new
michael@0 210 // compartment will be created.
michael@0 211 // - `metadata`: A metadata object associated with the sandbox. It should
michael@0 212 // be JSON-serializable.
michael@0 213 // For more details see:
michael@0 214 // https://developer.mozilla.org/en/Components.utils.Sandbox
michael@0 215 const Sandbox = iced(function Sandbox(options) {
michael@0 216 // Normalize options and rename to match `Cu.Sandbox` expectations.
michael@0 217 options = {
michael@0 218 // Do not expose `Components` if you really need them (bad idea!) you
michael@0 219 // still can expose via prototype.
michael@0 220 wantComponents: false,
michael@0 221 sandboxName: options.name,
michael@0 222 principal: 'principal' in options ? options.principal : systemPrincipal,
michael@0 223 wantXrays: 'wantXrays' in options ? options.wantXrays : true,
michael@0 224 wantGlobalProperties: 'wantGlobalProperties' in options ?
michael@0 225 options.wantGlobalProperties : [],
michael@0 226 sandboxPrototype: 'prototype' in options ? options.prototype : {},
michael@0 227 invisibleToDebugger: 'invisibleToDebugger' in options ?
michael@0 228 options.invisibleToDebugger : false,
michael@0 229 metadata: 'metadata' in options ? options.metadata : {}
michael@0 230 };
michael@0 231
michael@0 232 let sandbox = Cu.Sandbox(options.principal, options);
michael@0 233
michael@0 234 // Each sandbox at creation gets set of own properties that will be shadowing
michael@0 235 // ones from it's prototype. We override delete such `sandbox` properties
michael@0 236 // to avoid shadowing.
michael@0 237 delete sandbox.Iterator;
michael@0 238 delete sandbox.Components;
michael@0 239 delete sandbox.importFunction;
michael@0 240 delete sandbox.debug;
michael@0 241
michael@0 242 return sandbox;
michael@0 243 });
michael@0 244 exports.Sandbox = Sandbox;
michael@0 245
michael@0 246 // Evaluates code from the given `uri` into given `sandbox`. If
michael@0 247 // `options.source` is passed, then that code is evaluated instead.
michael@0 248 // Optionally following options may be given:
michael@0 249 // - `options.encoding`: Source encoding, defaults to 'UTF-8'.
michael@0 250 // - `options.line`: Line number to start count from for stack traces.
michael@0 251 // Defaults to 1.
michael@0 252 // - `options.version`: Version of JS used, defaults to '1.8'.
michael@0 253 const evaluate = iced(function evaluate(sandbox, uri, options) {
michael@0 254 let { source, line, version, encoding } = override({
michael@0 255 encoding: 'UTF-8',
michael@0 256 line: 1,
michael@0 257 version: '1.8',
michael@0 258 source: null
michael@0 259 }, options);
michael@0 260
michael@0 261 return source ? Cu.evalInSandbox(source, sandbox, version, uri, line)
michael@0 262 : loadSubScript(uri, sandbox, encoding);
michael@0 263 });
michael@0 264 exports.evaluate = evaluate;
michael@0 265
michael@0 266 // Populates `exports` of the given CommonJS `module` object, in the context
michael@0 267 // of the given `loader` by evaluating code associated with it.
michael@0 268 const load = iced(function load(loader, module) {
michael@0 269 let { sandboxes, globals } = loader;
michael@0 270 let require = Require(loader, module);
michael@0 271
michael@0 272 // We expose set of properties defined by `CommonJS` specification via
michael@0 273 // prototype of the sandbox. Also globals are deeper in the prototype
michael@0 274 // chain so that each module has access to them as well.
michael@0 275 let descriptors = descriptor({
michael@0 276 require: require,
michael@0 277 module: module,
michael@0 278 exports: module.exports,
michael@0 279 get Components() {
michael@0 280 // Expose `Components` property to throw error on usage with
michael@0 281 // additional information
michael@0 282 throw new ReferenceError(COMPONENT_ERROR);
michael@0 283 }
michael@0 284 });
michael@0 285
michael@0 286 let sandbox = sandboxes[module.uri] = Sandbox({
michael@0 287 name: module.uri,
michael@0 288 prototype: create(globals, descriptors),
michael@0 289 wantXrays: false,
michael@0 290 wantGlobalProperties: module.id == "sdk/indexed-db" ? ["indexedDB"] : [],
michael@0 291 invisibleToDebugger: loader.invisibleToDebugger,
michael@0 292 metadata: {
michael@0 293 addonID: loader.id,
michael@0 294 URI: module.uri
michael@0 295 }
michael@0 296 });
michael@0 297
michael@0 298 try {
michael@0 299 evaluate(sandbox, module.uri);
michael@0 300 } catch (error) {
michael@0 301 let { message, fileName, lineNumber } = error;
michael@0 302 let stack = error.stack || Error().stack;
michael@0 303 let frames = parseStack(stack).filter(isntLoaderFrame);
michael@0 304 let toString = String(error);
michael@0 305 let file = sourceURI(fileName);
michael@0 306
michael@0 307 // Note that `String(error)` where error is from subscript loader does
michael@0 308 // not puts `:` after `"Error"` unlike regular errors thrown by JS code.
michael@0 309 // If there is a JS stack then this error has already been handled by an
michael@0 310 // inner module load.
michael@0 311 if (String(error) === "Error opening input stream (invalid filename?)") {
michael@0 312 let caller = frames.slice(0).pop();
michael@0 313 fileName = caller.fileName;
michael@0 314 lineNumber = caller.lineNumber;
michael@0 315 message = "Module `" + module.id + "` is not found at " + module.uri;
michael@0 316 toString = message;
michael@0 317 }
michael@0 318 // Workaround for a Bug 910653. Errors thrown by subscript loader
michael@0 319 // do not include `stack` field and above created error won't have
michael@0 320 // fileName or lineNumber of the module being loaded, so we ensure
michael@0 321 // it does.
michael@0 322 else if (frames[frames.length - 1].fileName !== file) {
michael@0 323 frames.push({ fileName: file, lineNumber: lineNumber, name: "" });
michael@0 324 }
michael@0 325
michael@0 326 let prototype = typeof(error) === "object" ? error.constructor.prototype :
michael@0 327 Error.prototype;
michael@0 328
michael@0 329 throw create(prototype, {
michael@0 330 message: { value: message, writable: true, configurable: true },
michael@0 331 fileName: { value: fileName, writable: true, configurable: true },
michael@0 332 lineNumber: { value: lineNumber, writable: true, configurable: true },
michael@0 333 stack: { value: serializeStack(frames), writable: true, configurable: true },
michael@0 334 toString: { value: function() toString, writable: true, configurable: true },
michael@0 335 });
michael@0 336 }
michael@0 337
michael@0 338 if (module.exports && typeof(module.exports) === 'object')
michael@0 339 freeze(module.exports);
michael@0 340
michael@0 341 return module;
michael@0 342 });
michael@0 343 exports.load = load;
michael@0 344
michael@0 345 // Utility function to normalize module `uri`s so they have `.js` extension.
michael@0 346 function normalizeExt (uri) {
michael@0 347 return isJSURI(uri) ? uri :
michael@0 348 isJSONURI(uri) ? uri :
michael@0 349 isJSMURI(uri) ? uri :
michael@0 350 uri + '.js';
michael@0 351 }
michael@0 352
michael@0 353 // Strips `rootURI` from `string` -- used to remove absolute resourceURI
michael@0 354 // from a relative path
michael@0 355 function stripBase (rootURI, string) {
michael@0 356 return string.replace(rootURI, './');
michael@0 357 }
michael@0 358
michael@0 359 // Utility function to join paths. In common case `base` is a
michael@0 360 // `requirer.uri` but in some cases it may be `baseURI`. In order to
michael@0 361 // avoid complexity we require `baseURI` with a trailing `/`.
michael@0 362 const resolve = iced(function resolve(id, base) {
michael@0 363 if (!isRelative(id)) return id;
michael@0 364 let basePaths = base.split('/');
michael@0 365 // Pop the last element in the `base`, because it is from a
michael@0 366 // relative file
michael@0 367 // '../mod.js' from '/path/to/file.js' should resolve to '/path/mod.js'
michael@0 368 basePaths.pop();
michael@0 369 if (!basePaths.length)
michael@0 370 return normalize(id);
michael@0 371 let resolved = join(basePaths.join('/'), id);
michael@0 372
michael@0 373 // Joining and normalizing removes the './' from relative files.
michael@0 374 // We need to ensure the resolution still has the root
michael@0 375 if (isRelative(base))
michael@0 376 resolved = './' + resolved;
michael@0 377
michael@0 378 return resolved;
michael@0 379 });
michael@0 380 exports.resolve = resolve;
michael@0 381
michael@0 382 // Node-style module lookup
michael@0 383 // Takes an id and path and attempts to load a file using node's resolving
michael@0 384 // algorithm.
michael@0 385 // `id` should already be resolved relatively at this point.
michael@0 386 // http://nodejs.org/api/modules.html#modules_all_together
michael@0 387 const nodeResolve = iced(function nodeResolve(id, requirer, { manifest, rootURI }) {
michael@0 388 // Resolve again
michael@0 389 id = exports.resolve(id, requirer);
michael@0 390
michael@0 391 // we assume that extensions are correct, i.e., a directory doesnt't have '.js'
michael@0 392 // and a js file isn't named 'file.json.js'
michael@0 393
michael@0 394 let fullId = join(rootURI, id);
michael@0 395
michael@0 396 let resolvedPath;
michael@0 397 if (resolvedPath = loadAsFile(fullId))
michael@0 398 return stripBase(rootURI, resolvedPath);
michael@0 399 else if (resolvedPath = loadAsDirectory(fullId))
michael@0 400 return stripBase(rootURI, resolvedPath);
michael@0 401 // If manifest has dependencies, attempt to look up node modules
michael@0 402 // in the `dependencies` list
michael@0 403 else if (manifest.dependencies) {
michael@0 404 let dirs = getNodeModulePaths(dirname(join(rootURI, requirer))).map(dir => join(dir, id));
michael@0 405 for (let i = 0; i < dirs.length; i++) {
michael@0 406 if (resolvedPath = loadAsFile(dirs[i]))
michael@0 407 return stripBase(rootURI, resolvedPath);
michael@0 408 if (resolvedPath = loadAsDirectory(dirs[i]))
michael@0 409 return stripBase(rootURI, resolvedPath);
michael@0 410 }
michael@0 411 }
michael@0 412
michael@0 413 // We would not find lookup for things like `sdk/tabs`, as that's part of
michael@0 414 // the alias mapping. If during `generateMap`, the runtime lookup resolves
michael@0 415 // with `resolveURI` -- if during runtime, then `resolve` will throw.
michael@0 416 return void 0;
michael@0 417 });
michael@0 418 exports.nodeResolve = nodeResolve;
michael@0 419
michael@0 420 // Attempts to load `path` and then `path.js`
michael@0 421 // Returns `path` with valid file, or `undefined` otherwise
michael@0 422 function loadAsFile (path) {
michael@0 423 let found;
michael@0 424
michael@0 425 // As per node's loader spec,
michael@0 426 // we first should try and load 'path' (with no extension)
michael@0 427 // before trying 'path.js'. We will not support this feature
michael@0 428 // due to performance, but may add it if necessary for adoption.
michael@0 429 try {
michael@0 430 // Append '.js' to path name unless it's another support filetype
michael@0 431 path = normalizeExt(path);
michael@0 432 readURI(path);
michael@0 433 found = path;
michael@0 434 } catch (e) {}
michael@0 435
michael@0 436 return found;
michael@0 437 }
michael@0 438
michael@0 439 // Attempts to load `path/package.json`'s `main` entry,
michael@0 440 // followed by `path/index.js`, or `undefined` otherwise
michael@0 441 function loadAsDirectory (path) {
michael@0 442 let found;
michael@0 443 try {
michael@0 444 // If `path/package.json` exists, parse the `main` entry
michael@0 445 // and attempt to load that
michael@0 446 let main = getManifestMain(JSON.parse(readURI(path + '/package.json')));
michael@0 447 if (main != null) {
michael@0 448 let tmpPath = join(path, main);
michael@0 449 if (found = loadAsFile(tmpPath))
michael@0 450 return found
michael@0 451 }
michael@0 452 try {
michael@0 453 let tmpPath = path + '/index.js';
michael@0 454 readURI(tmpPath);
michael@0 455 return tmpPath;
michael@0 456 } catch (e) {}
michael@0 457 } catch (e) {
michael@0 458 try {
michael@0 459 let tmpPath = path + '/index.js';
michael@0 460 readURI(tmpPath);
michael@0 461 return tmpPath;
michael@0 462 } catch (e) {}
michael@0 463 }
michael@0 464 return void 0;
michael@0 465 }
michael@0 466
michael@0 467 // From `resolve` module
michael@0 468 // https://github.com/substack/node-resolve/blob/master/lib/node-modules-paths.js
michael@0 469 function getNodeModulePaths (start) {
michael@0 470 // Configurable in node -- do we need this to be configurable?
michael@0 471 let moduleDir = 'node_modules';
michael@0 472
michael@0 473 let parts = start.split('/');
michael@0 474 let dirs = [];
michael@0 475 for (let i = parts.length - 1; i >= 0; i--) {
michael@0 476 if (parts[i] === moduleDir) continue;
michael@0 477 let dir = join(parts.slice(0, i + 1).join('/'), moduleDir);
michael@0 478 dirs.push(dir);
michael@0 479 }
michael@0 480 return dirs;
michael@0 481 }
michael@0 482
michael@0 483
michael@0 484 function addTrailingSlash (path) {
michael@0 485 return !path ? null : !path.endsWith('/') ? path + '/' : path;
michael@0 486 }
michael@0 487
michael@0 488 // Utility function to determine of module id `name` is a built in
michael@0 489 // module in node (fs, path, etc.);
michael@0 490 function isNodeModule (name) {
michael@0 491 return !!~NODE_MODULES.indexOf(name);
michael@0 492 }
michael@0 493
michael@0 494 // Make mapping array that is sorted from longest path to shortest path
michael@0 495 // to allow overlays. Used by `resolveURI`, returns an array
michael@0 496 function sortPaths (paths) {
michael@0 497 return keys(paths).
michael@0 498 sort(function(a, b) { return b.length - a.length }).
michael@0 499 map(function(path) { return [ path, paths[path] ] });
michael@0 500 }
michael@0 501
michael@0 502 const resolveURI = iced(function resolveURI(id, mapping) {
michael@0 503 let count = mapping.length, index = 0;
michael@0 504
michael@0 505 // Do not resolve if already a resource URI
michael@0 506 if (isResourceURI(id)) return normalizeExt(id);
michael@0 507
michael@0 508 while (index < count) {
michael@0 509 let [ path, uri ] = mapping[index ++];
michael@0 510 if (id.indexOf(path) === 0)
michael@0 511 return normalizeExt(id.replace(path, uri));
michael@0 512 }
michael@0 513 return void 0; // otherwise we raise a warning, see bug 910304
michael@0 514 });
michael@0 515 exports.resolveURI = resolveURI;
michael@0 516
michael@0 517 // Creates version of `require` that will be exposed to the given `module`
michael@0 518 // in the context of the given `loader`. Each module gets own limited copy
michael@0 519 // of `require` that is allowed to load only a modules that are associated
michael@0 520 // with it during link time.
michael@0 521 const Require = iced(function Require(loader, requirer) {
michael@0 522 let {
michael@0 523 modules, mapping, resolve, load, manifest, rootURI, isNative, requireMap
michael@0 524 } = loader;
michael@0 525
michael@0 526 function require(id) {
michael@0 527 if (!id) // Throw if `id` is not passed.
michael@0 528 throw Error('you must provide a module name when calling require() from '
michael@0 529 + requirer.id, requirer.uri);
michael@0 530
michael@0 531 let requirement;
michael@0 532 let uri;
michael@0 533
michael@0 534 // TODO should get native Firefox modules before doing node-style lookups
michael@0 535 // to save on loading time
michael@0 536
michael@0 537 if (isNative) {
michael@0 538 // If a requireMap is available from `generateMap`, use that to
michael@0 539 // immediately resolve the node-style mapping.
michael@0 540 if (requireMap && requireMap[requirer.id])
michael@0 541 requirement = requireMap[requirer.id][id];
michael@0 542
michael@0 543 // For native modules, we want to check if it's a module specified
michael@0 544 // in 'modules', like `chrome`, or `@loader` -- if it exists,
michael@0 545 // just set the uri to skip resolution
michael@0 546 if (!requirement && modules[id])
michael@0 547 uri = requirement = id;
michael@0 548
michael@0 549 // If no requireMap was provided, or resolution not found in
michael@0 550 // the requireMap, and not a npm dependency, attempt a runtime lookup
michael@0 551 if (!requirement && !isNodeModule(id)) {
michael@0 552 // If `isNative` defined, this is using the new, native-style
michael@0 553 // loader, not cuddlefish, so lets resolve using node's algorithm
michael@0 554 // and get back a path that needs to be resolved via paths mapping
michael@0 555 // in `resolveURI`
michael@0 556 requirement = resolve(id, requirer.id, {
michael@0 557 manifest: manifest,
michael@0 558 rootURI: rootURI
michael@0 559 });
michael@0 560 }
michael@0 561
michael@0 562 // If not found in the map, not a node module, and wasn't able to be
michael@0 563 // looked up, it's something
michael@0 564 // found in the paths most likely, like `sdk/tabs`, which should
michael@0 565 // be resolved relatively if needed using traditional resolve
michael@0 566 if (!requirement) {
michael@0 567 requirement = isRelative(id) ? exports.resolve(id, requirer.id) : id;
michael@0 568 }
michael@0 569 } else {
michael@0 570 // Resolve `id` to its requirer if it's relative.
michael@0 571 requirement = requirer ? resolve(id, requirer.id) : id;
michael@0 572 }
michael@0 573
michael@0 574 // Resolves `uri` of module using loaders resolve function.
michael@0 575 uri = uri || resolveURI(requirement, mapping);
michael@0 576
michael@0 577 if (!uri) // Throw if `uri` can not be resolved.
michael@0 578 throw Error('Module: Can not resolve "' + id + '" module required by ' +
michael@0 579 requirer.id + ' located at ' + requirer.uri, requirer.uri);
michael@0 580
michael@0 581 let module = null;
michael@0 582 // If module is already cached by loader then just use it.
michael@0 583 if (uri in modules) {
michael@0 584 module = modules[uri];
michael@0 585 }
michael@0 586 else if (isJSMURI(uri)) {
michael@0 587 module = modules[uri] = Module(requirement, uri);
michael@0 588 module.exports = Cu.import(uri, {});
michael@0 589 freeze(module);
michael@0 590 }
michael@0 591 else if (isJSONURI(uri)) {
michael@0 592 let data;
michael@0 593
michael@0 594 // First attempt to load and parse json uri
michael@0 595 // ex: `test.json`
michael@0 596 // If that doesn't exist, check for `test.json.js`
michael@0 597 // for node parity
michael@0 598 try {
michael@0 599 data = JSON.parse(readURI(uri));
michael@0 600 module = modules[uri] = Module(requirement, uri);
michael@0 601 module.exports = data;
michael@0 602 freeze(module);
michael@0 603 }
michael@0 604 catch (err) {
michael@0 605 // If error thrown from JSON parsing, throw that, do not
michael@0 606 // attempt to find .json.js file
michael@0 607 if (err && /JSON\.parse/.test(err.message))
michael@0 608 throw err;
michael@0 609 uri = uri + '.js';
michael@0 610 }
michael@0 611 }
michael@0 612 // If not yet cached, load and cache it.
michael@0 613 // We also freeze module to prevent it from further changes
michael@0 614 // at runtime.
michael@0 615 if (!(uri in modules)) {
michael@0 616 // Many of the loader's functionalities are dependent
michael@0 617 // on modules[uri] being set before loading, so we set it and
michael@0 618 // remove it if we have any errors.
michael@0 619 module = modules[uri] = Module(requirement, uri);
michael@0 620 try {
michael@0 621 freeze(load(loader, module));
michael@0 622 }
michael@0 623 catch (e) {
michael@0 624 // Clear out modules cache so we can throw on a second invalid require
michael@0 625 delete modules[uri];
michael@0 626 // Also clear out the Sandbox that was created
michael@0 627 delete loader.sandboxes[uri];
michael@0 628 throw e;
michael@0 629 }
michael@0 630 }
michael@0 631
michael@0 632 return module.exports;
michael@0 633 }
michael@0 634 // Make `require.main === module` evaluate to true in main module scope.
michael@0 635 require.main = loader.main === requirer ? requirer : undefined;
michael@0 636 return iced(require);
michael@0 637 });
michael@0 638 exports.Require = Require;
michael@0 639
michael@0 640 const main = iced(function main(loader, id) {
michael@0 641 // If no main entry provided, and native loader is used,
michael@0 642 // read the entry in the manifest
michael@0 643 if (!id && loader.isNative)
michael@0 644 id = getManifestMain(loader.manifest);
michael@0 645 let uri = resolveURI(id, loader.mapping);
michael@0 646 let module = loader.main = loader.modules[uri] = Module(id, uri);
michael@0 647 return loader.load(loader, module).exports;
michael@0 648 });
michael@0 649 exports.main = main;
michael@0 650
michael@0 651 // Makes module object that is made available to CommonJS modules when they
michael@0 652 // are evaluated, along with `exports` and `require`.
michael@0 653 const Module = iced(function Module(id, uri) {
michael@0 654 return create(null, {
michael@0 655 id: { enumerable: true, value: id },
michael@0 656 exports: { enumerable: true, writable: true, value: create(null) },
michael@0 657 uri: { value: uri }
michael@0 658 });
michael@0 659 });
michael@0 660 exports.Module = Module;
michael@0 661
michael@0 662 // Takes `loader`, and unload `reason` string and notifies all observers that
michael@0 663 // they should cleanup after them-self.
michael@0 664 const unload = iced(function unload(loader, reason) {
michael@0 665 // subject is a unique object created per loader instance.
michael@0 666 // This allows any code to cleanup on loader unload regardless of how
michael@0 667 // it was loaded. To handle unload for specific loader subject may be
michael@0 668 // asserted against loader.destructor or require('@loader/unload')
michael@0 669 // Note: We don not destroy loader's module cache or sandboxes map as
michael@0 670 // some modules may do cleanup in subsequent turns of event loop. Destroying
michael@0 671 // cache may cause module identity problems in such cases.
michael@0 672 let subject = { wrappedJSObject: loader.destructor };
michael@0 673 notifyObservers(subject, 'sdk:loader:destroy', reason);
michael@0 674 });
michael@0 675 exports.unload = unload;
michael@0 676
michael@0 677 // Function makes new loader that can be used to load CommonJS modules
michael@0 678 // described by a given `options.manifest`. Loader takes following options:
michael@0 679 // - `globals`: Optional map of globals, that all module scopes will inherit
michael@0 680 // from. Map is also exposed under `globals` property of the returned loader
michael@0 681 // so it can be extended further later. Defaults to `{}`.
michael@0 682 // - `modules` Optional map of built-in module exports mapped by module id.
michael@0 683 // These modules will incorporated into module cache. Each module will be
michael@0 684 // frozen.
michael@0 685 // - `resolve` Optional module `id` resolution function. If given it will be
michael@0 686 // used to resolve module URIs, by calling it with require term, requirer
michael@0 687 // module object (that has `uri` property) and `baseURI` of the loader.
michael@0 688 // If `resolve` does not returns `uri` string exception will be thrown by
michael@0 689 // an associated `require` call.
michael@0 690 const Loader = iced(function Loader(options) {
michael@0 691 let {
michael@0 692 modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative
michael@0 693 } = override({
michael@0 694 paths: {},
michael@0 695 modules: {},
michael@0 696 globals: {
michael@0 697 console: console
michael@0 698 },
michael@0 699 resolve: options.isNative ?
michael@0 700 exports.nodeResolve :
michael@0 701 exports.resolve,
michael@0 702 }, options);
michael@0 703
michael@0 704 // We create an identity object that will be dispatched on an unload
michael@0 705 // event as subject. This way unload listeners will be able to assert
michael@0 706 // which loader is unloaded. Please note that we intentionally don't
michael@0 707 // use `loader` as subject to prevent a loader access leakage through
michael@0 708 // observer notifications.
michael@0 709 let destructor = freeze(create(null));
michael@0 710
michael@0 711 let mapping = sortPaths(paths);
michael@0 712
michael@0 713 // Define pseudo modules.
michael@0 714 modules = override({
michael@0 715 '@loader/unload': destructor,
michael@0 716 '@loader/options': options,
michael@0 717 'chrome': { Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
michael@0 718 CC: bind(CC, Components), components: Components,
michael@0 719 // `ChromeWorker` has to be inject in loader global scope.
michael@0 720 // It is done by bootstrap.js:loadSandbox for the SDK.
michael@0 721 ChromeWorker: ChromeWorker
michael@0 722 }
michael@0 723 }, modules);
michael@0 724
michael@0 725 modules = keys(modules).reduce(function(result, id) {
michael@0 726 // We resolve `uri` from `id` since modules are cached by `uri`.
michael@0 727 let uri = resolveURI(id, mapping);
michael@0 728 // In native loader, the mapping will not contain values for
michael@0 729 // pseudomodules -- store them as their ID rather than the URI
michael@0 730 if (isNative && !uri)
michael@0 731 uri = id;
michael@0 732 let module = Module(id, uri);
michael@0 733 module.exports = freeze(modules[id]);
michael@0 734 result[uri] = freeze(module);
michael@0 735 return result;
michael@0 736 }, {});
michael@0 737
michael@0 738 // Loader object is just a representation of a environment
michael@0 739 // state. We freeze it and mark make it's properties non-enumerable
michael@0 740 // as they are pure implementation detail that no one should rely upon.
michael@0 741 let returnObj = {
michael@0 742 destructor: { enumerable: false, value: destructor },
michael@0 743 globals: { enumerable: false, value: globals },
michael@0 744 mapping: { enumerable: false, value: mapping },
michael@0 745 // Map of module objects indexed by module URIs.
michael@0 746 modules: { enumerable: false, value: modules },
michael@0 747 // Map of module sandboxes indexed by module URIs.
michael@0 748 sandboxes: { enumerable: false, value: {} },
michael@0 749 resolve: { enumerable: false, value: resolve },
michael@0 750 // ID of the addon, if provided.
michael@0 751 id: { enumerable: false, value: options.id },
michael@0 752 // Whether the modules loaded should be ignored by the debugger
michael@0 753 invisibleToDebugger: { enumerable: false,
michael@0 754 value: options.invisibleToDebugger || false },
michael@0 755 load: { enumerable: false, value: options.load || load },
michael@0 756 // Main (entry point) module, it can be set only once, since loader
michael@0 757 // instance can have only one main module.
michael@0 758 main: new function() {
michael@0 759 let main;
michael@0 760 return {
michael@0 761 enumerable: false,
michael@0 762 get: function() { return main; },
michael@0 763 // Only set main if it has not being set yet!
michael@0 764 set: function(module) { main = main || module; }
michael@0 765 }
michael@0 766 }
michael@0 767 };
michael@0 768
michael@0 769 if (isNative) {
michael@0 770 returnObj.isNative = { enumerable: false, value: true };
michael@0 771 returnObj.manifest = { enumerable: false, value: manifest };
michael@0 772 returnObj.requireMap = { enumerable: false, value: requireMap };
michael@0 773 returnObj.rootURI = { enumerable: false, value: addTrailingSlash(rootURI) };
michael@0 774 }
michael@0 775
michael@0 776 return freeze(create(null, returnObj));
michael@0 777 });
michael@0 778 exports.Loader = Loader;
michael@0 779
michael@0 780 let isJSONURI = uri => uri.substr(-5) === '.json';
michael@0 781 let isJSMURI = uri => uri.substr(-4) === '.jsm';
michael@0 782 let isJSURI = uri => uri.substr(-3) === '.js';
michael@0 783 let isResourceURI = uri => uri.substr(0, 11) === 'resource://';
michael@0 784 let isRelative = id => id[0] === '.'
michael@0 785
michael@0 786 const generateMap = iced(function generateMap(options, callback) {
michael@0 787 let { rootURI, resolve, paths } = override({
michael@0 788 paths: {},
michael@0 789 resolve: exports.nodeResolve
michael@0 790 }, options);
michael@0 791
michael@0 792 rootURI = addTrailingSlash(rootURI);
michael@0 793
michael@0 794 let manifest;
michael@0 795 let manifestURI = join(rootURI, 'package.json');
michael@0 796
michael@0 797 if (rootURI)
michael@0 798 manifest = JSON.parse(readURI(manifestURI));
michael@0 799 else
michael@0 800 throw new Error('No `rootURI` given to generate map');
michael@0 801
michael@0 802 let main = getManifestMain(manifest);
michael@0 803
michael@0 804 findAllModuleIncludes(main, {
michael@0 805 resolve: resolve,
michael@0 806 manifest: manifest,
michael@0 807 rootURI: rootURI
michael@0 808 }, {}, callback);
michael@0 809
michael@0 810 });
michael@0 811 exports.generateMap = generateMap;
michael@0 812
michael@0 813 // Default `main` entry to './index.js' and ensure is relative,
michael@0 814 // since node allows 'lib/index.js' without relative `./`
michael@0 815 function getManifestMain (manifest) {
michael@0 816 let main = manifest.main || './index.js';
michael@0 817 return isRelative(main) ? main : './' + main;
michael@0 818 }
michael@0 819
michael@0 820 function findAllModuleIncludes (uri, options, results, callback) {
michael@0 821 let { resolve, manifest, rootURI } = options;
michael@0 822 results = results || {};
michael@0 823
michael@0 824 // Abort if JSON or JSM
michael@0 825 if (isJSONURI(uri) || isJSMURI(uri)) {
michael@0 826 callback(results);
michael@0 827 return void 0;
michael@0 828 }
michael@0 829
michael@0 830 findModuleIncludes(join(rootURI, uri), modules => {
michael@0 831 // If no modules are included in the file, just call callback immediately
michael@0 832 if (!modules.length) {
michael@0 833 callback(results);
michael@0 834 return void 0;
michael@0 835 }
michael@0 836
michael@0 837 results[uri] = modules.reduce((agg, mod) => {
michael@0 838 let resolved = resolve(mod, uri, { manifest: manifest, rootURI: rootURI });
michael@0 839
michael@0 840 // If resolution found, store the resolution; otherwise,
michael@0 841 // skip storing it as runtime lookup will handle this
michael@0 842 if (!resolved)
michael@0 843 return agg;
michael@0 844 agg[mod] = resolved;
michael@0 845 return agg;
michael@0 846 }, {});
michael@0 847
michael@0 848 let includes = keys(results[uri]);
michael@0 849 let count = 0;
michael@0 850 let subcallback = () => { if (++count >= includes.length) callback(results) };
michael@0 851 includes.map(id => {
michael@0 852 let moduleURI = results[uri][id];
michael@0 853 if (!results[moduleURI])
michael@0 854 findAllModuleIncludes(moduleURI, options, results, subcallback);
michael@0 855 else
michael@0 856 subcallback();
michael@0 857 });
michael@0 858 });
michael@0 859 }
michael@0 860
michael@0 861 // From Substack's detector
michael@0 862 // https://github.com/substack/node-detective
michael@0 863 //
michael@0 864 // Given a resource URI or source, return an array of strings passed into
michael@0 865 // the require statements from the source
michael@0 866 function findModuleIncludes (uri, callback) {
michael@0 867 let src = isResourceURI(uri) ? readURI(uri) : uri;
michael@0 868 let modules = [];
michael@0 869
michael@0 870 walk(src, function (node) {
michael@0 871 if (isRequire(node))
michael@0 872 modules.push(node.arguments[0].value);
michael@0 873 });
michael@0 874
michael@0 875 callback(modules);
michael@0 876 }
michael@0 877
michael@0 878 function walk (src, callback) {
michael@0 879 let nodes = Reflect.parse(src);
michael@0 880 traverse(nodes, callback);
michael@0 881 }
michael@0 882
michael@0 883 function traverse (node, cb) {
michael@0 884 if (Array.isArray(node)) {
michael@0 885 node.map(x => {
michael@0 886 if (x != null) {
michael@0 887 x.parent = node;
michael@0 888 traverse(x, cb);
michael@0 889 }
michael@0 890 });
michael@0 891 }
michael@0 892 else if (node && typeof node === 'object') {
michael@0 893 cb(node);
michael@0 894 keys(node).map(key => {
michael@0 895 if (key === 'parent' || !node[key]) return;
michael@0 896 node[key].parent = node;
michael@0 897 traverse(node[key], cb);
michael@0 898 });
michael@0 899 }
michael@0 900 }
michael@0 901
michael@0 902 // From Substack's detector
michael@0 903 // https://github.com/substack/node-detective
michael@0 904 // Check an AST node to see if its a require statement.
michael@0 905 // A modification added to only evaluate to true if it actually
michael@0 906 // has a value being passed in as an argument
michael@0 907 function isRequire (node) {
michael@0 908 var c = node.callee;
michael@0 909 return c
michael@0 910 && node.type === 'CallExpression'
michael@0 911 && c.type === 'Identifier'
michael@0 912 && c.name === 'require'
michael@0 913 && node.arguments.length
michael@0 914 && node.arguments[0].type === 'Literal';
michael@0 915 }
michael@0 916
michael@0 917 });
michael@0 918

mercurial