From 35b263c53e95d984cdd24cf5b857dadc833a765c Mon Sep 17 00:00:00 2001 From: TrickyPR <23250792+trickypr@users.noreply.github.com> Date: Sun, 21 Apr 2024 16:29:29 +1000 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=9A=A7=20Implement=20`browser=5Factio?= =?UTF-8?q?n`=20manifest=20key=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/content/package.json | 2 +- apps/extensions/lib/ext-browser.json | 6 + apps/extensions/lib/parent/ext-browser.js | 5 +- .../lib/parent/ext-browserAction.js | 38 ++++++ apps/extensions/lib/parent/ext-pageAction.js | 6 +- apps/extensions/lib/parent/ext-tabs.js | 2 +- .../lib/schemas/browser_action.json | 43 +++++++ apps/extensions/lib/schemas/tabs.json | 1 - apps/extensions/lib/types/utils.d.ts | 10 +- apps/extensions/package.json | 4 +- apps/extensions/scripts/buildTypes.js | 26 +++- apps/modules/lib/EBrowserActions.sys.mjs | 98 +++++++++++++++ apps/modules/lib/SvelteStore.sys.mjs | 116 ++++++++++++++++++ libs/link/package.json | 3 +- libs/link/types/_link.d.ts | 4 + libs/link/types/index.d.ts | 1 + libs/link/types/modules/EBrowserActions.d.ts | 49 ++++++++ libs/link/types/modules/SvelteStore.d.ts | 58 +++++++++ .../types/schemaTypes/browser_action.d.ts | 21 ++++ libs/link/types/schemaTypes/index.d.ts | 3 + .../link/types}/schemaTypes/tabs.d.ts | 1 + package.json | 2 +- pnpm-lock.yaml | 54 ++++---- scripts/unit-test.ts | 2 +- 24 files changed, 507 insertions(+), 48 deletions(-) create mode 100644 apps/extensions/lib/parent/ext-browserAction.js create mode 100644 apps/extensions/lib/schemas/browser_action.json create mode 100644 apps/modules/lib/EBrowserActions.sys.mjs create mode 100644 apps/modules/lib/SvelteStore.sys.mjs create mode 100644 libs/link/types/index.d.ts create mode 100644 libs/link/types/modules/EBrowserActions.d.ts create mode 100644 libs/link/types/modules/SvelteStore.d.ts create mode 100644 libs/link/types/schemaTypes/browser_action.d.ts create mode 100644 libs/link/types/schemaTypes/index.d.ts rename {apps/extensions/lib => libs/link/types}/schemaTypes/tabs.d.ts (95%) diff --git a/apps/content/package.json b/apps/content/package.json index 40d4942..5e21431 100644 --- a/apps/content/package.json +++ b/apps/content/package.json @@ -26,7 +26,7 @@ "svelte-preprocess": "^5.1.3", "svelte-sequential-preprocessor": "^2.0.1", "ts-loader": "^9.5.1", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", diff --git a/apps/extensions/lib/ext-browser.json b/apps/extensions/lib/ext-browser.json index e9ebae6..c216f04 100644 --- a/apps/extensions/lib/ext-browser.json +++ b/apps/extensions/lib/ext-browser.json @@ -1,4 +1,10 @@ { + "browserAction": { + "url": "chrome://bextensions/content/parent/ext-browserAction.js", + "schema": "chrome://bextensions/content/schemas/browser_action.json", + "scopes": ["addon_parent"], + "manifest": ["browser_action"] + }, "pageAction": { "url": "chrome://bextensions/content/parent/ext-pageAction.js", "schema": "chrome://extensions/content/schemas/page_action.json", diff --git a/apps/extensions/lib/parent/ext-browser.js b/apps/extensions/lib/parent/ext-browser.js index 5f2a7b2..da609c5 100644 --- a/apps/extensions/lib/parent/ext-browser.js +++ b/apps/extensions/lib/parent/ext-browser.js @@ -13,6 +13,7 @@ const { lazyESModuleGetters } = typedImportUtils const lazy = lazyESModuleGetters({ WindowTracker: 'resource://app/modules/BrowserWindowTracker.sys.mjs', + EBrowserActions: 'resource://app/modules/EBrowserActions.sys.mjs', EPageActions: 'resource://app/modules/EPageActions.sys.mjs', ExtensionParent: 'resource://gre/modules/ExtensionParent.sys.mjs', }) @@ -36,8 +37,10 @@ class TabTracker extends TabTrackerBase { } /** + * @template {import('@browser/tabs').WindowTab | null} T * @param {number} tabId - * @param {import('@browser/tabs').WindowTab} default_ + * @param {T} default_ + * @returns {T} */ getTab(tabId, default_) { const { tab } = lazy.WindowTracker.getWindowWithBrowserId(tabId) || { diff --git a/apps/extensions/lib/parent/ext-browserAction.js b/apps/extensions/lib/parent/ext-browserAction.js new file mode 100644 index 0000000..e3138c2 --- /dev/null +++ b/apps/extensions/lib/parent/ext-browserAction.js @@ -0,0 +1,38 @@ +// @ts-check +/// +/// +/// + +this.browserAction = class extends ExtensionAPIPersistent { + /** @type {import("resource://app/modules/EBrowserActions.sys.mjs").IBrowserAction | undefined} */ + browserAction + + async onManifestEntry() { + const { extension } = this + /** @type {browser_action__manifest.WebExtensionManifest__extended['browser_action']} */ + const options = extension.manifest.browser_action + + if (!options) { + return + } + + this.browserAction = lazy.EBrowserActions.BrowserAction(extension.id, { + icons: lazy.ExtensionParent.IconDetails.normalize( + { + path: options.default_icon || extension.manifest.icons, + iconType: 'browserAction', + themeIcon: options.theme_icons, + }, + extension, + ), + title: options.default_title || extension.id, + popupUrl: options.default_popup, + }) + + lazy.EBrowserActions.actions.addKey(extension.id, this.browserAction) + } + + onShutdown() { + lazy.EBrowserActions.actions.removeKey(this.extension.id) + } +} diff --git a/apps/extensions/lib/parent/ext-pageAction.js b/apps/extensions/lib/parent/ext-pageAction.js index 5339ca1..9dd939e 100644 --- a/apps/extensions/lib/parent/ext-pageAction.js +++ b/apps/extensions/lib/parent/ext-pageAction.js @@ -14,6 +14,10 @@ this.pageAction = class extends ExtensionAPIPersistent { const { extension } = this const options = extension.manifest.page_action + if (!options) { + return + } + this.pageAction = new lazy.EPageActions.PageAction({ extensionId: extension.id, tooltip: options.default_title, @@ -27,7 +31,7 @@ this.pageAction = class extends ExtensionAPIPersistent { { path: options.default_icon || extension.manifest.icons, iconType: 'browserAction', - themeIcon: options.theme_icons || extension.theme_icons, + themeIcon: options.theme_icons, }, extension, ), diff --git a/apps/extensions/lib/parent/ext-tabs.js b/apps/extensions/lib/parent/ext-tabs.js index a2d209e..d91a4ff 100644 --- a/apps/extensions/lib/parent/ext-tabs.js +++ b/apps/extensions/lib/parent/ext-tabs.js @@ -5,7 +5,7 @@ // @ts-check /// /// -/// +/// /** * @param {tabs__tabs.QueryInfo} queryInfo diff --git a/apps/extensions/lib/schemas/browser_action.json b/apps/extensions/lib/schemas/browser_action.json new file mode 100644 index 0000000..684d8e2 --- /dev/null +++ b/apps/extensions/lib/schemas/browser_action.json @@ -0,0 +1,43 @@ +[ + { + "namespace": "manifest", + "types": [ + { + "$extend": "WebExtensionManifest", + "properties": { + "browser_action": { + "type": "object", + "optional": true, + "properties": { + "default_icon": { + "optional": true, + "$ref": "IconPath" + }, + "default_popup": { + "optional": true, + "type": "string" + }, + "default_title": { + "optional": true, + "type": "string" + }, + "theme_icons": { + "optional": true, + "type": "array", + "items": { "$ref": "ThemeIcons" } + }, + "browser_style": { + "optional": true, + "type": "boolean" + }, + "default_area": { + "optional": true, + "type": "string" + } + } + } + } + } + ] + } +] diff --git a/apps/extensions/lib/schemas/tabs.json b/apps/extensions/lib/schemas/tabs.json index 40b0ef7..064190b 100644 --- a/apps/extensions/lib/schemas/tabs.json +++ b/apps/extensions/lib/schemas/tabs.json @@ -8,7 +8,6 @@ "types": [ { "$extend": "OptionalPermission", - "id": "ExtraPerms1", "choices": [ { "type": "string", diff --git a/apps/extensions/lib/types/utils.d.ts b/apps/extensions/lib/types/utils.d.ts index 67febf3..15d7e8b 100644 --- a/apps/extensions/lib/types/utils.d.ts +++ b/apps/extensions/lib/types/utils.d.ts @@ -155,6 +155,14 @@ declare global { [LISTENERS]: Map; [ONCE_MAP]: WeakMap } + + class ExtensionData { + id: string + + manifest: browser._manifest.WebExtensionManifest & + browser_action__manifest.WebExtensionManifest__extended + } + /** * Base class for WebExtension APIs. Each API creates a new class * that inherits from this class, the derived class is instantiated @@ -162,7 +170,7 @@ declare global { */ class ExtensionAPI extends EventEmitter { constructor(extension: any) - extension: any + extension: ExtensionData destroy(): void onManifestEntry(entry: any): void getAPI(context: any): void diff --git a/apps/extensions/package.json b/apps/extensions/package.json index b49203b..2cd0d68 100644 --- a/apps/extensions/package.json +++ b/apps/extensions/package.json @@ -1,10 +1,10 @@ { - "name": "extensions", + "name": "@browser/extensions", "type": "module", "version": "1.0.0", "scripts": { "build": "node ./scripts/buildTypes.js", - "dev": "watch 'pnpm build' ./lib/schemas/" + "dev": "watch 'pnpm build' ./lib/schemas/ ./scripts/" }, "dependencies": { "@browser/link": "workspace:*" diff --git a/apps/extensions/scripts/buildTypes.js b/apps/extensions/scripts/buildTypes.js index 1f2a339..9550e75 100644 --- a/apps/extensions/scripts/buildTypes.js +++ b/apps/extensions/scripts/buildTypes.js @@ -54,7 +54,7 @@ const { /** * @typedef {object} StringType * @property {'string'} type - * @property {{ name: string }[]} [enum] + * @property {({ name: string } | string)[]} [enum] */ /** @@ -78,7 +78,12 @@ const { */ const schemaFolder = path.join(process.cwd(), 'lib', 'schemas') -const outFolder = path.join(process.cwd(), 'lib', 'schemaTypes') +const outFolder = path.join( + process.cwd(), + '../..', + 'libs/link/types', + 'schemaTypes', +) const printer = createPrinter({ newLine: NewLineKind.LineFeed, @@ -87,8 +92,10 @@ const printer = createPrinter({ const QUESTION_TOKEN = factory.createToken(SyntaxKind.QuestionToken) +const modules = [] for (const file of fs.readdirSync(schemaFolder)) { const fileName = file.replace('.json', '') + modules.push(fileName) let text = fs.readFileSync(path.join(schemaFolder, file), 'utf8') const sourceFile = createSourceFile(file, text, ScriptTarget.Latest) @@ -132,6 +139,12 @@ for (const file of fs.readdirSync(schemaFolder)) { ) } +fs.writeFileSync( + path.join(outFolder, 'index.d.ts'), + '// @not-mpl \n' + + modules.map((m) => `/// `).join('\n'), +) + /** * @param {Type[]} types * @returns {import('typescript').TypeAliasDeclaration[]} @@ -139,7 +152,10 @@ for (const file of fs.readdirSync(schemaFolder)) { function generateTypes(types) { return types .map((type) => { - if (type.$extend) return null + if (type.$extend) { + type.id = `${type.$extend}__extended` + type.type = 'object' + } return factory.createTypeAliasDeclaration( undefined, @@ -256,7 +272,9 @@ function generateTypeNode(type) { if (type.enum) { return factory.createUnionTypeNode( type.enum.map((e) => - factory.createLiteralTypeNode(factory.createStringLiteral(e.name)), + factory.createLiteralTypeNode( + factory.createStringLiteral(typeof e === 'string' ? e : e.name), + ), ), ) } diff --git a/apps/modules/lib/EBrowserActions.sys.mjs b/apps/modules/lib/EBrowserActions.sys.mjs new file mode 100644 index 0000000..61117bf --- /dev/null +++ b/apps/modules/lib/EBrowserActions.sys.mjs @@ -0,0 +1,98 @@ +// @ts-check +/// +import { map, writable } from 'resource://app/modules/SvelteStore.sys.mjs' +import mitt from 'resource://app/modules/mitt.sys.mjs' + +import { derived } from './SvelteStore.sys.mjs' + +/** @typedef {import('resource://app/modules/EBrowserActions.sys.mjs').IBrowserAction} IBrowserAction */ +/** @implements {IBrowserAction} */ +class BrowserAction { + id + /** @type {ReturnType} */ + emmiter = mitt() + + /** @type{import('resource://app/modules/SvelteStore.sys.mjs').IWritable>} */ + icons = writable({}) + /** @type {import('resource://app/modules/SvelteStore.sys.mjs').IWritable} */ + title = writable('') + /** @type {import('resource://app/modules/SvelteStore.sys.mjs').IWritable} */ + popupUrl = writable(undefined) + + /** + * @param {string} id + */ + constructor(id) { + this.id = id + } + + getEmmiter() { + return this.emmiter + } + + getExtensionId() { + return this.id + } + + /** + * @param {Record} icons + */ + setIcons(icons) { + this.icons.set(icons) + } + getIcons() { + return this.icons + } + /** @param {number} preferredSize */ + getIcon(preferredSize) { + return derived([this.icons], (icon) => { + let bestSize + + if (icon[preferredSize]) { + bestSize = preferredSize + } else if (icon[preferredSize * 2]) { + bestSize = preferredSize * 2 + } else { + const sizes = Object.keys(icon) + .map((key) => parseInt(key, 10)) + .sort((a, b) => a - b) + bestSize = + sizes.find((candidate) => candidate > preferredSize) || + sizes.pop() || + 0 + } + + return icon[bestSize] + }) + } + + setTitle(title) { + this.title.set(title) + } + getTitle() { + return this.title + } + + setPopupUrl(url) { + this.popupUrl.set(url) + } + getPopupUrl() { + return this.popupUrl + } +} + +/** @type {typeof import('resource://app/modules/EBrowserActions.sys.mjs').EBrowserActions} */ +export const EBrowserActions = { + BrowserAction: (id, options) => { + const action = new BrowserAction(id) + + action.setIcons(options.icons) + action.setTitle(options.title) + action.setPopupUrl(options.popupUrl) + + return action + }, + actions: map({}), +} + +EBrowserActions.actions.subscribe((a) => console.log(JSON.stringify(a))) diff --git a/apps/modules/lib/SvelteStore.sys.mjs b/apps/modules/lib/SvelteStore.sys.mjs new file mode 100644 index 0000000..4cac276 --- /dev/null +++ b/apps/modules/lib/SvelteStore.sys.mjs @@ -0,0 +1,116 @@ +// @ts-check +/// + +/** @type {import('resource://app/modules/SvelteStore.sys.mjs').ReadableFn} */ +export function readable(initial, update) { + let value = initial + + let nextSubscription = 0 + let subscriptions = {} + + const init = !update + ? () => {} + : () => + update((v) => { + value = v + for (const id in subscriptions) { + subscriptions[id](value) + } + }) + + return { + subscribe(cb) { + if (nextSubscription === 0) init() + + const id = nextSubscription++ + subscriptions[id] = cb + cb(value) + return () => delete subscriptions[id] + }, + get: () => value, + } +} + +/** @type {import('resource://app/modules/SvelteStore.sys.mjs').WritableFn} */ +export function writable(initial) { + let value = initial + + let nextSubscription = 0 + let subscriptions = {} + + /** @param {typeof value} v */ + function set(v) { + value = v + for (const id in subscriptions) { + subscriptions[id](value) + } + } + + return { + set, + + subscribe(cb) { + const id = nextSubscription++ + subscriptions[id] = cb + cb(value) + return () => delete subscriptions[id] + }, + update: (cb) => set(cb(value)), + get: () => value, + } +} + +/** @type {import('resource://app/modules/SvelteStore.sys.mjs').MapFn} */ +export function map(initial) { + let value = initial + + let nextGlobalSubscription = 0 + let globalSubscriptions = {} + + function triggerGlobalUpdate() { + for (const id in globalSubscriptions) { + globalSubscriptions[id](value) + } + } + + return { + get: () => ({ ...value }), + /** @param {(value: typeof initial) => void} cb */ + subscribe(cb) { + const id = nextGlobalSubscription++ + globalSubscriptions[id] = cb + cb(value) + return () => delete globalSubscriptions[id] + }, + + /** @param {keyof typeof initial} k */ + key: (k) => value[k], + /** + * @param {keyof typeof initial} key + * @param {typeof initial[keyof typeof initial]} v + */ + addKey(key, v) { + value[key] = v + triggerGlobalUpdate() + return v + }, + /** @param {keyof typeof initial} key */ + removeKey(key) { + const v = value[key] + delete value[key] + triggerGlobalUpdate() + return v + }, + } +} + +/** @type {import('resource://app/modules/SvelteStore.sys.mjs').DerivedFn} */ +export function derived(stores, update) { + return readable(update(...stores.map((store) => store.get())), (set) => { + const performUpdate = () => + set(update(...stores.map((store) => store.get()))) + for (const store of stores) { + store.subscribe(performUpdate) + } + }) +} diff --git a/libs/link/package.json b/libs/link/package.json index b8f5316..51a31c8 100644 --- a/libs/link/package.json +++ b/libs/link/package.json @@ -1,8 +1,9 @@ { + "$schema": "https://json.schemastore.org/package.json", "name": "@browser/link", "version": "1.0.0", "description": "", - "types": "./types/_link.d.ts", + "types": "./types", "keywords": [], "author": "", "license": "ISC", diff --git a/libs/link/types/_link.d.ts b/libs/link/types/_link.d.ts index a0cf9a9..bb3820a 100644 --- a/libs/link/types/_link.d.ts +++ b/libs/link/types/_link.d.ts @@ -8,6 +8,8 @@ /// /// +/// + /// /// /// @@ -21,10 +23,12 @@ /// /// +/// /// /// /// /// +/// /// /// /// diff --git a/libs/link/types/index.d.ts b/libs/link/types/index.d.ts new file mode 100644 index 0000000..d2485d7 --- /dev/null +++ b/libs/link/types/index.d.ts @@ -0,0 +1 @@ +/// diff --git a/libs/link/types/modules/EBrowserActions.d.ts b/libs/link/types/modules/EBrowserActions.d.ts new file mode 100644 index 0000000..8aae7b0 --- /dev/null +++ b/libs/link/types/modules/EBrowserActions.d.ts @@ -0,0 +1,49 @@ +/* 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/. */ + +declare module 'resource://app/modules/EBrowserActions.sys.mjs' { + import type { Emitter } from 'mitt' + import type { + IMapStore, + IReadable, + } from 'resource://app/modules/SvelteStore.sys.mjs' + + export interface IBrowserAction { + getEmmiter(): Emitter<{ + click: { clickData: { modifiers: string[]; button: number } } + }> + + getExtensionId(): string + + setIcons(icons: Record): void + getIcons(): IReadable> + getIcon(resolution: number): IReadable + + setTitle(title: string): void + getTitle(): IReadable + + setPopupUrl(url: string): void + getPopupUrl(): IReadable + } + + export const EBrowserActions: { + BrowserAction: ( + id: string, + options: { + title: string + icons: Record + popupUrl?: string + }, + ) => IBrowserAction + actions: IMapStore + } +} + +declare interface MozESMExportFile { + EBrowserActions: 'resource://app/modules/EBrowserActions.sys.mjs' +} + +declare interface MozESMExportType { + EBrowserActions: typeof import('resource://app/modules/EBrowserActions.sys.mjs').EBrowserActions +} diff --git a/libs/link/types/modules/SvelteStore.d.ts b/libs/link/types/modules/SvelteStore.d.ts new file mode 100644 index 0000000..0169004 --- /dev/null +++ b/libs/link/types/modules/SvelteStore.d.ts @@ -0,0 +1,58 @@ +declare module 'resource://app/modules/SvelteStore.sys.mjs' { + export interface BasicStore { + /** + * Svelte will assume that the store is undefined until update is called. + * A sane implementaiton would call it once on subscribe + * + * @returns A method for unsubscribing from the store + */ + subscribe(update: (value: T) => void): () => void + } + + export interface BasicWritableStore extends BasicStore { + set(value: T): void + } + + export interface IReadable extends BasicStore { + get(): T + } + + export interface IWritable extends IReadable, BasicWritableStore { + update(call: (value: T) => T): void + } + + /** + * Get notified when keys are added or removed, but not changed. For changes, + * you should make the values stores + */ + export interface IMapStore + extends IReadable> { + addKey(key: K, value: V): V + removeKey(key: K): V + key(key: K): V | undefined + } + + export type ReadableFn = ( + value: T, + update?: (set: (value: T) => void) => void, + ) => IReadable + export const readable: ReadableFn + + export type WritableFn = (value: T) => IWritable + export const writable: WritableFn + + export type MapFn = ( + value: Record, + ) => IMapStore + export const map: MapFn + + export type StoreValues = T extends IReadable + ? U + : { [K in keyof T]: T[K] extends IReadable ? U : never } + + export type DerivedFn = >, V>( + stores: Stores, + update: (...values: StoreValues) => V, + ) => IReadable + export const derived: DerivedFn +} diff --git a/libs/link/types/schemaTypes/browser_action.d.ts b/libs/link/types/schemaTypes/browser_action.d.ts new file mode 100644 index 0000000..c13cf83 --- /dev/null +++ b/libs/link/types/schemaTypes/browser_action.d.ts @@ -0,0 +1,21 @@ +// @not-mpl +// This file is generated from '../schemas/browser_action.json'. This file inherits its license +// Please check that file's license +// +// DO NOT MODIFY MANUALLY + +declare module browser_action__manifest { + type WebExtensionManifest__extended = { + browser_action?: { + default_icon?: IconPath + default_popup?: string + default_title?: string + theme_icons?: ThemeIcons[] + browser_style?: boolean + default_area?: string + } + } + type ApiGetterReturn = { + manifest: {} + } +} diff --git a/libs/link/types/schemaTypes/index.d.ts b/libs/link/types/schemaTypes/index.d.ts new file mode 100644 index 0000000..6dfd373 --- /dev/null +++ b/libs/link/types/schemaTypes/index.d.ts @@ -0,0 +1,3 @@ +// @not-mpl +/// +/// \ No newline at end of file diff --git a/apps/extensions/lib/schemaTypes/tabs.d.ts b/libs/link/types/schemaTypes/tabs.d.ts similarity index 95% rename from apps/extensions/lib/schemaTypes/tabs.d.ts rename to libs/link/types/schemaTypes/tabs.d.ts index 1d13734..8e196c1 100644 --- a/apps/extensions/lib/schemaTypes/tabs.d.ts +++ b/libs/link/types/schemaTypes/tabs.d.ts @@ -5,6 +5,7 @@ // DO NOT MODIFY MANUALLY declare module tabs__manifest { + type OptionalPermission__extended = 'tabs' | 'tabHide' type ApiGetterReturn = { manifest: {} } diff --git a/package.json b/package.json index 4532abe..930b23b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "prettier-plugin-svelte": "^3.0.3", "tap-parser": "^15.3.2", "turbo": "^1.11.2", - "typescript": "^5.2.2" + "typescript": "^5.4.5" }, "pnpm": { "patchedDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9d21c7..2375a0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,7 +54,7 @@ importers: version: 3.0.3 prettier-plugin-organize-imports: specifier: ^3.2.3 - version: 3.2.3(prettier@3.0.3)(typescript@5.2.2) + version: 3.2.3(prettier@3.0.3)(typescript@5.4.5) prettier-plugin-svelte: specifier: ^3.0.3 version: 3.0.3(prettier@3.0.3)(svelte@4.2.12) @@ -65,8 +65,8 @@ importers: specifier: ^1.11.2 version: 1.11.2 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.4.5 + version: 5.4.5 apps/actors: dependencies: @@ -87,7 +87,7 @@ importers: version: 0.5.0 fnts: specifier: ^2.1.0 - version: 2.1.0(typescript@5.3.3) + version: 2.1.0(typescript@5.4.5) mitt: specifier: ^3.0.1 version: 3.0.1 @@ -133,7 +133,7 @@ importers: version: 2.7.6(webpack@5.89.0) postcss-loader: specifier: ^7.3.4 - version: 7.3.4(postcss@8.4.31)(typescript@5.3.3)(webpack@5.89.0) + version: 7.3.4(postcss@8.4.31)(typescript@5.4.5)(webpack@5.89.0) style-loader: specifier: ^3.3.3 version: 3.3.3(webpack@5.89.0) @@ -145,16 +145,16 @@ importers: version: 3.2.0(svelte@4.2.12) svelte-preprocess: specifier: ^5.1.3 - version: 5.1.3(postcss@8.4.31)(svelte@4.2.12)(typescript@5.3.3) + version: 5.1.3(postcss@8.4.31)(svelte@4.2.12)(typescript@5.4.5) svelte-sequential-preprocessor: specifier: ^2.0.1 version: 2.0.1 ts-loader: specifier: ^9.5.1 - version: 9.5.1(typescript@5.3.3)(webpack@5.89.0) + version: 9.5.1(typescript@5.4.5)(webpack@5.89.0) typescript: - specifier: ^5.3.3 - version: 5.3.3 + specifier: ^5.4.5 + version: 5.4.5 webpack: specifier: ^5.89.0 version: 5.89.0(webpack-cli@5.1.4) @@ -1407,7 +1407,7 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true - /cosmiconfig@8.3.6(typescript@5.3.3): + /cosmiconfig@8.3.6(typescript@5.4.5): resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} peerDependencies: @@ -1420,7 +1420,7 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - typescript: 5.3.3 + typescript: 5.4.5 dev: true /cross-spawn@7.0.3: @@ -2018,7 +2018,7 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true - /fnts@2.1.0(typescript@5.3.3): + /fnts@2.1.0(typescript@5.4.5): resolution: {integrity: sha512-+KFw3//Dbxw3R5xCzP+3BMsjhNiRaJ8w1Al/1aCzI7OqrqzIHCpNZShY1nUFPciqHqe8Z3URo+aA595ePa7Kqw==} peerDependencies: typescript: '>=4.7' @@ -2026,7 +2026,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.3.3 + typescript: 5.4.5 dev: false /follow-redirects@1.15.3: @@ -3081,14 +3081,14 @@ packages: find-up: 4.1.0 dev: true - /postcss-loader@7.3.4(postcss@8.4.31)(typescript@5.3.3)(webpack@5.89.0): + /postcss-loader@7.3.4(postcss@8.4.31)(typescript@5.4.5)(webpack@5.89.0): resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} engines: {node: '>= 14.15.0'} peerDependencies: postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 dependencies: - cosmiconfig: 8.3.6(typescript@5.3.3) + cosmiconfig: 8.3.6(typescript@5.4.5) jiti: 1.20.0 postcss: 8.4.31 semver: 7.5.4 @@ -3164,7 +3164,7 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /prettier-plugin-organize-imports@3.2.3(prettier@3.0.3)(typescript@5.2.2): + /prettier-plugin-organize-imports@3.2.3(prettier@3.0.3)(typescript@5.4.5): resolution: {integrity: sha512-KFvk8C/zGyvUaE3RvxN2MhCLwzV6OBbFSkwZ2OamCrs9ZY4i5L77jQ/w4UmUr+lqX8qbaqVq6bZZkApn+IgJSg==} peerDependencies: '@volar/vue-language-plugin-pug': ^1.0.4 @@ -3178,7 +3178,7 @@ packages: optional: true dependencies: prettier: 3.0.3 - typescript: 5.2.2 + typescript: 5.4.5 dev: true /prettier-plugin-svelte@3.0.3(prettier@3.0.3)(svelte@4.2.12): @@ -3786,7 +3786,7 @@ packages: svelte-hmr: 0.14.12(svelte@4.2.12) dev: true - /svelte-preprocess@5.1.3(postcss@8.4.31)(svelte@4.2.12)(typescript@5.3.3): + /svelte-preprocess@5.1.3(postcss@8.4.31)(svelte@4.2.12)(typescript@5.4.5): resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==} engines: {node: '>= 16.0.0', pnpm: ^8.0.0} requiresBuild: true @@ -3831,7 +3831,7 @@ packages: sorcery: 0.11.0 strip-indent: 3.0.0 svelte: 4.2.12(patch_hash=cm43hmf4gczhssi3isoosy53r4) - typescript: 5.3.3 + typescript: 5.4.5 dev: true /svelte-remixicon@2.4.0(svelte@4.2.12): @@ -3963,7 +3963,7 @@ packages: utf8-byte-length: 1.0.4 dev: true - /ts-loader@9.5.1(typescript@5.3.3)(webpack@5.89.0): + /ts-loader@9.5.1(typescript@5.4.5)(webpack@5.89.0): resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} engines: {node: '>=12.0.0'} peerDependencies: @@ -3975,7 +3975,7 @@ packages: micromatch: 4.0.5 semver: 7.5.4 source-map: 0.7.4 - typescript: 5.3.3 + typescript: 5.4.5 webpack: 5.89.0(webpack-cli@5.1.4) dev: true @@ -4063,22 +4063,10 @@ packages: mime-types: 2.1.35 dev: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} - engines: {node: '>=14.17'} - hasBin: true - /typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true - dev: true /undici-types@5.25.3: resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} diff --git a/scripts/unit-test.ts b/scripts/unit-test.ts index 5d5020b..254dd40 100644 --- a/scripts/unit-test.ts +++ b/scripts/unit-test.ts @@ -31,7 +31,7 @@ function runner(testPage: string) { testProcess?.kill() console.error('Process timed out') exit(1) - }, 10_000) + }, 100_000) testProcess.on('exit', () => clearTimeout(timeout)) From 207c9ea8e126422a0b8e0c88687f88b7c389b0cf Mon Sep 17 00:00:00 2001 From: TrickyPR <23250792+trickypr@users.noreply.github.com> Date: Sun, 21 Apr 2024 17:58:16 +1000 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8=20Impl=20basic=20`getTitle`=20and?= =?UTF-8?q?=20`setTitle`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/extensions/lib/ext-browser.json | 3 +- .../lib/parent/ext-browserAction.js | 27 +++++ .../lib/schemas/browser_action.json | 58 ++++++++++ apps/extensions/lib/types/utils.d.ts | 9 +- apps/extensions/scripts/buildTypes.js | 12 +- apps/modules/lib/EBrowserActions.sys.mjs | 4 + apps/modules/lib/SvelteStore.sys.mjs | 4 + apps/tests/integrations/_index.sys.mjs | 1 + .../integrations/extensions/browserAction.mjs | 107 ++++++++++++++++++ libs/link/types/index.d.ts | 4 + libs/link/types/modules/EBrowserActions.d.ts | 4 +- .../types/modules/ExtensionTestUtils.d.ts | 4 + libs/link/types/modules/SvelteStore.d.ts | 4 + .../types/schemaTypes/browser_action.d.ts | 15 +++ libs/link/types/schemaTypes/index.d.ts | 4 +- 15 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 apps/tests/integrations/extensions/browserAction.mjs diff --git a/apps/extensions/lib/ext-browser.json b/apps/extensions/lib/ext-browser.json index c216f04..50f819b 100644 --- a/apps/extensions/lib/ext-browser.json +++ b/apps/extensions/lib/ext-browser.json @@ -3,7 +3,8 @@ "url": "chrome://bextensions/content/parent/ext-browserAction.js", "schema": "chrome://bextensions/content/schemas/browser_action.json", "scopes": ["addon_parent"], - "manifest": ["browser_action"] + "manifest": ["browser_action"], + "paths": [["browserAction"]] }, "pageAction": { "url": "chrome://bextensions/content/parent/ext-pageAction.js", diff --git a/apps/extensions/lib/parent/ext-browserAction.js b/apps/extensions/lib/parent/ext-browserAction.js index e3138c2..7e2cdc1 100644 --- a/apps/extensions/lib/parent/ext-browserAction.js +++ b/apps/extensions/lib/parent/ext-browserAction.js @@ -1,3 +1,7 @@ +/* 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/. */ + // @ts-check /// /// @@ -30,9 +34,32 @@ this.browserAction = class extends ExtensionAPIPersistent { }) lazy.EBrowserActions.actions.addKey(extension.id, this.browserAction) + console.log( + 'from EBrowserAction', + JSON.stringify(lazy.EBrowserActions.actions.get()), + ) } onShutdown() { lazy.EBrowserActions.actions.removeKey(this.extension.id) } + + /** @returns {browser_action__browserAction.ApiGetterReturn} */ + // eslint-disable-next-line no-unused-vars + getAPI(context) { + const { browserAction } = this + + return { + browserAction: { + setTitle({ title }) { + // TODO: Tab & Window implementation + browserAction?.setTitle(title) + }, + async getTitle() { + // TODO: Tab & Window impl + return browserAction?.getTitle().get() + }, + }, + } + } } diff --git a/apps/extensions/lib/schemas/browser_action.json b/apps/extensions/lib/schemas/browser_action.json index 684d8e2..5b2c202 100644 --- a/apps/extensions/lib/schemas/browser_action.json +++ b/apps/extensions/lib/schemas/browser_action.json @@ -39,5 +39,63 @@ } } ] + }, + { + "namespace": "browserAction", + "description": "", + "types": [], + "functions": [ + { + "name": "setTitle", + "type": "function", + "async": true, + "parameters": [ + { + "name": "details", + "type": "object", + "properties": { + "title": { + "type": "string", + "optional": true + }, + "tabId": { + "type": "integer", + "optional": true + }, + "windowId": { + "type": "integer", + "optional": true + } + } + } + ] + }, + { + "name": "getTitle", + "type": "function", + "async": true, + "parameters": [ + { + "name": "details", + "type": "object", + "optional": true, + "properties": { + "tabId": { + "type": "integer", + "optional": true + }, + "windowId": { + "type": "integer", + "optional": true + } + } + } + ], + "returns": { + "type": "string", + "optional": true + } + } + ] } ] diff --git a/apps/extensions/lib/types/utils.d.ts b/apps/extensions/lib/types/utils.d.ts index 15d7e8b..a25acb0 100644 --- a/apps/extensions/lib/types/utils.d.ts +++ b/apps/extensions/lib/types/utils.d.ts @@ -156,13 +156,6 @@ declare global { [ONCE_MAP]: WeakMap } - class ExtensionData { - id: string - - manifest: browser._manifest.WebExtensionManifest & - browser_action__manifest.WebExtensionManifest__extended - } - /** * Base class for WebExtension APIs. Each API creates a new class * that inherits from this class, the derived class is instantiated @@ -170,7 +163,7 @@ declare global { */ class ExtensionAPI extends EventEmitter { constructor(extension: any) - extension: ExtensionData + extension: Extension destroy(): void onManifestEntry(entry: any): void getAPI(context: any): void diff --git a/apps/extensions/scripts/buildTypes.js b/apps/extensions/scripts/buildTypes.js index 9550e75..a21a869 100644 --- a/apps/extensions/scripts/buildTypes.js +++ b/apps/extensions/scripts/buildTypes.js @@ -91,6 +91,9 @@ const printer = createPrinter({ }) const QUESTION_TOKEN = factory.createToken(SyntaxKind.QuestionToken) +const UNDEFINED_TOKEN = factory.createKeywordTypeNode( + SyntaxKind.UndefinedKeyword, +) const modules = [] for (const file of fs.readdirSync(schemaFolder)) { @@ -259,7 +262,14 @@ function generateTypeNode(type) { ? typeof type.async !== 'undefined' && type.async ? factory.createTypeReferenceNode( factory.createIdentifier('Promise'), - [generateTypeNode(type.returns)], + [ + type.returns.optional + ? factory.createUnionTypeNode([ + generateTypeNode(type.returns), + UNDEFINED_TOKEN, + ]) + : generateTypeNode(type.returns), + ], ) : generateTypeNode(type.returns) : factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword), diff --git a/apps/modules/lib/EBrowserActions.sys.mjs b/apps/modules/lib/EBrowserActions.sys.mjs index 61117bf..7379b2c 100644 --- a/apps/modules/lib/EBrowserActions.sys.mjs +++ b/apps/modules/lib/EBrowserActions.sys.mjs @@ -1,3 +1,7 @@ +/* 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/. */ + // @ts-check /// import { map, writable } from 'resource://app/modules/SvelteStore.sys.mjs' diff --git a/apps/modules/lib/SvelteStore.sys.mjs b/apps/modules/lib/SvelteStore.sys.mjs index 4cac276..8d9ccba 100644 --- a/apps/modules/lib/SvelteStore.sys.mjs +++ b/apps/modules/lib/SvelteStore.sys.mjs @@ -1,3 +1,7 @@ +/* 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/. */ + // @ts-check /// diff --git a/apps/tests/integrations/_index.sys.mjs b/apps/tests/integrations/_index.sys.mjs index c1a4e99..b42d005 100644 --- a/apps/tests/integrations/_index.sys.mjs +++ b/apps/tests/integrations/_index.sys.mjs @@ -1,5 +1,6 @@ /* 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/. */ +import './extensions/browserAction.mjs' import './extensions/pageAction.mjs' import './extensions/tabs.mjs' diff --git a/apps/tests/integrations/extensions/browserAction.mjs b/apps/tests/integrations/extensions/browserAction.mjs new file mode 100644 index 0000000..1dcfe1e --- /dev/null +++ b/apps/tests/integrations/extensions/browserAction.mjs @@ -0,0 +1,107 @@ +/* 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/. */ + +// @ts-check +import { ExtensionTestUtils } from 'resource://app/modules/ExtensionTestUtils.sys.mjs' +import { TestManager } from 'resource://app/modules/TestManager.sys.mjs' +import { lazyESModuleGetters } from 'resource://app/modules/TypedImportUtils.sys.mjs' + +const lazy = lazyESModuleGetters({ + EBrowserActions: 'resource://app/modules/EBrowserActions.sys.mjs', +}) + +/** @param {number} len */ +const delay = + (len) => + /** + * @template T + * @param {T} v + * @returns {Promise} + */ (v) => + new Promise((res) => setTimeout(() => res(v), len)) + +await TestManager.withBrowser(['http://example.com/'], async (window) => { + await TestManager.test('browserAction - Icon & Panel', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + browser_action: { + default_icon: './flask-line.svg', + default_popup: 'browseraction.html', + default_title: 'pageaction title', + }, + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async () => { + b.test.assertEq( + 'pageaction title', + await b.browserAction.getTitle({}), + 'Page action title should match default', + ) + + await b.browserAction.setTitle({ title: 'new title' }) + + b.test.assertEq( + 'new title', + await b.browserAction.getTitle({}), + 'Page action title should have updated', + ) + b.test.sendMessage('done') + }) + }, + files: { + 'flask-line.svg': ``, + 'browseraction.html': ` + + + + + + + +

Hello world

+ + `, + }, + }, + test, + ) + + await extension + .startup() + .then(delay(100)) + .then((e) => { + test.truthy( + lazy.EBrowserActions.actions.key(extension.extension.id), + 'Browser action should be registered', + ) + test.equals( + lazy.EBrowserActions.actions + .key(extension.extension.id) + ?.getTitle() + .get(), + 'pageaction title', + 'Title is saved correctly', + ) + return e + }) + .then((e) => e.sendMsg('')) + .then((e) => e.awaitMsg('done')) + .then((e) => { + test.equals( + lazy.EBrowserActions.actions + .key(extension.extension.id) + ?.getTitle() + .get(), + 'new title', + 'Title updated', + ) + return e + }) + .then((e) => e.unload()) + }) +}) diff --git a/libs/link/types/index.d.ts b/libs/link/types/index.d.ts index d2485d7..ff00be5 100644 --- a/libs/link/types/index.d.ts +++ b/libs/link/types/index.d.ts @@ -1 +1,5 @@ +/* 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/. */ + /// diff --git a/libs/link/types/modules/EBrowserActions.d.ts b/libs/link/types/modules/EBrowserActions.d.ts index 8aae7b0..64225bf 100644 --- a/libs/link/types/modules/EBrowserActions.d.ts +++ b/libs/link/types/modules/EBrowserActions.d.ts @@ -10,7 +10,7 @@ declare module 'resource://app/modules/EBrowserActions.sys.mjs' { } from 'resource://app/modules/SvelteStore.sys.mjs' export interface IBrowserAction { - getEmmiter(): Emitter<{ + getEmiter(): Emitter<{ click: { clickData: { modifiers: string[]; button: number } } }> @@ -20,7 +20,7 @@ declare module 'resource://app/modules/EBrowserActions.sys.mjs' { getIcons(): IReadable> getIcon(resolution: number): IReadable - setTitle(title: string): void + setTitle(title: string | undefined): void getTitle(): IReadable setPopupUrl(url: string): void diff --git a/libs/link/types/modules/ExtensionTestUtils.d.ts b/libs/link/types/modules/ExtensionTestUtils.d.ts index 2630dce..27352c2 100644 --- a/libs/link/types/modules/ExtensionTestUtils.d.ts +++ b/libs/link/types/modules/ExtensionTestUtils.d.ts @@ -38,6 +38,10 @@ declare module 'resource://app/modules/ExtensionTestUtils.sys.mjs' { expectedError: ExpectedError, message: string, ) => unknown + + onMessage: { + addListener: (fn: Function) => void + } } } diff --git a/libs/link/types/modules/SvelteStore.d.ts b/libs/link/types/modules/SvelteStore.d.ts index 0169004..25693fe 100644 --- a/libs/link/types/modules/SvelteStore.d.ts +++ b/libs/link/types/modules/SvelteStore.d.ts @@ -1,3 +1,7 @@ +/* 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/. */ + declare module 'resource://app/modules/SvelteStore.sys.mjs' { export interface BasicStore { /** diff --git a/libs/link/types/schemaTypes/browser_action.d.ts b/libs/link/types/schemaTypes/browser_action.d.ts index c13cf83..54fb6ec 100644 --- a/libs/link/types/schemaTypes/browser_action.d.ts +++ b/libs/link/types/schemaTypes/browser_action.d.ts @@ -19,3 +19,18 @@ declare module browser_action__manifest { manifest: {} } } +declare module browser_action__browserAction { + type ApiGetterReturn = { + browserAction: { + setTitle: (details: { + title?: string + tabId?: number + windowId?: number + }) => unknown + getTitle: (details?: { + tabId?: number + windowId?: number + }) => Promise + } + } +} diff --git a/libs/link/types/schemaTypes/index.d.ts b/libs/link/types/schemaTypes/index.d.ts index 6dfd373..24201db 100644 --- a/libs/link/types/schemaTypes/index.d.ts +++ b/libs/link/types/schemaTypes/index.d.ts @@ -1,3 +1,3 @@ -// @not-mpl +// @not-mpl /// -/// \ No newline at end of file +/// From f15da336487150463f6ae003fb54aa5a593eed94 Mon Sep 17 00:00:00 2001 From: TrickyPR <23250792+trickypr@users.noreply.github.com> Date: Sun, 21 Apr 2024 18:33:48 +1000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=9A=A7=20Temporary=20(bad)=20onClicke?= =?UTF-8?q?d=20event=20impl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/parent/ext-browserAction.js | 56 +++++++++++++++++-- .../lib/schemas/browser_action.json | 24 ++++++++ apps/extensions/lib/types/utils.d.ts | 30 +++++++--- apps/extensions/scripts/buildTypes.js | 34 +++++++++-- apps/modules/lib/EBrowserActions.sys.mjs | 9 ++- .../integrations/extensions/browserAction.mjs | 32 ++++++++--- .../types/schemaTypes/browser_action.d.ts | 1 + libs/link/types/schemaTypes/index.d.ts | 4 +- 8 files changed, 156 insertions(+), 34 deletions(-) diff --git a/apps/extensions/lib/parent/ext-browserAction.js b/apps/extensions/lib/parent/ext-browserAction.js index 7e2cdc1..631c390 100644 --- a/apps/extensions/lib/parent/ext-browserAction.js +++ b/apps/extensions/lib/parent/ext-browserAction.js @@ -32,18 +32,58 @@ this.browserAction = class extends ExtensionAPIPersistent { title: options.default_title || extension.id, popupUrl: options.default_popup, }) + this.browserAction.getEmiter().on('click', (v) => this.emit('click', v)) lazy.EBrowserActions.actions.addKey(extension.id, this.browserAction) - console.log( - 'from EBrowserAction', - JSON.stringify(lazy.EBrowserActions.actions.get()), - ) } onShutdown() { lazy.EBrowserActions.actions.removeKey(this.extension.id) } + PERSISTENT_EVENTS = { + /** + * @param {object} options + * @param {object} options.fire + * @param {function} options.fire.async + * @param {function} options.fire.sync + * @param {function} options.fire.raw + * For primed listeners `fire.async`/`fire.sync`/`fire.raw` will + * collect the pending events to be send to the background context + * and implicitly wake up the background context (Event Page or + * Background Service Worker), or forward the event right away if + * the background context is running. + * @param {function} [options.fire.wakeup = undefined] + * For primed listeners, the `fire` object also provide a `wakeup` method + * which can be used by the primed listener to explicitly `wakeup` the + * background context (Event Page or Background Service Worker) and wait for + * it to be running (by awaiting on the Promise returned by wakeup to be + * resolved). + * @param {ProxyContextParent} [options.context=undefined] + * This property is expected to be undefined for primed listeners (which + * are created while the background extension context does not exist) and + * to be set to a ProxyContextParent instance (the same got by the getAPI + * method) when the method is called for a listener registered by a + * running extension context. + */ + onClicked({ fire }) { + const callback = async (_name, clickInfo) => { + if (fire.wakeup) await fire.wakeup() + fire.sync(clickInfo) + } + + this.on('click', callback) + return { + unregister: () => { + this.off('click', callback) + }, + convert(newFire) { + fire = newFire + }, + } + }, + } + /** @returns {browser_action__browserAction.ApiGetterReturn} */ // eslint-disable-next-line no-unused-vars getAPI(context) { @@ -59,6 +99,14 @@ this.browserAction = class extends ExtensionAPIPersistent { // TODO: Tab & Window impl return browserAction?.getTitle().get() }, + + onClicked: new EventManager({ + context, + module: 'browserAction', + event: 'onClicked', + inputHandling: true, + extensionApi: this, + }).api(), }, } } diff --git a/apps/extensions/lib/schemas/browser_action.json b/apps/extensions/lib/schemas/browser_action.json index 5b2c202..5089da4 100644 --- a/apps/extensions/lib/schemas/browser_action.json +++ b/apps/extensions/lib/schemas/browser_action.json @@ -44,6 +44,30 @@ "namespace": "browserAction", "description": "", "types": [], + "events": [ + { + "name": "onClicked", + "type": "function", + "description": "Notify when the browser action is clicked", + "parameters": [ + { + "name": "tab", + "$ref": "Tab" + }, + { + "name": "OnClickData", + "type": "object", + "properties": { + "modifiers": { + "type": "array", + "items": { "type": "string" } + }, + "button": { "type": "integer" } + } + } + ] + } + ], "functions": [ { "name": "setTitle", diff --git a/apps/extensions/lib/types/utils.d.ts b/apps/extensions/lib/types/utils.d.ts index a25acb0..42ffd57 100644 --- a/apps/extensions/lib/types/utils.d.ts +++ b/apps/extensions/lib/types/utils.d.ts @@ -4,6 +4,8 @@ /* eslint-disable @typescript-eslint/ban-types */ /// +import { Module } from 'module' + import { ConduitAddress } from 'resource://gre/modules/ConduitsParent.sys.mjs' import { Extension } from 'resource://gre/modules/Extension.sys.mjs' import { SchemaRoot } from 'resource://gre/modules/Schemas.sys.mjs' @@ -740,6 +742,16 @@ declare global { */ loadScript(scriptUrl: string): void } + + type EventApi = { + [x: number]: () => void + addListener: (...args: any[]) => void + removeListener: (...args: any[]) => void + hasListener: (...args: any[]) => boolean + setUserInput: any + _typechecking: { module: Module; event: Event } + } + /** * This is a generic class for managing event listeners. * @@ -766,7 +778,7 @@ declare global { * ExtensionContext in the chrome process or ExtensionContext in a * content process). */ - class EventManager { + class EventManager { static _initPersistentListeners(extension: any): boolean static _writePersistentListeners(extension: any): void static primeListeners(extension: any, isInStartup?: boolean): void @@ -800,7 +812,13 @@ declare global { key?: any, primeId?: any, ): void - constructor(params: any) + constructor(params: { + context: any + module: Module + event: Event + inputHandling: boolean + extensionApi: ExtensionAPI + }) context: any module: any event: any @@ -816,13 +834,7 @@ declare global { hasListener(callback: any): boolean revoke(): void close(): void - api(): { - [x: number]: () => void - addListener: (...args: any[]) => void - removeListener: (...args: any[]) => void - hasListener: (...args: any[]) => boolean - setUserInput: any - } + api(): EventApi } const LISTENERS: unique symbol const ONCE_MAP: unique symbol diff --git a/apps/extensions/scripts/buildTypes.js b/apps/extensions/scripts/buildTypes.js index a21a869..89bf255 100644 --- a/apps/extensions/scripts/buildTypes.js +++ b/apps/extensions/scripts/buildTypes.js @@ -23,6 +23,7 @@ const { * @property {string} description * @property {Type[]} [types] * @property {FunctionType[]} [functions] + * @property {FunctionType[]} [events] */ /** @@ -121,7 +122,11 @@ for (const file of fs.readdirSync(schemaFolder)) { undefined, 'ApiGetterReturn', undefined, - generateApiGetter(namespace.functions || [], namespace.namespace), + generateApiGetter( + namespace.functions || [], + namespace.events || [], + namespace.namespace, + ), ), ]) @@ -172,16 +177,17 @@ function generateTypes(types) { /** * @param {FunctionType[]} functions + * @param {FunctionType[]} events * @param {string} apiName */ -function generateApiGetter(functions, apiName) { +function generateApiGetter(functions, events, apiName) { return factory.createTypeLiteralNode([ factory.createPropertySignature( undefined, factory.createIdentifier(apiName), undefined, - factory.createTypeLiteralNode( - functions.map((fn) => + factory.createTypeLiteralNode([ + ...functions.map((fn) => factory.createPropertySignature( undefined, factory.createIdentifier(fn.name), @@ -189,7 +195,25 @@ function generateApiGetter(functions, apiName) { generateTypeNode(fn), ), ), - ), + ...events.map((event) => + factory.createPropertySignature( + undefined, + factory.createIdentifier(event.name), + undefined, + factory.createExpressionWithTypeArguments( + factory.createIdentifier('EventApi'), + [ + factory.createLiteralTypeNode( + factory.createStringLiteral(apiName), + ), + factory.createLiteralTypeNode( + factory.createStringLiteral(event.name), + ), + ], + ), + ), + ), + ]), ), ]) } diff --git a/apps/modules/lib/EBrowserActions.sys.mjs b/apps/modules/lib/EBrowserActions.sys.mjs index 7379b2c..bd40516 100644 --- a/apps/modules/lib/EBrowserActions.sys.mjs +++ b/apps/modules/lib/EBrowserActions.sys.mjs @@ -1,7 +1,6 @@ /* 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/. */ - // @ts-check /// import { map, writable } from 'resource://app/modules/SvelteStore.sys.mjs' @@ -13,8 +12,8 @@ import { derived } from './SvelteStore.sys.mjs' /** @implements {IBrowserAction} */ class BrowserAction { id - /** @type {ReturnType} */ - emmiter = mitt() + /** @type {ReturnType} */ + emiter = mitt() /** @type{import('resource://app/modules/SvelteStore.sys.mjs').IWritable>} */ icons = writable({}) @@ -30,8 +29,8 @@ class BrowserAction { this.id = id } - getEmmiter() { - return this.emmiter + getEmiter() { + return this.emiter } getExtensionId() { diff --git a/apps/tests/integrations/extensions/browserAction.mjs b/apps/tests/integrations/extensions/browserAction.mjs index 1dcfe1e..6dfcc81 100644 --- a/apps/tests/integrations/extensions/browserAction.mjs +++ b/apps/tests/integrations/extensions/browserAction.mjs @@ -1,7 +1,6 @@ /* 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/. */ - // @ts-check import { ExtensionTestUtils } from 'resource://app/modules/ExtensionTestUtils.sys.mjs' import { TestManager } from 'resource://app/modules/TestManager.sys.mjs' @@ -21,6 +20,13 @@ const delay = */ (v) => new Promise((res) => setTimeout(() => res(v), len)) +/** + * @param {() => Promise} fn + * @param {number} timeout + */ +const timeoutFail = (fn, timeout) => + Promise.race([delay(timeout)(false), fn().then(() => true)]) + await TestManager.withBrowser(['http://example.com/'], async (window) => { await TestManager.test('browserAction - Icon & Panel', async (test) => { const extension = ExtensionTestUtils.loadExtension( @@ -52,6 +58,10 @@ await TestManager.withBrowser(['http://example.com/'], async (window) => { ) b.test.sendMessage('done') }) + + b.browserAction.onClicked.addListener((tab, info) => { + b.test.sendMessage('clicked') + }) }, files: { 'flask-line.svg': ``, @@ -72,6 +82,7 @@ await TestManager.withBrowser(['http://example.com/'], async (window) => { ) await extension + .testCount(2) .startup() .then(delay(100)) .then((e) => { @@ -91,15 +102,18 @@ await TestManager.withBrowser(['http://example.com/'], async (window) => { }) .then((e) => e.sendMsg('')) .then((e) => e.awaitMsg('done')) - .then((e) => { - test.equals( - lazy.EBrowserActions.actions - .key(extension.extension.id) - ?.getTitle() - .get(), - 'new title', - 'Title updated', + .then(async (e) => { + const action = lazy.EBrowserActions.actions.key(extension.extension.id) + test.equals(action?.getTitle().get(), 'new title', 'Title updated') + + action + ?.getEmiter() + .emit('click', { clickData: { modifiers: [], button: 0 } }) + test.truthy( + await timeoutFail(() => e.awaitMsg('clicked'), 10), + 'Click event was passed in a timely manner', ) + return e }) .then((e) => e.unload()) diff --git a/libs/link/types/schemaTypes/browser_action.d.ts b/libs/link/types/schemaTypes/browser_action.d.ts index 54fb6ec..6f6ec23 100644 --- a/libs/link/types/schemaTypes/browser_action.d.ts +++ b/libs/link/types/schemaTypes/browser_action.d.ts @@ -31,6 +31,7 @@ declare module browser_action__browserAction { tabId?: number windowId?: number }) => Promise + onClicked: EventApi<'browserAction', 'onClicked'> } } } diff --git a/libs/link/types/schemaTypes/index.d.ts b/libs/link/types/schemaTypes/index.d.ts index 24201db..6dfd373 100644 --- a/libs/link/types/schemaTypes/index.d.ts +++ b/libs/link/types/schemaTypes/index.d.ts @@ -1,3 +1,3 @@ -// @not-mpl +// @not-mpl /// -/// +/// \ No newline at end of file From 6f47a4722d032431f85dc9ea9ce89b393a3d22b0 Mon Sep 17 00:00:00 2001 From: TrickyPR <23250792+trickypr@users.noreply.github.com> Date: Mon, 22 Apr 2024 19:37:14 +1000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=92=84=20Add=20browser=20actions=20to?= =?UTF-8?q?=20the=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/content/src/browser/browserImports.js | 1 + .../browser/components/BrowserAction.svelte | 62 +++++++ .../browser/components/ToolbarButton.svelte | 4 +- .../src/browser/components/WebsiteView.svelte | 8 + .../src/browser/components/browserAction.js | 162 ++++++++++++++++++ .../src/browser/utils/browserElement.js | 2 + apps/content/tsconfig.json | 4 +- .../lib/schemas/browser_action.json | 3 +- apps/modules/lib/EBrowserActions.sys.mjs | 3 + libs/link/types/globals/Elements.d.ts | 1 + libs/link/types/modules/EBrowserActions.d.ts | 5 +- libs/link/types/schemaTypes/index.d.ts | 4 +- 12 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 apps/content/src/browser/components/BrowserAction.svelte create mode 100644 apps/content/src/browser/components/browserAction.js diff --git a/apps/content/src/browser/browserImports.js b/apps/content/src/browser/browserImports.js index b18962a..97843a6 100644 --- a/apps/content/src/browser/browserImports.js +++ b/apps/content/src/browser/browserImports.js @@ -7,6 +7,7 @@ import { lazyESModuleGetters } from '../shared/lazy.js' export const browserImports = lazyESModuleGetters({ AppConstants: 'resource://gre/modules/AppConstants.sys.mjs', E10SUtils: 'resource://gre/modules/E10SUtils.sys.mjs', + EBrowserActions: 'resource://app/modules/EBrowserActions.sys.mjs', EPageActions: 'resource://app/modules/EPageActions.sys.mjs', NetUtil: 'resource://gre/modules/NetUtil.sys.mjs', PageThumbs: 'resource://gre/modules/PageThumbs.sys.mjs', diff --git a/apps/content/src/browser/components/BrowserAction.svelte b/apps/content/src/browser/components/BrowserAction.svelte new file mode 100644 index 0000000..7f1465f --- /dev/null +++ b/apps/content/src/browser/components/BrowserAction.svelte @@ -0,0 +1,62 @@ + + + + + { + open = true + const { clickModifiersFromEvent } = await import('./browserAction.js') + action.getEmiter().emit('click', { + tabId: browserView.browserId, + clickData: clickModifiersFromEvent(e), + }) + }} +> + + + +{#if $url} + {#await actionPanel then ap} + (open = false)} + class="popup" + id={`page-action-panel__${action.getExtensionId()}--${ + browserView.browserId + }`} + > + {/await} +{/if} + + diff --git a/apps/content/src/browser/components/ToolbarButton.svelte b/apps/content/src/browser/components/ToolbarButton.svelte index aa8aa81..dd4b4cb 100644 --- a/apps/content/src/browser/components/ToolbarButton.svelte +++ b/apps/content/src/browser/components/ToolbarButton.svelte @@ -4,9 +4,11 @@ - diff --git a/apps/content/src/browser/components/WebsiteView.svelte b/apps/content/src/browser/components/WebsiteView.svelte index 51f013b..cae4afe 100644 --- a/apps/content/src/browser/components/WebsiteView.svelte +++ b/apps/content/src/browser/components/WebsiteView.svelte @@ -5,6 +5,7 @@