diff options
Diffstat (limited to 'toolkit/components/extensions/types')
-rw-r--r-- | toolkit/components/extensions/types/README.md | 86 | ||||
-rw-r--r-- | toolkit/components/extensions/types/XPCShellContentUtils.sys.d.mts | 87 | ||||
-rw-r--r-- | toolkit/components/extensions/types/extensions.ts | 80 | ||||
-rw-r--r-- | toolkit/components/extensions/types/gecko.ts | 163 | ||||
-rw-r--r-- | toolkit/components/extensions/types/globals.ts | 33 |
5 files changed, 449 insertions, 0 deletions
diff --git a/toolkit/components/extensions/types/README.md b/toolkit/components/extensions/types/README.md new file mode 100644 index 0000000000..ebd01dec60 --- /dev/null +++ b/toolkit/components/extensions/types/README.md @@ -0,0 +1,86 @@ +# PoC Type-Checking JavaScript Using JSDocs + +## Intro + +TypeScript can be used on plain JavaScript files documented with JSDoc comments +to check types without a build step. This is a proof of concept to show +viability and benefits of doing this for a "typical" component. + +* [Handbook: Type Checking JavaScript Files][handbook] +* [JSDoc Reference][jsdoc] + +## New files + + * `tsconfig.json`: at the root of a TypeScript "project" configures how to find + and check files. + + * `types/globals.ts`: defines available globals, types and various utilities. + + * `types/XPCShellContentUtils.sys.mts`: an example of type definitions file for + an external module which was automatically generated by `tsc`. + +## How to use and expectations + +Use [npm or yarn to install][download] TypeScript. +Then run `tsc` in the extensions directory to check types: + +``` +mozilla-central/toolkit/components/extensions $ tsc +``` + +You can also use an editor which supports the [language server][langserv]. +VSCode should pick it up automatically, but others like Vim might need +[some configuring][nvim]. + +Other than continuing to use JSDocs, trying to follow guidelines below, and +hopefully remembering to run `tsc` occasionally, for now there is explicitly +*no expectation* that all new code must be fully typed, or pass typecheck +on every commit. + +If you are in a hurry, or run into a confusing type problem, feel free to +slap a @ts-ignore and/or ask for help from someone more familiar. Hopefully +as workflow gets better integrations, we all learn some typescript along +the way, and the whole process remains non-disruptive. + +## Guidelines for type-friendly code + +Using more modern and idiomatic code patterns can enable better type inference +by the TypeScript compiler. + +These fall under 5 main categories: + + 1) Declare and/or initialize all class fields in the constructor. + * (general good practice) + + 2) Use real getters and redefineGetter instead of defineLazyGetter. + * (also keeps related code closer together) + + 3) When extending, 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 separately. + * (don't use `let foo;` at the top of the file, use `var foo = {`) + + 5) Don't re-use local variables unnecessarily with different types. + * (general good practice, local variables are "free") + +### @ts-ignore recommendations + +*Don't* use `@ts-ignore` for class fields and function or method signatures. + +*Feel free* to use it: + * locally inside methods, + * when the alternative is more noise than signal, + * when doing so isn't preventing passthrough of type-inference through other + parts of the codebase. + + +[handbook]: https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html + +[jsdoc]: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + +[download]: https://www.typescriptlang.org/download + +[langserv]: https://github.com/typescript-language-server/typescript-language-server + +[nvim]: https://www.reddit.com/r/neovim/comments/131l9cw/theres_another_typescript_lsp_that_wraps_the/ diff --git a/toolkit/components/extensions/types/XPCShellContentUtils.sys.d.mts b/toolkit/components/extensions/types/XPCShellContentUtils.sys.d.mts new file mode 100644 index 0000000000..a2c90c4be1 --- /dev/null +++ b/toolkit/components/extensions/types/XPCShellContentUtils.sys.d.mts @@ -0,0 +1,87 @@ +// @ts-nocheck + +export namespace XPCShellContentUtils { + const currentScope: any; + const fetchScopes: Map<any, any>; + function initCommon(scope: any): void; + function init(scope: any): void; + function initMochitest(scope: any): void; + function ensureInitialized(scope: any): void; + /** + * Creates a new HttpServer for testing, and begins listening on the + * specified port. Automatically shuts down the server when the test + * unit ends. + * + * @param {object} [options = {}] + * The options object. + * @param {integer} [options.port = -1] + * The port to listen on. If omitted, listen on a random + * port. The latter is the preferred behavior. + * @param {sequence<string>?} [options.hosts = null] + * A set of hosts to accept connections to. Support for this is + * implemented using a proxy filter. + * + * @returns {HttpServer} + * The HTTP server instance. + */ + function createHttpServer({ port, hosts }?: { + port?: number; + hosts?: sequence<string>; + }): HttpServer; + + var remoteContentScripts: boolean; + type ContentPage = ContentPage; + + function registerJSON(server: any, path: any, obj: any): void; + function fetch(origin: any, url: any, options: any): Promise<any>; + /** + * Loads a content page into a hidden docShell. + * + * @param {string} url + * The URL to load. + * @param {object} [options = {}] + * @param {ExtensionWrapper} [options.extension] + * If passed, load the URL as an extension page for the given + * extension. + * @param {boolean} [options.remote] + * If true, load the URL in a content process. If false, load + * it in the parent process. + * @param {boolean} [options.remoteSubframes] + * If true, load cross-origin frames in separate content processes. + * This is ignored if |options.remote| is false. + * @param {string} [options.redirectUrl] + * An optional URL that the initial page is expected to + * redirect to. + * + * @returns {ContentPage} + */ + function loadContentPage(url: string, { extension, remote, remoteSubframes, redirectUrl, privateBrowsing, userContextId, }?: { + extension?: any; + remote?: boolean; + remoteSubframes?: boolean; + redirectUrl?: string; + }): ContentPage; +} +declare class ContentPage { + constructor(remote?: any, remoteSubframes?: any, extension?: any, privateBrowsing?: boolean, userContextId?: any); + remote: any; + remoteSubframes: any; + extension: any; + privateBrowsing: boolean; + userContextId: any; + browserReady: Promise<Element>; + _initBrowser(): Promise<Element>; + windowlessBrowser: any; + browser: Element; + get browsingContext(): any; + get SpecialPowers(): any; + loadFrameScript(func: any): void; + addFrameScriptHelper(func: any): void; + didChangeBrowserRemoteness(event: any): void; + loadURL(url: any, redirectUrl?: any): Promise<any>; + fetch(...args: any[]): Promise<any>; + spawn(params: any, task: any): any; + legacySpawn(params: any, task: any): any; + close(): Promise<void>; +} +export {}; diff --git a/toolkit/components/extensions/types/extensions.ts b/toolkit/components/extensions/types/extensions.ts new file mode 100644 index 0000000000..8f9555421b --- /dev/null +++ b/toolkit/components/extensions/types/extensions.ts @@ -0,0 +1,80 @@ +/** + * Type declarations for WebExtensions framework code. + */ + +// This has every possible property we import from all modules, which is not +// great, but should be manageable and easy to generate for each component. +// ESLint warns if we use one which is not actually defined, so still safe. +type LazyAll = { + BroadcastConduit: typeof import("ConduitsParent.sys.mjs").BroadcastConduit, + Extension: typeof import("Extension.sys.mjs").Extension, + ExtensionActivityLog: typeof import("ExtensionActivityLog.sys.mjs").ExtensionActivityLog, + ExtensionChild: typeof import("ExtensionChild.sys.mjs").ExtensionChild, + ExtensionCommon: typeof import("ExtensionCommon.sys.mjs").ExtensionCommon, + ExtensionContent: typeof import("ExtensionContent.sys.mjs").ExtensionContent, + ExtensionDNR: typeof import("ExtensionDNR.sys.mjs").ExtensionDNR, + ExtensionDNRLimits: typeof import("ExtensionDNRLimits.sys.mjs").ExtensionDNRLimits, + ExtensionDNRStore: typeof import("ExtensionDNRStore.sys.mjs").ExtensionDNRStore, + ExtensionData: typeof import("Extension.sys.mjs").ExtensionData, + ExtensionPageChild: typeof import("ExtensionPageChild.sys.mjs").ExtensionPageChild, + ExtensionParent: typeof import("ExtensionParent.sys.mjs").ExtensionParent, + ExtensionPermissions: typeof import("ExtensionPermissions.sys.mjs").ExtensionPermissions, + ExtensionStorage: typeof import("ExtensionStorage.sys.mjs").ExtensionStorage, + ExtensionStorageIDB: typeof import("ExtensionStorageIDB.sys.mjs").ExtensionStorageIDB, + ExtensionTelemetry: typeof import("ExtensionTelemetry.sys.mjs").ExtensionTelemetry, + ExtensionTestCommon: typeof import("resource://testing-common/ExtensionTestCommon.sys.mjs").ExtensionTestCommon, + ExtensionUtils: typeof import("ExtensionUtils.sys.mjs").ExtensionUtils, + ExtensionWorkerChild: typeof import("ExtensionWorkerChild.sys.mjs").ExtensionWorkerChild, + GeckoViewConnection: typeof import("resource://gre/modules/GeckoViewWebExtension.sys.mjs").GeckoViewConnection, + JSONFile: typeof import("resource://gre/modules/JSONFile.sys.mjs").JSONFile, + Management: typeof import("Extension.sys.mjs").Management, + MessageManagerProxy: typeof import("MessageManagerProxy.sys.mjs").MessageManagerProxy, + NativeApp: typeof import("NativeMessaging.sys.mjs").NativeApp, + NativeManifests: typeof import("NativeManifests.sys.mjs").NativeManifests, + PERMISSION_L10N: typeof import("ExtensionPermissionMessages.sys.mjs").PERMISSION_L10N, + QuarantinedDomains: typeof import("ExtensionPermissions.sys.mjs").QuarantinedDomains, + SchemaRoot: typeof import("Schemas.sys.mjs").SchemaRoot, + Schemas: typeof import("Schemas.sys.mjs").Schemas, + WebNavigationFrames: typeof import("WebNavigationFrames.sys.mjs").WebNavigationFrames, + WebRequest: typeof import("webrequest/WebRequest.sys.mjs").WebRequest, + extensionStorageSync: typeof import("ExtensionStorageSync.sys.mjs").extensionStorageSync, + getErrorNameForTelemetry: typeof import("ExtensionTelemetry.sys.mjs").getErrorNameForTelemetry, + getTrimmedString: typeof import("ExtensionTelemetry.sys.mjs").getTrimmedString, +}; + +// Utility type to extract all strings from a const array, to use as keys. +type Items<A> = A extends ReadonlyArray<infer U extends string> ? U : never; + +declare global { + type Lazy<Keys extends keyof LazyAll = keyof LazyAll> = Pick<LazyAll, Keys> & { [k: string]: any }; + + // Export JSDoc types, and make other classes available globally. + type ConduitAddress = import("ConduitsParent.sys.mjs").ConduitAddress; + type ConduitID = import("ConduitsParent.sys.mjs").ConduitID; + type Extension = import("Extension.sys.mjs").Extension; + + // Something about Class type not being exported when nested in a namespace? + type BaseContext = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.BaseContext>; + type BrowserExtensionContent = InstanceType<typeof import("ExtensionContent.sys.mjs").ExtensionContent.BrowserExtensionContent>; + type EventEmitter = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.EventEmitter>; + type ExtensionAPI = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.ExtensionAPI>; + type ExtensionError = InstanceType<typeof import("ExtensionUtils.sys.mjs").ExtensionUtils.ExtensionError>; + type LocaleData = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.LocaleData>; + type ProxyAPIImplementation = InstanceType<typeof import("ExtensionChild.sys.mjs").ExtensionChild.ProxyAPIImplementation>; + type SchemaAPIInterface = InstanceType<typeof import("ExtensionCommon.sys.mjs").ExtensionCommon.SchemaAPIInterface>; + type WorkerExtensionError = InstanceType<typeof import("ExtensionUtils.sys.mjs").ExtensionUtils.WorkerExtensionError>; + + // Other misc types. + type AddonWrapper = any; + type Context = BaseContext; + type NativeTab = Element; + type SavedFrame = object; + + // Can't define a const generic parameter in jsdocs yet. + // https://github.com/microsoft/TypeScript/issues/56634 + type ConduitInit<Send> = ConduitAddress & { send: Send; }; + type Conduit<Send> = import("../ConduitsChild.sys.mjs").PointConduit & { [s in `send${Items<Send>}`]: callback }; + type ConduitOpen = <const Send>(subject: object, address: ConduitInit<Send>) => Conduit<Send>; +} + +export {} diff --git a/toolkit/components/extensions/types/gecko.ts b/toolkit/components/extensions/types/gecko.ts new file mode 100644 index 0000000000..f6b5190f8d --- /dev/null +++ b/toolkit/components/extensions/types/gecko.ts @@ -0,0 +1,163 @@ +/** + * Global Gecko type declarations. + */ + +// @ts-ignore +import type { CiClass } from "lib.gecko.xpidl" + +declare global { + // Other misc types. + type Browser = InstanceType<typeof XULBrowserElement>; + type bytestring = string; + type callback = (...args: any[]) => any; + type ColorArray = number[]; + type integer = number; + type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue }; + + interface Document { + createXULElement(name: string): Element; + documentReadyForIdle: Promise<void>; + } + interface EventTarget { + ownerGlobal: Window; + } + interface Error { + code; + } + interface ErrorConstructor { + new (message?: string, options?: ErrorOptions, lineNo?: number): Error; + } + interface Window { + gBrowser; + } + // HACK to get the static isInstance for DOMException and Window? + interface Object { + isInstance(object: any): boolean; + } + + // XPIDL additions/overrides. + + interface nsISupports { + // OMG it works! + QueryInterface?<T extends CiClass<nsISupports>>(aCiClass: T): T['prototype']; + wrappedJSObject?: object; + } + interface nsIProperties { + get<T extends CiClass<nsISupports>>(prop: string, aCiClass: T): T['prototype']; + } + interface nsIPrefBranch { + getComplexValue<T extends CiClass<nsISupports>>(aPrefName: string, aCiClass: T): T['prototype']; + } + // TODO: incorporate above into lib.xpidl.d.ts generation, somehow? + + type Sandbox = typeof globalThis; + interface nsIXPCComponents_utils_Sandbox { + (principal: nsIPrincipal | nsIPrincipal[], options: object): Sandbox; + } + interface nsIXPCComponents_Utils { + cloneInto<T>(obj: T, ...args: any[]): T; + createObjectIn<T>(Sandbox, options?: T): T; + exportFunction<T extends callback>(f: T, ...args: any[]): T; + getWeakReference<T extends object>(T): { get(): T }; + readonly Sandbox: nsIXPCComponents_utils_Sandbox; + waiveXrays<T>(obj: T): T; + } + interface nsIDOMWindow extends Window { + docShell: nsIDocShell; + } + interface Document { + documentURIObject: nsIURI; + createXULElement(name: string): Element; + } + + // nsDocShell is the only thing implementing nsIDocShell, but it also + // implements nsIWebNavigation, and a few others, so this is "ok". + interface nsIDocShell extends nsIWebNavigation {} + interface nsISimpleEnumerator extends Iterable<any> {} + + namespace Components { + type Exception = Error; + } + namespace UrlbarUtils { + type RESULT_TYPE = any; + type RESULT_SOURCE = any; + } + + // Various mozilla globals. + var Cc, Cr, ChromeUtils, Components, dump, uneval; + + // [ChromeOnly] WebIDL, to be generated. + var BrowsingContext, ChannelWrapper, ChromeWindow, ChromeWorker, + ClonedErrorHolder, Glean, InspectorUtils, IOUtils, JSProcessActorChild, + JSProcessActorParent, JSWindowActor, JSWindowActorChild, + JSWindowActorParent, L10nRegistry, L10nFileSource, Localization, + MatchGlob, MatchPattern, MatchPatternSet, PathUtils, PreloadedScript, + StructuredCloneHolder, TelemetryStopwatch, WindowGlobalChild, + WebExtensionContentScript, WebExtensionParentActor, WebExtensionPolicy, + XULBrowserElement, nsIMessageListenerManager; + + interface XULElement extends Element {} + + // nsIServices is not a thing. + interface nsIServices { + scriptloader: mozIJSSubScriptLoader; + locale: mozILocaleService; + intl: mozIMozIntl; + storage: mozIStorageService; + appShell: nsIAppShellService; + startup: nsIAppStartup; + blocklist: nsIBlocklistService; + cache2: nsICacheStorageService; + catMan: nsICategoryManager; + clearData: nsIClearDataService; + clipboard: nsIClipboard; + console: nsIConsoleService; + cookieBanners: nsICookieBannerService; + cookies: nsICookieManager & nsICookieService; + appinfo: nsICrashReporter & nsIXULAppInfo & nsIXULRuntime; + DAPTelemetry: nsIDAPTelemetry; + DOMRequest: nsIDOMRequestService; + dns: nsIDNSService; + dirsvc: nsIDirectoryService & nsIProperties; + droppedLinkHandler: nsIDroppedLinkHandler; + eTLD: nsIEffectiveTLDService; + policies: nsIEnterprisePolicies; + env: nsIEnvironment; + els: nsIEventListenerService; + fog: nsIFOG; + focus: nsIFocusManager; + io: nsIIOService & nsINetUtil & nsISpeculativeConnect; + loadContextInfo: nsILoadContextInfoFactory; + domStorageManager: nsIDOMStorageManager & nsILocalStorageManager; + logins: nsILoginManager; + obs: nsIObserverService; + perms: nsIPermissionManager; + prefs: nsIPrefBranch & nsIPrefService; + profiler: nsIProfiler; + prompt: nsIPromptService; + sysinfo: nsISystemInfo & nsIPropertyBag2; + qms: nsIQuotaManagerService; + rfp: nsIRFPService; + scriptSecurityManager: nsIScriptSecurityManager; + search: nsISearchService; + sessionStorage: nsISessionStorageService; + strings: nsIStringBundleService; + telemetry: nsITelemetry; + textToSubURI: nsITextToSubURI; + tm: nsIThreadManager; + uriFixup: nsIURIFixup; + urlFormatter: nsIURLFormatter; + uuid: nsIUUIDGenerator; + vc: nsIVersionComparator; + wm: nsIWindowMediator; + ww: nsIWindowWatcher; + xulStore: nsIXULStore; + ppmm: any; + cpmm: any; + mm: any; + } + + var Ci: nsIXPCComponents_Interfaces; + var Cu: nsIXPCComponents_Utils; + var Services: nsIServices; +} diff --git a/toolkit/components/extensions/types/globals.ts b/toolkit/components/extensions/types/globals.ts new file mode 100644 index 0000000000..45722828e2 --- /dev/null +++ b/toolkit/components/extensions/types/globals.ts @@ -0,0 +1,33 @@ +/** + * Support types for toolkit/components/extensions code. + */ + +/// <reference lib="dom" /> +/// <reference path="./gecko.ts" /> +/// <reference path="./extensions.ts" /> + +// This now relies on types generated in bug 1872918, or get the built +// artifact tslib directly and put it in your src/node_modules/@types: +// https://phabricator.services.mozilla.com/D197620 +/// <reference types="lib.gecko.xpidl" /> + +// Exports for all other external modules redirected to globals.ts. +export var AppConstants, + GeckoViewConnection, GeckoViewWebExtension, IndexedDB, JSONFile, Log; + +/** + * This is a mock for the "class" from EventEmitter.sys.mjs. When we import + * it in extensions code using resource://gre/modules/EventEmitter.sys.mjs, + * the catch-all rule from tsconfig.json redirects it to this file. The export + * of the class below fulfills the import. The mock is needed when we subclass + * that EventEmitter, typescript gets confused because it's an old style + * function-and-prototype-based "class", and some types don't match up. + * + * TODO: Convert EventEmitter.sys.mjs into a proper class. + */ +export declare class EventEmitter { + on(event: string, listener: callback): void; + once(event: string, listener: callback): Promise<any>; + off(event: string, listener: callback): void; + emit(event: string, ...args: any[]): void; +} |