fx140: Add working require.js, more progress toward startup

This commit is contained in:
Abe Jellinek 2025-06-30 16:45:41 -04:00 committed by Dan Stillman
parent 14f4881d04
commit c506e41072
10 changed files with 923 additions and 1509 deletions

View file

@ -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",

View file

@ -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) {

View file

@ -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)

View file

@ -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",

View file

@ -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;
}

File diff suppressed because it is too large Load diff

653
resource/loader.sys.mjs Normal file
View file

@ -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;
}

View file

@ -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;
}));

106
resource/pathparser.mjs Normal file
View file

@ -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;
}
};
})();

View file

@ -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;