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
46 changes: 40 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,42 @@ on:
default: false

jobs:
# One real-browser check, not an OS x Node matrix. Release also waits for this reusable CI job.
browser-match-patterns:
name: Browser match-pattern smoke
# Keep Chromium's sandbox enabled; Ubuntu 23.10+ restricts user namespaces for downloaded binaries.
runs-on: ubuntu-22.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Install Chrome for Testing
id: chrome
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
with:
chrome-version: stable
install-dependencies: true

- name: Run browser match-pattern smoke
env:
CHROME_FOR_TESTING_PATH: ${{ steps.chrome.outputs.chrome-path }}
run: npm run test:browser-match-patterns -- "$CHROME_FOR_TESTING_PATH"

compute-matrix:
name: Compute matrix
runs-on: ubuntu-latest
Expand All @@ -28,19 +64,17 @@ jobs:
- id: set
run: |
if [[ "${{ inputs.full }}" == "true" ]]; then
echo 'matrix={"os":["ubuntu-latest","windows-latest"],"node":[18,20,22]}' >> $GITHUB_OUTPUT
echo 'name_suffix=(full matrix)' >> $GITHUB_OUTPUT
echo 'matrix={"os":["ubuntu-latest","windows-latest"],"node":[18,20,22]}' >> "$GITHUB_OUTPUT"
echo 'name_suffix=(full matrix)' >> "$GITHUB_OUTPUT"
else
echo 'matrix={"os":["ubuntu-latest"],"node":[20]}' >> $GITHUB_OUTPUT
echo 'name_suffix=' >> $GITHUB_OUTPUT
echo 'matrix={"os":["ubuntu-latest"],"node":[20]}' >> "$GITHUB_OUTPUT"
echo 'name_suffix=' >> "$GITHUB_OUTPUT"
fi

build-and-test:
name: Build, Lint, Test ${{ needs.compute-matrix.outputs.name_suffix }}
needs: compute-matrix
runs-on: ${{ matrix.os }}
env:
NODE_OPTIONS: --experimental-vm-modules
permissions:
contents: read
strategy:
Expand Down
6 changes: 3 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env sh

# Husky pre-commit hook: run tests and check linting/formatting
npm run test:related
npm run lint
# Husky pre-commit hook: fix staged files, then run tests
npm run lint:staged || exit 1
npm run test:related || exit 1
3 changes: 2 additions & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env sh

# Husky pre-push hook: typecheck, run full tests, and build
# Husky pre-push hook: check linting/formatting, typecheck, run full tests, and build

npm run lint || exit 1
npm run typecheck || exit 1
npm run test || exit 1
npm run build || exit 1
2 changes: 0 additions & 2 deletions .mailmap

This file was deleted.

13 changes: 9 additions & 4 deletions .release-it.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ function getContributors() {

if (existing) {
existing.count += count;

if (!existing.login && gh.login) {
existing.login = gh.login;
existing.url = gh.url;
}

if (!existing.name && displayName) {
existing.name = displayName;
}

if (!existing.email && displayEmail) {
existing.email = displayEmail;
}
Expand Down Expand Up @@ -118,19 +121,19 @@ module.exports = () => {
requireUpstream: false,
requireBranch: false,
commit: true,
// biome-ignore lint/suspicious/noTemplateCurlyInString: release-it placeholder
// release-it placeholder
commitMessage: "chore(release): v${version}",
tag: true,
// biome-ignore lint/suspicious/noTemplateCurlyInString: release-it placeholder
// release-it placeholder
tagName: "v${version}",
// biome-ignore lint/suspicious/noTemplateCurlyInString: release-it placeholder
// release-it placeholder
tagAnnotation: "v${version}",
push: true,
},

github: {
release: true,
// biome-ignore lint/suspicious/noTemplateCurlyInString: release-it placeholder
// release-it placeholder
releaseName: "v${version}",
autoGenerate: false,
// Ensure GitHub receives exactly the generated changelog body
Expand Down Expand Up @@ -192,7 +195,9 @@ module.exports = () => {
}

if (isMajor) return {level: 0};

if (isMinor) return {level: 1};

if (isPatch) return {level: 2};

return null;
Expand Down
96 changes: 89 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ npm ci
3) Useful scripts
- `npm run dev` — build in watch mode (tsup)
- `npm run build` — production build (tsup)
- `npm run lint` / `npm run lint:fix` — check/fix with Biome
- `npm run format` — format with Biome
- `npm run lint` — check code, formatting, and filenames with ESLint; does not edit files
- `npm run fix` — apply available ESLint fixes and report remaining violations
- `npm run lint:staged` — fix/check staged files and automatically stage successful fixes (also run by pre-commit)
- `npm run typecheck` — type-check with tsc
- `npm test` / `npm run test:ci` — tests (Jest)
- `npm run test:browser-match-patterns -- /absolute/path/to/browser` — real-browser match-pattern smoke (build first)

Minimum Node.js version: current LTS (at release time).

Expand Down Expand Up @@ -94,7 +96,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`.
- Create `src/<api-in-kebab-case>.ts`.
- 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).
Expand All @@ -105,7 +107,7 @@ How to add a new API wrapper:
- Re-export from `src/index.ts`.

3) Documentation
- Create `docs/<api>.md` following the template: “Documentation → Methods/Events (links to sections) → sections with real TypeScript signatures”.
- Create `docs/<api-in-kebab-case>.md` following the template: “Documentation → Methods/Events (links to sections) → sections with real TypeScript signatures”.
- Update the list in `README.md` (link to the new file and add a brief description where it helps).

4) Tests
Expand All @@ -117,9 +119,55 @@ See the list of not-yet-covered APIs in the "Not yet covered" section of `README

## Code quality: lint, format, types

- Formatting/linting: [Biome](https://biomejs.dev/) — `npm run format`, `npm run lint`.
- Formatting/linting: [ESLint](https://eslint.org/) with TypeScript support and
[ESLint Stylistic](https://eslint.style/). The single configuration is `eslint.config.js`.
- `npm run fix` applies available fixes; `npm run lint` only checks and fails on errors or warnings.
- Type checking: `npm run typecheck`.
- Husky + lint-staged run pre-commit checks (Biome and `jest --findRelatedTests`).
- Husky pre-commit runs `npm run lint:staged`, then `npm run test:related`.
`lint-staged` applies ESLint fixes to staged files and stages those fixes automatically. It temporarily hides
unstaged edits in partially staged files, then restores them without adding them to the commit.
Non-fixable lint errors (including filename errors) stop the commit; lint-staged restores the pre-lint state
on task failure. If tests fail after lint-staged succeeds, the formatting fixes remain staged for review.
- Pre-commit checks formatting only for staged files, so unrelated unstaged formatting does not block a commit.
Tests still run against the working tree. Use `npm run lint` for a full-project check.
- Husky pre-push runs lint, typecheck, full tests, and build without modifying source files.

Formatting rules:

- Four-space indentation, double quotes (except when escaping would be needed), semicolons, LF line endings,
no spaces inside object/import braces, and optional parentheses around a single untyped arrow parameter.
- Trailing commas in multiline arrays, objects, imports, exports, enums, tuples, and type parameters, but not function arguments.
- One blank line before `return` and before/after `if`, `for`, `while`, `do`, and `switch` statements.
No extra padding at block boundaries or between `if` and `else`. Consecutive single-line variable declarations stay together.
- One blank line before and after any statement or declaration spanning two or more lines, including variable
declarations, calls, assignments, functions, classes, and TypeScript types (`project/padding-around-multiline`).
Only neighboring statements are separated: no padding at file/block boundaries, between arguments, or between
object/type/class members. Import and re-export groups retain their existing sorting/grouping rules.
- At most one consecutive blank line; no trailing whitespace. Imports are sorted and separated from following code.
- Recommended JavaScript/TypeScript correctness checks. Explicit `any` is allowed; unused parameters, catch bindings,
and variables prefixed with `_` are allowed. Other unused bindings are reported, not silently deleted.
- JSON/JSONC: two-space indentation and expanded nonempty objects/arrays. JSON remains strict; JSONC permits comments.
- The former 120-column width is a readability guideline, not a failing `max-len` rule: ESLint does not automatically
wrap arbitrary long expressions like a dedicated formatter.

Filename rules (`project/file-naming`):

- A module defining and exporting a regular class must use the exact class name in PascalCase: `BrowserClient.ts`.
A module defining multiple exported classes must split them into separate matching files. Re-export barrels may
keep names such as `index.ts` or `sidebar.ts`.
- Exception classes extending `Error` (including native error subclasses and local inheritance chains) stay in their
owning module and do not determine its filename. For example, `SidebarError` stays in `sidebar.ts`.
- Other files use kebab-case, including documentation: `browser-detection.ts`, `browser-detection.md`.
- Tests use the subject's casing: `BrowserClient.test.ts` or `browser-detection.test.ts`.
Dot-separated suffixes such as `.integration.test`, `.spec`, `.config`, and `.d` stay lowercase.
- Standard project metadata names (`README.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
`LICENSE`, `LICENSE.md`, and `AGENTS.md`) are exempt. Names such as `package.json` and `tsconfig.json` already comply.
- The local naming rule also checks non-code filenames; it does not format Markdown/YAML or rename files.
Renames require updating imports and links. Generated output, dependencies, coverage, the lockfile, and local
environment/editor files are excluded.

The configuration regression tests in `tests/tooling/` run with the regular Jest suite. Hook tests use temporary
Git clones to verify staging, partial staging, and rollback without modifying the current checkout's Git state.

PRs with lint/type/build errors won’t be accepted.

Expand All @@ -133,7 +181,38 @@ Framework: **Jest** (`npm test`). Recommendations:
- For events, verify that the returned function actually removes the listener.
- Structure: co-locate tests with the module or use a `__tests__` folder.

In CI use `npm run test:ci`.
In CI use `npm run test:ci`. All test scripts (`npm test`, `npm run test:ci`, and `npm run test:related`) share the same Jest ESM launcher, including Node's `--experimental-vm-modules` flag. No manual `NODE_OPTIONS` setup is needed locally or in CI.

Import Jest helpers explicitly in test files, for example `import {describe, expect, jest, test} from "@jest/globals"`. In ESM, the `jest` object is not a global. These imports belong only in test suites; the published `@addon-core/browser/testing` runtime remains runner-independent.

### Browser match-pattern smoke

Before releasing changes to the URL matcher or host-permission fake, run the real-browser smoke in addition to unit
and clean-consumer tests. Obtain the full **Chrome for Testing** executable from the
[official downloads](https://googlechromelabs.github.io/chrome-for-testing/) or use a Chromium build with extension
support. No ChromeDriver, Playwright, or other automation package is needed. Do not use `chrome-headless-shell`.

```sh
npm run build
npm run test:browser-match-patterns -- "/absolute/path/to/chrome-for-testing"
```

On macOS, pass the executable inside the app bundle, for example
`/path/to/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`, not the `.app` directory.
The script verifies `--version` before starting. Regular Google Chrome is intentionally rejected:
[Chrome 137+ removed `--load-extension` from branded builds](https://groups.google.com/a/chromium.org/g/chromium-extensions/c/1-g8EFx2BBY/m/S0ET5wPjCAAJ).
A remaining timeout includes the selected binary/version, missing extension results, a setup hint, and bounded stderr.

The smoke compares the built harness with real MV3 extension APIs, using a temporary browser profile and loopback
HTTP server. It never uses your personal profile. It is separate from `npm test` so local unit tests need no browser.
If the browser is unavailable locally, report the smoke as **not run**, not as passed.

`.github/workflows/ci.yml` runs this command in one dedicated Ubuntu 22.04/Node 22 job using stable Chrome for Testing
provided by `browser-actions/setup-chrome` (action revision pinned). The installed version is printed in the log.
This runner keeps Chromium's sandbox enabled without working around the
[AppArmor restrictions on downloaded binaries in Ubuntu 23.10+](https://pptr.dev/troubleshooting#issues-with-apparmor-on-ubuntu).
The release workflow calls the same CI workflow and cannot publish if this job fails. This focused Chrome check is
not Firefox/Safari validation or complete browser parity. See [scope and examples](docs/testing/match-patterns.md).

---

Expand All @@ -149,6 +228,9 @@ In CI use `npm run test:ci`.

Releases are performed by maintainers.

The reusable CI workflow includes the [browser match-pattern smoke](#browser-match-pattern-smoke). Keep it green
alongside unit, type, build, and consumer checks before publishing; no separate manual browser-test waiver is implied.

Flow (aligned with GitFlow):
1) Merge features into `develop` via PRs.
2) Create a `release/x.y.z` branch from `develop`. Preview autogenerated CHANGELOG:
Expand Down
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ pnpm add @addon-core/browser
- [action](docs/action.md) — MV2/MV3 compatible; under the hood uses `chrome.action` (MV3) or `chrome.browserAction` (MV2) automatically.
- [alarms](docs/alarms.md)
- [audio](docs/audio.md)
- [browsingData](docs/browsingData.md)
- [browsingData](docs/browsing-data.md)
- [commands](docs/commands.md)
- [contextMenus](docs/contextMenus.md)
- [contextMenus](docs/context-menus.md)
- [cookies](docs/cookies.md)
- [documentScan](docs/documentScan.md)
- [documentScan](docs/document-scan.md)
- [downloads](docs/downloads.md)
- [extension](docs/extension.md)
- [history](docs/history.md)
Expand All @@ -60,11 +60,11 @@ pnpm add @addon-core/browser
- [scripting](docs/scripting.md)
- [sidebar](docs/sidebar.md) — Unified helpers for Chrome Side Panel (MV3) and Firefox/Opera `sidebarAction`.
- [storage](https://github.com/addon-stack/storage) — via separate package: [@addon-core/storage](https://www.npmjs.com/package/@addon-core/storage)
- [tabCapture](docs/tabCapture.md)
- [tabCapture](docs/tab-capture.md)
- [tabs](docs/tabs.md)
- [userScripts](docs/userScripts.md)
- [webNavigation](docs/webNavigation.md)
- [webRequest](docs/webRequest.md)
- [userScripts](docs/user-scripts.md)
- [webNavigation](docs/web-navigation.md)
- [webRequest](docs/web-request.md)
- [windows](docs/windows.md)

## Why this package
Expand Down Expand Up @@ -131,14 +131,21 @@ const off = onContextMenusClicked(async (info, tab) => {

## Helpers

- [browserDetection](docs/browserDetection.md) — Best-effort browser detection with `BrowserName`, `BrowserFamily`, `guessBrowser()`, `isBrowser()`, and `isBrowserFamily()`.
- [browserDetection](docs/browser-detection.md) — Best-effort browser detection with `BrowserName`, `BrowserFamily`, `guessBrowser()`, `isBrowser()`, and `isBrowserFamily()`.

## 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).

## Testing

The framework-agnostic [`@addon-core/browser/testing`](docs/testing.md) subpath provides deterministic fixtures,
configurable browser methods and events, stateful runtime/permissions/tabs/windows/scripting fakes, and reversible
browser-global installation. Importing it never changes `globalThis`, and it has no dependency on Jest or another test
runner.

## 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
Loading
Loading