feat: cross browser extension api wrapper

This commit is contained in:
Michal Szczepanski 2023-06-14 07:22:55 +02:00
parent 750f1f3bdf
commit cf2d6e1efd
16 changed files with 2316 additions and 1335 deletions

3
.env
View File

@ -1,2 +1 @@
IS_PRODUCTION=true
FOO=bar
IS_PRODUCTION=true

View File

@ -1,2 +1 @@
IS_PRODUCTION=false
FOO=bar
IS_PRODUCTION=false

View File

@ -14,6 +14,11 @@
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"max-len": ["error", {"code": 120, "ignoreComments": true, "ignoreTemplateLiterals": true}],
"no-console": "error",
"semi": [ "error", "always" ],

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
node_modules
dist
.parcel-cache
.parcel-cache
*.iml

4
.npmignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
.parcel-cache
*.iml

6
@types/process.d.ts vendored
View File

@ -1,6 +0,0 @@
declare const process: {
env: {
ENV: string;
URL: string;
}
}

View File

@ -1,17 +1,8 @@
# Typescript parceljs html page template
* parceljs
* typescript
* linter - eslint with @typescript-eslint
* prettier
* pre-commit hook for lint
* reading .env files
* simple HMR using `window.location.reload()`
# browser-api
* chrome / firefox extension api wrapper
### Development
```npm run dev```
starts server hosted on port
```localhost:1234```
### Production
``npm run prod``

3347
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,19 @@
{
"name": "typescript-parcel-base",
"name": "@pinmenote/browser-api",
"version": "0.0.1",
"author": "Michal Szczepanski",
"license": "MIT",
"description": "cross browser compatible api layer",
"bugs": {
"url": "https://github.com/pinmenote/browser-api/issues"
},
"homepage": "https://github.com/pinmenote/browser-api#readme",
"source": "src/index.ts",
"module": "dist/module.js",
"types": "dist/types.d.ts",
"scripts": {
"dev": "parcel src/index.html",
"prod": "parcel build src/index.html",
"build": "parcel build",
"dev": "NODE_ENV=development parcel build",
"lint": "eslint --ext .ts src/",
"lint:fix": "eslint --ext .ts,.tsx src/ --fix"
},
@ -11,14 +21,18 @@
"lint"
],
"devDependencies": {
"@parcel/transformer-typescript-tsc": "^2.8.3",
"@types/node": "^18.11.18",
"@parcel/packager-ts": "^2.9.2",
"@parcel/transformer-typescript-tsc": "^2.9.2",
"@parcel/transformer-typescript-types": "^2.9.2",
"@types/chrome": "^0.0.237",
"@types/firefox-webext-browser": "^111.0.1",
"@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"parcel": "^2.8.3",
"parcel": "^2.9.2",
"pre-commit": "^1.2.2",
"typescript": "^4.9.4"
}

157
src/browser.api.ts Normal file
View File

@ -0,0 +1,157 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Config } from './config';
import { fnConsoleLog } from './fn.console.log';
export type BrowserGlobalSender = browser.runtime.MessageSender | chrome.runtime.MessageSender;
export type BrowserGlobal = typeof chrome | typeof browser;
export type BrowserRuntime = typeof chrome.runtime | typeof browser.runtime;
export type BrowserTabs = typeof chrome.tabs | typeof browser.tabs;
export type BrowserTab = chrome.tabs.Tab | browser.tabs.Tab;
export type BrowserTabObject = chrome.tabs.Tab | browser.tabs.Tab;
export type BrowserLocalStore = typeof chrome.storage.local | typeof browser.storage.local;
export type BrowserDownloads = typeof chrome.downloads | typeof browser.downloads;
export type BrowserAction = typeof chrome.action | typeof browser.browserAction;
export type BrowserTabChangeInfo = chrome.tabs.TabChangeInfo | browser.tabs._OnUpdatedChangeInfo;
export interface BusMessage<T> {
type: string;
data?: T;
}
export class BrowserApi {
private static browserApi: BrowserGlobal;
private static isChromeValue = false;
static init() {
if (this.browserApi) return;
try {
this.browserApi = browser;
} catch (e) {
this.browserApi = chrome;
this.isChromeValue = true;
}
}
static get isChrome(): boolean {
return this.isChromeValue;
}
static get browser(): BrowserGlobal {
return this.browserApi;
}
static get runtime(): BrowserRuntime {
return this.browserApi.runtime;
}
static get tabs(): BrowserTabs {
return this.browserApi.tabs;
}
static activeTab = async (): Promise<BrowserTab> => {
const tabs = await this.browserApi.tabs.query({ active: true, currentWindow: true });
return tabs[0];
};
static setActiveTabUrl = async (url: string): Promise<void> => {
const tab = await this.activeTab();
await this.browserApi.tabs.update(tab.id, { url });
};
static get localStore(): BrowserLocalStore {
return this.browserApi.storage.local;
}
static get downloads(): BrowserDownloads {
return this.browserApi.downloads;
}
static get browserAction(): BrowserAction {
if (this.isChromeValue) return this.browserApi.action;
return this.browserApi.browserAction;
}
static get startUrl(): string {
return this.isChromeValue ? 'chrome-extension' : 'moz-extension';
}
static get disabledUrl(): string {
return this.isChromeValue ? 'chrome://' || 'chrome-extension://' : 'moz://' || 'moz-extension://';
}
static get runtimeUrl(): string {
if (BrowserApi.isChrome) {
return `chrome-extension://${chrome.runtime.id}`;
}
return 'moz-extension://';
}
static openOptionsPage(subpage = ''): void {
if (this.isChromeValue) {
const optionsPage = chrome.runtime.getManifest().options_ui?.page;
if (optionsPage) window.open(`chrome-extension://${chrome.runtime.id}/${optionsPage}${subpage}`);
return;
}
window.open(browser.runtime.getManifest().options_ui?.page);
window.close();
}
static shadowRoot(el: Element): ShadowRoot | null {
try {
if (this.isChromeValue) {
return chrome.dom.openOrClosedShadowRoot(el);
}
return el.openOrClosedShadowRoot();
} catch (e) {
fnConsoleLog('BrowserApiWrapper->shadowRoot->ERROR', el, e);
}
return null;
}
static sendTabMessage = <T>(msg: BusMessage<T>): Promise<void> => {
return new Promise((resolve: (...arg: any) => void, reject: (...arg: any) => void) => {
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-floating-promises */
this.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => {
const currentTab: BrowserTabObject | undefined = tabs[0];
if (currentTab?.id) {
try {
this.tabs.sendMessage(currentTab.id, msg, resolve);
} catch (e) {
fnConsoleLog('Error sendTabMessage', msg, e, 'lastError', BrowserApi.runtime.lastError);
reject(e);
}
}
});
});
};
static sendRuntimeMessage = async <T>(msg: BusMessage<T>): Promise<void> => {
return new Promise((resolve, reject) => {
try {
this.runtime.sendMessage(msg, (ack: any) => {
if (Config.showAckMessage) fnConsoleLog(`${msg.type}->ack`);
resolve(ack);
});
} catch (e) {
fnConsoleLog('runtime.lastError', msg, e, 'lastError', BrowserApi.runtime.lastError);
reject(e);
}
});
};
}

47
src/browser.storage.ts Normal file
View File

@ -0,0 +1,47 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { BrowserApi } from './browser.api';
export class BrowserStorage {
static async get<T>(key: string): Promise<T> {
const value = await BrowserApi.localStore.get(key);
return value[key];
}
static async getAll(): Promise<any> {
return await BrowserApi.localStore.get();
}
static async getBytesInUse(key?: string): Promise<number> {
return await BrowserApi.localStore.getBytesInUse(key);
}
static async set<T>(key: string, value: T): Promise<void> {
const v: { [key: string]: any } = {};
v[key] = value;
await BrowserApi.localStore.set(v);
}
static async remove(key: string): Promise<void> {
await BrowserApi.localStore.remove(key);
}
static async clear(): Promise<void> {
await BrowserApi.localStore.clear();
}
}

View File

@ -1,4 +1,4 @@
export const Config = {
isProduction: process.env.IS_PRODUCTION === 'true',
foo: process.env.FOO
showAckMessage: false
};

8
src/fn.console.log.ts Normal file
View File

@ -0,0 +1,8 @@
import { Config } from './config';
export const fnConsoleLog = (...args: any[]) => {
if (!Config.isProduction) {
//eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
console.log(...args);
}
};

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<script type="module" src="index.ts"></script>
</body>
</html>

View File

@ -1,12 +1,2 @@
import { Config } from './config';
const h1 = document.createElement('h1');
document.body.appendChild(h1);
h1.innerText = `is production ${Config.isProduction.toString()}, foo value from .env: ${Config.foo || ''}`;
// Hot reloading
if (!Config.isProduction) {
const ws = new WebSocket('ws://localhost:1234');
ws.onmessage = () => {
window.location.reload();
};
}
export * from './browser.api';
export * from './browser.storage';

View File

@ -2,7 +2,7 @@
"include": ["src/**/*"],
"compilerOptions": {
"target": "es2021",
"strict": true,
"types": ["node", "firefox-webext-browser", "chrome"],
"typeRoots": ["node_modules/@types", "@types"]
}
}