forked from mirrors/gecko-dev
		
	 0eb5f699ca
			
		
	
	
		0eb5f699ca
		
	
	
	
	
		
			
			Backed out changeset eb632d0b3a7b (bug 1622718) Backed out changeset 016b86b83932 (bug 1622718)
		
			
				
	
	
		
			570 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			570 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| /* exported Loader, resolveURI, Module, Require, unload */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| this.EXPORTED_SYMBOLS = ["Loader", "resolveURI", "Module", "Require", "unload"];
 | |
| 
 | |
| const { Constructor: CC, manager: Cm } = Components;
 | |
| const systemPrincipal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
 | |
| 
 | |
| const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| const { XPCOMUtils } = ChromeUtils.import(
 | |
|   "resource://gre/modules/XPCOMUtils.jsm"
 | |
| );
 | |
| const { normalize, dirname } = ChromeUtils.import(
 | |
|   "resource://gre/modules/osfile/ospath_unix.jsm"
 | |
| );
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   this,
 | |
|   "resProto",
 | |
|   "@mozilla.org/network/protocol;1?name=resource",
 | |
|   "nsIResProtocolHandler"
 | |
| );
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(
 | |
|   this,
 | |
|   "NetUtil",
 | |
|   "resource://gre/modules/NetUtil.jsm"
 | |
| );
 | |
| 
 | |
| // Define some shortcuts.
 | |
| const bind = Function.call.bind(Function.bind);
 | |
| function* getOwnIdentifiers(x) {
 | |
|   yield* Object.getOwnPropertyNames(x);
 | |
|   yield* Object.getOwnPropertySymbols(x);
 | |
| }
 | |
| 
 | |
| function isJSONURI(uri) {
 | |
|   return uri.endsWith(".json");
 | |
| }
 | |
| function isJSMURI(uri) {
 | |
|   return uri.endsWith(".jsm");
 | |
| }
 | |
| function isJSURI(uri) {
 | |
|   return uri.endsWith(".js");
 | |
| }
 | |
| const AbsoluteRegExp = /^(resource|chrome|file|jar):/;
 | |
| function isAbsoluteURI(uri) {
 | |
|   return AbsoluteRegExp.test(uri);
 | |
| }
 | |
| function isRelative(id) {
 | |
|   return id.startsWith(".");
 | |
| }
 | |
| 
 | |
| function readURI(uri) {
 | |
|   const nsURI = NetUtil.newURI(uri);
 | |
|   if (nsURI.scheme == "resource") {
 | |
|     // Resolve to a real URI, this will catch any obvious bad paths without
 | |
|     // logging assertions in debug builds, see bug 1135219
 | |
|     uri = resProto.resolveURI(nsURI);
 | |
|   }
 | |
| 
 | |
|   const stream = NetUtil.newChannel({
 | |
|     uri: NetUtil.newURI(uri, "UTF-8"),
 | |
|     loadUsingSystemPrincipal: true,
 | |
|   }).open();
 | |
|   const count = stream.available();
 | |
|   const data = NetUtil.readInputStreamToString(stream, count, {
 | |
|     charset: "UTF-8",
 | |
|   });
 | |
| 
 | |
|   stream.close();
 | |
| 
 | |
|   return data;
 | |
| }
 | |
| 
 | |
| // Combines all arguments into a resolved, normalized path
 | |
| function join(base, ...paths) {
 | |
|   // If this is an absolute URL, we need to normalize only the path portion,
 | |
|   // or we wind up stripping too many slashes and producing invalid URLs.
 | |
|   const match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec(
 | |
|     base
 | |
|   );
 | |
|   if (match) {
 | |
|     return match[1] + normalize([match[2], ...paths].join("/"));
 | |
|   }
 | |
| 
 | |
|   return normalize([base, ...paths].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.
 | |
| // - `prototype`: Ancestor for the sandbox that will be created. Defaults to
 | |
| //    `{}`.
 | |
| // - `invisibleToDebugger`: True, if the sandbox is part of the debugger
 | |
| //    implementation and should not be tracked by debugger API.
 | |
| // For more details see:
 | |
| // https://developer.mozilla.org/en/Components.utils.Sandbox
 | |
| 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,
 | |
|     sandboxPrototype: "prototype" in options ? options.prototype : {},
 | |
|     invisibleToDebugger:
 | |
|       "invisibleToDebugger" in options ? options.invisibleToDebugger : false,
 | |
|     freshCompartment: options.freshCompartment || false,
 | |
|   };
 | |
| 
 | |
|   const sandbox = Cu.Sandbox(systemPrincipal, options);
 | |
| 
 | |
|   delete sandbox.Components;
 | |
| 
 | |
|   return sandbox;
 | |
| }
 | |
| 
 | |
| // This allows defining some modules in AMD format while retaining CommonJS
 | |
| // compatibility with this loader by allowing the factory function to have
 | |
| // access to general CommonJS functions, e.g.
 | |
| //
 | |
| //   define(function(require, exports, module) {
 | |
| //     ... code ...
 | |
| //   });
 | |
| function define(factory) {
 | |
|   factory(this.require, this.exports, this.module);
 | |
| }
 | |
| 
 | |
| // Populates `exports` of the given CommonJS `module` object, in the context
 | |
| // of the given `loader` by evaluating code associated with it.
 | |
| function load(loader, module) {
 | |
|   const 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.
 | |
|   const properties = {
 | |
|     require,
 | |
|     module,
 | |
|     exports: module.exports,
 | |
|   };
 | |
|   if (loader.supportAMDModules) {
 | |
|     properties.define = define;
 | |
|   }
 | |
| 
 | |
|   // Create a new object in this sandbox, that will be used as
 | |
|   // the scope object for this particular module
 | |
|   const sandbox = new loader.sharedGlobalSandbox.Object();
 | |
|   Object.assign(sandbox, properties);
 | |
| 
 | |
|   const originalExports = module.exports;
 | |
|   try {
 | |
|     Services.scriptloader.loadSubScript(module.uri, sandbox);
 | |
|   } catch (error) {
 | |
|     // loadSubScript sometime throws string errors, which includes no stack.
 | |
|     // At least provide the current stack by re-throwing a real Error object.
 | |
|     if (typeof error == "string") {
 | |
|       if (
 | |
|         error.startsWith("Error creating URI") ||
 | |
|         error.startsWith("Error opening input stream (invalid filename?)")
 | |
|       ) {
 | |
|         throw new Error(
 | |
|           `Module \`${module.id}\` is not found at ${module.uri}`
 | |
|         );
 | |
|       }
 | |
|       throw new Error(
 | |
|         `Error while loading module \`${module.id}\` at ${module.uri}:` +
 | |
|           "\n" +
 | |
|           error
 | |
|       );
 | |
|     }
 | |
|     // Otherwise just re-throw everything else which should have a stack
 | |
|     throw error;
 | |
|   }
 | |
| 
 | |
|   // Only freeze the exports object if we created it ourselves. Modules
 | |
|   // which completely replace the exports object and still want it
 | |
|   // frozen need to freeze it themselves.
 | |
|   if (module.exports === originalExports) {
 | |
|     Object.freeze(module.exports);
 | |
|   }
 | |
| 
 | |
|   return module;
 | |
| }
 | |
| 
 | |
| // Utility function to normalize module `uri`s so they have `.js` extension.
 | |
| function normalizeExt(uri) {
 | |
|   if (isJSURI(uri) || isJSONURI(uri) || isJSMURI(uri)) {
 | |
|     return uri;
 | |
|   }
 | |
|   return 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 `/`.
 | |
| function resolve(id, base) {
 | |
|   if (!isRelative(id)) {
 | |
|     return id;
 | |
|   }
 | |
| 
 | |
|   const baseDir = dirname(base);
 | |
| 
 | |
|   let resolved;
 | |
|   if (baseDir.includes(":")) {
 | |
|     resolved = join(baseDir, id);
 | |
|   } else {
 | |
|     resolved = normalize(`${baseDir}/${id}`);
 | |
|   }
 | |
| 
 | |
|   // Joining and normalizing removes the "./" from relative files.
 | |
|   // We need to ensure the resolution still has the root
 | |
|   if (base.startsWith("./")) {
 | |
|     resolved = "./" + resolved;
 | |
|   }
 | |
| 
 | |
|   return resolved;
 | |
| }
 | |
| 
 | |
| function compileMapping(paths) {
 | |
|   // Make mapping array that is sorted from longest path to shortest path.
 | |
|   const mapping = Object.keys(paths)
 | |
|     .sort((a, b) => b.length - a.length)
 | |
|     .map(path => [path, paths[path]]);
 | |
| 
 | |
|   const PATTERN = /([.\\?+*(){}[\]^$])/g;
 | |
|   const escapeMeta = str => str.replace(PATTERN, "\\$1");
 | |
| 
 | |
|   const patterns = [];
 | |
|   paths = {};
 | |
| 
 | |
|   for (let [path, uri] of mapping) {
 | |
|     // Strip off any trailing slashes to make comparisons simpler
 | |
|     if (path.endsWith("/")) {
 | |
|       path = path.slice(0, -1);
 | |
|       uri = uri.replace(/\/+$/, "");
 | |
|     }
 | |
| 
 | |
|     paths[path] = uri;
 | |
| 
 | |
|     // We only want to match path segments explicitly. Examples:
 | |
|     // * "foo/bar" matches for "foo/bar"
 | |
|     // * "foo/bar" matches for "foo/bar/baz"
 | |
|     // * "foo/bar" does not match for "foo/bar-1"
 | |
|     // * "foo/bar/" does not match for "foo/bar"
 | |
|     // * "foo/bar/" matches for "foo/bar/baz"
 | |
|     //
 | |
|     // Check for an empty path, an exact match, or a substring match
 | |
|     // with the next character being a forward slash.
 | |
|     if (path == "") {
 | |
|       patterns.push("");
 | |
|     } else {
 | |
|       patterns.push(`${escapeMeta(path)}(?=$|/)`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const pattern = new RegExp(`^(${patterns.join("|")})`);
 | |
| 
 | |
|   // This will replace the longest matching path mapping at the start of
 | |
|   // the ID string with its mapped value.
 | |
|   return id => {
 | |
|     return id.replace(pattern, (m0, m1) => paths[m1]);
 | |
|   };
 | |
| }
 | |
| 
 | |
| function resolveURI(id, mapping) {
 | |
|   // Do not resolve if already a resource URI
 | |
|   if (isAbsoluteURI(id)) {
 | |
|     return normalizeExt(id);
 | |
|   }
 | |
| 
 | |
|   return normalizeExt(mapping(id));
 | |
| }
 | |
| 
 | |
| // 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.
 | |
| function Require(loader, requirer) {
 | |
|   const { modules, mapping, mappingCache, requireHook } = 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
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (requireHook) {
 | |
|       return requireHook(id, _require);
 | |
|     }
 | |
| 
 | |
|     return _require(id);
 | |
|   }
 | |
| 
 | |
|   function _require(id) {
 | |
|     let { uri, requirement } = getRequirements(id);
 | |
| 
 | |
|     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 = ChromeUtils.import(uri);
 | |
|     } 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;
 | |
|       } 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 {
 | |
|         Object.freeze(load(loader, module));
 | |
|       } catch (e) {
 | |
|         // Clear out modules cache so we can throw on a second invalid require
 | |
|         delete modules[uri];
 | |
|         throw e;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return module.exports;
 | |
|   }
 | |
| 
 | |
|   // Resolution function taking a module name/path and
 | |
|   // returning a resourceURI and a `requirement` used by the loader.
 | |
|   // Used by both `require` and `require.resolve`.
 | |
|   function getRequirements(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, uri;
 | |
| 
 | |
|     if (modules[id]) {
 | |
|       uri = requirement = id;
 | |
|     } else if (requirer) {
 | |
|       // Resolve `id` to its requirer if it's relative.
 | |
|       requirement = resolve(id, requirer.id);
 | |
|     } else {
 | |
|       requirement = id;
 | |
|     }
 | |
| 
 | |
|     // Resolves `uri` of module using loaders resolve function.
 | |
|     if (!uri) {
 | |
|       if (mappingCache.has(requirement)) {
 | |
|         uri = mappingCache.get(requirement);
 | |
|       } else {
 | |
|         uri = resolveURI(requirement, mapping);
 | |
|         mappingCache.set(requirement, uri);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Throw if `uri` can not be resolved.
 | |
|     if (!uri) {
 | |
|       throw Error(
 | |
|         "Module: Can not resolve '" +
 | |
|           id +
 | |
|           "' module required by " +
 | |
|           requirer.id +
 | |
|           " located at " +
 | |
|           requirer.uri,
 | |
|         requirer.uri
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return { uri: uri, requirement: requirement };
 | |
|   }
 | |
| 
 | |
|   // Expose the `resolve` function for this `Require` instance
 | |
|   require.resolve = _require.resolve = function(id) {
 | |
|     const { uri } = getRequirements(id);
 | |
|     return uri;
 | |
|   };
 | |
| 
 | |
|   // This is like webpack's require.context.  It returns a new require
 | |
|   // function that prepends the prefix to any requests.
 | |
|   require.context = prefix => {
 | |
|     return id => {
 | |
|       return require(prefix + id);
 | |
|     };
 | |
|   };
 | |
| 
 | |
|   return require;
 | |
| }
 | |
| 
 | |
| // Makes module object that is made available to CommonJS modules when they
 | |
| // are evaluated, along with `exports` and `require`.
 | |
| function Module(id, uri) {
 | |
|   return Object.create(null, {
 | |
|     id: { enumerable: true, value: id },
 | |
|     exports: {
 | |
|       enumerable: true,
 | |
|       writable: true,
 | |
|       value: Object.create(null),
 | |
|       configurable: true,
 | |
|     },
 | |
|     uri: { value: uri },
 | |
|   });
 | |
| }
 | |
| 
 | |
| // Takes `loader`, and unload `reason` string and notifies all observers that
 | |
| // they should cleanup after them-self.
 | |
| 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.
 | |
|   const subject = { wrappedJSObject: loader.destructor };
 | |
|   Services.obs.notifyObservers(subject, "devtools:loader:destroy", reason);
 | |
| }
 | |
| 
 | |
| // Function makes new loader that can be used to load CommonJS modules.
 | |
| // Loader takes following options:
 | |
| // - `paths`: Mandatory dictionary of require path mapped to absolute URIs.
 | |
| //   Object keys are path prefix used in require(), values are URIs where each
 | |
| //   prefix should be mapped to.
 | |
| // - `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 `{}`.
 | |
| // - `sandboxName`: String, name of the sandbox displayed in about:memory.
 | |
| // - `invisibleToDebugger`: Boolean. Should be true when loading debugger
 | |
| //   modules, in order to ignore them from the Debugger API.
 | |
| // - `sandboxPrototype`: Object used to define globals on all module's
 | |
| //   sandboxes.
 | |
| // - `requireHook`: Optional function used to replace native require function
 | |
| //   from loader. This function receive the module path as first argument,
 | |
| //   and native require method as second argument.
 | |
| function Loader(options) {
 | |
|   let { paths, globals } = options;
 | |
|   if (!globals) {
 | |
|     globals = {};
 | |
|   }
 | |
| 
 | |
|   // 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.
 | |
|   const destructor = Object.create(null);
 | |
| 
 | |
|   const mapping = compileMapping(paths);
 | |
| 
 | |
|   // Define pseudo modules.
 | |
|   const builtinModuleExports = {
 | |
|     "@loader/unload": destructor,
 | |
|     "@loader/options": options,
 | |
|     chrome: {
 | |
|       Cc,
 | |
|       Ci,
 | |
|       Cu,
 | |
|       Cr,
 | |
|       Cm,
 | |
|       CC: bind(CC, Components),
 | |
|       components: Components,
 | |
|       ChromeWorker,
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   const modules = {};
 | |
|   for (const id of Object.keys(builtinModuleExports)) {
 | |
|     // We resolve `uri` from `id` since modules are cached by `uri`.
 | |
|     const uri = resolveURI(id, mapping);
 | |
|     const module = Module(id, uri);
 | |
| 
 | |
|     // Lazily expose built-in modules in order to
 | |
|     // allow them to be loaded lazily.
 | |
|     Object.defineProperty(module, "exports", {
 | |
|       enumerable: true,
 | |
|       get: function() {
 | |
|         return builtinModuleExports[id];
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     modules[uri] = module;
 | |
|   }
 | |
| 
 | |
|   // Create the unique sandbox we will be using for all modules,
 | |
|   // so that we prevent creating a new compartment per module.
 | |
|   // The side effect is that all modules will share the same
 | |
|   // global objects.
 | |
|   const sharedGlobalSandbox = Sandbox({
 | |
|     name: options.sandboxName || "DevTools",
 | |
|     invisibleToDebugger: options.invisibleToDebugger || false,
 | |
|     prototype: options.sandboxPrototype || globals,
 | |
|     freshCompartment: options.freshCompartment,
 | |
|   });
 | |
| 
 | |
|   if (options.sandboxPrototype) {
 | |
|     // If we were given a sandboxPrototype, we have to define the globals on
 | |
|     // the sandbox directly. Note that this will not work for callers who
 | |
|     // depend on being able to add globals after the loader was created.
 | |
|     for (const name of getOwnIdentifiers(globals)) {
 | |
|       Object.defineProperty(
 | |
|         sharedGlobalSandbox,
 | |
|         name,
 | |
|         Object.getOwnPropertyDescriptor(globals, name)
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Loader object is just a representation of a environment
 | |
|   // state. We mark its properties non-enumerable
 | |
|   // as they are pure implementation detail that no one should rely upon.
 | |
|   const returnObj = {
 | |
|     destructor: { enumerable: false, value: destructor },
 | |
|     globals: { enumerable: false, value: globals },
 | |
|     mapping: { enumerable: false, value: mapping },
 | |
|     mappingCache: { enumerable: false, value: new Map() },
 | |
|     // Map of module objects indexed by module URIs.
 | |
|     modules: { enumerable: false, value: modules },
 | |
|     sharedGlobalSandbox: { enumerable: false, value: sharedGlobalSandbox },
 | |
|     supportAMDModules: {
 | |
|       enumerable: false,
 | |
|       value: options.supportAMDModules || false,
 | |
|     },
 | |
|     // Whether the modules loaded should be ignored by the debugger
 | |
|     invisibleToDebugger: {
 | |
|       enumerable: false,
 | |
|       value: options.invisibleToDebugger || false,
 | |
|     },
 | |
|     requireHook: {
 | |
|       enumerable: false,
 | |
|       writable: true,
 | |
|       value: options.requireHook,
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   return Object.create(null, returnObj);
 | |
| }
 |