Bug 1945464 - Design type-friendly XPCOM.declareLazy() r=Standard8,arai,mossop

Prototype of a type-friendly design for handling all of the different
kinds of lazy imports and getters.

Differential Revision: https://phabricator.services.mozilla.com/D238131
This commit is contained in:
zombie
2025-04-02 13:44:48 +00:00
parent 34cf835457
commit 499cbc99aa
4 changed files with 258 additions and 129 deletions

View File

@@ -119,13 +119,16 @@ export var XPCOMUtils = {
* The name of the getter to define on aObject for the service.
* @param {string} aContract
* The contract used to obtain the service.
* @param {string} aInterfaceName
* The name of the interface to query the service to.
* @param {nsID|string} aInterface
* The interface or name of interface to query the service to.
*/
defineLazyServiceGetter(aObject, aName, aContract, aInterfaceName) {
defineLazyServiceGetter(aObject, aName, aContract, aInterface) {
ChromeUtils.defineLazyGetter(aObject, aName, () => {
if (aInterfaceName) {
return Cc[aContract].getService(Ci[aInterfaceName]);
if (aInterface) {
if (typeof aInterface === "string") {
aInterface = Ci[aInterface];
}
return Cc[aContract].getService(aInterface);
}
return Cc[aContract].getService().wrappedJSObject;
});
@@ -283,6 +286,121 @@ export var XPCOMUtils = {
});
},
/**
* Defines properties on the given object which lazily import
* an ES module or run another utility getter when accessed.
*
* Use this version when you need to define getters on the
* global `this`, or any other object you can't assign to:
*
* @example
* XPCOMUtils.defineLazy(this, {
* AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
* verticalTabs: { pref: "sidebar.verticalTabs", default: false },
* MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService },
* expensiveThing: () => fetch_or_compute(),
* });
*
* Additionally, the given object is also returned, which enables
* type-friendly composition:
*
* @example
* const existing = {
* someProps: new Widget(),
* };
* const combined = XPCOMUtils.defineLazy(existing, {
* expensiveThing: () => fetch_or_compute(),
* });
*
* The `combined` variable is the same object reference as `existing`,
* but TypeScript also knows about lazy getters defined on it.
*
* Since you probably don't want aliases, you can use it like this to,
* for example, define (static) lazy getters on a class:
*
* @example
* const Widget = XPCOMUtils.defineLazy(
* class Widget {
* static normalProp = 3;
* },
* {
* verticalTabs: { pref: "sidebar.verticalTabs", default: false },
* }
* );
*
* @template {LazyDefinition} const L, T
*
* @param {T} lazy
* The object to define the getters on.
*
* @param {L} definition
* Each key:value property defines type and parameters for getters.
*
* - "resource://module" string
* @see {ChromeUtils.defineESModuleGetters}
*
* - () => value
* @see {ChromeUtils.defineLazyGetter}
*
* - { service: "contract", iid?: nsIID }
* @see {XPCOMUtils.defineLazyServiceGetter}
*
* - { pref: "name", default?, onUpdate?, transform? }
* @see {XPCOMUtils.defineLazyPreferenceGetter}
*
* @param {ImportESModuleOptionsDictionary} [options]
* When importing ESModules in devtools and worker contexts,
* the third parameter is required.
*/
defineLazy(lazy, definition, options) {
let modules = {};
for (let [key, val] of Object.entries(definition)) {
if (typeof val === "string") {
modules[key] = val;
} else if (typeof val === "function") {
ChromeUtils.defineLazyGetter(lazy, key, val);
} else if ("service" in val) {
XPCOMUtils.defineLazyServiceGetter(lazy, key, val.service, val.iid);
} else if ("pref" in val) {
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
key,
val.pref,
val.default,
val.onUpdate,
val.transform
);
} else {
throw new Error(`Unkown LazyDefinition for ${key}`);
}
}
ChromeUtils.defineESModuleGetters(lazy, modules, options);
return /** @type {T & DeclaredLazy<L>} */ (lazy);
},
/**
* @see {XPCOMUtils.defineLazy}
* A shorthand for above which always returns a new lazy object.
* Use this version if you have a global `lazy` const with all the getters:
*
* @example
* const lazy = XPCOMUtils.declareLazy({
* AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
* verticalTabs: { pref: "sidebar.verticalTabs", default: false },
* MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService },
* expensiveThing: () => fetch_or_compute(),
* });
*
* @template {LazyDefinition} const L
* @param {L} declaration
* @param {ImportESModuleOptionsDictionary} [options]
*/
declareLazy(declaration, options) {
return XPCOMUtils.defineLazy({}, declaration, options);
},
/**
* Defines a non-writable property on an object.
*

View File

@@ -25,6 +25,7 @@
* reloaded by the user, we have to ensure that the new extension pages are going
* to run in the same process of the existing addon debugging browser element).
*/
/* eslint-disable mozilla/valid-lazy */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
@@ -33,10 +34,7 @@ import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs"
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
import { Log } from "resource://gre/modules/Log.sys.mjs";
/** @type {Lazy} */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
const lazy = XPCOMUtils.declareLazy({
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
@@ -68,118 +66,99 @@ ChromeUtils.defineESModuleGetters(lazy, {
permissionToL10nId:
"resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "resourceProtocol", () =>
Services.io
.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler)
);
resourceProtocol: () =>
Services.io
.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler),
XPCOMUtils.defineLazyServiceGetters(lazy, {
aomStartup: [
"@mozilla.org/addons/addon-manager-startup;1",
"amIAddonManagerStartup",
],
spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
});
aomStartup: {
service: "@mozilla.org/addons/addon-manager-startup;1",
iid: Ci.amIAddonManagerStartup,
},
spellCheck: {
service: "@mozilla.org/spellchecker/engine;1",
iid: Ci.mozISpellCheckingEngine,
},
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"processCount",
"dom.ipc.processCount.extension"
);
processCount: { pref: "dom.ipc.processCount.extension", default: 1 },
// Temporary pref to be turned on when ready.
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"userContextIsolation",
"extensions.userContextIsolation.enabled",
false
);
userContextIsolation: {
pref: "extensions.userContextIsolation.enabled",
default: false,
},
userContextIsolationDefaultRestricted: {
pref: "extensions.userContextIsolation.defaults.restricted",
default: "[]",
},
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"userContextIsolationDefaultRestricted",
"extensions.userContextIsolation.defaults.restricted",
"[]"
);
dnrEnabled: { pref: "extensions.dnr.enabled", default: true },
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"dnrEnabled",
"extensions.dnr.enabled",
true
);
// All functionality is gated by the "userScripts" permission, and forgetting
// about its existence is enough to hide all userScripts functionality.
// MV3 userScripts API in development (bug 1875475), off by default.
// Not to be confused with MV2 and extensions.webextensions.userScripts.enabled!
userScriptsMV3Enabled: {
pref: "extensions.userScripts.mv3.enabled",
default: false,
},
// All functionality is gated by the "userScripts" permission, and forgetting
// about its existence is enough to hide all userScripts functionality.
// MV3 userScripts API in development (bug 1875475), off by default.
// Not to be confused with MV2 and extensions.webextensions.userScripts.enabled!
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"userScriptsMV3Enabled",
"extensions.userScripts.mv3.enabled",
false
);
// This pref modifies behavior for MV2. MV3 is enabled regardless.
eventPagesEnabled: { pref: "extensions.eventPages.enabled", default: true },
// This pref modifies behavior for MV2. MV3 is enabled regardless.
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"eventPagesEnabled",
"extensions.eventPages.enabled"
);
// This pref is used to check if storage.sync is still the Kinto-based backend
// (GeckoView should be the only one still using it).
storageSyncOldKintoBackend: {
pref: "webextensions.storage.sync.kinto",
default: true,
},
// This pref is used to check if storage.sync is still the Kinto-based backend
// (GeckoView should be the only one still using it).
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"storageSyncOldKintoBackend",
"webextensions.storage.sync.kinto",
false
);
// Deprecation of browser_style, through .supported & .same_as_mv2 prefs:
// - true true = warn only: deprecation message only (no behavioral changes).
// - true false = deprecate: default to false, even if default was true in MV2.
// - false = remove: always use false, even when true is specified.
// (if .same_as_mv2 is set, also warn if the default changed)
// Deprecation plan: https://bugzilla.mozilla.org/show_bug.cgi?id=1827910#c1
browserStyleMV3supported: {
pref: "extensions.browser_style_mv3.supported",
default: false,
},
browserStyleMV3sameAsMV2: {
pref: "extensions.browser_style_mv3.same_as_mv2",
default: false,
},
// Deprecation of browser_style, through .supported & .same_as_mv2 prefs:
// - true true = warn only: deprecation message only (no behavioral changes).
// - true false = deprecate: default to false, even if default was true in MV2.
// - false = remove: always use false, even when true is specified.
// (if .same_as_mv2 is set, also warn if the default changed)
// Deprecation plan: https://bugzilla.mozilla.org/show_bug.cgi?id=1827910#c1
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"browserStyleMV3supported",
"extensions.browser_style_mv3.supported",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"browserStyleMV3sameAsMV2",
"extensions.browser_style_mv3.same_as_mv2",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"processCrashThreshold",
"extensions.webextensions.crash.threshold",
// The default number of times an extension process is allowed to crash
// within a timeframe.
5
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"processCrashTimeframe",
"extensions.webextensions.crash.timeframe",
processCrashThreshold: {
pref: "extensions.webextensions.crash.threshold",
default: 5,
},
// The default timeframe used to count crashes, in milliseconds.
30 * 1000
);
processCrashTimeframe: {
pref: "extensions.webextensions.crash.timeframe",
default: 30 * 1000,
},
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"installIncludesOrigins",
"extensions.originControls.grantByDefault",
false
);
installIncludesOrigins: {
pref: "extensions.originControls.grantByDefault",
default: false,
},
LocaleData: () => ExtensionCommon.LocaleData,
async NO_PROMPT_PERMISSIONS() {
// Wait until all extension API schemas have been loaded and parsed.
await Management.lazyInit();
return new Set(
lazy.Schemas.getPermissionNames([
"PermissionNoPrompt",
"OptionalPermissionNoPrompt",
"PermissionPrivileged",
])
);
},
});
var {
GlobalManager,
@@ -192,27 +171,7 @@ var {
export { Management };
const { getUniqueId, promiseTimeout } = ExtensionUtils;
const { EventEmitter, redefineGetter, updateAllowedOrigins } = ExtensionCommon;
ChromeUtils.defineLazyGetter(
lazy,
"LocaleData",
() => ExtensionCommon.LocaleData
);
ChromeUtils.defineLazyGetter(lazy, "NO_PROMPT_PERMISSIONS", async () => {
// Wait until all extension API schemas have been loaded and parsed.
await Management.lazyInit();
return new Set(
lazy.Schemas.getPermissionNames([
"PermissionNoPrompt",
"OptionalPermissionNoPrompt",
"PermissionPrivileged",
])
);
});
const { sharedData } = Services.ppmm;
const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";

View File

@@ -69,6 +69,22 @@ declare global {
// https://github.com/microsoft/TypeScript/issues/56634
function ConduitGen<const Send>(_, init: Init<Send>, _actor?): Conduit<Send>;
type Items<A> = A extends ReadonlyArray<infer U extends string> ? U : never;
type LazyDefinition = Record<string,
string |
(() => any) |
{ service: string, iid: nsIID } |
{ pref: string, default?, onUpdate?, transform? }
>;
type DeclaredLazy<T> = {
[P in keyof T]:
T[P] extends (() => infer U) ? U :
T[P] extends keyof LazyModules ? Exports<T[P], P> :
T[P] extends { pref: string, default?: infer U } ? Widen<U> :
T[P] extends { service: string, iid?: infer U } ? nsQIResult<U> :
never;
}
}
import { PointConduit, ProcessConduitsChild } from "ConduitsChild.sys.mjs";
@@ -76,3 +92,37 @@ import { ConduitAddress } from "ConduitsParent.sys.mjs";
type Conduit<Send> = PointConduit & { [s in `send${Items<Send>}`]: callback };
type Init<Send> = ConduitAddress & { send: Send; };
type IfKey<T, K> = K extends keyof T ? T[K] : never;
type Exports<M, P> = M extends keyof LazyModules ? IfKey<LazyModules[M], P> : never;
type Widen<T> =
T extends boolean ? boolean :
T extends number ? number :
T extends string ? string :
never;
type LazyModules = {
"resource://gre/modules/AddonManager.sys.mjs": typeof import("resource://gre/modules/AddonManager.sys.mjs"),
"resource://gre/modules/addons/AddonSettings.sys.mjs": typeof import("resource://gre/modules/addons/AddonSettings.sys.mjs"),
"resource://gre/modules/addons/siteperms-addon-utils.sys.mjs": typeof import("resource://gre/modules/addons/siteperms-addon-utils.sys.mjs"),
"resource://gre/modules/AsyncShutdown.sys.mjs": typeof import("resource://gre/modules/AsyncShutdown.sys.mjs"),
"resource://gre/modules/E10SUtils.sys.mjs": typeof import("resource://gre/modules/E10SUtils.sys.mjs"),
"resource://gre/modules/ExtensionDNR.sys.mjs": typeof import("resource://gre/modules/ExtensionDNR.sys.mjs"),
"resource://gre/modules/ExtensionDNRStore.sys.mjs": typeof import("resource://gre/modules/ExtensionDNRStore.sys.mjs"),
"resource://gre/modules/ExtensionMenus.sys.mjs": typeof import("resource://gre/modules/ExtensionMenus.sys.mjs"),
"resource://gre/modules/ExtensionPermissionMessages.sys.mjs": typeof import("resource://gre/modules/ExtensionPermissionMessages.sys.mjs"),
"resource://gre/modules/ExtensionPermissions.sys.mjs": typeof import("resource://gre/modules/ExtensionPermissions.sys.mjs"),
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs": typeof import("resource://gre/modules/ExtensionPreferencesManager.sys.mjs"),
"resource://gre/modules/ExtensionProcessScript.sys.mjs": typeof import("resource://gre/modules/ExtensionProcessScript.sys.mjs"),
"resource://gre/modules/ExtensionScriptingStore.sys.mjs": typeof import("resource://gre/modules/ExtensionScriptingStore.sys.mjs"),
"resource://gre/modules/ExtensionStorage.sys.mjs": typeof import("resource://gre/modules/ExtensionStorage.sys.mjs"),
"resource://gre/modules/ExtensionStorageIDB.sys.mjs": typeof import("resource://gre/modules/ExtensionStorageIDB.sys.mjs"),
"resource://gre/modules/ExtensionStorageSync.sys.mjs": typeof import("resource://gre/modules/ExtensionStorageSync.sys.mjs"),
"resource://gre/modules/ExtensionTelemetry.sys.mjs": typeof import("resource://gre/modules/ExtensionTelemetry.sys.mjs"),
"resource://gre/modules/ExtensionUserScripts.sys.mjs": typeof import("resource://gre/modules/ExtensionUserScripts.sys.mjs"),
"resource://gre/modules/LightweightThemeManager.sys.mjs": typeof import("resource://gre/modules/LightweightThemeManager.sys.mjs"),
"resource://gre/modules/NetUtil.sys.mjs": typeof import("resource://gre/modules/NetUtil.sys.mjs"),
"resource://gre/modules/Schemas.sys.mjs": typeof import("resource://gre/modules/Schemas.sys.mjs"),
"resource://gre/modules/ServiceWorkerCleanUp.sys.mjs": typeof import("resource://gre/modules/ServiceWorkerCleanUp.sys.mjs"),
};

View File

@@ -1,8 +1,10 @@
// Exports for all modules redirected here by a catch-all rule in tsconfig.json.
export var
AddonWrapper, GeckoViewConnection, GeckoViewWebExtension,
IndexedDB, JSONFile, Log, UrlbarUtils, WebExtensionDescriptorActor;
AddonManager, AddonManagerPrivate, AddonSettings, AddonWrapper, AsyncShutdown,
ExtensionMenus, ExtensionProcessScript, ExtensionScriptingStore, ExtensionUserScripts,
NetUtil, E10SUtils, LightweightThemeManager, ServiceWorkerCleanUp, GeckoViewConnection,
GeckoViewWebExtension, IndexedDB, JSONFile, Log, UrlbarUtils, WebExtensionDescriptorActor;
/**
* A stub type for the "class" from EventEmitter.sys.mjs.