Bug 743359: Land the SDK module loader. r=mossop
authorIrakli Gozalishvili <rFobic@gmail.com>
Mon, 18 Jun 2012 10:03:02 +0100
changeset 96923 efa8bb276e3d
parent 96916 55d0b0de25f3
child 96924 a152c40b1fb1
push id22949
push useremorley@mozilla.com
push date2012-06-19 08:15 +0000
treeherdermozilla-central@19bfe36cace8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs743359
milestone16.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 743359: Land the SDK module loader. r=mossop
toolkit/addon-sdk/loader.js
new file mode 100644
--- /dev/null
+++ b/toolkit/addon-sdk/loader.js
@@ -0,0 +1,389 @@
+/* vim:set ts=2 sw=2 sts=2 expandtab */
+/* 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
+    factory(function require(uri) {
+      var imports = {};
+      this['Components'].utils.import(uri, imports);
+      return imports;
+    }, this, { uri: __URI__, id: id });
+    this.EXPORTED_SYMBOLS = Object.keys(this);
+  } 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(require, exports, module) {
+
+'use strict';
+
+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);
+
+// 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;
+
+// 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) {
+  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 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.
+// 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,
+    sandboxPrototype: 'prototype' in options ? options.prototype : {},
+    sameGroupAs: 'sandbox' in options ? options.sandbox : null
+  };
+
+  // Make `options.sameGroupAs` only if `sandbox` property is passed,
+  // otherwise `Cu.Sandbox` will throw.
+  if (!options.sameGroupAs)
+    delete options.sameGroupAs;
+
+  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);
+
+  let sandbox = sandboxes[module.uri] = Sandbox({
+    name: module.uri,
+    // Get an existing module sandbox, if any, so we can reuse its compartment
+    // when creating the new one to reduce memory consumption.
+    sandbox: sandboxes[keys(sandboxes).shift()],
+    // 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.
+    prototype: create(globals, descriptor({
+      require: require,
+      module: module,
+      exports: module.exports
+    })),
+    wantXrays: false
+  });
+
+  evaluate(sandbox, module.uri);
+
+  if (module.exports && typeof(module.exports) === 'object')
+    freeze(module.exports);
+
+  return module;
+});
+exports.load = load;
+
+// Utility function to check if id is relative.
+function isRelative(id) { return id[0] === '.'; }
+// Utility function to normalize module `uri`s so they have `.js` extension.
+function normalize(uri) { return uri.substr(-3) === '.js' ? uri : uri + '.js'; }
+// 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) {
+  let paths = id.split('/');
+  let result = base.split('/');
+  result.pop();
+  while (paths.length) {
+    let path = paths.shift();
+    if (path === '..')
+      result.pop();
+    else if (path !== '.')
+      result.push(path);
+  }
+  return result.join('/');
+});
+exports.resolve = resolve;
+
+const resolveURI = iced(function resolveURI(id, mapping) {
+  let count = mapping.length, index = 0;
+  while (index < count) {
+    let [ path, uri ] = mapping[index ++];
+    if (id.indexOf(path) === 0)
+      return normalize(id.replace(path, uri));
+  }
+});
+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 } = 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);
+
+    // Resolve `id` to its requirer if it's relative.
+    let requirement = requirer ? resolve(id, requirer.id) : id;
+
+
+    // Resolves `uri` of module using loaders resolve function.
+    let 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];
+    }
+    // Otherwise load and cache it. We also freeze module to prevent it from
+    // further changes at runtime.
+    else {
+      module = modules[uri] = Module(requirement, uri);
+      freeze(load(loader, module));
+    }
+
+    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) {
+  let module = Module(id, resolveURI(id, loader.mapping));
+  loader.main = module;
+  return 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 } = override({
+    paths: {},
+    modules: {},
+    globals: {},
+    resolve: 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));
+
+  // Make mapping array that is sorted from longest path to shortest path
+  // to allow overlays.
+  let mapping = keys(paths).
+    sort(function(a, b) { return b.length - a.length }).
+    map(function(path) { return [ path, paths[path] ] });
+
+  // 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 }
+  }, modules);
+
+  modules = keys(modules).reduce(function(result, id) {
+    // We resolve `uri` from `id` since modules are cached by `uri`.
+    let uri = resolveURI(id, mapping);
+    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.
+  return freeze(create(null, {
+    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 },
+    // 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; }
+      }
+    }
+  }));
+});
+exports.Loader = Loader;
+
+});