From b62fd3aa047c6dff2764c9c415eff4da7a5fdd2e Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Mon, 30 Jun 2025 16:45:41 -0400 Subject: [PATCH] Add working require.js, more progress toward startup --- chrome/content/zotero/xpcom/reader.js | 2 +- chrome/content/zotero/xpcom/router.js | 5 +- .../zotero/xpcom/utilities_internal.js | 2 +- eslint.config.mjs | 2 +- ...ncurrentCaller.js => concurrentCaller.mjs} | 26 +- resource/loader.jsm | 1250 ----------------- resource/loader.sys.mjs | 653 +++++++++ resource/pathparser.jsm | 126 -- resource/pathparser.mjs | 106 ++ resource/require.js | 260 ++-- 10 files changed, 923 insertions(+), 1509 deletions(-) rename resource/{concurrentCaller.js => concurrentCaller.mjs} (89%) delete mode 100644 resource/loader.jsm create mode 100644 resource/loader.sys.mjs delete mode 100644 resource/pathparser.jsm create mode 100644 resource/pathparser.mjs diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 385a9a6e3f..e9ea194fef 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -25,7 +25,7 @@ var { FilePicker } = ChromeUtils.importESModule('chrome://zotero/content/modules/filePicker.mjs'); -const { BlockingObserver } = ChromeUtils.import("chrome://zotero/content/BlockingObserver.jsm"); +const { BlockingObserver } = ChromeUtils.importESModule("chrome://zotero/content/BlockingObserver.sys.mjs"); const ZipReader = Components.Constructor( "@mozilla.org/libjar/zip-reader;1", diff --git a/chrome/content/zotero/xpcom/router.js b/chrome/content/zotero/xpcom/router.js index da07026b81..705532d237 100644 --- a/chrome/content/zotero/xpcom/router.js +++ b/chrome/content/zotero/xpcom/router.js @@ -1,6 +1,5 @@ -Components.utils.import("resource://zotero/pathparser.jsm", Zotero); -Zotero.Router = Zotero.PathParser; -delete Zotero.PathParser; +const { PathParser } = ChromeUtils.importESModule("resource://zotero/pathparser.mjs"); +Zotero.Router = PathParser; Zotero.Router.Utilities = { convertControllerToObjectType: function (params) { diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js index f150f5eea4..ce83e8e2fb 100644 --- a/chrome/content/zotero/xpcom/utilities_internal.js +++ b/chrome/content/zotero/xpcom/utilities_internal.js @@ -1810,7 +1810,7 @@ Zotero.Utilities.Internal = { }, serial: function (fn) { - Components.utils.import("resource://zotero/concurrentCaller.js"); + const { ConcurrentCaller } = ChromeUtils.importESModule("resource://zotero/concurrentCaller.mjs"); var caller = new ConcurrentCaller({ numConcurrent: 1, onError: e => Zotero.logError(e) diff --git a/eslint.config.mjs b/eslint.config.mjs index ba41ce70bd..d5b5e2e848 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -101,7 +101,7 @@ export default [ "resource/citeproc_rs*", "resource/csl-validator.js", "resource/jspath.js", - "resource/loader.jsm", + "resource/loader.mjs", "resource/pako.js", "resource/PluralForm.jsm", "resource/prop-types.js", diff --git a/resource/concurrentCaller.js b/resource/concurrentCaller.mjs similarity index 89% rename from resource/concurrentCaller.js rename to resource/concurrentCaller.mjs index 15fe89a2f6..f1fe90a1d1 100644 --- a/resource/concurrentCaller.js +++ b/resource/concurrentCaller.mjs @@ -23,20 +23,6 @@ ***** END LICENSE BLOCK ***** */ -var EXPORTED_SYMBOLS = ["ConcurrentCaller"]; - -if (!(typeof process === 'object' && process + '' === '[object process]')) { - // Components.utils.import('resource://zotero/require.js'); - // Not using Cu.import here since we don't want the require module to be cached - // for includes within ZoteroPane or other code where we want the window instance available to modules. - Components.classes["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Components.interfaces.mozIJSSubScriptLoader) - .loadSubScript('resource://zotero/require.js'); - var Promise = require('resource://zotero/bluebird.js'); -} else { - Promise = require('bluebird'); -} - /** * Call a fixed number of functions at once, queueing the rest until slots * open and returning a promise for the final completion. The functions do @@ -69,7 +55,7 @@ if (!(typeof process === 'object' && process + '' === '[object process]')) { * @param {Object} [options.Promise] The Zotero instance of Promise to allow * stubbing/spying in tests */ -var ConcurrentCaller = function (options = {}) { +export var ConcurrentCaller = function (options = {}) { if (typeof options == 'number') { this._log("ConcurrentCaller now takes an object rather than a number"); options = { @@ -286,20 +272,16 @@ ConcurrentCaller.prototype._getIntervalNeeded = function () { * Wait until the specified interval has elapsed or the current pause (if there is one) is over, * whichever is longer */ -ConcurrentCaller.prototype._waitForPause = Promise.coroutine(function* () { +ConcurrentCaller.prototype._waitForPause = async function () { var interval = this._getIntervalNeeded(); if (interval == 0) return; this._pausing = true; - yield Promise.delay(interval); + await new Promise(resolve => setTimeout(resolve, interval)); this._pausing = false; -}); +}; ConcurrentCaller.prototype._log = function (msg) { if (this._logger) { this._logger("[ConcurrentCaller] " + (this._id ? `[${this._id}] ` : "") + msg); } }; - -if (typeof process === 'object' && process + '' === '[object process]'){ - module.exports = ConcurrentCaller; -} diff --git a/resource/loader.jsm b/resource/loader.jsm deleted file mode 100644 index 5679702f86..0000000000 --- a/resource/loader.jsm +++ /dev/null @@ -1,1250 +0,0 @@ -/* 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/. */ - -;((factory) => { // Module boilerplate :( - if (typeof(require) === 'function') { // CommonJS - require("chrome").Cu.import(module.uri, exports); - } - else if (typeof Components != 'undefined') { // JSM - let module = { uri: __URI__, id: "toolkit/loader", exports: Object.create(null) } - factory(module); - Object.assign(this, module.exports); - this.EXPORTED_SYMBOLS = Object.getOwnPropertyNames(module.exports); - } - else { - throw Error("Loading environment is not supported"); - } -})(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 { addObserver, notifyObservers } = Cc['@mozilla.org/observer-service;1']. - getService(Ci.nsIObserverService); -const { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs", {}); -const { NetUtil } = ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs", {}); - - - - -// These are used for manipulating jar entry paths, which always use Unix -// separators (originally copied from `ospath_unix.jsm` as part of the "OS.Path -// to PathUtils" migration). - -/** - * Join path components. - * This is the recommended manner of getting the path of a file/subdirectory - * in a directory. - * - * Example: Obtaining $TMP/foo/bar in an OS-independent manner - * var tmpDir = OS.Constants.Path.tmpDir; - * var path = OS.Path.join(tmpDir, "foo", "bar"); - * - * Under Unix, this will return "/tmp/foo/bar". - * - * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the - * same as `OS.Path.join("foo", "bar")`. - */ -var pathJoin = function(...path) { - // If there is a path that starts with a "/", eliminate everything before - let paths = []; - for (let subpath of path) { - if (subpath == null) { - throw new TypeError("invalid path component"); - } - if (!subpath.length) { - continue; - } else if (subpath[0] == "/") { - paths = [subpath]; - } else { - paths.push(subpath); - } - } - return paths.join("/"); -}; - -/** - * Normalize a path by removing any unneeded ".", "..", "//". - */ -var normalize = function(path) { - let stack = []; - let absolute; - if (path.length >= 0 && path[0] == "/") { - absolute = true; - } else { - absolute = false; - } - path.split("/").forEach(function(v) { - switch (v) { - case "": - case ".": // fallthrough - break; - case "..": - if (!stack.length) { - if (absolute) { - throw new Error("Path is ill-formed: attempting to go past root"); - } else { - stack.push(".."); - } - } else if (stack[stack.length - 1] == "..") { - stack.push(".."); - } else { - stack.pop(); - } - break; - default: - stack.push(v); - } - }); - let string = stack.join("/"); - return absolute ? "/" + string : string; -}; - -/** - * Return the directory part of the path. - * The directory part of the path is everything before the last - * "/". If the last few characters of this part are also "/", - * they are ignored. - * - * If the path contains no directory, return ".". - */ -function dirname(path) { - let index = path.lastIndexOf("/"); - if (index == -1) { - return "."; - } - while (index >= 0 && path[index] == "/") { - --index; - } - return path.slice(0, index + 1); -} - - - - - -XPCOMUtils.defineLazyServiceGetter(this, "resProto", - "@mozilla.org/network/protocol;1?name=resource", - "nsIResProtocolHandler"); -XPCOMUtils.defineLazyServiceGetter(this, "zipCache", - "@mozilla.org/libjar/zip-reader-cache;1", - "nsIZipReaderCache"); - -ChromeUtils.defineLazyGetter(this, "XulApp", () => { - let xulappURI = module.uri.replace("toolkit/loader.js", - "sdk/system/xul-app.jsm"); - return Cu.import(xulappURI, {}); -}); - -// Define some shortcuts. -const bind = Function.call.bind(Function.bind); -const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; -const prototypeOf = Object.getPrototypeOf; -const getOwnIdentifiers = x => [...Object.getOwnPropertyNames(x), - ...Object.getOwnPropertySymbols(x)]; - -const NODE_MODULES = new Set([ - "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://developer.mozilla.org/en-US/Add-ons/SDK \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 = {}; - getOwnIdentifiers(object).forEach(function(name) { - value[name] = getOwnPropertyDescriptor(object, name) - }); - return value; -}); -Loader.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 || {}) - getOwnIdentifiers(extension).forEach(function(name) { - properties[name] = extension[name]; - }); - return Object.defineProperties({}, properties); -}); -Loader.override = override; - -function sourceURI(uri) { return String(uri).split(" -> ").pop(); } -Loader.sourceURI = iced(sourceURI); - -function isntLoaderFrame(frame) { return frame.fileName !== module.uri } - -function parseURI(uri) { return String(uri).split(" -> ").pop(); } -Loader.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; - }, []); -} -Loader.parseStack = parseStack; - -function serializeStack(frames) { - return frames.reduce(function(stack, frame) { - return frame.name + "@" + - frame.fileName + ":" + - frame.lineNumber + ":" + - frame.columnNumber + "\n" + - stack; - }, ""); -} -Loader.serializeStack = serializeStack; - -class DefaultMap extends Map { - constructor(createItem, items = undefined) { - super(items); - - this.createItem = createItem; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.createItem(key)); - } - - return super.get(key); - } -} - -const urlCache = { - /** - * Returns a list of fully-qualified URLs for entries within the zip - * file at the given URI which are either directories or files with a - * .js or .json extension. - * - * @param {nsIJARURI} uri - * @param {string} baseURL - * The original base URL, prior to resolution. - * - * @returns {Set} - */ - getZipFileContents(uri, baseURL) { - // Make sure the path has a trailing slash, and strip off the leading - // slash, so that we can easily check whether it is a path prefix. - let basePath = addTrailingSlash(uri.JAREntry).slice(1); - let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file; - - let enumerator = zipCache.getZip(file).findEntries("(*.js|*.json|*/)"); - - let results = new Set(); - for (let entry of XPCOMUtils.IterStringEnumerator(enumerator)) { - if (entry.startsWith(basePath)) { - let path = entry.slice(basePath.length); - - results.add(baseURL + path); - } - } - - return results; - }, - - zipContentsCache: new DefaultMap(baseURL => { - let uri = NetUtil.newURI(baseURL); - - if (baseURL.startsWith("resource:")) { - uri = NetUtil.newURI(resProto.resolveURI(uri)); - } - - if (uri instanceof Ci.nsIJARURI) { - return urlCache.getZipFileContents(uri, baseURL); - } - - return null; - }), - - filesCache: new DefaultMap(url => { - try { - let uri = NetUtil.newURI(url).QueryInterface(Ci.nsIFileURL); - - return uri.file.exists(); - } catch (e) { - return false; - } - }), - - QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference]), - - observe() { - // Clear any module resolution caches when the startup cache is flushed, - // since it probably means we're loading new copies of extensions. - this.zipContentsCache.clear(); - this.filesCache.clear(); - }, - - /** - * Returns the base URL for the given URL, if one can be determined. For - * a resource: URL, this is the root of the resource package. For a jar: - * URL, it is the root of the JAR file. Otherwise, null is returned. - * - * @param {string} url - * @returns {string?} - */ - getBaseURL(url) { - // By using simple string matching for the common case of resource: URLs - // backed by jar: URLs, we can avoid creating any nsIURI objects for the - // common case where the JAR contents are already cached. - if (url.startsWith("resource://")) { - return /^resource:\/\/[^\/]+\//.exec(url)[0]; - } - - let uri = NetUtil.newURI(url); - if (uri instanceof Ci.nsIJARURI) { - return `jar:${uri.JARFile.spec}!/`; - } - - return null; - }, - - /** - * Returns true if the target of the given URL exists as a local file, - * or as an entry in a local zip file. - * - * @param {string} url - * @returns {boolean} - */ - exists(url) { - if (!/\.(?:js|json)$/.test(url)) { - url = addTrailingSlash(url); - } - - let baseURL = this.getBaseURL(url); - let scripts = baseURL && this.zipContentsCache.get(baseURL); - if (scripts) { - return scripts.has(url); - } - - return this.filesCache.get(url); - }, -} -addObserver(urlCache, "startupcache-invalidate", true); - -function readURI(uri) { - let 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); - } - - let stream = NetUtil.newChannel({ - uri: NetUtil.newURI(uri, 'UTF-8'), - loadUsingSystemPrincipal: true} - ).open2(); - 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(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. - let match = /^((?:resource|file|chrome)\:\/\/[^\/]*|jar:[^!]+!)(.*)/.exec(base); - if (match) { - return match[1] + normalize(pathJoin(match[2], ...paths)); - } - - return normalize(pathJoin(base, ...paths)); -} -Loader.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 : {}, - waiveIntereposition: !!options.waiveIntereposition - }; - - if (options.metadata && options.metadata.addonID) { - options.addonId = options.metadata.addonID; - } - - 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; -}); -Loader.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); -}); -Loader.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, loadModuleHook } = 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; - if ((loader.useSharedGlobalSandbox || isSystemURI(module.uri)) && - loader.sharedGlobalBlocklist.indexOf(module.id) == -1) { - // Create a new object in this sandbox, that will be used as - // the scope object for this particular module - sandbox = new loader.sharedGlobalSandbox.Object(); - // Inject all expected globals in the scope object - getOwnIdentifiers(globals).forEach(function(name) { - descriptors[name] = getOwnPropertyDescriptor(globals, name) - descriptors[name].configurable = true; - }); - Object.defineProperties(sandbox, descriptors); - } - else { - sandbox = Sandbox({ - name: module.uri, - prototype: Object.create(globals, descriptors), - wantXrays: false, - wantGlobalProperties: module.id == "sdk/indexed-db" ? ["indexedDB"] : [], - invisibleToDebugger: loader.invisibleToDebugger, - metadata: { - addonID: loader.id, - URI: module.uri - } - }); - } - sandboxes[module.uri] = sandbox; - - 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 (/^Error opening input stream/.test(String(error))) { - 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 Object.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: () => toString, writable: true, configurable: true }, - }); - } - - if (loadModuleHook) { - module = loadModuleHook(module, require); - } - - if (loader.checkCompatibility) { - let err = XulApp.incompatibility(module); - if (err) { - throw err; - } - } - - if (module.exports && typeof(module.exports) === 'object') - freeze(module.exports); - - return module; -}); -Loader.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 baseDir = dirname(base); - if (!baseDir) - return normalize(id); - - let resolved = join(baseDir, 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; -}); -Loader.resolve = resolve; - -// Attempts to load `path` and then `path.js` -// Returns `path` with valid file, or `undefined` otherwise -function resolveAsFile(path) { - // Append '.js' to path name unless it's another support filetype - path = normalizeExt(path); - if (urlCache.exists(path)) { - return path; - } - - return null; -} - -// Attempts to load `path/package.json`'s `main` entry, -// followed by `path/index.js`, or `undefined` otherwise -function resolveAsDirectory(path) { - try { - // If `path/package.json` exists, parse the `main` entry - // and attempt to load that - let manifestPath = addTrailingSlash(path) + 'package.json'; - - let main = (urlCache.exists(manifestPath) && - getManifestMain(JSON.parse(readURI(manifestPath)))); - if (main) { - let found = resolveAsFile(join(path, main)); - if (found) { - return found - } - } - } catch (e) {} - - return resolveAsFile(addTrailingSlash(path) + 'index.js'); -} - -function resolveRelative(rootURI, modulesDir, id) { - let fullId = join(rootURI, modulesDir, id); - - let resolvedPath = (resolveAsFile(fullId) || - resolveAsDirectory(fullId)); - if (resolvedPath) { - return stripBase(rootURI, resolvedPath); - } - - return null; -} - -// From `resolve` module -// https://github.com/substack/node-resolve/blob/master/lib/node-modules-paths.js -function* getNodeModulePaths(rootURI, start) { - let moduleDir = 'node_modules'; - - let parts = start.split('/'); - while (parts.length) { - let leaf = parts.pop(); - let path = join(...parts, leaf, moduleDir); - if (leaf !== moduleDir && urlCache.exists(join(rootURI, path))) { - yield path; - } - } - - if (urlCache.exists(join(rootURI, moduleDir))) { - yield moduleDir; - } -} - -// 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, { rootURI }) { - // Resolve again - id = Loader.resolve(id, requirer); - - // If this is already an absolute URI then there is no resolution to do - if (isAbsoluteURI(id)) { - return null; - } - - // 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 resolvedPath; - - if ((resolvedPath = resolveRelative(rootURI, "", id))) { - return resolvedPath; - } - - // If the requirer is an absolute URI then the node module resolution below - // won't work correctly as we prefix everything with rootURI - if (isAbsoluteURI(requirer)) { - return null; - } - - // If manifest has dependencies, attempt to look up node modules - // in the `dependencies` list - for (let modulesDir of getNodeModulePaths(rootURI, dirname(requirer))) { - if ((resolvedPath = resolveRelative(rootURI, modulesDir, id))) { - return 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 null; -}); - -Loader.nodeResolve = nodeResolve; - -function addTrailingSlash(path) { - return path.replace(/\/*$/, "/"); -} - -const resolveURI = iced(function resolveURI(id, mapping) { - // Do not resolve if already a resource URI - if (isAbsoluteURI(id)) - return normalizeExt(id); - - for (let [path, uri] of mapping) { - // Strip off any trailing slashes to make comparisons simpler - let stripped = path.replace(/\/+$/, ""); - - // 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(stripped === "" || id === stripped || id.startsWith(stripped + "/")) { - return normalizeExt(id.replace(path, uri)); - } - } - return null; -}); -Loader.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: loaderResolve, load, - manifest, rootURI, isNative, requireMap, - requireHook - } = loader; - - if (isSystemURI(requirer.uri)) { - // Built-in modules don't require the expensive module resolution - // algorithm used by SDK add-ons, so give them the more efficient standard - // resolve instead. - isNative = false; - loaderResolve = Loader.resolve; - } - - 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) { - // Fix require() from react-autosuggest - if (id == 'React') id = 'react'; - - 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 = 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; - } - - // 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; - - // 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. - // TODO: write more tests for this use case - if (requireMap && requireMap[requirer.id]) - requirement = requireMap[requirer.id][id]; - - let { overrides } = manifest.jetpack; - for (let key in overrides) { - // ignore any overrides using relative keys - if (/^[.\/]/.test(key)) { - continue; - } - - // If the override is for x -> y, - // then using require("x/lib/z") to get reqire("y/lib/z") - // should also work - if (id == key || id.startsWith(key + "/")) { - id = overrides[key] + id.substr(key.length); - id = id.replace(/^[.\/]+/, ""); - } - } - - // 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 && !NODE_MODULES.has(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 = loaderResolve(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) ? Loader.resolve(id, requirer.id) : id; - } - } - else if (modules[id]) { - uri = requirement = id; - } - else if (requirer) { - // Resolve `id` to its requirer if it's relative. - requirement = loaderResolve(id, requirer.id); - } - else { - requirement = id; - } - - // Resolves `uri` of module using loaders resolve function. - uri = uri || resolveURI(requirement, mapping); - - // 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 resolve(id) { - let { 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); - }; - }; - - // Make `require.main === module` evaluate to true in main module scope. - require.main = loader.main === requirer ? requirer : undefined; - return iced(require); -}); -Loader.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; -}); -Loader.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 Object.create(null, { - id: { enumerable: true, value: id }, - exports: { enumerable: true, writable: true, value: Object.create(null), - configurable: true }, - uri: { value: uri } - }); -}); -Loader.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); -}); -Loader.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. -function Loader(options) { - if (options.sharedGlobalBlacklist && !options.sharedGlobalBlocklist) { - options.sharedGlobalBlocklist = options.sharedGlobalBlacklist; - } - let { - modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative, - metadata, sharedGlobal, sharedGlobalBlocklist, checkCompatibility, waiveIntereposition - } = override({ - paths: {}, - modules: {}, - globals: { - get console() { - // Import Console.jsm from here to prevent loading it until someone uses it - let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm"); - let console = new ConsoleAPI({ - consoleID: options.id ? "addon/" + options.id : "" - }); - Object.defineProperty(this, "console", { value: console }); - return this.console; - } - }, - checkCompatibility: false, - resolve: options.isNative ? - // Make the returned resolve function have the same signature - (id, requirer) => Loader.nodeResolve(id, requirer, { rootURI: rootURI }) : - Loader.resolve, - sharedGlobalBlocklist: ["sdk/indexed-db"], - waiveIntereposition: false - }, options); - - // Create overrides defaults, none at the moment - if (typeof manifest != "object" || !manifest) { - manifest = {}; - } - if (typeof manifest.jetpack != "object" || !manifest.jetpack) { - manifest.jetpack = { - overrides: {} - }; - } - if (typeof manifest.jetpack.overrides != "object" || !manifest.jetpack.overrides) { - manifest.jetpack.overrides = {}; - } - - // 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(Object.create(null)); - - // Make mapping array that is sorted from longest path to shortest path. - let mapping = Object.keys(paths) - .sort((a, b) => b.length - a.length) - .map(path => [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, - // `ChromeWorker` has to be inject in loader global scope. - // It is done by bootstrap.js:loadSandbox for the SDK. - ChromeWorker, - ChromeUtils, - } - }, modules); - - const builtinModuleExports = modules; - modules = {}; - for (let id of Object.keys(builtinModuleExports)) { - // 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); - - // 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] = freeze(module); - } - - // Create the unique sandbox we will be using for all modules, - // so that we prevent creating a new comportment per module. - // The side effect is that all modules will share the same - // global objects. - let sharedGlobalSandbox = Sandbox({ - name: "Addon-SDK", - wantXrays: false, - wantGlobalProperties: [], - invisibleToDebugger: options.invisibleToDebugger || false, - metadata: { - addonID: options.id, - URI: "Addon-SDK" - }, - prototype: options.sandboxPrototype || {} - }); - - // 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 }, - metadata: { enumerable: false, value: metadata }, - useSharedGlobalSandbox: { enumerable: false, value: !!sharedGlobal }, - sharedGlobalSandbox: { enumerable: false, value: sharedGlobalSandbox }, - sharedGlobalBlocklist: { enumerable: false, value: sharedGlobalBlocklist }, - sharedGlobalBlacklist: { enumerable: false, value: sharedGlobalBlocklist }, - // 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 }, - checkCompatibility: { enumerable: false, value: checkCompatibility }, - requireHook: { enumerable: false, value: options.requireHook }, - loadModuleHook: { enumerable: false, value: options.loadModuleHook }, - // 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(Object.create(null, returnObj)); -}; -Loader.Loader = Loader; - -var isSystemURI = uri => /^resource:\/\/(gre|devtools|testing-common)\//.test(uri); - -var isJSONURI = uri => uri.endsWith('.json'); -var isJSMURI = uri => uri.endsWith('.jsm'); -var isJSURI = uri => uri.endsWith('.js'); -var isAbsoluteURI = uri => uri.startsWith("resource://") || - uri.startsWith("chrome://") || - uri.startsWith("file://"); -var isRelative = id => id.startsWith("."); - -// 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; -} - -module.exports = iced(Loader); -}); - diff --git a/resource/loader.sys.mjs b/resource/loader.sys.mjs new file mode 100644 index 0000000000..c85821a36c --- /dev/null +++ b/resource/loader.sys.mjs @@ -0,0 +1,653 @@ +/* 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 */ + +const systemPrincipal = Components.Constructor( + "@mozilla.org/systemprincipal;1", + "nsIPrincipal" +)(); + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "resProto", + "@mozilla.org/network/protocol;1?name=resource", + "nsIResProtocolHandler" +); + +ChromeUtils.defineESModuleGetters( + lazy, + { + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + }, + { global: "contextual" } +); + +const VENDOR_URI = "resource://devtools/client/shared/vendor/"; +const REACT_ESM_MODULES = new Set([ + VENDOR_URI + "react-dev.js", + VENDOR_URI + "react.js", + VENDOR_URI + "react-dom-dev.js", + VENDOR_URI + "react-dom.js", + VENDOR_URI + "react-dom-factories.js", + VENDOR_URI + "react-dom-server-dev.js", + VENDOR_URI + "react-dom-server.js", + VENDOR_URI + "react-prop-types-dev.js", + VENDOR_URI + "react-prop-types.js", + VENDOR_URI + "react-test-renderer.js", +]); + +// Define some shortcuts. +function* getOwnIdentifiers(x) { + yield* Object.getOwnPropertyNames(x); + yield* Object.getOwnPropertySymbols(x); +} + +function isJSONURI(uri) { + return uri.endsWith(".json"); +} +function isESMURI(uri) { + return uri.endsWith(".mjs"); +} +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 = lazy.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 = lazy.resProto.resolveURI(nsURI); + } + + const stream = lazy.NetUtil.newChannel({ + uri: lazy.NetUtil.newURI(uri, "UTF-8"), + loadUsingSystemPrincipal: true, + }).open(); + const count = stream.available(); + const data = lazy.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 +// `{}`. +function Sandbox(options) { + // Normalize options and rename to match `Cu.Sandbox` expectations. + const sandboxOptions = { + // This will allow exposing Components as well as Cu, Ci and Cr. + wantComponents: true, + + // By default, Sandbox come with a very limited set of global. + // The list of all available symbol names is available over there: + // https://searchfox.org/mozilla-central/rev/31368c7795f44b7a15531d6c5e52dc97f82cf2d5/js/xpconnect/src/Sandbox.cpp#905-997 + // Request to expose all meaningful global here: + wantGlobalProperties: [ + "AbortController", + "atob", + "btoa", + "Blob", + "crypto", + "ChromeUtils", + "CSS", + "CSSRule", + "CustomStateSet", + "DOMParser", + "Element", + "Event", + "FileReader", + "FormData", + "Headers", + "InspectorCSSParser", + "InspectorUtils", + "MIDIInputMap", + "MIDIOutputMap", + "Node", + "TextDecoder", + "TextEncoder", + "TrustedHTML", + "TrustedScript", + "TrustedScriptURL", + "URL", + "URLSearchParams", + "Window", + "XMLHttpRequest", + ], + + sandboxName: options.name, + sandboxPrototype: "prototype" in options ? options.prototype : {}, + freshCompartment: options.freshCompartment || false, + }; + + return Cu.Sandbox(systemPrincipal, sandboxOptions); +} + +// 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 the shared global of the loader, that will be used + // as the scope object for this particular module. + const scopeFromSharedGlobal = new loader.sharedGlobal.Object(); + Object.assign(scopeFromSharedGlobal, properties); + + const originalExports = module.exports; + try { + Services.scriptloader.loadSubScript(module.uri, scopeFromSharedGlobal); + } 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) || isESMURI(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]); + }; +} + +export 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. +export 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); + + // Load all react modules as ES Modules, in the Browser Loader global. + // For this we have to ensure using ChromeUtils.importESModule with `global:"current"`, + // but executed from the Loader global scope. `syncImport` does that. + if (REACT_ESM_MODULES.has(uri)) { + // All CommonJS modules are still importing the .js/CommonJS version, + // but we hack these require() call to load the ESM version. + uri = uri.replace(/.js$/, ".mjs"); + } + + let module = null; + // If module is already cached by loader then just use it. + if (uri in modules) { + module = modules[uri]; + } else if (isESMURI(uri)) { + module = modules[uri] = Module(requirement, uri); + const rv = ChromeUtils.importESModule(uri, { + global: "contextual", + }); + module.exports = rv.default || rv; + } 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, 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`. +export 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. +export 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. +// - `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. +export 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, + }; + + 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() { + return builtinModuleExports[id]; + }, + }); + + modules[uri] = module; + } + + let sharedGlobal; + if (options.sharedGlobal) { + sharedGlobal = options.sharedGlobal; + } else { + // 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. + sharedGlobal = Sandbox({ + name: options.sandboxName || "Zotero", + prototype: options.sandboxPrototype || globals, + freshCompartment: options.freshCompartment, + }); + } + + if (options.sharedGlobal || options.sandboxPrototype) { + // If we were given a sharedGlobal or a sandboxPrototype, we have to define + // the globals on the shared global 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( + sharedGlobal, + 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 }, + sharedGlobal: { enumerable: false, value: sharedGlobal }, + supportAMDModules: { + enumerable: false, + value: options.supportAMDModules || false, + }, + requireHook: { + enumerable: false, + writable: true, + value: options.requireHook, + }, + }; + + return Object.create(null, returnObj); +} + +// NB: These methods are from the UNIX implementation of OS.Path. Refactoring +// this module to not use path methods on stringly-typed URIs is +// non-trivial. +function dirname(path) { + let index = path.lastIndexOf("/"); + if (index == -1) { + return "."; + } + while (index >= 0 && path[index] == "/") { + --index; + } + return path.slice(0, index + 1); +} + +function normalize(path) { + const stack = []; + let absolute; + if (path.length >= 0 && path[0] == "/") { + absolute = true; + } else { + absolute = false; + } + path.split("/").forEach(function (v) { + switch (v) { + case "": + case ".": // fallthrough + break; + case "..": + if (!stack.length) { + if (absolute) { + throw new Error("Path is ill-formed: attempting to go past root"); + } else { + stack.push(".."); + } + } else if (stack[stack.length - 1] == "..") { + stack.push(".."); + } else { + stack.pop(); + } + break; + default: + stack.push(v); + } + }); + const string = stack.join("/"); + return absolute ? "/" + string : string; +} diff --git a/resource/pathparser.jsm b/resource/pathparser.jsm deleted file mode 100644 index c38d89dc7a..0000000000 --- a/resource/pathparser.jsm +++ /dev/null @@ -1,126 +0,0 @@ -/** - * pathparser.js - tiny URL parser/router - * - * Copyright (c) 2014 Dan Stillman - * License: MIT - * https://github.com/dstillman/pathparser.js - */ -(function (root, factory) { - // AMD/RequireJS - if (typeof define === 'function' && define.amd) { - define(factory); - // CommonJS/Node - } else if (typeof exports === 'object') { - module.exports = factory(); - // Mozilla JSM - } else if (typeof Components != 'undefined' - && typeof Components.utils != 'undefined' - && typeof Components.utils.import == 'function') { - root.EXPORTED_SYMBOLS = ["PathParser"]; - root.PathParser = factory(); - // Browser global - } else { - root.PathParser = factory(); - } -}(this, function () { - "use strict"; - - var PathParser = function (params) { - this.rules = []; - this.params = params; - } - - PathParser.prototype = (function () { - function getParamsFromRule(rule, pathParts, queryParts) { - var params = {}; - var missingParams = {}; - - if (!rule.allowMissingParams && rule.parts.length != pathParts.length) { - return false; - } - - // Parse path components - for (var i = 0; i < rule.parts.length; i++) { - var rulePart = rule.parts[i]; - var part = pathParts[i]; - - if (part !== undefined) { - if (rulePart.charAt(0) == ':') { - params[rulePart.substr(1)] = part; - continue; - } - else if (rulePart !== part) { - return false; - } - } - else if (rulePart.charAt(0) != ':') { - return false; - } - else { - missingParams[rulePart.substr(1)] = true; - } - } - - // Parse query strings - for (var i = 0; i < queryParts.length; ++i) { - var nameValue = queryParts[i].split('=', 2); - var key = nameValue[0]; - // But ignore empty parameters and don't override named parameters - if (nameValue.length == 2 && !params[key] && !missingParams[key]) { - params[key] = nameValue[1]; - } - } - - return params; - } - - return { - add: function (route, handler, autoPopulateOnMatch = true, allowMissingParams = true) { - this.rules.push({ - parts: route.replace(/^\//, '').split('/'), - handler: handler, - autoPopulateOnMatch, - allowMissingParams - }); - }, - - run: function (url) { - if (url && url.length) { - url = url - // Remove redundant slashes - .replace(/\/+/g, '/') - // Strip leading and trailing '/' (at end or before query string) - .replace(/^\/|\/($|\?)/, '') - // Strip fragment identifiers - .replace(/#.*$/, ''); - } - - var urlSplit = url.split('?', 2); - var pathParts = urlSplit[0].split('/', 50); - var queryParts = urlSplit[1] ? urlSplit[1].split('&', 50) : []; - - for (var i=0; i < this.rules.length; i++) { - var rule = this.rules[i]; - var params = getParamsFromRule(rule, pathParts, queryParts); - if (params) { - params.url = url; - // Automatic parameter assignment - if (rule.autoPopulateOnMatch && this.params) { - for (var param in params) { - this.params[param] = params[param]; - } - } - // Call handler with 'this' bound to parameter object - if (rule.handler) { - rule.handler.call(params); - } - return true; - } - } - return false; - } - }; - })(); - - return PathParser; -})); \ No newline at end of file diff --git a/resource/pathparser.mjs b/resource/pathparser.mjs new file mode 100644 index 0000000000..075baf606e --- /dev/null +++ b/resource/pathparser.mjs @@ -0,0 +1,106 @@ +/** + * pathparser.js - tiny URL parser/router + * + * Copyright (c) 2014 Dan Stillman + * License: MIT + * https://github.com/dstillman/pathparser.js + */ + +"use strict"; + +export var PathParser = function (params) { + this.rules = []; + this.params = params; +}; + +PathParser.prototype = (function () { + function getParamsFromRule(rule, pathParts, queryParts) { + var params = {}; + var missingParams = {}; + + if (!rule.allowMissingParams && rule.parts.length != pathParts.length) { + return false; + } + + // Parse path components + for (var i = 0; i < rule.parts.length; i++) { + var rulePart = rule.parts[i]; + var part = pathParts[i]; + + if (part !== undefined) { + if (rulePart.charAt(0) == ':') { + params[rulePart.substr(1)] = part; + continue; + } + else if (rulePart !== part) { + return false; + } + } + else if (rulePart.charAt(0) != ':') { + return false; + } + else { + missingParams[rulePart.substr(1)] = true; + } + } + + // Parse query strings + for (var i = 0; i < queryParts.length; ++i) { + var nameValue = queryParts[i].split('=', 2); + var key = nameValue[0]; + // But ignore empty parameters and don't override named parameters + if (nameValue.length == 2 && !params[key] && !missingParams[key]) { + params[key] = nameValue[1]; + } + } + + return params; + } + + return { + add: function (route, handler, autoPopulateOnMatch = true, allowMissingParams = true) { + this.rules.push({ + parts: route.replace(/^\//, '').split('/'), + handler: handler, + autoPopulateOnMatch, + allowMissingParams + }); + }, + + run: function (url) { + if (url && url.length) { + url = url + // Remove redundant slashes + .replace(/\/+/g, '/') + // Strip leading and trailing '/' (at end or before query string) + .replace(/^\/|\/($|\?)/, '') + // Strip fragment identifiers + .replace(/#.*$/, ''); + } + + var urlSplit = url.split('?', 2); + var pathParts = urlSplit[0].split('/', 50); + var queryParts = urlSplit[1] ? urlSplit[1].split('&', 50) : []; + + for (var i=0; i < this.rules.length; i++) { + var rule = this.rules[i]; + var params = getParamsFromRule(rule, pathParts, queryParts); + if (params) { + params.url = url; + // Automatic parameter assignment + if (rule.autoPopulateOnMatch && this.params) { + for (var param in params) { + this.params[param] = params[param]; + } + } + // Call handler with 'this' bound to parameter object + if (rule.handler) { + rule.handler.call(params); + } + return true; + } + } + return false; + } + }; +})(); diff --git a/resource/require.js b/resource/require.js index f90f632fbc..e3840022eb 100644 --- a/resource/require.js +++ b/resource/require.js @@ -1,114 +1,164 @@ 'use strict'; -var require = (function() { - var win, cons, Zotero; - Components.utils.import('resource://zotero/loader.jsm'); - var requirer = Module('/', '/'); - var _runningTimers = {}; - if (typeof window != 'undefined') { - win = window; - } else { - win = {}; +/* 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/. */ - win.setTimeout = function (func, ms) { - var id = Math.floor(Math.random() * (1000000000000 - 1)) + 1 - var useMethodjit = Components.utils.methodjit; - var timer = Components.classes["@mozilla.org/timer;1"] - .createInstance(Components.interfaces.nsITimer); - timer.initWithCallback({"notify":function() { - // Remove timer from object so it can be garbage collected - delete _runningTimers[id]; - - // Execute callback function - try { - func(); - } catch(err) { - // Rethrow errors that occur so that they appear in the error - // console with the appropriate name and line numbers. While the - // the errors appear without this, the line numbers get eaten. - var scriptError = Components.classes["@mozilla.org/scripterror;1"] - .createInstance(Components.interfaces.nsIScriptError); - scriptError.init( - err.message || err.toString(), - err.fileName || err.filename || null, - null, - err.lineNumber || null, - null, - scriptError.errorFlag, - 'component javascript' - ); - Components.classes["@mozilla.org/consoleservice;1"] - .getService(Components.interfaces.nsIConsoleService) - .logMessage(scriptError); - typeof Zotero !== 'undefined' && Zotero.debug(err.stack, 1); - } - }}, ms, Components.interfaces.nsITimer.TYPE_ONE_SHOT); - _runningTimers[id] = timer; - return id; - }; - - win.clearTimeout = function (id) { - var timer = _runningTimers[id]; - if (timer) { - timer.cancel(); - } - delete _runningTimers[id]; - }; +/** + * Manages the base loader (loader.sys.mjs) instance used to load CJS modules. + */ - win.debug = function (msg) { - dump(msg + "\n\n"); - }; - } - - function getZotero() { - if (win.Zotero) Zotero = win.Zotero; - - if (typeof Zotero === 'undefined') { - try { - Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports).wrappedJSObject; - } catch (e) {} - } - return Zotero || {}; +const { + Loader, + Require, + resolveURI, + unload, +} = ChromeUtils.importESModule("resource://zotero/loader.sys.mjs"); + +const DEFAULT_SANDBOX_NAME = "Zotero (Module loader)"; + +var gNextLoaderID = 0; + +/** + * The main loader API. The standard instance of this loader is exported as + * |loader| below, but if a fresh copy of the loader is needed, then a new + * one can also be created. + * + * The two following boolean flags are used to control the sandboxes into + * which the modules are loaded. + * @param freshCompartment boolean + * If true, the modules will be forced to be loaded in a distinct + * compartment. It is typically used to load the modules in a distinct + * system compartment, different from the main one, which is shared by + * all ESMs, XPCOMs and modules loaded with this flag set to true. + * We use this in order to debug modules loaded in this shared system + * compartment. The debugger actor has to be running in a distinct + * compartment than the context it is debugging. + * @param useLoaderGlobal boolean + * If true, the loader will reuse the current global to load other + * modules instead of creating a sandbox with custom options. Cannot be + * used with freshCompartment. + */ +function ZoteroLoader({ + freshCompartment = false, + useLoaderGlobal = false, +} = {}) { + if (useLoaderGlobal && freshCompartment) { + throw new Error( + "Loader cannot use freshCompartment if useLoaderGlobal is true" + ); } - if (typeof win.console !== 'undefined') { - cons = console; - } - if (!cons) { - cons = {}; - for (let key of ['log', 'warn', 'error']) { - cons[key] = text => {getZotero(); typeof Zotero !== 'undefined' && false && Zotero.debug(`console.${key}: ${text}`)}; - } - } - if (!win.console) { - win.console = cons; - } - let globals = { - window: win, - document: typeof win.document !== 'undefined' && win.document || {}, - console: cons, - navigator: typeof win.navigator !== 'undefined' && win.navigator || {}, - setTimeout: win.setTimeout, - clearTimeout: win.clearTimeout, - requestAnimationFrame: win.setTimeout, - cancelAnimationFrame: win.clearTimeout, - IOUtils: IOUtils, - PathUtils: PathUtils, - TextEncoder: TextEncoder, - TextDecoder: TextDecoder, + const paths = { + '': 'resource://zotero/', + 'containers/': 'chrome://zotero/content/containers/', + 'components/': 'chrome://zotero/content/components/', + 'zotero/': 'chrome://zotero/content/' }; - Object.defineProperty(globals, 'Zotero', { get: getZotero }); - var loader = Loader({ - id: 'zotero/require', - paths: { - '': 'resource://zotero/', - 'containers/': 'chrome://zotero/content/containers/', - 'components/': 'chrome://zotero/content/components/', - 'zotero/': 'chrome://zotero/content/', + + // In case the Loader ESM is loaded in the existing global, + // also reuse this global for all CommonJS modules. + const sharedGlobal = + useLoaderGlobal || + // eslint-disable-next-line mozilla/reject-globalThis-modification + Cu.getRealmLocation(globalThis) == "Zotero global" + ? Cu.getGlobalForObject({}) + : undefined; + this.loader = new Loader({ + paths, + sharedGlobal, + freshCompartment, + sandboxName: useLoaderGlobal + ? "Zotero (Server Module Loader)" + : DEFAULT_SANDBOX_NAME, + // Make sure `define` function exists. JSON Viewer needs modules in AMD + // format, as it currently uses RequireJS from a content document and + // can't access our usual loaders. So, any modules shared with the JSON + // Viewer should include a define wrapper: + // + // // Make this available to both AMD and CJS environments + // define(function(require, exports, module) { + // ... code ... + // }); + // + supportAMDModules: true, + requireHook: (id, require) => { + // if (id.startsWith("raw!") || id.startsWith("theme-loader!")) { + // return requireRawId(id, require); + // } + return require(id); }, - globals }); - let require = Require(loader, requirer); - return require; -})(); + + this.require = Require(this.loader, { id: "zotero" }); + + // Various globals are available from ESM, but not from sandboxes, + // inject them into the globals list. + // Changes here should be mirrored to .eslintrc. + const injectedGlobals = { + BrowsingContext, + CanonicalBrowsingContext, + ChromeWorker, + console, + DebuggerNotificationObserver, + DOMPoint, + DOMQuad, + DOMRect, + fetch, + HeapSnapshot, + IOUtils, + L10nRegistry, + Localization, + NamedNodeMap, + NodeFilter, + PathUtils, + Services, + StructuredCloneHolder, + WebExtensionPolicy, + WebSocket, + WindowGlobalChild, + WindowGlobalParent, + }; + for (const name in injectedGlobals) { + this.loader.globals[name] = injectedGlobals[name]; + } + + // Fetch custom pseudo modules and globals + const { modules, globals } = { + // TODO: TEMP: Stub this out + modules: {}, + globals: {}, + } + + // Register custom pseudo modules to the current loader instance + for (const id in modules) { + const uri = resolveURI(id, this.loader.mapping); + this.loader.modules[uri] = { + get exports() { + return modules[id]; + }, + }; + } + + // Register custom globals to the current loader instance + Object.defineProperties( + this.loader.sharedGlobal, + Object.getOwnPropertyDescriptors(globals) + ); + + this.id = gNextLoaderID++; +} + +ZoteroLoader.prototype = { + destroy(reason = "shutdown") { + unload(this.loader, reason); + delete this.loader; + }, +}; + +// Export the standard instance of ZoteroLoader used by the tools. +// TODO: Zotero: Not making require.js an ESM for now, so this isn't exposed +// Should it be? +let loader = new ZoteroLoader(); + +var require = loader.require;