Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<api>.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).
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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.
Expand Down
103 changes: 103 additions & 0 deletions docs/utils.md
Original file line number Diff line number Diff line change
@@ -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)

---

<a name="checkLastError"></a>

### 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
});
```

---

<a name="callWithPromise"></a>

### callWithPromise

```ts
callWithPromise<T>(executor: (callback: (result: T) => void) => any): Promise<T>
```

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<chrome.runtime.PlatformInfo> {
return callWithPromise(cb =>
chrome.runtime.getPlatformInfo(info => cb(info))
);
}
```

---

<a name="safeListener"></a>

### safeListener

```ts
safeListener<T extends (...args: any[]) => 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
})
);
```

---

<a name="handleListener"></a>

### handleListener

```ts
handleListener<T extends (...args: any[]) => void>(target: chrome.events.Event<T>, 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();
```
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
10 changes: 5 additions & 5 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {browser} from "./browser";

type Event<T extends (...args: any) => void> = chrome.events.Event<T>;

export const throwRuntimeError = (): void => {
export const checkLastError = (): void => {
const error = browser().runtime.lastError;

if (error) {
Expand All @@ -22,7 +22,7 @@ export function callWithPromise<T>(executor: (callback: (result: T) => void) =>
isResolved = true;

try {
throwRuntimeError();
checkLastError();

resolve(result);
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading