From 7f3019ccf2557112b7bb67e15173bb148a23f160 Mon Sep 17 00:00:00 2001 From: Tomislav Jovanovic Date: Thu, 21 Dec 2023 14:00:33 +0000 Subject: [PATCH] Bug 1869678 - Use more idiomatic code patterns for better type inference r=robwu These fall under 5 main categories: 1) Declare and/or initialize all class fiels in the constructor. (general good practise) 2) Use real getters and redefineGetter instead of defineLazyGetter. (also keeps related code closer together) 3) When subclassing, don't override class fields with getters (or vice versa). https://github.com/microsoft/TypeScript/pull/33509 4) Declare and assign object literals at the same time, not separatelly. (don't use `let foo;` at the top of the file, use `var foo = {`) 5) Don't re-use local variables unnecesarily with different types. (general good practise, local variables are "free") Differential Revision: https://phabricator.services.mozilla.com/D196386 --- toolkit/components/extensions/.eslintrc.js | 18 +- .../extensions/ConduitsParent.sys.mjs | 8 +- .../components/extensions/Extension.sys.mjs | 79 ++++--- .../extensions/ExtensionActions.sys.mjs | 1 + .../extensions/ExtensionChild.sys.mjs | 21 +- .../extensions/ExtensionCommon.sys.mjs | 92 +++++--- .../extensions/ExtensionContent.sys.mjs | 62 +++--- .../extensions/ExtensionDNR.sys.mjs | 22 +- .../extensions/ExtensionDNRStore.sys.mjs | 12 +- .../extensions/ExtensionPageChild.sys.mjs | 61 +++--- .../extensions/ExtensionParent.sys.mjs | 199 ++++++++---------- .../extensions/ExtensionShortcuts.sys.mjs | 12 +- .../extensions/ExtensionStorageIDB.sys.mjs | 8 +- .../ExtensionStorageSyncKinto.sys.mjs | 1 + .../extensions/ExtensionTelemetry.sys.mjs | 2 +- .../extensions/ExtensionTestCommon.sys.mjs | 6 +- .../extensions/ExtensionWorkerChild.sys.mjs | 39 ++-- .../extensions/ExtensionXPCShellUtils.sys.mjs | 49 +++-- .../extensions/NativeMessaging.sys.mjs | 33 ++- toolkit/components/extensions/Schemas.sys.mjs | 34 ++- 20 files changed, 404 insertions(+), 355 deletions(-) diff --git a/toolkit/components/extensions/.eslintrc.js b/toolkit/components/extensions/.eslintrc.js index f62ff67ff0696..777ca9bf56d1a 100644 --- a/toolkit/components/extensions/.eslintrc.js +++ b/toolkit/components/extensions/.eslintrc.js @@ -56,8 +56,22 @@ module.exports = { }, ], - // No using variables before defined - "no-use-before-define": "error", + // No using things before they're defined. + "no-use-before-define": [ + "error", + { + allowNamedExports: true, + classes: true, + // The next two being false allows idiomatic patterns which are more + // type-inference friendly. Functions are hoisted, so this is safe. + functions: false, + // This flag is only meaningful for `var` declarations. + // When false, it still disallows use-before-define in the same scope. + // Since we only allow `var` at the global scope, this is no worse than + // how we currently declare an uninitialized `let` at the top of file. + variables: false, + }, + ], // Disallow using variables outside the blocks they are defined (especially // since only let and const are used, see "no-var"). diff --git a/toolkit/components/extensions/ConduitsParent.sys.mjs b/toolkit/components/extensions/ConduitsParent.sys.mjs index e0debfd1876a0..3342315323823 100644 --- a/toolkit/components/extensions/ConduitsParent.sys.mjs +++ b/toolkit/components/extensions/ConduitsParent.sys.mjs @@ -452,13 +452,13 @@ export class ConduitsParent extends JSWindowActorParent { return Hub.recvConduitOpened(arg, this); } - sender = Hub.remotes.get(sender); - if (!sender || sender.actor !== this) { + let remote = Hub.remotes.get(sender); + if (!remote || remote.actor !== this) { throw new Error(`Unknown sender or wrong actor for recv${name}`); } if (name === "ConduitClosed") { - return Hub.recvConduitClosed(sender); + return Hub.recvConduitClosed(remote); } let conduit = Hub.byMethod.get(name); @@ -466,7 +466,7 @@ export class ConduitsParent extends JSWindowActorParent { throw new Error(`Parent conduit for recv${name} not found`); } - return conduit._recv(name, arg, { actor: this, query, sender }); + return conduit._recv(name, arg, { actor: this, query, sender: remote }); } /** diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs index 164c0bfc461d1..7aafa867be080 100644 --- a/toolkit/components/extensions/Extension.sys.mjs +++ b/toolkit/components/extensions/Extension.sys.mjs @@ -172,7 +172,7 @@ export { Management }; const { getUniqueId, promiseTimeout } = ExtensionUtils; -const { EventEmitter, updateAllowedOrigins } = ExtensionCommon; +const { EventEmitter, redefineGetter, updateAllowedOrigins } = ExtensionCommon; ChromeUtils.defineLazyGetter( lazy, @@ -869,6 +869,19 @@ const manifestTypes = new Map([ * `loadManifest` has been called, and completed. */ export class ExtensionData { + /** + * Note: These fields are only available and meant to be used on Extension + * instances, declared here because methods from this class reference them. + */ + /** @type {object} TODO: move to the Extension class, bug 1871094. */ + addonData; + /** @type {nsIURI} */ + baseURI; + /** @type {nsIPrincipal} */ + principal; + /** @type {boolean} */ + temporarilyInstalled; + constructor(rootURI, isPrivileged = false) { this.rootURI = rootURI; this.resourceURL = rootURI.spec; @@ -2676,7 +2689,7 @@ class BootstrapScope { // APP_STARTED. In some situations, such as background and // persisted listeners, we also need to know that the addon // was updated. - this.updateReason = this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]; + this.updateReason = BootstrapScope.BOOTSTRAP_REASON_MAP[reason]; // Retain any previously granted permissions that may have migrated // into the optional list. if (data.oldPermissions) { @@ -2703,7 +2716,7 @@ class BootstrapScope { // eslint-disable-next-line no-use-before-define this.extension = new Extension( data, - this.BOOTSTRAP_REASON_TO_STRING_MAP[reason], + BootstrapScope.BOOTSTRAP_REASON_MAP[reason], this.updateReason ); return this.extension.startup(); @@ -2711,31 +2724,27 @@ class BootstrapScope { async shutdown(data, reason) { let result = await this.extension.shutdown( - this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] + BootstrapScope.BOOTSTRAP_REASON_MAP[reason] ); this.extension = null; return result; } -} -ChromeUtils.defineLazyGetter( - BootstrapScope.prototype, - "BOOTSTRAP_REASON_TO_STRING_MAP", - () => { - const { BOOTSTRAP_REASONS } = lazy.AddonManagerPrivate; - - return Object.freeze({ - [BOOTSTRAP_REASONS.APP_STARTUP]: "APP_STARTUP", - [BOOTSTRAP_REASONS.APP_SHUTDOWN]: "APP_SHUTDOWN", - [BOOTSTRAP_REASONS.ADDON_ENABLE]: "ADDON_ENABLE", - [BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE", - [BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL", - [BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL", - [BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE", - [BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE", + static get BOOTSTRAP_REASON_MAP() { + const BR = lazy.AddonManagerPrivate.BOOTSTRAP_REASONS; + const value = Object.freeze({ + [BR.APP_STARTUP]: "APP_STARTUP", + [BR.APP_SHUTDOWN]: "APP_SHUTDOWN", + [BR.ADDON_ENABLE]: "ADDON_ENABLE", + [BR.ADDON_DISABLE]: "ADDON_DISABLE", + [BR.ADDON_INSTALL]: "ADDON_INSTALL", + [BR.ADDON_UNINSTALL]: "ADDON_UNINSTALL", + [BR.ADDON_UPGRADE]: "ADDON_UPGRADE", + [BR.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE", }); + return redefineGetter(this, "BOOTSTRAP_REASON_TO_STRING_MAP", value); } -); +} class DictionaryBootstrapScope extends BootstrapScope { install(data, reason) {} @@ -2744,11 +2753,11 @@ class DictionaryBootstrapScope extends BootstrapScope { startup(data, reason) { // eslint-disable-next-line no-use-before-define this.dictionary = new Dictionary(data); - return this.dictionary.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + return this.dictionary.startup(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); } - shutdown(data, reason) { - this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + async shutdown(data, reason) { + this.dictionary.shutdown(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); this.dictionary = null; } } @@ -2756,16 +2765,16 @@ class DictionaryBootstrapScope extends BootstrapScope { class LangpackBootstrapScope extends BootstrapScope { install(data, reason) {} uninstall(data, reason) {} - update(data, reason) {} + async update(data, reason) {} startup(data, reason) { // eslint-disable-next-line no-use-before-define this.langpack = new Langpack(data); - return this.langpack.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + return this.langpack.startup(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); } - shutdown(data, reason) { - this.langpack.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + async shutdown(data, reason) { + this.langpack.shutdown(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); this.langpack = null; } } @@ -2779,12 +2788,12 @@ class SitePermissionBootstrapScope extends BootstrapScope { // eslint-disable-next-line no-use-before-define this.sitepermission = new SitePermission(data); return this.sitepermission.startup( - this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] + BootstrapScope.BOOTSTRAP_REASON_MAP[reason] ); } - shutdown(data, reason) { - this.sitepermission.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + async shutdown(data, reason) { + this.sitepermission.shutdown(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); this.sitepermission = null; } } @@ -2800,6 +2809,9 @@ let pendingExtensions = new Map(); * @augments ExtensionData */ export class Extension extends ExtensionData { + /** @type {Map>} */ + persistentListeners; + constructor(addonData, startupReason, updateReason) { super(addonData.resourceURI, addonData.isPrivileged); @@ -2832,6 +2844,7 @@ export class Extension extends ExtensionData { this.startupData = addonData.startupData || {}; this.startupReason = startupReason; this.updateReason = updateReason; + this.temporarilyInstalled = !!addonData.temporarilyInstalled; if ( updateReason || @@ -3077,10 +3090,6 @@ export class Extension extends ExtensionData { return [this.id, this.version, Services.locale.appLocaleAsBCP47]; } - get temporarilyInstalled() { - return !!this.addonData.temporarilyInstalled; - } - saveStartupData() { if (this.dontSaveStartupData) { return; diff --git a/toolkit/components/extensions/ExtensionActions.sys.mjs b/toolkit/components/extensions/ExtensionActions.sys.mjs index 33a5d10072190..8e8cf3abd2854 100644 --- a/toolkit/components/extensions/ExtensionActions.sys.mjs +++ b/toolkit/components/extensions/ExtensionActions.sys.mjs @@ -43,6 +43,7 @@ class PanelActionBase { enabled: true, title: options.default_title || extension.name, popup: options.default_popup || "", + icon: null, }; this.globals = Object.create(this.defaults); diff --git a/toolkit/components/extensions/ExtensionChild.sys.mjs b/toolkit/components/extensions/ExtensionChild.sys.mjs index 91bc92bed60f2..a06ac8024f11f 100644 --- a/toolkit/components/extensions/ExtensionChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionChild.sys.mjs @@ -39,7 +39,7 @@ import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { DefaultMap, ExtensionError, LimitedSet, getUniqueId } = ExtensionUtils; const { - defineLazyGetter, + redefineGetter, EventEmitter, EventManager, LocalAPIImplementation, @@ -299,12 +299,13 @@ class Port { } throw new this.context.Error("Attempt to postMessage on disconnected port"); } -} -defineLazyGetter(Port.prototype, "api", function () { - let api = this.getAPI(); - return Cu.cloneInto(api, this.context.cloneScope, { cloneFunctions: true }); -}); + get api() { + const scope = this.context.cloneScope; + const value = Cu.cloneInto(this.getAPI(), scope, { cloneFunctions: true }); + return redefineGetter(this, "api", value); + } +} /** * Each extension context gets its own Messenger object. It handles the @@ -522,7 +523,7 @@ class BrowserExtensionContent extends EventEmitter { emit(event, ...args) { Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, { event, args }); - super.emit(event, ...args); + return super.emit(event, ...args); } // TODO(Bug 1768471): consider folding this back into emit if we will change it to @@ -914,10 +915,10 @@ class ChildAPIManager { * hasListener methods. See SchemaAPIInterface for documentation. */ getParentEvent(path) { - path = path.split("."); + let parts = path.split("."); - let name = path.pop(); - let namespace = path.join("."); + let name = parts.pop(); + let namespace = parts.join("."); let impl = new ProxyAPIImplementation(namespace, name, this, true); return { diff --git a/toolkit/components/extensions/ExtensionCommon.sys.mjs b/toolkit/components/extensions/ExtensionCommon.sys.mjs index 517fedf0e09c0..e4cd09c0c884f 100644 --- a/toolkit/components/extensions/ExtensionCommon.sys.mjs +++ b/toolkit/components/extensions/ExtensionCommon.sys.mjs @@ -54,8 +54,6 @@ function getConsole() { }); } -export var ExtensionCommon; - // Run a function and report exceptions. function runSafeSyncWithoutClone(f, ...args) { try { @@ -122,6 +120,8 @@ function withHandlingUserInput(window, callable) { * prototype will be invoked separately for each object instance that * it's accessed on. * + * Note: for better type inference, prefer redefineGetter() below. + * * @param {object} object * The prototype object on which to define the getter. * @param {string | symbol} prop @@ -131,30 +131,39 @@ function withHandlingUserInput(window, callable) { * value. */ function defineLazyGetter(object, prop, getter) { - let redefine = (obj, value) => { - Object.defineProperty(obj, prop, { - enumerable: true, - configurable: true, - writable: true, - value, - }); - return value; - }; - Object.defineProperty(object, prop, { enumerable: true, configurable: true, - get() { - return redefine(this, getter.call(this)); + return redefineGetter(this, prop, getter.call(this), true); }, - set(value) { - redefine(this, value); + redefineGetter(this, prop, value, true); }, }); } +/** + * A more type-inference friendly version of defineLazyGetter() above. + * Call it from a real getter (and setter) for your class or object. + * On first run, it will redefine the property with the final value. + * + * @template Value + * @param {object} object + * @param {string | symbol} key + * @param {Value} value + * @returns {Value} + */ +function redefineGetter(object, key, value, writable = false) { + Object.defineProperty(object, key, { + enumerable: true, + configurable: true, + writable, + value, + }); + return value; +} + function checkLoadURI(uri, principal, options) { let ssm = Services.scriptSecurityManager; @@ -292,11 +301,11 @@ class EventEmitter { * The listener to call when events are emitted. */ once(event, listener) { - let wrapper = (...args) => { + let wrapper = (event, ...args) => { this.off(event, wrapper); this[ONCE_MAP].delete(listener); - return listener(...args); + return listener(event, ...args); }; this[ONCE_MAP].set(listener, wrapper); @@ -360,11 +369,28 @@ class ExtensionAPI extends EventEmitter { destroy() {} - onManifestEntry(entry) {} + /** @param {string} entryName */ + onManifestEntry(entryName) {} + /** @param {boolean} isAppShutdown */ + onShutdown(isAppShutdown) {} + + /** @param {BaseContext} context */ getAPI(context) { throw new Error("Not Implemented"); } + + /** @param {string} id */ + static onDisable(id) {} + + /** @param {string} id */ + static onUninstall(id) {} + + /** + * @param {string} id + * @param {Record} manifest + */ + static onUpdate(id, manifest) {} } /** @@ -374,6 +400,9 @@ class ExtensionAPI extends EventEmitter { * this.apiNamespace = class extends ExtensionAPIPersistent {}; */ class ExtensionAPIPersistent extends ExtensionAPI { + /** @type {Record} */ + PERSISTENT_EVENTS; + /** * Check for event entry. * @@ -440,6 +469,11 @@ class ExtensionAPIPersistent extends ExtensionAPI { * @abstract */ class BaseContext { + /** @type {boolean} */ + isTopContext; + /** @type {string} */ + viewType; + constructor(envType, extension) { this.envType = envType; this.onClose = new Set(); @@ -1895,6 +1929,7 @@ class LazyAPIManager extends SchemaAPIManager { constructor(processType, moduleData, schemaURLs) { super(processType); + /** @type {Promise | boolean} */ this.initialized = false; this.initModuleData(moduleData); @@ -1979,7 +2014,7 @@ defineLazyGetter(MultiAPIManager.prototype, "schema", function () { return new lazy.SchemaRoot(bases, new Map()); }); -function LocaleData(data) { +export function LocaleData(data) { this.defaultLocale = data.defaultLocale; this.selectedLocale = data.selectedLocale; this.locales = data.locales || new Map(); @@ -2186,15 +2221,13 @@ LocaleData.prototype = { get uiLocale() { return Services.locale.appLocaleAsBCP47; }, -}; -defineLazyGetter(LocaleData.prototype, "availableLocales", function () { - return new Set( - [this.BUILTIN, this.selectedLocale, this.defaultLocale].filter(locale => - this.messages.has(locale) - ) - ); -}); + get availableLocales() { + const locales = [this.BUILTIN, this.selectedLocale, this.defaultLocale]; + const value = new Set(locales.filter(locale => this.messages.has(locale))); + return redefineGetter(this, "availableLocales", value); + }, +}; /** * This is a generic class for managing event listeners. @@ -3012,7 +3045,7 @@ function updateAllowedOrigins(policy, origins, isAdd) { policy.allowedOrigins = new MatchPatternSet(Array.from(patternMap.values())); } -ExtensionCommon = { +export var ExtensionCommon = { BaseContext, CanOfAPIs, EventManager, @@ -3028,6 +3061,7 @@ ExtensionCommon = { checkLoadURI, checkLoadURL, defineLazyGetter, + redefineGetter, getConsole, ignoreEvent, instanceOf, diff --git a/toolkit/components/extensions/ExtensionContent.sys.mjs b/toolkit/components/extensions/ExtensionContent.sys.mjs index 9b67de4360a22..131d555bf0adf 100644 --- a/toolkit/components/extensions/ExtensionContent.sys.mjs +++ b/toolkit/components/extensions/ExtensionContent.sys.mjs @@ -59,6 +59,7 @@ const { CanOfAPIs, SchemaAPIManager, defineLazyGetter, + redefineGetter, runSafeSyncWithoutClone, } = ExtensionCommon; @@ -142,7 +143,7 @@ class CacheMap extends DefaultMap { super.get(url).timer.cancel(); } - super.delete(url); + return super.delete(url); } clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) { @@ -160,16 +161,17 @@ class CacheMap extends DefaultMap { class ScriptCache extends CacheMap { constructor(options, extension) { - super(SCRIPT_EXPIRY_TIMEOUT_MS, null, extension); - this.options = options; - } - - defaultConstructor(url) { - let promise = ChromeUtils.compileScript(url, this.options); - promise.then(script => { - promise.script = script; - }); - return promise; + super( + SCRIPT_EXPIRY_TIMEOUT_MS, + url => { + let promise = ChromeUtils.compileScript(url, options); + promise.then(script => { + promise.script = script; + }); + return promise; + }, + extension + ); } } @@ -207,7 +209,7 @@ class BaseCSSCache extends CacheMap { } } - super.delete(key); + return super.delete(key); } } @@ -907,13 +909,6 @@ class ContentScriptContextChild extends BaseContext { this.url = contentWindow.location.href; - defineLazyGetter(this, "chromeObj", () => { - let chromeObj = Cu.createObjectIn(this.sandbox); - - this.childManager.inject(chromeObj); - return chromeObj; - }); - lazy.Schemas.exportLazyGetter( this.sandbox, "browser", @@ -993,31 +988,28 @@ class ContentScriptContextChild extends BaseContext { this.sandbox = null; } -} - -defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function () { - return new Messenger(this); -}); -defineLazyGetter( - ContentScriptContextChild.prototype, - "childManager", - function () { + get childManager() { apiManager.lazyInit(); - - let localApis = {}; - let can = new CanOfAPIs(this, apiManager, localApis); - + let can = new CanOfAPIs(this, apiManager, {}); let childManager = new ChildAPIManager(this, this.messageManager, can, { envType: "content_parent", url: this.url, }); - this.callOnClose(childManager); + return redefineGetter(this, "childManager", childManager); + } - return childManager; + get chromeObj() { + let chromeObj = Cu.createObjectIn(this.sandbox); + this.childManager.inject(chromeObj); + return redefineGetter(this, "chromeObj", chromeObj); } -); + + get messenger() { + return redefineGetter(this, "messenger", new Messenger(this)); + } +} // Responsible for creating ExtensionContexts and injecting content // scripts into them when new documents are created. diff --git a/toolkit/components/extensions/ExtensionDNR.sys.mjs b/toolkit/components/extensions/ExtensionDNR.sys.mjs index b6d1f68f4659a..2757333b3b419 100644 --- a/toolkit/components/extensions/ExtensionDNR.sys.mjs +++ b/toolkit/components/extensions/ExtensionDNR.sys.mjs @@ -145,7 +145,7 @@ class RuleCondition { } } -class Rule { +export class Rule { constructor(rule) { this.id = rule.id; this.priority = rule.priority; @@ -633,6 +633,24 @@ class ModifyHeadersBase { this.channel = channel; } + /** + * @param {MatchedRule} matchedRule + * @returns {object[]} + */ + headerActionsFor(matchedRule) { + throw new Error("Not implemented."); + } + + /** + * @param {MatchedRule} matchedrule + * @param {string} name + * @param {string} value + * @param {boolean} merge + */ + setHeaderImpl(matchedrule, name, value, merge) { + throw new Error("Not implemented."); + } + /** @param {MatchedRule[]} matchedRules */ applyModifyHeaders(matchedRules) { for (const matchedRule of matchedRules) { @@ -1190,7 +1208,7 @@ class RuleValidator { } } -class RuleQuotaCounter { +export class RuleQuotaCounter { constructor(isStaticRulesets) { this.isStaticRulesets = isStaticRulesets; this.ruleLimitName = isStaticRulesets diff --git a/toolkit/components/extensions/ExtensionDNRStore.sys.mjs b/toolkit/components/extensions/ExtensionDNRStore.sys.mjs index 2e8777cc4281e..0dd6fad75a6cb 100644 --- a/toolkit/components/extensions/ExtensionDNRStore.sys.mjs +++ b/toolkit/components/extensions/ExtensionDNRStore.sys.mjs @@ -148,7 +148,7 @@ class StoreData { if (!(extension instanceof lazy.Extension)) { throw new Error("Missing mandatory extension parameter"); } - this.schemaVersion = schemaVersion || this.constructor.VERSION; + this.schemaVersion = schemaVersion || StoreData.VERSION; this.extVersion = extVersion ?? extension.version; this.#extUUID = extension.uuid; // Used to skip storing the data in the startupCache or storing the lastUpdateTag in @@ -220,8 +220,8 @@ class StoreData { } #updateRulesets({ - staticRulesets, - dynamicRuleset, + staticRulesets = null, + dynamicRuleset = null, lastUpdateTag = Services.uuid.generateUUID().toString(), } = {}) { if (staticRulesets) { @@ -667,11 +667,7 @@ class RulesetsStore { * @returns {StoreData} */ #getDefaults(extension) { - return new StoreData(extension, { - extUUID: extension.uuid, - extVersion: extension.version, - temporarilyInstalled: extension.temporarilyInstalled, - }); + return new StoreData(extension, { extVersion: extension.version }); } /** diff --git a/toolkit/components/extensions/ExtensionPageChild.sys.mjs b/toolkit/components/extensions/ExtensionPageChild.sys.mjs index 7c051c511e925..d84459f1edb9a 100644 --- a/toolkit/components/extensions/ExtensionPageChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionPageChild.sys.mjs @@ -29,13 +29,11 @@ import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { getInnerWindowID, promiseEvent } = ExtensionUtils; -const { BaseContext, CanOfAPIs, SchemaAPIManager, defineLazyGetter } = +const { BaseContext, CanOfAPIs, SchemaAPIManager, redefineGetter } = ExtensionCommon; const { ChildAPIManager, Messenger } = ExtensionChild; -export var ExtensionPageChild; - const initializeBackgroundPage = context => { // Override the `alert()` method inside background windows; // we alias it to console.log(). @@ -185,7 +183,7 @@ export function getContextChildManagerGetter( }; } -class ExtensionBaseContextChild extends BaseContext { +export class ExtensionBaseContextChild extends BaseContext { /** * This ExtensionBaseContextChild represents an addon execution environment * that is running in an addon or devtools child process. @@ -220,12 +218,6 @@ class ExtensionBaseContextChild extends BaseContext { }); } - defineLazyGetter(this, "browserObj", () => { - let browserObj = Cu.createObjectIn(contentWindow); - this.childManager.inject(browserObj); - return browserObj; - }); - lazy.Schemas.exportLazyGetter(contentWindow, "browser", () => { return this.browserObj; }); @@ -245,6 +237,12 @@ class ExtensionBaseContextChild extends BaseContext { }); } + get browserObj() { + const browserObj = Cu.createObjectIn(this.contentWindow); + this.childManager.inject(browserObj); + return redefineGetter(this, "browserObj", browserObj); + } + logActivity(type, name, data) { ExtensionActivityLogChild.log(this, type, name, data); } @@ -283,11 +281,16 @@ class ExtensionBaseContextChild extends BaseContext { super.unload(); } -} -defineLazyGetter(ExtensionBaseContextChild.prototype, "messenger", function () { - return new Messenger(this); -}); + get messenger() { + return redefineGetter(this, "messenger", new Messenger(this)); + } + + /** @type {ReturnType>} */ + get childManager() { + throw new Error("childManager getter must be overridden"); + } +} class ExtensionPageContextChild extends ExtensionBaseContextChild { /** @@ -322,15 +325,16 @@ class ExtensionPageContextChild extends ExtensionBaseContextChild { super.unload(); this.extension.views.delete(this); } -} -defineLazyGetter( - ExtensionPageContextChild.prototype, - "childManager", - getContextChildManagerGetter({ envType: "addon_parent" }) -); + get childManager() { + const childManager = getContextChildManagerGetter({ + envType: "addon_parent", + }).call(this); + return redefineGetter(this, "childManager", childManager); + } +} -class DevToolsContextChild extends ExtensionBaseContextChild { +export class DevToolsContextChild extends ExtensionBaseContextChild { /** * This DevToolsContextChild represents a devtools-related addon execution * environment that has access to the devtools API namespace and to the same subset @@ -361,15 +365,16 @@ class DevToolsContextChild extends ExtensionBaseContextChild { super.unload(); this.extension.devtoolsViews.delete(this); } -} -defineLazyGetter( - DevToolsContextChild.prototype, - "childManager", - getContextChildManagerGetter({ envType: "devtools_parent" }) -); + get childManager() { + const childManager = getContextChildManagerGetter({ + envType: "devtools_parent", + }).call(this); + return redefineGetter(this, "childManager", childManager); + } +} -ExtensionPageChild = { +export var ExtensionPageChild = { initialized: false, // Map diff --git a/toolkit/components/extensions/ExtensionParent.sys.mjs b/toolkit/components/extensions/ExtensionParent.sys.mjs index 1028ac6212415..da90a62954da7 100644 --- a/toolkit/components/extensions/ExtensionParent.sys.mjs +++ b/toolkit/components/extensions/ExtensionParent.sys.mjs @@ -46,7 +46,7 @@ const DUMMY_PAGE_URI = Services.io.newURI( "chrome://extensions/content/dummy.xhtml" ); -var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, defineLazyGetter } = +var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, redefineGetter } = ExtensionCommon; var { @@ -72,7 +72,6 @@ schemaURLs.add("chrome://extensions/content/schemas/experiments.json"); let GlobalManager; let ParentAPIManager; -let StartupCache; function verifyActorForContext(actor, context) { if (JSWindowActorParent.isInstance(actor)) { @@ -664,26 +663,25 @@ class ProxyContextParent extends BaseContext { super.unload(); apiManager.emit("proxy-context-unload", this); } -} -defineLazyGetter(ProxyContextParent.prototype, "apiCan", function () { - let obj = {}; - let can = new CanOfAPIs(this, this.extension.apiManager, obj); - return can; -}); + get apiCan() { + const apiCan = new CanOfAPIs(this, this.extension.apiManager, {}); + return redefineGetter(this, "apiCan", apiCan); + } -defineLazyGetter(ProxyContextParent.prototype, "apiObj", function () { - return this.apiCan.root; -}); + get apiObj() { + return redefineGetter(this, "apiObj", this.apiCan.root); + } -defineLazyGetter(ProxyContextParent.prototype, "sandbox", function () { - // NOTE: the required Blob and URL globals are used in the ext-registerContentScript.js - // API module to convert JS and CSS data into blob URLs. - return Cu.Sandbox(this.principal, { - sandboxName: this.uri.spec, - wantGlobalProperties: ["Blob", "URL"], - }); -}); + get sandbox() { + // Note: Blob and URL globals are used in ext-contentScripts.js. + const sandbox = Cu.Sandbox(this.principal, { + sandboxName: this.uri.spec, + wantGlobalProperties: ["Blob", "URL"], + }); + return redefineGetter(this, "sandbox", sandbox); + } +} /** * The parent side of proxied API context for extension content script @@ -728,11 +726,6 @@ class ExtensionPageContextParent extends ProxyContextParent { } } - onBrowserChange(browser) { - super.onBrowserChange(browser); - this.xulBrowser = browser; - } - unload() { super.unload(); this.extension.views.delete(this); @@ -1404,7 +1397,7 @@ class HiddenXULWindow { } } - let awaitFrameLoader = Promise.resolve(); + let awaitFrameLoader; if (browser.getAttribute("remote") === "true") { awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated"); @@ -2055,20 +2048,76 @@ let IconDetails = { }, }; +class CacheStore { + constructor(storeName) { + this.storeName = storeName; + } + + async getStore(path = null) { + let data = await StartupCache.dataPromise; + + let store = data.get(this.storeName); + if (!store) { + store = new Map(); + data.set(this.storeName, store); + } + + let key = path; + if (Array.isArray(path)) { + for (let elem of path.slice(0, -1)) { + let next = store.get(elem); + if (!next) { + next = new Map(); + store.set(elem, next); + } + store = next; + } + key = path[path.length - 1]; + } + + return [store, key]; + } + + async get(path, createFunc) { + let [store, key] = await this.getStore(path); + + let result = store.get(key); + + if (result === undefined) { + result = await createFunc(path); + store.set(key, result); + StartupCache.save(); + } + + return result; + } + + async set(path, value) { + let [store, key] = await this.getStore(path); + + store.set(key, value); + StartupCache.save(); + } + + async getAll() { + let [store] = await this.getStore(); + + return new Map(store); + } + + async delete(path) { + let [store, key] = await this.getStore(path); + + if (store.delete(key)) { + StartupCache.save(); + } + } +} + // A cache to support faster initialization of extensions at browser startup. // All cached data is removed when the browser is updated. // Extension-specific data is removed when the add-on is updated. -StartupCache = { - STORE_NAMES: Object.freeze([ - "general", - "locales", - "manifests", - "other", - "permissions", - "schemas", - "menus", - ]), - +var StartupCache = { _ensureDirectoryPromise: null, _saveTask: null, @@ -2179,80 +2228,18 @@ StartupCache = { delete(extension, path) { return this.general.delete([extension.id, extension.version, ...path]); }, + + general: new CacheStore("general"), + locales: new CacheStore("locales"), + manifests: new CacheStore("manifests"), + other: new CacheStore("other"), + permissions: new CacheStore("permissions"), + schemas: new CacheStore("schemas"), + menus: new CacheStore("menus"), }; Services.obs.addObserver(StartupCache, "startupcache-invalidate"); -class CacheStore { - constructor(storeName) { - this.storeName = storeName; - } - - async getStore(path = null) { - let data = await StartupCache.dataPromise; - - let store = data.get(this.storeName); - if (!store) { - store = new Map(); - data.set(this.storeName, store); - } - - let key = path; - if (Array.isArray(path)) { - for (let elem of path.slice(0, -1)) { - let next = store.get(elem); - if (!next) { - next = new Map(); - store.set(elem, next); - } - store = next; - } - key = path[path.length - 1]; - } - - return [store, key]; - } - - async get(path, createFunc) { - let [store, key] = await this.getStore(path); - - let result = store.get(key); - - if (result === undefined) { - result = await createFunc(path); - store.set(key, result); - StartupCache.save(); - } - - return result; - } - - async set(path, value) { - let [store, key] = await this.getStore(path); - - store.set(key, value); - StartupCache.save(); - } - - async getAll() { - let [store] = await this.getStore(); - - return new Map(store); - } - - async delete(path) { - let [store, key] = await this.getStore(path); - - if (store.delete(key)) { - StartupCache.save(); - } - } -} - -for (let name of StartupCache.STORE_NAMES) { - StartupCache[name] = new CacheStore(name); -} - export var ExtensionParent = { GlobalManager, HiddenExtensionPage, diff --git a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs index 799cf516b8c32..7b30fa2cdfa73 100644 --- a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs +++ b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs @@ -108,7 +108,7 @@ export class ExtensionShortcutKeyMap extends DefaultMap { // Class internals. constructor() { - super(); + super(() => new Set()); // Overridden in some unit test to make it easier to cover some // platform specific behaviors (in particular the platform specific. @@ -116,10 +116,6 @@ export class ExtensionShortcutKeyMap extends DefaultMap { this._os = lazy.ExtensionParent.PlatformInfo.os; } - defaultConstructor() { - return new Set(); - } - getPlatformShortcutString(shortcutString) { if (this._os == "mac") { // when running on macos, make sure to also track in the shortcutKeyMap @@ -150,7 +146,7 @@ export class ExtensionShortcutKeyMap extends DefaultMap { delete(shortcutString) { const platformShortcut = this.getPlatformShortcutString(shortcutString); - super.delete(platformShortcut); + return super.delete(platformShortcut); } } @@ -397,7 +393,7 @@ export class ExtensionShortcuts { this.keysetsMap.get(window).remove(); } let sidebarKey; - commands.forEach((command, name) => { + for (let [name, command] of commands) { if (command.shortcut) { let parts = command.shortcut.split("+"); @@ -419,7 +415,7 @@ export class ExtensionShortcuts { sidebarKey = keyElement; } } - }); + } doc.documentElement.appendChild(keyset); if (sidebarKey) { window.SidebarUI.updateShortcut({ keyId: sidebarKey.id }); diff --git a/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs b/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs index 72a14ef9b223e..3f338aae9076f 100644 --- a/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs @@ -2,7 +2,6 @@ * 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/. */ -export let ExtensionStorageIDB; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { IndexedDB } from "resource://gre/modules/IndexedDB.sys.mjs"; @@ -238,10 +237,7 @@ class ExtensionStorageLocalIDB extends IndexedDB { // Explicitly create a transaction, so that we can explicitly abort it // as soon as one of the put requests fails. const transaction = this.transaction(IDB_DATA_STORENAME, "readwrite"); - const objectStore = transaction.objectStore( - IDB_DATA_STORENAME, - "readwrite" - ); + const objectStore = transaction.objectStore(IDB_DATA_STORENAME); const transactionCompleted = transaction.promiseComplete(); if (!serialize) { @@ -585,7 +581,7 @@ async function migrateJSONFileData(extension, storagePrincipal) { * This ExtensionStorage class implements a backend for the storage.local API which * uses IndexedDB to store the data. */ -ExtensionStorageIDB = { +export var ExtensionStorageIDB = { BACKEND_ENABLED_PREF, IDB_MIGRATED_PREF_BRANCH, IDB_MIGRATE_RESULT_HISTOGRAM, diff --git a/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs b/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs index 95200a835e3e3..d10b140f7ef17 100644 --- a/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs @@ -373,6 +373,7 @@ async function storageSyncInit() { } return storageSyncInit.promise; } +storageSyncInit.promise = undefined; // Kinto record IDs have two conditions: // diff --git a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs index 9cdc2ec5111b5..06137b9a23fa2 100644 --- a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs +++ b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs @@ -330,7 +330,7 @@ export var ExtensionTelemetry = new Proxy(metricsCache, { // telemetry histogram counterpart, we would need to change this check // accordingly. if (!(prop in HISTOGRAMS_IDS)) { - throw new Error(`Unknown metric ${prop}`); + throw new Error(`Unknown metric ${String(prop)}`); } // Lazily create and cache the metric result object. diff --git a/toolkit/components/extensions/ExtensionTestCommon.sys.mjs b/toolkit/components/extensions/ExtensionTestCommon.sys.mjs index 760eba7749f59..94cb801cc5edf 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.sys.mjs +++ b/toolkit/components/extensions/ExtensionTestCommon.sys.mjs @@ -36,8 +36,6 @@ const { flushJarCache } = ExtensionUtils; const { instanceOf } = ExtensionCommon; -export var ExtensionTestCommon; - /** * A skeleton Extension-like object, used for testing, which installs an * add-on via the add-on manager when startup() is called, and @@ -226,7 +224,7 @@ const ExtensionTestAssertions = { const { persistentListeners } = extension; if ( - !persistentListeners?.size > 0 || + !persistentListeners?.size || !persistentListeners.get(apiNs)?.has(apiEvent) ) { return []; @@ -283,7 +281,7 @@ const ExtensionTestAssertions = { }, }; -ExtensionTestCommon = class ExtensionTestCommon { +export var ExtensionTestCommon = class ExtensionTestCommon { static get testAssertions() { return ExtensionTestAssertions; } diff --git a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs index 77089492f5d82..f93f6968e93cd 100644 --- a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs @@ -21,7 +21,7 @@ import { } from "resource://gre/modules/ExtensionPageChild.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; -const { BaseContext, defineLazyGetter } = ExtensionCommon; +const { BaseContext, redefineGetter } = ExtensionCommon; const { ChildAPIManager, @@ -154,14 +154,12 @@ class WorkerPort extends Port { api.portId = this.portId; return api; } -} -defineLazyGetter(WorkerPort.prototype, "api", function () { - // No need to clone the API object for the worker, because it runs - // on a different JSRuntime and doesn't have direct access to this - // object. - return this.getAPI(); -}); + get api() { + // No need to clone this for the worker, it's on a separate JSRuntime. + return redefineGetter(this, "api", this.getAPI()); + } +} /** * A Messenger subclass specialized for the background service worker. @@ -697,20 +695,19 @@ class WorkerContextChild extends BaseContext { super.unload(); } -} -defineLazyGetter(WorkerContextChild.prototype, "messenger", function () { - return new WorkerMessenger(this); -}); - -defineLazyGetter( - WorkerContextChild.prototype, - "childManager", - getContextChildManagerGetter( - { envType: "addon_parent" }, - WebIDLChildAPIManager - ) -); + get childManager() { + const childManager = getContextChildManagerGetter( + { envType: "addon_parent" }, + WebIDLChildAPIManager + ).call(this); + return redefineGetter(this, "childManager", childManager); + } + + get messenger() { + return redefineGetter(this, "messenger", new WorkerMessenger(this)); + } +} export var ExtensionWorkerChild = { /** @type {Map} */ diff --git a/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs b/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs index 1ef9dcb56902a..08df6b3c24ffd 100644 --- a/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs +++ b/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs @@ -32,6 +32,13 @@ let BASE_MANIFEST = Object.freeze({ }); class ExtensionWrapper { + /** @type {AddonWrapper} */ + addon; + /** @type {Promise} */ + addonPromise; + /** @type {nsIFile[]} */ + cleanupFiles; + constructor(testScope, extension = null) { this.testScope = testScope; @@ -447,20 +454,13 @@ class AOMExtensionWrapper extends ExtensionWrapper { return super.unload(); } - async upgrade(data) { - this.startupPromise = new Promise(resolve => { - this.resolveStartup = resolve; - }); - this.state = "restarting"; - - await this._flushCache(); - - let xpiFile = lazy.ExtensionTestCommon.generateXPI(data); - - this.cleanupFiles.push(xpiFile); - - return this._install(xpiFile); - } + /** + * Override for subclasses which don't set an ID in the constructor. + * + * @param {nsIURI} uri + * @param {string} id + */ + maybeSetID(uri, id) {} } class InstallableWrapper extends AOMExtensionWrapper { @@ -574,6 +574,21 @@ class InstallableWrapper extends AOMExtensionWrapper { return this._install(this.file); } + + async upgrade(data) { + this.startupPromise = new Promise(resolve => { + this.resolveStartup = resolve; + }); + this.state = "restarting"; + + await this._flushCache(); + + let xpiFile = lazy.ExtensionTestCommon.generateXPI(data); + + this.cleanupFiles.push(xpiFile); + + return this._install(xpiFile); + } } class ExternallyInstalledWrapper extends AOMExtensionWrapper { @@ -587,8 +602,6 @@ class ExternallyInstalledWrapper extends AOMExtensionWrapper { this.state = "restarting"; } - - maybeSetID(uri, id) {} } export var ExtensionTestUtils = { @@ -769,7 +782,7 @@ export var ExtensionTestUtils = { * * @returns {XPCShellContentUtils.ContentPage} */ - loadContentPage(url, options, ...args) { - return XPCShellContentUtils.loadContentPage(url, options, ...args); + loadContentPage(url, options) { + return XPCShellContentUtils.loadContentPage(url, options); }, }; diff --git a/toolkit/components/extensions/NativeMessaging.sys.mjs b/toolkit/components/extensions/NativeMessaging.sys.mjs index a0d7536005435..dcd8fe7807892 100644 --- a/toolkit/components/extensions/NativeMessaging.sys.mjs +++ b/toolkit/components/extensions/NativeMessaging.sys.mjs @@ -40,7 +40,15 @@ const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; -export var NativeApp = class extends EventEmitter { +XPCOMUtils.defineLazyPreferenceGetter(lazy, "maxRead", PREF_MAX_READ, MAX_READ); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "maxWrite", + PREF_MAX_WRITE, + MAX_WRITE +); + +export class NativeApp extends EventEmitter { /** * @param {BaseContext} context The context that initiated the native app. * @param {string} application The identifier of the native app. @@ -153,7 +161,7 @@ export var NativeApp = class extends EventEmitter { static encodeMessage(context, message) { message = context.jsonStringify(message); let buffer = new TextEncoder().encode(message).buffer; - if (buffer.byteLength > NativeApp.maxWrite) { + if (buffer.byteLength > lazy.maxWrite) { throw new context.Error("Write too big"); } return buffer; @@ -174,9 +182,9 @@ export var NativeApp = class extends EventEmitter { this.readPromise = this.proc.stdout .readUint32() .then(len => { - if (len > NativeApp.maxRead) { + if (len > lazy.maxRead) { throw new ExtensionError( - `Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.` + `Native application tried to send a message of ${len} bytes, which exceeds the limit of ${lazy.maxRead} bytes.` ); } return this.proc.stdout.readJSON(len); @@ -265,7 +273,7 @@ export var NativeApp = class extends EventEmitter { let buffer = msg; - if (buffer.byteLength > NativeApp.maxWrite) { + if (buffer.byteLength > lazy.maxWrite) { throw new ExtensionError("Write too big"); } @@ -380,17 +388,4 @@ export var NativeApp = class extends EventEmitter { return result; } -}; - -XPCOMUtils.defineLazyPreferenceGetter( - NativeApp, - "maxRead", - PREF_MAX_READ, - MAX_READ -); -XPCOMUtils.defineLazyPreferenceGetter( - NativeApp, - "maxWrite", - PREF_MAX_WRITE, - MAX_WRITE -); +} diff --git a/toolkit/components/extensions/Schemas.sys.mjs b/toolkit/components/extensions/Schemas.sys.mjs index 8eb16a6d20dcd..ff7f926705e5d 100644 --- a/toolkit/components/extensions/Schemas.sys.mjs +++ b/toolkit/components/extensions/Schemas.sys.mjs @@ -39,8 +39,6 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); -export let Schemas; - const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content"; const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged"; @@ -378,9 +376,12 @@ class Context { localize(value, context) { return value; }, + ...params.preprocessors, }; + this.postprocessors = POSTPROCESSORS; - this.isChromeCompat = false; + this.isChromeCompat = params.isChromeCompat ?? false; + this.manifestVersion = params.manifestVersion; this.currentChoices = new Set(); this.choicePathIndex = 0; @@ -390,17 +391,6 @@ class Context { this[method] = params[method].bind(params); } } - - let props = ["isChromeCompat", "manifestVersion", "preprocessors"]; - for (let prop of props) { - if (prop in params) { - if (prop in this && typeof this[prop] == "object") { - Object.assign(this[prop], params[prop]); - } else { - this[prop] = params[prop]; - } - } - } } get choicePath() { @@ -1211,7 +1201,7 @@ const FORMATS = { // Our pattern just checks the format, we could still have invalid // values (e.g., month=99 or month=02 and day=31). Let the Date // constructor do the dirty work of validating. - if (isNaN(new Date(string))) { + if (isNaN(Date.parse(string))) { throw new Error(`Invalid date string ${string}`); } return string; @@ -2554,6 +2544,8 @@ class ValueProperty extends Entry { // Represents a "property" defined in a schema namespace that is not a // constant. class TypeProperty extends Entry { + unsupported = false; + constructor(schema, path, name, type, writable, permissions) { super(schema); this.path = path; @@ -2692,6 +2684,8 @@ class SubModuleProperty extends Entry { // care of validating parameter lists (i.e., handling of optional // parameters and parameter type checking). class CallEntry extends Entry { + hasAsyncCallback = false; + constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) { super(schema); this.path = path; @@ -3118,9 +3112,11 @@ class Namespace extends Map { this._lazySchemas.unshift(...this.superNamespace._lazySchemas); } - for (let type of Object.keys(LOADERS)) { - this[type] = new DefaultMap(() => []); - } + // Keep in sync with LOADERS above. + this.types = new DefaultMap(() => []); + this.properties = new DefaultMap(() => []); + this.functions = new DefaultMap(() => []); + this.events = new DefaultMap(() => []); for (let schema of this._lazySchemas) { for (let type of schema.types || []) { @@ -3652,7 +3648,7 @@ export class SchemaRoot extends Namespace { } } -Schemas = { +export var Schemas = { initialized: false, REVOKE: Symbol("@@revoke"),