Skip to content

Commit 8ad7e4e

Browse files
authored
Merge branch 'main' into refactor/migration
2 parents 797c783 + c7b7bcc commit 8ad7e4e

103 files changed

Lines changed: 5833 additions & 1400 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Generation
2+
3+
- Source: https://github.com/aklinker1/webext-core
4+
- Submodule path: sources/webext-core
5+
- Commit: `3bfdf7953963c96b54f8dd8f6d2e8824f456273d` — docs: fix redirects (#127)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: webext-core
3+
description: Utilities for browser extensions - proxy services for cross-context RPC, type-safe messaging, URL match patterns, fake browser for testing, job scheduling, and shadow DOM isolation.
4+
---
5+
6+
# webext-core - Browser Extension Utilities
7+
8+
## When to Use
9+
10+
Apply this skill when:
11+
- `package.json` has any `@webext-core/*` dependency
12+
- Code imports `defineProxyService`, `flattenPromise` from `@webext-core/proxy-service`
13+
- Code imports `defineExtensionMessaging`, `defineWindowMessaging` from `@webext-core/messaging`
14+
- Code uses `MatchPattern` from `@webext-core/match-patterns`
15+
- Tests use `fakeBrowser` from `@webext-core/fake-browser`
16+
- Code uses `defineJobScheduler` from `@webext-core/job-scheduler`
17+
- Code uses `createIsolatedElement` from `@webext-core/isolated-element`
18+
19+
## Quick Reference
20+
21+
| Package | Purpose | Reference |
22+
|---------|---------|-----------|
23+
| `@webext-core/proxy-service` | Cross-context RPC — call background services from anywhere | [references/proxy-service.md](references/proxy-service.md) |
24+
| `@webext-core/messaging` | Type-safe extension/window/custom event messaging | [references/messaging.md](references/messaging.md) |
25+
| `@webext-core/match-patterns` | URL pattern matching utilities | [references/match-patterns.md](references/match-patterns.md) |
26+
| `@webext-core/fake-browser` | In-memory browser API for unit testing | [references/fake-browser.md](references/fake-browser.md) |
27+
| `@webext-core/job-scheduler` | Background job scheduling via Alarms API | [references/job-scheduler.md](references/job-scheduler.md) |
28+
| `@webext-core/isolated-element` | Shadow DOM containers for content script UIs | [references/isolated-element.md](references/isolated-element.md) |
29+
| `@webext-core/storage` | localStorage-like wrapper (prefer WXT storage if using WXT) | [references/storage.md](references/storage.md) |
30+
| Cross-package patterns and anti-patterns || [references/patterns.md](references/patterns.md) |
31+
32+
## Most Common Pattern
33+
34+
```ts
35+
// services/counter.ts
36+
import { defineProxyService } from '@webext-core/proxy-service';
37+
38+
const [registerCounter, getCounter] = defineProxyService('CounterService', () => {
39+
let count = 0;
40+
return {
41+
increment: () => ++count,
42+
getCount: () => count,
43+
};
44+
});
45+
46+
export { registerCounter, getCounter };
47+
48+
// background.ts — register the real implementation
49+
registerCounter();
50+
51+
// popup or content script — call via proxy (all methods return Promise)
52+
const counter = getCounter();
53+
const newCount = await counter.increment();
54+
```
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# webext-core - Fake Browser
2+
3+
Package: `@webext-core/fake-browser`
4+
5+
An in-memory implementation of `webextension-polyfill` for unit testing. Implements the `browser` APIs without requiring a real browser environment.
6+
7+
## Setup (Standalone / Non-WXT)
8+
9+
```ts
10+
// vitest.setup.ts
11+
import { fakeBrowser } from '@webext-core/fake-browser';
12+
13+
// Replace the global browser mock with the fake
14+
vi.mock('webextension-polyfill');
15+
globalThis.browser = fakeBrowser;
16+
```
17+
18+
## Setup (WXT)
19+
20+
WXT's `WxtVitest()` plugin configures `fakeBrowser` automatically. Import it from:
21+
22+
```ts
23+
import { fakeBrowser } from 'wxt/testing';
24+
```
25+
26+
## Usage in Tests
27+
28+
```ts
29+
import { describe, it, expect, beforeEach, vi } from 'vitest';
30+
import { fakeBrowser } from '@webext-core/fake-browser';
31+
32+
vi.mock('webextension-polyfill');
33+
34+
describe('my service', () => {
35+
beforeEach(() => {
36+
fakeBrowser.reset(); // Always reset state between tests
37+
});
38+
39+
it('stores and retrieves data', async () => {
40+
await browser.storage.local.set({ key: 'value' });
41+
const result = await browser.storage.local.get('key');
42+
expect(result).toEqual({ key: 'value' });
43+
});
44+
45+
it('creates and reads alarms', async () => {
46+
await browser.alarms.create('test', { delayInMinutes: 1 });
47+
const alarm = await browser.alarms.get('test');
48+
expect(alarm?.name).toBe('test');
49+
});
50+
});
51+
```
52+
53+
## Implemented APIs
54+
55+
| API | Status | Notes |
56+
|---|---|---|
57+
| `browser.alarms` | Full | `create`, `get`, `getAll`, `clear`, `clearAll`, `onAlarm` |
58+
| `browser.notifications` | Full | `onClosed`, `onClicked`, `onButtonClicked`, `onShown` |
59+
| `browser.runtime` | Partial | `id`, `getURL`, `sendMessage`, `onMessage`, `onInstalled`, `onStartup`, `onSuspend`, `onSuspendCanceled`, `onUpdateAvailable` |
60+
| `browser.storage` | Full | `local`, `session`, `sync`, `managed`; all four support `get`, `set`, `remove`, `clear`, `onChanged` |
61+
| `browser.tabs` | Partial | `get`, `getCurrent`, `create`, `duplicate`, `query`, `highlight`, `update`, `remove`; `onCreated`, `onUpdated`, `onActivated`, `onHighlighted`, `onRemoved` |
62+
| `browser.windows` | Partial | `get`, `getAll`, `create`, `getCurrent`, `getLastFocused`, `update`, `remove`; `onCreated`, `onRemoved`, `onFocusChanged` |
63+
| `browser.webNavigation` | Events only | `onBeforeNavigate`, `onCommitted`, `onDOMContentLoaded`, `onCompleted`, `onErrorOccurred`, `onCreatedNavigationTarget`, `onReferenceFragmentUpdated`, `onTabReplaced`, `onHistoryStateUpdated` |
64+
65+
## `EventForTesting` — Triggering Events in Tests
66+
67+
Every implemented event has a `.trigger(...args)` method that manually fires the event and returns a `Promise` resolving to an array of all listener return values.
68+
69+
```ts
70+
import { fakeBrowser } from '@webext-core/fake-browser';
71+
import { vi, expect, it } from 'vitest';
72+
73+
it('handles tab creation', async () => {
74+
const handler = vi.fn();
75+
browser.tabs.onCreated.addListener(handler);
76+
77+
// Manually fire the event
78+
await fakeBrowser.tabs.onCreated.trigger({
79+
id: 1,
80+
url: 'https://example.com',
81+
active: true,
82+
index: 0,
83+
pinned: false,
84+
highlighted: false,
85+
windowId: 1,
86+
incognito: false,
87+
});
88+
89+
expect(handler).toHaveBeenCalledOnce();
90+
});
91+
92+
it('handles alarm firing', async () => {
93+
const handler = vi.fn();
94+
browser.alarms.onAlarm.addListener(handler);
95+
96+
await fakeBrowser.alarms.onAlarm.trigger({ name: 'my-alarm', scheduledTime: Date.now() });
97+
98+
expect(handler).toHaveBeenCalledWith({ name: 'my-alarm', scheduledTime: expect.any(Number) });
99+
});
100+
101+
it('handles runtime.onInstalled', async () => {
102+
const handler = vi.fn();
103+
browser.runtime.onInstalled.addListener(handler);
104+
105+
await fakeBrowser.runtime.onInstalled.trigger({ reason: 'install' });
106+
107+
expect(handler).toHaveBeenCalledWith({ reason: 'install' });
108+
});
109+
```
110+
111+
## `reset()` Between Tests
112+
113+
`fakeBrowser.reset()` clears all in-memory state:
114+
115+
- All stored data in `storage.local`, `storage.session`, `storage.sync`, `storage.managed`
116+
- All registered alarms
117+
- All tabs (resets to a default tab)
118+
- All windows (resets to a default window)
119+
- All event listeners on every implemented API
120+
- Resets `runtime.id` to `'test-extension-id'`
121+
122+
```ts
123+
beforeEach(() => {
124+
fakeBrowser.reset();
125+
});
126+
```
127+
128+
Omitting this causes state to bleed between tests, producing hard-to-diagnose failures.
129+
130+
## `runtime.id` and `runtime.getURL`
131+
132+
The fake runtime uses `'test-extension-id'` as the extension ID by default:
133+
134+
```ts
135+
browser.runtime.id; // 'test-extension-id'
136+
browser.runtime.getURL('/icons/icon.png');
137+
// 'chrome-extension://test-extension-id/icons/icon.png'
138+
```
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# webext-core - Isolated Element
2+
3+
Package: `@webext-core/isolated-element`
4+
5+
Create a Shadow DOM container for content script UIs. Isolates styles from the host page and controls event bubbling. Supports Chrome, Firefox, and Safari.
6+
7+
## Basic Usage
8+
9+
```ts
10+
import { createIsolatedElement } from '@webext-core/isolated-element';
11+
12+
const { parentElement, isolatedElement, shadow } = await createIsolatedElement({
13+
name: 'my-extension-ui', // valid custom element name (must contain a hyphen)
14+
css: {
15+
textContent: `
16+
p { color: red; font-family: sans-serif; }
17+
button { background: #0070f3; color: white; padding: 8px 16px; border: none; }
18+
`,
19+
},
20+
});
21+
22+
// Mount your UI inside isolatedElement — styles are scoped here
23+
const p = document.createElement('p');
24+
p.textContent = 'Isolated text';
25+
isolatedElement.appendChild(p);
26+
27+
// Add parentElement to the page DOM to show the UI
28+
document.body.appendChild(parentElement);
29+
```
30+
31+
## Returned Values
32+
33+
| Value | Type | Description |
34+
|---|---|---|
35+
| `parentElement` | `HTMLElement` | The custom element you append to `document.body` |
36+
| `isolatedElement` | `HTMLElement` | A `<div>` inside the shadow root; mount your UI here |
37+
| `shadow` | `ShadowRoot` | The shadow root; use for advanced DOM manipulation |
38+
39+
## `createIsolatedElement` Options
40+
41+
```ts
42+
export interface CreateIsolatedElementOptions {
43+
name: string; // Tag name for the shadow host
44+
mode?: 'open' | 'closed'; // ShadowRoot.mode, default: 'closed'
45+
css?: { url: string } | { textContent: string }; // Styles loaded into shadow DOM
46+
isolateEvents?: boolean | string[]; // Stop events from bubbling to host page
47+
}
48+
```
49+
50+
### `name`
51+
52+
The tag name used for the shadow host element. Must be a valid HTML tag that supports shadow DOM attachment — either a known semantic element (`div`, `span`, `article`, `aside`, `blockquote`, `body`, `footer`, `h1``h6`, `header`, `main`, `nav`, `p`, `section`) or a valid **custom element name**.
53+
54+
Custom element naming rules:
55+
- Must contain at least one hyphen (`-`)
56+
- Must be all lowercase
57+
- Cannot start with a digit
58+
- Cannot be a reserved name (e.g., `annotation-xml`, `color-profile`, `font-face`, `font-face-src`, `font-face-uri`, `font-face-format`, `font-face-name`, `missing-glyph`)
59+
60+
```ts
61+
// Valid
62+
name: 'my-extension-ui'
63+
name: 'github-line-diff'
64+
name: 'div'
65+
66+
// Invalid — throws Error
67+
name: 'myui' // no hyphen
68+
name: 'MyExtUI' // uppercase
69+
name: '1-ext-ui' // starts with digit
70+
```
71+
72+
### `mode`
73+
74+
Shadow DOM mode. Defaults to `'closed'`, which prevents external JavaScript from accessing the shadow root via `element.shadowRoot`. Use `'open'` if you need programmatic access from outside.
75+
76+
### `css`
77+
78+
Inject styles that are scoped to the shadow DOM:
79+
80+
```ts
81+
// From a URL (fetched at creation time)
82+
css: { url: browser.runtime.getURL('/content-scripts/style.css') }
83+
84+
// Inline text
85+
css: { textContent: 'p { color: blue; }' }
86+
```
87+
88+
### `isolateEvents`
89+
90+
Prevents specified events from bubbling up through the shadow boundary to the host page. Useful to stop the page from intercepting keyboard shortcuts or click events from your UI.
91+
92+
```ts
93+
// Default set: ['keydown', 'keyup', 'keypress']
94+
isolateEvents: true
95+
96+
// Custom list
97+
isolateEvents: ['click', 'keydown', 'keyup', 'input', 'pointerdown']
98+
```
99+
100+
## Framework Integration (Vue)
101+
102+
```ts
103+
import { createIsolatedElement } from '@webext-core/isolated-element';
104+
import { createApp } from 'vue';
105+
import App from './App.vue';
106+
import browser from 'webextension-polyfill';
107+
108+
const { parentElement, isolatedElement } = await createIsolatedElement({
109+
name: 'my-extension-overlay',
110+
css: { url: browser.runtime.getURL('/assets/content-style.css') },
111+
mode: 'closed',
112+
isolateEvents: true,
113+
});
114+
115+
const app = createApp(App);
116+
app.mount(isolatedElement);
117+
document.body.appendChild(parentElement);
118+
```
119+
120+
## Framework Integration (React)
121+
122+
```ts
123+
import { createIsolatedElement } from '@webext-core/isolated-element';
124+
import { createRoot } from 'react-dom/client';
125+
import App from './App';
126+
127+
const { parentElement, isolatedElement } = await createIsolatedElement({
128+
name: 'my-extension-panel',
129+
css: { textContent: styles },
130+
isolateEvents: ['keydown', 'keyup'],
131+
});
132+
133+
createRoot(isolatedElement).render(<App />);
134+
document.body.appendChild(parentElement);
135+
```
136+
137+
## Cleanup
138+
139+
To remove the UI from the page, remove `parentElement` from the DOM:
140+
141+
```ts
142+
parentElement.remove();
143+
```

0 commit comments

Comments
 (0)