зеркало из https://github.com/mozilla/gecko-dev.git
571 строка
18 KiB
JavaScript
571 строка
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);
|
|
}
|