├── packages
├── messaging-demo
│ ├── src
│ │ ├── public
│ │ │ └── .keep
│ │ ├── utils
│ │ │ ├── google-messaging.ts
│ │ │ ├── duckduckgo-messaging.ts
│ │ │ └── messaging.ts
│ │ └── entrypoints
│ │ │ ├── background.ts
│ │ │ ├── google-injected.ts
│ │ │ ├── duckduckgo-injected.ts
│ │ │ ├── google.content.ts
│ │ │ ├── duckduckgo.content.ts
│ │ │ └── popup
│ │ │ ├── index.html
│ │ │ └── main.ts
│ ├── tsconfig.json
│ ├── package.json
│ ├── wxt.config.ts
│ └── README.md
├── proxy-service-demo
│ ├── src
│ │ ├── public
│ │ │ └── .keep
│ │ ├── entrypoints
│ │ │ ├── background.ts
│ │ │ └── popup
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ └── utils
│ │ │ └── math-service.ts
│ ├── tsconfig.json
│ ├── wxt.config.ts
│ ├── package.json
│ └── README.md
├── isolated-element-demo
│ ├── isolated-style.css
│ ├── global-style.css
│ ├── index.html
│ ├── .gitignore
│ ├── package.json
│ └── main.js
├── messaging
│ ├── src
│ │ ├── index.ts
│ │ ├── page.ts
│ │ ├── __mocks__
│ │ │ └── webextension-polyfill.ts
│ │ ├── utils.ts
│ │ ├── extension.ts
│ │ ├── types.ts
│ │ ├── extension.test-d.ts
│ │ └── window.ts
│ ├── tsconfig.json
│ ├── vitest.config.node.ts
│ ├── vitest.config.browser.ts
│ ├── README.md
│ └── package.json
├── storage
│ ├── tsconfig.json
│ ├── src
│ │ ├── __mocks__
│ │ │ └── webextension-polyfill.ts
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── defineExtensionStorage.ts
│ ├── README.md
│ └── package.json
├── tsconfig
│ ├── README.md
│ ├── package.json
│ ├── ts-library.json
│ └── base.json
├── job-scheduler
│ ├── src
│ │ └── __mocks__
│ │ │ └── webextension-polyfill.ts
│ ├── tsconfig.json
│ ├── README.md
│ └── package.json
├── proxy-service
│ ├── src
│ │ ├── __mocks__
│ │ │ └── webextension-polyfill.ts
│ │ ├── index.ts
│ │ ├── isBackground.ts
│ │ ├── types.ts
│ │ ├── flattenPromise.test.ts
│ │ ├── flattenPromise.ts
│ │ ├── defineProxyService.test.ts
│ │ ├── defineProxyService.ts
│ │ └── types.test-d.ts
│ ├── tsconfig.json
│ ├── README.md
│ └── package.json
├── fake-browser
│ ├── tsconfig.json
│ ├── scripts
│ │ ├── code-writer.ts
│ │ └── generate-base.ts
│ ├── README.md
│ ├── src
│ │ ├── apis
│ │ │ ├── webNavigation.ts
│ │ │ ├── alarms.ts
│ │ │ ├── tabs.test.ts
│ │ │ ├── webNavigation.test.ts
│ │ │ ├── notifications.ts
│ │ │ ├── runtime.ts
│ │ │ ├── windows.test.ts
│ │ │ ├── alarms.test.ts
│ │ │ ├── notifications.test.ts
│ │ │ ├── windows.ts
│ │ │ ├── runtime.test.ts
│ │ │ └── storage.ts
│ │ ├── index.ts
│ │ ├── utils
│ │ │ └── defineEventWithTrigger.ts
│ │ └── types.ts
│ └── package.json
├── match-patterns
│ ├── tsconfig.json
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.test.ts
└── isolated-element
│ ├── tsconfig.json
│ ├── README.md
│ ├── src
│ ├── options.ts
│ ├── index.ts
│ └── index.test.ts
│ └── package.json
├── vercel.json
├── docs
├── public
│ ├── favicon.ico
│ └── logo-with-shadow.png
├── tsconfig.json
├── nuxt.config.ts
├── .gitignore
├── package.json
├── app.config.ts
├── content
│ ├── fake-browser
│ │ ├── api.md
│ │ ├── 0.installation.md
│ │ ├── 3.reseting-state.md
│ │ ├── 2.triggering-events.md
│ │ ├── 1.testing-frameworks.md
│ │ └── 4.implemented-apis.md
│ ├── match-patterns
│ │ ├── api.md
│ │ └── 0.installation.md
│ ├── 0.get-started
│ │ ├── 1.browser-support.md
│ │ └── 0.introduction.md
│ ├── messaging
│ │ └── 1.protocol-maps.md
│ ├── storage
│ │ ├── 0.installation.md
│ │ ├── 1.typescript.md
│ │ └── api.md
│ ├── isolated-element
│ │ ├── 0.installation.md
│ │ └── api.md
│ ├── index.md
│ ├── proxy-service
│ │ ├── api.md
│ │ └── 1.defining-services.md
│ └── job-scheduler
│ │ ├── api.md
│ │ └── 0.installation.md
├── plugins
│ └── redirects.ts
└── _redirects.txt
├── .gitattributes
├── patches
├── simple-git-hooks@2.13.1.md
└── simple-git-hooks@2.13.1.patch
├── .prettierrc.yml
├── .prettierignore
├── .gitignore
├── .github
├── actions
│ └── setup
│ │ └── action.yml
└── workflows
│ ├── validate.yml
│ └── publish-packages.yml
├── LICENSE
├── package.json
└── README.md
/packages/messaging-demo/src/public/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/src/public/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "silent": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/isolated-element-demo/isolated-style.css:
--------------------------------------------------------------------------------
1 | a {
2 | font-size: large;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/messaging-demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.wxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/messaging/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './extension';
3 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.wxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/messaging/src/page.ts:
--------------------------------------------------------------------------------
1 | export * from './window';
2 | export * from './custom-event';
3 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aklinker1/webext-core/HEAD/docs/public/favicon.ico
--------------------------------------------------------------------------------
/packages/storage/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "exclude": ["lib"]
4 | }
5 |
--------------------------------------------------------------------------------
/docs/public/logo-with-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aklinker1/webext-core/HEAD/docs/public/logo-with-shadow.png
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/storage/src/__mocks__/webextension-polyfill.ts:
--------------------------------------------------------------------------------
1 | export { fakeBrowser as default } from '@webext-core/fake-browser';
2 |
--------------------------------------------------------------------------------
/packages/tsconfig/README.md:
--------------------------------------------------------------------------------
1 | # tsconfig
2 |
3 | Base TypeScript configuration for different types of packages in the repo.
4 |
--------------------------------------------------------------------------------
/docs/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | llms: {
3 | domain: 'webext-core.aklinker1.io',
4 | },
5 | });
6 |
--------------------------------------------------------------------------------
/packages/job-scheduler/src/__mocks__/webextension-polyfill.ts:
--------------------------------------------------------------------------------
1 | export { fakeBrowser as default } from '@webext-core/fake-browser';
2 |
--------------------------------------------------------------------------------
/packages/messaging/src/__mocks__/webextension-polyfill.ts:
--------------------------------------------------------------------------------
1 | export { fakeBrowser as default } from '@webext-core/fake-browser';
2 |
--------------------------------------------------------------------------------
/packages/proxy-service/src/__mocks__/webextension-polyfill.ts:
--------------------------------------------------------------------------------
1 | export { fakeBrowser as default } from '@webext-core/fake-browser';
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Generated code
2 | *.gen.ts linguist-generated
3 | *.gen.js linguist-generated
4 | pnpm-lock.yaml linguist-generated
5 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/src/entrypoints/background.ts:
--------------------------------------------------------------------------------
1 | export default defineBackground(() => {
2 | registerMathService();
3 | });
4 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/wxt.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'wxt';
2 |
3 | export default defineConfig({
4 | srcDir: 'src',
5 | });
6 |
--------------------------------------------------------------------------------
/packages/isolated-element-demo/global-style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | a {
8 | color: red;
9 | }
10 |
--------------------------------------------------------------------------------
/patches/simple-git-hooks@2.13.1.md:
--------------------------------------------------------------------------------
1 | See:
2 |
3 | - https://github.com/oven-sh/bun/issues/21754
4 | - https://github.com/toplenboren/simple-git-hooks/pull/136
5 |
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | singleQuote: true
2 | trailingComma: all
3 | endOfLine: lf
4 | printWidth: 100
5 | tabWidth: 2
6 | vueIndentScriptAndStyle: false
7 | arrowParens: avoid
8 |
--------------------------------------------------------------------------------
/packages/messaging/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"]
5 | },
6 | "exclude": ["lib"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/fake-browser/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "compilerOptions": {
4 | "lib": ["DOM", "ESNext"]
5 | },
6 | "exclude": ["lib"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/job-scheduler/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "compilerOptions": {
4 | "lib": ["DOM", "ESNext"]
5 | },
6 | "exclude": ["lib"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/match-patterns/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"]
5 | },
6 | "exclude": ["lib"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/proxy-service/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "compilerOptions": {
4 | "lib": ["DOM", "ESNext"]
5 | },
6 | "exclude": ["lib"]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/isolated-element/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/ts-library.json",
3 | "compilerOptions": {
4 | "lib": ["DOM", "ESNext"]
5 | },
6 | "exclude": ["lib"]
7 | }
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | lib/
4 | pnpm-lock.yaml
5 | /docs/.vitepress/cache
6 | /docs/api/*
7 | .nuxt
8 | .wxt.output
9 | docs/content/index.md
10 | docs/content/*/api.md
11 |
--------------------------------------------------------------------------------
/packages/proxy-service/src/index.ts:
--------------------------------------------------------------------------------
1 | export { defineProxyService } from './defineProxyService';
2 | export { flattenPromise } from './flattenPromise';
3 | export type { ProxyServiceConfig, ProxyService, DeepAsync } from './types';
4 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "check": "echo 'noop'",
8 | "build": "echo 'noop'"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | lib
4 | *.log
5 | coverage
6 | .env
7 | .env.*
8 | .turbo
9 | .web-extrc.yml
10 | /docs/.vitepress/cache
11 | .DS_Store
12 | tsconfig.vitest-temp.json
13 | /.cache
14 | .output
15 | .wxt
16 | .idea
17 |
--------------------------------------------------------------------------------
/packages/tsconfig/ts-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "TypeScript Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "lib": ["ESNext"],
7 | "module": "ESNext",
8 | "target": "es6"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/job-scheduler/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/job-scheduler`
2 |
3 | Simple job scheduler for web extension background scripts.
4 |
5 | ```bash
6 | pnpm i @webext-core/job-scheduler
7 | ```
8 |
9 | See [documentation](https://webext-core.aklinker1.io/guide/job-scheduler/) to get started!
10 |
--------------------------------------------------------------------------------
/packages/fake-browser/scripts/code-writer.ts:
--------------------------------------------------------------------------------
1 | import { CodeBlockWriter } from 'ts-morph';
2 |
3 | export function writeJsdoc(w: CodeBlockWriter, comment: string): CodeBlockWriter {
4 | const lines = ['/**', ...comment.split('\n').map(line => ` * ${line}`), ' */'];
5 | w.writeLine(lines.join('\n'));
6 | return w;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/fake-browser/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/fake-browser`
2 |
3 | An in-memory implementation of `webextension-polyfill` for testing.
4 |
5 | ```bash
6 | pnpm i -D @webext-core/fake-browser
7 | ```
8 |
9 | ## Get Started
10 |
11 | See [documentation](https://webext-core.aklinker1.io/guide/fake-browser/) to get started!
12 |
--------------------------------------------------------------------------------
/packages/proxy-service/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/proxy-service`
2 |
3 | A type-safe wrapper around the web extension messaging APIs that lets you call a function from anywhere, but execute it in the background. Supports all browsers (Chrome, Firefox, Safari, etc).
4 |
5 | ## Get Started
6 |
7 | See [documentation](https://webext-core.aklinker1.io/guide/proxy-service/) to get started!
8 |
--------------------------------------------------------------------------------
/packages/isolated-element-demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/isolated-element-demo/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: Basic Setup
2 | description: Install Bun and other dependencies
3 | runs:
4 | using: composite
5 | steps:
6 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
7 | with:
8 | bun-version-file: package.json
9 | no-cache: true
10 | - name: Install Dependencies
11 | shell: bash
12 | run: bun install --frozen-lockfile
13 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/utils/google-messaging.ts:
--------------------------------------------------------------------------------
1 | import { defineWindowMessaging } from '@webext-core/messaging/page';
2 |
3 | export interface GoogleMessagingProtocol {
4 | ping(): string;
5 | ping2(): string;
6 | fromInjected(): string;
7 | fromInjected2(): string;
8 | }
9 |
10 | export const googleMessaging = defineWindowMessaging({
11 | namespace: '@webext-core/messaging-demo/google',
12 | });
13 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .data
4 | .nuxt
5 | .nitro
6 | .cache
7 | dist
8 |
9 | # Node dependencies
10 | node_modules
11 |
12 | # Logs
13 | logs
14 | *.log
15 |
16 | # Misc
17 | .DS_Store
18 | .fleet
19 | .idea
20 | .eslintcache
21 |
22 | # Local env files
23 | .env
24 | .env.*
25 | !.env.example
26 |
27 | # npm pack
28 | *.tgz
29 |
30 | # Temp files
31 | .tmp
32 | .profile
33 | *.0x
34 |
35 | #VSC
36 | .history
37 | .wrangler
38 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/utils/duckduckgo-messaging.ts:
--------------------------------------------------------------------------------
1 | import { defineCustomEventMessaging } from '@webext-core/messaging/page';
2 |
3 | export interface DuckduckgoMessagingProtocol {
4 | ping(): string;
5 | ping2(): string;
6 | fromInjected(): string;
7 | fromInjected2(): string;
8 | }
9 |
10 | export const duckduckgoMessaging = defineCustomEventMessaging({
11 | namespace: '@webext-core/messaging-demo/duckduckgo',
12 | });
13 |
--------------------------------------------------------------------------------
/packages/messaging-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "messaging-demo",
3 | "type": "module",
4 | "private": true,
5 | "scripts": {
6 | "dev": "buildc --deps-only -- wxt",
7 | "build": "buildc -- wxt build",
8 | "check": "buildc --deps-only -- tsc --noEmit"
9 | },
10 | "dependencies": {
11 | "@webext-core/messaging": "workspace:*"
12 | },
13 | "devDependencies": {
14 | "wxt": "^0.19.5"
15 | },
16 | "buildc": {
17 | "cachable": false
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/isolated-element-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isolated-element-demo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "buildc --deps-only -- vite",
8 | "build": "buildc -- vite build",
9 | "check": "echo 'noop'"
10 | },
11 | "dependencies": {
12 | "@webext-core/isolated-element": "workspace:*"
13 | },
14 | "devDependencies": {
15 | "vite": "^4.0.0"
16 | },
17 | "buildc": {
18 | "cachable": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/storage/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/storage`
2 |
3 | A type-safe, localStorage-esk wrapper around the web extension storage APIs. Supports all browsers (Chrome, Firefox, Safari, etc).
4 |
5 | ```bash
6 | pnpm i @webext-core/storage
7 | ```
8 |
9 | ```ts
10 | import { localExtStorage } from '@webext-core/storage';
11 |
12 | const value = await localExtStorage.getItem('some-key');
13 | ```
14 |
15 | ## Get Started
16 |
17 | See [documentation](https://webext-core.aklinker1.io/guide/storage/) to get started!
18 |
--------------------------------------------------------------------------------
/packages/messaging/vitest.config.node.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, defaultInclude, defaultExclude, UserConfig } from 'vitest/config';
2 |
3 | const config = {
4 | node: {
5 | test: {
6 | name: 'node',
7 | include: [...defaultInclude],
8 | exclude: [
9 | ...defaultExclude,
10 | '**/*.browser.{test,spec}.ts',
11 | '**/__tests__/browser/**/*.{test,spec}.ts',
12 | ],
13 | },
14 | } as const satisfies UserConfig,
15 | };
16 |
17 | export default defineConfig(config.node);
18 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "proxy-service-demo",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "dev": "buildc --deps-only -- wxt",
8 | "build": "buildc -- wxt build",
9 | "check": "buildc --deps-only -- tsc --noEmit"
10 | },
11 | "dependencies": {
12 | "@webext-core/proxy-service": "workspace:*"
13 | },
14 | "devDependencies": {
15 | "wxt": "^0.19.5"
16 | },
17 | "buildc": {
18 | "cachable": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/messaging-demo/wxt.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'wxt';
2 |
3 | export default defineConfig({
4 | srcDir: 'src',
5 | manifest: {
6 | web_accessible_resources: [
7 | {
8 | resources: ['google-injected.js'],
9 | matches: ['*://*.google.com/*'],
10 | },
11 | {
12 | resources: ['duckduckgo-injected.js'],
13 | matches: ['*://*.duckduckgo.com/*'],
14 | },
15 | ],
16 | },
17 | runner: { startUrls: ['https://google.com/', 'https://duckduckgo.com/'] },
18 | });
19 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/entrypoints/background.ts:
--------------------------------------------------------------------------------
1 | export default defineBackground(() => {
2 | const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
3 |
4 | onMessage1('sleep', async ({ data }) => new Promise(res => setTimeout(res, data)));
5 | onMessage1('ping', async () => {
6 | await sleep(1000);
7 | return 'pong' as const;
8 | });
9 | onMessage2('ping2', async ({ data }) => {
10 | await sleep(1000);
11 | return data;
12 | });
13 | onMessage2('throw', () => {
14 | throw Error('Example error');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "skipLibCheck": true,
14 | "strict": true,
15 | "noEmit": true,
16 | "allowSyntheticDefaultImports": true
17 | },
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/messaging/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description In firefox, when dispatching events externally from web-extension, it's necessary to clone all properties of the dictionary. ref: https://github.com/aklinker1/webext-core/pull/70#discussion_r1775031410
3 | */
4 | export function prepareCustomEventDict(
5 | data: T,
6 | options: { targetScope?: object } = { targetScope: window ?? undefined },
7 | ): T {
8 | // @ts-expect-error not exist cloneInto types because implemented only in Firefox.
9 | return typeof cloneInto !== 'undefined' ? cloneInto(data, options.targetScope) : data;
10 | }
11 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "dev": "bun gen && nuxt dev --extends docus",
7 | "build": "bun gen && nuxt build --extends docus",
8 | "gen": "bun generate-api-references.ts",
9 | "postinstall": "nuxt prepare"
10 | },
11 | "dependencies": {
12 | "better-sqlite3": "^12.2.0",
13 | "docus": "latest",
14 | "nuxt": "^4.1.2"
15 | },
16 | "devDependencies": {
17 | "listr2": "^9.0.5",
18 | "prettier": "^3.6.2",
19 | "ts-morph": "^23.0.0"
20 | },
21 | "buildc": {
22 | "cachable": false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/messaging-demo/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/messaging` Demo Extension
2 |
3 | To startup the demo extension:
4 |
5 | ```bash
6 | cd webext-core
7 | pnpm i
8 | pnpm build
9 | cd packages/messaging-demo
10 | pnpm dev
11 | ```
12 |
13 | This will build the extension in watch mode, and open a browser.
14 |
15 | > If a browser fails to open, create a file called `.web-extrc.yml` with the following contents:
16 | >
17 | > ```yml
18 | > chromiumBinary: /path/to/your/chrome
19 | > ```
20 | >
21 | > See [`web-ext` docs](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary) for more details.
22 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/messaging` Demo Extension
2 |
3 | To startup the demo extension:
4 |
5 | ```bash
6 | cd webext-core
7 | pnpm i
8 | pnpm build
9 | cd packages/messaging-demo
10 | pnpm dev
11 | ```
12 |
13 | This will build the extension in watch mode, and open a browser.
14 |
15 | > If a browser fails to open, create a file called `.web-extrc.yml` with the following contents:
16 | >
17 | > ```yml
18 | > chromiumBinary: /path/to/your/chrome
19 | > ```
20 | >
21 | > See [`web-ext` docs](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary) for more details.
22 |
--------------------------------------------------------------------------------
/packages/messaging/vitest.config.browser.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, defaultExclude, UserConfig } from 'vitest/config';
2 |
3 | const config = {
4 | browser: {
5 | test: {
6 | name: 'browser mode',
7 | include: ['**/*.browser.{test,spec}.ts', '**/__tests__/browser/**/*.{test,spec}.ts'],
8 | exclude: [...defaultExclude],
9 | browser: {
10 | provider: 'playwright',
11 | enabled: true,
12 | name: 'chromium',
13 | headless: true,
14 | isolate: true,
15 | screenshotFailures: false,
16 | },
17 | },
18 | } as const satisfies UserConfig,
19 | };
20 |
21 | export default defineConfig(config.browser);
22 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/utils/messaging.ts:
--------------------------------------------------------------------------------
1 | import { defineExtensionMessaging } from '@webext-core/messaging';
2 |
3 | interface MessageProtocol1 {
4 | ping: () => 'pong';
5 | sleep: number;
6 | }
7 |
8 | export const { sendMessage: sendMessage1, onMessage: onMessage1 } =
9 | defineExtensionMessaging({ logger: console });
10 |
11 | // Define another protocol to make sure the library supports multiple
12 |
13 | interface MessageProtocol2 {
14 | ping2: (arg: string) => string;
15 | throw: undefined;
16 | }
17 |
18 | export const { sendMessage: sendMessage2, onMessage: onMessage2 } =
19 | defineExtensionMessaging({ logger: console });
20 |
--------------------------------------------------------------------------------
/docs/app.config.ts:
--------------------------------------------------------------------------------
1 | export default defineAppConfig({
2 | seo: {
3 | title: 'Web Ext Core',
4 | description:
5 | 'Web extension development made easy. A collection of easy-to-use utilities for writing and testing web extensions that work on all browsers.',
6 | },
7 | github: {
8 | url: 'https://github.com/aklinker1/webext-core',
9 | rootDir: 'docs',
10 | },
11 | socials: {
12 | github: 'https://github.com/aklinker1/webext-core',
13 | },
14 | header: {
15 | title: 'WebExt Core',
16 | logo: { light: '/logo-with-shadow.png', dark: '/logo-with-shadow.png' },
17 | },
18 | ui: {
19 | colors: {
20 | primary: 'cyan',
21 | },
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/packages/fake-browser/src/apis/webNavigation.ts:
--------------------------------------------------------------------------------
1 | import { BrowserOverrides } from '../types';
2 | import { defineEventWithTrigger } from '../utils/defineEventWithTrigger';
3 |
4 | export const webNavigation: BrowserOverrides['webNavigation'] = {
5 | onBeforeNavigate: defineEventWithTrigger(),
6 | onCommitted: defineEventWithTrigger(),
7 | onCompleted: defineEventWithTrigger(),
8 | onCreatedNavigationTarget: defineEventWithTrigger(),
9 | onDOMContentLoaded: defineEventWithTrigger(),
10 | onErrorOccurred: defineEventWithTrigger(),
11 | onHistoryStateUpdated: defineEventWithTrigger(),
12 | onReferenceFragmentUpdated: defineEventWithTrigger(),
13 | onTabReplaced: defineEventWithTrigger(),
14 | };
15 |
--------------------------------------------------------------------------------
/patches/simple-git-hooks@2.13.1.patch:
--------------------------------------------------------------------------------
1 | diff --git a/simple-git-hooks.js b/simple-git-hooks.js
2 | index 093890d21800c9feb34cb6da5b997b947da812cd..3b11e62bbfcbdf7d2dc8f553237750a220ecae6f 100644
3 | --- a/simple-git-hooks.js
4 | +++ b/simple-git-hooks.js
5 | @@ -100,6 +100,10 @@ function getProjectRootDirectoryFromNodeModules(projectPath) {
6 | if (indexOfDenoDir > -1) {
7 | return projDir.slice(0, indexOfDenoDir - 1).join('/');
8 | }
9 | + const indexOfBunDir = projDir.indexOf('.bun')
10 | + if (indexOfBunDir > -1) {
11 | + return projDir.slice(0, indexOfBunDir - 1).join('/');
12 | + }
13 |
14 | const indexOfStoreDir = projDir.indexOf('.store')
15 | if (indexOfStoreDir > -1) {
16 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/entrypoints/google-injected.ts:
--------------------------------------------------------------------------------
1 | export default defineUnlistedScript(async () => {
2 | console.log('[google-injected.ts] Injected script loaded');
3 |
4 | googleMessaging.onMessage('ping', event => {
5 | console.log('[google-injected.ts] Received', event);
6 | return 'pong';
7 | });
8 |
9 | googleMessaging.onMessage('ping2', event => {
10 | console.log('[google-injected.ts] Received2', event);
11 | return 'pong2';
12 | });
13 |
14 | googleMessaging.sendMessage('fromInjected', undefined).then(res => {
15 | console.log('[google-injected.ts] Response:', res);
16 | });
17 |
18 | const res2 = await googleMessaging.sendMessage('fromInjected2', undefined);
19 | console.log('[google-injected.ts] Response2:', res2);
20 | });
21 |
--------------------------------------------------------------------------------
/docs/content/fake-browser/api.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ---
4 | description: ""
5 | ---
6 |
7 | ::alert
8 |
9 | See [`@webext-core/fake-browser`](/fake-browser/installation/)
10 |
11 | ::
12 |
13 | ## `FakeBrowser`
14 |
15 | ```ts
16 | type FakeBrowser = BrowserOverrides & Browser;
17 | ```
18 |
19 | The standard `Browser` interface from `webextension-polyfill`, but with additional functions for triggering events and reseting state.
20 |
21 | ## `fakeBrowser`
22 |
23 | ```ts
24 | const fakeBrowser: FakeBrowser;
25 | ```
26 |
27 | An in-memory implementation of the `browser` global.
28 |
29 |
30 |
31 | ---
32 |
33 | _API reference generated by [`docs/generate-api-references.ts`](https://github.com/aklinker1/webext-core/blob/main/docs/generate-api-references.ts)_
--------------------------------------------------------------------------------
/packages/messaging-demo/src/entrypoints/duckduckgo-injected.ts:
--------------------------------------------------------------------------------
1 | export default defineUnlistedScript(async () => {
2 | console.log('[duckduckgo-injected.ts] Injected script loaded');
3 |
4 | duckduckgoMessaging.onMessage('ping', event => {
5 | console.log('[duckduckgo-injected.ts] Received', event);
6 | return 'pong';
7 | });
8 |
9 | duckduckgoMessaging.onMessage('ping2', event => {
10 | console.log('[duckduckgo-injected.ts] Received2', event);
11 | return 'pong2';
12 | });
13 |
14 | duckduckgoMessaging.sendMessage('fromInjected', undefined).then(res => {
15 | console.log('[duckduckgo-injected.ts] Response:', res);
16 | });
17 |
18 | const res2 = await duckduckgoMessaging.sendMessage('fromInjected2', undefined);
19 | console.log('[duckduckgo-injected.ts] Response2:', res2);
20 | });
21 |
--------------------------------------------------------------------------------
/packages/isolated-element-demo/main.js:
--------------------------------------------------------------------------------
1 | import { createIsolatedElement } from '@webext-core/isolated-element';
2 | import isolatedStyles from './isolated-style.css?raw';
3 |
4 | const createLink = () => {
5 | const a = document.createElement('a');
6 | a.href = 'https://github.com/aklinker1/webext-core';
7 | a.textContent = 'Link';
8 | return a;
9 | };
10 |
11 | // Create a link that uses the global red color style
12 | document.body.appendChild(createLink());
13 |
14 | // Create an isolated link that doesn't use the red color style
15 | createIsolatedElement({
16 | name: 'isolated-style',
17 | css: { textContent: isolatedStyles },
18 | }).then(({ isolatedElement, parentElement }) => {
19 | // Add link inside isolation
20 | isolatedElement.appendChild(createLink());
21 | // Add isolated element to dom
22 | document.body.appendChild(parentElement);
23 | });
24 |
--------------------------------------------------------------------------------
/docs/content/fake-browser/0.installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | toc: true
3 | description: ''
4 | ---
5 |
6 | :badge[Vitest]{type="success"} :badge[Jest]{type="success"} :badge[Bun]{type="success"} :badge[Mocha]{type="success"}
7 |
8 | ## Overview
9 |
10 | An in-memory implementation of [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill) for testing. Supports all test frameworks (Vitest, Jest, etc).
11 |
12 | ```bash
13 | pnpm i -D @webext-core/fake-browser
14 | ```
15 |
16 | ::alrt{type=warning}
17 | This package only really works with projects using node, so only the NPM install steps are shown.
18 | ::
19 |
20 | See [Testing Frameworks](/fake-browser/testing-frameworks) to setup mocks for your testing framework of choice.
21 |
22 | ## Examples
23 |
24 | See [Implemented APIs](/fake-browser/implemented-apis) for example tests and details on how to use each API.
25 |
--------------------------------------------------------------------------------
/docs/plugins/redirects.ts:
--------------------------------------------------------------------------------
1 | import redirects from '../_redirects.txt?raw';
2 |
3 | const parsedRedirects = redirects
4 | .split('\n')
5 | .map(line => line.trim())
6 | .filter(line => !!line && !line.startsWith('#'))
7 | .map(line => {
8 | const [from, to] = line.trim().split(/\s+/);
9 | return {
10 | from,
11 | to,
12 | };
13 | })
14 | .filter(redirect => redirect.from && redirect.to);
15 |
16 | export default defineNuxtPlugin(() => {
17 | // Client side redirects only since it is hosted as a SSG app
18 | if (process.server) return;
19 |
20 | const router = useRouter();
21 | router.beforeEach(route => {
22 | const redirect = parsedRedirects.find(({ from }) => route.path === from);
23 | if (redirect == null) return;
24 |
25 | const newUrl = new URL(location.href);
26 | newUrl.pathname = redirect.to;
27 | location.href = newUrl.href;
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/match-patterns/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/match-patterns`
2 |
3 | Utilities for working with [match patterns](https://developer.chrome.com/docs/extensions/mv3/match_patterns/).
4 |
5 | ```bash
6 | pnpm i @webext-core/match-patterns
7 | ```
8 |
9 | ```ts
10 | import { MatchPattern } from '@webext-core/match-patterns';
11 |
12 | const pattern = MatchPattern('*://*.google.com/*');
13 |
14 | pattern.includes('http://google.com/search?q=test'); // true
15 | pattern.includes('https://accounts.google.com'); // true
16 | pattern.includes('https://youtube.com/watch'); // false
17 | ```
18 |
19 | ## Get Started
20 |
21 | See [documentation](https://webext-core.aklinker1.io/guide/match-patterns/) to get started!
22 |
23 | ### Supported Protocols
24 |
25 | Not all protocols are supported. Open a PR to add support.
26 |
27 | - [x] ``
28 | - [x] `https` protocol
29 | - [x] `http` protocol
30 | - [ ] `file` protocol
31 | - [ ] `ftp` protocol
32 | - [ ] `urn` protocol
33 |
--------------------------------------------------------------------------------
/docs/content/fake-browser/3.reseting-state.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Resetting State
3 | description: ''
4 | ---
5 |
6 | Implemented APIs store state in memory. When unit testing, we often want to reset all that state before each test so each test has a blank state. There are 3 ways to reset that in-memory state:
7 |
8 | 1. Reset everything: `fakeBrowser.reset()`
9 | 2. Reset just one API: `fakeBrowser.{api}.resetState()`
10 | 3. Call `fakeBrowser.{api}.on{Event}.removeAllListeners()` to remove all the listeners setup for an event
11 |
12 | ::alert
13 | All the reset methods are synchronous
14 | ::
15 |
16 | For example, to clear the in-memory stored values for `browser.storage.local`, you could call any of the following:
17 |
18 | - `fakeBrowser.reset()`
19 | - `fakeBrowser.storage.resetState()`
20 |
21 | All these reset methods should show up in your editor's intellisense.
22 |
23 | ::alert
24 | Generally, you should put a call to `fakeBrowser.reset()` in a `beforeEach` block to cleanup the state before every test.
25 | ::
26 |
--------------------------------------------------------------------------------
/packages/messaging/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/messaging`
2 |
3 | A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs. Supports all browsers (Chrome, Firefox, Safari).
4 |
5 | ```ts
6 | // ./messaging.ts
7 | import { defineExtensionMessaging } from '@webext-core/messaging';
8 |
9 | interface ProtocolMap {
10 | getStringLength(s: string): number;
11 | }
12 |
13 | export const { sendMessage, onMessage } = defineExtensionMessaging();
14 | ```
15 |
16 | ```ts
17 | // ./background.ts
18 | import { onMessage } from './messaging';
19 |
20 | onMessage('getStringLength', message => {
21 | return message.data.length;
22 | });
23 | ```
24 |
25 | ```ts
26 | // ./content-script.js or anywhere else
27 | import { sendMessage } from './messaging';
28 |
29 | const length = await sendMessage('getStringLength', 'hello world');
30 |
31 | console.log(length); // 11
32 | ```
33 |
34 | ## Get Started
35 |
36 | See [documentation](https://webext-core.aklinker1.io/guide/messaging/) to get started!
37 |
--------------------------------------------------------------------------------
/packages/fake-browser/src/index.ts:
--------------------------------------------------------------------------------
1 | import { alarms } from './apis/alarms';
2 | import { notifications } from './apis/notifications';
3 | import { runtime } from './apis/runtime';
4 | import { storage } from './apis/storage';
5 | import { tabs } from './apis/tabs';
6 | import { webNavigation } from './apis/webNavigation';
7 | import { windows } from './apis/windows';
8 | import { BrowserOverrides, FakeBrowser } from './types';
9 | import { GeneratedBrowser } from './base.gen';
10 | import merge from 'lodash.merge';
11 |
12 | export type { FakeBrowser };
13 |
14 | const overrides: BrowserOverrides = {
15 | reset() {
16 | for (const [name, api] of Object.entries(fakeBrowser)) {
17 | if (name !== 'reset') (api as any).resetState?.();
18 | }
19 | },
20 |
21 | // Implemented
22 | alarms,
23 | notifications,
24 | runtime,
25 | storage,
26 | tabs,
27 | webNavigation,
28 | windows,
29 | };
30 |
31 | /**
32 | * An in-memory implementation of the `browser` global.
33 | */
34 | export const fakeBrowser: FakeBrowser = merge(GeneratedBrowser, overrides);
35 |
--------------------------------------------------------------------------------
/packages/proxy-service/src/isBackground.ts:
--------------------------------------------------------------------------------
1 | import Browser from 'webextension-polyfill';
2 |
3 | /**
4 | * Detect whether the code is running in the background (service worker or page)
5 | */
6 | export function isBackground() {
7 | // Not in an extension context
8 | if (!canAccessExtensionApi()) return false;
9 |
10 | const manifest = Browser.runtime.getManifest();
11 | // There is no background page in the manifest
12 | if (!manifest.background) return false;
13 |
14 | return manifest.manifest_version === 3 ? isBackgroundServiceWorker() : isBackgroundPage();
15 | }
16 |
17 | function canAccessExtensionApi(): boolean {
18 | return !!Browser.runtime?.id;
19 | }
20 |
21 | const KNOWN_BACKGROUND_PAGE_PATHNAMES = [
22 | // Firefox
23 | '/_generated_background_page.html',
24 | ];
25 |
26 | function isBackgroundPage(): boolean {
27 | return (
28 | typeof window !== 'undefined' && KNOWN_BACKGROUND_PAGE_PATHNAMES.includes(location.pathname)
29 | );
30 | }
31 |
32 | function isBackgroundServiceWorker(): boolean {
33 | return typeof window === 'undefined';
34 | }
35 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/entrypoints/google.content.ts:
--------------------------------------------------------------------------------
1 | export default defineContentScript({
2 | matches: ['*://*.google.com/*'],
3 |
4 | main(ctx) {
5 | console.log('[google.content.ts] Content script loaded');
6 |
7 | googleMessaging.onMessage('fromInjected', event => {
8 | console.log('[google.content.ts] Received:', event);
9 | return 'hello injected';
10 | });
11 |
12 | googleMessaging.onMessage('fromInjected2', event => {
13 | console.log('[google.content.ts] Received:', event);
14 | return 'hello injected2';
15 | });
16 |
17 | const script = document.createElement('script');
18 | script.src = browser.runtime.getURL('/google-injected.js');
19 | script.onload = async () => {
20 | const res = await googleMessaging.sendMessage('ping', undefined);
21 | console.log('[google.content.ts] Response:', res);
22 |
23 | const res2 = await googleMessaging.sendMessage('ping2', undefined);
24 | console.log('[google.content.ts] Response2:', res2);
25 | };
26 | document.head.appendChild(script);
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/src/utils/math-service.ts:
--------------------------------------------------------------------------------
1 | import { defineProxyService } from '@webext-core/proxy-service';
2 |
3 | class MathService {
4 | add(x: number, y: number): number {
5 | console.log(`MathService.add(${x}, ${y})`);
6 | return x + y;
7 | }
8 | subtract(x: number, y: number): number {
9 | console.log(`MathService.subtract(${x}, ${y})`);
10 | return x - y;
11 | }
12 | multiply(x: number, y: number): number {
13 | console.log(`MathService.multiply(${x}, ${y})`);
14 | return x * y;
15 | }
16 | divide(x: number, y: number): number {
17 | console.log(`MathService.divide(${x}, ${y})`);
18 | if (y === 0) throw Error('Cannot divide by zero');
19 | return x / y;
20 | }
21 | async factorial(x: number): Promise {
22 | console.log(`MathService.factorial(${x})`);
23 | await new Promise(res => setTimeout(res, 0));
24 | return x === 1 ? 1 : x * (await this.factorial(x - 1));
25 | }
26 | }
27 |
28 | export const [registerMathService, getMathService] = defineProxyService(
29 | 'MathService',
30 | () => new MathService(),
31 | );
32 |
--------------------------------------------------------------------------------
/packages/isolated-element/README.md:
--------------------------------------------------------------------------------
1 | # `@webext-core/isolated-element`
2 |
3 | Isolate content script UI's styles from the parent page and control event bubbling to the host page. Supports all browsers (Chrome, Firefox, Safari).
4 |
5 | ```bash
6 | pnpm i @webext-core/isolated-element
7 | ```
8 |
9 | ```ts
10 | import { createIsolatedElement } from '@webext-core/isolated-element';
11 | import browser from 'webextension-polyfill';
12 |
13 | function mountUI(root: HtmlElement) {
14 | const text = document.createElement('p');
15 | text.textContent = 'Isolated text';
16 | root.appendChild(text);
17 | }
18 |
19 | const { parentElement, isolatedElement } = await createIsolatedElement({
20 | name: 'some-name',
21 | css: {
22 | url: browser.runtime.getURL('/path/to/styles.css'),
23 | },
24 | isolateEvents: true, // or array of event names to isolate, e.g., ['click', 'keydown']
25 | });
26 |
27 | mountUi(isolatedElement);
28 | document.body.appendChild(parentElement);
29 | ```
30 |
31 | ## Get Started
32 |
33 | See [documentation](https://webext-core.aklinker1.io/guide/isolated-element/) to get started!
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Aaron
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/content/match-patterns/api.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ---
4 | description: ""
5 | ---
6 |
7 | ::alert
8 |
9 | See [`@webext-core/match-patterns`](/match-patterns/installation/)
10 |
11 | ::
12 |
13 | ## `InvalidMatchPattern`
14 |
15 | ```ts
16 | class InvalidMatchPattern extends Error {
17 | constructor(matchPattern: string, reason: string) {
18 | // ...
19 | }
20 | }
21 | ```
22 |
23 | ## `MatchPattern`
24 |
25 | ```ts
26 | class MatchPattern {
27 | constructor(matchPattern: string) {
28 | // ...
29 | }
30 | includes(url: string | URL | Location): boolean {
31 | // ...
32 | }
33 | }
34 | ```
35 |
36 | Class for parsing and performing operations on match patterns.
37 |
38 | ### Examples
39 |
40 | ```ts
41 | const pattern = new MatchPattern("*://google.com/*");
42 |
43 | pattern.includes("https://google.com"); // true
44 | pattern.includes("http://youtube.com/watch?v=123") // false
45 | ```
46 |
47 |
48 |
49 | ---
50 |
51 | _API reference generated by [`docs/generate-api-references.ts`](https://github.com/aklinker1/webext-core/blob/main/docs/generate-api-references.ts)_
--------------------------------------------------------------------------------
/packages/messaging-demo/src/entrypoints/duckduckgo.content.ts:
--------------------------------------------------------------------------------
1 | export default defineContentScript({
2 | matches: ['*://*.duckduckgo.com/*'],
3 |
4 | main(ctx) {
5 | console.log('[duckduckgo.content.ts] Content script loaded');
6 |
7 | duckduckgoMessaging.onMessage('fromInjected', event => {
8 | console.log('[duckduckgo.content.ts] Received:', event);
9 | return 'hello injected';
10 | });
11 |
12 | duckduckgoMessaging.onMessage('fromInjected2', event => {
13 | console.log('[duckduckgo.content.ts] Received:', event);
14 | return 'hello injected2';
15 | });
16 |
17 | const script = document.createElement('script');
18 | script.src = browser.runtime.getURL('/duckduckgo-injected.js');
19 | script.onload = async () => {
20 | const res = await duckduckgoMessaging.sendMessage('ping', undefined);
21 | console.log('[duckduckgo.content.ts] Response:', res);
22 |
23 | const res2 = await duckduckgoMessaging.sendMessage('ping2', undefined);
24 | console.log('[duckduckgo.content.ts] Response2:', res2);
25 | };
26 | document.head.appendChild(script);
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/packages/storage/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { ExtensionStorage } from './types';
2 | import { defineExtensionStorage } from './defineExtensionStorage';
3 | import Browser from 'webextension-polyfill';
4 |
5 | export { defineExtensionStorage };
6 |
7 | /**
8 | * An implementation of `ExtensionStorage` based on the `browser.storage.local` storage area.
9 | */
10 | export const localExtStorage = defineExtensionStorage(Browser.storage.local);
11 | /**
12 | * An implementation of `ExtensionStorage` based on the `browser.storage.local` storage area.
13 | *
14 | * - Added to Chrome 102 as of May 24th, 2022.
15 | * - Added to Safari 16.4 as of March 27th, 2023.
16 | * - Added to Firefox 115 as of July 4th, 2023.
17 | */
18 | export const sessionExtStorage = defineExtensionStorage(Browser.storage.session);
19 | /**
20 | * An implementation of `ExtensionStorage` based on the `browser.storage.sync` storage area.
21 | */
22 | export const syncExtStorage = defineExtensionStorage(Browser.storage.sync);
23 | /**
24 | * An implementation of `ExtensionStorage` based on the `browser.storage.managed` storage area.
25 | */
26 | export const managedExtStorage = defineExtensionStorage(Browser.storage.managed);
27 |
--------------------------------------------------------------------------------
/packages/fake-browser/src/utils/defineEventWithTrigger.ts:
--------------------------------------------------------------------------------
1 | import { Events } from 'webextension-polyfill';
2 |
3 | type EventCallback = (...args: any[]) => any;
4 |
5 | type EventWithTrigger = Events.Event & {
6 | /**
7 | * Manually trigger the event and return the results from all the active listeners.
8 | */
9 | trigger(...args: Parameters): Promise[]>;
10 | /**
11 | * Remove all listeners from the event.
12 | */
13 | removeAllListeners(): void;
14 | };
15 |
16 | export function defineEventWithTrigger(): EventWithTrigger {
17 | const listeners: T[] = [];
18 |
19 | return {
20 | hasListener(callback) {
21 | return listeners.includes(callback);
22 | },
23 | hasListeners() {
24 | return listeners.length > 0;
25 | },
26 | addListener(callback) {
27 | listeners.push(callback);
28 | },
29 | removeListener(callback) {
30 | const index = listeners.indexOf(callback);
31 | if (index >= 0) listeners.splice(index, 1);
32 | },
33 | removeAllListeners() {
34 | listeners.length = 0;
35 | },
36 | async trigger(...args) {
37 | return await Promise.all(listeners.map(l => l(...args)));
38 | },
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webext-core",
3 | "packageManager": "bun@1.3.1",
4 | "private": true,
5 | "workspaces": [
6 | "packages/*"
7 | ],
8 | "scripts": {
9 | "format": "prettier --write .",
10 | "format:check": "prettier --check .",
11 | "build": "buildc all",
12 | "prepare": "simple-git-hooks"
13 | },
14 | "devDependencies": {
15 | "@aklinker1/buildc": "^1.1.5",
16 | "@aklinker1/generate-changelog": "*",
17 | "@algolia/client-search": "^4.17.0",
18 | "@types/prettier": "^2.7.2",
19 | "@types/webextension-polyfill": "^0.9.1",
20 | "@vitest/browser": "^2.1.1",
21 | "@vitest/coverage-v8": "^2.1.1",
22 | "@webext-core/fake-browser": "workspace:*",
23 | "lint-staged": "^16.2.6",
24 | "parse-path": "^7.1.0",
25 | "playwright": "^1.47.0",
26 | "prettier": "^3.3.3",
27 | "simple-git-hooks": "^2.13.1",
28 | "tsup": "^6.4.0",
29 | "turbo": "^1.6.3",
30 | "typescript": "^5.5.4",
31 | "vitest": "^2.1.1",
32 | "webextension-polyfill": "^0.10.0"
33 | },
34 | "simple-git-hooks": {
35 | "pre-commit": "bun lint-staged"
36 | },
37 | "lint-staged": {
38 | "*": "prettier --ignore-unknown --write"
39 | },
40 | "patchedDependencies": {
41 | "simple-git-hooks@2.13.1": "patches/simple-git-hooks@2.13.1.patch"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/content/0.get-started/1.browser-support.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: What browsers do WebExt Core's packages support? All of them.
3 | ---
4 |
5 | ## Overview
6 |
7 | The `@webext-core` packages are simple wrappers around [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill) by Mozilla. As such, they will work on:
8 |
9 | | Browser | Supported Versions |
10 | | --------------------- | ------------------ |
11 | | Chrome | >= 87 |
12 | | Firefox | >= 78 |
13 | | Safari _1_ | >= 14 |
14 | | Edge | >= 88 |
15 |
16 | Other Chromium-based browsers are not officially supported, and may not work_2_. See Mozilla's [supported browsers documentation](https://github.com/mozilla/webextension-polyfill#supported-browsers) for more details.
17 |
18 | > _1_ `webextension-polyfill` works on Safari, however Safari does not implement the complete web extension standard. See the [browser compatiblity chart](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs) for more details.
19 | >
20 | > _2_ In practice, the browsers are close enough to chrome that they work 99% of the time. But make sure to test your extension before assuming it will work.
21 |
--------------------------------------------------------------------------------
/packages/proxy-service/src/types.ts:
--------------------------------------------------------------------------------
1 | import { ExtensionMessagingConfig } from '@webext-core/messaging';
2 |
3 | export type Proimsify = T extends Promise ? T : Promise;
4 |
5 | export type Service = ((...args: any[]) => Promise) | { [key: string]: any | Service };
6 |
7 | /**
8 | * A type that ensures a service has only async methods.
9 | * - ***If all methods are async***, it returns the original type.
10 | * - ***If the service has non-async methods***, it returns a `DeepAsync` of the service.
11 | */
12 | export type ProxyService =
13 | TService extends DeepAsync ? TService : DeepAsync;
14 |
15 | /**
16 | * A recursive type that deeply converts all methods in `TService` to be async.
17 | */
18 | export type DeepAsync = TService extends (...args: any) => any
19 | ? ToAsyncFunction
20 | : TService extends { [key: string]: any }
21 | ? {
22 | [fn in keyof TService]: DeepAsync;
23 | }
24 | : never;
25 |
26 | type ToAsyncFunction any> = (
27 | ...args: Parameters
28 | ) => Proimsify>;
29 |
30 | /**
31 | * Configure a proxy service's behavior. It uses `@webext-core/messaging` internally, so any
32 | * config from `ExtensionMessagingConfig` can be passed as well.
33 | */
34 | export interface ProxyServiceConfig extends ExtensionMessagingConfig {}
35 |
--------------------------------------------------------------------------------
/packages/messaging-demo/src/entrypoints/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
22 |
23 |
24 | Messaging Test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/packages/proxy-service-demo/src/entrypoints/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
22 |
23 |
24 | Proxy Service Test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # webext-core
2 |
3 | [](https://github.com/aklinker1/webext-core/actions/workflows/validate.yml)
4 |
5 | A set of core libraries and tools for building web extensions. These libraries are built on top of [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill) and support all browsers.
6 |
7 | - [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/messaging/): Light weight, type-safe wrapper around the web extension messaging APIs
8 | - [`@webext-core/storage`](https://webext-core.aklinker1.io/guide/storage/): Local storage based, **type-safe** wrappers around the storage API
9 | - [`@webext-core/job-scheduler`](https://webext-core.aklinker1.io/guide/job-scheduler/): Schedule reoccuring jobs using the Alarms API
10 | - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/): An in-memory implementation of `webextension-polyfill` for testing
11 | - [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/proxy-service/): Write services that can be called from any JS context, but run in the background service worker
12 | - [`@webext-core/isolated-element`](https://webext-core.aklinker1.io/guide/isolated-element/): Isolate content script UIs from the page's styles
13 |
14 | ## Documentation
15 |
16 | To get started, checkout the [documentation website](https://webext-core.aklinker1.io).
17 |
--------------------------------------------------------------------------------
/docs/content/fake-browser/2.triggering-events.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: ''
3 | ---
4 |
5 | When possible, events are triggered based on other calls to other browser APIs. For example:
6 |
7 | - Calling `fakeBrowser.runtime.sendMessage()` will trigger the `fakeBrowser.runtime.onMessage` listeners
8 | - Calling `fakeBrowser.tabs.create()` will trigger the `fakeBrowser.tabs.onCreated` listeners
9 |
10 | Some events, like `runtime.onInstalled` or `alarms.onAlarm`, can't be triggered as they would be in a real extension.
11 |
12 | ::alert
13 | In the case of `onInstalled`, when is an extension "installed" during tests? Never? Or when the tests start? Either way, not useful for testing.
14 | ::
15 |
16 | ::alert
17 | In the case of `onAlarm`, alarms are meant to trigger in the far future, usually a much longer timespan than the duration of a unit test. Also, timers in tests are notoriously flakey and difficult to work with.
18 | ::
19 |
20 | Instead, the `fakeBrowser` provides a `trigger` method on every implemented event that you can call to trigger them manually. Pass in the arguments that the listeners are called with:
21 |
22 | ```ts
23 | await fakeBrowser.runtime.onInstalled.trigger({ reason: 'install' });
24 | await fakeBrowser.alarms.onAlarm.trigger({
25 | name: 'alarm-name',
26 | periodInMinutes: 5,
27 | scheduledTime: Date.now(),
28 | });
29 | await fakeBrowser.tab.onCreated.trigger({ ... });
30 | ```
31 |
32 | :::info
33 | If you await the call to `trigger`, it will wait for all the listener to finish running.
34 | :::
35 |
--------------------------------------------------------------------------------
/packages/isolated-element/src/options.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Options that can be passed into `createIsolatedElement`.
3 | */
4 | export interface CreateIsolatedElementOptions {
5 | /**
6 | * An HTML tag name used for the shadow root container.
7 | *
8 | * Note that you can't attach a shadow root to every type of element. There are some that can't have a shadow DOM for security reasons (for example ).
9 | *
10 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#elements_you_can_attach_a_shadow_to
11 | * @example "sticky-note"
12 | * @example "anime-skip-player"
13 | * @example "github-better-line-count-diff"
14 | * @example "div"
15 | */
16 | name: string;
17 | /**
18 | * See [`ShadowRoot.mode`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode).
19 | *
20 | * @default 'closed'
21 | */
22 | mode?: 'open' | 'closed';
23 | /**
24 | * Either the URL to a CSS file or the text contents of a CSS file. The styles will be mounted inside the shadow DOM so they don't effect the rest of the page.
25 | */
26 | css?: { url: string } | { textContent: string };
27 | /**
28 | * When enabled, `event.stopPropagation` will be called on events trying to bubble out of the shadow root.
29 | *
30 | * - Set to `true` to stop the propagation of a default set of events, `["keyup", "keydown", "keypress"]`
31 | * - Set to an array of event names to stop the propagation of a custom list of events
32 | */
33 | isolateEvents?: boolean | string[];
34 | }
35 |
--------------------------------------------------------------------------------
/packages/match-patterns/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webext-core/match-patterns",
3 | "version": "1.0.3",
4 | "description": "Utilities for working with match patterns.",
5 | "license": "MIT",
6 | "keywords": [
7 | "web-extension",
8 | "browser-extension",
9 | "chrome-extension",
10 | "webext",
11 | "web-ext",
12 | "chrome",
13 | "firefox",
14 | "safari",
15 | "browser",
16 | "extension",
17 | "match",
18 | "pattern",
19 | "content",
20 | "script"
21 | ],
22 | "homepage": "https://github.com/aklinker1/webext-core/tree/main/packages/match-patterns",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/aklinker1/webext-core",
26 | "directory": "packages/match-patterns"
27 | },
28 | "type": "module",
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "files": [
33 | "lib"
34 | ],
35 | "main": "lib/index.cjs",
36 | "module": "lib/index.js",
37 | "types": "lib/index.d.ts",
38 | "exports": {
39 | ".": {
40 | "import": "./lib/index.js",
41 | "require": "./lib/index.cjs"
42 | }
43 | },
44 | "scripts": {
45 | "build": "buildc -- tsup src/index.ts --clean --out-dir lib --dts --format esm,cjs,iife --global-name webExtCoreMatchPatterns",
46 | "test": "buildc --deps-only -- vitest",
47 | "test:coverage": "buildc --deps-only -- vitest run --coverage",
48 | "check": "buildc --deps-only -- tsc --noEmit"
49 | },
50 | "devDependencies": {
51 | "tsconfig": "workspace:*"
52 | },
53 | "buildc": {
54 | "outDir": "lib"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docs/content/match-patterns/0.installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: ''
3 | ---
4 |
5 | :badge[MV2]{type="success"} :badge[MV3]{type="success"} :badge[Chrome]{type="success"} :badge[Firefox]{type="success"} :badge[Safari]{type="success"}
6 |
7 | ## Overview
8 |
9 | `@webext-core/match-patterns` provides utilities for working with match patterns.
10 |
11 | ## Installation
12 |
13 | ###### NPM
14 |
15 | ```bash
16 | pnpm i @webext-core/match-patterns
17 | ```
18 |
19 | ```ts
20 | import { MatchPattern } from '@webext-core/match-patterns';
21 | ```
22 |
23 | ###### CDN
24 |
25 | ```bash
26 | curl -o match-patterns.js https://cdn.jsdelivr.net/npm/@webext-core/match-patterns/lib/index.global.js
27 | ```
28 |
29 | ```html
30 |
31 |
34 | ```
35 |
36 | ## Usage
37 |
38 | `MatchPattern` includes one function: `includes`. It can be used to check if a URL is included (or matches) the match pattern.
39 |
40 | ```ts
41 | import { MatchPattern } from '@webext-core/match-patterns';
42 |
43 | const google = new MatchPattern('*://*.google.com');
44 | google.includes('https://accounts.google.com'); // true
45 | google.includes('https://google.com/search?q=test'); // true
46 |
47 | const youtube = new MatchPattern('*://youtube.com/watch');
48 | youtube.includes('https://youtube.com/watch'); // true
49 | youtube.includes('https://youtube.com/mrbeast'); // false
50 | youtube.includes('https://accounts.google.com'); // false
51 | ```
52 |
53 | `includes` also accepts URLs and `window.location`
54 |
55 | ```ts
56 | google.includes(new URL('https://google.com'));
57 | google.includes(window.location);
58 | ```
59 |
--------------------------------------------------------------------------------
/packages/storage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webext-core/storage",
3 | "version": "1.2.0",
4 | "description": "A type-safe, localStorage-esk wrapper around the web extension storage APIs. Supports all browsers (Chrome, Firefox, Safari, etc)",
5 | "license": "MIT",
6 | "keywords": [
7 | "web-extension",
8 | "browser-extension",
9 | "chrome-extension",
10 | "webext",
11 | "web-ext",
12 | "chrome",
13 | "firefox",
14 | "safari",
15 | "browser",
16 | "extension",
17 | "storage"
18 | ],
19 | "homepage": "https://github.com/aklinker1/webext-core/tree/main/packages/storage",
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/aklinker1/webext-core",
23 | "directory": "packages/storage"
24 | },
25 | "type": "module",
26 | "publishConfig": {
27 | "access": "public"
28 | },
29 | "files": [
30 | "lib"
31 | ],
32 | "main": "lib/index.cjs",
33 | "module": "lib/index.js",
34 | "types": "lib/index.d.ts",
35 | "exports": {
36 | ".": {
37 | "import": "./lib/index.js",
38 | "require": "./lib/index.cjs"
39 | }
40 | },
41 | "scripts": {
42 | "build": "buildc -- tsup src/index.ts --clean --out-dir lib --dts --format esm,cjs,iife --global-name webExtCoreStorage",
43 | "test": "buildc --deps-only -- vitest -r src",
44 | "test:coverage": "buildc --deps-only -- vitest run -r src --coverage",
45 | "check": "buildc --deps-only -- tsc --noEmit"
46 | },
47 | "dependencies": {
48 | "webextension-polyfill": "^0.10.0"
49 | },
50 | "devDependencies": {
51 | "@types/webextension-polyfill": "^0.10.5",
52 | "@webext-core/fake-browser": "workspace:*",
53 | "tsconfig": "workspace:*"
54 | },
55 | "buildc": {
56 | "outDir": "lib"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/docs/_redirects.txt:
--------------------------------------------------------------------------------
1 | # Vitepress -> Nuxt
2 |
3 | /guide/ /get-started/introduction
4 | /guide/browser-support.html /get-started/browser-support
5 | /guide/contributing.html /get-started/contributing
6 | /guide/fake-browser/ /fake-browser/installation
7 | /guide/fake-browser/testing-frameworks.html /fake-browser/testing-frameworks
8 | /guide/fake-browser/reseting-state.html /fake-browser/triggering-events
9 | /guide/fake-browser/triggering-events.html /fake-browser/reseting-state
10 | /guide/fake-browser/implemented-apis.html /fake-browser/implemented-apis
11 | /guide/isolated-element/ /isolated-element/installation
12 | /guide/job-scheduler/ /job-scheduler/installation
13 | /guide/match-patterns/ /match-patterns/installation
14 | /guide/messaging/ /messaging/installation
15 | /guide/messaging/protocol-maps.html /messaging/protocol-maps
16 | /guide/proxy-service/ /proxy-service/installation
17 | /guide/proxy-service/defining-services.html /proxy-service/defining-services
18 | /guide/storage/ /storage/installation
19 | /guide/storage/typescript.html /storage/typescript
20 | /api/fake-browser.html /fake-browser/api
21 | /api/isolated-element.html /isolated-element/api
22 | /api/job-scheduler.html /job-scheduler/api
23 | /api/match-patterns.html /match-patterns/api
24 | /api/messaging.html /messaging/api
25 | /api/proxy-service.html /proxy-service/api
26 | /api/storage.html /storage/api
27 |
--------------------------------------------------------------------------------
/packages/job-scheduler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webext-core/job-scheduler",
3 | "version": "1.0.0",
4 | "description": "Schedule and run jobs in your background script",
5 | "license": "MIT",
6 | "keywords": [
7 | "web-extension",
8 | "browser-extension",
9 | "chrome-extension",
10 | "webext",
11 | "web-ext",
12 | "chrome",
13 | "firefox",
14 | "safari",
15 | "browser",
16 | "extension",
17 | "job",
18 | "scheduler",
19 | "cron",
20 | "period",
21 | "periodic"
22 | ],
23 | "homepage": "https://github.com/aklinker1/webext-core/tree/main/packages/job-scheduler",
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/aklinker1/webext-core",
27 | "directory": "packages/job-scheduler"
28 | },
29 | "type": "module",
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "files": [
34 | "lib"
35 | ],
36 | "main": "lib/index.cjs",
37 | "module": "lib/index.js",
38 | "types": "lib/index.d.ts",
39 | "exports": {
40 | ".": {
41 | "import": "./lib/index.js",
42 | "require": "./lib/index.cjs"
43 | }
44 | },
45 | "scripts": {
46 | "build": "buildc -- tsup src/index.ts --clean --out-dir lib --dts --format esm,cjs,iife --global-name webExtCoreJobScheduler",
47 | "test": "buildc --deps-only -- vitest -r src",
48 | "test:coverage": "buildc --deps-only -- vitest run -r src --coverage",
49 | "check": "buildc --deps-only -- tsc --noEmit"
50 | },
51 | "dependencies": {
52 | "cron-parser": "^4.8.1",
53 | "webextension-polyfill": "^0.10.0",
54 | "format-duration": "^3.0.2"
55 | },
56 | "devDependencies": {
57 | "@webext-core/fake-browser": "workspace:*",
58 | "tsconfig": "workspace:*"
59 | },
60 | "buildc": {
61 | "outDir": "lib"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/isolated-element/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webext-core/isolated-element",
3 | "version": "1.1.3",
4 | "description": "Isolate content script UI's styles from the parent page. Supports all browsers (Chrome, Firefox, Safari)",
5 | "license": "MIT",
6 | "keywords": [
7 | "web-extension",
8 | "browser-extension",
9 | "chrome-extension",
10 | "webext",
11 | "web-ext",
12 | "chrome",
13 | "firefox",
14 | "safari",
15 | "browser",
16 | "extension",
17 | "islolate",
18 | "styles",
19 | "css",
20 | "content-script"
21 | ],
22 | "homepage": "https://github.com/aklinker1/webext-core/tree/main/packages/isolated-element",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/aklinker1/webext-core",
26 | "directory": "packages/isolated-element"
27 | },
28 | "type": "module",
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "files": [
33 | "lib"
34 | ],
35 | "main": "lib/index.cjs",
36 | "module": "lib/index.js",
37 | "types": "lib/index.d.ts",
38 | "exports": {
39 | ".": {
40 | "import": "./lib/index.js",
41 | "require": "./lib/index.cjs"
42 | }
43 | },
44 | "scripts": {
45 | "build": "buildc -- tsup src/index.ts --clean --out-dir lib --dts --format esm,cjs,iife --global-name webExtCoreIsolatedElement",
46 | "test": "buildc --deps-only -- vitest -r src",
47 | "test:coverage": "buildc --deps-only -- vitest run -r src --coverage",
48 | "check": "buildc --deps-only -- tsc --noEmit"
49 | },
50 | "devDependencies": {
51 | "@types/is-potential-custom-element-name": "^1.0.0",
52 | "jsdom": "^20.0.3",
53 | "tsconfig": "workspace:*"
54 | },
55 | "dependencies": {
56 | "is-potential-custom-element-name": "^1.0.1"
57 | },
58 | "buildc": {
59 | "outDir": "lib"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yml:
--------------------------------------------------------------------------------
1 | name: Validate
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | workflow_call:
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | checks:
13 | runs-on: ubuntu-22.04
14 | steps:
15 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
16 | - uses: ./.github/actions/setup
17 | - run: |
18 | bun format:check
19 | bun run --cwd packages/fake-browser check
20 | bun run --cwd packages/isolated-element check
21 | bun run --cwd packages/isolated-element-demo check
22 | bun run --cwd packages/job-scheduler check
23 | bun run --cwd packages/match-patterns check
24 | bun run --cwd packages/messaging check
25 | bun run --cwd packages/proxy-service check
26 | bun run --cwd packages/storage check
27 |
28 | build:
29 | runs-on: ubuntu-22.04
30 | steps:
31 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
32 | - uses: ./.github/actions/setup
33 | - run: node --version
34 | - run: bun run build
35 |
36 | tests:
37 | runs-on: ubuntu-22.04
38 | steps:
39 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
40 | - uses: ./.github/actions/setup
41 | - name: Install Playwright Browsers
42 | run: bun x playwright install --with-deps
43 | - run: |
44 | bun run --cwd packages/fake-browser test:coverage
45 | bun run --cwd packages/isolated-element test:coverage
46 | bun run --cwd packages/job-scheduler test:coverage
47 | bun run --cwd packages/match-patterns test:coverage
48 | bun run --cwd packages/messaging test:coverage
49 | bun run --cwd packages/proxy-service test:coverage
50 | bun run --cwd packages/storage test:coverage
51 |
--------------------------------------------------------------------------------
/packages/fake-browser/src/apis/alarms.ts:
--------------------------------------------------------------------------------
1 | import { Alarms } from 'webextension-polyfill';
2 | import { BrowserOverrides } from '../types';
3 | import { defineEventWithTrigger } from '../utils/defineEventWithTrigger';
4 |
5 | const alarmList: Alarms.Alarm[] = [];
6 | const onAlarm = defineEventWithTrigger<(name: Alarms.Alarm) => void>();
7 |
8 | export const alarms: BrowserOverrides['alarms'] = {
9 | resetState() {
10 | alarmList.length = 0;
11 | onAlarm.removeAllListeners();
12 | },
13 | async clear(name) {
14 | name ??= '';
15 | const index = alarmList.findIndex(alarm => alarm.name === name);
16 | if (index >= 0) {
17 | alarmList.splice(index, 1);
18 | return true;
19 | }
20 | return false;
21 | },
22 | async clearAll() {
23 | const hasAlarms = alarmList.length > 0;
24 | alarmList.length = 0;
25 | return hasAlarms;
26 | },
27 | // @ts-expect-error: multiple implementations
28 | create(
29 | arg0: string | undefined | Alarms.CreateAlarmInfoType,
30 | arg1: Alarms.CreateAlarmInfoType | undefined,
31 | ) {
32 | let name: string;
33 | let alarmInfo: Alarms.CreateAlarmInfoType;
34 | if (typeof arg0 === 'object') {
35 | name = '';
36 | alarmInfo = arg0;
37 | } else {
38 | name = arg0 ?? '';
39 | alarmInfo = arg1 as Alarms.CreateAlarmInfoType;
40 | }
41 | const i = alarmList.findIndex(alarm => alarm.name === name);
42 | if (i >= 0) alarmList.splice(i, 1);
43 |
44 | alarmList.push({
45 | name,
46 | scheduledTime: alarmInfo.when ?? Date.now() + (alarmInfo.delayInMinutes ?? 0) * 60e3,
47 | periodInMinutes: alarmInfo.periodInMinutes,
48 | });
49 | },
50 | async get(name) {
51 | name ??= '';
52 | return alarmList.find(alarm => alarm.name === name)!;
53 | },
54 | async getAll() {
55 | return alarmList;
56 | },
57 | onAlarm,
58 | };
59 |
--------------------------------------------------------------------------------
/packages/storage/src/types.ts:
--------------------------------------------------------------------------------
1 | export type AnySchema = Record;
2 |
3 | /**
4 | * Call this method to remove the listener that was added.
5 | */
6 | export type RemoveListenerCallback = () => void;
7 |
8 | export type OnChangeCallback<
9 | TSchema extends AnySchema,
10 | TKey extends keyof TSchema = keyof TSchema,
11 | > = (newValue: TSchema[TKey], oldValue: TSchema[TKey] | null) => void;
12 |
13 | /**
14 | * This is the interface for the storage objects exported from the package. It is similar to `localStorage`, except for a few differences:
15 | *
16 | * - ***It's async*** since the web extension storage APIs are async.
17 | * - It can store any data type, ***not just strings***.
18 | */
19 | export interface ExtensionStorage {
20 | /**
21 | * Clear all values from storage.
22 | */
23 | clear(): Promise;
24 |
25 | /**
26 | * Return the value in storage or `null` if the item is missing.
27 | */
28 | getItem(key: TKey): Promise[TKey] | null>;
29 |
30 | /**
31 | * Set the key and value in storage. Unlike with `localStorage`, passing `null` or `undefined`
32 | * will result in `null` being stored for the value.
33 | */
34 | setItem(key: TKey, value: TSchema[TKey]): Promise;
35 |
36 | /**
37 | * Remove the value from storage at a key.
38 | */
39 | removeItem(key: TKey): Promise;
40 |
41 | /**
42 | * Add a callback that is executed when a key is changed in the storage. Listeners are executed in
43 | * parallel, but the first listeners added are always started first.
44 | *
45 | * Returns a method that, when called, removes the listener that was added.
46 | */
47 | onChange(
48 | key: TKey,
49 | cb: OnChangeCallback,
50 | ): RemoveListenerCallback;
51 | }
52 |
--------------------------------------------------------------------------------
/packages/fake-browser/src/apis/tabs.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import { fakeBrowser } from '../index';
3 |
4 | describe('tabs', () => {
5 | beforeEach(fakeBrowser.reset);
6 |
7 | describe('create', () => {
8 | it('should create a new tab and add it to tabList', async () => {
9 | const newTab = await fakeBrowser.tabs.create({ url: 'https://example.com' });
10 |
11 | expect(newTab).toBeDefined();
12 | expect(newTab.id).toBe(1);
13 | expect(newTab.url).toBe('https://example.com');
14 |
15 | const tabs = await fakeBrowser.tabs.query({});
16 | expect(tabs).toHaveLength(2); // default tab + new tab
17 | });
18 |
19 | it('should trigger onCreated event', async () => {
20 | const listener = vi.fn();
21 | fakeBrowser.tabs.onCreated.addListener(listener);
22 |
23 | const newTab = await fakeBrowser.tabs.create({ url: 'https://example.com' });
24 |
25 | expect(listener).toHaveBeenCalledWith(
26 | expect.objectContaining({
27 | id: newTab.id,
28 | url: 'https://example.com',
29 | }),
30 | );
31 | });
32 | });
33 |
34 | describe('query', () => {
35 | it('should filter tabs by windowId', async () => {
36 | const window2 = await fakeBrowser.windows.create();
37 | const tab1 = await fakeBrowser.tabs.create({ url: 'https://window1.com' });
38 | const tab2 = await fakeBrowser.tabs.create({
39 | url: 'https://window2.com',
40 | windowId: window2.id,
41 | });
42 |
43 | const window1Tabs = await fakeBrowser.tabs.query({ windowId: 0 });
44 | expect(window1Tabs).toHaveLength(2); // default + tab1
45 |
46 | const window2Tabs = await fakeBrowser.tabs.query({ windowId: window2.id });
47 | expect(window2Tabs).toHaveLength(1);
48 | expect(window2Tabs[0].url).toBe('https://window2.com');
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/packages/fake-browser/src/apis/webNavigation.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, beforeEach } from 'vitest';
2 | import { fakeBrowser } from '..';
3 |
4 | describe('webNavigation', () => {
5 | beforeEach(() => {
6 | fakeBrowser.reset();
7 | });
8 |
9 | it('should properly overwrite onBeforeNavigate, adding the trigger method', () => {
10 | expect(fakeBrowser.webNavigation.onBeforeNavigate.trigger).toBeDefined();
11 | });
12 |
13 | it('should properly overwrite onCommitted, adding the trigger method', () => {
14 | expect(fakeBrowser.webNavigation.onCommitted.trigger).toBeDefined();
15 | });
16 |
17 | it('should properly overwrite onCompleted, adding the trigger method', () => {
18 | expect(fakeBrowser.webNavigation.onCompleted.trigger).toBeDefined();
19 | });
20 |
21 | it('should properly overwrite onCreatedNavigationTarget, adding the trigger method', () => {
22 | expect(fakeBrowser.webNavigation.onCreatedNavigationTarget.trigger).toBeDefined();
23 | });
24 |
25 | it('should properly overwrite onDOMContentLoaded, adding the trigger method', () => {
26 | expect(fakeBrowser.webNavigation.onDOMContentLoaded.trigger).toBeDefined();
27 | });
28 |
29 | it('should properly overwrite onErrorOccurred, adding the trigger method', () => {
30 | expect(fakeBrowser.webNavigation.onErrorOccurred.trigger).toBeDefined();
31 | });
32 |
33 | it('should properly overwrite onHistoryStateUpdated, adding the trigger method', () => {
34 | expect(fakeBrowser.webNavigation.onHistoryStateUpdated.trigger).toBeDefined();
35 | });
36 |
37 | it('should properly overwrite onReferenceFragmentUpdated, adding the trigger method', () => {
38 | expect(fakeBrowser.webNavigation.onReferenceFragmentUpdated.trigger).toBeDefined();
39 | });
40 |
41 | it('should properly overwrite onTabReplaced, adding the trigger method', () => {
42 | expect(fakeBrowser.webNavigation.onTabReplaced.trigger).toBeDefined();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/packages/proxy-service/src/flattenPromise.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { flattenPromise } from './flattenPromise';
3 |
4 | describe('flattenPromise', () => {
5 | it('should convert Promise to DeepAsync', async () => {
6 | const fnPromise = Promise.resolve((x: number, y: number) => x + y);
7 |
8 | const fn = flattenPromise(fnPromise);
9 | const actual = await fn(1, 2);
10 |
11 | expect(actual).toBe(3);
12 | });
13 |
14 | it('should convert shallow Promise