diff -r 000000000000 -r 6474c204b198 addon-sdk/source/lib/toolkit/loader.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/addon-sdk/source/lib/toolkit/loader.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,918 @@ +/* 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/. */ + +;(function(id, factory) { // Module boilerplate :( + if (typeof(define) === 'function') { // RequireJS + define(factory); + } else if (typeof(require) === 'function') { // CommonJS + factory.call(this, require, exports, module); + } else if (~String(this).indexOf('BackstagePass')) { // JSM + this[factory.name] = {}; + factory(function require(uri) { + var imports = {}; + this['Components'].utils.import(uri, imports); + return imports; + }, this[factory.name], { uri: __URI__, id: id }); + this.EXPORTED_SYMBOLS = [factory.name]; + } else if (~String(this).indexOf('Sandbox')) { // Sandbox + factory(function require(uri) {}, this, { uri: __URI__, id: id }); + } else { // Browser or alike + var globals = this + factory(function require(id) { + return globals[id]; + }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id }); + } +}).call(this, 'loader', function Loader(require, exports, module) { + +'use strict'; + +module.metadata = { + "stability": "unstable" +}; + +const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, + results: Cr, manager: Cm } = Components; +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); +const { notifyObservers } = Cc['@mozilla.org/observer-service;1']. + getService(Ci.nsIObserverService); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {}); +const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm"); +const { join: pathJoin, normalize, dirname } = Cu.import("resource://gre/modules/osfile/ospath_unix.jsm"); + +// Define some shortcuts. +const bind = Function.call.bind(Function.bind); +const getOwnPropertyNames = Object.getOwnPropertyNames; +const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +const define = Object.defineProperties; +const prototypeOf = Object.getPrototypeOf; +const create = Object.create; +const keys = Object.keys; + +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"]; + +const COMPONENT_ERROR = '`Components` is not available in this context.\n' + + 'Functionality provided by Components may be available in an SDK\n' + + 'module: https://jetpack.mozillalabs.com/sdk/latest/docs/ \n\n' + + 'However, if you still need to import Components, you may use the\n' + + '`chrome` module\'s properties for shortcuts to Component properties:\n\n' + + 'Shortcuts: \n' + + ' Cc = Components' + '.classes \n' + + ' Ci = Components' + '.interfaces \n' + + ' Cu = Components' + '.utils \n' + + ' CC = Components' + '.Constructor \n' + + 'Example: \n' + + ' let { Cc, Ci } = require(\'chrome\');\n'; + +// Workaround for bug 674195. Freezing objects from other compartments fail, +// so we use `Object.freeze` from the same component instead. +function freeze(object) { + if (prototypeOf(object) === null) { + Object.freeze(object); + } + else { + prototypeOf(prototypeOf(object.isPrototypeOf)). + constructor. // `Object` from the owner compartment. + freeze(object); + } + return object; +} + +// Returns map of given `object`-s own property descriptors. +const descriptor = iced(function descriptor(object) { + let value = {}; + getOwnPropertyNames(object).forEach(function(name) { + value[name] = getOwnPropertyDescriptor(object, name) + }); + return value; +}); +exports.descriptor = descriptor; + +// Freeze important built-ins so they can't be used by untrusted code as a +// message passing channel. +freeze(Object); +freeze(Object.prototype); +freeze(Function); +freeze(Function.prototype); +freeze(Array); +freeze(Array.prototype); +freeze(String); +freeze(String.prototype); + +// This function takes `f` function sets it's `prototype` to undefined and +// freezes it. We need to do this kind of deep freeze with all the exposed +// functions so that untrusted code won't be able to use them a message +// passing channel. +function iced(f) { + if (!Object.isFrozen(f)) { + f.prototype = undefined; + } + return freeze(f); +} + +// Defines own properties of given `properties` object on the given +// target object overriding any existing property with a conflicting name. +// Returns `target` object. Note we only export this function because it's +// useful during loader bootstrap when other util modules can't be used & +// thats only case where this export should be used. +const override = iced(function override(target, source) { + let properties = descriptor(target) + let extension = descriptor(source || {}) + getOwnPropertyNames(extension).forEach(function(name) { + properties[name] = extension[name]; + }); + return define({}, properties); +}); +exports.override = override; + +function sourceURI(uri) { return String(uri).split(" -> ").pop(); } +exports.sourceURI = iced(sourceURI); + +function isntLoaderFrame(frame) { return frame.fileName !== module.uri } + +function parseURI(uri) { return String(uri).split(" -> ").pop(); } +exports.parseURI = parseURI; + +function parseStack(stack) { + let lines = String(stack).split("\n"); + return lines.reduce(function(frames, line) { + if (line) { + let atIndex = line.indexOf("@"); + let columnIndex = line.lastIndexOf(":"); + let lineIndex = line.lastIndexOf(":", columnIndex - 1); + let fileName = parseURI(line.slice(atIndex + 1, lineIndex)); + let lineNumber = parseInt(line.slice(lineIndex + 1, columnIndex)); + let columnNumber = parseInt(line.slice(columnIndex + 1)); + let name = line.slice(0, atIndex).split("(").shift(); + frames.unshift({ + fileName: fileName, + name: name, + lineNumber: lineNumber, + columnNumber: columnNumber + }); + } + return frames; + }, []); +} +exports.parseStack = parseStack; + +function serializeStack(frames) { + return frames.reduce(function(stack, frame) { + return frame.name + "@" + + frame.fileName + ":" + + frame.lineNumber + ":" + + frame.columnNumber + "\n" + + stack; + }, ""); +} +exports.serializeStack = serializeStack; + +function readURI(uri) { + let stream = NetUtil.newChannel(uri, 'UTF-8', null).open(); + let count = stream.available(); + let data = NetUtil.readInputStreamToString(stream, count, { + charset: 'UTF-8' + }); + + stream.close(); + + return data; +} + +// Combines all arguments into a resolved, normalized path +function join (...paths) { + let resolved = normalize(pathJoin(...paths)) + // OS.File `normalize` strips out the second slash in + // `resource://` or `chrome://`, and third slash in + // `file:///`, so we work around this + resolved = resolved.replace(/^resource\:\/([^\/])/, 'resource://$1'); + resolved = resolved.replace(/^file\:\/([^\/])/, 'file:///$1'); + resolved = resolved.replace(/^chrome\:\/([^\/])/, 'chrome://$1'); + return resolved; +} +exports.join = join; + +// Function takes set of options and returns a JS sandbox. Function may be +// passed set of options: +// - `name`: A string value which identifies the sandbox in about:memory. Will +// throw exception if omitted. +// - `principal`: String URI or `nsIPrincipal` for the sandbox. Defaults to +// system principal. +// - `prototype`: Ancestor for the sandbox that will be created. Defaults to +// `{}`. +// - `wantXrays`: A Boolean value indicating whether code outside the sandbox +// wants X-ray vision with respect to objects inside the sandbox. Defaults +// to `true`. +// - `sandbox`: A sandbox to share JS compartment with. If omitted new +// compartment will be created. +// - `metadata`: A metadata object associated with the sandbox. It should +// be JSON-serializable. +// For more details see: +// https://developer.mozilla.org/en/Components.utils.Sandbox +const Sandbox = iced(function Sandbox(options) { + // Normalize options and rename to match `Cu.Sandbox` expectations. + options = { + // Do not expose `Components` if you really need them (bad idea!) you + // still can expose via prototype. + wantComponents: false, + sandboxName: options.name, + principal: 'principal' in options ? options.principal : systemPrincipal, + wantXrays: 'wantXrays' in options ? options.wantXrays : true, + wantGlobalProperties: 'wantGlobalProperties' in options ? + options.wantGlobalProperties : [], + sandboxPrototype: 'prototype' in options ? options.prototype : {}, + invisibleToDebugger: 'invisibleToDebugger' in options ? + options.invisibleToDebugger : false, + metadata: 'metadata' in options ? options.metadata : {} + }; + + let sandbox = Cu.Sandbox(options.principal, options); + + // Each sandbox at creation gets set of own properties that will be shadowing + // ones from it's prototype. We override delete such `sandbox` properties + // to avoid shadowing. + delete sandbox.Iterator; + delete sandbox.Components; + delete sandbox.importFunction; + delete sandbox.debug; + + return sandbox; +}); +exports.Sandbox = Sandbox; + +// Evaluates code from the given `uri` into given `sandbox`. If +// `options.source` is passed, then that code is evaluated instead. +// Optionally following options may be given: +// - `options.encoding`: Source encoding, defaults to 'UTF-8'. +// - `options.line`: Line number to start count from for stack traces. +// Defaults to 1. +// - `options.version`: Version of JS used, defaults to '1.8'. +const evaluate = iced(function evaluate(sandbox, uri, options) { + let { source, line, version, encoding } = override({ + encoding: 'UTF-8', + line: 1, + version: '1.8', + source: null + }, options); + + return source ? Cu.evalInSandbox(source, sandbox, version, uri, line) + : loadSubScript(uri, sandbox, encoding); +}); +exports.evaluate = evaluate; + +// Populates `exports` of the given CommonJS `module` object, in the context +// of the given `loader` by evaluating code associated with it. +const load = iced(function load(loader, module) { + let { sandboxes, globals } = loader; + let require = Require(loader, module); + + // We expose set of properties defined by `CommonJS` specification via + // prototype of the sandbox. Also globals are deeper in the prototype + // chain so that each module has access to them as well. + let descriptors = descriptor({ + require: require, + module: module, + exports: module.exports, + get Components() { + // Expose `Components` property to throw error on usage with + // additional information + throw new ReferenceError(COMPONENT_ERROR); + } + }); + + let sandbox = sandboxes[module.uri] = Sandbox({ + name: module.uri, + prototype: create(globals, descriptors), + wantXrays: false, + wantGlobalProperties: module.id == "sdk/indexed-db" ? ["indexedDB"] : [], + invisibleToDebugger: loader.invisibleToDebugger, + metadata: { + addonID: loader.id, + URI: module.uri + } + }); + + try { + evaluate(sandbox, module.uri); + } catch (error) { + let { message, fileName, lineNumber } = error; + let stack = error.stack || Error().stack; + let frames = parseStack(stack).filter(isntLoaderFrame); + let toString = String(error); + let file = sourceURI(fileName); + + // Note that `String(error)` where error is from subscript loader does + // not puts `:` after `"Error"` unlike regular errors thrown by JS code. + // If there is a JS stack then this error has already been handled by an + // inner module load. + if (String(error) === "Error opening input stream (invalid filename?)") { + let caller = frames.slice(0).pop(); + fileName = caller.fileName; + lineNumber = caller.lineNumber; + message = "Module `" + module.id + "` is not found at " + module.uri; + toString = message; + } + // Workaround for a Bug 910653. Errors thrown by subscript loader + // do not include `stack` field and above created error won't have + // fileName or lineNumber of the module being loaded, so we ensure + // it does. + else if (frames[frames.length - 1].fileName !== file) { + frames.push({ fileName: file, lineNumber: lineNumber, name: "" }); + } + + let prototype = typeof(error) === "object" ? error.constructor.prototype : + Error.prototype; + + throw create(prototype, { + message: { value: message, writable: true, configurable: true }, + fileName: { value: fileName, writable: true, configurable: true }, + lineNumber: { value: lineNumber, writable: true, configurable: true }, + stack: { value: serializeStack(frames), writable: true, configurable: true }, + toString: { value: function() toString, writable: true, configurable: true }, + }); + } + + if (module.exports && typeof(module.exports) === 'object') + freeze(module.exports); + + return module; +}); +exports.load = load; + +// Utility function to normalize module `uri`s so they have `.js` extension. +function normalizeExt (uri) { + return isJSURI(uri) ? uri : + isJSONURI(uri) ? uri : + isJSMURI(uri) ? uri : + uri + '.js'; +} + +// Strips `rootURI` from `string` -- used to remove absolute resourceURI +// from a relative path +function stripBase (rootURI, string) { + return string.replace(rootURI, './'); +} + +// Utility function to join paths. In common case `base` is a +// `requirer.uri` but in some cases it may be `baseURI`. In order to +// avoid complexity we require `baseURI` with a trailing `/`. +const resolve = iced(function resolve(id, base) { + if (!isRelative(id)) return id; + let basePaths = base.split('/'); + // Pop the last element in the `base`, because it is from a + // relative file + // '../mod.js' from '/path/to/file.js' should resolve to '/path/mod.js' + basePaths.pop(); + if (!basePaths.length) + return normalize(id); + let resolved = join(basePaths.join('/'), id); + + // Joining and normalizing removes the './' from relative files. + // We need to ensure the resolution still has the root + if (isRelative(base)) + resolved = './' + resolved; + + return resolved; +}); +exports.resolve = resolve; + +// Node-style module lookup +// Takes an id and path and attempts to load a file using node's resolving +// algorithm. +// `id` should already be resolved relatively at this point. +// http://nodejs.org/api/modules.html#modules_all_together +const nodeResolve = iced(function nodeResolve(id, requirer, { manifest, rootURI }) { + // Resolve again + id = exports.resolve(id, requirer); + + // we assume that extensions are correct, i.e., a directory doesnt't have '.js' + // and a js file isn't named 'file.json.js' + + let fullId = join(rootURI, id); + + let resolvedPath; + if (resolvedPath = loadAsFile(fullId)) + return stripBase(rootURI, resolvedPath); + else if (resolvedPath = loadAsDirectory(fullId)) + return stripBase(rootURI, resolvedPath); + // If manifest has dependencies, attempt to look up node modules + // in the `dependencies` list + else if (manifest.dependencies) { + let dirs = getNodeModulePaths(dirname(join(rootURI, requirer))).map(dir => join(dir, id)); + for (let i = 0; i < dirs.length; i++) { + if (resolvedPath = loadAsFile(dirs[i])) + return stripBase(rootURI, resolvedPath); + if (resolvedPath = loadAsDirectory(dirs[i])) + return stripBase(rootURI, resolvedPath); + } + } + + // We would not find lookup for things like `sdk/tabs`, as that's part of + // the alias mapping. If during `generateMap`, the runtime lookup resolves + // with `resolveURI` -- if during runtime, then `resolve` will throw. + return void 0; +}); +exports.nodeResolve = nodeResolve; + +// Attempts to load `path` and then `path.js` +// Returns `path` with valid file, or `undefined` otherwise +function loadAsFile (path) { + let found; + + // As per node's loader spec, + // we first should try and load 'path' (with no extension) + // before trying 'path.js'. We will not support this feature + // due to performance, but may add it if necessary for adoption. + try { + // Append '.js' to path name unless it's another support filetype + path = normalizeExt(path); + readURI(path); + found = path; + } catch (e) {} + + return found; +} + +// Attempts to load `path/package.json`'s `main` entry, +// followed by `path/index.js`, or `undefined` otherwise +function loadAsDirectory (path) { + let found; + try { + // If `path/package.json` exists, parse the `main` entry + // and attempt to load that + let main = getManifestMain(JSON.parse(readURI(path + '/package.json'))); + if (main != null) { + let tmpPath = join(path, main); + if (found = loadAsFile(tmpPath)) + return found + } + try { + let tmpPath = path + '/index.js'; + readURI(tmpPath); + return tmpPath; + } catch (e) {} + } catch (e) { + try { + let tmpPath = path + '/index.js'; + readURI(tmpPath); + return tmpPath; + } catch (e) {} + } + return void 0; +} + +// From `resolve` module +// https://github.com/substack/node-resolve/blob/master/lib/node-modules-paths.js +function getNodeModulePaths (start) { + // Configurable in node -- do we need this to be configurable? + let moduleDir = 'node_modules'; + + let parts = start.split('/'); + let dirs = []; + for (let i = parts.length - 1; i >= 0; i--) { + if (parts[i] === moduleDir) continue; + let dir = join(parts.slice(0, i + 1).join('/'), moduleDir); + dirs.push(dir); + } + return dirs; +} + + +function addTrailingSlash (path) { + return !path ? null : !path.endsWith('/') ? path + '/' : path; +} + +// Utility function to determine of module id `name` is a built in +// module in node (fs, path, etc.); +function isNodeModule (name) { + return !!~NODE_MODULES.indexOf(name); +} + +// Make mapping array that is sorted from longest path to shortest path +// to allow overlays. Used by `resolveURI`, returns an array +function sortPaths (paths) { + return keys(paths). + sort(function(a, b) { return b.length - a.length }). + map(function(path) { return [ path, paths[path] ] }); +} + +const resolveURI = iced(function resolveURI(id, mapping) { + let count = mapping.length, index = 0; + + // Do not resolve if already a resource URI + if (isResourceURI(id)) return normalizeExt(id); + + while (index < count) { + let [ path, uri ] = mapping[index ++]; + if (id.indexOf(path) === 0) + return normalizeExt(id.replace(path, uri)); + } + return void 0; // otherwise we raise a warning, see bug 910304 +}); +exports.resolveURI = resolveURI; + +// Creates version of `require` that will be exposed to the given `module` +// in the context of the given `loader`. Each module gets own limited copy +// of `require` that is allowed to load only a modules that are associated +// with it during link time. +const Require = iced(function Require(loader, requirer) { + let { + modules, mapping, resolve, load, manifest, rootURI, isNative, requireMap + } = loader; + + function require(id) { + if (!id) // Throw if `id` is not passed. + throw Error('you must provide a module name when calling require() from ' + + requirer.id, requirer.uri); + + let requirement; + let uri; + + // TODO should get native Firefox modules before doing node-style lookups + // to save on loading time + + if (isNative) { + // If a requireMap is available from `generateMap`, use that to + // immediately resolve the node-style mapping. + if (requireMap && requireMap[requirer.id]) + requirement = requireMap[requirer.id][id]; + + // For native modules, we want to check if it's a module specified + // in 'modules', like `chrome`, or `@loader` -- if it exists, + // just set the uri to skip resolution + if (!requirement && modules[id]) + uri = requirement = id; + + // If no requireMap was provided, or resolution not found in + // the requireMap, and not a npm dependency, attempt a runtime lookup + if (!requirement && !isNodeModule(id)) { + // If `isNative` defined, this is using the new, native-style + // loader, not cuddlefish, so lets resolve using node's algorithm + // and get back a path that needs to be resolved via paths mapping + // in `resolveURI` + requirement = resolve(id, requirer.id, { + manifest: manifest, + rootURI: rootURI + }); + } + + // If not found in the map, not a node module, and wasn't able to be + // looked up, it's something + // found in the paths most likely, like `sdk/tabs`, which should + // be resolved relatively if needed using traditional resolve + if (!requirement) { + requirement = isRelative(id) ? exports.resolve(id, requirer.id) : id; + } + } else { + // Resolve `id` to its requirer if it's relative. + requirement = requirer ? resolve(id, requirer.id) : id; + } + + // Resolves `uri` of module using loaders resolve function. + uri = uri || resolveURI(requirement, mapping); + + if (!uri) // Throw if `uri` can not be resolved. + throw Error('Module: Can not resolve "' + id + '" module required by ' + + requirer.id + ' located at ' + requirer.uri, requirer.uri); + + let module = null; + // If module is already cached by loader then just use it. + if (uri in modules) { + module = modules[uri]; + } + else if (isJSMURI(uri)) { + module = modules[uri] = Module(requirement, uri); + module.exports = Cu.import(uri, {}); + freeze(module); + } + else if (isJSONURI(uri)) { + let data; + + // First attempt to load and parse json uri + // ex: `test.json` + // If that doesn't exist, check for `test.json.js` + // for node parity + try { + data = JSON.parse(readURI(uri)); + module = modules[uri] = Module(requirement, uri); + module.exports = data; + freeze(module); + } + catch (err) { + // If error thrown from JSON parsing, throw that, do not + // attempt to find .json.js file + if (err && /JSON\.parse/.test(err.message)) + throw err; + uri = uri + '.js'; + } + } + // If not yet cached, load and cache it. + // We also freeze module to prevent it from further changes + // at runtime. + if (!(uri in modules)) { + // Many of the loader's functionalities are dependent + // on modules[uri] being set before loading, so we set it and + // remove it if we have any errors. + module = modules[uri] = Module(requirement, uri); + try { + freeze(load(loader, module)); + } + catch (e) { + // Clear out modules cache so we can throw on a second invalid require + delete modules[uri]; + // Also clear out the Sandbox that was created + delete loader.sandboxes[uri]; + throw e; + } + } + + return module.exports; + } + // Make `require.main === module` evaluate to true in main module scope. + require.main = loader.main === requirer ? requirer : undefined; + return iced(require); +}); +exports.Require = Require; + +const main = iced(function main(loader, id) { + // If no main entry provided, and native loader is used, + // read the entry in the manifest + if (!id && loader.isNative) + id = getManifestMain(loader.manifest); + let uri = resolveURI(id, loader.mapping); + let module = loader.main = loader.modules[uri] = Module(id, uri); + return loader.load(loader, module).exports; +}); +exports.main = main; + +// Makes module object that is made available to CommonJS modules when they +// are evaluated, along with `exports` and `require`. +const Module = iced(function Module(id, uri) { + return create(null, { + id: { enumerable: true, value: id }, + exports: { enumerable: true, writable: true, value: create(null) }, + uri: { value: uri } + }); +}); +exports.Module = Module; + +// Takes `loader`, and unload `reason` string and notifies all observers that +// they should cleanup after them-self. +const unload = iced(function unload(loader, reason) { + // subject is a unique object created per loader instance. + // This allows any code to cleanup on loader unload regardless of how + // it was loaded. To handle unload for specific loader subject may be + // asserted against loader.destructor or require('@loader/unload') + // Note: We don not destroy loader's module cache or sandboxes map as + // some modules may do cleanup in subsequent turns of event loop. Destroying + // cache may cause module identity problems in such cases. + let subject = { wrappedJSObject: loader.destructor }; + notifyObservers(subject, 'sdk:loader:destroy', reason); +}); +exports.unload = unload; + +// Function makes new loader that can be used to load CommonJS modules +// described by a given `options.manifest`. Loader takes following options: +// - `globals`: Optional map of globals, that all module scopes will inherit +// from. Map is also exposed under `globals` property of the returned loader +// so it can be extended further later. Defaults to `{}`. +// - `modules` Optional map of built-in module exports mapped by module id. +// These modules will incorporated into module cache. Each module will be +// frozen. +// - `resolve` Optional module `id` resolution function. If given it will be +// used to resolve module URIs, by calling it with require term, requirer +// module object (that has `uri` property) and `baseURI` of the loader. +// If `resolve` does not returns `uri` string exception will be thrown by +// an associated `require` call. +const Loader = iced(function Loader(options) { + let { + modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative + } = override({ + paths: {}, + modules: {}, + globals: { + console: console + }, + resolve: options.isNative ? + exports.nodeResolve : + exports.resolve, + }, options); + + // We create an identity object that will be dispatched on an unload + // event as subject. This way unload listeners will be able to assert + // which loader is unloaded. Please note that we intentionally don't + // use `loader` as subject to prevent a loader access leakage through + // observer notifications. + let destructor = freeze(create(null)); + + let mapping = sortPaths(paths); + + // Define pseudo modules. + modules = override({ + '@loader/unload': destructor, + '@loader/options': options, + 'chrome': { Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm, + CC: bind(CC, Components), components: Components, + // `ChromeWorker` has to be inject in loader global scope. + // It is done by bootstrap.js:loadSandbox for the SDK. + ChromeWorker: ChromeWorker + } + }, modules); + + modules = keys(modules).reduce(function(result, id) { + // We resolve `uri` from `id` since modules are cached by `uri`. + let uri = resolveURI(id, mapping); + // In native loader, the mapping will not contain values for + // pseudomodules -- store them as their ID rather than the URI + if (isNative && !uri) + uri = id; + let module = Module(id, uri); + module.exports = freeze(modules[id]); + result[uri] = freeze(module); + return result; + }, {}); + + // Loader object is just a representation of a environment + // state. We freeze it and mark make it's properties non-enumerable + // as they are pure implementation detail that no one should rely upon. + let returnObj = { + destructor: { enumerable: false, value: destructor }, + globals: { enumerable: false, value: globals }, + mapping: { enumerable: false, value: mapping }, + // Map of module objects indexed by module URIs. + modules: { enumerable: false, value: modules }, + // Map of module sandboxes indexed by module URIs. + sandboxes: { enumerable: false, value: {} }, + resolve: { enumerable: false, value: resolve }, + // ID of the addon, if provided. + id: { enumerable: false, value: options.id }, + // Whether the modules loaded should be ignored by the debugger + invisibleToDebugger: { enumerable: false, + value: options.invisibleToDebugger || false }, + load: { enumerable: false, value: options.load || load }, + // Main (entry point) module, it can be set only once, since loader + // instance can have only one main module. + main: new function() { + let main; + return { + enumerable: false, + get: function() { return main; }, + // Only set main if it has not being set yet! + set: function(module) { main = main || module; } + } + } + }; + + if (isNative) { + returnObj.isNative = { enumerable: false, value: true }; + returnObj.manifest = { enumerable: false, value: manifest }; + returnObj.requireMap = { enumerable: false, value: requireMap }; + returnObj.rootURI = { enumerable: false, value: addTrailingSlash(rootURI) }; + } + + return freeze(create(null, returnObj)); +}); +exports.Loader = Loader; + +let isJSONURI = uri => uri.substr(-5) === '.json'; +let isJSMURI = uri => uri.substr(-4) === '.jsm'; +let isJSURI = uri => uri.substr(-3) === '.js'; +let isResourceURI = uri => uri.substr(0, 11) === 'resource://'; +let isRelative = id => id[0] === '.' + +const generateMap = iced(function generateMap(options, callback) { + let { rootURI, resolve, paths } = override({ + paths: {}, + resolve: exports.nodeResolve + }, options); + + rootURI = addTrailingSlash(rootURI); + + let manifest; + let manifestURI = join(rootURI, 'package.json'); + + if (rootURI) + manifest = JSON.parse(readURI(manifestURI)); + else + throw new Error('No `rootURI` given to generate map'); + + let main = getManifestMain(manifest); + + findAllModuleIncludes(main, { + resolve: resolve, + manifest: manifest, + rootURI: rootURI + }, {}, callback); + +}); +exports.generateMap = generateMap; + +// Default `main` entry to './index.js' and ensure is relative, +// since node allows 'lib/index.js' without relative `./` +function getManifestMain (manifest) { + let main = manifest.main || './index.js'; + return isRelative(main) ? main : './' + main; +} + +function findAllModuleIncludes (uri, options, results, callback) { + let { resolve, manifest, rootURI } = options; + results = results || {}; + + // Abort if JSON or JSM + if (isJSONURI(uri) || isJSMURI(uri)) { + callback(results); + return void 0; + } + + findModuleIncludes(join(rootURI, uri), modules => { + // If no modules are included in the file, just call callback immediately + if (!modules.length) { + callback(results); + return void 0; + } + + results[uri] = modules.reduce((agg, mod) => { + let resolved = resolve(mod, uri, { manifest: manifest, rootURI: rootURI }); + + // If resolution found, store the resolution; otherwise, + // skip storing it as runtime lookup will handle this + if (!resolved) + return agg; + agg[mod] = resolved; + return agg; + }, {}); + + let includes = keys(results[uri]); + let count = 0; + let subcallback = () => { if (++count >= includes.length) callback(results) }; + includes.map(id => { + let moduleURI = results[uri][id]; + if (!results[moduleURI]) + findAllModuleIncludes(moduleURI, options, results, subcallback); + else + subcallback(); + }); + }); +} + +// From Substack's detector +// https://github.com/substack/node-detective +// +// Given a resource URI or source, return an array of strings passed into +// the require statements from the source +function findModuleIncludes (uri, callback) { + let src = isResourceURI(uri) ? readURI(uri) : uri; + let modules = []; + + walk(src, function (node) { + if (isRequire(node)) + modules.push(node.arguments[0].value); + }); + + callback(modules); +} + +function walk (src, callback) { + let nodes = Reflect.parse(src); + traverse(nodes, callback); +} + +function traverse (node, cb) { + if (Array.isArray(node)) { + node.map(x => { + if (x != null) { + x.parent = node; + traverse(x, cb); + } + }); + } + else if (node && typeof node === 'object') { + cb(node); + keys(node).map(key => { + if (key === 'parent' || !node[key]) return; + node[key].parent = node; + traverse(node[key], cb); + }); + } +} + +// From Substack's detector +// https://github.com/substack/node-detective +// Check an AST node to see if its a require statement. +// A modification added to only evaluate to true if it actually +// has a value being passed in as an argument +function isRequire (node) { + var c = node.callee; + return c + && node.type === 'CallExpression' + && c.type === 'Identifier' + && c.name === 'require' + && node.arguments.length + && node.arguments[0].type === 'Literal'; +} + +}); +