``` ├── .codecov.yml ├── .commitlintrc.yml ├── .gitattributes ├── .github/ ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE/ ├── bug_report.yml ├── config.yml ├── feature_request.md ├── actions/ ├── setup/ ├── action.yml ├── dependabot.yml ├── pull_request_template.md ├── workflows/ ├── pkg.pr.new.yml ├── pr-closed.yml ├── pr-title.yml ├── publish-docs.yml ├── release.yml ├── sync-releases.yml ├── update-browser-package.yml ├── validate.yml ├── vhs.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── .vscode/ ├── extensions.json ├── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ ├── .vitepress/ ├── Dockerfile ├── components/ ├── BlogHome.vue ├── BlogLayout.vue ├── BlogPostPreview.vue ├── EntrypointPatterns.vue ├── ExampleSearch.vue ├── ExampleSearchFilterByItem.vue ├── ExampleSearchResult.vue ├── Icon.vue ├── UsingWxtSection.vue ├── composables/ ├── useBlogDate.ts ├── useListExtensionDetails.ts ├── config.ts ├── loaders/ ├── blog.data.ts ├── cli.data.ts ├── theme/ ├── custom.css ├── index.ts ├── utils/ ├── head.ts ├── menus.ts ├── types.ts ├── analytics.md ├── api/ ├── cli/ ├── wxt-build.md ├── wxt-clean.md ├── wxt-init.md ├── wxt-prepare.md ├── wxt-submit-init.md ├── wxt-submit.md ├── wxt-zip.md ├── wxt.md ├── assets/ ├── cli-output.png ├── init-demo.gif ├── auto-icons.md ├── blog.md ├── blog/ ├── .drafts/ ├── 2024-10-19-real-world-messaging.md ├── 2024-12-06-using-imports-module.md ├── examples.md ├── guide/ ├── essentials/ ├── assets.md ├── config/ ├── auto-imports.md ├── browser-startup.md ├── build-mode.md ├── entrypoint-loaders.md ├── environment-variables.md ├── hooks.md ├── manifest.md ├── runtime.md ├── typescript.md ├── vite.md ├── content-scripts.md ├── e2e-testing.md ├── entrypoints.md ├── es-modules.md ├── extension-apis.md ├── frontend-frameworks.md ├── i18n.md ├── messaging.md ├── project-structure.md ├── publishing.md ├── remote-code.md ├── scripting.md ├── storage.md ├── target-different-browsers.md ├── testing-updates.md ├── unit-testing.md ├── wxt-modules.md ├── installation.md ├── introduction.md ├── resources/ ├── community.md ├── compare.md ├── faq.md ├── how-wxt-works.md ├── migrate.md ├── upgrading.md ├── i18n.md ├── index.md ├── public/ ├── _redirects ``` ## /.codecov.yml ```yml path="/.codecov.yml" coverage: status: project: default: informational: true patch: default: informational: true ``` ## /.commitlintrc.yml ```yml path="/.commitlintrc.yml" extends: - '@commitlint/config-conventional' rules: subject-case: - 0 - always - - sentence-case - start-case - pascal-case - upper-case ``` ## /.gitattributes ```gitattributes path="/.gitattributes" # See https://git-scm.com/docs/gitattributes#_pattern_format for more about `.gitattributes`. # Normalize EOL for all files that Git considers text files * text=auto eol=lf # Mark lock files as generated to avoid diffing pnpm-lock.yaml linguist-generated package-lock.json linguist-generated bun.lockb linguist-generated yarn.lock linguist-generated # Exclude templates from language statistics templates/**/* linguist-vendored # Other generated files packages/browser/src/gen/** linguist-generated ``` ## /.github/CODEOWNERS ```github/CODEOWNERS path="/.github/CODEOWNERS" # Set default * @aklinker1 @Timeraa # Secure Directories /.github/ @aklinker1 # Creator of specific wxt modules /packages/auto-icons/ @Timeraa /packages/unocss/ @Timeraa ``` ## /.github/FUNDING.yml ```yml path="/.github/FUNDING.yml" # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository#about-funding-files github: wxt-dev ``` ## /.github/ISSUE_TEMPLATE/bug_report.yml ```yml path="/.github/ISSUE_TEMPLATE/bug_report.yml" name: "\U0001F41E Bug report" description: Report an issue with WXT labels: [pending-triage] type: Bug body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: bug-description attributes: label: Describe the bug description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! placeholder: I am doing ... What I expect is ... What actually happening is ... validations: required: true - type: textarea id: reproduction attributes: label: Reproduction description: | Please provide a minimal reproduction. This can include: - A PR with a failing test case - A link to a github repo - A ZIP you upload to this issue A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required ([Why?](https://antfu.me/posts/why-reproductions-are-required)). If a report is vague (e.g. just a generic error message) and has no reproduction or a partial reproduction, it will be closed immediately and labeled with as "needs-reproduction". Once a reproduction is provided, it will be re-opened. placeholder: Reproduction URL or attach a ZIP validations: required: true - type: textarea id: reproduction-steps attributes: label: Steps to reproduce description: Please provide any reproduction steps that may need to be described. E.g. if it happens only when running the dev or build script make sure it's clear which one to use. placeholder: Run `npm install` followed by `npm run dev` - type: textarea id: system-info attributes: label: System Info description: Output of `npx envinfo --system --browsers --binaries --npmPackages wxt,vite` render: shell placeholder: System, Binaries, Browsers validations: required: true - type: dropdown id: package-manager attributes: label: Used Package Manager description: Select the used package manager options: - npm - yarn - pnpm - bun validations: required: true - type: checkboxes id: checkboxes attributes: label: Validations description: Before submitting the issue, please make sure you do the following options: - label: Read the [Contributing Guidelines](https://github.com/wxt-dev/wxt/blob/main/CONTRIBUTING.md). required: true - label: Read the [docs](https://wxt.dev/guide/installation.html). required: true - label: Check that there isn't [already an issue](https://github.com/wxt-dev/wxt/issues) that reports the same bug to avoid creating a duplicate. required: true - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/wxt-dev/wxt/discussions) or join our [Discord Chat Server](https://discord.gg/ZFsZqGery9). required: true - label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug. required: true ``` ## /.github/ISSUE_TEMPLATE/config.yml ```yml path="/.github/ISSUE_TEMPLATE/config.yml" blank_issues_enabled: false contact_links: - name: Discord Chat url: https://discord.gg/ZFsZqGery9 about: Ask questions and discuss with other WXT users in real time. - name: Questions & Discussions url: https://github.com/wxt-dev/wxt/discussions about: Use GitHub discussions for message-board style questions and discussions. ``` ## /.github/ISSUE_TEMPLATE/feature_request.md --- name: Feature request about: Suggest an idea for WXT title: '' type: Feature assignees: '' --- ### Feature Request Please describe your feature, be clear and concise. If you have a proposal for required type or API changes, list them here. #### Is your feature request related to a bug? If so, add a link here. If not, write "N/A" ### What are the alternatives? A clear and concise description of any alternative solutions or features you've considered. ### Additional context Add any other context or screenshots about the feature request here. ## /.github/actions/setup/action.yml ```yml path="/.github/actions/setup/action.yml" name: Basic Setup description: Install PNPM, Node, and dependencies inputs: install: default: 'true' type: boolean description: Whether or not to run 'pnpm install' installArgs: default: '' type: string description: Additional args to append to "pnpm install" runs: using: composite steps: - name: 🛠️ Setup PNPM uses: pnpm/action-setup@v4 - name: 🛠️ Setup NodeJS uses: actions/setup-node@v4 with: node-version: 18 cache: pnpm - name: 📦 Install Dependencies if: ${{ inputs.install == 'true' }} shell: bash run: pnpm install ${{ inputs.installArgs }} ``` ## /.github/dependabot.yml ```yml path="/.github/dependabot.yml" # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: 'monthly' - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'monthly' ``` ## /.github/pull_request_template.md ### Overview ### Manual Testing ### Related Issue This PR closes # ## /.github/workflows/pkg.pr.new.yml ```yml path="/.github/workflows/pkg.pr.new.yml" name: ✨ pkg.pr.new on: push: branches: - main pull_request: branches: - main permissions: contents: read jobs: build: name: Publish Test Packages runs-on: ubuntu-22.04 if: ${{ github.repository == 'wxt-dev/wxt' }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Build All Packages run: pnpm buildc all - name: Publish run: pnpx pkg-pr-new publish --compact --pnpm './packages/*' ``` ## /.github/workflows/pr-closed.yml ```yml path="/.github/workflows/pr-closed.yml" name: 🎉 PR closed on: pull_request_target: types: - closed permissions: contents: read pull-requests: write jobs: thank-you: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - name: Post Thank You Comment uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: comment: Thanks for helping make WXT better! with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: process.env.comment }) ``` ## /.github/workflows/pr-title.yml ```yml path="/.github/workflows/pr-title.yml" name: 🛡️ Check PR Title on: pull_request: types: [opened, edited] jobs: lint-pr-title: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Only fetch the config file from the repository sparse-checkout-cone-mode: false sparse-checkout: .commitlintrc.yml - name: Install dependencies run: npm install --global @commitlint/config-conventional commitlint - name: Check PR title with commitlint env: PR_TITLE: ${{ github.event.pull_request.title }} HELP_URL: https://github.com/wxt-dev/wxt/blob/main/CONTRIBUTING.md#conventional-pr-titles run: echo "$PR_TITLE" | npx commitlint --help-url $HELP_URL ``` ## /.github/workflows/publish-docs.yml ```yml path="/.github/workflows/publish-docs.yml" name: 📝 Publish Docs on: push: branches: - main workflow_dispatch: inputs: tag: description: Docker Image Tag required: true default: latest permissions: contents: read jobs: publish: # Only run if it's the upstream repository, not forks if: github.repository == 'wxt-dev/wxt' name: Publish Docs runs-on: ubuntu-22.04 permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Login to Docker Registry uses: docker/login-action@v3 with: registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }} username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} - name: Build docs run: | pnpm docs:build docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }} - name: Push Image run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }} - name: Deploy run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }} ``` ## /.github/workflows/release.yml ```yml path="/.github/workflows/release.yml" name: 🚀 Release on: workflow_dispatch: inputs: package: description: Package to release default: wxt type: choice options: - analytics - auto-icons - i18n - module-react - module-solid - module-svelte - module-vue - storage - unocss - webextension-polyfill - wxt permissions: contents: read jobs: validate: name: Validate uses: './.github/workflows/validate.yml' secrets: inherit publish: name: Publish runs-on: ubuntu-22.04 permissions: contents: write needs: - validate steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup uses: ./.github/actions/setup - name: Configure Git run: | git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' git config --global push.followTags true - name: Bump and Tag run: | pnpm tsx scripts/bump-package-version.ts ${{ inputs.package }} git push git push --tags - name: Publish to NPM working-directory: packages/${{ inputs.package }} run: | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc pnpm build pnpm publish - name: Create GitHub release run: pnpm tsx scripts/create-github-release.ts ${{ inputs.package }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## /.github/workflows/sync-releases.yml ```yml path="/.github/workflows/sync-releases.yml" name: 🔄 Sync Releases on: workflow_dispatch: inputs: package: description: Package to sync default: wxt type: choice options: - analytics - auto-icons - i18n - module-react - module-solid - module-svelte - module-vue - storage - webextension-polyfill - wxt permissions: contents: read jobs: sync: name: Sync Releases runs-on: ubuntu-22.04 permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup with: installArgs: --ignore-scripts - name: Sync Releases run: pnpm tsx scripts/sync-releases.ts ${{ inputs.package }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## /.github/workflows/update-browser-package.yml ```yml path="/.github/workflows/update-browser-package.yml" name: 🔄 Update @wxt-dev/browser on: workflow_dispatch: schedule: - cron: '0 0 * * *' # Every day at midnight permissions: contents: read jobs: sync: name: 'Sync with @types/chrome' runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup with: installArgs: --ignore-scripts - name: Generate Latest Code working-directory: packages/browser run: pnpm gen - name: Run Checks working-directory: packages/browser run: pnpm check - name: Commit Changes id: commit uses: stefanzweifel/git-auto-commit-action@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: commit_message: 'fix: Upgrade `@wxt-dev/browser` to latest `@types/chrome` version' - name: Publish Package if: steps.commit.outputs.changes_detected == 'true' working-directory: packages/browser run: | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc pnpm publish ``` ## /.github/workflows/validate.yml ```yml path="/.github/workflows/validate.yml" name: 🛡️ Validate on: workflow_call: pull_request: push: branches: - main permissions: contents: read jobs: checks: name: Checks runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Basic Checks run: pnpm check builds: name: Builds runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Build All Packages run: pnpm buildc all build-demo: name: Build Demo runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Build run: pnpm build:all working-directory: packages/wxt-demo - name: ZIP run: pnpm wxt zip working-directory: packages/wxt-demo tests: name: Tests runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Setup Bun uses: oven-sh/setup-bun@v2 - name: Run Tests run: pnpm test:coverage -- --reporter=default --reporter=hanging-process - name: Upload Coverage uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} windows-tests: name: Windows Tests runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Run Tests run: pnpm test template: name: Template runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: template: - react - solid - svelte - vanilla - vue steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Pack WXT package run: pnpm pack working-directory: packages/wxt - name: Install Dependencies run: npm i working-directory: templates/${{ matrix.template }} - name: Install Packed WXT run: npm i -D ../../packages/wxt/wxt-*.tgz working-directory: templates/${{ matrix.template }} - name: Type Check Template run: pnpm compile if: matrix.template != 'svelte' working-directory: templates/${{ matrix.template }} - name: Type Check Template run: pnpm check if: matrix.template == 'svelte' working-directory: templates/${{ matrix.template }} - name: Build Template run: pnpm build working-directory: templates/${{ matrix.template }} ``` ## /.github/workflows/vhs.yml ```yml path="/.github/workflows/vhs.yml" name: 📼 VHS on: push: paths: - 'docs/tapes/*.tape' workflow_dispatch: permissions: contents: read jobs: vhs: name: Create VHS runs-on: ubuntu-22.04 if: ${{ github.repository == 'wxt-dev/wxt' }} permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup with: install: false # This prevents pnpm dlx from downloading WXT in the video - name: Pre-install WXT run: | pnpm store add wxt@latest pnpm dlx wxt@latest --version - name: Record VHS uses: charmbracelet/vhs-action@v2.1.0 with: path: 'docs/tapes/init-demo.tape' - name: Save recorded GIF uses: stefanzweifel/git-auto-commit-action@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: commit_message: 'docs: Update `wxt init` GIF' # https://github.com/charmbracelet/vhs#output file_pattern: 'docs/assets/*.gif' ``` ## /.gitignore ```gitignore path="/.gitignore" .DS_Store .env .env.* .idea .output .webextrc .wxt *.log /docs/.vitepress/cache docs/.vitepress/.temp coverage dist node_modules TODOs.md web-ext.config.js web-ext.config.ts templates/*/pnpm-lock.yaml templates/*/yarn.lock templates/*/package-lock.json docs/api/reference stats.html .tool-versions .cache *-stats.txt ``` ## /.prettierignore ```prettierignore path="/.prettierignore" .output coverage dist .wxt docs/.vitepress/cache pnpm-lock.yaml CHANGELOG.md packages/browser/src/gen ``` ## /.prettierrc.yml ```yml path="/.prettierrc.yml" singleQuote: true endOfLine: lf ``` ## /.vscode/extensions.json ```json path="/.vscode/extensions.json" { "recommendations": [ "davidanson.vscode-markdownlint", "esbenp.prettier-vscode", "github.vscode-github-actions" ] } ``` ## /.vscode/settings.json ```json path="/.vscode/settings.json" { // Set default formatter "editor.defaultFormatter": "esbenp.prettier-vscode", "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, // Additional guidelines for Copilot "github.copilot.chat.codeGeneration.instructions": [ { "file": "CONTRIBUTING.md" } ] } ``` ## /CODE_OF_CONDUCT.md # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at aaronklinker1@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ## /CONTRIBUTING.md # Contributing Everyone is welcome to contribute to **WXT**! If you are changing the docs or fixing a bug, feel free to fork and open a PR. If you want to add a new feature, please create an issue or discussion first so we can decide if the feature is inline with the vision for WXT. ## Conventional Commits This project uses [Conventional Commit format](https://www.conventionalcommits.org/en/v1.0.0/) to automatically generate a changelog and better understand the changes in the project Here are some examples of conventional commit messages: - `feat: add new functionality` - `fix: correct typos in code` - `ci: add GitHub Actions for automated testing` ## Conventional PR Titles The title of your pull request should follow the [conventional commit format](#conventional-commits). When a pull request is merged to the main branch, all changes are going to be squashed into a single commit. The message of this commit will be the title of the pull request. And for every release, the commit messages are used to generate the changelog. ## Setup WXT uses `pnpm`, so make sure you have it installed. ```sh corepack enable ``` Then, simply run the install command: ```sh pnpm i ``` ## Development Here are some helpful commands: ```sh # Build WXT package cd packages/wxt pnpm build ``` ```sh # Build WXT package, then build demo extension cd packages/wxt-demo pnpm build ``` ```sh # Build WXT package, then start the demo extension in dev mode cd packages/wxt-demo pnpm dev ``` ```sh # Run unit and E2E tests pnpm test ``` ```sh # Start the docs website locally pnpm docs:dev ``` ## Profiling ```sh # Build the latest version pnpm --filter wxt build # CD to the demo directory cd packages/wxt-demo # 1. Generate a flamechart with 0x pnpm dlx 0x node_modules/wxt/bin/wxt.mjs build # 2. Inspect the process with chrome @ chrome://inspect pnpm node --inspect node_modules/wxt/bin/wxt.mjs build ``` ## Updating Docs Documentation is written with VitePress, and is located in the `docs/` directory. The API reference is generated from JSDoc comments in the source code. If there's a typo or change you want to make in there, you'll need to update the source code instead of a file in the `docs/` directory. ## Testing WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones, if they exist. > If they don't exist, feel free to create them, but that's a lot for a one-time contributor. A maintainer might add them to your PR though. To run tests for a specific file, add the filename at the end of the test command: ```sh pnpm test manifest-contents ``` All test (unit and E2E) for all packages are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support). If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR. ## Templates Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands ```sh cd templates/vue npm i npm run dev npm run build ``` Note that templates are hardcoded to a specific version of `wxt` from NPM, they do not use the local version. PR checks will test your PR's changes against the templates, but if you want to manually do it, update the package.json dependency: ```diff "devDependencies": { "typescript": "^5.3.2", "vite-plugin-solid": "^2.7.0", - "wxt": "^0.8.0" + "wxt": "../.." } ``` Then run `npm i` again. ### Adding Templates To add a template, copy the vanilla template and give it a new name. ```sh cp -r templates/vanilla templates/ ``` That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template. ## Releasing Updates Releases are done with GitHub actions: - Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release. - Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo. ## Upgrading Dependencies Use [`taze`](https://www.npmjs.com/package/taze) to upgrade dependencies throughout the entire monorepo. ```sh pnpm dlx taze -r ``` Configuration is in [`taze.config.ts`](./taze.config.ts). ## Install Unreleased Versions This repo uses https://pkg.pr.new to publish versions of all it's packages for almost every commit. You can install them via: ```sh npm i https://pkg.pr.new/[package-name]@[ref] ``` Or use one of the shorthands: ```sh # Install the latest build of `wxt` from a PR: npm i https://pkg.pr.new/wxt@1283 # Install the latest build of `@wxt-dev/module-react` on the `main` branch npm i https://pkg.pr.new/@wxt-dev/module-react@main # Install `@wxt-dev/storage` from a specific commit: npm i https://pkg.pr.new/@wxt-dev/module-react@426f907 ``` ## Blog Posts Anyone is welcome to submit a blog post on https://wxt.dev/blog! > [!NOTE] > Before starting on a blog post, please message Aaron on Discord or start a discussion on GitHub to get permission to write about a topic, but most topics are welcome: Major version updates, tutorials, etc. - **English only**: Blog posts should be written in English. Unfortunately, our maintainers doesn't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓 - **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that. ## /LICENSE ``` path="/LICENSE" MIT License Copyright (c) 2023 Aaron Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## /README.md

WXT Logo WXT

npm version downloads license | MIT coverage

Next-gen framework for developing web extensions.

It's like Nuxt, but for Web Extensions

Get StartedConfigurationExamplesChangelogDiscord

![Example CLI Output](https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/assets/cli-output.png) ## Demo https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94 ## Quick Start Bootstrap a new project: ```sh # npm npx wxt@latest init # pnpm pnpm dlx wxt@latest init # bun bunx wxt@latest init ``` Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT. ## Features - 🌐 Supports all browsers - ✅ Supports both MV2 and MV3 - ⚡ Dev mode with HMR & fast reload - 📂 File based entrypoints - 🚔 TypeScript - 🦾 Auto-imports - 🤖 Automated publishing - 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc - 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions - 🖍️ Quickly bootstrap a new project - 📏 Bundle analysis - ⬇️ Download and bundle remote URL imports ## Sponsors WXT is a [MIT-licensed](https://github.com/wxt-dev/wxt/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring WXT's development](https://github.com/sponsors/wxt-dev). WXT Sponsors ## Contributors Published under the [MIT](https://github.com/wxt-dev/wxt/blob/main/LICENSE) license. Made by [@aklinker1](https://github.com/aklinker1) and [community](https://github.com/wxt-dev/wxt/graphs/contributors) 💛

## /SECURITY.md # Security Policy While WXT is in prerelease, only the latest version will receive security updates. The latest version is: npm version ## /docs/.vitepress/Dockerfile ```vitepress/Dockerfile path="/docs/.vitepress/Dockerfile" FROM lipanski/docker-static-website:latest COPY dist . ``` ## /docs/.vitepress/components/BlogHome.vue ```vue path="/docs/.vitepress/components/BlogHome.vue" ``` ## /docs/.vitepress/components/BlogLayout.vue ```vue path="/docs/.vitepress/components/BlogLayout.vue" ``` ## /docs/.vitepress/components/BlogPostPreview.vue ```vue path="/docs/.vitepress/components/BlogPostPreview.vue" ``` ## /docs/.vitepress/components/EntrypointPatterns.vue ```vue path="/docs/.vitepress/components/EntrypointPatterns.vue" ``` ## /docs/.vitepress/components/ExampleSearch.vue ```vue path="/docs/.vitepress/components/ExampleSearch.vue" ``` ## /docs/.vitepress/components/ExampleSearchFilterByItem.vue ```vue path="/docs/.vitepress/components/ExampleSearchFilterByItem.vue" ``` ## /docs/.vitepress/components/ExampleSearchResult.vue ```vue path="/docs/.vitepress/components/ExampleSearchResult.vue" ``` ## /docs/.vitepress/components/Icon.vue ```vue path="/docs/.vitepress/components/Icon.vue" ``` ## /docs/.vitepress/components/UsingWxtSection.vue ```vue path="/docs/.vitepress/components/UsingWxtSection.vue" ``` ## /docs/.vitepress/composables/useBlogDate.ts ```ts path="/docs/.vitepress/composables/useBlogDate.ts" import { computed, toValue, MaybeRefOrGetter } from 'vue'; const MONTH_FORMATTER = new Intl.DateTimeFormat( globalThis?.navigator?.language, { month: 'long', }, ); export default function (date: MaybeRefOrGetter) { return computed(() => { const d = new Date(toValue(date)); return `${MONTH_FORMATTER.format(d)} ${d.getDate()}, ${d.getFullYear()}`; }); } ``` ## /docs/.vitepress/composables/useListExtensionDetails.ts ```ts path="/docs/.vitepress/composables/useListExtensionDetails.ts" import { ref } from 'vue'; export interface ChromeExtension { id: string; name: string; iconUrl: string; weeklyActiveUsers: number; shortDescription: string; storeUrl: string; rating: number | undefined; } const operationName = 'WxtDocsUsedBy'; const query = `query ${operationName}($ids:[String!]!) { chromeExtensions(ids: $ids) { id name iconUrl weeklyActiveUsers shortDescription storeUrl rating } }`; export default function (ids: string[]) { const data = ref(); const err = ref(); const isLoading = ref(true); fetch('https://queue.wxt.dev/api', { method: 'POST', body: JSON.stringify({ operationName, query, variables: { ids }, }), }) .then(async (res) => { isLoading.value = false; const { data: { chromeExtensions }, } = await res.json(); data.value = chromeExtensions; err.value = undefined; }) .catch((error) => { isLoading.value = false; console.error(error); data.value = undefined; err.value = error; }); return { data, err, isLoading, }; } ``` ## /docs/.vitepress/config.ts ```ts path="/docs/.vitepress/config.ts" import { DefaultTheme, defineConfig } from 'vitepress'; import typedocSidebar from '../api/reference/typedoc-sidebar.json'; import { menuGroup, menuItem, menuRoot, navItem, prepareTypedocSidebar, } from './utils/menus'; import { meta, script } from './utils/head'; import footnote from 'markdown-it-footnote'; import { version as wxtVersion } from '../../packages/wxt/package.json'; import { version as i18nVersion } from '../../packages/i18n/package.json'; import { version as autoIconsVersion } from '../../packages/auto-icons/package.json'; import { version as unocssVersion } from '../../packages/unocss/package.json'; import { version as storageVersion } from '../../packages/storage/package.json'; import { version as analyticsVersion } from '../../packages/analytics/package.json'; import addKnowledge from 'vitepress-knowledge'; import { groupIconMdPlugin, groupIconVitePlugin, localIconLoader, } from 'vitepress-plugin-group-icons'; import { Feed } from 'feed'; import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const origin = 'https://wxt.dev'; const title = 'Next-gen Web Extension Framework'; const titleSuffix = ' – WXT'; const description = "WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started."; const ogTitle = `${title}${titleSuffix}`; const ogUrl = origin; const ogImage = `${origin}/social-preview.png`; const otherPackages = { analytics: analyticsVersion, 'auto-icons': autoIconsVersion, i18n: i18nVersion, storage: storageVersion, unocss: unocssVersion, }; const knowledge = addKnowledge({ serverUrl: 'https://knowledge.wxt.dev', paths: { '/': 'docs', '/api/': 'api-reference', '/blog/': 'blog', }, layoutSelectors: { blog: '.container-content', }, pageSelectors: { 'examples.md': '#VPContent > .VPPage', 'blog.md': '#VPContent > .VPPage', }, }); // https://vitepress.dev/reference/site-config export default defineConfig({ extends: knowledge, titleTemplate: `:title${titleSuffix}`, title: 'WXT', description, vite: { clearScreen: false, plugins: [ groupIconVitePlugin({ customIcon: { 'wxt.config.ts': localIconLoader( import.meta.url, '../public/logo.svg', ), }, }), ], }, lastUpdated: true, sitemap: { hostname: origin, }, async buildEnd(site) { // @ts-expect-error: knowledge.buildEnd is not typed, but it exists. await knowledge.buildEnd(site); // Only construct the RSS document for production builds const { default: blogDataLoader } = await import('./loaders/blog.data'); const posts = await blogDataLoader.load(); const feed = new Feed({ copyright: 'MIT', id: 'wxt', title: 'WXT Blog', link: `${origin}/blog`, }); posts.forEach((post) => { feed.addItem({ date: post.frontmatter.date, link: new URL(post.url, origin).href, title: post.frontmatter.title, description: post.frontmatter.description, }); }); // console.log('rss.xml:'); // console.log(feed.rss2()); await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8'); }, head: [ meta('og:type', 'website'), meta('og:title', ogTitle), meta('og:image', ogImage), meta('og:url', ogUrl), meta('og:description', description), meta('twitter:card', 'summary_large_image', { useName: true }), script('https://umami.aklinker1.io/script.js', { 'data-website-id': 'c1840c18-a12c-4a45-a848-55ae85ef7915', async: '', }), ], markdown: { config: (md) => { md.use(footnote); md.use(groupIconMdPlugin); }, languageAlias: { mjs: 'js', }, }, themeConfig: { // https://vitepress.dev/reference/default-theme-config logo: { src: '/logo.svg', alt: 'WXT logo', }, footer: { message: [ ' Deploys by Netlify', ' Deploys by Netlify', 'Released under the MIT License.', ].join('
'), copyright: 'Copyright © 2023-present Aaron Klinker', }, editLink: { pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path', }, search: { provider: 'local', }, socialLinks: [ { icon: 'discord', link: 'https://discord.gg/ZFsZqGery9' }, { icon: 'github', link: 'https://github.com/wxt-dev/wxt' }, ], nav: [ navItem('Guide', '/guide/installation'), navItem('Examples', '/examples'), navItem('API', '/api/reference/wxt'), navItem('Blog', '/blog'), navItem(`v${wxtVersion}`, [ navItem('wxt', [ navItem(`v${wxtVersion}`, '/'), navItem( `Changelog`, 'https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md', ), ]), navItem( 'Other Packages', Object.entries(otherPackages).map(([name, version]) => navItem(`@wxt-dev/${name} — ${version}`, `/${name}`), ), ), ]), ], sidebar: { '/guide/': menuRoot([ menuGroup('Get Started', '/guide/', [ menuItem('Introduction', 'introduction.md'), menuItem('Installation', 'installation.md'), ]), menuGroup('Essentials', '/guide/essentials/', [ menuItem('Project Structure', 'project-structure.md'), menuItem('Entrypoints', 'entrypoints.md'), menuGroup( 'Configuration', '/guide/essentials/config/', [ menuItem('Manifest', 'manifest.md'), menuItem('Browser Startup', 'browser-startup.md'), menuItem('Auto-imports', 'auto-imports.md'), menuItem('Environment Variables', 'environment-variables.md'), menuItem('Runtime Config', 'runtime.md'), menuItem('Vite', 'vite.md'), menuItem('Build Mode', 'build-mode.md'), menuItem('TypeScript', 'typescript.md'), menuItem('Hooks', 'hooks.md'), menuItem('Entrypoint Loaders', 'entrypoint-loaders.md'), ], true, ), menuItem('Extension APIs', 'extension-apis.md'), menuItem('Assets', 'assets.md'), menuItem('Target Different Browsers', 'target-different-browsers.md'), menuItem('Content Scripts', 'content-scripts.md'), menuItem('Storage', 'storage.md'), menuItem('Messaging', 'messaging.md'), menuItem('I18n', 'i18n.md'), menuItem('Scripting', 'scripting.md'), menuItem('WXT Modules', 'wxt-modules.md'), menuItem('Frontend Frameworks', 'frontend-frameworks.md'), menuItem('ES Modules', 'es-modules.md'), menuItem('Remote Code', 'remote-code.md'), menuItem('Unit Testing', 'unit-testing.md'), menuItem('E2E Testing', 'e2e-testing.md'), menuItem('Publishing', 'publishing.md'), menuItem('Testing Updates', 'testing-updates.md'), ]), menuGroup('Resources', '/guide/resources/', [ menuItem('Compare', 'compare.md'), menuItem('FAQ', 'faq.md'), menuItem('Community', 'community.md'), menuItem('Upgrading WXT', 'upgrading.md'), menuItem('Migrate to WXT', 'migrate.md'), menuItem('How WXT Works', 'how-wxt-works.md'), ]), ]), '/api/': menuRoot([ menuGroup( 'CLI Reference', '/api/cli/', [ menuItem('wxt', 'wxt.md'), menuItem('wxt build', 'wxt-build.md'), menuItem('wxt zip', 'wxt-zip.md'), menuItem('wxt prepare', 'wxt-prepare.md'), menuItem('wxt clean', 'wxt-clean.md'), menuItem('wxt init', 'wxt-init.md'), menuItem('wxt submit', 'wxt-submit.md'), menuItem('wxt submit init', 'wxt-submit-init.md'), ], true, ), menuGroup('API Reference', prepareTypedocSidebar(typedocSidebar), true), ]), }, }, }); ``` ## /docs/.vitepress/loaders/blog.data.ts ```ts path="/docs/.vitepress/loaders/blog.data.ts" import { createContentLoader } from 'vitepress'; export default createContentLoader('blog/*.md'); ``` ## /docs/.vitepress/loaders/cli.data.ts ```ts path="/docs/.vitepress/loaders/cli.data.ts" import { resolve } from 'node:path'; import consola from 'consola'; import spawn from 'nano-spawn'; const cliDir = resolve('packages/wxt/src/cli/commands'); const cliDirGlob = resolve(cliDir, '**'); export default { watch: [cliDirGlob], async load() { consola.info(`Generating CLI docs`); const [wxt, build, zip, prepare, clean, init, submit, submitInit] = await Promise.all([ getWxtHelp(''), getWxtHelp('build'), getWxtHelp('zip'), getWxtHelp('prepare'), getWxtHelp('clean'), getWxtHelp('init'), getPublishExtensionHelp(''), getPublishExtensionHelp('init'), ]); consola.success(`Generated CLI docs`); return { wxt, build, zip, prepare, clean, init, submit, submitInit, }; }, }; async function getHelp(command: string): Promise { const args = command.split(' '); const res = await spawn(args[0], [...args.slice(1), '--help'], { cwd: 'packages/wxt', }); return res.stdout; } function getWxtHelp(command: string): Promise { return getHelp(`pnpm -s wxt ${command}`.trim()); } async function getPublishExtensionHelp(command: string): Promise { const res = await getHelp( `./node_modules/.bin/publish-extension ${command}`.trim(), ); return res.replace(/\$ publish-extension/g, '$ wxt submit'); } export interface Command { name: string; docs: string; } ``` ## /docs/.vitepress/theme/custom.css ```css path="/docs/.vitepress/theme/custom.css" /* Colors */ :root { --wxt-c-green-1: #0b8a00; --wxt-c-green-2: #096600; --wxt-c-green-3: #096600; --wxt-c-green-soft: rgba(11, 138, 0, 0.14); } .dark { --wxt-c-green-1: #67d45e; --wxt-c-green-2: #329929; --wxt-c-green-3: #21651b; --wxt-c-green-soft: rgba(103, 212, 94, 0.14); } /* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */ :root { --vp-c-brand-1: var(--wxt-c-green-1); --vp-c-brand-2: var(--wxt-c-green-2); --vp-c-brand-3: var(--wxt-c-green-3); --vp-c-brand-soft: var(--wxt-c-green-soft); } /* Customize Individual Components */ .vp-doc .no-vertical-dividers th, .vp-doc .no-vertical-dividers td { border: none; } .vp-doc .no-vertical-dividers tr { border: 1px solid var(--vp-c-divider); } body { overflow-y: scroll; } .VPSidebar { user-select: none; } .VPSidebarItem .badge { display: inline-block; min-width: 1.6em; padding: 0.3em 0.4em 0.2em; border-radius: 1rem; font-size: 0.75em; line-height: 1; margin-left: 0.5rem; text-align: center; vertical-align: middle; background-color: var(--vp-c-default-2); } .light-netlify { display: inline; } .dark .light-netlify { display: none; } .dark-netlify { display: none; } .dark .dark-netlify { display: inline; } ``` ## /docs/.vitepress/theme/index.ts ```ts path="/docs/.vitepress/theme/index.ts" import DefaultTheme from 'vitepress/theme'; import Icon from '../components/Icon.vue'; import EntrypointPatterns from '../components/EntrypointPatterns.vue'; import UsingWxtSection from '../components/UsingWxtSection.vue'; import ExampleSearch from '../components/ExampleSearch.vue'; import BlogLayout from '../components/BlogLayout.vue'; import './custom.css'; import 'virtual:group-icons.css'; export default { extends: DefaultTheme, enhanceApp(ctx) { ctx.app .component('Icon', Icon) .component('EntrypointPatterns', EntrypointPatterns) .component('UsingWxtSection', UsingWxtSection) .component('ExampleSearch', ExampleSearch) .component('blog', BlogLayout); }, }; ``` ## /docs/.vitepress/utils/head.ts ```ts path="/docs/.vitepress/utils/head.ts" import { HeadConfig } from 'vitepress/types/shared'; export function meta( property: string, content: string, options?: { useName: boolean }, ): HeadConfig { return [ 'meta', { [options?.useName ? 'name' : 'property']: property, content, }, ]; } export function script( src: string, props: Record = {}, ): HeadConfig { return [ 'script', { ...props, src, }, ]; } ``` ## /docs/.vitepress/utils/menus.ts ```ts path="/docs/.vitepress/utils/menus.ts" import { DefaultTheme } from 'vitepress'; type SidebarItem = DefaultTheme.SidebarItem; type NavItem = DefaultTheme.NavItem; type NavItemWithLink = DefaultTheme.NavItemWithLink; type NavItemWithChildren = DefaultTheme.NavItemWithChildren; type NavItemChildren = DefaultTheme.NavItemChildren; export function navItem(text: string): NavItemChildren; export function navItem(text: string, link: string): NavItemChildren; export function navItem(text: string, items: any[]): NavItemWithChildren; export function navItem(text: string, arg2?: unknown): any { if (typeof arg2 === 'string') { return { text, link: arg2 }; } else if (Array.isArray(arg2)) { return { text, items: arg2 }; } return { text }; } export function menuRoot(items: SidebarItem[]) { return items.map((item, index) => { // item.collapsed = false; // uncomment to expand all level-0 items return item; }); } export function menuGroup( text: string, items: SidebarItem[], collapsible?: boolean, ): SidebarItem; export function menuGroup( text: string, base: string, items: SidebarItem[], collapsible?: boolean, ): SidebarItem; export function menuGroup( text: string, a: string | SidebarItem[], b?: SidebarItem[] | boolean, c?: boolean, ): SidebarItem { if (typeof a === 'string' && Array.isArray(b)) { return { text, base: a, items: b, collapsed: c, }; } if (typeof a !== 'string' && !Array.isArray(b)) return { text, items: a, collapsed: b, }; throw Error('Unknown overload'); } export function menuItems(items: SidebarItem[]) { return { items, }; } export function menuItem( text: string, link: string, items?: SidebarItem[], ): SidebarItem { if (items) { return { text, link, items }; } return { text, link }; } /** * Clean up and add badges to typedoc leaf sections */ export function prepareTypedocSidebar(items: SidebarItem[]) { // skip contents file const filtered = items.slice(1); // remove Typedoc's collapse: true from text nodes const prepareItems = (items: DefaultTheme.SidebarItem[], depth = 0) => { for (const item of items) { if (item.items) { prepareItems(item.items, depth + 1); const hasLeaves = item.items.some((item) => !item.items); if (hasLeaves) { item.text += ` ${item.items.length}`; } } else { delete item.collapsed; } } }; // process prepareItems(filtered); // return return filtered; } ``` ## /docs/.vitepress/utils/types.ts ```ts path="/docs/.vitepress/utils/types.ts" export interface Example { name: string; description?: string; url: string; searchText: string; apis: string[]; permissions: string[]; packages: string[]; } export type ExamplesMetadata = { examples: Example[]; allApis: string[]; allPermissions: string[]; allPackages: string[]; }; export type KeySelectedObject = Record; ``` ## /docs/analytics.md ## /docs/api/cli/wxt-build.md # `wxt build`
{{ data.build }}
## /docs/api/cli/wxt-clean.md # `wxt clean`
{{ data.clean }}
## /docs/api/cli/wxt-init.md # `wxt init`
{{ data.init }}
## /docs/api/cli/wxt-prepare.md # `wxt prepare`
{{ data.prepare }}
## /docs/api/cli/wxt-submit-init.md # `wxt submit init` > Alias for [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension)
{{ data.submitInit }}
## /docs/api/cli/wxt-submit.md # `wxt submit` > Alias for [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension)
{{ data.submit }}
## /docs/api/cli/wxt-zip.md # `wxt zip`
{{ data.zip }}
## /docs/api/cli/wxt.md # `wxt`
{{ data.wxt }}
## /docs/assets/cli-output.png Binary file available at https://raw.githubusercontent.com/wxt-dev/wxt/refs/heads/main/docs/assets/cli-output.png ## /docs/assets/init-demo.gif Binary file available at https://raw.githubusercontent.com/wxt-dev/wxt/refs/heads/main/docs/assets/init-demo.gif ## /docs/auto-icons.md ## /docs/blog.md --- layout: page --- ## /docs/blog/.drafts/2024-10-19-real-world-messaging.md --- layout: blog title: Real World Messaging description: | The extension messaging APIs are difficult to learn. Let's go beyond the simple examples from Chrome and Firefox's documentation to build our own simple messaging system from scratch. authors: - name: Aaron Klinker github: aklinker1 date: 2024-10-20T04:54:23.601Z --- Test content **bold** _italic_ ## /docs/blog/2024-12-06-using-imports-module.md --- layout: blog title: Introducing #imports description: Learn how WXT's new #imports module works and how to use it. authors: - name: Aaron Klinker github: aklinker1 date: 2024-12-06T14:39:00.000Z --- WXT v0.20 introduced a new way of manually importing its APIs: **the `#imports` module**. This module was introduced to simplify import statements and provide more visibility into all the APIs WXT provides. ```ts import { browser } from 'wxt/browser'; // [!code --] import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; // [!code --] import { defineContentScript } from 'wxt/utils/define-content-script'; // [!code --] import { injectScript } from 'wxt/utils/inject-script'; // [!code --] import { // [!code ++] browser, createShadowRootUi, defineContentScript, injectScript // [!code ++] } from '#imports'; // [!code ++] ``` The `#imports` module is considered a "virtual module", because the file doesn't actually exist. At build-time, imports are split into individual statements for each API: :::code-group ```ts [What you write] import { defineContentScript, injectScript } from '#imports'; ``` ```ts [What the bundler sees] import { defineContentScript } from 'wxt/utils/define-content-script'; import { injectScript } from 'wxt/utils/inject-script'; ``` ::: Think of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size. This enables better tree-shaking compared to v0.19 and below. :::tip Need to lookup the full import path of an API? Open up your project's `.wxt/types/imports-module.d.ts` file. ::: ## Mocking When writing tests, you might need to mock APIs from the `#imports` module. While mocking these APIs is very easy, it may not be immediately clear how to accomplish it. Let's look at an example using Vitest. When [configured with `wxt/testing`](/guide/essentials/unit-testing#vitest), Vitest sees the same transformed code as the bundler. That means to mock an API from `#imports`, you need to call `vi.mock` with the real import path, not `#imports`: ```ts import { injectScript } from '#imports'; import { vi } from 'vitest'; vi.mock('wxt/utils/inject-script') const injectScriptMock = vi.mocked(injectScript); injectScriptMock.mockReturnValueOnce(...); ``` ## Conclusion You don't have to use `#imports` if you don't like - you can continue importing APIs from their submodules. However, using `#imports` is the recommended approach moving forwards. - As more APIs are added, you won't have to memorize additional import paths. - If breaking changes are made to import paths in future major versions, `#imports` won't break. Happy Coding 😄 > P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport). --- [Discuss this blog post on Github](https://github.com/wxt-dev/wxt/discussions/1543). ## /docs/examples.md --- layout: page ---

Examples


## /docs/guide/essentials/assets.md # Assets ## `/assets` Directory Any assets imported or referenced inside the `/assets/` directory will be processed by WXT's bundler. Here's how you access them: :::code-group ```ts [JS] import imageUrl from '~/assets/image.png'; const img = document.createElement('img'); img.src = imageUrl; ``` ```html [HTML] ``` ```css [CSS] .bg-image { background-image: url(~/assets/image.png); } ``` ```vue [Vue] ``` ```jsx [JSX] import image from '~/assets/image.png'; ; ``` ::: ## `/public` Directory Files inside `/public/` are copied into the output folder as-is, without being processed by WXT's bundler. Here's how you access them: :::code-group ```ts [JS] import imageUrl from '/image.png'; const img = document.createElement('img'); img.src = imageUrl; ``` ```html [HTML] ``` ```css [CSS] .bg-image { background-image: url(/image.png); } ``` ```vue [Vue] ``` ```jsx [JSX] ``` ::: :::warning Assets in the `public/` directory are **_not_** accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's [`web_accessible_resources` array](/api/reference/wxt/type-aliases/UserManifest#web-accessible-resources). ::: ## Inside Content Scripts Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname. But, inside content scripts, the hostname is whatever the tab is set to. So if you try to fetch the asset, manually or as an ``'s `src`, it will be loaded from the tab's website, not your extension. To fix this, you need to convert the image to a full URL using `browser.runtime.getURL`: ```ts [entrypoints/content.ts] import iconUrl from '/icon/128.png'; export default defineContentScript({ matches: ['*://*.google.com/*'], main() { console.log(iconUrl); // "/icon/128.png" console.log(browser.runtime.getURL(iconUrl)); // "chrome-extension:///icon/128.png" }, }); ``` ## WASM How a `.wasm` file is loaded varies greatly between packages, but most follow a basic setup: Use a JS API to load and execute the `.wasm` file. For an extension, that means two things: 1. The `.wasm` file needs to be present in output folder so it can be loaded. 2. You must import the JS API to load and initialize the `.wasm` file, usually provided by the NPM package. For an example, let's say you have a content script needs to parse TS code into AST. We'll use [`@oxc-parser/wasm`](https://www.npmjs.com/package/@oxc-parser/wasm) to do it! First, we need to copy the `.wasm` file to the output directory. We'll do it with a [WXT module](/guide/essentials/wxt-modules): ```ts // modules/oxc-parser-wasm.ts import { resolve } from 'node:path'; export default defineWxtModule((wxt) => { wxt.hook('build:publicAssets', (_, assets) => { assets.push({ absoluteSrc: resolve( 'node_modules/@oxc-parser/wasm/web/oxc_parser_wasm_bg.wasm', ), relativeDest: 'oxc_parser_wasm_bg.wasm', }); }); }); ``` Run `wxt build`, and you should see the WASM file copied into your `.output/chrome-mv3` folder! Next, since this is in a content script and we'll be fetching the WASM file over the network to load it, we need to add the file to the `web_accessible_resources`: ```ts [wxt.config.ts] export default defineConfig({ manifest: { web_accessible_resources: [ { // We'll use this matches in the content script as well matches: ['*://*.github.com/*'], // Use the same path as `relativeDest` from the WXT module resources: ['/oxc_parser_wasm_bg.wasm'], }, ], }, }); ``` And finally, we need to load and initialize the `.wasm` file inside the content script to use it: ```ts [entrypoints/content.ts] import initWasm, { parseSync } from '@oxc-parser/wasm'; export default defineContentScript({ matches: '*://*.github.com/*', async main(ctx) { if (!location.pathname.endsWith('.ts')) return; // Get text from GitHub const code = document.getElementById( 'read-only-cursor-text-area', )?.textContent; if (!code) return; const sourceFilename = document.getElementById('file-name-id')?.textContent; if (!sourceFilename) return; // Load the WASM file: await initWasm({ module_or_path: browser.runtime.getURL('/oxc_parser_wasm_bg.wasm'), }); // Once loaded, we can use `parseSync`! const ast = parseSync(code, { sourceFilename }); console.log(ast); }, }); ``` This code is taken directly from `@oxc-parser/wasm` docs with one exception: We manually pass in a file path. In a standard NodeJS or web project, the default path works just fine so you don't have to pass anything in. However, extensions are different. You should always explicitly pass in the full URL to the WASM file in your output directory, which is what `browser.runtime.getURL` returns. Run your extension, and you should see OXC parse the TS file! ## /docs/guide/essentials/config/auto-imports.md # Auto-imports WXT uses [`unimport`](https://www.npmjs.com/package/unimport), the same tool as Nuxt, to setup auto-imports. ```ts export default defineConfig({ // See https://www.npmjs.com/package/unimport#configurations imports: { // ... }, }); ``` By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories: - `/components/*` - `/composables/*` - `/hooks/*` - `/utils/*` All named and default exports from files in these directories are available everywhere else in your project without having to import them. To see the complete list of auto-imported APIs, run [`wxt prepare`](/api/cli/wxt-prepare) and look at your project's `.wxt/types/imports-module.d.ts` file. ## TypeScript For TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare). Add this command to your `postinstall` script so your editor has everything it needs to report type errors after installing dependencies: ```jsonc // package.json { "scripts": { "postinstall": "wxt prepare", // [!code ++] }, } ``` ## ESLint ESLint doesn't know about the auto-imported variables unless they are explicitly defined in the ESLint's `globals`. By default, WXT will generate the config if it detects ESLint is installed in your project. If the config isn't generated automatically, you can manually tell WXT to generate it. :::code-group ```ts [ESLint 9] export default defineConfig({ imports: { eslintrc: { enabled: 9, }, }, }); ``` ```ts [ESLint 8] export default defineConfig({ imports: { eslintrc: { enabled: 8, }, }, }); ``` ::: Then in your ESLint config, import and use the generated file: :::code-group ```js [ESLint 9] // eslint.config.mjs import autoImports from './.wxt/eslint-auto-imports.mjs'; export default [ autoImports, { // The rest of your config... }, ]; ``` ```js [ESLint 8] // .eslintrc.mjs export default { extends: ['./.wxt/eslintrc-auto-import.json'], // The rest of your config... }; ``` ::: ## Disabling Auto-imports Not all developers like auto-imports. To disable them, set `imports` to `false`. ```ts export default defineConfig({ imports: false, // [!code ++] }); ``` ## Explicit Imports (`#imports`) You can manually import all of WXT's APIs via the `#imports` module: ```ts import { createShadowRootUi, ContentScriptContext, MatchPattern, } from '#imports'; ``` To learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module). If you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place. ## /docs/guide/essentials/config/browser-startup.md --- outline: deep --- # Browser Startup > See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config. During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed. :::danger Chrome 137 removed support for the `--load-extension` CLI flag, which WXT relied on to open the browser with an extension installed. So this feature will not work for Chrome. You have two options: 1. Install [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (which still supports the `--load-extension` flag) and [point the `chrome` binary to it](#set-browser-binaries), or 2. [Disable this feature](#disable-opening-browser) and manually load your extension ::: ## Config Files You can configure browser startup in 3 places: 1. `/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers ```ts [web-ext.config.ts] import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ // ... }); ``` 2. `/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control 3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer ## Recipes ### Set Browser Binaries To set or customize the browser opened during development: ```ts [web-ext.config.ts] export default defineWebExtConfig({ binaries: { chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge" }, }); ``` By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above. ### Persist Data By default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script. Right now, Chromium based browsers are the only browsers that support overriding this behavior and persisting data when running the `dev` script multiple times. To persist data, set the `--user-data-dir` flag: :::code-group ```ts [Mac/Linux] export default defineWebExtConfig({ chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'], }); ``` ```ts [Windows] import { resolve } from 'node:path'; export default defineWebExtConfig({ // On Windows, the path must be absolute chromiumProfile: resolve('.wxt/chrome-data'), keepProfileChanges: true, }); ``` ::: Now, next time you run the `dev` script, a persistent profile will be created in `.wxt/chrome-data/{profile-name}`. With a persistent profile, you can install devtools extensions to help with development, allow the browser to remember logins, etc, without worrying about the profile being reset the next time you run the `dev` script. :::tip You can use any directory you'd like for `--user-data-dir`, the examples above create a persistent profile for each WXT project. To create a profile for all WXT projects, you can put the `chrome-data` directory inside your user's home directory. ::: ### Disable Opening Browser If you prefer to load the extension into your browser manually, you can disable the auto-open behavior: ```ts [web-ext.config.ts] export default defineWebExtConfig({ disabled: true, }); ``` ## /docs/guide/essentials/config/build-mode.md # Build Modes Because WXT is powered by Vite, it supports [modes](https://vite.dev/guide/env-and-mode.html#modes) in the same way. When running any dev or build commands, pass the `--mode` flag: ```sh wxt --mode production wxt build --mode development wxt zip --mode testing ``` By default, `--mode` is `development` for the dev command and `production` for all other commands (build, zip, etc). ## Get Mode at Runtime You can access the current mode in your extension using `import.meta.env.MODE`: ```ts switch (import.meta.env.MODE) { case 'development': // ... case 'production': // ... // Custom modes specified with --mode case 'testing': // ... case 'staging': // ... // ... } ``` ## /docs/guide/essentials/config/entrypoint-loaders.md # Entrypoint Loaders To generate the manifest and other files at build-time, WXT must import each entrypoint to get their options, like content script `matches`. For HTML files, this is easy. For JS/TS entrypoints, the process is more complicated. When loading your JS/TS entrypoints, they are imported into a NodeJS environment, not the `browser` environment that they normally run in. This can lead to issues commonly seen when running browser-only code in a NodeJS environment, like missing global variables. WXT does several pre-processing steps to try and prevent errors during this process: 1. Use `linkedom` to make a small set of browser globals (`window`, `document`, etc) available. 2. Use `@webext-core/fake-browser` to create a fake version of the `chrome` and `browser` globals expected by extensions. 3. Pre-process the JS/TS code, stripping out the `main` function then tree-shaking unused code from the file However, this process is not perfect. It doesn't setup all the globals found in the browser and the APIs may behave differently. As such, **_you should avoid using browser or extension APIs outside the `main` function of your entrypoints!_** :::tip If you're running into errors while importing entrypoints, run `wxt prepare --debug` to see more details about this process. When debugging, WXT will print out the pre-processed code to help you identify issues. ::: Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export. ## /docs/guide/essentials/config/environment-variables.md # Environment Variables ## Dotenv Files WXT supports [dotenv files the same way as Vite](https://vite.dev/guide/env-and-mode.html#env-files). Create any of the following files: ``` .env .env.local .env.[mode] .env.[mode].local .env.[browser] .env.[browser].local .env.[mode].[browser] .env.[mode].[browser].local ``` And any environment variables listed inside them will be available at runtime: ```sh # .env WXT_API_KEY=... ``` ```ts await fetch(`/some-api?apiKey=${import.meta.env.WXT_API_KEY}`); ``` Remember to prefix any environment variables with `WXT_` or `VITE_`, otherwise they won't be available at runtime, as per [Vite's convention](https://vite.dev/guide/env-and-mode.html#env-files). ## Built-in Environment Variables WXT provides some custom environment variables based on the current command: | Usage | Type | Description | | ---------------------------------- | --------- | ----------------------------------------------------- | | `import.meta.env.MANIFEST_VERSION` | `2 │ 3` | The target manifest version | | `import.meta.env.BROWSER` | `string` | The target browser | | `import.meta.env.CHROME` | `boolean` | Equivalent to `import.meta.env.BROWSER === "chrome"` | | `import.meta.env.FIREFOX` | `boolean` | Equivalent to `import.meta.env.BROWSER === "firefox"` | | `import.meta.env.SAFARI` | `boolean` | Equivalent to `import.meta.env.BROWSER === "safari"` | | `import.meta.env.EDGE` | `boolean` | Equivalent to `import.meta.env.BROWSER === "edge"` | | `import.meta.env.OPERA` | `boolean` | Equivalent to `import.meta.env.BROWSER === "opera"` | You can set the [`targetBrowsers`](/api/reference/wxt/interfaces/InlineConfig#targetbrowsers) option to make the `BROWSER` variable a more specific type, like `"chrome" | "firefox"`. You can also access all of [Vite's environment variables](https://vite.dev/guide/env-and-mode.html#env-variables): | Usage | Type | Description | | ---------------------- | --------- | --------------------------------------------------------------------------- | | `import.meta.env.MODE` | `string` | The [mode](/guide/essentials/config/build-mode) the extension is running in | | `import.meta.env.PROD` | `boolean` | When `NODE_ENV='production'` | | `import.meta.env.DEV` | `boolean` | Opposite of `import.meta.env.PROD` | :::details Other Vite Environment Variables Vite provides two other environment variables, but they aren't useful in WXT projects: - `import.meta.env.BASE_URL`: Use `browser.runtime.getURL` instead. - `import.meta.env.SSR`: Always `false`. ::: ## Manifest To use environment variables in the manifest, you need to use the function syntax: ```ts export default defineConfig({ extensionApi: 'chrome', modules: ['@wxt-dev/module-vue'], manifest: { // [!code --] oauth2: { // [!code --] client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code --] } // [!code --] } // [!code --] manifest: () => ({ // [!code ++] oauth2: { // [!code ++] client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code ++] } // [!code ++] }), // [!code ++] }); ``` WXT can't load your `.env` files until after the config file has been loaded. So by using the function syntax for `manifest`, it defers creating the object until after the `.env` files are loaded into the process. ## /docs/guide/essentials/config/hooks.md # Hooks WXT includes a system that lets you hook into the build process and make changes. ## Adding Hooks The easiest way to add a hook is via the `wxt.config.ts`. Here's an example hook that modifies the `manifest.json` file before it is written to the output directory: ```ts [wxt.config.ts] export default defineConfig({ hooks: { 'build:manifestGenerated': (wxt, manifest) => { if (wxt.config.mode === 'development') { manifest.title += ' (DEV)'; } }, }, }); ``` Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system. Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules) instead. ## Execution Order Because hooks can be defined in multiple places, including [WXT Modules](/guide/essentials/wxt-modules), the order which they're executed can matter. Hooks are executed in the following order: 1. NPM modules in the order listed in the [`modules` config](/api/reference/wxt/interfaces/InlineConfig#modules) 2. User modules in [`/modules` folder](/guide/essentials/project-structure), loaded alphabetically 3. Hooks listed in your `wxt.config.ts` To see the order for your project, run `wxt prepare --debug` flag and search for the "Hook execution order": ``` ⚙ Hook execution order: ⚙ 1. wxt:built-in:unimport ⚙ 2. src/modules/auto-icons.ts ⚙ 3. src/modules/example.ts ⚙ 4. src/modules/i18n.ts ⚙ 5. wxt.config.ts > hooks ``` Changing execution order is simple: - Prefix your user modules with a number (lower numbers are loaded first): ```html 📁 modules/ 📄 0.my-module.ts 📄 1.another-module.ts ``` - If you need to run an NPM module after user modules, just make it a user module and prefix the filename with a number! ```ts // modules/2.i18n.ts export { default } from '@wxt-dev/i18n/module'; ``` ## /docs/guide/essentials/config/manifest.md # Manifest In WXT, there is no `manifest.json` file in your source code. Instead, WXT generates the manifest from multiple sources: - Global options [defined in your `wxt.config.ts` file](#global-options) - Entrypoint-specific options [defined in your entrypoints](/guide/essentials/entrypoints#defining-manifest-options) - [WXT Modules](/guide/essentials/wxt-modules) added to your project can modify your manifest - [Hooks](/guide/essentials/config/hooks) defined in your project can modify your manifest Your extension's `manifest.json` will be output to `.output/{target}/manifest.json` when running `wxt build`. ## Global Options To add a property to your manifest, use the `manifest` config inside your `wxt.config.ts`: ```ts export default defineConfig({ manifest: { // Put manual changes here }, }); ``` You can also define the manifest as a function, and use JS to generate it based on the target browser, mode, and more. ```ts export default defineConfig({ manifest: ({ browser, manifestVersion, mode, command }) => { return { // ... }; }, }); ``` ### MV2 and MV3 Compatibility When adding properties to the manifest, always define the property in it's MV3 format when possible. When targeting MV2, WXT will automatically convert these properties to their MV2 format. For example, for this config: ```ts export default defineConfig({ manifest: { action: { default_title: 'Some Title', }, web_accessible_resources: [ { matches: ['*://*.google.com/*'], resources: ['icon/*.png'], }, ], }, }); ``` WXT will generate the following manifests: :::code-group ```json [MV2] { "manifest_version": 2, // ... "browser_action": { "default_title": "Some Title" }, "web_accessible_resources": ["icon/*.png"] } ``` ```json [MV3] { "manifest_version": 3, // ... "action": { "default_title": "Some Title" }, "web_accessible_resources": [ { "matches": ["*://*.google.com/*"], "resources": ["icon/*.png"] } ] } ``` ::: You can also specify properties specific to a single manifest version, and they will be stripped out when targeting the other manifest version. ## Name > [Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) If not provided via the `manifest` config, the manifest's `name` property defaults to your `package.json`'s `name` property. ## Version and Version Name > [Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) Your extension's `version` and `version_name` is based on the `version` from your `package.json`. - `version_name` is the exact string listed - `version` is the string cleaned up, with any invalid suffixes removed Example: ```json // package.json { "version": "1.3.0-alpha2" } ``` ```json // .output//manifest.json { "version": "1.3.0", "version_name": "1.3.0-alpha2" } ``` If a version is not present in your `package.json`, it defaults to `"0.0.0"`. ## Icons WXT automatically discovers your extension's icon by looking at files in the `public/` directory: ``` public/ ├─ icon-16.png ├─ icon-24.png ├─ icon-48.png ├─ icon-96.png └─ icon-128.png ``` Specifically, an icon must match one of these regex to be discovered: <<< @/../packages/wxt/src/core/utils/manifest.ts#snippet If you don't like these filename or you're migrating to WXT and don't want to rename the files, you can manually specify an `icon` in your manifest: ```ts export default defineConfig({ manifest: { icons: { 16: '/extension-icon-16.png', 24: '/extension-icon-24.png', 48: '/extension-icon-48.png', 96: '/extension-icon-96.png', 128: '/extension-icon-128.png', }, }, }); ``` Alternatively, you can use [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) to let WXT generate your icon at the required sizes. ## Permissions > [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/) Most of the time, you need to manually add permissions to your manifest. Only in a few specific situations are permissions added automatically: - During development: the `tabs` and `scripting` permissions will be added to enable hot reloading. - When a `sidepanel` entrypoint is present: The `sidepanel` permission is added. ```ts export default defineConfig({ manifest: { permissions: ['storage', 'tabs'], }, }); ``` ## Host Permissions > [Chrome docs](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions) ```ts export default defineConfig({ manifest: { host_permissions: ['https://www.google.com/*'], }, }); ``` :::warning If you use host permissions and target both MV2 and MV3, make sure to only include the required host permissions for each version: ```ts export default defineConfig({ manifest: ({ manifestVersion }) => ({ host_permissions: manifestVersion === 2 ? [...] : [...], }), }); ``` ::: ## Default Locale ```ts export default defineConfig({ manifest: { name: '__MSG_extName__', description: '__MSG_extDescription__', default_locale: 'en', }, }); ``` > See [I18n docs](/guide/essentials/i18n) for a full guide on internationalizing your extension. ## Actions In MV2, you have two options: [`browser_action`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action) and [`page_action`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action). In MV3, they were merged into a single [`action`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action) API. By default, whenever an `action` is generated, WXT falls back to `browser_action` when targeting MV2. ### Action With Popup To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints#popup). If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head: ```html ``` ### Action Without Popup If you want to use the `activeTab` permission or the `browser.action.onClicked` event, but don't want to show a popup: 1. Delete the [Popup entrypoint](/guide/essentials/entrypoints#popup) if it exists 2. Add the `action` key to your manifest: ```ts export default defineConfig({ manifest: { action: {}, }, }); ``` Same as an action with a popup, WXT will fallback on using `browser_action` for MV2. To use a `page_action` instead, add that key as well: ```ts export default defineConfig({ manifest: { action: {}, page_action: {}, }, }); ``` ## /docs/guide/essentials/config/runtime.md # Runtime Config > This API is still a WIP, with more features coming soon! Define runtime configuration in a single place, `/app.config.ts`: ```ts import { defineAppConfig } from '#imports'; // Define types for your config declare module 'wxt/utils/define-app-config' { export interface WxtAppConfig { theme?: 'light' | 'dark'; } } export default defineAppConfig({ theme: 'dark', }); ``` :::warning This file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](#environment-variables) ::: To access runtime config, WXT provides the `useAppConfig` function: ```ts import { useAppConfig } from '#imports'; console.log(useAppConfig()); // { theme: "dark" } ``` ## Environment Variables in App Config You can use environment variables in the `app.config.ts` file. ```ts declare module 'wxt/utils/define-app-config' { export interface WxtAppConfig { apiKey?: string; skipWelcome: boolean; } } export default defineAppConfig({ apiKey: import.meta.env.WXT_API_KEY, skipWelcome: import.meta.env.WXT_SKIP_WELCOME === 'true', }); ``` This has several advantages: - Define all expected environment variables in a single file - Convert strings to other types, like booleans or arrays - Provide default values if an environment variable is not provided ## /docs/guide/essentials/config/typescript.md # TypeScript Configuration When you run [`wxt prepare`](/api/cli/wxt-prepare), WXT generates a base TSConfig file for your project at `/.wxt/tsconfig.json`. At a minimum, you need to create a TSConfig in your root directory that looks like this: ```jsonc // /tsconfig.json { "extends": ".wxt/tsconfig.json", } ``` Or if you're in a monorepo, you may not want to extend the config. If you don't extend it, you need to add `.wxt/wxt.d.ts` to the TypeScript project: ```ts /// ``` ## Compiler Options To specify custom compiler options, add them in `/tsconfig.json`: ```jsonc // /tsconfig.json { "extends": ".wxt/tsconfig.json", "compilerOptions": { "jsx": "preserve", }, } ``` ## TSConfig Paths WXT provides a default set of path aliases. | Alias | To | Example | | ----- | ------------- | ----------------------------------------------- | | `~~` | `/*` | `import "~~/scripts"` | | `@@` | `/*` | `import "@@/scripts"` | | `~` | `/*` | `import { toLowerCase } from "~/utils/strings"` | | `@` | `/*` | `import { toLowerCase } from "@/utils/strings"` | To add your own, DO NOT add them to your `tsconfig.json`! Instead, use the [`alias` option](/api/reference/wxt/interfaces/InlineConfig#alias) in `wxt.config.ts`. This will add your custom aliases to `/.wxt/tsconfig.json` next time you run `wxt prepare`. It also adds your alias to the bundler so it can resolve imports. ```ts import { resolve } from 'node:path'; export default defineConfig({ alias: { // Directory: testing: resolve('utils/testing'), // File: strings: resolve('utils/strings.ts'), }, }); ``` ```ts import { fakeTab } from 'testing/fake-objects'; import { toLowerCase } from 'strings'; ``` ## /docs/guide/essentials/config/vite.md # Vite WXT uses [Vite](https://vitejs.dev/) under the hood to bundle your extension. This page explains how to customize your project's Vite config. Refer to [Vite's documentation](https://vite.dev/config/) to learn more about configuring the bundler. :::tip In most cases, you shouldn't change Vite's build settings. WXT provides sensible defaults that output a valid extension accepted by all stores when publishing. ::: ## Change Vite Config You can change Vite's config via the `wxt.config.ts` file: ```ts [wxt.config.ts] import { defineConfig } from 'wxt'; export default defineConfig({ vite: () => ({ // Override config here, same as `defineConfig({ ... })` // inside vite.config.ts files }), }); ``` ## Add Vite Plugins To add a plugin, install the NPM package and add it to the Vite config: ```ts [wxt.config.ts] import { defineConfig } from 'wxt'; import VueRouter from 'unplugin-vue-router/vite'; export default defineConfig({ vite: () => ({ plugins: [ VueRouter({ /* ... */ }), ], }), }); ``` :::warning Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. For example, `vite-plugin-remove-console` normally only runs when you build for production (`vite build`). However, WXT uses a combination of dev server and builds during development, so you need to manually tell it when to run: ```ts [wxt.config.ts] import { defineConfig } from 'wxt'; import removeConsole from 'vite-plugin-remove-console'; export default defineConfig({ vite: (configEnv) => ({ plugins: configEnv.mode === 'production' ? [removeConsole({ includes: ['log'] })] : [], }), }); ``` Search [GitHub issues](https://github.com/wxt-dev/wxt/issues?q=is%3Aissue+label%3A%22vite+plugin%22) if you run into issues with a specific plugin. If an issue doesn't exist for your plugin, [open a new one](https://github.com/wxt-dev/wxt/issues/new/choose). ::: ## /docs/guide/essentials/content-scripts.md --- outline: deep --- # Content Scripts > To create a content script, see [Entrypoint Types](/guide/essentials/entrypoints#content-scripts). ## Context The first argument to a content script's `main` function is its "context". ```ts // entrypoints/example.content.ts export default defineContentScript({ main(ctx) {}, }); ``` This object is responsible for tracking whether or not the content script's context is "invalidated". Most browsers, by default, do not stop content scripts if the extension is uninstalled, updated, or disabled. When this happens, content scripts start reporting this error: ``` Error: Extension context invalidated. ``` The `ctx` object provides several helpers to stop asynchronous code from running once the context is invalidated: ```ts ctx.addEventListener(...); ctx.setTimeout(...); ctx.setInterval(...); ctx.requestAnimationFrame(...); // and more ``` You can also check if the context is invalidated manually: ```ts if (ctx.isValid) { // do something } // OR if (ctx.isInvalid) { // do something } ``` ## CSS In regular web extensions, CSS for content scripts is usually a separate CSS file, that is added to a CSS array in the manifest: ```json { "content_scripts": [ { "css": ["content/style.css"], "js": ["content/index.js"], "matches": ["*://*/*"] } ] } ``` In WXT, to add CSS to a content script, simply import the CSS file into your JS entrypoint, and WXT will automatically add the bundled CSS output to the `css` array. ```ts // entrypoints/example.content/index.ts import './style.css'; export default defineContentScript({ // ... }); ``` To create a standalone content script that only includes a CSS file: 1. Create the CSS file: `entrypoints/example.content.css` 2. Use the `build:manifestGenerated` hook to add the content script to the manifest: ```ts [wxt.config.ts] export default defineConfig({ hooks: { 'build:manifestGenerated': (wxt, manifest) => { manifest.content_scripts ??= []; manifest.content_scripts.push({ // Build extension once to see where your CSS get's written to css: ['content-scripts/example.css'], matches: ['*://*/*'], }); }, }, }); ``` ## UI WXT provides 3 built-in utilities for adding UIs to a page from a content script: - [Integrated](#integrated) - `createIntegratedUi` - [Shadow Root](#shadow-root) -`createShadowRootUi` - [IFrame](#iframe) - `createIframeUi` Each has their own set of advantages and disadvantages. | Method | Isolated Styles | Isolated Events | HMR | Use page's context | | ----------- | :-------------: | :-----------------: | :-: | :----------------: | | Integrated | ❌ | ❌ | ❌ | ✅ | | Shadow Root | ✅ | ✅ (off by default) | ❌ | ✅ | | IFrame | ✅ | ✅ | ✅ | ❌ | ### Integrated Integrated content script UIs are injected alongside the content of a page. This means that they are affected by CSS on that page. :::code-group ```ts [Vanilla] // entrypoints/example-ui.content.ts export default defineContentScript({ matches: [''], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: 'body', onMount: (container) => { // Append children to the container const app = document.createElement('p'); app.textContent = '...'; container.append(app); }, }); // Call mount to add the UI to the DOM ui.mount(); }, }); ``` ```ts [Vue] // entrypoints/example-ui.content/index.ts import { createApp } from 'vue'; import App from './App.vue'; export default defineContentScript({ matches: [''], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: 'body', onMount: (container) => { // Create the app and mount it to the UI container const app = createApp(App); app.mount(container); return app; }, onRemove: (app) => { // Unmount the app when the UI is removed app.unmount(); }, }); // Call mount to add the UI to the DOM ui.mount(); }, }); ``` ```tsx [React] // entrypoints/example-ui.content/index.tsx import ReactDOM from 'react-dom/client'; import App from './App.tsx'; export default defineContentScript({ matches: [''], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: 'body', onMount: (container) => { // Create a root on the UI container and render a component const root = ReactDOM.createRoot(container); root.render(); return root; }, onRemove: (root) => { // Unmount the root when the UI is removed root.unmount(); }, }); // Call mount to add the UI to the DOM ui.mount(); }, }); ``` ```ts [Svelte] // entrypoints/example-ui.content/index.ts import App from './App.svelte'; import { mount, unmount } from 'svelte'; export default defineContentScript({ matches: [''], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: 'body', onMount: (container) => { // Create the Svelte app inside the UI container mount(App, { target: container, }); }, onRemove: (app) => { // Destroy the app when the UI is removed unmount(app); }, }); // Call mount to add the UI to the DOM ui.mount(); }, }); ``` ```tsx [Solid] // entrypoints/example-ui.content/index.ts import { render } from 'solid-js/web'; export default defineContentScript({ matches: [''], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: 'body', onMount: (container) => { // Render your app to the UI container const unmount = render(() =>
...
, container); return unmount; }, onRemove: (unmount) => { // Unmount the app when the UI is removed unmount(); }, }); // Call mount to add the UI to the DOM ui.mount(); }, }); ``` ::: See the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options. ### Shadow Root Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this. WXT's [`createShadowRootUi`](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions. To use `createShadowRootUi`, follow these steps: 1. Import your CSS file at the top of your content script 2. Set [`cssInjectionMode: "ui"`](/api/reference/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) inside `defineContentScript` 3. Define your UI with `createShadowRootUi()` 4. Mount the UI so it is visible to users :::code-group ```ts [Vanilla] // 1. Import the style import './style.css'; export default defineContentScript({ matches: [''], // 2. Set cssInjectionMode cssInjectionMode: 'ui', async main(ctx) { // 3. Define your UI const ui = await createShadowRootUi(ctx, { name: 'example-ui', position: 'inline', anchor: 'body', onMount(container) { // Define how your UI will be mounted inside the container const app = document.createElement('p'); app.textContent = 'Hello world!'; container.append(app); }, }); // 4. Mount the UI ui.mount(); }, }); ``` ```ts [Vue] // 1. Import the style import './style.css'; import { createApp } from 'vue'; import App from './App.vue'; export default defineContentScript({ matches: [''], // 2. Set cssInjectionMode cssInjectionMode: 'ui', async main(ctx) { // 3. Define your UI const ui = await createShadowRootUi(ctx, { name: 'example-ui', position: 'inline', anchor: 'body', onMount: (container) => { // Define how your UI will be mounted inside the container const app = createApp(App); app.mount(container); return app; }, onRemove: (app) => { // Unmount the app when the UI is removed app?.unmount(); }, }); // 4. Mount the UI ui.mount(); }, }); ``` ```tsx [React] // 1. Import the style import './style.css'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; export default defineContentScript({ matches: [''], // 2. Set cssInjectionMode cssInjectionMode: 'ui', async main(ctx) { // 3. Define your UI const ui = await createShadowRootUi(ctx, { name: 'example-ui', position: 'inline', anchor: 'body', onMount: (container) => { // Container is a body, and React warns when creating a root on the body, so create a wrapper div const app = document.createElement('div'); container.append(app); // Create a root on the UI container and render a component const root = ReactDOM.createRoot(app); root.render(); return root; }, onRemove: (root) => { // Unmount the root when the UI is removed root?.unmount(); }, }); // 4. Mount the UI ui.mount(); }, }); ``` ```ts [Svelte] // 1. Import the style import './style.css'; import App from './App.svelte'; import { mount, unmount } from 'svelte'; export default defineContentScript({ matches: [''], // 2. Set cssInjectionMode cssInjectionMode: 'ui', async main(ctx) { // 3. Define your UI const ui = await createShadowRootUi(ctx, { name: 'example-ui', position: 'inline', anchor: 'body', onMount: (container) => { // Create the Svelte app inside the UI container mount(App, { target: container, }); }, onRemove: () => { // Destroy the app when the UI is removed unmount(app); }, }); // 4. Mount the UI ui.mount(); }, }); ``` ```tsx [Solid] // 1. Import the style import './style.css'; import { render } from 'solid-js/web'; export default defineContentScript({ matches: [''], // 2. Set cssInjectionMode cssInjectionMode: 'ui', async main(ctx) { // 3. Define your UI const ui = await createShadowRootUi(ctx, { name: 'example-ui', position: 'inline', anchor: 'body', onMount: (container) => { // Render your app to the UI container const unmount = render(() =>
...
, container); }, onRemove: (unmount) => { // Unmount the app when the UI is removed unmount?.(); }, }); // 4. Mount the UI ui.mount(); }, }); ``` ::: See the [API Reference](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) for the complete list of options. Full examples: - [react-content-script-ui](https://github.com/wxt-dev/examples/tree/main/examples/react-content-script-ui) - [tailwindcss](https://github.com/wxt-dev/examples/tree/main/examples/tailwindcss) ### IFrame If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**. WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi), which simplifies setting up the IFrame. 1. Create an HTML page that will be loaded into your IFrame: ```html Your Content Script IFrame ``` 1. Add the page to the manifest's `web_accessible_resources`: ```ts [wxt.config.ts] export default defineConfig({ manifest: { web_accessible_resources: [ { resources: ['example-iframe.html'], matches: [...], }, ], }, }); ``` 1. Create and mount the IFrame: ```ts export default defineContentScript({ matches: [''], main(ctx) { // Define the UI const ui = createIframeUi(ctx, { page: '/example-iframe.html', position: 'inline', anchor: 'body', onMount: (wrapper, iframe) => { // Add styles to the iframe like width iframe.width = '123'; }, }); // Show UI to user ui.mount(); }, }); ``` See the [API Reference](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi) for the complete list of options. ## Isolated World vs Main World By default, all content scripts run in an isolated context where only the DOM is shared with the webpage it is running on - an "isolated world". In MV3, Chromium introduced the ability to run content scripts in the "main" world - where everything, not just the DOM, is available to the content script, just like if the script were loaded by the webpage. You can enable this for a content script by setting the `world` option: ```ts export default defineContentScript({ world: 'MAIN', }); ``` However, this approach has several notable drawbacks: - Doesn't support MV2 - `world: "MAIN"` is only supported by Chromium browsers - Main world content scripts don't have access to the extension API Instead, WXT recommends injecting a script into the main world manually using it's `injectScript` function. This will address the drawbacks mentioned before. - `injectScript` supports both MV2 and MV3 - `injectScript` supports all browsers - Having a "parent" content script means you can send messages back and forth, making it possible to access the extension API To use `injectScript`, we need two entrypoints, one content script and one unlisted script: ```html 📂 entrypoints/ 📄 example.content.ts 📄 example-main-world.ts ``` ```ts // entrypoints/example-main-world.ts export default defineUnlistedScript(() => { console.log('Hello from the main world'); }); ``` ```ts // entrypoints/example.content.ts export default defineContentScript({ matches: ['*://*/*'], async main() { console.log('Injecting script...'); await injectScript('/example-main-world.js', { keepInDom: true, }); console.log('Done!'); }, }); ``` ```json export default defineConfig({ manifest: { // ... web_accessible_resources: [ { resources: ["example-main-world.js"], matches: ["*://*/*"], } ] } }); ``` `injectScript` works by creating a `script` element on the page pointing to your script. This loads the script into the page's context so it runs in the main world. `injectScript` returns a promise, that when resolved, means the script has been evaluated by the browser and you can start communicating with it. :::warning Warning: `run_at` Caveat For MV3, `injectScript` is synchronous and the injected script will be evaluated at the same time as your the content script's `run_at`. However for MV2, `injectScript` has to `fetch` the script's text content and create an inline ` ``` ## Background By default, your background will be bundled into a single file as IIFE. You can change this by setting `type: "module"` in your background entrypoint: ```ts export default defineBackground({ type: 'module', // [!code ++] main() { // ... }, }); ``` This will change the output format to ESM, enable code-spliting between your background script and HTML pages, and set `"type": "module"` in your manifest. :::warning Only MV3 supports ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE. ::: ## Content Scripts WXT does not yet include built-in support for bundling content scripts as ESM. The plan is to add support for chunking to reduce bundle size, but not support HMR for now. There are several technical issues that make implementing a generic solution for HMR impossible. See [Content Script ESM Support #357](https://github.com/wxt-dev/wxt/issues/357) for details. If you can't wait, and need ESM support right now, you can implement ESM support manually. See the [ESM Content Script UI](https://github.com/wxt-dev/examples/tree/main/examples/esm-content-script-ui) example to learn how. ## /docs/guide/essentials/extension-apis.md # Extension APIs [Chrome Docs](https://developer.chrome.com/docs/extensions/reference/api) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs) Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc). WXT merges these two into a unified API accessed through the `browser` variable. ```ts import { browser } from 'wxt/browser'; browser.action.onClicked.addListener(() => { // ... }); ``` :::tip With auto-imports enabled, you don't even need to import this variable from `wxt/browser`! ::: The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime: <<< @/../packages/browser/src/index.mjs#snippet This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc). ## Accessing Types All types can be accessed via WXT's `Browser` namespace: ```ts import { type Browser } from 'wxt/browser'; function handleMessage(message: any, sender: Browser.runtime.MessageSender) { // ... } ``` ## Using `webextension-polyfill` If you want to use the `webextension-polyfill` when importing `browser`, you can do so by installing the `@wxt-dev/webextension-polyfill` package. See it's [Installation Guide](https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md) to get started. ## Feature Detection Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`. :::warning Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not. ::: To check if an API is available, use feature detection: ```ts if (browser.runtime.onSuspend != null) { browser.runtime.onSuspend.addListener(() => { // ... }); } ``` Here, [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) is your best friend: ```ts browser.runtime.onSuspend?.addListener(() => { // ... }); ``` Alternatively, if you're trying to use similar APIs under different names (to support MV2 and MV3), you can do something like this: ```ts (browser.action ?? browser.browser_action).onClicked.addListener(() => { // }); ``` ## /docs/guide/essentials/frontend-frameworks.md # Frontend Frameworks ## Built-in Modules WXT has preconfigured modules for the most popular frontend frameworks: - [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/tree/main/packages/module-react) - [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/tree/main/packages/module-vue) - [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/tree/main/packages/module-svelte) - [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/tree/main/packages/module-solid) Install the module for your framework, then add it to your config: :::code-group ```ts [React] import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-react'], }); ``` ```ts [Vue] import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-vue'], }); ``` ```ts [Svelte] import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-svelte'], }); ``` ```ts [Solid] import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-solid'], }); ``` ::: ## Adding Vite Plugins If your framework doesn't have an official WXT module, no worries! WXT supports any framework with a Vite plugin. Just add the Vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts and it will just work 👍 ```ts import { defineConfig } from 'wxt'; import react from '@vitejs/plugin-react'; export default defineConfig({ vite: () => ({ plugins: [react()], }), }); ``` > The WXT modules just simplify the configuration and add auto-imports. They're not much different than the above. ## Multiple Apps Since web extensions usually contain multiple UIs across multiple entrypoints (popup, options, changelog, side panel, content scripts, etc), you'll need to create individual app instances, one per entrypoint. Usually, this means each entrypoint should be a directory with it's own files inside it. Here's the recommended folder structure: ```html 📂 {srcDir}/ 📂 assets/ <---------- Put shared assets here 📄 tailwind.css 📂 components/ 📄 Button.tsx 📂 entrypoints/ 📂 options/ <--------- Use a folder with an index.html file in it 📁 pages/ <--------- A good place to put your router pages if you have them 📄 index.html 📄 App.tsx 📄 main.tsx <--------- Create and mount your app here 📄 style.css <--------- Entrypoint-specific styles 📄 router.ts ``` ## Configuring Routers All frameworks come with routers for building a multi-page app using the URL's path... But web extensions don't work like this. Since HTML files are static, `chrome-extension://{id}/popup.html`, there's no way to change the entire path for routing. Instead, you need to configure the router to run in "hash" mode, where the routing information is a part of the URL's hash, not the path (ie: `popup.html#/` and `popup.html#/account/settings`). Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers: - [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router) - [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode) - [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing) - [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router) ## /docs/guide/essentials/i18n.md # I18n [Chrome Docs](https://developer.chrome.com/docs/extensions/reference/api/i18n) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) This page discusses how to setup internationalization using the vanilla `browser.i18n` APIs and mentions some alternatives if you want to use something else. [[toc]] ## Usage 1. Add `default_locale` to your manifest: ```ts export default defineConfig({ manifest: { default_locale: 'en', }, }); ``` 2. Create `messages.json` files in the `public/` directory: ```html 📂 {srcDir}/ 📂 public/ 📂 _locales/ 📂 en/ 📄 messages.json 📂 de/ 📄 messages.json 📂 ko/ 📄 messages.json ``` ```jsonc // public/_locales/en/messages.json { "helloWorld": { "message": "Hello world!", }, } ``` 3. Get the translation: ```ts browser.i18n.getMessage('helloWorld'); ``` 4. _Optional_: Add translations for extension name and description: ```json { "extName": { "message": "..." }, "extDescription": { "message": "..." }, "helloWorld": { "message": "Hello world!" } } ``` ```ts export default defineConfig({ manifest: { name: '__MSG_extName__', description: '__MSG_extDescription__', default_locale: 'en', }, }); ``` ## Alternatives The vanilla API has very few features, which is why you may want to consider using third-party NPM packages like `i18next`, `react-i18n`, `vue-i18n`, etc. However, it is recommended you stick with the vanilla API (or a package based on top of the vanilla API, like [`@wxt-dev/i18n`](/i18n)), because: - They can localize text in your manifest and CSS files - Translations are loaded synchronously - Translations are not bundled multiple times, keeping your extension small - Zero configuration However, there is one major downside to the vanilla API and any packages built on top of it: - Language cannot be changed without changing your browser/system language Here are some examples of how to setup a third party i18n library: - [vue-i18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n) ## /docs/guide/essentials/messaging.md # Messaging [Chrome Docs](https://developer.chrome.com/docs/extensions/develop/concepts/messaging) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#communicating_with_background_scripts) Read the docs linked above to learn more about using the vanilla messaging APIs. ## Alternatives The vanilla APIs are difficult to use and are a pain point to many new extension developers. For this reason, WXT recommends installing an NPM package that wraps around the vanilla APIs. Here are some popular messaging libraries that support all browsers and work with WXT: - [`trpc-chrome`](https://www.npmjs.com/package/trpc-chrome) - [tRPC](https://trpc.io/) adapter for Web Extensions. - [`webext-bridge`](https://www.npmjs.com/package/webext-bridge) - Messaging in WebExtensions made super easy. Out of the box. - [`@webext-core/messaging`](https://www.npmjs.com/package/@webext-core/messaging) - Light weight, type-safe wrapper around the web extension messaging APIs - [`@webext-core/proxy-service`](https://www.npmjs.com/package/@webext-core/proxy-service) - A type-safe wrapper around the web extension messaging APIs that lets you call a function from anywhere, but execute it in the background. - [`Comctx`](https://github.com/molvqingtai/comctx) - Cross-context RPC solution with type safety and flexible adapters. ## /docs/guide/essentials/project-structure.md # Project Structure WXT follows a strict project structure. By default, it's a flat folder structure that looks like this: ```html 📂 {rootDir}/ 📁 .output/ 📁 .wxt/ 📁 assets/ 📁 components/ 📁 composables/ 📁 entrypoints/ 📁 hooks/ 📁 modules/ 📁 public/ 📁 utils/ 📄 .env 📄 .env.publish 📄 app.config.ts 📄 package.json 📄 tsconfig.json 📄 web-ext.config.ts 📄 wxt.config.ts ``` Here's a brief summary of each of these files and directories: - `.output/`: All build artifacts will go here - `.wxt/`: Generated by WXT, it contains TS config - `assets/`: Contains all CSS, images, and other assets that should be processed by WXT - `components/`: Auto-imported by default, contains UI components - `composables/`: Auto-imported by default, contains source code for your project's composable functions for Vue - `entrypoints/`: Contains all the entrypoints that get bundled into your extension - `hooks/`: Auto-imported by default, contains source code for your project's hooks for React and Solid - `modules/`: Contains [local WXT Modules](/guide/essentials/wxt-modules) for your project - `public/`: Contains any files you want to copy into the output folder as-is, without being processed by WXT - `utils/`: Auto-imported by default, contains generic utilities used throughout your project - `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables) - `.env.publish`: Contains Environment Variables for [publishing](/guide/essentials/publishing) - `app.config.ts`: Contains [Runtime Config](/guide/essentials/config/runtime) - `package.json`: The standard file used by your package manager - `tsconfig.json`: Config telling TypeScript how to behave - `web-ext.config.ts`: Configure [Browser Startup](/guide/essentials/config/browser-startup) - `wxt.config.ts`: The main config file for WXT projects ## Adding a `src/` Directory Many developers like having a `src/` directory to separate source code from configuration files. You can enable it inside the `wxt.config.ts` file: ```ts [wxt.config.ts] export default defineConfig({ srcDir: 'src', }); ``` After enabling it, your project structure should look like this: ```html 📂 {rootDir}/ 📁 .output/ 📁 .wxt/ 📁 modules/ 📁 public/ 📂 src/ 📁 assets/ 📁 components/ 📁 composables/ 📁 entrypoints/ 📁 hooks/ 📁 utils/ 📄 app.config.ts 📄 .env 📄 .env.publish 📄 package.json 📄 tsconfig.json 📄 web-ext.config.ts 📄 wxt.config.ts ``` ## Customizing Other Directories You can configure the following directories: ```ts [wxt.config.ts] export default defineConfig({ // Relative to project root srcDir: "src", // default: "." modulesDir: "wxt-modules", // default: "modules" outDir: "dist", // default: ".output" publicDir: "static", // default: "public" // Relative to srcDir entrypointsDir: "entries", // default: "entrypoints" }) ``` You can use absolute or relative paths. ## /docs/guide/essentials/publishing.md --- outline: deep --- # Publishing WXT can ZIP your extension and submit it to various stores for review or for self-hosting. ## First Time Publishing If you're publishing an extension to a store for the first time, you must manually navigate the process. WXT doesn't help you create listings, each store has unique steps and requirements that you need to familiarize yourself with. For specific details about each store, see the stores sections below. - [Chrome Web Store](#chrome-web-store) - [Firefox Addon Store](#firefox-addon-store) - [Edge Addons](#edge-addons) ## Automation WXT provides two commands to help automate submitting a new version for review and publishing: - `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command - `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved) To get started, run `wxt submit init` and follow the prompts, or run `wxt submit --help` to view all available options. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates. > In CI, make sure you add all the environment variables to the submit step. To submit a new version for publishing, build all the ZIPs you plan on releasing: ```sh wxt zip wxt zip -b firefox ``` Then run the `wxt submit` command, passing in all the ZIP files you want to release. In this case, we'll do a release for all 3 major stores: Chrome Web Store, Edge Addons, and Firefox Addons Store. If it's your first time running the command or you recently made changes to the release process, you'll want to test your secrets by passing the `--dry-run` flag. ```sh wxt submit --dry-run \ --chrome-zip .output/{your-extension}-{version}-chrome.zip \ --firefox-zip .output/{your-extension}-{version}-firefox.zip --firefox-sources-zip .output/{your-extension}-{version}-sources.zip \ --edge-zip .output/{your-extension}-{version}-chrome.zip ``` If the dry run passes, remove the flag and do the actual release: ```sh wxt submit \ --chrome-zip .output/{your-extension}-{version}-chrome.zip \ --firefox-zip .output/{your-extension}-{version}-firefox.zip --firefox-sources-zip .output/{your-extension}-{version}-sources.zip \ --edge-zip .output/{your-extension}-{version}-chrome.zip ``` :::warning See the [Firefox Addon Store](#firefox-addon-store) section for more details about the `--firefox-sources-zip` option. ::: ## GitHub Action Here's an example of a GitHub Action that submits new versions of an extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings. ```yml name: Release on: workflow_dispatch: jobs: submit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Zip extensions run: | pnpm zip pnpm zip:firefox - name: Submit to stores run: | pnpm wxt submit \ --chrome-zip .output/*-chrome.zip \ --firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip env: CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }} FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }} FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }} ``` The action above lays the foundation for a basic workflow, including `zip` and `submit` steps. To further enhance your GitHub Action and delve into more complex scenarios, consider exploring the following examples from real projects. They introduce advanced features such as version management, changelog generation, and GitHub releases, tailored for different needs: - [`aklinker1/github-better-line-counts`](https://github.com/aklinker1/github-better-line-counts/blob/main/.github/workflows/submit.yml) - Conventional commits, automated version bump and changelog generation, triggered manually, optional dry run for testing - [`GuiEpi/plex-skipper`](https://github.com/GuiEpi/plex-skipper/blob/main/.github/workflows/deploy.yml) - Triggered automatically when `package.json` version is changed, creates and uploads artifacts to GitHub release. > These examples are designed to provide clear insights and are a good starting point for customizing your own workflows. Feel free to explore and adapt them to your project needs. ## Stores ### Chrome Web Store > ✅ Supported • [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) • [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/) To create a ZIP for Chrome: ```sh wxt zip ``` ### Firefox Addon Store > ✅ Supported • [Developer Dashboard](https://addons.mozilla.org/developers/) • [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/) Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/). When running `wxt zip -b firefox`, WXT will zip both your extension and sources. Certain files (such as config files, hidden files, tests, and excluded entrypoints) are automatically excluded from your sources. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension. To customize which files are zipped, add the `zip` option to your config file. ```ts [wxt.config.ts] import { defineConfig } from 'wxt'; export default defineConfig({ zip: { // ... }, }); ``` If it's your first time submitting to the Firefox Addon Store, or if you've updated your project layout, always test your sources ZIP! The commands below should allow you to rebuild your extension from inside the extracted ZIP. :::code-group ```sh [pnpm] pnpm i pnpm zip:firefox ``` ```sh [npm] npm i npm run zip:firefox ``` ```sh [yarn] yarn yarn zip:firefox ``` ```sh [bun] bun i bun zip:firefox ``` ::: Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension. Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources. :::warning If you use a `.env` files, they can affect the chunk hashes in the output directory. Either delete the .env file before running `wxt zip -b firefox`, or include it in your sources zip with the [`zip.includeSources`](/api/reference/wxt/interfaces/InlineConfig#includesources) option. Be careful to not include any secrets in your `.env` files. See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details. ::: #### Private Packages If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip. ```ts [wxt.config.ts] export default defineConfig({ zip: { downloadPackages: [ '@mycompany/some-package', //... ], }, }); ``` Depending on your package manager, the `package.json` in the sources zip will be modified to use the downloaded dependencies via the `overrides` or `resolutions` field. :::warning WXT uses the command `npm pack ` to download the package. That means regardless of your package manager, you need to properly setup a `.npmrc` file. NPM and PNPM both respect `.npmrc` files, but Yarn and Bun have their own ways of authorizing private registries, so you'll need to add a `.npmrc` file. ::: ### Safari > 🚧 Not supported yet WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide: - [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari) - "Convert your existing extension to a Safari web extension using Xcode’s command-line tool." When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory. ```sh pnpm wxt build -b safari xcrun safari-web-extension-converter .output/safari-mv2 ``` ### Edge Addons > ✅ Supported • [Developer Dashboard](https://aka.ms/PartnerCenterLogin) • [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension) No need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP. However, if you have features specifically for Edge, create a separate ZIP with: ```sh wxt zip -b edge ``` ## /docs/guide/essentials/remote-code.md # Remote Code WXT will automatically download and bundle imports with the `url:` prefix so the extension does not depend on remote code, [a requirement from Google for MV3](https://developer.chrome.com/docs/extensions/migrating/improve-security/#remove-remote-code). ## Google Analytics For example, you can import Google Analytics: ```ts // utils/google-analytics.ts import 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX'; window.dataLayer = window.dataLayer || []; // NOTE: This line is different from Google's documentation window.gtag = function () { dataLayer.push(arguments); }; gtag('js', new Date()); gtag('config', 'G-XXXXXX'); ``` Then you can import this in your HTML files to enable Google Analytics: ```ts // popup/main.ts import '~/utils/google-analytics'; gtag('event', 'event_name', { key: 'value', }); ``` ## /docs/guide/essentials/scripting.md # Scripting [Chrome Docs](https://developer.chrome.com/docs/extensions/reference/api/scripting) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting) Refer to the browser docs above for basics on how the API works. ## Execute Script Return Values When using `browser.scripting.executeScript`, you can execute content scripts or unlisted scripts. To return a value, just return a value from the script's `main` function. ```ts // entrypoints/background.ts const res = await browser.scripting.executeScript({ target: { tabId }, files: ['content-scripts/example.js'], }); console.log(res); // "Hello John!" ``` ```ts // entrypoints/example.content.ts export default defineContentScript({ registration: 'runtime', main(ctx) { console.log('Script was executed!'); return 'Hello John!'; }, }); ``` ## /docs/guide/essentials/storage.md # Storage [Chrome Docs](https://developer.chrome.com/docs/extensions/reference/api/storage) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage) You can use the vanilla APIs (see docs above), use [WXT's built-in storage API](/storage), or install a package from NPM. ## Alternatives 1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases 2. DIY: If you're migrating to WXT and already have a storage wrapper, keep using it. In the future, if you want to delete that code, you can use one of these alternatives, but there's no reason to replace working code during a migration. 3. Any other NPM package: [There are lots of wrappers around the storage API](https://www.npmjs.com/search?q=chrome%20storage), you can find one you like. Here's some popular ones: - [`webext-storage`](https://www.npmjs.com/package/webext-storage) - A more usable typed storage API for Web Extensions - [`@webext-core/storage`](https://www.npmjs.com/package/@webext-core/storage) - A type-safe, localStorage-esque wrapper around the web extension storage APIs ## /docs/guide/essentials/target-different-browsers.md # Targeting Different Browsers When building an extension with WXT, you can create multiple builds of your extension targeting different browsers and manifest versions. ## Target a Browser Use the `-b` CLI flag to create a separate build of your extension for a specific browser. By default, `chrome` is targeted. ```sh wxt # same as: wxt -b chrome wxt -b firefox wxt -b custom ``` During development, if you target Firefox, Firefox will open. All other strings open Chrome by default. To customize which browsers open, see [Set Browser Binaries](/guide/essentials/config/browser-startup#set-browser-binaries). Additionally, WXT defines several constants you can use at runtime to detect which browser is in use: ```ts if (import.meta.env.BROWSER === 'firefox') { console.log('Do something only in Firefox builds'); } if (import.meta.env.FIREFOX) { // Shorthand, equivalent to the if-statement above } ``` Read about [Built-in Environment Variables](/guide/essentials/config/environment-variables.html#built-in-environment-variables) for more details. ## Target a Manifest Version To target specific manifest versions, use the `--mv2` or `--mv3` CLI flags. :::tip Default Manifest Version By default, WXT will target MV2 for Safari and Firefox and MV3 for all other browsers. ::: Similar to the browser, you can get the target manifest version at runtime using the [built-in environment variable](/guide/essentials/config/environment-variables.html#built-in-environment-variables): ```ts if (import.meta.env.MANIFEST_VERSION === 2) { console.log('Do something only in MV2 builds'); } ``` ## Filtering Entrypoints Every entrypoint can be included or excluded when targeting specific browsers via the `include` and `exclude` options. Here are some examples: - Content script only built when targeting `firefox`: ```ts export default defineContentScript({ include: ['firefox'], main(ctx) { // ... }, }); ``` - HTML file only built for all targets other than `chrome`: ```html ``` Alternatively, you can use the [`filterEntrypoints` config](/api/reference/wxt/interfaces/InlineConfig#filterentrypoints) to list all the entrypoints you want to build. ## /docs/guide/essentials/testing-updates.md # Testing Updates ## Testing Permission Changes When `permissions`/`host_permissions` change during an update, depending on what exactly changed, the browser will disable your extension until the user accepts the new permissions. You can test if your permission changes will result in a disabled extension: - Chromium: Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool) - Firefox: See their [Test Permission Requests](https://extensionworkshop.com/documentation/develop/test-permission-requests/) page - Safari: Everyone breaks something in production eventually... 🫡 Good luck soldier ## Update Event You can setup a callback that runs after your extension updates like so: ```ts browser.runtime.onInstalled.addListener(({ reason }) => { if (reason === 'update') { // Do something } }); ``` If the logic is simple, write a unit test to cover this logic. If you feel the need to manually test this callback, you can either: 1. In dev mode, remove the `if` statement and reload the extension from `chrome://extensions` 2. Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool) ## /docs/guide/essentials/unit-testing.md # Unit Testing [[toc]] ## Vitest WXT provides first class support for Vitest for unit testing: ```ts // vitest.config.ts import { defineConfig } from 'vitest/config'; import { WxtVitest } from 'wxt/testing'; export default defineConfig({ plugins: [WxtVitest()], }); ``` This plugin does several things: - Polyfills the extension API, `browser`, with an in-memory implementation using [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) - Adds all vite config or plugins in `wxt.config.ts` - Configures auto-imports (if enabled) - Applies internal WXT vite plugins for things like [bundling remote code](/guide/essentials/remote-code) - Sets up global variables provided by WXT (`import.meta.env.BROWSER`, `import.meta.env.MANIFEST_VERSION`, `import.meta.env.IS_CHROME`, etc) - Configures aliases (`@/*`, `@@/*`, etc) so imports can be resolved Here are real projects with unit testing setup. Look at the code and tests to see how they're written. - [`aklinker1/github-better-line-counts`](https://github.com/aklinker1/github-better-line-counts) - [`wxt-dev/examples`'s Vitest Example](https://github.com/wxt-dev/examples/tree/main/examples/vitest-unit-testing) ### Example Tests This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/utils/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension! ```ts import { describe, it, expect } from 'vitest'; import { fakeBrowser } from 'wxt/testing'; const accountStorage = storage.defineItem('local:account'); async function isLoggedIn(): Promise { const value = await accountStorage.getValue(); return value != null; } describe('isLoggedIn', () => { beforeEach(() => { // See https://webext-core.aklinker1.io/fake-browser/reseting-state fakeBrowser.reset(); }); it('should return true when the account exists in storage', async () => { const account: Account = { username: '...', preferences: { // ... }, }; await accountStorage.setValue(account); expect(await isLoggedIn()).toBe(true); }); it('should return false when the account does not exist in storage', async () => { await accountStorage.deleteValue(); expect(await isLoggedIn()).toBe(false); }); }); ``` ### Mocking WXT APIs First, you need to understand how the `#imports` module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path. For example, this is what your write in your source code: ```ts // What you write import { injectScript, createShadowRootUi } from '#imports'; ``` But Vitest sees this: ```ts import { injectScript } from 'wxt/utils/inject-script'; import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; ``` So in this case, if you wanted to mock `injectScript`, you need to pass in `"wxt/utils/inject-script"`, not `"#imports"`. ```ts vi.mock("wxt/utils/inject-script", () => ({ injectScript: ... })) ``` Refer to your project's `.wxt/types/imports-module.d.ts` file to lookup real import paths for `#imports`. If the file doesn't exist, run [`wxt prepare`](/guide/essentials/config/typescript). ## Other Testing Frameworks To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use. It is possible to do, but will require a bit more setup. Refer to Vitest's setup for an example of how to setup a test environment: https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts ## /docs/guide/essentials/wxt-modules.md # WXT Modules WXT provides a "module system" that let's you run code at different steps in the build process to modify it. [[toc]] ## Adding a Module There are two ways to add a module to your project: 1. **NPM**: install an NPM package, like [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) and add it to your config: ```ts [wxt.config.ts] export default defineConfig({ modules: ['@wxt-dev/auto-icons'], }); ``` > Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules. 2. **Local**: add a file to your project's `modules/` directory: ``` / modules/ my-module.ts ``` > To learn more about writing your own modules, read the [Writing Modules](/guide/essentials/wxt-modules) docs. ## Module Options WXT modules may require or allow setting custom options to change their behavior. There are two types of options: 1. **Build-time**: Any config used during the build process, like feature flags 2. **Runtime**: Any config accessed at runtime, like callback functions Build-time options are placed in your `wxt.config.ts`, while runtime options is placed in the [`app.config.ts` file](/guide/essentials/config/runtime). Refer to each module's documentation about what options are required and where they should be placed. If you use TypeScript, modules augment WXT's types so you will get type errors if options are missing or incorrect. ## Execution Order Modules are loaded in the same order as hooks are executed. Refer to the [Hooks documentation](/guide/essentials/config/hooks#execution-order) for more details. ## Writing Modules Here's what a basic WXT module looks like: ```ts import { defineWxtModule } from 'wxt/modules'; export default defineWxtModule({ setup(wxt) { // Your module code here... }, }); ``` Each module's setup function is executed after the `wxt.config.ts` file is loaded. The `wxt` object provides everything you need to write a module: - Use `wxt.hook(...)` to hook into the build's lifecycle and make changes - Use `wxt.config` to get the resolved config from the project's `wxt.config.ts` file - Use `wxt.logger` to log messages to the console - and more! Refer to the [API reference](/api/reference/wxt/interfaces/Wxt) for a complete list of properties and functions available. Also to make sure and read about all the [hooks that are available](https://wxt.dev/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules. ### Recipes Modules are complex and require a deeper understanding of WXT's code and how it works. The best way to learn is by example. #### Update resolved config ```ts import { defineWxtModule } from 'wxt/modules'; export default defineWxtModule({ setup(wxt) { wxt.hook('config:resolved', () => { wxt.config.outDir = 'dist'; }); }, }); ``` #### Add built-time config ```ts import { defineWxtModule } from 'wxt/modules'; import 'wxt'; export interface MyModuleOptions { // Add your build-time options here... } declare module 'wxt' { export interface InlineConfig { // Add types for the "myModule" key in wxt.config.ts myModule: MyModuleOptions; } } export default defineWxtModule({ configKey: 'myModule', // Build time config is available via the second argument of setup setup(wxt, options) { console.log(options); }, }); ``` #### Add runtime config ```ts import { defineWxtModule } from 'wxt/modules'; import 'wxt/utils/define-app-config'; export interface MyModuleRuntimeOptions { // Add your runtime options here... } declare module 'wxt/utils/define-app-config' { export interface WxtAppConfig { myModule: MyModuleOptions; } } ``` Runtime options are returned when calling ```ts const config = useAppConfig(); console.log(config.myModule); ``` This is very useful when [generating runtime code](#generate-runtime-module). #### Generate output file ```ts import { defineWxtModule } from 'wxt/modules'; export default defineWxtModule({ setup(wxt) { // Relative to the output directory const generatedFilePath = 'some-file.txt'; wxt.hook('build:publicAssets', (_, assets) => { assets.push({ relativeDest: generatedFilePath, contents: 'some generated text', }); }); wxt.hook('build:manifestGenerated', (_, manifest) => { manifest.web_accessible_resources ??= []; manifest.web_accessible_resources.push({ matches: ['*://*'], resources: [generatedFilePath], }); }); }, }); ``` This file could then be loaded at runtime: ```ts const res = await fetch(browser.runtime.getURL('/some-text.txt')); ``` #### Add custom entrypoints Once the existing files under the `entrypoints/` directory have been discovered, the `entrypoints:found` hook can be used to add custom entrypoints. :::info The `entrypoints:found` hook is triggered before validation is carried out on the list of entrypoints. Thus, any custom entrypoints will still be checked for duplicate names and logged during debugging. ::: ```ts import { defineWxtModule } from 'wxt/modules'; export default defineWxtModule({ setup(wxt) { wxt.hook('entrypoints:found', (_, entrypointInfos) => { // Add your new entrypoint entrypointInfos.push({ name: 'my-custom-script', inputPath: 'path/to/custom-script.js', type: 'content-script', }); }); }, }); ``` #### Generate runtime module Create a file in `.wxt`, add an alias to import it, and add auto-imports for exported variables. ```ts import { defineWxtModule } from 'wxt/modules'; import { resolve } from 'node:path'; export default defineWxtModule({ imports: [ // Add auto-imports { from: '#analytics', name: 'analytics' }, { from: '#analytics', name: 'reportEvent' }, { from: '#analytics', name: 'reportPageView' }, ], setup(wxt) { const analyticsModulePath = resolve( wxt.config.wxtDir, 'analytics/index.ts', ); const analyticsModuleCode = ` import { createAnalytics } from 'some-module'; export const analytics = createAnalytics(useAppConfig().analytics); export const { reportEvent, reportPageView } = analytics; `; addAlias(wxt, '#analytics', analyticsModulePath); wxt.hook('prepare:types', async (_, entries) => { entries.push({ path: analyticsModulePath, text: analyticsModuleCode, }); }); }, }); ``` #### Generate declaration file ```ts import { defineWxtModule } from 'wxt/modules'; import { resolve } from 'node:path'; export default defineWxtModule({ setup(wxt) { const typesPath = resolve(wxt.config.wxtDir, 'my-module/types.d.ts'); const typesCode = ` // Declare global types, perform type augmentation `; wxt.hook('prepare:types', async (_, entries) => { entries.push({ path: 'my-module/types.d.ts', text: ` // Declare global types, perform type augmentation, etc `, // IMPORTANT - without this line your declaration file will not be a part of the TS project: tsReference: true, }); }); }, }); ``` ### Example Modules You should also look through the code of modules other people have written and published. Here's some examples: - [`@wxt-dev/auto-icons`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons) - [`@wxt-dev/i18n`](https://github.com/wxt-dev/wxt/blob/main/packages/i18n) - [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/blob/main/packages/module-vue) - [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/blob/main/packages/module-solid) - [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/blob/main/packages/module-react) - [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/blob/main/packages/module-svelte) ## /docs/guide/installation.md # Installation Bootstrap a new project, start from scratch, or [migrate an existing project](/guide/resources/migrate). [[toc]] ## Bootstrap Project Run the [`init` command](/api/cli/wxt-init), and follow the instructions. :::code-group ```sh [PNPM] pnpm dlx wxt@latest init ``` ```sh [Bun] bunx wxt@latest init ``` ```sh [NPM] npx wxt@latest init ``` ```sh [Yarn] # Use NPM initially, but select Yarn when prompted npx wxt@latest init ``` ::: :::info Starter Templates: [Vanilla](https://github.com/wxt-dev/wxt/tree/main/templates/vanilla)
[Vue](https://github.com/wxt-dev/wxt/tree/main/templates/vue)
[React](https://github.com/wxt-dev/wxt/tree/main/templates/react)
[Svelte](https://github.com/wxt-dev/wxt/tree/main/templates/svelte)
[Solid](https://github.com/wxt-dev/wxt/tree/main/templates/solid) All templates use TypeScript by default. To use JavaScript, change the file extensions. ::: ### Demo ![wxt init demo](/assets/init-demo.gif) Once you've run the `dev` command, continue to [Next Steps](#next-steps)! ## From Scratch 1. Create a new project :::code-group ```sh [PNPM] cd my-project pnpm init ``` ```sh [Bun] cd my-project bun init ``` ```sh [NPM] cd my-project npm init ``` ```sh [Yarn] cd my-project yarn init ``` ::: 2. Install WXT: :::code-group ```sh [PNPM] pnpm i -D wxt ``` ```sh [Bun] bun i -D wxt ``` ```sh [NPM] npm i -D wxt ``` ```sh [Yarn] yarn add --dev wxt ``` ::: 3. Add an entrypoint, `my-project/entrypoints/background.ts`: :::code-group ```ts export default defineBackground(() => { console.log('Hello world!'); }); ``` ::: 4. Add scripts to your `package.json`: ```json [package.json] { "scripts": { "dev": "wxt", // [!code ++] "dev:firefox": "wxt -b firefox", // [!code ++] "build": "wxt build", // [!code ++] "build:firefox": "wxt build -b firefox", // [!code ++] "zip": "wxt zip", // [!code ++] "zip:firefox": "wxt zip -b firefox", // [!code ++] "postinstall": "wxt prepare" // [!code ++] } } ``` 5. Run your extension in dev mode :::code-group ```sh [PNPM] pnpm dev ``` ```sh [Bun] bun run dev ``` ```sh [NPM] npm run dev ``` ```sh [Yarn] yarn dev ``` ::: WXT will automatically open a browser window with your extension installed. ## Next Steps - Keep reading on about WXT's [Project Structure](/guide/essentials/project-structure) and other essential concepts to learn - Configure [automatic browser startup](/guide/essentials/config/browser-startup) during dev mode - Explore [WXT's example library](/examples) to see how to use specific APIs or perform common tasks - Checkout the [community page](/guide/resources/community) for a list of resources made by the community! ## /docs/guide/introduction.md # Welcome to WXT! WXT is a modern, open-source framework for building web extensions. Inspired by Nuxt, its goals are to: - Provide an awesome [DX](https://about.gitlab.com/topics/devops/what-is-developer-experience/) - Provide first-class support for all major browsers Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extensions. ## Prerequisites These docs assume you have a basic knowledge of how web extensions are structured and how you access the extension APIs. :::warning New to extension development? If you have never written an extension before, follow Chrome's [Hello World tutorial](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world) to first **_create an extension without WXT_**, then come back here. ::: You should also be aware of [Chrome's extension docs](https://developer.chrome.com/docs/extensions) and [Mozilla's extension docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions). WXT does not change how you use the extension APIs, and you'll need to refer to these docs often when using specific APIs.
---
Alright, got a basic understanding of how web extensions are structured? Do you know how to access the extension APIs? Then continue to the [Installation page](/guide/installation) to create your first WXT extension. ## /docs/guide/resources/community.md # Community This page is dedicated to all the awesome people how have made something for WXT or that works with WXT. Blog posts, YouTube videos, NPM packages, etc. If a section doesn't exist for the thing you made, add one! [[toc]] ## Blog Posts - [Building Modern Cross Browser Web Extensions](https://aabidk.dev/tags/wxt/) by Aabid ([@aabidk20](https://github.com/aabidk20)) ## NPM Packages - [`@webext-core/*`](https://webext-core.aklinker1.io/): Easy-to-use utilities for writing and testing web extensions that work on all browsers. - [`Comctx`](https://github.com/molvqingtai/comctx): Cross-context RPC solution with type safety and flexible adapters. ## /docs/guide/resources/compare.md # Compare Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another framework) and [CRXJS](https://crxjs.dev/vite-plugin) (a bundler plugin). ## Overview - ✅ - Full support - 🟡 - Partial support - ❌ - No support | Features | WXT | Plasmo | CRXJS | | ------------------------------------------------------- | :-----: | :-----: | :-----: | | Maintained | ✅ | 🟡 [^n] | 🟡 [^m] | | Supports all browsers | ✅ | ✅ | 🟡 [^j] | | MV2 Support | ✅ | ✅ | 🟡 [^a] | | MV3 Support | ✅ | ✅ | 🟡 [^a] | | Create Extension ZIPs | ✅ | ✅ | ❌ | | Create Firefox Sources ZIP | ✅ | ❌ | ❌ | | First-class TypeScript support | ✅ | ✅ | ✅ | | Entrypoint discovery | ✅ [^b] | ✅ [^b] | ❌ | | Inline entrypoint config | ✅ | ✅ | ❌ [^i] | | Auto-imports | ✅ | ❌ | ❌ | | Reusable module system | ✅ | ❌ | ❌ | | Supports all frontend frameworks | ✅ | 🟡 [^c] | ✅ | | Framework specific entrypoints (like `Popup.tsx`) | 🟡 [^d] | ✅ [^e] | ❌ | | Automated publishing | ✅ | ✅ | ❌ | | Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ | | Unlisted HTML Pages | ✅ | ✅ | ✅ | | Unlisted Scripts | ✅ | ❌ | ❌ | | ESM Content Scripts | ❌ [^l] | ❌ | ✅ | | Dev Mode | | | | | `.env` Files | ✅ | ✅ | ✅ | | Opens browser with extension installed | ✅ | ❌ | ❌ | | HMR for UIs | ✅ | 🟡 [^f] | ✅ | | Reload HTML Files on Change | ✅ | 🟡 [^g] | ✅ | | Reload Content Scripts on Change | ✅ | 🟡 [^g] | ✅ | | Reload Background on Change | 🟡 [^g] | 🟡 [^g] | 🟡 [^g] | | Respects Content Script `run_at` | ✅ | ✅ | ❌ [^h] | | Built-in Wrappers | | | | | Storage | ✅ | ✅ | ❌ [^k] | | Messaging | ❌ [^k] | ✅ | ❌ [^k] | | Content Script UI | ✅ | ✅ | ❌ [^k] | | I18n | ✅ | ❌ | ❌ | [^a]: Either MV2 or MV3, not both. [^b]: File based. [^c]: Only React, Vue, and Svelte. [^d]: `.html`, `.ts`, `.tsx`. [^e]: `.html`, `.ts`, `.tsx`, `.vue`, `.svelte`. [^f]: React only. [^g]: Reloads entire extension. [^h]: ESM-style loaders run asynchronously. [^i]: Entrypoint options all configured in `manifest.json`. [^j]: As of `v2.0.0-beta.23`, but v2 stable hasn't been released yet. [^k]: There is no built-in wrapper around this API. However, you can still access the standard APIs via `chrome`/`browser` globals or use any 3rd party NPM package. [^l]: WIP, moving very slowly. Follow [wxt-dev/wxt#357](https://github.com/wxt-dev/wxt/issues/357) for updates. [^m]: See [crxjs/chrome-extension-tools#974](https://github.com/crxjs/chrome-extension-tools/discussions/974) [^n]: Appears to be in maintenance mode with little to no maintainers nor feature development happening and _(see [wxt-dev/wxt#1404 (comment)](https://github.com/wxt-dev/wxt/pull/1404#issuecomment-2643089518))_ ## /docs/guide/resources/faq.md --- outline: false --- # FAQ Commonly asked questions about how to use WXT or why it behaves the way it does. [[toc]] ## Why aren't content scripts added to the manifest? During development, WXT registers content scripts dynamically so they can be reloaded individually when a file is saved without reloading your entire extension. To list the content scripts registered during development, open the service worker's console and run: ```js await chrome.scripting.getRegisteredContentScripts(); ``` ## How do I disable opening the browser automatically during development? See https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser ## How do I stay logged into a website during development? See https://wxt.dev/guide/essentials/config/browser-startup.html#persist-data ## My component library doesn't work in content scripts! This is usually caused by one of two things (or both) when using `createShadowRootUi`: 1. Styles are added outside the `ShadowRoot` :::details Some component libraries manually add CSS to the page by adding a `