## 1. Project Overview & Quickstart (crxjs/CONTRIBUTING)

# Technical Documentation: crxjs/CONTRIBUTING

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/crxjs/CONTRIBUTING)

# CONTRIBUTING

### Agent Skills & Rules
- Discovered 21 agent rule(s) and prompt configurations.



--- METRICS ---
- Files Extracted: 1
- Estimated Token Budget: ~25 tokens
- Canonical Reference: https://codewiki.google/github.com/crxjs/CONTRIBUTING


## 2. Official Technical Reference & Guides (crxjs/website)

## File: content/3.awesome/index.md

---
title: Awesome CRXJS
fullpage: true
icon: i-mdi:newspaper
---

::awesome
::

---

## File: content/2.concepts/1.manifest.md

---
title: Manifest
icon: 'carbon:cloud-satellite-config'
---

::alert{type="secondary"}
Compared to `manifest.json`, we strongly recommend using `defineManifest` as it provides stronger type support and dynamic features.
::

## Manifest Configuration

::tabs
  ::div{label="defineManifest" icon="catppuccin:folder-config"}
  CRXJS exports a helper function called `defineManifest`.
  It's similar to Vite's `defineConfig` and supports autocompletion and dynamic or asynchronous definitions.
  ### Basic Usage
  ```ts [manifest.config.ts]
  import { defineManifest } from '@crxjs/vite-plugin'

  export default defineManifest({
    manifest_version: 3,
    name: 'CRXJS from scratch',
    version: '1.0.0',
    action: {
      default_popup: 'index.html',
    },
  })
  ```

  ::collapsible
  #title
  ##### Version Number Configuration Using `defineManifest`

  #content
    ::stack
      :read-more{title="Chrome Extension Version Format" to="https://developer.chrome.com/docs/extensions/mv3/manifest/version/"}
      ```typescript [manifest.config.ts]
      import { defineManifest } from '@crxjs/vite-plugin'
      import packageJson from './package.json'
      const { version } = packageJson

      // Convert from Semver (example: 0.1.0-beta6)
      const [major, minor, patch, label = '0'] = version
        // can only contain digits, dots, or dash
        .replace(/[^\d.-]+/g, '')
        // split into version parts
        .split(/[.-]/)

      export default defineManifest(async (env) => ({
        manifest_version: 3,
        name:
          env.mode === 'staging'
            ? '[INTERNAL] CRXJS Power Tools'
            : 'CRXJS Power Tools',
        // up to four numbers separated by dots
        version: `${major}.${minor}.${patch}.${label}`,
        // semver is OK in "version_name"
        version_name: version,
      }))
      ```
    ::
  ::
  ::

  ::div{label="manifest.json" icon="vscode-icons:file-type-json"}
    When using a JSON file, consider utilizing the schema from [JSON Schema Store](https://json.schemastore.org/chrome-manifest.json) to leverage autocompletion and validation capabilities.

    Configure VSCode to use the JSON schema by adding this to your settings file:


    ```json [.vscode/settings.json]
    {
      "json.schemas": [
        {
          "fileMatch": ["manifest.json"],
          "url": "https://json.schemastore.org/chrome-manifest.json"
        }
      ]
    }
    ```

    ### Basic Usage
    ```json [manifest.json]
    {
      "manifest_version": 3,
      "name": "CRXJS from scratch",
      "version": "1.0.0",
      "action": {
        "default_popup": "index.html"
      }
    }
    ```
  ::
::


## Path Resolution Rules

### 1. Base Definition
All manifest resource configuration paths are resolved relative to the Vite project root directory (i.e., the directory where `vite.config.ts` is located).

### 2. Correct Configuration Example
```json [manifest.json]
{
  "options_page": "options.html",
  "devtools_page": "pages/devtools.html"
}
```
**Features**:  
- Uses simple relative paths (no `./` prefix required)  
- Supports multi-level directory nesting  

### 3. Common Error Patterns
```json [manifest.json]
{
  "options_page": "./src/options.html",
  "devtools_page": "/Users/project/pages/devtools.html"
}
```
**Prohibited behaviors**:  
- Using path modifiers like `./` or `../`  
- Hardcoding operating system-level absolute paths

---

## File: content/2.concepts/2.pages.md

---
title: Pages
icon: 'carbon:ibm-cloud-pak-manta-automated-data-lineage'
---

## Extra HTML pages

If you need to declare extra HTML pages beyond those the manifest accommodates,
place them in the Vite config under `build.rollupOptions.input`.

This example includes a welcome page to open when the user installs the
extension.

```javascript [vite.config.ts]
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        welcome: 'pages/welcome.html',
      },
    },
  },
})
```

Vite will serve these input pages during development and include an optimized
version of them in the production build.

---

## File: content/2.concepts/3.background.md

---
title: Background
icon: 'carbon:server-proxy'
---

Add a service worker to your extension in the manifest under
`background.service_worker`. CRXJS uses module-type service workers because Vite
uses the ES module format.

```json [manifest.json]
{
  "background": {
    "service_worker": "src/background.ts",
    "type": "module"
  }
}
```

CRXJS loads the service worker from the Vite Dev Server during development, and
HMR causes a full extension reload when Vite detects a change to the background
code.

Learn more about extension service workers in the
[Chrome Developer Docs](https://developer.chrome.com/docs/extensions/mv3/service_workers/).

---

## File: content/2.concepts/4.content.md

---
title: Content Scripts
icon: 'lucide:code-xml'
---

Since [@crxjs/vite-plugin v2.7.0](https://github.com/crxjs/chrome-extension-tools/releases/tag/vite-plugin-v2.7.0), content script HMR works in both Chrome's isolated world and main world.

::alert{type="warning" icon="lucide:triangle-alert"}
For React projects, use [`@vitejs/plugin-react`](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react). HMR with `@vitejs/plugin-react-swc` is not covered by CRXJS's integration tests and has a [known unresolved report](https://github.com/crxjs/chrome-extension-tools/issues/723). If you choose the SWC plugin, verify both extension-page and content-script HMR in your project.
::

## Execution model

A content script is extension JavaScript that Chrome runs on designated web
pages. The page where it runs is called the **host page**.

By default, Chrome runs content scripts in an isolated world. The content script
and host page share the same DOM, but their JavaScript global scopes and
variables are separate. A content script configured with `world: 'MAIN'` runs in
the host page's JavaScript environment instead. See Chrome's
[content script documentation](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world).

## Declare a content script

Declare static content scripts under `content_scripts` in the manifest. The
required `matches` patterns determine which host pages receive the script.

```ts [manifest.config.ts]
import { defineManifest } from '@crxjs/vite-plugin'

export default defineManifest({
  manifest_version: 3,
  name: 'My Extension',
  version: '1.0.0',
  content_scripts: [{
    js: ['src/content/main.ts'],
    matches: ['https://example.com/*'],
  }],
})
```

## CSS isolation

JavaScript isolation does not isolate CSS. Styles imported by a content script
participate in the host document's cascade: host-page rules can affect the UI
created by your content script, and broad content-script selectors can affect
the host page. Use narrowly scoped selectors or mount complex UI inside a Shadow
DOM when you need a style boundary.

## Static Assets

Feel free to import static assets! CRXJS automatically declares imported content
script dependencies as `web_accessible_resources` in the manifest.

## Use the extension URL

Content scripts share the origin of the host page, so convert imported static
assets to the extension origin using the Chrome API.

```js
import logo from './logo.png'
const url = chrome.runtime.getURL(logo)
```

## IIFE content scripts

CRXJS normally runs content scripts through an async loader. That loader enables
Vite HMR and regular ESM imports, but it can be too late for `document_start`
scripts that must patch host page APIs before page code runs.

Use an IIFE content script for timing-sensitive work like intercepting `fetch`,
`XMLHttpRequest`, DOM events, or globals in the main world. CRXJS emits the
script as a standalone bundle with its imports inlined, and Chrome runs that
file directly.

::alert{type="warning" icon="lucide:triangle-alert"}
IIFE content scripts do not receive in-place Vite HMR. During development CRXJS
rebuilds the standalone file, but the updated script only runs after the host
page reloads or navigates again.
::

### Recommended architecture

Keep most of your extension logic in the default isolated world content script.
It stays separated from page JavaScript, can use the extension APIs available to
content scripts, and receives full Vite HMR in development.

Use a `MAIN` world IIFE only for the part that must run inside the host page
environment. Treat it as a small bridge: install the early hook at
`document_start`, then pass commands or data to the isolated content script with
`window.postMessage`, `CustomEvent`, or DOM events. This keeps the non-HMR script
small and stable.

### Enable IIFE output

::tabs
  ::div{label="File name" icon="lucide:file-code"}
  Add `.iife` before the source extension. This works for
  `.iife.ts`, `.iife.tsx`, `.iife.js`, and `.iife.jsx`.

  ```ts [manifest.config.ts]
  import { defineManifest } from '@crxjs/vite-plugin'

  export default defineManifest({
    manifest_version: 3,
    name: 'My Extension',
    version: '1.0.0',
    content_scripts: [{
      js: ['src/content/interceptor.iife.ts'],
      matches: ['https://example.com/*'],
      run_at: 'document_start',
      world: 'MAIN',
    }],
  })
  ```
  ::

  ::div{label="standaloneFiles" icon="lucide:file-plus-2"}
  If you cannot rename the file, list it in `contentScripts.standaloneFiles`.

  ```ts [vite.config.ts]
  import { crx } from '@crxjs/vite-plugin'
  import { defineConfig } from 'vite'
  import manifest from './manifest.config'

  export default defineConfig({
    plugins: [
      crx({
        manifest,
        contentScripts: {
          standaloneFiles: ['src/content/interceptor.ts'],
        },
      }),
    ],
  })
  ```
  ::

  ::div{label="Dynamic" icon="lucide:zap"}
  For scripts registered with `chrome.scripting`, import the generated file path
  with either `?iife` or the `.iife` filename convention plus `?script`.

  ```ts [background.ts]
  import interceptor from './content/interceptor.ts?iife'
  // import interceptor from './content/interceptor.iife.ts?script'

  chrome.scripting.registerContentScripts([{
    id: 'network-interceptor',
    js: [interceptor],
    matches: ['https://example.com/*'],
    runAt: 'document_start',
    world: 'MAIN',
  }])
  ```
  ::
::

## HTML in content scripts

It is possible to inject an extension page into a host page using an iframe. The
host page CSP does not affect the injected iframe even if the host page
specifies the `frame-src` policy.

An injected extension page loads inside a cross-origin iframe, so it does not
have access to the host page DOM like a content script.

```js [content-script.js]
const src = chrome.runtime.getURL('pages/iframe.html')

const iframe = new DOMParser().parseFromString(
  `<iframe class="crx" src="${src}"></iframe>`,
).body.firstElementChild

document.body.append(iframe)
```

Injected extension pages do have access to the full Chrome API, however.

::stack

::div{class="p-3"}
If you load an HTML file from a content script, you need to declare the file as
a web-accessible resource.
::

```ts [manifest.config.ts] {15-18}
import { defineManifest } from '@crxjs/vite-plugin'
import pkg from './package.json'

export default defineManifest({
  manifest_version: 3,
  name: pkg.name,
  version: pkg.version,
  action: {
    default_popup: 'src/popup/index.html',
  },
  content_scripts: [{
    js: ['src/content/main.ts'],
    matches: ['https://*/*'],
  }],
  web_accessible_resources: [{
      resources: ["pages/iframe.html"],
      matches: ["https://*.google.com/*"]
  }]
})
```

::div{class="p-3"}
You will also need to add the HTML file to your Vite config under
`build.rollupOptions.input`.
::

```js [vite.config.ts]
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        welcome: 'pages/iframe.html',
      },
    },
  },
})
```

::

### Imported HTML

If you need to render complex HTML in a content script without a framework, an
HTML file can serve as a static fragment by importing it as text using the
`?raw` query. This technique does not require the file to be web-accessible, and
you don't need to declare it in the Vite config.

```js
import html from './root.html?raw'

const iframe = new DOMParser().parseFromString(html).body.firstElementChild
iframe.src = chrome.runtime.getURL('pages/iframe.html')

document.body.append(iframe)
```

Importing an HTML file as text lets you take advantage of IDE language services
for HTML files. Depending on your HTML, this technique may be more concise than
using `document.createElement()`.

---

## File: content/1.guide/2.installation/1.create-crxjs.md

---
title: create-crxjs
icon: 'lucide:rocket'
---

## Creating a CRXJS Application

::steps

### Get Starter Template

:pm-x{command="create-crxjs"}

### Navigating to the Project Directory

```bash
cd [project-name]
```

### Install Dependencies

:pm-install

### Development Server

:pm-run{script="dev"}

### Project Structure

::temp-tabs
::

---

## File: content/1.guide/2.installation/2.from-scratch.md

---
title: From Scratch
icon: 'lucide:arrow-down-0-1'
---

## install CRXJS from scratch

::steps

### Create a new project

```shell
mkdir crxjs-project
cd  crxjs-project
```

### Create `package.json`
```json{3} [package.json]
{
  "name": "crxjs-project",
  "type": "module",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}
```

### Install `Vite` and `CRXJS`
:pm-install{name="vite @crxjs/vite-plugin" save-dev}

### Create `index.html`
```html [index.html]
<body>
  <h1>Hello CRXJS</h1>
</body>

<style>
  body {
    width: 160px;
    color: #f3bae5;
    text-align: center;
    background-color: #288cd7;
  }
</style>
```

### Create `manifest.config.js`

```js [manifest.config.js]
import { defineManifest } from '@crxjs/vite-plugin'

export default defineManifest({
  manifest_version: 3,
  name: 'CRXJS from scratch',
  version: '1.0.0',
  action: {
    default_popup: 'index.html',
  },
})
```

### Create `vite.config.js`

```js [vite.config.js]
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.config.js'

export default defineConfig({
  plugins: [
    crx({ manifest }),
  ],
  server: {
    cors: {
      origin: [
        /chrome-extension:\/\//,
      ],
    },
  },
})
```

### Project Structure

::file-tree
---
title: Project Structure
icon: lucide:folder-tree
autoSlash: false
tree:
  - crxjs-project:
    - index.html
    - ^manifest.config.js^
    - package.json
    - vite.config.js
---
::

### Development Server

:pm-run{script="dev"}
::

---

## File: content/1.guide/5.test/1.installation.md

---
title: Quick Start
icon: 'lucide:rocket'
---

## Install Dependencies

::pm-install{name="vitest-environment-web-ext" save-dev}
::

## Configure Vitest

:::code-group

```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
      },
    },
  },
})
```

:::


## Type Definitions

To enable better type checking and IntelliSense, you need to configure the types.

```json [tsconfig.json]
{
  "compilerOptions": {
    "types": ["vitest-environment-web-ext/types"]
  }
}
```



## Update `package.json`
```json{3} [package.json]
{
  "scripts": {
    "test": "vitest"
  }
}
```

## Write Tests

:::code-group

```ts [test/extension.test.ts]
import { expect, test } from 'vitest'

test('popup should display correctly', async () => {
  const popupPage = await browser.getPopupPage()

  const title = await popupPage.title()
  expect(title).toBeTruthy()
})
```

:::

## Run Tests

::pm-run{script="test"}
::

---

## File: content/1.guide/5.test/2.options.md

---
title: Configuration Options
icon: 'lucide:settings'
---

## Basic Configuration

### path

- Type: `string`
- Required: Yes

The path to the browser extension file or directory. Can be a `.crx`, `.xpi` file, or a directory containing the extension manifest.

```ts [vitest.config.ts]
export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
      },
    },
  },
})
```

### compiler

- Type: `string | false`
- Default: `false`

The build command to execute before running tests.

```ts [vitest.config.ts]
export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
        compiler: 'npm run build',
      },
    },
  },
})
```

Set to `false` to disable compilation.

### autoLaunch :badge[Advanced]{}


- Type: `boolean`
- Default: `true`

Whether to automatically load and launch the browser extension.

```ts [vitest.config.ts]
export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
        autoLaunch: false,
      },
    },
  },
})
```
When set to `false`, you need to manually load the extension.
```ts [test/extension.test.ts]
import { beforeAll, describe, expect, it } from 'vitest'

describe('browser extension test', () => {
  beforeAll(async () => {
    browser.loadExtension('./dist')
    await browser.launch()
  })

  it('popup should display correctly', async () => {
    const popupPage = await browser.getPopupPage()

    const title = await popupPage.title()
    expect(title).toBeTruthy()
  })
})
```

## Playwright Configuration

### playwright.slowMo

- Type: `number`
- Default: `100`

Slows down Playwright operations by the specified milliseconds. Useful for debugging tests to observe operations more clearly.

```ts [vitest.config.ts]
export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
        playwright: {
          slowMo: 500,
        },
      },
    },
  },
})
```

### playwright.userDataDir

- Type: `string | boolean`
- Default: `false`

Browser user data directory cache path.

- `true`: Use default path `./.vitest-web-ext-cache`
- `string`: Use custom path
- `false`: Disable caching

```ts [vitest.config.ts]
export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
        playwright: {
          userDataDir: './.vitest-cache',
        },
      },
    },
  },
})
```

## Complete Example

```ts [vitest.config.ts]
export default defineConfig({
  test: {
    environment: 'web-ext',
    environmentOptions: {
      'web-ext': {
        path: './dist',
        compiler: 'npm run build',
        autoLaunch: true,
        playwright: {
          slowMo: 100,
          userDataDir: true,
        },
      },
    },
  },
})
```

---

## File: content/1.guide/5.test/3.api.md

---
title: API
icon: 'lucide:code'
---

## Overview

vitest-environment-web-ext injects some convenient methods into the test context, allowing you to quickly access various parts of the browser extension.

## context

The Playwright context of the test environment, which can be used to create custom pages or perform more low-level operations.

```ts [test/extension.test.ts]
import { expect, test } from 'vitest'

test('custom test', async () => {
  const newPage = await context.newPage()
  await newPage.goto('https://example.com')
  expect(await newPage.title()).toBe('Example')
})
```

## browser

### getPopupPage()

Gets the popup page of the browser extension.

```ts [test/extension.test.ts]
import { expect, test } from 'vitest'

test('popup should display correctly', async () => {
  const popupPage = await browser.getPopupPage()
  
  const title = await popupPage.title()
  expect(title).toBeTruthy()
  
  const content = await popupPage.textContent('body')
  expect(content).toContain('Hello')
})
```

### getSidePanelPage()

Gets the side panel page of the browser extension.

```ts [test/extension.test.ts]
import { expect, test } from 'vitest'

test('side panel should display correctly', async () => {
  const sidePanelPage = await browser.getSidePanelPage()
  
  const title = await sidePanelPage.title()
  expect(title).toBeTruthy()
})
```

### getManifest()

Gets the manifest.json content of the browser extension.

```ts [test/extension.test.ts]
import { expect, test } from 'vitest'

test('manifest should contain correct configuration', async () => {
  const manifest = await browser.getManifest()
  
  expect(manifest.name).toBe('My Extension')
  expect(manifest.version).toBe('1.0.0')
  expect(manifest.manifest_version).toBe(3)
})
```

::tip
If you have other needs, feel free to submit an [Issue](https://github.com/crxjs/vitest-environment-web-ext/issues) or Pull Request to help improve this project.
::

---

## File: content/1.guide/1.introduction.md

---
title: Introduction
icon: 'lucide:info'
---

CRXJS is committed to providing developers with an efficient, modern browser extension development experience. By directly parsing the manifest.json file, it enables a zero-configuration development workflow and supports global Hot Module Replacement (HMR), including content scripts in both the isolated world and main world since [@crxjs/vite-plugin v2.7.0](https://github.com/crxjs/chrome-extension-tools/releases/tag/vite-plugin-v2.7.0).
CRXJS is compatible with major frontend frameworks and suitable for various technology stacks, helping developers focus on feature implementation without worrying about tedious build and configuration details.

## Background and Motivation

With the rapid development of Web technologies, developers have higher requirements for the usability and efficiency of toolchains. While modern build tools like Vite and Webpack have greatly improved the web application development experience, the browser extension development field still has the following pain points:

- Complex configuration and excessive boilerplate code
- Limited hot-reload support for content scripts and page scripts
- Inflexible integration with different frontend frameworks
- Separation of build process from mainstream web application development
CRXJS provides a unified, concise and efficient solution to the above problems.

## Core Features

- **Hot Module Replacement (HMR)**

Supports global hot updates for page scripts and for content scripts in both the isolated world and main world since [@crxjs/vite-plugin v2.7.0](https://github.com/crxjs/chrome-extension-tools/releases/tag/vite-plugin-v2.7.0). During development, code changes can be reflected immediately without manual refresh or extension reload, significantly improving development efficiency.

- **Zero Configuration Development**

With [manifest.json](/concepts/manifest) as the only configuration entry, it automatically identifies and packages all extension resources. No need to manually maintain complex build scripts, lowering the entry barrier.

- **Framework Agnostic**

Compatible with various frontend frameworks such as React, Vue, Svelte, SolidJS and vanilla JavaScript, adapting to different projects and team technology choices.

- **Modern Development Experience**

Based on Vite, integrating modern frontend development features such as fast build, module hot replacement, and static resource import, making the extension development process consistent with mainstream web application development.

### Community and Support

CRXJS is an open-source project with an active community, and contributions are welcome. You can interact with the community through the following channels:

- [GitHub Repository](https://github.com/crxjs/chrome-extension-tools)
- [Join Discord Community](https://discord.com/invite/FnnE4XR7Wj)
- [Submit Issues or Suggestions](https://github.com/crxjs/chrome-extension-tools/issues)
- [Contribute](https://github.com/crxjs/chrome-extension-tools/tree/main?tab=readme-ov-file#-development)

Thank you for your participation and support!

---

## File: content/1.guide/3.loading.md

---
title: Loading
icon: 'lucide:hard-drive-upload'
---

## Loading your extension in the browser
::tabs
  ::div{label="Chrome" icon="logos:chrome"}
    ::steps
      ### Development Server
      :pm-run{script="dev"}

      ### Open Browser
      
      
        Open Chrome and navigate to 
        
        ::CopyText{text="chrome://extensions"}
        ::
      

      ### Open Developer Mode
      

      ### Load Unpacked
      Click on the "Load unpacked" button and select the `dist` directory of your project.
    ::
  ::

  ::div{label="Edge" icon="logos:microsoft-edge"}
    ::steps
      ### Development Server
      :pm-run{script="dev"}
      
      ### Open Browser
      
      
        Open Edge and navigate to

        ::CopyText{text="edge://extensions"}
        ::
      
      
      ### Open Developer Mode
      

      ### Load Unpacked
      Click on the "Load unpacked" button and select the `dist` directory of your project.
    ::
  ::
::

## Developing the popup

An extension popup normally closes when it loses focus. To keep it open while
editing, right-click the extension's toolbar icon and select **Inspect popup**.
The popup stays open while its DevTools window is attached.

Without a framework-specific HMR handler, changes to the popup's HTML or
JavaScript reload the extension page. Imported CSS can update in place without a
full reload, which lets the current page state remain intact. Framework plugins
such as React or Vue can provide more granular component updates.