From 7f3e933b75911bfba17bb6e6aad7813bc68069c8 Mon Sep 17 00:00:00 2001 From: trickypr <23250792+trickypr@users.noreply.github.com> Date: Sat, 13 Apr 2024 20:04:02 +1000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20First=20pass=20tab=20api=20implemen?= =?UTF-8?q?tation=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## APIs added - `tabs.query` (partial) ## Infra added - `apps/extensions/scripts/buildTypes.js`: Generates typescript definitions from the schema files to ensure everything is in sync ## Testing - Basic tests for `tabs.query` --- apps/content/src/browser/browser.js | 12 +- apps/content/src/browser/browserImports.js | 1 + .../src/browser/windowApi/WebsiteView.js | 11 + .../src/browser/windowApi/WindowTabs.js | 17 +- apps/extensions/lib/ext-browser.json | 6 + apps/extensions/lib/parent/ext-pageAction.js | 2 - apps/extensions/lib/parent/ext-tabs.js | 291 +++++++++++++ apps/extensions/lib/schemaTypes/tabs.d.ts | 45 ++ apps/extensions/lib/schemas/tabs.json | 197 +++++++++ apps/extensions/package.json | 12 +- apps/extensions/scripts/buildTypes.js | 273 ++++++++++++ apps/modules/lib/BrowserWindowTracker.sys.mjs | 18 +- apps/modules/lib/ExtensionTestUtils.sys.mjs | 41 +- apps/modules/lib/TestManager.sys.mjs | 6 +- apps/tests/integrations/_index.sys.mjs | 1 + .../integrations/extensions/pageAction.mjs | 4 +- apps/tests/integrations/extensions/tabs.mjs | 407 ++++++++++++++++++ apps/tests/package.json | 5 +- libs/link/types/globals/WindowApi.d.ts | 9 + .../types/modules/ExtensionTestUtils.d.ts | 44 +- libs/link/types/modules/TestManager.d.ts | 2 +- libs/link/types/windowApi/WebsiteView.d.ts | 5 + libs/link/types/windowApi/WindowTabs.d.ts | 4 +- pnpm-lock.yaml | 40 +- 24 files changed, 1406 insertions(+), 47 deletions(-) create mode 100644 apps/extensions/lib/parent/ext-tabs.js create mode 100644 apps/extensions/lib/schemaTypes/tabs.d.ts create mode 100644 apps/extensions/lib/schemas/tabs.json create mode 100644 apps/extensions/scripts/buildTypes.js create mode 100644 apps/tests/integrations/extensions/tabs.mjs diff --git a/apps/content/src/browser/browser.js b/apps/content/src/browser/browser.js index 98aff99..dc29d1f 100644 --- a/apps/content/src/browser/browser.js +++ b/apps/content/src/browser/browser.js @@ -4,12 +4,13 @@ // @ts-check import BrowserWindow from './BrowserWindow.svelte' import './browser.css' +import { browserImports } from './browserImports.js' import * as WindowTabs from './windowApi/WindowTabs.js' import { registerEventBus } from './windowApi/eventBus.js' // Handle window arguments let rawArgs = window.arguments && window.arguments[0] -/** @type {Record} */ +/** @type {Record} */ let args = {} if (rawArgs && rawArgs instanceof Ci.nsISupports) { @@ -18,8 +19,8 @@ if (rawArgs && rawArgs instanceof Ci.nsISupports) { args = rawArgs } -const initialUrls = args.initialUrl - ? [args.initialUrl] +const initialUrls = args.initialUrls + ? args.initialUrls : ['https://google.com/', 'https://svelte.dev/'] WindowTabs.initialize(initialUrls) @@ -27,3 +28,8 @@ WindowTabs.initialize(initialUrls) registerEventBus() new BrowserWindow({ target: document.body }) + +browserImports.WindowTracker.registerWindow(window) +window.addEventListener('unload', () => + browserImports.WindowTracker.removeWindow(window), +) diff --git a/apps/content/src/browser/browserImports.js b/apps/content/src/browser/browserImports.js index 9bc0499..b18962a 100644 --- a/apps/content/src/browser/browserImports.js +++ b/apps/content/src/browser/browserImports.js @@ -10,4 +10,5 @@ export const browserImports = lazyESModuleGetters({ EPageActions: 'resource://app/modules/EPageActions.sys.mjs', NetUtil: 'resource://gre/modules/NetUtil.sys.mjs', PageThumbs: 'resource://gre/modules/PageThumbs.sys.mjs', + WindowTracker: 'resource://app/modules/BrowserWindowTracker.sys.mjs', }) diff --git a/apps/content/src/browser/windowApi/WebsiteView.js b/apps/content/src/browser/windowApi/WebsiteView.js index 8ca47aa..0fdfb2e 100644 --- a/apps/content/src/browser/windowApi/WebsiteView.js +++ b/apps/content/src/browser/windowApi/WebsiteView.js @@ -7,6 +7,7 @@ import mitt from 'mitt' import { readable } from 'svelte/store' +import { browserImports } from '../browserImports.js' import { createBrowser } from '../utils/browserElement.js' import { eventBus } from './eventBus.js' @@ -31,6 +32,8 @@ export function create(uri) { const view = { windowBrowserId: nextWindowBrowserId++, browser: createBrowser(uri), + uri, + websiteState: 'loading', /** @type {import('mitt').Emitter} */ events: mitt(), @@ -49,6 +52,13 @@ export function create(uri) { registerViewThemeListener(view) }) + view.events.on('goTo', (e) => goTo(view, browserImports.NetUtil.newURI(e))) + view.events.on('locationChange', (e) => (view.uri = e.aLocation)) + view.events.on( + 'loadingChange', + (e) => (view.websiteState = e ? 'loading' : 'complete'), + ) + eventBus.on('iconUpdate', ({ browserId, iconUrl }) => { if (view.browser.browserId === browserId) { view.iconUrl = iconUrl @@ -293,6 +303,7 @@ class TabProgressListener { * @returns {void} */ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + if (!aWebProgress || !aWebProgress.isTopLevel) return this.view.events.emit('locationChange', { aWebProgress, aRequest, diff --git a/apps/content/src/browser/windowApi/WindowTabs.js b/apps/content/src/browser/windowApi/WindowTabs.js index bc0dac3..b279242 100644 --- a/apps/content/src/browser/windowApi/WindowTabs.js +++ b/apps/content/src/browser/windowApi/WindowTabs.js @@ -2,18 +2,11 @@ * 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 { writable } from '@amadeus-it-group/tansu' -import { derived } from 'svelte/store' +import { derived, writable } from '@amadeus-it-group/tansu' import { browserImports } from '../browserImports.js' import * as WebsiteViewApi from './WebsiteView.js' -/** - * @typedef {object} WebsiteTab - * @property {'website'} kind - * @property {WebsiteView} view - */ - export const activeTabId = writable(0) /** @@ -23,6 +16,9 @@ export const activeTabId = writable(0) */ export const selectedTabIds = writable([]) +window.activeTabId = activeTabId +window.selectedTabIds = selectedTabIds + /** * @param {number[]} ids */ @@ -53,7 +49,7 @@ activeTabId.subscribe((activeId) => { }) /** - * @type {import('@amadeus-it-group/tansu').WritableSignal} + * @type {import('@amadeus-it-group/tansu').WritableSignal} */ export const windowTabs = writable([]) @@ -63,6 +59,9 @@ export const activeTab = derived( $windowTabs.find((tab) => tab.view.windowBrowserId === $activeTabId), ) +window.windowTabs = windowTabs +window.activeTab = activeTab + /** * @param {string[]} urls */ diff --git a/apps/extensions/lib/ext-browser.json b/apps/extensions/lib/ext-browser.json index a5f0c5b..e9ebae6 100644 --- a/apps/extensions/lib/ext-browser.json +++ b/apps/extensions/lib/ext-browser.json @@ -5,5 +5,11 @@ "scopes": ["addon_parent"], "manifest": ["page_action"], "paths": [["pageAction"]] + }, + "tabs": { + "schema": "chrome://bextensions/content/schemas/tabs.json", + "url": "chrome://bextensions/content/parent/ext-tabs.js", + "scopes": ["addon_parent"], + "paths": [["tabs"]] } } diff --git a/apps/extensions/lib/parent/ext-pageAction.js b/apps/extensions/lib/parent/ext-pageAction.js index dd00f1e..5339ca1 100644 --- a/apps/extensions/lib/parent/ext-pageAction.js +++ b/apps/extensions/lib/parent/ext-pageAction.js @@ -68,9 +68,7 @@ this.pageAction = class extends ExtensionAPIPersistent { */ onClicked({ fire }) { const callback = async (_name, clickInfo) => { - console.log(fire, fire.wakeup, !!fire.wakeup) if (fire.wakeup) await fire.wakeup() - console.log('fire') fire.sync(clickInfo) } diff --git a/apps/extensions/lib/parent/ext-tabs.js b/apps/extensions/lib/parent/ext-tabs.js new file mode 100644 index 0000000..a2d209e --- /dev/null +++ b/apps/extensions/lib/parent/ext-tabs.js @@ -0,0 +1,291 @@ +/* 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 +/// +/// +/// + +/** + * @param {tabs__tabs.QueryInfo} queryInfo + * @returns {[import("@browser/tabs").WindowTab, Window][]} + */ +function query(queryInfo) { + const windows = [...lazy.WindowTracker.registeredWindows.entries()] + + const urlMatchSet = + (queryInfo.url && + (Array.isArray(queryInfo.url) + ? new MatchPatternSet(queryInfo.url) + : new MatchPatternSet([queryInfo.url]))) || + null + + return windows.flatMap(([windowId, window]) => { + const tabs = window.windowTabs() + const activeTab = window.activeTab() + + return tabs + .filter((tab) => { + const active = + queryInfo.active !== null + ? queryInfo.active + ? tab === activeTab + : tab !== activeTab + : true + const title = queryInfo.title + ? queryInfo.title === tab.view.title + : true + const url = + urlMatchSet === null + ? true + : urlMatchSet.matches(tab.view.uri.asciiSpec) + const window = + queryInfo.windowId === null ? true : queryInfo.windowId === windowId + + return active && title && url && window + }) + .map( + /** @returns {[import("@browser/tabs").WindowTab, Window]} */ (tab) => [ + tab, + window, + ], + ) + }) +} + +const serialize = + (extension) => + /** + * @param {[import("@browser/tabs").WindowTab, Window]} in + * @returns {tabs__tabs.Tab} + */ + ([tab, window]) => { + // TODO: Active tab & host permissions + const hasTabPermission = extension.hasPermission('tabs') + + return { + id: tab.view.browserId, + index: window.windowTabs().findIndex((wTab) => wTab === tab), + active: window.activeTab() === tab, + highlighted: false, // TODO + title: hasTabPermission && tab.view.title, + url: hasTabPermission && tab.view.uri.asciiSpec, + windowId: window.windowId, + } + } + +this.tabs = class extends ExtensionAPIPersistent { + PERSISTENT_EVENTS = {} + + /** + * @returns {tabs__tabs.ApiGetterReturn} + */ + getAPI(context) { + const { extension } = context + + /** + * @param {number} tabId + */ + async function get(tabId) { + const window = [...lazy.WindowTracker.registeredWindows.values()].find( + (window) => + window.windowTabs().some((tab) => tab.view.browserId === tabId), + ) + + if (!window) { + return Promise.reject({ + message: `Cannot find tab matching the id ${tabId}`, + }) + } + + const tab = window + .windowTabs() + .find((tab) => tab.view.browserId === tabId) + + if (!tab) { + return Promise.reject({ + message: `Cannot find tab matching the id ${tabId}`, + }) + } + + return { tab, window } + } + + return { + tabs: { + async get(tabId) { + const { tab, window } = await get(tabId) + return serialize(extension)([tab, window]) + }, + + async goBack(tabId) { + let tab + + if (tabId) { + tab = await get(tabId).then((all) => all.tab) + } else { + tab = lazy.WindowTracker.getActiveWindow()?.activeTab() + if (!tab) { + return + } + } + const complete = new Promise((res) => { + /** @param {boolean} isLoading */ + function complete(isLoading) { + if (isLoading) { + return + } + tab.view.events.off('loadingChange', complete) + res(undefined) + } + + tab.view.events.on('loadingChange', complete) + }) + tab.view.browser.goBack() + return complete + }, + + async goForward(tabId) { + let tab + + if (tabId) { + tab = await get(tabId).then((all) => all.tab) + } else { + tab = lazy.WindowTracker.getActiveWindow()?.activeTab() + if (!tab) { + return + } + } + + const complete = new Promise((res) => { + /** @param {boolean} isLoading */ + function complete(isLoading) { + if (isLoading) { + return + } + tab.view.events.off('loadingChange', complete) + res(undefined) + } + + tab.view.events.on('loadingChange', complete) + }) + tab.view.browser.goForward() + return complete + }, + + async query(queryInfo) { + return query(queryInfo).map(serialize(extension)) + }, + + async remove(tabIds) { + const windows = [...lazy.WindowTracker.registeredWindows.entries()] + + if (typeof tabIds === 'number') { + for (const window of windows.map((w) => w[1])) { + const tabs = window.windowTabs() + for (const tab of tabs) { + if (tab.view.browserId === tabIds) { + return window.windowTabs.update((tabs) => + tabs.filter((tab) => tab.view.browserId !== tabIds), + ) + } + } + } + + return + } + + for (const window of windows.map((w) => w[1])) { + const tabs = window.windowTabs() + for (const tab of tabs) { + if (tabIds.includes(tab.view.browserId || -1)) { + window.windowTabs.update((tabs) => + tabs.filter( + (tab) => !tabIds.includes(tab.view.browserId || -1), + ), + ) + break + } + } + } + }, + + async reload(tabIds) { + if (typeof tabIds === 'number') { + const { tab } = await get(tabIds) + tab.view.browser.reload() + return + } + + for (const id of tabIds) { + const { tab } = await get(id) + tab.view.browser.reload() + } + }, + + async update(tabId, updateProperties) { + const windows = lazy.WindowTracker.registeredWindows.values() + for (const window of windows) { + const tabs = window.windowTabs() + const hasTab = tabs.some((tab) => tab.view.browserId === tabId) + + if (!hasTab) { + continue + } + + let errors = null + let retTab + + window.windowTabs.update((tabs) => + tabs.map((tab) => { + if (tab.view.browserId === tabId) { + if (updateProperties.active) { + window.activeTabId.set(tab.view.windowBrowserId) + } + + if ( + updateProperties.highlighted && + window.activeTabId() !== tab.view.windowBrowserId + ) { + window.selectedTabIds.update((tabs) => { + if (tabs.includes(tab.view.windowBrowserId)) return tabs + return [...tabs, tab.view.windowBrowserId] + }) + } + + if (updateProperties.url) { + let url = context.uri.resolve(updateProperties.url) + + if ( + !context.checkLoadURL(url, { dontReportErrors: true }) + ) { + errors = `Invalid url: ${url}` + return tab + } + + tab.view.events.emit('goTo', url) + } + + retTab = tab + } + + return tab + }), + ) + + if (errors) { + return Promise.reject({ message: errors }) + } + + if (retTab) { + return serialize(extension)([retTab, window]) + } + + return + } + }, + }, + } + } +} diff --git a/apps/extensions/lib/schemaTypes/tabs.d.ts b/apps/extensions/lib/schemaTypes/tabs.d.ts new file mode 100644 index 0000000..1d13734 --- /dev/null +++ b/apps/extensions/lib/schemaTypes/tabs.d.ts @@ -0,0 +1,45 @@ +// @not-mpl +// This file is generated from '../schemas/tabs.json'. This file inherits its license +// Please check that file's license +// +// DO NOT MODIFY MANUALLY + +declare module tabs__manifest { + type ApiGetterReturn = { + manifest: {} + } +} +declare module tabs__tabs { + type Tab = { + id?: number + index: number + active: boolean + highlighted: boolean + title?: string + url?: string + windowId: number + } + type QueryInfo = { + active?: boolean + title?: string + url?: string | string[] + windowId?: number + } + type UpdateInfo = { + active?: boolean + highlighted?: boolean + url?: string + } + type TabStatus = 'loading' | 'complete' + type ApiGetterReturn = { + tabs: { + get: (tabId: number) => Promise + goBack: (tabId?: number) => unknown + goForward: (tabId?: number) => unknown + query: (queryInfo: QueryInfo) => Promise + remove: (tabIds: number | number[]) => unknown + reload: (tabIds: number | number[]) => unknown + update: (tabId: number, updateProperties: UpdateInfo) => unknown + } + } +} diff --git a/apps/extensions/lib/schemas/tabs.json b/apps/extensions/lib/schemas/tabs.json new file mode 100644 index 0000000..40b0ef7 --- /dev/null +++ b/apps/extensions/lib/schemas/tabs.json @@ -0,0 +1,197 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +[ + { + "namespace": "manifest", + "types": [ + { + "$extend": "OptionalPermission", + "id": "ExtraPerms1", + "choices": [ + { + "type": "string", + "enum": ["tabs", "tabHide"] + } + ] + } + ] + }, + { + "namespace": "tabs", + "description": "Provides access to information about currently open tabs", + "types": [ + { + "id": "Tab", + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -1, + "optional": true + }, + "index": { + "type": "integer", + "minimum": -1 + }, + "active": { + "type": "boolean" + }, + "highlighted": { + "type": "boolean" + }, + "title": { + "type": "string", + "optional": true, + "permissions": ["tabs"] + }, + "url": { + "type": "string", + "optional": true, + "permissions": ["tabs"] + }, + "windowId": { + "type": "integer" + } + } + }, + { + "type": "object", + "id": "QueryInfo", + "properties": { + "active": { + "type": "boolean", + "optional": true + }, + "title": { + "type": "string", + "optional": true + }, + "url": { + "choices": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "optional": true + }, + "windowId": { + "type": "integer", + "optional": true + } + } + }, + { + "type": "object", + "id": "UpdateInfo", + "properties": { + "active": { + "type": "boolean", + "optional": true + }, + "highlighted": { + "type": "boolean", + "optional": true + }, + "url": { + "type": "string", + "optional": true + } + } + }, + { + "type": "string", + "id": "TabStatus", + "enum": [{ "name": "loading" }, { "name": "complete" }] + } + ], + "functions": [ + { + "name": "get", + "type": "function", + "async": true, + "parameters": [ + { + "name": "tabId", + "type": "integer" + } + ], + "returns": { + "$ref": "Tab" + } + }, + { + "name": "goBack", + "type": "function", + "async": true, + "parameters": [{ "name": "tabId", "type": "integer", "optional": true }] + }, + { + "name": "goForward", + "type": "function", + "async": true, + "parameters": [{ "name": "tabId", "type": "integer", "optional": true }] + }, + { + "name": "query", + "type": "function", + "async": true, + "parameters": [ + { + "name": "queryInfo", + "$ref": "QueryInfo" + } + ], + "returns": { + "type": "array", + "items": { + "$ref": "Tab" + } + } + }, + { + "name": "remove", + "type": "function", + "async": true, + "parameters": [ + { + "name": "tabIds", + "choices": [ + { "type": "integer" }, + { "type": "array", "items": { "type": "integer" } } + ] + } + ] + }, + { + "name": "reload", + "type": "function", + "async": true, + "parameters": [ + { + "name": "tabIds", + "choices": [ + { "type": "integer" }, + { "type": "array", "items": { "type": "integer" } } + ] + } + ] + }, + { + "name": "update", + "type": "function", + "async": true, + "parameters": [ + { + "name": "tabId", + "type": "integer" + }, + { + "name": "updateProperties", + "$ref": "UpdateInfo" + } + ] + } + ] + } +] diff --git a/apps/extensions/package.json b/apps/extensions/package.json index ef75707..b49203b 100644 --- a/apps/extensions/package.json +++ b/apps/extensions/package.json @@ -1,8 +1,18 @@ { "name": "extensions", + "type": "module", "version": "1.0.0", + "scripts": { + "build": "node ./scripts/buildTypes.js", + "dev": "watch 'pnpm build' ./lib/schemas/" + }, "dependencies": { "@browser/link": "workspace:*" }, - "license": "ISC" + "license": "ISC", + "devDependencies": { + "prettier": "^3.0.3", + "typescript": "^5.4.5", + "watch": "^1.0.2" + } } diff --git a/apps/extensions/scripts/buildTypes.js b/apps/extensions/scripts/buildTypes.js new file mode 100644 index 0000000..1f2a339 --- /dev/null +++ b/apps/extensions/scripts/buildTypes.js @@ -0,0 +1,273 @@ +/* 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 * as fs from 'node:fs' +import * as path from 'node:path' +import * as prettier from 'prettier' +import ts from 'typescript' + +const { + ListFormat, + ScriptTarget, + createSourceFile, + NewLineKind, + createPrinter, + SyntaxKind, + factory, +} = ts + +/** + * @typedef {object} Namespace + * @property {string} namespace + * @property {string} description + * @property {Type[]} [types] + * @property {FunctionType[]} [functions] + */ + +/** + * @typedef {object} ObjectType + * @property {'object'} type + * @property {Record} [properties] + */ + +/** + * @typedef {object} FunctionType + * @property {'function'} type + * @property {string} name + * @property {FunctionParamType[]} [parameters] + * @property {PropertyType} [returns] + * @property {boolean} [async] + */ + +/** + * @typedef {object} UnionType + * @property {PropertyType[]} choices + */ + +/** + * @typedef {object} ArrayType + * @property {'array'} type + * @property {PropertyType} items + */ + +/** + * @typedef {object} StringType + * @property {'string'} type + * @property {{ name: string }[]} [enum] + */ + +/** + * @typedef {object} NumberType + * @property {'number' | 'integer'} type + */ + +/** + * @typedef {object} BooleanType + * @property {'boolean'} type + */ + +/** + * @typedef {{ '$ref': string }} RefType + */ + +/** + * @typedef {(ObjectType | FunctionType | UnionType | ArrayType | StringType | NumberType | BooleanType | RefType) & { optional?: boolean }} PropertyType + * @typedef {PropertyType & { id: string }} Type + * @typedef {PropertyType & { name: string }} FunctionParamType + */ + +const schemaFolder = path.join(process.cwd(), 'lib', 'schemas') +const outFolder = path.join(process.cwd(), 'lib', 'schemaTypes') + +const printer = createPrinter({ + newLine: NewLineKind.LineFeed, + omitTrailingSemicolon: true, +}) + +const QUESTION_TOKEN = factory.createToken(SyntaxKind.QuestionToken) + +for (const file of fs.readdirSync(schemaFolder)) { + const fileName = file.replace('.json', '') + let text = fs.readFileSync(path.join(schemaFolder, file), 'utf8') + const sourceFile = createSourceFile(file, text, ScriptTarget.Latest) + + const header = `// @not-mpl +// This file is generated from '../schemas/${file}'. This file inherits its license +// Please check that file's license +// +// DO NOT MODIFY MANUALLY\n\n` + + { + const startIndex = text.indexOf('[') + text = text.slice(startIndex) + } + + const namespaces = JSON.parse(text).map( + (/** @type {Namespace} */ namespace) => { + const block = factory.createModuleBlock([ + ...generateTypes(namespace.types || []), + factory.createTypeAliasDeclaration( + undefined, + 'ApiGetterReturn', + undefined, + generateApiGetter(namespace.functions || [], namespace.namespace), + ), + ]) + + return factory.createModuleDeclaration( + [factory.createToken(SyntaxKind.DeclareKeyword)], + factory.createIdentifier(`${fileName}__${namespace.namespace}`), + block, + ) + }, + ) + + fs.writeFileSync( + path.join(outFolder, `${fileName}.d.ts`), + await prettier.format( + header + printer.printList(ListFormat.None, namespaces, sourceFile), + { parser: 'typescript', semi: false, singleQuote: true }, + ), + ) +} + +/** + * @param {Type[]} types + * @returns {import('typescript').TypeAliasDeclaration[]} + */ +function generateTypes(types) { + return types + .map((type) => { + if (type.$extend) return null + + return factory.createTypeAliasDeclaration( + undefined, + type.id, + undefined, + generateTypeNode(type), + ) + }) + .filter(Boolean) +} + +/** + * @param {FunctionType[]} functions + * @param {string} apiName + */ +function generateApiGetter(functions, apiName) { + return factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier(apiName), + undefined, + factory.createTypeLiteralNode( + functions.map((fn) => + factory.createPropertySignature( + undefined, + factory.createIdentifier(fn.name), + undefined, + generateTypeNode(fn), + ), + ), + ), + ), + ]) +} + +/** + * @param {PropertyType} type + * @returns {type is RefType} + */ +function isRef(type) { + /** @type {RefType} */ + // @ts-ignore + const asRef = type + return typeof asRef.$ref !== 'undefined' +} + +/** + * @param {PropertyType} type + * @returns {type is UnionType} + */ +function isUnion(type) { + /** @type {UnionType} */ + // @ts-ignore + const asUnion = type + return typeof asUnion.choices !== 'undefined' +} + +/** + * @param {PropertyType} type + * @returns {import('typescript').TypeNode} + */ +function generateTypeNode(type) { + if (isRef(type)) { + return factory.createTypeReferenceNode(type.$ref) + } + + if (isUnion(type)) { + return factory.createUnionTypeNode(type.choices.map(generateTypeNode)) + } + + switch (type.type) { + case 'object': + return factory.createTypeLiteralNode( + Object.entries(type.properties || {}).map(([name, type]) => + factory.createPropertySignature( + undefined, + factory.createIdentifier(name), + typeof type.optional !== 'undefined' && type.optional + ? QUESTION_TOKEN + : undefined, + generateTypeNode(type), + ), + ), + ) + + case 'function': + return factory.createFunctionTypeNode( + undefined, + (type.parameters || []).map((param) => + factory.createParameterDeclaration( + undefined, + undefined, + factory.createIdentifier(param.name), + typeof param.optional !== 'undefined' && param.optional + ? QUESTION_TOKEN + : undefined, + generateTypeNode(param), + ), + ), + type.returns + ? typeof type.async !== 'undefined' && type.async + ? factory.createTypeReferenceNode( + factory.createIdentifier('Promise'), + [generateTypeNode(type.returns)], + ) + : generateTypeNode(type.returns) + : factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword), + ) + + case 'array': + return factory.createArrayTypeNode(generateTypeNode(type.items)) + + case 'string': + if (type.enum) { + return factory.createUnionTypeNode( + type.enum.map((e) => + factory.createLiteralTypeNode(factory.createStringLiteral(e.name)), + ), + ) + } + + return factory.createKeywordTypeNode(SyntaxKind.StringKeyword) + + case 'number': + case 'integer': + return factory.createKeywordTypeNode(SyntaxKind.NumberKeyword) + + case 'boolean': + return factory.createKeywordTypeNode(SyntaxKind.BooleanKeyword) + } +} diff --git a/apps/modules/lib/BrowserWindowTracker.sys.mjs b/apps/modules/lib/BrowserWindowTracker.sys.mjs index c234b2f..17805c3 100644 --- a/apps/modules/lib/BrowserWindowTracker.sys.mjs +++ b/apps/modules/lib/BrowserWindowTracker.sys.mjs @@ -1,14 +1,16 @@ /* 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 mitt from 'resource://app/modules/mitt.sys.mjs' /** @type {import('resource://app/modules/BrowserWindowTracker.sys.mjs')['WindowTracker']} */ export const WindowTracker = { - nextWindowId: 0, + /** + * 1 indexed to stop having a falsey window id + */ + nextWindowId: 1, events: mitt(), @@ -21,13 +23,13 @@ export const WindowTracker = { * @param w The window to register */ registerWindow(w) { - w.windowApi.id = this.nextWindowId++ - this.registeredWindows.set(w.windowApi.id, w) + w.windowId = this.nextWindowId++ + this.registeredWindows.set(w.windowId, w) this.events.emit('windowCreated', w) }, removeWindow(w) { - this.registeredWindows.delete(w.windowApi.id) + this.registeredWindows.delete(w.windowId) this.events.emit('windowDestroyed', w) }, @@ -37,9 +39,9 @@ export const WindowTracker = { getWindowWithBrowser(browser) { for (const window of this.registeredWindows.values()) { - const tab = window.windowApi.tabs.tabs.find( - (t) => t.getTabId() === browser.browserId, - ) + const tab = window + .windowTabs() + .find((t) => t.view.browserId === browser.browserId) if (tab) return { window, tab } } return null diff --git a/apps/modules/lib/ExtensionTestUtils.sys.mjs b/apps/modules/lib/ExtensionTestUtils.sys.mjs index 14dc282..6a44705 100644 --- a/apps/modules/lib/ExtensionTestUtils.sys.mjs +++ b/apps/modules/lib/ExtensionTestUtils.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 { lazyESModuleGetters } from 'resource://app/modules/TypedImportUtils.sys.mjs' @@ -178,10 +177,8 @@ const objectMap = (obj, fn) => */ class ExtensionTestUtilsImpl { /** - * @template {import('resource://app/modules/zora.sys.mjs').IAssert} A - * * @param {Partial} definition - * @param {import('resource://app/modules/ExtensionTestUtils.sys.mjs').AddonMiddleware} assert + * @param {import('resource://app/modules/TestManager.sys.mjs').IDefaultAssert} assert * * @returns {import('resource://app/modules/ExtensionTestUtils.sys.mjs').ExtensionWrapper} */ @@ -193,23 +190,30 @@ class ExtensionTestUtilsImpl { definition.background && serializeScript(definition.background), }) + let testCount = 0 + /** @type {number | null} */ + let expectedTestCount = null + function handleTestResults(kind, pass, msg, ...args) { if (kind == 'test-eq') { + testCount += 1 let [expected, actual] = args assert.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`) } else if (kind == 'test-log') { console.info(msg) } else if (kind == 'test-result') { + testCount += 1 assert.ok(pass, msg) + } else if (kind == 'test-done') { + testCount += 1 + assert.truthy(pass, msg) } } extension.on('test-result', handleTestResults) extension.on('test-eq', handleTestResults) extension.on('test-log', handleTestResults) - extension.on('test-done', (...args) => - console.warn('Not Implemented', ...args), - ) + extension.on('test-done', handleTestResults) extension.on('test-message', (...args) => console.log('message', ...args)) @@ -232,25 +236,42 @@ class ExtensionTestUtilsImpl { /* Ignore */ } await extension.startup() - return await startupPromise + await startupPromise } catch (e) { assert.fail(`Errored: ${e}`) } + + return this }, async unload() { await extension.shutdown() - return await extension._uninstallPromise + await extension._uninstallPromise + + if (expectedTestCount && testCount !== expectedTestCount) { + assert.fail( + `Expected ${expectedTestCount} to execute. ${testCount} extecuted instead`, + ) + } }, + /** + * @param {number} count + */ + testCount(count) { + expectedTestCount = count + return this + }, sendMsg(msg) { extension.testMessage(msg) + return this }, async awaitMsg(msg) { + const self = this return new Promise((res) => { const callback = (_, event) => { if (event == msg) { extension.off('test-message', callback) - res(void 0) + res(self) } } diff --git a/apps/modules/lib/TestManager.sys.mjs b/apps/modules/lib/TestManager.sys.mjs index 81e4d1d..0807f93 100644 --- a/apps/modules/lib/TestManager.sys.mjs +++ b/apps/modules/lib/TestManager.sys.mjs @@ -46,11 +46,11 @@ class TestManagerSingleton { } /** - * @param {string} initialUrl + * @param {string[]} initialUrls * @param {(win: Window) => Promise} using */ - async withBrowser(initialUrl, using) { - const args = { initialUrl } + async withBrowser(initialUrls, using) { + const args = { initialUrls } /** @type {Window} */ // @ts-expect-error Incorrect type gen diff --git a/apps/tests/integrations/_index.sys.mjs b/apps/tests/integrations/_index.sys.mjs index b7f8260..c1a4e99 100644 --- a/apps/tests/integrations/_index.sys.mjs +++ b/apps/tests/integrations/_index.sys.mjs @@ -2,3 +2,4 @@ * 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/pageAction.mjs' +import './extensions/tabs.mjs' diff --git a/apps/tests/integrations/extensions/pageAction.mjs b/apps/tests/integrations/extensions/pageAction.mjs index 6c62ca5..76ecdbc 100644 --- a/apps/tests/integrations/extensions/pageAction.mjs +++ b/apps/tests/integrations/extensions/pageAction.mjs @@ -16,8 +16,8 @@ async function spinLock(predicate) { } } -await TestManager.withBrowser('http://example.com/', async (window) => { - await TestManager.test('Extension Test', async (test) => { +await TestManager.withBrowser(['http://example.com/'], async (window) => { + await TestManager.test('pageAction - Icon & Panel', async (test) => { const extension = ExtensionTestUtils.loadExtension( { manifest: { diff --git a/apps/tests/integrations/extensions/tabs.mjs b/apps/tests/integrations/extensions/tabs.mjs new file mode 100644 index 0000000..9a954be --- /dev/null +++ b/apps/tests/integrations/extensions/tabs.mjs @@ -0,0 +1,407 @@ +/* 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' + +/** + * @param {() => boolean} predicate + * @returns {Promise} + */ +async function spinLock(predicate) { + while (!predicate()) { + await new Promise((res) => setTimeout(res, 100)) + } +} + +await TestManager.withBrowser( + ['https://example.com/', 'https://google.com'], + async (window) => { + await spinLock(() => + window + ?.windowTabs() + .map( + (tab) => + tab.view.browser?.mInitialized && + tab.view.websiteState === 'complete', + ) + .reduce((p, c) => p && c, true), + ) + + await TestManager.test('tabs - Basic Query', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + permissions: ['tabs'], + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async (msg) => { + const windowId = Number(msg) + const urlResults = await b.tabs.query({ + url: 'https://example.com/', + }) + b.test.assertEq( + 1, + urlResults.length, + 'There must only be one tab matching https://example.com', + ) + b.test.assertEq( + 'https://example.com/', + urlResults[0].url, + 'The url must match the original filter', + ) + + const windowResults = await b.tabs.query({ + windowId, + }) + b.test.assertEq( + 2, + windowResults.length, + 'Window should have 2 tabs', + ) + b.test.assertEq( + ['https://example.com/', 'https://www.google.com/'].join(','), + [windowResults[0].url, windowResults[1].url].join(','), + 'Test tab urls', + ) + b.test.assertEq( + [true, false].join(','), + [windowResults[0].active, windowResults[1].active].join(','), + 'Ensure that active tab is the first one', + ) + b.test.assertEq( + ['Example Domain', 'Google'].join(','), + [windowResults[0].title, windowResults[1].title].join(','), + 'Titles should be roughly correct', + ) + + b.test.sendMessage('done') + }) + }, + }, + test, + ) + + await extension + .testCount(6) + .startup() + .then((e) => e.sendMsg(window.windowId.toString())) + .then((e) => e.awaitMsg('done')) + .then((e) => e.unload()) + }) + + await TestManager.test('tabs - Remove', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + permissions: ['tabs'], + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async (msg) => { + const windowId = Number(msg) + + const windowResults = await b.tabs.query({ + windowId, + }) + b.test.assertEq( + ['https://example.com/', 'https://www.google.com/'].join(','), + [windowResults[0].url, windowResults[1].url].join(','), + 'Window is correctly setup', + ) + + await b.tabs.remove(windowResults[1].id) + + const resultsAfterRemove = await b.tabs.query({ + windowId, + }) + b.test.assertEq(1, resultsAfterRemove.length, 'Only one tab left') + b.test.assertEq( + ['https://example.com/'].join(','), + resultsAfterRemove.map((r) => r.url).join(','), + 'Window is correctly setup', + ) + + b.test.sendMessage('done') + }) + }, + }, + test, + ) + + await extension + .testCount(3) + .startup() + .then((e) => e.sendMsg(window.windowId.toString())) + .then((e) => e.awaitMsg('done')) + .then((e) => e.unload()) + }) + }, +) + +await TestManager.withBrowser( + ['https://example.com/', 'https://google.com'], + async (window) => { + await spinLock(() => + window + ?.windowTabs() + .map( + (tab) => + tab.view.browser?.mInitialized && + tab.view.websiteState === 'complete', + ) + .reduce((p, c) => p && c, true), + ) + + await TestManager.test('tabs - Update - Active', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + permissions: ['tabs'], + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async (msg) => { + const windowId = Number(msg) + + const windowResults = await b.tabs.query({ + windowId, + }) + b.test.assertEq( + ['https://example.com/', 'https://www.google.com/'].join(','), + [windowResults[0].url, windowResults[1].url].join(','), + 'Window is correctly setup', + ) + b.test.assertTrue( + windowResults[0].active, + 'First tab should be active', + ) + + await b.tabs.update(windowResults[1].id || -1, { active: true }) + + const resultsAfter = await b.tabs.query({ + windowId, + }) + b.test.assertFalse( + resultsAfter[0].active, + 'First tab should be inactive', + ) + b.test.assertTrue( + resultsAfter[1].active, + 'Second tab should be active', + ) + b.test.sendMessage('done') + }) + }, + }, + test, + ) + + await extension + .testCount(4) + .startup() + .then((e) => e.sendMsg(window.windowId.toString())) + .then((e) => e.awaitMsg('done')) + .then((e) => e.unload()) + }) + + await TestManager.test('tabs - Update - Url', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + permissions: ['tabs'], + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async (msg) => { + const windowId = Number(msg) + + let windowResults = await b.tabs.query({ + windowId, + }) + + try { + await b.tabs.update(windowResults[0].id || -1, { + url: 'chrome://browser/content/gtests.html', + }) + b.test.notifyFail('Failed to reject chrome:// url') + } catch (e) { + b.test.notifyPass('Failed to change url to chrome://') + } + + try { + await b.tabs.update(windowResults[0].id || -1, { + url: 'file://some/local/path.txt', + }) + b.test.notifyFail('Failed to reject file:// url') + } catch (e) { + b.test.notifyPass('Failed to change url to file://') + } + + try { + await b.tabs.update(windowResults[0].id || -1, { + url: 'about:addons', + }) + b.test.notifyFail('Failed to reject priviged about: url') + } catch (e) { + b.test.notifyPass('Failed to change url to priviged about:') + } + + try { + await b.tabs.update(windowResults[0].id || -1, { + url: 'javascript:console.log("Hello world!")', + }) + b.test.notifyFail('Failed to reject javascript: url') + } catch (e) { + b.test.notifyPass('Failed to change url to javascript:') + } + + try { + await b.tabs.update(windowResults[0].id || -1, { + url: 'data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh', + }) + b.test.notifyFail('Failed to reject data: url') + } catch (e) { + b.test.notifyPass('Failed to change url to data:') + } + + await b.tabs.update(windowResults[0].id || -1, { + url: 'about:blank', + }) + await new Promise((res) => setTimeout(res, 200)) + + windowResults = await b.tabs.query({ + windowId, + }) + + b.test.assertEq( + 'about:blank', + windowResults[0].url, + 'URL update works', + ) + + b.test.sendMessage('done') + }) + }, + }, + test, + ) + + await extension + .testCount(6) + .startup() + .then((e) => e.sendMsg(window.windowId.toString())) + .then((e) => e.awaitMsg('done')) + .then((e) => e.unload()) + }) + + await TestManager.test('tabs - Get', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + permissions: ['tabs'], + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async (msg) => { + const windowId = Number(msg) + + let windowResults = await b.tabs.query({ + windowId, + }) + const tab = await b.tabs.get(windowResults[0].id || -1) + + b.test.assertEq( + JSON.stringify(windowResults[0]), + JSON.stringify(tab), + 'Fetch result should be the same as the query result', + ) + + b.test.sendMessage('done') + }) + }, + }, + test, + ) + + await extension + .testCount(1) + .startup() + .then((e) => e.sendMsg(window.windowId.toString())) + .then((e) => e.awaitMsg('done')) + .then((e) => e.unload()) + }) + + await TestManager.test('tabs - Navigation', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { + permissions: ['tabs'], + }, + async background() { + /** @type {import('resource://app/modules/ExtensionTestUtils.sys.mjs').TestBrowser} */ + const b = this.browser + + b.test.onMessage.addListener(async (msg) => { + const windowId = Number(msg) + + let windowResults = await b.tabs.query({ + windowId, + }) + + // History stack already setup on the tab at index 0, so we can + // use that here instead. Its probibly going to cause flaky tests + // in the future, but it stops spawning a new window + + b.test.assertEq( + 'about:blank', + windowResults[0].url, + 'Previous tests should leave as about:blank', + ) + + await b.tabs.goBack(windowResults[0].id) + + b.test.assertEq( + 'https://example.com/', + await b.tabs.get(windowResults[0].id || -1).then((t) => t.url), + 'New url should be last page', + ) + + await b.tabs.goForward(windowResults[0].id) + + b.test.assertEq( + 'about:blank', + await b.tabs.get(windowResults[0].id || -1).then((t) => t.url), + 'New url should be next page', + ) + + b.test.sendMessage('done') + }) + }, + }, + test, + ) + + await extension + .testCount(3) + .startup() + .then((e) => e.sendMsg(window.windowId.toString())) + .then((e) => e.awaitMsg('done')) + .then((e) => e.unload()) + }) + }, +) diff --git a/apps/tests/package.json b/apps/tests/package.json index 9110712..4c57fae 100644 --- a/apps/tests/package.json +++ b/apps/tests/package.json @@ -12,5 +12,8 @@ }, "keywords": [], "author": "", - "license": "ISC" + "license": "ISC", + "devDependencies": { + "@types/firefox-webext-browser": "^120.0.0" + } } diff --git a/libs/link/types/globals/WindowApi.d.ts b/libs/link/types/globals/WindowApi.d.ts index 5772acb..f38c873 100644 --- a/libs/link/types/globals/WindowApi.d.ts +++ b/libs/link/types/globals/WindowApi.d.ts @@ -3,5 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ declare interface Window { + windowId: number eventBus: import('@browser/event-bus').EventBus + windowTabs: import('@amadeus-it-group/tansu').WritableSignal< + import('@browser/tabs').WindowTabs + > + activeTab: import('@amadeus-it-group/tansu').ReadableSignal< + import('@browser/tabs').WindowTab | undefined + > + activeTabId: import('@amadeus-it-group/tansu').WritableSignal + selectedTabIds: import('@amadeus-it-group/tansu').WritableSignal } diff --git a/libs/link/types/modules/ExtensionTestUtils.d.ts b/libs/link/types/modules/ExtensionTestUtils.d.ts index a4e7cdc..2630dce 100644 --- a/libs/link/types/modules/ExtensionTestUtils.d.ts +++ b/libs/link/types/modules/ExtensionTestUtils.d.ts @@ -8,6 +8,38 @@ declare module 'resource://app/modules/ExtensionTestUtils.sys.mjs' { import type { Extension } from 'resource://gre/modules/Extension.sys.mjs' export type WebExtensionManifest = browser._manifest.WebExtensionManifest + export type TestBrowser = typeof browser & { + test: { + withHandlingUserInput: (callback: () => unknown) => unknown + notifyFail: (message: string) => unknown + notifyPass: (message: string) => unknown + log: (message: string) => unknown + sendMessage: (arg1?, arg2?) => unknown + fail: (message) => unknown + succeed: (message) => unknown + assertTrue: (test, message: string) => unknown + assertFalse: (test, message: string) => unknown + assertBool: ( + test: string | boolean, + expected: boolean, + message: string, + ) => unknown + assertDeepEq: (expected, actual, message: string) => unknown + assertEq: (expected, actual, message: string) => unknown + assertNoLastError: () => unknown + assertLastError: (expectedError: string) => unknown + assertRejects: ( + promise: Promise, + expectedError: ExpectedError, + message: string, + ) => unknown + assertThrows: ( + func: () => unknown, + expectedError: ExpectedError, + message: string, + ) => unknown + } + } /* eslint @typescript-eslint/ban-types: 0 */ export type ExtSerializableScript = string | Function | Array @@ -19,11 +51,15 @@ declare module 'resource://app/modules/ExtensionTestUtils.sys.mjs' { export type ExtensionWrapper = { extension: Extension - startup(): Promise<[string, string]> - unload(): Promise + startup(): Promise + unload(): Promise - sendMsg(msg: string): void - awaitMsg(msg: string): Promise + /** + * Specifies the number of tests that that this extension should execute + */ + testCount(count: number): ExtensionWrapper + sendMsg(msg: string): ExtensionWrapper + awaitMsg(msg: string): Promise } /** diff --git a/libs/link/types/modules/TestManager.d.ts b/libs/link/types/modules/TestManager.d.ts index 947691f..909afda 100644 --- a/libs/link/types/modules/TestManager.d.ts +++ b/libs/link/types/modules/TestManager.d.ts @@ -23,7 +23,7 @@ declare module 'resource://app/modules/TestManager.sys.mjs' { assertFn: (assert: IDefaultAssert) => Promise | void, ): Promise withBrowser( - defaultUrl: string, + defaultUrls: string[], using: (win: Window) => Promise, ): Promise diff --git a/libs/link/types/windowApi/WebsiteView.d.ts b/libs/link/types/windowApi/WebsiteView.d.ts index 1eb2c6c..6490203 100644 --- a/libs/link/types/windowApi/WebsiteView.d.ts +++ b/libs/link/types/windowApi/WebsiteView.d.ts @@ -20,6 +20,7 @@ declare type OklchTheme = { } declare type WebsiteViewEvents = { + goTo: string loadingChange: boolean progressPercent: number changeIcon: string @@ -29,6 +30,8 @@ declare type WebsiteViewEvents = { securityChange: number } +declare type WebsiteState = 'loading' | 'complete' + declare type WebsiteView = { windowBrowserId: number theme?: OklchTheme @@ -36,7 +39,9 @@ declare type WebsiteView = { title?: string browser: XULBrowserElement + uri: nsIURIType browserId?: number + websiteState: WebsiteState events: import('mitt').Emitter diff --git a/libs/link/types/windowApi/WindowTabs.d.ts b/libs/link/types/windowApi/WindowTabs.d.ts index ba08371..7f941ac 100644 --- a/libs/link/types/windowApi/WindowTabs.d.ts +++ b/libs/link/types/windowApi/WindowTabs.d.ts @@ -5,6 +5,6 @@ /// declare module '@browser/tabs' { - export type WindowTab = { kind: 'tab'; view: WebsiteView } - export type WindowTabs = WindowTabs[] + export type WindowTab = { kind: 'website'; view: WebsiteView } + export type WindowTabs = WindowTab[] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a07c39..b9d21c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,16 @@ importers: '@browser/link': specifier: workspace:* version: link:../../libs/link + devDependencies: + prettier: + specifier: ^3.0.3 + version: 3.0.3 + typescript: + specifier: ^5.4.5 + version: 5.4.5 + watch: + specifier: ^1.0.2 + version: 1.0.2 apps/misc: {} @@ -187,6 +197,10 @@ importers: '@browser/link': specifier: workspace:* version: link:../../libs/link + devDependencies: + '@types/firefox-webext-browser': + specifier: ^120.0.0 + version: 120.0.0 libs/link: dependencies: @@ -694,7 +708,6 @@ packages: /@types/firefox-webext-browser@120.0.0: resolution: {integrity: sha512-L+tDlwNeq0kQGfAYc2sNfKhRWJz9CNRvlbq9HnLibKUiJ3VTThG8sj7xrJF4CtKpEA9eBAr91Z2nnKIAy+xUJg==} - dev: false /@types/html-minifier-terser@6.1.0: resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} @@ -1831,6 +1844,12 @@ packages: engines: {node: '>=0.8.x'} dev: true + /exec-sh@0.2.2: + resolution: {integrity: sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==} + dependencies: + merge: 1.2.1 + dev: true + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2667,6 +2686,10 @@ packages: engines: {node: '>= 8'} dev: true + /merge@1.2.1: + resolution: {integrity: sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==} + dev: true + /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -4051,6 +4074,12 @@ packages: 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==} dev: true @@ -4104,6 +4133,15 @@ packages: engines: {node: '>= 0.8'} dev: true + /watch@1.0.2: + resolution: {integrity: sha512-1u+Z5n9Jc1E2c7qDO8SinPoZuHj7FgbgU1olSFoyaklduDvvtX7GMMtlE6OC9FTXq4KvNAOfj6Zu4vI1e9bAKA==} + engines: {node: '>=0.1.95'} + hasBin: true + dependencies: + exec-sh: 0.2.2 + minimist: 1.2.8 + dev: true + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'}