From 574dfb762b2e46b241b183e03abdc1b0155f61ae Mon Sep 17 00:00:00 2001 From: Anjey Tsibylskij <130153594+atldays@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:53:27 +0300 Subject: [PATCH] feat(utils): export utilities module and rename helper to checkLastError - Add @addon-core/browser/utils export in package.json and tsup.config.ts. - Rename throwRuntimeError to checkLastError for better clarity and API consistency. - Create docs/utils.md with detailed descriptions and usage examples. - Refactor tests and internal references to match the new naming. --- CONTRIBUTING.md | 2 +- README.md | 29 ++++++++++--- docs/utils.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +++ src/utils.test.ts | 10 ++--- src/utils.ts | 4 +- tsup.config.ts | 2 +- 7 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 docs/utils.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a19c04a..89718a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ The goal is to cover as much of the WebExtensions/Chrome API surface as possible How to add a new API wrapper: 1) Implementation - Create `src/.ts`. -- Wrap callback‑style APIs into `Promise` and call `throwRuntimeError()` inside callbacks. +- Wrap callback‑style APIs into `Promise` and call `checkLastError()` inside callbacks. - Events must return an unsubscribe function `() => void` (see `handleListener`/`safeListener`). - Use precise types from `@types/chrome` (avoid `Parameters<>` in the final documentation — show real argument types). - Keep function names concise and consistent (see existing modules). diff --git a/README.md b/README.md index 32d0838..e591bbc 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,28 @@ A TypeScript promise-based wrapper for Chrome Extension APIs, supporting both Manifest V2 and V3 across Chrome, Opera, Edge, and other Chromium-based browsers. -[![npm version](https://img.shields.io/npm/v/%40addon-core%2Fbrowser.svg?logo=npm)](https://www.npmjs.com/package/@addon-core/browser) -[![npm downloads](https://img.shields.io/npm/dm/%40addon-core%2Fbrowser.svg)](https://www.npmjs.com/package/@addon-core/browser) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md) +[![npm version](https://img.shields.io/npm/v/%40addon-core%2Fbrowser.svg?logo=npm&style=for-the-badge)](https://www.npmjs.com/package/@addon-core/browser) +[![npm downloads](https://img.shields.io/npm/dm/%40addon-core%2Fbrowser.svg?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@addon-core/browser) +[![CI](https://img.shields.io/github/actions/workflow/status/addon-stack/browser/ci.yml?style=for-the-badge)](https://github.com/addon-stack/browser/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE.md) ## Installation +### npm + ```bash -# with your preferred package manager npm i @addon-core/browser -# or +``` + +### yarn + +```bash yarn add @addon-core/browser -# or +``` + +### pnpm + +```bash pnpm add @addon-core/browser ``` @@ -103,6 +113,13 @@ const off = onTabUpdated((tabId, changeInfo, tab) => { off(); ``` +## Utilities + +In addition to Chrome API wrappers, this package provides a set of low-level utilities for error handling, promise management, and listener safety. While these are primarily used internally, they are also exported via the `@addon-core/browser/utils` subpath for advanced usage. + +For a complete list of utility functions and examples, see the [Utilities Documentation](docs/utils.md). + + ## Not yet covered These commonly used WebExtensions/Chrome Extension APIs are not wrapped here yet (Chrome OS–only APIs are intentionally omitted). If you’d like to contribute, please see [CONTRIBUTING.md](CONTRIBUTING.md) and open an issue/PR. diff --git a/docs/utils.md b/docs/utils.md new file mode 100644 index 0000000..f13279d --- /dev/null +++ b/docs/utils.md @@ -0,0 +1,103 @@ +# utils + +Low-level helpers used across the package to handle common Chrome API patterns, error handling, and listeners. + +## Methods + +- [checkLastError()](#checkLastError) +- [callWithPromise(executor)](#callWithPromise) +- [safeListener(listener)](#safeListener) +- [handleListener(target, callback)](#handleListener) + +--- + + + +### checkLastError + +```ts +checkLastError(): void +``` + +Throws an `Error` if `chrome.runtime.lastError` is set. This is primarily used inside legacy callback-style code to ensure that any runtime errors are captured and propagated as standard exceptions. + +```ts +import { checkLastError } from "@addon-core/browser/utils"; + +chrome.runtime.getPlatformInfo(() => { + checkLastError(); // throws if lastError is present + // continue safely +}); +``` + +--- + + + +### callWithPromise + +```ts +callWithPromise(executor: (callback: (result: T) => void) => any): Promise +``` + +Wraps a callback-style Chrome API into a `Promise`. This is the core utility used throughout this package to provide a modern async/await interface. It automatically calls `checkLastError()` within the callback. + +```ts +import { callWithPromise } from "@addon-core/browser/utils"; + +export function getPlatformInfo(): Promise { + return callWithPromise(cb => + chrome.runtime.getPlatformInfo(info => cb(info)) + ); +} +``` + +--- + + + +### safeListener + +```ts +safeListener any>(listener: T): T +``` + +Wraps any listener function so that synchronous errors are caught and logged to the console. It also catches and logs rejected promises from async listeners. This ensures that one failing listener doesn't break the extension's execution flow. + +```ts +import { safeListener } from "@addon-core/browser/utils"; + +chrome.runtime.onMessage.addListener( + safeListener((msg, sender, sendResponse) => { + // your code that might throw or return a rejected promise + }) +); +``` + +--- + + + +### handleListener + +```ts +handleListener void>(target: chrome.events.Event, callback: T): () => void +``` + +Subscribes to a `chrome.events.Event` (like `chrome.tabs.onUpdated`) using `safeListener` and returns an unsubscribe function (`() => void`). This makes it easier to manage listener lifecycles. + +```ts +import { handleListener } from "@addon-core/browser/utils"; + +const off = handleListener( + chrome.tabs.onUpdated, + (tabId, changeInfo, tab) => { + if (changeInfo.status === "complete") { + console.log("Tab finished loading:", tabId); + } + } +); + +// Later, to remove the listener +off(); +``` diff --git a/package.json b/package.json index 6e2a241..706eaf8 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" + }, + "./utils": { + "types": "./dist/utils.d.ts", + "import": "./dist/utils.js", + "require": "./dist/utils.cjs" } }, "files": [ diff --git a/src/utils.test.ts b/src/utils.test.ts index d404d5a..905cc64 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,5 +1,5 @@ import {afterEach, beforeEach, describe, expect, jest, test} from "@jest/globals"; -import {callWithPromise, handleListener, safeListener, throwRuntimeError} from "./utils"; +import {callWithPromise, checkLastError, handleListener, safeListener} from "./utils"; describe("utils", () => { let originalChrome: any; @@ -23,20 +23,20 @@ describe("utils", () => { jest.resetAllMocks(); }); - describe("throwRuntimeError", () => { + describe("checkLastError", () => { test("should not throw if lastError is undefined", () => { globalThis.chrome = {runtime: {lastError: undefined}} as any; - expect(() => throwRuntimeError()).not.toThrow(); + expect(() => checkLastError()).not.toThrow(); }); test("should throw Error if lastError exists", () => { const errorMessage = "Some error"; globalThis.chrome = {runtime: {lastError: {message: errorMessage}}} as any; - expect(() => throwRuntimeError()).toThrow(errorMessage); + expect(() => checkLastError()).toThrow(errorMessage); }); test("should throw Error if WebExtension API is not available", () => { - expect(() => throwRuntimeError()).toThrow("WebExtension API not available in this context"); + expect(() => checkLastError()).toThrow("WebExtension API not available in this context"); }); }); diff --git a/src/utils.ts b/src/utils.ts index b54f751..2b97864 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import {browser} from "./browser"; type Event void> = chrome.events.Event; -export const throwRuntimeError = (): void => { +export const checkLastError = (): void => { const error = browser().runtime.lastError; if (error) { @@ -22,7 +22,7 @@ export function callWithPromise(executor: (callback: (result: T) => void) => isResolved = true; try { - throwRuntimeError(); + checkLastError(); resolve(result); } catch (e) { diff --git a/tsup.config.ts b/tsup.config.ts index 26942b1..c8f38ed 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import {defineConfig, type Options} from "tsup"; const common: Options = { - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/utils.ts"], bundle: true, outDir: "dist", sourcemap: true,