[
  {
    "path": ".codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default:\n        informational: true\n    patch:\n      default:\n        informational: true\n"
  },
  {
    "path": ".commitlintrc.yml",
    "content": "extends:\n  - '@commitlint/config-conventional'\nrules:\n  subject-case:\n    - 0\n    - always\n    - - sentence-case\n      - start-case\n      - pascal-case\n      - upper-case\n"
  },
  {
    "path": ".gitattributes",
    "content": "# See https://git-scm.com/docs/gitattributes#_pattern_format for more about `.gitattributes`.\n\n# Normalize EOL for all files that Git considers text files\n* text=auto eol=lf\n\n# Mark lock files as generated to avoid diffing\npnpm-lock.yaml linguist-generated\npackage-lock.json linguist-generated\nbun.lockb linguist-generated\nyarn.lock linguist-generated\n\n# Exclude templates from language statistics\ntemplates/**/* linguist-vendored\n\n# Other generated files\npackages/browser/src/gen/** linguist-generated\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Set default\n* @aklinker1\n\n# Secure Directories\n/.github/ @aklinker1\n\n# Creator of specific wxt modules\n/packages/auto-icons/ @Timeraa\n/packages/unocss/ @Timeraa\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# 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\n\ngithub: wxt-dev\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"\\U0001F41E Bug report\"\ndescription: Report an issue with WXT\nlabels: [pending-triage]\ntype: Bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Describe the bug\n      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!\n      placeholder: I am doing ... What I expect is ... What actually happening is ...\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Reproduction\n      description: |\n        Please provide a minimal reproduction. This can include:\n\n        - A PR with a failing test case\n        - A link to a github repo\n        - A ZIP you upload to this issue\n\n        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.\n      placeholder: Reproduction URL or attach a ZIP\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: Steps to reproduce\n      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.\n      placeholder: Run `npm install` followed by `npm run dev`\n  - type: textarea\n    id: system-info\n    attributes:\n      label: System Info\n      description: Output of `npx envinfo --system --browsers --binaries --npmPackages wxt,vite`\n      render: shell\n      placeholder: System, Binaries, Browsers\n    validations:\n      required: true\n  - type: dropdown\n    id: package-manager\n    attributes:\n      label: Used Package Manager\n      description: Select the used package manager\n      options:\n        - npm\n        - yarn\n        - pnpm\n        - bun\n    validations:\n      required: true\n  - type: checkboxes\n    id: checkboxes\n    attributes:\n      label: Validations\n      description: Before submitting the issue, please make sure you do the following\n      options:\n        - label: Read the [Contributing Guidelines](https://github.com/wxt-dev/wxt/blob/main/CONTRIBUTING.md).\n          required: true\n        - label: Read the [docs](https://wxt.dev/guide/installation.html).\n          required: true\n        - 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.\n          required: true\n        - 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).\n          required: true\n        - label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Discord Chat\n    url: https://discord.gg/ZFsZqGery9\n    about: Ask questions and discuss with other WXT users in real time.\n  - name: Questions & Discussions\n    url: https://github.com/wxt-dev/wxt/discussions\n    about: Use GitHub discussions for message-board style questions and discussions.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for WXT\ntitle: ''\ntype: Feature\nassignees: ''\n---\n\n### Feature Request\n\nPlease describe your feature, be clear and concise. If you have a proposal for required type or API changes, list them here.\n\n#### Is your feature request related to a bug?\n\nIf so, add a link here. If not, write \"N/A\"\n\n### What are the alternatives?\n\nA clear and concise description of any alternative solutions or features you've considered.\n\n### Additional context\n\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/actions/setup/action.yml",
    "content": "name: Basic Setup\ndescription: Install PNPM, Node, and dependencies\n\ninputs:\n  install:\n    default: 'true'\n    description: Whether or not to run 'pnpm install'\n\n  installArgs:\n    default: ''\n    description: Additional args to append to \"pnpm install\"\n\nruns:\n  using: composite\n\n  steps:\n    - name: 🛠️ Setup PNPM\n      uses: pnpm/action-setup@f2b2b233b538f500472c7274c7012f57857d8ce0 # v4.1.0\n\n    - name: 🛠️ Setup NodeJS\n      uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0\n      with:\n        node-version: 20\n        cache: pnpm\n\n    - name: 📦 Install Dependencies\n      if: ${{ inputs.install == 'true' }}\n      shell: bash\n      run: pnpm install ${{ inputs.installArgs }}\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: npm\n    directory: /\n    schedule:\n      interval: 'monthly'\n  - package-ecosystem: 'github-actions'\n    directory: '/'\n    schedule:\n      interval: 'monthly'\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "### Overview\n\n<!-- Describe your changes and why you made them -->\n\n### Manual Testing\n\n<!-- Describe how to test your changes to make sure the PR works as intended -->\n\n### Related Issue\n\n<!-- If this PR is related to an issue, please link it here -->\n\nThis PR closes #<issue_number>\n"
  },
  {
    "path": ".github/workflows/auto-label.yml",
    "content": "name: ✨ Auto-label PR\n\non:\n  pull_request_target:\n    types: [opened, synchronized, reopened]\n\njobs:\n  update-pr:\n    name: Update PR\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n    steps:\n      - name: Gather Info\n        id: check\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8\n        with:\n          script: |\n            const { data: pr } = await github.rest.pulls.get({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              pull_number: context.payload.pull_request.number\n            });\n\n            // Check if PR has assignees\n            const hasAssignees = pr.assignees && pr.assignees.length > 0;\n            core.setOutput('has_assignees', hasAssignees);\n            core.setOutput('author', pr.user.login);\n\n            // Get list of changed files\n            const { data: files } = await github.rest.pulls.listFiles({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              pull_number: context.payload.pull_request.number\n            });\n\n            // Find all packages that were modified\n            const packagesRegex = /^packages\\/([^\\/]+)\\//;\n            const affectedPackages = new Set();\n\n            for (const file of files) {\n              const match = file.filename.match(packagesRegex);\n              if (match) {\n                affectedPackages.add(match[1]);\n              }\n            }\n\n            const labels = Array.from(affectedPackages).map(pkg => `pkg/${pkg}`);\n            core.setOutput('labels', JSON.stringify(labels));\n            console.log('Detected package labels:', labels);\n\n            // Get current labels on the PR that match pkg/* pattern\n            const currentPkgLabels = pr.labels\n              .map(label => label.name)\n              .filter(name => name.startsWith('pkg/'));\n\n            core.setOutput('current_pkg_labels', JSON.stringify(currentPkgLabels));\n            console.log('Current pkg labels:', currentPkgLabels);\n\n      - name: Sync Author\n        if: steps.check.outputs.has_assignees == 'false'\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8\n        with:\n          script: |\n            await github.rest.issues.addAssignees({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.payload.pull_request.number,\n              assignees: ['${{ steps.check.outputs.author }}']\n            });\n\n            console.log('Assigned PR author: ${{ steps.check.outputs.author }}');\n\n      - name: Sync Labels\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8\n        with:\n          script: |\n            const newLabels = ${{ steps.check.outputs.labels }};\n            const currentLabels = ${{ steps.check.outputs.current_pkg_labels }};\n\n            // Find labels to add (in newLabels but not in currentLabels)\n            const labelsToAdd = newLabels.filter(label => !currentLabels.includes(label));\n\n            // Find labels to remove (in currentLabels but not in newLabels)\n            const labelsToRemove = currentLabels.filter(label => !newLabels.includes(label));\n\n            // Add new labels\n            if (labelsToAdd.length > 0) {\n              await github.rest.issues.addLabels({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.payload.pull_request.number,\n                labels: labelsToAdd\n              });\n              console.log('Added labels:', labelsToAdd);\n            }\n\n            // Remove obsolete labels\n            for (const label of labelsToRemove) {\n              await github.rest.issues.removeLabel({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.payload.pull_request.number,\n                name: label\n              });\n              console.log('Removed label:', label);\n            }\n\n            if (labelsToAdd.length === 0 && labelsToRemove.length === 0) {\n              console.log('No label changes needed');\n            }\n"
  },
  {
    "path": ".github/workflows/notify-unreleased-commits.yml",
    "content": "name: 🔔 Notify Unreleased Commits\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 20 * * 1' # Weekly at 8 PM UTC (3 PM CT)\n\njobs:\n  notify:\n    runs-on: ubuntu-22.04\n    if: github.repository_owner == 'wxt-dev'\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n\n      - name: List Commits\n        id: list-commits\n        run: |\n          ./scripts/list-unreleased-commits.sh > summary.txt\n\n      - name: Discord notification\n        run: |\n          PAYLOAD=$(jq -n --arg content \"${{ env.MESSAGE }}\" '{\"content\": $content}')\n          curl -X POST \\\n            -F \"payload_json=${PAYLOAD}\" \\\n            -F \"file1=@summary.txt\" \\\n            $DISCORD_WEBHOOK\n        env:\n          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_UNRELEASED_COMMITS }}\n          MESSAGE: |\n            If a package needs released, please [run the workflow](<https://github.com/wxt-dev/wxt/actions/workflows/release.yml>).\n\n            Before running, consider:\n            - Are there any breaking changes? If so, prepare a list of breaking changes and update the changelog and release notes after the release.\n            - Are there any PRs open that we wait to release after they're merged?\n"
  },
  {
    "path": ".github/workflows/pkg.pr.new.yml",
    "content": "name: ✨ pkg.pr.new\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    name: Publish Test Packages\n    runs-on: ubuntu-22.04\n    if: ${{ github.repository == 'wxt-dev/wxt' }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Build All Packages\n        run: pnpm buildc all\n\n      - name: Publish\n        run: pnpx pkg-pr-new publish --compact --pnpm './packages/*'\n"
  },
  {
    "path": ".github/workflows/pr-closed.yml",
    "content": "name: 🎉 PR closed\n\non:\n  pull_request_target:\n    types:\n      - closed\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  thank-you:\n    runs-on: ubuntu-latest\n    if: github.event.pull_request.merged == true\n\n    steps:\n      - name: Post Thank You Comment\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        env:\n          comment: Thanks for helping make WXT better!\n        with:\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: process.env.comment\n            })\n"
  },
  {
    "path": ".github/workflows/pr-title.yml",
    "content": "name: 🛡️ Check PR Title\n\non:\n  pull_request:\n    types: [opened, edited]\n\njobs:\n  lint-pr-title:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          # Only fetch the config file from the repository\n          sparse-checkout-cone-mode: false\n          sparse-checkout: .commitlintrc.yml\n\n      - name: Install dependencies\n        run: npm install --global @commitlint/config-conventional commitlint\n\n      - name: Check PR title with commitlint\n        env:\n          PR_TITLE: ${{ github.event.pull_request.title }}\n          HELP_URL: https://github.com/wxt-dev/wxt/blob/main/CONTRIBUTING.md#conventional-pr-titles\n        run: echo \"$PR_TITLE\" | npx commitlint --help-url $HELP_URL\n"
  },
  {
    "path": ".github/workflows/publish-docs.yml",
    "content": "name: 📝 Publish Docs\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: Docker Image Tag\n        required: true\n        default: latest\n\npermissions:\n  contents: read\n\njobs:\n  publish:\n    # Only run if it's the upstream repository, not forks\n    if: github.repository == 'wxt-dev/wxt'\n    name: Publish Docs\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Login to Docker Registry\n        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0\n        with:\n          registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}\n          username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}\n          password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}\n\n      - name: Build docs\n        run: |\n          pnpm docs:build\n          docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}\n\n      - name: Push Image\n        run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}\n\n      - name: Deploy\n        run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: 🚀 Release\non:\n  workflow_dispatch:\n    inputs:\n      package:\n        description: Package to release\n        default: wxt\n        type: choice\n        options:\n          - analytics\n          - auto-icons\n          - i18n\n          - is-background\n          - module-react\n          - module-solid\n          - module-svelte\n          - module-vue\n          - runner\n          - storage\n          - unocss\n          - webextension-polyfill\n          - wxt\n\npermissions:\n  contents: read\n\njobs:\n  validate:\n    name: Validate\n    uses: './.github/workflows/validate.yml'\n    secrets: inherit\n\n  publish:\n    name: Publish\n    runs-on: ubuntu-24.04\n    permissions:\n      contents: write # Push version changes\n      id-token: write # OIDC for NPM publishing\n    needs:\n      - validate\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n          ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Configure Git\n        run: |\n          git config user.name 'github-actions[bot]'\n          git config user.email 'github-actions[bot]@users.noreply.github.com'\n          git config --global push.followTags true\n\n      - name: Bump and Tag\n        run: |\n          pnpm tsx scripts/bump-package-version.ts ${{ inputs.package }}\n          git push\n          git push --tags\n\n      - name: Publish to NPM\n        working-directory: packages/${{ inputs.package }}\n        run: |\n          pnpm pack\n          sudo npm i -g npm@latest\n          /usr/local/bin/npm publish *.tgz\n\n      - name: Create GitHub release\n        run: pnpm tsx scripts/create-github-release.ts ${{ inputs.package }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/sync-releases.yml",
    "content": "name: 🔄 Sync Releases\non:\n  workflow_dispatch:\n    inputs:\n      package:\n        description: Package to sync\n        default: wxt\n        type: choice\n        options:\n          - analytics\n          - auto-icons\n          - i18n\n          - is-background\n          - module-react\n          - module-solid\n          - module-svelte\n          - module-vue\n          - runner\n          - storage\n          - webextension-polyfill\n          - wxt\n\npermissions:\n  contents: read\n\njobs:\n  sync:\n    name: Sync Releases\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n        with:\n          installArgs: --ignore-scripts\n\n      - name: Sync Releases\n        run: pnpm tsx scripts/sync-releases.ts ${{ inputs.package }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/update-browser-package.yml",
    "content": "name: 🔄 Update @wxt-dev/browser\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 0 * * *' # Every day at midnight\n\npermissions:\n  contents: read\n\njobs:\n  sync:\n    name: 'Sync with @types/chrome'\n    runs-on: ubuntu-latest\n    if: github.repository_owner == 'wxt-dev'\n    permissions:\n      contents: write # Push version changes\n      id-token: write # OIDC for NPM publishing\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ssh-key: ${{ secrets.DEPLOY_KEY }}\n\n      - name: Setup\n        uses: ./.github/actions/setup\n        with:\n          installArgs: --ignore-scripts\n\n      - name: Generate Latest Code\n        working-directory: packages/browser\n        run: pnpm gen\n\n      - name: Run Checks\n        working-directory: packages/browser\n        run: pnpm check\n\n      - name: Commit Changes\n        id: commit\n        uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          commit_message: 'fix: Upgrade `@wxt-dev/browser` to latest `@types/chrome` version'\n\n      - name: Publish to NPM\n        if: steps.commit.outputs.changes_detected == 'true'\n        working-directory: packages/browser\n        run: |\n          pnpm pack\n          sudo npm i -g npm@latest\n          /usr/local/bin/npm publish *.tgz\n"
  },
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: 🛡️ Validate\non:\n  workflow_call:\n  pull_request:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read\n\njobs:\n  checks:\n    name: Checks\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Basic Checks\n        run: pnpm check\n\n  builds:\n    name: Builds\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Build All Packages\n        run: pnpm buildc all\n\n  build-demo:\n    name: Build Demo\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Build\n        run: pnpm build:all\n        working-directory: packages/wxt-demo\n\n      - name: ZIP\n        run: pnpm wxt zip\n        working-directory: packages/wxt-demo\n\n  tests:\n    name: Tests (${{ matrix.title }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        include:\n          - title: 'Linux'\n            os: ubuntu-22.04\n            coverage: true\n          - title: 'Windows'\n            os: windows-latest\n            coverage: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2\n\n      - name: Run Tests\n        if: ${{ ! matrix.coverage }}\n        run: pnpm test\n\n      - name: Run Tests (Coverage)\n        if: matrix.coverage\n        run: pnpm test:coverage --reporter=default --reporter=hanging-process\n\n      - name: Upload Coverage\n        if: matrix.coverage\n        uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n  template:\n    name: Template\n    runs-on: ubuntu-22.04\n    strategy:\n      fail-fast: false\n      matrix:\n        template:\n          - react\n          - solid\n          - svelte\n          - vanilla\n          - vue\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Setup\n        uses: ./.github/actions/setup\n\n      - name: Pack WXT package\n        run: pnpm pack\n        working-directory: packages/wxt\n\n      - name: Install Dependencies\n        run: npm i\n        working-directory: templates/${{ matrix.template }}\n\n      - name: Install Packed WXT\n        run: npm i -D ../../packages/wxt/wxt-*.tgz\n        working-directory: templates/${{ matrix.template }}\n\n      - name: Type Check Template\n        run: pnpm compile\n        if: matrix.template != 'svelte'\n        working-directory: templates/${{ matrix.template }}\n\n      - name: Type Check Template\n        run: pnpm check\n        if: matrix.template == 'svelte'\n        working-directory: templates/${{ matrix.template }}\n\n      - name: Build Template\n        run: pnpm build\n        working-directory: templates/${{ matrix.template }}\n"
  },
  {
    "path": ".github/workflows/vhs.yml",
    "content": "name: 📼 VHS\non:\n  push:\n    paths:\n      - 'docs/tapes/*.tape'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  vhs:\n    name: Create VHS\n    runs-on: macos-latest\n    if: ${{ github.repository == 'wxt-dev/wxt' }}\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.ref_name }}\n\n      - name: Setup\n        uses: ./.github/actions/setup\n        with:\n          install: false\n\n      - name: Setup Go\n        uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00\n        with:\n          go-version: '1.25.1'\n\n      # This prevents pnpm dlx from downloading WXT in the video\n      - name: Pre-install WXT\n        run: |\n          pnpm store add wxt@latest\n          pnpm dlx wxt@latest --version\n\n      - name: Record VHS\n        run: |\n          brew install ttyd ffmpeg\n          go install github.com/charmbracelet/vhs@517bcda0faf416728bcf6b7fe489eb0e2469d9b5 # v0.10.0\n          vhs docs/tapes/init-demo.tape\n\n      - name: Save recorded GIF\n        uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          commit_message: 'docs: Update `wxt init` GIF'\n          commit_user_name: github-actions[bot]\n          commit_user_email: github-actions[bot]@users.noreply.github.com\n          commit_author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>\n          # https://github.com/charmbracelet/vhs#output\n          file_pattern: 'docs/assets/*.gif'\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.env\n.env.*\n.idea\n.output\n.webextrc\n.wxt\n.wxt-runner\n*.log\n/docs/.vitepress/cache\ndocs/.vitepress/.temp\ncoverage\ndist\nnode_modules\nTODOs.md\nweb-ext.config.js\nweb-ext.config.ts\ntemplates/*/pnpm-lock.yaml\ntemplates/*/yarn.lock\ntemplates/*/package-lock.json\ndocs/api/reference\nstats.html\n.tool-versions\n.cache\n*-stats.txt\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/DavidAnson/markdownlint/refs/heads/main/schema/markdownlint-config-schema.json\",\n  \"line-length\": false,\n  \"no-inline-html\": false,\n  \"first-line-heading\": false\n}\n"
  },
  {
    "path": ".markdownlintignore",
    "content": "node_modules\n.git\n.output\ndist\n\n# Generated files\npackages/wxt/README.md\npackages/*/CHANGELOG.md\ndocs/api\n"
  },
  {
    "path": ".prettierignore",
    "content": ".output\ncoverage\ndist\n.wxt\ndocs/.vitepress/cache\npnpm-lock.yaml\nCHANGELOG.md\npackages/browser/src/gen\n"
  },
  {
    "path": ".prettierrc.yml",
    "content": "singleQuote: true\nendOfLine: lf\nplugins:\n  - prettier-plugin-jsdoc\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"davidanson.vscode-markdownlint\",\n    \"esbenp.prettier-vscode\",\n    \"github.vscode-github-actions\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  // Set default formatter\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n\n  \"[json]\": { \"editor.defaultFormatter\": \"esbenp.prettier-vscode\" },\n  \"[yaml]\": { \"editor.defaultFormatter\": \"esbenp.prettier-vscode\" },\n  \"[typescript]\": { \"editor.defaultFormatter\": \"esbenp.prettier-vscode\" },\n  \"[markdown]\": { \"editor.defaultFormatter\": \"esbenp.prettier-vscode\" },\n\n  // Additional guidelines for Copilot\n  \"github.copilot.chat.codeGeneration.instructions\": [\n    { \"file\": \"CONTRIBUTING.md\" }\n  ],\n  \"oxc.enable\": true\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n<aaronklinker1@gmail.com>.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\n<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\n<https://www.contributor-covenant.org/faq>. Translations are available at\n<https://www.contributor-covenant.org/translations>.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nEveryone is welcome to contribute to **WXT**!\n\nIf you are changing the docs or fixing a bug, feel free to fork and open a PR.\n\nIf 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.\n\n## WXT's Vision\n\nWXT is two things:\n\n1. A build tool\n2. A set of runtime utilities\n\nThe long term goal of WXT is provide an opinionated build tool that keeps WXT projects standard, while providing light-weight runtime utils that simplify a lot of the boilerplate/overhead when setting up a new extension.\n\nI also want to provide a way for developers to use either one of those two things independently, and not require them to use both. This is why all of WXT's runtime utils are shipped as their own NPM packages, most of them not bundled inside the core `wxt` package. If you just want to use the packages, they're availalbe, and if you just want to use WXT's built tool, you don't have to import any of WXT's utilities, you can use your own.\n\n> The few runtime utils shipped inside WXT are things that should be used by 90% of extensions. That said, they're also legacy utils left in from before I started creating separate NPM packages, and in the future, they may be removed from the core package.\n\n## Conventional Commits\n\nThis 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\n\nHere are some examples of conventional commit messages:\n\n- `feat: add new functionality`\n- `fix: correct typos in code`\n- `ci: add GitHub Actions for automated testing`\n\n## Conventional PR Titles\n\nThe 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.\n\n## Breaking Changes Policy\n\nA quick word on WXT's breaking changes policy. I am willing to make breaking changes, but they have to be for a good enough reason - they have to make WXT better as a whole, they can't be based on one opinion.\n\nBreaking changes also require a major release. Major releases have happened once or twice a year, so after you merge your PR, you'll have to wait a little bit before it is released.\n\nTo make a breaking change:\n\n1. Make sure you're PR is targeting the `major` branch\n2. Add `!` after the conventional commit type (`fix!: ...`, `feat!: ...`, `chore!: ...`, etc) to indicate that it is a breaking change\n3. At the top of the PR, provide documentation that will inform developers about the breaking change, why it was done, and how to migrate their extension so nothing breaks.\n   - This documentation will be put [\"Upgrading WXT\"](https://wxt.dev/guide/resources/upgrading.html) page in the docs, read through previous breaking change docs for an idea of what is required.\n\n## Setup\n\nWXT uses `pnpm`, so make sure you have it installed.\n\n```sh\ncorepack enable\n```\n\nThen, simply run the install command:\n\n```sh\npnpm i\n```\n\n## Development\n\nHere are some helpful commands:\n\n```sh\n# Build WXT package\ncd packages/wxt\npnpm build\n```\n\n```sh\n# Build WXT package, then build demo extension\ncd packages/wxt-demo\npnpm build\n```\n\n```sh\n# Build WXT package, then start the demo extension in dev mode\ncd packages/wxt-demo\npnpm dev\n```\n\n```sh\n# Run unit and E2E tests\npnpm test\n```\n\n```sh\n# Start the docs website locally\npnpm docs:dev\n```\n\n## Profiling\n\n```sh\n# Build the latest version\npnpm --filter wxt build\n\n# CD to the demo directory\ncd packages/wxt-demo\n\n# 1. Generate a flamechart with 0x\npnpm dlx 0x node_modules/wxt/bin/wxt.mjs build\n# 2. Inspect the process with chrome @ chrome://inspect\npnpm node --inspect node_modules/wxt/bin/wxt.mjs build\n```\n\n## Updating Docs\n\nDocumentation is written with VitePress, and is located in the `docs/` directory.\n\nThe 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.\n\n## Testing\n\nWXT 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.\n\n> 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.\n\nTo run tests for a specific file, add the filename at the end of the test command:\n\n```sh\npnpm test manifest-contents\n```\n\nAll test (unit and E2E) for all packages are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).\n\nIf 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.\n\n## Templates\n\nEach 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\n\n```sh\ncd templates/vue\nnpm i\nnpm run dev\nnpm run build\n```\n\nNote 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:\n\n```diff\n  \"devDependencies\": {\n    \"typescript\": \"^5.3.2\",\n    \"vite-plugin-solid\": \"^2.7.0\",\n-   \"wxt\": \"^0.8.0\"\n+   \"wxt\": \"../..\"\n  }\n```\n\nThen run `npm i` again.\n\n### Adding Templates\n\nTo add a template, copy the vanilla template and give it a new name.\n\n```sh\ncp -r templates/vanilla templates/<new-template-name>\n```\n\nThat'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.\n\n## Upgrading Dependencies\n\nWXT has custom rules around what dependencies can be upgraded. Use the `scripts/upgrade-deps.ts` script to upgrade dependencies and follow these rules.\n\n```sh\npnpm tsx scripts/upgrade-deps.ts\n```\n\nTo see all the options, run:\n\n```sh\npnpm tsx scripts/upgrade-deps.ts --help\n```\n\n## Install Unreleased Versions\n\nThis repo uses <https://pkg.pr.new> to publish versions of all it's packages for almost every commit. You can install them via:\n\n```sh\nnpm i https://pkg.pr.new/[package-name]@[ref]\n```\n\nOr use one of the shorthands:\n\n```sh\n# Install the latest build of `wxt` from a PR:\nnpm i https://pkg.pr.new/wxt@1283\n\n# Install the latest build of `@wxt-dev/module-react` on the `main` branch\nnpm i https://pkg.pr.new/@wxt-dev/module-react@main\n\n# Install `@wxt-dev/storage` from a specific commit:\nnpm i https://pkg.pr.new/@wxt-dev/module-react@426f907\n```\n\n## Blog Posts\n\nAnyone is welcome to submit a blog post on <https://wxt.dev/blog>!\n\n> [!NOTE]\n> 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.\n\n- **English only**: Blog posts should be written in English. Unfortunately, our maintainers don't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓\n- **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.\n\n## Become a Maintainer\n\nIf you're interested in becoming a maintainer, send an email to Aaron at <aaronklinker1@gmail.com> with your github username saying you're interested. The process is very informal, I will add you quickly if you've contributed code or answered questions and helped out the community!\n\nMaintainers don't have to just write code - they can manage issues, answer questsions, review PRs, organize and prioritize work - there's lots of ways for you to help out.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Aaron\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "# Maintainers\n\nA couple of things for you to consider before merging a PR or giving the go-ahead on a feature proposal.\n\n## Be picky about new features and packages\n\nWe are responsible for maintaining them and fixing related bugs after the PR is merged. The community can always release their own WXT modules or packages, not everything needs to be built into WXT.\n\n## Prefer standards over customization\n\nDon't merge PRs that just add another way to do something, like [this one](https://github.com/wxt-dev/wxt/pull/2053#issuecomment-3857010196).\n\nWXT is opinionated, if you have questions about what is WXT's opinion or we need to create a new one, create an issue and @ me to discuss.\n\n## PRs should be small and targeted\n\nA PR should make one change. They should not make any unrelated changes outside of accomplishing the one thing. This makes PRs easier to review and they get merged more quickly - a win-win for everyone.\n\n## `@wxt-dev/*` packages are separate\n\nWe can't make changes to these packages assuming people are using them only with WXT. For example, I almost missed [this PR](https://github.com/wxt-dev/wxt/pull/2049#issuecomment-3861251599).\n\nI want these packages to be usable on their own so if people don't like WXT's build tool, they can still use our other awesome packages.\n\n## Minimize dependencies\n\nI don't like how heavy lots of frameworks are. It's unavoidable to a certain extent, but if you can do something without another dependency, try to.\n\nIf you need a dependency, look for one with zero dependencies and that's respectable.\n\n## Look at milestones\n\nI've organized WXT's long term plans into [milestones](https://github.com/wxt-dev/wxt/milestones), check those out to get an idea of my priorities for some of the larger features.\n\n## Require tests\n\nIf someone opens a PR to fix a bug but doesn't include tests, don't merge the PR. Tests are required to prevent regressions and maintain a codebase long term.\n\n## Ask for reproductions for bugs\n\nYou don't need to triage bugs if someone doesn't give you enough information. You can always ask for a repo with a reproduction or wait for more details.\n\nIf there's not an easy way to reproduce a bug, you're wasting your time triaging it. Just be mindful of your own time!\n\nHere's an example of how to ask for a reproduction: <https://github.com/wxt-dev/wxt/issues/2064#issuecomment-3862579110>. You could be more blunt than this.\n\n## Add yourself as a code owner\n\nIf you want to be responsible for a specific package or directory, add yourself to the [`.github/CODEOWNERS`](https://github.com/wxt-dev/wxt/blob/main/.github/CODEOWNERS) file to get added as a reviewer to PRs automatically. You can also add yourself to the default list to be added as a reviewer on all PRs.\n\n## Releasing Package Updates\n\nReleases are done with GitHub actions:\n\n- 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.\n- 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.\n\n## Creating New Packages\n\nExample PR: <https://github.com/wxt-dev/wxt/pull/2152>\n\n1. Create the package.\n\n2. Update CI workflow inputs.\n\n3. Add docs page and version for \"Other Packages\" dropdown.\n\n4. Merge the PR.\n\n5. Tag the commit (look at other tags for pattern):\n\n   ```sh\n   git tag <dir-name>-v<version>\n   git push --tags\n   ```\n\n6. Publish the package to NPM:\n\n   ```sh\n   cd packages/<dir-name>\n   pnpm publish --access public\n   ```\n\n7. Create a basic release on GitHub mentioning the new package is available.\n\nA couple of things to note:\n\n- pkg.pr.new will fail on the original PR. It's fine to ignore and merge your PR as long as it fails due to your new package not being published to NPM yet.\n- The regular release workflow DOES NOT WORK for new packages. You have to have at least one `<dir-name>-v<version>` tag created before you can run that workflow for your new package.\n- You don't need to create a CHANGELOG.md file for the package, it will be created automatically after future changes are released via the normal release workflow.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# <img align=\"top\" width=\"44\" src=\"https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg\" alt=\"WXT Logo\"> WXT\n\n[![npm version](https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048)](https://www.npmjs.com/package/wxt)\n[![downloads](https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048)](https://www.npmjs.com/package/wxt)\n[![license | MIT](https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048)](https://github.com/wxt-dev/wxt/blob/main/LICENSE)\n[![coverage](https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048)](https://codecov.io/github/wxt-dev/wxt)\n\nNext-gen framework for developing web extensions.<br/>⚡<br/><q><i>It's like Nuxt, but for Web Extensions</i></q>\n\n[Get Started](https://wxt.dev/guide/installation.html) •\n[Configuration](https://wxt.dev/api/config.html) •\n[Examples](https://wxt.dev/examples.html) •\n[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) •\n[Discord](https://discord.gg/ZFsZqGery9)\n\n</div>\n\n![Example CLI Output](https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/assets/cli-output.png)\n\n## Demo\n\n<https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94>\n\n## Quick Start\n\nBootstrap a new project:\n\n```sh\n# npm\nnpx wxt@latest init\n\n# pnpm\npnpm dlx wxt@latest init\n\n# bun\nbunx wxt@latest init\n```\n\nOr see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.\n\n## Features\n\n- 🌐 Supports all browsers\n- ✅ Supports both MV2 and MV3\n- ⚡ Dev mode with HMR & fast reload\n- 📂 File based entrypoints\n- 🚔 TypeScript\n- 🦾 Auto-imports\n- 🤖 Automated publishing\n- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc\n- 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions\n- 🖍️ Quickly bootstrap a new project\n- 📏 Bundle analysis\n- ⬇️ Download and bundle remote URL imports\n\n## Sponsors\n\nWXT 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).\n\n[![WXT Sponsors](https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors.svg)](https://github.com/sponsors/wxt-dev)\n\n## Contributors\n\nPublished under the [MIT](https://github.com/wxt-dev/wxt/blob/main/LICENSE) license.\nMade by [@aklinker1](https://github.com/aklinker1) and [community](https://github.com/wxt-dev/wxt/graphs/contributors) 💛\n\n[![WXT contributors](https://contrib.rocks/image?repo=wxt-dev/wxt)](https://github.com/wxt-dev/wxt/graphs/contributors)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nWhile WXT is in prerelease, only the latest version will receive security updates. The latest version is:\n\n<img alt=\"npm version\" src=\"https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048\">\n\n## Reporting a Vulnerability\n\nIf you discover a security vulnerability, please email me at <aaronklinker1@gmail.com>. I will respond within a few days to acknowledge receipt of your report.\n\nIf the vulnerability is accepted, I will open a public issue to track the fix. If the vulnerability is not accepted, no further action will be taken.\n"
  },
  {
    "path": "docs/.vitepress/Dockerfile",
    "content": "FROM lipanski/docker-static-website:latest\nCOPY dist .\n"
  },
  {
    "path": "docs/.vitepress/components/BlogHome.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n// @ts-expect-error: Vitepress data-loader magic, this import is correct\nimport { data } from '../loaders/blog.data';\nimport BlogPostPreview from './BlogPostPreview.vue';\n\nconst posts = computed(() =>\n  data\n    .map((post) => ({\n      ...post,\n      ...post.frontmatter,\n      date: new Date(post.frontmatter.date),\n    }))\n    .sort((a, b) => b.date.getTime() - a.date.getTime()),\n);\n</script>\n\n<template>\n  <div class=\"container\">\n    <div>\n      <div class=\"vp-doc\">\n        <h1>Blog</h1>\n      </div>\n\n      <ul>\n        <BlogPostPreview v-for=\"post of posts\" :key=\"post.url\" :post />\n      </ul>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n.container > div {\n  padding: 32px;\n  max-width: 900px;\n  width: 100%;\n  min-width: 0;\n}\n\nh1 {\n  padding-bottom: 16px;\n}\n\nul {\n  display: flex;\n  flex-direction: column;\n  list-style: none;\n}\nul,\nli {\n  padding: 0;\n  margin: 0;\n}\n\nul li {\n  padding-top: 16px;\n  margin-top: 16px;\n  border-top: 1px solid var(--vp-c-default);\n}\nul li:last-child {\n  padding-bottom: 16px;\n  margin-bottom: 16px;\n  border-bottom: 1px solid var(--vp-c-default);\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/BlogLayout.vue",
    "content": "<script lang=\"ts\" setup>\nimport useBlogDate from '../composables/useBlogDate';\nimport { useData } from 'vitepress';\n\nconst { frontmatter } = useData();\nconst date = useBlogDate(() => frontmatter.value.date);\n</script>\n\n<template>\n  <div class=\"vp-doc\">\n    <main class=\"container-content\">\n      <h1 v-html=\"$frontmatter.title\" />\n      <p class=\"meta-row\">\n        <a\n          class=\"author\"\n          v-for=\"author of $frontmatter.authors\"\n          :key=\"author.github\"\n          :href=\"`https://github.com/${author.github}`\"\n        >\n          <img :src=\"`https://github.com/${author.github}.png?size=96`\" />\n          <span>{{ author.name }}</span>\n        </a>\n        <span>&bull;</span>\n        <span>{{ date }}</span>\n      </p>\n      <Content />\n    </main>\n  </div>\n</template>\n\n<style scoped>\nvp-doc {\n  display: flex;\n}\nmain {\n  max-width: 1080px;\n  padding: 32px;\n  margin: auto;\n}\n@media (min-width: 768px) {\n  main {\n    padding: 64px;\n  }\n}\n.meta-row {\n  display: flex;\n  color: var(--vp-c-text-2);\n  gap: 16px;\n  overflow: hidden;\n  padding-bottom: 32px;\n}\n.meta-row > * {\n  flex-shrink: 0;\n}\n.author {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n  color: var(--vp-c-text-2);\n  font-weight: normal;\n  text-decoration: none;\n}\n.author img {\n  width: 24px;\n  height: 24px;\n  border-radius: 100%;\n}\n.author span {\n  padding: 0;\n  margin: 0;\n}\n.author:hover {\n  text-decoration: underline;\n  color: var(--vp-c-text-2);\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/BlogPostPreview.vue",
    "content": "<script lang=\"ts\" setup>\nimport useBlogDate from '../composables/useBlogDate';\n\nconst props = defineProps<{\n  post: {\n    title: string;\n    description?: string;\n    date: Date;\n    url: string;\n    authors: Array<{ name: string; github: string }>;\n  };\n}>();\n\nconst date = useBlogDate(() => props.post.date);\n</script>\n\n<template>\n  <li class=\"blog-list-item\">\n    <a :href=\"post.url\">\n      <div class=\"vp-doc\">\n        <h3 class=\"title\" v-html=\"post.title\" />\n        <p class=\"description\" v-html=\"post.description\" />\n        <p class=\"meta\">\n          {{ post.authors.map((author) => author.name).join(', ') }}\n          &bull;\n          {{ date }}\n        </p>\n      </div>\n    </a>\n  </li>\n</template>\n\n<style scoped>\nli {\n  padding: 0;\n  margin: 0;\n}\n\np {\n  margin: 0;\n}\nh3 {\n  margin: 0;\n  padding: 0;\n  border: none;\n}\n\nli > a > div {\n  display: flex;\n  flex-direction: column;\n  margin: 0 -16px;\n  padding: 16px;\n  border-radius: 16px;\n}\nli > a > div:hover {\n  background: var(--vp-c-default);\n}\nli .title {\n  color: var(--vp-c-text);\n  margin-bottom: 12px;\n}\nli .description {\n  font-size: 16px;\n  color: var(--vp-c-text-2);\n  margin-bottom: 8px;\n}\nli .meta {\n  font-weight: 400;\n  font-size: 12px;\n  color: var(--vp-c-text-2);\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/EntrypointPatterns.vue",
    "content": "<script lang=\"ts\" setup>\nconst props = defineProps<{\n  patterns: Array<[intput: string, output: string]>;\n}>();\n</script>\n\n<template>\n  <table class=\"no-vertical-dividers\">\n    <thead>\n      <tr>\n        <th style=\"width: 100%\">Filename</th>\n        <th></th>\n        <th>Output Path</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr v-for=\"pattern of patterns\">\n        <td style=\"white-space: nowrap; padding-right: 8px\">\n          <code>entrypoints/{{ pattern[0] }}</code>\n        </td>\n        <td style=\"padding: 6px; opacity: 50%\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"20\"\n            height=\"20\"\n            viewBox=\"0 0 24 24\"\n          >\n            <path\n              fill=\"currentColor\"\n              d=\"M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z\"\n            />\n          </svg>\n        </td>\n        <td style=\"white-space: nowrap; padding-left: 8px\">\n          <code>/{{ pattern[1] }}</code>\n        </td>\n      </tr>\n    </tbody>\n  </table>\n</template>\n"
  },
  {
    "path": "docs/.vitepress/components/ExampleSearch.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref, onMounted, computed, toRaw, Ref } from 'vue';\nimport ExampleSearchFilterByItem from './ExampleSearchFilterByItem.vue';\nimport ExampleSearchResult from './ExampleSearchResult.vue';\nimport { ExamplesMetadata, KeySelectedObject } from '../utils/types';\n\nconst props = defineProps<{\n  tag?: string;\n}>();\n\nconst exampleMetadata = ref<ExamplesMetadata>();\nonMounted(async () => {\n  const res = await fetch(\n    'https://raw.githubusercontent.com/wxt-dev/examples/main/metadata.json',\n  );\n  exampleMetadata.value = await res.json();\n});\n\nconst searchText = ref('');\nconst selectedApis = ref<KeySelectedObject>({});\nconst selectedPermissions = ref<KeySelectedObject>({});\nconst selectedPackages = ref<KeySelectedObject>({});\n\nfunction useRequiredItems(selectedItems: Ref<KeySelectedObject>) {\n  return computed(() =>\n    Array.from(\n      Object.entries(toRaw(selectedItems.value)).reduce(\n        (set, [pkg, checked]) => {\n          if (checked) set.add(pkg);\n          return set;\n        },\n        new Set<string>(),\n      ),\n    ),\n  );\n}\nconst requiredApis = useRequiredItems(selectedApis);\nconst requiredPermissions = useRequiredItems(selectedPermissions);\nconst requiredPackages = useRequiredItems(selectedPackages);\n\nfunction doesExampleMatchSelected(\n  exampleItems: string[],\n  requiredItems: Ref<string[]>,\n) {\n  const exampleItemsSet = new Set(exampleItems);\n  return !requiredItems.value.find((item) => !exampleItemsSet.has(item));\n}\n\nconst filteredExamples = computed(() => {\n  const text = searchText.value.toLowerCase();\n  return exampleMetadata.value.examples.filter((example) => {\n    const matchesText = example.searchText.toLowerCase().includes(text);\n    const matchesApis = doesExampleMatchSelected(example.apis, requiredApis);\n    const matchesPermissions = doesExampleMatchSelected(\n      example.permissions,\n      requiredPermissions,\n    );\n    const matchesPackages = doesExampleMatchSelected(\n      example.packages,\n      requiredPackages,\n    );\n    return matchesText && matchesApis && matchesPermissions && matchesPackages;\n  });\n});\n</script>\n\n<template>\n  <div class=\"example-layout\">\n    <div class=\"search\">\n      <input v-model=\"searchText\" placeholder=\"Search for an example...\" />\n    </div>\n\n    <div class=\"filters\">\n      <ExampleSearchFilterByItem\n        label=\"APIs\"\n        :items=\"exampleMetadata?.allApis\"\n        v-model=\"selectedApis\"\n      />\n      <ExampleSearchFilterByItem\n        label=\"Permissions\"\n        :items=\"exampleMetadata?.allPermissions\"\n        v-model=\"selectedPermissions\"\n      />\n      <ExampleSearchFilterByItem\n        label=\"Packages\"\n        :items=\"exampleMetadata?.allPackages\"\n        v-model=\"selectedPackages\"\n      />\n    </div>\n\n    <div class=\"results\">\n      <p v-if=\"exampleMetadata == null\">Loading examples...</p>\n      <template v-else>\n        <ul class=\"search-results\">\n          <ExampleSearchResult\n            v-for=\"example of filteredExamples\"\n            :key=\"example.name\"\n            :example\n          />\n        </ul>\n        <p v-if=\"filteredExamples.length === 0\">No matching examples</p>\n      </template>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.example-layout {\n  display: grid;\n  grid-template-columns: 1fr;\n  grid-template-rows: 1fr;\n  grid-template-areas:\n    'search'\n    'results';\n  gap: 16px;\n}\n@media only screen and (min-width: 720px) {\n  .example-layout {\n    grid-template-columns: 256px 1fr;\n    grid-template-rows: auto 1fr;\n    grid-template-areas:\n      'filters search'\n      'filters results';\n  }\n}\n.search {\n  grid-area: search;\n  background: var(--vp-c-bg-soft);\n  padding: 20px;\n  width: 100%;\n  display: flex;\n  border-radius: 16px;\n}\n.filters {\n  display: none;\n  grid-area: filters;\n}\n@media only screen and (min-width: 720px) {\n  .filters {\n    display: flex;\n    flex-direction: column;\n    gap: 2px;\n    border-radius: 16px;\n    overflow: hidden;\n    align-self: flex-start;\n  }\n}\n.results {\n  grid-area: results;\n}\n\n.box {\n  border-radius: 16px;\n  overflow: hidden;\n}\n\n.search input {\n  min-width: 0;\n  flex: 1;\n  font-size: 16px;\n}\n\n.checkbox-col {\n  flex: 1;\n  padding: 16px;\n  display: flex;\n  flex-direction: column;\n  overflow-y: auto;\n  max-height: 200px;\n  font-size: 14px;\n  gap: 4px;\n}\n\n.filter-btn {\n  color: var(--vp-c-brand-1);\n}\n\n.checkbox-col .header {\n  font-size: 12px;\n  font-weight: bold;\n  opacity: 50%;\n}\n\n.checkbox-col p {\n  display: flex;\n  gap: 4px;\n  align-items: flex-start;\n  text-wrap: wrap;\n  overflow-wrap: anywhere;\n  line-height: 140%;\n}\n\nspan {\n  padding-top: 1px;\n}\n\n.checkbox-col input[type='checkbox'] {\n  width: 16px;\n  height: 16px;\n  flex-shrink: 0;\n}\n\n.checkbox-col-container {\n  display: flex;\n}\n\n.search-results {\n  display: grid;\n  grid-template-columns: repeat(1, 1fr);\n  gap: 16px;\n}\n@media only screen and (min-width: 800px) {\n  .search-results {\n    grid-template-columns: repeat(2, 1fr);\n  }\n}\n@media only screen and (min-width: 1024px) {\n  .search-results {\n    grid-template-columns: repeat(3, 1fr);\n  }\n}\n\na {\n  background-color: red;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/ExampleSearchFilterByItem.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed, toRaw } from 'vue';\nimport { KeySelectedObject } from '../utils/types';\n\nconst props = defineProps<{\n  label: string;\n  items?: string[];\n}>();\n\nconst selectedItems = defineModel<KeySelectedObject>({\n  required: true,\n});\n\nconst count = computed(() => {\n  return Object.values(toRaw(selectedItems.value)).filter(Boolean).length;\n});\n\nfunction toggleItem(pkg: string) {\n  selectedItems.value = {\n    ...toRaw(selectedItems.value),\n    [pkg]: !selectedItems.value[pkg],\n  };\n}\n</script>\n\n<template>\n  <div class=\"filter-container\">\n    <p class=\"header\">\n      <span>Filter by {{ label }}</span> <span v-if=\"count\">({{ count }})</span>\n    </p>\n    <div class=\"scroll-container\">\n      <ul>\n        <li v-for=\"item in items\">\n          <label :title=\"item\">\n            <input\n              type=\"checkbox\"\n              :checked=\"selectedItems[item]\"\n              @input=\"toggleItem(item)\"\n            />\n            <span>{{ item }}</span>\n          </label>\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.filter-container {\n  height: 300px;\n  display: flex;\n  flex-direction: column;\n  background: var(--vp-c-bg-soft);\n}\n\n.scroll-container {\n  flex: 1;\n  overflow: hidden;\n  position: relative;\n}\n\n.scroll-container ul {\n  position: absolute;\n  overflow-y: auto;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n  font-size: small;\n  padding: 8px 16px 16px 16px;\n}\n\n.header {\n  padding: 8px 16px;\n  font-size: 12px;\n  font-weight: bold;\n  opacity: 50%;\n}\nlabel {\n  display: flex;\n  gap: 4px;\n  align-items: flex-start;\n  text-wrap: wrap;\n  overflow-wrap: anywhere;\n  line-height: 140%;\n  cursor: pointer;\n  text-wrap: nowrap;\n}\nspan {\n  padding-top: 1px;\n}\ninput[type='checkbox'] {\n  width: 16px;\n  height: 16px;\n  flex-shrink: 0;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/ExampleSearchResult.vue",
    "content": "<script lang=\"ts\" setup>\nimport { Example } from '../utils/types';\n\nconst props = defineProps<{\n  example: Example;\n}>();\n</script>\n\n<template>\n  <li>\n    <a :href=\"example.url\" target=\"_blank\">\n      <p class=\"name\">{{ example.name }}</p>\n      <p class=\"description\">{{ example.description }}</p>\n      <p class=\"link\">Open &rarr;</p>\n    </a>\n  </li>\n</template>\n\n<style scoped>\n* {\n  min-width: 0;\n}\na {\n  padding: 16px;\n  display: flex;\n  flex-direction: column;\n  border: 2px solid var(--vp-c-bg-soft);\n  border-radius: 16px;\n  color: var(--vp-c-text-1) !important;\n  gap: 8px;\n}\n\na:hover {\n  outline: 2px solid var(--vp-c-brand-2);\n}\n.name {\n  font-size: 16px;\n  font-weight: 500;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  flex-shrink: 0;\n\n  text-overflow: ellipsis;\n  overflow: hidden;\n  white-space: nowrap;\n}\n.description {\n  height: 53px;\n  opacity: 70%;\n  font-size: 14px;\n  font-weight: normal;\n  line-height: 120%;\n  min-height: 0;\n\n  display: -webkit-box;\n  -webkit-line-clamp: 3;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n\n.link {\n  opacity: 0;\n  transition: 250ms;\n  color: var(--vp-c-brand-2);\n  font-weight: bold;\n  text-align: right;\n}\na:hover .link {\n  opacity: 100%;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/Icon.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n\nconst props = defineProps<{\n  name: string;\n  icon?: string;\n}>();\n\nconst src = computed(() => {\n  if (props.icon) return props.icon;\n  return `https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/${props.name.toLowerCase()}.svg`;\n});\n</script>\n\n<template>\n  <img :src=\"src\" :alt=\"`${name} Logo`\" />\n</template>\n\n<style scoped>\nimg {\n  display: inline;\n  transform: translateY(5px);\n  margin-right: 8px;\n  width: 20px;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/components/UsingWxtSection.vue",
    "content": "<script lang=\"ts\" setup>\nimport { computed } from 'vue';\nimport useListExtensionDetails, {\n  ChromeExtension,\n} from '../composables/useListExtensionDetails';\n\n// Add extension IDs to end of the list. On the website, extensions will be sorted by a combination of weekly active users and rating.\n// Change the commit message or PR title to: \"docs: Added \"[extension name]\" to the homepage\"\nconst chromeExtensionIds = [\n  'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts\n  'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player\n  'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo\n  'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics\n  'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy\n  'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet\n  'oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音\n  'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD\n  'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes\n  'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector\n  'ceicccfeikoipigeghddpocceifjelph', // Plex Skipper\n  'aelkipgppclpfimeamgmlonimflbhlgf', // GitHub Custom Notifier\n  'djnlaiohfaaifbibleebjggkghlmcpcj', // Fluent Read\n  'nhclljcpfmmaiojbhhnkpjcfmacfcian', // Facebook Video Controls\n  'mblkhbaakhbhiimkbcnmeciblfhmafna', // ElemSnap - Quick capture of webpage elements and conversion to images,\n  'oajalfneblkfiejoadecnmodfpnaeblh', // MS Edge TTS (Text to Speech)\n  'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™\n  'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert\n  'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈\n  'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow\n  'fencadnndhdeggodopebjgdfdlhcimfk', // 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding\n  'bnacincmbaknlbegecpioobkfgejlojp', // MaxFocus: Link Preview\n  'bcpgdpedphodjcjlminjbdeejccjbimp', // 汇率转换-中文版本\n  'loeilaonggnalkaiiaepbegccilkmjjp', // Currency Converter Plus\n  'npcnninnjghigjfiecefheeibomjpkak', // Respond Easy\n  'cfkdcideecefncbglkhneoflfnmhoicc', // mindful - stay focused on your goals\n  'lnhejcpclabmbgpiiomjbhalblnnbffg', // 1Proompt\n  'fonflmjnjbkigocpoommgmhljdpljain', // NiceTab - https://github.com/web-dahuyou/NiceTab\n  'fcffekbnfcfdemeekijbbmgmkognnmkd', // Draftly for LinkedIn\n  'nkndldfehcidpejfkokbeghpnlbppdmo', // YouTube Summarized - Summarize any YouTube video\n  'dbichmdlbjdeplpkhcejgkakobjbjalc', // 社媒助手 - https://github.com/iszhouhua/social-media-copilot\n  'opepfpjeogkbgeigkbepobceinnfmjdd', // Dofollow Links for SEO\n  'pdnenlnelpdomajfejgapbdpmjkfpjkp', // ChatGPT Writer: Use AI on Any Site (GPT-4o, Claude, Gemini, and More)\n  'jobnhifpphkgoelnhnopgkdhbdkiadmj', // discord message translator\n  'ncokhechhpjgjonhjnlaneglmdkfkcbj', // Habit Tracker app widget for daily habit tracking\n  'lnjaiaapbakfhlbjenjkhffcdpoompki', // Catppuccin for GitHub File Explorer Icons\n  'cpaedhbidlpnbdfegakhiamfpndhjpgf', // WebChat: Chat with anyone on any website\n  'fcphghnknhkimeagdglkljinmpbagone', // YouTube Auto HD + FPS\n  'lpomjgbicdemjkgmbnkjncgdebogkhlb', // MultiViewer Companion\n  'ggiafipgeeaaahnjamgpjcgkdpanhddg', // Sync Watch - Watch videos together on any site\n  'nmldnjcblcihmegipecakhmnieiofmgl', // Keyword Rank Checker\n  'gppllamhaciichleihemgilcpledblpn', // YouTube Simple View - Hide distractions & more\n  'pccbghdfdnnkkbcdcibchpbffdgednkf', // Propbar - Property Data Enhancer\n  'lfknakglefggmdkjdfhhofkjnnolffkh', // Text Search Pro - Search by case and whole-word match!\n  'mbenhbocjckkbaojacmaepiameldglij', // Invoice Generator\n  'phlfhkmdofajnbhgmbmjkbkdgppgoppb', // Monthly Bill Tracker\n  'macmkmchfoclhpbncclinhjflmdkaoom', // Wandpen - Instantly improve your writing with AI\n  'lhmgechokhmdekdpgkkemoeecelcaonm', // YouTube Hider - Remove Comments By Keywords, Usernames & Tools\n  'imgheieooppmahcgniieddodaliodeeg', // QA Compass - Record standardized bug reports easily\n  'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled\n  'hmdcmlfkchdmnmnmheododdhjedfccka', // Eye Dropper\n  'eihpmapodnppeemkhkbhikmggfojdkjd', // Cursorful - Screen Recorder with Auto Zoom\n  'hjjkgbibknbahijglkffklflidncplkn', // Show IP – Live View of Website IPs for Developers\n  'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator\n  'ocllfkhcdopiafndigclebelbecaiocp', // ZenGram: Mindful Instagram, Your Way\n  'odffpjnpocjfcaclnenaaaddghkgijdb', // Blync: Preview Links, Selection Search, AI Assistant\n  'kofbbilhmnkcmibjbioafflgmpkbnmme', // HTML to Markdown - Convert webpages to markdown\n  'boecmgggeigllcdocgioijmleimjbfkg', // Walmart WFS Profit Calculator\n  'dlnjcbkmomenmieechnmgglgcljhoepd', // Youtube Live Chat Fullscreen\n  'keiealdacakpnbbljlmhfgcebmaadieg', // Python Code Runner\n  'hafcajcllbjnoolpfngclfmmgpikdhlm', // Monochromate\n  'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager\n  'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder\n  'gdjampjdgjmbifnhldgcnccdjkcoicmg', // radiofrance - news & broadcasts (French), music (international)\n  'jlnhphlghikichhgbnkepenehbmloenb', // Blens - Time Tracker and AI Insight\n  'njnammmpdodmfkodnfpammnpdcbhnlcm', // Always Light Mode - Setting website always in light mode\n  'lblmfclcfniabobmamfkdogcgdagbhhb', // DesignPicker - Color Picker & Font Detector\n  'pamnlaoeobcmhkliljfaofekeddpmfoh', // Web to PDF\n  'jmbcbeepjfenihlocplnbmbhimcoooka', // Online CSV Viewer\n  'nkjcoophmpcmmgadnljnlpbpfdfacgbo', // YouTube Video Transcript\n  'lcaieahkjgeggeiihblhcjbbjlppgieh', // NetSuite Record Scripts\n  'gmocfknjllodfiomnljmaehcplnekhlo', // VueTracker\n  'ggcfemmoabhhelfkhknhbnkmeahloiod', // CanCopy - A web extension that allow you to copy any content from website\n  'modkelfkcfjpgbfmnbnllalkiogfofhb', // Language Learning with AI\n  'npfopljnjbamegincfjelhjhnonnjloo', // Bilibili Feed History Helper\n  'edkhpdceeinkcacjdgebjehipmnbomce', // NZBDonkey - The ultimate NZB file download tool\n  'cckggnbnimdbbpmdinkkgbbncopbloob', // WeChat Markdown Editor(微信 Markdown 编辑器)\n  'jcblcjolcojmfopefcighfmkkefbaofg', // Tab Grab\n  'eehmoikadcijkapfjocnhjclpbaindlb', // BrowserLens - https://browserlens.com/\n  'hfhellofkjebbchcdffmicekjdomkcmc', // Epic Games Library Extension\n  'gknigcbhlammoakmmdddkblknanpjiac', // Zen Analytics Pixel Tracker - zapt.web.app\n  'cnklededohhcbmjjdlbjdkkihkgoggol', // Crypto Pulse - Compose your newtab with nature images, widgets & realtime Crypto Price & Bitcoin RSS.\n  'miponnamafdenpgjemkknimgjfibicdc', // Youtube Video Scheduler\n  'nhmbcmalgpkjbomhlhgdicanmkkaajmg', // Chatslator: Livestream Chat Translator\n  'mbamjfdjbcdgpopfnkkmlohadbbnplhm', // 公众号阅读增强器 - https://wxreader.honwhy.wang\n  'hannhecbnjnnbbafffmogdlnajpcomek', // 토탐정\n  'ehboaofjncodknjkngdggmpdinhdoijp', // 2FAS Pass - https://2fas.com/\n  'hnjamiaoicaepbkhdoknhhcedjdocpkd', // Quick Prompt - https://github.com/wenyuanw/quick-prompt\n  'kacblhilkacgfnkjfodalohcnllcgmjd', // Add QR Code Generator Icon Back To Address Bar\n  'fkbdlogfdjmpfepbbbjcgcfbgbcfcnne', // Piwik PRO Tracking Helper\n  'nkbikckldmljjiiajklecmgmajgapbfl', // PIPX - Take Control of Picture-in-Picture, Automatically\n  'hgppdobcpkfkmiegekaglonjajeojmdd', // Browsely - AI-powered browser extension\n  'ehmoihnjgkdimihkhokkmfjdgomohjgm', // Filmbudd Pro - Simple, private – and synced ratings and watch notes across all your devices\n  'alglchohmdikgdjhafiicilegegieafa', // MultiField CopyCat - Copy, Paste & Autofill Web Forms Instantly\n  'aamihahiiogceidpbnfgehacgiecephe', // ChatSight - Add Table of Contents to ChatGPT\n  'cndibmoanboadcifjkjbdpjgfedanolh', // BetterCampus (prev. BetterCanvas)\n  'hinfimgacobnellbncbcpdlpaapcofaa', // Leetcode Fonts - Change fonts in leetcode effortlessly\n  'kbkbfefhhabpkibojinapkkgciiacggg', // TranslateManga - Manga Translator & Manga Tracker\n  'emeakbgdecgmdjgegnejpppcnkcnoaen', // SiteData - Free Website Traffic Checker & Reverse AdSense Tool\n  'gpnckbhgpnjciklpoehkmligeaebigaa', // Livestream Chat Reader - Text-to-Speech for YouTube/Twitch chat\n  'fjlalaedpfcojcfpkgkglbjjbbkofgnl', // ChatGPT Token Counter - Count tokens in real time on chatgpt conversation\n  'fbgblmjbeebanackldpbmpacppflgmlj', // LinuxDo Scripts - 为 linux.do 用户提供了一些增强功能\n  'dfacnjidgbagicaekenjgclfnhdnjjdi', // Zen Virtual Piano - https://zen-piano.web.app/\n  'naeibcegmgpofimedkmfgjgphfhfhlab', // Crypto Pulse price tracker - https://get-crypto-pulse.web.app/\n  'ffglckbhfbfmdkefdmjbhpnffkcmlhdh', // Redirect Web - Automatically redirect pages or open them in another app\n  'eglpfhbhmelampoihamjomgkeobgdofl', // Capture It - Capture & Edit Screenshots\n  'jmghclbfbbapimhbgnpffbimphlpolnm', // Teams Chat Exporter\n  'jdcppdokgfbnhiacbeplahgnciahnhck', // Lofi BGM Player - Free lofi focus music for work & study\n  'cgpmbiiagnehkikhcbnhiagfomajncpa', // Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.\n  'mfjdonmgmgcijagclnkfhmjiblbfjaid', // KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows\n  'dhiekgdaipindoapjmcnpompdknjeijf', // Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers\n  'lapnciffpekdengooeolaienkeoilfeo', // All API Hub – AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub\n  'bhgobenflkkhfcgkikejaaejenoddcmo', // Scrape Similar - Extract data from websites into spreadsheets - https://github.com/zizzfizzix/scrape-similar\n  'kinlknncggaihnhdcalijdmpbhbflalm', // isTrust - https://github.com/Internet-Society-Belgium/isTrust/\n  'ojpakgiekphppgkcdihbjpafobhnhlkp', // Dymo\n  'pmgehhllikbjmadpenhabejhpemplhmd', // Extension Rank Checker - Extension Ranker\n];\n\nconst { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);\nconst sortedExtensions = computed(() => {\n  if (!data.value?.length) return [];\n\n  return [...data.value]\n    .filter((item) => item != null)\n    .map((item) => ({\n      ...item,\n      // Sort based on the user count weighted by the rating\n      sortKey: ((item.rating ?? 5) / 5) * item.weeklyActiveUsers,\n    }))\n    .filter((item) => !!item)\n    .sort((l, r) => r.sortKey - l.sortKey);\n});\n\nfunction getStoreUrl(extension: ChromeExtension) {\n  const url = new URL(extension.storeUrl);\n  url.searchParams.set('utm_source', 'wxt.dev');\n  return url.href;\n}\n</script>\n\n<template>\n  <p v-if=\"isLoading\" style=\"text-align: center; opacity: 50%\">Loading...</p>\n  <p\n    v-else-if=\"err || sortedExtensions.length === 0\"\n    style=\"text-align: center; opacity: 50%\"\n  >\n    Failed to load extension details.\n  </p>\n  <ul v-else>\n    <li\n      v-for=\"extension of sortedExtensions\"\n      :key=\"extension.id\"\n      class=\"relative\"\n    >\n      <img\n        :src=\"extension.iconUrl\"\n        :alt=\"`${extension.name} icon`\"\n        referrerpolicy=\"no-referrer\"\n      />\n      <div class=\"relative\">\n        <a\n          :href=\"getStoreUrl(extension)\"\n          target=\"_blank\"\n          :title=\"extension.name\"\n          class=\"extension-name\"\n          >{{ extension.name }}</a\n        >\n        <p class=\"description\" :title=\"extension.shortDescription\">\n          {{ extension.shortDescription }}\n        </p>\n      </div>\n      <p class=\"user-count\">\n        <span>{{ extension.weeklyActiveUsers.toLocaleString() }} users</span\n        ><template v-if=\"extension.rating != null\"\n          >,\n          <span>{{ extension.rating }} stars</span>\n        </template>\n      </p>\n    </li>\n  </ul>\n  <p class=\"centered pr\">\n    <a\n      href=\"https://github.com/wxt-dev/wxt/edit/main/docs/.vitepress/components/UsingWxtSection.vue\"\n      target=\"_blank\"\n      >Open a PR</a\n    >\n    to add your extension to the list!\n  </p>\n</template>\n\n<style scoped>\nli img {\n  width: 116px;\n  height: 116px;\n  padding: 16px;\n  border-radius: 8px;\n  background-color: var(--vp-c-default-soft);\n}\n\nul {\n  display: grid;\n  grid-template-columns: repeat(1, 1fr);\n  align-items: stretch;\n  gap: 16px;\n  list-style: none;\n  margin: 16px 0;\n  padding: 0;\n}\n@media (min-width: 960px) {\n  ul {\n    grid-template-columns: repeat(2, 1fr);\n  }\n}\n\nli {\n  margin: 0 !important;\n  padding: 16px;\n  display: flex;\n  background-color: var(--vp-c-bg-soft);\n  border-radius: 12px;\n  flex: 1;\n  gap: 24px;\n  align-items: center;\n}\n\n.centered {\n  text-align: center;\n}\n\nli a,\nli .user-count,\nli .description {\n  padding: 0;\n  margin: 0;\n}\nli .user-count {\n  opacity: 70%;\n  font-size: small;\n  position: absolute;\n  bottom: 12px;\n  right: 16px;\n}\n\nli a {\n  display: -webkit-box;\n  -webkit-line-clamp: 1;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  cursor: pointer;\n  padding: 0;\n  margin: 0;\n  text-decoration: none;\n}\nli a:hover {\n  text-decoration: underline;\n}\n\nli div {\n  flex: 1;\n}\n\nli .description {\n  opacity: 90%;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  margin-bottom: 16px;\n}\n\nli .extension-name {\n  font-size: large;\n}\n\n.pr {\n  opacity: 70%;\n}\n\n.relative {\n  position: relative;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/composables/useBlogDate.ts",
    "content": "import { computed, toValue, MaybeRefOrGetter } from 'vue';\n\nconst MONTH_FORMATTER = new Intl.DateTimeFormat(\n  globalThis?.navigator?.language,\n  {\n    month: 'long',\n  },\n);\n\nexport default function (date: MaybeRefOrGetter<Date | string>) {\n  return computed(() => {\n    const d = new Date(toValue(date));\n    return `${MONTH_FORMATTER.format(d)} ${d.getDate()}, ${d.getFullYear()}`;\n  });\n}\n"
  },
  {
    "path": "docs/.vitepress/composables/useListExtensionDetails.ts",
    "content": "import { ref } from 'vue';\n\nexport interface ChromeExtension {\n  id: string;\n  name: string;\n  iconUrl: string;\n  weeklyActiveUsers: number;\n  shortDescription: string;\n  storeUrl: string;\n  rating: number | undefined;\n}\n\nconst operationName = 'WxtDocsUsedBy';\nconst query = `query ${operationName}($ids:[String!]!) {\n  chromeExtensions(ids: $ids) {\n    id\n    name\n    iconUrl\n    weeklyActiveUsers\n    shortDescription\n    storeUrl\n    rating\n  }\n}`;\n\nexport default function (ids: string[]) {\n  const data = ref<ChromeExtension[]>();\n  const err = ref<unknown>();\n  const isLoading = ref(true);\n\n  fetch('https://queue.wxt.dev/api', {\n    method: 'POST',\n    body: JSON.stringify({\n      operationName,\n      query,\n      variables: { ids },\n    }),\n    headers: {\n      'Content-Type': 'application/json',\n    },\n  })\n    .then(async (res) => {\n      isLoading.value = false;\n      const {\n        data: { chromeExtensions },\n      } = await res.json();\n      data.value = chromeExtensions;\n      err.value = undefined;\n    })\n    .catch((error) => {\n      isLoading.value = false;\n      console.error(error);\n      data.value = undefined;\n      err.value = error;\n    });\n\n  return {\n    data,\n    err,\n    isLoading,\n  };\n}\n"
  },
  {
    "path": "docs/.vitepress/config.ts",
    "content": "import { DefaultTheme, defineConfig } from 'vitepress';\nimport typedocSidebar from '../api/reference/typedoc-sidebar.json';\nimport {\n  menuGroup,\n  menuItem,\n  menuRoot,\n  navItem,\n  prepareTypedocSidebar,\n} from './utils/menus';\nimport { meta, script } from './utils/head';\nimport footnote from 'markdown-it-footnote';\nimport { version as wxtVersion } from '../../packages/wxt/package.json';\nimport { version as i18nVersion } from '../../packages/i18n/package.json';\nimport { version as autoIconsVersion } from '../../packages/auto-icons/package.json';\nimport { version as unocssVersion } from '../../packages/unocss/package.json';\nimport { version as storageVersion } from '../../packages/storage/package.json';\nimport { version as analyticsVersion } from '../../packages/analytics/package.json';\nimport { version as runnerVersion } from '../../packages/runner/package.json';\nimport { version as isBackgroundVersion } from '../../packages/is-background/package.json';\nimport addKnowledge from 'vitepress-knowledge';\nimport {\n  groupIconMdPlugin,\n  groupIconVitePlugin,\n  localIconLoader,\n} from 'vitepress-plugin-group-icons';\nimport { Feed } from 'feed';\nimport { writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport llmstxt from 'vitepress-plugin-llms';\n\nconst origin = 'https://wxt.dev';\n\nconst title = 'Next-gen Web Extension Framework';\nconst titleSuffix = ' – WXT';\nconst description =\n  \"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.\";\nconst ogTitle = `${title}${titleSuffix}`;\nconst ogUrl = origin;\nconst ogImage = `${origin}/social-preview.png`;\n\nconst otherPackages = {\n  analytics: analyticsVersion,\n  'auto-icons': autoIconsVersion,\n  i18n: i18nVersion,\n  storage: storageVersion,\n  unocss: unocssVersion,\n  runner: runnerVersion,\n  'is-background': isBackgroundVersion,\n};\n\nconst knowledge = addKnowledge<DefaultTheme.Config>({\n  serverUrl: 'https://knowledge.wxt.dev',\n  paths: {\n    '/': 'docs',\n    '/api/': 'api-reference',\n    '/blog/': 'blog',\n  },\n  layoutSelectors: {\n    blog: '.container-content',\n  },\n  pageSelectors: {\n    'examples.md': '#VPContent > .VPPage',\n    'blog.md': '#VPContent > .VPPage',\n  },\n});\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n  extends: knowledge,\n\n  titleTemplate: `:title${titleSuffix}`,\n  title: 'WXT',\n  description,\n  vite: {\n    clearScreen: false,\n    plugins: [\n      llmstxt(),\n      groupIconVitePlugin({\n        customIcon: {\n          'wxt.config.ts': localIconLoader(\n            import.meta.url,\n            '../public/logo.svg',\n          ),\n        },\n      }),\n    ],\n  },\n  lastUpdated: true,\n  sitemap: {\n    hostname: origin,\n  },\n\n  async buildEnd(site) {\n    // @ts-expect-error: knowledge.buildEnd is not typed, but it exists.\n    await knowledge.buildEnd(site);\n\n    // Only construct the RSS document for production builds\n    const { default: blogDataLoader } = await import('./loaders/blog.data');\n    const posts = await blogDataLoader.load();\n    const feed = new Feed({\n      copyright: 'MIT',\n      id: 'wxt',\n      title: 'WXT Blog',\n      link: `${origin}/blog`,\n    });\n    posts.forEach((post) => {\n      feed.addItem({\n        date: post.frontmatter.date,\n        link: new URL(post.url, origin).href,\n        title: post.frontmatter.title,\n        description: post.frontmatter.description,\n      });\n    });\n    // console.log('rss.xml:');\n    // console.log(feed.rss2());\n    await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');\n  },\n\n  head: [\n    meta('og:type', 'website'),\n    meta('og:title', ogTitle),\n    meta('og:image', ogImage),\n    meta('og:url', ogUrl),\n    meta('og:description', description),\n    meta('twitter:card', 'summary_large_image', { useName: true }),\n    script('https://umami.aklinker1.io/script.js', {\n      'data-website-id': 'c1840c18-a12c-4a45-a848-55ae85ef7915',\n      async: '',\n    }),\n  ],\n\n  markdown: {\n    config: (md) => {\n      md.use(footnote);\n      md.use(groupIconMdPlugin);\n    },\n    languageAlias: {\n      mjs: 'js',\n    },\n  },\n\n  themeConfig: {\n    // https://vitepress.dev/reference/default-theme-config\n    logo: {\n      src: '/logo.svg',\n      alt: 'WXT logo',\n    },\n\n    footer: {\n      message: [\n        '<a class=\"light-netlify\" href=\"https://www.netlify.com\"> <img src=\"https://www.netlify.com/v3/img/components/netlify-color-bg.svg\" alt=\"Deploys by Netlify\" style=\"display: inline;\" /></a>',\n        '<a class=\"dark-netlify\" href=\"https://www.netlify.com\"> <img src=\"https://www.netlify.com/v3/img/components/netlify-color-accent.svg\" alt=\"Deploys by Netlify\" style=\"display: inline;\" /></a>',\n        'Released under the <a href=\"https://github.com/wxt-dev/wxt/blob/main/LICENSE\">MIT License</a>.',\n      ].join('<br/>'),\n      copyright:\n        'Copyright © 2023-present <a href=\"https://github.com/aklinker1\">Aaron Klinker</a>',\n    },\n\n    editLink: {\n      pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path',\n    },\n\n    search: {\n      provider: 'local',\n    },\n\n    socialLinks: [\n      { icon: 'discord', link: 'https://discord.gg/ZFsZqGery9' },\n      { icon: 'github', link: 'https://github.com/wxt-dev/wxt' },\n    ],\n\n    nav: [\n      navItem('Guide', '/guide/installation'),\n      navItem('Examples', '/examples'),\n      navItem('API', '/api/reference/wxt'),\n      navItem('Blog', '/blog'),\n      navItem(`v${wxtVersion}`, [\n        navItem('wxt', [\n          navItem(`v${wxtVersion}`, '/'),\n          navItem(\n            `Changelog`,\n            'https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md',\n          ),\n        ]),\n        navItem(\n          'Other Packages',\n          Object.entries(otherPackages).map(([name, version]) =>\n            navItem(`@wxt-dev/${name} — ${version}`, `/${name}`),\n          ),\n        ),\n      ]),\n    ],\n\n    sidebar: {\n      '/guide/': menuRoot([\n        menuGroup('Get Started', '/guide/', [\n          menuItem('Introduction', 'introduction.md'),\n          menuItem('Installation', 'installation.md'),\n        ]),\n        menuGroup('Essentials', '/guide/essentials/', [\n          menuItem('Project Structure', 'project-structure.md'),\n          menuItem('Entrypoints', 'entrypoints.md'),\n          menuGroup(\n            'Configuration',\n            '/guide/essentials/config/',\n            [\n              menuItem('Manifest', 'manifest.md'),\n              menuItem('Browser Startup', 'browser-startup.md'),\n              menuItem('Auto-imports', 'auto-imports.md'),\n              menuItem('Environment Variables', 'environment-variables.md'),\n              menuItem('Runtime Config', 'runtime.md'),\n              menuItem('Vite', 'vite.md'),\n              menuItem('Build Mode', 'build-mode.md'),\n              menuItem('TypeScript', 'typescript.md'),\n              menuItem('Hooks', 'hooks.md'),\n              menuItem('Entrypoint Loaders', 'entrypoint-loaders.md'),\n            ],\n            true,\n          ),\n          menuItem('Extension APIs', 'extension-apis.md'),\n          menuItem('Assets', 'assets.md'),\n          menuItem('Target Different Browsers', 'target-different-browsers.md'),\n          menuItem('Content Scripts', 'content-scripts.md'),\n          menuItem('Storage', 'storage.md'),\n          menuItem('Messaging', 'messaging.md'),\n          menuItem('I18n', 'i18n.md'),\n          menuItem('Scripting', 'scripting.md'),\n          menuItem('WXT Modules', 'wxt-modules.md'),\n          menuItem('Frontend Frameworks', 'frontend-frameworks.md'),\n          menuItem('ES Modules', 'es-modules.md'),\n          menuItem('Remote Code', 'remote-code.md'),\n          menuItem('Unit Testing', 'unit-testing.md'),\n          menuItem('E2E Testing', 'e2e-testing.md'),\n          menuItem('Publishing', 'publishing.md'),\n          menuItem('Testing Updates', 'testing-updates.md'),\n        ]),\n        menuGroup('Resources', '/guide/resources/', [\n          menuItem('Compare', 'compare.md'),\n          menuItem('FAQ', 'faq.md'),\n          menuItem('Community', 'community.md'),\n          menuItem('Upgrading WXT', 'upgrading.md'),\n          menuItem('Migrate to WXT', 'migrate.md'),\n          menuItem('How WXT Works', 'how-wxt-works.md'),\n        ]),\n      ]),\n      '/api/': menuRoot([\n        menuGroup(\n          'CLI Reference',\n          '/api/cli/',\n          [\n            menuItem('wxt', 'wxt.md'),\n            menuItem('wxt build', 'wxt-build.md'),\n            menuItem('wxt zip', 'wxt-zip.md'),\n            menuItem('wxt prepare', 'wxt-prepare.md'),\n            menuItem('wxt clean', 'wxt-clean.md'),\n            menuItem('wxt init', 'wxt-init.md'),\n            menuItem('wxt submit', 'wxt-submit.md'),\n            menuItem('wxt submit init', 'wxt-submit-init.md'),\n          ],\n          true,\n        ),\n        menuGroup('API Reference', prepareTypedocSidebar(typedocSidebar), true),\n      ]),\n    },\n  },\n});\n"
  },
  {
    "path": "docs/.vitepress/loaders/blog.data.ts",
    "content": "import { createContentLoader } from 'vitepress';\n\nexport default createContentLoader('blog/*.md');\n"
  },
  {
    "path": "docs/.vitepress/loaders/cli.data.ts",
    "content": "import { resolve } from 'node:path';\nimport consola from 'consola';\nimport spawn from 'nano-spawn';\n\nconst cliDir = resolve('packages/wxt/src/cli/commands');\nconst cliDirGlob = resolve(cliDir, '**');\n\nexport default {\n  watch: [cliDirGlob],\n  async load() {\n    consola.info(`Generating CLI docs`);\n\n    const [wxt, build, zip, prepare, clean, init, submit, submitInit] =\n      await Promise.all([\n        getWxtHelp(''),\n        getWxtHelp('build'),\n        getWxtHelp('zip'),\n        getWxtHelp('prepare'),\n        getWxtHelp('clean'),\n        getWxtHelp('init'),\n        getPublishExtensionHelp(''),\n        getPublishExtensionHelp('init'),\n      ]);\n\n    consola.success(`Generated CLI docs`);\n    return {\n      wxt,\n      build,\n      zip,\n      prepare,\n      clean,\n      init,\n      submit,\n      submitInit,\n    };\n  },\n};\n\nasync function getHelp(command: string): Promise<string> {\n  const args = command.split(' ');\n  const res = await spawn(args[0], [...args.slice(1), '--help'], {\n    cwd: 'packages/wxt',\n  });\n  return res.stdout;\n}\n\nfunction getWxtHelp(command: string): Promise<string> {\n  return getHelp(`pnpm -s wxt ${command}`.trim());\n}\n\nasync function getPublishExtensionHelp(command: string): Promise<string> {\n  const res = await getHelp(\n    `./node_modules/.bin/publish-extension ${command}`.trim(),\n  );\n  return res.replace(/\\$ publish-extension/g, '$ wxt submit');\n}\n\nexport interface Command {\n  name: string;\n  docs: string;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/custom.css",
    "content": "/* Colors */\n:root {\n  --wxt-c-green-1: #0b8a00;\n  --wxt-c-green-2: #096600;\n  --wxt-c-green-3: #096600;\n  --wxt-c-green-soft: rgba(11, 138, 0, 0.14);\n}\n\n.dark {\n  --wxt-c-green-1: #67d45e;\n  --wxt-c-green-2: #329929;\n  --wxt-c-green-3: #21651b;\n  --wxt-c-green-soft: rgba(103, 212, 94, 0.14);\n}\n\n/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */\n\n:root {\n  --vp-c-brand-1: var(--wxt-c-green-1);\n  --vp-c-brand-2: var(--wxt-c-green-2);\n  --vp-c-brand-3: var(--wxt-c-green-3);\n  --vp-c-brand-soft: var(--wxt-c-green-soft);\n}\n\n/* Customize Individual Components */\n\n.vp-doc .no-vertical-dividers th,\n.vp-doc .no-vertical-dividers td {\n  border: none;\n}\n\n.vp-doc .no-vertical-dividers tr {\n  border: 1px solid var(--vp-c-divider);\n}\n\nbody {\n  overflow-y: scroll;\n}\n\n.VPSidebar {\n  user-select: none;\n}\n\n.VPSidebarItem .badge {\n  display: inline-block;\n  min-width: 1.6em;\n  padding: 0.3em 0.4em 0.2em;\n  border-radius: 1rem;\n  font-size: 0.75em;\n  line-height: 1;\n  margin-left: 0.5rem;\n  text-align: center;\n  vertical-align: middle;\n  background-color: var(--vp-c-default-2);\n}\n\n.light-netlify {\n  display: inline;\n}\n.dark .light-netlify {\n  display: none;\n}\n.dark-netlify {\n  display: none;\n}\n.dark .dark-netlify {\n  display: inline;\n}\n"
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "content": "import DefaultTheme from 'vitepress/theme';\nimport Icon from '../components/Icon.vue';\nimport EntrypointPatterns from '../components/EntrypointPatterns.vue';\nimport UsingWxtSection from '../components/UsingWxtSection.vue';\nimport ExampleSearch from '../components/ExampleSearch.vue';\nimport BlogLayout from '../components/BlogLayout.vue';\nimport './custom.css';\nimport 'virtual:group-icons.css';\n\nexport default {\n  extends: DefaultTheme,\n  enhanceApp(ctx) {\n    ctx.app\n      .component('Icon', Icon)\n      .component('EntrypointPatterns', EntrypointPatterns)\n      .component('UsingWxtSection', UsingWxtSection)\n      .component('ExampleSearch', ExampleSearch)\n      .component('blog', BlogLayout);\n  },\n};\n"
  },
  {
    "path": "docs/.vitepress/utils/head.ts",
    "content": "import { HeadConfig } from 'vitepress/types/shared';\n\nexport function meta(\n  property: string,\n  content: string,\n  options?: { useName: boolean },\n): HeadConfig {\n  return [\n    'meta',\n    {\n      [options?.useName ? 'name' : 'property']: property,\n      content,\n    },\n  ];\n}\n\nexport function script(\n  src: string,\n  props: Record<string, string> = {},\n): HeadConfig {\n  return [\n    'script',\n    {\n      ...props,\n      src,\n    },\n  ];\n}\n"
  },
  {
    "path": "docs/.vitepress/utils/menus.ts",
    "content": "import { DefaultTheme } from 'vitepress';\n\ntype SidebarItem = DefaultTheme.SidebarItem;\ntype NavItem = DefaultTheme.NavItem;\ntype NavItemWithLink = DefaultTheme.NavItemWithLink;\ntype NavItemWithChildren = DefaultTheme.NavItemWithChildren;\ntype NavItemChildren = DefaultTheme.NavItemChildren;\n\nexport function navItem(text: string): NavItemChildren;\nexport function navItem(text: string, link: string): NavItemChildren;\nexport function navItem(text: string, items: any[]): NavItemWithChildren;\nexport function navItem(text: string, arg2?: unknown): any {\n  if (typeof arg2 === 'string') {\n    return { text, link: arg2 };\n  } else if (Array.isArray(arg2)) {\n    return { text, items: arg2 };\n  }\n  return { text };\n}\n\nexport function menuRoot(items: SidebarItem[]) {\n  return items.map((item, index) => {\n    // item.collapsed = false; // uncomment to expand all level-0 items\n    return item;\n  });\n}\n\nexport function menuGroup(\n  text: string,\n  items: SidebarItem[],\n  collapsible?: boolean,\n): SidebarItem;\nexport function menuGroup(\n  text: string,\n  base: string,\n  items: SidebarItem[],\n  collapsible?: boolean,\n): SidebarItem;\nexport function menuGroup(\n  text: string,\n  a: string | SidebarItem[],\n  b?: SidebarItem[] | boolean,\n  c?: boolean,\n): SidebarItem {\n  if (typeof a === 'string' && Array.isArray(b)) {\n    return {\n      text,\n      base: a,\n      items: b,\n      collapsed: c,\n    };\n  }\n  if (typeof a !== 'string' && !Array.isArray(b))\n    return {\n      text,\n      items: a,\n      collapsed: b,\n    };\n\n  throw Error('Unknown overload');\n}\n\nexport function menuItems(items: SidebarItem[]) {\n  return {\n    items,\n  };\n}\n\nexport function menuItem(\n  text: string,\n  link: string,\n  items?: SidebarItem[],\n): SidebarItem {\n  if (items) {\n    return { text, link, items };\n  }\n  return { text, link };\n}\n\n/** Clean up and add badges to typedoc leaf sections */\nexport function prepareTypedocSidebar(items: SidebarItem[]) {\n  // skip contents file\n  const filtered = items.slice(1);\n\n  // remove Typedoc's collapse: true from text nodes\n  const prepareItems = (items: DefaultTheme.SidebarItem[], depth = 0) => {\n    for (const item of items) {\n      if (item.items) {\n        prepareItems(item.items, depth + 1);\n        const hasLeaves = item.items.some((item) => !item.items);\n        if (hasLeaves) {\n          item.text += ` <span class=\"badge\">${item.items.length}</span>`;\n        }\n      } else {\n        delete item.collapsed;\n      }\n    }\n  };\n\n  // process\n  prepareItems(filtered);\n\n  // return\n  return filtered;\n}\n"
  },
  {
    "path": "docs/.vitepress/utils/types.ts",
    "content": "export interface Example {\n  name: string;\n  description?: string;\n  url: string;\n  searchText: string;\n  apis: string[];\n  permissions: string[];\n  packages: string[];\n}\n\nexport type ExamplesMetadata = {\n  examples: Example[];\n  allApis: string[];\n  allPermissions: string[];\n  allPackages: string[];\n};\n\nexport type KeySelectedObject = Record<string, boolean | undefined>;\n"
  },
  {
    "path": "docs/analytics.md",
    "content": "<!--@include: ../packages/analytics/README.md-->\n"
  },
  {
    "path": "docs/api/cli/wxt-build.md",
    "content": "# `wxt build`\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.build }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt-clean.md",
    "content": "# `wxt clean`\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.clean }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt-init.md",
    "content": "# `wxt init`\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.init }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt-prepare.md",
    "content": "# `wxt prepare`\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.prepare }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt-submit-init.md",
    "content": "# `wxt submit init`\n\n> Alias for [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension)\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.submitInit }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt-submit.md",
    "content": "# `wxt submit`\n\n> Alias for [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension)\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.submit }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt-zip.md",
    "content": "# `wxt zip`\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.zip }}</code></pre></div>\n"
  },
  {
    "path": "docs/api/cli/wxt.md",
    "content": "# `wxt`\n\n<script setup>\nimport { data } from '../../.vitepress/loaders/cli.data.ts'\n</script>\n\n<div class=\"language-sh vp-adaptive-theme active\"><pre class=\"shiki shiki-themes github-light github-dark vp-code\"><code>{{ data.wxt }}</code></pre></div>\n"
  },
  {
    "path": "docs/auto-icons.md",
    "content": "<!--@include: ../packages/auto-icons/README.md-->\n"
  },
  {
    "path": "docs/blog/.drafts/2024-10-19-real-world-messaging.md",
    "content": "---\nlayout: blog\ntitle: Real World Messaging\ndescription: |\n  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.\nauthors:\n  - name: Aaron Klinker\n    github: aklinker1\ndate: 2024-10-20T04:54:23.601Z\n---\n\nTest content **bold** _italic_\n"
  },
  {
    "path": "docs/blog/2024-12-06-using-imports-module.md",
    "content": "---\nlayout: blog\ntitle: Introducing <code>#imports</code>\ndescription: Learn how WXT's new <code>#imports</code> module works and how to use it.\nauthors:\n  - name: Aaron Klinker\n    github: aklinker1\ndate: 2024-12-06T14:39:00.000Z\n---\n\nWXT 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.\n\n<!-- prettier-ignore -->\n```ts\nimport { browser } from 'wxt/browser'; // [!code --]\nimport { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; // [!code --]\nimport { defineContentScript } from 'wxt/utils/define-content-script'; // [!code --]\nimport { injectScript } from 'wxt/utils/inject-script'; // [!code --]\nimport { // [!code ++]\n  browser, createShadowRootUi, defineContentScript, injectScript // [!code ++]\n} from '#imports'; // [!code ++]\n```\n\nThe `#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:\n\n:::code-group\n\n```ts [What you write]\nimport { defineContentScript, injectScript } from '#imports';\n```\n\n```ts [What the bundler sees]\nimport { defineContentScript } from 'wxt/utils/define-content-script';\nimport { injectScript } from 'wxt/utils/inject-script';\n```\n\n:::\n\nThink of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size.\n\nThis enables better tree-shaking compared to v0.19 and below.\n\n:::tip Need to lookup the full import path of an API?\nOpen up your project's `.wxt/types/imports-module.d.ts` file.\n:::\n\n## Mocking\n\nWhen 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.\n\nLet'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`:\n\n```ts\nimport { injectScript } from '#imports';\nimport { vi } from 'vitest';\n\nvi.mock('wxt/utils/inject-script')\nconst injectScriptMock = vi.mocked(injectScript);\n\ninjectScriptMock.mockReturnValueOnce(...);\n```\n\n## Conclusion\n\nYou 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.\n\n- As more APIs are added, you won't have to memorize additional import paths.\n- If breaking changes are made to import paths in future major versions, `#imports` won't break.\n\nHappy Coding 😄\n\n> 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).\n\n---\n\n[Discuss this blog post on Github](https://github.com/wxt-dev/wxt/discussions/1543).\n"
  },
  {
    "path": "docs/blog.md",
    "content": "---\nlayout: page\n---\n\n<script lang=\"ts\" setup>\nimport BlogHome from './.vitepress/components/BlogHome.vue';\n</script>\n\n<BlogHome />\n"
  },
  {
    "path": "docs/examples.md",
    "content": "---\nlayout: page\n---\n\n<style>\n.examples-container {\n  padding: 32px;\n}\n</style>\n\n<div class=\"examples-container\">\n  <div class=\"vp-doc\">\n    <h1>Examples</h1>\n  </div>\n\n  <br />\n\n  <ExampleSearch />\n</div>\n"
  },
  {
    "path": "docs/guide/essentials/assets.md",
    "content": "# Assets\n\n## `/assets` Directory\n\nAny assets imported or referenced inside the `<srcDir>/assets/` directory will be processed by WXT's bundler.\n\nHere's how you access them:\n\n:::code-group\n\n```ts [JS]\nimport imageUrl from '~/assets/image.png';\n\nconst img = document.createElement('img');\nimg.src = imageUrl;\n```\n\n```html [HTML]\n<!-- In HTML tags, you must use the relative path --->\n<img src=\"../assets/image.png\" />\n```\n\n```css [CSS]\n.bg-image {\n  background-image: url(~/assets/image.png);\n}\n```\n\n```vue [Vue]\n<script>\nimport image from '~/assets/image.png';\n</script>\n\n<template>\n  <img :src=\"image\" />\n</template>\n```\n\n```jsx [JSX]\nimport image from '~/assets/image.png';\n\n<img src={image} />;\n```\n\n:::\n\n## `/public` Directory\n\nFiles inside `<rootDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.\n\nHere's how you access them:\n\n:::code-group\n\n```ts [JS]\nimport imageUrl from '/image.png';\n\nconst img = document.createElement('img');\nimg.src = imageUrl;\n```\n\n```html [HTML]\n<img src=\"/image.png\" />\n```\n\n```css [CSS]\n.bg-image {\n  background-image: url(/image.png);\n}\n```\n\n```vue [Vue]\n<template>\n  <img src=\"/image.png\" />\n</template>\n```\n\n```jsx [JSX]\n<img src=\"/image.png\" />\n```\n\n:::\n\n:::warning\nAssets 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).\n:::\n\n## Inside Content Scripts\n\nAssets 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.\n\nBut, inside content scripts, the hostname is whatever the tab is set to. So if you try to fetch the asset, manually or as an `<img>`'s `src`, it will be loaded from the tab's website, not your extension.\n\nTo fix this, you need to convert the image to a full URL using `browser.runtime.getURL`:\n\n```ts [entrypoints/content.ts]\nimport iconUrl from '/icon/128.png';\n\nexport default defineContentScript({\n  matches: ['*://*.google.com/*'],\n  main() {\n    console.log(iconUrl); // \"/icon/128.png\"\n    console.log(browser.runtime.getURL(iconUrl)); // \"chrome-extension://<id>/icon/128.png\"\n  },\n});\n```\n\n## WASM\n\nHow 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.\n\nFor an extension, that means two things:\n\n1. The `.wasm` file needs to be present in output folder so it can be loaded.\n2. You must import the JS API to load and initialize the `.wasm` file, usually provided by the NPM package.\n\nFor 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!\n\nFirst, we need to copy the `.wasm` file to the output directory. We'll do it with a [WXT module](/guide/essentials/wxt-modules):\n\n```ts\n// modules/oxc-parser-wasm.ts\nimport { resolve } from 'node:path';\n\nexport default defineWxtModule((wxt) => {\n  wxt.hook('build:publicAssets', (_, assets) => {\n    assets.push({\n      absoluteSrc: resolve(\n        'node_modules/@oxc-parser/wasm/web/oxc_parser_wasm_bg.wasm',\n      ),\n      relativeDest: 'oxc_parser_wasm_bg.wasm',\n    });\n  });\n});\n```\n\nRun `wxt build`, and you should see the WASM file copied into your `.output/chrome-mv3` folder!\n\nNext, 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`:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  manifest: {\n    web_accessible_resources: [\n      {\n        // We'll use this matches in the content script as well\n        matches: ['*://*.github.com/*'],\n        // Use the same path as `relativeDest` from the WXT module\n        resources: ['/oxc_parser_wasm_bg.wasm'],\n      },\n    ],\n  },\n});\n```\n\nAnd finally, we need to load and initialize the `.wasm` file inside the content script to use it:\n\n```ts [entrypoints/content.ts]\nimport initWasm, { parseSync } from '@oxc-parser/wasm';\n\nexport default defineContentScript({\n  matches: '*://*.github.com/*',\n  async main(ctx) {\n    if (!location.pathname.endsWith('.ts')) return;\n\n    // Get text from GitHub\n    const code = document.getElementById(\n      'read-only-cursor-text-area',\n    )?.textContent;\n    if (!code) return;\n    const sourceFilename = document.getElementById('file-name-id')?.textContent;\n    if (!sourceFilename) return;\n\n    // Load the WASM file:\n    await initWasm({\n      module_or_path: browser.runtime.getURL('/oxc_parser_wasm_bg.wasm'),\n    });\n\n    // Once loaded, we can use `parseSync`!\n    const ast = parseSync(code, { sourceFilename });\n    console.log(ast);\n  },\n});\n```\n\nThis 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.\n\nRun your extension, and you should see OXC parse the TS file!\n"
  },
  {
    "path": "docs/guide/essentials/config/auto-imports.md",
    "content": "# Auto-imports\n\nWXT uses [`unimport`](https://www.npmjs.com/package/unimport), the same tool as Nuxt, to setup auto-imports.\n\n```ts\nexport default defineConfig({\n  // See https://www.npmjs.com/package/unimport#configurations\n  imports: {\n    // ...\n  },\n});\n```\n\nBy default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:\n\n- `<srcDir>/components/*`\n- `<srcDir>/composables/*`\n- `<srcDir>/hooks/*`\n- `<srcDir>/utils/*`\n\nAll named and default exports from files in these directories are available everywhere else in your project without having to import them.\n\nTo 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.\n\n## TypeScript\n\nFor TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare).\n\nAdd this command to your `postinstall` script so your editor has everything it needs to report type errors after installing dependencies:\n\n```jsonc\n// package.json\n{\n  \"scripts\": {\n    \"postinstall\": \"wxt prepare\", // [!code ++]\n  },\n}\n```\n\n## ESLint\n\nESLint 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.\n\n:::code-group\n\n```ts [ESLint 9]\nexport default defineConfig({\n  imports: {\n    eslintrc: {\n      enabled: 9,\n    },\n  },\n});\n```\n\n```ts [ESLint 8]\nexport default defineConfig({\n  imports: {\n    eslintrc: {\n      enabled: 8,\n    },\n  },\n});\n```\n\n:::\n\nThen in your ESLint config, import and use the generated file:\n\n:::code-group\n\n```js [ESLint 9]\n// eslint.config.mjs\nimport autoImports from './.wxt/eslint-auto-imports.mjs';\n\nexport default [\n  autoImports,\n  {\n    // The rest of your config...\n  },\n];\n```\n\n```js [ESLint 8]\n// .eslintrc.mjs\nexport default {\n  extends: ['./.wxt/eslintrc-auto-import.json'],\n  // The rest of your config...\n};\n```\n\n:::\n\n## Disabling Auto-imports\n\nNot all developers like auto-imports. To disable them, set `imports` to `false`.\n\n```ts\nexport default defineConfig({\n  imports: false, // [!code ++]\n});\n```\n\n## Explicit Imports (`#imports`)\n\nYou can manually import all of WXT's APIs via the `#imports` module:\n\n```ts\nimport {\n  createShadowRootUi,\n  ContentScriptContext,\n  MatchPattern,\n} from '#imports';\n```\n\nTo learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module).\n\nIf you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place.\n"
  },
  {
    "path": "docs/guide/essentials/config/browser-startup.md",
    "content": "---\noutline: deep\n---\n\n# Browser Startup\n\n> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.\n\nDuring development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.\n\n## Config Files\n\nYou can configure browser startup in 3 places:\n\n1. `<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers\n\n   ```ts [web-ext.config.ts]\n   import { defineWebExtConfig } from 'wxt';\n\n   export default defineWebExtConfig({\n     // ...\n   });\n   ```\n\n2. `<rootDir>/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control\n3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer\n\n## Recipes\n\n### Set Browser Binaries\n\nTo set or customize the browser opened during development:\n\n```ts [web-ext.config.ts]\nimport { defineWebExtConfig } from 'wxt';\n\nexport default defineWebExtConfig({\n  binaries: {\n    chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome\n    firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox\n    edge: '/path/to/edge', // Open MS Edge when running \"wxt -b edge\"\n  },\n});\n```\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  webExt: {\n    binaries: {\n      chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome\n      firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox\n      edge: '/path/to/edge', // Open MS Edge when running \"wxt -b edge\"\n    },\n  },\n});\n```\n\nBy 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.\n\n### Persist Data\n\nBy default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script.\n\nRight now, Chromium based browsers are the only browsers that support overriding this behavior and persisting data when running the `dev` script multiple times.\n\nTo persist data, set the `--user-data-dir` flag in any of the config files mentioned above:\n\n:::code-group\n\n```ts [Mac/Linux]\nimport { defineWebExtConfig } from 'wxt';\n\nexport default defineWebExtConfig({\n  chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],\n});\n```\n\n```ts [Windows]\nimport { resolve } from 'node:path';\nimport { defineWebExtConfig } from 'wxt';\n\nexport default defineWebExtConfig({\n  // On Windows, the path must be absolute\n  chromiumProfile: resolve('.wxt/chrome-data'),\n  keepProfileChanges: true,\n});\n```\n\n:::\n\nNow, 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.\n\n:::tip\nYou 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.\n:::\n\n### Disable Opening Browser\n\nIf you prefer to load the extension into your browser manually, you can disable the auto-open behavior:\n\n```ts [web-ext.config.ts]\nimport { defineWebExtConfig } from 'wxt';\n\nexport default defineWebExtConfig({\n  disabled: true,\n});\n```\n\n### Enabling Chrome Features\n\nSome APIs are disabled in Chrome during development because of the default flags `web-ext` uses to launch Chrome, like the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api).\n\nIf your extension depends on new features or services, you can enable them via `chromiumArgs`:\n\n```ts\nimport { defineWebExtConfig } from 'wxt';\n\nexport default defineWebExtConfig({\n  chromiumArgs: [\n    // For example, this flag enables the Prompt API\n    '--disable-features=DisableLoadExtensionCommandLineSwitch',\n  ],\n});\n```\n\nThere is no comprehensive list of what feature flags enable what APIs and services.\n\nAlternatively, if you can't find a flag that enables a feature you're looking for, [disable the opening the browser during development](#disable-opening-browser) and use your regular chrome profile for development.\n"
  },
  {
    "path": "docs/guide/essentials/config/build-mode.md",
    "content": "# Build Modes\n\nBecause WXT is powered by Vite, it supports [modes](https://vite.dev/guide/env-and-mode.html#modes) in the same way.\n\nWhen running any dev or build commands, pass the `--mode` flag:\n\n```sh\nwxt --mode production\nwxt build --mode development\nwxt zip --mode testing\n```\n\nBy default, `--mode` is `development` for the dev command and `production` for all other commands (build, zip, etc).\n\n## Get Mode at Runtime\n\nYou can access the current mode in your extension using `import.meta.env.MODE`:\n\n```ts\nswitch (import.meta.env.MODE) {\n  case 'development': // ...\n  case 'production': // ...\n\n  // Custom modes specified with --mode\n  case 'testing': // ...\n  case 'staging': // ...\n  // ...\n}\n```\n"
  },
  {
    "path": "docs/guide/essentials/config/entrypoint-loaders.md",
    "content": "# Entrypoint Loaders\n\nTo 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.\n\nWhen 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.\n\nWXT does several pre-processing steps to try and prevent errors during this process:\n\n1. Use `linkedom` to make a small set of browser globals (`window`, `document`, etc) available.\n2. Use `@webext-core/fake-browser` to create a fake version of the `chrome` and `browser` globals expected by extensions.\n3. Pre-process the JS/TS code, stripping out the `main` function then tree-shaking unused code from the file\n\nHowever, 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!_** See [Entrypoint Limitations](/guide/essentials/extension-apis#entrypoint-limitations) for more details.\n\n:::tip\nIf 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.\n:::\n\nOnce 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.\n"
  },
  {
    "path": "docs/guide/essentials/config/environment-variables.md",
    "content": "# Environment Variables\n\n## Dotenv Files\n\nWXT supports [dotenv files the same way as Vite](https://vite.dev/guide/env-and-mode.html#env-files). Create any of the following files:\n\n```plaintext\n.env\n.env.local\n.env.[mode]\n.env.[mode].local\n.env.[browser]\n.env.[browser].local\n.env.[mode].[browser]\n.env.[mode].[browser].local\n```\n\nAnd any environment variables listed inside them will be available at runtime:\n\n```sh\n# .env\nWXT_API_KEY=...\n```\n\n```ts\nawait fetch(`/some-api?apiKey=${import.meta.env.WXT_API_KEY}`);\n```\n\nRemember 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).\n\n## Built-in Environment Variables\n\nWXT provides some custom environment variables based on the current command:\n\n| Usage                              | Type      | Description                                           |\n| ---------------------------------- | --------- | ----------------------------------------------------- |\n| `import.meta.env.MANIFEST_VERSION` | `2 │ 3`   | The target manifest version                           |\n| `import.meta.env.BROWSER`          | `string`  | The target browser                                    |\n| `import.meta.env.CHROME`           | `boolean` | Equivalent to `import.meta.env.BROWSER === \"chrome\"`  |\n| `import.meta.env.FIREFOX`          | `boolean` | Equivalent to `import.meta.env.BROWSER === \"firefox\"` |\n| `import.meta.env.SAFARI`           | `boolean` | Equivalent to `import.meta.env.BROWSER === \"safari\"`  |\n| `import.meta.env.EDGE`             | `boolean` | Equivalent to `import.meta.env.BROWSER === \"edge\"`    |\n| `import.meta.env.OPERA`            | `boolean` | Equivalent to `import.meta.env.BROWSER === \"opera\"`   |\n\nYou can set the [`targetBrowsers`](/api/reference/wxt/interfaces/InlineConfig#targetbrowsers) option to make the `BROWSER` variable a more specific type, like `\"chrome\" | \"firefox\"`.\n\nYou can also access all of [Vite's environment variables](https://vite.dev/guide/env-and-mode.html#env-variables):\n\n| Usage                  | Type      | Description                                                                 |\n| ---------------------- | --------- | --------------------------------------------------------------------------- |\n| `import.meta.env.MODE` | `string`  | The [mode](/guide/essentials/config/build-mode) the extension is running in |\n| `import.meta.env.PROD` | `boolean` | When `NODE_ENV='production'`                                                |\n| `import.meta.env.DEV`  | `boolean` | Opposite of `import.meta.env.PROD`                                          |\n\n:::details Other Vite Environment Variables\nVite provides two other environment variables, but they aren't useful in WXT projects:\n\n- `import.meta.env.BASE_URL`: Use `browser.runtime.getURL` instead.\n- `import.meta.env.SSR`: Always `false`.\n  :::\n\n## Manifest\n\nTo use environment variables in the manifest, you need to use the function syntax:\n\n```ts\nexport default defineConfig({\n  modules: ['@wxt-dev/module-vue'],\n  manifest: { // [!code --]\n    oauth2: { // [!code --]\n      client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code --]\n    } // [!code --]\n  } // [!code --]\n  manifest: () => ({ // [!code ++]\n    oauth2: { // [!code ++]\n      client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code ++]\n    } // [!code ++]\n  }), // [!code ++]\n});\n```\n\nWXT 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.\n\nNote that Vite's runtime environment variables, like `import.meta.env.DEV`, will not be defined. Instead, access the `mode` like this:\n\n```ts\nexport default defineConfig({\n  manifest: ({ mode }) => {\n    const isDev = mode === 'development';\n    console.log('Is development mode:', isDev);\n\n    // ...\n  },\n});\n```\n"
  },
  {
    "path": "docs/guide/essentials/config/hooks.md",
    "content": "# Hooks\n\nWXT includes a system that lets you hook into the build process and make changes.\n\n## Adding Hooks\n\nThe 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:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  hooks: {\n    'build:manifestGenerated': (wxt, manifest) => {\n      if (wxt.config.mode === 'development') {\n        manifest.name += ' (DEV)';\n      }\n    },\n  },\n});\n```\n\nMost 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.\n\nYou can find the [list of all available hooks](/api/reference/wxt/interfaces/WxtHooks) in the API reference.\n\nPutting 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.\n\n## Execution Order\n\nBecause 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:\n\n1. NPM modules in the order listed in the [`modules` config](/api/reference/wxt/interfaces/InlineConfig#modules)\n2. User modules in [`/modules` folder](/guide/essentials/project-structure), loaded alphabetically\n3. Hooks listed in your `wxt.config.ts`\n\nTo see the order for your project, run `wxt prepare --debug` flag and search for the \"Hook execution order\":\n\n```plaintext\n⚙ Hook execution order:\n⚙   1. wxt:built-in:unimport\n⚙   2. src/modules/auto-icons.ts\n⚙   3. src/modules/example.ts\n⚙   4. src/modules/i18n.ts\n⚙   5. wxt.config.ts > hooks\n```\n\nChanging execution order is simple:\n\n- Prefix your user modules with a number (lower numbers are loaded first):\n  <!-- prettier-ignore -->\n  ```html\n  📁 modules/\n     📄 0.my-module.ts\n     📄 1.another-module.ts\n  ```\n\n- If you need to run an NPM module after user modules, just make it a user module and prefix the filename with a number!\n\n  ```ts\n  // modules/2.i18n.ts\n  export { default } from '@wxt-dev/i18n/module';\n  ```\n"
  },
  {
    "path": "docs/guide/essentials/config/manifest.md",
    "content": "# Manifest\n\nIn WXT, there is no `manifest.json` file in your source code. Instead, WXT generates the manifest from multiple sources:\n\n- Global options [defined in your `wxt.config.ts` file](#global-options)\n- Entrypoint-specific options [defined in your entrypoints](/guide/essentials/entrypoints#defining-manifest-options)\n- [WXT Modules](/guide/essentials/wxt-modules) added to your project can modify your manifest\n- [Hooks](/guide/essentials/config/hooks) defined in your project can modify your manifest\n\nYour extension's `manifest.json` will be output to `.output/{target}/manifest.json` when running `wxt build`.\n\n## Global Options\n\nTo add a property to your manifest, use the `manifest` config inside your `wxt.config.ts`:\n\n```ts\nexport default defineConfig({\n  manifest: {\n    // Put manual changes here\n  },\n});\n```\n\nYou can also define the manifest as a function, and use JS to generate it based on the target browser, mode, and more.\n\n```ts\nexport default defineConfig({\n  manifest: ({ browser, manifestVersion, mode, command }) => {\n    return {\n      // ...\n    };\n  },\n});\n```\n\n### MV2 and MV3 Compatibility\n\nWhen 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.\n\nFor example, for this config:\n\n```ts\nexport default defineConfig({\n  manifest: {\n    action: {\n      default_title: 'Some Title',\n    },\n    web_accessible_resources: [\n      {\n        matches: ['*://*.google.com/*'],\n        resources: ['icon/*.png'],\n      },\n    ],\n  },\n});\n```\n\nWXT will generate the following manifests:\n\n:::code-group\n\n```json [MV2]\n{\n  \"manifest_version\": 2,\n  // ...\n  \"browser_action\": {\n    \"default_title\": \"Some Title\"\n  },\n  \"web_accessible_resources\": [\"icon/*.png\"]\n}\n```\n\n```json [MV3]\n{\n  \"manifest_version\": 3,\n  // ...\n  \"action\": {\n    \"default_title\": \"Some Title\"\n  },\n  \"web_accessible_resources\": [\n    {\n      \"matches\": [\"*://*.google.com/*\"],\n      \"resources\": [\"icon/*.png\"]\n    }\n  ]\n}\n```\n\n:::\n\nYou can also specify properties specific to a single manifest version, and they will be stripped out when targeting the other manifest version.\n\n## Name\n\n> [Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/name/)\n\nIf not provided via the `manifest` config, the manifest's `name` property defaults to your `package.json`'s `name` property.\n\n## Version and Version Name\n\n> [Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/version/)\n\nYour extension's `version` and `version_name` is based on the `version` from your `package.json`.\n\n- `version_name` is the exact string listed\n- `version` is the string cleaned up, with any invalid suffixes removed\n\nExample:\n\n```json\n// package.json\n{\n  \"version\": \"1.3.0-alpha2\"\n}\n```\n\n```json\n// .output/<target>/manifest.json\n{\n  \"version\": \"1.3.0\",\n  \"version_name\": \"1.3.0-alpha2\"\n}\n```\n\nIf a version is not present in your `package.json`, it defaults to `\"0.0.0\"`.\n\n## Icons\n\nWXT automatically discovers your extension's icon by looking at files in the `public/` directory:\n\n```plaintext\npublic/\n├─ icon-16.png\n├─ icon-24.png\n├─ icon-48.png\n├─ icon-96.png\n└─ icon-128.png\n```\n\nSpecifically, an icon must match one of these regex to be discovered:\n\n<<< @/../packages/wxt/src/core/utils/manifest.ts#snippet\n\nIf 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:\n\n```ts\nexport default defineConfig({\n  manifest: {\n    icons: {\n      16: '/extension-icon-16.png',\n      24: '/extension-icon-24.png',\n      48: '/extension-icon-48.png',\n      96: '/extension-icon-96.png',\n      128: '/extension-icon-128.png',\n    },\n  },\n});\n```\n\nAlternatively, 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.\n\n## Permissions\n\n> [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/)\n\nMost of the time, you need to manually add permissions to your manifest. Only in a few specific situations are permissions added automatically:\n\n- During development: the `tabs` and `scripting` permissions will be added to enable hot reloading.\n- When a `sidepanel` entrypoint is present: The `sidepanel` permission is added.\n\n```ts\nexport default defineConfig({\n  manifest: {\n    permissions: ['storage', 'tabs'],\n  },\n});\n```\n\n## Host Permissions\n\n> [Chrome docs](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions)\n\n```ts\nexport default defineConfig({\n  manifest: {\n    host_permissions: ['https://www.google.com/*'],\n  },\n});\n```\n\n:::warning\nIf you use host permissions and target both MV2 and MV3, make sure to only include the required host permissions for each version:\n\n```ts\nexport default defineConfig({\n  manifest: ({ manifestVersion }) => ({\n    host_permissions: manifestVersion === 2 ? [...] : [...],\n  }),\n});\n```\n\n:::\n\n## Default Locale\n\n```ts\nexport default defineConfig({\n  manifest: {\n    name: '__MSG_extName__',\n    description: '__MSG_extDescription__',\n    default_locale: 'en',\n  },\n});\n```\n\n> See [I18n docs](/guide/essentials/i18n) for a full guide on internationalizing your extension.\n\n## Actions\n\nIn 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.\n\nBy default, whenever an `action` is generated, WXT falls back to `browser_action` when targeting MV2.\n\n### Action With Popup\n\nTo 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:\n\n```html\n<meta name=\"manifest.type\" content=\"page_action\" />\n```\n\n### Action Without Popup\n\nIf you want to use the `activeTab` permission or the `browser.action.onClicked` event, but don't want to show a popup:\n\n1. Delete the [Popup entrypoint](/guide/essentials/entrypoints#popup) if it exists\n2. Add the `action` key to your manifest:\n\n   ```ts\n   export default defineConfig({\n     manifest: {\n       action: {},\n     },\n   });\n   ```\n\nSame 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:\n\n```ts\nexport default defineConfig({\n  manifest: {\n    action: {},\n    page_action: {},\n  },\n});\n```\n"
  },
  {
    "path": "docs/guide/essentials/config/runtime.md",
    "content": "# Runtime Config\n\n> This API is still a WIP, with more features coming soon!\n\nDefine runtime configuration in a single place, `<srcDir>/app.config.ts`:\n\n```ts\nimport { defineAppConfig } from '#imports';\n\n// Define types for your config\ndeclare module 'wxt/utils/define-app-config' {\n  export interface WxtAppConfig {\n    theme?: 'light' | 'dark';\n  }\n}\n\nexport default defineAppConfig({\n  theme: 'dark',\n});\n```\n\n:::warning\nThis file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](/guide/essentials/config/environment-variables)\n:::\n\nTo access runtime config, WXT provides the `getAppConfig` function:\n\n```ts\nimport { getAppConfig } from '#imports';\n\nconsole.log(getAppConfig()); // { theme: \"dark\" }\n```\n\n## Environment Variables in App Config\n\nYou can use environment variables in the `app.config.ts` file.\n\n```ts\ndeclare module 'wxt/utils/define-app-config' {\n  export interface WxtAppConfig {\n    apiKey?: string;\n    skipWelcome: boolean;\n  }\n}\n\nexport default defineAppConfig({\n  apiKey: import.meta.env.WXT_API_KEY,\n  skipWelcome: import.meta.env.WXT_SKIP_WELCOME === 'true',\n});\n```\n\nThis has several advantages:\n\n- Define all expected environment variables in a single file\n- Convert strings to other types, like booleans or arrays\n- Provide default values if an environment variable is not provided\n"
  },
  {
    "path": "docs/guide/essentials/config/typescript.md",
    "content": "# TypeScript Configuration\n\nWhen you run [`wxt prepare`](/api/cli/wxt-prepare), WXT generates a base TSConfig file for your project at `<rootDir>/.wxt/tsconfig.json`.\n\nAt a minimum, you need to create a TSConfig in your root directory that looks like this:\n\n```jsonc\n// <rootDir>/tsconfig.json\n{\n  \"extends\": \".wxt/tsconfig.json\",\n}\n```\n\nOr 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:\n\n```ts\n/// <reference path=\"./.wxt/wxt.d.ts\" />\n```\n\n## Compiler Options\n\nTo specify custom compiler options, add them in `<rootDir>/tsconfig.json`:\n\n```jsonc\n// <rootDir>/tsconfig.json\n{\n  \"extends\": \".wxt/tsconfig.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n  },\n}\n```\n\n## TSConfig Paths\n\nWXT provides a default set of path aliases.\n\n| Alias | To            | Example                                         |\n| ----- | ------------- | ----------------------------------------------- |\n| `~~`  | `<rootDir>/*` | `import \"~~/scripts\"`                           |\n| `@@`  | `<rootDir>/*` | `import \"@@/scripts\"`                           |\n| `~`   | `<srcDir>/*`  | `import { toLowerCase } from \"~/utils/strings\"` |\n| `@`   | `<srcDir>/*`  | `import { toLowerCase } from \"@/utils/strings\"` |\n\nTo 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`.\n\nThis will add your custom aliases to `<rootDir>/.wxt/tsconfig.json` next time you run `wxt prepare`. It also adds your alias to the bundler so it can resolve imports.\n\n```ts\nimport { resolve } from 'node:path';\n\nexport default defineConfig({\n  alias: {\n    // Directory:\n    testing: resolve('utils/testing'),\n    // File:\n    strings: resolve('utils/strings.ts'),\n  },\n});\n```\n\n```ts\nimport { fakeTab } from 'testing/fake-objects';\nimport { toLowerCase } from 'strings';\n```\n"
  },
  {
    "path": "docs/guide/essentials/config/vite.md",
    "content": "# Vite\n\nWXT uses [Vite](https://vitejs.dev/) under the hood to bundle your extension.\n\nThis 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.\n\n:::tip\nIn 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.\n:::\n\n## Change Vite Config\n\nYou can change Vite's config via the `wxt.config.ts` file:\n\n```ts [wxt.config.ts]\nimport { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  vite: () => ({\n    // Override config here, same as `defineConfig({ ... })`\n    // inside vite.config.ts files\n  }),\n});\n```\n\n## Add Vite Plugins\n\nTo add a plugin, install the NPM package and add it to the Vite config:\n\n```ts [wxt.config.ts]\nimport { defineConfig } from 'wxt';\nimport VueRouter from 'unplugin-vue-router/vite';\n\nexport default defineConfig({\n  vite: () => ({\n    plugins: [\n      VueRouter({\n        /* ... */\n      }),\n    ],\n  }),\n});\n```\n\n:::warning\nDue 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:\n\n```ts [wxt.config.ts]\nimport { defineConfig } from 'wxt';\nimport removeConsole from 'vite-plugin-remove-console';\n\nexport default defineConfig({\n  vite: (configEnv) => ({\n    plugins:\n      configEnv.mode === 'production'\n        ? [removeConsole({ includes: ['log'] })]\n        : [],\n  }),\n});\n```\n\nSearch [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.\n\nIf an issue doesn't exist for your plugin, [open a new one](https://github.com/wxt-dev/wxt/issues/new/choose).\n:::\n"
  },
  {
    "path": "docs/guide/essentials/content-scripts.md",
    "content": "---\noutline: deep\n---\n\n# Content Scripts\n\n> To create a content script, see [Entrypoint Types](/guide/essentials/entrypoints#content-scripts).\n\n## Context\n\nThe first argument to a content script's `main` function is its \"context\".\n\n```ts\n// entrypoints/example.content.ts\nexport default defineContentScript({\n  main(ctx) {},\n});\n```\n\nThis 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:\n\n```plaintext\nError: Extension context invalidated.\n```\n\nThe `ctx` object provides several helpers to stop asynchronous code from running once the context is invalidated:\n\n```ts\nctx.addEventListener(...);\nctx.setTimeout(...);\nctx.setInterval(...);\nctx.requestAnimationFrame(...);\n// and more\n```\n\nYou can also check if the context is invalidated manually:\n\n```ts\nif (ctx.isValid) {\n  // do something\n}\n// OR\nif (ctx.isInvalid) {\n  // do something\n}\n```\n\n## CSS\n\nIn regular web extensions, CSS for content scripts is usually a separate CSS file, that is added to a CSS array in the manifest:\n\n```json\n{\n  \"content_scripts\": [\n    {\n      \"css\": [\"content/style.css\"],\n      \"js\": [\"content/index.js\"],\n      \"matches\": [\"*://*/*\"]\n    }\n  ]\n}\n```\n\nIn 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.\n\n```ts\n// entrypoints/example.content/index.ts\nimport './style.css';\n\nexport default defineContentScript({\n  // ...\n});\n```\n\nTo create a standalone content script that only includes a CSS file:\n\n1. Create the CSS file: `entrypoints/example.content.css`\n2. Use the `build:manifestGenerated` hook to add the content script to the manifest:\n\n   ```ts [wxt.config.ts]\n   export default defineConfig({\n     hooks: {\n       'build:manifestGenerated': (wxt, manifest) => {\n         manifest.content_scripts ??= [];\n         manifest.content_scripts.push({\n           // Build extension once to see where your CSS get's written to\n           css: ['content-scripts/example.css'],\n           matches: ['*://*/*'],\n         });\n       },\n     },\n   });\n   ```\n\n## UI\n\nWXT provides 3 built-in utilities for adding UIs to a page from a content script:\n\n- [Integrated](#integrated) - `createIntegratedUi`\n- [Shadow Root](#shadow-root) -`createShadowRootUi`\n- [IFrame](#iframe) - `createIframeUi`\n\nEach has their own set of advantages and disadvantages.\n\n| Method      | Isolated Styles |   Isolated Events   | HMR | Use page's context |\n| ----------- | :-------------: | :-----------------: | :-: | :----------------: |\n| Integrated  |       ❌        |         ❌          | ❌  |         ✅         |\n| Shadow Root |       ✅        | ✅ (off by default) | ❌  |         ✅         |\n| IFrame      |       ✅        |         ✅          | ✅  |         ❌         |\n\n### Integrated\n\nIntegrated content script UIs are injected alongside the content of a page. This means that they are affected by CSS on that page.\n\n:::code-group\n\n```ts [Vanilla]\n// entrypoints/example-ui.content.ts\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n\n  main(ctx) {\n    const ui = createIntegratedUi(ctx, {\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Append children to the container\n        const app = document.createElement('p');\n        app.textContent = '...';\n        container.append(app);\n      },\n    });\n\n    // Call mount to add the UI to the DOM\n    ui.mount();\n  },\n});\n```\n\n```ts [Vue]\n// entrypoints/example-ui.content/index.ts\nimport { createApp } from 'vue';\nimport App from './App.vue';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n\n  main(ctx) {\n    const ui = createIntegratedUi(ctx, {\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Create the app and mount it to the UI container\n        const app = createApp(App);\n        app.mount(container);\n        return app;\n      },\n      onRemove: (app) => {\n        // Unmount the app when the UI is removed\n        app.unmount();\n      },\n    });\n\n    // Call mount to add the UI to the DOM\n    ui.mount();\n  },\n});\n```\n\n```tsx [React]\n// entrypoints/example-ui.content/index.tsx\nimport ReactDOM from 'react-dom/client';\nimport App from './App.tsx';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n\n  main(ctx) {\n    const ui = createIntegratedUi(ctx, {\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Create a root on the UI container and render a component\n        const root = ReactDOM.createRoot(container);\n        root.render(<App />);\n        return root;\n      },\n      onRemove: (root) => {\n        // Unmount the root when the UI is removed\n        root.unmount();\n      },\n    });\n\n    // Call mount to add the UI to the DOM\n    ui.mount();\n  },\n});\n```\n\n```ts [Svelte]\n// entrypoints/example-ui.content/index.ts\nimport App from './App.svelte';\nimport { mount, unmount } from 'svelte';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n\n  main(ctx) {\n    const ui = createIntegratedUi(ctx, {\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Create the Svelte app inside the UI container\n        return mount(App, { target: container });\n      },\n      onRemove: (app) => {\n        // Destroy the app when the UI is removed\n        unmount(app);\n      },\n    });\n\n    // Call mount to add the UI to the DOM\n    ui.mount();\n  },\n});\n```\n\n```tsx [Solid]\n// entrypoints/example-ui.content/index.ts\nimport { render } from 'solid-js/web';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n\n  main(ctx) {\n    const ui = createIntegratedUi(ctx, {\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Render your app to the UI container\n        const unmount = render(() => <div>...</div>, container);\n        return unmount;\n      },\n      onRemove: (unmount) => {\n        // Unmount the app when the UI is removed\n        unmount();\n      },\n    });\n\n    // Call mount to add the UI to the DOM\n    ui.mount();\n  },\n});\n```\n\n:::\n\nSee the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options.\n\n### Shadow Root\n\nOften 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.\n\nWXT'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.\n\nTo use `createShadowRootUi`, follow these steps:\n\n1. Import your CSS file at the top of your content script\n2. Set [`cssInjectionMode: \"ui\"`](/api/reference/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) inside `defineContentScript`\n3. Define your UI with `createShadowRootUi()`\n4. Mount the UI so it is visible to users\n\n:::code-group\n\n```ts [Vanilla]\n// 1. Import the style\nimport './style.css';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  // 2. Set cssInjectionMode\n  cssInjectionMode: 'ui',\n\n  async main(ctx) {\n    // 3. Define your UI\n    const ui = await createShadowRootUi(ctx, {\n      name: 'example-ui',\n      position: 'inline',\n      anchor: 'body',\n      onMount(container) {\n        // Define how your UI will be mounted inside the container\n        const app = document.createElement('p');\n        app.textContent = 'Hello world!';\n        container.append(app);\n      },\n    });\n\n    // 4. Mount the UI\n    ui.mount();\n  },\n});\n```\n\n```ts [Vue]\n// 1. Import the style\nimport './style.css';\nimport { createApp } from 'vue';\nimport App from './App.vue';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  // 2. Set cssInjectionMode\n  cssInjectionMode: 'ui',\n\n  async main(ctx) {\n    // 3. Define your UI\n    const ui = await createShadowRootUi(ctx, {\n      name: 'example-ui',\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Define how your UI will be mounted inside the container\n        const app = createApp(App);\n        app.mount(container);\n        return app;\n      },\n      onRemove: (app) => {\n        // Unmount the app when the UI is removed\n        app?.unmount();\n      },\n    });\n\n    // 4. Mount the UI\n    ui.mount();\n  },\n});\n```\n\n```tsx [React]\n// 1. Import the style\nimport './style.css';\nimport ReactDOM from 'react-dom/client';\nimport App from './App.tsx';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  // 2. Set cssInjectionMode\n  cssInjectionMode: 'ui',\n\n  async main(ctx) {\n    // 3. Define your UI\n    const ui = await createShadowRootUi(ctx, {\n      name: 'example-ui',\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Container is a body, and React warns when creating a root on the body, so create a wrapper div\n        const app = document.createElement('div');\n        container.append(app);\n\n        // Create a root on the UI container and render a component\n        const root = ReactDOM.createRoot(app);\n        root.render(<App />);\n        return root;\n      },\n      onRemove: (root) => {\n        // Unmount the root when the UI is removed\n        root?.unmount();\n      },\n    });\n\n    // 4. Mount the UI\n    ui.mount();\n  },\n});\n```\n\n```ts [Svelte]\n// 1. Import the style\nimport './style.css';\nimport App from './App.svelte';\nimport { mount, unmount } from 'svelte';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  // 2. Set cssInjectionMode\n  cssInjectionMode: 'ui',\n\n  async main(ctx) {\n    // 3. Define your UI\n    const ui = await createShadowRootUi(ctx, {\n      name: 'example-ui',\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Create the Svelte app inside the UI container\n        return mount(App, { target: container });\n      },\n      onRemove: (app) => {\n        // Destroy the app when the UI is removed\n        unmount(app);\n      },\n    });\n\n    // 4. Mount the UI\n    ui.mount();\n  },\n});\n```\n\n```tsx [Solid]\n// 1. Import the style\nimport './style.css';\nimport { render } from 'solid-js/web';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  // 2. Set cssInjectionMode\n  cssInjectionMode: 'ui',\n\n  async main(ctx) {\n    // 3. Define your UI\n    const ui = await createShadowRootUi(ctx, {\n      name: 'example-ui',\n      position: 'inline',\n      anchor: 'body',\n      onMount: (container) => {\n        // Render your app to the UI container\n        const unmount = render(() => <div>...</div>, container);\n      },\n      onRemove: (unmount) => {\n        // Unmount the app when the UI is removed\n        unmount?.();\n      },\n    });\n\n    // 4. Mount the UI\n    ui.mount();\n  },\n});\n```\n\n:::\n\nSee the [API Reference](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) for the complete list of options.\n\nFull examples:\n\n- [react-content-script-ui](https://github.com/wxt-dev/examples/tree/main/examples/react-content-script-ui)\n- [tailwindcss](https://github.com/wxt-dev/examples/tree/main/examples/tailwindcss)\n\n### IFrame\n\nIf 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_**.\n\nWXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi), which simplifies setting up the IFrame.\n\n1. Create an HTML page that will be loaded into your IFrame:\n\n   ```html\n   <!-- entrypoints/example-iframe.html -->\n   <!doctype html>\n   <html lang=\"en\">\n     <head>\n       <meta charset=\"UTF-8\" />\n       <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n       <title>Your Content Script IFrame</title>\n     </head>\n     <body>\n       <!-- ... -->\n     </body>\n   </html>\n   ```\n\n1. Add the page to the manifest's `web_accessible_resources`:\n\n   ```ts [wxt.config.ts]\n   export default defineConfig({\n     manifest: {\n       web_accessible_resources: [\n         {\n           resources: ['example-iframe.html'],\n           matches: [...],\n         },\n       ],\n     },\n   });\n   ```\n\n1. Create and mount the IFrame:\n\n   ```ts\n   export default defineContentScript({\n     matches: ['<all_urls>'],\n\n     main(ctx) {\n       // Define the UI\n       const ui = createIframeUi(ctx, {\n         page: '/example-iframe.html',\n         position: 'inline',\n         anchor: 'body',\n         onMount: (wrapper, iframe) => {\n           // Add styles to the iframe like width\n           iframe.width = '123';\n         },\n       });\n\n       // Show UI to user\n       ui.mount();\n     },\n   });\n   ```\n\nSee the [API Reference](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi) for the complete list of options.\n\n## Isolated World vs Main World\n\nBy 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.\n\nYou can enable this for a content script by setting the `world` option:\n\n```ts\nexport default defineContentScript({\n  world: 'MAIN',\n});\n```\n\nHowever, this approach has several notable drawbacks:\n\n- Doesn't support MV2\n- `world: \"MAIN\"` is only supported by Chromium browsers\n- Main world content scripts don't have access to the extension API\n\nInstead, WXT recommends injecting a script into the main world manually using it's `injectScript` function. This will address the drawbacks mentioned before.\n\n- `injectScript` supports both MV2 and MV3\n- `injectScript` supports all browsers\n- Having a \"parent\" content script means you can send messages back and forth, making it possible to access the extension API\n\nTo use `injectScript`, we need two entrypoints, one content script and one unlisted script:\n\n<!-- prettier-ignore -->\n```html\n📂 entrypoints/\n   📄 example.content.ts\n   📄 example-main-world.ts\n```\n\n```ts\n// entrypoints/example-main-world.ts\nexport default defineUnlistedScript(() => {\n  console.log('Hello from the main world');\n});\n```\n\n```ts\n// entrypoints/example.content.ts\nexport default defineContentScript({\n  matches: ['*://*/*'],\n  async main() {\n    console.log('Injecting script...');\n    await injectScript('/example-main-world.js', {\n      keepInDom: true,\n    });\n    console.log('Done!');\n  },\n});\n```\n\n```json\nexport default defineConfig({\n  manifest: {\n    // ...\n    web_accessible_resources: [\n      {\n        resources: [\"example-main-world.js\"],\n        matches: [\"*://*/*\"],\n      }\n    ]\n  }\n});\n```\n\n`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.\n\n`injectScript` returns a promise, that when resolved, means the script has been evaluated by the browser and you can start communicating with it.\n\n:::warning Warning: `run_at` Caveat\nFor MV3, `injectScript` is synchronous and the injected script will be evaluated at the same time as your the content script's `run_at`.\n\nHowever for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.\n:::\n\nThe `script` element can be modified just before it is added to the DOM by using the `modifyScript` option. This can be used to e.g. modify `script.async`/`script.defer`, add event listeners to the element, or pass data to the script via `script.dataset`. An example:\n\n```ts\n// entrypoints/example.content.ts\nexport default defineContentScript({\n  matches: ['*://*/*'],\n  async main() {\n    await injectScript('/example-main-world.js', {\n      modifyScript(script) {\n        script.dataset['greeting'] = 'Hello there';\n      },\n    });\n  },\n});\n```\n\n```ts\n// entrypoints/example-main-world.ts\nexport default defineUnlistedScript(() => {\n  console.log(document.currentScript?.dataset['greeting']);\n});\n```\n\n`injectScript` returns the created script element. It can be used to e.g. send messages to the script in the form of custom events. The script can add an event listener for them via `document.currentScript`. An example of bidirectional communication:\n\n```ts\n// entrypoints/example.content.ts\nexport default defineContentScript({\n  matches: ['*://*/*'],\n  async main() {\n    const { script } = await injectScript('/example-main-world.js', {\n      modifyScript(script) {\n        // Add a listener before the injected script is loaded.\n        script.addEventListener('from-injected-script', (event) => {\n          if (event instanceof CustomEvent) {\n            console.log(`${event.type}:`, event.detail);\n          }\n        });\n      },\n    });\n\n    // Send an event after the injected script is loaded.\n    script.dispatchEvent(\n      new CustomEvent('from-content-script', {\n        detail: 'General Kenobi',\n      }),\n    );\n  },\n});\n```\n\n```ts\n// entrypoints/example-main-world.ts\nexport default defineUnlistedScript(() => {\n  const script = document.currentScript;\n\n  script?.addEventListener('from-content-script', (event) => {\n    if (event instanceof CustomEvent) {\n      console.log(`${event.type}:`, event.detail);\n    }\n  });\n\n  script?.dispatchEvent(\n    new CustomEvent('from-injected-script', {\n      detail: 'Hello there',\n    }),\n  );\n});\n```\n\n## Mounting UI to dynamic element\n\nIn many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.\n\n```ts\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n\n  main(ctx) {\n    const ui = createIntegratedUi(ctx, {\n      position: 'inline',\n      // It observes the anchor\n      anchor: '#your-target-dynamic-element',\n      onMount: (container) => {\n        // Append children to the container\n        const app = document.createElement('p');\n        app.textContent = '...';\n        container.append(app);\n      },\n    });\n\n    // Call autoMount to observe anchor element for add/remove.\n    ui.autoMount();\n  },\n});\n```\n\n:::tip\nWhen the `ui.remove` is called, `autoMount` also stops.\n:::\n\nSee the [API Reference](/api/reference/wxt/utils/content-script-ui/types/interfaces/ContentScriptUi#automount) for the complete list of options.\n\n## Dealing with SPAs\n\nIt is difficult to write content scripts for SPAs (single page applications) and websites using HTML5 history mode for navigation because content scripts are only ran on full page reloads. SPAs and websites that take advantage of HTML5 history mode **_do not perform a full reload when changing paths_**, and thus your content script isn't going to be ran when you expect it to be.\n\nLet's look at an example. Say you want to add a UI to YouTube when watching a video:\n\n```ts\nexport default defineContentScript({\n  matches: ['*://*.youtube.com/watch*'],\n  main(ctx) {\n    console.log('YouTube content script loaded');\n\n    mountUi(ctx);\n  },\n});\n\nfunction mountUi(ctx: ContentScriptContext): void {\n  // ...\n}\n```\n\nYou're only going to see \"YouTube content script loaded\" when reloading the watch page or when navigating directly to it from another website.\n\nTo get around this, you'll need to manually listen for the path to change and run your content script when the URL matches what you expect it to match.\n\n```ts\nconst watchPattern = new MatchPattern('*://*.youtube.com/watch*');\n\nexport default defineContentScript({\n  matches: ['*://*.youtube.com/*'],\n  main(ctx) {\n    ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {\n      if (watchPattern.includes(newUrl)) mainWatch(ctx);\n    });\n  },\n});\n\nfunction mainWatch(ctx: ContentScriptContext) {\n  mountUi(ctx);\n}\n```\n"
  },
  {
    "path": "docs/guide/essentials/e2e-testing.md",
    "content": "# E2E Testing\n\n## Playwright\n\n[Playwright](https://playwright.dev) is the only good option for writing Chrome Extension end-to-end tests.\n\nTo add E2E tests to your project, follow Playwright's [Chrome Extension docs](https://playwright.dev/docs/chrome-extensions). When you have to pass the path to your extension, pass the output directory, `/path/to/project/.output/chrome-mv3`.\n\nFor a complete example, see the [WXT's Playwright Example](https://github.com/wxt-dev/examples/tree/main/examples/playwright-e2e-testing).\n"
  },
  {
    "path": "docs/guide/essentials/entrypoints.md",
    "content": "---\noutline: deep\n---\n\n# Entrypoints\n\nWXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (TS, JSX, SCSS, etc).\n\n## Folder Structure\n\nInside the `entrypoints/` directory, an entrypoint is defined as a single file or directory (with an `index` file) inside it.\n\n:::code-group\n\n<!-- prettier-ignore -->\n```html [Single File]\n📂 entrypoints/\n   📄 {name}.{ext}\n```\n\n<!-- prettier-ignore -->\n```html [Directory]\n📂 entrypoints/\n   📂 {name}/\n      📄 index.{ext}\n```\n\n:::\n\nThe entrypoint's `name` dictates the type of entrypoint. For example, to add a [\"Background\" entrypoint](#background), either of these files would work:\n\n:::code-group\n\n<!-- prettier-ignore -->\n```html [Single File]\n📂 entrypoints/\n   📄 background.ts\n```\n\n<!-- prettier-ignore -->\n```html [Directory]\n📂 entrypoints/\n   📂 background/\n      📄 index.ts\n```\n\n:::\n\nRefer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns.\n\n### Including Other Files\n\nWhen using an entrypoint directory, `entrypoints/{name}/index.{ext}`, you can add related files next to the `index` file.\n\n<!-- prettier-ignore -->\n```html\n📂 entrypoints/\n   📂 popup/\n      📄 index.html     ← This file is the entrypoint\n      📄 main.ts\n      📄 style.css\n   📂 background/\n      📄 index.ts       ← This file is the entrypoint\n      📄 alarms.ts\n      📄 messaging.ts\n   📂 youtube.content/\n      📄 index.ts       ← This file is the entrypoint\n      📄 style.css\n```\n\n:::danger\n**DO NOT** put files related to an entrypoint directly inside the `entrypoints/` directory. WXT will treat them as entrypoints and try to build them, usually resulting in an error.\n\nInstead, use a directory for that entrypoint:\n\n<!-- prettier-ignore -->\n```html\n📂 entrypoints/\n   📄 popup.html <!-- [!code --] -->\n   📄 popup.ts <!-- [!code --] -->\n   📄 popup.css <!-- [!code --] -->\n   📂 popup/ <!-- [!code ++] -->\n      📄 index.html <!-- [!code ++] -->\n      📄 main.ts <!-- [!code ++] -->\n      📄 style.css <!-- [!code ++] -->\n```\n\n:::\n\n### Deeply Nested Entrypoints\n\nWhile the `entrypoints/` directory might resemble the `pages/` directory of other web frameworks, like Nuxt or Next.js, **it does not support deeply nesting entrypoints** in the same way.\n\nEntrypoints must be zero or one levels deep for WXT to discover and build them:\n\n<!-- prettier-ignore -->\n```html\n📂 entrypoints/\n   📂 youtube/ <!-- [!code --] -->\n       📂 content/ <!-- [!code --] -->\n          📄 index.ts <!-- [!code --] -->\n          📄 ... <!-- [!code --] -->\n       📂 injected/ <!-- [!code --] -->\n          📄 index.ts <!-- [!code --] -->\n          📄 ... <!-- [!code --] -->\n   📂 youtube.content/ <!-- [!code ++] -->\n      📄 index.ts <!-- [!code ++] -->\n      📄 ... <!-- [!code ++] -->\n   📂 youtube-injected/ <!-- [!code ++] -->\n      📄 index.ts <!-- [!code ++] -->\n      📄 ... <!-- [!code ++] -->\n```\n\n## Unlisted Entrypoints\n\nIn web extensions, there are two types of entrypoints:\n\n1. **Listed**: Referenced in the `manifest.json`\n2. **Unlisted**: Not referenced in the `manifest.json`\n\nThroughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:\n\n- Popup\n- Options\n- Background\n- Content Script\n\nHowever, not all entrypoints in web extensions are listed in the manifest. Some are not listed in the manifest, but are still used by extensions. For example:\n\n- A welcome page shown in a new tab when the extension is installed\n- JS files injected by content scripts into the main world\n\nFor more details on how to add unlisted entrypoints, see:\n\n- [Unlisted Pages](#unlisted-pages)\n- [Unlisted Scripts](#unlisted-scripts)\n- [Unlisted CSS](#unlisted-css)\n\n## Defining Manifest Options\n\nMost listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.\n\nFor example, here's how to define `matches` for content scripts:\n\n```ts [entrypoints/content.ts]\nexport default defineContentScript({\n  matches: ['*://*.wxt.dev/*'],\n  main() {\n    // ...\n  },\n});\n```\n\nFor HTML entrypoints, options are configured as `<meta>` tags. For example, to use a `page_action` for your MV2 popup:\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta name=\"manifest.type\" content=\"page_action\" />\n  </head>\n</html>\n```\n\n> Refer to the [Entrypoint Types](#entrypoint-types) sections for a list of options configurable inside each entrypoint, and how to define them.\n\nWhen building your extension, WXT will look at the options defined in your entrypoints, and generate the manifest accordingly.\n\n## Entrypoint Types\n\n### Background\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['background.[jt]s', 'background.js'],\n    ['background/index.[jt]s', 'background.js'],\n  ]\"\n/>\n\n:::code-group\n\n```ts [Minimal]\nexport default defineBackground(() => {\n  // Executed when background is loaded\n});\n```\n\n```ts [With Manifest Options]\nexport default defineBackground({\n  // Set manifest options\n  persistent: undefined | true | false,\n  type: undefined | 'module',\n\n  // Set include/exclude if the background should be removed from some builds\n  include: undefined | string[],\n  exclude: undefined | string[],\n\n  main() {\n    // Executed when background is loaded, CANNOT BE ASYNC\n  },\n});\n```\n\n:::\n\nFor MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.\n\nWhen defining your background entrypoint, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.\n\n<!-- prettier-ignore -->\n```ts\nbrowser.action.onClicked.addListener(() => { // [!code --]\n  // ... // [!code --]\n}); // [!code --]\n\nexport default defineBackground(() => {\n  browser.action.onClicked.addListener(() => { // [!code ++]\n    // ... // [!code ++]\n  }); // [!code ++]\n});\n```\n\nRefer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.\n\n### Bookmarks\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['bookmarks.html', 'bookmarks.html'],\n    ['bookmarks/index.html', 'bookmarks.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Title</title>\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\nWhen you define a Bookmarks entrypoint, WXT will automatically update the manifest to override the browser's bookmarks page with your own HTML page.\n\n### Content Scripts\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['content.[jt]sx?', 'content-scripts/content.js'],\n    ['content/index.[jt]sx?', 'content-scripts/content.js'],\n    ['{name}.content.[jt]sx?', 'content-scripts/{name}.js'],\n    ['{name}.content/index.[jt]sx?', 'content-scripts/{name}.js'],\n  ]\"\n/>\n\n```ts\nexport default defineContentScript({\n  // Set manifest options\n  matches: string[],\n  excludeMatches: undefined | [],\n  includeGlobs: undefined | [],\n  excludeGlobs: undefined | [],\n  allFrames: undefined | true | false,\n  runAt: undefined | 'document_start' | 'document_end' | 'document_idle',\n  matchAboutBlank: undefined | true | false,\n  matchOriginAsFallback: undefined | true | false,\n  world: undefined | 'ISOLATED' | 'MAIN',\n\n  // Set include/exclude if the background should be removed from some builds\n  include: undefined | string[],\n  exclude: undefined | string[],\n\n  // Configure how CSS is injected onto the page\n  cssInjectionMode: undefined | \"manifest\" | \"manual\" | \"ui\",\n\n  // Configure how/when content script will be registered\n  registration: undefined | \"manifest\" | \"runtime\",\n\n  main(ctx: ContentScriptContext) {\n    // Executed when content script is loaded, can be async\n  },\n});\n```\n\nWhen defining content script entrypoints, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.\n\n<!-- prettier-ignore -->\n```ts\nconst container = document.createElement('div'); // [!code --]\ndocument.body.append(container); // [!code --]\n\nexport default defineContentScript({\n  main: function () {\n    const container = document.createElement('div'); // [!code ++]\n    document.body.append(container); // [!code ++]\n  },\n});\n```\n\nRefer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.\n\nSee [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.\n\n### Devtools\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools_page)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['devtools.html', 'devtools.html'],\n    ['devtools/index.html', 'devtools.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\nFollow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.\n\n### History\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['history.html', 'history.html'],\n    ['history/index.html', 'history.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Title</title>\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\nWhen you define a History entrypoint, WXT will automatically update the manifest to override the browser's history page with your own HTML page.\n\n### Newtab\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['newtab.html', 'newtab.html'],\n    ['newtab/index.html', 'newtab.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Title</title>\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\nWhen you define a Newtab entrypoint, WXT will automatically update the manifest to override the browser's new tab page with your own HTML page.\n\n### Options\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['options.html', 'options.html'],\n    ['options/index.html', 'options.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Options Title</title>\n\n    <!-- Customize the manifest options -->\n    <meta name=\"manifest.open_in_tab\" content=\"true|false\" />\n    <meta name=\"manifest.chrome_style\" content=\"true|false\" />\n    <meta name=\"manifest.browser_style\" content=\"true|false\" />\n\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\n### Popup\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/action/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['popup.html', 'popup.html'],\n    ['popup/index.html', 'popup.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n    <!-- Set the `action.default_title` in the manifest -->\n    <title>Default Popup Title</title>\n\n    <!-- Customize the manifest options -->\n    <meta\n      name=\"manifest.default_icon\"\n      content=\"{\n        16: '/icon-16.png',\n        24: '/icon-24.png',\n        ...\n      }\"\n    />\n    <meta name=\"manifest.type\" content=\"page_action|browser_action\" />\n    <meta name=\"manifest.browser_style\" content=\"true|false\" />\n    <!-- Firefox only: where to place the action button -->\n    <meta\n      name=\"manifest.default_area\"\n      content=\"navbar|menupanel|tabstrip|personaltoolbar\"\n    />\n    <!-- Firefox only: icons for light/dark themes -->\n    <meta\n      name=\"manifest.theme_icons\"\n      content=\"[\n        { light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },\n        { light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 }\n      ]\"\n    />\n\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\n### Sandbox\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/sandbox/)\n\n:::warning Chromium Only\nFirefox does not support sandboxed pages.\n:::\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['sandbox.html', 'sandbox.html'],\n    ['sandbox/index.html', 'sandbox.html'],\n    ['{name}.sandbox.html', '{name}.html'],\n    ['{name}.sandbox/index.html', '{name}.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Title</title>\n\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\n### Side Panel\n\n[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['sidepanel.html', 'sidepanel.html'],\n    ['sidepanel/index.html', 'sidepanel.html'],\n    ['{name}.sidepanel.html', '{name}.html` '],\n    ['{name}.sidepanel/index.html', '{name}.html` '],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Side Panel Title</title>\n\n    <!-- Customize the manifest options -->\n    <meta\n      name=\"manifest.default_icon\"\n      content=\"{\n        16: '/icon-16.png',\n        24: '/icon-24.png',\n        ...\n      }\"\n    />\n    <meta name=\"manifest.open_at_install\" content=\"true|false\" />\n    <meta name=\"manifest.browser_style\" content=\"true|false\" />\n\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\nIn Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.\n\n### Unlisted CSS\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['{name}.(css|scss|sass|less|styl|stylus)', '{name}.css'],\n    ['{name}/index.(css|scss|sass|less|styl|stylus)', '{name}.css'],\n    ['content.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],\n    ['content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],\n    ['{name}.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],\n    ['{name}.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],\n  ]\"\n/>\n\n```css\nbody {\n  /* ... */\n}\n```\n\nFollow Vite's guide to setup your preprocessor of choice: <https://vitejs.dev/guide/features.html#css-pre-processors>\n\nCSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.\n\n### Unlisted Pages\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['{name}.html', '{name}.html'],\n    ['{name}/index.html', '{name}.html'],\n  ]\"\n/>\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Title</title>\n\n    <!-- Set include/exclude if the page should be removed from some builds -->\n    <meta name=\"manifest.include\" content=\"['chrome', ...]\" />\n    <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\nAt runtime, unlisted pages are accessible at `/{name}.html`:\n\n```ts\nconst url = browser.runtime.getURL('/{name}.html');\n\nconsole.log(url); // \"chrome-extension://{id}/{name}.html\"\nwindow.open(url); // Open the page in a new tab\n```\n\n### Unlisted Scripts\n\n<EntrypointPatterns\n  :patterns=\"[\n    ['{name}.[jt]sx?', '{name}.js'],\n    ['{name}/index.[jt]sx?', '{name}.js'],\n  ]\"\n/>\n\n:::code-group\n\n```ts [Minimal]\nexport default defineUnlistedScript(() => {\n  // Executed when script is loaded\n});\n```\n\n```ts [With Options]\nexport default defineUnlistedScript({\n  // Set include/exclude if the script should be removed from some builds\n  include: undefined | string[],\n  exclude: undefined | string[],\n\n  main() {\n    // Executed when script is loaded\n  },\n});\n```\n\n:::\n\nAt runtime, unlisted scripts are accessible from `/{name}.js`:\n\n```ts\nconst url = browser.runtime.getURL('/{name}.js');\n\nconsole.log(url); // \"chrome-extension://{id}/{name}.js\"\n```\n\nYou are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related assets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).\n\nWhen defining an unlisted script, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.\n\n<!-- prettier-ignore -->\n```ts\ndocument.querySelectorAll('a').forEach((anchor) => { // [!code --]\n  // ... // [!code --]\n}); // [!code --]\n\nexport default defineUnlistedScript(() => {\n  document.querySelectorAll('a').forEach((anchor) => { // [!code ++]\n    // ... // [!code ++]\n  }); // [!code ++]\n});\n```\n\nRefer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.\n"
  },
  {
    "path": "docs/guide/essentials/es-modules.md",
    "content": "# ES Modules\n\nYour source code should always be written as ESM. However, you have some control whether an entrypoint is bundled as ESM.\n\n## HTML Pages <Badge type=\"warning\" text=\"≥0.0.1\" />\n\nVite only supports bundling JS from HTML pages as ESM. Ensure you have added `type=\"module\"` to your `<script>` tags:\n\n<!-- prettier-ignore -->\n```html\n<script src=\"./main.ts\"></script> <!-- [!code --] -->\n<script src=\"./main.ts\" type=\"module\"></script> <!-- [!code ++] -->\n```\n\n## Background <Badge type=\"warning\" text=\"≥0.16.0\" />\n\nBy default, your background will be bundled into a single file as IIFE. You can change this by setting `type: \"module\"` in your background entrypoint:\n\n```ts\nexport default defineBackground({\n  type: 'module', // [!code ++]\n  main() {\n    // ...\n  },\n});\n```\n\nThis will change the output format to ESM, enable code-spliting between your background script and HTML pages, and set `\"type\": \"module\"` in your manifest.\n\n:::warning\nOnly 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.\n:::\n\n## Content Scripts\n\nWXT 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.\n\nIf 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.\n"
  },
  {
    "path": "docs/guide/essentials/extension-apis.md",
    "content": "# Extension APIs\n\n[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)\n\nDifferent browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).\n\nWXT merges these two into a unified API accessed through the `browser` variable.\n\n```ts\nimport { browser } from 'wxt/browser';\n\nbrowser.action.onClicked.addListener(() => {\n  // ...\n});\n```\n\n:::tip\nWith auto-imports enabled, you don't even need to import this variable from `wxt/browser`!\n:::\n\nThe `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime:\n\n<<< @/../packages/browser/src/index.mjs#snippet\n\nThis means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).\n\n## Accessing Types\n\nAll types can be accessed via WXT's `Browser` namespace:\n\n```ts\nimport { type Browser } from 'wxt/browser';\n\nfunction handleMessage(message: any, sender: Browser.runtime.MessageSender) {\n  // ...\n}\n```\n\n## Using `webextension-polyfill`\n\nIf you want to use the `webextension-polyfill` when importing `browser`, you can do so by installing the `@wxt-dev/webextension-polyfill` package.\n\nSee it's [Installation Guide](https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md) to get started.\n\n## Feature Detection\n\nDepending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`.\n\n:::warning\nTypes 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.\n:::\n\nTo check if an API is available, use feature detection:\n\n```ts\nif (browser.runtime.onSuspend != null) {\n  browser.runtime.onSuspend.addListener(() => {\n    // ...\n  });\n}\n```\n\nHere, [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) is your best friend:\n\n```ts\nbrowser.runtime.onSuspend?.addListener(() => {\n  // ...\n});\n```\n\nAlternatively, if you're trying to use similar APIs under different names (to support MV2 and MV3), you can do something like this:\n\n```ts\n(browser.action ?? browser.browser_action).onClicked.addListener(() => {\n  //\n});\n```\n\n### Augmenting the Browser Type\n\nWXT's `browser` types are based on the `@types/chrome` package. That means some Firefox-specific APIs may not be typed, like `browser.sidebarAction`. If you want to add types for these APIs, you can augment the browser type to add them yourself:\n\n```ts\n// <srcDir>/browser-types.d.ts\nimport '@wxt-dev/browser';\nimport type { SidebarAction } from 'webextension-polyfill';\n\ndeclare module '@wxt-dev/browser' {\n  namespace Browser {\n    export const sidebarAction: SidebarAction.Static;\n  }\n}\n```\n\n> For this to work, you may need to install `@wxt-dev/browser` as a direct dependency.\n>\n> ```sh\n> pnpm add @wxt-dev/browser\n> ```\n\n## Entrypoint Limitations\n\nBecause WXT imports your entrypoint files into a NodeJS, non-extension environment, the `chrome`/`browser` variables provided to extensions by the browser **will not be available**.\n\nTo prevent some basic errors, WXT polyfills these globals with the same in-memory, fake implementation it uses for testing: [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation/). However, not all the APIs have been implemented.\n\nSo it is extremely important to NEVER use `browser.*` extension APIs outside the main function of any JS/TS entrypoints (background, content scripts, and unlisted scripts). If you do, you'll see an error like this:\n\n```plaintext\n✖ Command failed after 440 ms\n\n ERROR  Browser.action.onClicked.addListener not implemented.\n```\n\nThe fix is simple, just move your API usage into the entrypoint's main function:\n\n:::code-group\n\n```ts [background.ts]\nbrowser.action.onClicked.addListener(() => {\n  /* ... */\n}); // [!code --]\n\nexport default defineBackground(() => {\n  browser.action.onClicked.addListener(() => {\n    /* ... */\n  }); // [!code ++]\n});\n```\n\n```ts [content.ts]\nbrowser.runtime.onMessage.addListener(() => {\n  /* ... */\n}); // [!code --]\n\nexport default defineContentScript({\n  main() {\n    browser.runtime.onMessage.addListener(() => {\n      /* ... */\n    }); // [!code ++]\n  },\n});\n```\n\n```ts [unlisted.ts]\nbrowser.runtime.onMessage.addListener(() => {\n  /* ... */\n}); // [!code --]\n\nexport default defineUnlistedScript(() => {\n  browser.runtime.onMessage.addListener(() => {\n    /* ... */\n  }); // [!code ++]\n});\n```\n\n:::\n\nRead [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) for more technical details about this limitation.\n"
  },
  {
    "path": "docs/guide/essentials/frontend-frameworks.md",
    "content": "# Frontend Frameworks\n\n## Built-in Modules\n\nWXT has preconfigured modules for the most popular frontend frameworks:\n\n- [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/tree/main/packages/module-react)\n- [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/tree/main/packages/module-vue)\n- [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/tree/main/packages/module-svelte)\n- [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/tree/main/packages/module-solid)\n\nInstall the module for your framework, then add it to your config:\n\n:::code-group\n\n```ts [React]\nimport { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  modules: ['@wxt-dev/module-react'],\n});\n```\n\n```ts [Vue]\nimport { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  modules: ['@wxt-dev/module-vue'],\n});\n```\n\n```ts [Svelte]\nimport { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  modules: ['@wxt-dev/module-svelte'],\n});\n```\n\n```ts [Solid]\nimport { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  modules: ['@wxt-dev/module-solid'],\n});\n```\n\n:::\n\n## Adding Vite Plugins\n\nIf your framework doesn't have an official WXT module, no worries! WXT supports any framework with a Vite plugin.\n\nJust 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 👍\n\n```ts\nimport { defineConfig } from 'wxt';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n  vite: () => ({\n    plugins: [react()],\n  }),\n});\n```\n\n> The WXT modules just simplify the configuration and add auto-imports. They're not much different than the above.\n\n## Multiple Apps\n\nSince 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.\n\nUsually, this means each entrypoint should be a directory with it's own files inside it. Here's the recommended folder structure:\n\n<!-- prettier-ignore -->\n```html\n📂 {srcDir}/\n   📂 assets/          <---------- Put shared assets here\n      📄 tailwind.css\n   📂 components/\n      📄 Button.tsx\n   📂 entrypoints/\n      📂 options/       <--------- Use a folder with an index.html file in it\n         📁 pages/      <--------- A good place to put your router pages if you have them\n         📄 index.html\n         📄 App.tsx\n         📄 main.tsx    <--------- Create and mount your app here\n         📄 style.css   <--------- Entrypoint-specific styles\n         📄 router.ts\n```\n\n## Configuring Routers\n\nAll 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.\n\nInstead, 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`).\n\nRefer 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:\n\n- [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)\n- [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)\n- [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)\n- [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)\n"
  },
  {
    "path": "docs/guide/essentials/i18n.md",
    "content": "# I18n\n\n[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)\n\nThis page discusses how to setup internationalization using the vanilla `browser.i18n` APIs and mentions some alternatives if you want to use something else.\n\n[[toc]]\n\n## Usage\n\n1. Add `default_locale` to your manifest:\n\n   ```ts\n   export default defineConfig({\n     manifest: {\n       default_locale: 'en',\n     },\n   });\n   ```\n\n2. Create `messages.json` files in the `public/` directory:\n\n   <!-- prettier-ignore -->\n   ```html\n   📂 {rootDir}/\n      📂 public/\n         📂 _locales/\n            📂 en/\n               📄 messages.json\n            📂 de/\n               📄 messages.json\n            📂 ko/\n               📄 messages.json\n   ```\n\n   ```jsonc\n   // public/_locales/en/messages.json\n   {\n     \"helloWorld\": {\n       \"message\": \"Hello world!\",\n     },\n   }\n   ```\n\n3. Get the translation:\n\n   ```ts\n   browser.i18n.getMessage('helloWorld');\n   ```\n\n4. _Optional_: Add translations for extension name and description:\n\n```json\n{\n  \"extName\": {\n    \"message\": \"...\"\n  },\n  \"extDescription\": {\n    \"message\": \"...\"\n  },\n  \"helloWorld\": {\n    \"message\": \"Hello world!\"\n  }\n}\n```\n\n```ts\nexport default defineConfig({\n  manifest: {\n    name: '__MSG_extName__',\n    description: '__MSG_extDescription__',\n    default_locale: 'en',\n  },\n});\n```\n\n## Alternatives\n\nThe 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.\n\nHowever, 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:\n\n- They can localize text in your manifest and CSS files\n- Translations are loaded synchronously\n- Translations are not bundled multiple times, keeping your extension small\n- Zero configuration\n\nHowever, there is one major downside to the vanilla API and any packages built on top of it:\n\n- Language cannot be changed without changing your browser/system language\n\nHere are some examples of how to setup a third party i18n library:\n\n- [vue-i18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n)\n"
  },
  {
    "path": "docs/guide/essentials/messaging.md",
    "content": "# Messaging\n\n[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)\n\nRead the docs linked above to learn more about using the vanilla messaging APIs.\n\n## Alternatives\n\nThe 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.\n\nHere are some popular messaging libraries that support all browsers and work with WXT:\n\n- [`trpc-chrome`](https://www.npmjs.com/package/trpc-chrome) - [tRPC](https://trpc.io/) adapter for Web Extensions.\n- [`webext-bridge`](https://www.npmjs.com/package/webext-bridge) - Messaging in WebExtensions made super easy. Out of the box.\n- [`@webext-core/messaging`](https://www.npmjs.com/package/@webext-core/messaging) - Light weight, type-safe wrapper around the web extension messaging APIs\n- [`@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.\n- [`Comctx`](https://github.com/molvqingtai/comctx) - Cross-context RPC solution with type safety and flexible adapters.\n"
  },
  {
    "path": "docs/guide/essentials/project-structure.md",
    "content": "# Project Structure\n\nWXT follows a strict project structure. By default, it's a flat folder structure that looks like this:\n\n<!-- prettier-ignore -->\n```html\n📂 {rootDir}/\n   📁 .output/\n   📁 .wxt/\n   📁 assets/\n   📁 components/\n   📁 composables/\n   📁 entrypoints/\n   📁 hooks/\n   📁 modules/\n   📁 public/\n   📁 utils/\n   📄 .env\n   📄 .env.publish\n   📄 app.config.ts\n   📄 package.json\n   📄 tsconfig.json\n   📄 web-ext.config.ts\n   📄 wxt.config.ts\n```\n\nHere's a brief summary of each of these files and directories:\n\n- `.output/`: All build artifacts will go here\n- `.wxt/`: Generated by WXT, it contains TS config\n- `assets/`: Contains all CSS, images, and other assets that should be processed by WXT\n- `components/`: Auto-imported by default, contains UI components\n- `composables/`: Auto-imported by default, contains source code for your project's composable functions for Vue\n- `entrypoints/`: Contains all the entrypoints that get bundled into your extension\n- `hooks/`: Auto-imported by default, contains source code for your project's hooks for React and Solid\n- `modules/`: Contains [local WXT Modules](/guide/essentials/wxt-modules) for your project\n- `public/`: Contains any files you want to copy into the output folder as-is, without being processed by WXT\n- `utils/`: Auto-imported by default, contains generic utilities used throughout your project\n- `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables)\n- `.env.publish`: Contains Environment Variables for [publishing](/guide/essentials/publishing)\n- `app.config.ts`: Contains [Runtime Config](/guide/essentials/config/runtime)\n- `package.json`: The standard file used by your package manager\n- `tsconfig.json`: Config telling TypeScript how to behave\n- `web-ext.config.ts`: Configure [Browser Startup](/guide/essentials/config/browser-startup)\n- `wxt.config.ts`: The main config file for WXT projects\n\n## Adding a `src/` Directory\n\nMany developers like having a `src/` directory to separate source code from configuration files. You can enable it inside the `wxt.config.ts` file:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  srcDir: 'src',\n});\n```\n\nAfter enabling it, your project structure should look like this:\n\n<!-- prettier-ignore -->\n```html\n📂 {rootDir}/\n   📁 .output/\n   📁 .wxt/\n   📁 modules/\n   📁 public/\n   📂 src/\n      📁 assets/\n      📁 components/\n      📁 composables/\n      📁 entrypoints/\n      📁 hooks/\n      📁 utils/\n      📄 app.config.ts\n   📄 .env\n   📄 .env.publish\n   📄 package.json\n   📄 tsconfig.json\n   📄 web-ext.config.ts\n   📄 wxt.config.ts\n```\n\n## Customizing Other Directories\n\nYou can configure the following directories:\n\n<!-- prettier-ignore -->\n```ts [wxt.config.ts]\nexport default defineConfig({\n  // Relative to project root\n  srcDir: \"src\",             // default: \".\"\n  modulesDir: \"wxt-modules\", // default: \"modules\"\n  outDir: \"dist\",            // default: \".output\"\n  publicDir: \"static\",       // default: \"public\"\n\n  // Relative to srcDir\n  entrypointsDir: \"entries\", // default: \"entrypoints\"\n})\n```\n\nYou can use absolute or relative paths.\n"
  },
  {
    "path": "docs/guide/essentials/publishing.md",
    "content": "---\noutline: deep\n---\n\n# Publishing\n\nWXT can ZIP your extension and submit it to various stores for review or for self-hosting.\n\n## First Time Publishing\n\nIf 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.\n\nFor specific details about each store, see the stores sections below.\n\n- [Chrome Web Store](#chrome-web-store)\n- [Firefox Addon Store](#firefox-addon-store)\n- [Edge Addons](#edge-addons)\n\n## Automation\n\nWXT provides two commands to help automate submitting a new version for review and publishing:\n\n- `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command\n- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)\n\nTo 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.\n\n> In CI, make sure you add all the environment variables to the submit step.\n\nTo submit a new version for publishing, build all the ZIPs you plan on releasing:\n\n```sh\nwxt zip\nwxt zip -b firefox\n```\n\nThen 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.\n\nIf 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.\n\n```sh\nwxt submit --dry-run \\\n  --chrome-zip .output/{your-extension}-{version}-chrome.zip \\\n  --firefox-zip .output/{your-extension}-{version}-firefox.zip --firefox-sources-zip .output/{your-extension}-{version}-sources.zip \\\n  --edge-zip .output/{your-extension}-{version}-chrome.zip\n```\n\nIf the dry run passes, remove the flag and do the actual release:\n\n```sh\nwxt submit \\\n  --chrome-zip .output/{your-extension}-{version}-chrome.zip \\\n  --firefox-zip .output/{your-extension}-{version}-firefox.zip --firefox-sources-zip .output/{your-extension}-{version}-sources.zip \\\n  --edge-zip .output/{your-extension}-{version}-chrome.zip\n```\n\n:::warning\nSee the [Firefox Addon Store](#firefox-addon-store) section for more details about the `--firefox-sources-zip` option.\n:::\n\n## GitHub Action\n\nHere'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.\n\n```yml\nname: Release\n\non:\n  workflow_dispatch:\n\njobs:\n  submit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - uses: pnpm/action-setup@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: 'pnpm'\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Zip extensions\n        run: |\n          pnpm zip\n          pnpm zip:firefox\n\n      - name: Submit to stores\n        run: |\n          pnpm wxt submit \\\n            --chrome-zip .output/*-chrome.zip \\\n            --firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip\n        env:\n          CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}\n          CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}\n          CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}\n          CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}\n          FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}\n          FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}\n          FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}\n```\n\nThe 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:\n\n- [`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\n- [`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.\n\n> 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.\n\n## Stores\n\n### Chrome Web Store\n\n> ✅ Supported &bull; [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) &bull; [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)\n\nTo create a ZIP for Chrome:\n\n```sh\nwxt zip\n```\n\n### Firefox Addon Store\n\n> ✅ Supported &bull; [Developer Dashboard](https://addons.mozilla.org/developers/) &bull; [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)\n\nFirefox 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/).\n\nWhen 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.\n\nTo customize which files are zipped, add the `zip` option to your config file.\n\n```ts [wxt.config.ts]\nimport { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  zip: {\n    // ...\n  },\n});\n```\n\nIf 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.\n\n:::code-group\n\n```sh [pnpm]\npnpm i\npnpm zip:firefox\n```\n\n```sh [npm]\nnpm i\nnpm run zip:firefox\n```\n\n```sh [yarn]\nyarn\nyarn zip:firefox\n```\n\n```sh [bun]\nbun i\nbun zip:firefox\n```\n\n:::\n\nEnsure 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.\n\nMake sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.\n\n:::warning\nIf 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.\n\nSee Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.\n:::\n\n#### Private Packages\n\nIf 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.\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  zip: {\n    downloadPackages: [\n      '@mycompany/some-package',\n      //...\n    ],\n  },\n});\n```\n\nDepending 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.\n\n:::warning\nWXT uses the command `npm pack <package-name>` 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.\n:::\n\n### Safari\n\n> 🚧 Not supported yet\n\nWXT 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:\n\n- [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.\"\n\nWhen running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.\n\n```sh\npnpm wxt build -b safari\nxcrun safari-web-extension-converter .output/safari-mv2\n```\n\n### Edge Addons\n\n> ✅ Supported &bull; [Developer Dashboard](https://aka.ms/PartnerCenterLogin) &bull; [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)\n\nNo need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP.\n\nHowever, if you have features specifically for Edge, create a separate ZIP with:\n\n```sh\nwxt zip -b edge\n```\n"
  },
  {
    "path": "docs/guide/essentials/remote-code.md",
    "content": "# Remote Code\n\nWXT 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).\n\n## Google Analytics\n\nFor example, you can import Google Analytics:\n\n```ts\n// utils/google-analytics.ts\nimport 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';\n\nwindow.dataLayer = window.dataLayer || [];\n// NOTE: This line is different from Google's documentation\nwindow.gtag = function () {\n  dataLayer.push(arguments);\n};\ngtag('js', new Date());\ngtag('config', 'G-XXXXXX');\n```\n\nThen you can import this in your HTML files to enable Google Analytics:\n\n```ts\n// popup/main.ts\nimport '~/utils/google-analytics';\n\ngtag('event', 'event_name', {\n  key: 'value',\n});\n```\n"
  },
  {
    "path": "docs/guide/essentials/scripting.md",
    "content": "# Scripting\n\n[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)\n\nRefer to the browser docs above for basics on how the API works.\n\n## Execute Script Return Values\n\nWhen 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.\n\n```ts\n// entrypoints/background.ts\nconst res = await browser.scripting.executeScript({\n  target: { tabId },\n  files: ['content-scripts/example.js'],\n});\nconsole.log(res); // \"Hello John!\"\n```\n\n```ts\n// entrypoints/example.content.ts\nexport default defineContentScript({\n  registration: 'runtime',\n  main(ctx) {\n    console.log('Script was executed!');\n    return 'Hello John!';\n  },\n});\n```\n"
  },
  {
    "path": "docs/guide/essentials/storage.md",
    "content": "# Storage\n\n[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)\n\nYou can use the vanilla APIs (see docs above), use [WXT's built-in storage API](/storage), or install a package from NPM.\n\n## Alternatives\n\n1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases\n\n2. 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.\n\n3. 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:\n   - [`webext-storage`](https://www.npmjs.com/package/webext-storage) - A more usable typed storage API for Web Extensions\n   - [`@webext-core/storage`](https://www.npmjs.com/package/@webext-core/storage) - A type-safe, localStorage-esque wrapper around the web extension storage APIs\n"
  },
  {
    "path": "docs/guide/essentials/target-different-browsers.md",
    "content": "# Targeting Different Browsers\n\nWhen building an extension with WXT, you can create multiple builds of your extension targeting different browsers and manifest versions.\n\n## Target a Browser\n\nUse the `-b` CLI flag to create a separate build of your extension for a specific browser. By default, `chrome` is targeted.\n\n```sh\nwxt            # same as: wxt -b chrome\nwxt -b firefox\nwxt -b custom\n```\n\nDuring 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).\n\nAdditionally, WXT defines several constants you can use at runtime to detect which browser is in use:\n\n```ts\nif (import.meta.env.BROWSER === 'firefox') {\n  console.log('Do something only in Firefox builds');\n}\nif (import.meta.env.FIREFOX) {\n  // Shorthand, equivalent to the if-statement above\n}\n```\n\nRead about [Built-in Environment Variables](/guide/essentials/config/environment-variables.html#built-in-environment-variables) for more details.\n\n## Target a Manifest Version\n\nTo target specific manifest versions, use the `--mv2` or `--mv3` CLI flags.\n\n:::tip Default Manifest Version\nBy default, WXT will target MV2 for Safari and Firefox and MV3 for all other browsers.\n:::\n\nSimilar 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):\n\n```ts\nif (import.meta.env.MANIFEST_VERSION === 2) {\n  console.log('Do something only in MV2 builds');\n}\n```\n\n## Filtering Entrypoints\n\nEvery entrypoint can be included or excluded when targeting specific browsers via the `include` and `exclude` options.\n\nHere are some examples:\n\n- Content script only built when targeting `firefox`:\n\n  ```ts\n  export default defineContentScript({\n    include: ['firefox'],\n\n    main(ctx) {\n      // ...\n    },\n  });\n  ```\n\n- HTML file only built for all targets other than `chrome`:\n\n  ```html\n  <!doctype html>\n  <html lang=\"en\">\n    <head>\n      <meta charset=\"UTF-8\" />\n      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n      <meta name=\"manifest.exclude\" content=\"['chrome', ...]\" />\n    </head>\n    <body>\n      <!-- ... -->\n    </body>\n  </html>\n  ```\n\nAlternatively, you can use the [`filterEntrypoints` config](/api/reference/wxt/interfaces/InlineConfig#filterentrypoints) to list all the entrypoints you want to build.\n"
  },
  {
    "path": "docs/guide/essentials/testing-updates.md",
    "content": "# Testing Updates\n\n## Testing Permission Changes\n\nWhen `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.\n\nYou can test if your permission changes will result in a disabled extension:\n\n- Chromium: Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)\n- Firefox: See their [Test Permission Requests](https://extensionworkshop.com/documentation/develop/test-permission-requests/) page\n- Safari: Everyone breaks something in production eventually... 🫡 Good luck soldier\n\n## Update Event\n\nYou can setup a callback that runs after your extension updates like so:\n\n```ts\nbrowser.runtime.onInstalled.addListener(({ reason }) => {\n  if (reason === 'update') {\n    // Do something\n  }\n});\n```\n\nIf 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:\n\n1. In dev mode, remove the `if` statement and reload the extension from `chrome://extensions`\n2. Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)\n"
  },
  {
    "path": "docs/guide/essentials/unit-testing.md",
    "content": "# Unit Testing\n\n[[toc]]\n\n## Vitest\n\nWXT provides first class support for Vitest for unit testing:\n\n```ts\n// vitest.config.ts\nimport { defineConfig } from 'vitest/config';\nimport { WxtVitest } from 'wxt/testing/vitest-plugin';\n\nexport default defineConfig({\n  plugins: [WxtVitest()],\n});\n```\n\nThis plugin does several things:\n\n- Polyfills the extension API, `browser`, with an in-memory implementation using [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation)\n- Adds all vite config or plugins in `wxt.config.ts`\n- Configures auto-imports (if enabled)\n- Applies internal WXT vite plugins for things like [bundling remote code](/guide/essentials/remote-code)\n- Sets up global variables provided by WXT (`import.meta.env.BROWSER`, `import.meta.env.MANIFEST_VERSION`, `import.meta.env.IS_CHROME`, etc)\n- Configures aliases (`@/*`, `@@/*`, etc) so imports can be resolved\n\nHere are real projects with unit testing setup. Look at the code and tests to see how they're written.\n\n- [`aklinker1/github-better-line-counts`](https://github.com/aklinker1/github-better-line-counts)\n- [`wxt-dev/examples`'s Vitest Example](https://github.com/wxt-dev/examples/tree/main/examples/vitest-unit-testing)\n\n### Example Tests\n\nThis 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!\n\n```ts\nimport { describe, it, expect } from 'vitest';\nimport { fakeBrowser } from 'wxt/testing/fake-browser';\n\nconst accountStorage = storage.defineItem<Account>('local:account');\n\nasync function isLoggedIn(): Promise<Account> {\n  const value = await accountStorage.getValue();\n  return value != null;\n}\n\ndescribe('isLoggedIn', () => {\n  beforeEach(() => {\n    // See https://webext-core.aklinker1.io/fake-browser/reseting-state\n    fakeBrowser.reset();\n  });\n\n  it('should return true when the account exists in storage', async () => {\n    const account: Account = {\n      username: '...',\n      preferences: {\n        // ...\n      },\n    };\n    await accountStorage.setValue(account);\n\n    expect(await isLoggedIn()).toBe(true);\n  });\n\n  it('should return false when the account does not exist in storage', async () => {\n    await accountStorage.deleteValue();\n\n    expect(await isLoggedIn()).toBe(false);\n  });\n});\n```\n\n### Mocking WXT APIs\n\nFirst, 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.\n\nFor example, this is what your write in your source code:\n\n```ts\n// What you write\nimport { injectScript, createShadowRootUi } from '#imports';\n```\n\nBut Vitest sees this:\n\n```ts\nimport { injectScript } from 'wxt/utils/inject-script';\nimport { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';\n```\n\nSo in this case, if you wanted to mock `injectScript`, you need to pass in `\"wxt/utils/inject-script\"`, not `\"#imports\"`.\n\n```ts\nvi.mock(\"wxt/utils/inject-script\", () => ({\n  injectScript: ...\n}))\n```\n\nRefer 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).\n\n## Other Testing Frameworks\n\nTo 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.\n\nIt 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:\n\n<https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts>\n"
  },
  {
    "path": "docs/guide/essentials/wxt-modules.md",
    "content": "---\noutline: deep\n---\n\n# WXT Modules\n\nWXT provides a \"module system\" that let's you run code at different steps in the build process to modify it.\n\n[[toc]]\n\n## Adding a Module\n\nThere are two ways to add a module to your project:\n\n1. **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:\n\n   ```ts [wxt.config.ts]\n   export default defineConfig({\n     modules: ['@wxt-dev/auto-icons'],\n   });\n   ```\n\n   > Searching for [\"wxt module\"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.\n\n2. **Local**: add a file to your project's `modules/` directory:\n\n   ```plaintext\n   <rootDir>/\n     modules/\n       my-module.ts\n   ```\n\n   > To learn more about writing your own modules, read the [Writing Modules](/guide/essentials/wxt-modules) docs.\n\n## Module Options\n\nWXT modules may require or allow setting custom options to change their behavior. There are two types of options:\n\n1. **Build-time**: Any config used during the build process, like feature flags\n2. **Runtime**: Any config accessed at runtime, like callback functions\n\nBuild-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.\n\nIf you use TypeScript, modules augment WXT's types so you will get type errors if options are missing or incorrect.\n\n## Execution Order\n\nModules are loaded in the same order as hooks are executed. Refer to the [Hooks documentation](/guide/essentials/config/hooks#execution-order) for more details.\n\n## Writing Modules\n\nHere's what a basic WXT module looks like:\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\n\nexport default defineWxtModule({\n  setup(wxt) {\n    // Your module code here...\n  },\n});\n```\n\nEach 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:\n\n- Use `wxt.hook(...)` to hook into the build's lifecycle and make changes\n- Use `wxt.config` to get the resolved config from the project's `wxt.config.ts` file\n- Use `wxt.logger` to log messages to the console\n- and more!\n\nRefer to the [API reference](/api/reference/wxt/interfaces/Wxt) for a complete list of properties and functions available.\n\nAlso make sure to read about [all the hooks that are available](/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.\n\n### Recipes\n\nModules are complex and require a deeper understanding of WXT's code and how it works. The best way to learn is by example.\n\n#### Update resolved config\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\n\nexport default defineWxtModule({\n  setup(wxt) {\n    wxt.hook('config:resolved', () => {\n      wxt.config.outDir = 'dist';\n    });\n  },\n});\n```\n\n#### Add built-time config\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\nimport 'wxt';\n\nexport interface MyModuleOptions {\n  // Add your build-time options here...\n}\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    // Add types for the \"myModule\" key in wxt.config.ts\n    myModule: MyModuleOptions;\n  }\n}\n\nexport default defineWxtModule<AnalyticModuleOptions>({\n  configKey: 'myModule',\n\n  // Build time config is available via the second argument of setup\n  setup(wxt, options) {\n    console.log(options);\n  },\n});\n```\n\n#### Add runtime config\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\nimport 'wxt/utils/define-app-config';\n\nexport interface MyModuleRuntimeOptions {\n  // Add your runtime options here...\n}\ndeclare module 'wxt/utils/define-app-config' {\n  export interface WxtAppConfig {\n    myModule: MyModuleOptions;\n  }\n}\n```\n\nRuntime options are returned when calling\n\n```ts\nconst config = getAppConfig();\nconsole.log(config.myModule);\n```\n\nThis is very useful when [generating runtime code](#generate-runtime-module).\n\n#### Add custom entrypoint options\n\nModules can add custom options to entrypoints by augmenting the entrypoint options types. This allows you to add custom configuration that can be accessed during the build process.\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\nimport 'wxt';\n\ndeclare module 'wxt' {\n  export interface BackgroundEntrypointOptions {\n    // Add custom options to the background entrypoint\n    myCustomOption?: string;\n  }\n}\n\nexport default defineWxtModule({\n  setup(wxt) {\n    wxt.hook('entrypoints:resolved', (_, entrypoints) => {\n      const background = entrypoints.find((e) => e.type === 'background');\n      if (background) {\n        console.log('Custom option:', background.options.myCustomOption);\n      }\n    });\n  },\n});\n```\n\nNow users can set the custom option in their entrypoint:\n\n```ts [entrypoints/background.ts]\nexport default defineBackground({\n  myCustomOption: 'custom value',\n  main() {\n    // ...\n  },\n});\n```\n\nThis works for all other JS and HTML entrypoints, here's an example of how to pass a custom option from an HTML file.\n\n```html [entrypoints/popup.html]\n<html>\n  <head>\n    <meta name=\"wxt.myHtmlOption\" content=\"custom value\" />\n    <title>Popup</title>\n  </head>\n  <body>\n    <!-- ... -->\n  </body>\n</html>\n```\n\n#### Generate output file\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\n\nexport default defineWxtModule({\n  setup(wxt) {\n    // Relative to the output directory\n    const generatedFilePath = 'some-file.txt';\n\n    wxt.hook('build:publicAssets', (_, assets) => {\n      assets.push({\n        relativeDest: generatedFilePath,\n        contents: 'some generated text',\n      });\n    });\n\n    wxt.hook('build:manifestGenerated', (_, manifest) => {\n      manifest.web_accessible_resources ??= [];\n      manifest.web_accessible_resources.push({\n        matches: ['*://*'],\n        resources: [generatedFilePath],\n      });\n    });\n  },\n});\n```\n\nThis file could then be loaded at runtime:\n\n```ts\nconst res = await fetch(browser.runtime.getURL('/some-text.txt'));\n```\n\n#### Add custom entrypoints\n\nOnce the existing files under the `entrypoints/` directory have been discovered, the `entrypoints:found` hook can be used to add custom entrypoints.\n\n:::info\nThe `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.\n:::\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\n\nexport default defineWxtModule({\n  setup(wxt) {\n    wxt.hook('entrypoints:found', (_, entrypointInfos) => {\n      // Add your new entrypoint\n      entrypointInfos.push({\n        name: 'my-custom-script',\n        inputPath: 'path/to/custom-script.js',\n        type: 'content-script',\n      });\n    });\n  },\n});\n```\n\n#### Generate runtime module\n\nCreate a file in `.wxt`, add an alias to import it, and add auto-imports for exported variables.\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\nimport { resolve } from 'node:path';\n\nexport default defineWxtModule({\n  imports: [\n    // Add auto-imports\n    { from: '#analytics', name: 'analytics' },\n    { from: '#analytics', name: 'reportEvent' },\n    { from: '#analytics', name: 'reportPageView' },\n  ],\n\n  setup(wxt) {\n    const analyticsModulePath = resolve(\n      wxt.config.wxtDir,\n      'analytics/index.ts',\n    );\n    const analyticsModuleCode = `\n      import { createAnalytics } from 'some-module';\n\n      export const analytics = createAnalytics(getAppConfig().analytics);\n      export const { reportEvent, reportPageView } = analytics;\n    `;\n\n    addAlias(wxt, '#analytics', analyticsModulePath);\n\n    wxt.hook('prepare:types', async (_, entries) => {\n      entries.push({\n        path: analyticsModulePath,\n        text: analyticsModuleCode,\n      });\n    });\n  },\n});\n```\n\n#### Generate declaration file\n\n```ts\nimport { defineWxtModule } from 'wxt/modules';\nimport { resolve } from 'node:path';\n\nexport default defineWxtModule({\n  setup(wxt) {\n    const typesPath = resolve(wxt.config.wxtDir, 'my-module/types.d.ts');\n    const typesCode = `\n      // Declare global types, perform type augmentation\n    `;\n\n    wxt.hook('prepare:types', async (_, entries) => {\n      entries.push({\n        path: 'my-module/types.d.ts',\n        text: `\n          // Declare global types, perform type augmentation, etc\n        `,\n        // IMPORTANT - without this line your declaration file will not be a part of the TS project:\n        tsReference: true,\n      });\n    });\n  },\n});\n```\n\n### Example Modules\n\nYou should also look through the code of modules other people have written and published. Here's some examples:\n\n- [`@wxt-dev/auto-icons`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons)\n- [`@wxt-dev/i18n`](https://github.com/wxt-dev/wxt/blob/main/packages/i18n)\n- [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/blob/main/packages/module-vue)\n- [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/blob/main/packages/module-solid)\n- [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/blob/main/packages/module-react)\n- [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/blob/main/packages/module-svelte)\n"
  },
  {
    "path": "docs/guide/installation.md",
    "content": "# Installation\n\nBootstrap a new project, start from scratch, or [migrate an existing project](/guide/resources/migrate).\n\n[[toc]]\n\n## Bootstrap Project\n\nRun the [`init` command](/api/cli/wxt-init), and follow the instructions.\n\n:::code-group\n\n```sh [PNPM]\npnpm dlx wxt@latest init\n```\n\n```sh [Bun]\nbunx wxt@latest init\n```\n\n```sh [NPM]\nnpx wxt@latest init\n```\n\n```sh [Yarn]\n# Use NPM initially, but select Yarn when prompted\nnpx wxt@latest init\n```\n\n:::\n\n:::info Starter Templates:\n[<Icon name=\"TypeScript\" style=\"margin-left: 16px;\" />Vanilla](https://github.com/wxt-dev/wxt/tree/main/templates/vanilla)<br/>[<Icon name=\"Vue\" style=\"margin-left: 16px;\" />Vue](https://github.com/wxt-dev/wxt/tree/main/templates/vue)<br/>[<Icon name=\"React\" style=\"margin-left: 16px;\" />React](https://github.com/wxt-dev/wxt/tree/main/templates/react)<br/>[<Icon name=\"Svelte\" style=\"margin-left: 16px;\" />Svelte](https://github.com/wxt-dev/wxt/tree/main/templates/svelte)<br/>[<Icon name=\"Solid\" icon=\"https://www.solidjs.com/img/favicons/favicon-32x32.png\"  style=\"margin-left: 16px;\" />Solid](https://github.com/wxt-dev/wxt/tree/main/templates/solid)\n\n<small style=\"opacity: 50%\">All templates use TypeScript by default. To use JavaScript, change the file extensions.</small>\n:::\n\n### Demo\n\n![wxt init demo](/assets/init-demo.gif)\n\nOnce you've run the `dev` command, continue to [Next Steps](#next-steps)!\n\n## From Scratch\n\n1. Create a new project\n   :::code-group\n\n   ```sh [PNPM]\n   cd my-project\n   pnpm init\n   ```\n\n   ```sh [Bun]\n   cd my-project\n   bun init\n   ```\n\n   ```sh [NPM]\n   cd my-project\n   npm init\n   ```\n\n   ```sh [Yarn]\n   cd my-project\n   yarn init\n   ```\n\n   :::\n\n2. Install WXT:\n   :::code-group\n\n   ```sh [PNPM]\n   pnpm i -D wxt\n   ```\n\n   ```sh [Bun]\n   bun i -D wxt\n   ```\n\n   ```sh [NPM]\n   npm i -D wxt\n   ```\n\n   ```sh [Yarn]\n   yarn add --dev wxt\n   ```\n\n   :::\n\n3. Add an entrypoint, `my-project/entrypoints/background.ts`:\n   :::code-group\n\n   ```ts\n   export default defineBackground(() => {\n     console.log('Hello world!');\n   });\n   ```\n\n   :::\n\n4. Add scripts to your `package.json`:\n\n   ```json [package.json]\n   {\n     \"scripts\": {\n       \"dev\": \"wxt\", // [!code ++]\n       \"dev:firefox\": \"wxt -b firefox\", // [!code ++]\n       \"build\": \"wxt build\", // [!code ++]\n       \"build:firefox\": \"wxt build -b firefox\", // [!code ++]\n       \"zip\": \"wxt zip\", // [!code ++]\n       \"zip:firefox\": \"wxt zip -b firefox\", // [!code ++]\n       \"postinstall\": \"wxt prepare\" // [!code ++]\n     }\n   }\n   ```\n\n5. Run your extension in dev mode\n   :::code-group\n\n   ```sh [PNPM]\n   pnpm dev\n   ```\n\n   ```sh [Bun]\n   bun run dev\n   ```\n\n   ```sh [NPM]\n   npm run dev\n   ```\n\n   ```sh [Yarn]\n   yarn dev\n   ```\n\n   :::\n   WXT will automatically open a browser window with your extension installed.\n\n## Next Steps\n\n- Keep reading on about WXT's [Project Structure](/guide/essentials/project-structure) and other essential concepts to learn\n- Configure [automatic browser startup](/guide/essentials/config/browser-startup) during dev mode\n- Explore [WXT's example library](/examples) to see how to use specific APIs or perform common tasks\n- Checkout the [community page](/guide/resources/community) for a list of resources made by the community!\n"
  },
  {
    "path": "docs/guide/introduction.md",
    "content": "# Welcome to WXT\n\nWXT is a modern, open-source framework for building web extensions. Inspired by Nuxt, its goals are to:\n\n- Provide an awesome [DX](https://about.gitlab.com/topics/devops/what-is-developer-experience/)\n- Provide first-class support for all major browsers\n\nCheck out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extensions.\n\n## Prerequisites\n\nThese docs assume you have a basic knowledge of how web extensions are structured and how you access the extension APIs.\n\n:::warning New to extension development?\nIf 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.\n:::\n\nYou 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.\n\n<br/>\n\n---\n\n<br/>\n\nAlright, 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.\n"
  },
  {
    "path": "docs/guide/resources/community.md",
    "content": "# Community\n\nThis 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!\n\n[[toc]]\n\n## Blog Posts\n\n- [Building Modern Cross Browser Web Extensions](https://aabidk.dev/tags/wxt/) by Aabid ([@aabidk20](https://github.com/aabidk20))\n- [Building Browser Extensions with WXT](https://rxliuli.com/blog/browser-extension-dev-01-introduction-to-basic-concepts/) by rxliuli ([@rxliuli](https://github.com/rxliuli))\n\n## NPM Packages\n\n- [`@webext-core/*`](https://webext-core.aklinker1.io/): Easy-to-use utilities for writing and testing web extensions that work on all browsers.\n- [`Comctx`](https://github.com/molvqingtai/comctx): Cross-context RPC solution with type safety and flexible adapters.\n- [`wxt-local-analytics`](https://github.com/HaNdTriX/wxt-local-analytics): Local analytics provider for [`@wxt-dev/analytics`](https://wxt.dev/analytics)\n"
  },
  {
    "path": "docs/guide/resources/compare.md",
    "content": "# Compare\n\nLets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another framework) and [CRXJS](https://crxjs.dev/vite-plugin) (a bundler plugin).\n\n## Overview\n\n- ✅ - Full support\n- 🟡 - Partial support\n- ❌ - No support\n\n| Features                                                |   WXT   | Plasmo  |  CRXJS  |\n| ------------------------------------------------------- | :-----: | :-----: | :-----: |\n| Maintained                                              |   ✅    | 🟡 [^n] | 🟡 [^m] |\n| Supports all browsers                                   |   ✅    |   ✅    | 🟡 [^j] |\n| MV2 Support                                             |   ✅    |   ✅    | 🟡 [^a] |\n| MV3 Support                                             |   ✅    |   ✅    | 🟡 [^a] |\n| Create Extension ZIPs                                   |   ✅    |   ✅    |   ❌    |\n| Create Firefox Sources ZIP                              |   ✅    |   ❌    |   ❌    |\n| First-class TypeScript support                          |   ✅    |   ✅    |   ✅    |\n| Entrypoint discovery                                    | ✅ [^b] | ✅ [^b] |   ❌    |\n| Inline entrypoint config                                |   ✅    |   ✅    | ❌ [^i] |\n| Auto-imports                                            |   ✅    |   ❌    |   ❌    |\n| Reusable module system                                  |   ✅    |   ❌    |   ❌    |\n| Supports all frontend frameworks                        |   ✅    | 🟡 [^c] |   ✅    |\n| Framework specific entrypoints (like `Popup.tsx`)       | 🟡 [^d] | ✅ [^e] |   ❌    |\n| Automated publishing                                    |   ✅    |   ✅    |   ❌    |\n| Remote Code Bundling (Google Analytics)                 |   ✅    |   ✅    |   ❌    |\n| Unlisted HTML Pages                                     |   ✅    |   ✅    |   ✅    |\n| Unlisted Scripts                                        |   ✅    |   ❌    |   ❌    |\n| ESM Content Scripts                                     | ❌ [^l] |   ❌    |   ✅    |\n| <strong style=\"opacity: 50%\">Dev Mode</strong>          |         |         |         |\n| `.env` Files                                            |   ✅    |   ✅    |   ✅    |\n| Opens browser with extension installed                  |   ✅    |   ❌    |   ❌    |\n| HMR for UIs                                             |   ✅    | 🟡 [^f] |   ✅    |\n| Reload HTML Files on Change                             |   ✅    | 🟡 [^g] |   ✅    |\n| Reload Content Scripts on Change                        |   ✅    | 🟡 [^g] |   ✅    |\n| Reload Background on Change                             | 🟡 [^g] | 🟡 [^g] | 🟡 [^g] |\n| Respects Content Script `run_at`                        |   ✅    |   ✅    | ❌ [^h] |\n| <strong style=\"opacity: 50%\">Built-in Wrappers</strong> |         |         |         |\n| Storage                                                 |   ✅    |   ✅    | ❌ [^k] |\n| Messaging                                               | ❌ [^k] |   ✅    | ❌ [^k] |\n| Content Script UI                                       |   ✅    |   ✅    | ❌ [^k] |\n| I18n                                                    |   ✅    |   ❌    |   ❌    |\n\n[^a]: Either MV2 or MV3, not both.\n\n[^b]: File based.\n\n[^c]: Only React, Vue, and Svelte.\n\n[^d]: `.html`, `.ts`, `.tsx`.\n\n[^e]: `.html`, `.ts`, `.tsx`, `.vue`, `.svelte`.\n\n[^f]: React only.\n\n[^g]: Reloads entire extension.\n\n[^h]: ESM-style loaders run asynchronously.\n\n[^i]: Entrypoint options all configured in `manifest.json`.\n\n[^j]: As of `v2.0.0-beta.23`, but v2 stable hasn't been released yet.\n\n[^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.\n\n[^l]: WIP, moving very slowly. Follow [wxt-dev/wxt#357](https://github.com/wxt-dev/wxt/issues/357) for updates.\n\n[^m]: See [crxjs/chrome-extension-tools#974](https://github.com/crxjs/chrome-extension-tools/discussions/974)\n\n[^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))_\n"
  },
  {
    "path": "docs/guide/resources/faq.md",
    "content": "---\noutline: false\n---\n\n# FAQ\n\nCommonly asked questions about how to use WXT or why it behaves the way it does.\n\n[[toc]]\n\n## Why aren't content scripts added to the manifest?\n\nDuring development, WXT registers content scripts dynamically so they can be reloaded individually when a file is saved without reloading your entire extension.\n\nTo list the content scripts registered during development, open the service worker's console and run:\n\n```js\nawait chrome.scripting.getRegisteredContentScripts();\n```\n\n## How do I disable opening the browser automatically during development?\n\nSee <https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser>\n\n## How do I stay logged into a website during development?\n\nSee <https://wxt.dev/guide/essentials/config/browser-startup.html#persist-data>\n\n## My component library doesn't work in content scripts\n\nThis is usually caused by one of two things (or both) when using `createShadowRootUi`:\n\n1. Styles are added outside the `ShadowRoot`\n\n   :::details\n   Some component libraries manually add CSS to the page by adding a `<style>` or `<link>` element. They place this element in the document's `<head>` by default. This causes your styles to be placed outside the `ShadowRoot` and it's isolation blocks the styles from being applied to your UI.\n\n   When a library does this, **you need to tell the library where to put its styles**. Here's the documentation for a few popular component libraries:\n   - Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)\n   - Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)\n\n   > If your library isn't listed above, try searching it's docs/issues for \"shadow root\", \"shadow dom\", or \"css container\". Not all libraries support shadow DOMs, you may have to open an issue to request this feature.\n\n   Here's an example of configuring Antd's styles:\n\n   ```tsx\n   import { StyleProvider } from '@ant-design/cssinjs';\n   import ReactDOM from 'react-dom/client';\n   import App from './App.tsx';\n\n   const ui = await create`ShadowRoot`Ui(ctx, {\n     // ...\n     onMount: (container, shadow) => {\n       const cssContainer = shadow.querySelector('head')!;\n       const root = ReactDOM.createRoot(container);\n       root.render(\n         <StyleProvider container={cssContainer}>\n           <App />\n         </StyleProvider>,\n       );\n       return root;\n     },\n   });\n   ```\n\n   :::\n\n2. UI elements are added outside the `ShadowRoot`\n\n   ::::::details\n   This is mostly caused by `Teleport` or `Portal` components that render an element somewhere else in the DOM, usually in the document's `<body>`. This is usually done for dialogs or popover components. This renders the element is outside the `ShadowRoot`, so styles are not applied to it.\n\n   To fix this, **you need to both provide a target to your app AND pass the target to the `Teleport`/`Portal`**.\n\n   First, store the reference to the `ShadowRoot`'s `<body>` element (not the document's `<body>`):\n\n   :::code-group\n\n   ```ts [Vue]\n   import { createApp } from 'vue';\n   import App from './App.vue';\n\n   const ui = await create`ShadowRoot`Ui(ctx, {\n     // ...\n     onMount: (container, shadow) => {\n       const teleportTarget = shadow.querySelector('body')!;\n       const app = createApp(App)\n         .provide('TeleportTarget', teleportTarget)\n         .mount(container);\n       return app;\n     },\n   });\n   ui.mount();\n   ```\n\n   ```tsx [React]\n   // hooks/PortalTargetContext.ts\n   import { createContext } from 'react';\n\n   export const PortalTargetContext = createContext<HTMLElement>();\n\n   // entrypoints/example.content.ts\n   import ReactDOM from 'react-dom/client';\n   import App from './App.tsx';\n   import PortalTargetContext from '~/hooks/PortalTargetContext';\n\n   const ui = await create`ShadowRoot`Ui(ctx, {\n     // ...\n     onMount: (container, shadow) => {\n       const portalTarget = shadow.querySelector('body')!;\n       const root = ReactDOM.createRoot(container);\n       root.render(\n         <PortalTargetContext.Provider value={portalTarget}>\n           <App />\n         </PortalTargetContext.Provider>,\n       );\n       return root;\n     },\n   });\n   ui.mount();\n   ```\n\n   :::\n\n   Then use the reference when teleporting/portaling part of your UI to a different place in the DOM:\n\n   :::code-group\n\n   ```vue [Vue]\n   <script lang=\"ts\" setup>\n   import { Teleport } from 'vue';\n\n   const teleportTarget = inject('TeleportTarget');\n   </script>\n\n   <template>\n     <div>\n       <Teleport :to=\"teleportTarget\">\n         <dialog>My dialog</dialog>\n       </Teleport>\n     </div>\n   </template>\n   ```\n\n   ```tsx [React]\n   import { useContext } from 'react';\n   import { createPortal } from 'react-dom';\n   import PortalTargetContext from '~/hooks/PortalTargetContext';\n\n   const MyComponent = () => {\n     const portalTarget = useContext(PortalTargetContext);\n\n     return <div>{createPortal(<dialog>My dialog</dialog>, portalTarget)}</div>;\n   };\n   ```\n\n   :::\n\n   If you use ShadCN, [see this blog post](https://aabidk.dev/blog/building-modern-cross-browser-web-extensions-content-scripts-and-ui/#using-radixui-portals-to-move-the-dialog-to-shadow-dom).\n\n   ::::::\n\nBoth issues have the same cause: the library puts something outside the `ShadowRoot`, and the `ShadowRoot`'s isolation prevents CSS from being applied to your UI.\n\nBoth issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.\n\n## Does WXT provide docs for LLMs?\n\nYes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).\n\n## Is there an LLM trained on WXT's docs that I chat with?\n\nYes! There's a \"Ask AI\" button in the bottom right of the page, try it out! Or visit <https://knowledge.wxt.dev/> for a fullscreen experience.\n\nAdditionally, if you want to train your own model or provide context to your editor, you can use the LLM knowledge files hosted by the site:\n\n<https://wxt.dev/knowledge/index.json>\n\nYou don't need to crawl the entire website, these files already contain all the relevant docs for training a LLM on WXT. But feel free to crawl it and generate your own files if you want!\n\n## How do I run my WXT project with docker / [devcontainers](https://containers.dev)?\n\nTo run the WXT dev server in a devcontainer, but load the dev build of your extension in your browser:\n\n1. **Bind-mount your project directory to your host**\n   If you're using VS Code, you can open your project folder with the `Dev Containers: Open Folder in Container...` command. This keeps the folder synchronized between your host and the devcontainer, ensuring that the extension `dist` directory remains accessible from the host.\n\n2. **Disable auto-opening the browser**\n   WXT automatically opens your browser during development, but since you're running inside a container, it won't be able to access it. Follow [the guide here](https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser) to disable browser auto-opening in your `wxt.config.ts`.\n\n3. **Tell WXT to listen on all network interfaces**\n   To enable hot-reloading, your extension has to connect to the WXT dev server running inside your container. WXT will only listen on `localhost` by default, which prevents connections from outside the devcontainer. To fix this you can instruct WXT to listen on all interfaces with `wxt --host 0.0.0.0`.\n\n## How do I use the new Prompt API in Chrome?\n\nThe service responsible for the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api) is not enabled by default if you let WXT open the browser during dev mode. When checking `LanguageModel.availability`, you will always receive \"unavailable\".\n\nYou have two options:\n\n1. Pass the `--disable-features=DisableLoadExtensionCommandLineSwitch` feature flag to enable the service in the browser WXT opens:\n\n   ```ts\n   // wxt.config.ts\n   export default defineConfig({\n     webExt: {\n       chromiumArgs: [\n         '--disable-features=DisableLoadExtensionCommandLineSwitch',\n       ],\n     },\n   });\n   ```\n\n2. Disable the runner and install your extension in your regular chrome profile manually:\n\n   ```ts\n   // wxt.config.ts\n   export default defineConfig({\n     webExt: {\n       disabled: true,\n     },\n   });\n   ```\n"
  },
  {
    "path": "docs/guide/resources/how-wxt-works.md",
    "content": "# How WXT Works\n\n:::warning 🚧 Under construction\nThese docs will be coming soon!\n:::\n"
  },
  {
    "path": "docs/guide/resources/migrate.md",
    "content": "---\noutline: deep\n---\n\n# Migrate to WXT\n\n> If you have problems migrating to WXT, feel free to ask for help in GitHub by [starting a discussion](https://github.com/wxt-dev/wxt/discussions/new?category=q-a) or in [Discord](https://discord.gg/ZFsZqGery9)!\n\n## Overview\n\nAlways start by generating a new vanilla project and merging it into your project one file at a time.\n\n```sh\ncd path/to/your/project\npnpm dlx wxt@latest init example-wxt --template vanilla\n```\n\n:::tip\nWe recommend reviewing [project structure](/guide/essentials/project-structure.md) before you get started.\nYou can customize directory names in `wxt.config.ts` to match your project's needs.\n:::\n\nIn general, you'll need to:\n\n&ensp;<input type=\"checkbox\" /> Install `wxt`<br />\n&ensp;<input type=\"checkbox\" /> [Extend `.wxt/tsconfig.json`](/guide/essentials/config/typescript#typescript-configuration) in your project's `tsconfig.json`<br />\n&ensp;<input type=\"checkbox\" /> Update/create `package.json` scripts to use `wxt` (don't forget about `postinstall`)<br />\n&ensp;<input type=\"checkbox\" /> Move entrypoints into `entrypoints/` directory<br />\n&ensp;<input type=\"checkbox\" /> Move assets into either the `assets/` or `public/` directories<br />\n&ensp;<input type=\"checkbox\" /> Move `manifest.json` content into `wxt.config.ts`<br />\n&ensp;<input type=\"checkbox\" /> Convert custom import syntax to be compatible with Vite<br />\n&ensp;<input type=\"checkbox\" /> Add a default export to JS entrypoints (`defineBackground`, `defineContentScript`, or `defineUnlistedScript`)<br />\n&ensp;<input type=\"checkbox\" /> Use the `browser` global instead of `chrome`<br />\n&ensp;<input type=\"checkbox\" /> ⚠️ Compare final `manifest.json` files, making sure permissions and host permissions are unchanged<br/>\n:::warning\nIf your extension is already live on the Chrome Web Store, use [Google's update testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool) to make sure no new permissions are being requested.\n:::\n\nEvery project is different, so there's no one-solution-fits-all to migrating your project. Just make sure `wxt dev` runs, `wxt build` results in a working extension, and the list of permissions in the `manifest.json` hasn't changed. If all that looks good, you've finished migrating your extension!\n\n## Popular Tools/Frameworks\n\nHere's specific steps for other popular frameworks/build tools.\n\n### Plasmo\n\n1. Install `wxt`\n2. Move entrypoints into `entrypoints/` directory\n   - For JS entrypoints, merge the named exports used to configure your JS entrypoints into WXT's default export\n   - For HTML entrypoints, you cannot use JSX/Vue/Svelte files directly, you need to create an HTML file and manually create and mount your app. Refer to the [React](https://github.com/wxt-dev/wxt/tree/main/templates/react/entrypoints/popup), [Vue](https://github.com/wxt-dev/wxt/tree/main/templates/vue/entrypoints/popup), and [Svelte](https://github.com/wxt-dev/wxt/tree/main/templates/svelte/src/entrypoints/popup) templates as an example.\n3. Move public `assets/*` into the `public/` directory\n4. If you use CSUI, migrate to WXT's `createContentScriptUi`\n5. Convert Plasmo's custom import resolutions to Vite's\n6. If importing remote code via a URL, add a `url:` prefix so it works with WXT\n7. Replace your [Plasmo tags](https://docs.plasmo.com/framework/workflows/build#with-a-custom-tag) (`--tag`) with [WXT build modes](/guide/essentials/config/build-mode) (`--mode`)\n8. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same.\n\n### CRXJS\n\nIf you used CRXJS's vite plugin, it's a simple refactor! The main difference between CRXJS and WXT is how the tools decide which entrypoints to build. CRXJS looks at your `manifest` (and vite config for \"unlisted\" entries), while WXT looks at files in the `entrypoints` directory.\n\nTo migrate:\n\n1. Move all entrypoints into the `entrypoints` directory, refactoring to WXT's style (TS files have a default export).\n2. Move [entrypoint specific options out of the manifest](/guide/essentials/entrypoints#defining-manifest-options) and into the entrypoint files themselves (like content script `matches` or `run_at`).\n3. Move any other `manifest.json` options [into the `wxt.config.ts` file](/guide/essentials/config/manifest), like permissions.\n4. For simplicity, you'll probably want to [disable auto-imports](/guide/essentials/config/auto-imports#disabling-auto-imports) at first (unless you were already using them via `unimport` or `unplugin-auto-imports`). If you like the feature, you can enable it later once you've finished the migration.\n5. Update your `package.json` to include all of [WXT's suggested scripts (see step 4)](/guide/installation#from-scratch)\n6. Specifically, make sure you add the `\"postinstall\": \"wxt prepare\"` script to your `package.json`.\n7. Delete your `vite.config.ts` file. Move any plugins into the `wxt.config.ts` file. If you use a frontend framework, [install the relevant WXT module](/guide/essentials/frontend-frameworks).\n8. Update your typescript project. [Extend WXT's generated config](/guide/essentials/config/typescript), and [add any path aliases to your `wxt.config.ts` file](/guide/essentials/config/typescript#tsconfig-paths).\n9. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same.\n\nHere's an example migration: [GitHub Better Line Counts - CRXJS &rarr; WXT](https://github.com/aklinker1/github-better-line-counts/commit/39d766d2ba86866efefc2e9004af554ee434e2a8)\n\n### `vite-plugin-web-extension`\n\nSince you're already using Vite, it's a simple refactor.\n\n1. Install `wxt`\n2. Move and refactor your entrypoints to WXT's style (with a default export)\n3. Update package.json scripts to use `wxt`\n4. Add `\"postinstall\": \"wxt prepare\"` script\n5. Move the `manifest.json` into `wxt.config.ts`\n6. Move any custom settings from `vite.config.ts` into `wxt.config.ts`'s\n7. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same.\n"
  },
  {
    "path": "docs/guide/resources/upgrading.md",
    "content": "---\noutline: deep\n---\n\n# Upgrading WXT\n\n## Overview\n\nTo upgrade WXT to the latest major version:\n\n1. Install it, skipping scripts so `wxt prepare` doesn't run - it will probably throw an error after a major version change (we'll run it later).\n\n   ```sh\n   pnpm i wxt@latest --ignore-scripts\n   ```\n\n2. Follow the upgrade steps below to fix any breaking changes.\n3. Run `wxt prepare`. It should succeed and type errors will go away afterwords.\n\n   ```sh\n   pnpm wxt prepare\n   ```\n\n4. Manually test to make sure both dev mode and production builds work.\n\nFor minor or patch version updates, there are no special steps. Just update it with your package manager:\n\n```sh\npnpm i wxt@latest\n```\n\n---\n\nListed below are all the breaking changes you should address when upgrading to a new version of WXT.\n\nCurrently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.\n\n## v0.19.0 &rarr; v0.20.0\n\nv0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.\n\n:::tip\nRead through all the changes once before updating your code.\n:::\n\n### `webextension-polyfill` Removed\n\nWXT's `browser` no longer uses the `webextension-polyfill`!\n\n:::details Why?\nSee <https://github.com/wxt-dev/wxt/issues/784>\n:::\n\nTo upgrade, you have two options:\n\n1. **Stop using the polyfill**\n   - If you're already using `extensionApi: \"chrome\"`, then you're not using the polyfill and there is nothing to change!\n   - Otherwise there is only one change: `browser.runtime.onMessage` no longer supports using promises to return a response:\n\n     ```ts\n     browser.runtime.onMessage.addListener(async () => { // [!code --]\n       const res = await someAsyncWork(); // [!code --]\n       return res; // [!code --]\n     browser.runtime.onMessage.addListener(async (_message, _sender, sendResponse) => { // [!code ++]\n       someAsyncWork().then((res) => { // [!code ++]\n         sendResponse(res); // [!code ++]\n       }); // [!code ++]\n       return true; // [!code ++]\n     });\n     ```\n\n2. **Continue using the polyfill** - If you want to keep using the polyfill, you can! One less thing to worry about during this upgrade.\n   - Install `webextension-polyfill` and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill):\n\n     ```sh\n     pnpm i webextension-polyfill @wxt-dev/webextension-polyfill\n     ```\n\n   - Add the WXT module to your config:\n\n     ```ts [wxt.config.ts]\n     export default defineConfig({\n       modules: ['@wxt-dev/webextension-polyfill'],\n     });\n     ```\n\nThe new `browser` object (and types) is backed by WXT's new package: [`@wxt-dev/browser`](https://www.npmjs.com/package/@wxt-dev/browser). This package continues WXT's mission of providing useful packages for the whole community. Just like [`@wxt-dev/storage`](https://www.npmjs.com/package/@wxt-dev/storage), [`@wxt-dev/i18n`](https://www.npmjs.com/package/@wxt-dev/i18n), [`@wxt-dev/analytics`](https://www.npmjs.com/package/@wxt-dev/analytics), it is designed to be easy to use in any web extension project, not just those using WXT, and provides a consistent API across all browsers and manifest versions.\n\n### `extensionApi` Config Removed\n\nThe `extensionApi` config has been removed. Before, this config provided a way to opt into using the new `browser` object prior to v0.20.0.\n\nRemove it from your `wxt.config.ts` file if present:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  extensionApi: 'chrome', // [!code --]\n});\n```\n\n### Extension API Type Changes\n\nWith the new `browser` introduced in v0.20, how you access types has changed. WXT now provides types based on `@types/chrome` instead of `@types/webextension-polyfill`.\n\nThese types are more up-to-date with MV3 APIs, contain less bugs, are better organized, and don't have any auto-generated names.\n\nTo access types, use the new `Browser` namespace from `wxt/browser`:\n\n<!-- prettier-ignore -->\n```ts\nimport type { Runtime } from 'wxt/browser'; // [!code --]\nimport type { Browser } from 'wxt/browser'; // [!code ++]\n\nfunction getMessageSenderUrl(sender: Runtime.MessageSender): string { // [!code --]\nfunction getMessageSenderUrl(sender: Browser.runtime.MessageSender): string { // [!code ++]\n  // ...\n}\n```\n\n> If you use auto-imports, `Browser` will be available without manually importing it.\n\nNot all type names will be the same as what `@types/webextension-polyfill` provides. You'll have to find the new type names by looking at the types of the `browser.*` API's you use.\n\n### `public/` and `modules/` Directories Moved\n\nThe default location for the `public/` and `modules/` directories have changed to better align with standards set by other frameworks (Nuxt, Next, Astro, etc). Now, each path is relative to the project's **root directory**, not the src directory.\n\n- If you follow the default folder structure, you don't need to make any changes.\n- If you set a custom `srcDir`, you have two options:\n  1. Move the your `public/` and `modules/` directories to the project root:\n     <!-- prettier-ignore -->\n     ```html\n      📂 {rootDir}/\n         📁 modules/ <!-- [!code ++] -->\n         📁 public/ <!-- [!code ++] -->\n         📂 src/\n            📁 components/\n            📁 entrypoints/\n            📁 modules/ <!-- [!code --] -->\n            📁 public/ <!-- [!code --] -->\n            📁 utils/\n            📄 app.config.ts\n         📄 wxt.config.ts\n      ```\n\n  2. Keep the folders in the same place and update your project config:\n\n     ```ts [wxt.config.ts]\n     export default defineConfig({\n       srcDir: 'src',\n       publicDir: 'src/public', // [!code ++]\n       modulesDir: 'src/modules', // [!code ++]\n     });\n     ```\n\n### Import Path Changes and `#imports`\n\nThe APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to individual exports under the `wxt/utils/*` path.\n\n:::details Why?\nAs WXT grows and more utilities are added, any helper with side-effects will not be tree-shaken out of your final bundle.\n\nThis can cause problems because not every API used by these side-effects is available in every type of entrypoint. Some APIs can only be used in the background, sandboxed pages can't use any extension API, etc. This was leading to JS throwing errors in the top-level scope, preventing your code from running.\n\nSplitting each util into it's own module solves this problem, making sure you're only importing APIs and side-effects into entrypoints they can run in.\n:::\n\nRefer to the updated [API Reference](/api/reference/) to see the list of new import paths.\n\nHowever, you don't need to memorize or learn the new import paths! v0.20 introduces a new virtual module, `#imports`, that abstracts all this away from developers. See the [blog post](/blog/2024-12-06-using-imports-module) for more details about how this module works.\n\nSo to upgrade, just replace any imports from `wxt/storage`, `wxt/client`, and `wxt/sandbox` with an import to the new `#imports` module:\n\n```ts\nimport { storage } from 'wxt/storage'; // [!code --]\nimport { defineContentScript } from 'wxt/sandbox'; // [!code --]\nimport { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]\nimport { storage } from '#imports'; // [!code ++]\nimport { defineContentScript } from '#imports'; // [!code ++]\nimport { ContentScriptContext, getAppConfig } from '#imports'; // [!code ++]\n```\n\nYou can combine the imports into a single import statement, but it's easier to just find/replace each statement.\n\n```ts\nimport { storage } from 'wxt/storage'; // [!code --]\nimport { defineContentScript } from 'wxt/sandbox'; // [!code --]\nimport { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]\nimport {\n  // [!code ++]\n  storage, // [!code ++]\n  defineContentScript, // [!code ++]\n  ContentScriptContext, // [!code ++]\n  getAppConfig, // [!code ++]\n} from '#imports'; // [!code ++]\n```\n\n:::tip\nBefore types will work, you'll need to run `wxt prepare` after installing v0.20 to generate the new TypeScript declarations.\n:::\n\n### `createShadowRootUi` CSS Changes\n\nWXT now resets styles inherited from the webpage (`visibility`, `color`, `font-size`, etc.) by setting `all: initial` inside the shadow root.\n\n:::warning\nThis doesn't effect `rem` units. You should continue using `postcss-rem-to-px` or an equivalent library if the webpage sets the HTML element's `font-size`.\n:::\n\nIf you use `createShadowRootUi`:\n\n1. Remove any manual CSS overrides that reset the style of specific websites. For example:\n\n   <!-- prettier-ignore -->\n   ```css [entrypoints/reddit.content/style.css]\n   body { /* [!code --] */\n     /* Override Reddit's default \"hidden\" visibility on elements */ /* [!code --] */\n     visibility: visible !important; /* [!code --] */\n   } /* [!code --] */\n   ```\n\n2. Double check that your UI looks the same as before.\n\nIf you run into problems with the new behavior, you can disable it and continue using your current CSS:\n\n```ts\nconst ui = await createShadowRootUi({\n  inheritStyles: true, // [!code ++]\n  // ...\n});\n```\n\n### Default Output Directories Changed\n\nThe default value for the [`outDirTemplate`](/api/reference/wxt/interfaces/InlineConfig#outdirtemplate) config has changed. Now, different build modes are output to different directories:\n\n- `--mode production` &rarr; `.output/chrome-mv3`: Production builds are unchanged\n- `--mode development` &rarr; `.output/chrome-mv3-dev`: Dev mode now has a `-dev` suffix so it doesn't overwrite production builds\n- `--mode custom` &rarr; `.output/chrome-mv3-custom`: Other custom modes end with a `-[mode]` suffix\n\nTo use the old behavior, writing all output to the same directory, set the `outDirTemplate` option:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  outDirTemplate: '{{browser}}-mv{{manifestVersion}}', // [!code ++]\n});\n```\n\n:::warning\nIf you've previously loaded the extension into your browser manually for development, you'll need to uninstall and re-install it from the new dev output directory.\n:::\n\n### Deprecated APIs Removed\n\n- `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process.\n  <!-- markdownlint-disable-next-line MD051 -->\n  > This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps.\n- `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead:\n  <!-- prettier-ignore -->\n  ```ts [wxt.config.ts]\n  export default defineConfig({\n    transformManifest(manifest) { // [!code --]\n    hooks: { // [!code ++]\n      'build:manifestGenerated': (_, manifest) => { // [!code ++]\n         // ...\n      }, // [!code ++]\n    },\n  });\n  ```\n\n### New Deprecations\n\n#### `runner` APIs Renamed\n\nTo improve consistency with the `web-ext.config.ts` filename, the \"runner\" API and config options have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version:\n\n1. The `runner` option has been renamed to `webExt`:\n\n   ```ts [wxt.config.ts]\n   export default defineConfig({\n     runner: { // [!code --]\n     webExt: { // [!code ++]\n       startUrls: [\"https://wxt.dev\"],\n     },\n   });\n   ```\n\n2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`:\n\n   ```ts [web-ext.config.ts]\n   import { defineRunnerConfig } from 'wxt'; // [!code --]\n   import { defineWebExtConfig } from 'wxt'; // [!code ++]\n   ```\n\n3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig`\n\n   ```ts\n   import type { ExtensionRunnerConfig } from 'wxt'; // [!code --]\n   import type { WebExtConfig } from 'wxt'; // [!code ++]\n   ```\n\n## v0.18.5 &rarr; v0.19.0\n\n### `vite-node` Entrypoint Loader\n\nThe default entrypoint loader has changed to `vite-node`. If you use any NPM packages that depend on the `webextension-polyfill`, you need to add them to Vite's `ssr.noExternal` option:\n\n<!-- prettier-ignore -->\n```ts [wxt.config.ts]\nexport default defineConfig({\n  vite: () => ({ // [!code ++]\n    ssr: { // [!code ++]\n      noExternal: ['@webext-core/messaging', '@webext-core/proxy-service'], // [!code ++]\n    }, // [!code ++]\n  }), // [!code ++]\n});\n```\n\n> [Read the full docs](/guide/essentials/config/entrypoint-loaders#vite-node) for more information.\n\n:::details This change enables:\n\nImporting variables and using them in the entrypoint options:\n\n```ts [entrypoints/content.ts]\nimport { GOOGLE_MATCHES } from '~/utils/constants'\n\nexport default defineContentScript({\n  matches: [GOOGLE_MATCHES],\n  main: () => ...,\n})\n```\n\nUsing Vite-specific APIs like `import.meta.glob` to define entrypoint options:\n\n```ts [entrypoints/content.ts]\nconst providers: Record<string, any> = import.meta.glob('../providers/*', {\n  eager: true,\n});\n\nexport default defineContentScript({\n  matches: Object.values(providers).flatMap(\n    (provider) => provider.default.paths,\n  ),\n  async main() {\n    console.log('Hello content.');\n  },\n});\n```\n\nBasically, you can now import and do things outside the `main` function of the entrypoint - you could not do that before. Still though, be careful. It is recommended to avoid running code outside the `main` function to keep your builds fast.\n\n:::\n\nTo continue using the old approach, add the following to your `wxt.config.ts` file:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  entrypointLoader: 'jiti', // [!code ++]\n});\n```\n\n:::warning\n`entrypointLoader: \"jiti\"` is deprecated and will be removed in the next major version.\n:::\n\n### Drop CJS Support\n\nWXT no longer ships with Common JS support. If you're using CJS, here's your migration steps:\n\n1. Add [`\"type\": \"module\"`](https://nodejs.org/api/packages.html#type) to your `package.json`.\n2. Change the file extension of any `.js` files that use CJS syntax to `.cjs`, or update them to use EMS syntax.\n\nVite also provides steps for migrating to ESM. Check them out for more details: <https://vitejs.dev/guide/migration#deprecate-cjs-node-api>\n\n## v0.18.0 &rarr; v0.18.5\n\n> When this version was released, it was not considered a breaking change... but it should have been.\n\n### New `modules/` Directory\n\nWXT now recognizes the `modules/` directory as a folder containing [WXT modules](/guide/essentials/wxt-modules).\n\nIf you already have `<srcDir>/modules` or `<srcDir>/Modules` directory, `wxt prepare` and other commands will fail.\n\nYou have two options:\n\n1. [Recommended] Keep your files where they are and tell WXT to look in a different folder:\n\n   ```ts [wxt.config.ts]\n   export default defineConfig({\n     modulesDir: 'wxt-modules', // defaults to \"modules\"\n   });\n   ```\n\n2. Rename your `modules` directory to something else.\n\n## v0.17.0 &rarr; v0.18.0\n\n### Automatic MV3 `host_permissions` to MV2 `permissions`\n\n> Out of an abundance of caution, this change has been marked as a breaking change because permission generation is different.\n\nIf you list `host_permissions` in your `wxt.config.ts`'s manifest and have released your extension, double check that your `permissions` and `host_permissions` have not changed for all browsers you target in your `.output/*/manifest.json` files. Permission changes can cause the extension to be disabled on update, and can cause a drop in users, so be sure to double check for differences compared to the previous manifest version.\n\n## v0.16.0 &rarr; v0.17.0\n\n### Storage - `defineItem` Requires `defaultValue` Option\n\nIf you were using `defineItem` with versioning and no default value, you will need to add `defaultValue: null` to the options and update the first type parameter:\n\n```ts\nconst item = storage.defineItem<number>(\"local:count\", { // [!code --]\nconst item = storage.defineItem<number | null>(\"local:count\", { // [!code ++]\ndefaultValue: null, // [!code ++]\n  version: ...,\n  migrations: ...,\n})\n```\n\nThe `defaultValue` property is now required if passing in the second options argument.\n\nIf you exclude the second options argument, it will default to being nullable, as before.\n\n```ts\nconst item: WxtStorageItem<number | null> =\n  storage.defineItem<number>('local:count');\nconst value: number | null = await item.getValue();\n```\n\n### Storage - Fix Types In `watch` Callback\n\n> If you don't use TypeScript, this isn't a breaking change, this is just a type change.\n\n```ts\nconst item = storage.defineItem<number>('local:count', { defaultValue: 0 });\nitem.watch((newValue: number | null, oldValue: number | null) => { // [!code --]\nitem.watch((newValue: number, oldValue: number) => { // [!code ++]\n  // ...\n});\n```\n\n## v0.15.0 &rarr; v0.16.0\n\n### Output Directory Structure Changed\n\nJS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes.\n\n```plaintext\n.output/\n  <target>/\n    chunks/\n      some-shared-chunk-<hash>.js\n      popup-<hash>.js // [!code --]\n    popup.html\n    popup.html\n    popup.js // [!code ++]\n```\n\n## v0.14.0 &rarr; v0.15.0\n\n### Renamed `zip.ignoredSources` to `zip.excludeSources`\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  zip: {\n    ignoredSources: [\n      /*...*/\n    ], // [!code --]\n    excludeSources: [\n      /*...*/\n    ], // [!code ++]\n  },\n});\n```\n\n### Renamed Undocumented Constants\n\nRenamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: <https://wxt.dev/guide/multiple-browsers.html#runtime>\n\n- `__BROWSER__` → `import.meta.env.BROWSER`\n- `__COMMAND__` → `import.meta.env.COMMAND`\n- `__MANIFEST_VERSION__` → `import.meta.env.MANIFEST_VERSION`\n- `__IS_CHROME__` → `import.meta.env.CHROME`\n- `__IS_FIREFOX__` → `import.meta.env.FIREFOX`\n- `__IS_SAFARI__` → `import.meta.env.SAFARI`\n- `__IS_EDGE__` → `import.meta.env.EDGE`\n- `__IS_OPERA__` → `import.meta.env.OPERA`\n\n## v0.13.0 &rarr; v0.14.0\n\n### Content Script UI API changes\n\n`createContentScriptUi` and `createContentScriptIframe`, and some of their options, have been renamed:\n\n- `createContentScriptUi({ ... })` &rarr; `createShadowRootUi({ ... })`\n- `createContentScriptIframe({ ... })` &rarr; `createIframeUi({ ... })`\n- `type: \"inline\" | \"overlay\" | \"modal\"` has been changed to `position: \"inline\" | \"overlay\" | \"modal\"`\n- `onRemove` is now called **_before_** the UI is removed from the DOM, previously it was called after the UI was removed\n- `mount` option has been renamed to `onMount`, to better match the related option, `onRemove`.\n\n## v0.12.0 &rarr; v0.13.0\n\n### New `wxt/storage` APIs\n\n`wxt/storage` no longer relies on [`unstorage`](https://www.npmjs.com/package/unstorage). Some `unstorage` APIs, like `prefixStorage`, have been removed, while others, like `snapshot`, are methods on the new `storage` object. Most of the standard usage remains the same. See <https://wxt.dev/guide/storage> and <https://wxt.dev/api/reference/wxt/storage/> for more details ([#300](https://github.com/wxt-dev/wxt/pull/300))\n\n## v0.11.0 &rarr; v0.12.0\n\n### API Exports Changed\n\n`defineContentScript` and `defineBackground` are now exported from `wxt/sandbox` instead of `wxt/client`. ([#284](https://github.com/wxt-dev/wxt/pull/284))\n\n- If you use auto-imports, no changes are required.\n- If you have disabled auto-imports, you'll need to manually update your import statements:\n\n  ```ts\n  import { defineBackground, defineContentScript } from 'wxt/client'; // [!code --]\n  import { defineBackground, defineContentScript } from 'wxt/sandbox'; // [!code ++]\n  ```\n\n## v0.10.0 &rarr; v0.11.0\n\n### Vite 5\n\nYou will need to update any other Vite plugins to a version that supports Vite 5.\n\n## v0.9.0 &rarr; v0.10.0\n\n### Extension Icon Discovery\n\nWXT no longer discovers icons other than `.png` files. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file.\n\n## v0.8.0 &rarr; v0.9.0\n\n### Removed `WebWorker` Types by Default\n\nRemoved [`\"WebWorker\"` types](https://www.typescriptlang.org/tsconfig/lib.html) from `.wxt/tsconfig.json`. These types are useful for MV3 projects using a service worker.\n\nTo add them back to your project, add the following to your project's TSConfig:\n\n```json\n{\n  \"extends\": \"./.wxt/tsconfig.json\",\n  \"compilerOptions\": {\n    // [!code ++]\n    \"lib\": [\"ESNext\", \"DOM\", \"WebWorker\"] // [!code ++]\n  } // [!code ++]\n}\n```\n\n## v0.7.0 &rarr; v0.8.0\n\n### `defineUnlistedScript`\n\nUnlisted scripts must now `export default defineUnlistedScript(...)`.\n\n### `BackgroundDefinition` Type\n\nRename `BackgroundScriptDefintition` to `BackgroundDefinition`.\n\n## v0.6.0 &rarr; v0.7.0\n\n### Content Script CSS Output Location Changed\n\nContent script CSS used to be output to `assets/<name>.css`, but is now `content-scripts/<name>.css` to match the docs.\n\n## v0.5.0 &rarr; v0.6.0\n\n### Require a Function for `vite` Config\n\nThe `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`.\n\n## v0.4.0 &rarr; v0.5.0\n\n### Revert Move Public Directory\n\nChange default `publicDir` to from `<rootDir>/public` to `<srcDir>/public`.\n\n## v0.3.0 &rarr; v0.4.0\n\n### Update Default Path Aliases\n\nUse relative path aliases inside `.wxt/tsconfig.json`.\n\n## v0.2.0 &rarr; v0.3.0\n\n### Move Public Directory\n\nChange default `publicDir` to from `<srcDir>/public` to `<rootDir>/public`.\n\n### Improve Type Safety\n\nAdd type safety to `browser.runtime.getURL`.\n\n## v0.1.0 &rarr; v0.2.0\n\n### Rename `defineBackground`\n\nRename `defineBackgroundScript` to `defineBackground`.\n"
  },
  {
    "path": "docs/i18n.md",
    "content": "---\noutline: deep\n---\n\n<!--@include: ../packages/i18n/README.md-->\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\ntitle: Next-gen Web Extension Framework\n\nhero:\n  name: WXT\n  text: Next-gen Web Extension Framework\n  tagline: An open source tool that makes web extension development faster than ever before.\n  image:\n    src: /hero-logo.svg\n    alt: WXT\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /guide/installation\n    - theme: alt\n      text: Learn More\n      link: /guide/introduction\n\nfeatures:\n  - icon: 🌐\n    title: Supported Browsers\n    details: WXT will build extensions for Chrome, Firefox, Edge, Safari, and any Chromium based browser.\n    link: /guide/essentials/target-different-browsers\n    linkText: Read docs\n  - icon: ✅\n    title: MV2 and MV3\n    details: Build Manifest V2 or V3 extensions for any browser using the same codebase.\n    link: /guide/essentials/config/manifest\n    linkText: Read docs\n  - icon: ⚡\n    title: Fast Dev Mode\n    details: Lightning fast HMR for UI development and fast reloads for content/background scripts enables faster iterations.\n  - icon: 📂\n    title: File Based Entrypoints\n    details: Manifest is generated based on files in the project with inline configuration.\n    link: /guide/essentials/project-structure\n    linkText: See project structure\n  - icon: 🚔\n    title: TypeScript\n    details: Create large projects with confidence using TS by default.\n  - icon: 🦾\n    title: Auto-imports\n    details: Nuxt-like auto-imports to speed up development.\n    link: /guide/essentials/config/auto-imports\n    linkText: Read docs\n  - icon: 🤖\n    title: Automated Publishing\n    details: Automatically zip, upload, submit, and publish extensions.\n  - icon: 🎨\n    title: Frontend Framework Agnostic\n    details: Works with any front-end framework with a Vite plugin.\n    link: /guide/essentials/frontend-frameworks\n    linkText: Add a framework\n  - icon: 📦\n    title: Module System\n    details: Reuse build-time and runtime-code across multiple extensions.\n    link: /guide/essentials/wxt-modules\n    linkText: Read docs\n  - icon: 🖍️\n    title: Bootstrap a New Project\n    details: Get started quickly with several awesome project templates.\n    link: /guide/installation#bootstrap-project\n    linkText: See templates\n  - icon: 📏\n    title: Bundle Analysis\n    details: Tools for analyzing the final extension bundle and minimizing your extension's size.\n  - icon: ⬇️\n    title: Bundle Remote Code\n    details: Downloads and bundles remote code imported from URLs.\n    link: /guide/essentials/remote-code\n    linkText: Read docs\n---\n\n## Sponsors\n\nWXT 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).\n\n<a href=\"https://github.com/sponsors/wxt-dev\"><img alt=\"WXT Sponsors\" src=\"https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors-wide.svg\"></a>\n\n## Put <span style=\"color: var(--vp-c-brand-1)\">Developer Experience</span> First\n\nWXT simplifies the web extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.\n\n<div style=\"margin: auto; width: 100%; max-width: 900px; text-align: center\">\n  <video src=\"https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94\" controls></video>\n  <br />\n  <small>\n    And who doesn't appreciate a beautiful CLI?\n  </small>\n</div>\n\n## Who's Using WXT?\n\nBattle tested and ready for production. Explore web extensions made with WXT.\n\n<ClientOnly>\n  <UsingWxtSection />\n</ClientOnly>\n"
  },
  {
    "path": "docs/is-background.md",
    "content": "<!--@include: ../packages/is-background/README.md-->\n"
  },
  {
    "path": "docs/public/_redirects",
    "content": "# Netlify Redirects File\n# https://docs.netlify.com/routing/redirects/\n\n# Old URLs -> New URLs\n\n# OLD\n/config.html                    /api/reference/wxt/interfaces/InlineConfig.html\n/api/config.html                /api/reference/wxt/interfaces/InlineConfig.html\n/api/config                     /api/reference/wxt/interfaces/InlineConfig.html\n/entrypoints                    /entrypoints/background.html\n/get-started/assets.html        /guide/assets.html\n/get-started/build-targets.html /guide/multiple-browsers.html\n/get-started/compare.html       /guide/compare.html\n/get-started/configuration.html /guide/configuration.html\n/get-started/entrypoints.html   /guide/entrypoints.html\n/get-started/publishing.html    /guide/publishing.html\n/get-started/testing.html       /guide/testing.html\n/guide/background.html          /entrypoints/background.html\n/guide/bookmarks.html           /entrypoints/bookmarks.html\n/guide/content-scripts.html     /entrypoints/content-scripts.html\n/guide/css.html                 /entrypoints/css.html\n/guide/devtools.html            /entrypoints/devtools.html\n/guide/history.html             /entrypoints/history.html\n/guide/manifest.html            /entrypoints/manifest.html\n/guide/newtab.html              /entrypoints/newtab.html\n/guide/options.html             /entrypoints/options.html\n/guide/popup.html               /entrypoints/popup.html\n/guide/sandbox.html             /entrypoints/sandbox.html\n/guide/sidepanel.html           /entrypoints/sidepanel.html\n/guide/unlisted-pages.html      /entrypoints/unlisted-pages.html\n/guide/unlisted-scripts.html    /entrypoints/unlisted-scripts.html\n/guide/build-targets.html       /guide/multiple-browsers.html\n/guide/installation.html        /get-started/installation.html\n/guide/introduction.html        /get-started/introduction.html\n/guide/upgrade-guide/wxt        /guide/resources/upgrading.html\n/guide/upgrade-guide/wxt.html   /guide/resources/upgrading.html\n\n# 0.19.0\n/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html\n\n# https://github.com/wxt-dev/wxt/issues/704\n# Generated via `pnpm docs:build && find docs/.vitepress/dist -type f -name \"*.html\"`\n\n/guide/i18n/build-integrations.html                           /i18n.html#build-integrations\n/guide/i18n/introduction.html                                 /i18n.html\n/guide/i18n/messages-file-format.html                         /i18n.html#messages-file-format\n/guide/i18n/editor-support.html                               /i18n.html#editor-support\n/guide/i18n/installation.html                                 /i18n.html#installation\n/guide/i18n/installation                                      /i18n.html#installation\n/guide/key-concepts/content-script-ui.html                    /guide/essentials/content-scripts.html#ui\n/guide/key-concepts/manifest.html                             /guide/essentials/config/manifest.html\n/guide/key-concepts/wxt-submit.html                           /api/cli/wxt-submit.html\n/guide/key-concepts/auto-imports.html                         /guide/essentials/config/auto-imports.html\n/guide/key-concepts/web-extension-polyfill.html               /guide/essentials/extension-apis.html\n/guide/key-concepts/frontend-frameworks.html                  /guide/essentials/frontend-frameworks.html\n/guide/key-concepts/multiple-browsers.html                    /guide/essentials/target-different-browsers.html\n/guide/go-further/entrypoint-loaders.html                     /guide/essentials/config/entrypoint-loaders.html\n/guide/go-further/es-modules.html                             /guide/essentials/es-modules.html\n/guide/go-further/handling-updates.html                       /guide/essentials/testing-updates.html\n/guide/go-further/custom-events.html                          /guide/essentials/content-scripts.html#dealing-with-spas\n/guide/go-further/debugging.html                              /TODO\n/guide/go-further/remote-code.html                            /guide/essentials/remote-code.html\n/guide/go-further/vite.html                                   /guide/essentials/config/vite.html\n/guide/go-further/testing.html                                /guide/essentials/unit-testing.html\n/guide/go-further/how-wxt-works.html                          /guide/resources/how-wxt-works.html\n/guide/go-further/reusable-modules.html                       /guide/essentials/wxt-modules.html\n/guide/extension-apis/messaging.html                          /guide/essentials/messaging.html\n/guide/extension-apis/i18n.html                               /guide/essentials/i18n.html\n/guide/extension-apis/storage.html                            /guide/essentials/storage.html\n/guide/extension-apis/scripting.html                          /guide/essentials/scripting.html\n/guide/extension-apis/others.html                             /guide/essentials/extension-apis.html\n/guide/upgrade-guide/wxt.html                                 /guide/resources/upgrading.html\n/guide/directory-structure/components.html                    /guide/essentials/project-structure.html\n/guide/directory-structure/hooks.html                         /guide/essentials/config/hooks.html\n/guide/directory-structure/assets.html                        /guide/essentials/assets.html#assets-directory\n/guide/directory-structure/package.html                       /guide/essentials/project-structure.html\n/guide/directory-structure/env.html                           /guide/essentials/config/runtime.html\n/guide/directory-structure/wxt-config.html                    /guide/essentials/project-structure.html\n/guide/directory-structure/wxt.html                           /guide/essentials/project-structure.html\n/guide/directory-structure/public/locales.html                /guide/essentials/project-structure.html\n/guide/directory-structure/public/index.html                  /guide/essentials/assets.html#public-directory\n/guide/directory-structure/entrypoints/devtools.html          /guide/essentials/entrypoints.html#devtools\n/guide/directory-structure/entrypoints/sidepanel.html         /guide/essentials/entrypoints.html#sidepanel\n/guide/directory-structure/entrypoints/content-scripts.html   /guide/essentials/entrypoints.html#content-scripts\n/guide/directory-structure/entrypoints/newtab.html            /guide/essentials/entrypoints.html#newtab\n/guide/directory-structure/entrypoints/bookmarks.html         /guide/essentials/entrypoints.html#bookmarks\n/guide/directory-structure/entrypoints/unlisted-pages.html    /guide/essentials/entrypoints.html#unlisted-pages\n/guide/directory-structure/entrypoints/unlisted-scripts.html  /guide/essentials/entrypoints.html#unlisted-scripts\n/guide/directory-structure/entrypoints/options.html           /guide/essentials/entrypoints.html#options\n/guide/directory-structure/entrypoints/background.html        /guide/essentials/entrypoints.html#background\n/guide/directory-structure/entrypoints/popup.html             /guide/essentials/entrypoints.html#popup\n/guide/directory-structure/entrypoints/history.html           /guide/essentials/entrypoints.html#history\n/guide/directory-structure/entrypoints/sandbox.html           /guide/essentials/entrypoints.html#sandbox\n/guide/directory-structure/entrypoints/css.html               /guide/essentials/entrypoints.html#unlisted-css\n/guide/directory-structure/web-ext-config.html                /guide/essentials/config/browser-startup.html\n/guide/directory-structure/tsconfig.html                      /guide/essentials/config/typescript.html\n/guide/directory-structure/utils.html                         /guide/essentials/project-structure.html\n/guide/directory-structure/app-config.html                    /guide/essentials/config/runtime.html\n/guide/directory-structure/composables.html                   /guide/essentials/project-structure.html\n/guide/directory-structure/output.html                        /guide/essentials/project-structure.html\n/get-started/assets.html                                      /guide/essentials/assets.html\n/get-started/introduction.html                                /guide/introduction.html\n/get-started/configuration.html                               /guide/essentials/config/manifest.html\n/get-started/publishing.html                                  /guide/essentials/publishing.html\n/get-started/migrate-to-wxt.html                              /guide/resources/migrate.html\n/get-started/compare.html                                     /guide/resources/compare.html\n/get-started/entrypoints.html                                 /guide/essentials/entrypoints.html\n/get-started/installation.html                                /guide/installation.html\n"
  },
  {
    "path": "docs/public/robots.txt",
    "content": "User-agent: *\nDisallow: /api.html\nDisallow: /config.html\n\nSitemap: https://wxt.dev/sitemap.xml\n"
  },
  {
    "path": "docs/runner.md",
    "content": "<!--@include: ../packages/runner/README.md-->\n"
  },
  {
    "path": "docs/storage.md",
    "content": "---\noutline: deep\n---\n\n# WXT Storage\n\n[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) &bull; [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage)\n\nA simplified wrapper around the extension storage APIs.\n\n## Installation\n\n### With WXT\n\nThis module is built-in to WXT, so you don't need to install anything.\n\n```ts\nimport { storage } from '#imports';\n```\n\nIf you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!\n\n### Without WXT\n\nInstall the NPM package:\n\n```sh\nnpm i @wxt-dev/storage\npnpm add @wxt-dev/storage\nyarn add @wxt-dev/storage\nbun add @wxt-dev/storage\n```\n\n```ts\nimport { storage } from '@wxt-dev/storage';\n```\n\n## Storage Permission\n\nTo use the `@wxt-dev/storage` API, the `\"storage\"` permission must be added to the manifest:\n\n```ts [wxt.config.ts]\nexport default defineConfig({\n  manifest: {\n    permissions: ['storage'],\n  },\n});\n```\n\n## Basic Usage\n\nAll storage keys must be prefixed by their storage area.\n\n```ts\n// ❌ This will throw an error\nawait storage.getItem('installDate');\n\n// ✅ This is good\nawait storage.getItem('local:installDate');\n```\n\nYou can use `local:`, `session:`, `sync:`, or `managed:`.\n\nIf you use TypeScript, you can add a type parameter to most methods to specify the expected type of the key's value:\n\n```ts\nawait storage.getItem<number>('local:installDate');\nawait storage.watch<number>(\n  'local:installDate',\n  (newInstallDate, oldInstallDate) => {\n    // ...\n  },\n);\nawait storage.getMeta<{ v: number }>('local:installDate');\n```\n\n> This approach is fine for one-off storage fields or generic helpers, but [defining storage items](#defining-storage-items) is the recommended way to add type-safety.\n\n## Watchers\n\nTo listen for storage changes, use the `storage.watch` function. It lets you set up a listener for a single key:\n\n```ts\nconst unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {\n  console.log('Count changed:', { newCount, oldCount });\n});\n```\n\nTo remove the listener, call the returned `unwatch` function:\n\n```ts\nconst unwatch = storage.watch(...);\n\n// Some time later...\nunwatch();\n```\n\n## Metadata\n\n`@wxt-dev/storage` also supports setting metadata for keys, stored at `key + \"$\"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.\n\n[Other than versioning](#versioning), you are responsible for managing a field's metadata:\n\n```ts\nawait Promise.all([\n  storage.setItem('local:preference', true),\n  storage.setMeta('local:preference', { lastModified: Date.now() }),\n]);\n```\n\nWhen setting different properties of metadata from multiple calls, the properties are combined instead of overwritten:\n\n```ts\nawait storage.setMeta('local:preference', { lastModified: Date.now() });\nawait storage.setMeta('local:preference', { v: 2 });\n\nawait storage.getMeta('local:preference'); // { v: 2, lastModified: 1703690746007 }\n```\n\nYou can remove all metadata associated with a key, or just specific properties:\n\n```ts\n// Remove all properties\nawait storage.removeMeta('local:preference');\n\n// Remove only the \"lastModified\" property\nawait storage.removeMeta('local:preference', 'lastModified');\n\n// Remove multiple properties\nawait storage.removeMeta('local:preference', ['lastModified', 'v']);\n```\n\n## Defining Storage Items\n\nWriting the key and type parameter for the same key over and over again can be annoying. As an alternative, you can use `storage.defineItem` to create a \"storage item\".\n\nStorage items contain the same APIs as the `storage` variable, but you can configure its type, default value, and more in a single place:\n\n```ts\n// utils/storage.ts\nconst showChangelogOnUpdate = storage.defineItem<boolean>(\n  'local:showChangelogOnUpdate',\n  {\n    fallback: true,\n  },\n);\n```\n\nNow, instead of using the `storage` variable, you can use the storage item instead:\n\n```ts\nawait showChangelogOnUpdate.getValue();\nawait showChangelogOnUpdate.setValue(false);\nawait showChangelogOnUpdate.removeValue();\nconst unwatch = showChangelogOnUpdate.watch((newValue) => {\n  // ...\n});\n```\n\n### Versioning\n\nYou can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1.\n\nFor example, consider a storage item that stores a list of websites that are ignored by an extension.\n\n:::code-group\n\n```ts [v1]\ntype IgnoredWebsiteV1 = string;\n\nexport const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>(\n  'local:ignoredWebsites',\n  {\n    fallback: [],\n    version: 1,\n  },\n);\n```\n\n<!-- prettier-ignore -->\n```ts [v2]\nimport { nanoid } from 'nanoid'; // [!code ++]\n\ntype IgnoredWebsiteV1 = string;\ninterface IgnoredWebsiteV2 { // [!code ++]\n  id: string; // [!code ++]\n  website: string; // [!code ++]\n} // [!code ++]\n\nexport const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>( // [!code --]\nexport const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]\n  'local:ignoredWebsites',\n  {\n    fallback: [],\n    version: 1, // [!code --]\n    version: 2, // [!code ++]\n    migrations: { // [!code ++]\n      // Ran when migrating from v1 to v2 // [!code ++]\n      2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { // [!code ++]\n        return websites.map((website) => ({ id: nanoid(), website })); // [!code ++]\n      }, // [!code ++]\n    }, // [!code ++]\n  },\n);\n```\n\n<!-- prettier-ignore -->\n```ts [v3]\nimport { nanoid } from 'nanoid';\n\ntype IgnoredWebsiteV1 = string;\ninterface IgnoredWebsiteV2 {\n  id: string;\n  website: string;\n}\ninterface IgnoredWebsiteV3 { // [!code ++]\n  id: string; // [!code ++]\n  website: string; // [!code ++]\n  enabled: boolean; // [!code ++]\n} // [!code ++]\n\nexport const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code --]\nexport const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code ++]\n  'local:ignoredWebsites',\n  {\n    fallback: [],\n    version: 2, // [!code --]\n    version: 3, // [!code ++]\n    migrations: {\n      // Ran when migrating from v1 to v2\n      2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => {\n        return websites.map((website) => ({ id: nanoid(), website }));\n      },\n      // Ran when migrating from v2 to v3 // [!code ++]\n      3: (websites: IgnoredWebsiteV2[]): IgnoredWebsiteV3[] => { // [!code ++]\n        return websites.map((website) => ({ ...website, enabled: true })); // [!code ++]\n      }, // [!code ++]\n    },\n  },\n);\n```\n\n:::\n\n:::info\nInternally, this uses a metadata property called `v` to track the value's current version.\n:::\n\nIn this case, we thought that the ignored website list might change in the future, and were able to set up a versioned storage item from the start.\n\nRealistically, you won't know an item needs versioning until you need to change its schema. Thankfully, it's simple to add versioning to an unversioned storage item.\n\nWhen a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work!\n\nLet's look at the same ignored websites example from before, but start with an unversioned item this time:\n\n:::code-group\n\n```ts [Unversioned]\nexport const ignoredWebsites = storage.defineItem<string[]>(\n  'local:ignoredWebsites',\n  {\n    fallback: [],\n  },\n);\n```\n\n<!-- prettier-ignore -->\n```ts [v2]\nimport { nanoid } from 'nanoid'; // [!code ++]\n\n// Retroactively add a type for the first version // [!code ++]\ntype IgnoredWebsiteV1 = string; // [!code ++]\ninterface IgnoredWebsiteV2 { // [!code ++]\n  id: string; // [!code ++]\n  website: string; // [!code ++]\n} // [!code ++]\n\nexport const ignoredWebsites = storage.defineItem<string[]>( // [!code --]\nexport const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]\n  'local:ignoredWebsites',\n  {\n    fallback: [],\n    version: 2, // [!code ++]\n    migrations: { // [!code ++]\n      // Ran when migrating from v1 to v2 // [!code ++]\n      2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { // [!code ++]\n        return websites.map((website) => ({ id: nanoid(), website })); // [!code ++]\n      }, // [!code ++]\n    }, // [!code ++]\n  },\n);\n```\n\n:::\n\n### Running Migrations\n\nAs soon as `storage.defineItem` is called, WXT checks if migrations need to be run, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc.) will automatically wait for the migration process to finish before actually reading or writing values.\n\n### Default Values\n\nWith `storage.defineItem`, there are multiple ways of defining default values:\n\n1. **`fallback`** - Return this value from `getValue` instead of `null` if the value is missing.\n\n   This option is great for providing default values for settings:\n\n   ```ts\n   const theme = storage.defineItem('local:theme', {\n     fallback: 'dark',\n   });\n   const allowEditing = storage.defineItem('local:allow-editing', {\n     fallback: true,\n   });\n   ```\n\n2. **`init`** - Initialize and save a value in storage if it is not already saved.\n\n   This is great for values that need to be initialized or set once:\n\n   ```ts\n   const userId = storage.defineItem('local:user-id', {\n     init: () => globalThis.crypto.randomUUID(),\n   });\n   const installDate = storage.defineItem('local:install-date', {\n     init: () => new Date().getTime(),\n   });\n   ```\n\n   The value is initialized in storage immediately.\n\n## Bulk Operations\n\nWhen getting or setting multiple values in storage, you can perform bulk operations to improve performance by reducing the number of individual storage calls. The `storage` API provides several methods for performing bulk operations:\n\n- **`getItems`** - Get multiple values at once.\n- **`getMetas`** - Get metadata for multiple items at once.\n- **`setItems`** - Set multiple values at once.\n- **`setMetas`** - Set metadata for multiple items at once.\n- **`removeItems`** - Remove multiple values (and optionally metadata) at once.\n\nAll these APIs support both string keys and defined storage items:\n\n```ts\nconst userId = storage.defineItem('local:userId');\n\nawait storage.setItems([\n  { key: 'local:installDate', value: Date.now() },\n  { item: userId, value: generateUserId() },\n]);\n```\n\nRefer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.\n"
  },
  {
    "path": "docs/tapes/init-demo.tape",
    "content": "# VHS documentation\n#\n# You can view all VHS documentation on the command line with `vhs manual`.\n# Or see https://github.com/charmbracelet/vhs#vhs-command-reference\n\n# Output file\nOutput docs/assets/init-demo.gif\n\n# The tools we will use\nRequire pnpm\n\n\n# === Scene ====\nSet Width 1400\nSet Height 800\n\n# The maximum FPS for GIF is `50` FPS.\nSet Framerate 50\n\n# Terminal theme with WXT brand colors (which was taken from the website)\n# Based on the standard charmbracelet/vhs theme:\n# https://github.com/charmbracelet/vhs/blob/88e634f4a10bbe305b6aea9a12b4d8dc3dd7f31c/style.go#L7-L28\nSet Theme {\"background\": \"#161618\", \"foreground\": \"#dddddd\", \"black\": \"#282a2e\", \"brightBlack\": \"#4d4d4d\", \"red\": \"#D74E6F\", \"brightRed\": \"#FE5F86\", \"green\": \"#67d45e\", \"brightGreen\": \"#67d45e\", \"yellow\": \"#D3E561\", \"brightYellow\": \"#EBFF71\", \"blue\": \"#8056FF\", \"brightBlue\": \"#9B79FF\", \"magenta\": \"#ED61D7\", \"brightMagenta\": \"#FF7AEA\", \"cyan\": \"#04D7D7\", \"brightCyan\": \"#00FEFE\", \"white\": \"#bfbfbf\", \"brightWhite\": \"#e6e6e6\", \"indigo\": \"#5B56E0\"}\nSet FontSize 32\n\n# Terminal settings\nSet Shell \"bash\"\n\n# Terminal prompt. It looks like \"● mycommand ...\"\nEnv PS1 \"\\e[0;32m●\\e[0m \"\n\n\n# ===== Preparation =====\n# Steps to prepare the recording environment, ensuring a clean and isolated setup.\n\nHide\n# Create a temporary folder for demo\nType 'vhs_sandbox=\"$(mktemp -d)\"' Enter\nType 'cd \"$vhs_sandbox\"' Enter\nType 'clear' Enter\nShow\n\n\n# ===== Actions =====\n\nType 'pnpm dlx wxt@latest init .' Sleep 1s Enter\nSleep 3.25s\nDown@750ms Enter@750ms # Select `vue` template\nSleep 1.25s\nDown@750ms Enter@750ms # Select `pnpm` as a package manager\nSleep 5s\n\n\n# ===== Cleaning =====\n\nHide\n# Delete the temporary folder\nType 'rm -rf \"$vhs_sandbox\"' Enter\nShow\n"
  },
  {
    "path": "docs/typedoc.json",
    "content": "{\n  \"$schema\": \"https://typedoc.org/schema.json\",\n  \"entryPointStrategy\": \"packages\",\n  \"entryPoints\": [\"../packages/wxt\"],\n  \"plugin\": [\n    \"typedoc-plugin-markdown\",\n    \"typedoc-vitepress-theme\",\n    \"typedoc-plugin-frontmatter\"\n  ],\n  \"out\": \"./api/reference\",\n  \"githubPages\": false,\n  \"excludePrivate\": true,\n  \"excludeProtected\": true,\n  \"excludeInternal\": true,\n  \"readme\": \"none\",\n  \"frontmatterGlobals\": {\n    \"editLink\": false\n  }\n}\n"
  },
  {
    "path": "docs/unocss.md",
    "content": "<!--@include: ../packages/unocss/README.md-->\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8\",\n  \"scripts\": {\n    \"check\": \"check && pnpm -r --sequential run check\",\n    \"test\": \"pnpm -r --sequential run test run\",\n    \"test:coverage\": \"pnpm -r --sequential run test:coverage\",\n    \"prepare\": \"simple-git-hooks\",\n    \"docs:gen\": \"typedoc --options docs/typedoc.json\",\n    \"docs:dev\": \"pnpm -s docs:gen && vitepress dev docs\",\n    \"docs:build\": \"pnpm -s docs:gen && vitepress build docs\",\n    \"docs:preview\": \"pnpm -s docs:gen && vitepress preview docs\"\n  },\n  \"devDependencies\": {\n    \"@aklinker1/buildc\": \"^1.1.7\",\n    \"@aklinker1/check\": \"^2.2.0\",\n    \"@commitlint/config-conventional\": \"^20.4.3\",\n    \"@commitlint/types\": \"^20.4.3\",\n    \"@types/semver\": \"^7.7.1\",\n    \"@vitest/coverage-v8\": \"^4.0.18\",\n    \"changelogen\": \"^0.6.2\",\n    \"consola\": \"^3.4.2\",\n    \"feed\": \"^5.2.0\",\n    \"markdown-it-footnote\": \"^4.0.0\",\n    \"markdownlint-cli\": \"^0.48.0\",\n    \"nano-spawn\": \"^2.0.0\",\n    \"nano-staged\": \"^0.8.0\",\n    \"p-map\": \"^7.0.4\",\n    \"prettier\": \"^3.8.1\",\n    \"prettier-plugin-jsdoc\": \"^1.8.0\",\n    \"semver\": \"^7.7.4\",\n    \"simple-git-hooks\": \"^2.13.1\",\n    \"tinyglobby\": \"^0.2.15\",\n    \"tsdown\": \"^0.21.0\",\n    \"tsx\": \"4.21.0\",\n    \"typedoc\": \"^0.25.4\",\n    \"typedoc-plugin-frontmatter\": \"^1.3.1\",\n    \"typedoc-plugin-markdown\": \"4.0.0-next.23\",\n    \"typedoc-vitepress-theme\": \"1.0.0-next.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vitepress\": \"^1.6.4\",\n    \"vitepress-knowledge\": \"^0.4.1\",\n    \"vitepress-plugin-group-icons\": \"^1.7.1\",\n    \"vitepress-plugin-llms\": \"^1.11.0\",\n    \"vitest-mock-extended\": \"^3.1.0\",\n    \"vue\": \"^3.5.29\",\n    \"wxt\": \"workspace:*\"\n  },\n  \"simple-git-hooks\": {\n    \"pre-commit\": \"./node_modules/.bin/nano-staged\"\n  },\n  \"nano-staged\": {\n    \"*\": \"prettier --ignore-unknown --write\"\n  }\n}\n"
  },
  {
    "path": "packages/analytics/CHANGELOG.md",
    "content": "# Changelog\n\n## v0.5.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.3...analytics-v0.5.4)\n\n### 🩹 Fixes\n\n- Continue using `useAppConfig` to support older versions of WXT ([bfd94556](https://github.com/wxt-dev/wxt/commit/bfd94556))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.5.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3)\n\n### 🩹 Fixes\n\n- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))\n- Allow `userId` option to return `undefined` ([636cf1f8](https://github.com/wxt-dev/wxt/commit/636cf1f8))\n- Improve background script detection logic for analytics package ([#1808](https://github.com/wxt-dev/wxt/pull/1808))\n\n### 🏡 Chore\n\n- Created new types, instead of `any` for `analytics` ([#2119](https://github.com/wxt-dev/wxt/pull/2119))\n\n### ❤️ Contributors\n\n- Smit ([@sm17p](https://github.com/sm17p))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))\n\n## v0.5.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.1...analytics-v0.5.2)\n\n### 🩹 Fixes\n\n- Normalize path for createAnalytics of analytics/index.ts ([#2013](https://github.com/wxt-dev/wxt/pull/2013))\n- Allow custom API URL in Google Analytics 4 provider options ([#1653](https://github.com/wxt-dev/wxt/pull/1653))\n\n### 💅 Refactors\n\n- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))\n\n### 🏡 Chore\n\n- Fix other type error after `chrome` types update ([31ebf966](https://github.com/wxt-dev/wxt/commit/31ebf966))\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n\n### ❤️ Contributors\n\n- Honwhy Wang <honwhy.wang@gmail.com>\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n\n## v0.5.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.0...analytics-v0.5.1)\n\n### 🚀 Enhancements\n\n- Integrate latest measurement protocol changes ([#1767](https://github.com/wxt-dev/wxt/pull/1767))\n\n### 🩹 Fixes\n\n- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))\n\n### 🏡 Chore\n\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))\n- Fix auto-fixable `markdownlint` errors ([#1710](https://github.com/wxt-dev/wxt/pull/1710))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Tanishq-aggarwal ([@tanishq-aggarwal](https://github.com/tanishq-aggarwal))\n\n## v0.5.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.4.1...analytics-v0.5.0)\n\n### 🩹 Fixes\n\n- ⚠️  Update min WXT version to 0.20 ([2e8baf0](https://github.com/wxt-dev/wxt/commit/2e8baf0))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))"
  },
  {
    "path": "packages/analytics/README.md",
    "content": "# WXT Analytics\n\nReport analytics events from your web extension extension.\n\n## Supported Analytics Providers\n\n- [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol)\n- [Umami](#umami)\n\n## Install With WXT\n\n1. Install the NPM package:\n\n   ```bash\n   pnpm i @wxt-dev/analytics\n   ```\n\n2. In your `wxt.config.ts`, add the WXT module:\n\n   ```ts\n   export default defineConfig({\n     modules: ['@wxt-dev/analytics/module'],\n   });\n   ```\n\n3. In your `<srcDir>/app.config.ts`, add a provider:\n\n   ```ts\n   // <srcDir>/app.config.ts\n   import { umami } from '@wxt-dev/analytics/providers/umami';\n\n   export default defineAppConfig({\n     analytics: {\n       debug: true,\n       providers: [\n         // ...\n       ],\n     },\n   });\n   ```\n\n4. Then use the `#analytics` module to report events:\n\n   ```ts\n   import { analytics } from '#analytics';\n\n   await analytics.track('some-event');\n   await analytics.page();\n   await analytics.identify('some-user-id');\n   analytics.autoTrack(document.body);\n   ```\n\n## Install Without WXT\n\n1. Install the NPM package:\n\n   ```bash\n   pnpm i @wxt-dev/analytics\n   ```\n\n2. Create an `analytics` instance:\n\n   ```ts\n   // utils/analytics.ts\n   import { createAnalytics } from '@wxt-dev/analytics';\n\n   export const analytics = createAnalytics({\n     providers: [\n       // ...\n     ],\n   });\n   ```\n\n3. Import your analytics module in the background to initialize the message listener:\n\n   ```ts\n   // background.ts\n   import './utils/analytics';\n   ```\n\n4. Then use your `analytics` instance to report events:\n\n   ```ts\n   import { analytics } from './utils/analytics';\n\n   await analytics.track('some-event');\n   await analytics.page();\n   await analytics.identify('some-user-id');\n   analytics.autoTrack(document.body);\n   ```\n\n## Providers\n\n### Google Analytics 4 (Measurement Protocol)\n\nThe [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4) is an alternative to GTag for reporting events to Google Analytics for MV3 extensions.\n\n> [Why use the Measurement Protocol instead of GTag?](https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4#measurement-protocol)\n\nFollow [Google's documentation](https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4#setup-credentials) to obtain your credentials and put them in your `.env` file:\n\n```dotenv\nWXT_GA_API_SECRET='...'\n```\n\nThen add the `googleAnalytics4` provider to your `<srcDir>/app.config.ts` file:\n\n```ts\nimport { googleAnalytics4 } from '@wxt-dev/analytics/providers/google-analytics-4';\n\nexport default defineAppConfig({\n  analytics: {\n    providers: [\n      googleAnalytics4({\n        apiSecret: import.meta.env.WXT_GA_API_SECRET,\n        measurementId: '...',\n      }),\n    ],\n  },\n});\n```\n\n### Umami\n\n[Umami](https://umami.is/) is a privacy-first, open source analytics platform.\n\nIn Umami's dashboard, create a new website. The website's name and domain can be anything. Obviously, an extension doesn't have a domain, so make one up if you don't have one.\n\nAfter the website has been created, save the website ID and domain to your `.env` file:\n\n```dotenv\nWXT_UMAMI_WEBSITE_ID='...'\nWXT_UMAMI_DOMAIN='...'\n```\n\nThen add the `umami` provider to your `<srcDir>/app.config.ts` file:\n\n```ts\nimport { umami } from '@wxt-dev/analytics/providers/umami';\n\nexport default defineAppConfig({\n  analytics: {\n    providers: [\n      umami({\n        apiUrl: 'https://<your-umami-instance>/api',\n        websiteId: import.meta.env.WXT_UMAMI_WEBSITE_ID,\n        domain: import.meta.env.WXT_UMAMI_DOMAIN,\n      }),\n    ],\n  },\n});\n```\n\n### Custom Provider\n\nIf your analytics platform is not supported, you can provide an implementation of the `AnalyticsProvider` type in your `app.config.ts` instead:\n\n```ts\nimport { defineAnalyticsProvider } from '@wxt-dev/analytics/client';\n\ninterface CustomAnalyticsOptions {\n  // ...\n}\n\nconst customAnalytics = defineAnalyticsProvider<CustomAnalyticsOptions>(\n  (analytics, analyticsConfig, providerOptions) => {\n    // ...\n  },\n);\n\nexport default defineAppConfig({\n  analytics: {\n    providers: [\n      customAnalytics({\n        // ...\n      }),\n    ],\n  },\n});\n```\n\nExample `AnalyticsProvider` implementations can be found at [`./modules/analytics/providers`](https://github.com/wxt-dev/wxt/tree/main/packages/analytics/modules/analytics/providers).\n\n## User Properties\n\nUser ID and properties are stored in `browser.storage.local`. To change this or customize where these values are stored, use the `userId` and `userProperties` config:\n\n```ts\n// app.config.ts\nimport { storage } from 'wxt/storage';\n\nexport default defineAppConfig({\n  analytics: {\n    userId: storage.defineItem('local:custom-user-id-key'),\n    userProperties: storage.defineItem('local:custom-user-properties-key'),\n  },\n});\n```\n\nTo set the values at runtime, use the `identify` function:\n\n```ts\nawait analytics.identify(userId, userProperties);\n```\n\nAlternatively, a common pattern is to use a random string as the user ID. This keeps the actual user information private, while still providing useful metrics in your analytics platform. This can be done very easily using WXT's storage API:\n\n```ts\n// app.config.ts\nimport { storage } from 'wxt/storage';\n\nexport default defineAppConfig({\n  analytics: {\n    userId: storage.defineItem('local:custom-user-id-key', {\n      init: () => crypto.randomUUID(),\n    }),\n  },\n});\n```\n\nIf you aren't using `wxt` or `@wxt-dev/storage`, you can define custom implementations for the `userId` and `userProperties` config:\n\n```ts\nconst analytics = createAnalytics({\n  userId: {\n    getValue: () => ...,\n    setValue: (userId) => ...,\n  }\n})\n```\n\n## Auto-track UI events\n\nCall `analytics.autoTrack(container)` to automatically track UI events so you don't have to manually add them. Currently it:\n\n- Tracks clicks to elements inside the `container`\n\nIn your extension's HTML pages, you'll want to call it with `document`:\n\n```ts\nanalytics.autoTrack(document);\n```\n\nBut in content scripts, you usually only care about interactions with your own UI:\n\n```ts\nconst ui = createIntegratedUi({\n  // ...\n  onMount(container) {\n    analytics.autoTrack(container);\n  },\n});\nui.mount();\n```\n\n## Enabling/Disabling\n\nBy default, **analytics is disabled**. You can configure how the value is stored (and change the default value) via the `enabled` config:\n\n```ts\n// app.config.ts\nimport { storage } from 'wxt/storage';\n\nexport default defineAppConfig({\n  analytics: {\n    enabled: storage.defineItem('local:analytics-enabled', {\n      fallback: true,\n    }),\n  },\n});\n```\n\nAt runtime, you can call `setEnabled` to change the value:\n\n```ts\nanalytics.setEnabled(true);\n```\n"
  },
  {
    "path": "packages/analytics/app.config.ts",
    "content": "import { defineAppConfig } from 'wxt/utils/define-app-config';\nimport { googleAnalytics4 } from './modules/analytics/providers/google-analytics-4';\nimport { umami } from './modules/analytics/providers/umami';\n\nexport default defineAppConfig({\n  analytics: {\n    debug: true,\n    providers: [\n      googleAnalytics4({\n        apiSecret: '...',\n        measurementId: '...',\n      }),\n      umami({\n        apiUrl: 'https://umami.aklinker1.io/api',\n        domain: 'analytics.wxt.dev',\n        websiteId: '8f1c2aa4-fad3-406e-a5b2-33e8d4501716',\n      }),\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/analytics/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Popup</title>\n  </head>\n  <body>\n    <label>\n      <input id=\"enabledCheckbox\" type=\"checkbox\" />\n      &emsp;Analytics enabled\n    </label>\n    <button id=\"button1\">Button 1</button>\n    <button class=\"cool-button\">Button 2</button>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/analytics/entrypoints/popup/main.ts",
    "content": "import { analytics } from '#analytics';\n\ndeclare const enabledCheckbox: HTMLInputElement;\n\nanalytics.autoTrack(document);\n\nenabledCheckbox.oninput = () => {\n  void analytics.setEnabled(enabledCheckbox.checked);\n};\n"
  },
  {
    "path": "packages/analytics/modules/analytics/background-plugin.ts",
    "content": "import '#analytics';\n\nexport default () => {};\n"
  },
  {
    "path": "packages/analytics/modules/analytics/client.ts",
    "content": "import { UAParser } from 'ua-parser-js';\nimport type {\n  Analytics,\n  AnalyticsConfig,\n  AnalyticsEventMetadata,\n  AnalyticsPageViewEvent,\n  AnalyticsProvider,\n  AnalyticsStorageItem,\n  AnalyticsTrackEvent,\n  BaseAnalyticsEvent,\n} from './types';\nimport { browser } from '@wxt-dev/browser';\nimport { isBackground } from '@wxt-dev/is-background';\n\ntype AnalyticsMessage = {\n  [K in keyof Analytics]: {\n    fn: K;\n    args: Parameters<Analytics[K]>;\n  };\n}[keyof Analytics];\n\ntype AnalyticsMethod =\n  | ((...args: Parameters<Analytics[keyof Analytics]>) => void)\n  | undefined;\n\ntype MethodForwarder = <K extends keyof Analytics>(\n  fn: K,\n) => (...args: Parameters<Analytics[K]>) => void;\n\nconst ANALYTICS_PORT = '@wxt-dev/analytics';\n\nconst INTERACTIVE_TAGS = new Set([\n  'A',\n  'BUTTON',\n  'INPUT',\n  'SELECT',\n  'TEXTAREA',\n]);\nconst INTERACTIVE_ROLES = new Set([\n  'button',\n  'link',\n  'checkbox',\n  'menuitem',\n  'tab',\n  'radio',\n]);\n\nexport function createAnalytics(config?: AnalyticsConfig): Analytics {\n  if (!browser?.runtime?.id)\n    throw Error(\n      'Cannot use WXT analytics in contexts without access to the browser.runtime APIs',\n    );\n  if (config == null) {\n    console.warn(\n      \"[@wxt-dev/analytics] Config not provided to createAnalytics. If you're using WXT, add the 'analytics' property to '<srcDir>/app.config.ts'.\",\n    );\n  }\n\n  if (isBackground()) return createBackgroundAnalytics(config);\n\n  return createFrontendAnalytics();\n}\n\n/**\n * Creates an analytics client in the background responsible for uploading\n * events to the server to avoid CORS errors.\n */\nfunction createBackgroundAnalytics(\n  config: AnalyticsConfig | undefined,\n): Analytics {\n  // User properties storage\n  const userIdStorage =\n    config?.userId ?? defineStorageItem<string>('wxt-analytics:user-id');\n  const userPropertiesStorage =\n    config?.userProperties ??\n    defineStorageItem<Record<string, string>>(\n      'wxt-analytics:user-properties',\n      {},\n    );\n  const enabled =\n    config?.enabled ??\n    defineStorageItem<boolean>('local:wxt-analytics:enabled', false);\n\n  // Cached values\n  const platformInfo = browser.runtime.getPlatformInfo();\n  const userAgent = UAParser();\n  let userId = Promise.resolve(userIdStorage.getValue()).then(\n    (id) => id ?? globalThis.crypto.randomUUID(),\n  );\n  let userProperties = userPropertiesStorage.getValue();\n  const manifest = browser.runtime.getManifest();\n\n  const getBackgroundMeta = () => ({\n    timestamp: Date.now(),\n    // Don't track sessions for the background, it can be running indefinitely\n    // and will inflate session duration stats.\n    sessionId: undefined,\n    language: navigator.language,\n    referrer: undefined,\n    screen: undefined,\n    url: location.href,\n    title: undefined,\n  });\n\n  const getBaseEvent = async (\n    meta: AnalyticsEventMetadata,\n  ): Promise<BaseAnalyticsEvent> => {\n    const { arch, os } = await platformInfo;\n    return {\n      meta,\n      user: {\n        id: await userId,\n        properties: {\n          version: config?.version ?? manifest.version_name ?? manifest.version,\n          wxtMode: import.meta.env.MODE,\n          wxtBrowser: import.meta.env.BROWSER,\n          arch,\n          os,\n          browser: userAgent.browser.name,\n          browserVersion: userAgent.browser.version,\n          ...(await userProperties),\n        },\n      },\n    };\n  };\n\n  const analytics = {\n    identify: async (\n      newUserId: string,\n      newUserProperties: Record<string, string> = {},\n      meta: AnalyticsEventMetadata = getBackgroundMeta(),\n    ) => {\n      // Update in-memory cache for all providers\n      userId = Promise.resolve(newUserId);\n      userProperties = Promise.resolve(newUserProperties);\n      // Persist user info to storage\n      await Promise.all([\n        userIdStorage.setValue?.(newUserId),\n        userPropertiesStorage.setValue?.(newUserProperties),\n      ]);\n      // Notify providers\n      const event = await getBaseEvent(meta);\n      if (config?.debug) console.debug('[@wxt-dev/analytics] identify', event);\n      if (await enabled.getValue()) {\n        await Promise.allSettled(\n          providers.map((provider) => provider.identify(event)),\n        );\n      } else if (config?.debug) {\n        console.debug(\n          '[@wxt-dev/analytics] Analytics disabled, identify() not uploaded',\n        );\n      }\n    },\n    page: async (\n      location: string,\n      meta: AnalyticsEventMetadata = getBackgroundMeta(),\n    ) => {\n      const baseEvent = await getBaseEvent(meta);\n      const event: AnalyticsPageViewEvent = {\n        ...baseEvent,\n        page: {\n          url: meta?.url ?? globalThis.location?.href,\n          location,\n          title: meta?.title ?? globalThis.document?.title,\n        },\n      };\n      if (config?.debug) console.debug('[@wxt-dev/analytics] page', event);\n      if (await enabled.getValue()) {\n        await Promise.allSettled(\n          providers.map((provider) => provider.page(event)),\n        );\n      } else if (config?.debug) {\n        console.debug(\n          '[@wxt-dev/analytics] Analytics disabled, page() not uploaded',\n        );\n      }\n    },\n    track: async (\n      eventName: string,\n      eventProperties?: Record<string, string | undefined>,\n      meta: AnalyticsEventMetadata = getBackgroundMeta(),\n    ) => {\n      const baseEvent = await getBaseEvent(meta);\n      const event: AnalyticsTrackEvent = {\n        ...baseEvent,\n        event: { name: eventName, properties: eventProperties },\n      };\n      if (config?.debug) console.debug('[@wxt-dev/analytics] track', event);\n      if (await enabled.getValue()) {\n        await Promise.allSettled(\n          providers.map((provider) => provider.track(event)),\n        );\n      } else if (config?.debug) {\n        console.debug(\n          '[@wxt-dev/analytics] Analytics disabled, track() not uploaded',\n        );\n      }\n    },\n    setEnabled: async (newEnabled) => {\n      await enabled.setValue?.(newEnabled);\n    },\n    autoTrack: () => {\n      // Noop, background doesn't have a UI\n      return () => {};\n    },\n  } satisfies Analytics;\n\n  const providers =\n    config?.providers?.map((provider) => provider(analytics, config)) ?? [];\n\n  // Listen for messages from the rest of the extension\n  browser.runtime.onConnect.addListener((port) => {\n    if (port.name === ANALYTICS_PORT) {\n      port.onMessage.addListener(({ fn, args }: AnalyticsMessage) => {\n        void (analytics[fn] as AnalyticsMethod)?.(...args);\n      });\n    }\n  });\n\n  return analytics;\n}\n\n/** Creates an analytics client for non-background contexts. */\nfunction createFrontendAnalytics(): Analytics {\n  const port = browser.runtime.connect({ name: ANALYTICS_PORT });\n  const sessionId = Date.now();\n  const getFrontendMetadata = (): AnalyticsEventMetadata => ({\n    sessionId,\n    timestamp: Date.now(),\n    language: navigator.language,\n    referrer: document.referrer || undefined,\n    screen: `${window.screen.width}x${window.screen.height}`,\n    url: location.href,\n    title: document.title || undefined,\n  });\n\n  const methodForwarder: MethodForwarder =\n    (fn) =>\n    (...args) => {\n      port.postMessage({ fn, args: [...args, getFrontendMetadata()] });\n    };\n\n  const analytics: Analytics = {\n    identify: methodForwarder('identify'),\n    page: methodForwarder('page'),\n    track: methodForwarder('track'),\n    setEnabled: methodForwarder('setEnabled'),\n    autoTrack: (root) => {\n      const onClick = (event: Event) => {\n        const element = event.target as HTMLElement | null;\n        if (\n          !element ||\n          (!INTERACTIVE_TAGS.has(element.tagName) &&\n            !INTERACTIVE_ROLES.has(element.getAttribute('role') ?? ''))\n        )\n          return;\n\n        void analytics.track('click', {\n          tagName: element.tagName?.toLowerCase(),\n          id: element.id || undefined,\n          className: element.className || undefined,\n          textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length\n          href: (element as HTMLAnchorElement).href,\n        });\n      };\n      root.addEventListener('click', onClick, { capture: true, passive: true });\n      return () => {\n        root.removeEventListener('click', onClick);\n      };\n    },\n  };\n  return analytics;\n}\n\nfunction defineStorageItem<T>(key: string): AnalyticsStorageItem<T | undefined>;\nfunction defineStorageItem<T>(\n  key: string,\n  defaultValue: T,\n): AnalyticsStorageItem<T>;\nfunction defineStorageItem(\n  key: string,\n  defaultValue?: unknown,\n): AnalyticsStorageItem<unknown> {\n  return {\n    getValue: async () =>\n      (await browser.storage.local.get<Record<string, unknown>>(key))[key] ??\n      defaultValue,\n    setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),\n  };\n}\n\nexport function defineAnalyticsProvider<T = never>(\n  definition: (\n    /** The analytics object. */\n    analytics: Analytics,\n    /** Config passed into the analytics module from `app.config.ts`. */\n    config: AnalyticsConfig,\n    /** Provider options */\n    options: T,\n  ) => ReturnType<AnalyticsProvider>,\n): (options: T) => AnalyticsProvider {\n  return (options) => (analytics, config) =>\n    definition(analytics, config, options);\n}\n"
  },
  {
    "path": "packages/analytics/modules/analytics/index.ts",
    "content": "import 'wxt';\nimport 'wxt/utils/define-app-config';\nimport {\n  addAlias,\n  addViteConfig,\n  addWxtPlugin,\n  defineWxtModule,\n} from 'wxt/modules';\nimport { relative, resolve } from 'node:path';\nimport type { AnalyticsConfig } from './types';\nimport { normalizePath } from 'wxt';\n\ndeclare module 'wxt/utils/define-app-config' {\n  export interface WxtAppConfig {\n    analytics: AnalyticsConfig;\n  }\n}\n\nexport default defineWxtModule({\n  name: 'analytics',\n  imports: [{ name: 'analytics', from: '#analytics' }],\n  setup(wxt) {\n    // Paths\n    const wxtAnalyticsFolder = resolve(wxt.config.wxtDir, 'analytics');\n    const wxtAnalyticsIndex = resolve(wxtAnalyticsFolder, 'index.ts');\n    const clientModuleId = process.env.NPM\n      ? '@wxt-dev/analytics'\n      : resolve(wxt.config.modulesDir, 'analytics/client');\n    const pluginModuleId = process.env.NPM\n      ? '@wxt-dev/analytics/background-plugin'\n      : resolve(wxt.config.modulesDir, 'analytics/background-plugin');\n\n    // Add required permissions\n    wxt.hook('build:manifestGenerated', (_, manifest) => {\n      manifest.permissions ??= [];\n      if (!manifest.permissions.includes('storage')) {\n        manifest.permissions.push('storage');\n      }\n    });\n\n    // Generate #analytics module\n    const wxtAnalyticsCode = `import { createAnalytics } from '${\n      process.env.NPM\n        ? clientModuleId\n        : normalizePath(relative(wxtAnalyticsFolder, clientModuleId))\n    }';\nimport { useAppConfig } from '#imports';\n\nexport const analytics = createAnalytics(useAppConfig().analytics);\n`;\n    addAlias(wxt, '#analytics', wxtAnalyticsIndex);\n    wxt.hook('prepare:types', async (_, entries) => {\n      entries.push({\n        path: wxtAnalyticsIndex,\n        text: wxtAnalyticsCode,\n      });\n    });\n\n    // Ensure there is a background entrypoint\n    wxt.hook('entrypoints:resolved', (_, entrypoints) => {\n      const hasBackground = entrypoints.find(\n        (entry) => entry.type === 'background',\n      );\n      if (!hasBackground) {\n        entrypoints.push({\n          type: 'background',\n          inputPath: 'virtual:user-background',\n          name: 'background',\n          options: {},\n          outputDir: wxt.config.outDir,\n          skipped: false,\n        });\n      }\n    });\n\n    // Ensure analytics is initialized in every context, mainly the background.\n    // TODO: Once there's a way to filter which entrypoints a plugin is applied to, only apply this to the background\n    addWxtPlugin(wxt, pluginModuleId);\n\n    // Fix issues with dependencies\n    addViteConfig(wxt, () => ({\n      optimizeDeps: {\n        // Ensure the \"#analytics\" import is processed by vite in the background plugin\n        exclude: ['@wxt-dev/analytics'],\n        // Ensure the CJS subdependency is preprocessed into ESM\n        include: ['@wxt-dev/analytics > ua-parser-js'],\n      },\n    }));\n  },\n});\n"
  },
  {
    "path": "packages/analytics/modules/analytics/providers/google-analytics-4.ts",
    "content": "import { defineAnalyticsProvider } from '../client';\nimport type { BaseAnalyticsEvent } from '../types';\n\nconst DEFAULT_ENGAGEMENT_TIME_IN_MSEC = 100;\n\nexport interface GoogleAnalytics4ProviderOptions {\n  apiUrl?: string;\n  apiSecret: string;\n  measurementId: string;\n}\n\nexport const googleAnalytics4 =\n  defineAnalyticsProvider<GoogleAnalytics4ProviderOptions>(\n    (_, config, options) => {\n      const send = async (\n        data: BaseAnalyticsEvent,\n        eventName: string,\n        eventProperties: Record<string, string | undefined> | undefined,\n      ): Promise<void> => {\n        const url = new URL(\n          config?.debug ? '/debug/mp/collect' : '/mp/collect',\n          options.apiUrl ?? 'https://www.google-analytics.com',\n        );\n        if (options.apiSecret)\n          url.searchParams.set('api_secret', options.apiSecret);\n        if (options.measurementId)\n          url.searchParams.set('measurement_id', options.measurementId);\n\n        const userProperties = {\n          language: data.meta.language,\n          screen: data.meta.screen,\n          ...data.user.properties,\n        };\n        const mappedUserProperties = Object.fromEntries(\n          Object.entries(userProperties).map(([name, value]) => [\n            name,\n            value == null ? undefined : { value },\n          ]),\n        );\n\n        await fetch(url.href, {\n          method: 'POST',\n          body: JSON.stringify({\n            client_id: data.user.id,\n            consent: {\n              ad_user_data: 'DENIED',\n              ad_personalization: 'DENIED',\n            },\n            user_properties: mappedUserProperties,\n            user_agent: navigator.userAgent,\n            events: [\n              {\n                name: eventName,\n                params: {\n                  session_id: data.meta.sessionId,\n                  engagement_time_msec: DEFAULT_ENGAGEMENT_TIME_IN_MSEC,\n                  ...eventProperties,\n                },\n              },\n            ],\n          }),\n        });\n      };\n\n      return {\n        identify: () => Promise.resolve(), // No-op, user data uploaded in page/track\n        page: (event) =>\n          send(event, 'page_view', {\n            page_title: event.page.title,\n            page_location: event.page.location,\n          }),\n        track: (event) => send(event, event.event.name, event.event.properties),\n      };\n    },\n  );\n"
  },
  {
    "path": "packages/analytics/modules/analytics/providers/umami.ts",
    "content": "import { defineAnalyticsProvider } from '../client';\n\nexport interface UmamiProviderOptions {\n  apiUrl: string;\n  websiteId: string;\n  domain: string;\n}\n\nexport const umami = defineAnalyticsProvider<UmamiProviderOptions>(\n  (_, config, options) => {\n    const send = (payload: UmamiPayload) => {\n      if (config.debug) {\n        console.debug('[@wxt-dev/analytics] Sending event to Umami:', payload);\n      }\n      return fetch(`${options.apiUrl}/send`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ type: 'event', payload }),\n      });\n    };\n\n    return {\n      identify: () => Promise.resolve(), // No-op, user data uploaded in page/track\n      page: async (event) => {\n        await send({\n          name: 'page_view',\n          website: options.websiteId,\n          url: event.page.url,\n          hostname: options.domain,\n          language: event.meta.language ?? '',\n          referrer: event.meta.referrer ?? '',\n          screen: event.meta.screen ?? '',\n          title: event.page.title ?? '<blank>',\n          data: event.user.properties,\n        });\n      },\n      track: async (event) => {\n        await send({\n          name: event.event.name,\n          website: options.websiteId,\n          url: event.meta.url ?? '/',\n          title: '<blank>',\n          hostname: options.domain,\n          language: event.meta.language ?? '',\n          referrer: event.meta.referrer ?? '',\n          screen: event.meta.screen ?? '',\n          data: {\n            ...event.event.properties,\n            ...event.user.properties,\n          },\n        });\n      },\n    };\n  },\n);\n\n/** @see https://umami.is/docs/api/sending-stats#post-/api/send */\ninterface UmamiPayload {\n  hostname?: string;\n  language?: string;\n  referrer?: string;\n  screen?: string;\n  title?: string;\n  url?: string;\n  website: string;\n  name: string;\n  data?: Record<string, string | undefined>;\n}\n"
  },
  {
    "path": "packages/analytics/modules/analytics/types.ts",
    "content": "export interface Analytics {\n  /** Report a page change. */\n  page: (url: string) => void;\n  /** Report a custom event. */\n  track: (\n    eventName: string,\n    eventProperties?: Record<string, string | undefined>,\n  ) => void;\n  /** Save information about the user. */\n  identify: (userId: string, userProperties?: Record<string, string>) => void;\n  /**\n   * Automatically setup and track user interactions, returning a function to\n   * remove any listeners that were setup.\n   */\n  autoTrack: (root: Document | ShadowRoot | Element) => () => void;\n  /** Calls `config.enabled.setValue`. */\n  setEnabled: (enabled: boolean) => void;\n}\n\nexport interface AnalyticsConfig {\n  /** Array of providers to send analytics to. */\n  providers: AnalyticsProvider[];\n  /** Enable debug logs and other provider-specific debugging features. */\n  debug?: boolean;\n  /**\n   * Your extension's version, reported alongside events.\n   *\n   * @default browser.runtime.getManifest().version`.\n   */\n  version?: string;\n  /**\n   * Configure how the enabled flag is persisted. Defaults to using\n   * `browser.storage.local`.\n   */\n  enabled?: AnalyticsStorageItem<boolean>;\n  /**\n   * Configure how the user Id is persisted. Defaults to using\n   * `browser.storage.local`.\n   */\n  userId?: AnalyticsStorageItem<string | undefined>;\n  /**\n   * Configure how user properties are persisted. Defaults to using\n   * `browser.storage.local`.\n   */\n  userProperties?: AnalyticsStorageItem<Record<string, string>>;\n}\n\nexport interface AnalyticsStorageItem<T> {\n  getValue: () => T | Promise<T>;\n  setValue?: (newValue: T) => void | Promise<void>;\n}\n\nexport type AnalyticsProvider = (\n  analytics: Analytics,\n  config: AnalyticsConfig,\n) => {\n  /** Upload a page view event. */\n  page: (event: AnalyticsPageViewEvent) => Promise<void>;\n  /** Upload a custom event. */\n  track: (event: AnalyticsTrackEvent) => Promise<void>;\n  /** Upload information about the user. */\n  identify: (event: BaseAnalyticsEvent) => Promise<void>;\n};\n\nexport interface BaseAnalyticsEvent {\n  meta: AnalyticsEventMetadata;\n  user: {\n    id: string;\n    properties: Record<string, string | undefined>;\n  };\n}\n\nexport interface AnalyticsEventMetadata {\n  /** Identifier of the session the event was fired from. */\n  sessionId: number | undefined;\n  /** `Date.now()` of when the event was reported. */\n  timestamp: number;\n  /** Ex: `\"1920x1080\"`. */\n  screen: string | undefined;\n  /** `document.referrer` */\n  referrer: string | undefined;\n  /** `navigator.language` */\n  language: string | undefined;\n  /** `location.href` */\n  url: string | undefined;\n  /** `document.title` */\n  title: string | undefined;\n}\n\nexport interface AnalyticsPageInfo {\n  url: string;\n  title: string | undefined;\n  location: string | undefined;\n}\n\nexport interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {\n  page: AnalyticsPageInfo;\n}\n\nexport interface AnalyticsTrackEvent extends BaseAnalyticsEvent {\n  event: {\n    name: string;\n    properties?: Record<string, string | undefined>;\n  };\n}\n"
  },
  {
    "path": "packages/analytics/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/analytics\",\n  \"version\": \"0.5.4\",\n  \"description\": \"Add analytics to your web extension\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"buildc --deps-only -- wxt\",\n    \"dev:build\": \"buildc --deps-only -- wxt build\",\n    \"check\": \"pnpm build && check\",\n    \"build\": \"buildc -- tsdown\",\n    \"prepack\": \"pnpm build\",\n    \"prepare\": \"buildc --deps-only -- wxt prepare\"\n  },\n  \"dependencies\": {\n    \"@wxt-dev/browser\": \"workspace:^\",\n    \"@wxt-dev/is-background\": \"workspace:^\",\n    \"ua-parser-js\": \"^1.0.40\"\n  },\n  \"peerDependencies\": {\n    \"wxt\": \">=0.20.0\"\n  },\n  \"devDependencies\": {\n    \"@types/ua-parser-js\": \"^0.7.39\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"workspace:*\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/analytics\"\n  },\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./module\": {\n      \"types\": \"./dist/module.d.mts\",\n      \"default\": \"./dist/module.mjs\"\n    },\n    \"./background-plugin\": {\n      \"types\": \"./dist/background-plugin.d.mts\",\n      \"default\": \"./dist/background-plugin.mjs\"\n    },\n    \"./types\": {\n      \"types\": \"./dist/types.d.mts\"\n    },\n    \"./providers/google-analytics-4\": {\n      \"types\": \"./dist/providers/google-analytics-4.d.mts\",\n      \"default\": \"./dist/providers/google-analytics-4.mjs\"\n    },\n    \"./providers/umami\": {\n      \"types\": \"./dist/providers/umami.d.mts\",\n      \"default\": \"./dist/providers/umami.mjs\"\n    }\n  },\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"files\": [\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "packages/analytics/public/.keep",
    "content": ""
  },
  {
    "path": "packages/analytics/tsconfig.json",
    "content": "{\n  \"extends\": [\"../../tsconfig.base.json\", \"./.wxt/tsconfig.json\"],\n  \"compilerOptions\": {\n    \"paths\": {\n      \"#analytics\": [\"./.wxt/analytics/index.ts\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/analytics/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nexport default defineConfig({\n  entry: {\n    index: './modules/analytics/client.ts',\n    module: './modules/analytics/index.ts',\n    'background-plugin': './modules/analytics/background-plugin.ts',\n    types: './modules/analytics/types.ts',\n    'providers/google-analytics-4':\n      './modules/analytics/providers/google-analytics-4.ts',\n    'providers/umami': './modules/analytics/providers/umami.ts',\n  },\n  external: ['#analytics'],\n  define: {\n    'process.env.NPM': 'true',\n  },\n});\n"
  },
  {
    "path": "packages/analytics/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\n\nexport default defineConfig({\n  // Unimport doesn't look for imports in node_modules, so when developing a\n  // WXT module, we need to disable this to simplify the build process\n  imports: false,\n\n  manifest: {\n    name: 'Analytics Demo',\n  },\n});\n"
  },
  {
    "path": "packages/auto-icons/CHANGELOG.md",
    "content": "# Changelog\n\n## v1.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.1.0...auto-icons-v1.1.1)\n\n### 🩹 Fixes\n\n- Auto icons override default icons ([#1616](https://github.com/wxt-dev/wxt/pull/1616))\n\n### 💅 Refactors\n\n- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))\n\n### 🏡 Chore\n\n- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n- **deps:** Upgrade non-breaking production dependencies ([#1877](https://github.com/wxt-dev/wxt/pull/1877))\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n\n### ❤️ Contributors\n\n- Omerfardemir <od080624@gmail.com>\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v1.1.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.2...auto-icons-v1.1.0)\n\n### 🚀 Enhancements\n\n- Add overlay option for dev icons ([#1825](https://github.com/wxt-dev/wxt/pull/1825))\n\n### 📖 Documentation\n\n- Rewrite and restructure the documentation website ([#933](https://github.com/wxt-dev/wxt/pull/933))\n- Use full URLs in README so they work on the docs site ([d20793d5](https://github.com/wxt-dev/wxt/commit/d20793d5))\n- Add SVG compatibility note ([#1830](https://github.com/wxt-dev/wxt/pull/1830))\n\n### 🏡 Chore\n\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- **deps:** Bump sharp from 0.33.4 to 0.33.5 ([#959](https://github.com/wxt-dev/wxt/pull/959))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n\n### ❤️ Contributors\n\n- Typed SIGTERM ([@typed-sigterm](https://github.com/typed-sigterm))\n- Kuba ([@zizzfizzix](https://github.com/zizzfizzix))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v1.0.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.1...auto-icons-v1.0.2)\n\n### 📖 Documentation\n\n- **auto-icons:** Fix configuration example typo ([#905](https://github.com/wxt-dev/wxt/pull/905))\n\n### 🏡 Chore\n\n- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))\n\n### ❤️ Contributors\n\n- Uncenter ([@uncenter](http://github.com/uncenter))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v1.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.0...auto-icons-v1.0.1)\n\n### 🩹 Fixes\n\n- **auto-icons:** Path option ([#880](https://github.com/wxt-dev/wxt/pull/880))\n\n### 🏡 Chore\n\n- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))"
  },
  {
    "path": "packages/auto-icons/README.md",
    "content": "# WXT Auto Icons\n\n[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/CHANGELOG.md)\n\n## Features\n\n- Generate extension icons with the correct sizes\n- Make the icon greyscale or include a visible overlay during development\n- SVG is supported\n\n## Usage\n\nInstall the package:\n\n```sh\nnpm i --save-dev @wxt-dev/auto-icons\npnpm i -D @wxt-dev/auto-icons\nyarn add --dev @wxt-dev/auto-icons\nbun i -D @wxt-dev/auto-icons\n```\n\nAdd the module to `wxt.config.ts`:\n\n```ts\nexport default defineConfig({\n  modules: ['@wxt-dev/auto-icons'],\n});\n```\n\nAnd finally, save the base icon to `<srcDir>/assets/icon.png`.\n\n## Configuration\n\nThe module can be configured via the `autoIcons` config:\n\n```ts\nexport default defineConfig({\n  modules: ['@wxt-dev/auto-icons'],\n  autoIcons: {\n    // ...\n  },\n});\n```\n\nOptions have JSDocs available in your editor, or you can read them in the source code: [`AutoIconsOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/src/index.ts).\n"
  },
  {
    "path": "packages/auto-icons/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/auto-icons\",\n  \"description\": \"WXT module for automatically generating extension icons in different sizes\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/auto-icons\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/README.md\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"icons\",\n    \"manifest\"\n  ],\n  \"author\": {\n    \"name\": \"Florian Metz\",\n    \"email\": \"me@timeraa.dev\"\n  },\n  \"contributors\": [\n    {\n      \"name\": \"Aaron Klinker\",\n      \"email\": \"aaronklinker1+wxt@gmail.com\"\n    }\n  ],\n  \"license\": \"MIT\",\n  \"version\": \"1.1.1\",\n  \"type\": \"module\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"pnpm build && check\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"wxt\": \">=0.19.0\"\n  },\n  \"dependencies\": {\n    \"defu\": \"^6.1.4\",\n    \"sharp\": \"^0.34.5\"\n  },\n  \"devDependencies\": {\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"wxt\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/auto-icons/src/__test__/index.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';\nimport { resolve } from 'node:path';\nimport * as fsPromises from 'node:fs/promises';\nimport sharp from 'sharp';\nimport type { Wxt, UserManifest } from 'wxt';\n\n// Import the actual module\nimport autoIconsModule from '../index';\nimport type { AutoIconsOptions } from '../index';\n\n// Mock dependencies\nvi.mock('node:fs/promises', () => ({\n  mkdir: vi.fn(),\n  access: vi.fn(),\n}));\n\nvi.mock('sharp', () => ({\n  default: vi.fn(),\n}));\n\n// Type definitions for better type safety\ninterface MockWxt {\n  config: {\n    srcDir: string;\n    outDir: string;\n    mode: 'development' | 'production';\n  };\n  logger: {\n    warn: Mock;\n  };\n  hooks: {\n    hook: Mock;\n  };\n}\n\ninterface PublicAsset {\n  type: string;\n  fileName: string;\n}\n\ninterface BuildOutput {\n  publicAssets: PublicAsset[];\n}\n\ndescribe('auto-icons module', () => {\n  const mockWxt: MockWxt = {\n    config: {\n      srcDir: '/mock/src',\n      outDir: '/mock/dist',\n      mode: 'development',\n    },\n    logger: {\n      warn: vi.fn(),\n    },\n    hooks: {\n      hook: vi.fn(),\n    },\n  };\n\n  const createMockSharpInstance = () => {\n    const instance = {\n      png: vi.fn(),\n      grayscale: vi.fn(),\n      resize: vi.fn(),\n      toFile: vi.fn().mockResolvedValue(undefined),\n    };\n\n    // Make methods chainable\n    instance.png.mockReturnValue(instance);\n    instance.grayscale.mockReturnValue(instance);\n    instance.resize.mockImplementation(() => {\n      // Create a new instance for each resize to simulate real sharp behavior\n      const resizedInstance = { ...instance };\n      resizedInstance.toFile = vi.fn().mockResolvedValue(undefined);\n      return resizedInstance;\n    });\n\n    return instance;\n  };\n\n  let mockSharpInstance: ReturnType<typeof createMockSharpInstance>;\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n    mockSharpInstance = createMockSharpInstance();\n    vi.mocked(sharp).mockReturnValue(\n      mockSharpInstance as unknown as sharp.Sharp,\n    );\n    vi.mocked(fsPromises.access).mockResolvedValue(undefined);\n    vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined as any);\n  });\n\n  describe('module setup', () => {\n    it('should have correct module metadata', () => {\n      expect(autoIconsModule.name).toBe('@wxt-dev/auto-icons');\n      expect(autoIconsModule.configKey).toBe('autoIcons');\n      expect(typeof autoIconsModule.setup).toBe('function');\n    });\n  });\n\n  describe('options handling', () => {\n    it('should use default options when not provided', async () => {\n      const options: AutoIconsOptions = {};\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      // Verify that the module was set up (hooks were registered)\n      expect(mockWxt.hooks.hook).toHaveBeenCalledWith(\n        'build:manifestGenerated',\n        expect.any(Function),\n      );\n      expect(mockWxt.hooks.hook).toHaveBeenCalledWith(\n        'build:done',\n        expect.any(Function),\n      );\n      expect(mockWxt.hooks.hook).toHaveBeenCalledWith(\n        'prepare:publicPaths',\n        expect.any(Function),\n      );\n    });\n\n    it('should merge custom options with defaults', async () => {\n      const options: AutoIconsOptions = {\n        sizes: [64, 32],\n        grayscaleOnDevelopment: false,\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      // Verify that the module was set up with custom options\n      expect(mockWxt.hooks.hook).toHaveBeenCalledWith(\n        'build:manifestGenerated',\n        expect.any(Function),\n      );\n      expect(mockWxt.hooks.hook).toHaveBeenCalledWith(\n        'build:done',\n        expect.any(Function),\n      );\n      expect(mockWxt.hooks.hook).toHaveBeenCalledWith(\n        'prepare:publicPaths',\n        expect.any(Function),\n      );\n    });\n  });\n\n  describe('error handling', () => {\n    it('should warn when disabled', async () => {\n      const options: AutoIconsOptions = {\n        enabled: false,\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      expect(mockWxt.logger.warn).toHaveBeenCalledWith(\n        '`[auto-icons]` @wxt-dev/auto-icons disabled',\n      );\n      expect(mockWxt.hooks.hook).not.toHaveBeenCalled();\n    });\n\n    it('should warn when base icon not found', async () => {\n      vi.mocked(fsPromises.access).mockRejectedValue(new Error('ENOENT'));\n\n      const options: AutoIconsOptions = {\n        enabled: true,\n        baseIconPath: 'assets/missing-icon.png',\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      expect(mockWxt.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining(\n          'Skipping icon generation, no base icon found at',\n        ),\n      );\n      expect(mockWxt.hooks.hook).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('manifest generation hook', () => {\n    it('should update manifest with default icons when no custom sizes provided', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const manifestHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];\n\n      expect(manifestHook).toBeDefined();\n\n      const manifest: UserManifest = {};\n      if (manifestHook) {\n        await manifestHook(mockWxt as unknown as Wxt, manifest);\n      }\n\n      // Should use default sizes: [128, 48, 32, 16]\n      expect(manifest.icons).toEqual({\n        128: 'icons/128.png',\n        48: 'icons/48.png',\n        32: 'icons/32.png',\n        16: 'icons/16.png',\n      });\n    });\n\n    it('should merge custom sizes with defaults', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n        sizes: [96, 64], // These will be merged with defaults\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const manifestHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];\n\n      expect(manifestHook).toBeDefined();\n\n      const manifest: UserManifest = {};\n      if (manifestHook) {\n        await manifestHook(mockWxt as unknown as Wxt, manifest);\n      }\n\n      // defu merges arrays, so we get both custom and default sizes\n      expect(manifest.icons).toEqual({\n        96: 'icons/96.png',\n        64: 'icons/64.png',\n        128: 'icons/128.png',\n        48: 'icons/48.png',\n        32: 'icons/32.png',\n        16: 'icons/16.png',\n      });\n    });\n\n    it('should warn when overwriting existing icons in manifest', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const manifestHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];\n\n      const manifest: UserManifest = {\n        icons: {\n          128: 'existing-icon.png',\n        },\n      };\n\n      if (manifestHook) {\n        await manifestHook(mockWxt as unknown as Wxt, manifest);\n      }\n\n      expect(mockWxt.logger.warn).toHaveBeenCalledWith(\n        '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',\n      );\n    });\n  });\n\n  describe('icon generation hook', () => {\n    it('should generate icons with default sizes', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n      };\n\n      const output: BuildOutput = {\n        publicAssets: [],\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      expect(buildHook).toBeDefined();\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n      }\n\n      expect(sharp).toHaveBeenCalledWith(\n        resolve('/mock/src', 'assets/icon.png'),\n      );\n      expect(mockSharpInstance.png).toHaveBeenCalled();\n\n      // Should resize to default sizes\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);\n\n      expect(fsPromises.mkdir).toHaveBeenCalledWith(\n        resolve('/mock/dist', 'icons'),\n        { recursive: true },\n      );\n\n      expect(output.publicAssets).toEqual([\n        { type: 'asset', fileName: 'icons/128.png' },\n        { type: 'asset', fileName: 'icons/48.png' },\n        { type: 'asset', fileName: 'icons/32.png' },\n        { type: 'asset', fileName: 'icons/16.png' },\n      ]);\n    });\n\n    it('should generate icons with custom sizes merged with defaults', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n        sizes: [96, 64],\n      };\n\n      const output: BuildOutput = {\n        publicAssets: [],\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      expect(buildHook).toBeDefined();\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n      }\n\n      // Should include both custom and default sizes\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(96);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(64);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);\n      expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);\n\n      expect(output.publicAssets).toEqual([\n        { type: 'asset', fileName: 'icons/96.png' },\n        { type: 'asset', fileName: 'icons/64.png' },\n        { type: 'asset', fileName: 'icons/128.png' },\n        { type: 'asset', fileName: 'icons/48.png' },\n        { type: 'asset', fileName: 'icons/32.png' },\n        { type: 'asset', fileName: 'icons/16.png' },\n      ]);\n    });\n\n    it('should apply grayscale in development mode', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n        grayscaleOnDevelopment: true,\n        sizes: [128],\n      };\n\n      const output: BuildOutput = { publicAssets: [] };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n      }\n\n      expect(mockSharpInstance.grayscale).toHaveBeenCalled();\n    });\n\n    it('should not apply grayscale in production mode', async () => {\n      const prodMockWxt = {\n        ...mockWxt,\n        config: {\n          ...mockWxt.config,\n          mode: 'production' as const,\n        },\n      };\n\n      const options: AutoIconsOptions = {\n        enabled: true,\n        grayscaleOnDevelopment: true,\n        sizes: [128],\n      };\n\n      const output: BuildOutput = { publicAssets: [] };\n\n      await autoIconsModule.setup!(prodMockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(prodMockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      if (buildHook) {\n        await buildHook(prodMockWxt as unknown as Wxt, output);\n      }\n\n      expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();\n    });\n\n    it('should not apply grayscale when disabled', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n        grayscaleOnDevelopment: false,\n        sizes: [128],\n      };\n\n      const output: BuildOutput = { publicAssets: [] };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n      }\n\n      expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('public paths hook', () => {\n    it('should add default icon paths to public paths', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const pathsHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];\n\n      expect(pathsHook).toBeDefined();\n\n      const paths: string[] = [];\n      if (pathsHook) {\n        pathsHook(mockWxt as unknown as Wxt, paths);\n      }\n\n      expect(paths).toEqual([\n        'icons/128.png',\n        'icons/48.png',\n        'icons/32.png',\n        'icons/16.png',\n      ]);\n    });\n  });\n\n  describe('edge cases and error handling', () => {\n    it('should handle empty sizes array', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n        sizes: [], // Empty array should still merge with defaults\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const manifestHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];\n\n      const manifest: UserManifest = {};\n      if (manifestHook) {\n        await manifestHook(mockWxt as unknown as Wxt, manifest);\n      }\n\n      // Should still have default sizes due to defu merge\n      expect(manifest.icons).toEqual({\n        128: 'icons/128.png',\n        48: 'icons/48.png',\n        32: 'icons/32.png',\n        16: 'icons/16.png',\n      });\n    });\n\n    it('should handle sharp processing errors gracefully', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n      };\n\n      // Make toFile throw an error - need to properly chain resize -> png -> toFile\n      const errorInstance = {\n        toFile: vi.fn().mockRejectedValue(new Error('Sharp processing failed')),\n        grayscale: vi.fn(),\n        composite: vi.fn(),\n      };\n      errorInstance.grayscale.mockReturnValue(errorInstance);\n      errorInstance.composite.mockReturnValue(errorInstance);\n\n      mockSharpInstance.resize = vi.fn().mockImplementation(() => ({\n        png: vi.fn().mockReturnValue(errorInstance),\n      }));\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      const output: BuildOutput = { publicAssets: [] };\n\n      // Should throw the sharp error\n      if (buildHook) {\n        await expect(\n          buildHook(mockWxt as unknown as Wxt, output),\n        ).rejects.toThrow('Sharp processing failed');\n      }\n    });\n\n    it('should handle file system errors during directory creation', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n      };\n\n      // Make ensureDir throw an error\n      vi.mocked(fsPromises.mkdir).mockRejectedValue(\n        new Error('Directory creation failed'),\n      );\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      const output: BuildOutput = { publicAssets: [] };\n\n      // The module doesn't await ensureDir, so it won't throw\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n        // But ensureDir should have been called\n        expect(fsPromises.mkdir).toHaveBeenCalled();\n      }\n    });\n\n    it('should handle custom base icon path correctly', async () => {\n      const customPath = 'custom/icon.png';\n      const options: AutoIconsOptions = {\n        enabled: true,\n        baseIconPath: customPath,\n      };\n\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n\n      const output: BuildOutput = { publicAssets: [] };\n\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n      }\n\n      // Should resolve the path relative to srcDir\n      expect(sharp).toHaveBeenCalledWith(resolve('/mock/src', customPath));\n    });\n  });\n\n  describe('integration test', () => {\n    it('should handle full workflow correctly', async () => {\n      const options: AutoIconsOptions = {\n        enabled: true,\n        baseIconPath: 'assets/custom-icon.png',\n        sizes: [96], // Will be merged with defaults\n        grayscaleOnDevelopment: false,\n      };\n\n      const manifest: UserManifest = {};\n      const output: BuildOutput = { publicAssets: [] };\n      const paths: string[] = [];\n\n      // Setup the module\n      await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);\n\n      // Execute all hooks\n      const manifestHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];\n      const buildHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'build:done')?.[1];\n      const pathsHook = vi\n        .mocked(mockWxt.hooks.hook)\n        .mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];\n\n      if (manifestHook) {\n        await manifestHook(mockWxt as unknown as Wxt, manifest);\n      }\n      if (buildHook) {\n        await buildHook(mockWxt as unknown as Wxt, output);\n      }\n      if (pathsHook) {\n        pathsHook(mockWxt as unknown as Wxt, paths);\n      }\n\n      // Verify results - defu merges arrays\n      expect(manifest.icons).toEqual({\n        96: 'icons/96.png',\n        128: 'icons/128.png',\n        48: 'icons/48.png',\n        32: 'icons/32.png',\n        16: 'icons/16.png',\n      });\n\n      expect(output.publicAssets).toEqual([\n        { type: 'asset', fileName: 'icons/96.png' },\n        { type: 'asset', fileName: 'icons/128.png' },\n        { type: 'asset', fileName: 'icons/48.png' },\n        { type: 'asset', fileName: 'icons/32.png' },\n        { type: 'asset', fileName: 'icons/16.png' },\n      ]);\n\n      expect(paths).toEqual([\n        'icons/96.png',\n        'icons/128.png',\n        'icons/48.png',\n        'icons/32.png',\n        'icons/16.png',\n      ]);\n\n      expect(sharp).toHaveBeenCalledWith(\n        resolve('/mock/src', 'assets/custom-icon.png'),\n      );\n      expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/auto-icons/src/index.ts",
    "content": "import 'wxt';\nimport { defineWxtModule } from 'wxt/modules';\nimport { resolve, relative } from 'node:path';\nimport defu from 'defu';\nimport sharp from 'sharp';\nimport { access, mkdir } from 'node:fs/promises';\n\nexport default defineWxtModule<AutoIconsOptions>({\n  name: '@wxt-dev/auto-icons',\n  configKey: 'autoIcons',\n  async setup(wxt, options) {\n    const parsedOptions = defu<Required<AutoIconsOptions>, AutoIconsOptions[]>(\n      options,\n      {\n        enabled: true,\n        baseIconPath: resolve(wxt.config.srcDir, 'assets/icon.png'),\n        developmentIndicator: 'grayscale',\n        sizes: [128, 48, 32, 16],\n      },\n    );\n\n    // Backward compatibility for the deprecated option\n    if (options?.grayscaleOnDevelopment !== undefined) {\n      wxt.logger.warn(\n        '`[auto-icons]` \"grayscaleOnDevelopment\" is deprecated. Use \"developmentIndicator\" instead.',\n      );\n\n      if (options?.developmentIndicator === undefined) {\n        parsedOptions.developmentIndicator = options!.grayscaleOnDevelopment\n          ? 'grayscale'\n          : false;\n      }\n    }\n\n    const resolvedPath = resolve(wxt.config.srcDir, parsedOptions.baseIconPath);\n\n    if (!parsedOptions.enabled)\n      return wxt.logger.warn(`\\`[auto-icons]\\` ${this.name} disabled`);\n\n    const iconExists = await access(resolvedPath).then(\n      () => true,\n      () => false,\n    );\n    if (!iconExists) {\n      return wxt.logger.warn(\n        `\\`[auto-icons]\\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,\n      );\n    }\n\n    wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {\n      if (manifest.icons)\n        wxt.logger.warn(\n          '`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',\n        );\n\n      manifest.icons = Object.fromEntries(\n        parsedOptions.sizes.map((size) => [size, `icons/${size}.png`]),\n      );\n    });\n\n    wxt.hooks.hook('build:done', async (wxt, output) => {\n      const outputFolder = wxt.config.outDir;\n\n      for (const size of parsedOptions.sizes) {\n        const resizedImage = sharp(resolvedPath).resize(size).png();\n\n        if (wxt.config.mode === 'development') {\n          if (parsedOptions.developmentIndicator === 'grayscale') {\n            resizedImage.grayscale();\n          } else if (parsedOptions.developmentIndicator === 'overlay') {\n            // Helper to build an overlay that places a yellow rectangle at the bottom\n            // of the icon with the text \"DEV\" in black. The overlay has the same\n            // dimensions as the icon so we can composite it with default gravity.\n            const buildDevOverlay = (size: number) => {\n              const rectHeight = Math.round(size * 0.5);\n              const fontSize = Math.round(size * 0.35);\n\n              return Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n                <svg width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\" xmlns=\"http://www.w3.org/2000/svg\">\n                  <rect x=\"0\" y=\"${size - rectHeight}\" width=\"${size}\" height=\"${rectHeight}\" fill=\"#ffff00\" />\n                  <text x=\"${size / 2}\" y=\"${size - rectHeight / 2}\" font-family=\"Arial, Helvetica, sans-serif\" font-size=\"${fontSize}\" font-weight=\"bold\" fill=\"black\" text-anchor=\"middle\" dominant-baseline=\"middle\">DEV</text>\n                </svg>`);\n            };\n            const overlayBuffer = await sharp(buildDevOverlay(size))\n              .png()\n              .toBuffer();\n\n            resizedImage.composite([\n              {\n                input: overlayBuffer,\n                left: 0,\n                top: 0,\n              },\n            ]);\n          }\n        }\n\n        mkdir(resolve(outputFolder, 'icons'), { recursive: true });\n        await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`));\n\n        output.publicAssets.push({\n          type: 'asset',\n          fileName: `icons/${size}.png`,\n        });\n      }\n    });\n\n    wxt.hooks.hook('prepare:publicPaths', (wxt, paths) => {\n      for (const size of parsedOptions.sizes) {\n        paths.push(`icons/${size}.png`);\n      }\n    });\n  },\n});\n\n/** Options for the auto-icons module */\nexport interface AutoIconsOptions {\n  /**\n   * Enable auto-icons generation\n   *\n   * @default true\n   */\n  enabled?: boolean;\n  /**\n   * Path to the image to use.\n   *\n   * Path is relative to the project's src directory.\n   *\n   * @default '<srcDir>/assets/icon.png'\n   */\n  baseIconPath?: string;\n  /**\n   * Apply a visual indicator to the icon when running in development mode.\n   *\n   * \"grayscale\" converts the icon to grayscale. \"overlay\" covers the bottom\n   * half with a yellow rectangle and writes \"DEV\" in black text. Set to `false`\n   * to disable any indicator.\n   *\n   * @default 'grayscale'\n   */\n  developmentIndicator?: 'grayscale' | 'overlay' | false;\n  /**\n   * Grayscale the image when in development mode to indicate development\n   *\n   * @deprecated Use `developmentIndicator` instead\n   * @default true\n   */\n  grayscaleOnDevelopment?: boolean;\n  /**\n   * Sizes to generate icons for\n   *\n   * @default [128, 48, 32, 16]\n   */\n  sizes?: number[];\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    autoIcons?: AutoIconsOptions;\n  }\n}\n"
  },
  {
    "path": "packages/auto-icons/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/auto-icons/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    mockReset: true,\n    restoreMocks: true,\n  },\n});\n"
  },
  {
    "path": "packages/browser/README.md",
    "content": "# `@wxt-dev/browser`\n\nProvides access to the `browser` or `chrome` extension APIs and related types.\n\n```ts\nimport { browser, Browser } from '@wxt-dev/browser';\n// Or if you're using WXT:\n// import { browser, Browser } from 'wxt/browser';\n\nconsole.log(browser.runtime.id);\n\nconst onMessage = (message: any, sender: Browser.runtime.MessageSender) => {\n  console.log(message);\n};\nbrowser.runtime.onMessage.addListener(onMessage);\n```\n\n## Installation\n\nIf you're using WXT, this package is already installed, you don't need to install it manually.\n\nOtherwise, you can install the package from NPM:\n\n```sh\npnpm install @wxt-dev/browser\n```\n\n## Upgrading to Latest Types\n\nJust run:\n\n```sh\npnpm upgrade @wxt-dev/browser\n```\n\nThis should update both the manually installed version and the subdependency inside WXT.\n\n## Contributing\n\n### Code Generation\n\nTypes are generated based on the `@types/chrome` package, and with modifications specifically for use with WXT.\n\n### Updating `@types/chrome` Version\n\nYou don't need to do anything! [A github action](https://github.com/wxt-dev/wxt/actions/workflows/update-browser-package.yml) is ran every day to generate and publish this package using the latest `@types/chrome` version.\n\nYou can manually generate types via:\n\n```sh\npnpm gen\n```\n\n### Why not just use `@types/chrome`?\n\nWith WXT, you must import the `browser` variable to use the extension APIs. The way `@types/chrome` is implemented forces you to define a global `chrome` variable. With WXT, this isn't acceptable, we don't want to pollute the global (type) scope or introduce conflicts with auto-imports.\n\nAdditionally, WXT overrides types to provide additional type safety for some APIs, like `browser.runtime.getURL` and `browser.i18n.getMessage`. With `@types/chrome`'s nested namespace approach, it's not possible to override the types for those functions.\n"
  },
  {
    "path": "packages/browser/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/browser\",\n  \"description\": \"Provides a cross-browser API for using extension APIs and types based on @types/chrome\",\n  \"version\": \"0.1.38\",\n  \"type\": \"module\",\n  \"main\": \"src/index.mjs\",\n  \"types\": \"src/index.d.ts\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/browser\"\n  },\n  \"scripts\": {\n    \"check\": \"check\",\n    \"gen\": \"tsx scripts/generate.ts\"\n  },\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"files\": [\n    \"src\"\n  ],\n  \"devDependencies\": {\n    \"@types/chrome\": \"0.1.38\",\n    \"@types/node\": \"^20.0.0\",\n    \"nano-spawn\": \"^2.0.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"dependencies\": {\n    \"@types/filesystem\": \"*\",\n    \"@types/har-format\": \"*\"\n  },\n  \"peerDependencies\": {}\n}\n"
  },
  {
    "path": "packages/browser/scripts/generate.ts",
    "content": "import spawn from 'nano-spawn';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { sep as posixSep } from 'node:path/posix';\n\n// Fetch latest version\n\nconsole.log('Getting latest version of \\x1b[36m@types/chrome\\x1b[0m');\nawait spawn('pnpm', ['i', '--ignore-scripts', '-D', '@types/chrome@latest']);\n\n// Generate new package.json\n\nconsole.log('Generating new \\x1b[36mpackage.json\\x1b[0m');\n\nconst pkgJsonPath = fileURLToPath(\n  import.meta.resolve('@types/chrome/package.json'),\n);\nconst pkgDir = dirname(pkgJsonPath);\nconst pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));\nconst pkgJsonTemplate = await readFile('templates/package.json', 'utf8');\nconst newPkgJson = JSON.parse(\n  pkgJsonTemplate.replaceAll('{{chromeTypesVersion}}', pkgJson.version),\n);\nnewPkgJson.dependencies = pkgJson.dependencies;\nnewPkgJson.peerDependencies = pkgJson.peerDependencies;\nnewPkgJson.peerDependenciesMeta = pkgJson.peerDependenciesMeta;\n\nconst outPkgJsonPath = resolve('package.json');\nawait writeFile(outPkgJsonPath, JSON.stringify(newPkgJson, null, 2));\nawait spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);\n\n// Generate declaration files\n\nconsole.log('Generating declaration files');\nconst outDir = resolve('src/gen');\nconst declarationFileMapping = (\n  await readdir(pkgDir, {\n    recursive: true,\n    encoding: 'utf8',\n  })\n)\n  // Filter to .d.ts files\n  .filter((file) => file.endsWith('.d.ts'))\n  // Map to usable paths\n  .map((file) => ({\n    file: file.replaceAll(sep, posixSep),\n    srcPath: join(pkgDir, file),\n    destPath: join(outDir, file),\n  }));\n\nfor (const { file, srcPath, destPath } of declarationFileMapping) {\n  const content = await readFile(srcPath, 'utf8');\n  const transformedContent = transformFile(file, content);\n  const destDir = dirname(destPath);\n  await mkdir(destDir, { recursive: true });\n  await writeFile(destPath, transformedContent);\n  console.log(`  \\x1b[2m-\\x1b[0m \\x1b[36m${file}\\x1b[0m`);\n}\n\n// Done!\n\nconsole.log('\\x1b[32m✔\\x1b[0m Done in ' + performance.now().toFixed(0) + ' ms');\n\n// Transformations\n\nfunction transformFile(file: string, content: string): string {\n  return (\n    // Add prefix\n    `/* DO NOT EDIT - generated by scripts/generate.ts */\\n\\n${content}\\n`\n      // Remove global type declaration\n      .replaceAll('chrome: typeof chrome;', '// chrome: typeof chrome;')\n      // Rename `chrome` namespace to `Browser` and export it\n      .replaceAll('declare namespace chrome', 'export namespace Browser')\n      // Update references to `chrome` namespace to `Browser`\n      .replaceAll('chrome.', 'Browser.')\n      // Fix links to developer.chrome.com\n      .replaceAll('developer.Browser.com', 'developer.chrome.com')\n  );\n}\n"
  },
  {
    "path": "packages/browser/src/__tests__/index.test.ts",
    "content": "/// <reference types=\"chrome\" />\nimport { describe, expectTypeOf, it } from 'vitest';\nimport { browser, type Browser } from '../index';\n\ndescribe('browser', () => {\n  describe('types', () => {\n    it('should provide types via the Browser import', () => {\n      expectTypeOf<Browser.runtime.MessageSender>().toEqualTypeOf<chrome.runtime.MessageSender>();\n      expectTypeOf<Browser.storage.AreaName>().toEqualTypeOf<chrome.storage.AreaName>();\n      expectTypeOf<Browser.i18n.LanguageDetectionResult>().toEqualTypeOf<chrome.i18n.LanguageDetectionResult>();\n    });\n\n    it('should provide values via the browser import', () => {\n      expectTypeOf(browser.runtime.id).toEqualTypeOf<string>();\n      expectTypeOf(\n        browser.storage.local,\n      ).toEqualTypeOf<Browser.storage.LocalStorageArea>();\n      expectTypeOf(\n        browser.i18n.detectLanguage,\n      ).returns.resolves.toEqualTypeOf<chrome.i18n.LanguageDetectionResult>();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/browser/src/gen/chrome-cast/index.d.ts",
    "content": "/* DO NOT EDIT - generated by scripts/generate.ts */\n\n/* eslint-disable @typescript-eslint/no-wrapper-object-types */\nexport namespace Browser {\n    ////////////////////\n    // Cast\n    // @see https://code.google.com/p/chromium/codesearch#chromium/src/ui/file_manager/externs/chrome_cast.js\n    ////////////////////\n    export namespace cast {\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.AutoJoinPolicy\n         */\n        export enum AutoJoinPolicy {\n            CUSTOM_CONTROLLER_SCOPED = \"custom_controller_scoped\",\n            TAB_AND_ORIGIN_SCOPED = \"tab_and_origin_scoped\",\n            ORIGIN_SCOPED = \"origin_scoped\",\n            PAGE_SCOPED = \"page_scoped\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.DefaultActionPolicy\n         */\n        export enum DefaultActionPolicy {\n            CREATE_SESSION = \"create_session\",\n            CAST_THIS_TAB = \"cast_this_tab\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.Capability\n         */\n        export enum Capability {\n            VIDEO_OUT = \"video_out\",\n            AUDIO_OUT = \"audio_out\",\n            VIDEO_IN = \"video_in\",\n            AUDIO_IN = \"audio_in\",\n            MULTIZONE_GROUP = \"multizone_group\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.ErrorCode\n         */\n        export enum ErrorCode {\n            CANCEL = \"cancel\",\n            TIMEOUT = \"timeout\",\n            API_NOT_INITIALIZED = \"api_not_initialized\",\n            INVALID_PARAMETER = \"invalid_parameter\",\n            EXTENSION_NOT_COMPATIBLE = \"extension_not_compatible\",\n            EXTENSION_MISSING = \"extension_missing\",\n            RECEIVER_UNAVAILABLE = \"receiver_unavailable\",\n            SESSION_ERROR = \"session_error\",\n            CHANNEL_ERROR = \"channel_error\",\n            LOAD_MEDIA_FAILED = \"load_media_failed\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.ReceiverAvailability\n         */\n        export enum ReceiverAvailability {\n            AVAILABLE = \"available\",\n            UNAVAILABLE = \"unavailable\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.SenderPlatform\n         */\n        export enum SenderPlatform {\n            CHROME = \"chrome\",\n            IOS = \"ios\",\n            ANDROID = \"android\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.ReceiverType\n         */\n        export enum ReceiverType {\n            CAST = \"cast\",\n            DIAL = \"dial\",\n            HANGOUT = \"hangout\",\n            CUSTOM = \"custom\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.ReceiverAction\n         */\n        export enum ReceiverAction {\n            CAST = \"cast\",\n            STOP = \"stop\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.SessionStatus\n         */\n        export enum SessionStatus {\n            CONNECTED = \"connected\",\n            DISCONNECTED = \"disconnected\",\n            STOPPED = \"stopped\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.VERSION\n         */\n        export var VERSION: number[];\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.isAvailable\n         */\n        export var isAvailable: boolean;\n\n        /**\n         * @param apiConfig\n         * @param successCallback\n         * @param errorCallback\n         */\n        export function initialize(\n            apiConfig: Browser.cast.ApiConfig,\n            successCallback: () => void,\n            errorCallback: (error: Browser.cast.Error) => void,\n        ): void;\n\n        /**\n         * @param successCallback\n         * @param errorCallback\n         * @param opt_sessionRequest\n         * @param opt_label\n         */\n        export function requestSession(\n            successCallback: (session: Browser.cast.Session) => void,\n            errorCallback: (error: Browser.cast.Error) => void,\n            sessionRequest?: Browser.cast.SessionRequest,\n            label?: string,\n        ): void;\n\n        /**\n         * @param sessionId The id of the session to join.\n         */\n        export function requestSessionById(sessionId: string): void;\n\n        /**\n         * @param listener\n         */\n        export function addReceiverActionListener(\n            listener: (receiver: Browser.cast.Receiver, receiverAction: Browser.cast.ReceiverAction) => void,\n        ): void;\n\n        /**\n         * @param listener\n         */\n        export function removeReceiverActionListener(\n            listener: (receiver: Browser.cast.Receiver, receiverAction: Browser.cast.ReceiverAction) => void,\n        ): void;\n\n        /**\n         * @param message The message to log.\n         */\n        export function logMessage(message: string): void;\n\n        /**\n         * @param receivers\n         * @param successCallback\n         * @param errorCallback\n         */\n        export function setCustomReceivers(\n            receivers: Browser.cast.Receiver[],\n            successCallback: () => void,\n            errorCallback: (error: Browser.cast.Error) => void,\n        ): void;\n\n        /**\n         * @param receiver\n         * @param successCallback\n         * @param errorCallback\n         */\n        export function setReceiverDisplayStatus(\n            receiver: Browser.cast.Receiver,\n            successCallback: () => void,\n            errorCallback: (error: Browser.cast.Error) => void,\n        ): void;\n\n        /**\n         * @param escaped A string to unescape.\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast#.unescape\n         */\n        export function unescape(escaped: string): string;\n\n        export class ApiConfig {\n            /**\n             * @param sessionRequest\n             * @param sessionListener\n             * @param receiverListener\n             * @param opt_autoJoinPolicy\n             * @param opt_defaultActionPolicy\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.ApiConfig\n             */\n            constructor(\n                sessionRequest: Browser.cast.SessionRequest,\n                sessionListener: (session: Browser.cast.Session) => void,\n                receiverListener: (receiverAvailability: Browser.cast.ReceiverAvailability) => void,\n                autoJoinPolicy?: Browser.cast.AutoJoinPolicy,\n                defaultActionPolicy?: Browser.cast.DefaultActionPolicy,\n            );\n\n            sessionRequest: Browser.cast.SessionRequest;\n            sessionListener: (session: Browser.cast.Session) => void;\n            receiverListener: (receiverAvailability: Browser.cast.ReceiverAvailability) => void;\n            autoJoinPolicy: Browser.cast.AutoJoinPolicy;\n            defaultActionPolicy: Browser.cast.DefaultActionPolicy;\n        }\n\n        export class Error {\n            /**\n             * @param code\n             * @param opt_description\n             * @param opt_details\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.Error\n             */\n            constructor(code: Browser.cast.ErrorCode, description?: string, details?: Object);\n\n            code: Browser.cast.ErrorCode;\n            description: string | null;\n            details: object;\n        }\n\n        export class Image {\n            /**\n             * @param url\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.Image\n             */\n            constructor(url: string);\n\n            url: string;\n            height: number | null;\n            width: number | null;\n        }\n\n        export class SenderApplication {\n            /**\n             * @param platform\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.SenderApplication\n             */\n            constructor(platform: Browser.cast.SenderPlatform);\n\n            platform: Browser.cast.SenderPlatform;\n            url: string | null;\n            packageId: string | null;\n        }\n\n        export class SessionRequest {\n            /**\n             * @param appId\n             * @param opt_capabilities\n             * @param opt_timeout\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.SessionRequest\n             */\n            constructor(appId: string, capabilities?: Browser.cast.Capability[], timeout?: number);\n\n            appId: string;\n            capabilities: Browser.cast.Capability[];\n            requestSessionTimeout: number;\n            language: string | null;\n        }\n\n        export class Session {\n            /**\n             * @param sessionId\n             * @param appId\n             * @param displayName\n             * @param appImages\n             * @param receiver\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.Session\n             */\n            constructor(\n                sessionId: string,\n                appId: string,\n                displayName: string,\n                appImages: Browser.cast.Image[],\n                receiver: Browser.cast.Receiver,\n            );\n\n            sessionId: string;\n            appId: string;\n            displayName: string;\n            appImages: Browser.cast.Image[];\n            receiver: Browser.cast.Receiver;\n            senderApps: Browser.cast.SenderApplication[];\n            namespaces: Array<{ name: string }>;\n            media: Browser.cast.media.Media[];\n            status: Browser.cast.SessionStatus;\n            statusText: string | null;\n            transportId: string;\n\n            /**\n             * @param newLevel\n             * @param successCallback\n             * @param errorCallback\n             */\n            setReceiverVolumeLevel(\n                newLevel: number,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param muted\n             * @param successCallback\n             * @param errorCallback\n             */\n            setReceiverMuted(\n                muted: boolean,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param successCallback\n             * @param errorCallback\n             */\n            leave(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;\n\n            /**\n             * @param successCallback\n             * @param errorCallback\n             */\n            stop(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;\n\n            /**\n             * @param namespace\n             * @param message\n             * @param successCallback\n             * @param errorCallback\n             */\n            sendMessage(\n                namespace: string,\n                message: string | object,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param listener\n             */\n            addUpdateListener(listener: (isAlive: boolean) => void): void;\n\n            /**\n             * @param listener\n             */\n            removeUpdateListener(listener: (isAlive: boolean) => void): void;\n\n            /**\n             * @param namespace\n             * @param listener\n             */\n            addMessageListener(namespace: string, listener: (namespace: string, message: string) => void): void;\n\n            /**\n             * @param namespace\n             * @param listener\n             */\n            removeMessageListener(namespace: string, listener: (namespace: string, message: string) => void): void;\n\n            /**\n             * @param listener\n             */\n            addMediaListener(listener: (media: Browser.cast.media.Media) => void): void;\n\n            /**\n             * @param listener\n             */\n            removeMediaListener(listener: (media: Browser.cast.media.Media) => void): void;\n\n            /**\n             * @param loadRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            loadMedia(\n                loadRequest: Browser.cast.media.LoadRequest,\n                successCallback: (media: Browser.cast.media.Media) => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param queueLoadRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueLoad(\n                queueLoadRequest: Browser.cast.media.QueueLoadRequest,\n                successCallback: (media: Browser.cast.media.Media) => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n        }\n\n        export class Receiver {\n            /**\n             * @param label\n             * @param friendlyName\n             * @param opt_capabilities\n             * @param opt_volume\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.Receiver\n             */\n            constructor(\n                label: string,\n                friendlyName: string,\n                capabilities?: Browser.cast.Capability[],\n                volume?: Browser.cast.Volume,\n            );\n\n            label: string;\n            friendlyName: string;\n            capabilities: Browser.cast.Capability[];\n            volume: Browser.cast.Volume;\n            receiverType: Browser.cast.ReceiverType;\n            displayStatus: Browser.cast.ReceiverDisplayStatus;\n        }\n\n        export class ReceiverDisplayStatus {\n            /**\n             * @param statusText\n             * @param appImages\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.ReceiverDisplayStatus\n             */\n            constructor(statusText: string, appImages: Browser.cast.Image[]);\n\n            statusText: string;\n            appImages: Browser.cast.Image[];\n        }\n\n        export class Volume {\n            /**\n             * @param opt_level\n             * @param opt_muted\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.Volume\n             */\n            constructor(level?: number, muted?: boolean);\n\n            level: number | null;\n            muted: boolean | null;\n        }\n    }\n\n    export namespace cast.media {\n        export var DEFAULT_MEDIA_RECEIVER_APP_ID: string;\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.MediaCommand\n         */\n        export enum MediaCommand {\n            PAUSE = \"pause\",\n            SEEK = \"seek\",\n            STREAM_VOLUME = \"stream_volume\",\n            STREAM_MUTE = \"stream_mute\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.MetadataType\n         */\n        export enum MetadataType {\n            GENERIC,\n            TV_SHOW,\n            MOVIE,\n            MUSIC_TRACK,\n            PHOTO,\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.PlayerState\n         */\n        export enum PlayerState {\n            IDLE = \"IDLE\",\n            PLAYING = \"PLAYING\",\n            PAUSED = \"PAUSED\",\n            BUFFERING = \"BUFFERING\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.ResumeState\n         */\n        export enum ResumeState {\n            PLAYBACK_START = \"PLAYBACK_START\",\n            PLAYBACK_PAUSE = \"PLAYBACK_PAUSE\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.StreamType\n         */\n        export enum StreamType {\n            BUFFERED = \"BUFFERED\",\n            LIVE = \"LIVE\",\n            OTHER = \"OTHER\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.IdleReason\n         */\n        export enum IdleReason {\n            CANCELLED = \"CANCELLED\",\n            INTERRUPTED = \"INTERRUPTED\",\n            FINISHED = \"FINISHED\",\n            ERROR = \"ERROR\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.RepeatMode\n         */\n        export enum RepeatMode {\n            OFF = \"REPEAT_OFF\",\n            ALL = \"REPEAT_ALL\",\n            SINGLE = \"REPEAT_SINGLE\",\n            ALL_AND_SHUFFLE = \"REPEAT_ALL_AND_SHUFFLE\",\n        }\n\n        export class QueueItem {\n            /**\n             * @param mediaInfo\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.QueueItem\n             */\n            constructor(mediaInfo: Browser.cast.media.MediaInfo);\n\n            activeTrackIds: Number[];\n            autoplay: boolean;\n            customData: Object;\n            itemId: number;\n            media: Browser.cast.media.MediaInfo;\n            preloadTime: number;\n            startTime: number;\n        }\n\n        export class QueueLoadRequest {\n            /**\n             * @param items\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.QueueLoadRequest\n             */\n            constructor(items: Browser.cast.media.QueueItem[]);\n\n            customData: Object;\n            items: Browser.cast.media.QueueItem[];\n            repeatMode: Browser.cast.media.RepeatMode;\n            startIndex: number;\n        }\n\n        export class QueueInsertItemsRequest {\n            /**\n             * @param itemsToInsert\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.QueueInsertItemsRequest\n             */\n            constructor(itemsToInsert: Browser.cast.media.QueueItem[]);\n\n            customData: Object;\n            insertBefore: number;\n            items: Browser.cast.media.QueueItem[];\n        }\n\n        export class QueueRemoveItemsRequest {\n            /**\n             * @param itemIdsToRemove\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.QueueRemoveItemsRequest\n             */\n            constructor(itemIdsToRemove: number[]);\n\n            customData: Object;\n            itemIds: number[];\n        }\n\n        export class QueueReorderItemsRequest {\n            /**\n             * @param itemIdsToReorder\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.QueueReorderItemsRequest\n             */\n            constructor(itemIdsToReorder: number[]);\n\n            customData: Object;\n            insertBefore: number;\n            itemIds: number[];\n        }\n\n        export class QueueUpdateItemsRequest {\n            /**\n             * @param itemsToUpdate\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.QueueUpdateItemsRequest\n             */\n            constructor(itemsToUpdate: Browser.cast.media.QueueItem[]);\n\n            customData: Object;\n            item: Browser.cast.media.QueueItem[];\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.TrackType\n         */\n        export enum TrackType {\n            TEXT = \"TEXT\",\n            AUDIO = \"AUDIO\",\n            VIDEO = \"VIDEO\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.TextTrackType\n         */\n        export enum TextTrackType {\n            SUBTITLES = \"SUBTITLES\",\n            CAPTIONS = \"CAPTIONS\",\n            DESCRIPTIONS = \"DESCRIPTIONS\",\n            CHAPTERS = \"CHAPTERS\",\n            METADATA = \"METADATA\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.TextTrackEdgeType\n         */\n        export enum TextTrackEdgeType {\n            NONE = \"NONE\",\n            OUTLINE = \"OUTLINE\",\n            DROP_SHADOW = \"DROP_SHADOW\",\n            RAISED = \"RAISED\",\n            DEPRESSED = \"DEPRESSED\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.TextTrackWindowType\n         */\n        export enum TextTrackWindowType {\n            NONE = \"NONE\",\n            NORMAL = \"NORMAL\",\n            ROUNDED_CORNERS = \"ROUNDED_CORNERS\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.TextTrackFontGenericFamily\n         */\n        export enum TextTrackFontGenericFamily {\n            SANS_SERIF = \"SANS_SERIF\",\n            MONOSPACED_SANS_SERIF = \"MONOSPACED_SANS_SERIF\",\n            SERIF = \"SERIF\",\n            MONOSPACED_SERIF = \"MONOSPACED_SERIF\",\n            CASUAL = \"CASUAL\",\n            CURSIVE = \"CURSIVE\",\n            SMALL_CAPITALS = \"SMALL_CAPITALS\",\n        }\n\n        /**\n         * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media#.TextTrackFontStyle\n         */\n        export enum TextTrackFontStyle {\n            NORMAL = \"NORMAL\",\n            BOLD = \"BOLD\",\n            BOLD_ITALIC = \"BOLD_ITALIC\",\n            ITALIC = \"ITALIC\",\n        }\n\n        export class GetStatusRequest {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.GetStatusRequest\n             */\n            constructor();\n\n            customData: Object;\n        }\n\n        export class PauseRequest {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.PauseRequest\n             */\n            constructor();\n\n            customData: Object;\n        }\n\n        export class PlayRequest {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.PlayRequest\n             */\n            constructor();\n\n            customData: Object;\n        }\n\n        export class SeekRequest {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.SeekRequest\n             */\n            constructor();\n\n            currentTime: number;\n            resumeState: Browser.cast.media.ResumeState;\n            customData: Object;\n        }\n\n        export class StopRequest {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.StopRequest\n             */\n            constructor();\n\n            customData: Object;\n        }\n\n        export class VolumeRequest {\n            /**\n             * @param volume\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.VolumeRequest\n             */\n            constructor(volume: Browser.cast.Volume);\n\n            volume: Browser.cast.Volume;\n            customData: Object;\n        }\n\n        export class LoadRequest {\n            /**\n             * @param mediaInfo\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.LoadRequest\n             */\n            constructor(mediaInfo: Browser.cast.media.MediaInfo);\n\n            activeTrackIds: number[];\n            autoplay: boolean;\n            currentTime: number;\n            customData: Object;\n            media: Browser.cast.media.MediaInfo;\n            playbackRate?: number | undefined;\n        }\n\n        export class EditTracksInfoRequest {\n            /**\n             * @param opt_activeTrackIds\n             * @param opt_textTrackStyle\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.EditTracksInfoRequest\n             */\n            constructor(activeTrackIds?: number[], textTrackStyle?: Browser.cast.media.TextTrackStyle);\n\n            activeTrackIds: number[];\n            textTrackStyle: Browser.cast.media.TextTrackStyle;\n        }\n\n        export class GenericMediaMetadata {\n            images: Browser.cast.Image[];\n            metadataType: Browser.cast.media.MetadataType;\n            releaseDate: string;\n            /** @deprecated Use releaseDate instead. */\n            releaseYear: number;\n            subtitle: string;\n            title: string;\n            /** @deprecated Use metadataType instead. */\n            type: Browser.cast.media.MetadataType;\n        }\n\n        export class MovieMediaMetadata {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.MovieMediaMetadata\n             */\n            constructor();\n\n            images: Browser.cast.Image[];\n            metadataType: Browser.cast.media.MetadataType;\n            releaseDate: string;\n            /** @deprecated Use releaseDate instead. */\n            releaseYear: number;\n            subtitle: string;\n            title: string;\n            studio: string;\n            /** @deprecated Use metadataType instead. */\n            type: Browser.cast.media.MetadataType;\n        }\n\n        export class TvShowMediaMetadata {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.TvShowMediaMetadata\n             */\n            constructor();\n\n            metadataType: Browser.cast.media.MetadataType;\n            seriesTitle: string;\n            title: string;\n            season: number;\n            episode: number;\n            images: Browser.cast.Image[];\n            originalAirdate: string;\n\n            /** @deprecated Use metadataType instead. */\n            type: Browser.cast.media.MetadataType;\n            /** @deprecated Use title instead. */\n            episodeTitle: string;\n            /** @deprecated Use season instead. */\n            seasonNumber: number;\n            /** @deprecated Use episode instead. */\n            episodeNumber: number;\n            /** @deprecated Use originalAirdate instead. */\n            releaseYear: number;\n        }\n\n        export class MusicTrackMediaMetadata {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.MusicTrackMediaMetadata\n             */\n            constructor();\n\n            metadataType: Browser.cast.media.MetadataType;\n            albumName: string;\n            title: string;\n            albumArtist: string;\n            artist: string;\n            composer: string;\n            songName: string;\n            trackNumber: number;\n            discNumber: number;\n            images: Browser.cast.Image[];\n            releaseDate: string;\n\n            /** @deprecated Use metadataType instead. */\n            type: Browser.cast.media.MetadataType;\n            /** @deprecated Use artist instead. */\n            artistName: string;\n            /** @deprecated Use releaseDate instead. */\n            releaseYear: number;\n        }\n\n        export class PhotoMediaMetadata {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.PhotoMediaMetadata\n             */\n            constructor();\n\n            metadataType: Browser.cast.media.MetadataType;\n            title: string;\n            artist: string;\n            location: string;\n            images: Browser.cast.Image[];\n            latitude: number;\n            longitude: number;\n            width: number;\n            height: number;\n            creationDateTime: string;\n\n            /** @deprecated Use metadataType instead. */\n            type: Browser.cast.media.MetadataType;\n        }\n\n        export class MediaInfo {\n            /**\n             * @param contentId\n             * @param contentType\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.MediaInfo\n             */\n            constructor(contentId: string, contentType: string);\n\n            contentId: string;\n            streamType: Browser.cast.media.StreamType;\n            contentType: string;\n            metadata: any;\n            duration?: number | null;\n            tracks?: Browser.cast.media.Track[] | null;\n            textTrackStyle?: Browser.cast.media.TextTrackStyle | null;\n            customData?: Object | null;\n        }\n\n        export class Media {\n            /**\n             * @param sessionId\n             * @param mediaSessionId\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.Media\n             */\n            constructor(sessionId: string, mediaSessionId: number);\n\n            activeTrackIds?: number[] | null;\n            currentItemId?: number | null;\n            customData?: Object | null;\n            idleReason: Browser.cast.media.IdleReason | null;\n            items?: Browser.cast.media.QueueItem[] | null;\n            liveSeekableRange?: Browser.cast.media.LiveSeekableRange | undefined;\n            loadingItemId?: number | null;\n            media?: Browser.cast.media.MediaInfo | null;\n            mediaSessionId: number;\n            playbackRate: number;\n            playerState: Browser.cast.media.PlayerState;\n            preloadedItemId?: number | null;\n            repeatMode: Browser.cast.media.RepeatMode;\n            sessionId: string;\n            supportedMediaCommands: Browser.cast.media.MediaCommand[];\n            volume: Browser.cast.Volume;\n\n            /** @deprecated Use getEstimatedTime instead */\n            currentTime: number;\n\n            /**\n             * @param getStatusRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            getStatus(\n                getStatusRequest: Browser.cast.media.GetStatusRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param playRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            play(\n                playRequest: Browser.cast.media.PlayRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param pauseRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            pause(\n                pauseRequest: Browser.cast.media.PauseRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param seekRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            seek(\n                seekRequest: Browser.cast.media.SeekRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param stopRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            stop(\n                stopRequest: Browser.cast.media.StopRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param volumeRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            setVolume(\n                volumeRequest: Browser.cast.media.VolumeRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param editTracksInfoRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            editTracksInfo(\n                editTracksInfoRequest: Browser.cast.media.EditTracksInfoRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param command\n             * @return whether or not the receiver supports the given Browser.cast.media.MediaCommand\n             */\n            supportsCommand(command: Browser.cast.media.MediaCommand): boolean;\n\n            /**\n             * @param listener\n             */\n            addUpdateListener(listener: (isAlive: boolean) => void): void;\n\n            /**\n             * @param listener\n             */\n            removeUpdateListener(listener: (isAlive: boolean) => void): void;\n\n            /**\n             * @return\n             * @suppress {deprecated} Uses currentTime member to compute estimated time.\n             */\n            getEstimatedTime(): number;\n\n            /**\n             * @param item\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueAppendItem(\n                item: Browser.cast.media.QueueItem,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param queueInsertItemsRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueInsertItems(\n                queueInsertItemsRequest: Browser.cast.media.QueueInsertItemsRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param itemId\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueJumpToItem(\n                itemId: number,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param itemId\n             * @param newIndex\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueMoveItemToNewIndex(\n                itemId: number,\n                newIndex: number,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueNext(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;\n\n            /**\n             * @param successCallback\n             * @param errorCallback\n             */\n            queuePrev(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;\n\n            /**\n             * @param itemId\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueRemoveItem(\n                itemId: number,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param queueReorderItemsRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueReorderItems(\n                queueReorderItemsRequest: Browser.cast.media.QueueReorderItemsRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param repeatMode\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueSetRepeatMode(\n                repeatMode: Browser.cast.media.RepeatMode,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n\n            /**\n             * @param queueUpdateItemsRequest\n             * @param successCallback\n             * @param errorCallback\n             */\n            queueUpdateItems(\n                queueUpdateItemsRequest: Browser.cast.media.QueueUpdateItemsRequest,\n                successCallback: () => void,\n                errorCallback: (error: Browser.cast.Error) => void,\n            ): void;\n        }\n\n        export class Track {\n            /**\n             * @param trackId\n             * @param trackType\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.Track\n             */\n            constructor(trackId: number, trackType: Browser.cast.media.TrackType);\n\n            trackId: number;\n            trackContentId: string;\n            trackContentType: string;\n            type: Browser.cast.media.TrackType;\n            name: string;\n            language: string;\n            subtype: Browser.cast.media.TextTrackType;\n            customData: Object;\n        }\n\n        export class TextTrackStyle {\n            /**\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.TextTrackStyle\n             */\n            constructor();\n\n            foregroundColor: string;\n            backgroundColor: string;\n            edgeType: Browser.cast.media.TextTrackEdgeType;\n            edgeColor: string;\n            windowType: Browser.cast.media.TextTrackWindowType;\n            windowColor: string;\n            windowRoundedCornerRadius: number;\n            fontScale: number;\n            fontFamily: string;\n            fontGenericFamily: Browser.cast.media.TextTrackFontGenericFamily;\n            fontStyle: Browser.cast.media.TextTrackFontStyle;\n            customData: Object;\n        }\n\n        export class LiveSeekableRange {\n            /**\n             * @param start\n             * @param end\n             * @param isMovingWindow\n             * @param isLiveDone\n             * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.LiveSeekableRange\n             */\n            constructor(start?: number, end?: number, isMovingWindow?: boolean, isLiveDone?: boolean);\n\n            start?: number | undefined;\n            end?: number | undefined;\n            isMovingWindow?: boolean | undefined;\n            isLiveDone?: boolean | undefined;\n        }\n    }\n\n    /**\n     * @see https://developers.google.com/cast/docs/reference/chrome/Browser.cast.media.timeout\n     */\n    export namespace cast.media.timeout {\n        export var load: number;\n        export var getStatus: number;\n        export var play: number;\n        export var pause: number;\n        export var seek: number;\n        export var stop: number;\n        export var setVolume: number;\n        export var editTracksInfo: number;\n        export var queueInsert: number;\n        export var queueLoad: number;\n        export var queueRemove: number;\n        export var queueReorder: number;\n        export var queueUpdate: number;\n    }\n}\n\n"
  },
  {
    "path": "packages/browser/src/gen/har-format/index.d.ts",
    "content": "/* DO NOT EDIT - generated by scripts/generate.ts */\n\nimport { Entry, Log } from \"har-format\";\n\ndeclare global {\n    export type HARFormatEntry = Entry;\n    export type HARFormatLog = Log;\n}\n\n"
  },
  {
    "path": "packages/browser/src/gen/index.d.ts",
    "content": "/* DO NOT EDIT - generated by scripts/generate.ts */\n\n/// <reference types=\"filesystem\" />\n/// <reference path=\"./har-format/index.d.ts\" />\n/// <reference path=\"./chrome-cast/index.d.ts\" />\n\n// Helpers\ntype SetRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;\ntype SetPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;\n\n////////////////////\n// Global object\n////////////////////\ninterface Window {\n    // chrome: typeof chrome;\n}\n\nexport namespace Browser {\n    ////////////////////\n    // Accessibility Features\n    ////////////////////\n    /**\n     * Use the `Browser.accessibilityFeatures` API to manage Chrome's accessibility features. This API relies on the ChromeSetting prototype of the type API for getting and setting individual accessibility features. In order to get feature states the extension must request `accessibilityFeatures.read` permission. For modifying feature state, the extension needs `accessibilityFeatures.modify` permission. Note that `accessibilityFeatures.modify` does not imply `accessibilityFeatures.read` permission.\n     *\n     * Permissions: \"accessibilityFeatures.read\", \"accessibilityFeatures.modify\"\n     */\n    export namespace accessibilityFeatures {\n        /** `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission. */\n        export const animationPolicy: Browser.types.ChromeSetting<\"allowed\" | \"once\" | \"none\">;\n\n        /**\n         * Auto mouse click after mouse stops moving. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const autoclick: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Caret highlighting. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 51\n         */\n        export const caretHighlight: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Cursor color. The value indicates whether the feature is enabled or not, doesn't indicate the color of it.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 85\n         */\n        export const cursorColor: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Cursor highlighting. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 51\n         */\n        export const cursorHighlight: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Dictation. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 90\n         */\n        export const dictation: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Docked magnifier. The value indicates whether docked magnifier feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 87\n         */\n        export const dockedMagnifier: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Focus highlighting. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 51\n         */\n        export const focusHighlight: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * High contrast rendering mode. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const highContrast: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Enlarged cursor. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const largeCursor: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Full screen magnification. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const screenMagnifier: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Select-to-speak. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 51\n         */\n        export const selectToSpeak: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Spoken feedback (text-to-speech). The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const spokenFeedback: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Sticky modifier keys (like shift or alt). The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const stickyKeys: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Switch Access. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         * @since Chrome 51\n         */\n        export const switchAccess: Browser.types.ChromeSetting<boolean>;\n\n        /**\n         * Virtual on-screen keyboard. The value indicates whether the feature is enabled or not.\n         * `get()` requires `accessibilityFeatures.read` permission. `set()` and `clear()` require `accessibilityFeatures.modify` permission.\n         * @platform ChromeOS only\n         */\n        export const virtualKeyboard: Browser.types.ChromeSetting<boolean>;\n    }\n\n    ////////////////////\n    // Action\n    ////////////////////\n    /**\n     * Use the `Browser.action` API to control the extension's icon in the Google Chrome toolbar.\n     * The action icons are displayed in the browser toolbar next to the omnibox. After installation, these appear in the extensions menu (the puzzle piece icon). Users can pin your extension icon to the toolbar.\n     *\n     * Manifest: \"action\"\n     * @since Chrome 88, MV3\n     */\n    export namespace action {\n        export interface BadgeColorDetails {\n            /** An array of four integers in the range [0,255] that make up the RGBA color of the badge. For example, opaque red is `[255, 0, 0, 255]`. Can also be a string with a CSS value, with opaque red being `#FF0000` or `#F00`. */\n            color: string | extensionTypes.ColorArray;\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n            tabId?: number | undefined;\n        }\n\n        export interface BadgeTextDetails {\n            /** Any number of characters can be passed, but only about four can fit in the space. If an empty string (`''`) is passed, the badge text is cleared. If `tabId` is specified and `text` is null, the text for the specified tab is cleared and defaults to the global badge text. */\n            text?: string | undefined;\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n            tabId?: number | undefined;\n        }\n\n        export interface TitleDetails {\n            /** The string the action should display when moused over. */\n            title: string;\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed.  */\n            tabId?: number | undefined;\n        }\n\n        export interface PopupDetails {\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n            tabId?: number | undefined;\n            /** The html file to show in a popup. If set to the empty string (`''`), no popup is shown. */\n            popup: string;\n        }\n\n        export interface TabIconDetails {\n            /** Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then image with size `scale` \\* n will be selected, where n is the size of the icon in the UI. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.path = {'16': foo}' */\n            path?: string | { [index: number]: string } | undefined;\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed.  */\n            tabId?: number | undefined;\n            /** Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then image with size `scale` \\* n will be selected, where n is the size of the icon in the UI. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'16': foo}' */\n            imageData?: ImageData | { [index: number]: ImageData } | undefined;\n        }\n\n        /** @since Chrome 99 */\n        export interface OpenPopupOptions {\n            /** The id of the window to open the action popup in. Defaults to the currently-active window if unspecified.  */\n            windowId?: number | undefined;\n        }\n\n        export interface TabDetails {\n            /** The ID of the tab to query state for. If no tab is specified, the non-tab-specific state is returned.  */\n            tabId?: number | undefined;\n        }\n\n        /**\n         * The collection of user-specified settings relating to an extension's action.\n         * @since Chrome 91\n         */\n        export interface UserSettings {\n            /** Whether the extension's action icon is visible on browser windows' top-level toolbar (i.e., whether the extension has been 'pinned' by the user). */\n            isOnToolbar: boolean;\n        }\n\n        /** @since Chrome 130 */\n        export interface UserSettingsChange {\n            /** Whether the extension's action icon is visible on browser windows' top-level toolbar (i.e., whether the extension has been 'pinned' by the user). */\n            isOnToolbar?: boolean;\n        }\n\n        /**\n         * Disables the action for a tab.\n         * @param tabId The ID of the tab for which you want to modify the action.\n         *\n         * Can return its result via Promise.\n         */\n        export function disable(tabId?: number): Promise<void>;\n        export function disable(callback: () => void): void;\n        export function disable(tabId: number | undefined, callback: () => void): void;\n\n        /**\n         * Enables the action for a tab. By default, actions are enabled.\n         * @param tabId The ID of the tab for which you want to modify the action.\n         *\n         * Can return its result via Promise.\n         */\n        export function enable(tabId?: number): Promise<void>;\n        export function enable(callback: () => void): void;\n        export function enable(tabId: number | undefined, callback: () => void): void;\n\n        /**\n         * Gets the background color of the action.\n         *\n         * Can return its result via Promise.\n         */\n        export function getBadgeBackgroundColor(details: TabDetails): Promise<extensionTypes.ColorArray>;\n        export function getBadgeBackgroundColor(\n            details: TabDetails,\n            callback: (result: extensionTypes.ColorArray) => void,\n        ): void;\n\n        /**\n         * Gets the badge text of the action. If no tab is specified, the non-tab-specific badge text is returned. If {@link declarativeNetRequest.ExtensionActionOptions.displayActionCountAsBadgeText displayActionCountAsBadgeText} is enabled, a placeholder text will be returned unless the {@link runtime.ManifestPermission declarativeNetRequestFeedback} permission is present or tab-specific badge text was provided.\n         *\n         * Can return its result via Promise.\n         */\n        export function getBadgeText(details: TabDetails): Promise<string>;\n        export function getBadgeText(details: TabDetails, callback: (result: string) => void): void;\n\n        /**\n         * Gets the text color of the action.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 110\n         */\n        export function getBadgeTextColor(details: TabDetails): Promise<extensionTypes.ColorArray>;\n        export function getBadgeTextColor(\n            details: TabDetails,\n            callback: (result: extensionTypes.ColorArray) => void,\n        ): void;\n\n        /**\n         * Gets the html document set as the popup for this action.\n         *\n         * Can return its result via Promise.\n         */\n        export function getPopup(details: TabDetails): Promise<string>;\n        export function getPopup(details: TabDetails, callback: (result: string) => void): void;\n\n        /**\n         * Gets the title of the action.\n         *\n         * Can return its result via Promise.\n         */\n        export function getTitle(details: TabDetails): Promise<string>;\n        export function getTitle(details: TabDetails, callback: (result: string) => void): void;\n\n        /**\n         * Returns the user-specified settings relating to an extension's action.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 91\n         */\n        export function getUserSettings(): Promise<UserSettings>;\n        export function getUserSettings(callback: (userSettings: UserSettings) => void): void;\n\n        /**\n         * Indicates whether the extension action is enabled for a tab (or globally if no `tabId` is provided). Actions enabled using only {@link declarativeContent} always return false.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 110\n         */\n        export function isEnabled(tabId?: number): Promise<boolean>;\n        export function isEnabled(callback: (isEnabled: boolean) => void): void;\n        export function isEnabled(tabId: number | undefined, callback: (isEnabled: boolean) => void): void;\n\n        /**\n         * Opens the extension's popup. Between Chrome 118 and Chrome 126, this is only available to policy installed extensions.\n         *\n         * @param options Specifies options for opening the popup.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 127\n         */\n        export function openPopup(options?: OpenPopupOptions): Promise<void>;\n        export function openPopup(callback: () => void): void;\n        export function openPopup(options: OpenPopupOptions | undefined, callback: () => void): void;\n\n        /**\n         * Sets the background color for the badge.\n         *\n         * Can return its result via Promise.\n         */\n        export function setBadgeBackgroundColor(details: BadgeColorDetails): Promise<void>;\n        export function setBadgeBackgroundColor(details: BadgeColorDetails, callback: () => void): void;\n\n        /**\n         * Sets the badge text for the action. The badge is displayed on top of the icon.\n         *\n         * Can return its result via Promise.\n         */\n        export function setBadgeText(details: BadgeTextDetails): Promise<void>;\n        export function setBadgeText(details: BadgeTextDetails, callback: () => void): void;\n\n        /**\n         * Sets the text color for the badge.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 110\n         */\n        export function setBadgeTextColor(details: BadgeColorDetails): Promise<void>;\n        export function setBadgeTextColor(details: BadgeColorDetails, callback: () => void): void;\n\n        /**\n         * Sets the icon for the action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the path or the imageData property must be specified.\n         *\n         * Can return its result via Promise.\n         */\n        export function setIcon(details: TabIconDetails): Promise<void>;\n        export function setIcon(details: TabIconDetails, callback: () => void): void;\n\n        /**\n         * Sets the html document to be opened as a popup when the user clicks on the action's icon.\n         *\n         * Can return its result via Promise.\n         */\n        export function setPopup(details: PopupDetails): Promise<void>;\n        export function setPopup(details: PopupDetails, callback: () => void): void;\n\n        /**\n         * Sets the title of the action. This shows up in the tooltip.\n         *\n         * Can return its result via Promise.\n         */\n        export function setTitle(details: TitleDetails): Promise<void>;\n        export function setTitle(details: TitleDetails, callback: () => void): void;\n\n        /** Fired when an action icon is clicked. This event will not fire if the action has a popup. */\n        export const onClicked: events.Event<(tab: Browser.tabs.Tab) => void>;\n\n        /**\n         * Fired when user-specified settings relating to an extension's action change.\n         * @since Chrome 130\n         */\n        export const onUserSettingsChanged: events.Event<(change: UserSettingsChange) => void>;\n    }\n\n    ////////////////////\n    // Alarms\n    ////////////////////\n    /**\n     * Use the `Browser.alarms` API to schedule code to run periodically or at a specified time in the future.\n     *\n     * Permissions: \"alarms\"\n     */\n    export namespace alarms {\n        export interface AlarmCreateInfo {\n            /** Length of time in minutes after which the {@link onAlarm} event should fire.  */\n            delayInMinutes?: number | undefined;\n            /** If set, the onAlarm event should fire every `periodInMinutes` minutes after the initial event specified by `when` or `delayInMinutes`. If not set, the alarm will only fire once. */\n            periodInMinutes?: number | undefined;\n            /** Time at which the alarm should fire, in milliseconds past the epoch (e.g. `Date.now() + n`). */\n            when?: number | undefined;\n        }\n\n        export interface Alarm {\n            /** If not null, the alarm is a repeating alarm and will fire again in `periodInMinutes` minutes. */\n            periodInMinutes?: number;\n            /** Time at which this alarm was scheduled to fire, in milliseconds past the epoch (e.g. `Date.now() + n`). For performance reasons, the alarm may have been delayed an arbitrary amount beyond this. */\n            scheduledTime: number;\n            /** Name of this alarm. */\n            name: string;\n        }\n\n        /**\n         * Creates an alarm. Near the time(s) specified by `alarmInfo`, the {@link onAlarm} event is fired. If there is another alarm with the same name (or no name if none is specified), it will be cancelled and replaced by this alarm.\n         *\n         * In order to reduce the load on the user's machine, Chrome limits alarms to at most once every 30 seconds but may delay them an arbitrary amount more. That is, setting `delayInMinutes` or `periodInMinutes` to less than `0.5` will not be honored and will cause a warning. `when` can be set to less than 30 seconds after \"now\" without warning but won't actually cause the alarm to fire for at least 30 seconds.\n         *\n         * To help you debug your app or extension, when you've loaded it unpacked, there's no limit to how often the alarm can fire.\n         * @param name Optional name to identify this alarm. Defaults to the empty string.\n         * @param alarmInfo Describes when the alarm should fire. The initial time must be specified by either `when` or `delayInMinutes` (but not both). If `periodInMinutes` is set, the alarm will repeat every `periodInMinutes` minutes after the initial event. If neither `when` or `delayInMinutes` is set for a repeating alarm, `periodInMinutes` is used as the default for `delayInMinutes`.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function create(alarmInfo: AlarmCreateInfo): Promise<void>;\n        export function create(name: string | undefined, alarmInfo: AlarmCreateInfo): Promise<void>;\n        export function create(alarmInfo: AlarmCreateInfo, callback: () => void): void;\n        export function create(name: string | undefined, alarmInfo: AlarmCreateInfo, callback: () => void): void;\n\n        /**\n         * Gets an array of all the alarms.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function getAll(): Promise<Alarm[]>;\n        export function getAll(callback: (alarms: Alarm[]) => void): void;\n\n        /**\n         * Clears all alarms.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function clearAll(): Promise<boolean>;\n        export function clearAll(callback: (wasCleared: boolean) => void): void;\n\n        /**\n         * Clears the alarm with the given name.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function clear(name?: string): Promise<boolean>;\n        export function clear(callback: (wasCleared: boolean) => void): void;\n        export function clear(name: string | undefined, callback: (wasCleared: boolean) => void): void;\n\n        /**\n         * Retrieves details about the specified alarm.\n         * @param name The name of the alarm to get. Defaults to the empty string.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function get(name?: string): Promise<Alarm | undefined>;\n        export function get(callback: (alarm?: Alarm) => void): void;\n        export function get(name: string | undefined, callback: (alarm?: Alarm) => void): void;\n\n        /** Fired when an alarm has elapsed. Useful for event pages. */\n        export const onAlarm: events.Event<(alarm: Alarm) => void>;\n    }\n\n    ////////////////////\n    // Audio\n    ////////////////////\n    /**\n     * The `Browser.audio` API is provided to allow users to get information about and control the audio devices attached to the system. This API is currently only available in kiosk mode for ChromeOS.\n     *\n     * Permissions: \"audio\"\n     * @platform ChromeOS only\n     * @since Chrome 59\n     */\n    export namespace audio {\n        export interface AudioDeviceInfo {\n            /** Device name */\n            deviceName: string;\n            /** Type of the device */\n            deviceType: DeviceType;\n            /** The user-friendly name (e.g. \"USB Microphone\"). */\n            displayName: string;\n            /** The unique identifier of the audio device. */\n            id: string;\n            /** True if this is the current active device. */\n            isActive: boolean;\n            /** The sound level of the device, volume for output, gain for input. */\n            level: number;\n            /** The stable/persisted device id string when available. */\n            stableDeviceId?: string;\n            /** Stream type associated with this device. */\n            streamType: StreamType;\n        }\n\n        export interface DeviceFilter {\n            /** If set, only audio devices whose active state matches this value will satisfy the filter. */\n            isActive?: boolean;\n            /** If set, only audio devices whose stream type is included in this list will satisfy the filter. */\n            streamTypes?: StreamType[];\n        }\n\n        export interface DeviceIdLists {\n            /**\n             * List of input devices specified by their ID.\n             * To indicate input devices should be unaffected, leave this property unset.\n             */\n            input?: string[];\n            /**\n             * List of output devices specified by their ID.\n             * To indicate output devices should be unaffected, leave this property unset.\n             */\n            output?: string[];\n        }\n\n        export interface DeviceProperties {\n            /**\n             * The audio device's desired sound level. Defaults to the device's current sound level.\n             * If used with audio input device, represents audio device gain.\n             * If used with audio output device, represents audio device volume.\n             */\n            level?: number;\n        }\n\n        /** Available audio device types. */\n        export enum DeviceType {\n            ALSA_LOOPBACK = \"ALSA_LOOPBACK\",\n            BLUETOOTH = \"BLUETOOTH\",\n            FRONT_MIC = \"FRONT_MIC\",\n            HDMI = \"HDMI\",\n            HEADPHONE = \"HEADPHONE\",\n            HOTWORD = \"HOTWORD\",\n            INTERNAL_MIC = \"INTERNAL_MIC\",\n            INTERNAL_SPEAKER = \"INTERNAL_SPEAKER\",\n            KEYBOARD_MIC = \"KEYBOARD_MIC\",\n            LINEOUT = \"LINEOUT\",\n            MIC = \"MIC\",\n            OTHER = \"OTHER\",\n            POST_DSP_LOOPBACK = \"POST_DSP_LOOPBACK\",\n            POST_MIX_LOOPBACK = \"POST_MIX_LOOPBACK\",\n            REAR_MIC = \"REAR_MIC\",\n            USB = \"USB\",\n        }\n\n        export interface LevelChangedEvent {\n            /** ID of device whose sound level has changed. */\n            deviceId: string;\n            /** The device's new sound level. */\n            level: number;\n        }\n\n        export interface MuteChangedEvent {\n            /** Whether or not the stream is now muted. */\n            isMuted: boolean;\n            /** The type of the stream for which the mute value changed. The updated mute value applies to all devices with this stream type. */\n            streamType: StreamType;\n        }\n\n        /** Type of stream an audio device provides. */\n        export enum StreamType {\n            INPUT = \"INPUT\",\n            OUTPUT = \"OUTPUT\",\n        }\n\n        /**\n         * Gets a list of audio devices filtered based on filter.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function getDevices(filter?: DeviceFilter): Promise<AudioDeviceInfo[]>;\n        export function getDevices(filter: DeviceFilter, callback: (devices: AudioDeviceInfo[]) => void): void;\n        export function getDevices(callback: (devices: AudioDeviceInfo[]) => void): void;\n\n        /**\n         * Gets the system-wide mute state for the specified stream type.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function getMute(streamType: `${StreamType}`): Promise<boolean>;\n        export function getMute(streamType: `${StreamType}`, callback: (value: boolean) => void): void;\n\n        /**\n         * Sets lists of active input and/or output devices.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function setActiveDevices(ids: DeviceIdLists): Promise<void>;\n        export function setActiveDevices(ids: DeviceIdLists, callback: () => void): void;\n\n        /**\n         * Sets mute state for a stream type. The mute state will apply to all audio devices with the specified audio stream type.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function setMute(streamType: `${StreamType}`, isMuted: boolean): Promise<void>;\n        export function setMute(streamType: `${StreamType}`, isMuted: boolean, callback: () => void): void;\n\n        /**\n         * Sets the properties for the input or output device.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function setProperties(id: string, properties: DeviceProperties): Promise<void>;\n        export function setProperties(id: string, properties: DeviceProperties, callback: () => void): void;\n\n        /**\n         * Fired when audio devices change, either new devices being added, or existing devices being removed.\n         */\n        export const onDeviceListChanged: Browser.events.Event<(devices: AudioDeviceInfo[]) => void>;\n\n        /**\n         * Fired when sound level changes for an active audio device.\n         */\n        export const onLevelChanged: Browser.events.Event<(event: LevelChangedEvent) => void>;\n\n        /**\n         * Fired when the mute state of the audio input or output changes.\n         * Note that mute state is system-wide and the new value applies to every audio device with specified stream type.\n         */\n        export const onMuteChanged: Browser.events.Event<(event: MuteChangedEvent) => void>;\n    }\n\n    ////////////////////\n    // Bookmarks\n    ////////////////////\n    /**\n     * Use the `Browser.bookmarks` API to create, organize, and otherwise manipulate bookmarks. Also see Override Pages, which you can use to create a custom Bookmark Manager page.\n     *\n     * Permissions: \"bookmarks\"\n     */\n    export namespace bookmarks {\n        /** A node (either a bookmark or a folder) in the bookmark tree. Child nodes are ordered within their parent folder. */\n        export interface BookmarkTreeNode {\n            /** An ordered list of children of this node. */\n            children?: BookmarkTreeNode[];\n            /** When this node was created, in milliseconds since the epoch (`new Date(dateAdded)`). */\n            dateAdded?: number;\n            /** When the contents of this folder last changed, in milliseconds since the epoch. */\n            dateGroupModified?: number;\n            /**\n             * When this node was last opened, in milliseconds since the epoch. Not set for folders.\n             * @since Chrome 114\n             */\n            dateLastUsed?: number;\n            /**\n             * If present, this is a folder that is added by the browser and that cannot be modified by the user or the extension. Child nodes may be modified, if this node does not have the `unmodifiable` property set. Omitted if the node can be modified by the user and the extension (default).\n             *\n             * There may be zero, one or multiple nodes of each folder type. A folder may be added or removed by the browser, but not via the extensions API.\n             * @since Chrome 134\n             */\n            folderType?: `${FolderType}`;\n            /** The unique identifier for the node. IDs are unique within the current profile, and they remain valid even after the browser is restarted. */\n            id: string;\n            /** The 0-based position of this node within its parent folder. */\n            index?: number;\n            /** The `id` of the parent folder. Omitted for the root node. */\n            parentId?: string;\n            /**\n             * Whether this node is synced with the user's remote account storage by the browser. This can be used to distinguish between account and local-only versions of the same {@link FolderType}. The value of this property may change for an existing node, for example as a result of user action.\n             *\n             * Note: this reflects whether the node is saved to the browser's built-in account provider. It is possible that a node could be synced via a third-party, even if this value is false.\n             *\n             * For managed nodes (nodes where `unmodifiable` is set to `true`), this property will always be `false`.\n             * @since Chrome 134\n             */\n            syncing: boolean;\n            /** The text displayed for the node. */\n            title: string;\n            /** Indicates the reason why this node is unmodifiable. The `managed` value indicates that this node was configured by the system administrator or by the custodian of a supervised user. Omitted if the node can be modified by the user and the extension (default). */\n            unmodifiable?: `${BookmarkTreeNodeUnmodifiable}`;\n            /* The URL navigated to when a user clicks the bookmark. Omitted for folders. */\n            url?: string;\n        }\n\n        /**\n         * Indicates the reason why this node is unmodifiable. The `managed` value indicates that this node was configured by the system administrator. Omitted if the node can be modified by the user and the extension (default).\n         * @since Chrome 44\n         */\n        export enum BookmarkTreeNodeUnmodifiable {\n            MANAGED = \"managed\",\n        }\n\n        /** Object passed to the create() function. */\n        export interface CreateDetails {\n            index?: number;\n            /** Defaults to the Other Bookmarks folder. */\n            parentId?: string;\n            title?: string;\n            url?: string;\n        }\n\n        /**\n         * Indicates the type of folder.\n         * @since Chrome 134\n         */\n\n        export enum FolderType {\n            /** The folder whose contents is displayed at the top of the browser window. */\n            BOOKMARKS_BAR = \"bookmarks-bar\",\n            /** Bookmarks which are displayed in the full list of bookmarks on all platforms. */\n            OTHER = \"other\",\n            /** Bookmarks generally available on the user's mobile devices, but modifiable by extension or in the bookmarks manager. */\n            MOBILE = \"mobile\",\n            /** A top-level folder that may be present if the system administrator or the custodian of a supervised user has configured bookmarks. */\n            MANAGED = \"managed\",\n        }\n\n        /** @deprecated Bookmark write operations are no longer limited by Chrome. */\n        export const MAX_WRITE_OPERATIONS_PER_HOUR: 1000000;\n\n        /** @deprecated Bookmark write operations are no longer limited by Chrome. */\n        export const MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE: 1000000;\n\n        /**\n         * The `id` associated with the root level node.\n         * @since Chrome 145\n         */\n        export const ROOT_NODE_ID = \"0\";\n\n        /**\n         * Creates a bookmark or folder under the specified parentId. If url is NULL or missing, it will be a folder.\n         *\n         * Can return its result via Promise since Chrome 90.\n         */\n        export function create(bookmark: CreateDetails): Promise<BookmarkTreeNode>;\n        export function create(bookmark: CreateDetails, callback: (result: BookmarkTreeNode) => void): void;\n\n        /**\n         * Retrieves the specified BookmarkTreeNode(s).\n         * @param idOrIdList A single string-valued id, or an array of string-valued ids\n         *\n         * Can return its result via Promise since Chrome 90.\n         */\n        export function get(idOrIdList: string | [string, ...string[]]): Promise<BookmarkTreeNode[]>;\n        export function get(\n            idOrIdList: string | [string, ...string[]],\n            callback: (results: BookmarkTreeNode[]) => void,\n        ): void;\n\n        /**\n         * Retrieves the children of the specified BookmarkTreeNode id.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function getChildren(id: string): Promise<BookmarkTreeNode[]>;\n        export function getChildren(id: string, callback: (results: BookmarkTreeNode[]) => void): void;\n\n        /**\n         * Retrieves the recently added bookmarks.\n         * @param numberOfItems The maximum number of items to return.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function getRecent(numberOfItems: number): Promise<BookmarkTreeNode[]>;\n        export function getRecent(numberOfItems: number, callback: (results: BookmarkTreeNode[]) => void): void;\n\n        /**\n         * Retrieves part of the Bookmarks hierarchy, starting at the specified node.\n         * @param id The ID of the root of the subtree to retrieve.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function getSubTree(id: string): Promise<BookmarkTreeNode[]>;\n        export function getSubTree(id: string, callback: (results: BookmarkTreeNode[]) => void): void;\n\n        /**\n         * Retrieves the entire Bookmarks hierarchy.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function getTree(): Promise<BookmarkTreeNode[]>;\n        export function getTree(callback: (results: BookmarkTreeNode[]) => void): void;\n\n        interface MoveDestination {\n            parentId?: string;\n            index?: number;\n        }\n\n        /**\n         * Moves the specified BookmarkTreeNode to the provided location.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function move(id: string, destination: MoveDestination): Promise<BookmarkTreeNode>;\n        export function move(\n            id: string,\n            destination: MoveDestination,\n            callback: (result: BookmarkTreeNode) => void,\n        ): void;\n\n        /**\n         * Removes a bookmark or an empty bookmark folder.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function remove(id: string): Promise<void>;\n        export function remove(id: string, callback: () => void): void;\n\n        /**\n         * Recursively removes a bookmark folder.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function removeTree(id: string): Promise<void>;\n        export function removeTree(id: string, callback: () => void): void;\n\n        interface SearchQuery {\n            /** A string of words and quoted phrases that are matched against bookmark URLs and titles.*/\n            query?: string;\n            /** The URL of the bookmark; matches verbatim. Note that folders have no URL. */\n            url?: string;\n            /** The title of the bookmark; matches verbatim. */\n            title?: string;\n        }\n\n        /**\n         * Searches for BookmarkTreeNodes matching the given query. Queries specified with an object produce BookmarkTreeNodes matching all specified properties.\n         * @param query Either a string of words and quoted phrases that are matched against bookmark URLs and titles, or an object. If an object, the properties `query`, `url`, and `title` may be specified and bookmarks matching all specified properties will be produced.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function search(query: string | SearchQuery): Promise<BookmarkTreeNode[]>;\n        export function search(query: string | SearchQuery, callback: (results: BookmarkTreeNode[]) => void): void;\n\n        interface UpdateChanges {\n            title?: string;\n            url?: string;\n        }\n\n        /**\n         * Updates the properties of a bookmark or folder. Specify only the properties that you want to change; unspecified properties will be left unchanged. **Note:** Currently, only 'title' and 'url' are supported.\n         *\n         * Can return its result via Promise since Chrome Chrome 90\n         */\n        export function update(id: string, changes: UpdateChanges): Promise<BookmarkTreeNode>;\n        export function update(id: string, changes: UpdateChanges, callback: (result: BookmarkTreeNode) => void): void;\n\n        /** Fired when a bookmark or folder changes. **Note:** Currently, only title and url changes trigger this.*/\n        export const onChanged: events.Event<(id: string, changeInfo: { title: string; url?: string }) => void>;\n\n        /** Fired when the children of a folder have changed their order due to the order being sorted in the UI. This is not called as a result of a move(). */\n        export const onChildrenReordered: events.Event<(id: string, reorderInfo: { childIds: string[] }) => void>;\n\n        /** Fired when a bookmark or folder is created. */\n        export const onCreated: events.Event<(id: string, bookmark: BookmarkTreeNode) => void>;\n\n        /** Fired when a bookmark import session is begun. Expensive observers should ignore onCreated updates until onImportEnded is fired. Observers should still handle other notifications immediately. */\n        export const onImportBegan: events.Event<() => void>;\n\n        /** Fired when a bookmark import session is ended.  */\n        export const onImportEnded: events.Event<() => void>;\n\n        /** Fired when a bookmark or folder is moved to a different parent folder. */\n        export const onMoved: events.Event<\n            (\n                id: string,\n                moveInfo: {\n                    parentId: string;\n                    index: number;\n                    oldParentId: string;\n                    oldIndex: number;\n                },\n            ) => void\n        >;\n\n        /** Fired when a bookmark or folder is removed. When a folder is removed recursively, a single notification is fired for the folder, and none for its contents. */\n        export const onRemoved: events.Event<\n            (\n                id: string,\n                removeInfo: {\n                    parentId: string;\n                    index: number;\n                    /** @since Chrome 48 */\n                    node: BookmarkTreeNode;\n                },\n            ) => void\n        >;\n    }\n\n    ////////////////////\n    // Browser Action\n    ////////////////////\n    /**\n     * Use browser actions to put icons in the main Google Chrome toolbar, to the right of the address bar. In addition to its icon, a browser action can have a tooltip, a badge, and a popup.\n     *\n     * Manifest: \"browser_action\"\n     *\n     * MV2 only\n     */\n    export namespace browserAction {\n        export interface BadgeBackgroundColorDetails {\n            /** An array of four integers in the range 0-255 that make up the RGBA color of the badge. Can also be a string with a CSS hex color value; for example, `#FF0000` or `#F00` (red). Renders colors at full opacity. */\n            color: string | extensionTypes.ColorArray;\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n            tabId?: number | null | undefined;\n        }\n\n        export interface BadgeTextDetails {\n            /** Any number of characters can be passed, but only about four can fit into the space. If an empty string (`''`) is passed, the badge text is cleared. If `tabId` is specified and `text` is null, the text for the specified tab is cleared and defaults to the global badge text. */\n            text?: string | null | undefined;\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n            tabId?: number | null | undefined;\n        }\n\n        export interface TitleDetails {\n            /** The string the browser action should display when moused over. */\n            title: string;\n            /** Optional. Limits the change to when a particular tab is selected. Automatically resets when the tab is closed.  */\n            tabId?: number | null | undefined;\n        }\n\n        export interface TabDetails {\n            /** The ID of the tab to query state for. If no tab is specified, the non-tab-specific state is returned. */\n            tabId?: number | null | undefined;\n        }\n\n        export type TabIconDetails =\n            & {\n                /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n                tabId?: number | null | undefined;\n            }\n            & (\n                | {\n                    /** Either an ImageData object or a dictionary {size -> ImageData} representing an icon to be set. If the icon is specified as a dictionary, the image used is chosen depending on the screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then an image with size `scale` \\* n is selected, where _n_ is the size of the icon in the UI. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'16': foo}' */\n                    imageData: ImageData | { [index: number]: ImageData };\n                    /** Either a relative image path or a dictionary {size -> relative image path} pointing to an icon to be set. If the icon is specified as a dictionary, the image used is chosen depending on the screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then an image with size `scale` \\* n is selected, where _n_ is the size of the icon in the UI. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.path = {'16': foo}' */\n                    path?: string | { [index: string]: string } | undefined;\n                }\n                | {\n                    /** Either an ImageData object or a dictionary {size -> ImageData} representing an icon to be set. If the icon is specified as a dictionary, the image used is chosen depending on the screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then an image with size `scale` \\* n is selected, where _n_ is the size of the icon in the UI. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'16': foo}' */\n                    imageData?: ImageData | { [index: number]: ImageData } | undefined;\n                    /** Either a relative image path or a dictionary {size -> relative image path} pointing to an icon to be set. If the icon is specified as a dictionary, the image used is chosen depending on the screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then an image with size `scale` \\* n is selected, where _n_ is the size of the icon in the UI. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.path = {'16': foo}' */\n                    path: string | { [index: string]: string };\n                }\n            );\n\n        export interface PopupDetails {\n            /** Limits the change to when a particular tab is selected. Automatically resets when the tab is closed. */\n            tabId?: number | null | undefined;\n            /** The relative path to the HTML file to show in a popup. If set to the empty string (`''`), no popup is shown.*/\n            popup: string;\n        }\n\n        /**\n         * Enables the browser action for a tab. Defaults to enabled.\n         * @param tabId The ID of the tab for which to modify the browser action.\n         * @param callback Since Chrome 67\n         */\n        export function enable(callback?: () => void): void;\n        export function enable(tabId: number | null | undefined, callback?: () => void): void;\n\n        /**\n         * Sets the background color for the badge.\n         * @param callback Since Chrome 67\n         */\n        export function setBadgeBackgroundColor(details: BadgeBackgroundColorDetails, callback?: () => void): void;\n\n        /**\n         * Sets the badge text for the browser action. The badge is displayed on top of the icon.\n         * @param callback Since Chrome 67\n         */\n        export function setBadgeText(details: BadgeTextDetails, callback?: () => void): void;\n\n        /**\n         * Sets the title of the browser action. This title appears in the tooltip.\n         * @param callback Since Chrome 67\n         */\n        export function setTitle(details: TitleDetails, callback?: () => void): void;\n\n        /** Gets the badge text of the browser action. If no tab is specified, the non-tab-specific badge text is returned. */\n        export function getBadgeText(details: TabDetails, callback: (result: string) => void): void;\n\n        /**\n         * Sets the HTML document to be opened as a popup when the user clicks the browser action icon.\n         * @param callback Since Chrome 67\n         */\n        export function setPopup(details: PopupDetails, callback?: () => void): void;\n\n        /**\n         * Disables the browser action for a tab.\n         * @param tabId The ID of the tab for which to modify the browser action.\n         * @param callback since Chrome 67\n         */\n        export function disable(callback?: () => void): void;\n        export function disable(tabId: number | null | undefined, callback?: () => void): void;\n\n        /** Gets the title of the browser action. */\n        export function getTitle(details: TabDetails, callback: (result: string) => void): void;\n\n        /** Gets the background color of the browser action. */\n        export function getBadgeBackgroundColor(\n            details: TabDetails,\n            callback: (result: extensionTypes.ColorArray) => void,\n        ): void;\n\n        /** Gets the HTML document that is set as the popup for this browser action. */\n        export function getPopup(details: TabDetails, callback: (result: string) => void): void;\n\n        /**\n         * Sets the icon for the browser action. The icon can be specified as the path to an image file, as the pixel data from a canvas element, or as a dictionary of one of those. Either the `path` or the `imageData` property must be specified.\n         */\n        export function setIcon(details: TabIconDetails, callback?: () => void): void;\n\n        /** Fired when a browser action icon is clicked. Does not fire if the browser action has a popup. */\n        export const onClicked: events.Event<(tab: Browser.tabs.Tab) => void>;\n    }\n\n    ////////////////////\n    // Browsing Data\n    ////////////////////\n    /**\n     * Use the `Browser.browsingData` API to remove browsing data from a user's local profile.\n     *\n     * Permissions: \"browsingData\"\n     */\n    export namespace browsingData {\n        export interface OriginTypes {\n            /** Extensions and packaged applications a user has installed (be _really_ careful!). */\n            extension?: boolean | undefined;\n            /** Websites that have been installed as hosted applications (be careful!). */\n            protectedWeb?: boolean | undefined;\n            /** Normal websites. */\n            unprotectedWeb?: boolean | undefined;\n        }\n\n        /** Options that determine exactly what data will be removed. */\n        export interface RemovalOptions {\n            /**\n             * When present, data for origins in this list is excluded from deletion. Can't be used together with `origins`. Only supported for cookies, storage and cache. Cookies are excluded for the whole registrable domain.\n             * @since Chrome 74\n             */\n            excludeOrigins?: string[] | undefined;\n            /** An object whose properties specify which origin types ought to be cleared. If this object isn't specified, it defaults to clearing only \"unprotected\" origins. Please ensure that you _really_ want to remove application data before adding 'protectedWeb' or 'extensions'. */\n            originTypes?: OriginTypes | undefined;\n            /**\n             * When present, only data for origins in this list is deleted. Only supported for cookies, storage and cache. Cookies are cleared for the whole registrable domain.\n             * @since Chrome 74\n             */\n            origins?: [string, ...string[]] | undefined;\n            /** Remove data accumulated on or after this date, represented in milliseconds since the epoch (accessible via the {@link Date.getTime getTime} method of the JavaScript `Date` object). If absent, defaults to 0 (which would remove all browsing data). */\n            since?: number | undefined;\n        }\n\n        /** A set of data types. Missing data types are interpreted as `false`. */\n        export interface DataTypeSet {\n            /** Websites' WebSQL data. */\n            webSQL?: boolean | undefined;\n            /** Websites' IndexedDB data. */\n            indexedDB?: boolean | undefined;\n            /** The browser's cookies. */\n            cookies?: boolean | undefined;\n            /**\n             * Stored passwords.\n             * @deprecated since Chrome 144. Support for password deletion through extensions has been removed. This data type will be ignored.\n             */\n            passwords?: boolean | undefined;\n            /**\n             * Server-bound certificates.\n             * @deprecated since Chrome 76. Support for server-bound certificates has been removed. This data type will be ignored.\n             */\n            serverBoundCertificates?: boolean | undefined;\n            /** The browser's download list. */\n            downloads?: boolean | undefined;\n            /** The browser's cache. */\n            cache?: boolean | undefined;\n            /** Cache storage. */\n            cacheStorage?: boolean | undefined;\n            /** Websites' appcaches. */\n            appcache?: boolean | undefined;\n            /** Websites' file systems. */\n            fileSystems?: boolean | undefined;\n            /**\n             * Plugins' data.\n             * @deprecated since Chrome 88. Support for Flash has been removed. This data type will be ignored.\n             */\n            pluginData?: boolean | undefined;\n            /** Websites' local storage data. */\n            localStorage?: boolean | undefined;\n            /** The browser's stored form data. */\n            formData?: boolean | undefined;\n            /** The browser's history. */\n            history?: boolean | undefined;\n            /** Service Workers. */\n            serviceWorkers?: boolean | undefined;\n        }\n\n        export interface SettingsResult {\n            options: RemovalOptions;\n            /** All of the types will be present in the result, with values of `true` if they are both selected to be removed and permitted to be removed, otherwise `false`. */\n            dataToRemove: DataTypeSet;\n            /** All of the types will be present in the result, with values of `true` if they are permitted to be removed (e.g., by enterprise policy) and `false` if not. */\n            dataRemovalPermitted: DataTypeSet;\n        }\n\n        /**\n         * Reports which types of data are currently selected in the 'Clear browsing data' settings UI. Note: some of the data types included in this API are not available in the settings UI, and some UI settings control more than one data type listed here.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function settings(): Promise<SettingsResult>;\n        export function settings(callback: (result: SettingsResult) => void): void;\n\n        /**\n         * Clears plugins' data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @deprecated since Chrome 88. Support for Flash has been removed. This function has no effect\n         */\n        export function removePluginData(options: RemovalOptions): Promise<void>;\n        export function removePluginData(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' service workers.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @since Chrome 72\n         */\n        export function removeServiceWorkers(options: RemovalOptions): Promise<void>;\n        export function removeServiceWorkers(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears the browser's stored form data (autofill).\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeFormData(options: RemovalOptions): Promise<void>;\n        export function removeFormData(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' file system data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeFileSystems(options: RemovalOptions): Promise<void>;\n        export function removeFileSystems(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears various types of browsing data stored in a user's profile.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param dataToRemove The set of data types to remove.\n         */\n        export function remove(options: RemovalOptions, dataToRemove: DataTypeSet): Promise<void>;\n        export function remove(options: RemovalOptions, dataToRemove: DataTypeSet, callback: () => void): void;\n\n        /**\n         * Clears the browser's stored passwords.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @deprecated since Chrome 144. Support for password deletion through extensions has been removed. This function has no effect.\n         */\n        export function removePasswords(options: RemovalOptions): Promise<void>;\n        export function removePasswords(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears the browser's cookies and server-bound certificates modified within a particular timeframe.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeCookies(options: RemovalOptions): Promise<void>;\n        export function removeCookies(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' WebSQL data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeWebSQL(options: RemovalOptions): Promise<void>;\n        export function removeWebSQL(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' appcache data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeAppcache(options: RemovalOptions): Promise<void>;\n        export function removeAppcache(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' cache storage data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeCacheStorage(options: RemovalOptions): Promise<void>;\n        export function removeCacheStorage(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears the browser's list of downloaded files (not the downloaded files themselves).\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeDownloads(options: RemovalOptions): Promise<void>;\n        export function removeDownloads(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' local storage data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeLocalStorage(options: RemovalOptions): Promise<void>;\n        export function removeLocalStorage(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears the browser's cache.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeCache(options: RemovalOptions): Promise<void>;\n        export function removeCache(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears the browser's history.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeHistory(options: RemovalOptions): Promise<void>;\n        export function removeHistory(options: RemovalOptions, callback: () => void): void;\n\n        /**\n         * Clears websites' IndexedDB data.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeIndexedDB(options: RemovalOptions): Promise<void>;\n        export function removeIndexedDB(options: RemovalOptions, callback: () => void): void;\n    }\n\n    ////////////////////\n    // Certificate Provider\n    ////////////////////\n    /**\n     * Use this API to expose certificates to the platform which can use these certificates for TLS authentications.\n     *\n     * Manifest: \"certificateProvider\"\n     * @platform ChromeOS only\n     * @since Chrome 46\n     */\n    export namespace certificateProvider {\n        /** Types of supported cryptographic signature algorithms. */\n        export enum Algorithm {\n            /**\n             * Specifies the RSASSA PKCS#1 v1.5 signature algorithm with the MD5-SHA-1 hashing. The extension must not prepend a DigestInfo prefix but only add PKCS#1 padding.\n             * @deprecated This algorithm is deprecated and will never be requested by Chrome as of version 109.\n             */\n            RSASSA_PKCS1_V1_5_MD5_SHA1 = \"RSASSA_PKCS1_v1_5_MD5_SHA1\",\n            /** Specifies the RSASSA PKCS#1 v1.5 signature algorithm with the SHA-1 hash function. */\n            RSASSA_PKCS1_V1_5_SHA1 = \"RSASSA_PKCS1_v1_5_SHA1\",\n            /** Specifies the RSASSA PKCS#1 v1.5 signature algorithm with the SHA-256 hashing function. */\n            RSASSA_PKCS1_V1_5_SHA256 = \"RSASSA_PKCS1_v1_5_SHA256\",\n            /** Specifies the RSASSA PKCS#1 v1.5 signature algorithm with the SHA-384 hashing function. */\n            RSASSA_PKCS1_V1_5_SHA384 = \"RSASSA_PKCS1_v1_5_SHA384\",\n            /** Specifies the RSASSA PKCS#1 v1.5 signature algorithm with the SHA-512 hashing function. */\n            RSASSA_PKCS1_V1_5_SHA512 = \"RSASSA_PKCS1_v1_5_SHA512\",\n            /** Specifies the RSASSA PSS signature algorithm with the SHA-256 hashing function, MGF1 mask generation function and the salt of the same size as the hash. */\n            RSASSA_PSS_SHA256 = \"RSASSA_PSS_SHA256\",\n            /** Specifies the RSASSA PSS signature algorithm with the SHA-384 hashing function, MGF1 mask generation function and the salt of the same size as the hash. */\n            RSASSA_PSS_SHA384 = \"RSASSA_PSS_SHA384\",\n            /** Specifies the RSASSA PSS signature algorithm with the SHA-512 hashing function, MGF1 mask generation function and the salt of the same size as the hash. */\n            RSASSA_PSS_SHA512 = \"RSASSA_PSS_SHA512\",\n        }\n\n        export interface CertificateInfo {\n            /** Must be the DER encoding of a X.509 certificate. Currently, only certificates of RSA keys are supported. */\n            certificate: ArrayBuffer;\n            /** Must be set to all hashes supported for this certificate. This extension will only be asked for signatures of digests calculated with one of these hash algorithms. This should be in order of decreasing hash preference. */\n            supportedHashes: `${Hash}`[];\n        }\n\n        /** @since Chrome 86 */\n        export interface CertificatesUpdateRequest {\n            /** Request identifier to be passed to {@link setCertificates}. */\n            certificatesRequestId: number;\n        }\n\n        /** @since Chrome 86 */\n        export interface ClientCertificateInfo {\n            /**\n             * The array must contain the DER encoding of the X.509 client certificate as its first element.\n             *\n             * This must include exactly one certificate.\n             */\n            certificateChain: ArrayBuffer[];\n            /** All algorithms supported for this certificate. The extension will only be asked for signatures using one of these algorithms. */\n            supportedAlgorithms: `${Algorithm}`[];\n        }\n\n        /**\n         * Types of errors that the extension can report.\n         * @since Chrome 86\n         */\n        export enum Error {\n            GENERAL_ERROR = \"GENERAL_ERROR\",\n        }\n\n        /** @deprecated Replaced by {@link Algorithm}.*/\n        export enum Hash {\n            /** Specifies the MD5 and SHA1 hashing algorithms. */\n            MD5_SHA1 = \"MD5_SHA1\",\n            /** Specifies the SHA1 hashing algorithm. */\n            SHA1 = \"SHA1\",\n            /** Specifies the SHA256 hashing algorithm. */\n            SHA256 = \"SHA256\",\n            /** Specifies the SHA384 hashing algorithm. */\n            SHA384 = \"SHA384\",\n            /** Specifies the SHA512 hashing algorithm. */\n            SHA512 = \"SHA512\",\n        }\n\n        /**\n         * The types of errors that can be presented to the user through the requestPin function.\n         * @since Chrome 57\n         */\n        export enum PinRequestErrorType {\n            /** Specifies the PIN is invalid. */\n            INVALID_PIN = \"INVALID_PIN\",\n            /** Specifies the PUK is invalid. */\n            INVALID_PUK = \"INVALID_PUK\",\n            /** Specifies the maximum attempt number has been exceeded. */\n            MAX_ATTEMPTS_EXCEEDED = \"MAX_ATTEMPTS_EXCEEDED\",\n            /** Specifies that the error cannot be represented by the above types. */\n            UNKNOWN_ERROR = \"UNKNOWN_ERROR\",\n        }\n\n        /**\n         * The type of code being requested by the extension with requestPin function.\n         * @since Chrome 57\n         */\n        export enum PinRequestType {\n            /** Specifies the requested code is a PIN. */\n            PIN = \"PIN\",\n            /** Specifies the requested code is a PUK. */\n            PUK = \"PUK\",\n        }\n\n        /** @since Chrome 57 */\n        export interface PinResponseDetails {\n            /** The code provided by the user. Empty if user closed the dialog or some other error occurred. */\n            userInput?: string | undefined;\n        }\n\n        /** @since Chrome 86 */\n        export interface ReportSignatureDetails {\n            /** Error that occurred while generating the signature, if any. */\n            error?: `${Error}` | undefined;\n            /** Request identifier that was received via the {@link onSignatureRequested} event. */\n            signRequestId: number;\n            /** The signature, if successfully generated. */\n            signature?: ArrayBuffer | undefined;\n        }\n\n        /** @since Chrome 57 */\n        export interface RequestPinDetails {\n            /** The number of attempts left. This is provided so that any UI can present this information to the user. Chrome is not expected to enforce this, instead stopPinRequest should be called by the extension with errorType = MAX_ATTEMPTS_EXCEEDED when the number of pin requests is exceeded. */\n            attemptsLeft?: number | undefined;\n            /** The error template displayed to the user. This should be set if the previous request failed, to notify the user of the failure reason. */\n            errorType?: `${PinRequestErrorType}` | undefined;\n            /** The type of code requested. Default is PIN. */\n            requestType?: `${PinRequestType}` | undefined;\n            /** The ID given by Chrome in SignRequest. */\n            signRequestId: number;\n        }\n\n        /** @since Chrome 86 */\n        export interface SetCertificatesDetails {\n            /** When called in response to {@link onCertificatesUpdateRequested}, should contain the received `certificatesRequestId` value. Otherwise, should be unset. */\n            certificatesRequestId?: number | undefined;\n            /** List of currently available client certificates. */\n            clientCertificates: ClientCertificateInfo[];\n            /** Error that occurred while extracting the certificates, if any. This error will be surfaced to the user when appropriate. */\n            error?: `${Error}` | undefined;\n        }\n\n        /**  @since Chrome 86 */\n        export interface SignatureRequest {\n            /** Signature algorithm to be used. */\n            algorithm: `${Algorithm}`;\n            /** The DER encoding of a X.509 certificate. The extension must sign `input` using the associated private key. */\n            certificate: ArrayBuffer;\n            /** Data to be signed. Note that the data is not hashed. */\n            input: ArrayBuffer;\n            /** Request identifier to be passed to {@link reportSignature}. */\n            signRequestId: number;\n        }\n\n        export interface SignRequest {\n            /** The DER encoding of a X.509 certificate. The extension must sign `digest` using the associated private key. */\n            certificate: ArrayBuffer;\n            /**  The digest that must be signed. */\n            digest: ArrayBuffer;\n            /** Refers to the hash algorithm that was used to create `digest`. */\n            hash: `${Hash}`;\n            /**\n             * The unique ID to be used by the extension should it need to call a method that requires it, e.g. requestPin.\n             * @since Chrome 57\n             */\n            signRequestId: number;\n        }\n\n        /** @since Chrome 57 */\n        export interface StopPinRequestDetails {\n            /** The error template. If present it is displayed to user. Intended to contain the reason for stopping the flow if it was caused by an error, e.g. MAX\\_ATTEMPTS\\_EXCEEDED. */\n            errorType?: `${PinRequestErrorType}` | undefined;\n            /** The ID given by Chrome in SignRequest. */\n            signRequestId: number;\n        }\n\n        /**\n         * Should be called as a response to {@link onSignatureRequested}.\n         *\n         * The extension must eventually call this function for every {@link onSignatureRequested} event; the API implementation will stop waiting for this call after some time and respond with a timeout error when this function is called.\n         *\n         * Can return its result via Promise since Chrome 96.\n         * @since Chrome 86\n         */\n        export function reportSignature(details: ReportSignatureDetails): Promise<void>;\n        export function reportSignature(details: ReportSignatureDetails, callback: () => void): void;\n\n        /**\n         * Requests the PIN from the user. Only one ongoing request at a time is allowed. The requests issued while another flow is ongoing are rejected. It's the extension's responsibility to try again later if another flow is in progress.\n         *\n         * Can return its result via Promise since Chrome 96.\n         * @param details Contains the details about the requested dialog.\n         * @since Chrome 57\n         */\n        export function requestPin(details: RequestPinDetails): Promise<PinResponseDetails | undefined>;\n        export function requestPin(\n            details: RequestPinDetails,\n            callback: (details?: PinResponseDetails | undefined) => void,\n        ): void;\n\n        /**\n         * Sets a list of certificates to use in the browser.\n         *\n         * The extension should call this function after initialization and on every change in the set of currently available certificates. The extension should also call this function in response to {@link onCertificatesUpdateRequested} every time this event is received.\n         *\n         * Can return its result via Promise since Chrome 96.\n         * @param details The certificates to set. Invalid certificates will be ignored.\n         * @since Chrome 86\n         */\n        export function setCertificates(details: SetCertificatesDetails): Promise<void>;\n        export function setCertificates(details: SetCertificatesDetails, callback: () => void): void;\n\n        /**\n         * Stops the pin request started by the {@link requestPin} function.\n         *\n         * Can return its result via Promise since Chrome 96.\n         * @param details Contains the details about the reason for stopping the request flow.\n         * @since Chrome 57\n         */\n        export function stopPinRequest(details: StopPinRequestDetails): Promise<void>;\n        export function stopPinRequest(details: StopPinRequestDetails, callback: () => void): void;\n\n        /**\n         * This event fires if the certificates set via {@link setCertificates} are insufficient or the browser requests updated information. The extension must call {@link setCertificates} with the updated list of certificates and the received `certificatesRequestId`.\n         * @since Chrome 86\n         */\n        export const onCertificatesUpdateRequested: events.Event<(request: CertificatesUpdateRequest) => void>;\n\n        /**\n         * This event fires every time the browser needs to sign a message using a certificate provided by this extension via {@link setCertificates}.\n         *\n         * The extension must sign the input data from `request` using the appropriate algorithm and private key and return it by calling {@link reportSignature} with the received `signRequestId`.\n         * @since Chrome 86\n         */\n        export const onSignatureRequested: events.Event<(request: SignatureRequest) => void>;\n    }\n\n    ////////////////////\n    // Commands\n    ////////////////////\n    /**\n     * Use the commands API to add keyboard shortcuts that trigger actions in your extension, for example, an action to open the browser action or send a command to the extension.\n     *\n     * Manifest: \"commands\"\n     */\n    export namespace commands {\n        export interface Command {\n            /** The name of the Extension Command */\n            name?: string;\n            /** The Extension Command description */\n            description?: string;\n            /** The shortcut active for this command, or blank if not active. */\n            shortcut?: string;\n        }\n\n        /**\n         * Returns all the registered extension commands for this extension and their shortcut (if active). Before Chrome 110, this command did not return `_execute_action`.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getAll(): Promise<Command[]>;\n        export function getAll(callback: (commands: Command[]) => void): void;\n\n        /** Fired when a registered command is activated using a keyboard shortcut. */\n        export const onCommand: events.Event<(command: string, tab?: tabs.Tab) => void>;\n    }\n\n    ////////////////////\n    // Content Settings\n    ////////////////////\n    /**\n     * Use the `Browser.contentSettings` API to change settings that control whether websites can use features such as cookies, JavaScript, and plugins. More generally speaking, content settings allow you to customize Chrome's behavior on a per-site basis instead of globally.\n     *\n     * Permissions: \"contentSettings\"\n     */\n    export namespace contentSettings {\n        /** @since Chrome 113 */\n        export enum AutoVerifyContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n        }\n\n        /** @since Chrome 46 */\n        export enum CameraContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            ASK = \"ask\",\n        }\n\n        /** @since Chrome 121 */\n        export enum ClipboardContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            ASK = \"ask\",\n        }\n\n        interface ContentSettingClearParams {\n            /** Where to clear the setting (default: regular). */\n            scope?: `${Scope}`;\n        }\n\n        interface ContentSettingGetParams {\n            /** Whether to check the content settings for an incognito session. (default false) */\n            incognito?: boolean;\n            /** The primary URL for which the content setting should be retrieved. Note that the meaning of a primary URL depends on the content type. */\n            primaryUrl: string;\n            /** The secondary URL for which the content setting should be retrieved. Defaults to the primary URL. Note that the meaning of a secondary URL depends on the content type, and not all content types use secondary URLs. */\n            secondaryUrl?: string;\n            /** A more specific identifier of the type of content for which the settings should be retrieved. */\n            resourceIdentifier?: ResourceIdentifier;\n        }\n\n        interface ContentSettingGetResult<T> {\n            /** The content setting. See the description of the individual ContentSetting objects for the possible values. */\n            setting: T;\n        }\n\n        interface ContentSettingSetParams<T> {\n            /** The pattern for the primary URL. For details on the format of a pattern, see Content Setting Patterns. */\n            primaryPattern: string;\n            /** The resource identifier for the content type. */\n            resourceIdentifier?: ResourceIdentifier;\n            /** Where to set the setting (default: regular). */\n            scope?: `${Scope}`;\n            /** The pattern for the secondary URL. Defaults to matching all URLs. For details on the format of a pattern, see Content Setting Patterns.*/\n            secondaryPattern?: string;\n            /** The setting applied by this rule. See the description of the individual ContentSetting objects for the possible values. */\n            setting: T;\n        }\n\n        export interface ContentSetting<T extends string> {\n            /**\n             * Clear all content setting rules set by this extension.\n             *\n             * Can return its result via Promise since Chrome 96.\n             */\n            clear(details: ContentSettingClearParams): Promise<void>;\n            clear(details: ContentSettingClearParams, callback: () => void): void;\n\n            /**\n             * Gets the current content setting for a given pair of URLs.\n             *\n             * Can return its result via Promise since Chrome 96.\n             */\n            get(details: ContentSettingGetParams): Promise<ContentSettingGetResult<T>>;\n            get(details: ContentSettingGetParams, callback: (details: ContentSettingGetResult<T>) => void): void;\n\n            /** Can return its result via Promise since Chrome 96. */\n            getResourceIdentifiers(): Promise<ResourceIdentifier[] | undefined>;\n            getResourceIdentifiers(callback: (resourceIdentifiers?: ResourceIdentifier[]) => void): void;\n\n            /**\n             * Applies a new content setting rule.\n             *\n             * Can return its result via Promise since Chrome 96.\n             */\n            set(details: ContentSettingSetParams<T>): Promise<void>;\n            set(details: ContentSettingSetParams<T>, callback: () => void): void;\n        }\n\n        /** @since Chrome 44 */\n        export enum CookiesContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            SESSION_ONLY = \"session_only\",\n        }\n\n        /** @since Chrome 44 */\n        export enum FullscreenContentSetting {\n            ALLOW = \"allow\",\n        }\n\n        /** @since Chrome 44 */\n        export enum ImagesContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n        }\n\n        /** @since Chrome 44 */\n        export enum JavascriptContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n        }\n\n        /** @since Chrome 44 */\n        export enum LocationContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            ASK = \"ask\",\n        }\n\n        /** @since Chrome 46 */\n        export enum MicrophoneContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            ASK = \"ask\",\n        }\n\n        /** @since Chrome 44 */\n        export enum MouselockContentSetting {\n            ALLOW = \"allow\",\n        }\n\n        /** @since Chrome 44 */\n        export enum MultipleAutomaticDownloadsContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            ASK = \"ask\",\n        }\n\n        /** @since Chrome 44 */\n        export enum NotificationsContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n            ASK = \"ask\",\n        }\n\n        /** @since Chrome 44 */\n        export enum PluginsContentSetting {\n            BLOCK = \"block\",\n        }\n\n        /** @since Chrome 44 */\n        export enum PopupsContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n        }\n\n        /** @since Chrome 44 */\n        export enum PpapiBrokerContentSetting {\n            BLOCK = \"block\",\n        }\n\n        /** The only content type using resource identifiers is contentSettings.plugins. For more information, see Resource Identifiers. */\n        export interface ResourceIdentifier {\n            /** A human readable description of the resource.  */\n            description?: string;\n            /** The resource identifier for the given content type. */\n            id: string;\n        }\n\n        /**\n         * The scope of the ContentSetting. One of\n         *\n         * `regular`: setting for regular profile (which is inherited by the incognito profile if not overridden elsewhere),\n         *\n         * `incognito_session_only`: setting for incognito profile that can only be set during an incognito session and is deleted when the incognito session ends (overrides regular settings).\n         * @since Chrome 44\n         */\n        export enum Scope {\n            REGULAR = \"regular\",\n            INCOGNITO_SESSION_ONLY = \"incognito_session_only\",\n        }\n\n        /** @since Chrome 141 */\n        export enum SoundContentSetting {\n            ALLOW = \"allow\",\n            BLOCK = \"block\",\n        }\n\n        /**\n         * Whether to allow sites to download multiple files automatically. One of\n         *\n         * `allow`: Allow sites to download multiple files automatically,\n         *\n         * `block`: Don't allow sites to download multiple files automatically,\n         *\n         * `ask`: Ask when a site wants to download files automatically after the first file.\n         *\n         * Default is `ask`.\n         *\n         * The primary URL is the URL of the top-level frame. The secondary URL is not used.\n         */\n        export const automaticDownloads: ContentSetting<`${MultipleAutomaticDownloadsContentSetting}`>;\n\n        /**\n         * Whether to allow sites to use the Private State Tokens API. One of\n         *\n         * `allow`: Allow sites to use the Private State Tokens API,\n         *\n         * `block`: Block sites from using the Private State Tokens API.\n         *\n         * Default is `allow`.\n         *\n         * When calling `set()`, the primary URL pattern must be `<all_urls>`. The secondary URL is not used.\n         * @since Chrome 113\n         */\n        export const autoVerify: ContentSetting<`${AutoVerifyContentSetting}`>;\n\n        /**\n         * Whether to allow sites to access the camera. One of\n         *\n         * `allow`: Allow sites to access the camera,\n         *\n         * `block`: Don't allow sites to access the camera,\n         *\n         * `ask`: Ask when a site wants to access the camera.\n         *\n         * Default is `ask`.\n         *\n         * The primary URL is the URL of the document which requested camera access. The secondary URL is not used. NOTE: The 'allow' setting is not valid if both patterns are '<all\\_urls>'.\n         * @since Chrome 46\n         */\n        export const camera: ContentSetting<`${CameraContentSetting}`>;\n\n        /**\n         * Whether to allow sites to access the clipboard via advanced capabilities of the Async Clipboard API. \"Advanced\" capabilities include anything besides writing built-in formats after a user gesture, i.e. the ability to read, the ability to write custom formats, and the ability to write without a user gesture. One of\n         *\n         * `allow`: Allow sites to use advanced clipboard capabilities,\n         *\n         * `block`: Don't allow sites to use advanced clipboard capabilties,\n         *\n         * `ask`: Ask when a site wants to use advanced clipboard capabilities.\n         *\n         * Default is `ask`.\n         *\n         * The primary URL is the URL of the document which requested clipboard access. The secondary URL is not used.\n         * @since Chrome 121\n         */\n        export const clipboard: ContentSetting<`${ClipboardContentSetting}`>;\n\n        /**\n         * Whether to allow cookies and other local data to be set by websites. One of\n         *\n         * `allow`: Accept cookies,\n         *\n         * `block`: Block cookies,\n         *\n         * `session_only`: Accept cookies only for the current session.\n         *\n         * Default is `allow`.\n         *\n         * The primary URL is the URL representing the cookie origin. The secondary URL is the URL of the top-level frame.\n         */\n        export const cookies: ContentSetting<`${CookiesContentSetting}`>;\n\n        /** @deprecated No longer has any effect. Fullscreen permission is now automatically granted for all sites. Value is always `allow`. */\n        export const fullscreen: ContentSetting<`${FullscreenContentSetting}`>;\n\n        /**\n         * Whether to show images. One of\n         *\n         * `allow`: Show images,\n         *\n         * `block`: Don't show images.\n         *\n         * Default is `allow`.\n         *\n         * The primary URL is the URL of the top-level frame. The secondary URL is the URL of the image.\n         */\n        export const images: ContentSetting<`${ImagesContentSetting}`>;\n\n        /**\n         * Whether to run JavaScript. One of\n         *\n         * `allow`: Run JavaScript,\n         *\n         * `block`: Don't run JavaScript.\n         *\n         * Default is `allow`.\n         *\n         * The primary URL is the URL of the top-level frame. The secondary URL is not used.\n         */\n        export const javascript: ContentSetting<`${JavascriptContentSetting}`>;\n\n        /**\n         * Whether to allow Geolocation. One of\n         *\n         * `allow`: Allow sites to track your physical location,\n         *\n         * `block`: Don't allow sites to track your physical location,\n         *\n         * `ask`: Ask before allowing sites to track your physical location.\n         *\n         * Default is `ask`.\n         *\n         * The primary URL is the URL of the document which requested location data. The secondary URL is the URL of the top-level frame (which may or may not differ from the requesting URL).\n         */\n        export const location: ContentSetting<`${LocationContentSetting}`>;\n\n        /**\n         * Whether to allow sites to access the microphone. One of\n         *\n         * `allow`: Allow sites to access the microphone,\n         *\n         * `block`: Don't allow sites to access the microphone,\n         *\n         * `ask`: Ask when a site wants to access the microphone.\n         *\n         * Default is `ask`.\n         *\n         * The primary URL is the URL of the document which requested microphone access. The secondary URL is not used. NOTE: The 'allow' setting is not valid if both patterns are '<all\\_urls>'.\n         * @since Chrome 46\n         */\n        export const microphone: ContentSetting<`${MicrophoneContentSetting}`>;\n\n        /** @deprecated No longer has any effect. Mouse lock permission is now automatically granted for all sites. Value is always `allow`. */\n        export const mouselock: ContentSetting<`${MouselockContentSetting}`>;\n\n        /**\n         * Whether to allow sites to show desktop notifications. One of\n         *\n         * `allow`: Allow sites to show desktop notifications,\n         *\n         * `block`: Don't allow sites to show desktop notifications,\n         *\n         * `ask`: Ask when a site wants to show desktop notifications.\n         *\n         * Default is `ask`.\n         *\n         * The primary URL is the URL of the document which wants to show the notification. The secondary URL is not used.\n         */\n        export const notifications: ContentSetting<`${NotificationsContentSetting}`>;\n\n        /** @deprecated With Flash support removed in Chrome 88, this permission no longer has any effect. Value is always `block`. Calls to `set()` and `clear()` will be ignored. */\n        export const plugins: ContentSetting<`${PluginsContentSetting}`>;\n\n        /**\n         * Whether to allow sites to show pop-ups. One of\n         *\n         * `allow`: Allow sites to show pop-ups,\n         *\n         * `block`: Don't allow sites to show pop-ups.\n         *\n         * Default is `block`.\n         *\n         * The primary URL is the URL of the top-level frame. The secondary URL is not used.\n         */\n        export const popups: ContentSetting<`${PopupsContentSetting}`>;\n\n        /** @deprecated Previously, controlled whether to allow sites to run plugins unsandboxed, however, with the Flash broker process removed in Chrome 88, this permission no longer has any effect. Value is always `block`. Calls to `set()` and `clear()` will be ignored. */\n        export const unsandboxedPlugins: ContentSetting<`${PpapiBrokerContentSetting}`>;\n    }\n\n    ////////////////////\n    // Context Menus\n    ////////////////////\n    /**\n     * Use the `Browser.contextMenus` API to add items to Google Chrome's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.\n     *\n     * Permissions: \"contextMenus\"\n     */\n    export namespace contextMenus {\n        /**\n         * The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'launcher'. The 'launcher' context is only supported by apps and is used to add menu items to the context menu that appears when clicking the app icon in the launcher/taskbar/dock/etc. Different platforms might put limitations on what is actually supported in a launcher context menu.\n         * @since Chrome 44\n         */\n        export enum ContextType {\n            ALL = \"all\",\n            PAGE = \"page\",\n            FRAME = \"frame\",\n            SELECTION = \"selection\",\n            LINK = \"link\",\n            EDITABLE = \"editable\",\n            IMAGE = \"image\",\n            VIDEO = \"video\",\n            AUDIO = \"audio\",\n            LAUNCHER = \"launcher\",\n            BROWSER_ACTION = \"browser_action\",\n            PAGE_ACTION = \"page_action\",\n            ACTION = \"action\",\n        }\n\n        /**\n         * Properties of the new context menu item.\n         * @since Chrome 123\n         */\n        export interface CreateProperties {\n            /** The initial state of a checkbox or radio button: `true` for selected, `false` for unselected. Only one radio button can be selected at a time in a given group. */\n            checked?: boolean;\n            /** List of contexts this menu item will appear in. Defaults to `['page']`. */\n            contexts?: [`${ContextType}`, ...`${ContextType}`[]];\n            /** Restricts the item to apply only to documents or frames whose URL matches one of the given patterns. For details on pattern formats, see Match Patterns.  */\n            documentUrlPatterns?: string[];\n            /**  Whether this context menu item is enabled or disabled. Defaults to `true`. */\n            enabled?: boolean;\n            /** The unique ID to assign to this item. Mandatory for event pages. Cannot be the same as another ID for this extension. */\n            id?: string;\n            /**  The ID of a parent menu item; this makes the item a child of a previously added item. */\n            parentId?: number | string;\n            /**  Similar to `documentUrlPatterns`, filters based on the `src` attribute of `img`, `audio`, and `video` tags and the `href` attribute of `a` tags. */\n            targetUrlPatterns?: string[];\n            /** The text to display in the item; this is _required_ unless `type` is `separator`. When the context is `selection`, use `%s` within the string to show the selected text. For example, if this parameter's value is \"Translate '%s' to Pig Latin\" and the user selects the word \"cool\", the context menu item for the selection is \"Translate 'cool' to Pig Latin\". */\n            title?: string;\n            /** The type of menu item. Defaults to `normal`. */\n            type?: `${ItemType}`;\n            /** Whether the item is visible in the menu. */\n            visible?: boolean;\n            /**\n             * A function that is called back when the menu item is clicked. This is not available inside of a service worker; instead, you should register a listener for {@link contextMenus.onClicked}.\n             * @param info Information about the item clicked and the context where the click happened.\n             * @param tab The details of the tab where the click took place. This parameter is not present for platform apps.\n             */\n            onclick?: (\n                info: OnClickData,\n                tab: tabs.Tab,\n            ) => void;\n        }\n\n        /**\n         * The type of menu item.\n         * @since Chrome 44\n         */\n        export enum ItemType {\n            NORMAL = \"normal\",\n            CHECKBOX = \"checkbox\",\n            RADIO = \"radio\",\n            SEPARATOR = \"separator\",\n        }\n\n        /** Information sent when a context menu item is clicked. */\n        export interface OnClickData {\n            /** A flag indicating the state of a checkbox or radio item after it is clicked. */\n            checked?: boolean;\n            /**  A flag indicating whether the element is editable (text input, textarea, etc.). */\n            editable: boolean;\n            /**\n             * The ID of the frame of the element where the context menu was clicked, if it was in a frame.\n             * @since Chrome 51\n             */\n            frameId?: number;\n            /** The URL of the frame of the element where the context menu was clicked, if it was in a frame. */\n            frameUrl?: string;\n            /** If the element is a link, the URL it points to. */\n            linkUrl?: string;\n            /** One of 'image', 'video', or 'audio' if the context menu was activated on one of these types of elements. */\n            mediaType?: `${ContextType.IMAGE}` | `${ContextType.VIDEO}` | `${ContextType.AUDIO}`;\n            /** The ID of the menu item that was clicked. */\n            menuItemId: number | string;\n            /** The URL of the page where the menu item was clicked. This property is not set if the click occurred in a context where there is no current page, such as in a launcher context menu. */\n            pageUrl?: string;\n            /** The parent ID, if any, for the item clicked.*/\n            parentMenuItemId?: number | string;\n            /** The text for the context selection, if any. */\n            selectionText?: string | undefined;\n            /** Will be present for elements with a 'src' URL. */\n            srcUrl?: string | undefined;\n            /** A flag indicating the state of a checkbox or radio item before it was clicked. */\n            wasChecked?: boolean | undefined;\n        }\n\n        /** The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored. */\n        export const ACTION_MENU_TOP_LEVEL_LIMIT: 6;\n\n        /**\n         * Creates a new context menu item. If an error occurs during creation, it may not be detected until the creation callback fires; details will be in {@link Browser.runtime.lastError}.\n         * @return The ID of the newly created item.\n         */\n        export function create(createProperties: CreateProperties, callback?: () => void): number | string;\n\n        /**\n         * Removes a context menu item.\n         * @param menuItemId The ID of the context menu item to remove.\n         *\n         * Can return its result via Promise since Chrome 123.\n         */\n        export function remove(menuItemId: string | number): Promise<void>;\n        export function remove(menuItemId: string | number, callback: () => void): void;\n\n        /**\n         * Removes all context menu items added by this extension.\n         *\n         * Can return its result via Promise since Chrome 123.\n         */\n        export function removeAll(): Promise<void>;\n        export function removeAll(callback: () => void): void;\n\n        /**\n         * Updates a previously created context menu item.\n         * @param id The ID of the item to update.\n         * @param updateProperties The properties to update. Accepts the same values as the {@link contextMenus.create} function.\n         *\n         * Can return its result via Promise since Chrome 123.\n         */\n        export function update(id: string | number, updateProperties: Omit<CreateProperties, \"id\">): Promise<void>;\n        export function update(\n            id: string | number,\n            updateProperties: Omit<CreateProperties, \"id\">,\n            callback: () => void,\n        ): void;\n\n        /** Fired when a context menu item is clicked. */\n        export const onClicked: events.Event<(info: OnClickData, tab?: tabs.Tab) => void>;\n    }\n\n    ////////////////////\n    // Cookies\n    ////////////////////\n    /**\n     * Use the `Browser.cookies` API to query and modify cookies, and to be notified when they change.\n     *\n     * Permissions: \"cookies\"\n     *\n     * Manifest: \"host_permissions\"\n     */\n    export namespace cookies {\n        /** A cookie's 'SameSite' state (https://tools.ietf.org/html/draft-west-first-party-cookies). 'no_restriction' corresponds to a cookie set with 'SameSite=None', 'lax' to 'SameSite=Lax', and 'strict' to 'SameSite=Strict'. 'unspecified' corresponds to a cookie set without the SameSite attribute. **/\n        export enum SameSiteStatus {\n            NO_RESTRICTION = \"no_restriction\",\n            LAX = \"lax\",\n            STRICT = \"strict\",\n            UNSPECIFIED = \"unspecified\",\n        }\n\n        /** Represents information about an HTTP cookie. */\n        export interface Cookie {\n            /** The domain of the cookie (e.g. \"www.google.com\", \"example.com\"). */\n            domain: string;\n            /** The name of the cookie. */\n            name: string;\n            /**\n             * The partition key for reading or modifying cookies with the Partitioned attribute.\n             * @since Chrome 119\n             */\n            partitionKey?: CookiePartitionKey;\n            /** The ID of the cookie store containing this cookie, as provided in getAllCookieStores(). */\n            storeId: string;\n            /** The value of the cookie. */\n            value: string;\n            /** True if the cookie is a session cookie, as opposed to a persistent cookie with an expiration date. */\n            session: boolean;\n            /** True if the cookie is a host-only cookie (i.e. a request's host must exactly match the domain of the cookie). */\n            hostOnly: boolean;\n            /** The expiration date of the cookie as the number of seconds since the UNIX epoch. Not provided for session cookies. */\n            expirationDate?: number;\n            /** The path of the cookie. */\n            path: string;\n            /** True if the cookie is marked as HttpOnly (i.e. the cookie is inaccessible to client-side scripts). */\n            httpOnly: boolean;\n            /** True if the cookie is marked as Secure (i.e. its scope is limited to secure channels, typically HTTPS). */\n            secure: boolean;\n            /**\n             * The cookie's same-site status (i.e. whether the cookie is sent with cross-site requests).\n             * @since Chrome 51\n             */\n            sameSite: `${SameSiteStatus}`;\n        }\n\n        /**\n         * Represents a partitioned cookie's partition key.\n         * @since Chrome 119\n         */\n        export interface CookiePartitionKey {\n            /**\n             * Indicates if the cookie was set in a cross-cross site context. This prevents a top-level site embedded in a cross-site context from accessing cookies set by the top-level site in a same-site context.\n             * @since Chrome 130\n             */\n            hasCrossSiteAncestor?: boolean | undefined;\n            /** The top-level site the partitioned cookie is available in. */\n            topLevelSite?: string | undefined;\n        }\n\n        /** Represents a cookie store in the browser. An incognito mode window, for instance, uses a separate cookie store from a non-incognito window. */\n        export interface CookieStore {\n            /** The unique identifier for the cookie store. */\n            id: string;\n            /** Identifiers of all the browser tabs that share this cookie store. */\n            tabIds: number[];\n        }\n\n        export interface GetAllDetails {\n            /** Restricts the retrieved cookies to those whose domains match or are subdomains of this one. */\n            domain?: string | undefined;\n            /** Filters the cookies by name. */\n            name?: string | undefined;\n            /**\n             * The partition key for reading or modifying cookies with the Partitioned attribute.\n             * @since Chrome 119\n             */\n            partitionKey?: CookiePartitionKey | undefined;\n            /** Restricts the retrieved cookies to those that would match the given URL. */\n            url?: string | undefined;\n            /** The cookie store to retrieve cookies from. If omitted, the current execution context's cookie store will be used. */\n            storeId?: string | undefined;\n            /** Filters out session vs. persistent cookies. */\n            session?: boolean | undefined;\n            /** Restricts the retrieved cookies to those whose path exactly matches this string. */\n            path?: string | undefined;\n            /** Filters the cookies by their Secure property. */\n            secure?: boolean | undefined;\n        }\n\n        export interface SetDetails {\n            /** The domain of the cookie. If omitted, the cookie becomes a host-only cookie. */\n            domain?: string | undefined;\n            /** The name of the cookie. Empty by default if omitted. */\n            name?: string | undefined;\n            /**\n             * The partition key for reading or modifying cookies with the Partitioned attribute.\n             * @since Chrome 119\n             */\n            partitionKey?: CookiePartitionKey | undefined;\n            /** The request-URI to associate with the setting of the cookie. This value can affect the default domain and path values of the created cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail. */\n            url: string;\n            /** The ID of the cookie store in which to set the cookie. By default, the cookie is set in the current execution context's cookie store. */\n            storeId?: string | undefined;\n            /** The value of the cookie. Empty by default if omitted. */\n            value?: string | undefined;\n            /** The expiration date of the cookie as the number of seconds since the UNIX epoch. If omitted, the cookie becomes a session cookie. */\n            expirationDate?: number | undefined;\n            /** The path of the cookie. Defaults to the path portion of the url parameter. */\n            path?: string | undefined;\n            /** Whether the cookie should be marked as HttpOnly. Defaults to false. */\n            httpOnly?: boolean | undefined;\n            /** Whether the cookie should be marked as Secure. Defaults to false. */\n            secure?: boolean | undefined;\n            /**\n             * The cookie's same-site status. Defaults to \"unspecified\", i.e., if omitted, the cookie is set without specifying a SameSite attribute.\n             * @since Chrome 51\n             */\n            sameSite?: `${SameSiteStatus}` | undefined;\n        }\n\n        /**\n         * Details to identify the cookie.\n         * @since Chrome 88\n         */\n        export interface CookieDetails {\n            /** The name of the cookie to access. */\n            name: string;\n            /**\n             * The partition key for reading or modifying cookies with the Partitioned attribute.\n             * @since Chrome 119\n             */\n            partitionKey?: CookiePartitionKey | undefined;\n            /** The ID of the cookie store in which to look for the cookie. By default, the current execution context's cookie store will be used. */\n            storeId?: string | undefined;\n            /** The URL with which the cookie to access is associated. This argument may be a full URL, in which case any data following the URL path (e.g. the query string) is simply ignored. If host permissions for this URL are not specified in the manifest file, the API call will fail. */\n            url: string;\n        }\n\n        export interface CookieChangeInfo {\n            /** Information about the cookie that was set or removed. */\n            cookie: Cookie;\n            /** True if a cookie was removed. */\n            removed: boolean;\n            /** The underlying reason behind the cookie's change. */\n            cause: `${OnChangedCause}`;\n        }\n\n        /**\n         * Details to identify the frame.\n         * @since Chrome 132\n         */\n        export interface FrameDetails {\n            /** The unique identifier for the document. If the frameId and/or tabId are provided they will be validated to match the document found by provided document ID. */\n            documentId?: string | undefined;\n            /** The unique identifier for the frame within the tab. */\n            frameId?: number | undefined;\n            /* The unique identifier for the tab containing the frame. */\n            tabId?: number | undefined;\n        }\n\n        /**\n         * The underlying reason behind the cookie's change. If a cookie was inserted, or removed via an explicit call to \"Browser.cookies.remove\", \"cause\" will be \"explicit\". If a cookie was automatically removed due to expiry, \"cause\" will be \"expired\". If a cookie was removed due to being overwritten with an already-expired expiration date, \"cause\" will be set to \"expired_overwrite\". If a cookie was automatically removed due to garbage collection, \"cause\" will be \"evicted\". If a cookie was automatically removed due to a \"set\" call that overwrote it, \"cause\" will be \"overwrite\". Plan your response accordingly.\n         * @since Chrome 44\n         */\n        export enum OnChangedCause {\n            EVICTED = \"evicted\",\n            EXPIRED = \"expired\",\n            EXPLICIT = \"explicit\",\n            EXPIRED_OVERWRITE = \"expired_overwrite\",\n            OVERWRITE = \"overwrite\",\n        }\n\n        /**\n         * Lists all existing cookie stores.\n         *\n         * Can return its result via Promise in Manifest V3 or later.\n         */\n        export function getAllCookieStores(): Promise<CookieStore[]>;\n        export function getAllCookieStores(callback: (cookieStores: CookieStore[]) => void): void;\n\n        /**\n         * The partition key for the frame indicated.\n         *\n         * Can return its result via Promise in Manifest V3 or later.\n         * @since Chrome 132\n         */\n        export function getPartitionKey(details: FrameDetails): Promise<{ partitionKey: CookiePartitionKey }>;\n        export function getPartitionKey(\n            details: FrameDetails,\n            callback: (details: { partitionKey: CookiePartitionKey }) => void,\n        ): void;\n\n        /**\n         * Retrieves all cookies from a single cookie store that match the given information. The cookies returned will be sorted, with those with the longest path first. If multiple cookies have the same path length, those with the earliest creation time will be first. This method only retrieves cookies for domains that the extension has host permissions to\n         * @param details Information to identify the cookie to remove.\n         *\n         * Can return its result via Promise in Manifest V3 or later.\n         */\n        export function getAll(details: GetAllDetails): Promise<Cookie[]>;\n        export function getAll(details: GetAllDetails, callback: (cookies: Cookie[]) => void): void;\n\n        /**\n         * Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.\n         * @param details Details about the cookie being set.\n         *\n         * Can return its result via Promise in Manifest V3 or later.\n         */\n        export function set(details: SetDetails): Promise<Cookie | null>;\n        export function set(details: SetDetails, callback: (cookie: Cookie | null) => void): void;\n\n        /**\n         * Deletes a cookie by name.\n         *\n         * Can return its result via Promise in Manifest V3 or later.\n         */\n        export function remove(details: CookieDetails): Promise<CookieDetails>;\n        export function remove(details: CookieDetails, callback?: (details: CookieDetails) => void): void;\n\n        /**\n         * Retrieves information about a single cookie. If more than one cookie of the same name exists for the given URL, the one with the longest path will be returned. For cookies with the same path length, the cookie with the earliest creation time will be returned.\n         *\n         * Can return its result via Promise in Manifest V3 or later.\n         */\n        export function get(details: CookieDetails): Promise<Cookie | null>;\n        export function get(details: CookieDetails, callback: (cookie: Cookie | null) => void): void;\n\n        /** Fired when a cookie is set or removed. As a special case, note that updating a cookie's properties is implemented as a two step process: the cookie to be updated is first removed entirely, generating a notification with \"cause\" of \"overwrite\" . Afterwards, a new cookie is written with the updated values, generating a second notification with \"cause\" \"explicit\". */\n        export const onChanged: events.Event<(changeInfo: CookieChangeInfo) => void>;\n    }\n\n    ////////////////////\n    // Debugger\n    ////////////////////\n    /**\n     * The `Browser.debugger` API serves as an alternate transport for Chrome's remote debugging protocol. Use `Browser.debugger` to attach to one or more tabs to instrument network interaction, debug JavaScript, mutate the DOM and CSS, and more. Use the {@link Debuggee} `tabId` to target tabs with `sendCommand` and route events by `tabId` from `onEvent` callbacks.\n     *\n     * Permissions: \"debugger\"\n     */\n    export namespace _debugger {\n        /** Debuggee identifier. Either tabId, extensionId or targetId must be specified */\n        export interface Debuggee {\n            /** The id of the tab which you intend to debug. */\n            tabId?: number;\n            /** The id of the extension which you intend to debug. Attaching to an extension background page is only possible when the `--silent-debugger-extension-api` command-line switch is used. */\n            extensionId?: string;\n            /** The opaque id of the debug target. */\n            targetId?: string;\n        }\n\n        /**\n         * Debugger session identifier. One of tabId, extensionId or targetId must be specified. Additionally, an optional sessionId can be provided. If sessionId is specified for arguments sent from {@link onEvent}, it means the event is coming from a child protocol session within the root debuggee session. If sessionId is specified when passed to {@link sendCommand}, it targets a child protocol session within the root debuggee session.\n         * @since Chrome 125\n         */\n        export interface DebuggerSession {\n            /** The id of the extension which you intend to debug. Attaching to an extension background page is only possible when the `--silent-debugger-extension-api` command-line switch is used.*/\n            extensionId?: string;\n            /** The opaque id of the Chrome DevTools Protocol session. Identifies a child session within the root session identified by tabId, extensionId or targetId. */\n            sessionId?: string;\n            /** The id of the tab which you intend to debug. */\n            tabId?: number;\n            /** The opaque id of the debug target. */\n            targetId?: string;\n        }\n\n        /**\n         * Connection termination reason.\n         * @since Chrome 44\n         */\n        export enum DetachReason {\n            CANCELED_BY_USER = \"canceled_by_user\",\n            TARGET_CLOSED = \"target_closed\",\n        }\n\n        /** Debug target information */\n        export interface TargetInfo {\n            /** Target type. */\n            type: `${TargetInfoType}`;\n            /** Target id. */\n            id: string;\n            /** The tab id, defined if type == 'page'. */\n            tabId?: number;\n            /** The extension id, defined if type = 'background_page'. */\n            extensionId?: string;\n            /** True if debugger is already attached. */\n            attached: boolean;\n            /** Target page title. */\n            title: string;\n            /** Target URL. */\n            url: string;\n            /** Target favicon URL.  */\n            faviconUrl?: string;\n        }\n\n        /**\n         * Target type.\n         * @since Chrome 44\n         */\n        export enum TargetInfoType {\n            BACKGROUND_PAGE = \"background_page\",\n            OTHER = \"other\",\n            PAGE = \"page\",\n            WORKER = \"worker\",\n        }\n\n        /**\n         * Attaches debugger to the given target.\n         * @param target Debugging target to which you want to attach.\n         * @param requiredVersion Required debugging protocol version (\"0.1\"). One can only attach to the debuggee with matching major version and greater or equal minor version. List of the protocol versions can be obtained in the documentation pages.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function attach(target: Debuggee, requiredVersion: string): Promise<void>;\n        export function attach(target: Debuggee, requiredVersion: string, callback: () => void): void;\n\n        /**\n         * Detaches debugger from the given target.\n         * @param target Debugging target from which you want to detach.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function detach(target: Debuggee): Promise<void>;\n        export function detach(target: Debuggee, callback: () => void): void;\n\n        /**\n         * Sends given command to the debugging target.\n         * @param target Debugging target to which you want to send the command.\n         * @param method Method name. Should be one of the methods defined by the remote debugging protocol.\n         * @param commandParams JSON object with request parameters. This object must conform to the remote debugging params scheme for given method.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function sendCommand(\n            target: DebuggerSession,\n            method: string,\n            commandParams?: { [key: string]: unknown },\n        ): Promise<object | undefined>;\n        export function sendCommand(\n            target: DebuggerSession,\n            method: string,\n            commandParams?: { [key: string]: unknown },\n            callback?: (result?: object) => void,\n        ): void;\n\n        /**\n         * Returns the list of available debug targets.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function getTargets(): Promise<TargetInfo[]>;\n        export function getTargets(callback: (result: TargetInfo[]) => void): void;\n\n        /** Fired when browser terminates debugging session for the tab. This happens when either the tab is being closed or Chrome DevTools is being invoked for the attached tab. */\n        export const onDetach: Browser.events.Event<(source: Debuggee, reason: `${DetachReason}`) => void>;\n        /** Fired whenever debugging target issues instrumentation event. */\n        export const onEvent: Browser.events.Event<(source: DebuggerSession, method: string, params?: object) => void>;\n    }\n\n    export { _debugger as debugger };\n\n    ////////////////////\n    // Declarative Content\n    ////////////////////\n    /**\n     * Use the `Browser.declarativeContent` API to take actions depending on the content of a page, without requiring permission to read the page's content.\n     *\n     * Permissions: \"declarativeContent\"\n     */\n    export namespace declarativeContent {\n        interface PageStateMatcherProperties {\n            /** Matches if the conditions of the `UrlFilter` are fulfilled for the top-level URL of the page. */\n            pageUrl?: events.UrlFilter | undefined;\n            /** Matches if all of the CSS selectors in the array match displayed elements in a frame with the same origin as the page's main frame. All selectors in this array must be compound selectors to speed up matching. Note: Listing hundreds of CSS selectors or listing CSS selectors that match hundreds of times per page can slow down web sites. */\n            css?: string[] | undefined;\n            /**\n             * Matches if the bookmarked state of the page is equal to the specified value. Requires the bookmarks permission.\n             * @since Chrome 45\n             */\n            isBookmarked?: boolean | undefined;\n        }\n\n        /** Matches the state of a web page based on various criteria. */\n        export class PageStateMatcher {\n            constructor(arg: PageStateMatcherProperties);\n        }\n\n        export interface RequestContentScriptProperties {\n            /** Whether the content script runs in all frames of the matching page, or in only the top frame. Default is `false`. */\n            allFrames?: boolean | undefined;\n\n            /** Names of CSS files to be injected as a part of the content script. */\n            css?: string[] | undefined;\n\n            /** Names of JavaScript files to be injected as a part of the content script. */\n            js?: string[] | undefined;\n\n            /** Whether to insert the content script on `about:blank` and `about:srcdoc`. Default is `false`. */\n            matchAboutBlank?: boolean | undefined;\n        }\n\n        /** Declarative event action that injects a content script. */\n        export class RequestContentScript {\n            constructor(arg: RequestContentScriptProperties);\n        }\n\n        /**\n         * A declarative event action that sets the extension's toolbar {@link action} to an enabled state while the corresponding conditions are met. This action can be used without host permissions. If the extension has the `activeTab` permission, clicking the page action grants access to the active tab.\n         *\n         * On pages where the conditions are not met the extension's toolbar action will be grey-scale, and clicking it will open the context menu, instead of triggering the action.\n         * @since MV3\n         */\n        export class ShowAction {}\n\n        /**\n         * A declarative event action that sets the extension's {@link pageAction} to an enabled state while the corresponding conditions are met. This action can be used without host permissions, but the extension must have a page action. If the extension has the `activeTab` permission, clicking the page action grants access to the active tab.\n         *\n         * On pages where the conditions are not met the extension's toolbar action will be grey-scale, and clicking it will open the context menu, instead of triggering the action.\n         *\n         * MV2 only\n         */\n        export class ShowPageAction {}\n\n        /**\n         * Declarative event action that sets the n-dip square icon for the extension's {@link pageAction} or {@link browserAction} while the corresponding conditions are met. This action can be used without host permissions, but the extension must have a page or browser action.\n         *\n         * Exactly one of `imageData` or `path` must be specified. Both are dictionaries mapping a number of pixels to an image representation. The image representation in `imageData` is an `ImageData` object; for example, from a `canvas` element, while the image representation in `path` is the path to an image file relative to the extension's manifest. If `scale` screen pixels fit into a device-independent pixel, the `scale * n` icon is used. If that scale is missing, another image is resized to the required size.\n         */\n        export class SetIcon {\n            constructor(options?: { imageData?: ImageData | { [size: string]: ImageData } | undefined });\n        }\n\n        /** Provides the Declarative Event API consisting of {@link events.Event.addRules addRules}, {@link events.Event.removeRules removeRules}, and {@link events.Event.getRules getRules}. */\n        export const onPageChanged: events.Event<() => void>;\n    }\n\n    ////////////////////\n    // Declarative Web Request\n    ////////////////////\n    /**\n     * Use the `Browser.declarativeWebRequest` API to intercept, block, or modify requests in-flight. It is significantly faster than the Browser.webRequest API because you can register rules that are evaluated in the browser rather than the JavaScript engine, which reduces roundtrip latencies and allows higher efficiency.\n     *\n     * Permissions: \"declarativeWebRequest\"\n     *\n     * MV2 only\n     * @deprecated Check out the {@link declarativeNetRequest} API instead\n     */\n    export namespace declarativeWebRequest {\n        /** Filters request headers for various criteria. Multiple criteria are evaluated as a conjunction. */\n        export interface HeaderFilter {\n            /** Matches if the header name is equal to the specified string. */\n            nameEquals?: string | undefined;\n            /** Matches if the header value contains all of the specified strings. */\n            valueContains?: string | string[] | undefined;\n            /** Matches if the header name ends with the specified string. */\n            nameSuffix?: string | undefined;\n            /** Matches if the header value ends with the specified string. */\n            valueSuffix?: string | undefined;\n            /** Matches if the header value starts with the specified string. */\n            valuePrefix?: string | undefined;\n            /** Matches if the header name contains all of the specified strings. */\n            nameContains?: string | string[] | undefined;\n            /** Matches if the header value is equal to the specified string. */\n            valueEquals?: string | undefined;\n            /** Matches if the header name starts with the specified string. */\n            namePrefix?: string | undefined;\n        }\n\n        /** Adds the response header to the response of this web request. As multiple response headers may share the same name, you need to first remove and then add a new response header in order to replace one. */\n        export interface AddResponseHeader {\n            /** HTTP response header name. */\n            name: string;\n            /** HTTP response header value. */\n            value: string;\n        }\n\n        /** Removes one or more cookies of response. Note that it is preferred to use the Cookies API because this is computationally less expensive. */\n        export interface RemoveResponseCookie {\n            /** Filter for cookies that will be removed. All empty entries are ignored. */\n            filter: FilterResponseCookie;\n        }\n\n        /** Removes all response headers of the specified names and values. */\n        export interface RemoveResponseHeader {\n            /** HTTP request header name (case-insensitive). */\n            name: string;\n            /** HTTP request header value (case-insensitive). */\n            value?: string | undefined;\n        }\n\n        /** Matches network events by various criteria. */\n        export interface RequestMatcher {\n            /** Matches if the MIME media type of a response (from the HTTP Content-Type header) is contained in the list. */\n            contentType?: string[] | undefined;\n            /** Matches if the conditions of the UrlFilter are fulfilled for the URL of the request. */\n            url?: events.UrlFilter | undefined;\n            /** Matches if the MIME media type of a response (from the HTTP Content-Type header) is not contained in the list. */\n            excludeContentType?: string[] | undefined;\n            /** Matches if none of the request headers is matched by any of the HeaderFilters. */\n            excludeResponseHeaders?: HeaderFilter[] | undefined;\n            /** Matches if none of the response headers is matched by any of the HeaderFilters. */\n            excludeResponseHeader?: HeaderFilter[] | undefined;\n            /**\n             * Matches if the conditions of the UrlFilter are fulfilled for the 'first party' URL of the request. The 'first party' URL of a request, when present, can be different from the request's target URL, and describes what is considered 'first party' for the sake of third-party checks for cookies.\n             * @deprecated since Chrome 82\n             */\n            firstPartyForCookiesUrl?: events.UrlFilter | undefined;\n            /** Matches if some of the request headers is matched by one of the HeaderFilters. */\n            requestHeaders?: HeaderFilter[] | undefined;\n            /** Matches if the request type of a request is contained in the list. Requests that cannot match any of the types will be filtered out. */\n            resourceType?: `${webRequest.ResourceType}`[] | undefined;\n            /** Matches if some of the response headers is matched by one of the HeaderFilters. */\n            responseHeaders?: HeaderFilter[] | undefined;\n            /** Contains a list of strings describing stages. Allowed values are 'onBeforeRequest', 'onBeforeSendHeaders', 'onHeadersReceived', 'onAuthRequired'. If this attribute is present, then it limits the applicable stages to those listed. Note that the whole condition is only applicable in stages compatible with all attributes. */\n            stages?: `${Stage}`[] | undefined;\n            /**\n             * If set to true, matches requests that are subject to third-party cookie policies. If set to false, matches all other requests.\n             * @deprecated since Chrome 87\n             */\n            thirdPartyForCookies?: boolean | undefined;\n        }\n\n        /** Masks all rules that match the specified criteria. */\n        export interface IgnoreRules {\n            /** If set, rules with the specified tag are ignored. This ignoring is not persisted, it affects only rules and their actions of the same network request stage. Note that rules are executed in descending order of their priorities. This action affects rules of lower priority than the current rule. Rules with the same priority may or may not be ignored. */\n            hasTag?: string | undefined;\n            /** If set, rules with a lower priority than the specified value are ignored. This boundary is not persisted, it affects only rules and their actions of the same network request stage. */\n            lowerPriorityThan?: number | undefined;\n        }\n\n        /** Declarative event action that redirects a network request to an empty document. */\n        // eslint-disable-next-line @typescript-eslint/no-empty-interface\n        export interface RedirectToEmptyDocument {}\n\n        /** Declarative event action that redirects a network request. */\n        export interface RedirectRequest {\n            /** Destination to where the request is redirected. */\n            redirectUrl: string;\n        }\n\n        /** A specification of a cookie in HTTP Responses. */\n        export interface ResponseCookie {\n            /** Value of the Domain cookie attribute. */\n            domain?: string | undefined;\n            /** Name of a cookie. */\n            name?: string | undefined;\n            /** Value of the Expires cookie attribute. */\n            expires?: string | undefined;\n            /** Value of the Max-Age cookie attribute */\n            maxAge?: number | undefined;\n            /** Value of a cookie, may be padded in double-quotes. */\n            value?: string | undefined;\n            /** Value of the Path cookie attribute. */\n            path?: string | undefined;\n            /** Existence of the HttpOnly cookie attribute. */\n            httpOnly?: string | undefined;\n            /** Existence of the Secure cookie attribute. */\n            secure?: string | undefined;\n        }\n\n        /** Adds a cookie to the response or overrides a cookie, in case another cookie of the same name exists already. Note that it is preferred to use the Cookies API because this is computationally less expensive. */\n        export interface AddResponseCookie {\n            cookie: ResponseCookie;\n        }\n\n        /** Edits one or more cookies of response. Note that it is preferred to use the Cookies API because this is computationally less expensive. */\n        export interface EditResponseCookie {\n            /** Filter for cookies that will be modified. All empty entries are ignored. */\n            filter: ResponseCookie;\n            /** Attributes that shall be overridden in cookies that machted the filter. Attributes that are set to an empty string are removed. */\n            modification: ResponseCookie;\n        }\n\n        /** Declarative event action that cancels a network request. */\n        // eslint-disable-next-line @typescript-eslint/no-empty-interface\n        export interface CancelRequest {}\n\n        /** Removes the request header of the specified name. Do not use SetRequestHeader and RemoveRequestHeader with the same header name on the same request. Each request header name occurs only once in each request. */\n        export interface RemoveRequestHeader {\n            /** HTTP request header name (case-insensitive). */\n            name: string;\n        }\n\n        /** Edits one or more cookies of request. Note that it is preferred to use the Cookies API because this is computationally less expensive. */\n        export interface EditRequestCookie {\n            /** Filter for cookies that will be modified. All empty entries are ignored. */\n            filter: RequestCookie;\n            /** Attributes that shall be overridden in cookies that machted the filter. Attributes that are set to an empty string are removed. */\n            modification: RequestCookie;\n        }\n\n        /** A filter of a cookie in HTTP Responses. */\n        export interface FilterResponseCookie {\n            /** Inclusive lower bound on the cookie lifetime (specified in seconds after current time). Only cookies whose expiration date-time is set to 'now + ageLowerBound' or later fulfill this criterion. Session cookies do not meet the criterion of this filter. The cookie lifetime is calculated from either 'max-age' or 'expires' cookie attributes. If both are specified, 'max-age' is used to calculate the cookie lifetime. */\n            ageLowerBound?: number | undefined;\n            /** Inclusive upper bound on the cookie lifetime (specified in seconds after current time). Only cookies whose expiration date-time is in the interval [now, now + ageUpperBound] fulfill this criterion. Session cookies and cookies whose expiration date-time is in the past do not meet the criterion of this filter. The cookie lifetime is calculated from either 'max-age' or 'expires' cookie attributes. If both are specified, 'max-age' is used to calculate the cookie lifetime. */\n            ageUpperBound?: number | undefined;\n            /** Value of the Domain cookie attribute. */\n            domain?: string | undefined;\n            /** Value of the Expires cookie attribute. */\n            expires?: string | undefined;\n            /** Existence of the HttpOnly cookie attribute. */\n            httpOnly?: string | undefined;\n            /** Value of the Max-Age cookie attribute */\n            maxAge?: number | undefined;\n            /** Name of a cookie. */\n            name?: string | undefined;\n            /** Value of the Path cookie attribute. */\n            path?: string | undefined;\n            /** Existence of the Secure cookie attribute. */\n            secure?: string | undefined;\n            /** Filters session cookies. Session cookies have no lifetime specified in any of 'max-age' or 'expires' attributes. */\n            session?: boolean | undefined;\n            /** Value of a cookie, may be padded in double-quotes. */\n            value?: string | undefined;\n        }\n\n        /** Sets the request header of the specified name to the specified value. If a header with the specified name did not exist before, a new one is created. Header name comparison is always case-insensitive. Each request header name occurs only once in each request. */\n        export interface SetRequestHeader {\n            /** HTTP request header name. */\n            name: string;\n            /** HTTP request header value. */\n            value: string;\n        }\n\n        /** A filter or specification of a cookie in HTTP Requests. */\n        export interface RequestCookie {\n            /** Name of a cookie. */\n            name?: string | undefined;\n            /** Value of a cookie, may be padded in double-quotes. */\n            value?: string | undefined;\n        }\n\n        /** Redirects a request by applying a regular expression on the URL. The regular expressions use the RE2 syntax. */\n        export interface RedirectByRegEx {\n            /** Destination pattern. */\n            to: string;\n            /** A match pattern that may contain capture groups. Capture groups are referenced in the Perl syntax ($1, $2, ...) instead of the RE2 syntax (\\1, \\2, ...) in order to be closer to JavaScript Regular Expressions. */\n            from: string;\n        }\n\n        /** Declarative event action that redirects a network request to a transparent image. */\n        // eslint-disable-next-line @typescript-eslint/no-empty-interface\n        export interface RedirectToTransparentImage {}\n\n        /** Adds a cookie to the request or overrides a cookie, in case another cookie of the same name exists already. Note that it is preferred to use the Cookies API because this is computationally less expensive. */\n        export interface AddRequestCookie {\n            cookie: RequestCookie;\n        }\n\n        /** Removes one or more cookies of request. Note that it is preferred to use the Cookies API because this is computationally less expensive. */\n        export interface RemoveRequestCookie {\n            /** Filter for cookies that will be removed. All empty entries are ignored. */\n            filter: RequestCookie;\n        }\n\n        export enum Stage {\n            ON_AUTH_REQUIRED = \"onAuthRequired\",\n            ON_BEFORE_REQUEST = \"onBeforeRequest\",\n            ON_BEFORE_SEND_HEADERS = \"onBeforeSendHeaders\",\n            ON_HEADERS_RECEIVED = \"onHeadersReceived\",\n        }\n\n        export interface MessageDetails {\n            /** A UUID of the document that made the request. */\n            documentId?: string;\n            /** The lifecycle the document is in. */\n            documentLifecycle: extensionTypes.DocumentLifecycle;\n            /** The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (`type` is `main_frame` or `sub_frame`), `frameId` indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab. */\n            frameId: number;\n            /** The type of frame the navigation occurred in. */\n            frameType: extensionTypes.FrameType;\n            /** The message sent by the calling script. */\n            message: string;\n            /** Standard HTTP method. */\n            method: string;\n            /** A UUID of the parent document owning this frame. This is not set if there is no parent. */\n            parentDocumentId?: string;\n            /** ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists. */\n            parentFrameId: number;\n            /** The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request. */\n            requestId: string;\n            /** The stage of the network request during which the event was triggered. */\n            stage: `${Stage}`;\n            /** The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab. */\n            tabId: number;\n            /** The time when this signal is triggered, in milliseconds since the epoch. */\n            timeStamp: number;\n            /** How the requested resource will be used. */\n            type: `${webRequest.ResourceType}`;\n            url: string;\n        }\n\n        export interface SendMessageToExtension {\n            /** The value that will be passed in the message attribute of the dictionary that is passed to the event handler. */\n            message: string;\n        }\n\n        /** Fired when a message is sent via {@link declarativeWebRequest.SendMessageToExtension} from an action of the declarative web request API. */\n        export const onMessage: events.Event<(details: MessageDetails) => void>;\n\n        /** Provides the Declarative Event API consisting of `addRules`, `removeRules`, and `getRules`. */\n        export const onRequest: events.Event<() => void>;\n    }\n\n    ////////////////////\n    // DesktopCapture\n    ////////////////////\n    /**\n     * The Desktop Capture API captures the content of the screen, individual windows, or individual tabs.\n     *\n     * Permissions: \"desktopCapture\"\n     */\n    export namespace desktopCapture {\n        /** Enum used to define set of desktop media sources used in {@link chooseDesktopMedia}. */\n        export enum DesktopCaptureSourceType {\n            SCREEN = \"screen\",\n            WINDOW = \"window\",\n            TAB = \"tab\",\n            AUDIO = \"audio\",\n        }\n\n        /**\n         * Contains properties that describe the stream.\n         * @since Chrome 57\n         */\n        export interface StreamOptions {\n            /** True if \"audio\" is included in parameter sources, and the end user does not uncheck the \"Share audio\" checkbox. Otherwise false, and in this case, one should not ask for audio stream through getUserMedia call. */\n            canRequestAudioTrack: boolean;\n        }\n        /**\n         * Shows desktop media picker UI with the specified set of sources.\n         * @param sources Set of sources that should be shown to the user. The sources order in the set decides the tab order in the picker.\n         * @param targetTab Optional tab for which the stream is created. If not specified then the resulting stream can be used only by the calling extension. The stream can only be used by frames in the given tab whose security origin matches `tab.url`. The tab's origin must be a secure origin, e.g. HTTPS.\n         * @param callback streamId: An opaque string that can be passed to `getUserMedia()` API to generate media stream that corresponds to the source selected by the user. If user didn't select any source (i.e. canceled the prompt) then the callback is called with an empty `streamId`. The created `streamId` can be used only once and expires after a few seconds when it is not used.\n         * @return An id that can be passed to cancelChooseDesktopMedia() in case the prompt need to be canceled.\n         */\n        export function chooseDesktopMedia(\n            sources: `${DesktopCaptureSourceType}`[],\n            callback: (streamId: string, options: StreamOptions) => void,\n        ): number;\n        export function chooseDesktopMedia(\n            sources: `${DesktopCaptureSourceType}`[],\n            targetTab: tabs.Tab | undefined,\n            callback: (streamId: string, options: StreamOptions) => void,\n        ): number;\n\n        /**\n         * Hides desktop media picker dialog shown by chooseDesktopMedia().\n         * @param desktopMediaRequestId Id returned by chooseDesktopMedia()\n         */\n        export function cancelChooseDesktopMedia(desktopMediaRequestId: number): void;\n    }\n\n    ////////////////////\n    // Dev Tools - Inspected Window\n    ////////////////////\n    /**\n     * Use the `Browser.devtools.inspectedWindow` API to interact with the inspected window: obtain the tab ID for the inspected page, evaluate the code in the context of the inspected window, reload the page, or obtain the list of resources within the page.\n     *\n     * Manifest: \"devtools_page\"\n     */\n    export namespace devtools.inspectedWindow {\n        /** A resource within the inspected page, such as a document, a script, or an image. */\n        export interface Resource {\n            /** The URL of the resource. */\n            url: string;\n            /** Gets the content of the resource. */\n            getContent(\n                callback: (\n                    /** Content of the resource (potentially encoded). */\n                    content: string,\n                    /** Empty if the content is not encoded, encoding name otherwise. Currently, only base64 is supported. */\n                    encoding: string,\n                ) => void,\n            ): void;\n            /**\n             * Sets the content of the resource.\n             * @param content New content of the resource. Only resources with the text type are currently supported.\n             * @param commit True if the user has finished editing the resource, and the new content of the resource should be persisted; false if this is a minor change sent in progress of the user editing the resource.\n             */\n            setContent(\n                content: string,\n                commit: boolean,\n                callback?: (\n                    /** Set to undefined if the resource content was set successfully; describes error otherwise. */\n                    error?: object,\n                ) => void,\n            ): void;\n        }\n\n        export interface ReloadOptions {\n            /** If specified, the string will override the value of the `User-Agent` HTTP header that's sent while loading the resources of the inspected page. The string will also override the value of the `navigator.userAgent` property that's returned to any scripts that are running within the inspected page. */\n            userAgent?: string | undefined;\n            /** When true, the loader will bypass the cache for all inspected page resources loaded before the `load` event is fired. The effect is similar to pressing Ctrl+Shift+R in the inspected window or within the Developer Tools window. */\n            ignoreCache?: boolean | undefined;\n            /** If specified, the script will be injected into every frame of the inspected page immediately upon load, before any of the frame's scripts. The script will not be injected after subsequent reloads—for example, if the user presses Ctrl+R. */\n            injectedScript?: string | undefined;\n        }\n\n        export interface EvaluationExceptionInfo {\n            /** Set if the error occurred on the DevTools side before the expression is evaluated. */\n            isError: boolean;\n            /** Set if the error occurred on the DevTools side before the expression is evaluated. */\n            code: string;\n            /** Set if the error occurred on the DevTools side before the expression is evaluated. */\n            description: string;\n            /** Set if the error occurred on the DevTools side before the expression is evaluated, contains the array of the values that may be substituted into the description string to provide more information about the cause of the error. */\n            details: any[];\n            /** Set if the evaluated code produces an unhandled exception. */\n            isException: boolean;\n            /** Set if the evaluated code produces an unhandled exception. */\n            value: string;\n        }\n\n        /** The ID of the tab being inspected. This ID may be used with {@link Browser.tabs} API. */\n        export const tabId: number;\n\n        /** Reloads the inspected page. */\n        export function reload(reloadOptions?: ReloadOptions): void;\n\n        /**\n         * Evaluates a JavaScript expression in the context of the main frame of the inspected page. The expression must evaluate to a JSON-compliant object, otherwise an exception is thrown. The eval function can report either a DevTools-side error or a JavaScript exception that occurs during evaluation. In either case, the `result` parameter of the callback is `undefined`. In the case of a DevTools-side error, the `isException` parameter is non-null and has `isError` set to true and `code` set to an error code. In the case of a JavaScript error, `isException` is set to true and `value` is set to the string value of thrown object.\n         *\n         * @param expression An expression to evaluate.\n         * @param options The options parameter can contain one or more options.\n         * @param callback A function called when evaluation completes.\n         */\n        export function eval<T = { [key: string]: unknown }>(\n            expression: string,\n            callback?: (result: T, exceptionInfo: EvaluationExceptionInfo) => void,\n        ): void;\n        export function eval<T = { [key: string]: unknown }>(\n            expression: string,\n            options: EvalOptions | undefined,\n            callback?: (result: T, exceptionInfo: EvaluationExceptionInfo) => void,\n        ): void;\n\n        /** Retrieves the list of resources from the inspected page. */\n        export function getResources(callback: (resources: Resource[]) => void): void;\n\n        /** Fired when a new resource is added to the inspected page. */\n        export const onResourceAdded: events.Event<(resource: Resource) => void>;\n\n        /** Fired when a new revision of the resource is committed (e.g. user saves an edited version of the resource in the Developer Tools). */\n        export const onResourceContentCommitted: events.Event<(resource: Resource, content: string) => void>;\n\n        export interface EvalOptions {\n            /** If specified, the expression is evaluated on the iframe whose URL matches the one specified. By default, the expression is evaluated in the top frame of the inspected page. */\n            frameURL?: string | undefined;\n            /** Evaluate the expression in the context of the content script of the calling extension, provided that the content script is already injected into the inspected page. If not, the expression is not evaluated and the callback is invoked with the exception parameter set to an object that has the `isError` field set to true and the `code` field set to `E_NOTFOUND`. */\n            useContentScriptContext?: boolean | undefined;\n            /**\n             * Evaluate the expression in the context of a content script of an extension that matches the specified origin. If given, scriptExecutionContext overrides the 'true' setting on useContentScriptContext.\n             * @since Chrome 107\n             */\n            scriptExecutionContext?: string | undefined;\n        }\n    }\n\n    ////////////////////\n    // Dev Tools - Network\n    ////////////////////\n    /**\n     * Use the `Browser.devtools.network` API to retrieve the information about network requests displayed by the Developer Tools in the Network panel.\n     *\n     * Manifest: \"devtools_page\"\n     */\n    export namespace devtools.network {\n        /** Represents a network request for a document resource (script, image and so on). See HAR Specification for reference. */\n        export interface Request extends HARFormatEntry {\n            /** Returns content of the response body. */\n            getContent(\n                callback: (\n                    /** Content of the response body (potentially encoded). */\n                    content: string,\n                    /** Empty if content is not encoded, encoding name otherwise. Currently, only base64 is supported. */\n                    encoding: string,\n                ) => void,\n            ): void;\n        }\n\n        /** Returns HAR log that contains all known network requests. */\n        export function getHAR(\n            callback: (\n                /** A HAR log. See HAR specification for details. */\n                harLog: HARFormatLog,\n            ) => void,\n        ): void;\n\n        /** Fired when a network request is finished and all request data are available. */\n        export const onRequestFinished: events.Event<(request: Request) => void>;\n\n        /** Fired when the inspected window navigates to a new page. */\n        export const onNavigated: events.Event<(url: string) => void>;\n    }\n\n    ////////////////////\n    // Dev Tools - Performance\n    ////////////////////\n    /**\n     * Use the `Browser.devtools.performance` API to listen to recording status updates in the Performance panel in DevTools.\n     * @since Chrome 129\n     */\n    export namespace devtools.performance {\n        /** Fired when the Performance panel starts recording. */\n        export const onProfilingStarted: events.Event<() => void>;\n        /** Fired when the Performance panel stops recording. */\n        export const onProfilingStopped: events.Event<() => void>;\n    }\n\n    ////////////////////\n    // Dev Tools - Panels\n    ////////////////////\n    /**\n     * Use the `Browser.devtools.panels` API to integrate your extension into Developer Tools window UI: create your own panels, access existing panels, and add sidebars.\n     *\n     * Manifest: \"devtools_page\"\n     */\n    export namespace devtools.panels {\n        /** Represents a panel created by an extension. */\n        export interface ExtensionPanel {\n            /**\n             * Appends a button to the status bar of the panel.\n             * @param iconPath Path to the icon of the button. The file should contain a 64x24-pixel image composed of two 32x24 icons. The left icon is used when the button is inactive; the right icon is displayed when the button is pressed.\n             * @param tooltipText Text shown as a tooltip when user hovers the mouse over the button.\n             * @param disabled Whether the button is disabled.\n             */\n            createStatusBarButton(iconPath: string, tooltipText: string, disabled: boolean): Button;\n            /** Fired when the user switches to the panel. */\n            onShown: events.Event<(window: Window) => void>;\n            /** Fired when the user switches away from the panel. */\n            onHidden: events.Event<() => void>;\n            /** Fired upon a search action (start of a new search, search result navigation, or search being canceled). */\n            onSearch: events.Event<(action: string, queryString?: string) => void>;\n            /**\n             * Shows the panel by activating the corresponding tab.\n             * @since Chrome 140\n             */\n            show(): void;\n        }\n\n        /** A button created by the extension. */\n        export interface Button {\n            /**\n             * Updates the attributes of the button. If some of the arguments are omitted or null, the corresponding attributes are not updated.\n             * @param iconPath Path to the new icon of the button.\n             * @param tooltipText Text shown as a tooltip when user hovers the mouse over the button.\n             * @param disabled Whether the button is disabled.\n             */\n            update(iconPath?: string | null, tooltipText?: string | null, disabled?: boolean | null): void;\n            /** Fired when the button is clicked. */\n            onClicked: events.Event<() => void>;\n        }\n\n        /** Represents the Elements panel. */\n        export interface ElementsPanel {\n            /**\n             * Creates a pane within panel's sidebar.\n             * @param title Text that is displayed in sidebar caption.\n             */\n            createSidebarPane(\n                title: string,\n                callback?: (\n                    /** An ExtensionSidebarPane object for created sidebar pane. */\n                    result: ExtensionSidebarPane,\n                ) => void,\n            ): void;\n            /** Fired when an object is selected in the panel. */\n            onSelectionChanged: events.Event<() => void>;\n        }\n\n        /** Represents the Sources panel. */\n        export interface SourcesPanel {\n            /**\n             * Creates a pane within panel's sidebar.\n             * @param title Text that is displayed in sidebar caption.\n             */\n            createSidebarPane(\n                title: string,\n                callback?: (\n                    /** An ExtensionSidebarPane object for created sidebar pane. */\n                    result: ExtensionSidebarPane,\n                ) => void,\n            ): void;\n            /** Fired when an object is selected in the panel. */\n            onSelectionChanged: events.Event<() => void>;\n        }\n\n        /** A sidebar created by the extension. */\n        export interface ExtensionSidebarPane {\n            /**\n             * Sets the height of the sidebar.\n             * @param height A CSS-like size specification, such as `100px` or `12ex`.\n             */\n            setHeight(height: string): void;\n            /**\n             * Sets an expression that is evaluated within the inspected page. The result is displayed in the sidebar pane.\n             * @param expression An expression to be evaluated in context of the inspected page. JavaScript objects and DOM nodes are displayed in an expandable tree similar to the console/watch.\n             * @param rootTitle An optional title for the root of the expression tree.\n             */\n            setExpression(expression: string, callback?: () => void): void;\n            setExpression(expression: string, rootTitle: string | undefined, callback?: () => void): void;\n            /**\n             * Sets a JSON-compliant object to be displayed in the sidebar pane.\n             * @param jsonObject An object to be displayed in context of the inspected page. Evaluated in the context of the caller (API client).\n             * @param rootTitle An optional title for the root of the expression tree.\n             */\n            setObject(jsonObject: { [key: string]: unknown }, callback?: () => void): void;\n            setObject(\n                jsonObject: { [key: string]: unknown },\n                rootTitle: string | undefined,\n                callback?: () => void,\n            ): void;\n            /**\n             * Sets an HTML page to be displayed in the sidebar pane.\n             * @param path Relative path of an extension page to display within the sidebar.\n             */\n            setPage(path: string): void;\n            /** Fired when the sidebar pane becomes visible as a result of user switching to the panel that hosts it. */\n            onShown: events.Event<(window: Window) => void>;\n            /** Fired when the sidebar pane becomes hidden as a result of the user switching away from the panel that hosts the sidebar pane. */\n            onHidden: events.Event<() => void>;\n        }\n\n        /**\n         * Theme used by DevTools.\n         * @since Chrome 99\n         */\n        export type Theme = \"default\" | \"dark\";\n\n        /** Elements panel. */\n        export const elements: ElementsPanel;\n\n        /** Sources panel. */\n        export const sources: SourcesPanel;\n\n        /**\n         * Creates an extension panel.\n         * @param title Title that is displayed next to the extension icon in the Developer Tools toolbar.\n         * @param iconPath Path of the panel's icon relative to the extension directory.\n         * @param pagePath Path of the panel's HTML page relative to the extension directory.\n         */\n        export function create(\n            title: string,\n            iconPath: string,\n            pagePath: string,\n            callback?: (\n                /** An ExtensionPanel object representing the created panel. */\n                panel: ExtensionPanel,\n            ) => void,\n        ): void;\n\n        /** Specifies the function to be called when the user clicks a resource link in the Developer Tools window. To unset the handler, either call the method with no parameters or pass null as the parameter. */\n        export function setOpenResourceHandler(\n            callback?: (\n                /** A {@link devtools.inspectedWindow.Resource} object for the resource that was clicked. */\n                resource: Browser.devtools.inspectedWindow.Resource,\n                /** Specifies the line number within the resource that was clicked. */\n                lineNumber: number,\n            ) => void,\n        ): void;\n\n        /**\n         * Specifies the function to be called when the current theme changes in DevTools. To unset the handler, either call the method with no parameters or pass `null` as the parameter.\n         * @since Chrome 99\n         */\n        export function setThemeChangeHandler(callback?: (theme: Theme) => void): void;\n\n        /**\n         * Requests DevTools to open a URL in a Developer Tools panel.\n         * @param url The URL of the resource to open.\n         * @param lineNumber Specifies the line number to scroll to when the resource is loaded.\n         * @param columnNumber Specifies the column number to scroll to when the resource is loaded.\n         */\n        export function openResource(url: string, lineNumber: number, callback?: () => void): void;\n        export function openResource(\n            url: string,\n            lineNumber: number,\n            columnNumber: number | undefined,\n            callback?: () => void,\n        ): void;\n\n        /**\n         * The name of the color theme set in user's DevTools settings.\n         * @since Chrome 59\n         */\n        export const themeName: Theme;\n    }\n\n    ////////////////////\n    // Dev Tools - Recorder\n    ////////////////////\n    /**\n     * Use the `Browser.devtools.recorder` API to customize the Recorder panel in DevTools.\n     * @since Chrome 105\n     */\n    export namespace devtools.recorder {\n        /** A plugin interface that the Recorder panel invokes to customize the Recorder panel. */\n        export interface RecorderExtensionPlugin {\n            /**\n             * Allows the extension to implement custom replay functionality.\n             *\n             * @param recording A recording of the user interaction with the page. This should match [Puppeteer's recording schema](https://github.com/puppeteer/replay/blob/main/docs/api/interfaces/Schema.UserFlow.md).\n             * @since Chrome 112\n             */\n            replay?(recording: { [key: string]: unknown }): void;\n\n            /**\n             * Converts a recording from the Recorder panel format into a string.\n             * @param recording A recording of the user interaction with the page. This should match [Puppeteer's recording schema](https://github.com/puppeteer/replay/blob/main/docs/api/interfaces/Schema.UserFlow.md).\n             */\n            stringify?(recording: { [key: string]: unknown }): void;\n\n            /**\n             * Converts a step of the recording from the Recorder panel format into a string.\n             * @param step A step of the recording of a user interaction with the page. This should match [Puppeteer's step schema](https://github.com/puppeteer/replay/blob/main/docs/api/modules/Schema.md#step).\n             */\n            stringifyStep?(step: { [key: string]: unknown }): void;\n        }\n\n        /**\n         * Represents a view created by extension to be embedded inside the Recorder panel.\n         * @since Chrome 112\n         */\n        export interface RecorderView {\n            /** Fired when the view is hidden. */\n            onHidden: events.Event<() => void>;\n            /** Fired when the view is shown. */\n            onShown: events.Event<() => void>;\n            /** Indicates that the extension wants to show this view in the Recorder panel. */\n            show(): void;\n        }\n\n        /**\n         * Creates a view that can handle the replay. This view will be embedded inside the Recorder panel.\n         * @param title Title that is displayed next to the extension icon in the Developer Tools toolbar.\n         * @param pagePath Path of the panel's HTML page relative to the extension directory.\n         * @since Chrome 112\n         */\n        export function createView(title: string, pagePath: string): RecorderView;\n\n        /**\n         * Registers a Recorder extension plugin.\n         * @param plugin An instance implementing the RecorderExtensionPlugin interface.\n         * @param name The name of the plugin.\n         * @param mediaType The media type of the string content that the plugin produces.\n         */\n        export function registerRecorderExtensionPlugin(\n            plugin: RecorderExtensionPlugin,\n            name: string,\n            mediaType: string,\n        ): void;\n    }\n\n    ////////////////////\n    // Document Scan\n    ////////////////////\n    /**\n     * Use the `Browser.documentScan` API to discover and retrieve images from attached document scanners.\n     * The Document Scan API is designed to allow apps and extensions to view the content of paper documents on an attached document scanner.\n     *\n     * Permissions: \"documentScan\"\n     * @platform ChromeOS only\n     * @since Chrome 44\n     */\n    export namespace documentScan {\n        /** @since Chrome 125 */\n        export interface CancelScanResponse<T> {\n            /** Provides the same job handle that was passed to {@link cancelScan}. */\n            job: T;\n            /** The backend's cancel scan result. If the result is `OperationResult.SUCCESS` or `OperationResult.CANCELLED`, the scan has been cancelled and the scanner is ready to start a new scan. If the result is `OperationResult.DEVICE_BUSY` , the scanner is still processing the requested cancellation; the caller should wait a short time and try the request again. Other result values indicate a permanent error that should not be retried. */\n            result: `${OperationResult}`;\n        }\n\n        /** @since Chrome 125 */\n        export interface CloseScannerResponse<T> {\n            /** The same scanner handle as was passed to {@link closeScanner}. */\n            scannerHandle: T;\n            /** The result of closing the scanner. Even if this value is not `SUCCESS`, the handle will be invalid and should not be used for any further operations. */\n            result: `${OperationResult}`;\n        }\n\n        /**\n         * How an option can be changed.\n         * @since Chrome 125\n         */\n        export enum Configurability {\n            /** The option is read-only. */\n            NOT_CONFIGURABLE = \"NOT_CONFIGURABLE\",\n            /** The option can be set in software. */\n            SOFTWARE_CONFIGURABLE = \"SOFTWARE_CONFIGURABLE\",\n            /** The option can be set by the user toggling or pushing a button on the scanner. */\n            HARDWARE_CONFIGURABLE = \"HARDWARE_CONFIGURABLE\",\n        }\n\n        /**\n         * Indicates how the scanner is connected to the computer.\n         * @since Chrome 125\n         */\n        export enum ConnectionType {\n            UNSPECIFIED = \"UNSPECIFIED\",\n            USB = \"USB\",\n            NETWORK = \"NETWORK\",\n        }\n\n        /**\n         * The data type of constraint represented by an {@link OptionConstraint}.\n         * @since Chrome 125\n         */\n        export enum ConstraintType {\n            /** The constraint on a range of `OptionType.INT` values. The `min`, `max`, and `quant` properties of `OptionConstraint` will be `long`, and its `list` property will be unset. */\n            INT_RANGE = \"INT_RANGE\",\n            /** The constraint on a range of `OptionType.FIXED` values. The `min`, `max`, and `quant` properties of `OptionConstraint` will be `double`, and its `list` property will be unset. */\n            FIXED_RANGE = \"FIXED_RANGE\",\n            /** The constraint on a specific list of `OptionType.INT` values. The `OptionConstraint.list` property will contain `long` values, and the other properties will be unset. */\n            INT_LIST = \"INT_LIST\",\n            /** The constraint on a specific list of `OptionType.FIXED` values. The `OptionConstraint.list` property will contain `double` values, and the other properties will be unset. */\n            FIXED_LIST = \"FIXED_LIST\",\n            /** The constraint on a specific list of `OptionType.STRING` values. The `OptionConstraint.list` property will contain `DOMString` values, and the other properties will be unset. */\n            STRING_LIST = \"STRING_LIST\",\n        }\n\n        /** @since Chrome 125 */\n        export interface DeviceFilter {\n            /** Only return scanners that are directly attached to the computer. */\n            local?: boolean;\n            /** Only return scanners that use a secure transport, such as USB or TLS. */\n            secure?: boolean;\n        }\n\n        /** @since Chrome 125 */\n        export interface GetOptionGroupsResponse<T> {\n            /** If `result` is `SUCCESS`, provides a list of option groups in the order supplied by the scanner driver. */\n            groups?: OptionGroup[];\n            /** The result of getting the option groups. If the value of this is `SUCCESS`, the `groups` property will be populated. */\n            result: `${OperationResult}`;\n            /** The same scanner handle as was passed to {@link getOptionGroups}. */\n            scannerHandle: T;\n        }\n\n        /** @since Chrome 125 */\n        export interface GetScannerListResponse {\n            /** The enumeration result. Note that partial results could be returned even if this indicates an error. */\n            result: `${OperationResult}`;\n            /** A possibly-empty list of scanners that match the provided {@link DeviceFilter}. */\n            scanners: ScannerInfo[];\n        }\n\n        /** @since Chrome 125 */\n        export interface OpenScannerResponse<T> {\n            /** If `result` is `SUCCESS`, provides a key-value mapping where the key is a device-specific option and the value is an instance of {@link ScannerOption}. */\n            options?: { [name: string]: unknown };\n            /** The result of opening the scanner. If the value of this is `SUCCESS`, the `scannerHandle` and `options` properties will be populated. */\n            result: `${OperationResult}`;\n            /** If `result` is `SUCCESS`, a handle to the scanner that can be used for further operations. */\n            scannerHandle?: string;\n            /** The scanner ID passed to {@link openScanner}. */\n            scannerId: T;\n        }\n\n        /**\n         * An enum that indicates the result of each operation.\n         * @since Chrome 125\n         */\n        export enum OperationResult {\n            /** An unknown or generic failure occurred. */\n            UNKNOWN = \"UNKNOWN\",\n            /**The operation succeeded. */\n            SUCCESS = \"SUCCESS\",\n            /** The operation is not supported. */\n            UNSUPPORTED = \"UNSUPPORTED\",\n            /** The operation was cancelled. */\n            CANCELLED = \"CANCELLED\",\n            /** The device is busy. */\n            DEVICE_BUSY = \"DEVICE_BUSY\",\n            /** Either the data or an argument passed to the method is not valid. */\n            INVALID = \"INVALID\",\n            /** The supplied value is the wrong data type for the underlying option. */\n            WRONG_TYPE = \"WRONG_TYPE\",\n            /** No more data is available. */\n            EOF = \"EOF\",\n            /** The document feeder is jammed */\n            ADF_JAMMED = \"ADF_JAMMED\",\n            /** The document feeder is empty */\n            ADF_EMPTY = \"ADF_EMPTY\",\n            /** The flatbed cover is open. */\n            COVER_OPEN = \"COVER_OPEN\",\n            /** An error occurred while communicating with the device. */\n            IO_ERROR = \"IO_ERROR\",\n            /** The device requires authentication. */\n            ACCESS_DENIED = \"ACCESS_DENIED\",\n            /** Not enough memory is available on the Chromebook to complete the operation. */\n            NO_MEMORY = \"NO_MEMORY\",\n            /** The device is not reachable. */\n            UNREACHABLE = \"UNREACHABLE\",\n            /** The device is disconnected. */\n            MISSING = \"MISSING\",\n            /** An error has occurred somewhere other than the calling application. */\n            INTERNAL_ERROR = \"INTERNAL_ERROR\",\n        }\n\n        /** @since Chrome 125 */\n        export interface OptionConstraint {\n            list?: string[] | number[];\n            max?: number;\n            min?: number;\n            quant?: number;\n            type: `${ConstraintType}`;\n        }\n\n        /** @since Chrome 125 */\n        export interface OptionGroup {\n            /** An array of option names in driver-provided order. */\n            members: string[];\n            /** Provides a printable title, for example \"Geometry options\". */\n            title: string;\n        }\n\n        /** @since Chrome 125 */\n        export interface OptionSetting {\n            /** Indicates the name of the option to set. */\n            name: string;\n            /** Indicates the data type of the option. The requested data type must match the real data type of the underlying option. */\n            type: `${OptionType}`;\n            /** Indicates the value to set. Leave unset to request automatic setting for options that have `autoSettable` enabled. The data type supplied for `value` must match `type`. */\n            value?: string | number | boolean | number;\n        }\n\n        /**\n         * The data type of an option.\n         * @since Chrome 125\n         */\n        export enum OptionType {\n            /** The option's data type is `unknown`. The value property will be unset. */\n            UNKNOWN = \"UNKNOWN\",\n            /** The `value` property will be one of `true` false. */\n            BOOL = \"BOOL\",\n            /** A signed 32-bit integer. The `value` property will be long or long[], depending on whether the option takes more than one value. */\n            INT = \"INT\",\n            /** A double in the range -32768-32767.9999 with a resolution of 1/65535. The `value` property will be double or double[] depending on whether the option takes more than one value. Double values that can't be exactly represented will be rounded to the available range and precision. */\n            FIXED = \"FIXED\",\n            /** A sequence of any bytes except NUL ('\\0'). The `value` property will be a DOMString. */\n            STRING = \"STRING\",\n            /** An option of this type has no value. Instead, setting an option of this type causes an option-specific side effect in the scanner driver. For example, a button-typed option could be used by a scanner driver to provide a means to select default values or to tell an automatic document feeder to advance to the next sheet of paper. */\n            BUTTON = \"BUTTON\",\n            /** Grouping option. No value. This is included for compatibility, but will not normally be returned in `ScannerOption` values. Use `getOptionGroups()` to retrieve the list of groups with their member options. */\n            GROUP = \"GROUP\",\n        }\n\n        /**\n         * Indicates the data type for {@link ScannerOption.unit}.\n         * @since Chrome 125\n         */\n        export enum OptionUnit {\n            /** The value is a unitless number. For example, it can be a threshold. */\n            UNITLESS = \"UNITLESS\",\n            /** The value is a number of pixels, for example, scan dimensions. */\n            PIXEL = \"PIXEL\",\n            /** The value is the number of bits, for example, color depth. */\n            BIT = \"BIT\",\n            /** The value is measured in millimeters, for example, scan dimensions. */\n            MM = \"MM\",\n            /** The value is measured in dots per inch, for example, resolution. */\n            DPI = \"DPI\",\n            /** The value is a percent, for example, brightness. */\n            PERCENT = \"PERCENT\",\n            /** The value is measured in microseconds, for example, exposure time. */\n            MICROSECOND = \"MICROSECOND\",\n        }\n\n        /** @since Chrome 125 */\n        export interface ReadScanDataResponse<T> {\n            /** If `result` is `SUCCESS`, contains the _next_ chunk of scanned image data. If `result` is `EOF`, contains the _last_ chunk of scanned image data. */\n            data?: ArrayBuffer;\n            /** If `result` is `SUCCESS`, an estimate of how much of the total scan data has been delivered so far, in the range 0 to 100. */\n            estimatedCompletion?: number;\n            /** Provides the job handle passed to {@link readScanData}. */\n            job: T;\n            /** The result of reading data. If its value is `SUCCESS`, then `data` contains the _next_ (possibly zero-length) chunk of image data that is ready for reading. If its value is `EOF`, the `data` contains the _last_ chunk of image data. */\n            result: `${OperationResult}`;\n        }\n\n        /** @since Chrome 125 */\n        export interface ScannerInfo {\n            /** Indicates how the scanner is connected to the computer. */\n            connectionType: `${ConnectionType}`;\n            /** For matching against other `ScannerInfo` entries that point to the same physical device. */\n            deviceUuid: string;\n            /** An array of MIME types that can be requested for returned scans. */\n            imageFormats: string[];\n            /** The scanner manufacturer. */\n            manufacturer: string;\n            /** The scanner model if it is available, or a generic description. */\n            model: string;\n            /** A human-readable name for the scanner to display in the UI. */\n            name: string;\n            /** A human-readable description of the protocol or driver used to access the scanner, such as Mopria, WSD, or epsonds. This is primarily useful for allowing a user to choose between protocols if a device supports multiple protocols. */\n            protocolType: string;\n            /** The ID of a specific scanner. */\n            scannerId: string;\n            /** If true, the scanner connection's transport cannot be intercepted by a passive listener, such as TLS or USB. */\n            secure: boolean;\n        }\n\n        /** @since Chrome 125 */\n        export interface ScannerOption {\n            /** Indicates whether and how the option can be changed. */\n            configurability: `${Configurability}`;\n            /** Defines {@link OptionConstraint} on the current scanner option. */\n            constraint?: OptionConstraint;\n            /** A longer description of the option. */\n            description: string;\n            /** Indicates the option is active and can be set or retrieved. If false, the `value` property will not be set. */\n            isActive: boolean;\n            /** Indicates that the UI should not display this option by default. */\n            isAdvanced: boolean;\n            /** Can be automatically set by the scanner driver. */\n            isAutoSettable: boolean;\n            /** Indicates that this option can be detected from software. */\n            isDetectable: boolean;\n            /** Emulated by the scanner driver if true. */\n            isEmulated: boolean;\n            /** The option name using lowercase ASCII letters, numbers, and dashes. Diacritics are not allowed. */\n            name: string;\n            /** A printable one-line title. */\n            title: string;\n            /** The data type contained in the `value` property, which is needed for setting this option. */\n            type: `${OptionType}`;\n            /** The unit of measurement for this option. */\n            unit: `${OptionUnit}`;\n            /** The current value of the option, if relevant. Note that the data type of this property must match the data type specified in `type`. */\n            value?: string | number | boolean | number[];\n        }\n\n        export interface ScanOptions {\n            /** The number of scanned images allowed. The default is 1. */\n            maxImages?: number;\n            /** The MIME types that are accepted by the caller. */\n            mimeTypes?: string[];\n        }\n\n        export interface ScanResults {\n            /** An array of data image URLs in a form that can be passed as the \"src\" value to an image tag. */\n            dataUrls: string[];\n            /** The MIME type of the `dataUrls`. */\n            mimeType: string;\n        }\n\n        /** @since Chrome 125 */\n        export interface SetOptionResult {\n            /**  Indicates the name of the option that was set. */\n            name: string;\n            /** Indicates the result of setting the option. */\n            result: `${OperationResult}`;\n        }\n\n        /** @since Chrome 125 */\n        export interface SetOptionsResponse<T> {\n            /**\n             * An updated key-value mapping from option names to {@link ScannerOption} values containing the new configuration after attempting to set all supplied options. This has the same structure as the `options` property in {@link OpenScannerResponse}.\n             *\n             * This property will be set even if some options were not set successfully, but will be unset if retrieving the updated configuration fails (for example, if the scanner is disconnected in the middle of scanning).\n             */\n            options?: { [name: string]: unknown };\n            /** An array of results, one each for every passed-in `OptionSetting`. */\n            results: SetOptionResult[];\n            /** Provides the scanner handle passed to {@link setOptions}. */\n            scannerHandle: T;\n        }\n\n        /** @since Chrome 125 */\n        export interface StartScanOptions {\n            /** Specifies the MIME type to return scanned data in. */\n            format: string;\n            /** If a non-zero value is specified, limits the maximum scanned bytes returned in a single {@link readScanData} response to that value. The smallest allowed value is 32768 (32 KB). If this property is not specified, the size of a returned chunk may be as large as the entire scanned image. */\n            maxReadSize?: number;\n        }\n\n        /** @since Chrome 125 */\n        export interface StartScanResponse<T> {\n            /** If `result` is `SUCCESS`, provides a handle that can be used to read scan data or cancel the job. */\n            job?: string;\n            /**  The result of starting a scan. If the value of this is `SUCCESS`, the `job` property will be populated. */\n            result: `${OperationResult}`;\n            /** Provides the same scanner handle that was passed to {@link startScan}. */\n            scannerHandle: T;\n        }\n\n        /**\n         * Cancels a started scan and returns a Promise that resolves with a {@link CancelScanResponse} object. If a callback is used, the object is passed to it instead.\n         * @param job The handle of an active scan job previously returned from a call to {@link startScan}.\n         * @since Chrome 125\n         */\n        export function cancelScan<T = string>(job: T): Promise<CancelScanResponse<T>>;\n        export function cancelScan<T = string>(job: T, callback: (response: CancelScanResponse<T>) => void): void;\n\n        /**\n         * Closes the scanner with the passed in handle and returns a Promise that resolves with a {@link CloseScannerResponse} object. If a callback is used, the object is passed to it instead. Even if the response is not a success, the supplied handle becomes invalid and should not be used for further operations.\n         * @param scannerHandle Specifies the handle of an open scanner that was previously returned from a call to {@link openScanner}.\n         * @since Chrome 125\n         */\n        export function closeScanner<T = string>(scannerHandle: T): Promise<CloseScannerResponse<T>>;\n        export function closeScanner<T = string>(\n            scannerHandle: T,\n            callback: (response: CloseScannerResponse<T>) => void,\n        ): void;\n\n        /**\n         * Gets the group names and member options from a scanner previously opened by {@link openScanner}. This method returns a Promise that resolves with a {@link GetOptionGroupsResponse} object. If a callback is passed to this function, returned data is passed to it instead.\n         * @param scannerHandle The handle of an open scanner returned from a call to {@link openScanner}.\n         * @since Chrome 125\n         */\n        export function getOptionGroups<T = string>(scannerHandle: T): Promise<GetOptionGroupsResponse<T>>;\n        export function getOptionGroups<T = string>(\n            scannerHandle: T,\n            callback: (response: GetOptionGroupsResponse<T>) => void,\n        ): void;\n\n        /**\n         * Gets the list of available scanners and returns a Promise that resolves with a {@link GetScannerListResponse} object. If a callback is passed to this function, returned data is passed to it instead.\n         * @param filter A {@link DeviceFilter} indicating which types of scanners should be returned.\n         * @since Chrome 125\n         */\n        export function getScannerList(filter: DeviceFilter): Promise<GetScannerListResponse>;\n        export function getScannerList(\n            filter: DeviceFilter,\n            callback: (response: GetScannerListResponse) => void,\n        ): void;\n\n        /**\n         * Opens a scanner for exclusive access and returns a Promise that resolves with an {@link OpenScannerResponse} object. If a callback is passed to this function, returned data is passed to it instead.\n         * @param scannerId The ID of a scanner to be opened. This value is one returned from a previous call to {@link getScannerList}.\n         * @since Chrome 125\n         */\n        export function openScanner<T = string>(scannerId: T): Promise<OpenScannerResponse<T>>;\n        export function openScanner<T = string>(\n            scannerId: T,\n            callback: (response: OpenScannerResponse<T>) => void,\n        ): void;\n\n        /**\n         * Reads the next chunk of available image data from an active job handle, and returns a Promise that resolves with a {@link ReadScanDataResponse} object. If a callback is used, the object is passed to it instead.\n         *\n         * **Note:**It is valid for a response result to be `SUCCESS` with a zero-length `data` member. This means the scanner is still working but does not yet have additional data ready. The caller should wait a short time and try again.\n         *\n         * When the scan job completes, the response will have the result value of `EOF`. This response may contain a final non-zero `data` member.\n         * @param job Active job handle previously returned from {@link startScan}.\n         * @since Chrome 125\n         */\n        export function readScanData<T = string>(job: T): Promise<ReadScanDataResponse<T>>;\n        export function readScanData<T = string>(job: T, callback: (response: ReadScanDataResponse<T>) => void): void;\n\n        /**\n         * Performs a document scan and returns a Promise that resolves with a {@link ScanResults} object. If a callback is passed to this function, the returned data is passed to it instead.\n         * @param options An object containing scan parameters.\n         */\n        export function scan(options: ScanOptions): Promise<ScanResults>;\n        export function scan(options: ScanOptions, callback: (result: ScanResults) => void): void;\n\n        /**\n         * Sets options on the specified scanner and returns a Promise that resolves with a {@link SetOptionsResponse} object containing the result of trying to set every value in the order of the passed-in {@link OptionSetting} object. If a callback is used, the object is passed to it instead.\n         * @param scannerHandle The handle of the scanner to set options on. This should be a value previously returned from a call to {@link openScanner}.\n         * @param options A list of `OptionSetting` objects to be applied to the scanner.\n         * @since Chrome 125\n         */\n        export function setOptions<T = string>(\n            scannerHandle: T,\n            options: OptionSetting[],\n        ): Promise<SetOptionsResponse<T>>;\n        export function setOptions<T = string>(\n            scannerHandle: T,\n            options: OptionSetting[],\n            callback: (response: SetOptionsResponse<T>) => void,\n        ): void;\n\n        /**\n         * Starts a scan on the specified scanner and returns a Promise that resolves with a {@link StartScanResponse}. If a callback is used, the object is passed to it instead. If the call was successful, the response includes a job handle that can be used in subsequent calls to read scan data or cancel a scan.\n         * @param scannerHandle The handle of an open scanner. This should be a value previously returned from a call to {@link openScanner}.\n         * @param options A {@link StartScanOptions} object indicating the options to be used for the scan. The `StartScanOptions.format` property must match one of the entries returned in the scanner's `ScannerInfo`.\n         * @since Chrome 125\n         */\n        export function startScan<T = string>(\n            scannerHandle: T,\n            options: StartScanOptions,\n        ): Promise<StartScanResponse<T>>;\n        export function startScan<T = string>(\n            scannerHandle: T,\n            options: StartScanOptions,\n            callback: (response: StartScanResponse<T>) => void,\n        ): void;\n    }\n\n    ////////////////////\n    // DOM\n    ////////////////////\n    /**\n     * Use the `Browser.dom` API to access special DOM APIs for Extensions\n     * @since Chrome 88\n     */\n    export namespace dom {\n        /**\n         * @since Chrome 88\n         * Requests chrome to return the open/closed shadow roots else return null.\n         * @param element reference of HTMLElement.\n         */\n        export function openOrClosedShadowRoot(element: HTMLElement): ShadowRoot | null;\n    }\n\n    ////////////////////\n    // Downloads\n    ////////////////////\n    /**\n     * Use the `Browser.downloads` API to programmatically initiate, monitor, manipulate, and search for downloads.\n     *\n     * Permissions: \"downloads\"\n     */\n    export namespace downloads {\n        export interface HeaderNameValuePair {\n            /** Name of the HTTP header. */\n            name: string;\n            /** Value of the HTTP header. */\n            value: string;\n        }\n\n        export enum FilenameConflictAction {\n            /** To avoid duplication, the filename is changed to include a counter before the filename extension. */\n            UNIQUIFY = \"uniquify\",\n            /** The existing file will be overwritten with the new file. */\n            OVERWRITE = \"overwrite\",\n            /** The user will be prompted with a file chooser dialog. */\n            PROMPT = \"prompt\",\n        }\n\n        export enum HttpMethod {\n            GET = \"GET\",\n            POST = \"POST\",\n        }\n\n        export interface DownloadOptions {\n            /** Post body. */\n            body?: string | undefined;\n            /** Use a file-chooser to allow the user to select a filename regardless of whether `filename` is set or already exists. */\n            saveAs?: boolean | undefined;\n            /** The URL to download. */\n            url: string;\n            /** A file path relative to the Downloads directory to contain the downloaded file, possibly containing subdirectories. Absolute paths, empty paths, and paths containing back-references \"..\" will cause an error. {@link onDeterminingFilename} allows suggesting a filename after the file's MIME type and a tentative filename have been determined. */\n            filename?: string | undefined;\n            /** Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys `name` and either `value` or `binaryValue`, restricted to those allowed by XMLHttpRequest. */\n            headers?: HeaderNameValuePair[] | undefined;\n            /** The HTTP method to use if the URL uses the HTTP[S] protocol. */\n            method?: `${HttpMethod}` | undefined;\n            /** The action to take if `filename` already exists. */\n            conflictAction?: `${FilenameConflictAction}` | undefined;\n        }\n\n        export interface DownloadDelta {\n            /** The id of the {@link DownloadItem} that changed. */\n            id: number;\n            /** The change in `danger`, if any. */\n            danger?: StringDelta | undefined;\n            /** The change in `url`, if any. */\n            url?: StringDelta | undefined;\n            /**\n             * The change in `finalUrl`, if any.\n             * @since Chrome 54\n             */\n            finalUrl?: StringDelta | undefined;\n            /** The change in `totalBytes`, if any. */\n            totalBytes?: DoubleDelta | undefined;\n            /** The change in `filename`, if any. */\n            filename?: StringDelta | undefined;\n            /** The change in `paused`, if any. */\n            paused?: BooleanDelta | undefined;\n            /** The change in `state`, if any. */\n            state?: StringDelta | undefined;\n            /** The change in `mime`, if any. */\n            mime?: StringDelta | undefined;\n            /** The change in `fileSize`, if any. */\n            fileSize?: DoubleDelta | undefined;\n            /** The change in `startTime`, if any. */\n            startTime?: StringDelta | undefined;\n            /** The change in `error`, if any. */\n            error?: StringDelta | undefined;\n            /** The change in `endTime`, if any. */\n            endTime?: StringDelta | undefined;\n            /** The change in `canResume`, if any. */\n            canResume?: BooleanDelta | undefined;\n            /** The change in `exists`, if any. */\n            exists?: BooleanDelta | undefined;\n        }\n\n        export interface BooleanDelta {\n            current?: boolean | undefined;\n            previous?: boolean | undefined;\n        }\n\n        export interface DoubleDelta {\n            current?: number | undefined;\n            previous?: number | undefined;\n        }\n\n        export interface StringDelta {\n            current?: string | undefined;\n            previous?: string | undefined;\n        }\n\n        export enum InterruptReason {\n            FILE_FAILED = \"FILE_FAILED\",\n            FILE_ACCESS_DENIED = \"FILE_ACCESS_DENIED\",\n            FILE_NO_SPACE = \"FILE_NO_SPACE\",\n            FILE_NAME_TOO_LONG = \"FILE_NAME_TOO_LONG\",\n            FILE_TOO_LARGE = \"FILE_TOO_LARGE\",\n            FILE_VIRUS_INFECTED = \"FILE_VIRUS_INFECTED\",\n            FILE_TRANSIENT_ERROR = \"FILE_TRANSIENT_ERROR\",\n            FILE_BLOCKED = \"FILE_BLOCKED\",\n            FILE_SECURITY_CHECK_FAILED = \"FILE_SECURITY_CHECK_FAILED\",\n            FILE_TOO_SHORT = \"FILE_TOO_SHORT\",\n            FILE_HASH_MISMATCH = \"FILE_HASH_MISMATCH\",\n            FILE_SAME_AS_SOURCE = \"FILE_SAME_AS_SOURCE\",\n            NETWORK_FAILED = \"NETWORK_FAILED\",\n            NETWORK_TIMEOUT = \"NETWORK_TIMEOUT\",\n            NETWORK_DISCONNECTED = \"NETWORK_DISCONNECTED\",\n            NETWORK_SERVER_DOWN = \"NETWORK_SERVER_DOWN\",\n            NETWORK_INVALID_REQUEST = \"NETWORK_INVALID_REQUEST\",\n            SERVER_FAILED = \"SERVER_FAILED\",\n            SERVER_NO_RANGE = \"SERVER_NO_RANGE\",\n            SERVER_BAD_CONTENT = \"SERVER_BAD_CONTENT\",\n            SERVER_UNAUTHORIZED = \"SERVER_UNAUTHORIZED\",\n            SERVER_CERT_PROBLEM = \"SERVER_CERT_PROBLEM\",\n            SERVER_FORBIDDEN = \"SERVER_FORBIDDEN\",\n            SERVER_UNREACHABLE = \"SERVER_UNREACHABLE\",\n            SERVER_CONTENT_LENGTH_MISMATCH = \"SERVER_CONTENT_LENGTH_MISMATCH\",\n            SERVER_CROSS_ORIGIN_REDIRECT = \"SERVER_CROSS_ORIGIN_REDIRECT\",\n            USER_CANCELED = \"USER_CANCELED\",\n            USER_SHUTDOWN = \"USER_SHUTDOWN\",\n            CRASH = \"CRASH\",\n        }\n\n        export enum State {\n            /** The download is currently receiving data from the server. */\n            IN_PROGRESS = \"in_progress\",\n            /** An error broke the connection with the file host. */\n            INTERRUPTED = \"interrupted\",\n            /** The download completed successfully. */\n            COMPLETE = \"complete\",\n        }\n\n        export enum DangerType {\n            /** The download's filename is suspicious. */\n            FILE = \"file\",\n            /** The download's URL is known to be malicious. */\n            URL = \"url\",\n            /** The downloaded file is known to be malicious. */\n            CONTENT = \"content\",\n            /** The download's URL is not commonly downloaded and could be dangerous. */\n            UNCOMMON = \"uncommon\",\n            /** The download came from a host known to distribute malicious binaries and is likely dangerous. */\n            HOST = \"host\",\n            /** The download is potentially unwanted or unsafe. E.g. it could make changes to browser or computer settings. */\n            UNWANTED = \"unwanted\",\n            /** The download presents no known danger to the user's computer. */\n            SAFE = \"safe\",\n            /** The user has accepted the dangerous download. */\n            ACCEPTED = \"accepted\",\n            /** Enterprise-related values. */\n            ALLOWLISTED_BY_POLICY = \"allowlistedByPolicy\",\n            ASYNC_SCANNING = \"asyncScanning\",\n            ASYNC_LOCAL_PASSWORD_SCANNING = \"asyncLocalPasswordScanning\",\n            PASSWORD_PROTECTED = \"passwordProtected\",\n            BLOCKED_TOO_LARGE = \"blockedTooLarge\",\n            SENSITIVE_CONTENT_WARNING = \"sensitiveContentWarning\",\n            SENSITIVE_CONTENT_BLOCK = \"sensitiveContentBlock\",\n            DEEP_SCANNED_FAILED = \"deepScannedFailed\",\n            DEEP_SCANNED_SAFE = \"deepScannedSafe\",\n            DEEP_SCANNED_OPENED_DANGEROUS = \"deepScannedOpenedDangerous\",\n            PROMPT_FOR_SCANNING = \"promptForScanning\",\n            PROMPT_FOR_LOCAL_PASSWORD_SCANNING = \"promptForLocalPasswordScanning\",\n            ACCOUNT_COMPROMISE = \"accountCompromise\",\n            BLOCKED_SCAN_FAILED = \"blockedScanFailed\",\n            /** For use by the Secure Enterprise Browser extension. When required, Chrome will block the download to disc and download the file directly to Google Drive. */\n            FORCE_SAVE_TO_GDRIVE = \"forceSaveToGdrive\",\n            /** For use by the Secure Enterprise Browser extension. When required, Chrome will block the download to disc and download the file directly to OneDrive. */\n            FORCE_SAVE_TO_ONEDRIVE = \"forceSaveToOnedrive\",\n        }\n\n        export interface DownloadItem {\n            /** Number of bytes received so far from the host, without considering file compression. */\n            bytesReceived: number;\n            /** Indication of whether this download is thought to be safe or known to be suspicious. */\n            danger: `${DangerType}`;\n            /** The absolute URL that this download initiated from, before any redirects. */\n            url: string;\n            /**\n             * The absolute URL that this download is being made from, after all redirects.\n             * @since Chrome 54\n             */\n            finalUrl: string;\n            /** Number of bytes in the whole file, without considering file compression, or -1 if unknown. */\n            totalBytes: number;\n            /** Absolute local path. */\n            filename: string;\n            /** True if the download has stopped reading data from the host, but kept the connection open. */\n            paused: boolean;\n            /** Indicates whether the download is progressing, interrupted, or complete. */\n            state: `${State}`;\n            /** The file's MIME type. */\n            mime: string;\n            /** Number of bytes in the whole file post-decompression, or -1 if unknown. */\n            fileSize: number;\n            /** The time when the download began in ISO 8601 format. May be passed directly to the Date constructor: `Browser.downloads.search({}, function(items){items.forEach(function(item){console.log(new Date(item.startTime))})})` */\n            startTime: string;\n            /** Why the download was interrupted. Several kinds of HTTP errors may be grouped under one of the errors beginning with `SERVER_`. Errors relating to the network begin with `NETWORK_`, errors relating to the process of writing the file to the file system begin with `FILE_`, and interruptions initiated by the user begin with `USER_`. */\n            error?: `${InterruptReason}` | undefined;\n            /** The time when the download ended in ISO 8601 format. May be passed directly to the Date constructor: `Browser.downloads.search({}, function(items){items.forEach(function(item){if (item.endTime) console.log(new Date(item.endTime))})})` */\n            endTime?: string | undefined;\n            /** An identifier that is persistent across browser sessions. */\n            id: number;\n            /** False if this download is recorded in the history, true if it is not recorded. */\n            incognito: boolean;\n            /** Absolute URL. */\n            referrer: string;\n            /** Estimated time when the download will complete in ISO 8601 format. May be passed directly to the Date constructor: `Browser.downloads.search({}, function(items){items.forEach(function(item){if (item.estimatedEndTime) console.log(new Date(item.estimatedEndTime))})})` */\n            estimatedEndTime?: string | undefined;\n            /** True if the download is in progress and paused, or else if it is interrupted and can be resumed starting from where it was interrupted. */\n            canResume: boolean;\n            /** Whether the downloaded file still exists. This information may be out of date because Chrome does not automatically watch for file removal. Call {@link search}() in order to trigger the check for file existence. When the existence check completes, if the file has been deleted, then an {@link onChanged} event will fire. Note that {@link search}() does not wait for the existence check to finish before returning, so results from {@link search}() may not accurately reflect the file system. Also, {@link search}() may be called as often as necessary, but will not check for file existence any more frequently than once every 10 seconds. */\n            exists: boolean;\n            /** The identifier for the extension that initiated this download if this download was initiated by an extension. Does not change once it is set. */\n            byExtensionId?: string | undefined;\n            /** The localized name of the extension that initiated this download if this download was initiated by an extension. May change if the extension changes its name or if the user changes their locale. */\n            byExtensionName?: string | undefined;\n        }\n\n        export interface GetFileIconOptions {\n            /** The size of the returned icon. The icon will be square with dimensions size * size pixels. The default and largest size for the icon is 32x32 pixels. The only supported sizes are 16 and 32. It is an error to specify any other size. */\n            size?: 16 | 32 | undefined;\n        }\n\n        export interface DownloadQuery {\n            /** Set elements of this array to {@link DownloadItem} properties in order to sort search results. For example, setting `orderBy=['startTime']` sorts the {@link DownloadItem} by their start time in ascending order. To specify descending order, prefix with a hyphen: '-startTime'. */\n            orderBy?: string[] | undefined;\n            /** Limits results to {@link DownloadItem} whose `url` matches the given regular expression. */\n            urlRegex?: string | undefined;\n            /** Limits results to {@link DownloadItem} that ended before the time in ISO 8601 format. */\n            endedBefore?: string | undefined;\n            /** Limits results to {@link DownloadItem} whose `totalBytes` is greater than the given integer. */\n            totalBytesGreater?: number | undefined;\n            /** Indication of whether this download is thought to be safe or known to be suspicious. */\n            danger?: `${DangerType}` | undefined;\n            /** Number of bytes in the whole file, without considering file compression, or -1 if unknown. */\n            totalBytes?: number | undefined;\n            /** True if the download has stopped reading data from the host, but kept the connection open. */\n            paused?: boolean | undefined;\n            /** Limits results to {@link DownloadItem} whose `filename` matches the given regular expression. */\n            filenameRegex?: string | undefined;\n            /**\n             * The absolute URL that this download is being made from, after all redirects.\n             * @since Chrome 54\n             */\n            finalUrl?: string;\n            /**\n             * Limits results to {@link DownloadItem} whose `finalUrl` matches the given regular expression.\n             * @since Chrome 54\n             */\n            finalUrlRegex?: string;\n            /** This array of search terms limits results to {@link DownloadItem} whose `filename` or `url` or `finalUrl` contain all of the search terms that do not begin with a dash '-' and none of the search terms that do begin with a dash. */\n            query?: string[] | undefined;\n            /** Limits results to {@link DownloadItem} whose `totalBytes` is less than the given integer. */\n            totalBytesLess?: number | undefined;\n            /** The `id` of the {@link DownloadItem} to query. */\n            id?: number | undefined;\n            /** Number of bytes received so far from the host, without considering file compression. */\n            bytesReceived?: number | undefined;\n            /** Limits results to {@link DownloadItem} that ended after the time in ISO 8601 format. */\n            endedAfter?: string | undefined;\n            /** Absolute local path. */\n            filename?: string | undefined;\n            /** Indicates whether the download is progressing, interrupted, or complete. */\n            state?: `${State}` | undefined;\n            /** Limits results to {@link DownloadItem} that started after the time in ISO 8601 format. */\n            startedAfter?: string | undefined;\n            /** The file's MIME type. */\n            mime?: string | undefined;\n            /** Number of bytes in the whole file post-decompression, or -1 if unknown. */\n            fileSize?: number | undefined;\n            /** The time when the download began in ISO 8601 format. */\n            startTime?: string | undefined;\n            /** The absolute URL that this download initiated from, before any redirects. */\n            url?: string | undefined;\n            /** Limits results to {@link DownloadItem} that started before the time in ISO 8601 format. */\n            startedBefore?: string | undefined;\n            /** The maximum number of matching {@link DownloadItem} returned. Defaults to 1000. Set to 0 in order to return all matching {@link DownloadItem}. See {@link search} for how to page through results. */\n            limit?: number | undefined;\n            /** Why a download was interrupted. */\n            error?: `${InterruptReason}` | undefined;\n            /** The time when the download ended in ISO 8601 format. */\n            endTime?: string | undefined;\n            /** Whether the downloaded file exists; */\n            exists?: boolean | undefined;\n        }\n\n        export interface FilenameSuggestion {\n            /** The {@link DownloadItem}'s new target {@link DownloadItem.filename}, as a path relative to the user's default Downloads directory, possibly containing subdirectories. Absolute paths, empty paths, and paths containing back-references \"..\" will be ignored. `filename` is ignored if there are any {@link onDeterminingFilename} listeners registered by any extensions. */\n            filename: string;\n            /** The action to take if `filename` already exists. */\n            conflictAction?: `${FilenameConflictAction}` | undefined;\n        }\n\n        /** @since Chrome 105 */\n        export interface UiOptions {\n            /** Enable or disable the download UI. */\n            enabled: boolean;\n        }\n\n        /**\n         * Find {@link DownloadItem}. Set `query` to the empty object to get all {@link DownloadItem}. To get a specific {@link DownloadItem}, set only the `id` field. To page through a large number of items, set `orderBy: ['-startTime']`, set `limit` to the number of items per page, and set `startedAfter` to the `startTime` of the last item from the last page.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function search(query: DownloadQuery): Promise<DownloadItem[]>;\n        export function search(query: DownloadQuery, callback: (results: DownloadItem[]) => void): void;\n\n        /**\n         * Pause the download. If the request was successful the download is in a paused state. Otherwise {@link runtime.lastError} contains an error message. The request will fail if the download is not active.\n         * @param downloadId The id of the download to pause.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function pause(downloadId: number): Promise<void>;\n        export function pause(downloadId: number, callback: () => void): void;\n\n        /**\n         * Retrieve an icon for the specified download. For new downloads, file icons are available after the {@link onCreated} event has been received. The image returned by this function while a download is in progress may be different from the image returned after the download is complete. Icon retrieval is done by querying the underlying operating system or toolkit depending on the platform. The icon that is returned will therefore depend on a number of factors including state of the download, platform, registered file types and visual theme. If a file icon cannot be determined, {@link runtime.lastError} will contain an error message.\n         * @param downloadId The identifier for the download.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getFileIcon(downloadId: number, options?: GetFileIconOptions): Promise<string | undefined>;\n        export function getFileIcon(downloadId: number, callback: (iconURL?: string) => void): void;\n        export function getFileIcon(\n            downloadId: number,\n            options: GetFileIconOptions,\n            callback: (iconURL?: string) => void,\n        ): void;\n\n        /**\n         * Resume a paused download. If the request was successful the download is in progress and unpaused. Otherwise {@link runtime.lastError} contains an error message. The request will fail if the download is not active.\n         * @param downloadId The id of the download to resume.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function resume(downloadId: number): Promise<void>;\n        export function resume(downloadId: number, callback: () => void): void;\n\n        /**\n         * Cancel a download. When `callback` is run, the download is cancelled, completed, interrupted or doesn't exist anymore.\n         * @param downloadId The id of the download to cancel.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function cancel(downloadId: number): Promise<void>;\n        export function cancel(downloadId: number, callback: () => void): void;\n\n        /**\n         * Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both `filename` and `saveAs` are specified, then the Save As dialog will be displayed, pre-populated with the specified `filename`. If the download started successfully, `callback` will be called with the new {@link DownloadItem}'s `downloadId`. If there was an error starting the download, then `callback` will be called with `downloadId=undefined` and {@link runtime.lastError} will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. Extensions must not parse it.\n         * @param options What to download and how.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function download(options: DownloadOptions): Promise<number>;\n        export function download(options: DownloadOptions, callback: (downloadId: number) => void): void;\n\n        /**\n         * Opens the downloaded file now if the {@link DownloadItem} is complete; otherwise returns an error through {@link runtime.lastError}. This method requires the `\"downloads.open\"` permission in addition to the `\"downloads\"` permission. An {@link onChanged} event fires when the item is opened for the first time. This method can only be called in response to a user gesture.\n         * @param downloadId The identifier for the downloaded file.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 123.\n         */\n        export function open(downloadId: number): Promise<void>;\n        export function open(\n            downloadId: number,\n            /** @since Chrome 123 */\n            callback: () => void,\n        ): void;\n\n        /**\n         * Show the downloaded file in its folder in a file manager.\n         * @param downloadId The identifier for the downloaded file.\n         */\n        export function show(downloadId: number): void;\n\n        /** Show the default Downloads folder in a file manager. */\n        export function showDefaultFolder(): void;\n\n        /**\n         * Erase matching {@link DownloadItem} from history without deleting the downloaded file. An {@link onErased} event will fire for each {@link DownloadItem} that matches `query`, then `callback` will be called.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function erase(query: DownloadQuery): Promise<number[]>;\n        export function erase(query: DownloadQuery, callback: (erasedIds: number[]) => void): void;\n\n        /**\n         * Remove the downloaded file if it exists and the {@link DownloadItem} is complete; otherwise return an error through {@link runtime.lastError}.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function removeFile(downloadId: number): Promise<void>;\n        export function removeFile(downloadId: number, callback: () => void): void;\n\n        /**\n         * Prompt the user to accept a dangerous download. Can only be called from a visible context (tab, window, or page/browser action popup). Does not automatically accept dangerous downloads. If the download is accepted, then an {@link onChanged} event will fire, otherwise nothing will happen. When all the data is fetched into a temporary file and either the download is not dangerous or the danger has been accepted, then the temporary file is renamed to the target filename, the `state` changes to 'complete', and {@link onChanged} fires.\n         * @param downloadId The identifier for the {@link DownloadItem}.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function acceptDanger(downloadId: number): Promise<void>;\n        export function acceptDanger(downloadId: number, callback: () => void): void;\n\n        /**\n         * Enable or disable the gray shelf at the bottom of every window associated with the current browser profile. The shelf will be disabled as long as at least one extension has disabled it. Enabling the shelf while at least one other extension has disabled it will return an error through {@link runtime.lastError}. Requires the `\"downloads.shelf\"` permission in addition to the `\"downloads\"` permission.\n         * @deprecated since Chrome 117. Use {@link setUiOptions} instead.\n         */\n        export function setShelfEnabled(enabled: boolean): void;\n\n        /**\n         * Change the download UI of every window associated with the current browser profile. As long as at least one extension has set {@link UiOptions.enabled} to false, the download UI will be hidden. Setting {@link UiOptions.enabled} to true while at least one other extension has disabled it will return an error through {@link runtime.lastError}. Requires the `\"downloads.ui\"` permission in addition to the `\"downloads\"` permission.\n         * @since Chrome 105\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 105.\n         */\n        export function setUiOptions(options: UiOptions): Promise<void>;\n        export function setUiOptions(options: UiOptions, callback: () => void): void;\n\n        /** When any of a {@link DownloadItem}'s properties except `bytesReceived` and `estimatedEndTime` changes, this event fires with the `downloadId` and an object containing the properties that changed. */\n        export const onChanged: events.Event<(downloadDelta: DownloadDelta) => void>;\n\n        /** This event fires with the {@link DownloadItem} object when a download begins. */\n        export const onCreated: events.Event<(downloadItem: DownloadItem) => void>;\n\n        /** Fires with the `downloadId` when a download is erased from history. */\n        export const onErased: events.Event<(downloadId: number) => void>;\n\n        /** During the filename determination process, extensions will be given the opportunity to override the target {@link DownloadItem.filename}. Each extension may not register more than one listener for this event. Each listener must call `suggest` exactly once, either synchronously or asynchronously. If the listener calls `suggest` asynchronously, then it must return `true`. If the listener neither calls `suggest` synchronously nor returns `true`, then `suggest` will be called automatically. The {@link DownloadItem} will not complete until all listeners have called `suggest`. Listeners may call `suggest` without any arguments in order to allow the download to use `downloadItem.filename` for its filename, or pass a `suggestion` object to `suggest` in order to override the target filename. If more than one extension overrides the filename, then the last extension installed whose listener passes a `suggestion` object to `suggest` wins. In order to avoid confusion regarding which extension will win, users should not install extensions that may conflict. If the download is initiated by {@link download} and the target filename is known before the MIME type and tentative filename have been determined, pass `filename` to {@link download} instead. */\n        export const onDeterminingFilename: events.Event<\n            (downloadItem: DownloadItem, suggest: (suggestion?: FilenameSuggestion) => void) => void\n        >;\n    }\n\n    ////////////////////\n    // Enterprise Platform Keys\n    ////////////////////\n    /**\n     * Use the `Browser.enterprise.platformKeys` API to generate keys and install certificates for these keys. The certificates will be managed by the platform and can be used for TLS authentication, network access or by other extension through {@link Browser.platformKeys}.\n     *\n     * Permissions: \"enterprise.platformKeys\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @platform ChromeOS only\n     */\n    export namespace enterprise.platformKeys {\n        export interface Token {\n            /**\n             * Uniquely identifies this Token.\n             * Static IDs are `user` and `system`, referring to the platform's user-specific and the system-wide hardware token, respectively. Any other tokens (with other identifiers) might be returned by enterprise.platformKeys.getTokens.\n             */\n            id: string;\n            /**\n             * Implements the WebCrypto's SubtleCrypto interface. The cryptographic operations, including key generation, are hardware-backed.\n             *\n             * Only non-extractable keys can be generated. The supported key types are RSASSA-PKCS1-V1_5 with `modulusLength` up to 2048 and ECDSA with `namedCurve` P-256. Each key can be used for signing data at most once, unless the extension is allowlisted by the KeyPermissions policy, in which case the key can be used indefinitely.\n             *\n             * Keys generated on a specific `Token` cannot be used with any other Tokens, nor can they be used with `window.crypto.subtle`. Equally, `Key` objects created with `window.crypto.subtle` cannot be used with this interface.\n             */\n            subtleCrypto: SubtleCrypto;\n            /**\n             * Implements the WebCrypto's SubtleCrypto interface. The cryptographic operations, including key generation, are software-backed. Protection of the keys, and thus implementation of the non-extractable property, is done in software, so the keys are less protected than hardware-backed keys.\n             *\n             * Only non-extractable keys can be generated. The only supported key type is RSASSA-PKCS1-V1_5 with `modulusLength` up to 2048. up to 2048. Each key can be used for signing data at most once, unless the extension is allowlisted through the KeyPermissions policy, in which case the key can be used indefinitely.\n             *\n             * Keys generated on a specific `Token` cannot be used with any other Tokens, nor can they be used with `window.crypto.subtle`. Equally, `Key` objects created with `window.crypto.subtle` cannot be used with this interface.\n             * @since Chrome 97\n             */\n            softwareBackedSubtleCrypto: SubtleCrypto;\n        }\n\n        /** @since Chrome 110 */\n        export interface ChallengeKeyOptions {\n            /**\n             * A challenge as emitted by the Verified Access Web API.\n             */\n            challenge: ArrayBuffer;\n            /**\n             * Which Enterprise Key to challenge.\n             * @since Chrome 110\n             */\n            scope: `${Scope}`;\n            /**\n             * If present, registers the challenged key with the specified scope's token.\n             * The key can then be associated with a certificate and used like any other signing key.\n             * Subsequent calls to this function will then generate a new Enterprise Key in the specified scope.\n             */\n            registerKey?: RegisterKeyOptions | undefined;\n        }\n\n        /** @since Chrome 110 */\n        export interface RegisterKeyOptions {\n            /**\n             * Which algorithm the registered key should use.\n             */\n            algorithm: `${Algorithm}`;\n        }\n\n        /**\n         * Type of key to generate.\n         * @since Chrome 110\n         */\n        export enum Algorithm {\n            ECDSA = \"ECDSA\",\n            RSA = \"RSA\",\n        }\n\n        /**\n         * Whether to use the Enterprise User Key or the Enterprise Machine Key.\n         * @since Chrome 110\n         */\n        export enum Scope {\n            USER = \"USER\",\n            MACHINE = \"MACHINE\",\n        }\n\n        /**\n         * Returns the available Tokens. In a regular user's session the list will always contain the user's token with id \"user\". If a system-wide TPM token is available, the returned list will also contain the system-wide token with id \"system\". The system-wide token will be the same for all sessions on this device (device in the sense of e.g. a Chromebook).\n         *\n         * Can return its result via Promise since Chrome 131.\n         */\n        export function getTokens(): Promise<Token[]>;\n        export function getTokens(callback: (tokens: Token[]) => void): void;\n\n        /**\n         * Returns the list of all client certificates available from the given token. Can be used to check for the existence and expiration of client certificates that are usable for a certain authentication.\n         * @param tokenId The id of a Token returned by getTokens.\n         *\n         * Can return its result via Promise since Chrome 131.\n         */\n        export function getCertificates(tokenId: string): Promise<ArrayBuffer[]>;\n        export function getCertificates(tokenId: string, callback: (certificates: ArrayBuffer[]) => void): void;\n\n        /**\n         * Imports `certificate` to the given token if the certified key is already stored in this token. After a successful certification request, this function should be used to store the obtained certificate and to make it available to the operating system and browser for authentication.\n         * @param tokenId The id of a Token returned by getTokens.\n         * @param certificate The DER encoding of a X.509 certificate.\n         *\n         * Can return its result via Promise since Chrome 131.\n         */\n        export function importCertificate(tokenId: string, certificate: ArrayBuffer): Promise<void>;\n        export function importCertificate(tokenId: string, certificate: ArrayBuffer, callback: () => void): void;\n\n        /**\n         * Removes `certificate` from the given token if present. Should be used to remove obsolete certificates so that they are not considered during authentication and do not clutter the certificate choice. Should be used to free storage in the certificate store.\n         * @param tokenId The id of a Token returned by getTokens.\n         * @param certificate The DER encoding of a X.509 certificate.\n         *\n         * Can return its result via Promise since Chrome 131.\n         */\n        export function removeCertificate(tokenId: string, certificate: ArrayBuffer): Promise<void>;\n        export function removeCertificate(tokenId: string, certificate: ArrayBuffer, callback: () => void): void;\n\n        /**\n         * Similar to `challengeMachineKey` and `challengeUserKey`, but allows specifying the algorithm of a registered key. Challenges a hardware-backed Enterprise Machine Key and emits the response as part of a remote attestation protocol. Only useful on ChromeOS and in conjunction with the Verified Access Web API which both issues challenges and verifies responses.\n         *\n         * A successful verification by the Verified Access Web API is a strong signal that the current device is a legitimate ChromeOS device, the current device is managed by the domain specified during verification, the current signed-in user is managed by the domain specified during verification, and the current device state complies with enterprise device policy. For example, a policy may specify that the device must not be in developer mode. Any device identity emitted by the verification is tightly bound to the hardware of the current device. If `user` Scope is specified, the identity is also tightly bound to the current signed-in user.\n         *\n         * This function is highly restricted and will fail if the current device is not managed, the current user is not managed, or if this operation has not explicitly been enabled for the caller by enterprise device policy. The challenged key does not reside in the `system` or `user` token and is not accessible by any other API.\n         *\n         * @param options Object containing the fields defined in {@link ChallengeKeyOptions}.\n         *\n         * Can return its result via Promise since Chrome 131.\n         * @since Chrome 110\n         */\n        export function challengeKey(options: ChallengeKeyOptions): Promise<ArrayBuffer>;\n        export function challengeKey(options: ChallengeKeyOptions, callback: (response: ArrayBuffer) => void): void;\n\n        /**\n         * @deprecated Deprecated since Chrome 110, use {@link challengeKey} instead.\n         *\n         * Challenges a hardware-backed Enterprise Machine Key and emits the response as part of a remote attestation protocol. Only useful on Chrome OS and in conjunction with the Verified Access Web API which both issues challenges and verifies responses. A successful verification by the Verified Access Web API is a strong signal of all of the following:\n         *\n         * * The current device is a legitimate ChromeOS device.\n         * * The current device is managed by the domain specified during verification.\n         * * The current signed-in user is managed by the domain specified during verification.\n         * * The current device state complies with enterprise device policy. For example, a policy may specify that the device must not be in developer mode.\n         * * Any device identity emitted by the verification is tightly bound to the hardware of the current device.\n         *\n         * This function is highly restricted and will fail if the current device is not managed, the current user is not managed, or if this operation has not explicitly been enabled for the caller by enterprise device policy. The Enterprise Machine Key does not reside in the `system` token and is not accessible by any other API.\n         * @param challenge A challenge as emitted by the Verified Access Web API.\n         * @param registerKey If set, the current Enterprise Machine Key is registered with the `system` token and relinquishes the Enterprise Machine Key role. The key can then be associated with a certificate and used like any other signing key. This key is 2048-bit RSA. Subsequent calls to this function will then generate a new Enterprise Machine Key. Since Chrome 59.\n         *\n         * Can return its result via Promise since Chrome 131.\n         * @since Chrome 50\n         */\n        export function challengeMachineKey(challenge: ArrayBuffer): Promise<ArrayBuffer>;\n        export function challengeMachineKey(challenge: ArrayBuffer, registerKey: boolean): Promise<ArrayBuffer>;\n        export function challengeMachineKey(challenge: ArrayBuffer, callback: (response: ArrayBuffer) => void): void;\n        export function challengeMachineKey(\n            challenge: ArrayBuffer,\n            registerKey: boolean,\n            callback: (response: ArrayBuffer) => void,\n        ): void;\n\n        /**\n         * @deprecated Deprecated since Chrome 110, use {@link challengeKey} instead.\n         *\n         * Challenges a hardware-backed Enterprise User Key and emits the response as part of a remote attestation protocol. Only useful on ChromeOS and in conjunction with the Verified Access Web API which both issues challenges and verifies responses. A successful verification by the Verified Access Web API is a strong signal of all of the following:\n         *\n         * * The current device is a legitimate ChromeOS device.\n         * * The current device is managed by the domain specified during verification.\n         * * The current signed-in user is managed by the domain specified during verification.\n         * * The current device state complies with enterprise user policy. For example, a policy may specify that the device must not be in developer mode.\n         * * The public key emitted by the verification is tightly bound to the hardware of the current device and to the current signed-in user.\n         *\n         * This function is highly restricted and will fail if the current device is not managed, the current user is not managed, or if this operation has not explicitly been enabled for the caller by enterprise user policy. The Enterprise User Key does not reside in the `user` token and is not accessible by any other API.\n         * @param challenge A challenge as emitted by the Verified Access Web API.\n         * @param registerKey If set, the current Enterprise User Key is registered with the `user` token and relinquishes the Enterprise User Key role. The key can then be associated with a certificate and used like any other signing key. This key is 2048-bit RSA. Subsequent calls to this function will then generate a new Enterprise User Key.\n         * @param callback Called back with the challenge response.\n         * @since Chrome 50\n         */\n        export function challengeUserKey(challenge: ArrayBuffer, registerKey: boolean): Promise<ArrayBuffer>;\n        export function challengeUserKey(\n            challenge: ArrayBuffer,\n            registerKey: boolean,\n            callback: (response: ArrayBuffer) => void,\n        ): void;\n    }\n\n    ////////////////////\n    // Enterprise Device Attributes\n    ////////////////////\n    /**\n     * Use the `Browser.enterprise.deviceAttributes` API to read device attributes.\n     *\n     * Permissions: \"enterprise.deviceAttributes\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @platform ChromeOS only\n     * @since Chrome 46\n     */\n    export namespace enterprise.deviceAttributes {\n        /**\n         * Fetches the value of the device identifier of the directory API, that is generated by the server and identifies the cloud record of the device for querying in the cloud directory API. If the current user is not affiliated, returns an empty string.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getDirectoryDeviceId(): Promise<string>;\n        export function getDirectoryDeviceId(callback: (deviceId: string) => void): void;\n\n        /**\n         * Fetches the device's serial number. Please note the purpose of this API is to administrate the device (e.g. generating Certificate Sign Requests for device-wide certificates). This API may not be used for tracking devices without the consent of the device's administrator. If the current user is not affiliated, returns an empty string.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @since Chrome 66\n         */\n        export function getDeviceSerialNumber(): Promise<string>;\n        export function getDeviceSerialNumber(callback: (serialNumber: string) => void): void;\n\n        /**\n         * Fetches the administrator-annotated Asset Id. If the current user is not affiliated or no Asset Id has been set by the administrator, returns an empty string.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @since Chrome 66\n         */\n        export function getDeviceAssetId(): Promise<string>;\n        export function getDeviceAssetId(callback: (assetId: string) => void): void;\n\n        /**\n         * Fetches the administrator-annotated Location. If the current user is not affiliated or no Annotated Location has been set by the administrator, returns an empty string.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @since Chrome 66\n         */\n        export function getDeviceAnnotatedLocation(): Promise<string>;\n        export function getDeviceAnnotatedLocation(callback: (annotatedLocation: string) => void): void;\n\n        /**\n         * Fetches the device's hostname as set by DeviceHostnameTemplate policy. If the current user is not affiliated or no hostname has been set by the enterprise policy, returns an empty string.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @since Chrome 82\n         */\n        export function getDeviceHostname(): Promise<string>;\n        export function getDeviceHostname(callback: (hostname: string) => void): void;\n    }\n\n    ////////////////////\n    // Enterprise Hardware Platform\n    ////////////////////\n    /**\n     * Use the `Browser.enterprise.hardwarePlatform` API to get the manufacturer and model of the hardware platform where the browser runs.\n     *\n     * Permissions: \"enterprise.hardwarePlatform\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @since Chrome 71\n     */\n    export namespace enterprise.hardwarePlatform {\n        export interface HardwarePlatformInfo {\n            manufacturer: string;\n            model: string;\n        }\n\n        /**\n         * Obtains the manufacturer and model for the hardware platform and, if the extension is authorized, returns it via callback.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getHardwarePlatformInfo(): Promise<HardwarePlatformInfo>;\n        export function getHardwarePlatformInfo(callback: (info: HardwarePlatformInfo) => void): void;\n    }\n\n    ////////////////////\n    // Enterprise Login\n    ////////////////////\n    /**\n     * Use the `Browser.enterprise.login` API to exit Managed Guest sessions. Note: This API is only available to extensions installed by enterprise policy in ChromeOS Managed Guest sessions.\n     *\n     * Permissions: \"enterprise.login\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @platform ChromeOS only\n     * @since Chrome 139\n     */\n    export namespace enterprise.login {\n        /** Exits the current managed guest session. */\n        export function exitCurrentManagedGuestSession(): Promise<void>;\n        export function exitCurrentManagedGuestSession(callback: () => void): void;\n    }\n\n    ////////////////////\n    // Enterprise Networking Attributes\n    ////////////////////\n    /**\n     * Use the `Browser.enterprise.networkingAttributes` API to read information about your current network. Note: This API is only available to extensions force-installed by enterprise policy.\n     *\n     * Permissions: \"enterprise.networkingAttributes\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @platform ChromeOS only\n     * @since Chrome 85\n     */\n    export namespace enterprise.networkingAttributes {\n        export interface NetworkDetails {\n            /** The device's MAC address. */\n            macAddress: string;\n            /** Optional. The device's local IPv4 address (undefined if not configured). */\n            ipv4?: string | undefined;\n            /** Optional. The device's local IPv6 address (undefined if not configured). */\n            ipv6?: string | undefined;\n        }\n\n        /**\n         * Retrieves the network details of the device's default network. If the user is not affiliated or the device is not connected to a network, runtime.lastError will be set with a failure reason.\n         * @param callback Called with the device's default network's NetworkDetails.\n         */\n        export function getNetworkDetails(callback: (networkDetails: NetworkDetails) => void): void;\n    }\n\n    ////////////////////\n    // Events\n    ////////////////////\n    /**\n     * The `Browser.events` namespace contains common types used by APIs dispatching events to notify you when something interesting happens.\n     */\n    export namespace events {\n        /** Filters URLs for various criteria. See event filtering. All criteria are case sensitive. */\n        export interface UrlFilter {\n            /**\n             * Matches if the host part of the URL is an IP address and is contained in any of the CIDR blocks specified in the array.\n             * @since Chrome 123\n             */\n            cidrBlocks?: string[] | undefined;\n            /** Matches if the scheme of the URL is equal to any of the schemes specified in the array. */\n            schemes?: string[] | undefined;\n            /** Matches if the URL (without fragment identifier) matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the RE2 syntax. */\n            urlMatches?: string | undefined;\n            /** Matches if the path segment of the URL contains a specified string. */\n            pathContains?: string | undefined;\n            /** Matches if the host name of the URL ends with a specified string. */\n            hostSuffix?: string | undefined;\n            /** Matches if the host name of the URL starts with a specified string. */\n            hostPrefix?: string | undefined;\n            /** Matches if the host name of the URL contains a specified string. To test whether a host name component has a prefix 'foo', use hostContains: '.foo'. This matches 'www.foobar.com' and 'foo.com', because an implicit dot is added at the beginning of the host name. Similarly, hostContains can be used to match against component suffix ('foo.') and to exactly match against components ('.foo.'). Suffix- and exact-matching for the last components need to be done separately using hostSuffix, because no implicit dot is added at the end of the host name. */\n            hostContains?: string | undefined;\n            /** Matches if the URL (without fragment identifier) contains a specified string. Port numbers are stripped from the URL if they match the default port number. */\n            urlContains?: string | undefined;\n            /** Matches if the query segment of the URL ends with a specified string. */\n            querySuffix?: string | undefined;\n            /** Matches if the URL (without fragment identifier) starts with a specified string. Port numbers are stripped from the URL if they match the default port number. */\n            urlPrefix?: string | undefined;\n            /** Matches if the host name of the URL is equal to a specified string. */\n            hostEquals?: string | undefined;\n            /** Matches if the URL (without fragment identifier) is equal to a specified string. Port numbers are stripped from the URL if they match the default port number. */\n            urlEquals?: string | undefined;\n            /** Matches if the query segment of the URL contains a specified string. */\n            queryContains?: string | undefined;\n            /** Matches if the path segment of the URL starts with a specified string. */\n            pathPrefix?: string | undefined;\n            /** Matches if the path segment of the URL is equal to a specified string. */\n            pathEquals?: string | undefined;\n            /** Matches if the path segment of the URL ends with a specified string. */\n            pathSuffix?: string | undefined;\n            /** Matches if the query segment of the URL is equal to a specified string. */\n            queryEquals?: string | undefined;\n            /** Matches if the query segment of the URL starts with a specified string. */\n            queryPrefix?: string | undefined;\n            /** Matches if the URL (without fragment identifier) ends with a specified string. Port numbers are stripped from the URL if they match the default port number. */\n            urlSuffix?: string | undefined;\n            /** Matches if the port of the URL is contained in any of the specified port lists. For example `[80, 443, [1000, 1200]]` matches all requests on port 80, 443 and in the range 1000-1200. */\n            ports?: Array<number | number[]> | undefined;\n            /** Matches if the URL without query segment and fragment identifier matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the RE2 syntax. */\n            originAndPathMatches?: string | undefined;\n        }\n\n        export interface Event<T extends (...args: any) => void> {\n            /**\n             * Registers an event listener callback to an event.\n             * @param callback Called when an event occurs. The parameters of this function depend on the type of event.\n             */\n            addListener(callback: T): void;\n\n            /**\n             * Returns currently registered rules.\n             * @param ruleIdentifiers If an array is passed, only rules with identifiers contained in this array are returned.\n             */\n            getRules(\n                /** @param rules Rules that were registered, the optional parameters are filled with values */\n                callback: (rules: Rule[]) => void,\n            ): void;\n            getRules(\n                ruleIdentifiers: string[],\n                /** @param rules Rules that were registered, the optional parameters are filled with values */\n                callback: (rules: Rule[]) => void,\n            ): void;\n\n            /**\n             * @param callback Listener whose registration status shall be tested.\n             * @returns True if _callback_ is registered to the event.\n             */\n            hasListener(callback: T): boolean;\n\n            /**\n             * Unregisters currently registered rules.\n             * @param ruleIdentifiers If an array is passed, only rules with identifiers contained in this array are unregistered.\n             */\n            removeRules(ruleIdentifiers: string[] | undefined, callback?: () => void): void;\n            removeRules(callback?: () => void): void;\n\n            /**\n             * Registers rules to handle events.\n             * @param rules Rules to be registered. These do not replace previously registered rules.\n             * @param callback Called with registered rules.\n             */\n            addRules(\n                rules: Rule[],\n                /** @param rules Rules that were registered, the optional parameters are filled with values */\n                callback?: (rules: Rule[]) => void,\n            ): void;\n\n            /**\n             * Deregisters an event listener callback from an event.\n             * @param callback Listener that shall be unregistered.\n             */\n            removeListener(callback: T): void;\n\n            /** @returns True if any event listeners are registered to the event. */\n            hasListeners(): boolean;\n        }\n\n        /** Description of a declarative rule for handling events. */\n        export interface Rule {\n            /** Optional priority of this rule. Defaults to 100. */\n            priority?: number | undefined;\n            /** List of conditions that can trigger the actions. */\n            conditions: any[];\n            /** Optional identifier that allows referencing this rule. */\n            id?: string | undefined;\n            /** List of actions that are triggered if one of the conditions is fulfilled. */\n            actions: any[];\n            /** Tags can be used to annotate rules and perform operations on sets of rules. */\n            tags?: string[] | undefined;\n        }\n    }\n\n    ////////////////////\n    // Extension\n    ////////////////////\n    /**\n     * The `Browser.extension` API has utilities that can be used by any extension page. It includes support for exchanging messages between an extension and its content scripts or between extensions, as described in detail in Message Passing.\n     */\n    export namespace extension {\n        /**\n         * The type of extension view.\n         * @since Chrome 44\n         */\n        export enum ViewType {\n            TAB = \"tab\",\n            POPUP = \"popup\",\n        }\n\n        export interface FetchProperties {\n            /**\n             * Find a view according to a tab id. If this field is omitted, returns all views.\n             * @since Chrome 54\n             */\n            tabId?: number | undefined;\n            /** The window to restrict the search to. If omitted, returns all views. */\n            windowId?: number | undefined;\n            /** The type of view to get. If omitted, returns all views (including background pages and tabs). */\n            type?: `${ViewType}` | undefined;\n        }\n\n        /** True for content scripts running inside incognito tabs, and for extension pages running inside an incognito process. The latter only applies to extensions with 'split' incognito_behavior. */\n        export const inIncognitoContext: boolean;\n\n        /**\n         * Set for the lifetime of a callback if an ansychronous extension api has resulted in an error. If no error has occurred lastError will be `undefined`.\n         * @deprecated since Chrome 58. Please use {@link runtime.lastError}\n         */\n        export const lastError: runtime.LastError | undefined;\n\n        /** Returns the JavaScript 'window' object for the background page running inside the current extension. Returns null if the extension has no background page. */\n        export function getBackgroundPage(): Window | null;\n\n        /**\n         * Converts a relative path within an extension install directory to a fully-qualified URL.\n         * @param path A path to a resource within an extension expressed relative to its install directory.\n         * @deprecated since Chrome 58. Please use {@link runtime.getURL}\n         */\n        export function getURL(path: string): string;\n\n        /** Sets the value of the ap CGI parameter used in the extension's update URL. This value is ignored for extensions that are hosted in the Chrome Extension Gallery. */\n        export function setUpdateUrlData(data: string): void;\n\n        /** Returns an array of the JavaScript 'window' objects for each of the pages running inside the current extension. */\n        export function getViews(fetchProperties?: FetchProperties): Window[];\n\n        /**\n         * Retrieves the state of the extension's access to the 'file://' scheme. This corresponds to the user-controlled per-extension 'Allow access to File URLs' setting accessible via the `chrome://extensions` page.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         */\n        export function isAllowedFileSchemeAccess(): Promise<boolean>;\n        export function isAllowedFileSchemeAccess(callback: (isAllowedAccess: boolean) => void): void;\n\n        /**\n         * Retrieves the state of the extension's access to Incognito-mode. This corresponds to the user-controlled per-extension 'Allowed in Incognito' setting accessible via the `chrome://extensions` page.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         */\n        export function isAllowedIncognitoAccess(): Promise<boolean>;\n        export function isAllowedIncognitoAccess(callback: (isAllowedAccess: boolean) => void): void;\n\n        /**\n         * Sends a single request to other listeners within the extension. Similar to {@link runtime.connect}, but only sends a single request with an optional response. The {@link extension.onRequest} event is fired in each page of the extension.\n         *\n         * MV2 only\n         * @param extensionId The extension ID of the extension you want to connect to. If omitted, default is your own extension.\n         * @deprecated Please use {@link runtime.sendMessage}\n         */\n        export function sendRequest<Request = any, Response = any>(\n            extensionId: string | undefined,\n            request: Request,\n            callback?: (response: Response) => void,\n        ): void;\n        export function sendRequest<Request = any, Response = any>(\n            request: Request,\n            callback?: (response: Response) => void,\n        ): void;\n\n        /**\n         * Returns an array of the JavaScript 'window' objects for each of the tabs running inside the current extension. If `windowId` is specified, returns only the 'window' objects of tabs attached to the specified window.\n         *\n         * MV2 only\n         * @deprecated Please use {@link extension.getViews} `{type: \"tab\"}`.\n         */\n        export function getExtensionTabs(windowId?: number): Window[];\n\n        /**\n         * Fired when a request is sent from either an extension process or a content script.\n         *\n         * MV2 only\n         * @deprecated Please use {@link runtime.onMessage}.\n         */\n        export const onRequest: Browser.events.Event<\n            | ((request: any, sender: runtime.MessageSender, sendResponse: (response: any) => void) => void)\n            | ((sender: runtime.MessageSender, sendResponse: (response: any) => void) => void)\n        >;\n\n        /**\n         * Fired when a request is sent from another extension.\n         *\n         * MV2 only\n         * @deprecated Please use {@link runtime.onMessageExternal}.\n         */\n        export const onRequestExternal: Browser.events.Event<\n            | ((request: any, sender: runtime.MessageSender, sendResponse: (response: any) => void) => void)\n            | ((sender: runtime.MessageSender, sendResponse: (response: any) => void) => void)\n        >;\n    }\n\n    ////////////////////\n    // Extension Types\n    ////////////////////\n    /** The `Browser.extensionTypes` API contains type declarations for Chrome extensions. */\n    export namespace extensionTypes {\n        /** @since Chrome 139 */\n        export type ColorArray = [number, number, number, number];\n\n        /**\n         * The origin of injected CSS.\n         * @since Chrome 66\n         */\n        export type CSSOrigin = \"author\" | \"user\";\n\n        /**\n         * The document lifecycle of the frame.\n         * @since Chrome 106\n         */\n        export type DocumentLifecycle = \"prerender\" | \"active\" | \"cached\" | \"pending_deletion\";\n\n        /**\n         * The type of frame.\n         * @since Chrome 106\n         */\n        export type FrameType = \"outermost_frame\" | \"fenced_frame\" | \"sub_frame\";\n\n        /** Details about the format and quality of an image. */\n        export interface ImageDetails {\n            /** The format of the resulting image. Default is `\"jpeg\"`. */\n            format?: ImageFormat | undefined;\n            /** When format is `\"jpeg\"`, controls the quality of the resulting image. This value is ignored for PNG images. As quality is decreased, the resulting image will have more visual artifacts, and the number of bytes needed to store it will decrease. */\n            quality?: number | undefined;\n        }\n\n        /**\n         * The format of an image.\n         * @since Chrome 44\n         */\n        export type ImageFormat = \"jpeg\" | \"png\";\n\n        /** Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time. */\n        export interface InjectDetails {\n            /** If allFrames is `true`, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's `false` and is only injected into the top frame. If `true` and `frameId` is set, then the code is inserted in the selected frame and all of its child frames. */\n            allFrames?: boolean | undefined;\n            /**\n             * JavaScript or CSS code to inject.\n             *\n             * **Warning:** Be careful using the `code` parameter. Incorrect use of it may open your extension to cross site scripting attacks\n             */\n            code?: string | undefined;\n            /**\n             * The origin of the CSS to inject. This may only be specified for CSS, not JavaScript. Defaults to `\"author\"`.\n             * @since Chrome 66\n             */\n            cssOrigin?: CSSOrigin | undefined;\n            /** JavaScript or CSS file to inject. */\n            file?: string | undefined;\n            /**\n             * The frame where the script or CSS should be injected. Defaults to 0 (the top-level frame).\n             * @since Chrome 50\n             */\n            frameId?: number | undefined;\n            /** If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is `false`. */\n            matchAboutBlank?: boolean;\n            /** The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\". */\n            runAt?: RunAt | undefined;\n        }\n\n        /**\n         * The soonest that the JavaScript or CSS will be injected into the tab.\n         *\n         * \"document_start\" : Script is injected after any files from css, but before any other DOM is constructed or any other script is run.\n         *\n         * \"document_end\" : Script is injected immediately after the DOM is complete, but before subresources like images and frames have loaded.\n         *\n         * \"document_idle\" : The browser chooses a time to inject the script between \"document_end\" and immediately after the `window.onload` event fires. The exact moment of injection depends on how complex the document is and how long it is taking to load, and is optimized for page load speed. Content scripts running at \"document_idle\" don't need to listen for the `window.onload` event; they are guaranteed to run after the DOM completes. If a script definitely needs to run after `window.onload`, the extension can check if `onload` has already fired by using the `document.readyState` property.\n         * @since Chrome 44\n         */\n        export type RunAt = \"document_start\" | \"document_end\" | \"document_idle\";\n    }\n\n    ////////////////////\n    // File Browser Handler\n    ////////////////////\n    /**\n     * Use the `Browser.fileBrowserHandler` API to extend the Chrome OS file browser. For example, you can use this API to enable users to upload files to your website.\n     *\n     * Permissions: \"fileBrowserHandler\"\n     * @platform ChromeOS only\n     */\n    export namespace fileBrowserHandler {\n        /** Event details payload for fileBrowserHandler.onExecute event. */\n        export interface FileHandlerExecuteEventDetails {\n            /** The ID of the tab that raised this event. Tab IDs are unique within a browser session. */\n            tab_id?: number;\n            /** Array of Entry instances representing files that are targets of this action (selected in ChromeOS file browser). */\n            entries: any[];\n        }\n\n        /** Fired when file system action is executed from ChromeOS file browser. */\n        export const onExecute: events.Event<(id: string, details: FileHandlerExecuteEventDetails) => void>;\n    }\n\n    ////////////////////\n    // File System Provider\n    ////////////////////\n    /**\n     * Use the `Browser.fileSystemProvider` API to create file systems, that can be accessible from the file manager on Chrome OS.\n     *\n     * Permissions: \"fileSystemProvider\"\n     * @platform ChromeOS only\n     */\n    export namespace fileSystemProvider {\n        export interface AbortRequestedOptions {\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** An ID of the request to be aborted. */\n            operationRequestId: number;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        /** @since Chrome 45 */\n        export interface Action {\n            /** The identifier of the action. Any string or {@link CommonActionId} for common actions. */\n            id: string;\n            /** The title of the action. It may be ignored for common actions. */\n            title?: string;\n        }\n\n        export interface AddWatcherRequestedOptions {\n            /** The path of the entry to be observed. */\n            entryPath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Whether observing should include all child entries recursively. It can be true for directories only. */\n            recursive: boolean;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface Change {\n            /** The type of the change which happened to the entry. */\n            changeType: `${ChangeType}`;\n            /**\n             * Information relating to the file if backed by a cloud file system.\n             * @since Chrome 125\n             */\n            cloudFileInfo?: CloudFileInfo;\n            /** The path of the changed entry. */\n            entryPath: string;\n        }\n\n        /** Type of a change detected on the observed directory. */\n        export enum ChangeType {\n            CHANGED = \"CHANGED\",\n            DELETED = \"DELETED\",\n        }\n\n        export interface CloseFileRequestedOptions {\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** A request ID used to open the file. */\n            openRequestId: number;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        /** @since Chrome 125 */\n        export interface CloudFileInfo {\n            /** A tag that represents the version of the file. */\n            versionTag?: string;\n        }\n\n        /** @since Chrome 117 */\n        export interface CloudIdentifier {\n            /** The provider's identifier for the given file/directory. */\n            id: string;\n            /** Identifier for the cloud storage provider (e.g. 'drive.google.com'). */\n            providerName: string;\n        }\n\n        /**\n         * List of common actions. `\"SHARE\"` is for sharing files with others. `\"SAVE_FOR_OFFLINE\"` for pinning (saving for offline access). `\"OFFLINE_NOT_NECESSARY\"` for notifying that the file doesn't need to be stored for offline access anymore. Used by {@link onGetActionsRequested} and {@link onExecuteActionRequested}.\n         * @since Chrome 45\n         */\n        export enum CommonActionId {\n            SAVE_FOR_OFFLINE = \"SAVE_FOR_OFFLINE\",\n            OFFLINE_NOT_NECESSARY = \"OFFLINE_NOT_NECESSARY\",\n            SHARE = \"SHARE\",\n        }\n\n        /** @since Chrome 44 */\n        export interface ConfigureRequestedOptions {\n            /** The identifier of the file system to be configured. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface CopyEntryRequestedOptions {\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n            /** The source path of the entry to be copied. */\n            sourcePath: string;\n            /** The destination path for the copy operation. */\n            targetPath: string;\n        }\n\n        export interface CreateDirectoryRequestedOptions {\n            /** The path of the directory to be created. */\n            directoryPath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Whether the operation is recursive (for directories only). */\n            recursive: boolean;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface CreateFileRequestedOptions {\n            /** The path of the file to be created. */\n            filePath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface DeleteEntryRequestedOptions {\n            /** The path of the entry to be deleted. */\n            entryPath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Whether the operation is recursive (for directories only). */\n            recursive: boolean;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface EntryMetadata {\n            /**\n             * Information that identifies a specific file in the underlying cloud file system. Must be provided if requested in `options` and the file is backed by cloud storage.\n             * @since Chrome 125\n             */\n            cloudFileInfo?: CloudFileInfo;\n            /**\n             * Cloud storage representation of this entry. Must be provided if requested in `options` and the file is backed by cloud storage. For local files not backed by cloud storage, it should be undefined when requested.\n             * @since Chrome 117\n             */\n            cloudIdentifier?: CloudIdentifier;\n            /** True if it is a directory. Must be provided if requested in `options`. */\n            isDirectory?: boolean;\n            /** Mime type for the entry. Always optional, but should be provided if requested in `options`. */\n            mimeType?: string;\n            /** The last modified time of this entry. Must be provided if requested in `options`. */\n            modificationTime?: Date;\n            /** Name of this entry (not full path name). Must not contain '/'. For root it must be empty. Must be provided if requested in `options`. */\n            name?: string;\n            /** File size in bytes. Must be provided if requested in `options`. */\n            size?: number;\n            /** Thumbnail image as a data URI in either PNG, JPEG or WEBP format, at most 32 KB in size. Optional, but can be provided only when explicitly requested by the {@link onGetMetadataRequested} event. */\n            thumbnail?: string;\n        }\n\n        /** @since Chrome 45 */\n        export interface ExecuteActionRequestedOptions {\n            /** The identifier of the action to be executed. */\n            actionId: string;\n            /**\n             * The set of paths of the entries to be used for the action.\n             * @since Chrome 47\n             */\n            entryPaths: string[];\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface FileSystemInfo {\n            /** A human-readable name for the file system. */\n            displayName: string;\n            /** The identifier of the file system. */\n            fileSystemId: string;\n            /** List of currently opened files. */\n            openedFiles: OpenedFile[];\n            /** The maximum number of files that can be opened at once. If 0, then not limited. */\n            openedFilesLimit: number;\n            /**\n             * Whether the file system supports the `tag` field for observing directories.\n             * @since Chrome 45\n             */\n            supportsNotifyTag?: boolean;\n            /**\n             * List of watchers.\n             * @since Chrome 45\n             */\n            watchers: Watcher[];\n            /** Whether the file system supports operations which may change contents of the file system (such as creating, deleting or writing to files). */\n            writable: boolean;\n        }\n\n        /** @since Chrome 45 */\n        export interface GetActionsRequestedOptions {\n            /**\n             * List of paths of entries for the list of actions.\n             * @since Chrome 47\n             */\n            entryPaths: string[];\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface GetMetadataRequestedOptions {\n            /**\n             * Set to `true` if `cloudFileInfo` value is requested.\n             * @since Chrome 125\n             */\n            cloudFileInfo: boolean;\n            /**\n             * Set to `true` if `cloudIdentifier` value is requested.\n             * @since Chrome 117\n             */\n            cloudIdentifier: boolean;\n            /** The path of the entry to fetch metadata about. */\n            entryPath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /**\n             * Set to `true` if `is_directory` value is requested.\n             * @since Chrome 49\n             */\n            isDirectory: boolean;\n            /**\n             * Set to `true` if `mimeType` value is requested.\n             * @since Chrome 49\n             */\n            mimeType: boolean;\n            /**\n             * Set to `true` if `modificationTime` value is requested.\n             * @since Chrome 49\n             */\n            modificationTime: boolean;\n            /**\n             * Set to `true` if `name` value is requested.\n             * @since Chrome 49\n             */\n            name: boolean;\n            /** The unique identifier of this request. */\n            requestId: number;\n            /**\n             * Set to `true` if `size` value is requested.\n             * @since Chrome 49\n             */\n            size: boolean;\n            /** Set to `true` if `thumbnail` value is requested. */\n            thumbnail: boolean;\n        }\n\n        export interface MountOptions {\n            /** A human-readable name for the file system. */\n            displayName: string;\n            /** The string identifier of the file system. Must be unique per each extension. */\n            fileSystemId: string;\n            /** The maximum number of files that can be opened at once. If not specified, or 0, then not limited. */\n            openedFilesLimit?: number;\n            /**\n             * Whether the framework should resume the file system at the next sign-in session. True by default.\n             * @since Chrome 64\n             */\n            persistent?: boolean;\n            /**\n             * Whether the file system supports the `tag` field for observed directories.\n             * @since Chrome 45\n             */\n            supportsNotifyTag?: boolean;\n            /** Whether the file system supports operations which may change contents of the file system (such as creating, deleting or writing to files). */\n            writable?: boolean;\n        }\n\n        export interface MoveEntryRequestedOptions {\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n            /** The source path of the entry to be moved into a new place. */\n            sourcePath: string;\n            /** The destination path for the copy operation. */\n            targetPath: string;\n        }\n\n        export interface NotifyOptions {\n            /** The type of the change which happened to the observed entry. If it is DELETED, then the observed entry will be automatically removed from the list of observed entries. */\n            changeType: `${ChangeType}`;\n            /** List of changes to entries within the observed directory (including the entry itself) */\n            changes?: Change[];\n            /** The identifier of the file system related to this change. */\n            fileSystemId: string;\n            /** The path of the observed entry. */\n            observedPath: string;\n            /** Mode of the observed entry. */\n            recursive: boolean;\n            /** Tag for the notification. Required if the file system was mounted with the `supportsNotifyTag` option. Note, that this flag is necessary to provide notifications about changes which changed even when the system was shutdown. */\n            tag?: string;\n        }\n\n        export interface OpenedFile {\n            /** The path of the opened file. */\n            filePath: string;\n            /** Whether the file was opened for reading or writing. */\n            mode: `${OpenFileMode}`;\n            /** A request ID to be be used by consecutive read/write and close requests. */\n            openRequestId: number;\n        }\n\n        /** Mode of opening a file. Used by {@link onOpenFileRequested}. */\n        export enum OpenFileMode {\n            READ = \"READ\",\n            WRITE = \"WRITE\",\n        }\n\n        export interface OpenFileRequestedOptions {\n            /** The path of the file to be opened. */\n            filePath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Whether the file will be used for reading or writing. */\n            mode: `${OpenFileMode}`;\n            /** A request ID which will be used by consecutive read/write and close requests. */\n            requestId: number;\n        }\n\n        /** Error codes used by providing extensions in response to requests as well as in case of errors when calling methods of the API. For success, `\"OK\"` must be used.*/\n        export enum ProviderError {\n            OK = \"OK\",\n            FAILED = \"FAILED\",\n            IN_USE = \"IN_USE\",\n            EXISTS = \"EXISTS\",\n            NOT_FOUND = \"NOT_FOUND\",\n            ACCESS_DENIED = \"ACCESS_DENIED\",\n            TOO_MANY_OPENED = \"TOO_MANY_OPENED\",\n            NO_MEMORY = \"NO_MEMORY\",\n            NO_SPACE = \"NO_SPACE\",\n            NOT_A_DIRECTORY = \"NOT_A_DIRECTORY\",\n            INVALID_OPERATION = \"INVALID_OPERATION\",\n            SECURITY = \"SECURITY\",\n            ABORT = \"ABORT\",\n            NOT_A_FILE = \"NOT_A_FILE\",\n            NOT_EMPTY = \"NOT_EMPTY\",\n            INVALID_URL = \"INVALID_URL\",\n            IO = \"IO\",\n        }\n\n        export interface ReadDirectoryRequestedOptions {\n            /** The path of the directory which contents are requested. */\n            directoryPath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /**\n             * Set to `true` if `is_directory` value is requested.\n             * @since Chrome 49\n             */\n            isDirectory: boolean;\n            /**\n             * Set to `true` if `mimeType` value is requested.\n             * @since Chrome 49\n             */\n            mimeType: boolean;\n            /**\n             * Set to `true` if `modificationTime` value is requested.\n             * @since Chrome 49\n             */\n            modificationTime: boolean;\n            /**\n             * Set to `true` if `name` value is requested.\n             * @since Chrome 49\n             */\n            name: boolean;\n            /** The unique identifier of this request. */\n            requestId: number;\n            /**\n             * Set to `true` if `size` value is requested.\n             * @since Chrome 49\n             */\n            size: boolean;\n            /**\n             * Set to `true` if `thumbnail` value is requested.\n             * @since Chrome 49\n             */\n            thumbnail: boolean;\n        }\n\n        export interface ReadFileRequestedOptions {\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Number of bytes to be returned. */\n            length: number;\n            /** Position in the file (in bytes) to start reading from. */\n            offset: number;\n            /** A request ID used to open the file. */\n            openRequestId: number;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface RemoveWatcherRequestedOptions {\n            /** The path of the watched entry. */\n            entryPath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Mode of the watcher. */\n            recursive: boolean;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface TruncateRequestedOptions {\n            /** The path of the file to be truncated. */\n            filePath: string;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Number of bytes to be retained after the operation completes. */\n            length: number;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface UnmountOptions {\n            /** The identifier of the file system to be unmounted. */\n            fileSystemId: string;\n        }\n\n        export interface UnmountRequestedOptions {\n            /** The identifier of the file system to be unmounted. */\n            fileSystemId: string;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        export interface Watcher {\n            /** The path of the entry being observed. */\n            entryPath: string;\n            /** Tag used by the last notification for the watcher. */\n            lastTag?: string;\n            /** Whether watching should include all child entries recursively. It can be true for directories only. */\n            recursive: boolean;\n        }\n\n        export interface WriteFileRequestedOptions {\n            /** Buffer of bytes to be written to the file. */\n            data: ArrayBuffer;\n            /** The identifier of the file system related to this operation. */\n            fileSystemId: string;\n            /** Position in the file (in bytes) to start writing the bytes from. */\n            offset: number;\n            /** A request ID used to open the file. */\n            openRequestId: number;\n            /** The unique identifier of this request. */\n            requestId: number;\n        }\n\n        /**\n         * Returns information about a file system with the passed `fileSystemId`.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function get(fileSystemId: string): Promise<FileSystemInfo>;\n        export function get(fileSystemId: string, callback: (fileSystem: FileSystemInfo) => void): void;\n\n        /**\n         * Returns all file systems mounted by the extension.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function getAll(): Promise<FileSystemInfo[]>;\n        export function getAll(callback: (fileSystems: FileSystemInfo[]) => void): void;\n\n        /**\n         * Mounts a file system with the given `fileSystemId` and `displayName`. `displayName` will be shown in the left panel of the Files app. `displayName` can contain any characters including '/', but cannot be an empty string. `displayName` must be descriptive but doesn't have to be unique. The `fileSystemId` must not be an empty string.\n         *\n         * Depending on the type of the file system being mounted, the `source` option must be set appropriately.\n         *\n         * In case of an error, {@link runtime.lastError} will be set with a corresponding error code.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function mount(options: MountOptions): Promise<void>;\n        export function mount(options: MountOptions, callback: () => void): void;\n\n        /**\n         * Notifies about changes in the watched directory at `observedPath` in `recursive` mode. If the file system is mounted with `supportsNotifyTag`, then `tag` must be provided, and all changes since the last notification always reported, even if the system was shutdown. The last tag can be obtained with {@link getAll}.\n         *\n         * To use, the `file_system_provider.notify` manifest option must be set to true.\n         *\n         * Value of `tag` can be any string which is unique per call, so it's possible to identify the last registered notification. Eg. if the providing extension starts after a reboot, and the last registered notification's tag is equal to \"123\", then it should call {@link notify} for all changes which happened since the change tagged as \"123\". It cannot be an empty string.\n         *\n         * Not all providers are able to provide a tag, but if the file system has a changelog, then the tag can be eg. a change number, or a revision number.\n         *\n         * Note that if a parent directory is removed, then all descendant entries are also removed, and if they are watched, then the API must be notified about the fact. Also, if a directory is renamed, then all descendant entries are in fact removed, as there is no entry under their original paths anymore.\n         *\n         * In case of an error, {@link runtime.lastError} will be set will a corresponding error code.\n         *\n         * Can return its result via Promise since Chrome 96.\n         * @since Chrome 45\n         */\n        export function notify(options: NotifyOptions): Promise<void>;\n        export function notify(options: NotifyOptions, callback: () => void): void;\n\n        /**\n         * Unmounts a file system with the given `fileSystemId`. It must be called after {@link onUnmountRequested} is invoked. Also, the providing extension can decide to perform unmounting if not requested (eg. in case of lost connection, or a file error).\n         *\n         * In case of an error, {@link runtime.lastError} will be set with a corresponding error code.\n         *\n         * Can return its result via Promise since Chrome 96.\n         */\n        export function unmount(options: UnmountOptions): Promise<void>;\n        export function unmount(options: UnmountOptions, callback: () => void): void;\n\n        /** Raised when aborting an operation with `operationRequestId` is requested. The operation executed with `operationRequestId` must be immediately stopped and `successCallback` of this abort request executed. If aborting fails, then `errorCallback` must be called. Note, that callbacks of the aborted operation must not be called, as they will be ignored. Despite calling `errorCallback`, the request may be forcibly aborted. */\n        export const onAbortRequested: events.Event<\n            (\n                options: AbortRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (error: `${ProviderError}`) => void,\n            ) => void\n        >;\n\n        /**\n         * Raised when setting a new directory watcher is requested. If an error occurs, then `errorCallback` must be called.\n         * @since Chrome 45\n         */\n        export const onAddWatcherRequested: events.Event<\n            (\n                options: AddWatcherRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (error: `${ProviderError}`) => void,\n            ) => void\n        >;\n\n        /** Raised when opening a file previously opened with `openRequestId` is requested to be closed.*/\n        export const onCloseFileRequested: events.Event<\n            (\n                options: CloseFileRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (error: `${ProviderError}`) => void,\n            ) => void\n        >;\n\n        /**\n         * Raised when showing a configuration dialog for `fileSystemId` is requested. If it's handled, the `file_system_provider.configurable` manifest option must be set to true.\n         * @since Chrome 44\n         */\n        export const onConfigureRequested: events.Event<\n            (\n                options: ConfigureRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (error: `${ProviderError}`) => void,\n            ) => void\n        >;\n\n        /** Raised when copying an entry (recursively if a directory) is requested. If an error occurs, then `errorCallback` must be called. */\n        export const onCopyEntryRequested: events.Event<\n            (\n                options: CopyEntryRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (error: `${ProviderError}`) => void,\n            ) => void\n        >;\n\n        /** Raised when creating a directory is requested. The operation must fail with the EXISTS error if the target directory already exists. If `recursive` is true, then all of the missing directories on the directory path must be created. */\n        export const onCreateDirectoryRequested: events.Event<\n            (\n                options: CreateDirectoryRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when creating a file is requested. If the file already exists, then `errorCallback` must be called with the `\"EXISTS\"` error code. */\n        export const onCreateFileRequested: events.Event<\n            (\n                options: CreateFileRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when deleting an entry is requested. If `recursive` is true, and the entry is a directory, then all of the entries inside must be recursively deleted as well. */\n        export const onDeleteEntryRequested: events.Event<\n            (\n                options: DeleteEntryRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /**\n         * Raised when executing an action for a set of files or directories is\\\\ requested. After the action is completed, `successCallback` must be called. On error, `errorCallback` must be called.\n         * @since Chrome 48\n         */\n        export const onExecuteActionRequested: events.Event<\n            (\n                options: ExecuteActionRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /**\n         * Raised when a list of actions for a set of files or directories at `entryPaths` is requested. All of the returned actions must be applicable to each entry. If there are no such actions, an empty array should be returned. The actions must be returned with the `successCallback` call. In case of an error, `errorCallback` must be called.\n         * @since Chrome 48\n         */\n        export const onGetActionsRequested: events.Event<\n            (\n                options: GetActionsRequestedOptions,\n                successCallback: (\n                    actions: Action[],\n                ) => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when metadata of a file or a directory at `entryPath` is requested. The metadata must be returned with the `successCallback` call. In case of an error, `errorCallback` must be called. */\n        export const onGetMetadataRequested: events.Event<\n            (\n                options: GetMetadataRequestedOptions,\n                successCallback: (\n                    metadata: EntryMetadata,\n                ) => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /**\n         * Raised when showing a dialog for mounting a new file system is requested. If the extension/app is a file handler, then this event shouldn't be handled. Instead `app.runtime.onLaunched` should be handled in order to mount new file systems when a file is opened. For multiple mounts, the `file_system_provider.multiple_mounts` manifest option must be set to true.\n         * @since Chrome 44\n         */\n        export const onMountRequested: events.Event<\n            (\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when moving an entry (recursively if a directory) is requested. If an error occurs, then `errorCallback` must be called. */\n        export const onMoveEntryRequested: events.Event<\n            (\n                options: MoveEntryRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when opening a file at `filePath` is requested. If the file does not exist, then the operation must fail. Maximum number of files opened at once can be specified with `MountOptions`. */\n        export const onOpenFileRequested: events.Event<\n            (\n                options: OpenFileRequestedOptions,\n                successCallback: (\n                    /**\n                     * @since Chrome 125\n                     */\n                    metadata?: EntryMetadata,\n                ) => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when contents of a directory at `directoryPath` are requested. The results must be returned in chunks by calling the `successCallback` several times. In case of an error, `errorCallback` must be called. */\n        export const onReadDirectoryRequested: events.Event<\n            (\n                options: ReadDirectoryRequestedOptions,\n                successCallback: (\n                    entries: EntryMetadata[],\n                    hasMore: boolean,\n                ) => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when reading contents of a file opened previously with `openRequestId` is requested. The results must be returned in chunks by calling `successCallback` several times. In case of an error, `errorCallback` must be called. */\n        export const onReadFileRequested: events.Event<\n            (\n                options: ReadFileRequestedOptions,\n                successCallback: (\n                    data: ArrayBuffer,\n                    hasMore: boolean,\n                ) => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /**\n         * Raised when the watcher should be removed. If an error occurs, then `errorCallback` must be called.\n         * @since Chrome 45\n         */\n        export const onRemoveWatcherRequested: events.Event<\n            (\n                options: RemoveWatcherRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when truncating a file to a desired length is requested. If an error occurs, then `errorCallback` must be called. */\n        export const onTruncateRequested: events.Event<\n            (\n                options: TruncateRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when unmounting for the file system with the `fileSystemId` identifier is requested. In the response, the {@link unmount} API method must be called together with `successCallback`. If unmounting is not possible (eg. due to a pending operation), then `errorCallback` must be called. */\n        export const onUnmountRequested: events.Event<\n            (\n                options: UnmountRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n\n        /** Raised when writing contents to a file opened previously with `openRequestId` is requested. */\n        export const onWriteFileRequested: events.Event<\n            (\n                options: WriteFileRequestedOptions,\n                successCallback: () => void,\n                errorCallback: (\n                    error: `${ProviderError}`,\n                ) => void,\n            ) => void\n        >;\n    }\n\n    ////////////////////\n    // Font Settings\n    ////////////////////\n    /**\n     * Use the `Browser.fontSettings` API to manage Chrome's font settings.\n     *\n     * Permissions: \"fontSettings\"\n     */\n    export namespace fontSettings {\n        /** Represents a font name. */\n        export interface FontName {\n            /** The display name of the font. */\n            displayName: string;\n            /** The font ID. */\n            fontId: string;\n        }\n\n        /** A CSS generic font family. */\n        export enum GenericFamily {\n            STANDARD = \"standard\",\n            SANSSERIF = \"sansserif\",\n            SERIF = \"serif\",\n            FIXED = \"fixed\",\n            CURSIVE = \"cursive\",\n            FANTASY = \"fantasy\",\n            MATH = \"math\",\n        }\n\n        export enum LevelOfControl {\n            /** Cannot be controlled by any extension */\n            NOT_CONTROLLABLE = \"not_controllable\",\n            /** Controlled by extensions with higher precedence */\n            CONTROLLED_BY_OTHER_EXTENSIONS = \"controlled_by_other_extensions\",\n            /** Can be controlled by this extension */\n            CONTROLLABLE_BY_THIS_EXTENSION = \"controllable_by_this_extension\",\n            /** Controlled by this extension */\n            CONTROLLED_BY_THIS_EXTENSION = \"controlled_by_this_extension\",\n        }\n\n        /** An ISO 15924 script code. The default, or global, script is represented by script code \"Zyyy\". */\n        export enum ScriptCode {\n            AFAK = \"Afak\",\n            ARAB = \"Arab\",\n            ARMI = \"Armi\",\n            ARMN = \"Armn\",\n            AVST = \"Avst\",\n            BALI = \"Bali\",\n            BAMU = \"Bamu\",\n            BASS = \"Bass\",\n            BATK = \"Batk\",\n            BENG = \"Beng\",\n            BLIS = \"Blis\",\n            BOPO = \"Bopo\",\n            BRAH = \"Brah\",\n            BRAI = \"Brai\",\n            BUGI = \"Bugi\",\n            BUHD = \"Buhd\",\n            CAKM = \"Cakm\",\n            CANS = \"Cans\",\n            CARI = \"Cari\",\n            CHAM = \"Cham\",\n            CHER = \"Cher\",\n            CIRT = \"Cirt\",\n            COPT = \"Copt\",\n            CPRT = \"Cprt\",\n            CYRL = \"Cyrl\",\n            CYRS = \"Cyrs\",\n            DEVA = \"Deva\",\n            DSRT = \"Dsrt\",\n            DUPL = \"Dupl\",\n            EGYD = \"Egyd\",\n            EGYH = \"Egyh\",\n            EGYP = \"Egyp\",\n            ELBA = \"Elba\",\n            ETHI = \"Ethi\",\n            GEOK = \"Geok\",\n            GEOR = \"Geor\",\n            GLAG = \"Glag\",\n            GOTH = \"Goth\",\n            GRAN = \"Gran\",\n            GREK = \"Grek\",\n            GUJR = \"Gujr\",\n            GURU = \"Guru\",\n            HANG = \"Hang\",\n            HANI = \"Hani\",\n            HANO = \"Hano\",\n            HANS = \"Hans\",\n            HANT = \"Hant\",\n            HEBR = \"Hebr\",\n            HLUW = \"Hluw\",\n            HMNG = \"Hmng\",\n            HUNG = \"Hung\",\n            INDS = \"Inds\",\n            ITAL = \"Ital\",\n            JAVA = \"Java\",\n            JPAN = \"Jpan\",\n            JURC = \"Jurc\",\n            KALI = \"Kali\",\n            KHAR = \"Khar\",\n            KHMR = \"Khmr\",\n            KHOJ = \"Khoj\",\n            KNDA = \"Knda\",\n            KPEL = \"Kpel\",\n            KTHI = \"Kthi\",\n            LANA = \"Lana\",\n            LAOO = \"Laoo\",\n            LATF = \"Latf\",\n            LATG = \"Latg\",\n            LATN = \"Latn\",\n            LEPC = \"Lepc\",\n            LIMB = \"Limb\",\n            LINA = \"Lina\",\n            LINB = \"Linb\",\n            LISU = \"Lisu\",\n            LOMA = \"Loma\",\n            LYCI = \"Lyci\",\n            LYDI = \"Lydi\",\n            MAND = \"Mand\",\n            MANI = \"Mani\",\n            MAYA = \"Maya\",\n            MEND = \"Mend\",\n            MERC = \"Merc\",\n            MERO = \"Mero\",\n            MLYM = \"Mlym\",\n            MONG = \"Mong\",\n            MOON = \"Moon\",\n            MROO = \"Mroo\",\n            MTEI = \"Mtei\",\n            MYMR = \"Mymr\",\n            NARB = \"Narb\",\n            NBAT = \"Nbat\",\n            NKGB = \"Nkgb\",\n            NKOO = \"Nkoo\",\n            NSHU = \"Nshu\",\n            OGAM = \"Ogam\",\n            OLCK = \"Olck\",\n            ORKH = \"Orkh\",\n            ORYA = \"Orya\",\n            OSMA = \"Osma\",\n            PALM = \"Palm\",\n            PERM = \"Perm\",\n            PHAG = \"Phag\",\n            PHLI = \"Phli\",\n            PHLP = \"Phlp\",\n            PHLV = \"Phlv\",\n            PHNX = \"Phnx\",\n            PLRD = \"Plrd\",\n            PRTI = \"Prti\",\n            RJNG = \"Rjng\",\n            RORO = \"Roro\",\n            RUNR = \"Runr\",\n            SAMR = \"Samr\",\n            SARA = \"Sara\",\n            SARB = \"Sarb\",\n            SAUR = \"Saur\",\n            SGNW = \"Sgnw\",\n            SHAW = \"Shaw\",\n            SHRD = \"Shrd\",\n            SIND = \"Sind\",\n            SINH = \"Sinh\",\n            SORA = \"Sora\",\n            SUND = \"Sund\",\n            SYLO = \"Sylo\",\n            SYRC = \"Syrc\",\n            SYRE = \"Syre\",\n            SYRJ = \"Syrj\",\n            SYRN = \"Syrn\",\n            TAGB = \"Tagb\",\n            TAKR = \"Takr\",\n            TALE = \"Tale\",\n            TALU = \"Talu\",\n            TAML = \"Taml\",\n            TANG = \"Tang\",\n            TAVT = \"Tavt\",\n            TELU = \"Telu\",\n            TENG = \"Teng\",\n            TFNG = \"Tfng\",\n            TGLG = \"Tglg\",\n            THAA = \"Thaa\",\n            THAI = \"Thai\",\n            TIBT = \"Tibt\",\n            TIRH = \"Tirh\",\n            UGAR = \"Ugar\",\n            VAII = \"Vaii\",\n            VISP = \"Visp\",\n            WARA = \"Wara\",\n            WOLE = \"Wole\",\n            XPEO = \"Xpeo\",\n            XSUX = \"Xsux\",\n            YIII = \"Yiii\",\n            ZMTH = \"Zmth\",\n            ZSYM = \"Zsym\",\n            ZYYY = \"Zyyy\",\n        }\n\n        export interface ClearFontDetails {\n            /** The generic font family for which the font should be cleared. */\n            genericFamily: `${GenericFamily}`;\n            /** The script for which the font should be cleared. If omitted, the global script font setting is cleared. */\n            script?: `${ScriptCode}` | undefined;\n        }\n\n        export interface GetFontDetails {\n            /** The generic font family for which the font should be retrieved. */\n            genericFamily: `${GenericFamily}`;\n            /** The script for which the font should be retrieved. If omitted, the font setting for the global script (script code \"Zyyy\") is retrieved. */\n            script?: `${ScriptCode}` | undefined;\n        }\n\n        export interface SetFontDetails {\n            /** The font ID. The empty string means to fallback to the global script font setting. */\n            fontId: string;\n            /** The generic font family for which the font should be set. */\n            genericFamily: `${GenericFamily}`;\n            /** The script code which the font should be set. If omitted, the font setting for the global script (script code \"Zyyy\") is set. */\n            script?: `${ScriptCode}` | undefined;\n        }\n\n        export interface FontChangedResult {\n            /** The generic font family for which the font setting has changed. */\n            genericFamily: `${GenericFamily}`;\n            /** The level of control this extension has over the setting. */\n            levelOfControl: `${LevelOfControl}`;\n            /** Optional. The script code for which the font setting has changed.  */\n            script?: `${ScriptCode}`;\n            /** The font ID. See the description in {@link getFont}. */\n            fontId: string;\n        }\n\n        export interface FontResult {\n            /** The level of control this extension has over the setting. */\n            levelOfControl: `${LevelOfControl}`;\n            /** The font ID. Rather than the literal font ID preference value, this may be the ID of the font that the system resolves the preference value to. So, `fontId` can differ from the font passed to {@link setFont}, if, for example, the font is not available on the system. The empty string signifies fallback to the global script font setting. */\n            fontId: string;\n        }\n\n        export interface FontSizeResult {\n            /** The font size in pixels. */\n            pixelSize: number;\n            /** The level of control this extension has over the setting. */\n            levelOfControl: `${LevelOfControl}`;\n        }\n\n        export interface FontSizeDetails {\n            /** The font size in pixels. */\n            pixelSize: number;\n        }\n\n        /**\n         * Sets the default font size.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function setDefaultFontSize(details: FontSizeDetails): Promise<void>;\n        export function setDefaultFontSize(details: FontSizeDetails, callback: () => void): void;\n\n        /**\n         * Gets the font for a given script and generic font family.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getFont(details: GetFontDetails): Promise<FontResult>;\n        export function getFont(details: GetFontDetails, callback: (details: FontResult) => void): void;\n\n        /**\n         * Gets the default font size.\n         * @param details This parameter is currently unused.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getDefaultFontSize(details?: { [key: string]: unknown }): Promise<FontSizeResult>;\n        export function getDefaultFontSize(callback: (options: FontSizeResult) => void): void;\n        export function getDefaultFontSize(\n            details: { [key: string]: unknown } | undefined,\n            callback: (options: FontSizeResult) => void,\n        ): void;\n\n        /**\n         * Gets the minimum font size.\n         * @param details This parameter is currently unused.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getMinimumFontSize(details?: { [key: string]: unknown }): Promise<FontSizeResult>;\n        export function getMinimumFontSize(callback: (options: FontSizeResult) => void): void;\n        export function getMinimumFontSize(\n            details: { [key: string]: unknown } | undefined,\n            callback: (options: FontSizeResult) => void,\n        ): void;\n\n        /**\n         * Sets the minimum font size.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function setMinimumFontSize(details: FontSizeDetails): Promise<void>;\n        export function setMinimumFontSize(details: FontSizeDetails, callback: () => void): void;\n\n        /**\n         * Gets the default size for fixed width fonts.\n         * @param details This parameter is currently unused.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getDefaultFixedFontSize(details?: { [key: string]: unknown }): Promise<FontSizeResult>;\n        export function getDefaultFixedFontSize(callback: (details: FontSizeResult) => void): void;\n        export function getDefaultFixedFontSize(\n            details: { [key: string]: unknown } | undefined,\n            callback: (details: FontSizeResult) => void,\n        ): void;\n\n        /**\n         * Clears the default font size set by this extension, if any.\n         * @param details This parameter is currently unused.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function clearDefaultFontSize(details?: { [key: string]: unknown }): Promise<void>;\n        export function clearDefaultFontSize(callback: () => void): void;\n        export function clearDefaultFontSize(\n            details: { [key: string]: unknown } | undefined,\n            callback: () => void,\n        ): void;\n\n        /**\n         * Sets the default size for fixed width fonts.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function setDefaultFixedFontSize(details: FontSizeDetails): Promise<void>;\n        export function setDefaultFixedFontSize(details: FontSizeDetails, callback: () => void): void;\n\n        /**\n         * Clears the font set by this extension, if any.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function clearFont(details: ClearFontDetails): Promise<void>;\n        export function clearFont(details: ClearFontDetails, callback: () => void): void;\n\n        /**\n         * Sets the font for a given script and generic font family.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function setFont(details: SetFontDetails): Promise<void>;\n        export function setFont(details: SetFontDetails, callback: () => void): void;\n\n        /**\n         * Clears the minimum font size set by this extension, if any.\n         * @param details This parameter is currently unused.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function clearMinimumFontSize(details?: { [key: string]: unknown }): Promise<void>;\n        export function clearMinimumFontSize(callback: () => void): void;\n        export function clearMinimumFontSize(\n            details: { [key: string]: unknown } | undefined,\n            callback: () => void,\n        ): void;\n\n        /**\n         * Gets a list of fonts on the system.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getFontList(): Promise<FontName[]>;\n        export function getFontList(callback: (results: FontName[]) => void): void;\n\n        /**\n         * Clears the default fixed font size set by this extension, if any.\n         * @param details This parameter is currently unused.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function clearDefaultFixedFontSize(details?: { [key: string]: unknown }): Promise<void>;\n        export function clearDefaultFixedFontSize(callback: () => void): void;\n        export function clearDefaultFixedFontSize(\n            details: { [key: string]: unknown } | undefined,\n            callback: () => void,\n        ): void;\n\n        /** Fired when the default fixed font size setting changes. */\n        export const onDefaultFixedFontSizeChanged: events.Event<(details: FontSizeResult) => void>;\n\n        /** Fired when the default font size setting changes. */\n        export const onDefaultFontSizeChanged: events.Event<(details: FontSizeResult) => void>;\n\n        /** Fired when the minimum font size setting changes. */\n        export const onMinimumFontSizeChanged: events.Event<(details: FontSizeResult) => void>;\n\n        /** Fired when a font setting changes. */\n        export const onFontChanged: events.Event<(details: FontChangedResult) => void>;\n    }\n\n    ////////////////////\n    // Google Cloud Messaging\n    ////////////////////\n    /**\n     * Use `Browser.gcm` to enable apps and extensions to send and receive messages through Firebase Cloud Messaging (FCM).\n     *\n     * Permissions: \"gcm\"\n     */\n    export namespace gcm {\n        export interface OutgoingMessage {\n            /** The ID of the server to send the message to as assigned by Google API Console. */\n            destinationId: string;\n            /** The ID of the message. It must be unique for each message in scope of the applications. See the Cloud Messaging documentation for advice for picking and handling an ID. */\n            messageId: string;\n            /** Time-to-live of the message in seconds. If it is not possible to send the message within that time, an onSendError event will be raised. A time-to-live of 0 indicates that the message should be sent immediately or fail if it's not possible. The default value of time-to-live is 86,400 seconds (1 day) and the maximum value is 2,419,200 seconds (28 days). */\n            timeToLive?: number | undefined;\n            /** Message data to send to the server. Case-insensitive `goog.` and `google`, as well as case-sensitive `collapse_key` are disallowed as key prefixes. Sum of all key/value pairs should not exceed {@link gcm.MAX_MESSAGE_SIZE}. */\n            data: { [key: string]: unknown };\n        }\n\n        /** The maximum size (in bytes) of all key/value pairs in a message. */\n        export const MAX_MESSAGE_SIZE: 4096;\n\n        /**\n         * Registers the application with FCM. The registration ID will be returned by the `callback`. If `register` is called again with the same list of `senderIds`, the same registration ID will be returned.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         * @param senderIds A list of server IDs that are allowed to send messages to the application. It should contain at least one and no more than 100 sender IDs.\n         */\n        export function register(senderIds: string[]): Promise<string>;\n        export function register(senderIds: string[], callback: (registrationId: string) => void): void;\n\n        /**\n         * Unregister the application from FCM.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function unregister(): Promise<void>;\n        export function unregister(callback: () => void): void;\n\n        /**\n         * Sends a message according to its contents.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         * @param message A message to send to the other party via FCM.\n         */\n        export function send(message: OutgoingMessage): Promise<string>;\n        export function send(message: OutgoingMessage, callback: (messageId: string) => void): void;\n\n        /** Fired when a message is received through FCM. */\n        export const onMessage: events.Event<\n            (message: {\n                /** The collapse key of a message. See the Non-collapsible and collapsible messages for details. */\n                collapseKey?: string;\n                /** The message data. */\n                data: { [key: string]: unknown };\n                /** The sender who issued the message. */\n                from?: string;\n            }) => void\n        >;\n\n        /** Fired when a FCM server had to delete messages sent by an app server to the application. See Lifetime of a message for details on handling this event. */\n        export const onMessagesDeleted: events.Event<() => void>;\n\n        /** Fired when it was not possible to send a message to the FCM server. */\n        export const onSendError: events.Event<\n            (error: {\n                /** Additional details related to the error, when available. */\n                details: { [key: string]: unknown };\n                /** The error message describing the problem. */\n                errorMessage: string;\n                /** The ID of the message with this error, if error is related to a specific message. */\n                messageId?: string;\n            }) => void\n        >;\n    }\n\n    ////////////////////\n    // History\n    ////////////////////\n    /**\n     * Use the `Browser.history` API to interact with the browser's record of visited pages. You can add, remove, and query for URLs in the browser's history. To override the history page with your own version, see Override Pages.\n     *\n     * Permissions: \"history\"\n     */\n    export namespace history {\n        /** An object encapsulating one visit to a URL. */\n        export interface VisitItem {\n            /** The transition type for this visit from its referrer. */\n            transition: `${TransitionType}`;\n            /**\n             * True if the visit originated on this device. False if it was synced from a different device\n             * @since Chrome 115\n             */\n            isLocal: boolean;\n            /** When this visit occurred, represented in milliseconds since the epoch. */\n            visitTime?: number;\n            /** The unique identifier for this visit. */\n            visitId: string;\n            /** The visit ID of the referrer. */\n            referringVisitId: string;\n            /** The unique identifier for the corresponding {@link history.HistoryItem}. */\n            id: string;\n        }\n\n        /** An object encapsulating one result of a history query. */\n        export interface HistoryItem {\n            /** The number of times the user has navigated to this page by typing in the address. */\n            typedCount?: number;\n            /** The title of the page when it was last loaded. */\n            title?: string;\n            /** The URL navigated to by a user. */\n            url?: string;\n            /** When this page was last loaded, represented in milliseconds since the epoch. */\n            lastVisitTime?: number;\n            /** The number of times the user has navigated to this page. */\n            visitCount?: number;\n            /** The unique identifier for the item. */\n            id: string;\n        }\n\n        /**\n         * The transition type for this visit from its referrer.\n         * @since Chrome 44\n         */\n        export enum TransitionType {\n            /** The user arrived at this page by clicking a link on another page. */\n            LINK = \"link\",\n            /** The user arrived at this page by typing the URL in the address bar. This is also used for other explicit navigation actions. */\n            TYPED = \"typed\",\n            /** The user arrived at this page through a suggestion in the UI, for example, through a menu item. */\n            AUTO_BOOKMARK = \"auto_bookmark\",\n            /** The user arrived at this page through subframe navigation that they didn't request, such as through an ad loading in a frame on the previous page. These don't always generate new navigation entries in the back and forward menus. */\n            AUTO_SUBFRAME = \"auto_subframe\",\n            /** The user arrived at this page by selecting something in a subframe. */\n            MANUAL_SUBFRAME = \"manual_subframe\",\n            /** The user arrived at this page by typing in the address bar and selecting an entry that didn't look like a URL, such as a Google Search suggestion. For example, a match might have the URL of a Google Search result page, but it might appear to the user as \"Search Google for ...\". These are different from typed navigations because the user didn't type or see the destination URL. They're also related to keyword navigations. */\n            GENERATED = \"generated\",\n            /** The page was specified in the command line or is the start page. */\n            AUTO_TOPLEVEL = \"auto_toplevel\",\n            /** The user arrived at this page by filling out values in a form and submitting the form. Not all form submissions use this transition type. */\n            FORM_SUBMIT = \"form_submit\",\n            /** The user reloaded the page, either by clicking the reload button or by pressing Enter in the address bar. Session restore and Reopen closed tab also use this transition type. */\n            RELOAD = \"reload\",\n            /** The URL for this page was generated from a replaceable keyword other than the default search provider. */\n            KEYWORD = \"keyword\",\n            /** Corresponds to a visit generated for a keyword. */\n            KEYWORD_GENERATED = \"keyword_generated\",\n        }\n\n        export interface HistoryQuery {\n            /** A free-text query to the history service. Leave this empty to retrieve all pages. */\n            text: string;\n            /** The maximum number of results to retrieve. Defaults to 100. */\n            maxResults?: number | undefined;\n            /** Limit results to those visited after this date, represented in milliseconds since the epoch. If property is not specified, it will default to 24 hours. */\n            startTime?: number | undefined;\n            /** Limit results to those visited before this date, represented in milliseconds since the epoch. */\n            endTime?: number | undefined;\n        }\n\n        /** @since Chrome 88 */\n        export interface UrlDetails {\n            /** The URL for the operation. It must be in the format as returned from a call to {@link history.search}. */\n            url: string;\n        }\n\n        export interface Range {\n            /** Items added to history before this date, represented in milliseconds since the epoch. */\n            endTime: number;\n            /** Items added to history after this date, represented in milliseconds since the epoch. */\n            startTime: number;\n        }\n\n        export interface RemovedResult {\n            /** True if all history was removed. If true, then urls will be empty. */\n            allHistory: boolean;\n            urls?: string[];\n        }\n\n        /**\n         * Searches the history for the last visit time of each page matching the query.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function search(query: HistoryQuery): Promise<HistoryItem[]>;\n        export function search(query: HistoryQuery, callback: (results: HistoryItem[]) => void): void;\n\n        /**\n         * Adds a URL to the history at the current time with a transition type of \"link\".\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function addUrl(details: UrlDetails): Promise<void>;\n        export function addUrl(details: UrlDetails, callback: () => void): void;\n\n        /**\n         * Removes all items within the specified date range from the history. Pages will not be removed from the history unless all visits fall within the range.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function deleteRange(range: Range): Promise<void>;\n        export function deleteRange(range: Range, callback: () => void): void;\n\n        /**\n         * Deletes all items from the history.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function deleteAll(): Promise<void>;\n        export function deleteAll(callback: () => void): void;\n\n        /**\n         * Retrieves information about visits to a URL.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getVisits(details: UrlDetails): Promise<VisitItem[]>;\n        export function getVisits(details: UrlDetails, callback: (results: VisitItem[]) => void): void;\n\n        /**\n         * Removes all occurrences of the given URL from the history.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function deleteUrl(details: UrlDetails): Promise<void>;\n        export function deleteUrl(details: UrlDetails, callback: () => void): void;\n\n        /** Fired when a URL is visited, providing the {@link HistoryItem} data for that URL. This event fires before the page has loaded. */\n        export const onVisited: events.Event<(result: HistoryItem) => void>;\n\n        /** Fired when one or more URLs are removed from history. When all visits have been removed the URL is purged from history. */\n        export const onVisitRemoved: events.Event<(removed: RemovedResult) => void>;\n    }\n\n    ////////////////////\n    // i18n\n    ////////////////////\n    /**\n     * Use the `Browser.i18n` infrastructure to implement internationalization across your whole app or extension.\n     *\n     * Manifest: \"default_locale\"\n     */\n    export namespace i18n {\n        export interface DetectedLanguage {\n            language: string;\n            /** The percentage of the detected language */\n            percentage: number;\n        }\n\n        /** Holds detected language reliability and array of {@link DetectedLanguage} */\n        export interface LanguageDetectionResult {\n            /** CLD detected language reliability */\n            isReliable: boolean;\n            /** Array of detectedLanguage */\n            languages: DetectedLanguage[];\n        }\n\n        /** @since Chrome 79 */\n        export interface GetMessageOptions {\n            /** Escape `<` in translation to `&lt;`. This applies only to the message itself, not to the placeholders. Developers might want to use this if the translation is used in an HTML context. Closure Templates used with Closure Compiler generate this automatically. */\n            escapeLt?: boolean | undefined;\n        }\n\n        /**\n         * Gets the accept-languages of the browser. This is different from the locale used by the browser; to get the locale, use {@link i18n.getUILanguage}.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         */\n        export function getAcceptLanguages(): Promise<string[]>;\n        export function getAcceptLanguages(callback: (languages: string[]) => void): void;\n\n        /**\n         * Gets the localized string for the specified message. If the message is missing, this method returns an empty string (''). If the format of the `getMessage()` call is wrong — for example, messageName is not a string or the substitutions array has more than 9 elements — this method returns `undefined`.\n         * @param messageName The name of the message, as specified in the `messages.json` file.\n         * @param substitutions Up to 9 substitution strings, if the message requires any.\n         */\n        export function getMessage(messageName: string, substitutions?: string | Array<string | number>): string;\n        export function getMessage(\n            messageName: string,\n            substitutions: string | Array<string | number> | undefined,\n            options?: GetMessageOptions,\n        ): string;\n\n        /** Gets the browser UI language of the browser. This is different from {@link i18n.getAcceptLanguages} which returns the preferred user languages. */\n        export function getUILanguage(): string;\n\n        /** Detects the language of the provided text using CLD.\n         * @param text User input string to be translated.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         * @since Chrome 47\n         */\n        export function detectLanguage(text: string): Promise<LanguageDetectionResult>;\n        export function detectLanguage(text: string, callback: (result: LanguageDetectionResult) => void): void;\n    }\n\n    ////////////////////\n    // Identity\n    ////////////////////\n    /**\n     * Use the `Browser.identity` API to get OAuth2 access tokens.\n     *\n     * Permissions: \"identity\"\n     */\n    export namespace identity {\n        export interface AccountInfo {\n            /** A unique identifier for the account. This ID will not change for the lifetime of the account. */\n            id: string;\n        }\n\n        /** @since Chrome 84 */\n        export enum AccountStatus {\n            /** Specifies that Sync is enabled for the primary account. */\n            SYNC = \"SYNC\",\n            /** Specifies the existence of a primary account, if any. */\n            ANY = \"ANY\",\n        }\n\n        /** @since Chrome 84 */\n        export interface ProfileDetails {\n            /** A status of the primary account signed into a profile whose `ProfileUserInfo` should be returned. Defaults to `SYNC` account status. */\n            accountStatus?: `${AccountStatus}`;\n        }\n\n        export interface TokenDetails {\n            /** Fetching a token may require the user to sign-in to Chrome, or approve the application's requested scopes. If the interactive flag is `true`, `getAuthToken` will prompt the user as necessary. When the flag is `false` or omitted, `getAuthToken` will return failure any time a prompt would be required. */\n            interactive?: boolean;\n            /** The account ID whose token should be returned. If not specified, the function will use an account from the Chrome profile: the Sync account if there is one, or otherwise the first Google web account. */\n            account?: AccountInfo;\n            /**\n             * The `enableGranularPermissions` flag allows extensions to opt-in early to the granular permissions consent screen, in which requested permissions are granted or denied individually.\n             * @since Chrome 87\n             */\n            enableGranularPermissions?: boolean;\n            /**\n             * A list of OAuth2 scopes to request.\n             *\n             * When the `scopes` field is present, it overrides the list of scopes specified in manifest.json.\n             */\n            scopes?: string[];\n        }\n\n        export interface ProfileUserInfo {\n            /** An email address for the user account signed into the current profile. Empty if the user is not signed in or the `identity.email` manifest permission is not specified. */\n            email: string;\n            /** A unique identifier for the account. This ID will not change for the lifetime of the account. Empty if the user is not signed in or (in M41+) the `identity.email` manifest permission is not specified. */\n            id: string;\n        }\n\n        export interface InvalidTokenDetails {\n            /** The specific token that should be removed from the cache. */\n            token: string;\n        }\n\n        export interface WebAuthFlowDetails {\n            /** The URL that initiates the auth flow. */\n            url: string;\n\n            /**\n             * Whether to launch auth flow in interactive mode.\n             *\n             * Since some auth flows may immediately redirect to a result URL, `launchWebAuthFlow` hides its web view until the first navigation either redirects to the final URL, or finishes loading a page meant to be displayed.\n             *\n             * If the `interactive` flag is `true`, the window will be displayed when a page load completes. If the flag is `false` or omitted, `launchWebAuthFlow` will return with an error if the initial navigation does not complete the flow.\n             *\n             * For flows that use JavaScript for redirection, `abortOnLoadForNonInteractive` can be set to `false` in combination with setting `timeoutMsForNonInteractive` to give the page a chance to perform any redirects.\n             */\n            interactive?: boolean;\n            /**\n             * Whether to terminate `launchWebAuthFlow` for non-interactive requests after the page loads. This parameter does not affect interactive flows.\n             *\n             * When set to `true` (default) the flow will terminate immediately after the page loads. When set to `false`, the flow will only terminate after the `timeoutMsForNonInteractive` passes. This is useful for identity providers that use JavaScript to perform redirections after the page loads.\n             * @since Chrome 113\n             */\n            abortOnLoadForNonInteractive?: boolean;\n            /**\n             * The maximum amount of time, in milliseconds, `launchWebAuthFlow` is allowed to run in non-interactive mode in total. Only has an effect if `interactive` is `false`.\n             * @since Chrome 113\n             */\n            timeoutMsForNonInteractive?: number;\n        }\n\n        /** @since Chrome 105 */\n        export interface GetAuthTokenResult {\n            /** A list of OAuth2 scopes granted to the extension. */\n            grantedScopes?: string[];\n            /** The specific token associated with the request. */\n            token?: string;\n        }\n\n        /**\n         * Resets the state of the Identity API:\n         *\n         *  * Removes all OAuth2 access tokens from the token cache\n         *  * Removes user's account preferences\n         *  * De-authorizes the user from all auth flows\n         *\n         * Can return its result via Promise since Chrome 106.\n         * @since Chrome 87\n         */\n        export function clearAllCachedAuthTokens(): Promise<void>;\n        export function clearAllCachedAuthTokens(callback: () => void): void;\n\n        /**\n         * Retrieves a list of AccountInfo objects describing the accounts present on the profile.\n         *\n         * getAccounts is only supported on dev channel.\n         */\n        export function getAccounts(): Promise<AccountInfo[]>;\n        export function getAccounts(callback: (accounts: AccountInfo[]) => void): void;\n\n        /**\n         * Gets an OAuth2 access token using the client ID and scopes specified in the oauth2 section of manifest.json.\n         *\n         * The Identity API caches access tokens in memory, so it's ok to call getAuthToken non-interactively any time a token is required. The token cache automatically handles expiration.\n         *\n         * For a good user experience it is important interactive token requests are initiated by UI in your app explaining what the authorization is for. Failing to do this will cause your users to get authorization requests, or Chrome sign in screens if they are not signed in, with with no context. In particular, do not use getAuthToken interactively when your app is first launched.\n         * @param details Token options.\n         *\n         * Can return its result via Promise since Chrome 105.\n         */\n        export function getAuthToken(details?: TokenDetails): Promise<GetAuthTokenResult>;\n        export function getAuthToken(details: TokenDetails, callback: (result: GetAuthTokenResult) => void): void;\n        export function getAuthToken(callback: (result: GetAuthTokenResult) => void): void;\n\n        /**\n         * Retrieves email address and obfuscated gaia id of the user signed into a profile.\n         *\n         * Requires the `identity.email` manifest permission. Otherwise, returns an empty result.\n         *\n         * This API is different from identity.getAccounts in two ways. The information returned is available offline, and it only applies to the primary account for the profile.\n         * @param details Profile options.\n         *\n         * Can return its result via Promise since Chrome 105.\n         */\n        export function getProfileUserInfo(details?: ProfileDetails): Promise<ProfileUserInfo>;\n        export function getProfileUserInfo(\n            details: ProfileDetails,\n            callback: (userInfo: ProfileUserInfo) => void,\n        ): void;\n        export function getProfileUserInfo(callback: (userInfo: ProfileUserInfo) => void): void;\n\n        /**\n         * Removes an OAuth2 access token from the Identity API's token cache.\n         *\n         * If an access token is discovered to be invalid, it should be passed to removeCachedAuthToken to remove it from the cache. The app may then retrieve a fresh token with `getAuthToken`.\n         * @param details Token information.\n         *\n         * Can return its result via Promise since Chrome 105.\n         */\n        export function removeCachedAuthToken(details: InvalidTokenDetails): Promise<void>;\n        export function removeCachedAuthToken(details: InvalidTokenDetails, callback: () => void): void;\n\n        /**\n         * Starts an auth flow at the specified URL.\n         *\n         * This method enables auth flows with non-Google identity providers by launching a web view and navigating it to the first URL in the provider's auth flow. When the provider redirects to a URL matching the pattern `https://<app-id>.chromiumapp.org/*`, the window will close, and the final redirect URL will be passed to the `callback` function.\n         *\n         * For a good user experience it is important interactive auth flows are initiated by UI in your app explaining what the authorization is for. Failing to do this will cause your users to get authorization requests with no context. In particular, do not launch an interactive auth flow when your app is first launched.\n         * @param details WebAuth flow options.\n         *\n         * Can return its result via Promise since Chrome 106\n         */\n        export function launchWebAuthFlow(details: WebAuthFlowDetails): Promise<string | undefined>;\n        export function launchWebAuthFlow(details: WebAuthFlowDetails, callback: (responseUrl?: string) => void): void;\n\n        /**\n         * Generates a redirect URL to be used in `launchWebAuthFlow`.\n         *\n         * The generated URLs match the pattern `https://<app-id>.chromiumapp.org/*`.\n         * @param path The path appended to the end of the generated URL.\n         */\n        export function getRedirectURL(path?: string): string;\n\n        /** Fired when signin state changes for an account on the user's profile. */\n        export const onSignInChanged: Browser.events.Event<(account: AccountInfo, signedIn: boolean) => void>;\n    }\n\n    ////////////////////\n    // Idle\n    ////////////////////\n    /**\n     * Use the `Browser.idle` API to detect when the machine's idle state changes.\n     *\n     * Permissions: \"idle\"\n     */\n    export namespace idle {\n        /** @since Chrome 44 */\n        export enum IdleState {\n            ACTIVE = \"active\",\n            IDLE = \"idle\",\n            LOCKED = \"locked\",\n        }\n\n        /**\n         * Returns \"locked\" if the system is locked, \"idle\" if the user has not generated any input for a specified number of seconds, or \"active\" otherwise.\n         * @param detectionIntervalInSeconds The system is considered idle if detectionIntervalInSeconds seconds have elapsed since the last user input detected.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function queryState(detectionIntervalInSeconds: number): Promise<`${IdleState}`>;\n        export function queryState(\n            detectionIntervalInSeconds: number,\n            callback: (newState: `${IdleState}`) => void,\n        ): void;\n\n        /**\n         * Sets the interval, in seconds, used to determine when the system is in an idle state for onStateChanged events. The default interval is 60 seconds.\n         * @param intervalInSeconds Threshold, in seconds, used to determine when the system is in an idle state.\n         */\n        export function setDetectionInterval(intervalInSeconds: number): void;\n\n        /**\n         * Gets the time, in seconds, it takes until the screen is locked automatically while idle. Returns a zero duration if the screen is never locked automatically.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         * @since Chrome 73\n         * @platform ChromeOS only\n         */\n        export function getAutoLockDelay(): Promise<number>;\n        export function getAutoLockDelay(callback: (delay: number) => void): void;\n\n        /** Fired when the system changes to an active, idle or locked state. The event fires with \"locked\" if the screen is locked or the screensaver activates, \"idle\" if the system is unlocked and the user has not generated any input for a specified number of seconds, and \"active\" when the user generates input on an idle system. */\n        export const onStateChanged: events.Event<(newState: `${IdleState}`) => void>;\n    }\n\n    ////////////////////\n    // Input - IME\n    ////////////////////\n    /**\n     * Use the `Browser.input.ime` API to implement a custom IME for Chrome OS. This allows your extension to handle keystrokes, set the composition, and manage the candidate window.\n     *\n     * Permissions: \"input\"\n     * @platform ChromeOS only\n     */\n    export namespace input.ime {\n        /** See https://www.w3.org/TR/uievents/#events-KeyboardEvent */\n        export interface KeyboardEvent {\n            /** Whether or not the SHIFT key is pressed. */\n            shiftKey?: boolean | undefined;\n            /** Whether or not the ALT key is pressed. */\n            altKey?: boolean | undefined;\n            /**\n             * Whether or not the ALTGR key is pressed.\n             * @since Chrome 79\n             */\n            altgrKey?: boolean | undefined;\n            /**\n             * The ID of the request\n             * @deprecated Use the `requestId` param from the `onKeyEvent` event instead.\n             */\n            requestId?: string | undefined;\n            /** Value of the key being pressed. */\n            key: string;\n            /** Whether or not the CTRL key is pressed. */\n            ctrlKey?: boolean | undefined;\n            /** One of keyup or keydown. */\n            type: `${KeyboardEventType}`;\n            /** The extension ID of the sender of this keyevent. */\n            extensionId?: string | undefined;\n            /** Value of the physical key being pressed. The value is not affected by current keyboard layout or modifier state. */\n            code: string;\n            /** The deprecated HTML keyCode, which is system- and implementation-dependent numerical code signifying the unmodified identifier associated with the key pressed. */\n            keyCode?: number | undefined;\n            /** Whether or not the CAPS_LOCK is enabled. */\n            capsLock?: boolean | undefined;\n        }\n\n        /** @since Chrome 44 */\n        export enum KeyboardEventType {\n            KEYUP = \"keyup\",\n            KEYDOWN = \"keydown\",\n        }\n\n        /**\n         * The auto-capitalize type of the text field.\n         * @since Chrome 69\n         */\n        export enum AutoCapitalizeType {\n            CHARACTERS = \"characters\",\n            WORDS = \"words\",\n            SENTENCES = \"sentences\",\n        }\n\n        /**\n         * Type of value this text field edits, (Text, Number, URL, etc)\n         * @since Chrome 44\n         */\n        export enum InputContextType {\n            TEXT = \"text\",\n            SEARCH = \"search\",\n            TEL = \"tel\",\n            URL = \"url\",\n            EMAIL = \"email\",\n            NUMBER = \"number\",\n            PASSWORD = \"password\",\n            NULL = \"null\",\n        }\n\n        /** Describes an input Context */\n        export interface InputContext {\n            /** This is used to specify targets of text field operations. This ID becomes invalid as soon as onBlur is called. */\n            contextID: number;\n            /** Type of value this text field edits, (Text, Number, URL, etc) */\n            type: `${InputContextType}`;\n            /** Whether the text field wants auto-correct. */\n            autoCorrect: boolean;\n            /** Whether the text field wants auto-complete. */\n            autoComplete: boolean;\n            /** Whether the text field wants spell-check. */\n            spellCheck: boolean;\n            /**\n             * The auto-capitalize type of the text field.\n             * @since Chrome 69\n             */\n            autoCapitalize: `${AutoCapitalizeType}`;\n            /**\n             * Whether text entered into the text field should be used to improve typing suggestions for the user.\n             * @since Chrome 68\n             */\n            shouldDoLearning: boolean;\n        }\n\n        /** A menu item used by an input method to interact with the user from the language menu. */\n        export interface MenuItem {\n            /** String that will be passed to callbacks referencing this MenuItem. */\n            id: string;\n            /** Text displayed in the menu for this item. */\n            label?: string | undefined;\n            /** The type of menu item. */\n            style?: `${MenuItemStyle}` | undefined;\n            /** Indicates this item is visible. */\n            visible?: boolean | undefined;\n            /** Indicates this item should be drawn with a check. */\n            checked?: boolean | undefined;\n            /** Indicates this item is enabled. */\n            enabled?: boolean | undefined;\n        }\n\n        /**\n         * The type of menu item. Radio buttons between separators are considered grouped.\n         * @since Chrome 44\n         */\n        export enum MenuItemStyle {\n            CHECK = \"check\",\n            RADIO = \"radio\",\n            SEPARATOR = \"separator\",\n        }\n\n        export interface CommitTextParameters {\n            /** The text to commit */\n            text: string;\n            /** ID of the context where the text will be committed */\n            contextID: number;\n        }\n\n        export interface CandidateUsage {\n            /** The title string of details description. */\n            title: string;\n            /** The body string of detail description. */\n            body: string;\n        }\n\n        export interface CandidateTemplate {\n            /** The candidate */\n            candidate: string;\n            /** The candidate's id */\n            id: number;\n            /** The id to add these candidates under */\n            parentId?: number | undefined;\n            /** Short string displayed to next to the candidate, often the shortcut key or index */\n            label?: string | undefined;\n            /** Additional text describing the candidate */\n            annotation?: string | undefined;\n            /** The usage or detail description of word. */\n            usage?: CandidateUsage | undefined;\n        }\n\n        export interface CandidatesParameters {\n            /** ID of the context that owns the candidate window. */\n            contextID: number;\n            /** List of candidates to show in the candidate window */\n            candidates: CandidateTemplate[];\n        }\n\n        export interface CompositionParameterSegment {\n            /** Index of the character to start this segment at */\n            start: number;\n            /** Index of the character to end this segment after. */\n            end: number;\n            /** The type of the underline to modify this segment. */\n            style: `${UnderlineStyle}`;\n        }\n\n        export interface CompositionParameters {\n            /** ID of the context where the composition text will be set */\n            contextID: number;\n            /** Text to set */\n            text: string;\n            /** List of segments and their associated types. */\n            segments?: CompositionParameterSegment[] | undefined;\n            /** Position in the text of the cursor. */\n            cursor: number;\n            /** Position in the text that the selection starts at. */\n            selectionStart?: number | undefined;\n            /** Position in the text that the selection ends at. */\n            selectionEnd?: number | undefined;\n        }\n\n        /** @since Chrome 88 */\n        export interface MenuParameters {\n            /** MenuItems to add or update. They will be added in the order they exist in the array. */\n            items: MenuItem[];\n            /** ID of the engine to use. */\n            engineID: string;\n        }\n\n        /**\n         * Which mouse buttons was clicked.\n         * @since Chrome 44\n         */\n        export enum MouseButton {\n            LEFT = \"left\",\n            MIDDLE = \"middle\",\n            RIGHT = \"right\",\n        }\n\n        /**\n         * The screen type under which the IME is activated.\n         * @since Chrome 44\n         */\n        export enum ScreenType {\n            NORMAL = \"normal\",\n            LOGIN = \"login\",\n            LOCK = \"lock\",\n            SECONDARY_LOGIN = \"secondary-login\",\n        }\n\n        /**\n         * The type of the underline to modify this segment.\n         * @since Chrome 44\n         */\n        export enum UnderlineStyle {\n            UNDERLINE = \"underline\",\n            DOUBLE_UNDERLINE = \"doubleUnderline\",\n            NO_UNDERLINE = \"noUnderline\",\n        }\n\n        /**\n         * Where to display the candidate window. If set to 'cursor', the window follows the cursor. If set to 'composition', the window is locked to the beginning of the composition.\n         * @since Chrome 44\n         */\n        export enum WindowPosition {\n            CURSOR = \"cursor\",\n            COMPOSITION = \"composition\",\n        }\n\n        /** Type of assistive window. */\n        export enum AssistiveWindowType {\n            UNDO = \"undo\",\n        }\n\n        /**\n         * ID of a button in an assistive window.\n         * @since Chrome 85\n         */\n        export enum AssistiveWindowButton {\n            UNDO = \"undo\",\n            ADD_TO_DICTIONARY = \"addToDictionary\",\n        }\n\n        /**\n         * Properties of the assistive window.\n         * @since Chrome 85\n         */\n        export interface AssistiveWindowProperties {\n            type: `${AssistiveWindowType}`;\n            /** Sets true to show AssistiveWindow, sets false to hide. */\n            visible: boolean;\n            /** Strings for ChromeVox to announce */\n            announceString?: string | undefined;\n        }\n\n        export interface CandidateWindowParameterProperties {\n            /** True to show the cursor, false to hide it. */\n            cursorVisible?: boolean | undefined;\n            /** True if the candidate window should be rendered vertical, false to make it horizontal. */\n            vertical?: boolean | undefined;\n            /** The number of candidates to display per page. */\n            pageSize?: number | undefined;\n            /** True to display the auxiliary text, false to hide it. */\n            auxiliaryTextVisible?: boolean | undefined;\n            /** Text that is shown at the bottom of the candidate window. */\n            auxiliaryText?: string | undefined;\n            /** True to show the Candidate window, false to hide it. */\n            visible?: boolean | undefined;\n            /** Where to display the candidate window. */\n            windowPosition?: `${WindowPosition}` | undefined;\n            /**\n             * The index of the current chosen candidate out of total candidates.\n             * @since Chrome 84\n             */\n            currentCandidateIndex?: number | undefined;\n            /**\n             * The total number of candidates for the candidate window.\n             * @since Chrome 84\n             */\n            totalCandidates?: number | undefined;\n        }\n\n        export interface CandidateWindowParameter {\n            /** ID of the engine to set properties on. */\n            engineID: string;\n            properties: CandidateWindowParameterProperties;\n        }\n\n        export interface ClearCompositionParameters {\n            /** ID of the context where the composition will be cleared */\n            contextID: number;\n        }\n\n        export interface CursorPositionParameters {\n            /** ID of the candidate to select. */\n            candidateID: number;\n            /** ID of the context that owns the candidate window. */\n            contextID: number;\n        }\n\n        export interface SendKeyEventParameters {\n            /** ID of the context where the key events will be sent, or zero to send key events to non-input field. */\n            contextID: number;\n            /** Data on the key event. */\n            keyData: KeyboardEvent[];\n        }\n\n        export interface DeleteSurroundingTextParameters {\n            /** ID of the engine receiving the event. */\n            engineID: string;\n            /** ID of the context where the surrounding text will be deleted. */\n            contextID: number;\n            /** The offset from the caret position where deletion will start. This value can be negative. */\n            offset: number;\n            /** The number of characters to be deleted */\n            length: number;\n        }\n\n        export interface AssistiveWindowButtonHighlightedParameters {\n            /** The text for the screenreader to announce. */\n            announceString?: string | undefined;\n            /** The ID of the button */\n            buttonID: `${AssistiveWindowButton}`;\n            /** ID of the context owning the assistive window. */\n            contextID: number;\n            /** Whether the button should be highlighted. */\n            highlighted: boolean;\n            /** The window type the button belongs to. */\n            windowType: `${AssistiveWindowType}`;\n        }\n\n        export interface AssistiveWindowPropertiesParameters {\n            /** ID of the context owning the assistive window. */\n            contextID: number;\n            /** Properties of the assistive window. */\n            properties: AssistiveWindowProperties;\n        }\n\n        export interface SurroundingTextInfo {\n            /** The text around the cursor. This is only a subset of all text in the input field. */\n            text: string;\n            /** The ending position of the selection. This value indicates caret position if there is no selection. */\n            focus: number;\n            /**\n             * The offset position of `text`. Since `text` only includes a subset of text around the cursor, offset indicates the absolute position of the first character of `text`.\n             * @since Chrome 46\n             */\n            offset: number;\n            /** The beginning position of the selection. This value indicates caret position if there is no selection. */\n            anchor: number;\n        }\n\n        export interface AssistiveWindowButtonClickedDetails {\n            /** The ID of the button clicked. */\n            buttonID: `${AssistiveWindowButton}`;\n            /** The type of the assistive window. */\n            windowType: `${AssistiveWindowType}`;\n        }\n\n        /**\n         * Adds the provided menu items to the language menu when this IME is active.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setMenuItems(parameters: MenuParameters): Promise<void>;\n        export function setMenuItems(parameters: MenuParameters, callback: () => void): void;\n\n        /**\n         * Commits the provided text to the current input.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function commitText(parameters: CommitTextParameters): Promise<boolean>;\n        export function commitText(parameters: CommitTextParameters, callback: (success: boolean) => void): void;\n\n        /**\n         * Sets the current candidate list. This fails if this extension doesn't own the active IME\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setCandidates(parameters: CandidatesParameters): Promise<boolean>;\n        export function setCandidates(parameters: CandidatesParameters, callback: (success: boolean) => void): void;\n\n        /**\n         * Set the current composition. If this extension does not own the active IME, this fails.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setComposition(parameters: CompositionParameters): Promise<boolean>;\n        export function setComposition(parameters: CompositionParameters, callback: (success: boolean) => void): void;\n\n        /**\n         * Updates the state of the MenuItems specified\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function updateMenuItems(parameters: MenuParameters): Promise<void>;\n        export function updateMenuItems(parameters: MenuParameters, callback: () => void): void;\n\n        /**\n         * Shows/Hides an assistive window with the given properties.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setAssistiveWindowProperties(\n            parameters: AssistiveWindowPropertiesParameters,\n        ): Promise<boolean>;\n        export function setAssistiveWindowProperties(\n            parameters: AssistiveWindowPropertiesParameters,\n            callback: (success: boolean) => void,\n        ): void;\n\n        /**\n         * Highlights/Unhighlights a button in an assistive window.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setAssistiveWindowButtonHighlighted(\n            parameters: AssistiveWindowButtonHighlightedParameters,\n        ): Promise<void>;\n        export function setAssistiveWindowButtonHighlighted(\n            parameters: AssistiveWindowButtonHighlightedParameters,\n            callback: () => void,\n        ): void;\n\n        /**\n         * Sets the properties of the candidate window. This fails if the extension doesn't own the active IME\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setCandidateWindowProperties(parameters: CandidateWindowParameter): Promise<boolean>;\n        export function setCandidateWindowProperties(\n            parameters: CandidateWindowParameter,\n            callback: (success: boolean) => void,\n        ): void;\n\n        /**\n         * Clear the current composition. If this extension does not own the active IME, this fails.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function clearComposition(parameters: ClearCompositionParameters): Promise<boolean>;\n        export function clearComposition(\n            parameters: ClearCompositionParameters,\n            callback: (success: boolean) => void,\n        ): void;\n\n        /**\n         * Set the position of the cursor in the candidate window. This is a no-op if this extension does not own the active IME.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function setCursorPosition(\n            parameters: CursorPositionParameters,\n        ): Promise<boolean>;\n        export function setCursorPosition(\n            parameters: CursorPositionParameters,\n            callback: (success: boolean) => void,\n        ): void;\n\n        /**\n         * Sends the key events. This function is expected to be used by virtual keyboards. When key(s) on a virtual keyboard is pressed by a user, this function is used to propagate that event to the system.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function sendKeyEvents(parameters: SendKeyEventParameters): Promise<void>;\n        export function sendKeyEvents(parameters: SendKeyEventParameters, callback: () => void): void;\n\n        /** Hides the input view window, which is popped up automatically by system. If the input view window is already hidden, this function will do nothing. */\n        export function hideInputView(): void;\n\n        /**\n         * Deletes the text around the caret.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 111.\n         */\n        export function deleteSurroundingText(parameters: DeleteSurroundingTextParameters): Promise<void>;\n        export function deleteSurroundingText(parameters: DeleteSurroundingTextParameters, callback: () => void): void;\n\n        /**\n         * Indicates that the key event received by onKeyEvent is handled. This should only be called if the onKeyEvent listener is asynchronous.\n         * @param requestId Request id of the event that was handled. This should come from keyEvent.requestId\n         * @param response True if the keystroke was handled, false if not\n         */\n        export function keyEventHandled(requestId: string, response: boolean): void;\n\n        /** This event is sent when focus leaves a text box. It is sent to all extensions that are listening to this event, and enabled by the user. */\n        export const onBlur: events.Event<(contextID: number) => void>;\n\n        /**\n         * This event is sent when a button in an assistive window is clicked.\n         * @since Chrome 85\n         */\n        export const onAssistiveWindowButtonClicked: events.Event<\n            (details: AssistiveWindowButtonClickedDetails) => void\n        >;\n\n        /** This event is sent if this extension owns the active IME. */\n        export const onCandidateClicked: events.Event<\n            (engineID: string, candidateID: number, button: `${MouseButton}`) => void\n        >;\n\n        /** Fired when a key event is sent from the operating system. The event will be sent to the extension if this extension owns the active IME. The listener function should return true if the event was handled false if it was not. If the event will be evaluated asynchronously, this function must return undefined and the IME must later call keyEventHandled() with the result. */\n        export const onKeyEvent: events.Event<(engineID: string, keyData: KeyboardEvent, requestId: string) => void>;\n\n        /** This event is sent when an IME is deactivated. It signals that the IME will no longer be receiving onKeyPress events. */\n        export const onDeactivated: events.Event<(engineID: string) => void>;\n\n        /** This event is sent when the properties of the current InputContext change, such as the the type. It is sent to all extensions that are listening to this event, and enabled by the user. */\n        export const onInputContextUpdate: events.Event<(context: InputContext) => void>;\n\n        /** This event is sent when an IME is activated. It signals that the IME will be receiving onKeyPress events. */\n        export const onActivate: events.Event<(engineID: string, screen: `${ScreenType}`) => void>;\n\n        // /** This event is sent when focus enters a text box. It is sent to all extensions that are listening to this event, and enabled by the user. */\n        export const onFocus: events.Event<(context: InputContext) => void>;\n\n        /** Called when the user selects a menu item */\n        export const onMenuItemActivated: events.Event<(engineID: string, name: string) => void>;\n\n        /** Called when the editable string around caret is changed or when the caret position is moved. The text length is limited to 100 characters for each back and forth direction. */\n        export const onSurroundingTextChanged: events.Event<\n            (engineID: string, surroundingInfo: SurroundingTextInfo) => void\n        >;\n\n        /** This event is sent when chrome terminates ongoing text input session. */\n        export const onReset: events.Event<(engineID: string) => void>;\n    }\n\n    ////////////////////\n    // Instance ID\n    ////////////////////\n    /**\n     * Use `Browser.instanceID` to access the Instance ID service.\n     *\n     * Permissions: \"gcm\"\n     * @since Chrome 44\n     */\n    export namespace instanceID {\n        /**\n         * Resets the app instance identifier and revokes all tokens associated with it.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function deleteID(): Promise<void>;\n        export function deleteID(callback: () => void): void;\n\n        /** Parameters for {@link deleteToken}. */\n        interface DeleteTokenParams {\n            /**\n             * The authorized entity that is used to obtain the token.\n             * @since Chrome 46\n             */\n            authorizedEntity: string;\n            /**\n             * The scope that is used to obtain the token.\n             * @since Chrome 46\n             */\n            scope: string;\n        }\n\n        /**\n         * Revokes a granted token.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function deleteToken(deleteTokenParams: DeleteTokenParams): Promise<void>;\n        export function deleteToken(\n            deleteTokenParams: DeleteTokenParams,\n            callback: () => void,\n        ): void;\n\n        /**\n         * Retrieves the time when the InstanceID has been generated.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @return The time when the Instance ID has been generated, represented in milliseconds since the epoch.\n         */\n        export function getCreationTime(): Promise<number>;\n        export function getCreationTime(callback: (creationTime: number) => void): void;\n\n        /**\n         * Retrieves an identifier for the app instance.\n         * The same ID will be returned as long as the application identity has not been revoked or expired.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @return An Instance ID assigned to the app instance.\n         */\n        export function getID(): Promise<string>;\n        export function getID(callback: (instanceID: string) => void): void;\n\n        /** Parameters for {@link getToken}. */\n        interface GetTokenParams {\n            /**\n             * Identifies the entity that is authorized to access resources associated with this Instance ID. It can be a project ID from Google developer console.\n             * @since Chrome 46\n             */\n            authorizedEntity: string;\n            /**\n             * Allows including a small number of string key/value pairs that will be associated with the token and may be used in processing the request.\n             * @deprecated since Chrome 89. `options` are deprecated and will be ignored.\n             */\n            options?: { [key: string]: string };\n            /**\n             * Identifies authorized actions that the authorized entity can take. E.g. for sending GCM messages, `GCM` scope should be used.\n             * @since Chrome 46\n             */\n            scope: string;\n        }\n        /**\n         * Return a token that allows the authorized entity to access the service defined by scope.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @return A token assigned by the requested service.\n         */\n        export function getToken(getTokenParams: GetTokenParams): Promise<string>;\n        export function getToken(getTokenParams: GetTokenParams, callback: (token: string) => void): void;\n\n        export const onTokenRefresh: events.Event<() => void>;\n    }\n\n    ////////////////////\n    // LoginState\n    ////////////////////\n    /**\n     * Use the `Browser.loginState` API to read and monitor the login state.\n     *\n     * Permissions: \"loginState\"\n     * @platform ChromeOS only\n     * @since Chrome 78\n     */\n    export namespace loginState {\n        export enum ProfileType {\n            /** Specifies that the extension is in the signin profile. */\n            SIGNIN_PROFILE = \"SIGNIN_PROFILE\",\n            /** Specifies that the extension is in the user profile. */\n            USER_PROFILE = \"USER_PROFILE\",\n            /** Specifies that the extension is in the lock screen profile. */\n            LOCK_PROFILE = \"LOCK_PROFILE\",\n        }\n\n        export enum SessionState {\n            /** Specifies that the session state is unknown. */\n            UNKNOWN = \"UNKNOWN\",\n            /** Specifies that the user is in the out-of-box-experience screen. */\n            IN_OOBE_SCREEN = \"IN_OOBE_SCREEN\",\n            /** Specifies that the user is in the login screen. */\n            IN_LOGIN_SCREEN = \"IN_LOGIN_SCREEN\",\n            /** Specifies that the user is in the session. */\n            IN_SESSION = \"IN_SESSION\",\n            /** Specifies that the user is in the lock screen. */\n            IN_LOCK_SCREEN = \"IN_LOCK_SCREEN\",\n            /** Specifies that the device is in RMA mode, finalizing repairs. */\n            IN_RMA_SCREEN = \"IN_RMA_SCREEN\",\n        }\n\n        /**\n         * Gets the type of the profile the extension is in.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getProfileType(): Promise<`${ProfileType}`>;\n        export function getProfileType(callback: (result: `${ProfileType}`) => void): void;\n\n        /**\n         * Gets the current session state.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getSessionState(): Promise<`${SessionState}`>;\n        export function getSessionState(callback: (sessionState: `${SessionState}`) => void): void;\n\n        /** Dispatched when the session state changes. `sessionState` is the new session state.*/\n        export const onSessionStateChanged: events.Event<(sessionState: `${SessionState}`) => void>;\n    }\n\n    ////////////////////\n    // Management\n    ////////////////////\n    /**\n     * The `Browser.management` API provides ways to manage installed apps and extensions.\n     *\n     * Permissions: \"management\"\n     */\n    export namespace management {\n        /**\n         * A reason the item is disabled.\n         * @since Chrome 44\n         */\n        export enum ExtensionDisabledReason {\n            UNKNOWN = \"unknown\",\n            PERMISSIONS_INCREASE = \"permissions_increase\",\n        }\n\n        /** Information about an installed extension, app, or theme. */\n        export interface ExtensionInfo {\n            /** A reason the item is disabled. */\n            disabledReason?: `${ExtensionDisabledReason}`;\n            /** The launch url (only present for apps). */\n            appLaunchUrl?: string;\n            /** The description of this extension, app, or theme. */\n            description: string;\n            /** Returns a list of API based permissions. */\n            permissions: string[];\n            /** A list of icon information. Note that this just reflects what was declared in the manifest, and the actual image at that url may be larger or smaller than what was declared, so you might consider using explicit width and height attributes on img tags referencing these images. See the manifest documentation on icons for more details. */\n            icons?: IconInfo[];\n            /** Returns a list of host based permissions. */\n            hostPermissions: string[];\n            /** Whether it is currently enabled or disabled. */\n            enabled: boolean;\n            /** The URL of the homepage of this extension, app, or theme. */\n            homepageUrl?: string;\n            /** Whether this extension can be disabled or uninstalled by the user. */\n            mayDisable: boolean;\n            /**\n             * Whether this extension can be enabled by the user. This is only returned for extensions which are not enabled.\n             * @since Chrome 62\n             */\n            mayEnable?: boolean;\n            /** How the extension was installed. */\n            installType: `${ExtensionInstallType}`;\n            /** The version of this extension, app, or theme. */\n            version: string;\n            /**\n             * The version name of this extension, app, or theme if the manifest specified one.\n             * @since Chrome 50\n             */\n            versionName?: string;\n            /** The extension's unique identifier. */\n            id: string;\n            /** Whether the extension, app, or theme declares that it supports offline. */\n            offlineEnabled: boolean;\n            /** The update URL of this extension, app, or theme. */\n            updateUrl?: string;\n            /** The type of this extension, app, or theme. */\n            type: `${ExtensionType}`;\n            /** The url for the item's options page, if it has one. */\n            optionsUrl: string;\n            /** The name of this extension, app, or theme. */\n            name: string;\n            /** A short version of the name of this extension, app, or theme. */\n            shortName: string;\n            /**\n             * True if this is an app.\n             * @deprecated since Chrome 33. Please use {@link management.ExtensionInfo.type}.\n             */\n            isApp: boolean;\n            /** The app launch type (only present for apps). */\n            launchType?: `${LaunchType}`;\n            /** The currently available launch types (only present for apps). */\n            availableLaunchTypes?: `${LaunchType}`[];\n        }\n\n        /**\n         * How the extension was installed\n         * @since Chrome 44\n         */\n        export enum ExtensionInstallType {\n            /** The extension was installed because of an administrative policy. */\n            ADMIN = \"admin\",\n            /** The extension was loaded unpacked in developer mode. */\n            DEVELOPMENT = \"development\",\n            /** The extension was installed normally via a .crx file. */\n            NORMAL = \"normal\",\n            /** The extension was installed by other software on the machine. */\n            SIDELOAD = \"sideload\",\n            /** The extension was installed by other means. */\n            OTHER = \"other\",\n        }\n\n        /**\n         * The type of this extension, app, or theme.\n         * @since Chrome 44\n         */\n        export enum ExtensionType {\n            EXTENSION = \"extension\",\n            HOSTED_APP = \"hosted_app\",\n            PACKAGE_APP = \"package_app\",\n            LEGACY_PACKAGED_APP = \"legacy_packaged_app\",\n            THEME = \"theme\",\n            LOGIN_SCREEN_EXTENSION = \"login_screen_extension\",\n        }\n\n        /** Information about an icon belonging to an extension, app, or theme. */\n        export interface IconInfo {\n            /** The URL for this icon image. To display a grayscale version of the icon (to indicate that an extension is disabled, for example), append `?grayscale=true` to the URL. */\n            url: string;\n            /** A number representing the width and height of the icon. Likely values include (but are not limited to) 128, 48, 24, and 16. */\n            size: number;\n        }\n\n        /** These are all possible app launch types. */\n        export enum LaunchType {\n            OPEN_AS_REGULAR_TAB = \"OPEN_AS_REGULAR_TAB\",\n            OPEN_AS_PINNED_TAB = \"OPEN_AS_PINNED_TAB\",\n            OPEN_AS_WINDOW = \"OPEN_AS_WINDOW\",\n            OPEN_FULL_SCREEN = \"OPEN_FULL_SCREEN\",\n        }\n\n        /**\n         * Options for how to handle the extension's uninstallation.\n         * @since Chrome 88\n         */\n        export interface UninstallOptions {\n            /** Whether or not a confirm-uninstall dialog should prompt the user. Defaults to false for self uninstalls. If an extension uninstalls another extension, this parameter is ignored and the dialog is always shown. */\n            showConfirmDialog?: boolean | undefined;\n        }\n\n        /**\n         * Enables or disables an app or extension. In most cases this function must be called in the context of a user gesture (e.g. an onclick handler for a button), and may present the user with a native confirmation UI as a way of preventing abuse.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id This should be the id from an item of {@link management.ExtensionInfo}.\n         * @param enabled Whether this item should be enabled or disabled.\n         */\n        export function setEnabled(id: string, enabled: boolean): Promise<void>;\n        export function setEnabled(id: string, enabled: boolean, callback: () => void): void;\n\n        /**\n         * Returns a list of permission warnings for the given extension id.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id The ID of an already installed extension.\n         */\n        export function getPermissionWarningsById(id: string): Promise<string[]>;\n        export function getPermissionWarningsById(id: string, callback: (permissionWarnings: string[]) => void): void;\n\n        /**\n         * Returns information about the installed extension, app, or theme that has the given ID.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id The ID from an item of {@link management.ExtensionInfo}.\n         */\n        export function get(id: string): Promise<ExtensionInfo>;\n        export function get(id: string, callback: (result: ExtensionInfo) => void): void;\n\n        /**\n         * Returns a list of information about installed extensions and apps.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function getAll(): Promise<ExtensionInfo[]>;\n        export function getAll(callback: (result: ExtensionInfo[]) => void): void;\n\n        /**\n         * Returns a list of permission warnings for the given extension manifest string. Note: This function can be used without requesting the 'management' permission in the manifest.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param manifestStr Extension manifest JSON string.\n         */\n        export function getPermissionWarningsByManifest(manifestStr: string): Promise<string[]>;\n        export function getPermissionWarningsByManifest(\n            manifestStr: string,\n            callback: (permissionWarnings: string[]) => void,\n        ): void;\n\n        /**\n         * Launches an application.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id The extension id of the application.\n         */\n        export function launchApp(id: string): Promise<void>;\n        export function launchApp(id: string, callback: () => void): void;\n\n        /**\n         * Uninstalls a currently installed app or extension. Note: This function does not work in managed environments when the user is not allowed to uninstall the specified extension/app. If the uninstall fails (e.g. the user cancels the dialog) the promise will be rejected or the callback will be called with {@link runtime.lastError} set.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id This should be the id from an item of {@link management.ExtensionInfo}.\n         */\n        export function uninstall(id: string, options?: UninstallOptions): Promise<void>;\n        export function uninstall(id: string, callback: () => void): void;\n        export function uninstall(id: string, options: UninstallOptions | undefined, callback: () => void): void;\n\n        /**\n         * Returns information about the calling extension, app, or theme. Note: This function can be used without requesting the 'management' permission in the manifest.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function getSelf(): Promise<ExtensionInfo>;\n        export function getSelf(callback: (result: ExtensionInfo) => void): void;\n\n        /**\n         * Launches the replacement_web_app specified in the manifest. Prompts the user to install if not already installed.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @since Chrome 77\n         */\n        export function installReplacementWebApp(): Promise<void>;\n        export function installReplacementWebApp(callback: () => void): void;\n\n        /**\n         * Uninstalls the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest. This function does not work in managed environments when the user is not allowed to uninstall the specified extension/app.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function uninstallSelf(options?: UninstallOptions): Promise<void>;\n        export function uninstallSelf(callback: () => void): void;\n        export function uninstallSelf(options: UninstallOptions | undefined, callback: () => void): void;\n\n        /**\n         * Display options to create shortcuts for an app. On Mac, only packaged app shortcuts can be created.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id This should be the id from an app item of {@link management.ExtensionInfo}.\n         */\n        export function createAppShortcut(id: string): Promise<void>;\n        export function createAppShortcut(id: string, callback: () => void): void;\n\n        /**\n         * Set the launch type of an app.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param id This should be the id from an app item of {@link management.ExtensionInfo}.\n         * @param launchType The target launch type. Always check and make sure this launch type is in {@link ExtensionInfo.availableLaunchTypes}, because the available launch types vary on different platforms and configurations.\n         */\n        export function setLaunchType(id: string, launchType: `${LaunchType}`): Promise<void>;\n        export function setLaunchType(id: string, launchType: `${LaunchType}`, callback: () => void): void;\n\n        /**\n         * Generate an app for a URL. Returns the generated bookmark app.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param url The URL of a web page. The scheme of the URL can only be \"http\" or \"https\".\n         * @param title The title of the generated app.\n         */\n        export function generateAppForLink(url: string, title: string): Promise<ExtensionInfo>;\n        export function generateAppForLink(url: string, title: string, callback: (result: ExtensionInfo) => void): void;\n\n        /** Fired when an app or extension has been disabled. */\n        export const onDisabled: events.Event<(info: ExtensionInfo) => void>;\n\n        /** Fired when an app or extension has been uninstalled. */\n        export const onUninstalled: events.Event<(id: string) => void>;\n\n        /** Fired when an app or extension has been installed. */\n        export const onInstalled: events.Event<(info: ExtensionInfo) => void>;\n\n        /** Fired when an app or extension has been enabled. */\n        export const onEnabled: events.Event<(info: ExtensionInfo) => void>;\n    }\n\n    ////////////////////\n    // Notifications\n    ////////////////////\n    /**\n     * Use the `Browser.notifications` API to create rich notifications using templates and show these notifications to users in the system tray.\n     *\n     * Permissions: \"notifications\"\n     */\n    export namespace notifications {\n        export interface NotificationButton {\n            /** @deprecated since Chrome 59. Button icons not visible for Mac OS X users. */\n            iconUrl?: string;\n            title: string;\n        }\n\n        export interface NotificationItem {\n            /** Additional details about this item. */\n            message: string;\n            /** Title of one item of a list notification. */\n            title: string;\n        }\n\n        export interface NotificationOptions {\n            /**\n             * A URL to the app icon mask. URLs have the same restrictions as {@link notifications.NotificationOptions.iconUrl iconUrl}.\n             *\n             * The app icon mask should be in alpha channel, as only the alpha channel of the image will be considered.\n             * @deprecated since Chrome 59. The app icon mask is not visible for Mac OS X users.\n             */\n            appIconMaskUrl?: string;\n            /** Text and icons for up to two notification action buttons. */\n            buttons?: NotificationButton[];\n            /** Alternate notification content with a lower-weight font. */\n            contextMessage?: string;\n            /** A timestamp associated with the notification, in milliseconds past the epoch (e.g. `Date.now() + n`). */\n            eventTime?: number;\n            /**\n             * A URL to the sender's avatar, app icon, or a thumbnail for image notifications.\n             *\n             * URLs can be a data URL, a blob URL, or a URL relative to a resource within this extension's .crx file\n             *\n             * **Note:** This value is required for the {@link notifications.create}() method.\n             */\n            iconUrl?: string;\n            /**\n             * A URL to the image thumbnail for image-type notifications. URLs have the same restrictions as {@link notifications.NotificationOptions.iconUrl iconUrl}.\n             * @deprecated since Chrome 59. The image is not visible for Mac OS X users.\n             */\n            imageUrl?: string;\n            /** @deprecated since Chrome 67. This UI hint is ignored as of Chrome 67 */\n            isClickable?: boolean;\n            /** Items for multi-item notifications. Users on Mac OS X only see the first item. */\n            items?: NotificationItem[];\n            /**\n             * Main notification content.\n             *\n             * **Note:** This value is required for the {@link notifications.create}() method.\n             */\n            message?: string;\n            /** Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default. On platforms that don't support a notification center (Windows, Linux & Mac), -2 and -1 result in an error as notifications with those priorities will not be shown at all. */\n            priority?: number;\n            /** Current progress ranges from 0 to 100. */\n            progress?: number;\n            /**\n             * Indicates that the notification should remain visible on screen until the user activates or dismisses the notification. This defaults to false.\n             * @since Chrome 50\n             */\n            requireInteraction?: boolean;\n            /**\n             * Indicates that no sounds or vibrations should be made when the notification is being shown. This defaults to false.\n             * @since Chrome 70\n             */\n            silent?: boolean;\n            /**\n             * Title of the notification (e.g. sender name for email).\n             *\n             * **Note:** This value is required for the {@link notifications.create}() method.\n             */\n            title?: string;\n            /** Which type of notification to display.\n             *\n             * **Note:** This value is required for the {@link notifications.create}() method.\n             */\n            type?: `${TemplateType}`;\n        }\n\n        type NotificationCreateOptions = SetRequired<NotificationOptions, \"type\" | \"title\" | \"message\" | \"iconUrl\">;\n\n        export enum PermissionLevel {\n            /** Specifies that the user has elected to show notifications from the app or extension. This is the default at install time. */\n            GRANTED = \"granted\",\n            /** Specifies that the user has elected not to show notifications from the app or extension. */\n            DENIED = \"denied\",\n        }\n\n        export enum TemplateType {\n            /** Contains an icon, title, message, expandedMessage, and up to two buttons. */\n            BASIC = \"basic\",\n            /** Contains an icon, title, message, expandedMessage, image, and up to two buttons. */\n            IMAGE = \"image\",\n            /** Contains an icon, title, message, items, and up to two buttons. Users on Mac OS X only see the first item. */\n            LIST = \"list\",\n            /** Contains an icon, title, message, progress, and up to two buttons. */\n            PROGRESS = \"progress\",\n        }\n\n        /**\n         * Clears the specified notification.\n         * @param notificationId The id of the notification to be cleared. This is returned by {@link notifications.create} method.\n         *\n         * Can return its result via Promise since Chrome 116\n         */\n        export function clear(notificationId: string): Promise<boolean>;\n        export function clear(notificationId: string, callback: (wasCleared: boolean) => void): void;\n\n        /**\n         * Creates and displays a notification.\n         * @param notificationId Identifier of the notification. If not set or empty, an ID will automatically be generated. If it matches an existing notification, this method first clears that notification before proceeding with the create operation. The identifier may not be longer than 500 characters.\n         *\n         * The `notificationId` parameter is required before Chrome 42.\n         * @param options Contents of the notification.\n         *\n         * Can return its result via Promise since Chrome 116\n         */\n        export function create(notificationId: string, options: NotificationCreateOptions): Promise<string>;\n        export function create(options: NotificationCreateOptions): Promise<string>;\n        export function create(\n            notificationId: string,\n            options: NotificationCreateOptions,\n            callback: (notificationId: string) => void,\n        ): void;\n        export function create(options: NotificationCreateOptions, callback: (notificationId: string) => void): void;\n\n        /**\n         * Retrieves all the notifications of this app or extension.\n         *\n         * Can return its result via Promise since Chrome 116\n         */\n        export function getAll(): Promise<{ [key: string]: true }>;\n        export function getAll(callback: (notifications: { [key: string]: true }) => void): void;\n\n        /**\n         * Retrieves whether the user has enabled notifications from this app or extension.\n         *\n         * Can return its result via Promise since Chrome 116\n         */\n        export function getPermissionLevel(): Promise<`${PermissionLevel}`>;\n        export function getPermissionLevel(callback: (level: `${PermissionLevel}`) => void): void;\n\n        /**\n         * Updates an existing notification.\n         * @param notificationId The id of the notification to be updated. This is returned by {@link notifications.create} method.\n         * @param options Contents of the notification to update to.\n         *\n         * Can return its result via Promise since Chrome 116\n         */\n        export function update(notificationId: string, options: NotificationOptions): Promise<boolean>;\n        export function update(\n            notificationId: string,\n            options: NotificationOptions,\n            callback: (wasUpdated: boolean) => void,\n        ): void;\n\n        /** The user pressed a button in the notification. */\n        export const onButtonClicked: events.Event<(notificationId: string, buttonIndex: number) => void>;\n\n        /** The user clicked in a non-button area of the notification. */\n        export const onClicked: events.Event<(notificationId: string) => void>;\n\n        /** The notification closed, either by the system or by user action. */\n        export const onClosed: events.Event<(notificationId: string, byUser: boolean) => void>;\n\n        /** The user changes the permission level. As of Chrome 47, only ChromeOS has UI that dispatches this event. */\n        export const onPermissionLevelChanged: events.Event<(level: `${PermissionLevel}`) => void>;\n\n        /**\n         * The user clicked on a link for the app's notification settings. As of Chrome 47, only ChromeOS has UI that dispatches this event. As of Chrome 65, that UI has been removed from ChromeOS, too.\n         * @deprecated since Chrome 65. Custom notification settings button is no longer supported.\n         */\n        export const onShowSettings: events.Event<() => void>;\n    }\n\n    ////////////////////\n    // Offscreen\n    ////////////////////\n    /**\n     * Use the `offscreen` API to create and manage offscreen documents.\n     *\n     * Permissions: \"offscreen\"\n     * @since Chrome 109, MV3\n     */\n    export namespace offscreen {\n        /** The reason(s) the extension is creating the offscreen document. */\n        export enum Reason {\n            /** A reason used for testing purposes only. */\n            TESTING = \"TESTING\",\n            /** Specifies that the offscreen document is responsible for playing audio. */\n            AUDIO_PLAYBACK = \"AUDIO_PLAYBACK\",\n            /** Specifies that the offscreen document needs to embed and script an iframe in order to modify the iframe's content. */\n            IFRAME_SCRIPTING = \"IFRAME_SCRIPTING\",\n            /** Specifies that the offscreen document needs to embed an iframe and scrape its DOM to extract information. */\n            DOM_SCRAPING = \"DOM_SCRAPING\",\n            /** Specifies that the offscreen document needs to interact with Blob objects (including `URL.createObjectURL()`). */\n            BLOBS = \"BLOBS\",\n            /** Specifies that the offscreen document needs to use the DOMParser API. */\n            DOM_PARSER = \"DOM_PARSER\",\n            /** Specifies that the offscreen document needs to interact with media streams from user media (e.g. `getUserMedia()`). */\n            USER_MEDIA = \"USER_MEDIA\",\n            /** Specifies that the offscreen document needs to interact with media streams from display media (e.g. `getDisplayMedia()`). */\n            DISPLAY_MEDIA = \"DISPLAY_MEDIA\",\n            /** Specifies that the offscreen document needs to use WebRTC APIs. */\n            WEB_RTC = \"WEB_RTC\",\n            /** Specifies that the offscreen document needs to interact with the Clipboard API. */\n            CLIPBOARD = \"CLIPBOARD\",\n            /** Specifies that the offscreen document needs access to localStorage. */\n            LOCAL_STORAGE = \"LOCAL_STORAGE\",\n            /** Specifies that the offscreen document needs to spawn workers. */\n            WORKERS = \"WORKERS\",\n            /** Specifies that the offscreen document needs to use navigator.getBattery. */\n            BATTERY_STATUS = \"BATTERY_STATUS\",\n            /** Specifies that the offscreen document needs to use window.matchMedia. */\n            MATCH_MEDIA = \"MATCH_MEDIA\",\n            /** Specifies that the offscreen document needs to use navigator.geolocation. */\n            GEOLOCATION = \"GEOLOCATION\",\n        }\n\n        export interface CreateParameters {\n            /** The reason(s) the extension is creating the offscreen document. */\n            reasons: `${Reason}`[];\n            /** The (relative) URL to load in the document. */\n            url: string;\n            /** A developer-provided string that explains, in more detail, the need for the background context. The user agent _may_ use this in display to the user. */\n            justification: string;\n        }\n\n        /**\n         * Creates a new offscreen document for the extension.\n         * @param parameters The parameters describing the offscreen document to create.\n         *\n         * Can return its result via Promise in Manifest V3.\n         */\n        export function createDocument(parameters: CreateParameters): Promise<void>;\n        export function createDocument(parameters: CreateParameters, callback: () => void): void;\n\n        /**\n         * Closes the currently-open offscreen document for the extension.\n         *\n         * Can return its result via Promise in Manifest V3.\n         */\n        export function closeDocument(): Promise<void>;\n        export function closeDocument(callback: () => void): void;\n\n        /**\n         * Determines whether the extension has an active document.\n         *\n         * Can return its result via Promise in Manifest V3.\n         */\n        export function hasDocument(): Promise<boolean>;\n        export function hasDocument(callback: (result: boolean) => void): void;\n    }\n\n    ////////////////////\n    // Omnibox\n    ////////////////////\n    /**\n     * The omnibox API allows you to register a keyword with Google Chrome's address bar, which is also known as the omnibox.\n     *\n     * Manifest: \"omnibox\"\n     */\n    export namespace omnibox {\n        /** A suggest result. */\n        export interface SuggestResult {\n            /** The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry. */\n            content: string;\n            /** The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. dimmed match. You must escape the five predefined entities to display them as text: stackoverflow.com/a/1091953/89484 */\n            description: string;\n            /**\n             * Whether the suggest result can be deleted by the user.\n             * @since Chrome 63\n             */\n            deletable?: boolean | undefined;\n        }\n\n        /** A suggest result. */\n        export interface DefaultSuggestResult {\n            /** The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. dimmed match. */\n            description: string;\n        }\n\n        /**\n         * The style type.\n         * @since Chrome 44\n         */\n        export enum DescriptionStyleType {\n            URL = \"url\",\n            MATCH = \"match\",\n            DIM = \"dim\",\n        }\n\n        /**\n         * The window disposition for the omnibox query. This is the recommended context to display results. For example, if the omnibox command is to navigate to a certain URL, a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab.\n         * @since Chrome 44\n         */\n        export enum OnInputEnteredDisposition {\n            CURRENT_TAB = \"currentTab\",\n            NEW_FOREGROUND_TAB = \"newForegroundTab\",\n            NEW_BACKGROUND_TAB = \"newBackgroundTab\",\n        }\n\n        /**\n         * Sets the description and styling for the default suggestion. The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 100\n         * @param suggestion A partial SuggestResult object, without the 'content' parameter.\n         */\n        export function setDefaultSuggestion(suggestion: DefaultSuggestResult): Promise<void>;\n        export function setDefaultSuggestion(suggestion: DefaultSuggestResult, callback: () => void): void;\n\n        /** User has accepted what is typed into the omnibox. */\n        export const onInputEntered: events.Event<(text: string, disposition: `${OnInputEnteredDisposition}`) => void>;\n\n        /** User has changed what is typed into the omnibox. */\n        export const onInputChanged: events.Event<\n            (text: string, suggest: (suggestResults: SuggestResult[]) => void) => void\n        >;\n\n        /** User has started a keyword input session by typing the extension's keyword. This is guaranteed to be sent exactly once per input session, and before any onInputChanged events. */\n        export const onInputStarted: events.Event<() => void>;\n\n        /** User has ended the keyword input session without accepting the input. */\n        export const onInputCancelled: events.Event<() => void>;\n\n        /**\n         * User has deleted a suggested result\n         * @since Chrome 63\n         */\n        export const onDeleteSuggestion: events.Event<(text: string) => void>;\n    }\n\n    ////////////////////\n    // Page Action\n    ////////////////////\n    /**\n     * Use the `Browser.pageAction` API to put icons in the main Google Chrome toolbar, to the right of the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages. Page actions appear grayed out when inactive.\n     *\n     * Manifest: \"page_action\"\n     *\n     * MV2 only\n     */\n    export namespace pageAction {\n        export interface TitleDetails {\n            /** The id of the tab for which you want to modify the page action. */\n            tabId: number;\n            /** The tooltip string. */\n            title: string;\n        }\n\n        export interface TabDetails {\n            /** The ID of the tab to query state for. */\n            tabId: number;\n        }\n\n        export interface PopupDetails {\n            /** The id of the tab for which you want to modify the page action. */\n            tabId: number;\n            /** The relative path to the HTML file to show in a popup. If set to the empty string (`''`), no popup is shown. */\n            popup: string;\n        }\n\n        export type IconDetails =\n            & {\n                /** @deprecated This argument is ignored. */\n                iconIndex?: number | undefined;\n                /** The id of the tab for which you want to modify the page action. */\n                tabId: number;\n            }\n            & (\n                | {\n                    /** Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then image with size `scale` \\* n will be selected, where n is the size of the icon in the UI. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'16': foo}' */\n                    imageData: ImageData | { [index: number]: ImageData };\n                    /** Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then image with size `scale` \\* n will be selected, where n is the size of the icon in the UI. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.path = {'16': foo}' */\n                    path?: string | { [index: string]: string } | undefined;\n                }\n                | {\n                    /** Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then image with size `scale` \\* n will be selected, where n is the size of the icon in the UI. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'16': foo}' */\n                    imageData?: ImageData | { [index: number]: ImageData } | undefined;\n                    /** Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals `scale`, then image with size `scale` \\* n will be selected, where n is the size of the icon in the UI. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.path = {'16': foo}' */\n                    path: string | { [index: string]: string };\n                }\n            );\n\n        /**\n         * Hides the page action. Hidden page actions still appear in the Chrome toolbar, but are grayed out.\n         * @param tabId The id of the tab for which you want to modify the page action.\n         * @param callback Since Chrome 67\n         */\n        export function hide(tabId: number, callback?: () => void): void;\n\n        /**\n         * Shows the page action. The page action is shown whenever the tab is selected.\n         * @param tabId The id of the tab for which you want to modify the page action.\n         * @param callback Since Chrome 67\n         */\n        export function show(tabId: number, callback?: () => void): void;\n\n        /**\n         * Sets the title of the page action. This is displayed in a tooltip over the page action.\n         * @param callback Since Chrome 67\n         */\n        export function setTitle(details: TitleDetails, callback?: () => void): void;\n\n        /**\n         * Sets the HTML document to be opened as a popup when the user clicks on the page action's icon.\n         * @param callback Since Chrome 67\n         */\n        export function setPopup(details: PopupDetails, callback?: () => void): void;\n\n        /** Gets the title of the page action. */\n        export function getTitle(details: TabDetails, callback: (result: string) => void): void;\n\n        /** Gets the html document set as the popup for this page action. */\n        export function getPopup(details: TabDetails, callback: (result: string) => void): void;\n\n        /** Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the path or the imageData property must be specified. */\n        export function setIcon(details: IconDetails, callback?: () => void): void;\n\n        /** Fired when a page action icon is clicked. This event will not fire if the page action has a popup. */\n        export const onClicked: events.Event<(tab: Browser.tabs.Tab) => void>;\n    }\n\n    ////////////////////\n    // Page Capture\n    ////////////////////\n    /**\n     * Use the `Browser.pageCapture` API to save a tab as MHTML.\n     *\n     * Permissions: \"pageCapture\"\n     */\n    export namespace pageCapture {\n        export interface SaveDetails {\n            /** The id of the tab to save as MHTML. */\n            tabId: number;\n        }\n\n        /**\n         * Saves the content of the tab with given id as MHTML.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function saveAsMHTML(details: SaveDetails): Promise<Blob | undefined>;\n        export function saveAsMHTML(details: SaveDetails, callback: (mhtmlData?: Blob) => void): void;\n    }\n\n    ////////////////////\n    // Permissions\n    ////////////////////\n    /**\n     * Use the `Browser.permissions` API to request declared optional permissions at run time rather than install time, so users understand why the permissions are needed and grant only those that are necessary.\n     */\n    export namespace permissions {\n        export interface Permissions {\n            /** The list of host permissions, including those specified in the `optional_permissions` or `permissions` keys in the manifest, and those associated with [Content Scripts](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts). */\n            origins?: string[];\n            /** List of named permissions (does not include hosts or origins). */\n            permissions?: Browser.runtime.ManifestPermission[];\n        }\n\n        export interface AddHostAccessRequest {\n            /** The id of a document where host access requests can be shown. Must be the top-level document within a tab. If provided, the request is shown on the tab of the specified document and is removed when the document navigates to a new origin. Adding a new request will override any existent request for `tabId`. This or `tabId` must be specified. */\n            documentId?: string;\n            /** The URL pattern where host access requests can be shown. If provided, host access requests will only be shown on URLs that match this pattern. */\n            pattern?: string;\n            /** The id of the tab where host access requests can be shown. If provided, the request is shown on the specified tab and is removed when the tab navigates to a new origin. Adding a new request will override an existent request for `documentId`. This or `documentId` must be specified. */\n            tabId?: number;\n        }\n\n        /**\n         * Adds a host access request. Request will only be signaled to the user if extension can be granted access to the host in the request. Request will be reset on cross-origin navigation. When accepted, grants persistent access to the site’s top origin\n         * @since Chrome 133\n         */\n        export function addHostAccessRequest(request: AddHostAccessRequest): Promise<void>;\n        export function addHostAccessRequest(request: AddHostAccessRequest, callback: () => void): void;\n\n        /**\n         * Checks if the extension has the specified permissions.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function contains(permissions: Permissions): Promise<boolean>;\n        export function contains(permissions: Permissions, callback: (result: boolean) => void): void;\n\n        /**\n         * Gets the extension's current set of permissions.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getAll(): Promise<Permissions>;\n        export function getAll(callback: (permissions: Permissions) => void): void;\n\n        /**\n         * Requests access to the specified permissions, displaying a prompt to the user if necessary.\n         * These permissions must either be defined in the optional_permissions field of the manifest or be required permissions that were withheld by the user.\n         * Paths on origin patterns will be ignored.\n         * You can request subsets of optional origin permissions; for example, if you specify `*://*\\/*` in the `optional_permissions` section of the manifest, you can request `http://example.com/`.\n         * If there are any problems requesting the permissions, {@link runtime.lastError} will be set.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function request(permissions: Permissions): Promise<boolean>;\n        export function request(permissions: Permissions, callback: (granted: boolean) => void): void;\n\n        /**\n         * Removes access to the specified permissions. If there are any problems removing the permissions, {@link runtime.lastError} will be set.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function remove(permissions: Permissions): Promise<boolean>;\n        export function remove(permissions: Permissions, callback: (removed: boolean) => void): void;\n\n        export interface RemoveHostAccessRequest {\n            /** The id of a document where host access request will be removed. Must be the top-level document within a tab. This or `tabId` must be specified. */\n            documentId?: string;\n            /** The URL pattern where host access request will be removed. If provided, this must exactly match the pattern of an existing host access request. */\n            pattern?: string;\n            /** The id of the tab where host access request will be removed. This or `documentId` must be specified. */\n            tabId?: number;\n        }\n\n        /**\n         * Removes a host access request, if existent.\n         * @since Chrome 133\n         */\n        export function removeHostAccessRequest(request: RemoveHostAccessRequest): Promise<void>;\n        export function removeHostAccessRequest(request: RemoveHostAccessRequest, callback: () => void): void;\n\n        /** Fired when access to permissions has been removed from the extension. */\n        export const onRemoved: Browser.events.Event<(permissions: Permissions) => void>;\n\n        /** Fired when the extension acquires new permissions. */\n        export const onAdded: Browser.events.Event<(permissions: Permissions) => void>;\n    }\n\n    ////////////////////\n    // Platform Keys\n    ////////////////////\n    /**\n     * Use the `Browser.platformKeys` API to access client certificates managed by the platform. If the user or policy grants the permission, an extension can use such a certficate in its custom authentication protocol. E.g. this allows usage of platform managed certificates in third party VPNs (see Browser.vpnProvider).\n     *\n     * Permissions: \"platformKeys\"\n     * @platform ChromeOS only\n     * @since Chrome 45\n     */\n    export namespace platformKeys {\n        export interface Match {\n            /** The DER encoding of a X.509 certificate. */\n            certificate: ArrayBuffer;\n            /** The KeyAlgorithm of the certified key. This contains algorithm parameters that are inherent to the key of the certificate (e.g. the key length). Other parameters like the hash function used by the sign function are not included. */\n            keyAlgorithm: KeyAlgorithm;\n        }\n\n        export interface ClientCertificateRequest {\n            /** This field is a list of the types of certificates requested, sorted in order of the server's preference. Only certificates of a type contained in this list will be retrieved. If `certificateTypes` is the empty list, however, certificates of any type will be returned. */\n            certificateTypes: `${ClientCertificateType}`[];\n            /** List of distinguished names of certificate authorities allowed by the server. Each entry must be a DER-encoded X.509 DistinguishedName. */\n            certificateAuthorities: ArrayBuffer[];\n        }\n\n        export enum ClientCertificateType {\n            ECDSA_SIGN = \"ecdsaSign\",\n            RAS_SIGN = \"rasSign\",\n        }\n\n        export interface SelectDetails {\n            /** Only certificates that match this request will be returned. */\n            request: ClientCertificateRequest;\n            /** If given, the `selectClientCertificates` operates on this list. Otherwise, obtains the list of all certificates from the platform's certificate stores that are available to this extensions. Entries that the extension doesn't have permission for or which doesn't match the request, are removed. */\n            clientCerts?: ArrayBuffer[] | undefined;\n            /** If true, the filtered list is presented to the user to manually select a certificate and thereby granting the extension access to the certificate(s) and key(s). Only the selected certificate(s) will be returned. If is false, the list is reduced to all certificates that the extension has been granted access to (automatically or manually). */\n            interactive: boolean;\n        }\n\n        export interface VerificationDetails {\n            /** Each chain entry must be the DER encoding of a X.509 certificate, the first entry must be the server certificate and each entry must certify the entry preceding it. */\n            serverCertificateChain: ArrayBuffer[];\n            /** The hostname of the server to verify the certificate for, e.g. the server that presented the `serverCertificateChain`. */\n            hostname: string;\n        }\n\n        export interface VerificationResult {\n            /** The result of the trust verification: true if trust for the given verification details could be established and false if trust is rejected for any reason. */\n            trusted: boolean;\n            /**\n             * If the trust verification failed, this array contains the errors reported by the underlying network layer. Otherwise, this array is empty.\n             *\n             * Note: This list is meant for debugging only and may not contain all relevant errors. The errors returned may change in future revisions of this API, and are not guaranteed to be forwards or backwards compatible.\n             */\n            debug_errors: string[];\n        }\n\n        /**\n         * This method filters from a list of client certificates the ones that are known to the platform, match `request` and for which the extension has permission to access the certificate and its private key. If `interactive` is true, the user is presented a dialog where they can select from matching certificates and grant the extension access to the certificate. The selected/filtered client certificates will be passed to `callback`.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 121.\n         */\n        export function selectClientCertificates(details: SelectDetails): Promise<Match[]>;\n        export function selectClientCertificates(\n            details: SelectDetails,\n            callback: (matches: Match[]) => void,\n        ): void;\n\n        /**\n         * Passes the key pair of `certificate` for usage with {@link platformKeys.subtleCrypto} to `callback`.\n         * @param certificate The certificate of a {@link Match} returned by {@link selectClientCertificates}.\n         * @param parameters Determines signature/hash algorithm parameters additionally to the parameters fixed by the key itself. The same parameters are accepted as by WebCrypto's importKey function, e.g. `RsaHashedImportParams` for a RSASSA-PKCS1-v1_5 key and `EcKeyImportParams` for EC key. Additionally for RSASSA-PKCS1-v1_5 keys, hashing algorithm name parameter can be specified with one of the following values: \"none\", \"SHA-1\", \"SHA-256\", \"SHA-384\", or \"SHA-512\", e.g. `{\"hash\": { \"name\": \"none\" } }`. The sign function will then apply PKCS#1 v1.5 padding but not hash the given data.\n         *\n         * Currently, this method only supports the \"RSASSA-PKCS1-v1\\_5\" and \"ECDSA\" algorithms.\n         */\n        export function getKeyPair(\n            certificate: ArrayBuffer,\n            parameters: { [key: string]: unknown },\n            callback: (publicKey: CryptoKey, privateKey: CryptoKey | null) => void,\n        ): void;\n\n        /**\n         * Passes the key pair identified by `publicKeySpkiDer` for usage with {@link platformKeys.subtleCrypto} to `callback`.\n         *\n         * @param publicKeySpkiDer A DER-encoded X.509 SubjectPublicKeyInfo, obtained e.g. by calling WebCrypto's exportKey function with format=\"spki\".\n         * @param parameters Provides signature and hash algorithm parameters, in addition to those fixed by the key itself. The same parameters are accepted as by WebCrypto's [importKey](https://www.w3.org/TR/WebCryptoAPI/#SubtleCrypto-method-importKey) function, e.g. `RsaHashedImportParams` for a RSASSA-PKCS1-v1\\_5 key. For RSASSA-PKCS1-v1\\_5 keys, we need to also pass a \"hash\" parameter `{ \"hash\": { \"name\": string } }`. The \"hash\" parameter represents the name of the hashing algorithm to be used in the digest operation before a sign. It is possible to pass \"none\" as the hash name, in which case the sign function will apply PKCS#1 v1.5 padding and but not hash the given data.\n         *\n         * Currently, this method supports the \"ECDSA\" algorithm with named-curve P-256 and \"RSASSA-PKCS1-v1\\_5\" algorithm with one of the hashing algorithms \"none\", \"SHA-1\", \"SHA-256\", \"SHA-384\", and \"SHA-512\".\n         * @since Chrome 85\n         */\n        export function getKeyPairBySpki(\n            publicKeySpkiDer: ArrayBuffer,\n            parameters: { [key: string]: unknown },\n            callback: (publicKey: CryptoKey, privateKey: CryptoKey | null) => void,\n        ): void;\n\n        /** An implementation of WebCrypto's SubtleCrypto that allows crypto operations on keys of client certificates that are available to this extension. */\n        export function subtleCrypto(): SubtleCrypto | undefined;\n\n        /**\n         * Checks whether `details.serverCertificateChain` can be trusted for `details.hostname` according to the trust settings of the platform. Note: The actual behavior of the trust verification is not fully specified and might change in the future. The API implementation verifies certificate expiration, validates the certification path and checks trust by a known CA. The implementation is supposed to respect the EKU serverAuth and to support subject alternative names.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 121.\n         */\n        export function verifyTLSServerCertificate(details: VerificationDetails): Promise<VerificationResult>;\n        export function verifyTLSServerCertificate(\n            details: VerificationDetails,\n            callback: (result: VerificationResult) => void,\n        ): void;\n    }\n\n    ////////////////////\n    // Power\n    ////////////////////\n    /**\n     * Use the `Browser.power` API to override the system's power management features.\n     *\n     * Permissions: \"power\"\n     */\n    export namespace power {\n        export enum Level {\n            /** Prevents the display from being turned off or dimmed, or the system from sleeping in response to user inactivity */\n            DISPLAY = \"display\",\n            /** Prevents the system from sleeping in response to user inactivity. */\n            SYSTEM = \"system\",\n        }\n\n        /** Requests that power management be temporarily disabled. `level` describes the degree to which power management should be disabled. If a request previously made by the same app is still active, it will be replaced by the new request. */\n        export function requestKeepAwake(level: `${Level}`): void;\n\n        /** Releases a request previously made via requestKeepAwake(). */\n        export function releaseKeepAwake(): void;\n\n        /**\n         * Reports a user activity in order to awake the screen from a dimmed or turned off state or from a screensaver. Exits the screensaver if it is currently active.\n         * Can return its result via Promise in Manifest V3 or later.\n         * @platform ChromeOS only\n         * @since Chrome 113\n         */\n        export function reportActivity(): Promise<void>;\n        export function reportActivity(callback: () => void): void;\n    }\n\n    ////////////////////\n    // Printer Provider\n    ////////////////////\n    /**\n     * The `Browser.printerProvider` API exposes events used by print manager to query printers controlled by extensions, to query their capabilities and to submit print jobs to these printers.\n     *\n     * Permissions: \"printerProvider\"\n     * @since Chrome 44\n     */\n    export namespace printerProvider {\n        export interface PrinterInfo {\n            /** Unique printer ID. */\n            id: string;\n            /** Printer's human readable name. */\n            name: string;\n            /** Printer's human readable description. */\n            description?: string | undefined;\n        }\n\n        /** Error codes returned in response to {@link onPrintRequested} event. */\n        export enum PrintError {\n            /** Specifies that the operation was completed successfully. */\n            OK = \"OK\",\n            /** Specifies that a general failure occured. */\n            FAILED = \"FAILED\",\n            /** Specifies that the print ticket is invalid. For example, the ticket is inconsistent with some capabilities, or the extension is not able to handle all settings from the ticket. */\n            INVALID_TICKET = \"INVALID_TICKET\",\n            /** Specifies that the document is invalid. For example, data may be corrupted or the format is incompatible with the extension. */\n            INVALID_DATA = \"INVALID_DATA\",\n        }\n\n        export interface PrinterCapabilities {\n            /** Device capabilities in CDD format. */\n            capabilities: { [key: string]: unknown };\n        }\n\n        export interface PrintJob {\n            /** ID of the printer which should handle the job. */\n            printerId: string;\n            /** The print job title. */\n            title: string;\n            /** Print ticket in CJT format. */\n            ticket: { [key: string]: unknown };\n            /** The document content type. Supported formats are `application/pdf` and `image/pwg-raster`. */\n            contentType: string;\n            /** Blob containing the document data to print. Format must match `contentType`. */\n            document: Blob;\n        }\n\n        /** from https://developer.chrome.com/docs/apps/reference/usb#type-Device */\n        export interface Device {\n            /** An opaque ID for the USB device. It remains unchanged until the device is unplugged. */\n            device: number;\n            /**\n             * The iManufacturer string read from the device, if available.\n             * @since Chrome 46\n             */\n            manufacturerName: string;\n            /** The product ID. */\n            productId: number;\n            /**\n             * The iProduct string read from the device, if available.\n             * @since Chrome 46\n             */\n            productName: string;\n            /**\n             * The iSerialNumber string read from the device, if available.\n             * @since Chrome 46\n             */\n            serialNumber: string;\n            /** The device vendor ID. */\n            vendorId: number;\n            /**\n             * The device version (bcdDevice field).\n             * @since Chrome 51\n             */\n            version: number;\n        }\n\n        /** Event fired when print manager requests printers provided by extensions. */\n        export const onGetPrintersRequested: events.Event<\n            (resultCallback: (printerInfo: PrinterInfo[]) => void) => void\n        >;\n\n        /**\n         * Event fired when print manager requests information about a USB device that may be a printer.\n         *\n         * Note: An application should not rely on this event being fired more than once per device. If a connected device is supported it should be returned in the {@link onGetPrintersRequested} event.\n         * @since Chrome 45\n         */\n        export const onGetUsbPrinterInfoRequested: events.Event<\n            (device: Device, resultCallback: (printerInfo?: PrinterInfo) => void) => void\n        >;\n\n        /** Event fired when print manager requests printer capabilities. */\n        export const onGetCapabilityRequested: events.Event<\n            (printerId: string, resultCallback: (capabilities: PrinterCapabilities) => void) => void\n        >;\n\n        /** Event fired when print manager requests printing. */\n        export const onPrintRequested: events.Event<\n            (printJob: PrintJob, resultCallback: (result: `${PrintError}`) => void) => void\n        >;\n    }\n\n    ////////////////////\n    // Printing\n    ////////////////////\n    /**\n     * Use the `Browser.printing` API to send print jobs to printers installed on Chromebook.\n\n    * Permissions: \"printing\"\n    * @platform ChromeOS only\n    * @since Chrome 81\n    */\n    export namespace printing {\n        export interface GetPrinterInfoResponse {\n            /** Printer capabilities in [CDD format](https://developers.google.com/cloud-print/docs/cdd#cdd-example). The property may be missing. */\n            capabilities?: { [key: string]: unknown };\n            /** The status of the printer. */\n            status: PrinterStatus;\n        }\n\n        /** Status of the print job. */\n        export enum JobStatus {\n            /** Print job is received on Chrome side but was not processed yet. */\n            PENDING = \"PENDING\",\n            /** Print job is sent for printing. */\n            IN_PROGRESS = \"IN_PROGRESS\",\n            /** Print job was interrupted due to some error. */\n            FAILED = \"FAILED\",\n            /** Print job was canceled by the user or via API. */\n            CANCELED = \"CANCELED\",\n            /** Print job was printed without any errors. */\n            PRINTED = \"PRINTED\",\n        }\n\n        export interface Printer {\n            /** The human-readable description of the printer. */\n            description: string;\n            /** The printer's identifier; guaranteed to be unique among printers on the device. */\n            id: string;\n            /** The flag which shows whether the printer fits DefaultPrinterSelection rules. Note that several printers could be flagged. */\n            isDefault: boolean;\n            /** The name of the printer. */\n            name: string;\n            /**\n             * The value showing how recent the printer was used for printing from Chrome.\n             * The lower the value is the more recent the printer was used.\n             * The minimum value is 0.\n             * Missing value indicates that the printer wasn't used recently.\n             * This value is guaranteed to be unique amongst printers.\n             */\n            recentlyUsedRank?: number;\n            /** The source of the printer (user or policy configured). */\n            source: PrinterSource;\n            /** The printer URI. This can be used by extensions to choose the printer for the user. */\n            uri: string;\n        }\n\n        /** The source of the printer. */\n        export enum PrinterSource {\n            /** Printer was added by user. */\n            USER = \"USER\",\n            /** Printer was added via policy. */\n            POLICY = \"POLICY\",\n        }\n\n        /** The status of the printer. */\n        export enum PrinterStatus {\n            /** The door of the printer is open. Printer still accepts print jobs. */\n            DOOR_OPEN = \"DOOR_OPEN\",\n            /** The tray of the printer is missing. Printer still accepts print jobs. */\n            TRAY_MISSING = \"TRAY_MISSING\",\n            /** The printer is out of ink. Printer still accepts print jobs. */\n            OUT_OF_INK = \"OUT_OF_INK\",\n            /** The printer is out of paper. Printer still accepts print jobs. */\n            OUT_OF_PAPER = \"OUT_OF_PAPER\",\n            /** The output area of the printer (e.g. tray) is full. Printer still accepts print jobs. */\n            OUTPUT_FULL = \"OUTPUT_FULL\",\n            /** The printer has a paper jam. Printer still accepts print jobs. */\n            PAPER_JAM = \"PAPER_JAM\",\n            /** Some generic issue. Printer still accepts print jobs. */\n            GENERIC_ISSUE = \"GENERIC_ISSUE\",\n            /** The printer is stopped and doesn't print but still accepts print jobs. */\n            STOPPED = \"STOPPED\",\n            /** The printer is unreachable and doesn't accept print jobs. */\n            UNREACHABLE = \"UNREACHABLE\",\n            /** The SSL certificate is expired. Printer accepts jobs but they fail. */\n            EXPIRED_CERTIFICATE = \"EXPIRED_CERTIFICATE\",\n            /** The printer is available. */\n            AVAILABLE = \"AVAILABLE\",\n        }\n\n        export interface SubmitJobRequest {\n            /**\n             * The print job to be submitted.\n             * Supported content types are \"application/pdf\" and \"image/png\". The Cloud Job Ticket shouldn't include `FitToPageTicketItem`, `PageRangeTicketItem` and `ReverseOrderTicketItem` fields since they are irrelevant for native printing. `VendorTicketItem` is optional\n             * All other fields must be present.\n             */\n            job: Browser.printerProvider.PrintJob;\n        }\n\n        export interface SubmitJobResponse {\n            /** The id of created print job. This is a unique identifier among all print jobs on the device. If status is not OK, jobId will be null. */\n            jobId: string | null;\n            /** The status of the request. */\n            status: SubmitJobStatus;\n        }\n\n        /** The status of submitJob request. */\n        export enum SubmitJobStatus {\n            /** Sent print job request is accepted. */\n            OK = \"OK\",\n            /** Sent print job request is rejected by the user. */\n            USER_REJECTED = \"USER_REJECTED\",\n        }\n\n        /** The maximum number of times that getPrinterInfo can be called per minute. */\n        export const MAX_GET_PRINTER_INFO_CALLS_PER_MINUTE: 20;\n\n        /** The maximum number of times that submitJob can be called per minute. */\n        export const MAX_SUBMIT_JOB_CALLS_PER_MINUTE: 40;\n\n        /**\n         * Cancels previously submitted job.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 100.\n         */\n        export function cancelJob(jobId: string): Promise<void>;\n        export function cancelJob(jobId: string, callback: () => void): void;\n\n        /**\n         * Returns the status of the print job. This call will fail with a runtime error if the print job with the given `jobId` doesn't exist. `jobId`: The id of the print job to return the status of. This should be the same id received in a {@link SubmitJobResponse}.\n         * @since Chrome 135\n         */\n        export function getJobStatus(jobId: string): Promise<`${JobStatus}`>;\n        export function getJobStatus(jobId: string, callback: (status: `${JobStatus}`) => void): void;\n\n        /**\n         * Returns the status and capabilities of the printer in CDD format. This call will fail with a runtime error if no printers with given id are installed.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 100.\n         */\n        export function getPrinterInfo(printerId: string): Promise<GetPrinterInfoResponse>;\n        export function getPrinterInfo(printerId: string, callback: (response: GetPrinterInfoResponse) => void): void;\n\n        /**\n         * Returns the list of available printers on the device. This includes manually added, enterprise and discovered printers.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 100.\n         */\n        export function getPrinters(): Promise<Printer[]>;\n        export function getPrinters(callback: (printers: Printer[]) => void): void;\n\n        /**\n         * Submits the job for printing. If the extension is not listed in the PrintingAPIExtensionsAllowlist policy, the user is prompted to accept the print job.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 120.\n         */\n        export function submitJob(request: SubmitJobRequest): Promise<SubmitJobResponse>;\n        export function submitJob(request: SubmitJobRequest, callback: (response: SubmitJobResponse) => void): void;\n\n        /**\n         * Event fired when the status of the job is changed. This is only fired for the jobs created by this extension.\n         */\n        export const onJobStatusChanged: Browser.events.Event<(jobId: string, status: `${JobStatus}`) => void>;\n    }\n\n    ////////////////////\n    // Printing Metrics\n    ////////////////////\n    /**\n     * Use the `Browser.printingMetrics` API to fetch data about printing usage.\n     *\n     * Permissions: \"printingMetrics\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @platform ChromeOS only\n     * @since Chrome 79\n     */\n    export namespace printingMetrics {\n        export enum ColorMode {\n            /** Specifies that black and white mode was used. */\n            BLACK_AND_WHITE = \"BLACK_AND_WHITE\",\n            /** Specifies that color mode was used. */\n            COLOR = \"COLOR\",\n        }\n\n        export enum DuplexMode {\n            /** Specifies that one-sided printing was used. */\n            ONE_SIDED = \"ONE_SIDED\",\n            /** Specifies that two-sided printing was used, flipping on long edge. */\n            TWO_SIDED_LONG_EDGE = \"TWO_SIDED_LONG_EDGE\",\n            /** Specifies that two-sided printing was used, flipping on short edge. */\n            TWO_SIDED_SHORT_EDGE = \"TWO_SIDED_SHORT_EDGE\",\n        }\n\n        export interface MediaSize {\n            /** Height (in micrometers) of the media used for printing. */\n            height: number;\n            /**\n             * Vendor-provided ID, e.g. \"iso_a3_297x420mm\" or \"na_index-3x5_3x5in\".\n             * Possible values are values of \"media\" IPP attribute and can be found on [IANA page](https://www.iana.org/assignments/ipp-registrations/ipp-registrations.xhtml).\n             */\n            vendorId: string;\n            /** Width (in micrometers) of the media used for printing. */\n            width: number;\n        }\n\n        export interface Printer {\n            /** Displayed name of the printer. */\n            name: string;\n            /** The source of the printer. */\n            source: PrinterSource;\n            /** The full path for the printer. Contains protocol, hostname, port, and queue. */\n            uri: string;\n        }\n\n        /** The source of the printer. */\n        export enum PrinterSource {\n            /** Specifies that the printer was added by user. */\n            USER = \"USER\",\n            /** Specifies that the printer was added via policy. */\n            POLICY = \"POLICY\",\n        }\n\n        export interface PrintJobInfo {\n            /** The job completion time (in milliseconds past the Unix epoch). */\n            completionTime: number;\n            /** The job creation time (in milliseconds past the Unix epoch). */\n            creationTime: number;\n            /** The ID of the job. */\n            id: string;\n            /** The number of pages in the document. */\n            numberOfPages: number;\n            /** The info about the printer which printed the document. */\n            printer: Printer;\n            /**\n             * The status of the printer.\n             * @since Chrome 85\n             */\n            printer_status: Browser.printing.PrinterStatus;\n            /** The settings of the print job. */\n            settings: PrintSettings;\n            /** Source showing who initiated the print job. */\n            source: PrintJobSource;\n            /** ID of source. Null if source is PRINT_PREVIEW or ANDROID_APP. */\n            sourceId: string | null;\n            /** The final status of the job. */\n            status: PrintJobStatus;\n            /** The title of the document which was printed. */\n            title: string;\n        }\n\n        /** The source of the print job. */\n        export enum PrintJobSource {\n            /** Specifies that the job was created from the Print Preview page initiated by the user. */\n            PRINT_PREVIEW = \"PRINT_PREVIEW\",\n            /** Specifies that the job was created from an Android App. */\n            ANDROID_APP = \"ANDROID_APP\",\n            /** Specifies that the job was created by extension via Chrome API. */\n            EXTENSION = \"EXTENSION\",\n            /** Specifies that the job was created by an Isolated Web App via API. */\n            ISOLATED_WEB_APP = \"ISOLATED_WEB_APP\",\n        }\n\n        /** Specifies the final status of the print job. */\n        export enum PrintJobStatus {\n            /** Specifies that the print job was interrupted due to some error. */\n            FAILED = \"FAILED\",\n            /** Specifies that the print job was canceled by the user or via API. */\n            CANCELED = \"CANCELED\",\n            /** Specifies that the print job was printed without any errors. */\n            PRINTED = \"PRINTED\",\n        }\n\n        export interface PrintSettings {\n            /** The requested color mode. */\n            color: ColorMode;\n            /** The requested number of copies. */\n            copies: number;\n            /** The requested duplex mode. */\n            duplex: DuplexMode;\n            /** The requested media size. */\n            mediaSize: MediaSize;\n        }\n\n        /**\n         * Returns the list of the finished print jobs.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getPrintJobs(): Promise<PrintJobInfo[]>;\n        export function getPrintJobs(callback: (jobs: PrintJobInfo[]) => void): void;\n\n        /** Event fired when the print job is finished. This includes any of termination statuses: FAILED, CANCELED and PRINTED. */\n        export const onPrintJobFinished: Browser.events.Event<(jobInfo: PrintJobInfo) => void>;\n    }\n\n    ////////////////////\n    // Privacy\n    ////////////////////\n    /**\n     * Use the `Browser.privacy` API to control usage of the features in Chrome that can affect a user's privacy. This API relies on the ChromeSetting prototype of the type API for getting and setting Chrome's configuration.\n     * Note: The Chrome Privacy Whitepaper gives background detail regarding the features which this API can control.\n     *\n     * Permissions: \"privacy\"\n     */\n    export namespace privacy {\n        /**\n         * The IP handling policy of WebRTC.\n         * @since Chrome 48\n         */\n        export enum IPHandlingPolicy {\n            DEFAULT = \"default\",\n            DEFAULT_PUBLIC_AND_PRIVATE_INTERFACES = \"default_public_and_private_interfaces\",\n            DEFAULT_PUBLIC_INTERFACE_ONLY = \"default_public_interface_only\",\n            DISABLE_NON_PROXIED_UDP = \"disable_non_proxied_udp\",\n        }\n\n        /** Settings that enable or disable features that require third-party network services provided by Google and your default search provider. */\n        export const services: {\n            /**\n             * If enabled, Chrome uses a web service to help resolve navigation errors.\n             * This preference's value is a boolean, defaulting to `true`.\n             */\n            alternateErrorPagesEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome offers to automatically fill in addresses and other form data.\n             * This preference's value is a boolean, defaulting to `true`.\n             * @since Chrome 70\n             */\n            autofillAddressEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome offers to automatically fill in credit card forms.\n             * This preference's value is a boolean, defaulting to `true`.\n             * @since Chrome 70\n             */\n            autofillCreditCardEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome offers to automatically fill in forms.\n             * This preference's value is a boolean, defaulting to `true`.\n             * @deprecated since Chrome 70. Please use privacy.services.autofillAddressEnabled and privacy.services.autofillCreditCardEnabled. This remains for backward compatibility in this release and will be removed in the future */\n            autofillEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, the password manager will ask if you want to save passwords.\n             * This preference's value is a boolean, defaulting to `true`.\n             */\n            passwordSavingEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome does its best to protect you from phishing and malware.\n             * This preference's value is a boolean, defaulting to `true`.\n             */\n            safeBrowsingEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome will send additional information to Google when SafeBrowsing blocks a page, such as the content of the blocked page.\n             * This preference's value is a boolean, defaulting to `false`.\n             */\n            safeBrowsingExtendedReportingEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome sends the text you type into the Omnibox to your default search engine, which provides predictions of websites and searches that are likely completions of what you've typed so far.\n             * This preference's value is a boolean, defaulting to `true`.\n             */\n            searchSuggestEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome uses a web service to help correct spelling errors.\n             * This preference's value is a boolean, defaulting to `false`.\n             */\n            spellingServiceEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome offers to translate pages that aren't in a language you read.\n             * This preference's value is a boolean, defaulting to `true`.\n             */\n            translationServiceEnabled: Browser.types.ChromeSetting<boolean>;\n        };\n\n        /** Settings that influence Chrome's handling of network connections in general. */\n        export const network: {\n            /**\n             * If enabled, Chrome attempts to speed up your web browsing experience by pre-resolving DNS entries and preemptively opening TCP and SSL connections to servers.\n             * This preference only affects actions taken by Chrome's internal prediction service. It does not affect webpage-initiated prefectches or preconnects.\n             * This preference's value is a boolean, defaulting to `true`.\n             */\n            networkPredictionEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * Allow users to specify the media performance/privacy tradeoffs which impacts how WebRTC traffic will be routed and how much local address information is exposed.\n             * This preference's value is of type IPHandlingPolicy, defaulting to `default`.\n             *  @since Chrome 48\n             */\n            webRTCIPHandlingPolicy: Browser.types.ChromeSetting<`${IPHandlingPolicy}`>;\n        };\n\n        /** Settings that determine what information Chrome makes available to websites. */\n        export const websites: {\n            /**\n             * If disabled, the Attribution Reporting API and Private Aggregation API are deactivated.\n             * The value of this preference is of type boolean, and the default value is `true`.\n             * Extensions may only disable these APIs by setting the value to `false`. If you try setting these APIs to `true`, it will throw an error.\n             * @since Chrome 111\n             */\n            adMeasurementEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome sends 'Do Not Track' (`DNT: 1`) header with your requests.\n             * The value of this preference is of type boolean, and the default value is `false`.\n             * @since Chrome 65\n             */\n            doNotTrackEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If disabled, the Fledge API is deactivated.\n             * The value of this preference is of type boolean, and the default value is `true`.\n             * Extensions may only disable this API by setting the value to `false`. If you try setting this API to `true`, it will throw an error.\n             * @since Chrome 111\n             */\n            fledgeEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome sends auditing pings when requested by a website (`<a ping>`).\n             * The value of this preference is of type boolean, and the default value is `true`.\n             */\n            hyperlinkAuditingEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome provides a unique ID to plugins in order to run protected content.\n             * The value of this preference is of type boolean, and the default value is `true`.\n             * @platform Windows and ChromeOS only\n             */\n            protectedContentEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If enabled, Chrome sends `referer` headers with your requests. Yes, the name of this preference doesn't match the misspelled header. No, we're not going to change it.\n             * The value of this preference is of type boolean, and the default value is `true`.\n             */\n            referrersEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If disabled, Related Website Sets is deactivated.\n             * The value of this preference is of type boolean, and the default value is `true`.\n             * Extensions may only disable this API by setting the value to `false`. If you try setting this API to `true`, it will throw an error.\n             * @since Chrome 121\n             */\n            relatedWebsiteSetsEnabled: Browser.types.ChromeSetting<boolean>;\n\n            /** If disabled, Chrome blocks third-party sites from setting cookies. The value of this preference is of type boolean, and the default value is `true`. Extensions may not enable this API in Incognito mode, where third-party cookies are blocked and can only be allowed at the site level. If you try setting this API to true in Incognito, it will throw an error. */\n            thirdPartyCookiesAllowed: Browser.types.ChromeSetting<boolean>;\n\n            /**\n             * If disabled, the Topics API is deactivated.\n             * The value of this preference is of type boolean, and the default value is `true`.\n             * Extensions may only disable this API by setting the value to `false`. If you try setting this API to `true`, it will throw an error.\n             * @since Chrome 111\n             */\n            topicsEnabled: Browser.types.ChromeSetting<boolean>;\n        };\n    }\n\n    ////////////////////\n    // Proxy\n    ////////////////////\n    /**\n     * Use the `Browser.proxy` API to manage Chrome's proxy settings. This API relies on the ChromeSetting prototype of the type API for getting and setting the proxy configuration.\n     *\n     * Permissions: \"proxy\"\n     */\n    export namespace proxy {\n        /** @since Chrome 54 */\n        export enum Mode {\n            /** Never use a proxy */\n            DIRECT = \"direct\",\n            /** Auto detect proxy settings */\n            AUTO_DETECT = \"auto_detect\",\n            /** Use specified PAC script */\n            PAC_SCRIPT = \"pac_script\",\n            /** Manually specify proxy servers */\n            FIXED_SERVERS = \"fixed_servers\",\n            /** Use system proxy settings */\n            SYSTEM = \"system\",\n        }\n\n        /** An object holding proxy auto-config information. Exactly one of the fields should be non-empty. */\n        export interface PacScript {\n            /** URL of the PAC file to be used. */\n            url?: string | undefined;\n            /** If true, an invalid PAC script will prevent the network stack from falling back to direct connections. Defaults to false. */\n            mandatory?: boolean | undefined;\n            /** A PAC script. */\n            data?: string | undefined;\n        }\n\n        /** An object encapsulating a complete proxy configuration. */\n        export interface ProxyConfig {\n            /** The proxy rules describing this configuration. Use this for 'fixed_servers' mode. */\n            rules?: ProxyRules | undefined;\n            /** The proxy auto-config (PAC) script for this configuration. Use this for 'pac_script' mode. */\n            pacScript?: PacScript | undefined;\n            mode: `${Mode}`;\n        }\n\n        /** An object encapsulating a single proxy server's specification. */\n        export interface ProxyServer {\n            /** The hostname or IP address of the proxy server. Hostnames must be in ASCII (in Punycode format). IDNA is not supported, yet. */\n            host: string;\n            /** The scheme (protocol) of the proxy server itself. Defaults to 'http'. */\n            scheme?: `${Scheme}` | undefined;\n            /** The port of the proxy server. Defaults to a port that depends on the scheme. */\n            port?: number | undefined;\n        }\n\n        /** An object encapsulating the set of proxy rules for all protocols. Use either 'singleProxy' or (a subset of) 'proxyForHttp', 'proxyForHttps', 'proxyForFtp' and 'fallbackProxy'. */\n        export interface ProxyRules {\n            /** The proxy server to be used for FTP requests. */\n            proxyForFtp?: ProxyServer | undefined;\n            /** The proxy server to be used for HTTP requests. */\n            proxyForHttp?: ProxyServer | undefined;\n            /** The proxy server to be used for everything else or if any of the specific proxyFor... is not specified. */\n            fallbackProxy?: ProxyServer | undefined;\n            /** The proxy server to be used for all per-URL requests (that is http, https, and ftp). */\n            singleProxy?: ProxyServer | undefined;\n            /** The proxy server to be used for HTTPS requests. */\n            proxyForHttps?: ProxyServer | undefined;\n            /** List of servers to connect to without a proxy server. */\n            bypassList?: string[] | undefined;\n        }\n\n        /** @since Chrome 54 */\n        export enum Scheme {\n            HTTP = \"http\",\n            HTTPS = \"https\",\n            QUIC = \"quic\",\n            SOCKS4 = \"socks4\",\n            SOCKS5 = \"socks5\",\n        }\n\n        export interface ErrorDetails {\n            /** Additional details about the error such as a JavaScript runtime error. */\n            details: string;\n            /** The error description. */\n            error: string;\n            /** If true, the error was fatal and the network transaction was aborted. Otherwise, a direct connection is used instead. */\n            fatal: boolean;\n        }\n\n        /** Proxy settings to be used. The value of this setting is a ProxyConfig object. */\n        export const settings: types.ChromeSetting<ProxyConfig>;\n\n        /** Notifies about proxy errors. */\n        export const onProxyError: events.Event<(details: ErrorDetails) => void>;\n    }\n\n    ////////////////////\n    // ReadingList\n    ////////////////////\n    /**\n     * Use the `Browser.readingList` API to read from and modify the items in the Reading List.\n     *\n     * Permissions: \"readingList\"\n     * @since Chrome 120, MV3\n     */\n    export namespace readingList {\n        export interface AddEntryOptions {\n            /** Will be `true` if the entry has been read. */\n            hasBeenRead: boolean;\n            /** The title of the entry. */\n            title: string;\n            /** The url of the entry. */\n            url: string;\n        }\n\n        export interface QueryInfo {\n            /** Indicates whether to search for read (`true`) or unread (`false`) items. */\n            hasBeenRead?: boolean | undefined;\n            /** A title to search for. */\n            title?: string | undefined;\n            /** A url to search for. */\n            url?: string | undefined;\n        }\n\n        export interface ReadingListEntry {\n            /** The time the entry was created. Recorded in milliseconds since Jan 1, 1970. */\n            creationTime: number;\n            /** Will be `true` if the entry has been read. */\n            hasBeenRead: boolean;\n            /** The last time the entry was updated. This value is in milliseconds since Jan 1, 1970. */\n            lastUpdateTime: number;\n            /** The title of the entry. */\n            title: string;\n            /** The url of the entry. */\n            url: string;\n        }\n\n        export interface RemoveOptions {\n            /** The url to remove. */\n            url: string;\n        }\n\n        export interface UpdateEntryOptions {\n            /** The updated read status. The existing status remains if a value isn't provided. */\n            hasBeenRead?: boolean | undefined;\n            /** The new title. The existing tile remains if a value isn't provided. */\n            title?: string | undefined;\n            /** The url that will be updated. */\n            url: string;\n        }\n\n        /**\n         * Adds an entry to the reading list if it does not exist.\n         *\n         * Can return its result via Promise.\n         * @param entry The entry to add to the reading list.\n         */\n        export function addEntry(entry: AddEntryOptions): Promise<void>;\n        export function addEntry(entry: AddEntryOptions, callback: () => void): void;\n\n        /**\n         * Retrieves all entries that match the `QueryInfo` properties. Properties that are not provided will not be matched.\n         *\n         * Can return its result via Promise.\n         * @param info The properties to search for.\n         */\n        export function query(info: QueryInfo): Promise<ReadingListEntry[]>;\n        export function query(info: QueryInfo, callback: (entries: ReadingListEntry[]) => void): void;\n\n        /**\n         * Removes an entry from the reading list if it exists.\n         *\n         * Can return its result via Promise.\n         * @param info The entry to remove from the reading list.\n         */\n        export function removeEntry(info: RemoveOptions): Promise<void>;\n        export function removeEntry(info: RemoveOptions, callback: () => void): void;\n\n        /**\n         * Updates a reading list entry if it exists.\n         *\n         * Can return its result via Promise.\n         * @param info The entry to update.\n         */\n        export function updateEntry(info: UpdateEntryOptions): Promise<void>;\n        export function updateEntry(info: UpdateEntryOptions, callback: () => void): void;\n\n        /** Triggered when a `ReadingListEntry` is added to the reading list. */\n        export const onEntryAdded: events.Event<(entry: ReadingListEntry) => void>;\n\n        /** Triggered when a `ReadingListEntry` is removed from the reading list. */\n        export const onEntryRemoved: events.Event<(entry: ReadingListEntry) => void>;\n\n        /** Triggered when a `ReadingListEntry` is updated in the reading list. */\n        export const onEntryUpdated: events.Event<(entry: ReadingListEntry) => void>;\n    }\n\n    ////////////////////\n    // Search\n    ////////////////////\n    /**\n     * Use the `Browser.search` API to search via the default provider.\n     *\n     * Permissions: \"search\"\n     * @since Chrome 87\n     */\n    export namespace search {\n        export enum Disposition {\n            /** Specifies that the search results display in the calling tab or the tab from the active browser. */\n            CURRENT_TAB = \"CURRENT_TAB\",\n            /** Specifies that the search results display in a new tab. */\n            NEW_TAB = \"NEW_TAB\",\n            /** Specifies that the search results display in a new window. */\n            NEW_WINDOW = \"NEW_WINDOW\",\n        }\n\n        export type QueryInfo =\n            & {\n                /** String to query with the default search provider. */\n                text?: string | undefined;\n            }\n            & (\n                | {\n                    /** Location where search results should be displayed. `CURRENT_TAB` is the default. */\n                    disposition?: `${Disposition}` | undefined;\n                    /** Location where search results should be displayed. `tabId` cannot be used with `disposition`. */\n                    tabId?: undefined;\n                }\n                | {\n                    /** Location where search results should be displayed. `CURRENT_TAB` is the default. */\n                    disposition?: undefined;\n                    /** Location where search results should be displayed. `tabId` cannot be used with `disposition`. */\n                    tabId?: number | undefined;\n                }\n            );\n\n        /**\n         * Used to query the default search provider. In case of an error, {@link runtime.lastError} will be set.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function query(options: QueryInfo): Promise<void>;\n        export function query(options: QueryInfo, callback: () => void): void;\n    }\n\n    ////////////////////\n    // Runtime\n    ////////////////////\n    /**\n     * Use the `Browser.runtime` API to retrieve the service worker, return details about the manifest, and listen for and respond to events in the extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.\n     */\n    export namespace runtime {\n        export interface LastError {\n            /** Details about the error which occurred.  */\n            message?: string;\n        }\n\n        /** Populated with an error message if calling an API function fails; otherwise undefined. This is only defined within the scope of that function's callback. If an error is produced, but `runtime.lastError` is not accessed within the callback, a message is logged to the console listing the API function that produced the error. API functions that return promises do not set this property. */\n        export const lastError: LastError | undefined;\n\n        /** The ID of the extension/app. */\n        export const id: string;\n\n        /**\n         * The operating system Chrome is running on.\n         * @since Chrome 44\n         */\n        export enum PlatformOs {\n            /** Specifies the MacOS operating system. */\n            MAC = \"mac\",\n            /** Specifies the Windows operating system. */\n            WIN = \"win\",\n            /** Specifies the Android operating system. */\n            ANDROID = \"android\",\n            /** Specifies the Chrome operating system. */\n            CROS = \"cros\",\n            /** Specifies the Linux operating system. */\n            LINUX = \"linux\",\n            /** Specifies the OpenBSD operating system. */\n            OPENBSD = \"openbsd\",\n            /** Specifies the Fuchsia operating system. */\n            FUCHSIA = \"fuchsia\",\n        }\n\n        /**\n         * The machine's processor architecture.\n         * @since Chrome 44\n         */\n        export enum PlatformArch {\n            /** Specifies the processer architecture as arm. */\n            ARM = \"arm\",\n            /** Specifies the processer architecture as arm64. */\n            ARM64 = \"arm64\",\n            /** Specifies the processer architecture as x86-32. */\n            X86_32 = \"x86-32\",\n            /** Specifies the processer architecture as x86-64. */\n            X86_64 = \"x86-64\",\n            /** Specifies the processer architecture as mips. */\n            MIPS = \"mips\",\n            /** Specifies the processer architecture as mips64. */\n            MIPS64 = \"mips64\",\n            /** Specifies the processer architecture as riscv64. */\n            RISCV64 = \"riscv64\",\n        }\n\n        /**\n         * The native client architecture. This may be different from arch on some platforms.\n         * @since Chrome 44\n         */\n        export enum PlatformNaclArch {\n            /** Specifies the native client architecture as arm. */\n            ARM = \"arm\",\n            /** Specifies the native client architecture as x86-32. */\n            X86_32 = \"x86-32\",\n            /** Specifies the native client architecture as x86-64. */\n            X86_64 = \"x86-64\",\n            /** Specifies the native client architecture as mips. */\n            MIPS = \"mips\",\n            /** Specifies the native client architecture as mips64. */\n            MIPS64 = \"mips64\",\n        }\n\n        /** @since Chrome 114 */\n        export enum ContextType {\n            /** Specifies the context type as a tab */\n            TAB = \"TAB\",\n            /** Specifies the context type as an extension popup window */\n            POPUP = \"POPUP\",\n            /** Specifies the context type as a service worker. */\n            BACKGROUND = \"BACKGROUND\",\n            /** Specifies the context type as an offscreen document. */\n            OFFSCREEN_DOCUMENT = \"OFFSCREEN_DOCUMENT\",\n            /** Specifies the context type as a side panel. */\n            SIDE_PANEL = \"SIDE_PANEL\",\n            /** Specifies the context type as developer tools. */\n            DEVELOPER_TOOLS = \"DEVELOPER_TOOLS\",\n        }\n\n        /**\n         * The reason that this event is being dispatched.\n         * @since Chrome 44\n         */\n        export enum OnInstalledReason {\n            /** Specifies the event reason as an installation. */\n            INSTALL = \"install\",\n            /** Specifies the event reason as an extension update. */\n            UPDATE = \"update\",\n            /** Specifies the event reason as a Chrome update. */\n            CHROME_UPDATE = \"chrome_update\",\n            /** Specifies the event reason as an update to a shared module. */\n            SHARED_MODULE_UPDATE = \"shared_module_update\",\n        }\n\n        /**\n         * The reason that the event is being dispatched. 'app_update' is used when the restart is needed because the application is updated to a newer version. 'os_update' is used when the restart is needed because the browser/OS is updated to a newer version. 'periodic' is used when the system runs for more than the permitted uptime set in the enterprise policy.\n         * @since Chrome 44\n         */\n        export enum OnRestartRequiredReason {\n            /** Specifies the event reason as an update to the app. */\n            APP_UPDATE = \"app_update\",\n            /** Specifies the event reason as an update to the operating system. */\n            OS_UPDATE = \"os_update\",\n            /** Specifies the event reason as a periodic restart of the app. */\n            PERIODIC = \"periodic\",\n        }\n\n        /**\n         * A filter to match against certain extension contexts. Matching contexts must match all specified filters; any filter that is not specified matches all available contexts. Thus, a filter of `{}` will match all available contexts.\n         * @since Chrome 114\n         */\n        export interface ContextFilter {\n            contextIds?: string[] | undefined;\n            contextTypes?: `${ContextType}`[] | undefined;\n            documentIds?: string[] | undefined;\n            documentOrigins?: string[] | undefined;\n            documentUrls?: string[] | undefined;\n            frameIds?: number[] | undefined;\n            incognito?: boolean | undefined;\n            tabIds?: number[] | undefined;\n            windowIds?: number[] | undefined;\n        }\n\n        export interface ConnectInfo {\n            /** Will be passed into onConnect for processes that are listening for the connection event. */\n            name?: string | undefined;\n            /** Whether the TLS channel ID will be passed into onConnectExternal for processes that are listening for the connection event. */\n            includeTlsChannelId?: boolean | undefined;\n        }\n\n        export interface InstalledDetails {\n            /** The reason that this event is being dispatched. */\n            reason: `${OnInstalledReason}`;\n            /** Indicates the previous version of the extension, which has just been updated. This is present only if 'reason' is 'update'. */\n            previousVersion?: string;\n            /** Indicates the ID of the imported shared module extension which updated. This is present only if 'reason' is 'shared_module_update'. */\n            id?: string;\n        }\n\n        /**\n         * A context hosting extension content.\n         * @since Chrome 114\n         */\n        export interface ExtensionContext {\n            /** A unique identifier for this context */\n            contextId: string;\n            /** The type of context this corresponds to. */\n            contextType: `${ContextType}`;\n            /** A UUID for the document associated with this context, or undefined if this context is hosted not in a document.*/\n            documentId?: string;\n            /** The origin of the document associated with this context, or undefined if the context is not hosted in a document. */\n            documentOrigin?: string;\n            /** The URL of the document associated with this context, or undefined if the context is not hosted in a document. */\n            documentUrl?: string;\n            /** The ID of the frame for this context, or `-1` if this context is not hosted in a frame. */\n            frameId: number;\n            /** Whether the context is associated with an incognito profile. */\n            incognito: boolean;\n            /** The ID of the tab for this context, or `-1` if this context is not hosted in a tab. */\n            tabId: number;\n            /** The ID of the window for this context, or `-1` if this context is not hosted in a window. */\n            windowId: number;\n        }\n\n        export interface MessageOptions {\n            /** Whether the TLS channel ID will be passed into onMessageExternal for processes that are listening for the connection event. */\n            includeTlsChannelId?: boolean | undefined;\n        }\n\n        /** An object containing information about the script context that sent a message or request */\n        export interface MessageSender {\n            /** The ID of the extension that opened the connection, if any. */\n            id?: string;\n            /** The {@link tabs.Tab} which opened the connection, if any. This property will **only** be present when the connection was opened from a tab (including content scripts), and **only** if the receiver is an extension, not an app. */\n            tab?: Browser.tabs.Tab;\n            /**\n             * The name of the native application that opened the connection, if any.\n             * @since Chrome 74\n             */\n            nativeApplication?: string;\n            /** The frame that opened the connection. 0 for top-level frames, positive for child frames. This will only be set when tab is set. */\n            frameId?: number;\n            /** The URL of the page or frame that opened the connection. If the sender is in an iframe, it will be iframe's URL not the URL of the page which hosts it. */\n            url?: string;\n            /** The TLS channel ID of the page or frame that opened the connection, if requested by the extension, and if available */\n            tlsChannelId?: string;\n            /**\n             * The origin of the page or frame that opened the connection. It can vary from the url property (e.g., about:blank) or can be opaque (e.g., sandboxed iframes). This is useful for identifying if the origin can be trusted if we can't immediately tell from the URL.\n             * @since Chrome 80\n             */\n            origin?: string;\n            /**\n             * The lifecycle the document that opened the connection is in at the time the port was created. Note that the lifecycle state of the document may have changed since port creation.\n             * @since Chrome 106\n             */\n            documentLifecycle?: extensionTypes.DocumentLifecycle;\n            /**\n             * A UUID of the document that opened the connection.\n             * @since Chrome 106\n             */\n            documentId?: string;\n        }\n\n        /** An object containing information about the current platform. */\n        export interface PlatformInfo {\n            /** The operating system Chrome is running on. */\n            os: `${PlatformOs}`;\n            /** The machine's processor architecture. */\n            arch: `${PlatformArch}`;\n            /** The native client architecture. This may be different from arch on some platforms. */\n            nacl_arch?: `${PlatformNaclArch}`;\n        }\n\n        /** An object which allows two way communication with other pages. */\n        export interface Port {\n            /** Send a message to the other end of the port. If the port is disconnected, an error is thrown. */\n            postMessage: (message: any) => void;\n            /** Immediately disconnect the port. Calling `disconnect()` on an already-disconnected port has no effect. When a port is disconnected, no new events will be dispatched to this port. */\n            disconnect: () => void;\n            /** This property will **only** be present on ports passed to {@link runtime.onConnect onConnect} / {@link runtime.onConnectExternal onConnectExternal} / {@link runtime.onConnectExternal onConnectNative} listeners. */\n            sender?: MessageSender;\n            /** Fired when the port is disconnected from the other end(s). {@link runtime.lastError} may be set if the port was disconnected by an error. If the port is closed via {@link Port.disconnect disconnect}, then this event is _only_ fired on the other end. This event is fired at most once (see also Port lifetime). */\n            onDisconnect: events.Event<(port: Port) => void>;\n            /** This event is fired when {@link Port.postMessage postMessage} is called by the other end of the port. */\n            onMessage: events.Event<(message: any, port: Port) => void>;\n            /** The name of the port, as specified in the call to {@link runtime.connect}. */\n            name: string;\n        }\n\n        export interface UpdateAvailableDetails {\n            /** The version number of the available update. */\n            version: string;\n        }\n\n        export interface UpdateCheckDetails {\n            /** The version of the available update. */\n            version: string;\n        }\n\n        /**\n         * Result of the update check.\n         * @since Chrome 44\n         */\n        export enum RequestUpdateCheckStatus {\n            /** Specifies that the status check has been throttled. This can occur after repeated checks within a short amount of time. */\n            THROTTLED = \"throttled\",\n            /** Specifies that there are no available updates to install. */\n            NO_UPDATE = \"no_update\",\n            /** Specifies that there is an available update to install. */\n            UPDATE_AVAILABLE = \"update_available\",\n        }\n\n        export interface RequestUpdateCheckResult {\n            /** Result of the update check. */\n            status: `${RequestUpdateCheckStatus}`;\n            /** If an update is available, this contains the version of the available update. */\n            version?: string;\n        }\n\n        export interface ManifestIcons {\n            [size: number]: string;\n        }\n\n        export interface ManifestAction {\n            default_icon?: ManifestIcons | undefined;\n            default_title?: string | undefined;\n            default_popup?: string | undefined;\n        }\n\n        /** Source: https://developer.chrome.com/docs/extensions/reference/permissions-list */\n        export type ManifestPermission =\n            | \"accessibilityFeatures.modify\"\n            | \"accessibilityFeatures.read\"\n            | \"activeTab\"\n            | \"alarms\"\n            | \"audio\"\n            | \"background\"\n            | \"bookmarks\"\n            | \"browsingData\"\n            | \"certificateProvider\"\n            | \"clipboardRead\"\n            | \"clipboardWrite\"\n            | \"contentSettings\"\n            | \"contextMenus\"\n            | \"cookies\"\n            | \"debugger\"\n            | \"declarativeContent\"\n            | \"declarativeNetRequest\"\n            | \"declarativeNetRequestFeedback\"\n            | \"declarativeNetRequestWithHostAccess\"\n            | \"declarativeWebRequest\"\n            | \"desktopCapture\"\n            | \"documentScan\"\n            | \"downloads\"\n            | \"downloads.open\"\n            | \"downloads.shelf\"\n            | \"downloads.ui\"\n            | \"enterprise.deviceAttributes\"\n            | \"enterprise.hardwarePlatform\"\n            | \"enterprise.login\"\n            | \"enterprise.networkingAttributes\"\n            | \"enterprise.platformKeys\"\n            | \"experimental\"\n            | \"favicon\"\n            | \"fileBrowserHandler\"\n            | \"fileSystemProvider\"\n            | \"fontSettings\"\n            | \"gcm\"\n            | \"geolocation\"\n            | \"history\"\n            | \"identity\"\n            | \"identity.email\"\n            | \"idle\"\n            | \"loginState\"\n            | \"management\"\n            | \"nativeMessaging\"\n            | \"notifications\"\n            | \"offscreen\"\n            | \"pageCapture\"\n            | \"platformKeys\"\n            | \"power\"\n            | \"printerProvider\"\n            | \"printing\"\n            | \"printingMetrics\"\n            | \"privacy\"\n            | \"processes\"\n            | \"proxy\"\n            | \"readingList\"\n            | \"scripting\"\n            | \"search\"\n            | \"sessions\"\n            | \"sidePanel\"\n            | \"storage\"\n            | \"system.cpu\"\n            | \"system.display\"\n            | \"system.memory\"\n            | \"system.storage\"\n            | \"systemLog\"\n            | \"tabCapture\"\n            | \"tabGroups\"\n            | \"tabs\"\n            | \"topSites\"\n            | \"tts\"\n            | \"ttsEngine\"\n            | \"unlimitedStorage\"\n            | \"userScripts\"\n            | \"vpnProvider\"\n            | \"wallpaper\"\n            | \"webAuthenticationProxy\"\n            | \"webNavigation\"\n            | \"webRequest\"\n            | \"webRequestBlocking\"\n            | \"webRequestAuthProvider\";\n\n        /**\n         * @deprecated Use `ManifestPermission` instead.\n         */\n        export type ManifestPermissions = ManifestPermission;\n\n        /** Source : https://developer.chrome.com/docs/extensions/reference/api/permissions */\n        export type ManifestOptionalPermission = Exclude<\n            ManifestPermission,\n            | \"debugger\"\n            | \"declarativeNetRequest\"\n            | \"devtools\"\n            | \"experimental\"\n            | \"fontSettings\"\n            | \"geolocation\"\n            | \"proxy\"\n            | \"tts\"\n            | \"ttsEngine\"\n            | \"unlimitedStorage\"\n            | \"wallpaper\"\n            | \"webAuthenticationProxy\"\n        >;\n\n        /**\n         * @deprecated Use `ManifestOptionalPermission` instead.\n         */\n        export type ManifestOptionalPermissions = ManifestOptionalPermission;\n\n        export interface SearchProvider {\n            name?: string | undefined;\n            keyword?: string | undefined;\n            favicon_url?: string | undefined;\n            search_url: string;\n            encoding?: string | undefined;\n            suggest_url?: string | undefined;\n            instant_url?: string | undefined;\n            image_url?: string | undefined;\n            search_url_post_params?: string | undefined;\n            suggest_url_post_params?: string | undefined;\n            instant_url_post_params?: string | undefined;\n            image_url_post_params?: string | undefined;\n            alternate_urls?: string[] | undefined;\n            prepopulated_id?: number | undefined;\n            is_default?: boolean | undefined;\n        }\n\n        export interface ManifestBase {\n            // Required\n            manifest_version: number;\n            name: string;\n            version: string;\n\n            // Recommended\n            default_locale?: string | undefined;\n            description?: string | undefined;\n            icons?: ManifestIcons | undefined;\n\n            // Optional\n            author?: {\n                email: string;\n            } | undefined;\n            background_page?: string | undefined;\n            chrome_settings_overrides?: {\n                homepage?: string | undefined;\n                search_provider?: SearchProvider | undefined;\n                startup_pages?: string[] | undefined;\n            } | undefined;\n            chrome_ui_overrides?: {\n                bookmarks_ui?: {\n                    remove_bookmark_shortcut?: boolean | undefined;\n                    remove_button?: boolean | undefined;\n                } | undefined;\n            } | undefined;\n            chrome_url_overrides?: {\n                bookmarks?: string | undefined;\n                history?: string | undefined;\n                newtab?: string | undefined;\n            } | undefined;\n            commands?: {\n                [name: string]: {\n                    suggested_key?: {\n                        default?: string | undefined;\n                        windows?: string | undefined;\n                        mac?: string | undefined;\n                        chromeos?: string | undefined;\n                        linux?: string | undefined;\n                    } | undefined;\n                    description?: string | undefined;\n                    global?: boolean | undefined;\n                };\n            } | undefined;\n            content_capabilities?: {\n                matches?: string[] | undefined;\n                permissions?: string[] | undefined;\n            } | undefined;\n            content_scripts?:\n                | Array<{\n                    matches?: string[] | undefined;\n                    exclude_matches?: string[] | undefined;\n                    css?: string[] | undefined;\n                    js?: string[] | undefined;\n                    run_at?: string | undefined;\n                    all_frames?: boolean | undefined;\n                    match_about_blank?: boolean | undefined;\n                    include_globs?: string[] | undefined;\n                    exclude_globs?: string[] | undefined;\n                }>\n                | undefined;\n            converted_from_user_script?: boolean | undefined;\n            current_locale?: string | undefined;\n            devtools_page?: string | undefined;\n            event_rules?:\n                | Array<{\n                    event?: string | undefined;\n                    actions?:\n                        | Array<{\n                            type: string;\n                        }>\n                        | undefined;\n                    conditions?: Browser.declarativeContent.PageStateMatcherProperties[] | undefined;\n                }>\n                | undefined;\n            externally_connectable?: {\n                ids?: string[] | undefined;\n                matches?: string[] | undefined;\n                accepts_tls_channel_id?: boolean | undefined;\n            } | undefined;\n            file_browser_handlers?:\n                | Array<{\n                    id?: string | undefined;\n                    default_title?: string | undefined;\n                    file_filters?: string[] | undefined;\n                }>\n                | undefined;\n            file_system_provider_capabilities?: {\n                configurable?: boolean | undefined;\n                watchable?: boolean | undefined;\n                multiple_mounts?: boolean | undefined;\n                source?: string | undefined;\n            } | undefined;\n            homepage_url?: string | undefined;\n            import?:\n                | Array<{\n                    id: string;\n                    minimum_version?: string | undefined;\n                }>\n                | undefined;\n            export?: {\n                whitelist?: string[] | undefined;\n            } | undefined;\n            incognito?: string | undefined;\n            input_components?:\n                | Array<{\n                    name?: string | undefined;\n                    type?: string | undefined;\n                    id?: string | undefined;\n                    description?: string | undefined;\n                    language?: string[] | string | undefined;\n                    layouts?: string[] | undefined;\n                    indicator?: string | undefined;\n                }>\n                | undefined;\n            key?: string | undefined;\n            minimum_chrome_version?: string | undefined;\n            nacl_modules?:\n                | Array<{\n                    path: string;\n                    mime_type: string;\n                }>\n                | undefined;\n            oauth2?: {\n                client_id: string;\n                scopes?: string[] | undefined;\n            } | undefined;\n            offline_enabled?: boolean | undefined;\n            omnibox?: {\n                keyword: string;\n            } | undefined;\n            options_page?: string | undefined;\n            options_ui?: {\n                page?: string | undefined;\n                chrome_style?: boolean | undefined;\n                open_in_tab?: boolean | undefined;\n            } | undefined;\n            platforms?:\n                | Array<{\n                    nacl_arch?: string | undefined;\n                    sub_package_path: string;\n                }>\n                | undefined;\n            plugins?:\n                | Array<{\n                    path: string;\n                }>\n                | undefined;\n            requirements?: {\n                \"3D\"?: {\n                    features?: string[] | undefined;\n                } | undefined;\n                plugins?: {\n                    npapi?: boolean | undefined;\n                } | undefined;\n            } | undefined;\n            sandbox?: {\n                pages: string[];\n                content_security_policy?: string | undefined;\n            } | undefined;\n            short_name?: string | undefined;\n            spellcheck?: {\n                dictionary_language?: string | undefined;\n                dictionary_locale?: string | undefined;\n                dictionary_format?: string | undefined;\n                dictionary_path?: string | undefined;\n            } | undefined;\n            storage?: {\n                managed_schema: string;\n            } | undefined;\n            tts_engine?: {\n                voices: Array<{\n                    voice_name: string;\n                    lang?: string | undefined;\n                    gender?: string | undefined;\n                    event_types?: string[] | undefined;\n                }>;\n            } | undefined;\n            update_url?: string | undefined;\n            version_name?: string | undefined;\n            [key: string]: any;\n        }\n\n        export interface ManifestV2 extends ManifestBase {\n            // Required\n            manifest_version: 2;\n\n            // Pick one (or none)\n            browser_action?: ManifestAction | undefined;\n            page_action?: ManifestAction | undefined;\n\n            // Optional\n            background?:\n                | {\n                    scripts?: string[] | undefined;\n                    page?: string | undefined;\n                    persistent?: boolean | undefined;\n                }\n                | undefined;\n            content_security_policy?: string | undefined;\n            optional_permissions?: (ManifestOptionalPermission | string)[] | undefined;\n            permissions?: (ManifestPermission | string)[] | undefined;\n            web_accessible_resources?: string[] | undefined;\n        }\n\n        export interface ManifestV3 extends ManifestBase {\n            // Required\n            manifest_version: 3;\n\n            // Optional\n            action?: ManifestAction | undefined;\n            background?:\n                | {\n                    service_worker: string;\n                    type?: \"module\"; // If the service worker uses ES modules\n                }\n                | undefined;\n            content_scripts?:\n                | Array<{\n                    matches?: string[] | undefined;\n                    exclude_matches?: string[] | undefined;\n                    css?: string[] | undefined;\n                    js?: string[] | undefined;\n                    run_at?: string | undefined;\n                    all_frames?: boolean | undefined;\n                    match_about_blank?: boolean | undefined;\n                    include_globs?: string[] | undefined;\n                    exclude_globs?: string[] | undefined;\n                    world?: \"ISOLATED\" | \"MAIN\" | undefined;\n                }>\n                | undefined;\n            content_security_policy?: {\n                extension_pages?: string;\n                sandbox?: string;\n            };\n            host_permissions?: string[] | undefined;\n            optional_permissions?: ManifestOptionalPermission[] | undefined;\n            optional_host_permissions?: string[] | undefined;\n            permissions?: ManifestPermission[] | undefined;\n            web_accessible_resources?:\n                | Array<\n                    & {\n                        resources: string[];\n                        use_dynamic_url?: boolean | undefined;\n                    }\n                    & (\n                        | { extension_ids: string[]; matches?: string[] | undefined }\n                        | { matches: string[]; extension_ids?: string[] | undefined }\n                    )\n                >\n                | undefined;\n        }\n\n        export type Manifest = ManifestV2 | ManifestV3;\n\n        /**\n         * Attempts to connect listeners within an extension (such as the background page), or other extensions/apps. This is useful for content scripts connecting to their extension processes, inter-app/extension communication, and web messaging. Note that this does not connect to any listeners in a content script. Extensions may connect to content scripts embedded in tabs via {@link tabs.connect}.\n         *\n         * @param extensionId The ID of the extension to connect to. If omitted, a connection will be attempted with your own extension. Required if sending messages from a web page for web messaging.\n         * @returns Port through which messages can be sent and received. The port's {@link Port onDisconnect} event is fired if the extension does not exist.\n         */\n        export function connect(connectInfo?: ConnectInfo): Port;\n        export function connect(extensionId: string | undefined, connectInfo?: ConnectInfo): Port;\n\n        /**\n         * Connects to a native application in the host machine. This method requires the `\"nativeMessaging\"` permission. See Native Messaging for more information.\n         * @param application The name of the registered application to connect to.\n         */\n        export function connectNative(application: string): Port;\n        /**\n         * Retrieves the JavaScript 'window' object for the background page running inside the current extension/app. If the background page is an event page, the system will ensure it is loaded before calling the callback. If there is no background page, an error is set.\n         *\n         * Foreground only\n         *\n         * Can return its result via Promise since Chrome 99.\n         * @deprecated since Chrome 133. Background pages do not exist in MV3 extensions.\n         */\n        export function getBackgroundPage(): Promise<Window | undefined>;\n        export function getBackgroundPage(callback: (backgroundPage?: Window) => void): void;\n\n        /**\n         * Fetches information about active contexts associated with this extension\n         * @param filter A filter to find matching contexts. A context matches if it matches all specified fields in the filter. Any unspecified field in the filter matches all contexts.\n         * @since Chrome 116 MV3.\n         */\n        export function getContexts(filter: ContextFilter): Promise<ExtensionContext[]>;\n        export function getContexts(filter: ContextFilter, callback: (contexts: ExtensionContext[]) => void): void;\n\n        /**\n         * Returns details about the app or extension from the manifest. The object returned is a serialization of the full manifest file.\n         * @return The manifest details.\n         */\n        export function getManifest(): Manifest;\n\n        /**\n         * Returns a DirectoryEntry for the package directory.\n         *\n         * Foreground only\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 122.\n         */\n        export function getPackageDirectoryEntry(): Promise<DirectoryEntry>;\n        export function getPackageDirectoryEntry(callback: (directoryEntry: DirectoryEntry) => void): void;\n\n        /**\n         * Returns information about the current platform.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         */\n        export function getPlatformInfo(): Promise<PlatformInfo>;\n        export function getPlatformInfo(callback: (platformInfo: PlatformInfo) => void): void;\n\n        /**\n         * Converts a relative path within an app/extension install directory to a fully-qualified URL.\n         * @param path A path to a resource within an app/extension expressed relative to its install directory.\n         * @returns The fully-qualified URL to the resource.\n         */\n        export function getURL(path: string): string;\n\n        /**\n         * Returns the extension's version as declared in the manifest.\n         * @returns The extension's version.\n         * @since Chrome 143\n         */\n        export function getVersion(): string;\n\n        /** Reloads the app or extension. This method is not supported in kiosk mode. For kiosk mode, use {@link Browser.runtime.restart()} method. */\n        export function reload(): void;\n\n        /**\n         * Requests an immediate update check be done for this app/extension.\n         *\n         * **Important**: Most extensions/apps should **not** use this method, since Chrome already does automatic checks every few hours, and you can listen for the {@link runtime.onUpdateAvailable} event without needing to call requestUpdateCheck.\n         *\n         * This method is only appropriate to call in very limited circumstances, such as if your extension talks to a backend service, and the backend service has determined that the client extension version is very far out of date and you'd like to prompt a user to update. Most other uses of requestUpdateCheck, such as calling it unconditionally based on a repeating timer, probably only serve to waste client, network, and server resources.\n         *\n         * Note: When called with a callback, instead of returning an object this function will return the two properties as separate arguments passed to the callback.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 109.\n         */\n        export function requestUpdateCheck(): Promise<RequestUpdateCheckResult>;\n        export function requestUpdateCheck(\n            callback: (status: `${RequestUpdateCheckStatus}`, details?: UpdateCheckDetails) => void,\n        ): void;\n\n        /** Restart the ChromeOS device when the app runs in kiosk mode. Otherwise, it's no-op. */\n        export function restart(): void;\n\n        /**\n         * Restart the ChromeOS device when the app runs in kiosk mode after the given seconds. If called again before the time ends, the reboot will be delayed. If called with a value of `-1`, the reboot will be cancelled. It's a no-op in non-kiosk mode. It's only allowed to be called repeatedly by the first extension to invoke this API.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         * @since Chrome 53\n         */\n        export function restartAfterDelay(seconds: number): Promise<void>;\n        export function restartAfterDelay(seconds: number, callback: () => void): void;\n\n        /**\n         * Sends a single message to event listeners within your extension or a different extension/app. Similar to {@link runtime.connect} but only sends a single message, with an optional response. If sending to your extension, the {@link runtime.onMessage} event will be fired in every frame of your extension (except for the sender's frame), or {@link runtime.onMessageExternal}, if a different extension. Note that extensions cannot send messages to content scripts using this method. To send messages to content scripts, use {@link tabs.sendMessage}.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         * @param extensionId The ID of the extension to send the message to. If omitted, the message will be sent to your own extension/app. Required if sending messages from a web page for web messaging.\n         * @param message The message to send. This message should be a JSON-ifiable object.\n         */\n        export function sendMessage<M = any, R = any>(message: M, options?: MessageOptions): Promise<R>;\n        export function sendMessage<M = any, R = any>(message: M, callback: (response: R) => void): void;\n        export function sendMessage<M = any, R = any>(\n            message: M,\n            options: MessageOptions | undefined,\n            callback: (response: R) => void,\n        ): void;\n        export function sendMessage<M = any, R = any>(\n            extensionId: string | undefined | null,\n            message: M,\n            options?: MessageOptions,\n        ): Promise<R>;\n        export function sendMessage<M = any, R = any>(\n            extensionId: string | undefined | null,\n            message: M,\n            callback: (response: R) => void,\n        ): void;\n        export function sendMessage<M = any, R = any>(\n            extensionId: string | undefined | null,\n            message: M,\n            options: MessageOptions | undefined,\n            callback: (response: R) => void,\n        ): void;\n\n        /**\n         * Send a single message to a native application. This method requires the `\"nativeMessaging\"` permission\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         * @param application The name of the native messaging host.\n         * @param message The message that will be passed to the native messaging host.\n         */\n        export function sendNativeMessage(application: string, message: object): Promise<any>;\n        export function sendNativeMessage(\n            application: string,\n            message: object,\n            callback: (response: any) => void,\n        ): void;\n\n        /**\n         * Sets the URL to be visited upon uninstallation. This may be used to clean up server-side data, do analytics, and implement surveys. Maximum 1023 characters.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         * @param url URL to be opened after the extension is uninstalled. This URL must have an http: or https: scheme. Set an empty string to not open a new tab upon uninstallation.\n         */\n        export function setUninstallURL(url: string): Promise<void>;\n        export function setUninstallURL(url: string, callback: () => void): void;\n\n        /**\n         * Open your Extension's options page, if possible.\n         *\n         * The precise behavior may depend on your manifest's options_ui or options_page key, or what Chrome happens to support at the time. For example, the page may be opened in a new tab, within chrome://extensions, within an App, or it may just focus an open options page. It will never cause the caller page to reload.\n         *\n         * If your Extension does not declare an options page, or Chrome failed to create one for some other reason, the callback will set {@link runtime.lastError lastError} .\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99\n         */\n        export function openOptionsPage(): Promise<void>;\n        export function openOptionsPage(callback: () => void): void;\n\n        /** Fired when a connection is made from either an extension process or a content script (by {@link runtime.connect}). */\n        export const onConnect: events.Event<(port: Port) => void>;\n\n        /** Fired when a connection is made from another extension (by {@link runtime.connect}), or from an externally connectable web site. */\n        export const onConnectExternal: events.Event<(port: Port) => void>;\n\n        /**\n         * Fired when a connection is made from a native application. This event requires the `\"nativeMessaging\"` permission. It is only supported on Chrome OS.\n         * @since Chrome 76\n         */\n        export const onConnectNative: events.Event<(port: Port) => void>;\n\n        /** Sent to the event page just before it is unloaded. This gives the extension opportunity to do some clean up. Note that since the page is unloading, any asynchronous operations started while handling this event are not guaranteed to complete. If more activity for the event page occurs before it gets unloaded the onSuspendCanceled event will be sent and the page won't be unloaded. */\n        export const onSuspend: events.Event<() => void>;\n\n        /** Fired when a profile that has this extension installed first starts up. This event is not fired when an incognito profile is started, even if this extension is operating in 'split' incognito mode. */\n        export const onStartup: events.Event<() => void>;\n\n        /** Fired when the extension is first installed, when the extension is updated to a new version, and when Chrome is updated to a new version. */\n        export const onInstalled: events.Event<(details: InstalledDetails) => void>;\n\n        /** Sent after onSuspend to indicate that the app won't be unloaded after all. */\n        export const onSuspendCanceled: events.Event<() => void>;\n\n        /** Fired when a message is sent from either {@link runtime.sendMessage} or {@link tabs.sendMessage}. */\n        export const onMessage: events.Event<\n            (message: any, sender: MessageSender, sendResponse: (response?: any) => void) => void\n        >;\n\n        /** Fired when a message is sent from another extension (by {@link runtime.sendMessage}). Cannot be used in a content script. */\n        export const onMessageExternal: events.Event<\n            (message: any, sender: MessageSender, sendResponse: (response?: any) => void) => void\n        >;\n\n        /** Fired when an app or the device that it runs on needs to be restarted. The app should close all its windows at its earliest convenient time to let the restart to happen. If the app does nothing, a restart will be enforced after a 24-hour grace period has passed. Currently, this event is only fired for Chrome OS kiosk apps. */\n        export const onRestartRequired: events.Event<(reason: `${OnRestartRequiredReason}`) => void>;\n\n        /** Fired when an update is available, but isn't installed immediately because the app is currently running. If you do nothing, the update will be installed the next time the background page gets unloaded, if you want it to be installed sooner you can explicitly call Browser.runtime.reload(). If your extension is using a persistent background page, the background page of course never gets unloaded, so unless you call Browser.runtime.reload() manually in response to this event the update will not get installed until the next time Chrome itself restarts. If no handlers are listening for this event, and your extension has a persistent background page, it behaves as if Browser.runtime.reload() is called in response to this event. */\n        export const onUpdateAvailable: events.Event<(details: UpdateAvailableDetails) => void>;\n\n        /**\n         * Fired when a Chrome update is available, but isn't installed immediately because a browser restart is required.\n         * @deprecated Please use {@link runtime.onRestartRequired}.\n         */\n        export const onBrowserUpdateAvailable: events.Event<() => void>;\n\n        /**\n         * Fired when a connection is made from a user script from this extension.\n         * @since chrome 115 MV3\n         */\n        export const onUserScriptConnect: events.Event<(port: Port) => void>;\n\n        /**\n         * Fired when a message is sent from a user script associated with the same extension.\n         * @since chrome 115, MV3\n         */\n        export const onUserScriptMessage: events.Event<\n            (message: any, sender: MessageSender, sendResponse: (response?: any) => void) => void\n        >;\n    }\n\n    ////////////////////\n    // Scripting\n    ////////////////////\n    /**\n     * Use the `Browser.scripting` API to execute script in different contexts.\n     *\n     * Permissions: \"scripting\"\n     * @since Chrome 88, MV3\n     */\n    export namespace scripting {\n        /** The origin for a style change. See style origins for more info. */\n        export enum StyleOrigin {\n            AUTHOR = \"AUTHOR\",\n            USER = \"USER\",\n        }\n\n        /**\n         * The JavaScript world for a script to execute within.\n         * @since Chrome 95\n         */\n        export enum ExecutionWorld {\n            /** Specifies the isolated world, which is the execution environment unique to this extension. */\n            ISOLATED = \"ISOLATED\",\n            /** Specifies the main world of the DOM, which is the execution environment shared with the host page's JavaScript. */\n            MAIN = \"MAIN\",\n        }\n\n        export interface InjectionResult<T extends any = any> {\n            /**\n             * The document associated with the injection.\n             * @since Chrome 106\n             */\n            documentId: string;\n            /**\n             * The frame associated with the injection.\n             * @since Chrome 90\n             */\n            frameId: number;\n            /** The result of the script execution. */\n            result?: T;\n        }\n\n        export type InjectionTarget =\n            & {\n                /** The ID of the tab into which to inject. */\n                tabId: number;\n            }\n            & (\n                | {\n                    /** Whether the script should inject into all frames within the tab. Defaults to false. This must not be true if `frameIds` or `documentIds` is specified. */\n                    allFrames?: boolean | undefined;\n                    /**\n                     * The IDs of specific documentIds to inject into. This must not be set if `frameIds` is set.\n                     * @since Chrome 106\n                     */\n                    documentIds?: never | undefined;\n                    /** The IDs of specific frames to inject into. */\n                    frameIds?: never | undefined;\n                }\n                | {\n                    /** Whether the script should inject into all frames within the tab. Defaults to false. This must not be true if `frameIds` or `documentIds` is specified. */\n                    allFrames?: false | undefined;\n                    /**\n                     * The IDs of specific documentIds to inject into. This must not be set if `frameIds` is set.\n                     * @since Chrome 106\n                     */\n                    documentIds?: never | undefined;\n                    /** The IDs of specific frames to inject into. */\n                    frameIds: number[] | undefined;\n                }\n                | {\n                    /** Whether the script should inject into all frames within the tab. Defaults to false. This must not be true if `frameIds` or `documentIds` is specified. */\n                    allFrames?: false | undefined;\n                    /**\n                     * The IDs of specific documentIds to inject into. This must not be set if `frameIds` is set.\n                     * @since Chrome 106\n                     */\n                    documentIds?: string[] | undefined;\n                    /** The IDs of specific frames to inject into. */\n                    frameIds?: never | undefined;\n                }\n            );\n\n        export type CSSInjection =\n            & {\n                /** The style origin for the injection. Defaults to `'AUTHOR'`. */\n                origin?: `${StyleOrigin}` | undefined;\n                /** Details specifying the target into which to insert the CSS. */\n                target: InjectionTarget;\n            }\n            & (\n                | {\n                    /** A string containing the CSS to inject. Exactly one of `files` and `css` must be specified. */\n                    css: string;\n                    /** The path of the CSS files to inject, relative to the extension's root directory. Exactly one of `files` and `css` must be specified. */\n                    files?: never | undefined;\n                }\n                | {\n                    /** A string containing the CSS to inject. Exactly one of `files` and `css` must be specified. */\n                    css?: never | undefined;\n                    /** The path of the CSS files to inject, relative to the extension's root directory. Exactly one of `files` and `css` must be specified. */\n                    files: string[];\n                }\n            );\n\n        export type ScriptInjection<Args extends any[], Result> =\n            & {\n                /** Details specifying the target into which to inject the script. */\n                target: InjectionTarget;\n                /** The JavaScript \"world\" to run the script in. Defaults to `ISOLATED`. */\n                world?: `${ExecutionWorld}`;\n                /**\n                 * Whether the injection should be triggered in the target as soon as possible. Note that this is not a guarantee that injection will occur prior to page load, as the page may have already loaded by the time the script reaches the target.\n                 * @since Chrome 102\n                 */\n                injectImmediately?: boolean;\n            }\n            & (\n                | {\n                    /** A JavaScript function to inject. This function will be serialized, and then deserialized for injection. This means that any bound parameters and execution context will be lost. Exactly one of `files` or `func` must be specified. */\n                    func?: never | undefined;\n                    /** The path of the JS or CSS files to inject, relative to the extension's root directory. Exactly one of files or func must be specified. */\n                    files: string[];\n                }\n                | ({\n                    /** A JavaScript function to inject. This function will be serialized, and then deserialized for injection. This means that any bound parameters and execution context will be lost. Exactly one of `files` or `func` must be specified. */\n                    func: () => Result;\n                    /** The path of the JS or CSS files to inject, relative to the extension's root directory. Exactly one of files or func must be specified. */\n                    files?: never | undefined;\n                } | {\n                    /** The arguments to pass to the provided function. This is only valid if the `func` parameter is specified. These arguments must be JSON-serializable. */\n                    args: Args;\n                    /** A JavaScript function to inject. This function will be serialized, and then deserialized for injection. This means that any bound parameters and execution context will be lost. Exactly one of `files` or `func` must be specified. */\n                    func: (...args: Args) => Result;\n                    /** The path of the JS or CSS files to inject, relative to the extension's root directory. Exactly one of files or func must be specified. */\n                    files?: never | undefined;\n                })\n            );\n\n        type Awaited<T> = T extends PromiseLike<infer U> ? U : T;\n\n        /** @since Chrome 96 */\n        type RegisteredContentScript =\n            & {\n                /** The id of the content script, specified in the API call. Must not start with a '_' as it's reserved as a prefix for generated script IDs. */\n                id: string;\n                /** If specified true, it will inject into all frames, even if the frame is not the top-most frame in the tab. Each frame is checked independently for URL requirements; it will not inject into child frames if the URL requirements are not met. Defaults to false, meaning that only the top frame is matched. */\n                allFrames?: boolean | undefined;\n                /** Excludes pages that this content script would otherwise be injected into. See Match Patterns for more details on the syntax of these strings. */\n                excludeMatches?: string[] | undefined;\n                /**\n                 * Indicates whether the script can be injected into frames where the URL contains an unsupported scheme; specifically: about:, data:, blob:, or filesystem:. In these cases, the URL's origin is checked to determine if the script should be injected. If the origin is `null` (as is the case for data: URLs) then the used origin is either the frame that created the current frame or the frame that initiated the navigation to this frame. Note that this may not be the parent frame.\n                 * @since Chrome 119\n                 */\n                matchOriginAsFallback?: boolean | undefined;\n                /** Specifies which pages this content script will be injected into. See Match Patterns for more details on the syntax of these strings. Must be specified for {@link registerContentScripts}. */\n                matches?: string[] | undefined;\n                /** Specifies if this content script will persist into future sessions. The default is true. */\n                persistAcrossSessions?: boolean | undefined;\n                /** Specifies when JavaScript files are injected into the web page. The preferred and default value is `document_idle`. */\n                runAt?: extensionTypes.RunAt | undefined;\n                /** The JavaScript \"world\" to run the script in. Defaults to `ISOLATED`. */\n                world?: `${ExecutionWorld}` | undefined;\n            }\n            & (\n                | {\n                    /** The list of JavaScript files to be injected into matching pages. These are injected in the order they appear in this array. */\n                    js: string[];\n                    /** The list of CSS files to be injected into matching pages. These are injected in the order they appear in this array, before any DOM is constructed or displayed for the page. */\n                    css?: string[] | undefined;\n                }\n                | {\n                    /** The list of JavaScript files to be injected into matching pages. These are injected in the order they appear in this array. */\n                    js?: string[] | undefined;\n                    /** The list of CSS files to be injected into matching pages. These are injected in the order they appear in this array, before any DOM is constructed or displayed for the page. */\n                    css: string[];\n                }\n            );\n\n        /** @since Chrome 96 */\n        export interface ContentScriptFilter {\n            /** If specified, {@link getRegisteredContentScripts} will only return scripts with an id specified in this list. */\n            ids?: string[] | undefined;\n        }\n\n        /**\n         * Injects a script into a target context. By default, the script will be run at `document_idle`, or immediately if the page has already loaded. If the `injectImmediately` property is set, the script will inject without waiting, even if the page has not finished loading. If the script evaluates to a promise, the browser will wait for the promise to settle and return the resulting value.\n         *\n         * Can return its result via Promise since Chrome 90.\n         * @param injection The details of the script which to inject.\n         */\n        export function executeScript<Args extends any[], Result>(\n            injection: ScriptInjection<Args, Result>,\n        ): Promise<Array<InjectionResult<Awaited<Result>>>>;\n        export function executeScript<Args extends any[], Result>(\n            injection: ScriptInjection<Args, Result>,\n            callback: (results: Array<InjectionResult<Awaited<Result>>>) => void,\n        ): void;\n\n        /**\n         * Inserts a CSS stylesheet into a target context. If multiple frames are specified, unsuccessful injections are ignored.\n         *\n         * Can return its result via Promise since Chrome 90.\n         * @param injection The details of the styles to insert.\n         */\n        export function insertCSS(injection: CSSInjection): Promise<void>;\n        export function insertCSS(injection: CSSInjection, callback: () => void): void;\n\n        /**\n         * Removes a CSS stylesheet that was previously inserted by this extension from a target context.\n         * @param injection The details of the styles to remove. Note that the `css`, `files`, and `origin` properties must exactly match the stylesheet inserted through {@link insertCSS}. Attempting to remove a non-existent stylesheet is a no-op.\n         * @since Chrome 90\n         */\n        export function removeCSS(injection: CSSInjection): Promise<void>;\n        export function removeCSS(injection: CSSInjection, callback: () => void): void;\n\n        /**\n         * Registers one or more content scripts for this extension\n         * @param scripts Contains a list of scripts to be registered. If there are errors during script parsing/file validation, or if the IDs specified already exist, then no scripts are registered.\n         * @since Chrome 96\n         */\n        export function registerContentScripts(scripts: RegisteredContentScript[]): Promise<void>;\n        export function registerContentScripts(scripts: RegisteredContentScript[], callback: () => void): void;\n\n        /**\n         * Unregisters content scripts for this extension.\n         * @param filter If specified, only unregisters dynamic content scripts which match the filter. Otherwise, all of the extension's dynamic content scripts are unregistered.\n         * @since Chrome 96\n         */\n        export function unregisterContentScripts(filter?: ContentScriptFilter): Promise<void>;\n        export function unregisterContentScripts(callback: () => void): void;\n        export function unregisterContentScripts(filter: ContentScriptFilter | undefined, callback: () => void): void;\n\n        /**\n         * Returns all dynamically registered content scripts for this extension that match the given filter.\n         * @param filter An object to filter the extension's dynamically registered scripts.\n         * @since Chrome 96\n         */\n        export function getRegisteredContentScripts(filter?: ContentScriptFilter): Promise<RegisteredContentScript[]>;\n        export function getRegisteredContentScripts(callback: (scripts: RegisteredContentScript[]) => void): void;\n        export function getRegisteredContentScripts(\n            filter: ContentScriptFilter | undefined,\n            callback: (scripts: RegisteredContentScript[]) => void,\n        ): void;\n\n        /**\n         * Updates one or more content scripts for this extension.\n         * @param scripts Contains a list of scripts to be updated. A property is only updated for the existing script if it is specified in this object. If there are errors during script parsing/file validation, or if the IDs specified do not correspond to a fully registered script, then no scripts are updated.\n         * @since Chrome 96\n         */\n        export function updateContentScripts(scripts: RegisteredContentScript[]): Promise<void>;\n        export function updateContentScripts(scripts: RegisteredContentScript[], callback: () => void): void;\n    }\n\n    ////////////////////\n    // Sessions\n    ////////////////////\n    /**\n     * Use the `Browser.sessions` API to query and restore tabs and windows from a browsing session.\n     *\n     * Permissions: \"sessions\"\n     */\n    export namespace sessions {\n        export interface Filter {\n            /** The maximum number of entries to be fetched in the requested list. Omit this parameter to fetch the maximum number of entries ({@link sessions.MAX_SESSION_RESULTS}). */\n            maxResults?: number | undefined;\n        }\n\n        export interface Session {\n            /** The time when the window or tab was closed or modified, represented in seconds since the epoch. */\n            lastModified: number;\n            /** The {@link tabs.Tab}, if this entry describes a tab. Either this or {@link sessions.Session.window} will be set. */\n            tab?: tabs.Tab | undefined;\n            /** The {@link windows.Window}, if this entry describes a window. Either this or {@link sessions.Session.tab} will be set. */\n            window?: windows.Window | undefined;\n        }\n\n        export interface Device {\n            /** The name of the foreign device. */\n            deviceName: string;\n            /** A list of open window sessions for the foreign device, sorted from most recently to least recently modified session. */\n            sessions: Session[];\n        }\n\n        /** The maximum number of {@link sessions.Session} that will be included in a requested list. */\n        export const MAX_SESSION_RESULTS: 25;\n\n        /**\n         * Gets the list of recently closed tabs and/or windows.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getRecentlyClosed(filter?: Filter): Promise<Session[]>;\n        export function getRecentlyClosed(callback: (sessions: Session[]) => void): void;\n        export function getRecentlyClosed(filter: Filter | undefined, callback: (sessions: Session[]) => void): void;\n\n        /**\n         * Retrieves all devices with synced sessions.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function getDevices(filter?: Filter): Promise<Device[]>;\n        export function getDevices(callback: (devices: Device[]) => void): void;\n        export function getDevices(filter: Filter | undefined, callback: (devices: Device[]) => void): void;\n\n        /**\n         * Reopens a {@link windows.Window} or {@link tabs.Tab}, with an optional callback to run when the entry has been restored.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param sessionId The {@link windows.Window.sessionId}, or {@link tabs.Tab.sessionId} to restore. If this parameter is not specified, the most recently closed session is restored.\n         */\n        export function restore(sessionId?: string): Promise<Session>;\n        export function restore(callback: (restoredSession: Session) => void): void;\n        export function restore(sessionId: string | undefined, callback: (restoredSession: Session) => void): void;\n\n        /** Fired when recently closed tabs and/or windows are changed. This event does not monitor synced sessions changes. */\n        export const onChanged: events.Event<() => void>;\n    }\n\n    ////////////////////\n    // Storage\n    ////////////////////\n    /**\n     * Use the `Browser.storage` API to store, retrieve, and track changes to user data.\n     *\n     * Permissions: \"storage\"\n     */\n    export namespace storage {\n        /** NoInfer for old TypeScript versions */\n        type NoInferX<T> = T[][T extends any ? 0 : never];\n        // The next line prevents things without the export keyword from being automatically exported (like NoInferX)\n        export {};\n\n        export interface StorageArea {\n            /**\n             * Gets the amount of space (in bytes) being used by one or more items.\n             * @param keys A single key or list of keys to get the total usage for. An empty list will return 0. Pass in `null` to get the total usage of all of storage.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 95.\n             */\n            getBytesInUse<T = { [key: string]: any }>(keys?: keyof T | Array<keyof T> | null): Promise<number>;\n            getBytesInUse<T = { [key: string]: any }>(callback: (bytesInUse: number) => void): void;\n            getBytesInUse<T = { [key: string]: any }>(\n                keys: keyof T | Array<keyof T> | null | undefined,\n                callback: (bytesInUse: number) => void,\n            ): void;\n\n            /**\n             * Removes all items from storage.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 95.\n             */\n            clear(): Promise<void>;\n            clear(callback: () => void): void;\n\n            /**\n             * Sets multiple items.\n             * @param items An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected. Primitive values such as numbers will serialize as expected. Values with a `typeof` `object` and `function` will typically serialize to `{}`, with the exception of `Array` (serializes as expected), `Date`, and `Regex` (serialize using their `String` representation).\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 95.\n             */\n            set<T = { [key: string]: any }>(items: Partial<T>): Promise<void>;\n            set<T = { [key: string]: any }>(items: Partial<T>, callback: () => void): void;\n\n            /**\n             * Removes one or more items from storage.\n             * @param keys A single key or a list of keys for items to remove.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 95.\n             */\n            remove<T = { [key: string]: any }>(keys: keyof T | Array<keyof T>): Promise<void>;\n            remove<T = { [key: string]: any }>(keys: keyof T | Array<keyof T>, callback: () => void): void;\n\n            /**\n             * Gets one or more items from storage.\n             * @param keys A single key to get, list of keys to get, or a dictionary specifying default values (see description of the object). An empty list or object will return an empty result object. Pass in `null` to get the entire contents of storage.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 95.\n             */\n            get<T = { [key: string]: unknown }>(\n                keys?: NoInferX<keyof T> | Array<NoInferX<keyof T>> | Partial<NoInferX<T>> | null,\n            ): Promise<T>;\n            get<T = { [key: string]: unknown }>(callback: (items: T) => void): void;\n            get<T = { [key: string]: unknown }>(\n                keys: NoInferX<keyof T> | Array<NoInferX<keyof T>> | Partial<NoInferX<T>> | null | undefined,\n                callback: (items: T) => void,\n            ): void;\n\n            /**\n             * Sets the desired access level for the storage area. By default, session storage is restricted to trusted contexts (extension pages and service workers), while `managed`, `local`, and `sync` storage allow access from both trusted and untrusted contexts.\n             * @param accessOptions The access level of the storage area.\n             *\n             * Can return its result via Promise in Manifest V3 or later.\n             * @since Chrome 102\n             */\n            setAccessLevel(accessOptions: { accessLevel: `${AccessLevel}` }): Promise<void>;\n            setAccessLevel(accessOptions: { accessLevel: `${AccessLevel}` }, callback: () => void): void;\n\n            /** Fired when one or more items change. */\n            onChanged: events.Event<(changes: { [key: string]: StorageChange }) => void>;\n\n            /**\n             * Gets all keys from storage.\n             *\n             * Can return its result via Promise in Manifest V3 or later.\n             * @since Chrome 130\n             */\n            getKeys(): Promise<string[]>;\n            getKeys(callback: (keys: string[]) => void): void;\n        }\n\n        export interface StorageChange {\n            /** The new value of the item, if there is a new value. */\n            newValue?: unknown;\n            /** The old value of the item, if there was an old value. */\n            oldValue?: unknown;\n        }\n\n        export interface LocalStorageArea extends StorageArea {\n            /** The maximum amount (in bytes) of data that can be stored in local storage, as measured by the JSON stringification of every value plus every key's length. This value will be ignored if the extension has the unlimitedStorage permission. Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or a rejected Promise if using async/await. */\n            QUOTA_BYTES: 10485760;\n        }\n\n        export interface SyncStorageArea extends StorageArea {\n            /** @deprecated The storage.sync API no longer has a sustained write operation quota. */\n            MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE: 1000000;\n            /** The maximum total amount (in bytes) of data that can be stored in sync storage, as measured by the JSON stringification of every value plus every key's length. Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected. */\n            QUOTA_BYTES: 102400;\n            /** The maximum size (in bytes) of each individual item in sync storage, as measured by the JSON stringification of its value plus its key length. Updates containing items larger than this limit will fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected. */\n            QUOTA_BYTES_PER_ITEM: 8192;\n            /** The maximum number of items that can be stored in sync storage. Updates that would cause this limit to be exceeded will fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected. */\n            MAX_ITEMS: 512;\n            /**\n             * The maximum number of `set`, `remove`, or `clear` operations that can be performed each hour. This is 1 every 2 seconds, a lower ceiling than the short term higher writes-per-minute limit.\n             *\n             * Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected.\n             */\n            MAX_WRITE_OPERATIONS_PER_HOUR: 1800;\n            /**\n             * The maximum number of `set`, `remove`, or `clear` operations that can be performed each minute. This is 2 per second, providing higher throughput than writes-per-hour over a shorter period of time.\n             *\n             * Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected.\n             */\n            MAX_WRITE_OPERATIONS_PER_MINUTE: 120;\n        }\n\n        export interface SessionStorageArea extends StorageArea {\n            /** The maximum amount (in bytes) of data that can be stored in memory, as measured by estimating the dynamically allocated memory usage of every value and key. Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected. */\n            QUOTA_BYTES: 10485760;\n        }\n\n        export type AreaName = \"sync\" | \"local\" | \"managed\" | \"session\";\n\n        /**\n         * The storage area's access level.\n         * @since Chrome 102\n         */\n        export enum AccessLevel {\n            /** Specifies contexts originating from the extension itself. */\n            TRUSTED_CONTEXTS = \"TRUSTED_CONTEXTS\",\n            /** Specifies contexts originating from outside the extension. */\n            TRUSTED_AND_UNTRUSTED_CONTEXTS = \"TRUSTED_AND_UNTRUSTED_CONTEXTS\",\n        }\n\n        /** Items in the `local` storage area are local to each machine. */\n        export const local: LocalStorageArea;\n\n        /** Items in the `sync` storage area are synced using Chrome Sync. */\n        export const sync: SyncStorageArea;\n\n        /** Items in the `managed` storage area are set by an enterprise policy configured by the domain administrator, and are read-only for the extension; trying to modify this namespace results in an error. For information on configuring a policy, see Manifest for storage areas. */\n        export const managed: StorageArea;\n\n        /**\n         * Items in the `session` storage area are stored in-memory and will not be persisted to disk.\n         *\n         * MV3 only\n         * @since Chrome 102\n         */\n        export const session: SessionStorageArea;\n\n        /** Fired when one or more items change. */\n        export const onChanged: events.Event<(changes: { [key: string]: StorageChange }, areaName: AreaName) => void>;\n    }\n\n    ////////////////////\n    // System CPU\n    ////////////////////\n    /**\n     * Use the `system.cpu` API to query CPU metadata.\n     *\n     * Permissions: \"system.cpu\"\n     */\n    export namespace system.cpu {\n        export interface CpuTime {\n            /** The cumulative time used by userspace programs on this processor. */\n            user: number;\n            /** The cumulative time used by kernel programs on this processor. */\n            kernel: number;\n            /** The cumulative time spent idle by this processor. */\n            idle: number;\n            /** The total cumulative time for this processor. This value is equal to user + kernel + idle. */\n            total: number;\n        }\n\n        /** @deprecated Use {@link CpuTime} instead. */\n        // eslint-disable-next-line @typescript-eslint/no-empty-interface\n        interface ProcessorUsage extends CpuTime {}\n\n        export interface ProcessorInfo {\n            /** Cumulative usage info for this logical processor. */\n            usage: CpuTime;\n        }\n\n        export interface CpuInfo {\n            /** The number of logical processors. */\n            numOfProcessors: number;\n            /** The architecture name of the processors. */\n            archName: string;\n            /** The model name of the processors. */\n            modelName: string;\n            /**\n             * A set of feature codes indicating some of the processor's capabilities.\n             * The currently supported codes are \"mmx\", \"sse\", \"sse2\", \"sse3\", \"ssse3\", \"sse4_1\", \"sse4_2\", and \"avx\".\n             */\n            features: string[];\n            /** Information about each logical processor. */\n            processors: ProcessorInfo[];\n            /**\n             * List of CPU temperature readings from each thermal zone of the CPU. Temperatures are in degrees Celsius.\n             * @since Chrome 60\n             */\n            temperatures: number[];\n        }\n\n        /**\n         * Queries basic CPU information of the system.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function getInfo(): Promise<CpuInfo>;\n        export function getInfo(callback: (info: CpuInfo) => void): void;\n    }\n\n    ////////////////////\n    // System Memory\n    ////////////////////\n    /**\n     * The `Browser.system.memory` API.\n     *\n     * Permissions: \"system.memory\"\n     */\n    export namespace system.memory {\n        export interface MemoryInfo {\n            /** The total amount of physical memory capacity, in bytes. */\n            capacity: number;\n            /** The amount of available capacity, in bytes. */\n            availableCapacity: number;\n        }\n\n        /**\n         * Get physical memory information.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function getInfo(): Promise<MemoryInfo>;\n        export function getInfo(callback: (info: MemoryInfo) => void): void;\n    }\n\n    ////////////////////\n    // System Storage\n    ////////////////////\n    /**\n     * Use the `Browser.system.storage` API to query storage device information and be notified when a removable storage device is attached and detached.\n     *\n     * Permissions: \"system.storage\"\n     */\n    export namespace system.storage {\n        export enum EjectDeviceResultCode {\n            /** The ejection command is successful -- the application can prompt the user to remove the device. */\n            SUCCESS = \"success\",\n            /** The device is in use by another application. The ejection did not succeed; the user should not remove the device until the other application is done with the device. */\n            IN_USE = \"in_use\",\n            /** There is no such device known. */\n            NO_SUCH_DEVICE = \"no_such_device\",\n            /** The ejection command failed. */\n            FAILURE = \"failure\",\n        }\n\n        export interface StorageUnitInfo {\n            /** The transient ID that uniquely identifies the storage device. This ID will be persistent within the same run of a single application. It will not be a persistent identifier between different runs of an application, or between different applications. */\n            id: string;\n            /** The name of the storage unit. */\n            name: string;\n            /** The media type of the storage unit. */\n            type: `${StorageUnitType}`;\n            /** The total amount of the storage space, in bytes. */\n            capacity: number;\n        }\n\n        export enum StorageUnitType {\n            /** The storage has fixed media, e.g. hard disk or SSD. */\n            FIXED = \"fixed\",\n            /** The storage is removable, e.g. USB flash drive. */\n            REMOVABLE = \"removable\",\n            /** The storage type is unknown. */\n            UNKNOWN = \"unknown\",\n        }\n\n        export interface StorageAvailableCapacityInfo {\n            /** A copied `id` of getAvailableCapacity function parameter `id`. */\n            id: string;\n            /** The available capacity of the storage device, in bytes. */\n            availableCapacity: number;\n        }\n\n        /**\n         * Get the storage information from the system. The argument passed to the callback is an array of StorageUnitInfo objects.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function getInfo(): Promise<StorageUnitInfo[]>;\n        export function getInfo(callback: (info: StorageUnitInfo[]) => void): void;\n\n        /**\n         * Ejects a removable storage device.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function ejectDevice(id: string): Promise<`${EjectDeviceResultCode}`>;\n        export function ejectDevice(id: string, callback: (result: `${EjectDeviceResultCode}`) => void): void;\n\n        /**\n         * Get the available capacity of a specified `id` storage device. The `id` is the transient device ID from StorageUnitInfo.\n         *\n         * Can return its result via Promise in Manifest V3.\n         * @since Dev channel only.\n         */\n        export function getAvailableCapacity(id: string): Promise<StorageAvailableCapacityInfo>;\n        export function getAvailableCapacity(id: string, callback: (info: StorageAvailableCapacityInfo) => void): void;\n\n        /** Fired when a new removable storage is attached to the system. */\n        export const onAttached: events.Event<(info: StorageUnitInfo) => void>;\n\n        /** Fired when a removable storage is detached from the system. */\n        export const onDetached: events.Event<(id: string) => void>;\n    }\n\n    ////////////////////\n    // System Display //\n    ////////////////////\n    /**\n     * Use the `system.display` API to query display metadata.\n     *\n     * Permissions: \"system.display\"\n     */\n    export namespace system.display {\n        export enum LayoutPosition {\n            TOP = \"top\",\n            RIGHT = \"right\",\n            BOTTOM = \"bottom\",\n            LEFT = \"left\",\n        }\n\n        /**\n         * Mirror mode, i.e. different ways of how a display is mirrored to other displays.\n         * @since Chrome 65\n         */\n        export enum MirrorMode {\n            /** Specifies the default mode (extended or unified desktop). */\n            OFF = \"off\",\n            /** Specifies that the default source display will be mirrored to all other displays. */\n            NORMAL = \"normal\",\n            /**\n             * Specifies that the specified source display will be mirrored to the provided destination displays.\n             * All other connected displays will be extended.\n             */\n            MIXED = \"mixed\",\n        }\n\n        export interface Bounds {\n            /**  The x-coordinate of the upper-left corner. */\n            left: number;\n            /**  The y-coordinate of the upper-left corner. */\n            top: number;\n            /** The width of the display in pixels. */\n            width: number;\n            /** The height of the display in pixels. */\n            height: number;\n        }\n\n        export interface Insets {\n            /** The x-axis distance from the left bound. */\n            left: number;\n            /** The y-axis distance from the top bound. */\n            top: number;\n            /** The x-axis distance from the right bound. */\n            right: number;\n            /** The y-axis distance from the bottom bound. */\n            bottom: number;\n        }\n\n        /** @since Chrome 57 */\n        export interface Point {\n            /** The x-coordinate of the point. */\n            x: number;\n            /** The y-coordinate of the point. */\n            y: number;\n        }\n\n        /** @since Chrome 57 */\n        export interface TouchCalibrationPair {\n            /** The coordinates of the display point. */\n            displayPoint: Point;\n            /** The coordinates of the touch point corresponding to the display point. */\n            touchPoint: Point;\n        }\n\n        /** @since Chrome 52 */\n        export interface DisplayMode {\n            /** The display mode width in device independent (user visible) pixels. */\n            width: number;\n            /** The display mode height in device independent (user visible) pixels. */\n            height: number;\n            /** The display mode width in native pixels. */\n            widthInNativePixels: number;\n            /** The display mode height in native pixels. */\n            heightInNativePixels: number;\n            /**\n             * True if this mode is interlaced, false if not provided.\n             * @since Chrome 74\n             */\n            isInterlaced?: boolean;\n            /**\n             * The display mode UI scale factor.\n             * @deprecated Deprecated since Chrome 70. Use `displayZoomFactor`\n             */\n            uiScale?: number;\n            /** The display mode device scale factor. */\n            deviceScaleFactor: number;\n            /**\n             * The display mode refresh rate in hertz.\n             * @since Chrome 67\n             */\n            refreshRate: number;\n            /** True if the mode is the display's native mode. */\n            isNative: boolean;\n            /** True if the display mode is currently selected. */\n            isSelected: boolean;\n        }\n\n        /** @since Chrome 53 */\n        export interface DisplayLayout {\n            /** The unique identifier of the display. */\n            id: string;\n            /** The unique identifier of the parent display. Empty if this is the root. */\n            parentId: string;\n            /**\n             * The layout position of this display relative to the parent.\n             * This will be ignored for the root.\n             */\n            position: `${LayoutPosition}`;\n            /** The offset of the display along the connected edge. 0 indicates that the topmost or leftmost corners are aligned. */\n            offset: number;\n        }\n\n        /** The pairs of point used to calibrate the display. */\n        export interface TouchCalibrationPairQuad {\n            /** First pair of touch and display point required for touch calibration. */\n            pair1: TouchCalibrationPair;\n            /** Second pair of touch and display point required for touch calibration. */\n            pair2: TouchCalibrationPair;\n            /** Third pair of touch and display point required for touch calibration. */\n            pair3: TouchCalibrationPair;\n            /** Fourth pair of touch and display point required for touch calibration. */\n            pair4: TouchCalibrationPair;\n        }\n\n        export interface DisplayProperties {\n            /**\n             * If set to true, changes the display mode to unified desktop (see `enableUnifiedDesktop` for details).\n             * If set to false, unified desktop mode will be disabled.\n             * This is only valid for the primary display.\n             * If provided, mirroringSourceId must not be provided and other properties will be ignored.\n             * This is has no effect if not provided.\n             * @platform ChromeOS only\n             * @since Chrome 59\n             */\n            isUnified?: boolean | undefined;\n            /**\n             * If set and not empty, enables mirroring for this display only.\n             * Otherwise disables mirroring for all displays.\n             * This value should indicate the id of the source display to mirror, which must not be the same as the id passed to setDisplayProperties.\n             * If set, no other property may be set.\n             * @platform ChromeOS only\n             * @deprecated Deprecated since Chrome 68. Use ´setMirrorMode´\n             */\n            mirroringSourceId?: string | undefined;\n            /**\n             * If set to true, makes the display primary.\n             * No-op if set to false.\n             * Note: If set, the display is considered primary for all other properties (i.e. `isUnified` may be set and bounds origin may not).\n             */\n            isPrimary?: boolean | undefined;\n            /**\n             * If set, sets the display's overscan insets to the provided values.\n             * Note that overscan values may not be negative or larger than a half of the screen's size.\n             * Overscan cannot be changed on the internal monitor.\n             */\n            overscan?: Insets | undefined;\n            /**\n             * If set, updates the display's rotation.\n             * Legal values are [0, 90, 180, 270].\n             * The rotation is set clockwise, relative to the display's vertical position.\n             * It's applied after overscan parameter.\n             */\n            rotation?: 0 | 90 | 180 | 270 | undefined;\n            /**\n             * If set, updates the display's logical bounds origin along the x-axis.\n             * Applied together with `boundsOriginY`.\n             * Defaults to the current value if not set and `boundsOriginY` is set.\n             * Note that when updating the display origin, some constraints will be applied, so the final bounds origin may be different than the one set.\n             * The final bounds can be retrieved using `getInfo`.\n             * The bounds origin cannot be changed on the primary display.\n             */\n            boundsOriginX?: number | undefined;\n            /**\n             * If set, updates the display's logical bounds origin along the y-axis.\n             * See documentation for `boundsOriginX` parameter.\n             */\n            boundsOriginY?: number | undefined;\n            /**\n             * If set, updates the display mode to the mode matching this value.\n             * If other parameters are invalid, this will not be applied.\n             * If the display mode is invalid, it will not be applied and an error will be set, but other properties will still be applied.\n             * @since Chrome 52\n             */\n            displayMode?: DisplayMode | undefined;\n            /**\n             * If set, updates the zoom associated with the display.\n             * This zoom performs re-layout and repaint thus resulting in a better quality zoom than just performing a pixel by pixel stretch enlargement.\n             * @since Chrome 65\n             */\n            displayZoomFactor?: number | undefined;\n        }\n\n        /**\n         * Options affecting how the information is returned.\n         * @since Chrome 59\n         */\n        export interface GetInfoFlags {\n            /**\n             * If set to true, only a single `DisplayUnitInfo` will be returned by `getInfo` when in unified desktop mode (see `enableUnifiedDesktop`).\n             * @default false\n             */\n            singleUnified?: boolean | undefined;\n        }\n\n        /**\n         * An enum to tell if the display is detected and used by the system.\n         * The display is considered 'inactive', if it is not detected by the system (maybe disconnected, or considered disconnected due to sleep mode, etc).\n         * This state is used to keep existing display when the all displays are disconnected, for example.\n         * @since Chrome 117\n         */\n        export enum ActiveState {\n            ACTIVE = \"active\",\n            INACTIVE = \"inactive\",\n        }\n\n        export interface DisplayUnitInfo {\n            /**\n             * Active if the display is detected and used by the system.\n             * @since Chrome 117\n             */\n            activeState: `${ActiveState}`;\n            /** The unique identifier of the display. */\n            id: string;\n            /** The user-friendly name (e.g. 'HP LCD monitor'). */\n            name: string;\n            /**\n             * @platform ChromeOS and Web UI only\n             * @since Chrome 67\n             */\n            edid?: Edid;\n            /**\n             * Identifier of the display that is being mirrored if mirroring is enabled, otherwise empty.\n             * This will be set for all displays (including the display being mirrored).\n             * @platform ChromeOS only\n             */\n            mirroringSourceId: string;\n            /**\n             * Identifiers of the displays to which the source display is being mirrored.\n             * Empty if no displays are being mirrored.\n             * This must not include `mirroringSourceId`.\n             * @platform ChromeOS only\n             * @since Chrome 64\n             */\n            mirroringDestinationIds: string[];\n            /** True if this is the primary display. */\n            isPrimary: boolean;\n            /** True if this is an internal display. */\n            isInternal: boolean;\n            /** True if this display is enabled. */\n            isEnabled: boolean;\n            /**\n             * True for all displays when in unified desktop mode. See documentation for `enableUnifiedDesktop`.\n             * @since Chrome 59\n             */\n            isUnified: boolean;\n            /** The number of pixels per inch along the x-axis. */\n            dpiX: number;\n            /** The number of pixels per inch along the y-axis. */\n            dpiY: number;\n            /** The display's clockwise rotation in degrees relative to the vertical position.\n             * Currently exposed only on ChromeOS. Will be set to 0 on other platforms.\n             * A value of -1 will be interpreted as auto-rotate when the device is in a physical tablet state.\n             * @platform ChromeOS only\n             */\n            rotation: number;\n            /** The display's logical bounds. */\n            bounds: Bounds;\n            /**\n             * The display's insets within its screen's bounds.\n             * Currently exposed only on ChromeOS. Will be set to empty insets on other platforms.\n             * @platform ChromeOS only\n             */\n            overscan: Insets;\n            /**\n             * The usable work area of the display within the display bounds.\n             * The work area excludes areas of the display reserved for OS, for example taskbar and launcher.\n             */\n            workArea: Bounds;\n            /**\n             * The list of available display modes.\n             * The current mode will have isSelected=true.\n             * Will be set to an empty array on other platforms.\n             * @platform ChromeOS only\n             * @since Chrome 52\n             */\n            modes: DisplayMode[];\n            /**\n             * True if this display has a touch input device associated with it.\n             * @since Chrome 57\n             */\n            hasTouchSupport: boolean;\n            /**\n             * A list of zoom factor values that can be set for the display.\n             * @since Chrome 67\n             */\n            availableDisplayZoomFactors: number[];\n            /**\n             * The ratio between the display's current and default zoom.\n             * For example, value 1 is equivalent to 100% zoom, and value 1.5 is equivalent to 150% zoom.\n             * @since Chrome 65\n             */\n            displayZoomFactor: number;\n        }\n\n        /** @since Chrome 67 */\n        export interface Edid {\n            /** 3 character manufacturer code. */\n            manufacturerId: string;\n            /** 2 byte manufacturer-assigned code. */\n            productId: string;\n            /** Year of manufacturer. */\n            yearOfManufacture: number;\n        }\n\n        export interface MirrorModeInfo {\n            /** The mirror mode that should be set. */\n            mode?: `${MirrorMode}`;\n        }\n\n        export interface MirrorModeInfoMixed extends MirrorModeInfo {\n            /** The mirror mode that should be set. */\n            mode: \"mixed\";\n            /** The id of the mirroring source display. */\n            mirroringSourceId?: string | undefined;\n            /** The ids of the mirroring destination displays. */\n            mirroringDestinationIds?: string[] | undefined;\n        }\n\n        /**\n         * Requests the information for all attached display devices.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @param flags Options affecting how the information is returned.\n         */\n        export function getInfo(callback: (displayInfo: DisplayUnitInfo[]) => void): void;\n        export function getInfo(flags: GetInfoFlags, callback: (displayInfo: DisplayUnitInfo[]) => void): void;\n        export function getInfo(): Promise<DisplayUnitInfo[]>;\n        export function getInfo(flags: GetInfoFlags): Promise<DisplayUnitInfo[]>;\n\n        /**\n         * Requests the layout info for all displays\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @platform ChromeOS and Web UI only\n         * @since Chrome 53\n         */\n        export function getDisplayLayout(callback: (layouts: DisplayLayout[]) => void): void;\n        export function getDisplayLayout(): Promise<DisplayLayout[]>;\n\n        /**\n         * requires(CrOS Kiosk apps | WebUI) This is only available to Chrome OS Kiosk apps and Web UI.\n         * @description\n         * Updates the properties for the display specified by `id`,\n         * according to the information provided in `info`.\n         * On failure, `runtime.lastError` will be set.\n         * @platform ChromeOS and Web UI only\n         * @param id The display's unique identifier.\n         * @param info The information about display properties that should be changed. A property will be changed only if a new value for it is specified in `info`.\n         */\n        export function setDisplayProperties(id: string, info: DisplayProperties, callback: () => void): void;\n        export function setDisplayProperties(id: string, info: DisplayProperties): Promise<void>;\n\n        /**\n         * Set the layout for all displays.\n         * Any display not included will use the default layout.\n         * If a layout would overlap or be otherwise invalid it will be adjusted to a valid layout.\n         * After layout is resolved, an onDisplayChanged event will be triggered.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @platform ChromeOS and Web UI only\n         * @since Chrome 53\n         * @param layouts The layout information, required for all displays except the primary display.\n         */\n        export function setDisplayLayout(layouts: DisplayLayout[], callback: () => void): void;\n        export function setDisplayLayout(layouts: DisplayLayout[]): Promise<void>;\n\n        /**\n         * Enables/disables the unified desktop feature.\n         * If enabled while mirroring is active, the desktop mode will not change until mirroring is turned off.\n         * Otherwise, the desktop mode will switch to unified immediately.\n         * @since Chrome 46\n         * @platform ChromeOS and Web UI only\n         * @param enabled True if unified desktop should be enabled.\n         */\n        export function enableUnifiedDesktop(enabled: boolean): void;\n\n        /**\n         * Starts overscan calibration for a display.\n         * This will show an overlay on the screen indicating the current overscan insets.\n         * If overscan calibration for display `id` is in progress this will reset calibration.\n         * @since Chrome 53\n         * @param id The display's unique identifier.\n         */\n        export function overscanCalibrationStart(id: string): void;\n\n        /**\n         * Adjusts the current overscan insets for a display.\n         * Typically this should either move the display along an axis (e.g. left+right have the same value)\n         * or scale it along an axis (e.g. top+bottom have opposite values).\n         * Each Adjust call is cumulative with previous calls since Start.\n         * @since Chrome 53\n         * @param id The display's unique identifier.\n         * @param delta The amount to change the overscan insets.\n         */\n        export function overscanCalibrationAdjust(id: string, delta: Insets): void;\n\n        /**\n         * Resets the overscan insets for a display to the last saved value (i.e before Start was called).\n         * @since Chrome 53\n         * @param id The display's unique identifier.\n         */\n        export function overscanCalibrationReset(id: string): void;\n\n        /**\n         * Complete overscan adjustments for a display by saving the current values and hiding the overlay.\n         * @since Chrome 53\n         * @param id The display's unique identifier.\n         */\n        export function overscanCalibrationComplete(id: string): void;\n\n        /**\n         * Displays the native touch calibration UX for the display with `id` as display id.\n         * This will show an overlay on the screen with required instructions on how to proceed.\n         * The callback will be invoked in case of successful calibration only.\n         * If the calibration fails, this will throw an error.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @since Chrome 57\n         * @param id The display's unique identifier.\n         */\n        export function showNativeTouchCalibration(id: string, callback: (success: boolean) => void): void;\n        export function showNativeTouchCalibration(id: string): Promise<boolean>;\n\n        /**\n         * Starts custom touch calibration for a display.\n         * This should be called when using a custom UX for collecting calibration data.\n         * If another touch calibration is already in progress this will throw an error.\n         * @since Chrome 57\n         * @param id The display's unique identifier.\n         */\n        export function startCustomTouchCalibration(id: string): void;\n\n        /**\n         * Sets the touch calibration pairs for a display.\n         * These `pairs` would be used to calibrate the touch screen for display with `id` called in startCustomTouchCalibration().\n         * Always call `startCustomTouchCalibration` before calling this method.\n         * If another touch calibration is already in progress this will throw an error.\n         * @since Chrome 57\n         * @param pairs The pairs of point used to calibrate the display.\n         * @param bounds Bounds of the display when the touch calibration was performed. `bounds.left` and `bounds.top` values are ignored.\n         * @throws Error\n         */\n        export function completeCustomTouchCalibration(pairs: TouchCalibrationPairQuad, bounds: Bounds): void;\n\n        /**\n         * Resets the touch calibration for the display and brings it back to its default state by clearing any touch calibration data associated with the display.\n         * @since Chrome 57\n         * @param id The display's unique identifier.\n         */\n        export function clearTouchCalibration(id: string): void;\n\n        /**\n         * Sets the display mode to the specified mirror mode.\n         * Each call resets the state from previous calls.\n         * Calling setDisplayProperties() will fail for the mirroring destination displays.\n         * @platform ChromeOS and Web UI only\n         * @param info The information of the mirror mode that should be applied to the display mode.\n         * @since Chrome 65\n         */\n        export function setMirrorMode(info: MirrorModeInfo | MirrorModeInfoMixed, callback: () => void): void;\n        export function setMirrorMode(info: MirrorModeInfo | MirrorModeInfoMixed): Promise<void>;\n\n        /** Fired when anything changes to the display configuration. */\n        export const onDisplayChanged: Browser.events.Event<() => void>;\n    }\n\n    ////////////////////\n    // SystemLog\n    ////////////////////\n    /**\n     * Use the `Browser.systemLog` API to record Chrome system logs from extensions.\n     *\n     * Permissions: \"systemLog\"\n     *\n     * Note: Only available to policy installed extensions.\n     * @platform ChromeOS only\n     * @since Chrome 125\n     */\n    export namespace systemLog {\n        export interface MessageOptions {\n            message: string;\n        }\n\n        /**\n         * Adds a new log record.\n         * Can return its result via Promise in Manifest V3 or later.\n         */\n        export function add(options: MessageOptions): Promise<void>;\n        export function add(options: MessageOptions, callback: () => void): void;\n    }\n\n    ////////////////////\n    // TabCapture\n    ////////////////////\n    /**\n     * Use the `Browser.tabCapture` API to interact with tab media streams.\n     *\n     * Permissions: \"tabCapture\"\n     */\n    export namespace tabCapture {\n        export interface CaptureInfo {\n            /** The id of the tab whose status changed. */\n            tabId: number;\n            /** The new capture status of the tab. */\n            status: `${TabCaptureState}`;\n            /** Whether an element in the tab being captured is in fullscreen mode. */\n            fullscreen: boolean;\n        }\n\n        export interface MediaStreamConstraint {\n            mandatory: object;\n            optional?: object | undefined;\n        }\n\n        export interface CaptureOptions {\n            audio?: boolean | undefined;\n            video?: boolean | undefined;\n            audioConstraints?: MediaStreamConstraint | undefined;\n            videoConstraints?: MediaStreamConstraint | undefined;\n        }\n\n        /** @since Chrome 71 */\n        export interface GetMediaStreamOptions {\n            /** Optional tab id of the tab which will later invoke `getUserMedia()` to consume the stream. If not specified then the resulting stream can be used only by the calling extension. The stream can only be used by frames in the given tab whose security origin matches the consumber tab's origin. The tab's origin must be a secure origin, e.g. HTTPS. */\n            consumerTabId?: number | undefined;\n            /** Optional tab id of the tab which will be captured. If not specified then the current active tab will be selected. Only tabs for which the extension has been granted the `activeTab` permission can be used as the target tab. */\n            targetTabId?: number | undefined;\n        }\n\n        export enum TabCaptureState {\n            PENDING = \"pending\",\n            ACTIVE = \"active\",\n            STOPPED = \"stopped\",\n            ERROR = \"error\",\n        }\n\n        /**\n         * Captures the visible area of the currently active tab. Capture can only be started on the currently active tab after the extension has been invoked, similar to the way that activeTab works. Capture is maintained across page navigations within the tab, and stops when the tab is closed, or the media stream is closed by the extension.\n         * @param options Configures the returned media stream.\n         */\n        export function capture(options: CaptureOptions, callback: (stream: MediaStream | null) => void): void;\n\n        /**\n         * Returns a list of tabs that have requested capture or are being captured, i.e. status != stopped and status != error. This allows extensions to inform the user that there is an existing tab capture that would prevent a new tab capture from succeeding (or to prevent redundant requests for the same tab).\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function getCapturedTabs(): Promise<CaptureInfo[]>;\n        export function getCapturedTabs(callback: (result: CaptureInfo[]) => void): void;\n\n        /**\n         * Creates a stream ID to capture the target tab. Similar to Browser.tabCapture.capture() method, but returns a media stream ID, instead of a media stream, to the consumer tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function getMediaStreamId(options?: GetMediaStreamOptions): Promise<string>;\n        export function getMediaStreamId(callback: (streamId: string) => void): void;\n        export function getMediaStreamId(\n            options: GetMediaStreamOptions | undefined,\n            callback: (streamId: string) => void,\n        ): void;\n\n        /** Event fired when the capture status of a tab changes. This allows extension authors to keep track of the capture status of tabs to keep UI elements like page actions in sync. */\n        export const onStatusChanged: events.Event<(info: CaptureInfo) => void>;\n    }\n\n    ////////////////////\n    // Tabs\n    ////////////////////\n    /**\n     * Use the `Browser.tabs` API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.\n     *\n     * Permissions: The majority of the Browser.tabs API can be used without declaring any permission. However, the \"tabs\" permission is required in order to populate the url, title, and favIconUrl properties of Tab.\n     */\n    export namespace tabs {\n        /**\n         * The tab's muted state and the reason for the last state change.\n         * @since Chrome 46\n         */\n        export interface MutedInfo {\n            /** Whether the tab is muted (prevented from playing sound). The tab may be muted even if it has not played or is not currently playing sound. Equivalent to whether the 'muted' audio indicator is showing. */\n            muted: boolean;\n            /* The reason the tab was muted or unmuted. Not set if the tab's mute state has never been changed. */\n            reason?: `${MutedInfoReason}` | undefined;\n            /** The ID of the extension that changed the muted state. Not set if an extension was not the reason the muted state last changed. */\n            extensionId?: string | undefined;\n        }\n\n        /**\n         * An event that caused a muted state change.\n         * @since Chrome 46\n         */\n        export enum MutedInfoReason {\n            /** A user input action set the muted state. */\n            USER = \"user\",\n            /** Tab capture was started, forcing a muted state change. */\n            CAPTURE = \"capture\",\n            /** An extension set the muted state. */\n            EXTENSION = \"extension\",\n        }\n\n        export interface Tab {\n            /** The tab's loading status. */\n            status?: `${TabStatus}` | undefined;\n            /** The zero-based index of the tab within its window. */\n            index: number;\n            /** The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists. */\n            openerTabId?: number | undefined;\n            /** The title of the tab. This property is only present if the extension has the `\"tabs\"` permission or has host permissions for the page. */\n            title?: string | undefined;\n            /** The last committed URL of the main frame of the tab. This property is only present if the extension has the `\"tabs\"` permission or has host permissions for the page. May be an empty string if the tab has not yet committed. See also {@link Tab.pendingUrl}. */\n            url?: string | undefined;\n            /**\n             * The URL the tab is navigating to, before it has committed. This property is only present if the extension has the `\"tabs\"` permission or has host permissions for the page and there is a pending navigation.\n             * @since Chrome 79\n             */\n            pendingUrl?: string | undefined;\n            /** Whether the tab is pinned. */\n            pinned: boolean;\n            /** Whether the tab is highlighted. */\n            highlighted: boolean;\n            /** The ID of the window that contains the tab. */\n            windowId: number;\n            /** Whether the tab is active in its window. Does not necessarily mean the window is focused. */\n            active: boolean;\n            /** The URL of the tab's favicon. This property is only present if the extension has the `tabs` permission or has host permissions for the page. It may also be an empty string if the tab is loading. */\n            favIconUrl?: string | undefined;\n            /**\n             * Whether the tab is frozen. A frozen tab cannot execute tasks, including event handlers or timers. It is visible in the tab strip and its content is loaded in memory. It is unfrozen on activation.\n             * @since Chrome 132\n             */\n            frozen: boolean;\n            /** The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a tab may not be assigned an ID; for example, when querying foreign tabs using the {@link sessions} API, in which case a session ID may be present. Tab ID can also be set to `Browser.tabs.TAB_ID_NONE` for apps and devtools windows. */\n            id?: number | undefined;\n            /** Whether the tab is in an incognito window. */\n            incognito: boolean;\n            /**\n             * Whether the tab is selected.\n             * @deprecated since Chrome 33. Please use {@link Tab.highlighted}.\n             */\n            selected: boolean;\n            /**\n             * Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the 'speaker audio' indicator is showing.\n             * @since Chrome 45\n             */\n            audible?: boolean | undefined;\n            /**\n             * Whether the tab is discarded. A discarded tab is one whose content has been unloaded from memory, but is still visible in the tab strip. Its content is reloaded the next time it is activated.\n             * @since Chrome 54\n             */\n            discarded: boolean;\n            /**\n             * Whether the tab can be discarded automatically by the browser when resources are low.\n             * @since Chrome 54\n             */\n            autoDiscardable: boolean;\n            /**\n             * The tab's muted state and the reason for the last state change.\n             * @since Chrome 46\n             */\n            mutedInfo?: MutedInfo | undefined;\n            /** The width of the tab in pixels. */\n            width?: number | undefined;\n            /** The height of the tab in pixels. */\n            height?: number | undefined;\n            /** The session ID used to uniquely identify a tab obtained from the {@link sessions} API. */\n            sessionId?: string | undefined;\n            /**\n             * The ID of the Split View that the tab belongs to.\n             * @since Chrome 140\n             */\n            splitViewId?: number | undefined;\n            /**\n             * The ID of the group that the tab belongs to.\n             * @since Chrome 88\n             */\n            groupId: number;\n            /**\n             * The last time the tab became active in its window as the number of milliseconds since epoch.\n             * @since Chrome 121\n             */\n            lastAccessed?: number | undefined;\n        }\n\n        /** The tab's loading status. */\n        export enum TabStatus {\n            UNLOADED = \"unloaded\",\n            LOADING = \"loading\",\n            COMPLETE = \"complete\",\n        }\n\n        /** The type of window. */\n        export enum WindowType {\n            NORMAL = \"normal\",\n            POPUP = \"popup\",\n            PANEL = \"panel\",\n            APP = \"app\",\n            DEVTOOLS = \"devtools\",\n        }\n\n        /** Defines how zoom changes in a tab are handled and at what scope. */\n        export interface ZoomSettings {\n            /** Defines how zoom changes are handled, i.e., which entity is responsible for the actual scaling of the page; defaults to `automatic`. */\n            mode?: `${ZoomSettingsMode}` | undefined;\n            /** Defines whether zoom changes persist for the page's origin, or only take effect in this tab; defaults to `per-origin` when in `automatic` mode, and `per-tab` otherwise. */\n            scope?: `${ZoomSettingsScope}` | undefined;\n            /**\n             * Used to return the default zoom level for the current tab in calls to {@link tabs.getZoomSettings}.\n             * @since Chrome 43\n             */\n            defaultZoomFactor?: number | undefined;\n        }\n\n        /**\n         * Defines how zoom changes are handled, i.e., which entity is responsible for the actual scaling of the page; defaults to `automatic`.\n         * @since Chrome 44\n         */\n        export enum ZoomSettingsMode {\n            /** Zoom changes are handled automatically by the browser. */\n            AUTOMATIC = \"automatic\",\n            /** Overrides the automatic handling of zoom changes. The `onZoomChange` event will still be dispatched, and it is the extension's responsibility to listen for this event and manually scale the page. This mode does not support `per-origin` zooming, and thus ignores the `scope` zoom setting and assumes `per-tab`. */\n            MANUAL = \"manual\",\n            /** Disables all zooming in the tab. The tab reverts to the default zoom level, and all attempted zoom changes are ignored. */\n            DISABLED = \"disabled\",\n        }\n\n        /**\n         * Defines whether zoom changes persist for the page's origin, or only take effect in this tab; defaults to `per-origin` when in `automatic` mode, and `per-tab` otherwise.\n         * @since Chrome 44\n         */\n        export enum ZoomSettingsScope {\n            /** Zoom changes persist in the zoomed page's origin, i.e., all other tabs navigated to that same origin are zoomed as well. Moreover, `per-origin` zoom changes are saved with the origin, meaning that when navigating to other pages in the same origin, they are all zoomed to the same zoom factor. The `per-origin` scope is only available in the `automatic` mode. */\n            PER_ORIGIN = \"per-origin\",\n            /** Zoom changes only take effect in this tab, and zoom changes in other tabs do not affect the zooming of this tab. Also, `per-tab` zoom changes are reset on navigation; navigating a tab always loads pages with their `per-origin` zoom factors. */\n            PER_TAB = \"per-tab\",\n        }\n\n        /**\n         * The maximum number of times that {@link captureVisibleTab} can be called per second. {@link captureVisibleTab} is expensive and should not be called too often.\n         * @since Chrome 92\n         */\n        export const MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND = 2;\n\n        /**\n         * An ID that represents the absence of a split tab.\n         * @since Chrome 140\n         */\n        export const SPLIT_VIEW_ID_NONE: -1;\n\n        /**\n         * An ID that represents the absence of a browser tab.\n         * @since Chrome 46\n         */\n        export const TAB_ID_NONE = -1;\n\n        /**\n         * An index that represents the absence of a tab index in a tab_strip.\n         * @since Chrome 123\n         */\n        export const TAB_INDEX_NONE = -1;\n\n        export interface CreateProperties {\n            /** The position the tab should take in the window. The provided value is clamped to between zero and the number of tabs in the window. */\n            index?: number | undefined;\n            /** The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as the newly created tab. */\n            openerTabId?: number | undefined;\n            /** The URL to initially navigate the tab to. Fully-qualified URLs must include a scheme (i.e., 'http://www.google.com', not 'www.google.com'). Relative URLs are relative to the current page within the extension. Defaults to the New Tab Page. */\n            url?: string | undefined;\n            /** Whether the tab should be pinned. Defaults to `false` */\n            pinned?: boolean | undefined;\n            /** The window in which to create the new tab. Defaults to the current window. */\n            windowId?: number | undefined;\n            /** Whether the tab should become the active tab in the window. Does not affect whether the window is focused (see {@link windows.update}). Defaults to `true`. */\n            active?: boolean | undefined;\n            /**\n             * Whether the tab should become the selected tab in the window. Defaults to `true`\n             * @deprecated since Chrome 33. Please use {@link CreateProperties.active active}.\n             */\n            selected?: boolean | undefined;\n        }\n\n        export interface MoveProperties {\n            /** The position to move the window to. Use `-1` to place the tab at the end of the window. */\n            index: number;\n            /** Defaults to the window the tab is currently in. */\n            windowId?: number | undefined;\n        }\n\n        export interface UpdateProperties {\n            /** Whether the tab should be pinned. */\n            pinned?: boolean | undefined;\n            /** The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab. */\n            openerTabId?: number | undefined;\n            /** A URL to navigate the tab to. JavaScript URLs are not supported; use {@link scripting.executeScript} instead. */\n            url?: string | undefined;\n            /** Adds or removes the tab from the current selection. */\n            highlighted?: boolean | undefined;\n            /** Whether the tab should be active. Does not affect whether the window is focused (see {@link windows.update}).*/\n            active?: boolean | undefined;\n            /**\n             * Whether the tab should be selected.\n             * @deprecated since Chrome 33. Please use {@link highlighted}.\n             */\n            selected?: boolean | undefined;\n            /**\n             * Whether the tab should be muted.\n             * @since Chrome 45\n             */\n            muted?: boolean | undefined;\n            /**\n             * Whether the tab should be discarded automatically by the browser when resources are low.\n             * @since Chrome 54\n             */\n            autoDiscardable?: boolean | undefined;\n        }\n\n        export interface ReloadProperties {\n            /** Whether to bypass local caching. Defaults to `false`. */\n            bypassCache?: boolean | undefined;\n        }\n\n        export interface ConnectInfo {\n            /** Is passed into onConnect for content scripts that are listening for the connection event. */\n            name?: string | undefined;\n            /** Open a port to a specific frame identified by `frameId` instead of all frames in the tab. */\n            frameId?: number | undefined;\n            /**\n             * Open a port to a specific document identified by `documentId` instead of all frames in the tab.\n             * @since Chrome 106\n             */\n            documentId?: string;\n        }\n\n        export interface MessageSendOptions {\n            /** Send a message to a specific frame identified by `frameId` instead of all frames in the tab. */\n            frameId?: number | undefined;\n            /**\n             * Send a message to a specific document identified by `documentId` instead of all frames in the tab.\n             * @since Chrome 106\n             */\n            documentId?: string;\n        }\n\n        export interface GroupOptions {\n            /** Configurations for creating a group. Cannot be used if groupId is already specified. */\n            createProperties?: {\n                /** The window of the new group. Defaults to the current window. */\n                windowId?: number | undefined;\n            } | undefined;\n            /** The ID of the group to add the tabs to. If not specified, a new group will be created. */\n            groupId?: number | undefined;\n            /** The tab ID or list of tab IDs to add to the specified group. */\n            tabIds?: number | [number, ...number[]] | undefined;\n        }\n\n        export interface HighlightInfo {\n            /** One or more tab indices to highlight. */\n            tabs: number | number[];\n            /** The window that contains the tabs. */\n            windowId?: number | undefined;\n        }\n\n        export interface QueryInfo {\n            /** The tab loading status. */\n            status?: `${TabStatus}` | undefined;\n            /** Whether the tabs are in the last focused window. */\n            lastFocusedWindow?: boolean | undefined;\n            /** The ID of the parent window, or {@link windows.WINDOW_ID_CURRENT} for the current window. */\n            windowId?: number | undefined;\n            /** The type of window the tabs are in. */\n            windowType?: `${WindowType}` | undefined;\n            /** Whether the tabs are active in their windows. */\n            active?: boolean | undefined;\n            /** The position of the tabs within their windows. */\n            index?: number | undefined;\n            /** Match page titles against a pattern. This property is ignored if the extension does not have the `\"tabs\"` permission or host permissions for the page. */\n            title?: string | undefined;\n            /** Match tabs against one or more URL patterns. Fragment identifiers are not matched. This property is ignored if the extension does not have the `\"tabs\"` permission or host permissions for the page. */\n            url?: string | string[] | undefined;\n            /** Whether the tabs are in the current window. */\n            currentWindow?: boolean | undefined;\n            /** Whether the tabs are highlighted. */\n            highlighted?: boolean | undefined;\n            /**\n             * Whether the tabs are discarded. A discarded tab is one whose content has been unloaded from memory, but is still visible in the tab strip. Its content is reloaded the next time it is activated.\n             * @since Chrome 54\n             */\n            discarded?: boolean | undefined;\n            /**\n             * Whether the tabs are frozen. A frozen tab cannot execute tasks, including event handlers or timers. It is visible in the tab strip and its content is loaded in memory. It is unfrozen on activation.\n             * @since Chrome 132\n             */\n            frozen?: boolean | undefined;\n            /**\n             * Whether the tabs can be discarded automatically by the browser when resources are low.\n             * @since Chrome 54\n             */\n            autoDiscardable?: boolean | undefined;\n            /** Whether the tabs are pinned. */\n            pinned?: boolean | undefined;\n            /**\n             * The ID of the Split View that the tabs are in, or `tabs.SPLIT_VIEW_ID_NONE` for tabs that aren't in a Split View.\n             * @since Chrome 140\n             */\n            splitViewId?: number | undefined;\n            /**\n             * Whether the tabs are audible.\n             * @since Chrome 45\n             */\n            audible?: boolean | undefined;\n            /**\n             * Whether the tabs are muted.\n             * @since Chrome 45\n             */\n            muted?: boolean | undefined;\n            /**\n             * The ID of the group that the tabs are in, or {@link Browser.tabGroups.TAB_GROUP_ID_NONE} for ungrouped tabs.\n             * @since Chrome 88\n             */\n            groupId?: number | undefined;\n        }\n\n        export interface OnHighlightedInfo {\n            /** All highlighted tabs in the window. */\n            tabIds: number[];\n            /** The window whose tabs changed. */\n            windowId: number;\n        }\n\n        export interface OnRemovedInfo {\n            /** True when the tab was closed because its parent window was closed. */\n            isWindowClosing: boolean;\n            /** The window whose tab is closed */\n            windowId: number;\n        }\n\n        export interface OnUpdatedInfo {\n            /** The tab's new audible state. */\n            audible?: boolean;\n            /**\n             * The tab's new auto-discardable state.\n             * @since Chrome 54\n             */\n            autoDiscardable?: boolean;\n            /**\n             * The tab's new discarded state.\n             * @since Chrome 54\n             */\n            discarded?: boolean;\n            /** The tab's new favicon URL. */\n            favIconUrl?: string;\n            /**\n             * The tab's new frozen state.\n             * @since Chrome 132\n             */\n            frozen?: boolean;\n            /**\n             * The tab's new group.\n             * @since Chrome 88\n             */\n            groupId?: number;\n            /**\n             * The tab's new muted state and the reason for the change.\n             * @since Chrome 46\n             */\n            mutedInfo?: MutedInfo;\n            /** The tab's new pinned state. */\n            pinned?: boolean;\n            /**\n             * The tab's new Split View.\n             * @since Chrome 140\n             */\n            splitViewId?: number;\n            /** The tab's loading status. */\n            status?: `${TabStatus}`;\n            /**\n             * The tab's new title.\n             * @since Chrome 48\n             */\n            title?: string;\n            /** The tab's URL if it has changed. */\n            url?: string;\n        }\n\n        export interface OnAttachedInfo {\n            newPosition: number;\n            newWindowId: number;\n        }\n\n        export interface OnMovedInfo {\n            fromIndex: number;\n            toIndex: number;\n            windowId: number;\n        }\n\n        export interface OnDetachedInfo {\n            oldPosition: number;\n            oldWindowId: number;\n        }\n\n        export interface OnActivatedInfo {\n            /** The ID of the tab that has become active. */\n            tabId: number;\n            /** The ID of the window the active tab changed inside of. */\n            windowId: number;\n        }\n\n        export interface OnSelectionChangedInfo {\n            /** The ID of the window the selected tab changed inside of. */\n            windowId: number;\n        }\n\n        export interface OnActiveChangedInfo {\n            /** The ID of the window the selected tab changed inside of. */\n            windowId: number;\n        }\n\n        export interface OnHighlightChangedInfo {\n            /** All highlighted tabs in the window. */\n            tabIds: number[];\n            /** The window whose tabs changed. */\n            windowId: number;\n        }\n\n        export interface OnZoomChangeInfo {\n            newZoomFactor: number;\n            oldZoomFactor: number;\n            tabId: number;\n            zoomSettings: ZoomSettings;\n        }\n\n        /**\n         * Injects JavaScript code into a page. For details, see the programmatic injection section of the content scripts doc.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         *\n         * MV2 only\n         * @param tabId The ID of the tab in which to run the script; defaults to the active tab of the current window.\n         * @param details Details of the script to run. Either the code or the file property must be set, but both may not be set at the same time\n         * @deprecated since Chrome 99. Replaced by {@link scripting.executeScript} in Manifest V3.\n         */\n        export function executeScript(details: extensionTypes.InjectDetails): Promise<any[] | undefined>;\n        export function executeScript(\n            tabId: number | undefined,\n            details: extensionTypes.InjectDetails,\n        ): Promise<any[] | undefined>;\n        export function executeScript(details: extensionTypes.InjectDetails, callback: (result?: any[]) => void): void;\n        export function executeScript(\n            tabId: number | undefined,\n            details: extensionTypes.InjectDetails,\n            callback: (result?: any[]) => void,\n        ): void;\n\n        /**\n         * Retrieves details about the specified tab\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function get(tabId: number): Promise<Tab>;\n        export function get(tabId: number, callback: (tab: Tab) => void): void;\n\n        /**\n         * Gets details about all tabs in the specified window.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         *\n         * MV2 only\n         * @deprecated Please use {@link tabs.query} `{windowId: windowId}`.\n         */\n        export function getAllInWindow(windowId?: number): Promise<Tab[]>;\n        export function getAllInWindow(callback: (tabs: Tab[]) => void): void;\n        export function getAllInWindow(windowId: number | undefined, callback: (tabs: Tab[]) => void): void;\n\n        /**\n         * Gets the tab that this script call is being made from. Returns `undefined` if called from a non-tab context (for example, a background page or popup view).\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function getCurrent(): Promise<Tab | undefined>;\n        export function getCurrent(callback: (tab?: Tab) => void): void;\n\n        /**\n         * Gets the tab that is selected in the specified window.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         *\n         * MV2 only\n         * @param windowId Defaults to the current window.\n         * @deprecated Please use {@link tabs.query} `{active: true}`.\n         */\n        export function getSelected(windowId?: number): Promise<Tab>;\n        export function getSelected(callback: (tab: Tab) => void): void;\n        export function getSelected(windowId: number | undefined, callback: (tab: Tab) => void): void;\n\n        /**\n         * Creates a new tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function create(createProperties: CreateProperties): Promise<Tab>;\n        export function create(createProperties: CreateProperties, callback: (tab: Tab) => void): void;\n\n        /**\n         * Moves one or more tabs to a new position within its window, or to a new window. Note that tabs can only be moved to and from normal (window.type === \"normal\") windows.\n         * @param tabId The tab ID to move.\n         * @param tabIds List of tab IDs to move.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function move(tabId: number, moveProperties: MoveProperties): Promise<Tab>;\n        export function move(tabIds: number[], moveProperties: MoveProperties): Promise<Tab[]>;\n        export function move(tabId: number, moveProperties: MoveProperties, callback: (tab: Tab) => void): void;\n        export function move(tabIds: number[], moveProperties: MoveProperties, callback: (tabs: Tab[]) => void): void;\n\n        /**\n         * Modifies the properties of a tab. Properties that are not specified in `updateProperties` are not modified.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId Defaults to the selected tab of the current window.\n         */\n        export function update(updateProperties: UpdateProperties): Promise<Tab | undefined>;\n        export function update(tabId: number | undefined, updateProperties: UpdateProperties): Promise<Tab | undefined>;\n        export function update(updateProperties: UpdateProperties, callback: (tab?: Tab) => void): void;\n        export function update(\n            tabId: number | undefined,\n            updateProperties: UpdateProperties,\n            callback: (tab?: Tab) => void,\n        ): void;\n\n        /**\n         * Closes one or more tabs.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The tab ID to close.\n         * @param tabIds List of tab IDs to close.\n         */\n        export function remove(tabId: number): Promise<void>;\n        export function remove(tabIds: number[]): Promise<void>;\n        export function remove(tabId: number, callback: () => void): void;\n        export function remove(tabIds: number[], callback: () => void): void;\n\n        /**\n         * Captures the visible area of the currently active tab in the specified window. In order to call this method, the extension must have either the [<all\\_urls>](https://developer.chrome.com/extensions/develop/concepts/declare-permissions) permission or the [activeTab](https://developer.chrome.com/docs/extensions/develop/concepts/activeTab) permission. In addition to sites that extensions can normally access, this method allows extensions to capture sensitive sites that are otherwise restricted, including chrome:-scheme pages, other extensions' pages, and data: URLs. These sensitive sites can only be captured with the activeTab permission. File URLs may be captured only if the extension has been granted file access.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param windowId The target window. Defaults to the current window.\n         */\n        export function captureVisibleTab(): Promise<string>;\n        export function captureVisibleTab(windowId: number): Promise<string>;\n        export function captureVisibleTab(options: extensionTypes.ImageDetails): Promise<string>;\n        export function captureVisibleTab(windowId: number, options: extensionTypes.ImageDetails): Promise<string>;\n        export function captureVisibleTab(callback: (dataUrl: string) => void): void;\n        export function captureVisibleTab(windowId: number, callback: (dataUrl: string) => void): void;\n        export function captureVisibleTab(\n            options: extensionTypes.ImageDetails,\n            callback: (dataUrl: string) => void,\n        ): void;\n        export function captureVisibleTab(\n            windowId: number,\n            options: extensionTypes.ImageDetails,\n            callback: (dataUrl: string) => void,\n        ): void;\n\n        /**\n         * Reload a tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to reload; defaults to the selected tab of the current window.\n         */\n        export function reload(): Promise<void>;\n        export function reload(tabId: number): Promise<void>;\n        export function reload(reloadProperties: ReloadProperties): Promise<void>;\n        export function reload(tabId: number, reloadProperties: ReloadProperties): Promise<void>;\n        export function reload(callback: () => void): void;\n        export function reload(tabId: number | undefined, callback: () => void): void;\n        export function reload(reloadProperties: ReloadProperties | undefined, callback: () => void): void;\n        export function reload(\n            tabId: number | undefined,\n            reloadProperties: ReloadProperties | undefined,\n            callback: () => void,\n        ): void;\n\n        /**\n         * Duplicates a tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to duplicate.\n         */\n        export function duplicate(tabId: number): Promise<Tab | undefined>;\n        export function duplicate(tabId: number, callback: (tab?: Tab) => void): void;\n\n        /**\n         * Sends a single message to the content script(s) in the specified tab. The {@link runtime.onMessage} event is fired in each content script running in the specified tab for the current extension.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 99.\n         */\n        export function sendMessage<M = any, R = any>(\n            tabId: number,\n            message: M,\n            options?: MessageSendOptions,\n        ): Promise<R>;\n        export function sendMessage<M = any, R = any>(\n            tabId: number,\n            message: M,\n            /** @since Chrome 99 */\n            callback: (response: R) => void,\n        ): void;\n        export function sendMessage<M = any, R = any>(\n            tabId: number,\n            message: M,\n            options: MessageSendOptions | undefined,\n            /** @since Chrome 99 */\n            callback: (response: R) => void,\n        ): void;\n\n        /**\n         * Sends a single request to the content script(s) in the specified tab, with an optional callback to run when a response is sent back. The {@link extension.onRequest} event is fired in each content script running in the specified tab for the current extension.\n         *\n         * MV2 only\n         * @deprecated Please use {@link runtime.sendMessage}.\n         */\n        export function sendRequest<Request = any, Response = any>(\n            tabId: number,\n            request: Request,\n        ): Promise<Response>;\n        export function sendRequest<Request = any, Response = any>(\n            tabId: number,\n            request: Request,\n            /** @since Chrome 99 */\n            callback?: (response: Response) => void,\n        ): void;\n\n        /**\n         * Connects to the content script(s) in the specified tab. The {@link runtime.onConnect} event is fired in each content script running in the specified tab for the current extension.\n         * @returns A port that can be used to communicate with the content scripts running in the specified tab. The port's {@link runtime.Port} event is fired if the tab closes or does not exist.\n         */\n        export function connect(tabId: number, connectInfo?: ConnectInfo): runtime.Port;\n\n        /**\n         * Injects CSS into a page. Styles inserted with this method can be removed with {@link scripting.removeCSS}`. For details, see the programmatic injection section of the content scripts doc.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         *\n         * MV2 only\n         * @param tabId The ID of the tab in which to insert the CSS; defaults to the active tab of the current window.\n         * @param details Details of the CSS text to insert. Either the code or the file property must be set, but both may not be set at the same time.\n         * @deprecated since Chrome 99. Replaced by {@link scripting.insertCSS} in Manifest V3.\n         */\n        export function insertCSS(details: extensionTypes.InjectDetails): Promise<void>;\n        export function insertCSS(tabId: number | undefined, details: extensionTypes.InjectDetails): Promise<void>;\n        export function insertCSS(details: extensionTypes.InjectDetails, callback: () => void): void;\n        export function insertCSS(tabId: number | undefined, details: extensionTypes.InjectDetails): Promise<void>;\n        export function insertCSS(tabId: number, details: extensionTypes.InjectDetails, callback: () => void): void;\n\n        /**\n         * Highlights the given tabs and focuses on the first of group. Will appear to do nothing if the specified tab is currently active.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function highlight(highlightInfo: HighlightInfo): Promise<windows.Window>;\n        export function highlight(highlightInfo: HighlightInfo, callback: (window: windows.Window) => void): void;\n\n        /** Gets all tabs that have the specified properties, or all tabs if no properties are specified. */\n        export function query(queryInfo: QueryInfo): Promise<Tab[]>;\n        export function query(queryInfo: QueryInfo, callback: (result: Tab[]) => void): void;\n\n        /**\n         * Detects the primary language of the content in a tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to get the current zoom factor from; defaults to the active tab of the current window.\n         */\n        export function detectLanguage(tabId?: number): Promise<string>;\n        export function detectLanguage(callback: (language: string) => void): void;\n        export function detectLanguage(tabId: number | undefined, callback: (language: string) => void): void;\n\n        /**\n         * Zooms a specified tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to zoom; defaults to the active tab of the current window.\n         * @param zoomFactor The new zoom factor. A value of `0` sets the tab to its current default zoom factor. Values greater than 0 specify a (possibly non-default) zoom factor for the tab.\n         */\n        export function setZoom(zoomFactor: number): Promise<void>;\n        export function setZoom(tabId: number | undefined, zoomFactor: number): Promise<void>;\n        export function setZoom(zoomFactor: number, callback: () => void): void;\n        export function setZoom(tabId: number | undefined, zoomFactor: number, callback: () => void): void;\n\n        /**\n         * Gets the current zoom factor of a specified tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to get the current zoom factor from; defaults to the active tab of the current window.\n         */\n        export function getZoom(tabId?: number): Promise<number>;\n        export function getZoom(callback: (zoomFactor: number) => void): void;\n        export function getZoom(tabId: number | undefined, callback: (zoomFactor: number) => void): void;\n\n        /**\n         * Sets the zoom settings for a specified tab, which define how zoom changes are handled. These settings are reset to defaults upon navigating the tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to change the zoom settings for; defaults to the active tab of the current window.\n         * @param zoomSettings Defines how zoom changes are handled and at what scope.\n         */\n        export function setZoomSettings(zoomSettings: ZoomSettings): Promise<void>;\n        export function setZoomSettings(tabId: number | undefined, zoomSettings: ZoomSettings): Promise<void>;\n        export function setZoomSettings(zoomSettings: ZoomSettings, callback: () => void): void;\n        export function setZoomSettings(\n            tabId: number | undefined,\n            zoomSettings: ZoomSettings,\n            callback: () => void,\n        ): void;\n\n        /**\n         * Gets the current zoom settings of a specified tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to get the current zoom settings from; defaults to the active tab of the current window.\n         */\n        export function getZoomSettings(tabId?: number): Promise<ZoomSettings>;\n        export function getZoomSettings(callback: (zoomSettings: ZoomSettings) => void): void;\n        export function getZoomSettings(\n            tabId: number | undefined,\n            callback: (zoomSettings: ZoomSettings) => void,\n        ): void;\n\n        /**\n         * Discards a tab from memory. Discarded tabs are still visible on the tab strip and are reloaded when activated.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to be discarded. If specified, the tab is discarded unless it is active or already discarded. If omitted, the browser discards the least important tab. This can fail if no discardable tabs exist..\n         * @since Chrome 54\n         */\n        export function discard(tabId?: number): Promise<Tab | undefined>;\n        export function discard(callback: (tab?: Tab) => void): void;\n        export function discard(tabId: number | undefined, callback: (tab?: Tab) => void): void;\n\n        /**\n         * Go forward to the next page, if one is available.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to navigate forward; defaults to the selected tab of the current window.\n         * @since Chrome 72\n         */\n        export function goForward(tabId?: number): Promise<void>;\n        export function goForward(callback: () => void): void;\n        export function goForward(tabId: number | undefined, callback: () => void): void;\n\n        /**\n         * Go back to the previous page, if one is available.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabId The ID of the tab to navigate back; defaults to the selected tab of the current window.\n         * @since Chrome 72\n         */\n        export function goBack(tabId?: number): Promise<void>;\n        export function goBack(callback: () => void): void;\n        export function goBack(tabId: number | undefined, callback: () => void): void;\n\n        /**\n         * Adds one or more tabs to a specified group, or if no group is specified, adds the given tabs to a newly created group.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @since Chrome 88\n         */\n        export function group(options: GroupOptions): Promise<number>;\n        export function group(options: GroupOptions, callback: (groupId: number) => void): void;\n\n        /**\n         * Removes one or more tabs from their respective groups. If any groups become empty, they are deleted\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         * @param tabIds The tab ID or list of tab IDs to remove from their respective groups.\n         * @since Chrome 88\n         */\n        export function ungroup(tabIds: number | [number, ...number[]]): Promise<void>;\n        export function ungroup(tabIds: number | [number, ...number[]], callback: () => void): void;\n\n        /** Fired when the highlighted or selected tabs in a window changes */\n        export const onHighlighted: events.Event<\n            (highlightInfo: OnHighlightedInfo) => void\n        >;\n\n        /** Fired when a tab is closed. */\n        export const onRemoved: events.Event<\n            (tabId: number, removeInfo: OnRemovedInfo) => void\n        >;\n\n        /** Fired when a tab is updated. */\n        export const onUpdated: events.Event<\n            (tabId: number, changeInfo: OnUpdatedInfo, tab: Tab) => void\n        >;\n\n        /** Fired when a tab is attached to a window, for example because it was moved between windows. */\n        export const onAttached: events.Event<\n            (tabId: number, attachInfo: OnAttachedInfo) => void\n        >;\n\n        /** Fired when a tab is moved within a window. Only one move event is fired, representing the tab the user directly moved. Move events are not fired for the other tabs that must move in response to the manually-moved tab. This event is not fired when a tab is moved between windows; for details, see {@link tabs.onDetached}. */\n        export const onMoved: events.Event<\n            (tabId: number, moveInfo: OnMovedInfo) => void\n        >;\n\n        /** Fired when a tab is detached from a window; for example, because it was moved between windows. */\n        export const onDetached: events.Event<\n            (tabId: number, detachInfo: OnDetachedInfo) => void\n        >;\n\n        /** Fired when a tab is created. Note that the tab's URL and tab group membership may not be set at the time this event is fired, but you can listen to onUpdated events so as to be notified when a URL is set or the tab is added to a tab group. */\n        export const onCreated: events.Event<(tab: Tab) => void>;\n\n        /** Fires when the active tab in a window changes. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events so as to be notified when a URL is set */\n        export const onActivated: events.Event<\n            (activeInfo: OnActivatedInfo) => void\n        >;\n\n        /** Fired when a tab is replaced with another tab due to prerendering or instant */\n        export const onReplaced: events.Event<\n            (addedTabId: number, removedTabId: number) => void\n        >;\n\n        /**\n         * Fires when the selected tab in a window changes.\n         *\n         * MV2 only\n         * @deprecated Please use {@link tabs.onActivated}.\n         */\n        export const onSelectionChanged: events.Event<\n            (tabId: number, selectInfo: OnSelectionChangedInfo) => void\n        >;\n\n        /**\n         * Fires when the selected tab in a window changes. Note that the tab's URL may not be set at the time this event fired, but you can listen to {@link tabs.onUpdated} events so as to be notified when a URL is set.\n         *\n         * MV2 only\n         * @deprecated Please use {@link tabs.onActivated}.\n         */\n        export const onActiveChanged: events.Event<\n            (tabId: number, selectInfo: OnActiveChangedInfo) => void\n        >;\n\n        /**\n         * Fired when the highlighted or selected tabs in a window changes.\n         *\n         * MV2 only\n         * @deprecated Please use {@link tabs.onHighlighted}.\n         */\n        export const onHighlightChanged: events.Event<\n            (selectInfo: OnHighlightChangedInfo) => void\n        >;\n\n        /** Fired when a tab is zoomed */\n        export const onZoomChange: events.Event<\n            (ZoomChangeInfo: OnZoomChangeInfo) => void\n        >;\n    }\n\n    ////////////////////\n    // Tab Groups\n    ////////////////////\n    /**\n     * Use the `Browser.tabGroups` API to interact with the browser's tab grouping system. You can use this API to modify and rearrange tab groups in the browser. To group and ungroup tabs, or to query what tabs are in groups, use the `Browser.tabs` API.\n     *\n     * Permissions: \"tabGroups\"\n     * @since Chrome 89, MV3\n     */\n    export namespace tabGroups {\n        /** An ID that represents the absence of a group. */\n        export const TAB_GROUP_ID_NONE: -1;\n\n        /** The group's color. */\n        export enum Color {\n            BLUE = \"blue\",\n            CYAN = \"cyan\",\n            GREEN = \"green\",\n            GREY = \"grey\",\n            ORANGE = \"orange\",\n            PINK = \"pink\",\n            PURPLE = \"purple\",\n            RED = \"red\",\n            YELLOW = \"yellow\",\n        }\n\n        export interface TabGroup {\n            /** Whether the group is collapsed. A collapsed group is one whose tabs are hidden. */\n            collapsed: boolean;\n            /** The group's color. */\n            color: `${Color}`;\n            /** The ID of the group. Group IDs are unique within a browser session. */\n            id: number;\n            /**\n             * Whether the group is shared.\n             * @since Chrome 137\n             */\n            shared: boolean;\n            /** The title of the group. */\n            title?: string;\n            /** The ID of the window that contains the group. */\n            windowId: number;\n        }\n\n        export interface MoveProperties {\n            /** The position to move the group to. Use `-1` to place the group at the end of the window. */\n            index: number;\n            /** The window to move the group to. Defaults to the window the group is currently in. Note that groups can only be moved to and from windows with {@link windows.windowTypeEnum windows.windowType} type `\"normal\"`. */\n            windowId?: number;\n        }\n\n        export interface QueryInfo {\n            /** Whether the groups are collapsed. */\n            collapsed?: boolean | undefined;\n            /** The color of the groups. */\n            color?: `${Color}` | undefined;\n            /**\n             * Whether the group is shared.\n             * @since Chrome 137\n             */\n            shared?: boolean | undefined;\n            /** Match group titles against a pattern. */\n            title?: string | undefined;\n            /** The ID of the parent window, or {@link windows.WINDOW_ID_CURRENT} for the current window. */\n            windowId?: number | undefined;\n        }\n\n        export interface UpdateProperties {\n            /** Whether the group should be collapsed. */\n            collapsed?: boolean;\n            /** The color of the group. */\n            color?: `${Color}`;\n            /** The title of the group. */\n            title?: string;\n        }\n\n        /**\n         * Retrieves details about the specified group.\n         *\n         * Can return its result via Promise since Chrome 90.\n         */\n        export function get(groupId: number): Promise<TabGroup>;\n        export function get(groupId: number, callback: (group: TabGroup) => void): void;\n\n        /**\n         * Moves the group and all its tabs within its window, or to a new window.\n         * @param groupId The ID of the group to move.\n         *\n         * Can return its result via Promise since Chrome 90.\n         */\n        export function move(groupId: number, moveProperties: MoveProperties): Promise<TabGroup | undefined>;\n        export function move(\n            groupId: number,\n            moveProperties: MoveProperties,\n            callback: (group?: TabGroup) => void,\n        ): void;\n\n        /**\n         * Gets all groups that have the specified properties, or all groups if no properties are specified.\n         *\n         * Can return its result via Promise since Chrome 90.\n         */\n        export function query(queryInfo: QueryInfo): Promise<TabGroup[]>;\n        export function query(queryInfo: QueryInfo, callback: (result: TabGroup[]) => void): void;\n\n        /**\n         * Modifies the properties of a group. Properties that are not specified in `updateProperties` are not modified.\n         * @param groupId The ID of the group to modify.\n         *\n         * Can return its result via Promise since Chrome 90.\n         */\n        export function update(groupId: number, updateProperties: UpdateProperties): Promise<TabGroup | undefined>;\n        export function update(\n            groupId: number,\n            updateProperties: UpdateProperties,\n            callback: (group?: TabGroup) => void,\n        ): void;\n\n        /** Fired when a group is created. */\n        export const onCreated: events.Event<(group: TabGroup) => void>;\n        /** Fired when a group is moved within a window. Move events are still fired for the individual tabs within the group, as well as for the group itself. This event is not fired when a group is moved between windows; instead, it will be removed from one window and created in another. */\n        export const onMoved: events.Event<(group: TabGroup) => void>;\n        /** Fired when a group is closed, either directly by the user or automatically because it contained zero tabs. */\n        export const onRemoved: events.Event<(group: TabGroup) => void>;\n        /** Fired when a group is updated. */\n        export const onUpdated: events.Event<(group: TabGroup) => void>;\n    }\n\n    ////////////////////\n    // Top Sites\n    ////////////////////\n    /**\n     * Use the `Browser.topSites` API to access the top sites (i.e. most visited sites) that are displayed on the new tab page. These do not include shortcuts customized by the user.\n     *\n     * Permissions: \"topSites\"\n     */\n    export namespace topSites {\n        /** An object encapsulating a most visited URL, such as the default shortcuts on the new tab page. */\n        export interface MostVisitedURL {\n            /** The most visited URL. */\n            url: string;\n            /** The title of the page */\n            title: string;\n        }\n\n        /**\n         * Gets a list of top sites.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function get(): Promise<MostVisitedURL[]>;\n        export function get(callback: (data: MostVisitedURL[]) => void): void;\n    }\n\n    ////////////////////\n    // Text to Speech\n    ////////////////////\n    /**\n     * Use the `Browser.tts` API to play synthesized text-to-speech (TTS). See also the related {@link ttsEngine} API, which allows an extension to implement a speech engine.\n     *\n     * Permissions: \"tts\"\n     */\n    export namespace tts {\n        /** @since Chrome 54 */\n        export enum EventType {\n            START = \"start\",\n            END = \"end\",\n            WORD = \"word\",\n            SENTENCE = \"sentence\",\n            MARKER = \"marker\",\n            INTERRUPTED = \"interrupted\",\n            CANCELLED = \"cancelled\",\n            ERROR = \"error\",\n            PAUSE = \"pause\",\n            RESUME = \"resume\",\n        }\n\n        /** An event from the TTS engine to communicate the status of an utterance. */\n        export interface TtsEvent {\n            /** The index of the current character in the utterance. For word events, the event fires at the end of one word and before the beginning of the next. The `charIndex` represents a point in the text at the beginning of the next word to be spoken. */\n            charIndex?: number;\n            /** The error description, if the event type is `error`. */\n            errorMessage?: string;\n            /**\n             * The length of the next part of the utterance. For example, in a `word` event, this is the length of the word which will be spoken next. It will be set to -1 if not set by the speech engine.\n             * @since Chrome 74\n             */\n            length?: number;\n            /** The type can be `start` as soon as speech has started, `word` when a word boundary is reached, `sentence` when a sentence boundary is reached, `marker` when an SSML mark element is reached, `end` when the end of the utterance is reached, `interrupted` when the utterance is stopped or interrupted before reaching the end, `cancelled` when it's removed from the queue before ever being synthesized, or `error` when any other error occurs. When pausing speech, a `pause` event is fired if a particular utterance is paused in the middle, and `resume` if an utterance resumes speech. Note that pause and resume events may not fire if speech is paused in-between utterances. */\n            type: `${EventType}`;\n        }\n\n        /**\n         * The speech options for the TTS engine.\n         * @since Chrome 77\n         */\n        export interface TtsOptions {\n            /** The TTS event types that you are interested in listening to. If missing, all event types may be sent. */\n            desiredEventTypes?: string[];\n            /** If true, enqueues this utterance if TTS is already in progress. If false (the default), interrupts any current speech and flushes the speech queue before speaking this new utterance. */\n            enqueue?: boolean;\n            /** The extension ID of the speech engine to use, if known. */\n            extensionId?: string;\n            /**\n             * Gender of voice for synthesized speech.\n             * @deprecated since Chrome 77. Gender is deprecated and will be ignored.\n             */\n            gender?: `${VoiceGender}`;\n            /** The language to be used for synthesis, in the form _language_\\-_region_. Examples: 'en', 'en-US', 'en-GB', 'zh-CN'. */\n            lang?: string;\n            /** Speaking pitch between 0 and 2 inclusive, with 0 being lowest and 2 being highest. 1.0 corresponds to a voice's default pitch. */\n            pitch?: number;\n            /** Speaking rate relative to the default rate for this voice. 1.0 is the default rate, normally around 180 to 220 words per minute. 2.0 is twice as fast, and 0.5 is half as fast. Values below 0.1 or above 10.0 are strictly disallowed, but many voices will constrain the minimum and maximum rates further—for example a particular voice may not actually speak faster than 3 times normal even if you specify a value larger than 3.0. */\n            rate?: number;\n            /** The TTS event types the voice must support. */\n            requiredEventTypes?: string[];\n            /** The name of the voice to use for synthesis. If empty, uses any available voice. */\n            voiceName?: string;\n            /** Speaking volume between 0 and 1 inclusive, with 0 being lowest and 1 being highest, with a default of 1.0. */\n            volume?: number;\n            /**\n             * This function is called with events that occur in the process of speaking the utterance.\n             * @param event The update event from the text-to-speech engine indicating the status of this utterance.\n             */\n            onEvent?: (\n                event: TtsEvent,\n            ) => void;\n        }\n\n        /** A description of a voice available for speech synthesis. */\n        export interface TtsVoice {\n            /** All of the callback event types that this voice is capable of sending. */\n            eventTypes?: `${EventType}`[];\n            /** The ID of the extension providing this voice. */\n            extensionId?: string;\n            /**\n             * This voice's gender.\n             * @deprecated since Chrome 70. Gender is deprecated and will be ignored.\n             */\n            gender?: `${VoiceGender}`;\n            /** The language that this voice supports, in the form language-region. Examples: 'en', 'en-US', 'en-GB', 'zh-CN'. */\n            lang?: string;\n            /** If true, the synthesis engine is a remote network resource. It may be higher latency and may incur bandwidth costs. */\n            remote?: boolean;\n            /** The name of the voice. */\n            voiceName?: string;\n        }\n\n        /** @deprecated since Chrome 70. Gender is deprecated and is ignored.*/\n        export enum VoiceGender {\n            FEMALE = \"female\",\n            MALE = \"male\",\n        }\n\n        /**\n         * Gets an array of all available voices.\n         *\n         * Can return its result via Promise since Chrome Chrome 101\n         */\n        export function getVoices(): Promise<TtsVoice[]>;\n        export function getVoices(callback: (voices: TtsVoice[]) => void): void;\n\n        /**\n         * Checks whether the engine is currently speaking. On Mac OS X, the result is true whenever the system speech engine is speaking, even if the speech wasn't initiated by Chrome.\n         *\n         * Can return its result via Promise since Chrome Chrome 101\n         */\n        export function isSpeaking(): Promise<boolean>;\n        export function isSpeaking(callback: (speaking: boolean) => void): void;\n\n        /** Pauses speech synthesis, potentially in the middle of an utterance. A call to resume or stop will un-pause speech. */\n        export function pause(): void;\n\n        /** If speech was paused, resumes speaking where it left off. */\n        export function resume(): void;\n\n        /**\n         * Speaks text using a text-to-speech engine.\n         * @param utterance The text to speak, either plain text or a complete, well-formed SSML document. Speech engines that do not support SSML will strip away the tags and speak the text. The maximum length of the text is 32,768 characters.\n         * @param options Optional. The speech options.\n\n         * Can return its result via Promise since Chrome Chrome 101\n         */\n        export function speak(utterance: string, options?: TtsOptions): Promise<void>;\n        export function speak(utterance: string, callback: () => void): void;\n        export function speak(utterance: string, options: TtsOptions, callback: () => void): void;\n\n        /** Stops any current speech and flushes the queue of any pending utterances. In addition, if speech was paused, it will now be un-paused for the next call to speak. */\n        export function stop(): void;\n\n        /**\n         * Called when the list of {@link TtsVoice} that would be returned by getVoices has changed.\n         * @since Chrome 124\n         */\n        const onVoicesChanged: Browser.events.Event<() => void>;\n    }\n\n    ////////////////////\n    // Text to Speech Engine\n    ////////////////////\n    /**\n     * Use the `Browser.ttsEngine` API to implement a text-to-speech(TTS) engine using an extension. If your extension registers using this API, it will receive events containing an utterance to be spoken and other parameters when any extension or Chrome App uses the {@link tts} API to generate speech. Your extension can then use any available web technology to synthesize and output the speech, and send events back to the calling function to report the status.\n     *\n     * Permissions: \"ttsEngine\"\n     */\n    export namespace ttsEngine {\n        /**\n         * Parameters containing an audio buffer and associated data.\n         * @since Chrome 92\n         */\n        export interface AudioBuffer {\n            /** The audio buffer from the text-to-speech engine. It should have length exactly audioStreamOptions.bufferSize and encoded as mono, at audioStreamOptions.sampleRate, and as linear pcm, 32-bit signed float i.e. the Float32Array type in javascript. */\n            audioBuffer: ArrayBuffer;\n            /** The character index associated with this audio buffer. */\n            charIndex?: number;\n            /** True if this audio buffer is the last for the text being spoken. */\n            isLastBuffer?: boolean;\n        }\n        /**\n         * Contains the audio stream format expected to be produced by an engine.\n         * @since Chrome 92\n         */\n        export interface AudioStreamOptions {\n            /** The number of samples within an audio buffer. */\n            bufferSize: number;\n            /** The sample rate expected in an audio buffer. */\n            sampleRate: number;\n        }\n\n        /**\n         * The install status of a voice.\n         * @since Chrome 132\n         */\n        export enum LanguageInstallStatus {\n            FAILED = \"failed\",\n            INSTALLED = \"installed\",\n            INSTALLING = \"installing\",\n            NOT_INSTALLED = \"notInstalled\",\n        }\n\n        /**\n         * Install status of a language.\n         * @since Chrome 132\n         */\n        export interface LanguageStatus {\n            /** Detail about installation failures. Optionally populated if the language failed to install. */\n            error?: string;\n            /** Installation status. */\n            installStatus: `${LanguageInstallStatus}`;\n            /** Language string in the form of language code-region code, where the region may be omitted. Examples are en, en-AU, zh-CH. */\n            lang: string;\n        }\n\n        /**\n         * Options for uninstalling a given language.\n         * @since Chrome 132\n         */\n        export interface LanguageUninstallOptions {\n            /** True if the TTS client wants the language to be immediately uninstalled. The engine may choose whether or when to uninstall the language, based on this parameter and the requestor information. If false, it may use other criteria, such as recent usage, to determine when to uninstall. */\n            uninstallImmediately: boolean;\n        }\n\n        /**\n         * Options specified to the tts.speak() method.\n         * @since Chrome 92\n         */\n        export interface SpeakOptions {\n            /** The language to be used for synthesis, in the form language-region. Examples: 'en', 'en-US', 'en-GB', 'zh-CN'. */\n            lang?: string;\n            /** The name of the voice to use for synthesis. */\n            voiceName?: string;\n            /**\n             * Gender of voice for synthesized speech.\n             * @deprecated Gender is deprecated since Chrome 92 and will be ignored.\n             */\n            gender?: `${VoiceGender}`;\n            /** Speaking volume between 0 and 1 inclusive, with 0 being lowest and 1 being highest, with a default of 1.0. */\n            volume?: number;\n            /** Speaking rate relative to the default rate for this voice. 1.0 is the default rate, normally around 180 to 220 words per minute. 2.0 is twice as fast, and 0.5 is half as fast. This value is guaranteed to be between 0.1 and 10.0, inclusive. When a voice does not support this full range of rates, don't return an error. Instead, clip the rate to the range the voice supports. */\n            rate?: number;\n            /** Speaking pitch between 0 and 2 inclusive, with 0 being lowest and 2 being highest. 1.0 corresponds to this voice's default pitch. */\n            pitch?: number;\n        }\n\n        /**\n         * Identifier for the client requesting status.\n         * @since Chrome 131\n         */\n        export interface TtsClient {\n            /** Client making a language management request. For an extension, this is the unique extension ID. For Chrome features, this is the human-readable name of the feature. */\n            id: string;\n            /** Type of requestor. */\n            source: `${TtsClientSource}`;\n        }\n\n        /**\n         * Type of requestor.\n         * @since Chrome 131\n         */\n        export enum TtsClientSource {\n            CHROMEFEATURE = \"chromefeature\",\n            EXTENSION = \"extension\",\n        }\n\n        /**\n         * @since Chrome 54\n         * @deprecated Gender is deprecated and will be ignored.\n         */\n        export enum VoiceGender {\n            MALE = \"male\",\n            FEMALE = \"female\",\n        }\n\n        /**\n         * Called by an engine when a language install is attempted, and when a language is uninstalled. Also called in response to a status request from a client. When a voice is installed or uninstalled, the engine should also call ttsEngine.updateVoices to register the voice.\n         * @since Chrome 132\n         */\n        export function updateLanguage(status: LanguageStatus): void;\n\n        /**\n         * Called by an engine to update its list of voices. This list overrides any voices declared in this extension's manifest.\n         * @since Chrome 66\n         */\n        export function updateVoices(voices: tts.TtsVoice[]): void;\n\n        /**\n         * Fired when a TTS client requests to install a new language. The engine should attempt to download and install the language, and call ttsEngine.updateLanguage with the result. On success, the engine should also call ttsEngine.updateVoices to register the newly available voices.\n         * @since Chrome 131\n         */\n        export const onInstallLanguageRequest: Browser.events.Event<(requestor: TtsClient, lang: string) => void>;\n\n        /**\n         * Fired when a TTS client requests the install status of a language.\n         * @since Chrome 132\n         */\n        export const onLanguageStatusRequest: Browser.events.Event<(requestor: TtsClient, lang: string) => void>;\n\n        /** Optional: if an engine supports the pause event, it should pause the current utterance being spoken, if any, until it receives a resume event or stop event. Note that a stop event should also clear the paused state. */\n        export const onPause: Browser.events.Event<() => void>;\n\n        /** Optional: if an engine supports the pause event, it should also support the resume event, to continue speaking the current utterance, if any. Note that a stop event should also clear the paused state. */\n        export const onResume: Browser.events.Event<() => void>;\n\n        /** Called when the user makes a call to tts.speak() and one of the voices from this extension's manifest is the first to match the options object. */\n        export const onSpeak: Browser.events.Event<\n            (utterance: string, options: SpeakOptions, sendTtsEvent: (event: Browser.tts.TtsEvent) => void) => void\n        >;\n\n        /**\n         * Called when the user makes a call to tts.speak() and one of the voices from this extension's manifest is the first to match the options object. Differs from ttsEngine.onSpeak in that Chrome provides audio playback services and handles dispatching tts events.\n         * @since Chrome 92\n         */\n\n        export const onSpeakWithAudioStream: Browser.events.Event<\n            (\n                utterance: string,\n                options: SpeakOptions,\n                audioStreamOptions: AudioStreamOptions,\n                sendTtsAudio: (audioBufferParams: AudioBuffer) => void,\n                sendError: (errorMessage?: string) => void,\n            ) => void\n        >;\n\n        /** Fired when a call is made to tts.stop and this extension may be in the middle of speaking. If an extension receives a call to onStop and speech is already stopped, it should do nothing (not raise an error). If speech is in the paused state, this should cancel the paused state. */\n        export const onStop: Browser.events.Event<() => void>;\n\n        /**\n         * Fired when a TTS client indicates a language is no longer needed.\n         * @since Chrome 132\n         */\n        export const onUninstallLanguageRequest: Browser.events.Event<\n            (requestor: TtsClient, lang: string, uninstallOptions: LanguageUninstallOptions) => void\n        >;\n    }\n\n    ////////////////////\n    // Types\n    ////////////////////\n    /**\n     * The `Browser.types` API contains type declarations for Chrome.\n     */\n    export namespace types {\n        /**\n         * The scope of the ChromeSetting. One of\n         * * `regular`: setting for the regular profile (which is inherited by the incognito profile if not overridden elsewhere),\n         * * `regular_only`: setting for the regular profile only (not inherited by the incognito profile),\n         * * `incognito_persistent`: setting for the incognito profile that survives browser restarts (overrides regular preferences)\n         * * `incognito_session_only`: setting for the incognito profile that can only be set during an incognito session and is deleted when the incognito session ends (overrides regular and incognito_persistent preferences).\n         * @since Chrome 44\n         */\n        export type ChromeSettingScope = \"regular\" | \"regular_only\" | \"incognito_persistent\" | \"incognito_session_only\";\n\n        /**\n         * One of\n         * * `not_controllable`: cannot be controlled by any extension\n         * * `controlled_by_other_extensions`: controlled by extensions with higher precedence\n         * * `controllable_by_this_extension`: can be controlled by this extension\n         * * `controlled_by_this_extension`: controlled by this extension\n         * @since Chrome 44\n         */\n        export type LevelOfControl =\n            | \"not_controllable\"\n            | \"controlled_by_other_extensions\"\n            | \"controllable_by_this_extension\"\n            | \"controlled_by_this_extension\";\n\n        /** Which setting to change. */\n        export interface ChromeSettingSetDetails<T> {\n            /**\n             * The value of the setting.\n             * Note that every setting has a specific value type, which is described together with the setting. An extension should not set a value of a different type.\n             */\n            value: T;\n            /** Where to set the setting (default: regular). */\n            scope?: ChromeSettingScope | undefined;\n        }\n\n        /** Which setting to consider. */\n        export interface ChromeSettingGetDetails {\n            /** Whether to return the value that applies to the incognito session (default false). */\n            incognito?: boolean | undefined;\n        }\n\n        /** Details of the currently effective value. */\n        export interface ChromeSettingGetResult<T> {\n            /** The level of control of the setting. */\n            levelOfControl: LevelOfControl;\n            /** The value of the setting. */\n            value: T;\n            /**\n             * Whether the effective value is specific to the incognito session.\n             * This property will only be present if the `incognito` property in the `details` parameter of `get()` was true.\n             */\n            incognitoSpecific?: boolean;\n        }\n\n        /** Which setting to clear. */\n        export interface ChromeSettingClearDetails {\n            /** Where to clear the setting (default: regular). */\n            scope?: ChromeSettingScope | undefined;\n        }\n\n        /** Details of the currently effective value. */\n        export interface ChromeSettingOnChangeDetails<T> {\n            /** Whether the value that has changed is specific to the incognito session. This property will only be present if the user has enabled the extension in incognito mode. */\n            incognitoSpecific?: boolean;\n            /** The value of the setting after the change. */\n            value: T;\n            /** The level of control of the setting. */\n            levelOfControl: LevelOfControl;\n        }\n\n        /**\n         * An interface that allows access to a Chrome browser setting.\n         * See {@link Browser.accessibilityFeatures} for an example.\n         */\n        export interface ChromeSetting<T> {\n            /**\n             * Sets the value of a setting.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n             */\n            set(details: ChromeSettingSetDetails<T>): Promise<void>;\n            set(details: ChromeSettingSetDetails<T>, callback: () => void): void;\n\n            /**\n             * Gets the value of a setting.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n             */\n            get(details: ChromeSettingGetDetails): Promise<ChromeSettingGetResult<T>>;\n            get(details: ChromeSettingGetDetails, callback: (details: ChromeSettingGetResult<T>) => void): void;\n\n            /**\n             * Clears the setting, restoring any default value.\n             *\n             * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n             */\n            clear(details: ChromeSettingClearDetails): Promise<void>;\n            clear(details: ChromeSettingClearDetails, callback: () => void): void;\n\n            /** Fired after the setting changes. */\n            onChange: events.Event<(details: ChromeSettingOnChangeDetails<T>) => void>;\n        }\n    }\n\n    ////////////////////\n    // VPN Provider\n    ////////////////////\n    /**\n     * Use the `Browser.vpnProvider` API to implement a VPN client.\n     *\n     * Permissions: \"vpnProvider\"\n     * @platform ChromeOS only\n     * @since Chrome 43\n     */\n    export namespace vpnProvider {\n        export interface Parameters {\n            /** IP address for the VPN interface in CIDR notation. IPv4 is currently the only supported mode. */\n            address: string;\n            /** Broadcast address for the VPN interface. (default: deduced from IP address and mask) */\n            broadcastAddress?: string | undefined;\n            /** MTU setting for the VPN interface. (default: 1500 bytes) */\n            mtu?: string | undefined;\n            /** Exclude network traffic to the list of IP blocks in CIDR notation from the tunnel. This can be used to bypass traffic to and from the VPN server. When many rules match a destination, the rule with the longest matching prefix wins. Entries that correspond to the same CIDR block are treated as duplicates. Such duplicates in the collated (exclusionList + inclusionList) list are eliminated and the exact duplicate entry that will be eliminated is undefined. */\n            exclusionList: string[];\n            /** Include network traffic to the list of IP blocks in CIDR notation to the tunnel. This parameter can be used to set up a split tunnel. By default no traffic is directed to the tunnel. Adding the entry \"0.0.0.0/0\" to this list gets all the user traffic redirected to the tunnel. When many rules match a destination, the rule with the longest matching prefix wins. Entries that correspond to the same CIDR block are treated as duplicates. Such duplicates in the collated (exclusionList + inclusionList) list are eliminated and the exact duplicate entry that will be eliminated is undefined. */\n            inclusionList: string[];\n            /** A list of search domains. (default: no search domain) */\n            domainSearch?: string[] | undefined;\n            /** A list of IPs for the DNS servers. */\n            dnsServers: string[];\n            /**\n             * Whether or not the VPN extension implements auto-reconnection.\n             *\n             * If true, the `linkDown`, `linkUp`, `linkChanged`, `suspend`, and `resume` platform messages will be used to signal the respective events. If false, the system will forcibly disconnect the VPN if the network topology changes, and the user will need to reconnect manually. (default: false)\n             *\n             * This property is new in Chrome 51; it will generate an exception in earlier versions. try/catch can be used to conditionally enable the feature based on browser support.\n             * @since Chrome 51\n             */\n            reconnect?: string | undefined;\n        }\n\n        /** @deprecated Use {@link Parameters} instead */\n        // eslint-disable-next-line @typescript-eslint/no-empty-interface\n        interface VpnSessionParameters extends Parameters {}\n\n        /** The enum is used by the platform to notify the client of the VPN session status. */\n        export enum PlatformMessage {\n            /** Indicates that the VPN configuration connected. */\n            CONNECTED = \"connected\",\n            /** Indicates that the VPN configuration disconnected. */\n            DISCONNECTED = \"disconnected\",\n            /** Indicates that an error occurred in VPN connection, for example a timeout. A description of the error is given as the error argument to onPlatformMessage. */\n            ERROR = \"error\",\n            /** Indicates that the default physical network connection is down. */\n            LINK_DOWN = \"linkDown\",\n            /** Indicates that the default physical network connection is back up. */\n            LINK_UP = \"linkUp\",\n            /** Indicates that the default physical network connection changed, e.g. wifi->mobile. */\n            LINK_CHANGED = \"linkChanged\",\n            /** Indicates that the OS is preparing to suspend, so the VPN should drop its connection. The extension is not guaranteed to receive this event prior to suspending. */\n            SUSPEND = \"suspend\",\n            /** Indicates that the OS has resumed and the user has logged back in, so the VPN should try to reconnect. */\n            RESUME = \"resume\",\n        }\n\n        /** The enum is used by the platform to indicate the event that triggered {@link onUIEvent}. */\n        export enum UIEvent {\n            /** Requests that the VPN client show the add configuration dialog box to the user. */\n            SHOW_ADD_DIALOG = \"showAddDialog\",\n            /** Requests that the VPN client show the configuration settings dialog box to the user. */\n            SHOW_CONFIGURE_DIALOG = \"showConfigureDialog\",\n        }\n\n        /** The enum is used by the VPN client to inform the platform of its current state. This helps provide meaningful messages to the user. */\n        export enum VpnConnectionState {\n            /** Specifies that VPN connection was successful. */\n            CONNECTED = \"connected\",\n            /** Specifies that VPN connection has failed. */\n            FAILURE = \"failure\",\n        }\n\n        /**\n         * Creates a new VPN configuration that persists across multiple login sessions of the user.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param name The name of the VPN configuration.\n         */\n        export function createConfig(name: string): Promise<string>;\n        export function createConfig(name: string, callback: (id: string) => void): void;\n\n        /**\n         * Destroys a VPN configuration created by the extension.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param id ID of the VPN configuration to destroy.\n         */\n        export function destroyConfig(id: string): Promise<void>;\n        export function destroyConfig(id: string, callback: () => void): void;\n\n        /**\n         * Sets the parameters for the VPN session. This should be called immediately after `\"connected\"` is received from the platform. This will succeed only when the VPN session is owned by the extension.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param parameters The parameters for the VPN session.\n         */\n        export function setParameters(parameters: Parameters): Promise<void>;\n        export function setParameters(parameters: Parameters, callback: () => void): void;\n\n        /**\n         * Sends an IP packet through the tunnel created for the VPN session. This will succeed only when the VPN session is owned by the extension.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param data The IP packet to be sent to the platform.\n         */\n        export function sendPacket(data: ArrayBuffer): Promise<void>;\n        export function sendPacket(data: ArrayBuffer, callback: () => void): void;\n\n        /**\n         * Notifies the VPN session state to the platform. This will succeed only when the VPN session is owned by the extension.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         * @param state The VPN session state of the VPN client.\n         */\n        export function notifyConnectionStateChanged(state: `${VpnConnectionState}`): Promise<void>;\n        export function notifyConnectionStateChanged(state: `${VpnConnectionState}`, callback: () => void): void;\n\n        /** Triggered when a message is received from the platform for a VPN configuration owned by the extension. */\n        export const onPlatformMessage: events.Event<\n            (id: string, message: `${PlatformMessage}`, error: string) => void\n        >;\n\n        /** Triggered when an IP packet is received via the tunnel for the VPN session owned by the extension. */\n        export const onPacketReceived: events.Event<(data: ArrayBuffer) => void>;\n\n        /** Triggered when a configuration created by the extension is removed by the platform. */\n        export const onConfigRemoved: events.Event<(id: string) => void>;\n\n        // /** Triggered when a configuration is created by the platform for the extension. */\n        export const onConfigCreated: events.Event<\n            (id: string, name: string, data: { [key: string]: unknown }) => void\n        >;\n\n        /** Triggered when there is a UI event for the extension. UI events are signals from the platform that indicate to the app that a UI dialog needs to be shown to the user. */\n        export const onUIEvent: events.Event<(event: `${UIEvent}`, id?: string) => void>;\n    }\n\n    ////////////////////\n    // Wallpaper\n    ////////////////////\n    /**\n     * Use the `Browser.wallpaper` API to change the ChromeOS wallpaper.\n     *\n     * Permissions: \"wallpaper\"\n     * @platform ChromeOS only\n     * @since Chrome 43\n     */\n    export namespace wallpaper {\n        /**\n         * The supported wallpaper layouts.\n         * @since Chrome 44\n         */\n        export enum WallpaperLayout {\n            STRETCH = \"STRETCH\",\n            CENTER = \"CENTER\",\n            CENTER_CROPPED = \"CENTER_CROPPED\",\n        }\n\n        export interface WallpaperDetails {\n            /** The jpeg or png encoded wallpaper image as an ArrayBuffer. */\n            data?: ArrayBuffer | undefined;\n            /** The URL of the wallpaper to be set (can be relative). */\n            url?: string | undefined;\n            /** The supported wallpaper layouts. */\n            layout: `${WallpaperLayout}`;\n            /** The file name of the saved wallpaper. */\n            filename: string;\n            /** True if a 128x60 thumbnail should be generated. Layout and ratio are not supported yet. */\n            thumbnail?: boolean | undefined;\n        }\n\n        /**\n         * Sets wallpaper to the image at url or wallpaperData with the specified layout\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 96.\n         */\n        export function setWallpaper(details: WallpaperDetails): Promise<ArrayBuffer | undefined>;\n        export function setWallpaper(details: WallpaperDetails, callback: (thumbnail?: ArrayBuffer) => void): void;\n    }\n\n    ////////////////////\n    // Web Authentication Proxy\n    ////////////////////\n    /**\n     * The `Browser.webAuthenticationProxy` API lets remote desktop software running on a remote host intercept Web Authentication API (WebAuthn) requests in order to handle them on a local client.\n     *\n     * Permissions: \"webAuthenticationProxy\"\n     * @since Chrome 115, MV3\n     */\n    export namespace webAuthenticationProxy {\n        export interface CreateRequest {\n            /** The `PublicKeyCredentialCreationOptions` passed to `navigator.credentials.create()`, serialized as a JSON string. The serialization format is compatible with [`PublicKeyCredential.parseCreationOptionsFromJSON()`](https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON). */\n            requestDetailsJson: string;\n            /** An opaque identifier for the request. */\n            requestId: number;\n        }\n\n        export interface CreateResponseDetails {\n            /** The `DOMException` yielded by the remote request, if any. */\n            error?: DOMExceptionDetails | undefined;\n            /** The `requestId` of the `CreateRequest`. */\n            requestId: number;\n            /** The `PublicKeyCredential`, yielded by the remote request, if any, serialized as a JSON string by calling [`PublicKeyCredential.toJSON()`](https://w3c.github.io/webauthn/#dom-publickeycredential-tojson). */\n            responseJson?: string | undefined;\n        }\n\n        export interface DOMExceptionDetails {\n            name: string;\n            message: string;\n        }\n\n        export interface GetRequest {\n            /** The `PublicKeyCredentialRequestOptions` passed to `navigator.credentials.get()`, serialized as a JSON string. The serialization format is compatible with [`PublicKeyCredential.parseRequestOptionsFromJSON()`](https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON). */\n            requestDetailsJson: string;\n            /**  An opaque identifier for the request. */\n            requestId: number;\n        }\n\n        export interface GetResponseDetails {\n            /** The `DOMException` yielded by the remote request, if any. */\n            error?: DOMExceptionDetails | undefined;\n            /** The `requestId` of the `CreateRequest`. */\n            requestId: number;\n            /** The `PublicKeyCredential`, yielded by the remote request, if any, serialized as a JSON string by calling [`PublicKeyCredential.toJSON()`](https://w3c.github.io/webauthn/#dom-publickeycredential-tojson). */\n            responseJson?: string | undefined;\n        }\n\n        export interface IsUvpaaRequest {\n            /** An opaque identifier for the request. */\n            requestId: number;\n        }\n\n        export interface IsUvpaaResponseDetails {\n            isUvpaa: boolean;\n            requestId: number;\n        }\n\n        /**\n         * Makes this extension the active Web Authentication API request proxy.\n         *\n         * Remote desktop extensions typically call this method after detecting attachment of a remote session to this host. Once this method returns without error, regular processing of WebAuthn requests is suspended, and events from this extension API are raised.\n         *\n         * This method fails with an error if a different extension is already attached.\n         *\n         * The attached extension must call `detach()` once the remote desktop session has ended in order to resume regular WebAuthn request processing. Extensions automatically become detached if they are unloaded.\n         *\n         * Refer to the `onRemoteSessionStateChange` event for signaling a change of remote session attachment from a native application to to the (possibly suspended) extension.\n         */\n        export function attach(): Promise<string | undefined>;\n        export function attach(callback: (error?: string | undefined) => void): void;\n\n        /** Reports the result of a `navigator.credentials.create()` call. The extension must call this for every `onCreateRequest` event it has received, unless the request was canceled (in which case, an `onRequestCanceled` event is fired). */\n        export function completeCreateRequest(details: CreateResponseDetails): Promise<void>;\n        export function completeCreateRequest(details: CreateResponseDetails, callback: () => void): void;\n\n        /** Reports the result of a `navigator.credentials.get()` call. The extension must call this for every `onGetRequest` event it has received, unless the request was canceled (in which case, an `onRequestCanceled` event is fired). */\n        export function completeGetRequest(details: GetResponseDetails): Promise<void>;\n        export function completeGetRequest(details: GetResponseDetails, callback: () => void): void;\n\n        /** Reports the result of a `PublicKeyCredential.isUserVerifyingPlatformAuthenticator()` call. The extension must call this for every `onIsUvpaaRequest` event it has received. */\n        export function completeIsUvpaaRequest(details: IsUvpaaResponseDetails): Promise<void>;\n        export function completeIsUvpaaRequest(details: IsUvpaaResponseDetails, callback: () => void): void;\n\n        /**\n         * Removes this extension from being the active Web Authentication API request proxy.\n         *\n         * This method is typically called when the extension detects that a remote desktop session was terminated. Once this method returns, the extension ceases to be the active Web Authentication API request proxy.\n         *\n         * Refer to the `onRemoteSessionStateChange` event for signaling a change of remote session attachment from a native application to to the (possibly suspended) extension.\n         */\n        export function detach(): Promise<string | undefined>;\n        export function detach(callback: (error?: string | undefined) => void): void;\n\n        /** Fires when a WebAuthn `navigator.credentials.create()` call occurs. The extension must supply a response by calling `completeCreateRequest()` with the `requestId` from `requestInfo`. */\n        export const onCreateRequest: events.Event<(requestInfo: CreateRequest) => void>;\n\n        /** Fires when a WebAuthn `navigator.credentials.get()` call occurs. The extension must supply a response by calling `completeGetRequest()` with the `requestId` from `requestInfo` */\n        export const onGetRequest: events.Event<(requestInfo: GetRequest) => void>;\n\n        /** Fires when a `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` call occurs. The extension must supply a response by calling `completeIsUvpaaRequest()` with the `requestId` from `requestInfo` */\n        export const onIsUvpaaRequest: events.Event<(requestInfo: IsUvpaaRequest) => void>;\n\n        /**\n         * A native application associated with this extension can cause this event to be fired by writing to a file with a name equal to the extension's ID in a directory named `WebAuthenticationProxyRemoteSessionStateChange` inside the [default user data directory](https://chromium.googlesource.com/chromium/src/+/main/docs/user_data_dir.md#default-location)\n         *\n         * The contents of the file should be empty. I.e., it is not necessary to change the contents of the file in order to trigger this event.\n         *\n         * The native host application may use this event mechanism to signal a possible remote session state change (i.e. from detached to attached, or vice versa) while the extension service worker is possibly suspended. In the handler for this event, the extension can call the `attach()` or `detach()` API methods accordingly.\n         *\n         * The event listener must be registered synchronously at load time.\n         */\n        export const onRemoteSessionStateChange: events.Event<() => void>;\n\n        /** Fires when a `onCreateRequest` or `onGetRequest` event is canceled (because the WebAuthn request was aborted by the caller, or because it timed out). When receiving this event, the extension should cancel processing of the corresponding request on the client side. Extensions cannot complete a request once it has been canceled. */\n        export const onRequestCanceled: events.Event<(requestId: number) => void>;\n    }\n\n    ////////////////////\n    // Web Navigation\n    ////////////////////\n    /**\n     * Use the `Browser.webNavigation` API to receive notifications about the status of navigation requests in-flight.\n     *\n     * Permissions: \"webNavigation\"\n     */\n    export namespace webNavigation {\n        /** @since Chrome 44 */\n        export enum TransitionQualifier {\n            CLIENT_REDIRECT = \"client_redirect\",\n            SERVER_REDIRECT = \"server_redirect\",\n            FORWARD_BACK = \"forward_back\",\n            FROM_ADDRESS_BAR = \"from_address_bar\",\n        }\n\n        /**\n         * Cause of the navigation. The same transition types as defined in the history API are used. These are the same transition types as defined in the history API except with `\"start_page\"` in place of `\"auto_toplevel\"` (for backwards compatibility).\n         * @since Chrome 44\n         */\n        export enum TransitionType {\n            LINK = \"link\",\n            TYPED = \"typed\",\n            AUTO_BOOKMARK = \"auto_bookmark\",\n            AUTO_SUBFRAME = \"auto_subframe\",\n            MANUAL_SUBFRAME = \"manual_subframe\",\n            GENERATED = \"generated\",\n            START_PAGE = \"start_page\",\n            FORM_SUBMIT = \"form_submit\",\n            RELOAD = \"reload\",\n            KEYWORD = \"keyword\",\n            KEYWORD_GENERATED = \"keyword_generated\",\n        }\n\n        export type GetFrameDetails =\n            & ({\n                /**\n                 * The ID of the process that runs the renderer for this tab.\n                 * @deprecated since Chrome 49. Frames are now uniquely identified by their tab ID and frame ID; the process ID is no longer needed and therefore ignored.\n                 */\n                processId?: number | undefined;\n            })\n            & (\n                {\n                    /** The ID of the tab in which the frame is. */\n                    tabId?: number | undefined;\n                    /** The ID of the frame in the given tab. */\n                    frameId?: number | undefined;\n                    /**\n                     * The UUID of the document. If the frameId and/or tabId are provided they will be validated to match the document found by provided document ID.\n                     * @since Chrome 106\n                     */\n                    documentId: string;\n                } | {\n                    /** The ID of the tab in which the frame is. */\n                    tabId: number;\n                    /** The ID of the frame in the given tab. */\n                    frameId: number;\n                    /**\n                     * The UUID of the document. If the frameId and/or tabId are provided they will be validated to match the document found by provided document ID.\n                     * @since Chrome 106\n                     */\n                    documentId?: string | undefined;\n                }\n            );\n\n        export interface GetFrameResultDetails {\n            /** The URL currently associated with this frame, if the frame identified by the frameId existed at one point in the given tab. The fact that an URL is associated with a given frameId does not imply that the corresponding frame still exists. */\n            url: string;\n            /** A UUID of the document loaded. */\n            documentId: string;\n            /**\n             * The lifecycle the document is in.\n             * @since Chrome 106\n             */\n            documentLifecycle: extensionTypes.DocumentLifecycle;\n            /** True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired. */\n            errorOccurred: boolean;\n            /** The type of frame the navigation occurred in. */\n            frameType: extensionTypes.FrameType;\n            /**\n             * A UUID of the parent document owning this frame. This is not set if there is no parent.\n             * @since Chrome 106\n             */\n            parentDocumentId?: string | undefined;\n            /** The ID of the parent frame, or `-1` if this is the main frame. */\n            parentFrameId: number;\n        }\n\n        export interface GetAllFrameDetails {\n            /** The ID of the tab. */\n            tabId: number;\n        }\n\n        /** A list of frames in the given tab, null if the specified tab ID is invalid. */\n        export interface GetAllFrameResultDetails extends GetFrameResultDetails {\n            /** The ID of the process that runs the renderer for this frame. */\n            processId: number;\n            /** The ID of the frame. 0 indicates that this is the main frame; a positive value indicates the ID of a subframe. */\n            frameId: number;\n        }\n\n        export interface WebNavigationReplacementCallbackDetails {\n            /** The ID of the tab that was replaced. */\n            replacedTabId: number;\n            /** The ID of the tab that replaced the old tab. */\n            tabId: number;\n            /** The time when the replacement happened, in milliseconds since the epoch. */\n            timeStamp: number;\n        }\n\n        export interface WebNavigationBaseCallbackDetails {\n            /** The lifecycle the document is in. */\n            documentLifecycle: extensionTypes.DocumentLifecycle;\n            /** 0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab. */\n            frameId: number;\n            /** The type of frame the navigation occurred in. */\n            frameType: extensionTypes.FrameType;\n            /** A UUID of the parent document owning this frame. This is not set if there is no parent. */\n            parentDocumentId?: string;\n            /** The ID of the parent frame, or `-1` if this is the main frame. */\n            parentFrameId: number;\n            /** The ID of the process that runs the renderer for this frame. */\n            processId: number;\n            /** The ID of the tab in which the navigation occurs. */\n            tabId: number;\n            /** The time when the browser was about to start the navigation, in milliseconds since the epoch */\n            timeStamp: number;\n            url: string;\n        }\n\n        export interface WebNavigationFramedCallbackDetails extends WebNavigationBaseCallbackDetails {\n            /**\n             * A UUID of the document loaded.\n             * @since Chrome 106\n             */\n            documentId: string;\n        }\n\n        export interface WebNavigationFramedErrorCallbackDetails extends WebNavigationBaseCallbackDetails {\n            /**\n             * A UUID of the document loaded.\n             * @since Chrome 106\n             */\n            documentId: string;\n            /** The error description. */\n            error: string;\n        }\n\n        export interface WebNavigationSourceCallbackDetails {\n            /** The ID of the frame with sourceTabId in which the navigation is triggered. 0 indicates the main frame. */\n            sourceFrameId: number;\n            /** The ID of the process that runs the renderer for the source frame. */\n            sourceProcessId: number;\n            /** The ID of the tab in which the navigation is triggered. */\n            sourceTabId: number;\n            /** The ID of the tab in which the url is opened */\n            tabId: number;\n            /** The time when the browser was about to create a new view, in milliseconds since the epoch. */\n            timeStamp: number;\n            /** The URL to be opened in the new window. */\n            url: string;\n        }\n\n        export interface WebNavigationTransitionCallbackDetails extends WebNavigationBaseCallbackDetails {\n            /**\n             * A UUID of the document loaded.\n             * @since Chrome 106\n             */\n            documentId: string;\n            /** Cause of the navigation. */\n            transitionType: `${TransitionType}`;\n            /** A list of transition qualifiers.*/\n            transitionQualifiers: `${TransitionQualifier}`[];\n        }\n\n        export interface WebNavigationEventFilter {\n            /** Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event. */\n            url: Browser.events.UrlFilter[];\n        }\n\n        interface WebNavigationEvent<T extends (...args: any) => void>\n            extends Omit<Browser.events.Event<T>, \"addListener\">\n        {\n            addListener(callback: T, filters?: WebNavigationEventFilter): void;\n        }\n\n        /**\n         * Retrieves information about the given frame. A frame refers to an <iframe> or a <frame> of a web page and is identified by a tab ID and a frame ID.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 93.\n         * @param details Information about the frame to retrieve information about.\n         */\n        export function getFrame(\n            details: GetFrameDetails,\n        ): Promise<GetFrameResultDetails | null>;\n        export function getFrame(\n            details: GetFrameDetails,\n            callback: (details: GetFrameResultDetails | null) => void,\n        ): void;\n\n        /**\n         * Retrieves information about all frames of a given tab.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 93.\n         * @param details Information about the tab to retrieve all frames from.\n         */\n        export function getAllFrames(\n            details: GetAllFrameDetails,\n        ): Promise<GetAllFrameResultDetails[] | null>;\n        export function getAllFrames(\n            details: GetAllFrameDetails,\n            callback: (details: GetAllFrameResultDetails[] | null) => void,\n        ): void;\n\n        /** Fired when the reference fragment of a frame was updated. All future events for that frame will use the updated URL. */\n        export const onReferenceFragmentUpdated: WebNavigationEvent<\n            (details: WebNavigationTransitionCallbackDetails) => void\n        >;\n\n        /** Fired when a document, including the resources it refers to, is completely loaded and initialized. */\n        export const onCompleted: WebNavigationEvent<(details: WebNavigationFramedCallbackDetails) => void>;\n\n        // /** Fired when the frame's history was updated to a new URL. All future events for that frame will use the updated URL. */\n        export const onHistoryStateUpdated: WebNavigationEvent<\n            (details: WebNavigationTransitionCallbackDetails) => void\n        >;\n\n        /** Fired when a new window, or a new tab in an existing window, is created to host a navigation. */\n        export const onCreatedNavigationTarget: WebNavigationEvent<\n            (details: WebNavigationSourceCallbackDetails) => void\n        >;\n\n        /** Fired when the contents of the tab is replaced by a different (usually previously pre-rendered) tab*/\n        export const onTabReplaced: events.Event<(details: WebNavigationReplacementCallbackDetails) => void>;\n\n        /** Fired when a navigation is about to occur. */\n        export const onBeforeNavigate: WebNavigationEvent<(details: WebNavigationBaseCallbackDetails) => void>;\n\n        /** Fired when a navigation is committed. The document (and the resources it refers to, such as images and subframes) might still be downloading, but at least part of the document has been received from the server and the browser has decided to switch to the new document. */\n        export const onCommitted: WebNavigationEvent<(details: WebNavigationTransitionCallbackDetails) => void>;\n\n        /** Fired when the page's DOM is fully constructed, but the referenced resources may not finish loading. */\n        export const onDOMContentLoaded: WebNavigationEvent<(details: WebNavigationFramedCallbackDetails) => void>;\n\n        /** Fired when an error occurs and the navigation is aborted. This can happen if either a network error occurred, or the user aborted the navigation. */\n        export const onErrorOccurred: WebNavigationEvent<(details: WebNavigationFramedErrorCallbackDetails) => void>;\n    }\n\n    ////////////////////\n    // Web Request\n    ////////////////////\n    /**\n     * Use the `Browser.webRequest` API to observe and analyze traffic and to intercept, block, or modify requests in-flight.\n     *\n     * Permissions: \"webRequest\"\n     *\n     * Manifest: \"host_permissions\"\n     */\n    export namespace webRequest {\n        interface WebRequestEvent<T extends (...args: any) => void, U extends string[]>\n            extends Omit<Browser.events.Event<T>, \"addListener\">\n        {\n            addListener(callback: T, filter: RequestFilter, extraInfoSpec?: U | undefined): void;\n        }\n\n        export interface AuthCredentials {\n            username: string;\n            password: string;\n        }\n\n        /** An HTTP Header, represented as an object containing a key and either a value or a binaryValue. */\n        export interface HttpHeader {\n            /** Name of the HTTP header. */\n            name: string;\n            /** Value of the HTTP header if it can be represented by UTF-8. */\n            value?: string | undefined;\n            /** Value of the HTTP header if it cannot be represented by UTF-8, stored as individual byte values (0..255). */\n            binaryValue?: ArrayBuffer | undefined;\n        }\n\n        /** Returns value for event handlers that have the 'blocking' extraInfoSpec applied. Allows the event handler to modify network requests. */\n        export interface BlockingResponse {\n            /** If true, the request is cancelled. This prevents the request from being sent. This can be used as a response to the onBeforeRequest, onBeforeSendHeaders, onHeadersReceived and onAuthRequired events. */\n            cancel?: boolean | undefined;\n            /** Only used as a response to the onBeforeRequest and onHeadersReceived events. If set, the original request is prevented from being sent/completed and is instead redirected to the given URL. Redirections to non-HTTP schemes such as `data:` are allowed. Redirects initiated by a redirect action use the original request method for the redirect, with one exception: If the redirect is initiated at the onHeadersReceived stage, then the redirect will be issued using the GET method. Redirects from URLs with `ws://` and `wss://` schemes are **ignored**. */\n            redirectUrl?: string | undefined;\n            /** Only used as a response to the onHeadersReceived event. If set, the server is assumed to have responded with these response headers instead. Only return `responseHeaders` if you really want to modify the headers in order to limit the number of conflicts (only one extension may modify `responseHeaders` for each request). */\n            responseHeaders?: HttpHeader[] | undefined;\n            /** Only used as a response to the onAuthRequired event. If set, the request is made using the supplied credentials. */\n            authCredentials?: AuthCredentials | undefined;\n            /** Only used as a response to the onBeforeSendHeaders event. If set, the request is made with these request headers instead. */\n            requestHeaders?: HttpHeader[] | undefined;\n        }\n\n        /**\n         * Contains data passed within form data. For urlencoded form it is stored as string if data is utf-8 string and as ArrayBuffer otherwise. For form-data it is ArrayBuffer. If form-data represents uploading file, it is string with filename, if the filename is provided.\n         * @since Chrome 66\n         */\n        export type FormDataItem = string | ArrayBuffer;\n\n        /** @since Chrome 70 */\n        export enum IgnoredActionType {\n            AUTH_CREDENTIALS = \"auth_credentials\",\n            REDIRECT = \"redirect\",\n            REQUEST_HEADERS = \"request_headers\",\n            RESPONSE_HEADERS = \"response_headers\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnAuthRequiredOptions {\n            /** Specifies that the response headers should be included in the event. */\n            RESPONSE_HEADERS = \"responseHeaders\",\n            /** Specifies the request is blocked until the callback function returns. */\n            BLOCKING = \"blocking\",\n            /** Specifies that the callback function is handled asynchronously. */\n            ASYNC_BLOCKING = \"asyncBlocking\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnBeforeRedirectOptions {\n            /** Specifies that the response headers should be included in the event. */\n            RESPONSE_HEADERS = \"responseHeaders\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnBeforeRequestOptions {\n            /** Specifies the request is blocked until the callback function returns. */\n            BLOCKING = \"blocking\",\n            /** Specifies that the request body should be included in the event. */\n            REQUEST_BODY = \"requestBody\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnBeforeSendHeadersOptions {\n            /** Specifies that the request header should be included in the event. */\n            REQUEST_HEADERS = \"requestHeaders\",\n            /** Specifies the request is blocked until the callback function returns. */\n            BLOCKING = \"blocking\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnCompletedOptions {\n            /** Specifies that the response headers should be included in the event. */\n            RESPONSE_HEADERS = \"responseHeaders\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnErrorOccurredOptions {\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnHeadersReceivedOptions {\n            /** Specifies the request is blocked until the callback function returns. */\n            BLOCKING = \"blocking\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n            /** Specifies that the response headers should be included in the event. */\n            RESPONSE_HEADERS = \"responseHeaders\",\n            /** Specifies that the SecurityInfo should be included in the event. */\n            SECURITY_INFO = \"securityInfo\",\n            /** Specifies that the SecurityInfo with raw bytes of certificates should be included in the event. */\n            SECURITY_INFO_RAW_DER = \"securityInfoRawDer\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnResponseStartedOptions {\n            /** Specifies that the response headers should be included in the event. */\n            RESPONSE_HEADERS = \"responseHeaders\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** @since Chrome 44 */\n        export enum OnSendHeadersOptions {\n            /** Specifies that the request header should be included in the event. */\n            REQUEST_HEADERS = \"requestHeaders\",\n            /** Specifies that headers can violate Cross-Origin Resource Sharing (CORS). */\n            EXTRA_HEADERS = \"extraHeaders\",\n        }\n\n        /** An object describing filters to apply to webRequest events. */\n        export interface RequestFilter {\n            tabId?: number | undefined;\n            /** A list of request types. Requests that cannot match any of the types will be filtered out. */\n            types?: `${ResourceType}`[] | undefined;\n            /** A list of URLs or URL patterns. Requests that cannot match any of the URLs will be filtered out. */\n            urls: string[];\n            windowId?: number | undefined;\n        }\n\n        /** @since Chrome 44 */\n        export enum ResourceType {\n            /** Specifies the resource as the main frame. */\n            MAIN_FRAME = \"main_frame\",\n            /** Specifies the resource as a sub frame. */\n            SUB_FRAME = \"sub_frame\",\n            /** Specifies the resource as a stylesheet. */\n            STYLESHEET = \"stylesheet\",\n            /** Specifies the resource as a script. */\n            SCRIPT = \"script\",\n            /** Specifies the resource as an image. */\n            IMAGE = \"image\",\n            /** Specifies the resource as a font. */\n            FONT = \"font\",\n            /** Specifies the resource as an object. */\n            OBJECT = \"object\",\n            /** Specifies the resource as an XMLHttpRequest. */\n            XMLHTTPREQUEST = \"xmlhttprequest\",\n            /** Specifies the resource as a ping. */\n            PING = \"ping\",\n            /** Specifies the resource as a Content Security Policy (CSP) report. */\n            CSP_REPORT = \"csp_report\",\n            /** Specifies the resource as a media object. */\n            MEDIA = \"media\",\n            /** Specifies the resource as a WebSocket. */\n            WEBSOCKET = \"websocket\",\n            /** Specifies the resource as a WebBundle. */\n            WEBBUNDLE = \"webbundle\",\n            /** Specifies the resource as a type not included in the listed types. */\n            OTHER = \"other\",\n        }\n\n        /** @since Chrome 144 */\n        export interface SecurityInfo {\n            /** A list of certificates */\n            certificates: {\n                /** Fingerprints of the certificate. */\n                fingerprint: {\n                    /** sha256 fingerprint of the certificate. */\n                    sha256: string;\n                };\n                /** Raw bytes of DER encoded server certificate */\n                rawDER?: ArrayBuffer;\n            }[];\n\n            /** State of the connection. One of secure, insecure, broken. */\n            state: string;\n        }\n\n        /** Contains data uploaded in a URL request. */\n        export interface UploadData {\n            /** An ArrayBuffer with a copy of the data. */\n            bytes?: ArrayBuffer;\n            /** A string with the file's path and name. */\n            file?: string;\n        }\n\n        /** The maximum number of times that `handlerBehaviorChanged` can be called per 10 minute sustained interval. `handlerBehaviorChanged` is an expensive function call that shouldn't be called often. */\n        export const MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES: 20;\n\n        /** Common properties for all webRequest events (except {@link onActionIgnored}). */\n        export interface WebRequestDetails {\n            /**\n             * The UUID of the document making the request.\n             * @since Chrome 106\n             */\n            documentId?: string;\n            /**\n             * The lifecycle the document is in.\n             * @since Chrome 106\n             */\n            documentLifecycle: extensionTypes.DocumentLifecycle;\n            /** The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (`type` is `main_frame` or `sub_frame`), `frameId` indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab. */\n            frameId: number;\n            /**\n             * The type of frame the request occurred in.\n             * @since Chrome 106\n             */\n            frameType: extensionTypes.FrameType;\n            /**\n             * The origin where the request was initiated. This does not change through redirects. If this is an opaque origin, the string 'null' will be used.\n             * @since Chrome 63\n             */\n            initiator?: string;\n            /** Standard HTTP method. */\n            method: string;\n            /**\n             * The UUID of the parent document owning this frame. This is not set if there is no parent.\n             * @since Chrome 106\n             */\n            parentDocumentId?: string;\n            /** ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists. */\n            parentFrameId: number;\n            /** The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request. */\n            requestId: string;\n            /** The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab. */\n            tabId: number;\n            /** The time when this signal is triggered, in milliseconds since the epoch. */\n            timeStamp: number;\n            /** How the requested resource will be used. */\n            type: `${ResourceType}`;\n            url: string;\n        }\n\n        export interface OnAuthRequiredDetails extends WebRequestDetails {\n            /** The server requesting authentication. */\n            challenger: {\n                host: string;\n                port: number;\n            };\n            /** True for Proxy-Authenticate, false for WWW-Authenticate. */\n            isProxy: boolean;\n            /** The authentication realm provided by the server, if there is one. */\n            realm?: string;\n            /** The HTTP response headers that were received along with this response. */\n            responseHeaders?: HttpHeader[];\n            /** The authentication scheme, e.g. Basic or Digest. */\n            scheme: string;\n            /**\n             * Standard HTTP status code returned by the server.\n             * @since Chrome 43\n             */\n            statusCode: number;\n            /** HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers.*/\n            statusLine: string;\n        }\n\n        export interface OnBeforeRedirectDetails extends WebRequestDetails {\n            /** Indicates if this response was fetched from disk cache. */\n            fromCache: boolean;\n            /** The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address. */\n            ip?: string;\n            /** The new URL. */\n            redirectUrl: string;\n            /** The HTTP response headers that were received along with this redirect. */\n            responseHeaders?: HttpHeader[];\n            /** Standard HTTP status code returned by the server. */\n            statusCode: number;\n            /** HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers.*/\n            statusLine: string;\n        }\n\n        export interface OnBeforeRequestDetails\n            extends SetPartial<WebRequestDetails, \"documentId\" | \"documentLifecycle\" | \"frameType\">\n        {\n            /** Contains the HTTP request body data. Only provided if extraInfoSpec contains 'requestBody'. */\n            requestBody: {\n                /** Errors when obtaining request body data. */\n                error?: string;\n                /** If the request method is POST and the body is a sequence of key-value pairs encoded in UTF8, encoded as either multipart/form-data, or application/x-www-form-urlencoded, this dictionary is present and for each key contains the list of all values for that key. If the data is of another media type, or if it is malformed, the dictionary is not present. An example value of this dictionary is {'key': \\['value1', 'value2'\\]}. */\n                formData?: { [key: string]: FormDataItem[] };\n                /** If the request method is PUT or POST, and the body is not already parsed in formData, then the unparsed request body elements are contained in this array. */\n                raw?: UploadData[];\n            } | undefined;\n        }\n\n        export interface OnBeforeSendHeadersDetails extends WebRequestDetails {\n            /** The HTTP request headers that are going to be sent out with this request. */\n            requestHeaders?: HttpHeader[];\n        }\n\n        export interface OnCompletedDetails extends WebRequestDetails {\n            /** Indicates if this response was fetched from disk cache. */\n            fromCache: boolean;\n            /** The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address. */\n            ip?: string;\n            /** The HTTP response headers that were received along with this response. */\n            responseHeaders?: HttpHeader[];\n            /** Standard HTTP status code returned by the server. */\n            statusCode: number;\n            /** HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers.*/\n            statusLine: string;\n        }\n\n        export interface OnErrorOccurredDetails extends WebRequestDetails {\n            /** The error description. This string is _not_ guaranteed to remain backwards compatible between releases. You must not parse and act based upon its content. */\n            error: string;\n            /** Indicates if this response was fetched from disk cache. */\n            fromCache: boolean;\n            /** The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address. */\n            ip?: string;\n        }\n\n        export interface OnHeadersReceivedDetails extends WebRequestDetails {\n            /** The HTTP response headers that have been received with this response. */\n            responseHeaders?: HttpHeader[];\n            /**\n             * Information about the TLS/QUIC connection used for the underlying connection. Only provided if `securityInfo` is specified in the `extraInfoSpec` parameter.\n             * @since Chrome 144\n             */\n            securityInfo?: SecurityInfo;\n            /** Standard HTTP status code returned by the server. */\n            statusCode: number;\n            /** HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers.*/\n            statusLine: string;\n        }\n\n        export interface OnResponseStartedDetails extends WebRequestDetails {\n            /** Indicates if this response was fetched from disk cache. */\n            fromCache: boolean;\n            /** The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address. */\n            ip?: string;\n            /** The HTTP response headers that were received along with this response. */\n            responseHeaders?: HttpHeader[];\n            /** Standard HTTP status code returned by the server. */\n            statusCode: number;\n            /** HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers. */\n            statusLine: string;\n        }\n\n        export interface OnSendHeadersDetails extends WebRequestDetails {\n            /** The HTTP request headers that have been sent out with this request. */\n            requestHeaders?: HttpHeader[];\n        }\n\n        /**\n         * Needs to be called when the behavior of the webRequest handlers has changed to prevent incorrect handling due to caching. This function call is expensive. Don't call it often.\n         * Can return its result via Promise in Manifest V3 or later since Chrome 116.\n         */\n        export function handlerBehaviorChanged(): Promise<void>;\n        export function handlerBehaviorChanged(callback: () => void): void;\n\n        export const onActionIgnored: events.Event<\n            (details: {\n                // The proposed action which was ignored.\n                action: `${IgnoredActionType}`;\n                // The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request.\n                requestId: string;\n            }) => void\n        >;\n\n        /** Fired when a request is about to occur. */\n        export const onBeforeRequest: WebRequestEvent<\n            (details: OnBeforeRequestDetails) => BlockingResponse | undefined,\n            `${OnBeforeRequestOptions}`[]\n        >;\n\n        /** Fired before sending an HTTP request, once the request headers are available. This may occur after a TCP connection is made to the server, but before any HTTP data is sent. */\n        export const onBeforeSendHeaders: WebRequestEvent<\n            // eslint-disable-next-line @typescript-eslint/no-invalid-void-type\n            (details: OnBeforeSendHeadersDetails) => BlockingResponse | undefined,\n            `${OnBeforeSendHeadersOptions}`[]\n        >;\n\n        /** Fired just before a request is going to be sent to the server (modifications of previous onBeforeSendHeaders callbacks are visible by the time onSendHeaders is fired). */\n        export const onSendHeaders: WebRequestEvent<\n            (details: OnSendHeadersDetails) => void,\n            `${OnSendHeadersOptions}`[]\n        >;\n\n        /** Fired when HTTP response headers of a request have been received. */\n        export const onHeadersReceived: WebRequestEvent<\n            (details: OnHeadersReceivedDetails) => BlockingResponse | undefined,\n            `${OnHeadersReceivedOptions}`[]\n        >;\n\n        /**\n         * Fired when an authentication failure is received.\n         * The listener has three options: it can provide authentication credentials, it can cancel the request and display the error page, or it can take no action on the challenge.\n         * If bad user credentials are provided, this may be called multiple times for the same request.\n         * Note, only one of `blocking` or `asyncBlocking` modes must be specified in the extraInfoSpec parameter.\n         *\n         * Requires the `webRequestAuthProvider` permission.\n         */\n        export const onAuthRequired: WebRequestEvent<\n            (\n                details: OnAuthRequiredDetails,\n                /** @since Chrome 58 */\n                asyncCallback?: (response: BlockingResponse) => void,\n            ) => BlockingResponse | undefined,\n            `${OnAuthRequiredOptions}`[]\n        >;\n        // export const onAuthRequired: WebAuthenticationChallengeEvent;\n\n        /** Fired when the first byte of the response body is received. For HTTP requests, this means that the status line and response headers are available. */\n        export const onResponseStarted: WebRequestEvent<\n            (details: OnResponseStartedDetails) => void,\n            `${OnResponseStartedOptions}`[]\n        >;\n\n        /** Fired when a server-initiated redirect is about to occur. */\n        export const onBeforeRedirect: WebRequestEvent<\n            (details: OnBeforeRedirectDetails) => void,\n            `${OnBeforeRedirectOptions}`[]\n        >;\n\n        /** Fired when a request is completed. */\n        export const onCompleted: WebRequestEvent<\n            (details: OnCompletedDetails) => void,\n            `${OnCompletedOptions}`[]\n        >;\n\n        /** Fired when an error occurs. */\n        export const onErrorOccurred: WebRequestEvent<\n            (details: OnErrorOccurredDetails) => void,\n            `${OnErrorOccurredOptions}`[]\n        >;\n    }\n\n    ////////////////////\n    // Windows\n    ////////////////////\n    /**\n     * Use the `Browser.windows` API to interact with browser windows. You can use this API to create, modify, and rearrange windows in the browser.\n     *\n     * Permissions: The Browser.windows API can be used without declaring any permission. However, the \"tabs\" permission is required in order to populate the url, title, and favIconUrl properties of Tab objects.\n     */\n    export namespace windows {\n        interface WindowsEvent<T extends (...args: any) => void> extends Omit<Browser.events.Event<T>, \"addListener\"> {\n            addListener(callback: T, filter?: {\n                windowTypes: `${WindowType}`[];\n            }): void;\n        }\n\n        export interface Window {\n            /** Array of {@link tabs.Tab} objects representing the current tabs in the window. */\n            tabs?: Browser.tabs.Tab[] | undefined;\n            /** The offset of the window from the top edge of the screen in pixels. In some circumstances a window may not be assigned a `top` property; for example, when querying closed windows from the {@link sessions} API. */\n            top?: number | undefined;\n            /** The height of the window, including the frame, in pixels. In some circumstances a window may not be assigned a `height` property, for example when querying closed windows from the {@link sessions} API. */\n            height?: number | undefined;\n            /** The width of the window, including the frame, in pixels. In some circumstances a window may not be assigned a `width` property; for example, when querying closed windows from the {@link sessions} API. */\n            width?: number | undefined;\n            /** The state of this browser window. */\n            state?: `${WindowState}` | undefined;\n            /** Whether the window is currently the focused window. */\n            focused: boolean;\n            /** Whether the window is set to be always on top. */\n            alwaysOnTop: boolean;\n            /** Whether the window is incognito. */\n            incognito: boolean;\n            /** The type of browser window this is. */\n            type?: `${WindowType}` | undefined;\n            /** The ID of the window. Window IDs are unique within a browser session. In some circumstances a window may not be assigned an `ID` property; for example, when querying windows using the {@link sessions} API, in which case a session ID may be present. */\n            id?: number | undefined;\n            /** The offset of the window from the left edge of the screen in pixels. In some circumstances a window may not be assigned a `left` property; for example, when querying closed windows from the {@link sessions} API. */\n            left?: number | undefined;\n            /** The session ID used to uniquely identify a window, obtained from the {@link sessions} API. */\n            sessionId?: string | undefined;\n        }\n\n        /** @since Chrome 88 */\n        export interface QueryOptions {\n            /** If true, the {@link windows.Window} object has a `tabs` property that contains a list of the {@link tabs.Tab} objects. The `Tab` objects only contain the `url`, `pendingUrl`, `title`, and `favIconUrl` properties if the extension's manifest file includes the `\"tabs\"` permission. */\n            populate?: boolean | undefined;\n            /** If set, the {@link windows.Window} returned is filtered based on its type. If unset, the default filter is set to `['normal', 'popup']`. */\n            windowTypes?: `${WindowType}`[] | undefined;\n        }\n\n        export interface CreateData {\n            /** The ID of the tab to add to the new window. */\n            tabId?: number | undefined;\n            /** A URL or array of URLs to open as tabs in the window. Fully-qualified URLs must include a scheme, e.g., 'http://www.google.com', not 'www.google.com'. Non-fully-qualified URLs are considered relative within the extension. Defaults to the New Tab Page. */\n            url?: string | string[] | undefined;\n            /** The number of pixels to position the new window from the top edge of the screen. If not specified, the new window is offset naturally from the last focused window. This value is ignored for panels. */\n            top?: number | undefined;\n            /** The height in pixels of the new window, including the frame. If not specified, defaults to a natural height. */\n            height?: number | undefined;\n            /** The width in pixels of the new window, including the frame. If not specified, defaults to a natural width. */\n            width?: number | undefined;\n            /** If `true`, opens an active window. If `false`, opens an inactive window. */\n            focused?: boolean | undefined;\n            /** Whether the new window should be an incognito window. */\n            incognito?: boolean | undefined;\n            /** Specifies what type of browser window to create. */\n            type?: `${CreateType}` | undefined;\n            /** The number of pixels to position the new window from the left edge of the screen. If not specified, the new window is offset naturally from the last focused window. This value is ignored for panels. */\n            left?: number | undefined;\n            /**\n             * The initial state of the window. The `minimized`, `maximized`, and `fullscreen` states cannot be combined with `left`, `top`, `width`, or `height`.\n             * @since Chrome 44\n             */\n            state?: `${WindowState}` | undefined;\n            /**\n             * If `true`, the newly-created window's 'window.opener' is set to the caller and is in the same [unit of related browsing contexts](https://www.w3.org/TR/html51/browsers.html#unit-of-related-browsing-contexts) as the caller.\n             * @since Chrome 64\n             */\n            setSelfAsOpener?: boolean | undefined;\n        }\n\n        export interface UpdateInfo {\n            /** The offset from the top edge of the screen to move the window to in pixels. This value is ignored for panels. */\n            top?: number | undefined;\n            /** If `true`, causes the window to be displayed in a manner that draws the user's attention to the window, without changing the focused window. The effect lasts until the user changes focus to the window. This option has no effect if the window already has focus. Set to `false` to cancel a previous `drawAttention` request. */\n            drawAttention?: boolean | undefined;\n            /** The height to resize the window to in pixels. This value is ignored for panels. */\n            height?: number | undefined;\n            /** The width to resize the window to in pixels. This value is ignored for panels. */\n            width?: number | undefined;\n            /** The new state of the window. The 'minimized', 'maximized', and 'fullscreen' states cannot be combined with 'left', 'top', 'width', or 'height'. */\n            state?: `${WindowState}` | undefined;\n            /** If `true`, brings the window to the front; cannot be combined with the state 'minimized'. If `false`, brings the next window in the z-order to the front; cannot be combined with the state 'fullscreen' or 'maximized'. */\n            focused?: boolean | undefined;\n            /** The offset from the left edge of the screen to move the window to in pixels. This value is ignored for panels. */\n            left?: number | undefined;\n        }\n\n        /**\n         * Specifies what type of browser window to create.\n         * 'panel' is deprecated and is available only to existing whitelisted extensions on Chrome OS.\n         * @since Chrome 44\n         */\n        export enum CreateType {\n            /** Specifies the window as a standard window. */\n            NORMAL = \"normal\",\n            /** Specifies the window as a popup window. */\n            POPUP = \"popup\",\n            /** @deprecated Specifies the window as a panel. */\n            PANEL = \"panel\",\n        }\n\n        /**\n         * The state of this browser window. In some circumstances a window may not be assigned a `state` property; for example, when querying closed windows from the {@link sessions} API.\n         * @since Chrome 44\n         */\n        export enum WindowState {\n            /** Normal window state (not minimized, maximized, or fullscreen). */\n            NORMAL = \"normal\",\n            /** Minimized window state. */\n            MINIMIZED = \"minimized\",\n            /** Maximized window state. */\n            MAXIMIZED = \"maximized\",\n            /** Fullscreen window state. */\n            FULLSCREEN = \"fullscreen\",\n            /** Locked fullscreen window state. This fullscreen state cannot be exited by user action and is available only to allowlisted extensions on Chrome OS. */\n            LOCKED_FULLSCREEN = \"locked-fullscreen\",\n        }\n\n        /**\n         * The type of browser window this is. In some circumstances a window may not be assigned a `type` property; for example, when querying closed windows from the {@link sessions} API.\n         * @since Chrome 44\n         */\n        export enum WindowType {\n            /** A normal browser window. */\n            NORMAL = \"normal\",\n            /** A browser popup. */\n            POPUP = \"popup\",\n            /** @deprecated A Chrome App panel-style window. Extensions can only see their own panel windows. */\n            PANEL = \"panel\",\n            /** @deprecated A Chrome App window. Extensions can only see their app own windows. */\n            APP = \"app\",\n            /** A Developer Tools window. */\n            DEVTOOLS = \"devtools\",\n        }\n\n        /** The windowId value that represents the current window. */\n        export const WINDOW_ID_CURRENT: -2;\n\n        /** The windowId value that represents the absence of a Chrome browser window */\n        export const WINDOW_ID_NONE: -1;\n\n        /**\n         * Gets details about a window.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function get(windowId: number, queryOptions?: QueryOptions): Promise<Window>;\n        export function get(windowId: number, callback: (window: Window) => void): void;\n        export function get(\n            windowId: number,\n            queryOptions: QueryOptions | undefined,\n            callback: (window: Window) => void,\n        ): void;\n\n        /**\n         * Gets the current window.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function getCurrent(queryOptions?: QueryOptions): Promise<Window>;\n        export function getCurrent(callback: (window: Window) => void): void;\n        export function getCurrent(queryOptions: QueryOptions | undefined, callback: (window: Window) => void): void;\n\n        /**\n         * Creates (opens) a new browser window with any optional sizing, position, or default URL provided.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function create(createData?: CreateData): Promise<Window | undefined>;\n        export function create(callback: (window?: Window) => void): void;\n        export function create(createData: CreateData | undefined, callback: (window?: Window) => void): void;\n\n        /**\n         * Gets all windows.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function getAll(queryOptions?: QueryOptions): Promise<Window[]>;\n        export function getAll(callback: (windows: Window[]) => void): void;\n        export function getAll(queryOptions: QueryOptions | undefined, callback: (windows: Window[]) => void): void;\n\n        /**\n         * Updates the properties of a window. Specify only the properties that to be changed; unspecified properties are unchanged.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function update(windowId: number, updateInfo: UpdateInfo): Promise<Window>;\n        export function update(windowId: number, updateInfo: UpdateInfo, callback: (window: Window) => void): void;\n\n        /**\n         * Removes (closes) a window and all the tabs inside it.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function remove(windowId: number): Promise<void>;\n        export function remove(windowId: number, callback: () => void): void;\n\n        /**\n         * Gets the window that was most recently focused — typically the window 'on top'.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 88.\n         */\n        export function getLastFocused(queryOptions?: QueryOptions): Promise<Window>;\n        export function getLastFocused(callback: (window: Window) => void): void;\n        export function getLastFocused(\n            queryOptions: QueryOptions | undefined,\n            callback: (window: Window) => void,\n        ): void;\n\n        /** Fired when a window is removed (closed). */\n        export const onRemoved: WindowsEvent<(windowId: number) => void>;\n\n        /** Fired when a window is created. */\n        export const onCreated: WindowsEvent<(window: Window) => void>;\n\n        /** Fired when the currently focused window changes. Returns `Browser.windows.WINDOW_ID_NONE` if all Chrome windows have lost focus. **Note:** On some Linux window managers, `WINDOW_ID_NONE` is always sent immediately preceding a switch from one Chrome window to another. */\n        export const onFocusChanged: WindowsEvent<(windowId: number) => void>;\n\n        /**\n         * Fired when a window has been resized; this event is only dispatched when the new bounds are committed, and not for in-progress changes.\n         * @since Chrome 86\n         */\n        export const onBoundsChanged: events.Event<(window: Window) => void>;\n    }\n\n    ////////////////////\n    // declarativeNetRequest\n    ////////////////////\n    /**\n     * The `Browser.declarativeNetRequest` API is used to block or modify network requests by specifying declarative rules. This lets extensions modify network requests without intercepting them and viewing their content, thus providing more privacy.\n     *\n     * Permissions: \"declarativeNetRequest\", \"declarativeNetRequestWithHostAccess\", \"declarativeNetRequestFeedback\"\n     *\n     * Manifest: \"host_permissions\"\n     * @since Chrome 84\n     */\n    export namespace declarativeNetRequest {\n        /** Ruleset ID for the dynamic rules added by the extension. */\n        export const DYNAMIC_RULESET_ID: \"_dynamic\";\n\n        /**\n         * Time interval within which `MAX_GETMATCHEDRULES_CALLS_PER_INTERVAL getMatchedRules` calls can be made, specified in minutes.\n         * Additional calls will fail immediately and set {@link runtime.lastError}.\n         * Note: `getMatchedRules` calls associated with a user gesture are exempt from the quota.\n         */\n        export const GETMATCHEDRULES_QUOTA_INTERVAL: 10;\n\n        /**\n         * The minimum number of static rules guaranteed to an extension across its enabled static rulesets.\n         * Any rules above this limit will count towards the global rule limit.\n         * @since Chrome 89\n         */\n        export const GUARANTEED_MINIMUM_STATIC_RULES: 30000;\n\n        /** The number of times `getMatchedRules` can be called within a period of `GETMATCHEDRULES_QUOTA_INTERVAL`. */\n        export const MAX_GETMATCHEDRULES_CALLS_PER_INTERVAL: 20;\n\n        /** The maximum number of dynamic rules that an extension can add. */\n        export const MAX_NUMBER_OF_DYNAMIC_RULES: 30000;\n\n        /**\n         * The maximum number of static `Rulesets` an extension can enable at any one time.\n         * @since Chrome 94\n         */\n        export const MAX_NUMBER_OF_ENABLED_STATIC_RULESETS: 50;\n\n        /** The maximum number of combined dynamic and session scoped rules an extension can add. */\n        export const MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES: 5000;\n\n        /**\n         * The maximum number of regular expression rules that an extension can add.\n         * This limit is evaluated separately for the set of dynamic rules and those specified in the rule resources file.\n         */\n        export const MAX_NUMBER_OF_REGEX_RULES: 1000;\n\n        /**\n         * The maximum number of session scoped rules that an extension can add.\n         * @since Chrome 120\n         */\n        export const MAX_NUMBER_OF_SESSION_RULES: 5000;\n\n        /** The maximum number of static `Rulesets` an extension can specify as part of the `\"rule_resources\"` manifest key. */\n        export const MAX_NUMBER_OF_STATIC_RULESETS: 100;\n\n        /**\n         * The maximum number of \"unsafe\" dynamic rules that an extension can add.\n         * @since Chrome 120\n         */\n        export const MAX_NUMBER_OF_UNSAFE_DYNAMIC_RULES: 5000;\n\n        /**\n         * The maximum number of \"unsafe\" session scoped rules that an extension can add.\n         * @since Chrome 120\n         */\n        export const MAX_NUMBER_OF_UNSAFE_SESSION_RULES: 5000;\n\n        /**\n         * Ruleset ID for the session-scoped rules added by the extension.\n         * @since Chrome 90\n         */\n        export const SESSION_RULESET_ID: \"_session\";\n\n        /**\n         * This describes the HTTP request method of a network request.\n         * @since Chrome 91\n         */\n        export enum RequestMethod {\n            CONNECT = \"connect\",\n            DELETE = \"delete\",\n            GET = \"get\",\n            HEAD = \"head\",\n            OPTIONS = \"options\",\n            PATCH = \"patch\",\n            POST = \"post\",\n            PUT = \"put\",\n            OTHER = \"other\",\n        }\n\n        /** This describes the resource type of the network request. */\n        export enum ResourceType {\n            MAIN_FRAME = \"main_frame\",\n            SUB_FRAME = \"sub_frame\",\n            STYLESHEET = \"stylesheet\",\n            SCRIPT = \"script\",\n            IMAGE = \"image\",\n            FONT = \"font\",\n            OBJECT = \"object\",\n            XMLHTTPREQUEST = \"xmlhttprequest\",\n            PING = \"ping\",\n            CSP_REPORT = \"csp_report\",\n            MEDIA = \"media\",\n            WEBSOCKET = \"websocket\",\n            WEBTRANSPORT = \"webtransport\",\n            WEBBUNDLE = \"webbundle\",\n            OTHER = \"other\",\n        }\n\n        /** Describes the kind of action to take if a given RuleCondition matches. */\n        export enum RuleActionType {\n            /** Block the network request. */\n            BLOCK = \"block\",\n            /** Redirect the network request. */\n            REDIRECT = \"redirect\",\n            /** Allow the network request. The request won't be intercepted if there is an allow rule which matches it. */\n            ALLOW = \"allow\",\n            /** Upgrade the network request url's scheme to https if the request is http or ftp. */\n            UPGRADE_SCHEME = \"upgradeScheme\",\n            /** Modify request/response headers from the network request. */\n            MODIFY_HEADERS = \"modifyHeaders\",\n            /** Allow all requests within a frame hierarchy, including the frame request itself. */\n            ALLOW_ALL_REQUESTS = \"allowAllRequests\",\n        }\n\n        /**\n         * Describes the reason why a given regular expression isn't supported.\n         * @since Chrome 87\n         */\n        export enum UnsupportedRegexReason {\n            /** The regular expression is syntactically incorrect, or uses features not available in the RE2 syntax. */\n            SYNTAX_ERROR = \"syntaxError\",\n            /** The regular expression exceeds the memory limit. */\n            MEMORY_LIMIT_EXCEEDED = \"memoryLimitExceeded\",\n        }\n\n        /**\n         * This describes whether the request is first or third party to the frame in which it originated.\n         * A request is said to be first party if it has the same domain (eTLD+1) as the frame in which the request originated.\n         */\n        export enum DomainType {\n            /** The network request is first party to the frame in which it originated. */\n            FIRST_PARTY = \"firstParty\",\n            /* The network request is third party to the frame in which it originated. */\n            THIRD_PARTY = \"thirdParty\",\n        }\n\n        /**\n         * This describes the possible operations for a \"modifyHeaders\" rule.\n         * @since Chrome 86\n         */\n        export enum HeaderOperation {\n            /** Adds a new entry for the specified header. When modifying the headers of a request, this operation is only supported for specific headers. */\n            APPEND = \"append\",\n            /** Sets a new value for the specified header, removing any existing headers with the same name. */\n            SET = \"set\",\n            /** Removes all entries for the specified header. */\n            REMOVE = \"remove\",\n        }\n\n        export interface RequestDetails {\n            /**\n             * The unique identifier for the frame's document, if this request is for a frame.\n             * @since Chrome 106\n             */\n            documentId?: string | undefined;\n            /**\n             * The lifecycle of the frame's document, if this request is for a frame.\n             * @since Chrome 106\n             */\n            documentLifecycle?: extensionTypes.DocumentLifecycle | undefined;\n            /** The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (`type` is `main_frame` or `sub_frame`), `frameId` indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab. */\n            frameId: number;\n            /**\n             * The type of the frame, if this request is for a frame.\n             * @since Chrome 106\n             */\n            frameType?: extensionTypes.FrameType | undefined;\n            /** The origin where the request was initiated. This does not change through redirects. If this is an opaque origin, the string 'null' will be used. */\n            initiator?: string | undefined;\n            /** Standard HTTP method. */\n            method: string;\n            /**\n             * The unique identifier for the frame's parent document, if this request is for a frame and has a parent.\n             * @since Chrome 106\n             */\n            parentDocumentId?: string | undefined;\n            /** ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists. */\n            parentFrameId: number;\n            /** The ID of the request. Request IDs are unique within a browser session. */\n            requestId: string;\n            /** The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab. */\n            tabId: number;\n            /** The resource type of the request. */\n            type: `${ResourceType}`;\n            /** The URL of the request. */\n            url: string;\n        }\n\n        export interface Rule {\n            /** The action to take if this rule is matched. */\n            action: RuleAction;\n            /** The condition under which this rule is triggered. */\n            condition: RuleCondition;\n            /** An id which uniquely identifies a rule. Mandatory and should be >= 1. */\n            id: number;\n            /** Rule priority. Defaults to 1. When specified, should be >= 1. */\n            priority?: number | undefined;\n        }\n\n        export interface RuleAction {\n            /** Describes how the redirect should be performed. Only valid for redirect rules. */\n            redirect?: Redirect | undefined;\n            /**\n             * The request headers to modify for the request. Only valid if RuleActionType is \"modifyHeaders\".\n             * @since Chrome 86\n             */\n            requestHeaders?: ModifyHeaderInfo[] | undefined;\n            /**\n             * The response headers to modify for the request. Only valid if RuleActionType is \"modifyHeaders\".\n             * @since Chrome 86\n             */\n            responseHeaders?: ModifyHeaderInfo[] | undefined;\n            /** The type of action to perform. */\n            type: `${RuleActionType}`;\n        }\n\n        export interface RuleCondition {\n            /**\n             * Specifies whether the network request is first-party or third-party to the domain from which it originated.\n             * If omitted, all requests are accepted.\n             */\n            domainType?: `${DomainType}` | undefined;\n\n            /**\n             * The rule will only match network requests originating from the list of `domains`.\n             * @deprecated since Chrome 101. Use {@link initiatorDomains} instead\n             */\n            domains?: string[] | undefined;\n\n            /**\n             * The rule will not match network requests originating from the list of `excludedDomains`.\n             * @deprecated since Chrome 101. Use {@link excludedInitiatorDomains} instead\n             */\n            excludedDomains?: string[] | undefined;\n\n            /**\n             * The rule will only match network requests originating from the list of `initiatorDomains`.\n             * If the list is omitted, the rule is applied to requests from all domains.\n             * An empty list is not allowed.\n             *\n             * Notes:\n             * Sub-domains like \"a.example.com\" are also allowed.\n             * The entries must consist of only ascii characters.\n             * Use punycode encoding for internationalized domains.\n             * This matches against the request initiator and not the request url.\n             * @since Chrome 101\n             */\n            initiatorDomains?: string[] | undefined;\n\n            /**\n             * The rule will not match network requests originating from the list of `excludedInitiatorDomains`.\n             * If the list is empty or omitted, no domains are excluded.\n             * This takes precedence over `initiatorDomains`.\n             *\n             * Notes:\n             * Sub-domains like \"a.example.com\" are also allowed.\n             * The entries must consist of only ascii characters.\n             * Use punycode encoding for internationalized domains.\n             * This matches against the request initiator and not the request url.\n             * @since Chrome 101\n             */\n            excludedInitiatorDomains?: string[] | undefined;\n\n            /**\n             * The rule will only match network requests when the domain matches one from the list of `requestDomains`.\n             * If the list is omitted, the rule is applied to requests from all domains.\n             * An empty list is not allowed.\n             *\n             * Notes:\n             * Sub-domains like \"a.example.com\" are also allowed.\n             * The entries must consist of only ascii characters.\n             * Use punycode encoding for internationalized domains.\n             * @since Chrome 101\n             */\n            requestDomains?: string[] | undefined;\n\n            /**\n             * The rule will not match network requests when the domains matches one from the list of `excludedRequestDomains`.\n             * If the list is empty or omitted, no domains are excluded.\n             * This takes precedence over `requestDomains`.\n             *\n             * Notes:\n             * Sub-domains like \"a.example.com\" are also allowed.\n             * The entries must consist of only ascii characters.\n             * Use punycode encoding for internationalized domains.\n             * @since Chrome 101\n             */\n            excludedRequestDomains?: string[] | undefined;\n\n            /**\n             * List of request methods which the rule won't match.\n             * Only one of `requestMethods` and `excludedRequestMethods` should be specified.\n             * If neither of them is specified, all request methods are matched.\n             * @since Chrome 91\n             */\n            excludedRequestMethods?: `${RequestMethod}`[] | undefined;\n\n            /**\n             * List of resource types which the rule won't match.\n             * Only one of `resourceTypes` and `excludedResourceTypes` should be specified.\n             * If neither of them is specified, all resource types except \"main_frame\" are blocked.\n             */\n            excludedResourceTypes?: `${ResourceType}`[] | undefined;\n\n            /**\n             * List of {@link tabs.Tab.id} which the rule should not match.\n             * An ID of {@link tabs.TAB_ID_NONE} excludes requests which don't originate from a tab.\n             * Only supported for session-scoped rules.\n             * @since Chrome 92\n             */\n            excludedTabIds?: number[] | undefined;\n\n            /**\n             * The rule will only match network requests when the associated top-level frame's domain matches one from the list of `topDomains`. If the list is omitted, the rule is applied to requests associated with all top-level frame domains. An empty list is not allowed.\n             *\n             * Notes:\n             * - Sub-domains like \"a.example.com\" are also allowed.\n             * - The entries must consist of only ascii characters.\n             * - Use punycode encoding for internationalized domains.\n             * - Sub-domains of the listed domains are also matched.\n             * - For requests with no associated top-level frame (e.g. ServiceWorker initiated requests, the request initiator's domain is considered instead.\n             * @since Chrome 141\n             */\n            topDomains?: string[] | undefined;\n\n            /**\n             * The rule will not match network requests when the associated top-level frame's domain matches one from the list of `excludedTopDomains`. If the list is empty or omitted, no domains are excluded. This takes precedence over `topDomains`.\n             *\n             * Notes:\n             * - Sub-domains like \"a.example.com\" are also allowed.\n             * - The entries must consist of only ascii characters.\n             * - Use punycode encoding for internationalized domains.\n             * - Sub-domains of the listed domains are also excluded.\n             * - For requests with no associated top-level frame (e.g. ServiceWorker initiated requests, the request initiator's domain is considered instead.\n             * @since Chrome 141\n             */\n            excludedTopDomains?: string[] | undefined;\n\n            /** Whether the `urlFilter` or `regexFilter` (whichever is specified) is case sensitive. Default is false. */\n            isUrlFilterCaseSensitive?: boolean | undefined;\n\n            /**\n             * Regular expression to match against the network request url.\n             * This follows the RE2 syntax.\n             *\n             * Note: Only one of `urlFilter` or `regexFilter` can be specified.\n             *\n             * Note: The `regexFilter` must be composed of only ASCII characters.\n             * This is matched against a url where the host is encoded in the punycode format (in case of internationalized domains) and any other non-ascii characters are url encoded in utf-8.\n             */\n            regexFilter?: string | undefined;\n\n            /**\n             * List of HTTP request methods which the rule can match. An empty list is not allowed.\n             *\n             * Note: Specifying a `requestMethods` rule condition will also exclude non-HTTP(s) requests, whereas specifying `excludedRequestMethods` will not.\n             */\n            requestMethods?: `${RequestMethod}`[] | undefined;\n\n            /**\n             * List of {@link tabs.Tab.id} which the rule should match.\n             * An ID of {@link tabs.TAB_ID_NONE} matches requests which don't originate from a tab.\n             * An empty list is not allowed. Only supported for session-scoped rules.\n             * @since Chrome 92\n             */\n            tabIds?: number[] | undefined;\n\n            /**\n             * The pattern which is matched against the network request url.\n             * Supported constructs:\n             *\n             * '*' : Wildcard: Matches any number of characters.\n             *\n             * '|' : Left/right anchor: If used at either end of the pattern, specifies the beginning/end of the url respectively.\n             *\n             * '||' : Domain name anchor: If used at the beginning of the pattern, specifies the start of a (sub-)domain of the URL.\n             *\n             * '^' : Separator character: This matches anything except a letter, a digit or one of the following: _ - . %.\n             * This can also match the end of the URL.\n             *\n             * Therefore `urlFilter` is composed of the following parts: (optional Left/Domain name anchor) + pattern + (optional Right anchor).\n             *\n             * If omitted, all urls are matched. An empty string is not allowed.\n             *\n             * A pattern beginning with || is not allowed. Use instead.\n             *\n             * Note: Only one of `urlFilter` or `regexFilter` can be specified.\n             *\n             * Note: The `urlFilter` must be composed of only ASCII characters.\n             * This is matched against a url where the host is encoded in the punycode format (in case of internationalized domains) and any other non-ascii characters are url encoded in utf-8.\n             * For example, when the request url is http://abc.рф?q=ф, the `urlFilter` will be matched against the url http://abc.xn--p1ai/?q=%D1%84.\n             */\n            urlFilter?: string | undefined;\n\n            /**\n             * List of resource types which the rule can match.\n             * An empty list is not allowed.\n             *\n             * Note: this must be specified for `allowAllRequests` rules and may only include the `sub_frame` and `main_frame` resource types.\n             */\n            resourceTypes?: `${ResourceType}`[] | undefined;\n\n            /**\n             * Rule does not match if the request matches any response header condition in this list (if specified). If both `excludedResponseHeaders` and `responseHeaders` are specified, then the `excludedResponseHeaders` property takes precedence.\n             * @since Chrome 128\n             */\n            excludedResponseHeaders?: HeaderInfo[];\n\n            /**\n             * Rule matches if the request matches any response header condition in this list (if specified).\n             * @since Chrome 128\n             */\n            responseHeaders?: HeaderInfo[];\n        }\n\n        /** @since Chrome 145 */\n        export enum RuleConditionKeys {\n            URL_FILTER = \"urlFilter\",\n            REGEX_FILTER = \"regexFilter\",\n            IS_URL_FILTER_CASE_SENSITIVE = \"isUrlFilterCaseSensitive\",\n            INITIATOR_DOMAINS = \"initiatorDomains\",\n            EXCLUDED_INITIATOR_DOMAINS = \"excludedInitiatorDomains\",\n            REQUEST_DOMAINS = \"requestDomains\",\n            EXCLUDED_REQUEST_DOMAINS = \"excludedRequestDomains\",\n            TOP_DOMAINS = \"topDomains\",\n            EXCLUDED_TOP_DOMAINS = \"excludedTopDomains\",\n            DOMAINS = \"domains\",\n            EXCLUDED_DOMAINS = \"excludedDomains\",\n            RESOURCE_TYPES = \"resourceTypes\",\n            EXCLUDED_RESOURCE_TYPES = \"excludedResourceTypes\",\n            REQUEST_METHODS = \"requestMethods\",\n            EXCLUDED_REQUEST_METHODS = \"excludedRequestMethods\",\n            DOMAIN_TYPE = \"domainType\",\n            TAB_IDS = \"tabIds\",\n            EXCLUDED_TAB_IDS = \"excludedTabIds\",\n            RESPONSE_HEADERS = \"responseHeaders\",\n            EXCLUDED_RESPONSE_HEADERS = \"excludedResponseHeaders\",\n        }\n\n        export interface MatchedRule {\n            /** A matching rule's ID. */\n            ruleId: number;\n            /** ID of the {@link Ruleset} this rule belongs to. For a rule originating from the set of dynamic rules, this will be equal to {@link DYNAMIC_RULESET_ID}. */\n            rulesetId: string;\n        }\n\n        export interface MatchedRuleInfo {\n            rule: MatchedRule;\n            /** The tabId of the tab from which the request originated if the tab is still active. Else -1. */\n            tabId: number;\n            /** The time the rule was matched. Timestamps will correspond to the Javascript convention for times, i.e. number of milliseconds since the epoch. */\n            timeStamp: number;\n        }\n\n        export interface MatchedRulesFilter {\n            /** If specified, only matches rules after the given timestamp. */\n            minTimeStamp?: number | undefined;\n            /** If specified, only matches rules for the given tab. Matches rules not associated with any active tab if set to -1. */\n            tabId?: number | undefined;\n        }\n\n        /** @since Chrome 128 */\n        export interface HeaderInfo {\n            /** If specified, this condition is not matched if the header exists but its value contains at least one element in this list. This uses the same match pattern syntax as `values`. */\n            excludedValues?: string[];\n            /** The name of the header. This condition matches on the name only if both `values` and `excludedValues` are not specified. */\n            header: string;\n            /**\n             * If specified, this condition matches if the header's value matches at least one pattern in this list. This supports case-insensitive header value matching plus the following constructs:\n             *\n             * **'\\*'** : Matches any number of characters.\n             *\n             * **'?'** : Matches zero or one character(s).\n             *\n             * **'\\*'** and **'?'** can be escaped with a backslash, e.g. **'\\\\\\*'** and **'\\\\?'**\n             */\n            values?: string[];\n        }\n\n        /** @since Chrome 86 */\n        export interface ModifyHeaderInfo {\n            /** The name of the header to be modified. */\n            header: string;\n            /** The operation to be performed on a header. */\n            operation: `${HeaderOperation}`;\n            /** The new value for the header. Must be specified for `append` and `set` operations. */\n            value?: string | undefined;\n        }\n\n        export interface QueryKeyValue {\n            key: string;\n            /**\n             * If true, the query key is replaced only if it's already present. Otherwise, the key is also added if it's missing. Defaults to false.\n             * @since Chrome 94\n             */\n            replaceOnly?: boolean | undefined;\n            value: string;\n        }\n\n        export interface QueryTransform {\n            /** The list of query key-value pairs to be added or replaced. */\n            addOrReplaceParams?: QueryKeyValue[] | undefined;\n            /** The list of query keys to be removed. */\n            removeParams?: string[] | undefined;\n        }\n\n        export interface URLTransform {\n            /** The new fragment for the request. Should be either empty, in which case the existing fragment is cleared; or should begin with '#'. */\n            fragment?: string | undefined;\n            /** The new host for the request. */\n            host?: string | undefined;\n            /** The new password for the request. */\n            password?: string | undefined;\n            /** The new path for the request. If empty, the existing path is cleared. */\n            path?: string | undefined;\n            /** The new port for the request. If empty, the existing port is cleared. */\n            port?: string | undefined;\n            /** The new query for the request. Should be either empty, in which case the existing query is cleared; or should begin with '?'. */\n            query?: string | undefined;\n            /** Add, remove or replace query key-value pairs. */\n            queryTransform?: QueryTransform | undefined;\n            /** The new scheme for the request. Allowed values are \"http\", \"https\", \"ftp\" and \"chrome-extension\". */\n            scheme?: string | undefined;\n            /** The new username for the request. */\n            username?: string | undefined;\n        }\n\n        /** @since Chrome 87 */\n        export interface RegexOptions {\n            /** Whether the `regex` specified is case sensitive. Default is true. */\n            isCaseSensitive?: boolean | undefined;\n            /** The regular expression to check. */\n            regex: string;\n            /** Whether the `regex` specified requires capturing. Capturing is only required for redirect rules which specify a `regexSubstitution` action. The default is false. */\n            requireCapturing?: boolean | undefined;\n        }\n\n        /** @since Chrome 87 */\n        export interface IsRegexSupportedResult {\n            isSupported: boolean;\n            /** Specifies the reason why the regular expression is not supported. Only provided if `isSupported` is false. */\n            reason?: `${UnsupportedRegexReason}`;\n        }\n\n        /** @since Chrome 89 */\n        export interface TabActionCountUpdate {\n            /** The amount to increment the tab's action count by. Negative values will decrement the count. */\n            increment: number;\n            /** The tab for which to update the action count. */\n            tabId: number;\n        }\n\n        /** @since Chrome 88 */\n        export interface ExtensionActionOptions {\n            /**\n             * Whether to automatically display the action count for a page as the extension's badge text.\n             * This preference is persisted across sessions.\n             */\n            displayActionCountAsBadgeText?: boolean | undefined;\n            /** Details of how the tab's action count should be adjusted. */\n            tabUpdate?: TabActionCountUpdate | undefined;\n        }\n\n        /** @since Chrome 111 */\n        export interface GetDisabledRuleIdsOptions {\n            /** The id corresponding to a static {@link Ruleset}. */\n            rulesetId: string;\n        }\n\n        /** @since Chrome 111 */\n        export interface GetRulesFilter {\n            /** If specified, only rules with matching IDs are included. */\n            ruleIds?: number[] | undefined;\n        }\n\n        export interface Redirect {\n            /** Path relative to the extension directory. Should start with '/'. */\n            extensionPath?: string | undefined;\n            /**\n             * Substitution pattern for rules which specify a `regexFilter`.\n             * The first match of `regexFilter` within the url will be replaced with this pattern.\n             * Within `regexSubstitution`, backslash-escaped digits (\\1 to \\9) can be used to insert the corresponding capture groups.\n             * \\0 refers to the entire matching text.\n             */\n            regexSubstitution?: string | undefined;\n            /** Url transformations to perform. */\n            transform?: URLTransform | undefined;\n            /** The redirect url. Redirects to JavaScript urls are not allowed. */\n            url?: string | undefined;\n        }\n\n        /** @since Chrome 87 */\n        export interface UpdateRuleOptions {\n            /** Rules to add. */\n            addRules?: Rule[] | undefined;\n            /**\n             * IDs of the rules to remove.\n             * Any invalid IDs will be ignored.\n             */\n            removeRuleIds?: number[] | undefined;\n        }\n\n        /** @since Chrome 111 */\n        export interface UpdateStaticRulesOptions {\n            /** Set of ids corresponding to rules in the {@link Ruleset} to disable. */\n            disableRuleIds?: number[];\n            /** Set of ids corresponding to rules in the {@link Ruleset} to enable. */\n            enableRuleIds?: number[];\n            /** The id corresponding to a static {@link Ruleset}. */\n            rulesetId: string;\n        }\n\n        /** @since Chrome 87 */\n        export interface UpdateRulesetOptions {\n            /** The set of ids corresponding to a static {@link Ruleset} that should be disabled. */\n            disableRulesetIds?: string[] | undefined;\n            /** The set of ids corresponding to a static {@link Ruleset} that should be enabled. */\n            enableRulesetIds?: string[] | undefined;\n        }\n\n        export interface MatchedRuleInfoDebug {\n            /** Details about the request for which the rule was matched. */\n            request: RequestDetails;\n            rule: MatchedRule;\n        }\n\n        export interface Ruleset {\n            /** Whether the ruleset is enabled by default. */\n            enabled: boolean;\n            /** A non-empty string uniquely identifying the ruleset. IDs beginning with '_' are reserved for internal use. */\n            id: string;\n            /** The path of the JSON ruleset relative to the extension directory. */\n            path: string;\n        }\n\n        export interface RulesMatchedDetails {\n            /** Rules matching the given filter. */\n            rulesMatchedInfo: MatchedRuleInfo[];\n        }\n\n        /** @since Chrome 103 */\n        export interface TestMatchOutcomeResult {\n            /** The rules (if any) that match the hypothetical request. */\n            matchedRules: MatchedRule[];\n        }\n\n        /** @since Chrome 103 */\n        export interface TestMatchRequestDetails {\n            /** The initiator URL (if any) for the hypothetical request. */\n            initiator?: string;\n            /** Standard HTTP method of the hypothetical request. Defaults to \"get\" for HTTP requests and is ignored for non-HTTP requests. */\n            method?: `${RequestMethod}`;\n            /**\n             * The headers provided by a hypothetical response if the request does not get blocked or redirected before it is sent. Represented as an object which maps a header name to a list of string values. If not specified, the hypothetical response would return empty response headers, which can match rules which match on the non-existence of headers. E.g. `{\"content-type\": [\"text/html; charset=utf-8\", \"multipart/form-data\"]}`\n             * @since Chrome 129\n             */\n            responseHeaders?: { [name: string]: unknown };\n            /** The ID of the tab in which the hypothetical request takes place. Does not need to correspond to a real tab ID. Default is -1, meaning that the request isn't related to a tab. */\n            tabId?: number;\n            /**\n             * The associated top-level frame URL (if any) for the request.\n             * @since Chrome 145\n             */\n            topUrl?: string;\n            /** The resource type of the hypothetical request. */\n            type: `${ResourceType}`;\n            /** The URL of the hypothetical request. */\n            url: string;\n        }\n\n        /**\n         * Returns the number of static rules an extension can enable before the global static rule limit is reached.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @since Chrome 89\n         */\n        export function getAvailableStaticRuleCount(): Promise<number>;\n        export function getAvailableStaticRuleCount(callback: (count: number) => void): void;\n\n        /**\n         * Returns the list of static rules in the given {@link Ruleset} that are currently disabled.\n         *\n         * Can return its result via Promise in Manifest V3.\n         * @param options Specifies the ruleset to query.\n         * @since Chrome 111\n         */\n        export function getDisabledRuleIds(options: GetDisabledRuleIdsOptions): Promise<number[]>;\n        export function getDisabledRuleIds(\n            options: GetDisabledRuleIdsOptions,\n            callback: (disabledRuleIds: number[]) => void,\n        ): void;\n\n        /**\n         * Returns the current set of dynamic rules for the extension. Callers can optionally filter the list of fetched rules by specifying a `filter`.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @param filter An object to filter the list of fetched rules.\n         */\n        export function getDynamicRules(filter?: GetRulesFilter): Promise<Rule[]>;\n        export function getDynamicRules(callback: (rules: Rule[]) => void): void;\n        export function getDynamicRules(filter: GetRulesFilter | undefined, callback: (rules: Rule[]) => void): void;\n\n        /**\n         * Returns the ids for the current set of enabled static rulesets.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function getEnabledRulesets(): Promise<string[]>;\n        export function getEnabledRulesets(callback: (rulesetIds: string[]) => void): void;\n\n        /**\n         * Returns all rules matched for the extension. Callers can optionally filter the list of matched rules by specifying a `filter`. This method is only available to extensions with the `\"declarativeNetRequestFeedback\"` permission or having the `\"activeTab\"` permission granted for the `tabId` specified in `filter`. Note: Rules not associated with an active document that were matched more than five minutes ago will not be returned.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @param filter An object to filter the list of matched rules.\n         */\n        export function getMatchedRules(filter?: MatchedRulesFilter): Promise<RulesMatchedDetails>;\n        export function getMatchedRules(callback: (details: RulesMatchedDetails) => void): void;\n        export function getMatchedRules(\n            filter: MatchedRulesFilter | undefined,\n            callback: (details: RulesMatchedDetails) => void,\n        ): void;\n\n        /**\n         * Returns the current set of session scoped rules for the extension. Callers can optionally filter the list of fetched rules by specifying a `filter`.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @param filter An object to filter the list of fetched rules.\n         * @since Chrome 90\n         */\n        export function getSessionRules(filter?: GetRulesFilter): Promise<Rule[]>;\n        export function getSessionRules(callback: (rules: Rule[]) => void): void;\n        export function getSessionRules(filter: GetRulesFilter | undefined, callback: (rules: Rule[]) => void): void;\n\n        /**\n         * Checks if the given regular expression will be supported as a `regexFilter` rule condition.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @param regexOptions The regular expression to check.\n         * @since Chrome 87\n         */\n        export function isRegexSupported(regexOptions: RegexOptions): Promise<IsRegexSupportedResult>;\n        export function isRegexSupported(\n            regexOptions: RegexOptions,\n            callback: (result: IsRegexSupportedResult) => void,\n        ): void;\n\n        /**\n         * Configures if the action count for tabs should be displayed as the extension action's badge text and provides a way for that action count to be incremented.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @since Chrome 88\n         */\n        export function setExtensionActionOptions(options: ExtensionActionOptions): Promise<void>;\n        export function setExtensionActionOptions(options: ExtensionActionOptions, callback: () => void): void;\n\n        /**\n         * Checks if any of the extension's declarativeNetRequest rules would match a hypothetical request. Note: Only available for unpacked extensions as this is only intended to be used during extension development.\n         *\n         * Can return its result via Promise in Manifest V3.\n         * @since Chrome 103\n         */\n        export function testMatchOutcome(request: TestMatchRequestDetails): Promise<TestMatchOutcomeResult>;\n        export function testMatchOutcome(\n            request: TestMatchRequestDetails,\n            callback: (result: TestMatchOutcomeResult) => void,\n        ): void;\n\n        /**\n         * Modifies the current set of dynamic rules for the extension. The rules with IDs listed in `options.removeRuleIds` are first removed, and then the rules given in `options.addRules` are added. Notes:\n         *\n         * *   This update happens as a single atomic operation: either all specified rules are added and removed, or an error is returned.\n         * *   These rules are persisted across browser sessions and across extension updates.\n         * *   Static rules specified as part of the extension package can not be removed using this function.\n         * *   {@link MAX_NUMBER_OF_DYNAMIC_RULES} is the maximum number of dynamic rules an extension can add. The number of [unsafe rules](https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#safe_rules) must not exceed {@link MAX_NUMBER_OF_UNSAFE_DYNAMIC_RULES}.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function updateDynamicRules(options: UpdateRuleOptions): Promise<void>;\n        export function updateDynamicRules(options: UpdateRuleOptions, callback: () => void): void;\n\n        /**\n         * Updates the set of enabled static rulesets for the extension. The rulesets with IDs listed in `options.disableRulesetIds` are first removed, and then the rulesets listed in `options.enableRulesetIds` are added.\n         * Note that the set of enabled static rulesets is persisted across sessions but not across extension updates, i.e. the `rule_resources` manifest key will determine the set of enabled static rulesets on each extension update.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         */\n        export function updateEnabledRulesets(options: UpdateRulesetOptions): Promise<void>;\n        export function updateEnabledRulesets(options: UpdateRulesetOptions, callback: () => void): void;\n\n        /**\n         * Modifies the current set of session scoped rules for the extension. The rules with IDs listed in `options.removeRuleIds` are first removed, and then the rules given in `options.addRules` are added. Notes:\n         *\n         * *   This update happens as a single atomic operation: either all specified rules are added and removed, or an error is returned.\n         * *   These rules are not persisted across sessions and are backed in memory.\n         * *   {@link MAX_NUMBER_OF_SESSION_RULES} is the maximum number of session rules an extension can add.\n         *\n         * Can return its result via Promise in Manifest V3 or later since Chrome 91.\n         * @since Chrome 90\n         */\n        export function updateSessionRules(options: UpdateRuleOptions): Promise<void>;\n        export function updateSessionRules(options: UpdateRuleOptions, callback: () => void): void;\n\n        /**\n         * Disables and enables individual static rules in a {@link Ruleset}. Changes to rules belonging to a disabled {@link Ruleset} will take effect the next time that it becomes enabled.\n         *\n         * Can return its result via Promise in Manifest V3.\n         * @since Chrome 111\n         */\n        export function updateStaticRules(options: UpdateStaticRulesOptions): Promise<void>;\n        export function updateStaticRules(options: UpdateStaticRulesOptions, callback?: () => void): void;\n\n        /** Fired when a rule is matched with a request. Only available for unpacked extensions with the `declarativeNetRequestFeedback` permission as this is intended to be used for debugging purposes only. */\n        export const onRuleMatchedDebug: events.Event<(info: MatchedRuleInfoDebug) => void>;\n    }\n\n    ////////////////////\n    // SidePanel\n    ////////////////////\n    /**\n     * Use the `Browser.sidePanel` API to host content in the browser's side panel alongside the main content of a webpage.\n     *\n     * Permissions: \"sidePanel\"\n     * @since Chrome 114, MV3\n     */\n    export namespace sidePanel {\n        /** @since Chrome 141 */\n        export type CloseOptions =\n            | {\n                /** The tab in which to close the side panel. If a tab-specific side panel is open in the specified tab, it will be closed for that tab. At least one of this or `windowId` must be provided.  */\n                tabId: number;\n                /** The window in which to close the side panel. If a global side panel is open in the specified window, it will be closed for all tabs in that window where no tab-specific panel is active. At least one of this or `tabId` must be provided. */\n                windowId?: number | undefined;\n            }\n            | {\n                /** The tab in which to close the side panel. If a tab-specific side panel is open in the specified tab, it will be closed for that tab. At least one of this or `windowId` must be provided.  */\n                tabId?: number | undefined;\n                /** The window in which to close the side panel. If a global side panel is open in the specified window, it will be closed for all tabs in that window where no tab-specific panel is active. At least one of this or `tabId` must be provided. */\n                windowId: number;\n            };\n\n        export interface GetPanelOptions {\n            /**\n             * If specified, the side panel options for the given tab will be returned.\n             * Otherwise, returns the default side panel options (used for any tab that doesn't have specific settings).\n             */\n            tabId?: number | undefined;\n        }\n\n        /** @since Chrome 116 */\n        export type OpenOptions =\n            & {\n                /**\n                 * The tab in which to open the side panel.\n                 * If the corresponding tab has a tab-specific side panel, the panel will only be open for that tab.\n                 * If there is not a tab-specific panel, the global panel will be open in the specified tab and any other tabs without a currently-open tab- specific panel.\n                 * This will override any currently-active side panel (global or tab-specific) in the corresponding tab.\n                 * At least one of this and `windowId` must be provided. */\n                tabId?: number | undefined;\n                /**\n                 * The window in which to open the side panel.\n                 * This is only applicable if the extension has a global (non-tab-specific) side panel or `tabId` is also specified.\n                 * This will override any currently-active global side panel the user has open in the given window.\n                 * At least one of this and `tabId` must be provided.\n                 */\n                windowId?: number | undefined;\n            }\n            & ({\n                tabId: number;\n            } | {\n                windowId: number;\n            });\n\n        export interface PanelBehavior {\n            /** Whether clicking the extension's icon will toggle showing the extension's entry in the side panel. Defaults to false. */\n            openPanelOnActionClick?: boolean | undefined;\n        }\n\n        /** @since Chrome 142 */\n        export interface PanelClosedInfo {\n            /** The path of the local resource within the extension package whose content is displayed in the panel. */\n            path: string;\n            /** The optional ID of the tab where the side panel was closed. This is provided only when the panel is tab-specific. */\n            tabId?: number;\n            /** The ID of the window where the side panel was closed. This is available for both global and tab-specific panels. */\n            windowId: number;\n        }\n\n        /** @since Chrome 140 */\n        export interface PanelLayout {\n            side: `${Side}`;\n        }\n\n        /** @since Chrome 141 */\n        export interface PanelOpenedInfo {\n            /** The path of the local resource within the extension package whose content is displayed in the panel. */\n            path: string;\n            /** The optional ID of the tab where the side panel is opened. This is provided only when the panel is tab-specific. */\n            tabId?: number;\n            /** The ID of the window where the side panel is opened. This is available for both global and tab-specific panels. */\n            windowId: number;\n        }\n\n        export interface PanelOptions {\n            /** Whether the side panel should be enabled. This is optional. The default value is true. */\n            enabled?: boolean | undefined;\n            /** The path to the side panel HTML file to use. This must be a local resource within the extension package. */\n            path?: string | undefined;\n            /**\n             * If specified, the side panel options will only apply to the tab with this id.\n             * If omitted, these options set the default behavior (used for any tab that doesn't have specific settings).\n             * Note: if the same path is set for this tabId and the default tabId, then the panel for this tabId will be a different instance than the panel for the default tabId.\n             */\n            tabId?: number | undefined;\n        }\n\n        /**\n         * Defines the possible alignment for the side panel in the browser UI.\n         * @since Chrome 140\n         */\n        export enum Side {\n            LEFT = \"left\",\n            RIGHT = \"right\",\n        }\n\n        export interface SidePanel {\n            /** Developer specified path for side panel display. */\n            default_path: string;\n        }\n\n        /**\n         * Closes the extension's side panel. This is a no-op if the panel is already closed.\n         * @param options Specifies the context in which to close the side panel.\n         * @since Chrome 141\n         */\n        export function close(options: CloseOptions): Promise<void>;\n        export function close(options: CloseOptions, callback: () => void): void;\n\n        /**\n         * Returns the side panel's current layout.\n         * @since Chrome 140\n         */\n        export function getLayout(): Promise<PanelLayout>;\n        export function getLayout(callback: (layout: PanelLayout) => void): void;\n\n        /**\n         * Returns the active panel configuration.\n         *\n         * Can return its result via Promise.\n         * @param options Specifies the context to return the configuration for.\n         */\n        export function getOptions(options: GetPanelOptions): Promise<PanelOptions>;\n        export function getOptions(options: GetPanelOptions, callback: (options: PanelOptions) => void): void;\n\n        /**\n         * Returns the extension's current side panel behavior.\n         *\n         * Can return its result via Promise.\n         */\n        export function getPanelBehavior(): Promise<PanelBehavior>;\n        export function getPanelBehavior(callback: (behavior: PanelBehavior) => void): void;\n\n        /**\n         * Opens the side panel for the extension. This may only be called in response to a user action.\n         *\n         * Can return its result via Promise.\n         * @param options Specifies the context in which to open the side panel.\n         * @since Chrome 116\n         */\n        export function open(options: OpenOptions): Promise<void>;\n        export function open(options: OpenOptions, callback: () => void): void;\n\n        /**\n         * Configures the side panel.\n         *\n         * Can return its result via Promise.\n         * @param options The configuration options to apply to the panel.\n         */\n        export function setOptions(options: PanelOptions): Promise<void>;\n        export function setOptions(options: PanelOptions, callback: () => void): void;\n\n        /**\n         * Configures the extension's side panel behavior. This is an upsert operation.\n         *\n         * Can return its result via Promise.\n         * @param behavior The new behavior to be set.\n         */\n        export function setPanelBehavior(behavior: PanelBehavior): Promise<void>;\n        export function setPanelBehavior(behavior: PanelBehavior, callback: () => void): void;\n\n        /**\n         * Fired when the extension's side panel is closed.\n         * @since Chrome 142\n         */\n        const onClosed: events.Event<(info: PanelClosedInfo) => void>;\n\n        /**\n         * Fired when the extension's side panel is opened.\n         * @since Chrome 141\n         */\n        const onOpened: events.Event<(info: PanelOpenedInfo) => void>;\n    }\n\n    ////////////////////\n    // User Scripts\n    ////////////////////\n    /**\n     * Use the `userScripts` API to execute user scripts in the User Scripts context.\n     *\n     * Permissions: \"userScripts\"\n     * @since Chrome 120, MV3\n     */\n    export namespace userScripts {\n        /** The JavaScript world for a user script to execute within. */\n        export enum ExecutionWorld {\n            /** Specifies the execution environment of the DOM, which is the execution environment shared with the host page's JavaScript. */\n            MAIN = \"MAIN\",\n            /** Specifies the execution environment that is specific to user scripts and is exempt from the page's CSP. */\n            USER_SCRIPT = \"USER_SCRIPT\",\n        }\n\n        /** @since Chrome 135 */\n        export type InjectionResult<T = unknown> =\n            & {\n                /** The document associated with the injection. */\n                documentId: string;\n                /** The frame associated with the injection. */\n                frameId: number;\n            }\n            & (\n                | {\n                    /** The error, if any */\n                    error?: never;\n                    /** The result of the script execution. */\n                    result: T;\n                }\n                | {\n                    /** The error, if any */\n                    error: string;\n                    /** The result of the script execution. */\n                    result?: never;\n                }\n            );\n\n        export interface WorldProperties {\n            /** Specifies the world csp. The default is the `ISOLATED` world csp. */\n            csp?: string | undefined;\n            /** Specifies whether messaging APIs are exposed. The default is `false`.*/\n            messaging?: boolean | undefined;\n            /**\n             * Specifies the ID of the specific user script world to update. If not provided, updates the properties of the default user script world. Values with leading underscores (`_`) are reserved.\n             * @since Chrome 133\n             */\n            worldId?: string | undefined;\n        }\n\n        export interface UserScriptFilter {\n            /** {@link getScripts} only returns scripts with the IDs specified in this list. */\n            ids?: string[] | undefined;\n        }\n\n        // /** @since Chrome 135 */\n        export type InjectionTarget =\n            & {\n                /** The ID of the tab into which to inject. */\n                tabId: number;\n            }\n            & (\n                | {\n                    /** Whether the script should inject into all frames within the tab. Defaults to false. */\n                    allFrames?: boolean | undefined;\n                    /** The IDs of specific documentIds to inject into. */\n                    documentIds?: never;\n                    /** The IDs of specific frames to inject into. */\n                    frameIds?: never;\n                }\n                | {\n                    /** Whether the script should inject into all frames within the tab. Defaults to false. */\n                    allFrames?: false | undefined;\n                    /** The IDs of specific documentIds to inject into. */\n                    documentIds?: never;\n                    /** The IDs of specific frames to inject into. */\n                    frameIds: number[] | undefined;\n                }\n                | {\n                    /** Whether the script should inject into all frames within the tab. Defaults to false. */\n                    allFrames?: false | undefined;\n                    /** The IDs of specific documentIds to inject into. */\n                    documentIds?: string[] | undefined;\n                    /** The IDs of specific frames to inject into. */\n                    frameIds?: never;\n                }\n            );\n\n        export interface RegisteredUserScript {\n            /** If true, it will inject into all frames, even if the frame is not the top-most frame in the tab. Each frame is checked independently for URL requirements; it will not inject into child frames if the URL requirements are not met. Defaults to false, meaning that only the top frame is matched. */\n            allFrames?: boolean | undefined;\n            /** Specifies wildcard patterns for pages this user script will NOT be injected into. */\n            excludeGlobs?: string[] | undefined;\n            /**Excludes pages that this user script would otherwise be injected into. See Match Patterns for more details on the syntax of these strings. */\n            excludeMatches?: string[] | undefined;\n            /** The ID of the user script specified in the API call. This property must not start with a '_' as it's reserved as a prefix for generated script IDs. */\n            id: string;\n            /** Specifies wildcard patterns for pages this user script will be injected into. */\n            includeGlobs?: string[] | undefined;\n            /** The list of ScriptSource objects defining sources of scripts to be injected into matching pages. This property must be specified for {@link register}, and when specified it must be a non-empty array.*/\n            js: ScriptSource[];\n            /** Specifies which pages this user script will be injected into. See Match Patterns for more details on the syntax of these strings. This property must be specified for {@link register}. */\n            matches?: string[] | undefined;\n            /** Specifies when JavaScript files are injected into the web page. The preferred and default value is `document_idle` */\n            runAt?: extensionTypes.RunAt | undefined;\n            /** The JavaScript execution environment to run the script in. The default is `USER_SCRIPT` */\n            world?: `${ExecutionWorld}` | undefined;\n            /**\n             * Specifies the user script world ID to execute in. If omitted, the script will execute in the default user script world. Only valid if `world` is omitted or is `USER_SCRIPT`. Values with leading underscores (`_`) are reserved.\n             * @since Chrome 133\n             */\n            worldId?: string | undefined;\n        }\n\n        /** @since Chrome 135 */\n        export interface UserScriptInjection {\n            /** Whether the injection should be triggered in the target as soon as possible. Note that this is not a guarantee that injection will occur prior to page load, as the page may have already loaded by the time the script reaches the target. */\n            injectImmediately?: boolean | undefined;\n            /** The list of ScriptSource objects defining sources of scripts to be injected into the target. */\n            js: [ScriptSource, ...ScriptSource[]];\n            /** Details specifying the target into which to inject the script. */\n            target: InjectionTarget;\n            /** The JavaScript \"world\" to run the script in. The default is `USER_SCRIPT`. */\n            world?: `${ExecutionWorld}` | undefined;\n            /** Specifies the user script world ID to execute in. If omitted, the script will execute in the default user script world. Only valid if `world` is omitted or is `USER_SCRIPT`. Values with leading underscores (`_`) are reserved. */\n            worldId?: string | undefined;\n        }\n\n        export type ScriptSource = {\n            /** A string containing the JavaScript code to inject. */\n            code: string;\n            /** The path of the JavaScript file to inject relative to the extension's root directory. */\n            file?: undefined;\n        } | {\n            /** A string containing the JavaScript code to inject. */\n            code?: undefined;\n            /** The path of the JavaScript file to inject relative to the extension's root directory. */\n            file: string;\n        };\n\n        /**\n         * Configures the `USER_SCRIPT` execution environment.\n         *\n         * Can return its result via Promise.\n         * @param properties Contains the user script world configuration.\n         */\n        export function configureWorld(properties: WorldProperties): Promise<void>;\n        export function configureWorld(properties: WorldProperties, callback: () => void): void;\n\n        /**\n         * Returns all dynamically-registered user scripts for this extension.\n         *\n         * Can return its result via Promise.\n         * @param filter If specified, this method returns only the user scripts that match it.\n         */\n        export function getScripts(filter?: UserScriptFilter): Promise<RegisteredUserScript[]>;\n        export function getScripts(callback: (scripts: RegisteredUserScript[]) => void): void;\n        export function getScripts(\n            filter: UserScriptFilter | undefined,\n            callback: (scripts: RegisteredUserScript[]) => void,\n        ): void;\n\n        /**\n         * Retrieves all registered world configurations.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 133\n         */\n        export function getWorldConfigurations(): Promise<WorldProperties[]>;\n        export function getWorldConfigurations(callback: (worlds: WorldProperties[]) => void): void;\n\n        /**\n         * Injects a script into a target context. By default, the script will be run at `document_idle`, or immediately if the page has already loaded. If the `injectImmediately` property is set, the script will inject without waiting, even if the page has not finished loading. If the script evaluates to a promise, the browser will wait for the promise to settle and return the resulting value.\n         *\n         * Can return its result via Promise.\n         * @since Chrome 135\n         */\n        export function execute<T>(injection: UserScriptInjection): Promise<InjectionResult<T>[]>;\n        export function execute<T>(\n            injection: UserScriptInjection,\n            callback: (result: InjectionResult<T>[]) => void,\n        ): void;\n\n        /**\n         * Registers one or more user scripts for this extension.\n         *\n         * Can return its result via Promise.\n         * @param scripts - Contains a list of user scripts to be registered.\n         */\n        export function register(scripts: RegisteredUserScript[]): Promise<void>;\n        export function register(scripts: RegisteredUserScript[], callback: () => void): void;\n\n        /**\n         * Resets the configuration for a user script world. Any scripts that inject into the world with the specified ID will use the default world configuration.\n         * @param worldId The ID of the user script world to reset. If omitted, resets the default world's configuration.\n         */\n        export function resetWorldConfiguration(worldId?: string): Promise<void>;\n        export function resetWorldConfiguration(callback: () => void): void;\n        export function resetWorldConfiguration(worldId: string | undefined, callback: () => void): void;\n\n        /**\n         * Unregisters all dynamically-registered user scripts for this extension.\n         *\n         * Can return its result via Promise.\n         * @param filter If specified, this method unregisters only the user scripts that match it.\n         */\n        export function unregister(filter?: UserScriptFilter): Promise<void>;\n        export function unregister(callback: () => void): void;\n        export function unregister(filter: UserScriptFilter | undefined, callback: () => void): void;\n\n        /**\n         * Updates one or more user scripts for this extension.\n         *\n         * Can return its result via Promise.\n         * @param scripts Contains a list of user scripts to be updated. A property is only updated for the existing script if it is specified in this object. If there are errors during script parsing/file validation, or if the IDs specified do not correspond to a fully registered script, then no scripts are updated.\n         */\n        export function update(scripts: RegisteredUserScript[]): Promise<void>;\n        export function update(scripts: RegisteredUserScript[], callback: () => void): void;\n    }\n}\n\n"
  },
  {
    "path": "packages/browser/src/index.d.ts",
    "content": "import { Browser } from './gen';\n\nexport const browser: typeof Browser;\nexport { Browser };\n"
  },
  {
    "path": "packages/browser/src/index.mjs",
    "content": "// #region snippet\nexport const browser = globalThis.browser?.runtime?.id\n  ? globalThis.browser\n  : globalThis.chrome;\n// #endregion snippet\n"
  },
  {
    "path": "packages/browser/templates/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/browser\",\n  \"description\": \"Provides a cross-browser API for using extension APIs and types based on @types/chrome\",\n  \"version\": \"{{chromeTypesVersion}}\",\n  \"type\": \"module\",\n  \"main\": \"src/index.mjs\",\n  \"types\": \"src/index.d.ts\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/browser\"\n  },\n  \"scripts\": {\n    \"check\": \"check\",\n    \"gen\": \"tsx scripts/generate.ts\"\n  },\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"files\": [\n    \"src\"\n  ],\n  \"devDependencies\": {\n    \"@types/chrome\": \"{{chromeTypesVersion}}\",\n    \"@types/node\": \"^20.0.0\",\n    \"nano-spawn\": \"^2.0.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n"
  },
  {
    "path": "packages/browser/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {}\n}\n"
  },
  {
    "path": "packages/i18n/CHANGELOG.md",
    "content": "# Changelog\n\n## v0.2.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.4...i18n-v0.2.5)\n\n### 🩹 Fixes\n\n- Add `.jsonc` support for locale files to match docs ([#2066](https://github.com/wxt-dev/wxt/pull/2066))\n\n### 🏡 Chore\n\n- Manually fix markdownlint errors ([#1711](https://github.com/wxt-dev/wxt/pull/1711))\n- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n- Upgrade major deps ([#2070](https://github.com/wxt-dev/wxt/pull/2070))\n\n### ❤️ Contributors\n\n- Ilya Kubariev ([@gymnasy55](https://github.com/gymnasy55))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.2.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)\n\n### 🩹 Fixes\n\n- Standardize locale codes and warn about unsupported ones ([#1617](https://github.com/wxt-dev/wxt/pull/1617))\n- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))\n\n### 📖 Documentation\n\n- Add react language ID to README ([#1347](https://github.com/wxt-dev/wxt/pull/1347))\n- Fix public path reference ([bcb20874](https://github.com/wxt-dev/wxt/commit/bcb20874))\n\n### 🏡 Chore\n\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Okinea Dev ([@okineadev](https://github.com/okineadev))\n- Redwoodlid ([@redwoodlid](https://github.com/redwoodlid))\n\n## v0.2.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)\n\n### 🩹 Fixes\n\n- Prevent app crashes from parse errors due to incomplete file saves ([#1114](https://github.com/wxt-dev/wxt/pull/1114))\n\n### 📖 Documentation\n\n- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))\n\n### ❤️ Contributors\n\n- Bread Grocery <breadgrocery@gmail.com>\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.2.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.1...i18n-v0.2.2)\n\n### 🚀 Enhancements\n\n- Add support for configuring the `locales` directory ([#1109](https://github.com/wxt-dev/wxt/pull/1109))\n\n### ❤️ Contributors\n\n- Bread Grocery <breadgrocery@gmail.com>\n\n## v0.2.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.0...i18n-v0.2.1)\n\n### 🩹 Fixes\n\n- Add missing \"type\" keyword to type export in generated file ([22b5294](https://github.com/wxt-dev/wxt/commit/22b5294))\n\n### 📖 Documentation\n\n- Rewrite and restructure the documentation website ([#933](https://github.com/wxt-dev/wxt/pull/933))\n\n### 🏡 Chore\n\n- Fix typo in internal function name ([21894d2](https://github.com/wxt-dev/wxt/commit/21894d2))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.2.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.1.1...i18n-v0.2.0)\n\n### 🩹 Fixes\n\n- ⚠️  Remove invalid options argument ([#1048](https://github.com/wxt-dev/wxt/pull/1048))\n\nTo upgrade, if you were passing a final `options` argument, remove it. If you used the third argument to escape `<` symbol... You'll need to do it yourself:\n\n```diff\n- i18n.t(\"someKey\", [\"sub1\"], { escapeLt: true });\n+ i18n.t(\"someKey\", [\"sub1\"]).replaceAll(\"<\", \"&lt;\");\n```\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.1.0...i18n-v0.1.1)\n\n### 🩹 Fixes\n\n- Friendly error messages for `null` and `undefined` values inside message files ([#1041](https://github.com/wxt-dev/wxt/pull/1041))\n\n### 🏡 Chore\n\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n\n### ❤️ Contributors\n\n- Windmillcode0 <shieldmousetower734@gmail.com>\n- Aaron ([@aklinker1](http://github.com/aklinker1))"
  },
  {
    "path": "packages/i18n/README.md",
    "content": "# `@wxt-dev/i18n`\n\n[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/i18n/CHANGELOG.md)\n\n`@wxt-dev/i18n` is a simple, type-safe wrapper around the `browser.i18n` APIs. It provides several benefits over the standard API:\n\n1. Simple messages file format\n2. Plural form support\n3. Integrates with the [I18n Ally VS Code extension](#vs-code)\n\nIt also provides several benefits over other non-web extension specific i18n packages:\n\n1. Does not bundle localization files into every entrypoint\n2. Don't need to fetch the localization files asynchronously\n3. Can localize text in manifest and CSS files\n\nHowever, it does have one major downside:\n\n1. Like the `browser.i18n` API, to change the language, users must change the browser's language\n\n> [!IMPORTANT]\n> You don't have to use `wxt` to use this package - it will work in any bundler setup. See [Installation without WXT](#without-wxt) for more details.\n\n## Installation\n\n### With WXT\n\n1. Install `@wxt-dev/i18n` via your package manager:\n\n   ```sh\n   pnpm i @wxt-dev/i18n\n   ```\n\n2. Add the WXT module to your `wxt.config.ts` file and setup a default locale:\n\n   ```ts\n   export default defineConfig({\n     modules: ['@wxt-dev/i18n/module'],\n     manifest: {\n       default_locale: 'en',\n     },\n   });\n   ```\n\n3. Create a localization file at `<srcDir>/locales/<default_locale>.yml` or move your existing localization files there.\n\n   ```yml\n   # <srcDir>/locales/en.yml\n   helloWorld: Hello world!\n   ```\n\n   > `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<rootDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!\n\n4. To get a translation, use the auto-imported `i18n` object or import it manually:\n\n   ```ts\n   import { i18n } from '#i18n';\n\n   i18n.t('helloWorld'); // \"Hello world!\"\n   ```\n\nAnd you're done! Using WXT, you get type-safety out of the box.\n\n### Without WXT\n\n1. Install `@wxt-dev/i18n` via your package manager:\n\n   ```sh\n   pnpm i @wxt-dev/i18n\n   ```\n\n2. Create a messages file at `_locales/<lang>/messages.json` or move your existing translations there:\n\n   ```json\n   {\n     \"helloWorld\": {\n       \"message\": \"Hello world!\"\n     }\n   }\n   ```\n\n   > [!NOTE]\n   > For the best DX, you should to integrate `@wxt-dev/i18n` into your build process. This enables:\n   >\n   > 1. Plural forms\n   > 2. Simple messages file format\n   > 3. Type safety\n   >\n   > See [Build Integrations](#build-integrations) to set it up.\n\n3. Create the `i18n` object, export it, and use it wherever you want!\n\n   ```ts\n   import { createI18n } from '@wxt-dev/i18n';\n\n   export const i18n = createI18n();\n\n   i18n.t('helloWorld'); // \"Hello world!\";\n   ```\n\n## Configuration\n\nThe module can be configured via the `i18n` config:\n\n```ts\nexport default defineConfig({\n  modules: ['@wxt-dev/i18n'],\n  i18n: {\n    // ...\n  },\n});\n```\n\nOptions have JSDocs available in your editor, or you can read them in the source code: [`I18nOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/i18n/src/module.ts).\n\n## Messages File Format\n\n> [!DANGER]\n> You can only use the file format discussed on this page if you have [integrated `@wxt-dev/i18n` into your build process](#build-integrations). If you have not integrated it into your build process, you must use JSON files in the `_locales` directory, like a normal web extension.\n\n### File Extensions\n\nYou can define your messages in several different file types:\n\n- `.yml`\n- `.yaml`\n- `.json`\n- `.jsonc`\n- `.json5`\n- `.toml`\n\n### Nested Keys\n\nYou can have translations at the top level or nest them into groups:\n\n```yml\nok: OK\ncancel: Cancel\nwelcome:\n  title: Welcome to XYZ\ndialogs:\n  confirmation:\n    title: 'Are you sure?'\n```\n\nTo access a nested key, use `.`:\n\n```ts\ni18n.t('ok'); // \"OK\"\ni18n.t('cancel'); // \"Cancel\"\ni18n.t('welcome.title'); // \"Welcome to XYZ\"\ni18n.t('dialogs.confirmation.title'); // \"Are you sure?\"\n```\n\n### Substitutions\n\nBecause `@wxt-dev/i18n` is based on `browser.i18n`, you define substitutions the same way, with `$1`-`$9`:\n\n```yml\nhello: Hello $1!\norder: Thanks for ordering your $1\n```\n\n#### Escapting `$`\n\nTo escape the dollar sign, put another `$` in front of it:\n\n```yml\ndollars: $$$1\n```\n\n```ts\ni18n.t('dollars', ['1.00']); // \"$1.00\"\n```\n\n### Plural Forms\n\n> [!WARNING]\n> Plural support languages like Arabic, that have different forms for \"few\" or \"many\", is not supported right now. Feel free to open a PR if you are interested in this!\n\nTo get a different translation based on a count:\n\n```yml\nitems:\n  1: 1 item\n  n: $1 items\n```\n\nThen you pass in the count as the second argument to `i18n.t`:\n\n```ts\ni18n.t('items', 0); // \"0 items\"\ni18n.t('items', 1); // \"1 item\"\ni18n.t('items', 2); // \"2 items\"\n```\n\nTo add a custom translation for 0 items:\n\n```yml\nitems:\n  0: No items\n  1: 1 item\n  n: $1 items\n```\n\n```ts\ni18n.t('items', 0); // \"No items\"\ni18n.t('items', 1); // \"1 item\"\ni18n.t('items', 2); // \"2 items\"\n```\n\nIf you need to pass a custom substitution for `$1` instead of the count, just add the substitution:\n\n```yml\nitems:\n  0: No items\n  1: $1 item\n  n: $1 items\n```\n\n```ts\ni18n.t('items', 0, ['Zero']); // \"No items\"\ni18n.t('items', 1, ['One']); // \"One item\"\ni18n.t('items', 2, ['Multiple']); // \"Multiple items\"\n```\n\n### Verbose Keys\n\n`@wxt-dev/i18n` is compatible with the message format used by [`browser.i18n`](https://developer.chrome.com/docs/extensions/reference/api/i18n).\n\n> [!IMPORTANT]\n> This means if you're migrating to `@wxt-dev/i18n` and you're already using the verbose format, you don't have to change anything!\n\nA key is treated as \"verbose\" when it is:\n\n1. At the top level (not nested)\n2. Only contains the following properties: `message`, `description` and/or `placeholder`\n\n```json\n{\n  \"appName\": {\n    \"message\": \"GitHub - Better Line Counts\",\n    \"description\": \"The app's name, should not be translated\"\n  },\n  \"ok\": \"OK\",\n  \"deleteConfirmation\": {\n    \"title\": \"Delete XYZ?\",\n    \"message\": \"You cannot undo this action once taken.\"\n  }\n}\n```\n\nIn this example, only `appName` is considered verbose. `deleteConfirmation` is not verbose because it contains the additional property `title`.\n\n```ts\ni18n.t('appName'); // ✅ \"GitHub - Better Line Counts\"\ni18n.t('appName.message'); // ❌\ni18n.t('ok'); // ✅ \"OK\"\ni18n.t('deleteConfirmation'); // ❌\ni18n.t('deleteConfirmation.title'); // ✅ \"Delete XYZ?\"\ni18n.t('deleteConfirmation.message'); // ✅ \"You cannot undo this action once taken.\"\n```\n\nIf this is confusing, don't worry! With type-safety, you'll get a type error if you do it wrong. If type-safety is disabled, you'll get a runtime warning in the devtools console.\n\n> [!WARNING]\n> Using the verbose format is not recommended. I have yet to see a translation service and software that supports this format out of the box. Stick with the simple format when you can.\n\n## Build Integrations\n\nTo use the custom messages file format, you'll need to use `@wxt-dev/i18n/build` to transform the custom format to the one expected by browsers.\n\n### WXT\n\nSee [Installation with WXT](#with-wxt).\n\nBut TLDR, all you need is:\n\n```ts\n// wxt.config.ts\nexport default defineConfig({\n  modules: ['@wxt-dev/i18n/module'],\n});\n```\n\nTypes are generated whenever you run `wxt prepare` or another build command.\n\n### Custom\n\nIf you're not using WXT, you'll have to pre-process the localization files yourself. Here's a basic script to generate the `_locales/.../messages.json` and `wxt-i18n-structure.d.ts` files:\n\n```ts\n// build-i18n.js\nimport {\n  parseMessagesFile,\n  generateChromeMessagesFile,\n  generateTypeFile,\n} from '@wxt-dev/i18n/build';\n\n// Read your localization files\nconst messages = {\n  en: await parseMessagesFile('path/locales/en.yml'),\n  de: await parseMessagesFile('path/locales/de.yml'),\n  // ...\n};\n\n// Generate JSON files for the extension\nawait generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en);\nawait generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de);\n// ...\n\n// Generate a types file based on your default_locale\nawait generateTypeFile('wxt-i18n-structure.d.ts', messages.en);\n```\n\nThen run the script:\n\n```sh\nnode generate-i18n.js\n```\n\nIf your build tool has a dev mode, you'll also want to rerun the script whenever you change a localization file.\n\n#### Type Safety\n\nOnce you've generated `wxt-i18n-structure.d.ts` (see the [Custom](#custom) section), you can use it to pass the generated type into `createI18n`:\n\n```ts\nimport type { WxtI18nStructure } from './wxt-i18n-structure';\n\nexport const i18n = createI18n<WxtI18nStructure>();\n```\n\nAnd just like that, you have type safety!\n\n## Editor Support\n\nFor better DX, you can configure your editor with plugins and extensions.\n\n### VS Code\n\nThe [I18n Ally Extension](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) adds several features to VS Code:\n\n- Go to translation definition\n- Inline previews of text\n- Hover to see other translations\n\nYou'll need to configure it the extension so it knows where your localization files are and what function represents getting a translation:\n\n`.vscode/i18n-ally-custom-framework.yml`:\n\n```yml\n# An array of strings which contain Language Ids defined by VS Code\n# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers\nlanguageIds:\n  - typescript\n  - typescriptreact\n\n# Look for t(\"...\")\nusageMatchRegex:\n  - \"[^\\\\w\\\\d]t\\\\(['\\\"`]({key})['\\\"`]\"\n\n# Disable other built-in i18n ally frameworks\nmonopoly: true\n```\n\n`.vscode/settings.json`:\n\n```json\n{\n  \"i18n-ally.localesPaths\": [\"src/locales\"],\n  \"i18n-ally.keystyle\": \"nested\"\n}\n```\n\n### Zed\n\nAs of time of writing, Aug 18, 2024, no extensions exist for Zed to add I18n support.\n\n### Jetbrains IDEs\n\nInstall the [I18n Ally plugin](https://plugins.jetbrains.com/plugin/17212-i18n-ally). The docs are limited around their Jetbrains support, but you'll probably need to configure the plugin similar to [VS Code](#vs-code)... Not sure where the files go though.\n\nPlease open a PR if you figure it out!\n"
  },
  {
    "path": "packages/i18n/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/i18n\",\n  \"description\": \"Type-safe wrapper around browser.i18n.getMessage with additional features\",\n  \"version\": \"0.2.5\",\n  \"type\": \"module\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/i18n\"\n  },\n  \"homepage\": \"http://wxt.dev/guide/i18n/installation.html\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"i18n\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"buildc --deps-only -- check\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"dependencies\": {\n    \"@wxt-dev/browser\": \"workspace:^\",\n    \"chokidar\": \"^5.0.0\",\n    \"confbox\": \"^0.2.4\",\n    \"tinyglobby\": \"^0.2.15\"\n  },\n  \"peerDependencies\": {\n    \"wxt\": \">=0.19.7\"\n  },\n  \"peerDependenciesMeta\": {\n    \"wxt\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.17.6\",\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"vitest-plugin-random-seed\": \"^1.1.2\",\n    \"wxt\": \"workspace:*\"\n  },\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    },\n    \"./build\": {\n      \"import\": {\n        \"types\": \"./dist/build.d.mts\",\n        \"default\": \"./dist/build.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/build.d.cts\",\n        \"default\": \"./dist/build.cjs\"\n      }\n    },\n    \"./module\": {\n      \"import\": {\n        \"types\": \"./dist/module.d.mts\",\n        \"default\": \"./dist/module.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/module.d.cts\",\n        \"default\": \"./dist/module.cjs\"\n      }\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "packages/i18n/src/__tests__/build.test.ts",
    "content": "import { describe, it, expect, vi } from 'vitest';\nimport {\n  parseMessagesFile,\n  generateChromeMessagesFile,\n  generateTypeFile,\n} from '../build';\nimport {\n  stringifyTOML,\n  stringifyYAML,\n  stringifyJSON5,\n  stringifyJSON,\n  stringifyJSONC,\n} from 'confbox';\nimport { writeFile, readFile } from 'node:fs/promises';\n\nvi.mock('node:fs/promises');\nconst mockWriteFile = vi.mocked(writeFile);\nconst mockReadFile = vi.mocked(readFile);\n\ndescribe('Built Tools', () => {\n  it('should correctly convert all types of message formats', async () => {\n    const fileText = stringifyYAML({\n      simple: 'example',\n      sub: 'Hello $1',\n      nested: {\n        example: 'This is nested',\n        array: ['One', 'Two'],\n        notChrome: {\n          message: 'test 1',\n        },\n      },\n      chrome: {\n        message: 'test 2',\n        description: 'test',\n      },\n      plural0: {\n        0: 'Zero items',\n        1: 'One item',\n        n: '$1 items',\n      },\n      plural1: {\n        1: 'One item',\n        n: '$1 items',\n      },\n      pluralN: {\n        n: '$1 items',\n      },\n      pluralSub: {\n        1: 'Hello $2, I have one problem',\n        n: 'Hello $2, I have $1 problems',\n      },\n    });\n\n    mockReadFile.mockResolvedValue(fileText);\n\n    const messages = await parseMessagesFile(`file.yml`);\n    await generateChromeMessagesFile('output.json', messages);\n    await generateTypeFile('output.d.ts', messages);\n    const actualChromeMessagesFile = mockWriteFile.mock.calls[0][1];\n    const actualDtsFile = mockWriteFile.mock.calls[1][1];\n\n    expect(mockWriteFile).toBeCalledTimes(2);\n    expect(actualChromeMessagesFile).toMatchInlineSnapshot(`\n      \"{\n        \"simple\": {\n          \"message\": \"example\"\n        },\n        \"sub\": {\n          \"message\": \"Hello $1\"\n        },\n        \"nested_example\": {\n          \"message\": \"This is nested\"\n        },\n        \"nested_array_0\": {\n          \"message\": \"One\"\n        },\n        \"nested_array_1\": {\n          \"message\": \"Two\"\n        },\n        \"nested_notChrome_message\": {\n          \"message\": \"test 1\"\n        },\n        \"chrome\": {\n          \"message\": \"test 2\",\n          \"description\": \"test\"\n        },\n        \"plural0\": {\n          \"message\": \"Zero items | One item | $1 items\"\n        },\n        \"plural1\": {\n          \"message\": \"One item | $1 items\"\n        },\n        \"pluralN\": {\n          \"message\": \"$1 items\"\n        },\n        \"pluralSub\": {\n          \"message\": \"Hello $2, I have one problem | Hello $2, I have $1 problems\"\n        }\n      }\n      \"\n    `);\n    expect(actualDtsFile).toMatchInlineSnapshot(`\n      \"export type GeneratedI18nStructure = {\n        \"simple\": { substitutions: 0, plural: false };\n        \"sub\": { substitutions: 1, plural: false };\n        \"nested.example\": { substitutions: 0, plural: false };\n        \"nested.array.0\": { substitutions: 0, plural: false };\n        \"nested.array.1\": { substitutions: 0, plural: false };\n        \"nested.notChrome.message\": { substitutions: 0, plural: false };\n        \"chrome\": { substitutions: 0, plural: false };\n        \"plural0\": { substitutions: 1, plural: true };\n        \"plural1\": { substitutions: 1, plural: true };\n        \"pluralN\": { substitutions: 1, plural: true };\n        \"pluralSub\": { substitutions: 2, plural: true };\n        \"@@extension_id\": { substitutions: 0, plural: false };\n        \"@@ui_locale\": { substitutions: 0, plural: false };\n        \"@@bidi_dir\": { substitutions: 0, plural: false };\n        \"@@bidi_reversed_dir\": { substitutions: 0, plural: false };\n        \"@@bidi_start_edge\": { substitutions: 0, plural: false };\n        \"@@bidi_end_edge\": { substitutions: 0, plural: false };\n      }\n      \"\n    `);\n  });\n\n  it.each([\n    ['yaml', stringifyYAML],\n    ['yml', stringifyYAML],\n    ['toml', stringifyTOML],\n    ['json', stringifyJSON],\n    ['jsonc', stringifyJSONC],\n    ['json5', stringifyJSON5],\n  ])('Parse and generate: %s', async (extension, stringify) => {\n    const fileText = stringify({\n      simple: 'example',\n    });\n    const expectedDts = `export type GeneratedI18nStructure = {\n  \"simple\": { substitutions: 0, plural: false };\n  \"@@extension_id\": { substitutions: 0, plural: false };\n  \"@@ui_locale\": { substitutions: 0, plural: false };\n  \"@@bidi_dir\": { substitutions: 0, plural: false };\n  \"@@bidi_reversed_dir\": { substitutions: 0, plural: false };\n  \"@@bidi_start_edge\": { substitutions: 0, plural: false };\n  \"@@bidi_end_edge\": { substitutions: 0, plural: false };\n}\n`;\n    const expectedChromeMessages =\n      JSON.stringify({ simple: { message: 'example' } }, null, 2) + '\\n';\n\n    mockReadFile.mockResolvedValue(fileText);\n\n    const messages = await parseMessagesFile(`file.${extension}`);\n    await generateChromeMessagesFile('output.json', messages);\n    await generateTypeFile('output.d.ts', messages);\n\n    expect(mockWriteFile).toBeCalledTimes(2);\n    expect(mockWriteFile).toBeCalledWith(\n      'output.json',\n      expectedChromeMessages,\n      'utf8',\n    );\n    expect(mockWriteFile).toBeCalledWith('output.d.ts', expectedDts, 'utf8');\n  });\n\n  it('should throw an error if messages file contains null or undefined', async () => {\n    const invalidFileContent = stringifyYAML({\n      simple: 'example',\n      invalidField: null,\n    });\n\n    mockReadFile.mockResolvedValue(invalidFileContent);\n\n    await expect(parseMessagesFile('invalid.yml')).rejects.toThrowError(\n      'Messages file should not contain `null` (found at \"invalidField\")',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/i18n/src/__tests__/index.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { createI18n } from '../index';\nimport { browser } from '@wxt-dev/browser';\n\nvi.mock('@wxt-dev/browser', async () => {\n  const { vi } = await import('vitest');\n  return {\n    browser: {\n      i18n: {\n        getMessage: vi.fn(),\n      },\n    },\n  };\n});\nconst getMessageMock = vi.mocked(browser.i18n.getMessage);\n\ndescribe('createI18n', () => {\n  beforeEach(() => {\n    getMessageMock.mockReturnValue('Some text.');\n  });\n\n  it.each([\n    ['key', 'key'],\n    ['some_key', 'some_key'],\n    ['some.nested.key', 'some_nested_key'],\n  ])('should retrieve \"%s\" as \"%s\"', (input, expectedKey) => {\n    const i18n = createI18n();\n    const expectedValue = String(Math.random());\n    getMessageMock.mockReturnValue(expectedValue);\n\n    const actual = i18n.t(input);\n\n    expect(actual).toBe(expectedValue);\n    expect(getMessageMock).toBeCalledTimes(1);\n    expect(getMessageMock).toBeCalledWith(expectedKey);\n  });\n\n  it.each([\n    ['n items', 0, 'n items'],\n    ['n items', 1, 'n items'],\n    ['n items', 2, 'n items'],\n    ['n items', 3, 'n items'],\n    ['1 item | n items', 0, 'n items'],\n    ['1 item | n items', 1, '1 item'],\n    ['1 item | n items', 2, 'n items'],\n    ['1 item | n items', 3, 'n items'],\n    ['0 items | 1 item | n items', 0, '0 items'],\n    ['0 items | 1 item | n items', 1, '1 item'],\n    ['0 items | 1 item | n items', 2, 'n items'],\n    ['0 items | 1 item | n items', 3, 'n items'],\n  ])(\n    'should retrieve plural forms correctly',\n    (rawMessage, count, expected) => {\n      const i18n = createI18n();\n      getMessageMock.mockReturnValue(rawMessage);\n      const key = 'items';\n\n      const actual = i18n.t(key, count);\n\n      expect(actual).toBe(expected);\n      expect(getMessageMock).toBeCalledTimes(1);\n      expect(getMessageMock).toBeCalledWith(key, [String(count)]);\n    },\n  );\n\n  it('should allow overriding the plural substitutions', () => {\n    const i18n = createI18n();\n    i18n.t('key', 3, ['custom']);\n    expect(getMessageMock).toBeCalledWith('key', ['custom']);\n  });\n});\n"
  },
  {
    "path": "packages/i18n/src/__tests__/types.test.ts",
    "content": "import { beforeEach, describe, it, vi } from 'vitest';\nimport { createI18n } from '..';\nimport { browser } from '@wxt-dev/browser';\n\nvi.mock('@wxt-dev/browser', async () => {\n  const { vi } = await import('vitest');\n  return {\n    browser: {\n      i18n: {\n        getMessage: vi.fn(),\n      },\n    },\n  };\n});\nconst getMessageMock = vi.mocked(browser.i18n.getMessage);\n\nconst n: number = 1;\n\ndescribe('I18n Types', () => {\n  beforeEach(() => {\n    getMessageMock.mockReturnValue('Some text.');\n  });\n\n  describe('No type-safety', () => {\n    const i18n = createI18n();\n\n    describe('t', () => {\n      it('should allow passing any combination of arguments', () => {\n        i18n.t('any');\n        i18n.t('any', ['one']);\n        i18n.t('any', ['one', 'two']);\n        i18n.t('any', n, ['one', 'two']);\n      });\n    });\n  });\n\n  describe('With type-safety', () => {\n    const i18n = createI18n<{\n      simple: { plural: false; substitutions: 0 };\n      simpleSub1: { plural: false; substitutions: 1 };\n      simpleSub2: { plural: false; substitutions: 2 };\n      plural: { plural: true; substitutions: 0 };\n      pluralSub1: { plural: true; substitutions: 1 };\n      pluralSub2: { plural: true; substitutions: 2 };\n    }>();\n\n    describe('t', () => {\n      it('should only allow passing valid combinations of arguments', () => {\n        i18n.t('simple');\n        // @ts-expect-error\n        i18n.t('simple', []);\n        // @ts-expect-error\n        i18n.t('simple', ['one']);\n        // @ts-expect-error\n        i18n.t('simple', n);\n\n        i18n.t('simpleSub1', ['one']);\n        // @ts-expect-error\n        i18n.t('simpleSub1');\n        // @ts-expect-error\n        i18n.t('simpleSub1', []);\n        // @ts-expect-error\n        i18n.t('simpleSub1', ['one', 'two']);\n        // @ts-expect-error\n        i18n.t('simpleSub1', n);\n\n        i18n.t('simpleSub2', ['one', 'two']);\n        // @ts-expect-error\n        i18n.t('simpleSub2');\n        // @ts-expect-error\n        i18n.t('simpleSub2', ['one']);\n        // @ts-expect-error\n        i18n.t('simpleSub2', ['one', 'two', 'three']);\n        // @ts-expect-error\n        i18n.t('simpleSub2', n);\n\n        i18n.t('plural', n);\n        // @ts-expect-error\n        i18n.t('plural');\n        // @ts-expect-error\n        i18n.t('plural', []);\n        // @ts-expect-error\n        i18n.t('plural', ['one']);\n        // @ts-expect-error\n        i18n.t('plural', n, ['sub']);\n\n        i18n.t('pluralSub1', n);\n        i18n.t('pluralSub1', n, undefined);\n        i18n.t('pluralSub1', n, ['one']);\n        // @ts-expect-error\n        i18n.t('pluralSub1');\n        // @ts-expect-error\n        i18n.t('pluralSub1', ['one']);\n        // @ts-expect-error\n        i18n.t('pluralSub1', n, []);\n        // @ts-expect-error\n        i18n.t('pluralSub1', n, ['one', 'two']);\n\n        i18n.t('pluralSub2', n, ['one', 'two']);\n        // @ts-expect-error\n        i18n.t('pluralSub2');\n        // @ts-expect-error\n        i18n.t('pluralSub2', ['one', 'two']);\n        // @ts-expect-error\n        i18n.t('pluralSub2', n, ['one']);\n        // @ts-expect-error\n        i18n.t('pluralSub2', n, ['one', 'two', 'three']);\n        // @ts-expect-error\n        i18n.t('pluralSub2', n);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/i18n/src/__tests__/utils.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { ChromeMessage } from '../build';\nimport {\n  applyChromeMessagePlaceholders,\n  getSubstitutionCount,\n  standardizeLocale,\n} from '../utils';\n\ndescribe('Utils', () => {\n  describe('applyChromeMessagePlaceholders', () => {\n    it('should return the combined string', () => {\n      const input = {\n        message: 'Hello $username$, welcome to $appName$',\n        placeholders: {\n          username: { content: '$1' },\n          appName: { content: 'Example' },\n        },\n      } satisfies ChromeMessage;\n      const expected = input.message\n        .replace('$username$', input.placeholders.username.content)\n        .replace('$appName$', input.placeholders.appName.content);\n\n      const actual = applyChromeMessagePlaceholders(input);\n\n      expect(actual).toBe(expected);\n    });\n\n    it('should ignore the case', () => {\n      const input = {\n        message: 'Hello $USERNAME$, welcome $username$',\n        placeholders: {\n          username: { content: '$1' },\n        },\n      } satisfies ChromeMessage;\n      const expected = input.message\n        .replace('$USERNAME$', input.placeholders.username.content)\n        .replace('$username$', input.placeholders.username.content);\n\n      const actual = applyChromeMessagePlaceholders(input);\n\n      expect(actual).toBe(expected);\n    });\n  });\n\n  describe('getSubstitutionCount', () => {\n    it('should return the last substution present in the message', () => {\n      expect(getSubstitutionCount('I like $1, but I like $2 better')).toBe(2);\n    });\n\n    it('should return 0 when no substitutions are present', () => {\n      expect(getSubstitutionCount('test')).toBe(0);\n    });\n\n    it('should ignore escaped dollar signs', () => {\n      expect(getSubstitutionCount('buy $1 now for $$2 dollars')).toBe(1);\n    });\n\n    it('should return the highest substitution when skipping numbers', () => {\n      expect(getSubstitutionCount('I like $1, but I like $8 better')).toBe(8);\n    });\n\n    it('should only allow up to 9 substitutions', () => {\n      expect(getSubstitutionCount('Hello $9')).toBe(9);\n      expect(getSubstitutionCount('Hello $10')).toBe(1);\n    });\n  });\n\n  describe('standardizeLocale', () => {\n    it('should convert two-letter locale codes to lowercase', () => {\n      expect(standardizeLocale('en')).toEqual('en');\n      expect(standardizeLocale('EN')).toEqual('en');\n    });\n\n    it('should convert locale code extensions to uppercase', () => {\n      expect(standardizeLocale('en_US')).toEqual('en_US');\n      expect(standardizeLocale('en_us')).toEqual('en_US');\n      expect(standardizeLocale('es_419')).toEqual('es_419');\n    });\n\n    it('should convert dashes to underscores', () => {\n      expect(standardizeLocale('en_US')).toEqual('en_US');\n      expect(standardizeLocale('en-US')).toEqual('en_US');\n    });\n\n    it('should return the input string as-is for unknown formats', () => {\n      expect(standardizeLocale('en_USSS')).toEqual('en_USSS');\n      expect(standardizeLocale('en-')).toEqual('en-');\n      expect(standardizeLocale('------')).toEqual('------');\n      expect(standardizeLocale('test')).toEqual('test');\n      expect(standardizeLocale('hello-world')).toEqual('hello-world');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/i18n/src/build.ts",
    "content": "/**\n * This module contains utils for generating types and `messages.json` files\n * based on the custom messages file format.\n *\n * @module @wxt-dev/i18n/build\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { parseYAML, parseJSON5, parseTOML } from 'confbox';\nimport { dirname, extname } from 'node:path';\nimport { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';\n\nexport { SUPPORTED_LOCALES } from './supported-locales';\n\n//\n// TYPES\n//\n\nexport type SimpleMessage = string;\n\nexport type PluralMessage = Record<'n' | number, string>;\n\nexport interface ChromeMessage {\n  message: string;\n  description?: string;\n  placeholders?: Record<string, { content: string; example?: string }>;\n}\n\nexport type Message =\n  | SimpleMessage\n  | PluralMessage\n  | ChromeMessage\n  | Message[]\n  | { [key: string]: Message };\n\nexport type MessagesObject = Record<string, Message>;\n\nexport interface ParsedBaseMessage {\n  key: string[];\n  substitutions: number;\n}\n\nexport interface ParsedChromeMessage extends ParsedBaseMessage, ChromeMessage {\n  type: 'chrome';\n}\nexport interface ParsedSimpleMessage extends ParsedBaseMessage {\n  type: 'simple';\n  message: string;\n}\nexport interface ParsedPluralMessage extends ParsedBaseMessage {\n  type: 'plural';\n  plurals: { [count: string]: string };\n}\n\nexport type ParsedMessage =\n  | ParsedChromeMessage\n  | ParsedSimpleMessage\n  | ParsedPluralMessage;\n\nexport type MessageFormat = 'JSON5' | 'YAML' | 'TOML';\n\n//\n// CONSTANTS\n//\n\n/**\n * See\n * https://developer.chrome.com/docs/extensions/reference/api/i18n#overview-predefined\n */\nconst PREDEFINED_MESSAGES: Record<string, ChromeMessage> = {\n  '@@extension_id': {\n    message: '<browser.runtime.id>',\n    description:\n      \"The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\\nNote: You can't use this message in a manifest file.\",\n  },\n  '@@ui_locale': {\n    message: '<browser.i18n.getUiLocale()>',\n    description: '',\n  },\n  '@@bidi_dir': {\n    message: '<ltr|rtl>',\n    description:\n      'The text direction for the current locale, either \"ltr\" for left-to-right languages such as English or \"rtl\" for right-to-left languages such as Japanese.',\n  },\n  '@@bidi_reversed_dir': {\n    message: '<rtl|ltr>',\n    description:\n      'If the `@@bidi_dir` is \"ltr\", then this is \"rtl\"; otherwise, it\\'s \"ltr\".',\n  },\n  '@@bidi_start_edge': {\n    message: '<left|right>',\n    description:\n      'If the `@@bidi_dir` is \"ltr\", then this is \"left\"; otherwise, it\\'s \"right\".',\n  },\n  '@@bidi_end_edge': {\n    message: '<right|left>',\n    description:\n      'If the `@@bidi_dir` is \"ltr\", then this is \"right\"; otherwise, it\\'s \"left\".',\n  },\n};\n\nconst EXT_FORMATS_MAP: Record<string, MessageFormat> = {\n  '.json': 'JSON5',\n  '.jsonc': 'JSON5',\n  '.json5': 'JSON5',\n  '.yaml': 'YAML',\n  '.yml': 'YAML',\n  '.toml': 'TOML',\n};\n\nconst PARSERS: Record<MessageFormat, (text: string) => MessagesObject> = {\n  YAML: parseYAML,\n  JSON5: parseJSON5,\n  TOML: parseTOML,\n};\n\nconst ALLOWED_CHROME_MESSAGE_KEYS: Set<string> = new Set<keyof ChromeMessage>([\n  'description',\n  'message',\n  'placeholders',\n]);\n\n//\n// PARSING\n//\n\n/** Parse a messages file, extract the messages. Supports JSON, JSON5, and YAML. */\nexport async function parseMessagesFile(\n  file: string,\n): Promise<ParsedMessage[]> {\n  const text = await readFile(file, 'utf8');\n  const ext = extname(file).toLowerCase();\n\n  return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5');\n}\n\n/** Parse a string, extracting the messages. Supports JSON, JSON5, and YAML. */\nexport function parseMessagesText(\n  text: string,\n  format: 'JSON5' | 'YAML' | 'TOML',\n): ParsedMessage[] {\n  return parseMessagesObject(PARSERS[format](text));\n}\n\n/** Given the JS object form of a raw messages file, extract the messages. */\nexport function parseMessagesObject(object: MessagesObject): ParsedMessage[] {\n  return _parseMessagesObject(\n    [],\n    {\n      ...object,\n      ...PREDEFINED_MESSAGES,\n    },\n    0,\n  );\n}\n\nfunction _parseMessagesObject(\n  path: string[],\n  object: Message,\n  depth: number,\n): ParsedMessage[] {\n  switch (typeof object) {\n    case 'string':\n    case 'bigint':\n    case 'boolean':\n    case 'number':\n    case 'symbol': {\n      const message = String(object);\n      const substitutions = getSubstitutionCount(message);\n      return [\n        {\n          type: 'simple',\n          key: path,\n          substitutions,\n          message,\n        },\n      ];\n    }\n    case 'object':\n      if (object == null) {\n        throw new Error(\n          `Messages file should not contain \\`${object}\\` (found at \"${path.join('.')}\")`,\n        );\n      }\n\n      if (Array.isArray(object))\n        return object.flatMap((item, i) =>\n          _parseMessagesObject(path.concat(String(i)), item, depth + 1),\n        );\n\n      if (isPluralMessage(object)) {\n        const message = Object.values(object).join('|');\n        const substitutions = getSubstitutionCount(message);\n        return [\n          {\n            type: 'plural',\n            key: path,\n            substitutions,\n            plurals: object,\n          },\n        ];\n      }\n\n      if (depth === 1 && isChromeMessage(object)) {\n        const message = applyChromeMessagePlaceholders(object);\n        const substitutions = getSubstitutionCount(message);\n        return [\n          {\n            type: 'chrome',\n            key: path,\n            substitutions,\n            ...object,\n          },\n        ];\n      }\n\n      return Object.entries(object).flatMap(([key, value]) =>\n        _parseMessagesObject(path.concat(key), value, depth + 1),\n      );\n    default:\n      throw Error(`\"Could not parse object of type \"${typeof object}\"`);\n  }\n}\n\nfunction isPluralMessage(object: Message): object is PluralMessage {\n  return Object.keys(object).every(\n    (key) => key === 'n' || isFinite(Number(key)),\n  );\n}\n\nfunction isChromeMessage(object: Message): object is ChromeMessage {\n  return Object.keys(object).every((key) =>\n    ALLOWED_CHROME_MESSAGE_KEYS.has(key),\n  );\n}\n\n//\n// OUTPUT\n//\n\nexport function generateTypeText(messages: ParsedMessage[]): string {\n  const renderMessageEntry = (message: ParsedMessage): string => {\n    // Use '.' for deep keys at runtime and types\n    const key = message.key.join('.');\n\n    const features = [\n      `substitutions: ${message.substitutions}`,\n      `plural: ${message.type === 'plural'}`,\n    ];\n    return `  \"${key}\": { ${features.join(', ')} };`;\n  };\n\n  return `export type GeneratedI18nStructure = {\n${messages.map(renderMessageEntry).join('\\n')}\n}\n`;\n}\n\nexport async function generateTypeFile(\n  outFile: string,\n  messages: ParsedMessage[],\n): Promise<void> {\n  const text = generateTypeText(messages);\n  await mkdir(dirname(outFile), { recursive: true });\n  await writeFile(outFile, text, 'utf8');\n}\n\nexport function generateChromeMessages(\n  messages: ParsedMessage[],\n): Record<string, ChromeMessage> {\n  return messages.reduce<Record<string, ChromeMessage>>((acc, message) => {\n    // Use _ for deep keys in _locales/.../messages.json\n    const key = message.key.join('_');\n    // Don't output predefined messages\n    if (PREDEFINED_MESSAGES[key]) return acc;\n\n    switch (message.type) {\n      case 'chrome':\n        acc[key] = {\n          message: message.message,\n          description: message.description,\n          placeholders: message.placeholders,\n        };\n        break;\n      case 'plural':\n        acc[key] = {\n          message: Object.values(message.plurals).join(' | '),\n        };\n        break;\n      case 'simple':\n        acc[key] = {\n          message: message.message,\n        };\n        break;\n    }\n    return acc;\n  }, {});\n}\n\nexport function generateChromeMessagesText(messages: ParsedMessage[]): string {\n  const raw = generateChromeMessages(messages);\n  return JSON.stringify(raw, null, 2);\n}\n\nexport async function generateChromeMessagesFile(\n  file: string,\n  messages: ParsedMessage[],\n): Promise<void> {\n  const text = generateChromeMessagesText(messages);\n  await writeFile(file, text + '\\n', 'utf8');\n}\n"
  },
  {
    "path": "packages/i18n/src/index.ts",
    "content": "/** @module @wxt-dev/i18n */\nimport {\n  I18nStructure,\n  DefaultI18nStructure,\n  I18n,\n  Substitution,\n} from './types';\nimport { browser } from '@wxt-dev/browser';\n\nexport function createI18n<\n  T extends I18nStructure = DefaultI18nStructure,\n>(): I18n<T> {\n  const t = (key: string, ...args: any[]) => {\n    // Resolve args\n    let sub: Substitution[] | undefined;\n    let count: number | undefined;\n    args.forEach((arg, i) => {\n      if (arg == null) {\n        // ignore nullish args\n      } else if (typeof arg === 'number') {\n        count = arg;\n      } else if (Array.isArray(arg)) {\n        sub = arg;\n      } else {\n        throw Error(\n          `Unknown argument at index ${i}. Must be a number for pluralization, substitution array, or options object.`,\n        );\n      }\n    });\n\n    // Default substitutions to [n]\n    if (count != null && sub == null) {\n      sub = [String(count)];\n    }\n\n    // Load the localization\n    let message: string;\n    if (sub?.length) {\n      // Convert all substitutions to strings\n      const stringSubs = sub?.map((sub) => String(sub));\n      message = browser.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);\n    } else {\n      message = browser.i18n.getMessage(key.replaceAll('.', '_'));\n    }\n    if (!message) {\n      console.warn(`[i18n] Message not found: \"${key}\"`);\n    }\n    if (count == null) return message;\n\n    // Apply pluralization\n    const plural = message.split(' | ');\n    switch (plural.length) {\n      // \"n items\"\n      case 1:\n        return plural[0];\n      // \"1 item | n items\"\n      case 2:\n        return plural[count === 1 ? 0 : 1];\n      // \"0 items | 1 item | n items\"\n      case 3:\n        return plural[count === 0 || count === 1 ? count : 2];\n      default:\n        throw Error('Unknown plural formatting');\n    }\n  };\n\n  return { t } as I18n<T>;\n}\n"
  },
  {
    "path": "packages/i18n/src/module.ts",
    "content": "/**\n * The WXT Module to integrate `@wxt-dev/i18n` into your project.\n *\n * ```ts\n * export default defineConfig({\n *   modules: ['@wxt-dev/i18n/module'],\n * });\n * ```\n *\n * @module @wxt-dev/i18n/module\n */\n\nimport 'wxt';\nimport { addAlias, defineWxtModule } from 'wxt/modules';\nimport {\n  generateChromeMessagesText,\n  parseMessagesFile,\n  generateTypeText,\n  SUPPORTED_LOCALES,\n} from './build';\nimport { glob } from 'tinyglobby';\nimport { basename, extname, join, resolve } from 'node:path';\nimport { watch } from 'chokidar';\nimport { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';\nimport { writeFile } from 'node:fs/promises';\nimport { standardizeLocale } from './utils';\n\nexport default defineWxtModule<I18nOptions>({\n  name: '@wxt-dev/i18n',\n  configKey: 'i18n',\n  imports: [{ from: '#i18n', name: 'i18n' }],\n  setup(wxt, options) {\n    if (wxt.config.manifest.default_locale == null) {\n      wxt.logger.warn(\n        `\\`[i18n]\\` manifest.default_locale not set, \\`@wxt-dev/i18n\\` disabled.`,\n      );\n      return;\n    }\n    wxt.logger.info(\n      '`[i18n]` Default locale: ' + wxt.config.manifest.default_locale,\n    );\n\n    const { localesDir = resolve(wxt.config.srcDir, 'locales') } =\n      options ?? {};\n\n    const getLocalizationFiles = async () => {\n      const files = await glob('*.{json,json5,jsonc,yml,yaml,toml}', {\n        cwd: localesDir,\n        absolute: true,\n        expandDirectories: false,\n      });\n\n      const unsupportedLocales: string[] = [];\n\n      const res = files.map((file) => {\n        const rawLocale = basename(file).replace(extname(file), '');\n        const locale = standardizeLocale(rawLocale);\n        if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale);\n        return { file, locale };\n      });\n\n      if (unsupportedLocales.length > 0)\n        wxt.logger.warn(\n          `Unsupported locales: [${unsupportedLocales.join(', ')}].\\n\\nWeb extensions only support a limited set of locales as described here: https://developer.chrome.com/docs/extensions/reference/api/i18n#locales`,\n        );\n\n      return res;\n    };\n\n    const generateOutputJsonFiles = async (): Promise<\n      GeneratedPublicFile[]\n    > => {\n      const files = await getLocalizationFiles();\n      return await Promise.all(\n        files.map(async ({ file, locale }) => {\n          const messages = await parseMessagesFile(file);\n          return {\n            contents: generateChromeMessagesText(messages),\n            relativeDest: join('_locales', locale, 'messages.json'),\n          };\n        }),\n      );\n    };\n\n    const generateTypes = async (): Promise<WxtDirFileEntry> => {\n      const files = await getLocalizationFiles();\n      const defaultLocaleFile = files.find(\n        ({ locale }) => locale === wxt.config.manifest.default_locale,\n      )!;\n      if (defaultLocaleFile == null) {\n        throw Error(\n          `\\`[i18n]\\` Required localization file does not exist: \\`<localesDir>/${wxt.config.manifest.default_locale}.{json|json5|jsonc|yml|yaml|toml}\\``,\n        );\n      }\n\n      const messages = await parseMessagesFile(defaultLocaleFile.file);\n      return {\n        path: typesPath,\n        text: generateTypeText(messages),\n      };\n    };\n\n    const updateLocalizations = async (file: string): Promise<void> => {\n      wxt.logger.info(\n        `\\`[i18n]\\` Localization file changed: \\`${basename(file)}\\``,\n      );\n\n      // Regenerate files\n      const [typesFile, jsonFiles] = await Promise.all([\n        generateTypes(),\n        generateOutputJsonFiles(),\n      ]);\n\n      // Write files to disk\n      await Promise.all([\n        writeFile(\n          resolve(wxt.config.wxtDir, typesFile.path),\n          typesFile.text,\n          'utf8',\n        ),\n        ...jsonFiles.map((file) =>\n          writeFile(\n            resolve(wxt.config.outDir, file.relativeDest),\n            file.contents,\n            'utf8',\n          ),\n        ),\n      ]);\n\n      // TODO: Implement HMR instead of reloading extension. The reload is\n      // fast, but it causes the popup to close, which I'd like to prevent.\n      wxt.server?.reloadExtension();\n      wxt.logger.success(`\\`[i18n]\\` Extension reloaded`);\n    };\n\n    // Create .wxt/i18n.ts\n\n    const sourcePath = resolve(wxt.config.wxtDir, 'i18n/index.ts');\n    const typesPath = resolve(wxt.config.wxtDir, 'i18n/structure.d.ts');\n\n    wxt.hooks.hook('prepare:types', async (_, entries) => {\n      entries.push({\n        path: sourcePath,\n        text: `import { createI18n } from '@wxt-dev/i18n';\nimport type { GeneratedI18nStructure } from './structure';\n\nexport const i18n = createI18n<GeneratedI18nStructure>();\n\nexport { type GeneratedI18nStructure }\n`,\n      });\n    });\n\n    addAlias(wxt, '#i18n', sourcePath);\n\n    // Generate separate declaration file containing types - this prevents\n    // firing the dev server's default file watcher when updating the types,\n    // which would cause a full rebuild and reload of the extension.\n\n    wxt.hooks.hook('prepare:types', async (_, entries) => {\n      entries.push(await generateTypes());\n    });\n\n    // Generate _locales/.../messages.json files\n\n    wxt.hooks.hook('build:publicAssets', async (_, assets) => {\n      const outFiles = await generateOutputJsonFiles();\n      assets.push(...outFiles);\n    });\n\n    // Reload extension during development\n\n    if (wxt.config.command === 'serve') {\n      wxt.hooks.hookOnce('build:done', () => {\n        const watcher = watch(localesDir);\n        watcher.on('change', (path) => {\n          updateLocalizations(path).catch(wxt.logger.error);\n        });\n      });\n    }\n  },\n});\n\n/** Options for the i18n module */\nexport interface I18nOptions {\n  /**\n   * Directory containing files that define the translations.\n   *\n   * @default '${config.srcDir}/locales'\n   */\n  localesDir?: string;\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    i18n?: I18nOptions;\n  }\n}\n"
  },
  {
    "path": "packages/i18n/src/supported-locales.ts",
    "content": "/** From https://developer.chrome.com/docs/extensions/reference/api/i18n#locales */\nexport const SUPPORTED_LOCALES = new Set([\n  'ar',\n  'am',\n  'bg',\n  'bn',\n  'ca',\n  'cs',\n  'da',\n  'de',\n  'el',\n  'en',\n  'en_AU',\n  'en_GB',\n  'en_US',\n  'es',\n  'es_419',\n  'et',\n  'fa',\n  'fi',\n  'fil',\n  'fr',\n  'gu',\n  'he',\n  'hi',\n  'hr',\n  'hu',\n  'id',\n  'it',\n  'ja',\n  'kn',\n  'ko',\n  'lt',\n  'lv',\n  'ml',\n  'mr',\n  'ms',\n  'nl',\n  'no',\n  'pl',\n  'pt_BR',\n  'pt_PT',\n  'ro',\n  'ru',\n  'sk',\n  'sl',\n  'sr',\n  'sv',\n  'sw',\n  'ta',\n  'te',\n  'th',\n  'tr',\n  'uk',\n  'vi',\n  'zh_CN',\n  'zh_TW',\n]);\n"
  },
  {
    "path": "packages/i18n/src/types.ts",
    "content": "export interface I18nFeatures {\n  plural: boolean;\n  substitutions: SubstitutionCount;\n}\n\nexport type I18nStructure = {\n  [K: string]: I18nFeatures;\n};\n\nexport type DefaultI18nStructure = {\n  [K: string]: any;\n};\n\n// prettier-ignore\nexport type SubstitutionTuple<T extends SubstitutionCount> =\n    T extends 1 ? [$1: Substitution]\n  : T extends 2 ? [$1: Substitution, $2: Substitution]\n  : T extends 3 ? [$1: Substitution, $2: Substitution, $3: Substitution]\n  : T extends 4 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution]\n  : T extends 5 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution]\n  : T extends 6 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution]\n  : T extends 7 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution]\n  : T extends 8 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution]\n  : T extends 9 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution, $9: Substitution]\n  : never\n\nexport type TFunction<T extends I18nStructure> = {\n  // Non-plural, no substitutions\n  <K extends keyof T>(\n    // prettier-ignore\n    key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0 } ? P : never; }[keyof T],\n  ): string;\n\n  // Non-plural with substitutions\n  <K extends keyof T>(\n    // prettier-ignore\n    key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? P : never; }[keyof T],\n    substitutions: T[K] extends I18nFeatures\n      ? SubstitutionTuple<T[K]['substitutions']>\n      : never,\n  ): string;\n\n  // Plural with 1 substitution\n  <K extends keyof T>(\n    // prettier-ignore\n    key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 1 } ? P : never; }[keyof T],\n    n: number,\n    substitutions?: SubstitutionTuple<1>,\n  ): string;\n\n  // Plural without substitutions\n  <K extends keyof T>(\n    // prettier-ignore\n    key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 0 | 1 } ? P : never; }[keyof T],\n    n: number,\n  ): string;\n\n  // Plural with substitutions\n  <K extends keyof T>(\n    // prettier-ignore\n    key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: SubstitutionCount } ? P : never; }[keyof T],\n    n: number,\n    substitutions: T[K] extends I18nFeatures\n      ? SubstitutionTuple<T[K]['substitutions']>\n      : never,\n  ): string;\n};\n\nexport interface I18n<T extends DefaultI18nStructure> {\n  t: TFunction<T>;\n}\n\nexport type Substitution = string | number;\n\ntype SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;\n"
  },
  {
    "path": "packages/i18n/src/utils.ts",
    "content": "import { ChromeMessage } from './build';\n\nexport function applyChromeMessagePlaceholders(message: ChromeMessage): string {\n  if (message.placeholders == null) return message.message;\n\n  return Object.entries(message.placeholders ?? {}).reduce(\n    (text, [name, value]) => {\n      return text.replaceAll(new RegExp(`\\\\$${name}\\\\$`, 'gi'), value.content);\n    },\n    message.message,\n  );\n}\n\nexport function getSubstitutionCount(message: string): number {\n  return (\n    1 +\n    Array.from({ length: MAX_SUBSTITUTIONS }).findLastIndex((_, i) =>\n      message.match(new RegExp(`(?<!\\\\$)\\\\$${i + 1}`)),\n    )\n  );\n}\n\nconst MAX_SUBSTITUTIONS = 9;\n\n/** Given a string, standardize it to the format `xx_YY`. */\nexport function standardizeLocale(locale: string): string {\n  if (locale.length === 2) return locale.toLowerCase();\n\n  const [is_match, prefix, suffix] =\n    locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? [];\n  if (is_match) {\n    return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`;\n  }\n\n  return locale;\n}\n"
  },
  {
    "path": "packages/i18n/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\"]\n  },\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/i18n/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nconst entry = ['src/index.ts', 'src/build.ts', 'src/module.ts'];\n\nexport default defineConfig([\n  {\n    entry,\n  },\n  {\n    entry,\n    format: 'cjs',\n  },\n]);\n"
  },
  {
    "path": "packages/i18n/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config';\nimport RandomSeed from 'vitest-plugin-random-seed';\n\nexport default defineProject({\n  test: {\n    mockReset: true,\n    restoreMocks: true,\n  },\n  plugins: [RandomSeed()],\n});\n"
  },
  {
    "path": "packages/is-background/README.md",
    "content": "# `@wxt-dev/is-background`\n\nExports a getter to determine if the current JS context is the background or not.\n\n## Installation\n\n```sh\npnpm add @wxt-dev/is-background\n```\n\n## Usage\n\n```ts\nimport { isBackground } from '@wxt-dev/is-background';\n\nisBackground(); // true | false\n```\n"
  },
  {
    "path": "packages/is-background/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/is-background\",\n  \"type\": \"module\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Check if the current context is the background or not.\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"pnpm build && check\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"test:coverage\": \"pnpm test run --coverage\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"dependencies\": {\n    \"@wxt-dev/browser\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/is-background\"\n  },\n  \"keywords\": [\n    \"wxt\",\n    \"chrome\",\n    \"web\",\n    \"extension\",\n    \"is\",\n    \"background\",\n    \"script\",\n    \"page\",\n    \"service\",\n    \"worker\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"types\": \"./dist/index.d.mts\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/is-background/src/__tests__/getter.test.ts",
    "content": "import { describe, it, vi, expect } from 'vitest';\nimport { getIsBackground } from '../getter';\n\nlet mockBrowser: any;\nvi.mock('@wxt-dev/browser', () => ({\n  get browser() {\n    return mockBrowser;\n  },\n}));\n\nconst backgroundWindow = Symbol('Background');\nconst otherWindow = Symbol('Other');\n\nfunction setupEnv(options: {\n  window: symbol | undefined;\n  hasExtensionApis: boolean;\n  hasGetBackgroundPage: boolean;\n  hasServiceWorkerGlobalScope: boolean;\n}): void {\n  vi.unstubAllGlobals();\n  mockBrowser = undefined;\n\n  if (options.window) {\n    vi.stubGlobal('window', options.window);\n  }\n\n  if (options.hasExtensionApis) {\n    mockBrowser = {\n      runtime: {\n        id: 'test',\n      },\n      ...(options.hasGetBackgroundPage && {\n        extension: {\n          getBackgroundPage: () => backgroundWindow,\n        },\n      }),\n    };\n  }\n\n  if (options.hasServiceWorkerGlobalScope) {\n    class ServiceWorkerGlobalScope {}\n    vi.stubGlobal('ServiceWorkerGlobalScope', ServiceWorkerGlobalScope);\n    vi.stubGlobal('self', new ServiceWorkerGlobalScope());\n  }\n}\n\ndescribe('isBackground Getter', () => {\n  describe('Non-extension contexts', () => {\n    it('should return false', () => {\n      setupEnv({\n        hasServiceWorkerGlobalScope: false,\n        hasExtensionApis: false,\n        hasGetBackgroundPage: false,\n        window: otherWindow,\n      });\n\n      expect(getIsBackground()).toBe(false);\n    });\n  });\n\n  describe('Chromium & Safari', () => {\n    const hasExtensionApis = true;\n\n    describe('MV2', () => {\n      const hasServiceWorkerGlobalScope = false;\n      const hasGetBackgroundPage = true;\n\n      it('should return true inside the background page', () => {\n        setupEnv({\n          hasServiceWorkerGlobalScope,\n          hasExtensionApis,\n          hasGetBackgroundPage,\n          window: backgroundWindow,\n        });\n\n        expect(getIsBackground()).toBe(true);\n      });\n\n      it('should return false outside the background page', () => {\n        setupEnv({\n          hasServiceWorkerGlobalScope,\n          hasExtensionApis,\n          hasGetBackgroundPage,\n          window: otherWindow,\n        });\n\n        expect(getIsBackground()).toBe(false);\n      });\n    });\n\n    describe('MV3', () => {\n      const hasGetBackgroundPage = false;\n\n      it('should return true inside the service worker', () => {\n        setupEnv({\n          hasServiceWorkerGlobalScope: true,\n          hasExtensionApis,\n          hasGetBackgroundPage,\n          window: undefined,\n        });\n\n        expect(getIsBackground()).toBe(true);\n      });\n\n      it('should return false outside the service worker', () => {\n        setupEnv({\n          hasServiceWorkerGlobalScope: false,\n          hasExtensionApis,\n          hasGetBackgroundPage,\n          window: otherWindow,\n        });\n\n        expect(getIsBackground()).toBe(false);\n      });\n    });\n  });\n\n  describe('Firefox, MV2 & MV3', () => {\n    const hasServiceWorkerGlobalScope = false;\n    const hasExtensionApis = true;\n    const hasGetBackgroundPage = true;\n\n    it('should return true inside the background page', () => {\n      setupEnv({\n        hasServiceWorkerGlobalScope,\n        hasExtensionApis,\n        hasGetBackgroundPage,\n        window: backgroundWindow,\n      });\n\n      expect(getIsBackground()).toBe(true);\n    });\n\n    it('should return false outside the background page', () => {\n      setupEnv({\n        hasServiceWorkerGlobalScope,\n        hasExtensionApis,\n        hasGetBackgroundPage,\n        window: otherWindow,\n      });\n\n      expect(getIsBackground()).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/is-background/src/getter.ts",
    "content": "import { browser } from '@wxt-dev/browser';\n\ndeclare class ServiceWorkerGlobalScope {}\n\nexport function getIsBackground(): boolean {\n  // Are we in an extension context?\n  if (!browser?.runtime?.id) return false;\n\n  // Is this a true MV3 service worker?\n  //\n  // - ✅ Chromium MV3\n  // - ❌ Firefox MV3 - Uses a non-persistent HTML page instead of a service worker.\n  // - ✅ Safari MV3\n  if (\n    typeof ServiceWorkerGlobalScope !== 'undefined' &&\n    self instanceof ServiceWorkerGlobalScope\n  ) {\n    return true;\n  }\n\n  // Is this the background page?\n  //\n  // - ✅ Chromium MV2\n  // - ✅ Firefox MV2\n  // - ✅ Firefox MV3 - Works with the non-persistent HTML page\n  // - ✅ Safari MV2\n  return (\n    typeof window !== 'undefined' &&\n    typeof browser.extension?.getBackgroundPage === 'function' &&\n    browser.extension.getBackgroundPage() === window\n  );\n}\n"
  },
  {
    "path": "packages/is-background/src/index.ts",
    "content": "/**\n * This module uses a lazy getter function so the logic isn't ran until it's\n * needed.\n *\n * This has a few benefits:\n *\n * 1. Easier to mock in tests\n * 2. Safe to import in NodeJS environments (but it should be safe to run just\n *    in-case)\n * 3. Keeps startup fast by waiting to run the slow functions (`instanceof` or\n *    `browser.extension.getBackgroundPage`) until needed\n *\n * @module @wxt-dev/is-background\n */\nimport { getIsBackground } from './getter';\n\nlet cached: boolean | undefined;\n\n/**\n * Getter that returns if the current context is apart of an extension's\n * background or not.\n *\n * > This function caches the result when called for the first time so it doesn't\n * > have to recalculate.\n *\n * @returns True when in a background page or service worker.\n */\nexport function isBackground(): boolean {\n  if (cached == null) cached = getIsBackground();\n  return cached;\n}\n"
  },
  {
    "path": "packages/is-background/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/module-react/CHANGELOG.md",
    "content": "# Changelog\n\n## v1.2.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.2.1...module-react-v1.2.2)\n\n### 🩹 Fixes\n\n- Add `@vitejs/plugin-react` v6 support ([c61fa8f8](https://github.com/wxt-dev/wxt/commit/c61fa8f8))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v1.2.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.2.0...module-react-v1.2.1)\n\n### 🏡 Chore\n\n- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v1.2.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.5...module-react-v1.2.0)\n\n### 🚀 Enhancements\n\n- Add `vitePluginsBefore` option ([#2170](https://github.com/wxt-dev/wxt/pull/2170))\n- Vite 8 support ([bfd4af5d](https://github.com/wxt-dev/wxt/commit/bfd4af5d))\n\n### 🏡 Chore\n\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Martin Broder <hello@martinbroder.com>\n\n## v1.1.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.4...module-react-v1.1.5)\n\n### 🏡 Chore\n\n- **deps:** Support @vitejs/plugin-react@5 ([9170159b](https://github.com/wxt-dev/wxt/commit/9170159b))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v1.1.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.3...module-react-v1.1.4)\n\n### 🩹 Fixes\n\n- Add support for WXT v0.20.0 ([c9dca022](https://github.com/wxt-dev/wxt/commit/c9dca022))\n\n### 🏡 Chore\n\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- **deps:** Upgrade to Vite 6 and related dependencies ([#1496](https://github.com/wxt-dev/wxt/pull/1496))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- **deps:** Update all dependencies ([#1648](https://github.com/wxt-dev/wxt/pull/1648))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Okinea Dev <hi@okinea.dev>\n\n## v1.1.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.2...module-react-v1.1.3)\n\n### 🩹 Fixes\n\n- Use react 19 in `@wxt-dev/module-react` and `templates/react` ([#1285](https://github.com/wxt-dev/wxt/pull/1285))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.1.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.1...module-react-v1.1.2)\n\n### 🩹 Fixes\n\n- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))\n\n### 🏡 Chore\n\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.0...module-react-v1.1.1)\n\n### 🏡 Chore\n\n- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))\n- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))\n- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v1.1.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.0.0...module-react-v1.1.0)\n\n### 🚀 Enhancements\n\n- Enable auto-imports for JSX/TSX files ([#773](https://github.com/wxt-dev/wxt/pull/773))\n\n### 🩹 Fixes\n\n- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))\n- Use `prepare` instead of `postinstall` for local dev setup ([#788](https://github.com/wxt-dev/wxt/pull/788))\n\n### 🏡 Chore\n\n- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))\n- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))\n- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))\n\n## v1.0.0\n\nInitial release 🎉"
  },
  {
    "path": "packages/module-react/README.md",
    "content": "# `@wxt-dev/module-react`\n\nEnables the use of [React](https://react.dev/) in your web extension, in HTML pages and content scripts.\n\nThis plugin makes a few changes:\n\n1. Adds `@vitejs/plugin-react` to vite\n2. Adds the [`react` preset](https://github.com/unjs/unimport/blob/main/src/presets/react.ts) to auto-imports\n\n## Usage\n\n```sh\npnpm i react react-dom\npnpm i -D @wxt-dev/module-react\n```\n\nThen add the module to your config:\n\n```ts\n// wxt.config.ts\nexport default defineConfig({\n  // Required\n  modules: ['@wxt-dev/module-react'],\n\n  // Optional: Pass options to the module:\n  react: {\n    vite: {\n      // ...\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/module-react/components/App.tsx",
    "content": "export default function () {\n  const [count, setCount] = useState(0);\n  const increment = () => setCount((count) => count + 1);\n  return <button onClick={increment}>Count: {count}</button>;\n}\n"
  },
  {
    "path": "packages/module-react/entrypoints/content/index.tsx",
    "content": "import {\n  defineContentScript,\n  ContentScriptContext,\n  createShadowRootUi,\n} from '#imports';\nimport React from 'react';\nimport ReactDOM from 'react-dom/client';\n\nexport default defineContentScript({\n  matches: ['*://*/*'],\n\n  async main(ctx) {\n    const ui = await createUi(ctx);\n    ui.mount();\n  },\n});\n\nfunction createUi(ctx: ContentScriptContext) {\n  return createShadowRootUi(ctx, {\n    name: 'react-ui',\n    position: 'inline',\n    append: 'first',\n    onMount(container) {\n      const root = ReactDOM.createRoot(container);\n      root.render(\n        <React.StrictMode>\n          <App />\n        </React.StrictMode>,\n      );\n      return root;\n    },\n    onRemove(root) {\n      root?.unmount();\n    },\n  });\n}\n"
  },
  {
    "path": "packages/module-react/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/module-react/entrypoints/popup/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\n\nconst root = document.getElementById('app')!;\n\nReactDOM.createRoot(root).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "packages/module-react/modules/react.ts",
    "content": "import 'wxt';\nimport { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';\nimport react, { Options as PluginOptions } from '@vitejs/plugin-react';\nimport type { PluginOption } from 'vite';\n\nexport default defineWxtModule<ReactModuleOptions>({\n  name: '@wxt-dev/module-react',\n  configKey: 'react',\n  setup(wxt, options) {\n    const { vite, vitePluginsBefore } = options ?? {};\n\n    addViteConfig(wxt, () => ({\n      plugins: [...(vitePluginsBefore ?? []), react(vite)],\n    }));\n\n    addImportPreset(wxt, 'react');\n\n    // Enable auto-imports for JSX files\n    wxt.hook('config:resolved', (wxt) => {\n      // In older versions of WXT, `wxt.config.imports` could be false\n      if (!wxt.config.imports) return;\n\n      wxt.config.imports.dirsScanOptions ??= {};\n      wxt.config.imports.dirsScanOptions.filePatterns = [\n        // Default plus JSX/TSX\n        '*.{ts,js,mjs,cjs,mts,cts,jsx,tsx}',\n      ];\n    });\n  },\n});\n\nexport interface ReactModuleOptions {\n  vite?: PluginOptions;\n  /**\n   * Vite plugins to add before the `react()` plugin. Some plugins like the\n   * `@tanstack/router-plugin` need to be added before `react()` to work\n   * correctly.\n   */\n  vitePluginsBefore?: PluginOption[];\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    react?: ReactModuleOptions;\n  }\n}\n"
  },
  {
    "path": "packages/module-react/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/module-react\",\n  \"description\": \"WXT module to enable React support\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/module-react\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/blob/main/packages/module-react/README.md\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"react\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"version\": \"1.2.2\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"check\": \"pnpm build && check\",\n    \"build\": \"buildc -- tsdown\",\n    \"prepare\": \"buildc --deps-only -- wxt prepare\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"vite\": \"^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0\",\n    \"wxt\": \">=0.19.16\"\n  },\n  \"dependencies\": {\n    \"@vitejs/plugin-react\": \"^4.4.1 || ^5.0.0 || ^6.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"publint\": \"^0.3.18\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.0.0\",\n    \"wxt\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/module-react/public/.keep",
    "content": ""
  },
  {
    "path": "packages/module-react/tsconfig.json",
    "content": "{\n  \"extends\": [\"../../tsconfig.base.json\", \"./.wxt/tsconfig.json\"],\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/module-react/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nconst entry = {\n  index: './modules/react.ts',\n};\n\nexport default defineConfig([\n  {\n    entry,\n  },\n  {\n    entry,\n    format: 'cjs',\n  },\n]);\n"
  },
  {
    "path": "packages/module-solid/CHANGELOG.md",
    "content": "# Changelog\n\n## v1.1.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.3...module-solid-v1.1.4)\n\n### 🩹 Fixes\n\n- Add support for WXT v0.20.0 ([c9dca022](https://github.com/wxt-dev/wxt/commit/c9dca022))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump solid-js from 1.9.3 to 1.9.4 ([#1391](https://github.com/wxt-dev/wxt/pull/1391))\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- **deps:** Bump vite-plugin-solid from 2.10.2 to 2.11.6 ([#1491](https://github.com/wxt-dev/wxt/pull/1491))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- **deps:** Upgrade to Vite 6 and related dependencies ([#1496](https://github.com/wxt-dev/wxt/pull/1496))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- **deps:** Update all dependencies ([#1648](https://github.com/wxt-dev/wxt/pull/1648))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Okinea Dev <hi@okinea.dev>\n\n## v1.1.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.2...module-solid-v1.1.3)\n\n### 🩹 Fixes\n\n- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))\n\n### 🏡 Chore\n\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.1.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.1...module-solid-v1.1.2)\n\n### 🏡 Chore\n\n- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))\n- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))\n- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v1.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.0...module-solid-v1.1.1)\n\n### 🩹 Fixes\n\n- Use `prepare` instead of `postinstall` for local dev setup ([#788](https://github.com/wxt-dev/wxt/pull/788))\n\n## v1.1.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.1...module-solid-v1.1.0)\n\n### 🚀 Enhancements\n\n- Enable auto-imports for JSX/TSX files ([#773](https://github.com/wxt-dev/wxt/pull/773))\n\n### 🩹 Fixes\n\n- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))\n\n### 🏡 Chore\n\n- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))\n- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))\n\n## v1.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.0...module-solid-v1.0.1)\n\n### 🩹 Fixes\n\n- Add `target: esnext` by default ([#733](https://github.com/wxt-dev/wxt/pull/733))\n\n### 🏡 Chore\n\n- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))\n\n## v1.0.0\n\nInitial release 🎉"
  },
  {
    "path": "packages/module-solid/README.md",
    "content": "# `@wxt-dev/module-solid`\n\nEnables the use of [SolidJS](https://www.solidjs.com/) in your web extension, in HTML pages and content scripts.\n\nThis plugin makes a few changes:\n\n1. Adds `vite-plugin-solid` to and sets `build.target: esnext` in the vite config\n2. Adds the [`solid-js` preset](https://github.com/unjs/unimport/blob/main/src/presets/solid.ts) to auto-imports\n\n## Usage\n\n```sh\npnpm i solid-js\npnpm i -D @wxt-dev/module-solid\n```\n\nThen add the module to your config:\n\n```ts\n// wxt.config.ts\nexport default defineConfig({\n  // Required\n  modules: ['@wxt-dev/module-solid'],\n\n  // Optional: Pass options to the module:\n  solid: {\n    vite: {\n      // ...\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/module-solid/components/App.tsx",
    "content": "import { Component } from 'solid-js';\n\nexport const App: Component = () => {\n  const [count, setCount] = createSignal(0);\n  const increment = () => setCount((count) => count + 1);\n  return <button onClick={increment}>Count: {count()}</button>;\n};\n"
  },
  {
    "path": "packages/module-solid/entrypoints/content/index.tsx",
    "content": "import {\n  defineContentScript,\n  ContentScriptContext,\n  createShadowRootUi,\n} from '#imports';\nimport { render } from 'solid-js/web';\n\nexport default defineContentScript({\n  matches: ['*://*/*'],\n\n  async main(ctx) {\n    const ui = await createUi(ctx);\n    ui.mount();\n  },\n});\n\nfunction createUi(ctx: ContentScriptContext) {\n  return createShadowRootUi(ctx, {\n    name: 'solid-ui',\n    position: 'inline',\n    append: 'first',\n    onMount(container) {\n      return render(() => <App />, container);\n    },\n    onRemove(unmount) {\n      unmount?.();\n    },\n  });\n}\n"
  },
  {
    "path": "packages/module-solid/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/module-solid/entrypoints/popup/main.tsx",
    "content": "import { render } from 'solid-js/web';\n\nconst root = document.getElementById('app')!;\n\nrender(() => <App />, root);\n"
  },
  {
    "path": "packages/module-solid/modules/solid.ts",
    "content": "import 'wxt';\nimport { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';\nimport solid, { Options as PluginOptions } from 'vite-plugin-solid';\n\nexport default defineWxtModule<SolidModuleOptions>({\n  name: '@wxt-dev/module-solid',\n  configKey: 'solid',\n  setup(wxt, options) {\n    const { vite } = options ?? {};\n\n    addViteConfig(wxt, () => ({\n      plugins: [solid(vite)],\n      build: {\n        target: 'esnext',\n      },\n    }));\n\n    addImportPreset(wxt, 'solid-js');\n\n    // Enable auto-imports for JSX files\n    wxt.hook('config:resolved', (wxt) => {\n      // In older versions of WXT, `wxt.config.imports` could be false\n      if (!wxt.config.imports) return;\n\n      wxt.config.imports.dirsScanOptions ??= {};\n      wxt.config.imports.dirsScanOptions.filePatterns = [\n        // Default plus JSX/TSX\n        '*.{ts,js,mjs,cjs,mts,cts,jsx,tsx}',\n      ];\n    });\n  },\n});\n\nexport interface SolidModuleOptions {\n  vite?: PluginOptions;\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    solid?: SolidModuleOptions;\n  }\n}\n"
  },
  {
    "path": "packages/module-solid/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/module-solid\",\n  \"description\": \"WXT module to enable SolidJS support\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/module-solid\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/blob/main/packages/module-solid/README.md\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"solidjs\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"version\": \"1.1.4\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"check\": \"pnpm build && check\",\n    \"build\": \"buildc -- tsdown\",\n    \"prepare\": \"buildc --deps-only -- wxt prepare\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"wxt\": \">=0.19.16\"\n  },\n  \"dependencies\": {\n    \"vite-plugin-solid\": \"^2.11.10\"\n  },\n  \"devDependencies\": {\n    \"publint\": \"^0.3.18\",\n    \"solid-js\": \"^1.9.11\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/module-solid/public/.keep",
    "content": ""
  },
  {
    "path": "packages/module-solid/tsconfig.json",
    "content": "{\n  \"extends\": [\"../../tsconfig.base.json\", \"./.wxt/tsconfig.json\"],\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\"\n  },\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/module-solid/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nconst entry = {\n  index: './modules/solid.ts',\n};\n\nexport default defineConfig([\n  {\n    entry,\n  },\n  {\n    entry,\n    format: 'cjs',\n  },\n]);\n"
  },
  {
    "path": "packages/module-svelte/CHANGELOG.md",
    "content": "# Changelog\n\n## v2.0.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.4...module-svelte-v2.0.5)\n\n### 🩹 Fixes\n\n- Update `@sveltejs/vite-plugin-svelte` version range to support Vite 8 ([48571f50](https://github.com/wxt-dev/wxt/commit/48571f50))\n\n### 🏡 Chore\n\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))\n- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v2.0.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.3...module-svelte-v2.0.4)\n\n### 🩹 Fixes\n\n- Add support for @sveltejs/vite-plugin-svelte@6 ([459f73fd](https://github.com/wxt-dev/wxt/commit/459f73fd))\n\n### 🏡 Chore\n\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Okinea Dev <hi@okinea.dev>\n\n## v2.0.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.2...module-svelte-v2.0.3)\n\n### 🩹 Fixes\n\n- Upgrade `vite-plugin-svelte` to support Vite 6 ([#1375](https://github.com/wxt-dev/wxt/pull/1375))\n\n### ❤️ Contributors\n\n- Eli ([@lishaduck](http://github.com/lishaduck))\n\n## v2.0.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.1...module-svelte-v2.0.2)\n\n### 🩹 Fixes\n\n- Svelte production errors and warnings on resolve conditions ([#1283](https://github.com/wxt-dev/wxt/pull/1283))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v2.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.0...module-svelte-v2.0.1)\n\n### 🩹 Fixes\n\n- Modify dev `vite.resolve.conditions` to support Vite 6 + Svelte 5 ([#1230](https://github.com/wxt-dev/wxt/pull/1230))\n\n### 🏡 Chore\n\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v2.0.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.1...module-svelte-v2.0.0)\n\n### 🚀 Enhancements\n\n- ⚠️  Svelte 5 support ([#1104](https://github.com/wxt-dev/wxt/pull/1104))\n\n### 🏡 Chore\n\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n\n#### ⚠️ Breaking Changes\n\nUpgraded `@sveltejs/vite-plugin-svelte` from v3 to v4. This drops support for Svelte 4 and below. To continue using older versions of Svelte, use v1 of this module.\n\nTo upgrade to svelte 5, just install `svelte@5`.\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.0...module-svelte-v1.0.1)\n\n### 🩹 Fixes\n\n- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))\n\n### 📖 Documentation\n\n- Fix link to unimport ([#826](https://github.com/wxt-dev/wxt/pull/826))\n\n### 🏡 Chore\n\n- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))\n- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))\n- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))\n- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))\n- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))\n- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n- Eetann ([@eetann](http://github.com/eetann))\n\n## v1.0.0\n\nInitial release 🎉"
  },
  {
    "path": "packages/module-svelte/README.md",
    "content": "# `@wxt-dev/module-svelte`\n\nEnables the use of [Svelte](https://svelte.dev/) in your web extension, in HTML pages and content scripts.\n\nThis plugin makes a few changes:\n\n1. Adds `@sveltejs/vite-plugin-svelte` to vite\n2. Adds the [`svelte` preset](https://github.com/unjs/unimport/blob/main/src/presets/svelte.ts) to auto-imports\n\n## Usage\n\n```sh\npnpm i svelte\npnpm i -D @wxt-dev/module-svelte\n```\n\nThen add the module to your config:\n\n```ts\n// wxt.config.ts\nexport default defineConfig({\n  // Required\n  modules: ['@wxt-dev/module-svelte'],\n\n  // Optional: Pass options to the module:\n  svelte: {\n    vite: {\n      // ...\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/module-svelte/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/module-svelte\",\n  \"description\": \"WXT module to enable Svelte support\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/module-svelte\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/blob/main/packages/module-svelte/README.md\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"svelte\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"version\": \"2.0.5\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"pnpm build && check\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"wxt\": \">=0.18.6\",\n    \"svelte\": \">=5\"\n  },\n  \"dependencies\": {\n    \"@sveltejs/vite-plugin-svelte\": \">=4\"\n  },\n  \"devDependencies\": {\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/module-svelte/src/index.ts",
    "content": "import 'wxt';\nimport { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';\nimport {\n  svelte,\n  vitePreprocess,\n  Options as PluginOptions,\n} from '@sveltejs/vite-plugin-svelte';\n\nexport default defineWxtModule<SvelteModuleOptions>({\n  name: '@wxt-dev/module-svelte',\n  configKey: 'svelte',\n  setup(wxt, options) {\n    const { vite } = options ?? {};\n\n    addViteConfig(wxt, ({ mode }) => ({\n      plugins: [\n        svelte({\n          // Using a svelte.config.js file causes a segmentation fault when importing the file\n          configFile: false,\n          preprocess: [vitePreprocess()],\n          ...vite,\n        }),\n      ],\n      resolve: {\n        conditions: ['browser', mode],\n      },\n    }));\n\n    addImportPreset(wxt, 'svelte');\n  },\n});\n\nexport interface SvelteModuleOptions {\n  vite?: Partial<PluginOptions>;\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    svelte?: SvelteModuleOptions;\n  }\n}\n"
  },
  {
    "path": "packages/module-svelte/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/module-svelte/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nconst entry = {\n  index: './src/index.ts',\n};\n\nexport default defineConfig([\n  {\n    entry,\n  },\n  {\n    entry,\n    format: 'cjs',\n  },\n]);\n"
  },
  {
    "path": "packages/module-vue/CHANGELOG.md",
    "content": "# Changelog\n\n## v1.0.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.2...module-vue-v1.0.3)\n\n### 🏡 Chore\n\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- Wxt & @wxt-dev/module-vue support Vite 7 ([#1771](https://github.com/wxt-dev/wxt/pull/1771))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Ayu ([@ayu-exorcist](https://github.com/ayu-exorcist))\n- Okinea Dev <hi@okinea.dev>\n\n## v1.0.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.1...module-vue-v1.0.2)\n\n### 🩹 Fixes\n\n- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))\n\n### 🏡 Chore\n\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- **deps:** Bump @vitejs/plugin-vue from 5.1.1 to 5.1.4 ([#1020](https://github.com/wxt-dev/wxt/pull/1020))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.0...module-vue-v1.0.1)\n\n### 🩹 Fixes\n\n- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))\n\n### 🏡 Chore\n\n- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))\n- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))\n- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))\n- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))\n- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))\n- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v1.0.0\n\nInitial release 🎉"
  },
  {
    "path": "packages/module-vue/README.md",
    "content": "# `@wxt-dev/module-vue`\n\nEnables the use of [Vue](https://vuejs.org/) in your web extension, in HTML pages and content scripts.\n\nThis plugin makes a few changes:\n\n1. Adds `@vitejs/plugin-vue` to vite config\n2. Adds the [`vue` preset](https://github.com/unjs/unimport/blob/main/src/presets/vue.ts) to auto-imports\n3. Applies sourcemap fix to prevent HMR errors during development\n4. Enable auto-imports in `.vue` files\n\n## Usage\n\n```sh\npnpm i vue\npnpm i -D @wxt-dev/module-vue\n```\n\nThen add the module to your config:\n\n```ts\n// wxt.config.ts\nexport default defineConfig({\n  // Required\n  modules: ['@wxt-dev/module-vue'],\n\n  // Optional: Pass options to the module:\n  vue: {\n    vite: {\n      script: {\n        propsDestructure: true,\n      },\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/module-vue/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/module-vue\",\n  \"description\": \"WXT module to enable Vue support\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/module-vue\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/blob/main/packages/module-vue/README.md\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"vue\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"version\": \"1.0.3\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"default\": \"./dist/index.cjs\"\n      }\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"pnpm build && check\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"wxt\": \">=0.19.16\"\n  },\n  \"dependencies\": {\n    \"@vitejs/plugin-vue\": \"^5.2.3 || ^6.0.0\"\n  },\n  \"devDependencies\": {\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/module-vue/src/index.ts",
    "content": "import 'wxt';\nimport { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';\nimport vue, { Options as ViteOptions } from '@vitejs/plugin-vue';\n\nexport default defineWxtModule<VueModuleOptions>({\n  name: '@wxt-dev/module-vue',\n  configKey: 'vue',\n  setup(wxt, options) {\n    const { vite } = options ?? {};\n\n    // Add plugin & set sourcemap option\n    addViteConfig(wxt, ({ command }) => ({\n      // @ts-ignore: Ignore vite version issues\n      plugins: [vue(vite)],\n      build: {\n        // Fixes known issue: https://github.com/wxt-dev/wxt/pull/607\n        sourcemap: false,\n      },\n    }));\n\n    // Enable auto-imports in template files\n    wxt.hook('config:resolved', (wxt) => {\n      if (!wxt.config.imports) return;\n\n      wxt.config.imports.addons ??= {};\n      if (!Array.isArray(wxt.config.imports.addons)) {\n        wxt.config.imports.addons.vueTemplate = true;\n      } else {\n        wxt.logger.warn(\n          'Could not enable auto-imports in vue templates when using and array for imports.addons',\n        );\n      }\n    });\n\n    addImportPreset(wxt, 'vue');\n  },\n});\n\nexport interface VueModuleOptions {\n  vite?: ViteOptions;\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    vue?: VueModuleOptions;\n  }\n}\n"
  },
  {
    "path": "packages/module-vue/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/module-vue/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nconst entry = {\n  index: './src/index.ts',\n};\n\nexport default defineConfig([\n  {\n    entry,\n  },\n  {\n    entry,\n    format: 'cjs',\n  },\n]);\n"
  },
  {
    "path": "packages/runner/CHANGELOG.md",
    "content": "# Changelog\n\n## v0.1.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/runner-v0.1.1...runner-v0.1.2)\n\n### 🚀 Enhancements\n\n- **config:** Add browser path for Zen via Homebrew ([#1813](https://github.com/wxt-dev/wxt/pull/1813))\n\n### 🩹 Fixes\n\n- Improve Chrome path search ([#1823](https://github.com/wxt-dev/wxt/pull/1823))\n- **paths:** Add browser paths for Arc & Dia on macos ([#1814](https://github.com/wxt-dev/wxt/pull/1814))\n\n### 🏡 Chore\n\n- Fix auto-fixable `markdownlint` errors ([#1710](https://github.com/wxt-dev/wxt/pull/1710))\n- Manually fix markdownlint errors ([#1711](https://github.com/wxt-dev/wxt/pull/1711))\n- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Sam Carlton ([@ThatGuySam](https://github.com/ThatGuySam))\n- Alexander Kachkaev <alexander@kachkaev.ru>\n\n## v0.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/runner-v0.1.0...runner-v0.1.1)"
  },
  {
    "path": "packages/runner/README.md",
    "content": "# `@wxt-dev/runner`\n\nProgrammatically open a browser and install a web extension from a local directory.\n\n## Usage\n\n### With WXT\n\n> [!WARNING]\n> This package is intended to replace [`web-ext`](https://github.com/mozilla/web-ext) in the future, but it is not ready at the moment. Once it's ready for testing in WXT, more details will be added here.\n\n```ts\n// ~/wxt.runner.config.ts OR <project>/wxt.runner.config.ts\nimport { defineRunnerConfig } from 'wxt';\n\nexport default defineRunnerConfig({\n  // Options go here\n});\n```\n\n### JS API\n\n```ts\nimport { run } from '@wxt-dev/runner';\n\nawait run({\n  extensionDir: '/path/to/extension',\n  // Other options...\n});\n```\n\n## Features\n\n- Supports all Chromium and Firefox based browsers\n- Zero dependencies\n- One-line config for persisting data between launches\n\n## Requirements\n\n`@wxt-dev/runner` requires a JS runtime that implements the `WebSocket` standard:\n\n| JS Runtime | Version     |\n| ---------- | ----------- |\n| NodeJS     | &ge; 22.4.0 |\n| Bun        | &ge; 1.2.0  |\n\nYou also need to have a specific version of the browser installed that supports the latest features so extensions can be loaded:\n\n| Browser  | Version  |\n| -------- | -------- |\n| Chromium | Unknown  |\n| Firefox  | &ge; 139 |\n\n## TODO\n\n- [x] Provide install functions to allow hooking into already running instances of Chrome/Firefox\n  - [ ] Try to setup E2E tests on Firefox with Puppeteer using this approach\n  - [ ] Try to setup E2E tests on Chrome with Puppeteer using this approach\n\n## Options\n\n### Target\n\nTo open a specific browser, use the `target` option:\n\n```ts\nimport { run } from '@wxt-dev/runner';\n\nawait run({\n  extensionDir: 'path/to/extension',\n  target: 'firefox',\n});\n```\n\nDefaults to opening `chrome`. You may see type-hints for a list of popular browsers, but you can enter any string you want here.\n\n### Data Persistence\n\nBrowsers block you from using your normal browser profiles when using the [BiDi and CDP protocols](#implementation-details) for security reasons.\n\nTo change how the new profile's data is saved between sessions, use the `dataPersistence` option:\n\n```ts\nimport { run } from '@wxt-dev/runner';\n\nawait run({\n  dataPersistence: 'user',\n});\n```\n\n- `\"none\"` (default): Use a brand new browser profile every time the browser is opened (stored in the system's tmp directory)\n- `\"project\"`: Create a new profile that is re-used for your current directory (by default stored in `.wxt-runner` or `.wxt/runner` for WXT projects)\n- `\"user\"`: Create a new profile that is re-used for all projects using `@wxt-dev/runner` (by default stored in `$HOME/.wxt-runner`)\n\nThese presets configure different flags for different operating systems when spawning the browser process.\n\nIf you want to customize your data persistence beyond what these presets define, [you can override the browser flags yourself](#arguments) to configure persistence.\n\n### Browser Binaries\n\n`@wxt-dev/runner` will look for browser binaries/executables in [a hard-coded list of paths](https://github.com/wxt-dev/wxt/blob/main/packages/runner/src/browser-paths.ts). It does not and will not explore your filesystem/`$PATH` to find where the browser is installed. That means there are times you will need to specify the path to a browser's binary on your system:\n\n- Your browser's path is non-standard or missing from the hard-coded list.\n- You want to use a specific version/release of the browser.\n- You're using a less popular browser and `@wxt-dev/runner` doesn't have hard-coded paths for it.\n\nTo do this, use the `browserBinaries` option and set the path to the browser's binary:\n\n```ts\nimport { run } from '@wxt-dev/runner';\n\nawait run({\n  extensionDir: 'path/to/extension',\n  browserBinaries: {\n    chrome: '/path/to/chrome',\n    firefox: '/path/to/firefox',\n  },\n});\n```\n\n### Arguments\n\nTo pass custom arguments to the browser on startup, use the `chromiumArgs` or `firefoxArgs` options:\n\n```ts\nimport { run } from '@wxt-dev/runner';\n\nawait run({\n  extensionDir: 'path/to/extension',\n  chromiumArgs: ['--window-size=1920,1080'],\n  firefoxArgs: ['--window-size', '1920,1080'],\n});\n```\n\n### Start URLs\n\nTo open specific URLs in tabs by default, you also use the `chromiumArgs` or `firefoxArgs` options.\n\nAny URLs passed as a CLI argument will be opened in the browser when it starts.\n\n```ts\nimport { run } from '@wxt-dev/runner';\n\nawait run({\n  extensionDir: 'path/to/extension',\n  chromiumArgs: ['https://example.com'],\n  firefoxArgs: ['https://example.com'],\n});\n```\n\n### Debugging\n\nTo see debug logs, set the `DEBUG` env var to `\"@wxt-dev/runner\"`. This will print the resolved config, commands used to spawn the browser, any messages sent on the browser's communication protocol, and more for you to debug.\n\n<details>\n<summary>Example debug output</summary>\n\n```plaintext\n@wxt-dev/runner:options User options: { extensionDir: 'demo-extension', target: undefined }\n@wxt-dev/runner:options Resolved options: {\n  browserBinary: '/usr/bin/chromium',\n  chromiumArgs: [\n    '--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',\n    '--disable-component-extensions-with-background-pages',\n    '--disable-background-networking',\n    '--disable-component-update',\n    '--disable-client-side-phishing-detection',\n    '--disable-sync',\n    '--metrics-recording-only',\n    '--disable-default-apps',\n    '--no-default-browser-check',\n    '--no-first-run',\n    '--disable-background-timer-throttling',\n    '--disable-ipc-flooding-protection',\n    '--password-store=basic',\n    '--use-mock-keychain',\n    '--force-fieldtrials=*BackgroundTracing/default/',\n    '--disable-hang-monitor',\n    '--disable-prompt-on-repost',\n    '--disable-domain-reliability',\n    '--propagate-iph-for-testing',\n    '--remote-debugging-port=0',\n    '--remote-debugging-pipe',\n    '--user-data-dir=/tmp/wxt-runner-pWXLO1',\n    '--enable-unsafe-extension-debugging'\n  ],\n  dataDir: '/tmp/wxt-runner-pWXLO1',\n  dataPersistence: 'none',\n  chromiumRemoteDebuggingPort: 0,\n  extensionDir: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension',\n  firefoxArgs: [\n    '--new-instance',\n    '--no-remote',\n    '--profile',\n    '/tmp/wxt-runner-pWXLO1',\n    '--remote-debugging-port=0',\n    'about:debugging#/runtime/this-firefox'\n  ],\n  firefoxRemoteDebuggingPort: 0,\n  target: 'chrome'\n}\n@wxt-dev/runner:chrome:stderr DevTools listening on ws://127.0.0.1:38397/devtools/browser/93dc4de5-64cb-4e0b-a9d3-7549527015f0\n@wxt-dev/runner:cdp Sending command: {\n  id: 1,\n  method: 'Extensions.loadUnpacked',\n  params: {\n    path: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension'\n  }\n}\n@wxt-dev/runner:cdp Received response: { id: 1, result: { id: 'hckhakegfgenefhikdcfkaaonnclljmf' } }\n```\n\n</details>\n\n## Implementation Details\n\nAll this package does is spawn a child process to open the browser with some default flags before using remote protocols to install the extension.\n\n### Firefox\n\nWe use the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi) to install the extension. This just involves connecting to a web socket and sending a few messages.\n\n### Chrome\n\nWe use the [CDP](https://chromedevtools.github.io/devtools-protocol/) with `--remote-debugging-pipe` and `--enable-unsafe-extension-debugging` to install the extension by sending a message via IO pipes 3 and 4.\n\nWe don't use Webdriver Bidi because it's not built into Chrome yet. It requires us instantiating a separate child process for `chromedriver`. This is slower and more difficult than just using the CDP built into Chrome.\n"
  },
  {
    "path": "packages/runner/demo-extension/background.js",
    "content": "console.log('Hello background!');\n"
  },
  {
    "path": "packages/runner/demo-extension/manifest.json",
    "content": "{\n  \"name\": \"Test\",\n  \"version\": \"1.0.0\",\n  \"manifest_version\": 3,\n  \"background\": {\n    \"service_worker\": \"background.js\",\n    \"scripts\": [\"background.js\"]\n  }\n}\n"
  },
  {
    "path": "packages/runner/dev.ts",
    "content": "//\n// USAGE:\n//   pnpm dev\n//   pnpm dev firefox-nightly\n//   pnpm dev <target>\n//\n\nimport { run } from './src';\n\n// Uncomment to enable debug logs\nprocess.env.DEBUG = '@wxt-dev/runner';\n\nawait run({\n  extensionDir: 'demo-extension',\n  target: process.argv[2],\n});\n"
  },
  {
    "path": "packages/runner/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/runner\",\n  \"description\": \"Launch Chrome and Firefox with a web extension installed\",\n  \"version\": \"0.1.2\",\n  \"type\": \"module\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/runner\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/tree/main/packages/runner#readme\",\n  \"keywords\": [\n    \"web-extension\",\n    \"chrome-extension\",\n    \"wxt\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"scripts\": {\n    \"check\": \"pnpm build && check\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"dev\": \"tsx --trace-warnings dev.ts\",\n    \"build\": \"buildc -- tsdown\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"devDependencies\": {\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"main\": \"dist/index.mjs\",\n  \"types\": \"dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "packages/runner/src/__tests__/install.test.ts",
    "content": "import { describe, it, expect, vi } from 'vitest';\nimport { mock } from 'vitest-mock-extended';\nimport { createCdpConnection, type CDPConnection } from '../cdp';\nimport { createBidiConnection, type BidiConnection } from '../bidi';\nimport type { ChildProcess } from 'node:child_process';\nimport { installChromium, installFirefox } from '../install';\n\nvi.mock('../cdp');\nconst createCdpConnectionMock = vi.mocked(createCdpConnection);\n\nvi.mock('../bidi');\nconst createBidiConnectionMock = vi.mocked(createBidiConnection);\n\ndescribe('Install', () => {\n  describe('Chromium', () => {\n    it('Should send the install command to the process', async () => {\n      const browserProcess = mock<ChildProcess>();\n      const connection = mock<CDPConnection>({\n        [Symbol.dispose]: vi.fn(),\n      });\n      const extensionDir = '/path/to/extension';\n      const expectedExtensionId = 'chromium-extension-id';\n\n      createCdpConnectionMock.mockReturnValue(connection);\n      connection.send.mockImplementation(async (method) => {\n        if (method === 'Extensions.loadUnpacked')\n          return { id: expectedExtensionId };\n        throw Error('Unknown method');\n      });\n\n      const res = await installChromium(browserProcess, extensionDir);\n\n      expect(createCdpConnectionMock).toBeCalledTimes(1);\n      expect(createCdpConnectionMock).toBeCalledWith(browserProcess);\n\n      expect(connection.send).toBeCalledTimes(1);\n      expect(connection.send).toBeCalledWith('Extensions.loadUnpacked', {\n        path: extensionDir,\n      });\n\n      expect(res).toEqual({ id: expectedExtensionId });\n    });\n  });\n\n  describe('Firefox', () => {\n    it('Should connect to the server, start a session, then install the extension', async () => {\n      const debuggerUrl = 'http://127.0.0.1:9222';\n      const extensionDir = '/path/to/extension';\n      const expectedExtensionId = 'firefox-extension-id';\n      const connection = mock<BidiConnection>({\n        [Symbol.dispose]: vi.fn(),\n      });\n\n      createBidiConnectionMock.mockResolvedValue(connection);\n      connection.send.mockImplementation(async (method) => {\n        if (method === 'session.new') return { sessionId: 'session-id' };\n        if (method === 'webExtension.install')\n          return { extension: expectedExtensionId };\n      });\n\n      const res = await installFirefox(debuggerUrl, extensionDir);\n\n      expect(createBidiConnectionMock).toBeCalledTimes(1);\n      expect(createBidiConnectionMock).toBeCalledWith(debuggerUrl);\n\n      expect(connection.send).toBeCalledTimes(2);\n      expect(connection.send).toBeCalledWith('session.new', {\n        capabilities: {},\n      });\n      expect(connection.send).toBeCalledWith('webExtension.install', {\n        extensionData: {\n          type: 'path',\n          path: extensionDir,\n        },\n      });\n\n      expect(res).toEqual({ extension: expectedExtensionId });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runner/src/__tests__/options.test.ts",
    "content": "import { describe, it, expect, vi, beforeAll } from 'vitest';\nimport { ResolvedRunOptions, resolveRunOptions } from '../options';\nimport { resolve, join } from 'node:path';\nimport { tmpdir, homedir } from 'node:os';\nimport { mkdir } from 'node:fs/promises';\n\nvi.mock('node:os', async () => {\n  const { vi } = await import('vitest');\n  const os: any = await vi.importActual('node:os');\n  const { join } = await import('node:path');\n  return {\n    ...os,\n    tmpdir: () => join(os.tmpdir(), 'tmpdir-mock'),\n    homedir: () => join(os.tmpdir(), 'homedir-mock'),\n  };\n});\n\ndescribe('Options', () => {\n  beforeAll(async () => {\n    // Make sure mock test directories exist\n    await mkdir(tmpdir(), { recursive: true });\n    await mkdir(homedir(), { recursive: true });\n  });\n\n  describe('extensionDir', () => {\n    it('should default to the current working directory', async () => {\n      const actual = await resolveRunOptions({});\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        extensionDir: process.cwd(),\n      });\n    });\n\n    it('should resolve relative to the current working directory', async () => {\n      const actual = await resolveRunOptions({\n        extensionDir: './path/to/extension',\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        extensionDir: resolve(process.cwd(), './path/to/extension'),\n      });\n    });\n  });\n\n  describe('target', () => {\n    it('should be \"chrome\" by default', async () => {\n      const actual = await resolveRunOptions({\n        extensionDir: 'path/to/extension',\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        target: 'chrome',\n      });\n    });\n\n    it('should be what is passed in', async () => {\n      const actual = await resolveRunOptions({\n        extensionDir: 'path/to/extension',\n        target: 'custom',\n        browserBinaries: {\n          custom: '/path/to/custom/browser',\n        },\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        target: 'custom',\n      });\n    });\n\n    it('should throw an error if the target binary could not be found', async () => {\n      const actual = resolveRunOptions({\n        extensionDir: 'path/to/extension',\n        target: 'custom',\n      });\n      await expect(actual).rejects.toThrow('Could not find \"custom\" binary.');\n    });\n  });\n\n  describe('browserBinary', () => {\n    it('should denormalize the browserBinary', async () => {\n      const path =\n        process.platform === 'win32'\n          ? 'C:/path/to/custom/browser.exe'\n          : '/path/to/custom/browser';\n      const expectedPath =\n        process.platform === 'win32'\n          ? 'C:\\\\path\\\\to\\\\custom\\\\browser.exe'\n          : path;\n      const actual = await resolveRunOptions({\n        extensionDir: 'path/to/extension',\n        target: 'custom',\n        browserBinaries: {\n          custom: path,\n        },\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        browserBinary: expectedPath,\n      });\n    });\n  });\n\n  describe('chromiumArgs', () => {\n    it('should log a warning when --user-data-dir is passed in', async () => {\n      const warnSpy = vi.spyOn(console, 'warn');\n      await resolveRunOptions({\n        chromiumArgs: ['--user-data-dir=some/custom/path'],\n      });\n      expect(warnSpy).toBeCalledTimes(1);\n      expect(warnSpy).toHaveBeenCalledWith(\n        expect.stringContaining(\n          'Custom Chromium --user-data-dir argument ignored',\n        ),\n      );\n    });\n\n    it('should log a warning when --remote-debugging-port is passed in', async () => {\n      const warnSpy = vi.spyOn(console, 'warn');\n      await resolveRunOptions({\n        chromiumArgs: ['--remote-debugging-port=9222'],\n      });\n      expect(warnSpy).toBeCalledTimes(1);\n      expect(warnSpy).toHaveBeenCalledWith(\n        expect.stringContaining(\n          'Custom Chromium --remote-debugging-port argument ignored',\n        ),\n      );\n    });\n\n    it('should combine default args with user provided ones', async () => {\n      const actual = await resolveRunOptions({\n        chromiumArgs: ['--window-size=1920,1080'],\n      });\n      expect(actual.chromiumArgs).toEqual([\n        // Defaults\n        '--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',\n        '--disable-component-extensions-with-background-pages',\n        '--disable-background-networking',\n        '--disable-component-update',\n        '--disable-client-side-phishing-detection',\n        '--disable-sync',\n        '--metrics-recording-only',\n        '--disable-default-apps',\n        '--no-default-browser-check',\n        '--no-first-run',\n        '--disable-background-timer-throttling',\n        '--disable-ipc-flooding-protection',\n        '--password-store=basic',\n        '--use-mock-keychain',\n        '--force-fieldtrials=*BackgroundTracing/default/',\n        '--disable-hang-monitor',\n        '--disable-prompt-on-repost',\n        '--disable-domain-reliability',\n        '--propagate-iph-for-testing',\n        // Debugging\n        expect.stringContaining('--remote-debugging-port='),\n        '--remote-debugging-pipe',\n        expect.stringContaining('--user-data-dir='), // See dataPersistence tests\n        '--enable-unsafe-extension-debugging',\n        // User provided\n        '--window-size=1920,1080',\n      ]);\n    });\n  });\n\n  describe('firefoxArgs', () => {\n    it('should log a warning when --profile is passed in', async () => {\n      const warnSpy = vi.spyOn(console, 'warn');\n      await resolveRunOptions({\n        firefoxArgs: ['--profile=some/custom/path'],\n      });\n      expect(warnSpy).toBeCalledTimes(1);\n      expect(warnSpy).toHaveBeenCalledWith(\n        expect.stringContaining('Custom Firefox --profile argument ignored'),\n      );\n    });\n\n    it('should log a warning when --remote-debugging-port is passed in', async () => {\n      const warnSpy = vi.spyOn(console, 'warn');\n      await resolveRunOptions({\n        firefoxArgs: ['--remote-debugging-port=9222'],\n      });\n      expect(warnSpy).toBeCalledTimes(1);\n      expect(warnSpy).toHaveBeenCalledWith(\n        expect.stringContaining(\n          'Custom Firefox --remote-debugging-port argument ignored',\n        ),\n      );\n    });\n\n    it('should combine default args with user provided ones', async () => {\n      const actual = await resolveRunOptions({\n        firefoxArgs: ['--window-size=1920,1080'],\n      });\n      expect(actual.firefoxArgs).toEqual([\n        // Defaults\n        '--new-instance',\n        '--no-remote',\n        '--profile',\n        expect.any(String), // See dataPersistence tests\n        expect.stringContaining('--remote-debugging-port='),\n        'about:debugging#/runtime/this-firefox',\n        // User provided\n        '--window-size=1920,1080',\n      ]);\n    });\n  });\n\n  describe('chromiumRemoteDebuggingPort', () => {\n    it('should default to 0', async () => {\n      const actual = await resolveRunOptions({});\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        chromiumRemoteDebuggingPort: 0,\n        chromiumArgs: expect.arrayContaining([`--remote-debugging-port=0`]),\n      });\n    });\n\n    it('should respect user provided port', async () => {\n      const actual = await resolveRunOptions({\n        chromiumRemoteDebuggingPort: 9222,\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        chromiumRemoteDebuggingPort: 9222,\n        chromiumArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),\n      });\n    });\n  });\n\n  describe('firefoxRemoteDebuggingPort', () => {\n    it('should default to 0', async () => {\n      const actual = await resolveRunOptions({});\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        firefoxRemoteDebuggingPort: 0,\n        firefoxArgs: expect.arrayContaining([`--remote-debugging-port=0`]),\n      });\n    });\n\n    it('should respect user provided port', async () => {\n      const actual = await resolveRunOptions({\n        firefoxRemoteDebuggingPort: 9222,\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        firefoxRemoteDebuggingPort: 9222,\n        firefoxArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),\n      });\n    });\n  });\n\n  describe('dataPersistence', () => {\n    it('should default to \"none\"', async () => {\n      const actual = await resolveRunOptions({});\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        dataPersistence: 'none',\n      });\n    });\n\n    it('should use a temporary directory when set to \"none\"', async () => {\n      const actual = await resolveRunOptions({\n        dataPersistence: 'none',\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        dataPersistence: 'none',\n        dataDir: expect.stringContaining(join(tmpdir(), 'wxt-runner-')),\n        chromiumArgs: expect.arrayContaining([\n          expect.stringContaining(\n            `--user-data-dir=${join(tmpdir(), 'wxt-runner-')}`,\n          ),\n        ]),\n        firefoxArgs: expect.arrayContaining([\n          expect.stringContaining(join(tmpdir(), 'wxt-runner-')),\n        ]),\n      });\n    });\n\n    it('should use a directory in the current working directory when set to \"project\"', async () => {\n      const actual = await resolveRunOptions({\n        dataPersistence: 'project',\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        dataPersistence: 'project',\n        dataDir: expect.stringContaining(join(process.cwd(), '.wxt-runner')),\n        chromiumArgs: expect.arrayContaining([\n          expect.stringContaining(\n            `--user-data-dir=${join(process.cwd(), '.wxt-runner')}`,\n          ),\n        ]),\n        firefoxArgs: expect.arrayContaining([\n          expect.stringContaining(join(process.cwd(), '.wxt-runner')),\n        ]),\n      });\n    });\n\n    it('should use a directory in the user\\'s home directory when set to \"user\"', async () => {\n      const actual = await resolveRunOptions({\n        dataPersistence: 'user',\n      });\n      expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({\n        dataPersistence: 'user',\n        dataDir: expect.stringContaining(join(homedir(), '.wxt-runner')),\n        chromiumArgs: expect.arrayContaining([\n          expect.stringContaining(\n            `--user-data-dir=${join(homedir(), '.wxt-runner')}`,\n          ),\n        ]),\n        firefoxArgs: expect.arrayContaining([\n          expect.stringContaining(join(homedir(), '.wxt-runner')),\n        ]),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runner/src/bidi.ts",
    "content": "import { openWebSocket } from './web-socket';\nimport { debug } from './debug';\n\nconst debugBidi = debug.scoped('bidi');\n\nexport interface BidiConnection extends Disposable {\n  send<T>(method: string, params: any, timeout?: number): Promise<T>;\n  close(): void;\n}\n\nexport async function createBidiConnection(\n  baseUrl: string,\n): Promise<BidiConnection> {\n  const url = new URL('/session', baseUrl);\n  debugBidi('Connecting to BiDi server @', url.href);\n\n  const webSocket = await openWebSocket(url.href);\n  debugBidi('Connected');\n\n  let requestId = 0;\n\n  return {\n    send(method, params, timeout = 10e3) {\n      const id = ++requestId;\n      const command = { id, method, params };\n      debugBidi('Sending command:', command);\n\n      return new Promise((resolve, reject) => {\n        const cleanup = () => {\n          webSocket.removeEventListener('message', onMessage);\n          webSocket.removeEventListener('error', onError);\n        };\n\n        setTimeout(() => {\n          cleanup();\n          reject(\n            new Error(\n              `Timed out after ${timeout}ms waiting for ${method} response`,\n            ),\n          );\n        }, timeout);\n\n        const onMessage = (event: MessageEvent) => {\n          const data = JSON.parse(event.data);\n          if (data.id === id) {\n            debugBidi('Received response:', data);\n            cleanup();\n            if (data.type === 'success') resolve(data.result);\n            else reject(Error(data.message, { cause: data }));\n          }\n        };\n        const onError = (error: any) => {\n          cleanup();\n          reject(new Error('Error sending request', { cause: error }));\n        };\n\n        webSocket.addEventListener('message', onMessage);\n        webSocket.addEventListener('error', onError);\n\n        webSocket.send(JSON.stringify(command));\n      });\n    },\n\n    close() {\n      debugBidi('Closing connection...');\n      webSocket.close();\n      debugBidi('Closed connection');\n    },\n    [Symbol.dispose]() {\n      debugBidi('Disposing connection...');\n      webSocket.close();\n      debugBidi('Disposed connection');\n    },\n  };\n}\n"
  },
  {
    "path": "packages/runner/src/browser-paths.ts",
    "content": "export type BrowserPlatform = 'windows' | 'mac' | 'linux';\n\nexport type KnownTarget =\n  | 'arc'\n  | 'chromium'\n  | 'chrome'\n  | 'chrome-beta'\n  | 'chrome-dev'\n  | 'chrome-canary'\n  | 'dia'\n  | 'edge'\n  | 'edge-beta'\n  | 'edge-dev'\n  | 'edge-canary'\n  | 'firefox'\n  | 'firefox-nightly'\n  | 'firefox-developer-edition'\n  | 'zen';\n\nexport const KNOWN_BROWSER_PATHS: Record<\n  KnownTarget,\n  Record<BrowserPlatform, string[]>\n> = {\n  // Chromium based targets\n\n  arc: {\n    mac: ['/Applications/Arc.app/Contents/MacOS/Arc'],\n    linux: [],\n    windows: [],\n  },\n  chromium: {\n    mac: [],\n    linux: [\n      // Arch\n      '/usr/bin/chromium',\n    ],\n    windows: [],\n  },\n  chrome: {\n    mac: [\n      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n      '/Applications/Chrome.app/Contents/MacOS/Google Chrome',\n    ],\n    linux: [],\n    windows: ['C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe'],\n  },\n  'chrome-beta': {\n    mac: [],\n    linux: [],\n    windows: [],\n  },\n  'chrome-canary': {\n    mac: [\n      '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',\n      '/Applications/Chrome Canary.app/Contents/MacOS/Google Chrome Canary',\n    ],\n    linux: [],\n    windows: [],\n  },\n  'chrome-dev': {\n    mac: [],\n    linux: [],\n    windows: [],\n  },\n  dia: {\n    mac: ['/Applications/Dia.app/Contents/MacOS/Dia'],\n    linux: [],\n    windows: [],\n  },\n  edge: {\n    mac: [],\n    linux: [],\n    windows: [],\n  },\n  'edge-beta': {\n    mac: [],\n    linux: [],\n    windows: [],\n  },\n  'edge-canary': {\n    mac: [],\n    linux: [],\n    windows: [],\n  },\n  'edge-dev': {\n    mac: [],\n    linux: [],\n    windows: [],\n  },\n\n  // Firefox based targets\n\n  firefox: {\n    mac: ['/Applications/Firefox.app/Contents/MacOS/firefox'],\n    linux: [\n      // Arch\n      '/usr/bin/firefox',\n    ],\n    windows: [],\n  },\n  'firefox-nightly': {\n    mac: ['/Applications/Firefox Nightly.app/Contents/MacOS/firefox'],\n    linux: [],\n    windows: ['C:\\\\Program Files\\\\Firefox Nightly\\\\firefox.exe'],\n  },\n  'firefox-developer-edition': {\n    mac: ['/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox'],\n    linux: [\n      // Arch\n      '/usr/bin/firefox-developer-edition',\n    ],\n    windows: [],\n  },\n  zen: {\n    mac: [\n      '/Applications/Zen Browser.app/Contents/MacOS/zen',\n      // Homebrew Cask\n      // https://github.com/Homebrew/homebrew-cask/blob/main/Casks/z/zen.rb#L23C13-L23C19\n      '/Applications/Zen.app/Contents/MacOS/zen',\n    ],\n    linux: [],\n    windows: [],\n  },\n};\n\n/**\n * When targeting a browser, this map contains the other targets to fall back on\n * when a binary could not be found for the primary target.\n */\nexport const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = {\n  chrome: [\n    'arc',\n    'chromium',\n    'chrome-canary',\n    'chrome-beta',\n    'chrome-dev',\n    'dia',\n    'edge',\n    'edge-canary',\n    'edge-beta',\n    'edge-dev',\n  ],\n  firefox: ['firefox-developer-edition', 'firefox-nightly', 'zen'],\n};\n"
  },
  {
    "path": "packages/runner/src/cdp.ts",
    "content": "import type { ChildProcess } from 'node:child_process';\nimport type { Readable, Writable } from 'node:stream';\nimport { debug } from './debug';\n\nconst debugCdp = debug.scoped('cdp');\n\nexport interface CDPConnection extends Disposable {\n  send<T>(method: string, params: any, timeout?: number): Promise<T>;\n  close(): void;\n}\n\nexport function createCdpConnection(\n  browserProcess: ChildProcess,\n): CDPConnection {\n  const inputStream = browserProcess.stdio[3] as Writable;\n  const outputStream = browserProcess.stdio[4] as Readable;\n\n  let requestId = 0;\n\n  return {\n    send(method, params, timeout = 10e3) {\n      const id = ++requestId;\n      const command = { id, method, params };\n      debugCdp('Sending command:', command);\n\n      return new Promise((resolve, reject) => {\n        const timer = setTimeout(() => {\n          reject(new Error(`CDP command timed out: ${method}`));\n        }, timeout);\n\n        const onData = (data: Buffer) => {\n          // Trim the trailing null character\n          const text = data.toString().slice(0, -1);\n          const res = JSON.parse(text);\n\n          if (res.id !== id) return;\n\n          debugCdp('Received response:', res);\n          clearTimeout(timer);\n          outputStream.removeListener('data', onData);\n\n          if ('error' in res) {\n            reject(new Error(res.error.message, { cause: res.error }));\n          } else {\n            resolve(res.result);\n          }\n        };\n\n        outputStream.addListener('data', onData);\n\n        inputStream.write(JSON.stringify(command) + '\\0');\n      });\n    },\n    close() {},\n    [Symbol.dispose]() {},\n  };\n}\n"
  },
  {
    "path": "packages/runner/src/debug.ts",
    "content": "export interface Debug {\n  (...args: any[]): void;\n  scoped: (scope: string) => Debug;\n}\n\nfunction createDebug(scopes: string[]): Debug {\n  const debug = (...args: any[]) => {\n    const scope = scopes.join(':');\n    if (\n      process.env.DEBUG === '1' ||\n      process.env.DEBUG === 'true' ||\n      scope.startsWith(process.env.DEBUG ?? '@NOT')\n    ) {\n      const params = scope ? [`\\x1b[31m${scope}\\x1b[0m`, ...args] : args;\n      console.log(...params);\n    }\n  };\n\n  debug.scoped = (scope: string) => createDebug([...scopes, scope]);\n\n  return debug;\n}\n\nexport const debug = createDebug(['@wxt-dev/runner']);\n"
  },
  {
    "path": "packages/runner/src/index.ts",
    "content": "export * from './run';\nexport * from './options';\nexport * from './install';\n"
  },
  {
    "path": "packages/runner/src/install.ts",
    "content": "import type { ChildProcess } from 'node:child_process';\nimport { createBidiConnection } from './bidi';\nimport { createCdpConnection } from './cdp';\n\n/**\n * Install an extension to an already running instance of Firefox.\n *\n * @param debuggerUrl The URL of the Firefox BiDi server (ex:\n *   `ws://127.0.0.1:45912`).\n * @param extensionDir Absolute path to the directory containing the extension\n *   to be installed.\n */\nexport async function installFirefox(\n  debuggerUrl: string,\n  extensionDir: string,\n): Promise<BidiWebExtensionInstallResponse> {\n  using bidi = await createBidiConnection(debuggerUrl);\n\n  // Start a session\n  await bidi.send<unknown>('session.new', { capabilities: {} });\n\n  // Install the extension\n  return await bidi.send<BidiWebExtensionInstallResponse>(\n    'webExtension.install',\n    {\n      extensionData: {\n        type: 'path',\n        path: extensionDir,\n      },\n    },\n  );\n}\n\nexport type BidiWebExtensionInstallResponse = {\n  extension: string;\n};\n\n/**\n * Given a child process of Chrome, install an extension. The process must be\n * started with the following flags:\n *\n * - `--remote-debugging-pipe`\n * - `--user-data-dir=...`\n * - `--enable-unsafe-extension-debugging`\n *\n * Otherwise it the CDP doesn't have permission to install extensions.\n */\nexport async function installChromium(\n  browserProcess: ChildProcess,\n  extensionDir: string,\n): Promise<CdpExtensionsLoadUnpackedResponse> {\n  using cdp = createCdpConnection(browserProcess);\n  return await cdp.send<CdpExtensionsLoadUnpackedResponse>(\n    'Extensions.loadUnpacked',\n    {\n      path: extensionDir,\n    },\n  );\n}\n\nexport type CdpExtensionsLoadUnpackedResponse = {\n  id: string;\n};\n"
  },
  {
    "path": "packages/runner/src/options.ts",
    "content": "import {\n  FALLBACK_TARGETS,\n  KNOWN_BROWSER_PATHS,\n  KnownTarget,\n  type BrowserPlatform,\n} from './browser-paths';\nimport { resolve, join } from 'node:path';\nimport { homedir, tmpdir } from 'node:os';\nimport { debug } from './debug';\nimport { mkdtemp, open } from 'node:fs/promises';\n\nconst debugOptions = debug.scoped('options');\n\nexport type UnknownTarget = string & {};\nexport type Target = KnownTarget | UnknownTarget;\n\nexport type RunOptions = {\n  /** Paths to binaries to use for each target. */\n  browserBinaries?: Record<string, string>;\n  /**\n   * Customize the arguments passed to the chromium binary. Conflicting\n   * arguments with required ones to install extensions are ignored.\n   */\n  chromiumArgs?: string[];\n  /**\n   * Control how data is persisted between launches. Either save data at a user\n   * level, project level, or don't persist data at all. Defaults to `project`.\n   */\n  dataPersistence?: 'user' | 'project' | 'none';\n  /**\n   * Customize where your profile's data is stored when using `dataPersistence:\n   * 'project'`. Can be absolute or relative to the current working directory.\n   */\n  projectDataDir?: string;\n  /**\n   * Customize the port Chrome's debugger is listening on. Defaults to a random\n   * open port.\n   */\n  chromiumRemoteDebuggingPort?: number;\n  /**\n   * Directory where the extension will be installed from. Should contain a\n   * `manifest.json` file. Can be relative to the current working directory.\n   * Defaults to the current working directory.\n   */\n  extensionDir?: string;\n  /**\n   * Customize the arguments passed to the firefox binary. Conflicting arguments\n   * with required ones to install extensions are ignored.\n   */\n  firefoxArgs?: string[];\n  /**\n   * Customize the port Firefox's debugger is listening on. Defaults to a random\n   * open port.\n   */\n  firefoxRemoteDebuggingPort?: number;\n  /**\n   * Specify the browser to open. Defaults to `\"chrome\"`, but you can pass any\n   * string.\n   */\n  target?: Target;\n};\n\nexport type ResolvedRunOptions = {\n  /** Absolute path to the browser binary. */\n  browserBinary: string;\n  chromiumArgs: string[];\n  chromiumRemoteDebuggingPort: number;\n  /** Absolute path to the directory where browser data will be stored. */\n  dataDir: string;\n  dataPersistence: 'user' | 'project' | 'none';\n  /** Absolute path to the extension directory. */\n  extensionDir: string;\n  firefoxArgs: string[];\n  firefoxRemoteDebuggingPort: number;\n  target: string;\n};\n\nexport async function resolveRunOptions(\n  options: RunOptions | undefined,\n): Promise<ResolvedRunOptions> {\n  debugOptions('User options:', options);\n\n  const target = options?.target || 'chrome';\n\n  const _browserBinary =\n    options?.browserBinaries?.[target] ?? (await findBrowserBinary(target));\n  if (!_browserBinary)\n    throw Error(\n      `Could not find \"${target}\" binary.\\n\\nIf it is installed in a custom location, you can specify the path with the browserPaths option.`,\n    );\n\n  // Denormalize the path so it uses the correct path separator for the OS\n  const browserBinary = resolve(_browserBinary);\n\n  const chromiumRemoteDebuggingPort = options?.chromiumRemoteDebuggingPort ?? 0;\n  const firefoxRemoteDebuggingPort = options?.firefoxRemoteDebuggingPort ?? 0;\n  const dataPersistence = options?.dataPersistence ?? 'none';\n  const dataDir =\n    dataPersistence === 'user'\n      ? join(homedir(), '.wxt-runner', target)\n      : dataPersistence === 'project'\n        ? options?.projectDataDir\n          ? resolve(options.projectDataDir)\n          : resolve('.wxt-runner', target)\n        : dataPersistence === 'none'\n          ? await mkdtemp(join(tmpdir(), 'wxt-runner-'))\n          : resolve(dataPersistence);\n\n  const resolved: ResolvedRunOptions = {\n    browserBinary,\n    chromiumArgs: resolveChromiumArgs(\n      options?.chromiumArgs,\n      chromiumRemoteDebuggingPort,\n      dataDir,\n    ),\n    dataDir,\n    dataPersistence,\n    chromiumRemoteDebuggingPort,\n    extensionDir: resolve(options?.extensionDir ?? '.'),\n    firefoxArgs: resolveFirefoxArgs(\n      options?.firefoxArgs,\n      firefoxRemoteDebuggingPort,\n      dataDir,\n    ),\n    firefoxRemoteDebuggingPort,\n    target,\n  };\n  debugOptions('Resolved options:', resolved);\n  return resolved;\n}\n\nasync function findBrowserBinary(target: string): Promise<string | undefined> {\n  const targets = new Set<KnownTarget>([target as KnownTarget]);\n  FALLBACK_TARGETS[target as KnownTarget]?.forEach((fallback) =>\n    targets.add(fallback),\n  );\n  const platform = getPlatform();\n\n  for (const target of targets) {\n    const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];\n    for (const path of potentialPaths) {\n      if (await pathExists(path)) return path;\n    }\n  }\n}\n\nfunction getPlatform(): BrowserPlatform {\n  switch (process.platform) {\n    case 'win32':\n      return 'windows';\n    case 'darwin':\n      return 'mac';\n    default:\n      return 'linux';\n  }\n}\n\nfunction resolveChromiumArgs(\n  userArgs: string[] | undefined,\n  chromiumRemoteDebuggingPort: ResolvedRunOptions['chromiumRemoteDebuggingPort'],\n  dataDir: string,\n): string[] {\n  return deduplicateArgs(\n    [\n      // Limit features to improve performance\n      ...CHROME_LAUNCHER_DEFAULT_FLAGS,\n      // Enable debugging\n      `--remote-debugging-port=${chromiumRemoteDebuggingPort}`,\n      // Required for installing extensions\n      `--remote-debugging-pipe`,\n      `--user-data-dir=${dataDir}`,\n      `--enable-unsafe-extension-debugging`,\n    ],\n    userArgs,\n    {\n      '--remote-debugging-port':\n        '\\x1b[1m\\x1b[33mCustom Chromium --remote-debugging-port argument ignored.\\x1b[0m Use \\x1b[36mchromiumRemoteDebuggingPort\\x1b[0m option instead.',\n      '--user-data-dir':\n        '\\x1b[1m\\x1b[33mCustom Chromium --user-data-dir argument ignored.\\x1b[0m Use \\x1b[36mdataPersistence\\x1b[0m option instead.',\n    },\n  );\n}\n\nfunction resolveFirefoxArgs(\n  userArgs: string[] | undefined,\n  firefoxRemoteDebuggingPort: ResolvedRunOptions['firefoxRemoteDebuggingPort'],\n  dataDir: string,\n): string[] {\n  return deduplicateArgs(\n    [\n      // Allows opening multiple instances of Firefox at the same time\n      `--new-instance`,\n      `--no-remote`,\n      `--profile`,\n      dataDir,\n      // Required for installing extensions\n      `--remote-debugging-port=${firefoxRemoteDebuggingPort}`,\n      // Default URL to start with\n      `about:debugging#/runtime/this-firefox`,\n    ],\n    userArgs,\n    {\n      '--remote-debugging-port':\n        '\\x1b[1m\\x1b[33mCustom Firefox --remote-debugging-port argument ignored.\\x1b[0m Use \\x1b[36mfirefoxDebuggerPort\\x1b[0m option instead.',\n      '--profile':\n        '\\x1b[1m\\x1b[33mCustom Firefox --profile argument ignored.\\x1b[0m Use \\x1b[36mdataPersistence\\x1b[0m option instead.',\n    },\n  );\n}\n\nfunction deduplicateArgs(\n  requiredArgs: string[],\n  userArgs: string[] | undefined,\n  warnings: Record<string, string>,\n): string[] {\n  const getKey = (arg: string) => {\n    return arg.startsWith('--') ? arg.split('=')[0] : arg;\n  };\n  const alreadyAdded = new Set<string>(requiredArgs.map(getKey));\n\n  const args = [...requiredArgs];\n  userArgs?.forEach((arg) => {\n    const key = getKey(arg);\n    if (alreadyAdded.has(key)) {\n      if (warnings[key]) console.warn(`[@wxt-dev/runner] ${warnings[key]}`);\n    } else {\n      alreadyAdded.add(key);\n      args.push(arg);\n    }\n  });\n\n  return args;\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n  try {\n    await open(path, 'r');\n    return true;\n  } catch (err) {\n    // @ts-expect-error: Unknown error type\n    if (err?.code === 'ENOENT') return false;\n    throw err;\n  }\n}\n\n/**\n * Copied from\n * https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts with\n * some flags commented out. Run tests after updating to compare.\n */\nconst CHROME_LAUNCHER_DEFAULT_FLAGS = [\n  '--disable-features=' +\n    [\n      // Disable built-in Google Translate service\n      'Translate',\n      // Disable the Chrome Optimization Guide background networking\n      'OptimizationHints',\n      //  Disable the Chrome Media Router (cast target discovery) background networking\n      'MediaRouter',\n      /// Avoid the startup dialog for _Do you want the application “Chromium.app” to accept incoming network connections?_. This is a sub-component of the MediaRouter.\n      'DialMediaRouteProvider',\n      // Disable the feature of: Calculate window occlusion on Windows will be used in the future to throttle and potentially unload foreground tabs in occluded windows.\n      'CalculateNativeWinOcclusion',\n      // Disables the Discover feed on NTP\n      'InterestFeedContentSuggestions',\n      // Don't update the CT lists\n      'CertificateTransparencyComponentUpdater',\n      // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.\n      'AutofillServerCommunication',\n      // Disables \"Enhanced ad privacy in Chrome\" dialog (though as of 2024-03-20 it shouldn't show up if the profile has no stored country).\n      'PrivacySandboxSettings4',\n    ].join(','),\n\n  // Disable some extensions that aren't affected by --disable-extensions\n  '--disable-component-extensions-with-background-pages',\n  // Disable various background network services, including extension updating,\n  //   safe browsing service, upgrade detector, translate, UMA\n  '--disable-background-networking',\n  // Don't update the browser 'components' listed at chrome://components/\n  '--disable-component-update',\n  // Disables client-side phishing detection.\n  '--disable-client-side-phishing-detection',\n  // Disable syncing to a Google account\n  '--disable-sync',\n  // Disable reporting to UMA, but allows for collection\n  '--metrics-recording-only',\n  // Disable installation of default apps on first run\n  '--disable-default-apps',\n  // Disable the default browser check, do not prompt to set it as such\n  '--no-default-browser-check',\n  // Skip first run wizards\n  '--no-first-run',\n  // Disable task throttling of timer tasks from background pages.\n  '--disable-background-timer-throttling',\n  // Disable the default throttling of IPC between renderer & browser processes.\n  '--disable-ipc-flooding-protection',\n  // Avoid potential instability of using Gnome Keyring or KDE wallet. crbug.com/571003 crbug.com/991424\n  '--password-store=basic',\n  // Use mock keychain on Mac to prevent blocking permissions dialogs\n  '--use-mock-keychain',\n  // Disable background tracing (aka slow reports & deep reports) to avoid 'Tracing already started'\n  '--force-fieldtrials=*BackgroundTracing/default/',\n\n  // Suppresses hang monitor dialogs in renderer processes. This flag may allow slow unload handlers on a page to prevent the tab from closing.\n  '--disable-hang-monitor',\n  // Reloading a page that came from a POST normally prompts the user.\n  '--disable-prompt-on-repost',\n  // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.\n  '--disable-domain-reliability',\n  // Disable the in-product Help (IPH) system.\n  '--propagate-iph-for-testing',\n];\n"
  },
  {
    "path": "packages/runner/src/promises.ts",
    "content": "export function promiseWithResolvers<T>(): {\n  promise: Promise<T>;\n  resolve: (value: T) => void;\n  reject: (error: unknown) => void;\n} {\n  let resolve: (value: T) => void;\n  let reject: (error: unknown) => void;\n\n  const promise = new Promise<T>((res, rej) => {\n    resolve = res;\n    reject = rej;\n  });\n\n  return {\n    promise,\n    resolve: resolve!,\n    reject: reject!,\n  };\n}\n"
  },
  {
    "path": "packages/runner/src/run.ts",
    "content": "import { debug } from './debug';\nimport {\n  resolveRunOptions,\n  type ResolvedRunOptions,\n  type RunOptions,\n} from './options';\nimport { spawn } from 'node:child_process';\nimport { installChromium, installFirefox } from './install';\nimport { promiseWithResolvers } from './promises';\n\nconst debugFirefox = debug.scoped('firefox');\nconst debugChrome = debug.scoped('chrome');\n\nexport interface Runner {\n  stop(): void;\n}\n\nexport async function run(options: RunOptions): Promise<Runner> {\n  const resolvedOptions = await resolveRunOptions(options);\n\n  if (\n    resolvedOptions.target.includes('firefox') ||\n    resolvedOptions.target.includes('zen')\n  ) {\n    return runFirefox(resolvedOptions);\n  } else {\n    return runChromium(resolvedOptions);\n  }\n}\n\nasync function runFirefox(options: ResolvedRunOptions): Promise<Runner> {\n  const urlRes = promiseWithResolvers<string>();\n  const urlTimeout = setTimeout(() => {\n    urlRes.reject(Error('Timed out after 10s waiting for the browser to open'));\n  }, 10e3);\n\n  // Firefox notifies the user if an instance is already running, so we don't add any logs for it.\n\n  const browserProcess = spawn(\n    `\"${options.browserBinary}\"`,\n    options.firefoxArgs,\n    {\n      stdio: ['ignore', 'pipe', 'pipe'],\n      shell: true,\n    },\n  );\n  const debugFirefoxStderr = debugFirefox.scoped('stderr');\n  browserProcess.stderr.on('data', (data: string) => {\n    const message = data.toString().trim();\n    debugFirefoxStderr(message);\n\n    if (message.startsWith('WebDriver BiDi listening on ws://')) {\n      clearTimeout(urlTimeout);\n      urlRes.resolve(message.slice(28));\n    }\n  });\n  const debugFirefoxStdout = debugFirefox.scoped('stdout');\n  browserProcess.stdout.on('data', (data: string) => {\n    const message = data.toString().trim();\n    debugFirefoxStdout(message);\n  });\n\n  const baseUrl = await urlRes.promise;\n  await installFirefox(baseUrl, options.extensionDir);\n\n  return {\n    stop() {\n      browserProcess.kill('SIGINT');\n    },\n  };\n}\n\nasync function runChromium(options: ResolvedRunOptions): Promise<Runner> {\n  const browserProcess = spawn(\n    `\"${options.browserBinary}\"`,\n    options.chromiumArgs,\n    {\n      stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],\n      shell: true,\n    },\n  );\n\n  const opened = promiseWithResolvers<void>();\n  const openedTimeout = setTimeout(() => {\n    opened.reject(Error('Timed out after 10s waiting for browser to open.'));\n  }, 10e3);\n\n  const debugChromeStderr = debugChrome.scoped('stderr');\n  browserProcess.stderr!.on('data', (data: string) => {\n    const message = data.toString().trim();\n    debugChromeStderr(message);\n\n    // This message signifies Chrome started up correctly.\n    if (message.startsWith('DevTools listening on')) {\n      clearTimeout(openedTimeout);\n      opened.resolve();\n    }\n  });\n  const debugChromeStdout = debugChrome.scoped('stdout');\n  browserProcess.stdout!.on('data', (data: string) => {\n    const message = data.toString().trim();\n    debugChromeStdout(message);\n\n    // This message signifies Chrome was already open, and thus we couldn't open the required new instance.\n    if (message === 'Opening in existing browser session.') {\n      clearTimeout(openedTimeout);\n      opened.reject(\n        Error(\n          'An instance of the browser is already running. Close it and try again.',\n        ),\n      );\n    }\n  });\n\n  // Wait for the browser to open before proceeding.\n  await opened.promise;\n\n  await installChromium(browserProcess, options.extensionDir);\n\n  return {\n    stop() {\n      browserProcess.kill('SIGINT');\n    },\n  };\n}\n"
  },
  {
    "path": "packages/runner/src/web-socket.ts",
    "content": "export function openWebSocket(url: string): Promise<WebSocket> {\n  if (typeof WebSocket === 'undefined') {\n    throw new Error(\n      'To open Firefox, your JS runtime must support the standard WebSocket API (NodeJS >=22.4.0, Bun, etc).',\n    );\n  }\n\n  return new Promise<WebSocket>((resolve, reject) => {\n    const webSocket = new WebSocket(url);\n\n    const cleanup = () => {\n      webSocket.removeEventListener('open', onOpen);\n      webSocket.removeEventListener('error', onError);\n      webSocket.removeEventListener('close', onClose);\n    };\n    const onOpen = async () => {\n      cleanup();\n      resolve(webSocket);\n    };\n    const onClose = (event: CloseEvent) => {\n      cleanup();\n      reject(\n        new Error(\n          `Connection closed: code=${event.code}, reason=${event.reason}`,\n        ),\n      );\n    };\n    const onError = (error: any) => {\n      cleanup();\n      reject(new Error('Error connecting to WebSocket', { cause: error }));\n    };\n\n    webSocket.addEventListener('open', onOpen);\n    webSocket.addEventListener('error', onError);\n    webSocket.addEventListener('close', onClose);\n  });\n}\n"
  },
  {
    "path": "packages/runner/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {}\n}\n"
  },
  {
    "path": "packages/runner/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config';\n\nexport default defineProject({\n  test: {\n    mockReset: true,\n    restoreMocks: true,\n  },\n});\n"
  },
  {
    "path": "packages/storage/CHANGELOG.md",
    "content": "# Changelog\n\n## v1.2.8\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.7...storage-v1.2.8)\n\n### 🩹 Fixes\n\n- Correctly update version metadata when setting a value for the first time ([#2139](https://github.com/wxt-dev/wxt/pull/2139))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v1.2.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.6...storage-v1.2.7)\n\n### 🩹 Fixes\n\n- Add another `defineItem` signature when `init` function is passed ([#1909](https://github.com/wxt-dev/wxt/pull/1909))\n- **storage:** Set version number on init ([#1996](https://github.com/wxt-dev/wxt/pull/1996))\n\n### 💅 Refactors\n\n- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))\n\n### 📖 Documentation\n\n- Rename keys name of getMetas() to be proper ([#2105](https://github.com/wxt-dev/wxt/pull/2105))\n\n### 🏡 Chore\n\n- Fix type errors after `chrome` type upgrades ([6036c6e8](https://github.com/wxt-dev/wxt/commit/6036c6e8))\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n\n### ❤️ Contributors\n\n- Willow ([@42willow](https://github.com/42willow))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Dan McGee <dpmcgee@gmail.com>\n\n## v1.2.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.5...storage-v1.2.6)\n\n## v1.2.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.4...storage-v1.2.5)\n\n## v1.2.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.3...storage-v1.2.4)\n\n## v1.2.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.2...storage-v1.2.3)\n\n## v1.2.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.1...storage-v1.2.2)\n\n## v1.2.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.0...storage-v1.2.1)\n\n## v1.2.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.1.1...storage-v1.2.0)\n\n### 🚀 Enhancements\n\n- **storage:** Add `onMigrationComplete` callback ([#1514](https://github.com/wxt-dev/wxt/pull/1514))\n- **storage:** Add `debug` option to enable migration logs ([#1513](https://github.com/wxt-dev/wxt/pull/1513))\n\n### 🩹 Fixes\n\n- Fix typescript error on `defineItem` fallback ([#1601](https://github.com/wxt-dev/wxt/pull/1601))\n- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))\n\n### 🏡 Chore\n\n- **deps:** Update all dependencies ([#1568](https://github.com/wxt-dev/wxt/pull/1568))\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))\n- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Anh71me ([@iyume](https://github.com/iyume))\n- Ergou <ma2808203259@hotmail.com>\n\n## v1.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.1.0...storage-v1.1.1)\n\n### 🩹 Fixes\n\n- Return early if no migration is needed ([#1502](https://github.com/wxt-dev/wxt/pull/1502))\n\n### 🏡 Chore\n\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- **deps:** Upgrade to Vite 6 and related dependencies ([#1496](https://github.com/wxt-dev/wxt/pull/1496))\n\n### ❤️ Contributors\n\n- ergou ([@RayGuo-ergou](https://github.com/RayGuo-ergou))\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Okinea Dev ([@okineadev](http://github.com/okineadev))\n\n## v1.1.0\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.1...storage-v1.1.0)\n\n### 🚀 Enhancements\n\n- Add `storage.clear` ([#1368](https://github.com/wxt-dev/wxt/pull/1368))\n\n### 📖 Documentation\n\n- Update link ([654a54a](https://github.com/wxt-dev/wxt/commit/654a54a))\n\n### ❤️ Contributors\n\n- Chengxi ([@chengxilo](http://github.com/chengxilo))\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.0...storage-v1.0.1)\n\n### 🩹 Fixes\n\n- Use `browser` for mv2 storage ([#1200](https://github.com/wxt-dev/wxt/pull/1200))\n\n### 📖 Documentation\n\n- Cleanup changelog ([f5b7f7e](https://github.com/wxt-dev/wxt/commit/f5b7f7e))\n\n### 🏡 Chore\n\n- Init changelog for storage package ([6fc227b](https://github.com/wxt-dev/wxt/commit/6fc227b))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v1.0.0\n\nExtracted `wxt/storage` into it's own package, `@wxt-dev/storage`!\n\nIt's still shipped inside WXT and accessible via `wxt/storage`, but now:\n\n- Non-WXT projects can use the storage wrapper.\n- We can make breaking changes to the API separately.\n\n[Read the docs](https://wxt.dev/storage.html) for more details.\n\n> This is apart of the v1.0 initiative for WXT."
  },
  {
    "path": "packages/storage/README.md",
    "content": "# WXT Storage\n\n[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/storage/CHANGELOG.md) &bull; [Docs](https://wxt.dev/storage.html)\n\nA simplified wrapper around the extension storage APIs.\n\n## Installation\n\n### With WXT\n\nThis module is built-in to WXT, so you don't need to install anything.\n\n```ts\nimport { storage } from 'wxt/storage';\n```\n\nIf you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!\n\n### Without WXT\n\nInstall the NPM package:\n\n```sh\nnpm i @wxt-dev/storage\npnpm add @wxt-dev/storage\nyarn add @wxt-dev/storage\nbun add @wxt-dev/storage\n```\n\n```ts\nimport { storage } from '@wxt-dev/storage';\n```\n\n## Usage\n\nRead full docs on the [documentation website](https://wxt.dev/storage.html).\n"
  },
  {
    "path": "packages/storage/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/storage\",\n  \"description\": \"Web extension storage API provided by WXT, supports all browsers.\",\n  \"version\": \"1.2.8\",\n  \"type\": \"module\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/storage\"\n  },\n  \"homepage\": \"https://wxt.dev/storage.html\",\n  \"keywords\": [\n    \"wxt\",\n    \"storage\",\n    \"extension\",\n    \"addon\",\n    \"chrome\",\n    \"firefox\",\n    \"edge\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"buildc --deps-only -- check\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"dependencies\": {\n    \"@wxt-dev/browser\": \"workspace:^\",\n    \"async-mutex\": \"^0.5.0\",\n    \"dequal\": \"^2.0.3\"\n  },\n  \"devDependencies\": {\n    \"@webext-core/fake-browser\": \"^1.3.4\",\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"import\": \"./dist/index.mjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "packages/storage/src/__tests__/index.test.ts",
    "content": "import { fakeBrowser } from '@webext-core/fake-browser';\nimport { browser } from '@wxt-dev/browser';\nimport { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';\nimport { MigrationError, type WxtStorageItem, storage } from '../index';\n\n/**\n * This works because fakeBrowser is synchronous, and is will finish any number\n * of chained calls within a single tick of the event loop, ie: a timeout of 0.\n */\nasync function waitForMigrations() {\n  return new Promise((res) => setTimeout(res));\n}\n\n/**\n * Same as `waitForMigrations`, the fake browser being synchronous means the\n * `init` logic is finished by the next task in the event queue.\n */\nasync function waitForInit() {\n  return new Promise((res) => setTimeout(res));\n}\n\ndescribe('Storage Utils', () => {\n  beforeEach(() => {\n    fakeBrowser.reset();\n    storage.unwatch();\n\n    // Setup a spy to check for excessive calls\n    fakeBrowser.storage.local.set = vi.spyOn(fakeBrowser.storage.local, 'set');\n  });\n\n  describe.each(['local', 'sync', 'managed', 'session'] as const)(\n    'storage - %s',\n    (storageArea) => {\n      describe('getItem', () => {\n        it('should return the value from the correct storage area', async () => {\n          const expected = 123;\n          await fakeBrowser.storage[storageArea].set({ count: expected });\n\n          const actual = await storage.getItem(`${storageArea}:count`);\n\n          expect(actual).toBe(expected);\n        });\n\n        it('should return the value if multiple : are used in the key', async () => {\n          const expected = 'value';\n          await fakeBrowser.storage[storageArea].set({ 'some:key': expected });\n\n          const actual = await storage.getItem(`${storageArea}:some:key`);\n\n          expect(actual).toBe(expected);\n        });\n\n        it(\"should return null if the value doesn't exist\", async () => {\n          const actual = await storage.getItem(`${storageArea}:count`);\n\n          expect(actual).toBeNull();\n        });\n\n        it('should return the default value if passed in options', async () => {\n          const expected = 0;\n          const actual = await storage.getItem(`${storageArea}:count`, {\n            defaultValue: expected,\n          });\n\n          expect(actual).toBe(expected);\n        });\n      });\n\n      describe('getItems', () => {\n        it('should get values from multiple storage keys', async () => {\n          const item1 = {\n            key: `${storageArea}:one`,\n            expectedValue: 1,\n          } as const;\n          const item2 = {\n            key: `${storageArea}:two`,\n            expectedValue: null,\n          } as const;\n\n          await fakeBrowser.storage[storageArea].set({\n            one: item1.expectedValue,\n          });\n\n          const actual = await storage.getItems([item1.key, item2.key]);\n\n          expect(actual).toEqual([\n            { key: item1.key, value: item1.expectedValue },\n            { key: item2.key, value: item2.expectedValue },\n          ]);\n        });\n\n        it('should get values from multiple storage items', async () => {\n          const item1 = storage.defineItem(`${storageArea}:one`);\n          const expectedValue1 = 1;\n          const item2 = storage.defineItem(`${storageArea}:two`);\n          const expectedValue2 = null;\n\n          await fakeBrowser.storage[storageArea].set({\n            one: expectedValue1,\n          });\n\n          const actual = await storage.getItems([item1, item2]);\n\n          expect(actual).toEqual([\n            { key: item1.key, value: expectedValue1 },\n            { key: item2.key, value: expectedValue2 },\n          ]);\n        });\n\n        it('should get values for a combination of different input types', async () => {\n          const key1 = `${storageArea}:one` as const;\n          const expectedValue1 = 1;\n          const item2 = storage.defineItem<number>(`${storageArea}:two`);\n          const expectedValue2 = 2;\n\n          await fakeBrowser.storage[storageArea].set({\n            one: expectedValue1,\n            two: expectedValue2,\n          });\n\n          const actual = await storage.getItems([key1, item2]);\n\n          expect(actual).toEqual([\n            { key: key1, value: expectedValue1 },\n            { key: item2.key, value: expectedValue2 },\n          ]);\n        });\n\n        it('should return fallback values for keys when provided', async () => {\n          const key1 = `${storageArea}:one` as const;\n          const expectedValue1 = null;\n          const key2 = `${storageArea}:two` as const;\n          const fallback2 = 2;\n          const expectedValue2 = fallback2;\n\n          const actual = await storage.getItems([\n            key1,\n            { key: key2, options: { fallback: fallback2 } },\n          ]);\n\n          expect(actual).toEqual([\n            { key: key1, value: expectedValue1 },\n            { key: key2, value: expectedValue2 },\n          ]);\n        });\n\n        it('should return fallback values for items when provided', async () => {\n          const item1 = storage.defineItem<number>(`${storageArea}:one`);\n          const expectedValue1 = null;\n          const item2 = storage.defineItem(`${storageArea}:two`, {\n            fallback: 2,\n          });\n          const expectedValue2 = item2.fallback;\n\n          const actual = await storage.getItems([item1, item2]);\n\n          expect(actual).toEqual([\n            { key: item1.key, value: expectedValue1 },\n            { key: item2.key, value: expectedValue2 },\n          ]);\n        });\n      });\n\n      describe('getMeta', () => {\n        it('should return item metadata from key+$', async () => {\n          const expected = { v: 1 };\n          await fakeBrowser.storage[storageArea].set({ count$: expected });\n\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual(expected);\n        });\n\n        it('should return an empty object if missing', async () => {\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual({});\n        });\n      });\n\n      describe('setItem', () => {\n        it('should set the value in the correct storage area', async () => {\n          const key = `${storageArea}:count` as const;\n          const value = 321;\n\n          await storage.setItem(key, value);\n        });\n\n        it.each([undefined, null])(\n          'should remove the item from storage when setting the value to %s',\n          async (value) => {\n            await fakeBrowser.storage[storageArea].set({ count: 345 });\n            await storage.setItem(`${storageArea}:count`, value as null);\n\n            // For some reason storage sets the value to \"null\" instead of deleting it. So using\n            // fakeBrowser during the expect fails. Using storage works. I've confirmed that this\n            // doesn't happen in a real extension environment.\n            expect(await storage.getItem(`${storageArea}:count`)).toBeNull();\n          },\n        );\n      });\n\n      describe('setItems', () => {\n        it('should set multiple items in storage', async () => {\n          const expected = [\n            { key: `${storageArea}:count` as const, value: 234 },\n            { key: `${storageArea}:installDate` as const, value: null },\n          ];\n          await fakeBrowser.storage[storageArea].set({\n            count: 123,\n            installDate: 321,\n          });\n\n          await storage.setItems(expected);\n          const actual = await storage.getItems(\n            expected.map((item) => item.key),\n          );\n\n          expect(actual).toHaveLength(2);\n          expected.forEach((item) => {\n            expect(actual).toContainEqual(item);\n          });\n        });\n      });\n\n      describe('setMeta', () => {\n        it('should set metadata at key+$', async () => {\n          const existing = { v: 1 };\n          await browser.storage[storageArea].set({ count$: existing });\n          const newValues = {\n            date: Date.now(),\n          };\n          const expected = { ...existing, ...newValues };\n\n          await storage.setMeta(`${storageArea}:count`, newValues);\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual(expected);\n        });\n\n        it.each([undefined, null])(\n          'should remove any properties set to %s',\n          async (version) => {\n            const existing = { v: 1 };\n            await browser.storage[storageArea].set({ count$: existing });\n            const expected = {};\n\n            await storage.setMeta(`${storageArea}:count`, { v: version });\n            const actual = await storage.getMeta(`${storageArea}:count`);\n\n            expect(actual).toEqual(expected);\n          },\n        );\n      });\n\n      describe('setMetas', () => {\n        it('should set key metadata correctly', async () => {\n          const key1 = `${storageArea}:one` as const;\n          const initialMeta1 = {};\n          const setMeta1 = { v: 1 };\n          const expectedMeta1 = setMeta1;\n\n          const key2 = `${storageArea}:two` as const;\n          const initialMeta2 = { v: 1 };\n          const setMeta2 = { v: 2 };\n          const expectedMeta2 = setMeta2;\n\n          const key3 = `${storageArea}:three` as const;\n          const initialMeta3 = { v: 1 };\n          const setMeta3 = { d: Date.now() };\n          const expectedMeta3 = { ...initialMeta3, ...setMeta3 };\n\n          await fakeBrowser.storage[storageArea].set({\n            one$: initialMeta1,\n            two$: initialMeta2,\n            three$: initialMeta3,\n          });\n\n          await storage.setMetas([\n            { key: key1, meta: setMeta1 },\n            { key: key2, meta: setMeta2 },\n            { key: key3, meta: setMeta3 },\n          ]);\n\n          expect(await storage.getMeta(key1)).toEqual(expectedMeta1);\n          expect(await storage.getMeta(key2)).toEqual(expectedMeta2);\n          expect(await storage.getMeta(key3)).toEqual(expectedMeta3);\n        });\n\n        it('should set item metadata correctly', async () => {\n          const item1 = storage.defineItem(`${storageArea}:one`);\n          const initialMeta1 = {};\n          const setMeta1 = { v: 1 };\n          const expectedMeta1 = setMeta1;\n\n          const item2 = storage.defineItem(`${storageArea}:two`);\n          const initialMeta2 = { v: 1 };\n          const setMeta2 = { v: 2 };\n          const expectedMeta2 = setMeta2;\n\n          const item3 = storage.defineItem(`${storageArea}:three`);\n          const initialMeta3 = { v: 1 };\n          const setMeta3 = { d: Date.now() };\n          const expectedMeta3 = { ...initialMeta3, ...setMeta3 };\n\n          await fakeBrowser.storage[storageArea].set({\n            one$: initialMeta1,\n            two$: initialMeta2,\n            three$: initialMeta3,\n          });\n\n          await storage.setMetas([\n            { item: item1, meta: setMeta1 },\n            { item: item2, meta: setMeta2 },\n            { item: item3, meta: setMeta3 },\n          ]);\n\n          expect(await item1.getMeta()).toEqual(expectedMeta1);\n          expect(await item2.getMeta()).toEqual(expectedMeta2);\n          expect(await item3.getMeta()).toEqual(expectedMeta3);\n        });\n      });\n\n      describe('removeItem', () => {\n        it('should remove the key from storage', async () => {\n          await fakeBrowser.storage[storageArea].set({ count: 456 });\n\n          await storage.removeItem(`${storageArea}:count`);\n          const actual = await storage.getItem(`${storageArea}:count`);\n\n          expect(actual).toBeNull();\n        });\n\n        it('should not remove the metadata by default', async () => {\n          const expected = { v: 1 };\n          await fakeBrowser.storage[storageArea].set({\n            count$: expected,\n            count: 3,\n          });\n\n          await storage.removeItem(`${storageArea}:count`);\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual(expected);\n        });\n\n        it('should remove the metadata when requested', async () => {\n          await fakeBrowser.storage[storageArea].set({\n            count$: { v: 1 },\n            count: 3,\n          });\n\n          await storage.removeItem(`${storageArea}:count`, {\n            removeMeta: true,\n          });\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual({});\n        });\n      });\n\n      describe('removeItems', () => {\n        it('should remove multiple keys', async () => {\n          const key1 = `${storageArea}:one` as const;\n          const key2 = `${storageArea}:two` as const;\n          await fakeBrowser.storage[storageArea].set({\n            one: 1,\n            two: 2,\n          });\n\n          await storage.removeItems([key1, key2]);\n\n          expect(await storage.getItem(key1)).toBeNull();\n          expect(await storage.getItem(key2)).toBeNull();\n        });\n\n        it('should remove multiple keys and metadata when requested', async () => {\n          const key1 = `${storageArea}:one` as const;\n          const key2 = `${storageArea}:two` as const;\n          await fakeBrowser.storage[storageArea].set({\n            one: 1,\n            one$: { v: 1 },\n            two: 2,\n            two$: { v: 1 },\n          });\n\n          await storage.removeItems([\n            key1,\n            { key: key2, options: { removeMeta: true } },\n          ]);\n\n          expect(await storage.getItem(key1)).toBeNull();\n          expect(await storage.getMeta(key1)).toEqual({ v: 1 });\n          expect(await storage.getItem(key2)).toBeNull();\n          expect(await storage.getMeta(key2)).toEqual({});\n        });\n\n        it('should remove multiple items', async () => {\n          const item1 = storage.defineItem(`${storageArea}:one`);\n          const item2 = storage.defineItem(`${storageArea}:two`);\n          await fakeBrowser.storage[storageArea].set({\n            one: 1,\n            two: 2,\n          });\n\n          await storage.removeItems([item1, item2]);\n\n          expect(await item1.getValue()).toBeNull();\n          expect(await item2.getValue()).toBeNull();\n        });\n\n        it('should remove multiple items and metadata when requested', async () => {\n          const item1 = storage.defineItem(`${storageArea}:one`);\n          const item2 = storage.defineItem(`${storageArea}:two`);\n          await fakeBrowser.storage[storageArea].set({\n            one: 1,\n            one$: { v: 1 },\n            two: 2,\n            two$: { v: 1 },\n          });\n\n          await storage.removeItems([\n            item1,\n            { item: item2, options: { removeMeta: true } },\n          ]);\n\n          expect(await item1.getValue()).toBeNull();\n          expect(await item1.getMeta()).toEqual({ v: 1 });\n          expect(await item2.getValue()).toBeNull();\n          expect(await item2.getMeta()).toEqual({});\n        });\n\n        it('should remove multiple items', async () => {\n          const item1 = storage.defineItem(`${storageArea}:one`);\n          const item2 = storage.defineItem(`${storageArea}:two`);\n\n          await fakeBrowser.storage.local.set({\n            one: 1,\n            two: 2,\n          });\n\n          await storage.removeItems([item1, item2]);\n\n          expect(await item1.getValue()).toBeNull();\n          expect(await item2.getValue()).toBeNull();\n        });\n\n        it('should remove items using { item: WxtStorageItem, options?: RemoveItemOptions } objects', async () => {\n          const item1 = storage.defineItem('local:item1');\n          const item2 = storage.defineItem('session:item2');\n\n          await item1.setValue('value1');\n          await item1.setMeta({ v: 1 });\n          await item2.setValue('value2');\n          await item2.setMeta({ v: 1 });\n\n          await storage.removeItems([\n            { item: item1, options: { removeMeta: true } },\n            { item: item2, options: { removeMeta: false } },\n          ]);\n\n          expect(await item1.getValue()).toBeNull();\n          expect(await item1.getMeta()).toEqual({});\n          expect(await item2.getValue()).toBeNull();\n          expect(await item2.getMeta()).toEqual({ v: 1 });\n        });\n\n        it('should handle a mix of different input types', async () => {\n          const item1 = storage.defineItem('local:item1');\n          const item2 = storage.defineItem('session:item2');\n          const item3 = storage.defineItem('local:item3');\n\n          await item1.setValue('value1');\n          await item2.setValue('value2');\n          await item3.setValue('value3');\n\n          await storage.removeItems([\n            'local:item1',\n            item2,\n            { key: 'local:item3', options: { removeMeta: true } },\n          ]);\n\n          expect(await storage.getItem('local:item1')).toBeNull();\n          expect(await item2.getValue()).toBeNull();\n          expect(await item3.getValue()).toBeNull();\n          expect(await item3.getMeta()).toEqual({});\n        });\n\n        it('should not throw an error when removing non-existent items', async () => {\n          const item = storage.defineItem('local:non_existent');\n\n          await expect(storage.removeItems([item])).resolves.not.toThrow();\n        });\n      });\n\n      describe('clear', () => {\n        it('should remove all items', async () => {\n          await fakeBrowser.storage[storageArea].set({\n            one: 1,\n            two: 2,\n          });\n\n          await storage.clear(storageArea);\n          expect(await fakeBrowser.storage[storageArea].get()).toEqual({});\n        });\n      });\n\n      describe('removeMeta', () => {\n        it('should remove all metadata', async () => {\n          await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });\n\n          await storage.removeMeta(`${storageArea}:count`);\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual({});\n        });\n\n        it('should only remove specific properties', async () => {\n          await fakeBrowser.storage[storageArea].set({\n            count$: { v: 4, d: Date.now() },\n          });\n\n          await storage.removeMeta(`${storageArea}:count`, ['d']);\n          const actual = await storage.getMeta(`${storageArea}:count`);\n\n          expect(actual).toEqual({ v: 4 });\n        });\n      });\n\n      describe('snapshot', () => {\n        it('should return a snapshot of the entire storage without area prefixes', async () => {\n          const expected = {\n            count: 1,\n            count$: { v: 2 },\n            example: 'test',\n          };\n\n          await fakeBrowser.storage[storageArea].set(expected);\n          const actual = await storage.snapshot(storageArea);\n\n          expect(actual).toEqual(expected);\n        });\n\n        it('should exclude specific properties and their metadata', async () => {\n          const input = {\n            count: 1,\n            count$: { v: 2 },\n            example: 'test',\n          };\n          const excludeKeys = ['count'];\n          const expected = {\n            example: 'test',\n          };\n\n          await fakeBrowser.storage[storageArea].set(input);\n          const actual = await storage.snapshot(storageArea, { excludeKeys });\n\n          expect(actual).toEqual(expected);\n        });\n      });\n\n      describe('restoreSnapshot', () => {\n        it('should restore a snapshot object by setting all values in storage', async () => {\n          const data = {\n            one: 'one',\n            two: 'two',\n          };\n          const existing = {\n            two: 'two-two',\n            three: 'three',\n          };\n          await fakeBrowser.storage[storageArea].set(existing);\n\n          await storage.restoreSnapshot(storageArea, data);\n          const actual = await storage.snapshot(storageArea);\n\n          expect(actual).toEqual({ ...existing, ...data });\n        });\n\n        it('should overwrite, not merge, any metadata keys in the snapshot', async () => {\n          const existing = {\n            count: 1,\n            count$: {\n              v: 2,\n            },\n          };\n          const data = {\n            count$: {\n              restoredAt: Date.now(),\n            },\n          };\n          const expected = {\n            ...existing,\n            count$: data.count$,\n          };\n          await fakeBrowser.storage[storageArea].set(existing);\n\n          await storage.restoreSnapshot(storageArea, data);\n          const actual = await storage.snapshot(storageArea);\n\n          expect(actual).toEqual(expected);\n        });\n      });\n\n      describe('watch', () => {\n        it('should not trigger if the changed key is different from the requested key', async () => {\n          const cb = vi.fn();\n\n          storage.watch(`${storageArea}:key`, cb);\n          await storage.setItem(`${storageArea}:not-the-key`, '123');\n\n          expect(cb).not.toBeCalled();\n        });\n\n        it(\"should not trigger if the value doesn't change\", async () => {\n          const cb = vi.fn();\n          const value = '123';\n\n          await storage.setItem(`${storageArea}:key`, value);\n          storage.watch(`${storageArea}:key`, cb);\n          await storage.setItem(`${storageArea}:key`, value);\n\n          expect(cb).not.toBeCalled();\n        });\n\n        it('should call the callback when the value changes', async () => {\n          const cb = vi.fn();\n          const newValue = '123';\n          const oldValue = null;\n\n          storage.watch(`${storageArea}:key`, cb);\n          await storage.setItem(`${storageArea}:key`, newValue);\n\n          expect(cb).toBeCalledTimes(1);\n          expect(cb).toBeCalledWith(newValue, oldValue);\n        });\n\n        it('should remove the listener when calling the returned function', async () => {\n          const cb = vi.fn();\n\n          const unwatch = storage.watch(`${storageArea}:key`, cb);\n          unwatch();\n          await storage.setItem(`${storageArea}:key`, '123');\n\n          expect(cb).not.toBeCalled();\n        });\n      });\n\n      describe('unwatch', () => {\n        it('should remove all watch listeners', async () => {\n          const cb = vi.fn();\n\n          storage.watch(`${storageArea}:key`, cb);\n          storage.unwatch();\n          await storage.setItem(`${storageArea}:key`, '123');\n\n          expect(cb).not.toBeCalled();\n        });\n      });\n    },\n  );\n\n  describe('Invalid storage areas', () => {\n    it('should not accept keys without a valid storage area prefix', async () => {\n      // @ts-expect-error\n      await storage.getItem('test').catch(() => {});\n      // @ts-expect-error\n      await storage.getItem('loca:test').catch(() => {});\n    });\n\n    it('should throw an error when using an invalid storage area', async () => {\n      // @ts-expect-error: Test passes if there is a type error here\n      await expect(storage.getItem('invalidArea:key')).rejects.toThrow(\n        'Invalid area',\n      );\n    });\n  });\n\n  describe('defineItem', () => {\n    describe('versioning', () => {\n      it('should migrate values to the latest when a version upgrade is detected', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n          count$: { v: 1 },\n        });\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n\n        const item = storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            2: migrateToV2,\n            3: migrateToV3,\n          },\n        });\n        await waitForMigrations();\n\n        const actualValue = await item.getValue();\n        const actualMeta = await item.getMeta();\n\n        expect(actualValue).toEqual(12);\n        expect(actualMeta).toEqual({ v: 3 });\n\n        expect(migrateToV2).toBeCalledTimes(1);\n        expect(migrateToV2).toBeCalledWith(2);\n\n        expect(migrateToV3).toBeCalledTimes(1);\n        expect(migrateToV3).toBeCalledWith(4);\n      });\n\n      it('should set the version without running migrations for empty storage items', async () => {\n        const migrate = vi.fn((n: number) => n * 2);\n\n        const item = storage.defineItem<number>('local:key', {\n          init: () => 1,\n          version: 2,\n          migrations: {\n            2: migrate,\n          },\n        });\n        const value = await item.getValue();\n        const meta = await item.getMeta();\n\n        expect(value).toEqual(1);\n        expect(meta).toEqual({ v: 2 });\n        expect(migrate).toBeCalledTimes(0);\n      });\n\n      it('should set the version and run migrations if a storage value already exists and v2 is added', async () => {\n        const key = 'local:key';\n\n        // Mimic as if an initial value was set by a previous version. We expect\n        // this value to be migrated and doubled to V2.\n        await storage.setItem(key, 1);\n\n        const migrate = vi.fn((n: number) => n * 2);\n        const item = storage.defineItem<number>(key, {\n          init: () => 1,\n          version: 2,\n          migrations: {\n            2: migrate,\n          },\n        });\n        const value = await item.getValue();\n        const meta = await item.getMeta();\n\n        expect(value).toEqual(2);\n        expect(meta).toEqual({ v: 2 });\n        expect(migrate).toBeCalledTimes(1);\n      });\n\n      it('should call onMigrationComplete callback function if defined', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n          count$: { v: 1 },\n        });\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n        const onMigrationComplete = vi.fn((count, _v) => count);\n\n        storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            2: migrateToV2,\n            3: migrateToV3,\n          },\n          onMigrationComplete,\n        });\n        await waitForMigrations();\n\n        expect(onMigrationComplete).toBeCalledTimes(1);\n        expect(onMigrationComplete).toBeCalledWith(12, 3);\n      });\n\n      it(\"should not run migrations if the value doesn't exist yet\", async () => {\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n\n        const item = storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            2: migrateToV2,\n            3: migrateToV3,\n          },\n        });\n        await waitForMigrations();\n\n        const actualValue = await item.getValue();\n        const actualMeta = await item.getMeta();\n\n        expect(actualValue).toEqual(0);\n        expect(actualMeta).toEqual({});\n\n        expect(migrateToV2).not.toBeCalled();\n        expect(migrateToV3).not.toBeCalled();\n      });\n\n      it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n        });\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n\n        const item = storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 2,\n          migrations: {\n            2: migrateToV2,\n          },\n        });\n        await waitForMigrations();\n\n        const actualValue = await item.getValue();\n        const actualMeta = await item.getMeta();\n\n        expect(actualValue).toEqual(4);\n        expect(actualMeta).toEqual({ v: 2 });\n\n        expect(migrateToV2).toBeCalledTimes(1);\n        expect(migrateToV2).toBeCalledWith(2);\n      });\n\n      it('should not run old migrations if the version is unchanged', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n          count$: { v: 3 },\n        });\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n\n        storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            2: migrateToV2,\n            3: migrateToV3,\n          },\n        });\n        await waitForMigrations();\n\n        expect(migrateToV2).not.toBeCalled();\n        expect(migrateToV3).not.toBeCalled();\n      });\n\n      it('should skip missing migration functions', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n          count$: { v: 0 },\n        });\n        const migrateToV1 = vi.fn((oldCount) => oldCount * 1);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n\n        const item = storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            1: migrateToV1,\n            3: migrateToV3,\n          },\n        });\n        await waitForMigrations();\n\n        const actualValue = await item.getValue();\n        const actualMeta = await item.getMeta();\n\n        expect(actualValue).toEqual(6);\n        expect(actualMeta).toEqual({ v: 3 });\n\n        expect(migrateToV1).toBeCalledTimes(1);\n        expect(migrateToV1).toBeCalledWith(2);\n\n        expect(migrateToV3).toBeCalledTimes(1);\n        expect(migrateToV3).toBeCalledWith(2);\n      });\n\n      it('should throw an error if the new version is less than the previous version', async () => {\n        const prevVersion = 2;\n        const nextVersion = 1;\n        await fakeBrowser.storage.local.set({\n          count: 0,\n          count$: { v: prevVersion },\n        });\n\n        const item = storage.defineItem(`local:count`, {\n          defaultValue: 0,\n          version: nextVersion,\n        });\n        await waitForMigrations();\n\n        await expect(item.migrate()).rejects.toThrow(\n          'Version downgrade detected (v2 -> v1) for \"local:count\"',\n        );\n      });\n\n      it('should throw an error when defining an item with an invalid version', () => {\n        expect(() => storage.defineItem('local:key', { version: 0 })).toThrow(\n          'Storage item version cannot be less than 1',\n        );\n      });\n\n      it('should handle errors in migration functions', async () => {\n        const cause = Error('Some error');\n        const expectedError = new MigrationError('local:key', 2, { cause });\n        const item = storage.defineItem<number>('local:key', {\n          version: 3,\n          migrations: {\n            2: () => {\n              throw cause;\n            },\n          },\n        });\n        await fakeBrowser.storage.local.set({ key: 1, key$: { v: 1 } });\n\n        await expect(item.migrate()).rejects.toThrow(expectedError);\n      });\n\n      it('should print migration logs if debug option is true', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n          count$: { v: 1 },\n        });\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n        const consoleSpy = vi.spyOn(console, 'debug');\n\n        storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            2: migrateToV2,\n            3: migrateToV3,\n          },\n          debug: true,\n        });\n        await waitForMigrations();\n\n        expect(consoleSpy).toHaveBeenCalledTimes(4);\n        expect(consoleSpy).toHaveBeenCalledWith(\n          `[@wxt-dev/storage] Running storage migration for local:count: v1 -> v3`,\n        );\n        expect(consoleSpy).toHaveBeenCalledWith(\n          `[@wxt-dev/storage] Storage migration processed for version: v2`,\n        );\n        expect(consoleSpy).toHaveBeenCalledWith(\n          `[@wxt-dev/storage] Storage migration processed for version: v3`,\n        );\n        expect(consoleSpy).toHaveBeenCalledWith(\n          `[@wxt-dev/storage] Storage migration completed for local:count v3`,\n          { migratedValue: expect.any(Number) },\n        );\n      });\n\n      it('should not print migration logs if debug option is undefined or false', async () => {\n        await fakeBrowser.storage.local.set({\n          count: 2,\n          count$: { v: 1 },\n          count2: 2,\n          count2$: { v: 1 },\n        });\n        const migrateToV2 = vi.fn((oldCount) => oldCount * 2);\n        const migrateToV3 = vi.fn((oldCount) => oldCount * 3);\n        const consoleSpy = vi.spyOn(console, 'debug');\n\n        storage.defineItem<number, { v: number }>(`local:count`, {\n          defaultValue: 0,\n          version: 3,\n          migrations: {\n            2: migrateToV2,\n            3: migrateToV3,\n          },\n        });\n\n        storage.defineItem<number, { v: number }>(`local:count2`, {\n          defaultValue: 0,\n          version: 2,\n          migrations: {\n            2: migrateToV2,\n          },\n          debug: false,\n        });\n        await waitForMigrations();\n        expect(consoleSpy).toHaveBeenCalledTimes(0);\n      });\n\n      describe('calling setValue', () => {\n        const migrateToV2 = vi.fn((v1) => v1);\n        const defineTestItem = () =>\n          storage.defineItem<number>('local:count', {\n            version: 2,\n            migrations: {\n              2: migrateToV2,\n            },\n          });\n\n        it('should set the version metadata when setting the value for the first time', async () => {\n          const item = defineTestItem();\n\n          expect(await item.getValue()).toBeNull();\n          expect(await item.getMeta()).toEqual({});\n          expect(fakeBrowser.storage.local.set).toBeCalledTimes(0);\n\n          await item.setValue(1);\n\n          expect(await item.getValue()).toBe(1);\n          expect(await item.getMeta()).toEqual({ v: 2 });\n          // Called twice, once for setting the value, once for updating the metadata.\n          expect(fakeBrowser.storage.local.set).toBeCalledTimes(2);\n          expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 1 });\n          expect(fakeBrowser.storage.local.set).toBeCalledWith({\n            count$: { v: 2 },\n          });\n\n          await item.setValue(2);\n          expect(await item.getValue()).toBe(2);\n          expect(await item.getMeta()).toEqual({ v: 2 });\n          // Only called one more time, just for setting the value\n          expect(fakeBrowser.storage.local.set).toBeCalledTimes(3);\n          expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 2 });\n\n          // Migration function never called throughout the whole test\n          expect(migrateToV2).not.toBeCalled();\n        });\n\n        it('should not set the version metadata when a value is already in storage', async () => {\n          await fakeBrowser.storage.local.set({ count: 1, count$: { v: 2 } });\n          vi.mocked(fakeBrowser.storage.local.set).mockClear();\n\n          const item = defineTestItem();\n          await item.setValue(2);\n\n          expect(await item.getValue()).toEqual(2);\n          expect(await item.getMeta()).toEqual({ v: 2 });\n\n          expect(fakeBrowser.storage.local.set).toBeCalledTimes(1);\n          expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 2 });\n\n          expect(migrateToV2).not.toBeCalled();\n        });\n      });\n    });\n\n    describe('getValue', () => {\n      it('should return the value from storage', async () => {\n        const expected = 2;\n        const item = storage.defineItem<number>(`local:count`);\n        await fakeBrowser.storage.local.set({ count: expected });\n\n        const actual = await item.getValue();\n\n        expect(actual).toBe(expected);\n      });\n\n      it('should return null if missing', async () => {\n        const item = storage.defineItem<number>(`local:count`);\n\n        const actual = await item.getValue();\n\n        expect(actual).toBeNull();\n      });\n\n      it('should return the provided default value if missing', async () => {\n        const expected = 0;\n        const item = storage.defineItem(`local:count`, {\n          defaultValue: expected,\n        });\n\n        const actual = await item.getValue();\n\n        expect(actual).toEqual(expected);\n      });\n    });\n\n    describe('getMeta', () => {\n      it('should return the value from storage at key+$', async () => {\n        const expected = { v: 2 };\n        const item = storage.defineItem<number, { v: number }>(`local:count`);\n        await fakeBrowser.storage.local.set({ count$: expected });\n\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual(expected);\n      });\n\n      it('should return an empty object if missing', async () => {\n        const expected = {};\n        const item = storage.defineItem<number, { v: number }>(`local:count`);\n\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual(expected);\n      });\n    });\n\n    describe('setValue', () => {\n      it('should set the value in storage', async () => {\n        const expected = 1;\n        const item = storage.defineItem<number>(`local:count`);\n\n        await item.setValue(expected);\n        const actual = await item.getValue();\n\n        expect(actual).toBe(expected);\n      });\n\n      it.each([undefined, null])(\n        'should remove the value in storage when %s is passed in',\n        async (value) => {\n          const item = storage.defineItem<number>(`local:count`);\n\n          // @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose\n          await item.setValue(value);\n          const actual = await item.getValue();\n\n          expect(actual).toBeNull();\n        },\n      );\n    });\n\n    describe('setMeta', () => {\n      it('should set metadata at key+$', async () => {\n        const expected = { date: Date.now() };\n        const item = storage.defineItem<number, { date: number }>(\n          `local:count`,\n        );\n\n        await item.setMeta(expected);\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual(expected);\n      });\n\n      it('should add to metadata if already present', async () => {\n        const existing = { v: 2 };\n        const newFields = { date: Date.now() };\n        const expected = { ...existing, ...newFields };\n        const item = storage.defineItem<number, { date: number; v: number }>(\n          `local:count`,\n        );\n        await fakeBrowser.storage.local.set({\n          count$: existing,\n        });\n\n        await item.setMeta(newFields);\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual(expected);\n      });\n    });\n\n    describe('removeValue', () => {\n      it('should remove the key from storage', async () => {\n        const item = storage.defineItem(`local:count`);\n        await fakeBrowser.storage.local.set({ count: 456 });\n\n        await item.removeValue();\n        const actual = await item.getValue();\n\n        expect(actual).toBeNull();\n      });\n\n      it('should not remove the metadata by default', async () => {\n        const item = storage.defineItem(`local:count`);\n        const expected = { v: 1 };\n        await fakeBrowser.storage.local.set({\n          count$: expected,\n          count: 3,\n        });\n\n        await item.removeValue();\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual(expected);\n      });\n\n      it('should remove the metadata when requested', async () => {\n        const item = storage.defineItem(`local:count`);\n        await fakeBrowser.storage.local.set({\n          count$: { v: 1 },\n          count: 3,\n        });\n\n        await item.removeValue({ removeMeta: true });\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual({});\n      });\n    });\n\n    describe('removeMeta', () => {\n      it('should remove all metadata', async () => {\n        const item = storage.defineItem<number, { v: number }>(`local:count`);\n        await fakeBrowser.storage.local.set({ count$: { v: 4 } });\n\n        await item.removeMeta();\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual({});\n      });\n\n      it('should only remove specific properties', async () => {\n        const item = storage.defineItem<number, { v: number; d: number }>(\n          `local:count`,\n        );\n        await fakeBrowser.storage.local.set({\n          count$: { v: 4, d: Date.now() },\n        });\n\n        await item.removeMeta(['d']);\n        const actual = await item.getMeta();\n\n        expect(actual).toEqual({ v: 4 });\n      });\n    });\n\n    describe('watch', () => {\n      it(\"should not trigger if the changed key is different from the item's key\", async () => {\n        const item = storage.defineItem(`local:key`);\n        const cb = vi.fn();\n\n        item.watch(cb);\n        await storage.setItem(`local:not-the-key`, '123');\n\n        expect(cb).not.toBeCalled();\n      });\n\n      it(\"should not trigger if the value doesn't change\", async () => {\n        const item = storage.defineItem(`local:key`);\n        const cb = vi.fn();\n        const value = '123';\n\n        await item.setValue(value);\n        item.watch(cb);\n        await item.setValue(value);\n\n        expect(cb).not.toBeCalled();\n      });\n\n      it('should call the callback when the value changes', async () => {\n        const item = storage.defineItem(`local:key`);\n        const cb = vi.fn();\n        const newValue = '123';\n        const oldValue = null;\n\n        item.watch(cb);\n        await item.setValue(newValue);\n\n        expect(cb).toBeCalledTimes(1);\n        expect(cb).toBeCalledWith(newValue, oldValue);\n      });\n\n      it('should use the default value for the newValue when the item is removed', async () => {\n        const defaultValue = 'default';\n        const item = storage.defineItem<string>(`local:key`, {\n          defaultValue,\n        });\n        const cb = vi.fn();\n        const oldValue = '123';\n        await item.setValue(oldValue);\n\n        item.watch(cb);\n        await item.removeValue();\n\n        expect(cb).toBeCalledTimes(1);\n        expect(cb).toBeCalledWith(defaultValue, oldValue);\n      });\n\n      it(\"should use the default value for the oldItem when the item didn't exist in storage yet\", async () => {\n        const defaultValue = 'default';\n        const item = storage.defineItem<string>(`local:key`, {\n          defaultValue,\n        });\n        const cb = vi.fn();\n        const newValue = '123';\n        await item.removeValue();\n\n        item.watch(cb);\n        await item.setValue(newValue);\n\n        expect(cb).toBeCalledTimes(1);\n        expect(cb).toBeCalledWith(newValue, defaultValue);\n      });\n\n      it('should remove the listener when calling the returned function', async () => {\n        const item = storage.defineItem(`local:key`);\n        const cb = vi.fn();\n\n        const unwatch = item.watch(cb);\n        unwatch();\n        await item.setValue('123');\n\n        expect(cb).not.toBeCalled();\n      });\n    });\n\n    describe('unwatch', () => {\n      it('should remove all watch listeners', async () => {\n        const item = storage.defineItem(`local:key`);\n        const cb = vi.fn();\n\n        item.watch(cb);\n        storage.unwatch();\n        await item.setValue('123');\n\n        expect(cb).not.toBeCalled();\n      });\n    });\n\n    describe.each(['fallback', 'defaultValue'] as const)(\n      '%s option',\n      (fallbackKey) => {\n        it('should return the default value when provided', () => {\n          const fallback = 123;\n          const item = storage.defineItem(`local:test`, {\n            [fallbackKey]: fallback,\n          });\n\n          expect(item.fallback).toBe(fallback);\n          expect(item.defaultValue).toBe(fallback);\n        });\n\n        it('should return null when not provided', () => {\n          const item = storage.defineItem<number>(`local:test`);\n\n          expect(item.fallback).toBeNull();\n          expect(item.defaultValue).toBeNull();\n        });\n      },\n    );\n\n    describe('init option', () => {\n      it('should only call init once (per JS context) when calling getValue successively, avoiding race conditions', async () => {\n        const expected = 1;\n        const init = vi\n          .fn()\n          .mockResolvedValueOnce(expected)\n          .mockResolvedValue('not' + expected);\n        const item = storage.defineItem('local:test', { init });\n\n        await waitForInit();\n\n        const p1 = item.getValue();\n        const p2 = item.getValue();\n\n        await expect(p1).resolves.toBe(expected);\n        await expect(p2).resolves.toBe(expected);\n\n        expect(init).toBeCalledTimes(1);\n      });\n\n      it('should initialize the value in storage immediately', async () => {\n        const expected = 1;\n        const init = vi.fn().mockReturnValue(expected);\n        storage.defineItem('local:test', { init });\n\n        await waitForInit();\n\n        await expect(storage.getItem('local:test')).resolves.toBe(expected);\n      });\n\n      it(\"should re-initialize a value on the next call to getValue if it's been removed\", async () => {\n        const init = vi.fn().mockImplementation(Math.random);\n        const item = storage.defineItem<number>('local:key', { init });\n        await waitForInit();\n\n        await item.removeValue();\n        // Make sure it's actually blank before running the test\n        expect(await browser.storage.local.get()).toEqual({});\n        init.mockClear();\n\n        const [value1, value2] = await Promise.all([\n          item.getValue(),\n          item.getValue(),\n        ]);\n        expect(init).toBeCalledTimes(1);\n        expect(value1).toBe(value2);\n      });\n    });\n\n    describe('types', () => {\n      it('should define a nullable value when options are not passed', () => {\n        const item = storage.defineItem<number>(`local:test`);\n        expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();\n\n        const item2 = storage.defineItem<number>(`local:test`, {});\n        expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number | null, {}>>();\n\n        const item3 = storage.defineItem<number>(`local:test`, {\n          fallback: undefined,\n        });\n        expectTypeOf(item3).toEqualTypeOf<WxtStorageItem<number | null, {}>>();\n\n        const item4 = storage.defineItem<number>(`local:test`, {\n          defaultValue: undefined,\n        });\n        expectTypeOf(item4).toEqualTypeOf<WxtStorageItem<number | null, {}>>();\n      });\n\n      it('should define a non-null value when options are passed with a nullish default value', () => {\n        const item = storage.defineItem(`local:test`, {\n          defaultValue: 123,\n        });\n        expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();\n\n        const item2 = storage.defineItem(`local:test`, {\n          fallback: 123,\n        });\n        expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();\n      });\n\n      it('should define a nullable value when options are passed with null default value', () => {\n        const item = storage.defineItem<number | null>(`local:test`, {\n          defaultValue: null,\n        });\n        expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();\n      });\n\n      it('should define a non-null value when options are passed with a non-null init function', () => {\n        const item = storage.defineItem(`local:test`, {\n          init: () => 123,\n        });\n        expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();\n\n        const item2 = storage.defineItem(`local:test`, {\n          init: () => Promise.resolve(123),\n        });\n        expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();\n      });\n    });\n  });\n\n  describe('Multiple Storage Areas', () => {\n    describe('getItems', () => {\n      it('should get the values of multiple storage items efficiently', async () => {\n        const item1 = storage.defineItem<number>('local:item1');\n        const item2 = storage.defineItem<string>('session:item2');\n        const item3 = storage.defineItem<boolean>('local:item3');\n\n        await item1.setValue(42);\n        await item2.setValue('hello');\n        await item3.setValue(false);\n\n        const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');\n        const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');\n\n        const values = await storage.getItems([item1, item2, item3]);\n\n        expect(values).toEqual([\n          {\n            key: 'local:item1',\n            value: 42,\n          },\n          {\n            key: 'session:item2',\n            value: 'hello',\n          },\n          {\n            key: 'local:item3',\n            value: false,\n          },\n        ]);\n\n        expect(localGetSpy).toBeCalledTimes(1);\n        expect(localGetSpy).toBeCalledWith(['item1', 'item3']);\n        expect(sessionGetSpy).toBeCalledTimes(1);\n        expect(sessionGetSpy).toBeCalledWith(['item2']);\n      });\n\n      it('should return values in the same order as input', async () => {\n        const item1 = storage.defineItem<number>('local:item1');\n        const item2 = storage.defineItem<string>('session:item2');\n        const item3 = storage.defineItem<boolean>('local:item3');\n\n        expect(await storage.getItems([item1, item2, item3])).toEqual([\n          { key: item1.key, value: null },\n          { key: item2.key, value: null },\n          { key: item3.key, value: null },\n        ]);\n        expect(await storage.getItems([item3, item2, item1])).toEqual([\n          { key: item3.key, value: null },\n          { key: item2.key, value: null },\n          { key: item1.key, value: null },\n        ]);\n      });\n    });\n\n    describe('getMetas', () => {\n      it('should get the metadata of multiple storage items efficiently', async () => {\n        const item1 = storage.defineItem<number, { v: number }>('local:item1');\n        const item2 = storage.defineItem<string, { date: number }>(\n          'session:item2',\n        );\n        const item3 = storage.defineItem<boolean, { flag: boolean }>(\n          'local:item3',\n        );\n\n        await item1.setMeta({ v: 1 });\n        await item2.setMeta({ date: 1234567890 });\n        // item3 has no meta\n\n        const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');\n        const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');\n\n        const metas = await storage.getMetas([item1, item2, item3]);\n\n        expect(metas).toEqual([\n          { key: item1.key, meta: { v: 1 } },\n          { key: item2.key, meta: { date: 1234567890 } },\n          { key: item3.key, meta: {} },\n        ]);\n\n        expect(localGetSpy).toBeCalledTimes(1);\n        expect(localGetSpy).toBeCalledWith(['item1$', 'item3$']);\n        expect(sessionGetSpy).toBeCalledTimes(1);\n        expect(sessionGetSpy).toBeCalledWith(['item2$']);\n      });\n\n      it('should return the metadata in the same order as input', async () => {\n        const item1 = storage.defineItem<number, { v: number }>('local:item1');\n        const item2 = storage.defineItem<string, { date: number }>(\n          'session:item2',\n        );\n        const item3 = storage.defineItem<boolean, { flag: boolean }>(\n          'local:item3',\n        );\n\n        expect(await storage.getMetas([item1, item2, item3])).toEqual([\n          { key: item1.key, meta: {} },\n          { key: item2.key, meta: {} },\n          { key: item3.key, meta: {} },\n        ]);\n        expect(await storage.getMetas([item3, item2, item1])).toEqual([\n          { key: item3.key, meta: {} },\n          { key: item2.key, meta: {} },\n          { key: item1.key, meta: {} },\n        ]);\n      });\n    });\n\n    describe('setItems', () => {\n      it('should set the values of multiple storage items efficiently', async () => {\n        const item1 = storage.defineItem<number>('local:item1');\n        const value1 = 100;\n        const item2 = storage.defineItem<string>('session:item2');\n        const value2 = 'test';\n        const item3 = storage.defineItem<boolean>('local:item3');\n        const value3 = true;\n\n        const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');\n        const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');\n\n        await storage.setItems([\n          { item: item1, value: value1 },\n          { item: item2, value: value2 },\n          { item: item3, value: value3 },\n        ]);\n\n        expect(localSetSpy).toBeCalledTimes(1);\n        expect(localSetSpy).toBeCalledWith({ item1: value1, item3: value3 });\n        expect(sessionSetSpy).toBeCalledTimes(1);\n        expect(sessionSetSpy).toBeCalledWith({ item2: value2 });\n      });\n    });\n\n    describe('setMetas', () => {\n      it('should set metadata efficiently', async () => {\n        const item1 = storage.defineItem<number, { v: number }>('local:one');\n        const item2 = storage.defineItem<string, { v: number }>('session:two');\n        const item3 = storage.defineItem<boolean, { v: number }>('local:three');\n        await waitForInit();\n\n        const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');\n        const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');\n        const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');\n        const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');\n\n        await storage.setMetas([\n          { item: item1, meta: { v: 1 } },\n          { item: item2, meta: { v: 2 } },\n          { item: item3, meta: { v: 3 } },\n        ]);\n\n        console.log(localGetSpy.mock.calls);\n        expect(localGetSpy).toBeCalledTimes(1);\n        expect(localGetSpy).toBeCalledWith(['one$', 'three$']);\n        expect(sessionGetSpy).toBeCalledTimes(1);\n        expect(sessionGetSpy).toBeCalledWith(['two$']);\n\n        expect(localSetSpy).toBeCalledTimes(1);\n        expect(localSetSpy).toBeCalledWith({\n          one$: { v: 1 },\n          three$: { v: 3 },\n        });\n        expect(sessionSetSpy).toBeCalledTimes(1);\n        expect(sessionSetSpy).toBeCalledWith({\n          two$: { v: 2 },\n        });\n      });\n    });\n\n    it('should return a nullable type when getItem is called without a fallback', async () => {\n      const res = await storage.getItem<string>('local:test');\n      expectTypeOf(res).toBeNullable();\n    });\n\n    it('should return a non-null type when getItem is called with a fallback', async () => {\n      const res = await storage.getItem('local:test', {\n        fallback: 'test',\n      });\n      expectTypeOf(res).not.toBeNullable();\n    });\n\n    it('should return a non-null type when getItem is called with a fallback and the first type parameter is passed', async () => {\n      const res = await storage.getItem<string>('local:test', {\n        fallback: 'test',\n      });\n      expectTypeOf(res).not.toBeNullable();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/storage/src/index.ts",
    "content": "/**\n * Simplified storage APIs with support for versioned fields, snapshots,\n * metadata, and item definitions.\n *\n * See [the guide](https://wxt.dev/storage.html) for more information.\n *\n * @module @wxt-dev/storage\n */\nimport { browser, type Browser } from '@wxt-dev/browser';\nimport { Mutex } from 'async-mutex';\nimport { dequal } from 'dequal/lite';\n\nexport const storage = createStorage();\n\nfunction createStorage(): WxtStorage {\n  const drivers: Record<StorageArea, WxtStorageDriver> = {\n    local: createDriver('local'),\n    session: createDriver('session'),\n    sync: createDriver('sync'),\n    managed: createDriver('managed'),\n  };\n\n  const getDriver = (area: StorageArea) => {\n    const driver = drivers[area];\n    if (driver == null) {\n      const areaNames = Object.keys(drivers).join(', ');\n      throw Error(`Invalid area \"${area}\". Options: ${areaNames}`);\n    }\n    return driver;\n  };\n\n  const resolveKey = (key: StorageItemKey) => {\n    const deliminatorIndex = key.indexOf(':');\n    const driverArea = key.substring(0, deliminatorIndex) as StorageArea;\n\n    const driverKey = key.substring(deliminatorIndex + 1);\n    if (driverKey == null) {\n      throw Error(\n        `Storage key should be in the form of \"area:key\", but received \"${key}\"`,\n      );\n    }\n\n    return {\n      driverArea,\n      driverKey,\n      driver: getDriver(driverArea),\n    };\n  };\n\n  const getMetaKey = (key: string) => key + '$';\n\n  const mergeMeta = (oldMeta: any, newMeta: any): any => {\n    const newFields = { ...oldMeta };\n\n    Object.entries(newMeta).forEach(([key, value]) => {\n      if (value == null) delete newFields[key];\n      else newFields[key] = value;\n    });\n\n    return newFields;\n  };\n\n  const getValueOrFallback = (value: any, fallback: any) =>\n    value ?? fallback ?? null;\n\n  const getMetaValue = (properties: any) =>\n    typeof properties === 'object' && !Array.isArray(properties)\n      ? properties\n      : {};\n\n  const getItem = async (\n    driver: WxtStorageDriver,\n    driverKey: string,\n    opts: GetItemOptions<any> | undefined,\n  ) => {\n    const res = await driver.getItem<any>(driverKey);\n    return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue);\n  };\n\n  const getMeta = async (driver: WxtStorageDriver, driverKey: string) => {\n    const metaKey = getMetaKey(driverKey);\n    const res = await driver.getItem<any>(metaKey);\n    return getMetaValue(res);\n  };\n\n  const setItem = async (\n    driver: WxtStorageDriver,\n    driverKey: string,\n    value: any,\n  ) => {\n    await driver.setItem(driverKey, value ?? null);\n  };\n\n  const setMeta = async (\n    driver: WxtStorageDriver,\n    driverKey: string,\n    properties: any | undefined,\n  ) => {\n    const metaKey = getMetaKey(driverKey);\n    const existingFields = getMetaValue(await driver.getItem(metaKey));\n    await driver.setItem(metaKey, mergeMeta(existingFields, properties));\n  };\n\n  const removeItem = async (\n    driver: WxtStorageDriver,\n    driverKey: string,\n    opts: RemoveItemOptions | undefined,\n  ) => {\n    await driver.removeItem(driverKey);\n\n    if (opts?.removeMeta) {\n      const metaKey = getMetaKey(driverKey);\n      await driver.removeItem(metaKey);\n    }\n  };\n\n  const removeMeta = async (\n    driver: WxtStorageDriver,\n    driverKey: string,\n    properties: string | string[] | undefined,\n  ) => {\n    const metaKey = getMetaKey(driverKey);\n\n    if (properties == null) {\n      await driver.removeItem(metaKey);\n    } else {\n      const newFields = getMetaValue(await driver.getItem(metaKey));\n      [properties].flat().forEach((field) => delete newFields[field]);\n      await driver.setItem(metaKey, newFields);\n    }\n  };\n\n  const watch = (\n    driver: WxtStorageDriver,\n    driverKey: string,\n    cb: WatchCallback<any>,\n  ) => driver.watch(driverKey, cb);\n\n  return {\n    getItem: async (key, opts) => {\n      const { driver, driverKey } = resolveKey(key);\n      return await getItem(driver, driverKey, opts);\n    },\n\n    getItems: async (keys) => {\n      const areaToKeyMap = new Map<StorageArea, string[]>();\n      const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();\n      const orderedKeys: StorageItemKey[] = [];\n\n      keys.forEach((key) => {\n        let keyStr: StorageItemKey;\n        let opts: GetItemOptions<any> | undefined;\n\n        if (typeof key === 'string') {\n          // key: string\n          keyStr = key;\n        } else if ('getValue' in key) {\n          // key: WxtStorageItem\n          keyStr = key.key;\n          opts = { fallback: key.fallback };\n        } else {\n          // key: { key, options }\n          keyStr = key.key;\n          opts = key.options;\n        }\n\n        orderedKeys.push(keyStr);\n        const { driverArea, driverKey } = resolveKey(keyStr);\n        const areaKeys = areaToKeyMap.get(driverArea) ?? [];\n\n        areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));\n        keyToOptsMap.set(keyStr, opts);\n      });\n\n      const resultsMap = new Map<StorageItemKey, any>();\n      await Promise.all(\n        Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {\n          const driverResults = await drivers[driverArea].getItems(keys);\n\n          driverResults.forEach((driverResult) => {\n            const key = `${driverArea}:${driverResult.key}` as StorageItemKey;\n            const opts = keyToOptsMap.get(key);\n            const value = getValueOrFallback(\n              driverResult.value,\n              opts?.fallback ?? opts?.defaultValue,\n            );\n\n            resultsMap.set(key, value);\n          });\n        }),\n      );\n\n      return orderedKeys.map((key) => ({\n        key,\n        value: resultsMap.get(key),\n      }));\n    },\n\n    getMeta: async (key) => {\n      const { driver, driverKey } = resolveKey(key);\n      return await getMeta(driver, driverKey);\n    },\n\n    getMetas: async (args) => {\n      const keys = args.map((arg) => {\n        const key = typeof arg === 'string' ? arg : arg.key;\n        const { driverArea, driverKey } = resolveKey(key);\n\n        return {\n          key,\n          driverArea,\n          driverKey,\n          driverMetaKey: getMetaKey(driverKey),\n        };\n      });\n\n      const areaToDriverMetaKeysMap = keys.reduce<\n        Partial<Record<StorageArea, (typeof keys)[number][]>>\n      >((map, key) => {\n        map[key.driverArea] ??= [];\n        map[key.driverArea]!.push(key);\n        return map;\n      }, {});\n\n      const resultsMap: Record<string, any> = {};\n      await Promise.all(\n        Object.entries(areaToDriverMetaKeysMap).map(async ([area, keys]) => {\n          const areaRes = await browser.storage[area as StorageArea].get(\n            keys.map((key) => key.driverMetaKey),\n          );\n          keys.forEach((key) => {\n            resultsMap[key.key] = areaRes[key.driverMetaKey] ?? {};\n          });\n        }),\n      );\n\n      return keys.map((key) => ({\n        key: key.key,\n        meta: resultsMap[key.key],\n      }));\n    },\n\n    setItem: async (key, value) => {\n      const { driver, driverKey } = resolveKey(key);\n      await setItem(driver, driverKey, value);\n    },\n\n    setItems: async (items) => {\n      const areaToKeyValueMap: Partial<\n        Record<StorageArea, Array<{ key: string; value: any }>>\n      > = {};\n      items.forEach((item) => {\n        const { driverArea, driverKey } = resolveKey(\n          'key' in item ? item.key : item.item.key,\n        );\n        areaToKeyValueMap[driverArea] ??= [];\n        areaToKeyValueMap[driverArea].push({\n          key: driverKey,\n          value: item.value,\n        });\n      });\n\n      await Promise.all(\n        Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {\n          const driver = getDriver(driverArea as StorageArea);\n          await driver.setItems(values);\n        }),\n      );\n    },\n\n    setMeta: async (key, properties) => {\n      const { driver, driverKey } = resolveKey(key);\n      await setMeta(driver, driverKey, properties);\n    },\n\n    setMetas: async (items) => {\n      const areaToMetaUpdatesMap: Partial<\n        Record<StorageArea, { key: string; properties: any }[]>\n      > = {};\n      items.forEach((item) => {\n        const { driverArea, driverKey } = resolveKey(\n          'key' in item ? item.key : item.item.key,\n        );\n        areaToMetaUpdatesMap[driverArea] ??= [];\n        areaToMetaUpdatesMap[driverArea].push({\n          key: driverKey,\n          properties: item.meta,\n        });\n      });\n\n      await Promise.all(\n        Object.entries(areaToMetaUpdatesMap).map(\n          async ([storageArea, updates]) => {\n            const driver = getDriver(storageArea as StorageArea);\n            const metaKeys = updates.map(({ key }) => getMetaKey(key));\n            const existingMetas = await driver.getItems(metaKeys);\n            const existingMetaMap = Object.fromEntries(\n              existingMetas.map(({ key, value }) => [key, getMetaValue(value)]),\n            );\n\n            const metaUpdates = updates.map(({ key, properties }) => {\n              const metaKey = getMetaKey(key);\n              return {\n                key: metaKey,\n                value: mergeMeta(existingMetaMap[metaKey] ?? {}, properties),\n              };\n            });\n\n            await driver.setItems(metaUpdates);\n          },\n        ),\n      );\n    },\n\n    removeItem: async (key, opts) => {\n      const { driver, driverKey } = resolveKey(key);\n      await removeItem(driver, driverKey, opts);\n    },\n\n    removeItems: async (keys) => {\n      const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};\n\n      keys.forEach((key) => {\n        let keyStr: StorageItemKey;\n        let opts: RemoveItemOptions | undefined;\n\n        if (typeof key === 'string') {\n          // key: string\n          keyStr = key;\n        } else if ('getValue' in key) {\n          // key: WxtStorageItem\n          keyStr = key.key;\n        } else if ('item' in key) {\n          // key: { item, options }\n          keyStr = key.item.key;\n          opts = key.options;\n        } else {\n          // key: { key, options }\n          keyStr = key.key;\n          opts = key.options;\n        }\n\n        const { driverArea, driverKey } = resolveKey(keyStr);\n        areaToKeysMap[driverArea] ??= [];\n        areaToKeysMap[driverArea].push(driverKey);\n\n        if (opts?.removeMeta) {\n          areaToKeysMap[driverArea].push(getMetaKey(driverKey));\n        }\n      });\n\n      await Promise.all(\n        Object.entries(areaToKeysMap).map(async ([driverArea, keys]) => {\n          const driver = getDriver(driverArea as StorageArea);\n          await driver.removeItems(keys);\n        }),\n      );\n    },\n\n    clear: async (base) => {\n      const driver = getDriver(base);\n      await driver.clear();\n    },\n\n    removeMeta: async (key, properties) => {\n      const { driver, driverKey } = resolveKey(key);\n      await removeMeta(driver, driverKey, properties);\n    },\n\n    snapshot: async (base, opts) => {\n      const driver = getDriver(base);\n      const data = await driver.snapshot();\n\n      opts?.excludeKeys?.forEach((key) => {\n        delete data[key];\n        delete data[getMetaKey(key)];\n      });\n\n      return data;\n    },\n\n    restoreSnapshot: async (base, data) => {\n      const driver = getDriver(base);\n      await driver.restoreSnapshot(data);\n    },\n\n    watch: (key, cb) => {\n      const { driver, driverKey } = resolveKey(key);\n      return watch(driver, driverKey, cb);\n    },\n\n    unwatch() {\n      Object.values(drivers).forEach((driver) => {\n        driver.unwatch();\n      });\n    },\n\n    defineItem: (key, opts?: WxtStorageItemOptions<any>) => {\n      const { driver, driverKey } = resolveKey(key);\n\n      const {\n        version: targetVersion = 1,\n        migrations = {},\n        onMigrationComplete,\n        debug = false,\n      } = opts ?? {};\n\n      if (targetVersion < 1) {\n        throw Error(\n          'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',\n        );\n      }\n\n      let needsVersionSet = false;\n\n      const migrate: WxtStorageItem<any, any>['migrate'] = async () => {\n        const driverMetaKey = getMetaKey(driverKey);\n        const [{ value }, { value: meta }] = await driver.getItems([\n          driverKey,\n          driverMetaKey,\n        ]);\n\n        // Used in setValue to also set the version when needed\n        needsVersionSet = value == null && meta?.v == null && !!targetVersion;\n\n        if (value == null) return;\n\n        const currentVersion = meta?.v ?? 1;\n        if (currentVersion > targetVersion) {\n          throw Error(\n            `Version downgrade detected (v${currentVersion} -> v${targetVersion}) for \"${key}\"`,\n          );\n        }\n\n        if (currentVersion === targetVersion) {\n          return;\n        }\n\n        if (debug) {\n          console.debug(\n            `[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,\n          );\n        }\n        const migrationsToRun = Array.from(\n          { length: targetVersion - currentVersion },\n          (_, i) => currentVersion + i + 1,\n        );\n        let migratedValue = value;\n        for (const migrateToVersion of migrationsToRun) {\n          try {\n            migratedValue =\n              (await migrations?.[migrateToVersion]?.(migratedValue)) ??\n              migratedValue;\n            if (debug) {\n              console.debug(\n                `[@wxt-dev/storage] Storage migration processed for version: v${migrateToVersion}`,\n              );\n            }\n          } catch (err) {\n            throw new MigrationError(key, migrateToVersion, {\n              cause: err,\n            });\n          }\n        }\n        await driver.setItems([\n          { key: driverKey, value: migratedValue },\n          { key: driverMetaKey, value: { ...meta, v: targetVersion } },\n        ]);\n\n        if (debug) {\n          console.debug(\n            `[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`,\n            { migratedValue },\n          );\n        }\n\n        onMigrationComplete?.(migratedValue, targetVersion);\n      };\n\n      const migrationsDone =\n        opts?.migrations == null\n          ? Promise.resolve()\n          : migrate().catch((err) => {\n              console.error(\n                `[@wxt-dev/storage] Migration failed for ${key}`,\n                err,\n              );\n            });\n\n      const initMutex = new Mutex();\n\n      const getFallback = () => opts?.fallback ?? opts?.defaultValue ?? null;\n\n      const getOrInitValue = () =>\n        initMutex.runExclusive(async () => {\n          const value = await driver.getItem<any>(driverKey);\n          // Don't init value if it already exists or the init function isn't provided\n          if (value != null || opts?.init == null) return value;\n\n          const newValue = await opts.init();\n          await driver.setItem<any>(driverKey, newValue);\n          if (value == null && targetVersion > 1) {\n            await setMeta(driver, driverKey, { v: targetVersion });\n          }\n          return newValue;\n        });\n\n      // Initialize the value once migrations have finished\n      migrationsDone.then(getOrInitValue);\n\n      return {\n        key,\n\n        get defaultValue() {\n          return getFallback();\n        },\n        get fallback() {\n          return getFallback();\n        },\n\n        getValue: async () => {\n          await migrationsDone;\n\n          if (opts?.init) {\n            return await getOrInitValue();\n          } else {\n            return await getItem(driver, driverKey, opts);\n          }\n        },\n\n        getMeta: async () => {\n          await migrationsDone;\n\n          return await getMeta(driver, driverKey);\n        },\n\n        setValue: async (value) => {\n          await migrationsDone;\n\n          if (needsVersionSet) {\n            needsVersionSet = false;\n            await Promise.all([\n              // Note: These calls cannot be done in a single `setItems` call;\n              // metadata needs to be merged together with existing data and\n              // setItems overwrites the whole value without merging.\n              setItem(driver, driverKey, value),\n              setMeta(driver, driverKey, { v: targetVersion }),\n            ]);\n          } else {\n            await setItem(driver, driverKey, value);\n          }\n        },\n\n        setMeta: async (properties) => {\n          await migrationsDone;\n\n          return await setMeta(driver, driverKey, properties);\n        },\n\n        removeValue: async (opts) => {\n          await migrationsDone;\n\n          return await removeItem(driver, driverKey, opts);\n        },\n\n        removeMeta: async (properties) => {\n          await migrationsDone;\n\n          return await removeMeta(driver, driverKey, properties);\n        },\n\n        watch: (cb) =>\n          watch(driver, driverKey, (newValue, oldValue) =>\n            cb(newValue ?? getFallback(), oldValue ?? getFallback()),\n          ),\n\n        migrate,\n      };\n    },\n  };\n}\n\nfunction createDriver(storageArea: StorageArea): WxtStorageDriver {\n  const getStorageArea = () => {\n    if (browser.runtime == null) {\n      throw Error(`'wxt/storage' must be loaded in a web extension environment\n\n - If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371\n - If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html\n`);\n    }\n\n    if (browser.storage == null) {\n      throw Error(\n        \"You must add the 'storage' permission to your manifest to use 'wxt/storage'\",\n      );\n    }\n\n    const area = browser.storage[storageArea];\n    if (area == null)\n      throw Error(`\"browser.storage.${storageArea}\" is undefined`);\n\n    return area;\n  };\n\n  const watchListeners = new Set<(changes: StorageAreaChanges) => void>();\n\n  return {\n    getItem: async (key) => {\n      const res = await getStorageArea().get<Record<string, any>>(key);\n      return res[key];\n    },\n\n    getItems: async (keys) => {\n      const result = await getStorageArea().get(keys);\n      return keys.map((key) => ({ key, value: result[key] ?? null }));\n    },\n\n    setItem: async (key, value) => {\n      if (value == null) {\n        await getStorageArea().remove(key);\n      } else {\n        await getStorageArea().set({ [key]: value });\n      }\n    },\n\n    setItems: async (values) => {\n      const map = values.reduce<Record<string, unknown>>(\n        (map, { key, value }) => {\n          map[key] = value;\n          return map;\n        },\n        {},\n      );\n\n      await getStorageArea().set(map);\n    },\n\n    removeItem: async (key) => {\n      await getStorageArea().remove(key);\n    },\n\n    removeItems: async (keys) => {\n      await getStorageArea().remove(keys);\n    },\n\n    clear: async () => {\n      await getStorageArea().clear();\n    },\n\n    snapshot: async () => {\n      return await getStorageArea().get();\n    },\n\n    restoreSnapshot: async (data) => {\n      await getStorageArea().set(data);\n    },\n\n    watch(key, cb) {\n      const listener = (changes: StorageAreaChanges) => {\n        const change = changes[key] as {\n          newValue?: any;\n          oldValue?: any | null;\n        } | null;\n\n        if (change == null || dequal(change.newValue, change.oldValue)) return;\n\n        cb(change.newValue ?? null, change.oldValue ?? null);\n      };\n\n      getStorageArea().onChanged.addListener(listener);\n      watchListeners.add(listener);\n\n      return () => {\n        getStorageArea().onChanged.removeListener(listener);\n        watchListeners.delete(listener);\n      };\n    },\n\n    unwatch() {\n      watchListeners.forEach((listener) => {\n        getStorageArea().onChanged.removeListener(listener);\n      });\n      watchListeners.clear();\n    },\n  };\n}\n\nexport interface WxtStorage {\n  /**\n   * Get an item from storage, or return `null` if it doesn't exist.\n   *\n   * @example\n   *   await storage.getItem<number>('local:installDate');\n   */\n  getItem<TValue>(\n    key: StorageItemKey,\n    opts: GetItemOptions<TValue> & { fallback: TValue },\n  ): Promise<TValue>;\n\n  getItem<TValue>(\n    key: StorageItemKey,\n    opts?: GetItemOptions<TValue>,\n  ): Promise<TValue | null>;\n\n  /**\n   * Get multiple items from storage. The return order is guaranteed to be the\n   * same as the order requested.\n   *\n   * @example\n   *   await storage.getItems(['local:installDate', 'session:someCounter']);\n   */\n  getItems(\n    keys: Array<\n      | StorageItemKey\n      | WxtStorageItem<any, any>\n      | { key: StorageItemKey; options?: GetItemOptions<any> }\n    >,\n  ): Promise<Array<{ key: StorageItemKey; value: any }>>;\n\n  /**\n   * Return an object containing metadata about the key. Object is stored at\n   * `key + \"$\"`. If value is not an object, it returns an empty object.\n   *\n   * @example\n   *   await storage.getMeta('local:installDate');\n   */\n  getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;\n\n  /**\n   * Get the metadata of multiple storage items.\n   *\n   * @param keys List of keys or items to get the metadata of.\n   * @returns An array containing storage keys and their metadata.\n   */\n  getMetas(\n    keys: Array<StorageItemKey | WxtStorageItem<any, any>>,\n  ): Promise<Array<{ key: StorageItemKey; meta: any }>>;\n\n  /**\n   * Set a value in storage. Setting a value to `null` or `undefined` is\n   * equivalent to calling `removeItem`.\n   *\n   * @example\n   *   await storage.setItem<number>('local:installDate', Date.now());\n   */\n  setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;\n\n  /**\n   * Set multiple values in storage. If a value is set to `null` or `undefined`,\n   * the key is removed.\n   *\n   * @example\n   *   await storage.setItem([\n   *   { key: \"local:installDate\", value: Date.now() },\n   *   { key: \"session:someCounter, value: 5 },\n   *   ]);\n   */\n  setItems(\n    values: Array<\n      | { key: StorageItemKey; value: any }\n      | { item: WxtStorageItem<any, any>; value: any }\n    >,\n  ): Promise<void>;\n\n  /**\n   * Sets metadata properties. If some properties are already set, but are not\n   * included in the `properties` parameter, they will not be removed.\n   *\n   * @example\n   *   await storage.setMeta('local:installDate', { appVersion });\n   */\n  setMeta<T extends Record<string, unknown>>(\n    key: StorageItemKey,\n    properties: T | null,\n  ): Promise<void>;\n\n  /**\n   * Set the metadata of multiple storage items.\n   *\n   * @param metas List of storage keys or items and metadata to set for each.\n   */\n  setMetas(\n    metas: Array<\n      | { key: StorageItemKey; meta: Record<string, any> }\n      | { item: WxtStorageItem<any, any>; meta: Record<string, any> }\n    >,\n  ): Promise<void>;\n\n  /**\n   * Removes an item from storage.\n   *\n   * @example\n   *   await storage.removeItem('local:installDate');\n   */\n  removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;\n\n  /** Remove a list of keys from storage. */\n  removeItems(\n    keys: Array<\n      | StorageItemKey\n      | WxtStorageItem<any, any>\n      | { key: StorageItemKey; options?: RemoveItemOptions }\n      | { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }\n    >,\n  ): Promise<void>;\n\n  /** Removes all items from the provided storage area. */\n  clear(base: StorageArea): Promise<void>;\n\n  /**\n   * Remove the entire metadata for a key, or specific properties by name.\n   *\n   * @example\n   *   // Remove all metadata properties from the item\n   *   await storage.removeMeta('local:installDate');\n   *\n   *   // Remove only specific the \"v\" field\n   *   await storage.removeMeta('local:installDate', 'v');\n   */\n  removeMeta(\n    key: StorageItemKey,\n    properties?: string | string[],\n  ): Promise<void>;\n\n  /** Return all the items in storage. */\n  snapshot(\n    base: StorageArea,\n    opts?: SnapshotOptions,\n  ): Promise<Record<string, unknown>>;\n\n  /**\n   * Restores the results of `snapshot`. If new properties have been saved since\n   * the snapshot, they are not overridden. Only values existing in the snapshot\n   * are overridden.\n   */\n  restoreSnapshot(base: StorageArea, data: any): Promise<void>;\n\n  /** Watch for changes to a specific key in storage. */\n  watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;\n\n  /** Remove all watch listeners. */\n  unwatch(): void;\n\n  /**\n   * Define a storage item with a default value, type, or versioning.\n   *\n   * Read full docs: https://wxt.dev/storage.html#defining-storage-items\n   */\n  defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(\n    key: StorageItemKey,\n  ): WxtStorageItem<TValue | null, TMetadata>;\n  defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(\n    key: StorageItemKey,\n    options: WxtStorageItemOptions<TValue> & { fallback: TValue },\n  ): WxtStorageItem<TValue, TMetadata>;\n  defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(\n    key: StorageItemKey,\n    options: WxtStorageItemOptions<TValue> & { defaultValue: TValue },\n  ): WxtStorageItem<TValue, TMetadata>;\n  defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(\n    key: StorageItemKey,\n    options: WxtStorageItemOptions<TValue> & {\n      init: () => TValue | Promise<TValue>;\n    },\n  ): WxtStorageItem<TValue, TMetadata>;\n  defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(\n    key: StorageItemKey,\n    options: WxtStorageItemOptions<TValue>,\n  ): WxtStorageItem<TValue | null, TMetadata>;\n}\n\ninterface WxtStorageDriver {\n  getItem<T>(key: string): Promise<T | null>;\n  getItems(keys: string[]): Promise<{ key: string; value: any }[]>;\n  setItem<T>(key: string, value: T | null): Promise<void>;\n  setItems(values: Array<{ key: string; value: any }>): Promise<void>;\n  removeItem(key: string): Promise<void>;\n  removeItems(keys: string[]): Promise<void>;\n  clear(): Promise<void>;\n  snapshot(): Promise<Record<string, unknown>>;\n  restoreSnapshot(data: Record<string, unknown>): Promise<void>;\n  watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;\n  unwatch(): void;\n}\n\nexport interface WxtStorageItem<\n  TValue,\n  TMetadata extends Record<string, unknown>,\n> {\n  /** The storage key passed when creating the storage item. */\n  key: StorageItemKey;\n\n  /** @deprecated Renamed to fallback, use it instead. */\n  defaultValue: TValue;\n\n  /** The value provided by the `fallback` option. */\n  fallback: TValue;\n\n  /** Get the latest value from storage. */\n  getValue(): Promise<TValue>;\n\n  /** Get metadata. */\n  getMeta(): Promise<NullablePartial<TMetadata>>;\n\n  /** Set the value in storage. */\n  setValue(value: TValue): Promise<void>;\n\n  /** Set metadata properties. */\n  setMeta(properties: NullablePartial<TMetadata>): Promise<void>;\n\n  /** Remove the value from storage. */\n  removeValue(opts?: RemoveItemOptions): Promise<void>;\n\n  /** Remove all metadata or certain properties from metadata. */\n  removeMeta(properties?: string[]): Promise<void>;\n\n  /** Listen for changes to the value in storage. */\n  watch(cb: WatchCallback<TValue>): Unwatch;\n\n  /**\n   * If there are migrations defined on the storage item, migrate to the latest\n   * version.\n   *\n   * **This function is ran automatically whenever the extension updates**, so\n   * you don't have to call it manually.\n   */\n  migrate(): Promise<void>;\n}\n\nexport type StorageArea = 'local' | 'session' | 'sync' | 'managed';\nexport type StorageItemKey = `${StorageArea}:${string}`;\n\nexport interface GetItemOptions<T> {\n  /** @deprecated Renamed to `fallback`, use it instead. */\n  defaultValue?: T;\n\n  /** Default value returned when `getItem` would otherwise return `null`. */\n  fallback?: T;\n}\n\nexport interface RemoveItemOptions {\n  /**\n   * Optionally remove metadata when deleting a key.\n   *\n   * @default false\n   */\n  removeMeta?: boolean;\n}\n\nexport interface SnapshotOptions {\n  /**\n   * Exclude a list of keys. The storage area prefix should be removed since the\n   * snapshot is for a specific storage area already.\n   */\n  excludeKeys?: string[];\n}\n\nexport interface WxtStorageItemOptions<T> {\n  /** @deprecated Renamed to `fallback`, use it instead. */\n  defaultValue?: T;\n\n  /** Default value returned when `getValue` would otherwise return `null`. */\n  fallback?: T;\n\n  /**\n   * If passed, a value in storage will be initialized immediately after\n   * defining the storage item. This function returns the value that will be\n   * saved to storage during the initialization process if a value doesn't\n   * already exist.\n   */\n  init?: () => T | Promise<T>;\n\n  /**\n   * Provide a version number for the storage item to enable migrations. When\n   * changing the version in the future, migration functions will be ran on\n   * application startup.\n   */\n  version?: number;\n\n  /**\n   * A map of version numbers to the functions used to migrate the data to that\n   * version.\n   */\n  migrations?: Record<number, (oldValue: any) => any>;\n\n  /**\n   * Print debug logs, such as migration process.\n   *\n   * @default false\n   */\n  debug?: boolean;\n\n  /** A callback function that runs on migration complete. */\n  onMigrationComplete?: (migratedValue: T, targetVersion: number) => void;\n}\n\nexport type StorageAreaChanges = {\n  [key: string]: Browser.storage.StorageChange;\n};\n\n/**\n * Same as `Partial`, but includes `| null`. It makes all the properties of an\n * object optional and nullable.\n */\ntype NullablePartial<T> = {\n  [key in keyof T]+?: T[key] | undefined | null;\n};\n\n/** Callback called when a value in storage is changed. */\nexport type WatchCallback<T> = (newValue: T, oldValue: T) => void;\n\n/** Call to remove a watch listener */\nexport type Unwatch = () => void;\n\nexport class MigrationError extends Error {\n  constructor(\n    public key: string,\n    public version: number,\n    options?: ErrorOptions,\n  ) {\n    super(`v${version} migration failed for \"${key}\"`, options);\n  }\n}\n"
  },
  {
    "path": "packages/storage/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/storage/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config';\n\nexport default defineProject({\n  test: {\n    mockReset: true,\n    restoreMocks: true,\n    setupFiles: ['vitest.setup.ts'],\n  },\n});\n"
  },
  {
    "path": "packages/storage/vitest.setup.ts",
    "content": "import { fakeBrowser } from '@webext-core/fake-browser';\nimport { vi } from 'vitest';\n\nvi.stubGlobal('chrome', fakeBrowser);\n"
  },
  {
    "path": "packages/unocss/CHANGELOG.md",
    "content": "# Changelog\n\n## v1.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/unocss-v1.0.0...unocss-v1.0.1)\n\n### 🩹 Fixes\n\n- Respect `configOrPath` for dev server ([#1169](https://github.com/wxt-dev/wxt/pull/1169))\n\n### 📖 Documentation\n\n- Use full URLs in README so they work on the docs site ([d20793d](https://github.com/wxt-dev/wxt/commit/d20793d))\n- Fix unocss readme ([#1329](https://github.com/wxt-dev/wxt/pull/1329))\n\n### 🏡 Chore\n\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Ntnyq ([@ntnyq](http://github.com/ntnyq))"
  },
  {
    "path": "packages/unocss/README.md",
    "content": "# WXT UnoCSS\n\nUse UnoCSS in your WXT extension!\n\n## Usage\n\nInstall the package:\n\n```sh\nnpm i --save-dev @wxt-dev/unocss unocss\npnpm i -D @wxt-dev/unocss unocss\nyarn add --dev @wxt-dev/unocss unocss\nbun i -D @wxt-dev/unocss unocss\n```\n\nAdd the module to `wxt.config.ts`:\n\n```ts\nexport default defineConfig({\n  modules: ['@wxt-dev/unocss'],\n});\n```\n\nNow in your entrypoint, import UnoCSS:\n\n```ts\nimport 'virtual:uno.css';\n```\n\n> [!IMPORTANT]\n> While in dev mode, you may see a warning about `uno.css` not being found. This is because in development, we don't know which files should be injected with UnoCSS styles. The warning can be safely ignored as the styles will be properly applied during the build process.\n\n## Configuration\n\nThe module can be configured via the `unocss` config:\n\n```ts\nexport default defineConfig({\n  modules: ['@wxt-dev/unocss'],\n  unocss: {\n    // Exclude unocss from running for the background\n    excludeEntrypoints: ['background'],\n  },\n});\n```\n\nOptions have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/unocss/src/index.ts).\n"
  },
  {
    "path": "packages/unocss/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/unocss\",\n  \"description\": \"UnoCSS integration for WXT\",\n  \"version\": \"1.0.1\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/unocss\"\n  },\n  \"homepage\": \"http://wxt.dev/guide/unocss.html\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"unocss\",\n    \"css\"\n  ],\n  \"author\": {\n    \"name\": \"Florian Metz\",\n    \"email\": \"me@timeraa.dev\"\n  },\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"buildc -- tsdown\",\n    \"check\": \"buildc --deps-only -- check\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"unocss\": \">=0.60.0\",\n    \"wxt\": \">=0.19.0\"\n  },\n  \"devDependencies\": {\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"unocss\": \"^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0\",\n    \"wxt\": \"workspace:*\"\n  },\n  \"dependencies\": {\n    \"defu\": \"^6.1.4\",\n    \"tinyglobby\": \"^0.2.15\"\n  }\n}\n"
  },
  {
    "path": "packages/unocss/src/index.ts",
    "content": "import 'wxt';\nimport { defineWxtModule } from 'wxt/modules';\nimport defu from 'defu';\nimport UnoCSS from 'unocss/vite';\n\nexport default defineWxtModule<UnoCSSOptions>({\n  name: '@wxt-dev/unocss',\n  configKey: 'unocss',\n  async setup(wxt, options) {\n    const resolvedOptions = defu<Required<UnoCSSOptions>, UnoCSSOptions[]>(\n      options,\n      {\n        enabled: true,\n        excludeEntrypoints: ['background'],\n        configOrPath: undefined,\n      },\n    );\n\n    if (!resolvedOptions.enabled)\n      return wxt.logger.warn(`\\`[unocss]\\` ${this.name} disabled`);\n\n    const excludedEntrypoints = new Set(resolvedOptions.excludeEntrypoints);\n    if (wxt.config.debug) {\n      wxt.logger.debug(\n        `\\`[unocss]\\` Excluded entrypoints:`,\n        [...excludedEntrypoints].join(', '),\n      );\n    }\n\n    wxt.hooks.hook('vite:devServer:extendConfig', (config) => {\n      config.plugins?.push(UnoCSS(resolvedOptions.configOrPath));\n    });\n\n    wxt.hooks.hook('vite:build:extendConfig', async (entries, config) => {\n      if (entries.every((entry) => excludedEntrypoints.has(entry.name))) return;\n      config.plugins?.push(UnoCSS(resolvedOptions.configOrPath));\n    });\n  },\n});\n\n/** Options for the UnoCSS module */\nexport interface UnoCSSOptions<Theme extends object = object> {\n  /**\n   * Enable UnoCSS\n   *\n   * @default true\n   */\n  enabled?: boolean;\n  /**\n   * List of entrypoint names that UnoCSS is not used in. By default, the UnoCSS\n   * vite plugin is added to all build steps, but this option is used to exclude\n   * it from specific builds.\n   *\n   * @example\n   *   {undefined} ('popup',\n   *   'options');\n   *\n   * @default [ ]\n   */\n  excludeEntrypoints?: string[];\n  /**\n   * The path to your `unocss.config.ts` file, relative to <rootDir>, or inline\n   * configuration.\n   */\n  configOrPath?: Parameters<typeof UnoCSS<Theme>>[0];\n}\n\ndeclare module 'wxt' {\n  export interface InlineConfig {\n    unocss?: UnoCSSOptions;\n  }\n}\n"
  },
  {
    "path": "packages/unocss/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/webextension-polyfill/README.md",
    "content": "# `@wxt-dev/webextension-polyfill`\n\nConfigures `wxt/browser` to import `browser` from [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) instead of using the regular `chrome`/`browser` globals WXT normally provides.\n\n## Usage\n\n```sh\npnpm i @wxt-dev/webextension-polyfill webextension-polyfill\n```\n\nThen add the module to your config:\n\n```ts\n// wxt.config.ts\nexport default defineConfig({\n  modules: ['@wxt-dev/webextension-polyfill'],\n});\n```\n"
  },
  {
    "path": "packages/webextension-polyfill/entrypoints/content/index.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*/*'],\n  async main() {\n    console.log(browser.runtime.id);\n  },\n});\n"
  },
  {
    "path": "packages/webextension-polyfill/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/webextension-polyfill/entrypoints/popup/main.ts",
    "content": "const root = document.getElementById('app')!;\n\nroot.textContent = browser.runtime.id;\n"
  },
  {
    "path": "packages/webextension-polyfill/modules/webextension-polyfill/browser.ts",
    "content": "export { default as browser } from 'webextension-polyfill';\n"
  },
  {
    "path": "packages/webextension-polyfill/modules/webextension-polyfill/index.ts",
    "content": "import 'wxt';\nimport { addViteConfig, defineWxtModule } from 'wxt/modules';\nimport { resolve } from 'node:path';\n\nexport default defineWxtModule({\n  name: '@wxt-dev/webextension-polyfill',\n  setup(wxt) {\n    addViteConfig(wxt, () => ({\n      resolve: {\n        alias: {\n          'wxt/browser': process.env.NPM\n            ? '@wxt-dev/webextension-polyfill/browser'\n            : resolve(__dirname, 'browser.ts'),\n        },\n      },\n    }));\n  },\n});\n"
  },
  {
    "path": "packages/webextension-polyfill/package.json",
    "content": "{\n  \"name\": \"@wxt-dev/webextension-polyfill\",\n  \"description\": \"Use webextension-polyfill with WXT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\",\n    \"directory\": \"packages/webextension-polyfill\"\n  },\n  \"homepage\": \"https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md\",\n  \"keywords\": [\n    \"wxt\",\n    \"module\",\n    \"webextension-polyfill\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./browser\": {\n      \"types\": \"./dist/browser.d.mts\",\n      \"default\": \"./dist/browser.mjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"check\": \"pnpm build && check\",\n    \"build\": \"buildc -- tsdown\",\n    \"prepare\": \"buildc --deps-only -- wxt prepare\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"peerDependencies\": {\n    \"webextension-polyfill\": \"*\",\n    \"wxt\": \">=0.20.0\"\n  },\n  \"devDependencies\": {\n    \"@types/webextension-polyfill\": \"^0.12.5\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"webextension-polyfill\": \"^0.12.0\",\n    \"wxt\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/webextension-polyfill/public/.keep",
    "content": ""
  },
  {
    "path": "packages/webextension-polyfill/tsconfig.json",
    "content": "{\n  \"extends\": [\"../../tsconfig.base.json\", \"./.wxt/tsconfig.json\"],\n  \"exclude\": [\"node_modules/**\", \"dist/**\"]\n}\n"
  },
  {
    "path": "packages/webextension-polyfill/tsdown.config.ts",
    "content": "import { defineConfig } from 'tsdown';\n\nexport default defineConfig({\n  entry: {\n    index: 'modules/webextension-polyfill/index.ts',\n    browser: 'modules/webextension-polyfill/browser.ts',\n  },\n  define: {\n    'process.env.NPM': 'true',\n  },\n});\n"
  },
  {
    "path": "packages/wxt/.oxlintignore",
    "content": "src/core/utils/building/__tests__/test-entrypoints\n"
  },
  {
    "path": "packages/wxt/CHANGELOG.md",
    "content": "# Changelog\n\n## v0.20.20\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.19...wxt-v0.20.20)\n\n### 🩹 Fixes\n\n- Unlisted script return values broken with Vite 8 sourcemaps ([#2197](https://github.com/wxt-dev/wxt/pull/2197))\n\n### 📖 Documentation\n\n- Add Dymo extension ID to UsingWxtSection.vue ([#2187](https://github.com/wxt-dev/wxt/pull/2187))\n- Add Extension Rank Checker to the list of extensions ([#2193](https://github.com/wxt-dev/wxt/pull/2193))\n\n### 🏡 Chore\n\n- **deps:** Add vite-node 6 support ([64b68671](https://github.com/wxt-dev/wxt/commit/64b68671))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Joseph Hu ([@KiJO94GO](https://github.com/KiJO94GO))\n- FJRG2007 ツ ([@FJRG2007](https://github.com/FJRG2007))\n- Hampus Tågerud ([@hampustagerud](https://github.com/hampustagerud))\n\n## v0.20.19\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.18...wxt-v0.20.19)\n\n### 🚀 Enhancements\n\n- Vite 8 support ([bfd4af5d](https://github.com/wxt-dev/wxt/commit/bfd4af5d))\n\n### 🔥 Performance\n\n- Use filter to improve plugin performance with rolldown ([#1787](https://github.com/wxt-dev/wxt/pull/1787))\n\n### 🩹 Fixes\n\n- List eslint 10 as supported ([55dc263d](https://github.com/wxt-dev/wxt/commit/55dc263d))\n\n### 📖 Documentation\n\n- Added \"Scrape Similar\" to the homepage ([#2158](https://github.com/wxt-dev/wxt/pull/2158))\n- Added \"isTrust\" to the homepage ([#2161](https://github.com/wxt-dev/wxt/pull/2161))\n\n### 🏡 Chore\n\n- Add `prettier-plugin-jsdoc` to project ([#2171](https://github.com/wxt-dev/wxt/pull/2171))\n- Replace fast-glob and ora with lighter alternatives ([#2173](https://github.com/wxt-dev/wxt/pull/2173))\n- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))\n- Replace `fs-extra` with `node:fs/promises` ([#2174](https://github.com/wxt-dev/wxt/pull/2174))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](https://github.com/Timeraa))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Etoome ([@etoome](https://github.com/etoome))\n- Kuba ([@zizzfizzix](https://github.com/zizzfizzix))\n\n## v0.20.18\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.17...wxt-v0.20.18)\n\n### 🚀 Enhancements\n\n- Create tests for all func of `network.ts` ([#2132](https://github.com/wxt-dev/wxt/pull/2132))\n- Use Navigation API for location change detection with polling fallback ([#2136](https://github.com/wxt-dev/wxt/pull/2136))\n- **modules:** Add support for augumenting entrypoint options ([#2149](https://github.com/wxt-dev/wxt/pull/2149))\n- New `@wxt-dev/is-background` package ([#2152](https://github.com/wxt-dev/wxt/pull/2152))\n- Add `globalName` entrypoint option. ([#2017](https://github.com/wxt-dev/wxt/pull/2017))\n\n### 🩹 Fixes\n\n- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))\n- Enable mv3 dev mode with firefox 147 ([#2135](https://github.com/wxt-dev/wxt/pull/2135))\n- **types:** Add type safety to `browser.runtime.executeScript` `files` option ([#2142](https://github.com/wxt-dev/wxt/pull/2142))\n- **types:** Include CSS entrypoints in PublicPath generation ([#2150](https://github.com/wxt-dev/wxt/pull/2150))\n- Fix submit command ([#2157](https://github.com/wxt-dev/wxt/pull/2157))\n\n### 📖 Documentation\n\n- Added \"Glossy New Tab\" to the homepage ([#2133](https://github.com/wxt-dev/wxt/pull/2133))\n- Add All API Hub to showcase extensions ([#2137](https://github.com/wxt-dev/wxt/pull/2137))\n- Add `wxt-local-analytics` to community resources ([#2141](https://github.com/wxt-dev/wxt/pull/2141))\n- Add deep outline to modules page to show recipes ([653608c9](https://github.com/wxt-dev/wxt/commit/653608c9))\n\n### 🏡 Chore\n\n- Add missing `async` to network.ts ([#2107](https://github.com/wxt-dev/wxt/pull/2107))\n- Make type for `__ENTRYPOINT__` of vitest.globalSetup.ts ([#2128](https://github.com/wxt-dev/wxt/pull/2128))\n- Remove unnecessary `@ts-ignore` of `builders/vite` index.ts ([#2127](https://github.com/wxt-dev/wxt/pull/2127))\n- Rename `zipdir` to `zipDir` ([#2126](https://github.com/wxt-dev/wxt/pull/2126))\n- Simplify imports in `wxt/e2e` ([#2122](https://github.com/wxt-dev/wxt/pull/2122))\n- Remove `@ts-expect-error` from manifest.test.ts and fix typo ([#2123](https://github.com/wxt-dev/wxt/pull/2123))\n\n### 🤖 CI\n\n- Fix VHS action ([#2155](https://github.com/wxt-dev/wxt/pull/2155))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Jonathan Viney ([@jviney](https://github.com/jviney))\n- Tam Dang ([@dahomita](https://github.com/dahomita))\n- Nick Doan ([@nickbar01234](https://github.com/nickbar01234))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Anthony Ciccarello ([@aciccarello](https://github.com/aciccarello))\n- Henrik Wenz ([@HaNdTriX](https://github.com/HaNdTriX))\n- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))\n- Oleksii ([@Aler1x](https://github.com/Aler1x))\n- Qixing-jk <vq3d5d8c@duck.com>\n- Muzammil ([@oyzamil](https://github.com/oyzamil))\n\n## v0.20.17\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.16...wxt-v0.20.17)\n\n### 🩹 Fixes\n\n- Update left-over CJS require of `publish-browser-extension` to ESM ([#2120](https://github.com/wxt-dev/wxt/pull/2120))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.16\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.15...wxt-v0.20.16)\n\n### 🩹 Fixes\n\n- Revert \"rename `name` to `names` prop of `rollup` to fix deprecated warning \" ([#2106](https://github.com/wxt-dev/wxt/pull/2106))\n\n### 🏡 Chore\n\n- Speed up E2E tests by skipping `pnpm install` when possible ([#2113](https://github.com/wxt-dev/wxt/pull/2113))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.15\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.14...wxt-v0.20.15)\n\n### 🚀 Enhancements\n\n- Export `normalizePath` from `wxt` module ([#2080](https://github.com/wxt-dev/wxt/pull/2080))\n- **popup:** Add Firefox `default_area` and `theme_icons` support ([#2097](https://github.com/wxt-dev/wxt/pull/2097))\n- Add log `--level` cli option ([#1973](https://github.com/wxt-dev/wxt/pull/1973))\n\n### 🩹 Fixes\n\n- Content script is incorrectly invalidated when injected multiple times ([#2035](https://github.com/wxt-dev/wxt/pull/2035))\n- Use `uiContainer` to apply position when creating shadow root ui ([#2036](https://github.com/wxt-dev/wxt/pull/2036))\n- Make `ExtensionRunner#closeBrowser` optional ([#2092](https://github.com/wxt-dev/wxt/pull/2092))\n- Rename `name` to `names` prop of `rollup` to fix deprecated warning ([#2106](https://github.com/wxt-dev/wxt/pull/2106))\n\n### 💅 Refactors\n\n- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))\n- Cleanup esm ([#1950](https://github.com/wxt-dev/wxt/pull/1950))\n- `async`/`await` cleanup ([#2088](https://github.com/wxt-dev/wxt/pull/2088))\n- Cleanup unnecssary checks ([#2089](https://github.com/wxt-dev/wxt/pull/2089))\n- Simplify import paths ([#2093](https://github.com/wxt-dev/wxt/pull/2093))\n\n### 📖 Documentation\n\n- Fix JSDoc ([6ae5fb5e](https://github.com/wxt-dev/wxt/commit/6ae5fb5e))\n\n### 🏡 Chore\n\n- Cleanup comment line lengths ([#2090](https://github.com/wxt-dev/wxt/pull/2090))\n- Cleanup `@ts-*` comments ([#2094](https://github.com/wxt-dev/wxt/pull/2094))\n- Update JSDoc to use `@internal` ([#2091](https://github.com/wxt-dev/wxt/pull/2091))\n- Fix typos ([#2086](https://github.com/wxt-dev/wxt/pull/2086))\n- Remove unused code ([#2087](https://github.com/wxt-dev/wxt/pull/2087))\n- Simplify imports of `cli/__tests__/index.ts` ([#2104](https://github.com/wxt-dev/wxt/pull/2104))\n\n### ❤️ Contributors\n\n- Nick Doan ([@nickbar01234](https://github.com/nickbar01234))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Wotan-allfather <wotan-ai@proton.me>\n- Namu ([@namuorg](https://github.com/namuorg))\n- Eli ([@lishaduck](https://github.com/lishaduck))\n\n## v0.20.14\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.13...wxt-v0.20.14)\n\n### 🚀 Enhancements\n\n- Add `onBeforeMount` callback to `createIframeUi` options ([#1899](https://github.com/wxt-dev/wxt/pull/1899))\n\n### 🩹 Fixes\n\n- Remove failing test ([6fb4cee4](https://github.com/wxt-dev/wxt/commit/6fb4cee4))\n- Rename and make firefoxPrefs in web-ext.config.ts work ([#2068](https://github.com/wxt-dev/wxt/pull/2068))\n- **content-script-context:** Handle undefined browser.runtime ([#2042](https://github.com/wxt-dev/wxt/pull/2042))\n- `MaxListenersExceededWarning` ([#2058](https://github.com/wxt-dev/wxt/pull/2058))\n\n### 📖 Documentation\n\n- Fix alignment of 2 last comments of regex patterns ([#2012](https://github.com/wxt-dev/wxt/pull/2012))\n\n### 🏡 Chore\n\n- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))\n- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))\n- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))\n- Upgrade major deps ([#2070](https://github.com/wxt-dev/wxt/pull/2070))\n- Refresh lockfile and upgrade subdependencies ([#2071](https://github.com/wxt-dev/wxt/pull/2071))\n\n### ❤️ Contributors\n\n- Dareka ([@darekadareka](https://github.com/darekadareka))\n- Mark ([@mjfaga](https://github.com/mjfaga))\n- ariasuni ([@ariasuni](https://github.com/ariasuni))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Bohuslavsemenov ([@bohuslavsemenov](https://github.com/bohuslavsemenov))\n\n## v0.20.13\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.12...wxt-v0.20.13)\n\n### 🚀 Enhancements\n\n- Remove script element immediately in injectScript ([#1761](https://github.com/wxt-dev/wxt/pull/1761))\n- Add `modifyScript` option to `injectScript` ([#1762](https://github.com/wxt-dev/wxt/pull/1762))\n- Make `injectScript` return the created script element ([#1838](https://github.com/wxt-dev/wxt/pull/1838))\n- Support `.wxtrc` config file ([#1833](https://github.com/wxt-dev/wxt/pull/1833))\n\n### 🩹 Fixes\n\n- Make `injectScript` wait until script is actually loaded ([#1763](https://github.com/wxt-dev/wxt/pull/1763))\n- Don't return promises from unlisted scripts that do not have an async `main` function ([#1907](https://github.com/wxt-dev/wxt/pull/1907))\n\n### 💅 Refactors\n\n- Use `script.text` instead of `innerHTML` in `injectScript` ([#1764](https://github.com/wxt-dev/wxt/pull/1764))\n\n### ❤️ Contributors\n\n- Rxliuli ([@rxliuli](https://github.com/rxliuli))\n- Sebastian Landwehr <info@sebastianlandwehr.com>\n- Johan Kiviniemi ([@ion1](https://github.com/ion1))\n\n## v0.20.12\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.11...wxt-v0.20.12)\n\n### 🚀 Enhancements\n\n- Remove `data-wxt-*` attributes ([#1913](https://github.com/wxt-dev/wxt/pull/1913))\n- Default to using `use_dynamic_url: true` for content script css files ([#1993](https://github.com/wxt-dev/wxt/pull/1993))\n\n### 🩹 Fixes\n\n- Wxt normal logs are drowned by `dotenv@17.0.0` ads ([#1883](https://github.com/wxt-dev/wxt/pull/1883))\n- Optimize `splitShadowRootCss` ([#1934](https://github.com/wxt-dev/wxt/pull/1934))\n- `wxt prepare` fails with the error \"__vite_ssr_exportName__ is not defined\" when using Vite 7 ([#1884](https://github.com/wxt-dev/wxt/pull/1884))\n\n### 🏡 Chore\n\n- Use `linkedom` a test instead of `happy-dom` ([#1957](https://github.com/wxt-dev/wxt/pull/1957))\n- Add support for `vite-node` v5 ([#2001](https://github.com/wxt-dev/wxt/pull/2001))\n- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))\n- **dev-deps:** Upgrade `happy-dom` to address CVE-2025-61927 ([#2002](https://github.com/wxt-dev/wxt/pull/2002))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Jaguar Zhou ([@aiktb](https://github.com/aiktb))\n- Alexander Harding <noreply@harding.dev>\n- Florian Kühne ([@ZerGo0](https://github.com/ZerGo0))\n\n## v0.20.11\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.10...wxt-v0.20.11)\n\n### 🩹 Fixes\n\n- Split `wxt/testing` into separate modules to fix issues with `jsdom` and `happy-dom` ([#1844](https://github.com/wxt-dev/wxt/pull/1844))\n- `input_components` is supported by mv3 ([#1881](https://github.com/wxt-dev/wxt/pull/1881))\n\n### 🏡 Chore\n\n- **deps:** Upgrade project subdependencies ([#1882](https://github.com/wxt-dev/wxt/pull/1882))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Qijia Liu <liumeo@pku.edu.cn>\n- Marcellino Ornelas ([@marcellino-ornelas](https://github.com/marcellino-ornelas))\n\n## v0.20.10\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.9...wxt-v0.20.10)\n\n### 🏡 Chore\n\n- **deps:** Upgrade non-breaking production dependencies ([#1877](https://github.com/wxt-dev/wxt/pull/1877))\n- **deps:** Upgrade web-ext-run to support CDP ([#1879](https://github.com/wxt-dev/wxt/pull/1879))\n- **deps:** `publish-browser-extension` upgrade ([#1880](https://github.com/wxt-dev/wxt/pull/1880))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.9\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.8...wxt-v0.20.9)\n\n### 🩹 Fixes\n\n- **types:** Use TType for DocumentEventMap key in ctx.addEventListener ([#1863](https://github.com/wxt-dev/wxt/pull/1863))\n- Prevent Unlisted CSS from being excluded in the build when using CSS preprocessor ([#1590](https://github.com/wxt-dev/wxt/pull/1590))\n\n### 🏡 Chore\n\n- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))\n- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))\n- Upgrade nano-spawn to v1 ([5fefd8e0](https://github.com/wxt-dev/wxt/commit/5fefd8e0))\n- Upgrade faker to v10 ([984568e0](https://github.com/wxt-dev/wxt/commit/984568e0))\n- Upgrade dotenv to v17.2.2 ([380a9630](https://github.com/wxt-dev/wxt/commit/380a9630))\n- Upgrade happy-dom to v18.0.1 ([c1c3d3b7](https://github.com/wxt-dev/wxt/commit/c1c3d3b7))\n- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))\n- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Kim Gyeongjae ([@PortalCube](https://github.com/PortalCube))\n- Atomie CHEN <atomic_cwh@163.com>\n\n## v0.20.8\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.7...wxt-v0.20.8)\n\n### 🩹 Fixes\n\n- Support http server websocket inside containers ([#1707](https://github.com/wxt-dev/wxt/pull/1707))\n- Recreating keyboardShortcuts interface on file changes. ([#1465](https://github.com/wxt-dev/wxt/pull/1465))\n- Fix bad regex from #1707 ([#1712](https://github.com/wxt-dev/wxt/pull/1712), [#1707](https://github.com/wxt-dev/wxt/issues/1707))\n- Fix bundle variable assignment error with `rolldown-vite` ([#1715](https://github.com/wxt-dev/wxt/pull/1715))\n- Support negation patterns when including/excluding files from ZIP files ([#1517](https://github.com/wxt-dev/wxt/pull/1517))\n- **rolldown-compat:** Only apply footer to content and unlisted scripts via plugin ([#1793](https://github.com/wxt-dev/wxt/pull/1793))\n- Init project should allow the .git folder to exist ([#1731](https://github.com/wxt-dev/wxt/pull/1731))\n- Adding missing chromiumPort parameter in web-ext.ts to enable fixed port debugging ([#1818](https://github.com/wxt-dev/wxt/pull/1818))\n\n### 📖 Documentation\n\n- Fix API reference sidebar by naming new module ([eb0bffb5](https://github.com/wxt-dev/wxt/commit/eb0bffb5))\n- Update JSDoc with ways to cancel timeouts and intervals set by content script context ([030f23d2](https://github.com/wxt-dev/wxt/commit/030f23d2))\n\n### 🏡 Chore\n\n- Fix auto-fixable `markdownlint` errors ([#1710](https://github.com/wxt-dev/wxt/pull/1710))\n- Remove automd ([7d25110a](https://github.com/wxt-dev/wxt/commit/7d25110a))\n- Wxt & @wxt-dev/module-vue support Vite 7 ([#1771](https://github.com/wxt-dev/wxt/pull/1771))\n- Fix type error ([0333ce5c](https://github.com/wxt-dev/wxt/commit/0333ce5c))\n\n### 🤖 CI\n\n- Use matrix for test jobs ([#1708](https://github.com/wxt-dev/wxt/pull/1708))\n\n### ❤️ Contributors\n\n- Sylva <sylvain.lavabre@live.fr>\n- Btea ([@btea](https://github.com/btea))\n- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Ayu ([@ayu-exorcist](https://github.com/ayu-exorcist))\n- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))\n\n## v0.20.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.6...wxt-v0.20.7)\n\n### 🚀 Enhancements\n\n- Add `@font-face` to be processed by `splitShadowRootCss` ([#1635](https://github.com/wxt-dev/wxt/pull/1635))\n- Add DisableLoadExtensionCommandLineSwitch flag to Chromium runner ([#1698](https://github.com/wxt-dev/wxt/pull/1698))\n\n### 🩹 Fixes\n\n- Improve CSS reset inside shadow roots ([da5cd325](https://github.com/wxt-dev/wxt/commit/da5cd325))\n\n### 🏡 Chore\n\n- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))\n- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))\n- **deps:** Update all dependencies ([#1648](https://github.com/wxt-dev/wxt/pull/1648))\n- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))\n\n### ❤️ Contributors\n\n- Richard Lee ([@dlackty](https://github.com/dlackty))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Seaders ([@seaders](https://github.com/seaders))\n\n## v0.20.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.5...wxt-v0.20.6)\n\n## v0.20.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.4...wxt-v0.20.5)\n\n### 🩹 Fixes\n\n- Don't use crypto.randUUID for shadow root UIs ([3577c0b](https://github.com/wxt-dev/wxt/commit/3577c0b))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.3...wxt-v0.20.4)\n\n### 🚀 Enhancements\n\n- Ignore popup/index.ts instead of erroring ([#1520](https://github.com/wxt-dev/wxt/pull/1520))\n- Ignore elements with a `vite-ignore` or `wxt-ignore` attribute ([#1603](https://github.com/wxt-dev/wxt/pull/1603))\n- Add `{{packageVersion}}` as template variable ([#1604](https://github.com/wxt-dev/wxt/pull/1604))\n\n### 🩹 Fixes\n\n- Adding missing `\"\"` to `PublicPath` and `browser.runtime.getUrl` ([#1597](https://github.com/wxt-dev/wxt/pull/1597))\n- Fix CORS error in Firefox ([#1607](https://github.com/wxt-dev/wxt/pull/1607))\n\n### 📖 Documentation\n\n- **Content Script UI:** Add additional details about when `onRemove` is called ([656a9b3](https://github.com/wxt-dev/wxt/commit/656a9b3))\n\n### ❤️ Contributors\n\n- Yunsup Sim ([@SimYunSup](https://github.com/SimYunSup))\n- ТΞNSΛI ([@Tensai75](https://github.com/Tensai75))\n- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.2...wxt-v0.20.3)\n\n### 🚀 Enhancements\n\n- Automatically place document-level CSS outside shadow root ([#1594](https://github.com/wxt-dev/wxt/pull/1594))\n\n### 🩹 Fixes\n\n- Fix double hashing of inline script keys ([b0f4ac8](https://github.com/wxt-dev/wxt/commit/b0f4ac8))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.1...wxt-v0.20.2)\n\n### 🩹 Fixes\n\n- Fix hashing issue with inline scripts ([#1591](https://github.com/wxt-dev/wxt/pull/1591))\n\n### 📖 Documentation\n\n- Fix typo in changelog ([acb6cd1](https://github.com/wxt-dev/wxt/commit/acb6cd1))\n\n### ❤️ Contributors\n\n- Yunsup Sim ([@SimYunSup](https://github.com/SimYunSup))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n\n## v0.20.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.0...wxt-v0.20.1)\n\n### 🚀 Enhancements\n\n- Enable wxt usage inside of devcontainers ([#1406](https://github.com/wxt-dev/wxt/pull/1406))\n- Type-safe `import.meta.env.BROWSER` with new `targetBrowsers` config ([#1574](https://github.com/wxt-dev/wxt/pull/1574))\n\n### 🩹 Fixes\n\n- Don't remove top-level destructured variable definitions when importing entrypoints ([#1561](https://github.com/wxt-dev/wxt/pull/1561))\n- Add JSDoc type annotation to auto-imports for ESlint ([#1558](https://github.com/wxt-dev/wxt/pull/1558))\n\n### 📖 Documentation\n\n- Fix knowledge file generation ([#1550](https://github.com/wxt-dev/wxt/pull/1550))\n\n### 🏡 Chore\n\n- **deps:** Update all dependencies ([#1568](https://github.com/wxt-dev/wxt/pull/1568))\n- Update comment ([61b42ef](https://github.com/wxt-dev/wxt/commit/61b42ef))\n\n### ❤️ Contributors\n\n- 7sDream ([@7sDream](https://github.com/7sDream))\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- Nostro ([@nostrorom](https://github.com/nostrorom))\n- Khalil Yao ([@yyz945947732](https://github.com/yyz945947732))\n- Alec WM ([@alecdwm](https://github.com/alecdwm))\n\n## v0.20.0\n\nExcited to release the next major version of WXT! Follow the [Upgrade guide](https://wxt.dev/guide/resources/upgrading.html) to update!\n\n---\n\n[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.29...wxt-v0.20.0)\n\n### 🚀 Enhancements\n\n- ⚠️  Remove `webextension-polyfill` ([#1084](https://github.com/wxt-dev/wxt/pull/1084))\n- ⚠️  Individual exports and introduce the `#imports` module ([#1258](https://github.com/wxt-dev/wxt/pull/1258))\n- ⚠️  Reset inherited styles inside shadow root ([#1269](https://github.com/wxt-dev/wxt/pull/1269))\n- ⚠️  Auto-import types ([#1315](https://github.com/wxt-dev/wxt/pull/1315))\n\n### 🩹 Fixes\n\n- ⚠️  Add suffix to non-production output directories ([#1086](https://github.com/wxt-dev/wxt/pull/1086))\n- ⚠️  Remove deprecated `jiti` entrypoint loader ([#1087](https://github.com/wxt-dev/wxt/pull/1087))\n- ⚠️  Rename `runner` to `webExt` ([#1180](https://github.com/wxt-dev/wxt/pull/1180))\n- ⚠️  Remove `transformManfiest` option ([#1181](https://github.com/wxt-dev/wxt/pull/1181))\n- Remove unnecessary `VITE_CJS_IGNORE_WARNING` flag ([b0ef178](https://github.com/wxt-dev/wxt/commit/b0ef178))\n- ⚠️  Make `publicDir` and `modulesDir` relative to project root ([#1216](https://github.com/wxt-dev/wxt/pull/1216))\n- ⚠️  Move `wxt/storage` to `wxt/utils/storage` ([#1271](https://github.com/wxt-dev/wxt/pull/1271))\n- Add back `ExtensionRunnerConfig` as deprecated ([#1311](https://github.com/wxt-dev/wxt/pull/1311))\n- Missing browser in shadow-root file ([#1317](https://github.com/wxt-dev/wxt/pull/1317))\n\n### 📖 Documentation\n\n- Fix api reference for `wxt/utils/storage` ([99b5076](https://github.com/wxt-dev/wxt/commit/99b5076))\n- Fix broken links ([82d8024](https://github.com/wxt-dev/wxt/commit/82d8024))\n\n### 🏡 Chore\n\n- Fix type errors ([aad17c8](https://github.com/wxt-dev/wxt/commit/aad17c8))\n- Remove duplicate test ([e54df0a](https://github.com/wxt-dev/wxt/commit/e54df0a))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](https://github.com/aklinker1))\n- 1natsu ([@1natsu172](https://github.com/1natsu172))\n\n## v0.19.29\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.28...wxt-v0.19.29)\n\n### 🚀 Enhancements\n\n- Tolerate syntax errors ([#1437](https://github.com/wxt-dev/wxt/pull/1437))\n\n### 🩹 Fixes\n\n- Support `registration: \"runtime\"` for MV2 ([#1431](https://github.com/wxt-dev/wxt/pull/1431))\n\n### ❤️ Contributors\n\n- Thomas ([@harmonyharmo](http://github.com/harmonyharmo))\n- Alec Larson ([@aleclarson](http://github.com/aleclarson))\n\n## v0.19.28\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.27...wxt-v0.19.28)\n\n### 🩹 Fixes\n\n- Ignore `manifest.manifest_version` option and warn about incorrect usage ([#1419](https://github.com/wxt-dev/wxt/pull/1419))\n- Resolve wxt modules from the root ([#1417](https://github.com/wxt-dev/wxt/pull/1417))\n- Properly detect and reload changed files when using Vite's `?suffix` imports ([#1432](https://github.com/wxt-dev/wxt/pull/1432))\n- Regression on variable expansion in dotenv files ([#1449](https://github.com/wxt-dev/wxt/pull/1449))\n- Allow vite 6.0.9+ ([#1497](https://github.com/wxt-dev/wxt/pull/1497))\n\n### 📖 Documentation\n\n- Host pre-aggregated LLM knowledge files ([#1355](https://github.com/wxt-dev/wxt/pull/1355))\n\n### 🏡 Chore\n\n- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))\n- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))\n- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))\n- **deps:** Upgrade to Vite 6 and related dependencies ([#1496](https://github.com/wxt-dev/wxt/pull/1496))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Stefan Maric ([@stefanmaric](https://github.com/stefanmaric))\n- Okinea Dev ([@okineadev](http://github.com/okineadev))\n- 1natsu ([@1natsu172](http://github.com/1natsu172))\n- Alec Larson ([@aleclarson](http://github.com/aleclarson))\n\n## v0.19.27\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.26...wxt-v0.19.27)\n\n### 🩹 Fixes\n\n- Allow `vite-node@^3.0.0` ([#1378](https://github.com/wxt-dev/wxt/pull/1378))\n- Use path triple slash directive for types that are referenced via path ([#1407](https://github.com/wxt-dev/wxt/pull/1407))\n- Support `publish-browser-extension` v3.0.0 ([ac92d40](https://github.com/wxt-dev/wxt/commit/ac92d40))\n- Respect `inlineConfig.mode` when setting NODE_ENV ([#1416](https://github.com/wxt-dev/wxt/pull/1416))\n- Auto-import does not work when using `@vitejs/plugin-vue` alone ([#1412](https://github.com/wxt-dev/wxt/pull/1412))\n\n### ❤️ Contributors\n\n- Jaguar Zhou ([@aiktb](http://github.com/aiktb))\n- Alec Larson ([@aleclarson](http://github.com/aleclarson))\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- St3h3n <st3h3n@gmail.com>\n- Eli ([@lishaduck](http://github.com/lishaduck))\n\n## v0.19.26\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.25...wxt-v0.19.26)\n\n### 🩹 Fixes\n\n- **context:** Deduplicate `wxt:content-script-started` ([#1364](https://github.com/wxt-dev/wxt/pull/1364))\n\n### ❤️ Contributors\n\n- Deniz Uğur ([@DenizUgur](http://github.com/DenizUgur))\n\n## v0.19.25\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.24...wxt-v0.19.25)\n\n### 🩹 Fixes\n\n- Drop support for vite 6.0.9+ until websocket fix is finished ([8e2badc](https://github.com/wxt-dev/wxt/commit/8e2badc))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.24\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.23...wxt-v0.19.24)\n\n### 🩹 Fixes\n\n- **init:** Remove \"experimental\" from bun package manager ([b150a52](https://github.com/wxt-dev/wxt/commit/b150a52))\n- **web-ext:** Correctly pass `browserConsole` option to open devtools in Firefox ([#1308](https://github.com/wxt-dev/wxt/pull/1308))\n- `autoMount` not working if anchor is already present ([#1350](https://github.com/wxt-dev/wxt/pull/1350))\n\n### 🏡 Chore\n\n- Fix typo in CLI docs ([028c601](https://github.com/wxt-dev/wxt/commit/028c601))\n\n### ❤️ Contributors\n\n- 1natsu ([@1natsu172](http://github.com/1natsu172))\n- Paulo Pinto ([@psrpinto](https://github.com/psrpinto))\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.23\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.22...wxt-v0.19.23)\n\n### 🚀 Enhancements\n\n- **hooks:** Move `entrypoints:resolved` before debug logs and add `entrypoints:found` ([#1292](https://github.com/wxt-dev/wxt/pull/1292))\n\n### 🩹 Fixes\n\n- Allow runtime registered content scripts to not have `matches` ([#1306](https://github.com/wxt-dev/wxt/pull/1306))\n- Properly close readline instance on close ([#1278](https://github.com/wxt-dev/wxt/pull/1278))\n\n### 📖 Documentation\n\n- Sync READMEs ([27298b7](https://github.com/wxt-dev/wxt/commit/27298b7))\n\n### 🏡 Chore\n\n- Simplify keyboard shortcuts ([#1284](https://github.com/wxt-dev/wxt/pull/1284))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Abhigyan Trips ([@abhigyantrips](http://github.com/abhigyantrips))\n\n## v0.19.22\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.21...wxt-v0.19.22)\n\n### 🩹 Fixes\n\n- Exclude entire `import.meta.env` object from content script output ([#1267](https://github.com/wxt-dev/wxt/pull/1267))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.21\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.20...wxt-v0.19.21)\n\n### 🩹 Fixes\n\n- Exclude skipped entrypoints from manifest ([#1265](https://github.com/wxt-dev/wxt/pull/1265))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.20\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.19...wxt-v0.19.20)\n\n### 🚀 Enhancements\n\n- `autoMount` content script UIs ([#1210](https://github.com/wxt-dev/wxt/pull/1210))\n\n### 🩹 Fixes\n\n- Only show reopen browser shortcut when it can be done ([#1263](https://github.com/wxt-dev/wxt/pull/1263))\n- Import entrypoint improvements ([#1207](https://github.com/wxt-dev/wxt/pull/1207))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- 1natsu ([@1natsu172](http://github.com/1natsu172))\n\n## v0.19.19\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.18...wxt-v0.19.19)\n\n### 🚀 Enhancements\n\n- Keyboard shortcut to reopen the browser without restarting the dev command ([#1211](https://github.com/wxt-dev/wxt/pull/1211))\n\n### 🩹 Fixes\n\n- Prevent changing dev server port when reloading config ([#1241](https://github.com/wxt-dev/wxt/pull/1241))\n- Ensure content scripts are registered immediately in dev mode ([#1253](https://github.com/wxt-dev/wxt/pull/1253))\n- Exclude skipped entrypoints from Firefox sources zip ([#1238](https://github.com/wxt-dev/wxt/pull/1238))\n\n### ❤️ Contributors\n\n- Nishu ([@nishu-murmu](http://github.com/nishu-murmu))\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.18\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.17...wxt-v0.19.18)\n\n### 🩹 Fixes\n\n- Correct `dev.reloadCommand` restriction to consider only commands with suggested keys ([#1226](https://github.com/wxt-dev/wxt/pull/1226))\n\n### 🌊 Types\n\n- Add overloads to `ContentScriptContext#addEventListener` for different targets ([#1245](https://github.com/wxt-dev/wxt/pull/1245))\n\n### 🏡 Chore\n\n- Refactor `findEntrypoints` to return all entrypoints with `skipped` set properly ([#1244](https://github.com/wxt-dev/wxt/pull/1244))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Jaguar Zhou ([@aiktb](http://github.com/aiktb))\n\n## v0.19.17\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.16...wxt-v0.19.17)\n\n### 🚀 Enhancements\n\n- New `server:created`, `server:started`, and `server:closed` hooks ([#1179](https://github.com/wxt-dev/wxt/pull/1179))\n\n### 🩹 Fixes\n\n- Re-initialize WXT modules correctly during development ([#1176](https://github.com/wxt-dev/wxt/pull/1176))\n- ESLint config being generated when ESLint is not installed. ([#1198](https://github.com/wxt-dev/wxt/pull/1198))\n- Update `vite` dependency range to support v6 ([#1215](https://github.com/wxt-dev/wxt/pull/1215))\n- Automatically convert MV3 `content_security_policy` to MV2 ([#1168](https://github.com/wxt-dev/wxt/pull/1168))\n- Correctly remove child elements with integrated UI remove ([#1219](https://github.com/wxt-dev/wxt/pull/1219))\n- Make content script `matches` optional ([#1220](https://github.com/wxt-dev/wxt/pull/1220))\n\n### 📖 Documentation\n\n- Fix analyze typo in type ([#1187](https://github.com/wxt-dev/wxt/pull/1187))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- 1natsu ([@1natsu172](http://github.com/1natsu172))\n- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v0.19.16\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.15...wxt-v0.19.16)\n\n### 🚀 Enhancements\n\n- **hooks:** Add new `config:resolved` hook ([#1177](https://github.com/wxt-dev/wxt/pull/1177))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.15\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.14...wxt-v0.19.15)\n\n### 🚀 Enhancements\n\n- Extract `wxt/storage` to its own package, `@wxt-dev/storage` ([#1129](https://github.com/wxt-dev/wxt/pull/1129))\n\n### 🩹 Fixes\n\n- Add \"/\" to `PublicPath` and `browser.runtime.getURL` ([#1171](https://github.com/wxt-dev/wxt/pull/1171))\n- Add extension ID to event used to invalidate `ContentScriptContext` ([#1175](https://github.com/wxt-dev/wxt/pull/1175))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Nishu ([@nishu-murmu](http://github.com/nishu-murmu))\n\n## v0.19.14\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.13...wxt-v0.19.14)\n\n### 🚀 Enhancements\n\n- **storage:** Support storage items in batch functions ([#990](https://github.com/wxt-dev/wxt/pull/990))\n- Automatically disable 'Show warning about Self-XSS when pasing code' in new chrome ([#1159](https://github.com/wxt-dev/wxt/pull/1159))\n\n### 🩹 Fixes\n\n- Throw when config file does not exist ([#1156](https://github.com/wxt-dev/wxt/pull/1156))\n\n### 📖 Documentation\n\n- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))\n- Fix typo in `popup` and `options` EntrypointOptions ([#1121](https://github.com/wxt-dev/wxt/pull/1121))\n\n### 🏡 Chore\n\n- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))\n- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Rxliuli ([@rxliuli](http://github.com/rxliuli))\n- Kongmoumou ([@kongmoumou](http://github.com/kongmoumou))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n- Bread Grocery <breadgrocery@gmail.com>\n\n## v0.19.13\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.12...wxt-v0.19.13)\n\n### 🚀 Enhancements\n\n- **env:** Load env from `.env.[browser]` variants ([#1078](https://github.com/wxt-dev/wxt/pull/1078))\n\n### 🩹 Fixes\n\n- Don't use `#private` member variables in `ContentScriptContext` ([#1103](https://github.com/wxt-dev/wxt/pull/1103))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Craig Slusher ([@sleekslush](http://github.com/sleekslush))\n\n## v0.19.12\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.11...wxt-v0.19.12)\n\n### 🚀 Enhancements\n\n- Add support for `WXT_` environment variable prefix ([#1076](https://github.com/wxt-dev/wxt/pull/1076))\n- **config:** Add `outDirTemplate` for customizing output directory structure ([#1074](https://github.com/wxt-dev/wxt/pull/1074))\n\n### 🔥 Performance\n\n- Replace `execa` with `nano-spawn` for smaller package size ([#1042](https://github.com/wxt-dev/wxt/pull/1042))\n- Downgrade `esbuild` so a single version is shared between sub-dependencies ([#1045](https://github.com/wxt-dev/wxt/pull/1045))\n\n### 🩹 Fixes\n\n- Use directory name when `zip.name` and `package.json#name` are not provided ([#1028](https://github.com/wxt-dev/wxt/pull/1028))\n- Ensure consistent hook execution order and add docs ([#1081](https://github.com/wxt-dev/wxt/pull/1081))\n\n### 📖 Documentation\n\n- Rewrite and restructure the documentation website ([#933](https://github.com/wxt-dev/wxt/pull/933))\n\n### 🏡 Chore\n\n- Remove email from changelog ([#1027](https://github.com/wxt-dev/wxt/pull/1027))\n- **deps:** Bump magicast from 0.3.4 to 0.3.5 ([#1017](https://github.com/wxt-dev/wxt/pull/1017))\n- **deps:** Bump esbuild from 0.23.0 to 0.24.0 ([#1018](https://github.com/wxt-dev/wxt/pull/1018))\n- **deps:** Bump linkedom from 0.18.4 to 0.18.5 ([#1034](https://github.com/wxt-dev/wxt/pull/1034))\n- **deps:** Bump execa from 9.3.1 to 9.4.0 ([#1031](https://github.com/wxt-dev/wxt/pull/1031))\n- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))\n- Shrink down on dependencies ([#1050](https://github.com/wxt-dev/wxt/pull/1050))\n- Enable `extensionApi: chrome` in template projects ([#1083](https://github.com/wxt-dev/wxt/pull/1083))\n\n### ❤️ Contributors\n\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n- Mezannic ([@mezannic](http://github.com/mezannic))\n\n## v0.19.11\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.10...wxt-v0.19.11)\n\n### 🚀 Enhancements\n\n- **zip:** Hooks ([#993](https://github.com/wxt-dev/wxt/pull/993))\n- **zip:** `wxt zip --sources` and auto sources for opera ([#1014](https://github.com/wxt-dev/wxt/pull/1014))\n\n### 🩹 Fixes\n\n- Reverse env files priority ([#1016](https://github.com/wxt-dev/wxt/pull/1016))\n- #1005 fixed, by updating type-definations to getItem method. ([#1007](https://github.com/wxt-dev/wxt/pull/1007), [#1005](https://github.com/wxt-dev/wxt/issues/1005))\n\n### 🏡 Chore\n\n- Move some files around ([#996](https://github.com/wxt-dev/wxt/pull/996))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n- Gurvir Singh ([@baraich](http://github.com/baraich))\n- Mezannic ([@mezannic](http://github.com/mezannic))\n- Aaron ([@aklinker1](http://github.com/aklinker1))\n\n## v0.19.10\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.9...wxt-v0.19.10)\n\n### 🔥 Performance\n\n- Reduce hypersensitive onChange of watcher ([#978](https://github.com/wxt-dev/wxt/pull/978))\n\n### 🩹 Fixes\n\n- Fix config manifest type ([#973](https://github.com/wxt-dev/wxt/pull/973))\n\n### 📖 Documentation\n\n- Examples reference outDir vs. outputDir ([#982](https://github.com/wxt-dev/wxt/pull/982))\n- Improved docs and links ([#970](https://github.com/wxt-dev/wxt/pull/970))\n\n### 🌊 Types\n\n- Fix `ExtensionRunnerConfig.chromiumPref` type ([fda1e18](https://github.com/wxt-dev/wxt/commit/fda1e18))\n\n### ❤️ Contributors\n\n- 1natsu ([@1natsu172](http://github.com/1natsu172))\n- Okinea Dev ([@okineadev](http://github.com/okineadev))\n- The-syndrome <meltdown-syndrome@proton.me>\n- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))\n\n## v0.19.9\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.8...wxt-v0.19.9)\n\n### 🚀 Enhancements\n\n- **modules:** Add `wxt.hook` alias for `wxt.hooks.hook` ([c5f78d0](https://github.com/wxt-dev/wxt/commit/c5f78d0))\n- Use `@types/chrome` for config manifest type ([#969](https://github.com/wxt-dev/wxt/pull/969))\n\n### 🩹 Fixes\n\n- Allow adding multiple hyphens in an entrypoint name ([#949](https://github.com/wxt-dev/wxt/pull/949))\n- Duplicate `BuildOutput.publicAssets` ([#951](https://github.com/wxt-dev/wxt/pull/951))\n- Properly overload `import.meta.env` with WXT's own environment globals ([#966](https://github.com/wxt-dev/wxt/pull/966))\n\n### 📖 Documentation\n\n- Add docs around `importEntrypoint` to relevant functions ([143b5ac](https://github.com/wxt-dev/wxt/commit/143b5ac))\n\n### 🌊 Types\n\n- **modules:** Use `NestedHooks` instead of `Partial` for hooks object ([0ebb013](https://github.com/wxt-dev/wxt/commit/0ebb013))\n\n### 🏡 Chore\n\n- Fix type error ([#946](https://github.com/wxt-dev/wxt/pull/946))\n- Add  `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))\n- **deps:** Bump ora from 8.0.1 to 8.1.0 ([#961](https://github.com/wxt-dev/wxt/pull/961))\n- **deps:** Bump unimport from 3.9.1 to 3.11.1 ([#960](https://github.com/wxt-dev/wxt/pull/960))\n- **deps:** Bump execa from 9.3.0 to 9.3.1 ([#957](https://github.com/wxt-dev/wxt/pull/957))\n- Cleanup leftover E2E test artifacts ([#968](https://github.com/wxt-dev/wxt/pull/968))\n\n### ❤️ Contributors\n\n- 1natsu ([@1natsu172](http://github.com/1natsu172))\n\n## v0.19.8\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.7...wxt-v0.19.8)\n\n### 🔥 Performance\n\n- Ignore output directories for all `vite.watcher` ([#924](https://github.com/wxt-dev/wxt/pull/924))\n\n### 🩹 Fixes\n\n- Fallback to GitHub API for listing templates when ungh is down ([#944](https://github.com/wxt-dev/wxt/pull/944))\n\n### 🏡 Chore\n\n- **types:** Cleanup types in wxt/browser/chrome ([#932](https://github.com/wxt-dev/wxt/pull/932))\n\n### ❤️ Contributors\n\n- 1natsu\n\n## v0.19.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.6...wxt-v0.19.7)\n\n### 🚀 Enhancements\n\n- **testing:** Run WXT modules when setting up test environment ([#926](https://github.com/wxt-dev/wxt/pull/926))\n- **modules:** Add `addAlias` helper ([#928](https://github.com/wxt-dev/wxt/pull/928))\n\n### 🩹 Fixes\n\n- **testing:** Stub `chrome` and `browser` globals with `fakeBrowser` automatically ([#925](https://github.com/wxt-dev/wxt/pull/925))\n- Ensure TSConfig paths start with `../` or  `./` so they are valid ([#927](https://github.com/wxt-dev/wxt/pull/927))\n\n### 📖 Documentation\n\n- Fix module name for `wxt/browser/chrome` ([751706d](https://github.com/wxt-dev/wxt/commit/751706d))\n\n### 🏡 Chore\n\n- Remove warning log for missing public directory ([5f2e1c3](https://github.com/wxt-dev/wxt/commit/5f2e1c3))\n\n## v0.19.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.5...wxt-v0.19.6)\n\n### 🔥 Performance\n\n- Ignore non-source code from the file watcher ([#919](https://github.com/wxt-dev/wxt/pull/919))\n\n### 🩹 Fixes\n\n- Typo in sidepanel options (`browse_style` → `browser_style`) ([#914](https://github.com/wxt-dev/wxt/pull/914))\n- **types:** Don't report type errors when using string templates with `browser.i18n.getMessage` ([#916](https://github.com/wxt-dev/wxt/pull/916))\n\n### ❤️ Contributors\n\n- 1natsu ([@1natsu](https://github.com/1natsu172))\n\n## v0.19.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.4...wxt-v0.19.5)\n\n### 🚀 Enhancements\n\n- Allow zipping hidden files by listing them explicitly in `includeSources` ([#902](https://github.com/wxt-dev/wxt/pull/902))\n- Allow excluding files when zipping ([#906](https://github.com/wxt-dev/wxt/pull/906))\n\n### 🩹 Fixes\n\n- #907 move wxt devDeps execa to dependencies ([#908](https://github.com/wxt-dev/wxt/pull/908), [#907](https://github.com/wxt-dev/wxt/issues/907))\n- Update chromium setting for enabling content script sourcemaps ([e6529e6](https://github.com/wxt-dev/wxt/commit/e6529e6))\n\n### ❤️ Contributors\n\n- 1natsu ([@1natsu](https://github.com/1natsu172))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))\n\n## v0.19.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.3...wxt-v0.19.4)\n\n### 🚀 Enhancements\n\n- Add `injectScript` helper ([#900](https://github.com/wxt-dev/wxt/pull/900))\n\n### 🩹 Fixes\n\n- Do not clear `.wxt/tsconfig.json` in `findEntrypoints` if it exists ([#898](https://github.com/wxt-dev/wxt/pull/898))\n- **types:** `PublicPath` type resolution with `extensionApi: \"chrome\"` ([#901](https://github.com/wxt-dev/wxt/pull/901))\n- Fix `createIframeUi` `page` option types ([3a8e613](https://github.com/wxt-dev/wxt/commit/3a8e613))\n\n### ❤️ Contributors\n\n- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))\n\n## v0.19.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.2...wxt-v0.19.3)\n\n### 🩹 Fixes\n\n- Add `consola` to `wxt` dependencies ([#892](https://github.com/wxt-dev/wxt/pull/892))\n- **content-script-context:** Fix initialization logic for Firefox ([#895](https://github.com/wxt-dev/wxt/pull/895))\n\n### 📖 Documentation\n\n- Improve `prepare:types` hook JSDoc ([#886](https://github.com/wxt-dev/wxt/pull/886))\n\n### ❤️ Contributors\n\n- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))\n- Himanshu Patil ([@mehimanshupatil](https://github.com/mehimanshupatil))\n\n## v0.19.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.1...wxt-v0.19.2)\n\n### 🩹 Fixes\n\n- Remove unused top-level functions and variables when loading entrypoints with the `vite-node` loader ([#875](https://github.com/wxt-dev/wxt/pull/875))\n- Don't default to dev mode for production builds when using `vite-node` loader ([#877](https://github.com/wxt-dev/wxt/pull/877))\n\n### 📖 Documentation\n\n- Update README and homepage features with WXT modules ([ed07a49](https://github.com/wxt-dev/wxt/commit/ed07a49))\n\n### 🏡 Chore\n\n- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))\n\n## v0.19.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.0...wxt-v0.19.1)\n\n### 🚀 Enhancements\n\n- Auto Icons Module ([#851](https://github.com/wxt-dev/wxt/pull/851))\n\n### 🔥 Performance\n\n- Tree-shake unrelated code inside `vite-node` entrypoint loader ([#867](https://github.com/wxt-dev/wxt/pull/867))\n\n### 🩹 Fixes\n\n- Define web globals when importing entrypoints ([#865](https://github.com/wxt-dev/wxt/pull/865))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v0.19.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.15...wxt-v0.19.0)\n\n### 🚀 Enhancements\n\n- **experimental:** First-class support for excluding `webextension-polyfill` ([#847](https://github.com/wxt-dev/wxt/pull/847))\n- **storage:** `init` option and rename `defaultValue` to `fallback` ([#827](https://github.com/wxt-dev/wxt/pull/827))\n- **hooks:** Add `prepare:publicPaths` hook ([#858](https://github.com/wxt-dev/wxt/pull/858))\n- ⚠️  Use `vite-node` to load entrypoints by default ([#859](https://github.com/wxt-dev/wxt/pull/859))\n\n### 🔥 Performance\n\n- **size:** ⚠️  Switch from `tsup` to `unbuild` for building WXT ([#848](https://github.com/wxt-dev/wxt/pull/848))\n\n### 🩹 Fixes\n\n- Wrong module hook type ([#849](https://github.com/wxt-dev/wxt/pull/849))\n\n### 📖 Documentation\n\n- Update labels in content script UI positioning screenshot ([2b6ff8d](https://github.com/wxt-dev/wxt/commit/2b6ff8d))\n- Add upgrade guide and breaking changes ([#860](https://github.com/wxt-dev/wxt/pull/860))\n\n### 🏡 Chore\n\n- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))\n- **dev-deps:** Upgrade `vitest` from 1.6 to 2.0 ([#836](https://github.com/wxt-dev/wxt/pull/836))\n- **deps:** Upgrade `async-mutex` from 0.4.1 to 0.5.0 ([#837](https://github.com/wxt-dev/wxt/pull/837))\n- **deps:** Upgrade `esbuild` from 0.19.12 to 0.23.0 ([#838](https://github.com/wxt-dev/wxt/pull/838))\n- **deps:** Upgrade `vite-node` from 1.6 to 2.0 ([#839](https://github.com/wxt-dev/wxt/pull/839))\n- **deps:** Upgrade `ora` from 7 to 8 ([#841](https://github.com/wxt-dev/wxt/pull/841))\n- **deps:** Upgrade `webextension-polyfill` from 0.10 to 0.12 ([#842](https://github.com/wxt-dev/wxt/pull/842))\n- **deps:** Upgrade `minimatch` from 9 to 10 ([#840](https://github.com/wxt-dev/wxt/pull/840))\n- **dev-deps:** Upgrade `happy-dom` from 13 to 14 ([#843](https://github.com/wxt-dev/wxt/pull/843))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v0.18.15\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.14...wxt-v0.18.15)\n\n### 🩹 Fixes\n\n- Don't throw error if localization file is missing ([#832](https://github.com/wxt-dev/wxt/pull/832))\n- Build latest version of package before packing ([88a1244](https://github.com/wxt-dev/wxt/commit/88a1244))\n- Externalize app config during dependency optimization ([#833](https://github.com/wxt-dev/wxt/pull/833))\n\n### 📖 Documentation\n\n- Fix links to Guide pages ([#821](https://github.com/wxt-dev/wxt/pull/821))\n\n### ❤️ Contributors\n\n- Eetann ([@eetann](http://github.com/eetann))\n\n## v0.18.14\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.13...wxt-v0.18.14)\n\n### 🩹 Fixes\n\n- **modules:** Add types from all wxt node_modules, not just ones with config ([#817](https://github.com/wxt-dev/wxt/pull/817))\n\n## v0.18.13\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.12...wxt-v0.18.13)\n\n### 🚀 Enhancements\n\n- **config:** `dev.server.hostname` ([#807](https://github.com/wxt-dev/wxt/pull/807))\n- Add XPath support to getAnchor() ([#813](https://github.com/wxt-dev/wxt/pull/813))\n\n### 🩹 Fixes\n\n- Add debug logs for vite builder ([#816](https://github.com/wxt-dev/wxt/pull/816))\n\n### ❤️ Contributors\n\n- Ayub Kokabi ([@sir-kokabi](http://github.com/sir-kokabi))\n\n## v0.18.12\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.11...wxt-v0.18.12)\n\n### 🚀 Enhancements\n\n- Support runtime config in `app.config.ts` ([#792](https://github.com/wxt-dev/wxt/pull/792))\n\n### 🔥 Performance\n\n- Create zip using streams ([#793](https://github.com/wxt-dev/wxt/pull/793))\n\n### 🩹 Fixes\n\n- Add missing name to ESLint v9 autoImports config ([#801](https://github.com/wxt-dev/wxt/pull/801))\n\n### 📖 Documentation\n\n- Update README ([#802](https://github.com/wxt-dev/wxt/pull/802))\n\n### 🏡 Chore\n\n- **deps:** Upgrade `web-ext-run` (0.2.0 to 0.2.1) ([#804](https://github.com/wxt-dev/wxt/pull/804))\n\n### ❤️ Contributors\n\n- Ntnyq ([@ntnyq](http://github.com/ntnyq))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v0.18.11\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.10...wxt-v0.18.11)\n\n### 🚀 Enhancements\n\n- Add eslint 9 config support ([#762](https://github.com/wxt-dev/wxt/pull/762))\n\n### 🩹 Fixes\n\n- Respect custom `outDir` when cleaning and zipping ([#774](https://github.com/wxt-dev/wxt/pull/774))\n- **dev:** Catch error when attempting to reload a tab in a saved tab group ([#786](https://github.com/wxt-dev/wxt/pull/786))\n\n### 🏡 Chore\n\n- Replace consola with wxt.logger ([#776](https://github.com/wxt-dev/wxt/pull/776))\n- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))\n\n### ❤️ Contributors\n\n- KnightYoshi ([@KnightYoshi](http://github.com/KnightYoshi))\n- Asakura Mizu ([@AsakuraMizu](http://github.com/AsakuraMizu))\n\n## v0.18.10\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.9...wxt-v0.18.10)\n\n### 🚀 Enhancements\n\n- Add `prepare:types` hook to extend `.wxt/` directory generation ([#767](https://github.com/wxt-dev/wxt/pull/767))\n- **modules:** Allow adding generated public files ([#769](https://github.com/wxt-dev/wxt/pull/769))\n\n### 🩹 Fixes\n\n- Await `prepare:types` hook ([b29d49c](https://github.com/wxt-dev/wxt/commit/b29d49c))\n\n### 🏡 Chore\n\n- Refactor package manager test fixtures ([39f6c29](https://github.com/wxt-dev/wxt/commit/39f6c29))\n- Consolidate `unimport` code into a built-in module ([#771](https://github.com/wxt-dev/wxt/pull/771))\n\n## v0.18.9\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.8...wxt-v0.18.9)\n\n### 🚀 Enhancements\n\n- **experimental:** Replace `viteRuntime` option with `entrypointImporter` option, and implement `vite-node` importer ([#761](https://github.com/wxt-dev/wxt/pull/761))\n\n## v0.18.8\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.7...wxt-v0.18.8)\n\n### 🚀 Enhancements\n\n- **dev:** Reload extension when public files change ([#752](https://github.com/wxt-dev/wxt/pull/752))\n\n### 🩹 Fixes\n\n- Don't load plugins twice in HTML pages ([#746](https://github.com/wxt-dev/wxt/pull/746))\n- Ignore .wxt file changes in dev ([#755](https://github.com/wxt-dev/wxt/pull/755))\n- **modules:** `addViteConfig` ignored user vite config ([#760](https://github.com/wxt-dev/wxt/pull/760))\n\n### 🏡 Chore\n\n- Refactor client web socket util ([#753](https://github.com/wxt-dev/wxt/pull/753))\n- Add E2E test for `addImportPreset` ([9fc6408](https://github.com/wxt-dev/wxt/commit/9fc6408))\n\n## v0.18.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.6...wxt-v0.18.7)\n\n### 🩹 Fixes\n\n- **dev:** Fix CSP error loading HTML plugins ([#723](https://github.com/wxt-dev/wxt/pull/723))\n- Generalize react-refresh preamble logic to virtualize all inline scripts ([#728](https://github.com/wxt-dev/wxt/pull/728))\n- **zip:** Revert dotfile changes from #674 ([#742](https://github.com/wxt-dev/wxt/pull/742), [#674](https://github.com/wxt-dev/wxt/issues/674))\n\n### 🏡 Chore\n\n- **deps:** Upgrade and sync all dependencies ([#725](https://github.com/wxt-dev/wxt/pull/725))\n- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))\n\n## v0.18.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.5...wxt-v0.18.6)\n\n### 🚀 Enhancements\n\n- **modules:** Add new `addImportPreset` helper function ([#720](https://github.com/wxt-dev/wxt/pull/720))\n\n### 🩹 Fixes\n\n- **types:** Module `options` param is optional, but types did not reflect that ([#719](https://github.com/wxt-dev/wxt/pull/719))\n- **modules:** Re-export `WxtModule` type from `wxt/modules` to prevent TS2742 ([#721](https://github.com/wxt-dev/wxt/pull/721))\n- **modules:** Add modules to TS project so type augmentation works ([#722](https://github.com/wxt-dev/wxt/pull/722))\n\n### 🏡 Chore\n\n- **dev-deps:** Upgrade typescript to 5.4 ([#718](https://github.com/wxt-dev/wxt/pull/718))\n\n## v0.18.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.4...wxt-v0.18.5)\n\n### 🚀 Enhancements\n\n- Module system ([#672](https://github.com/wxt-dev/wxt/pull/672))\n\n### 🩹 Fixes\n\n- To fix the issue with Entrypoint Side Effects link errors ([#705](https://github.com/wxt-dev/wxt/pull/705))\n- **storage:** Add storage area typing for storage keys ([#708](https://github.com/wxt-dev/wxt/pull/708))\n\n### 📖 Documentation\n\n- Restructure wxt.dev ([#701](https://github.com/wxt-dev/wxt/pull/701))\n- Add eslintrc info to auto-imports page ([454b076](https://github.com/wxt-dev/wxt/commit/454b076))\n\n### 🏡 Chore\n\n- **deps:** Bump linkedom from 0.16.8 to 0.18.2 ([#690](https://github.com/wxt-dev/wxt/pull/690))\n- **deps:** Bump nypm from 0.3.6 to 0.3.8 ([#692](https://github.com/wxt-dev/wxt/pull/692))\n- **deps-dev:** Bump tsx from 4.7.0 to 4.11.2 ([#699](https://github.com/wxt-dev/wxt/pull/699))\n- **deps-dev:** Bump execa from 8.0.1 to 9.1.0 ([#691](https://github.com/wxt-dev/wxt/pull/691))\n- Fix flakey TS config ([a257217](https://github.com/wxt-dev/wxt/commit/a257217))\n- Remove log in tests ([eb3b9bc](https://github.com/wxt-dev/wxt/commit/eb3b9bc))\n- **deps:** Upgrade braces to 3.0.3 ([#711](https://github.com/wxt-dev/wxt/pull/711))\n\n### 🤖 CI\n\n- Add support for managing multiple changelogs ([#712](https://github.com/wxt-dev/wxt/pull/712))\n\n### ❤️ Contributors\n\n- TheOnlyTails ([@TheOnlyTails](http://github.com/TheOnlyTails))\n- Rxliuli ([@rxliuli](http://github.com/rxliuli))\n\n## v0.18.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.3...v0.18.4)\n\n### 🩹 Fixes\n\n- Allow zipping hidden files in sources by listing them explicitly in `includeSources` ([#674](https://github.com/wxt-dev/wxt/pull/674))\n- Properly invalidate content script context ([#683](https://github.com/wxt-dev/wxt/pull/683))\n\n### 📖 Documentation\n\n- Update contributing guidelines ([8125d74](https://github.com/wxt-dev/wxt/commit/8125d74))\n\n### ❤️ Contributors\n\n- Hujiulong ([@hujiulong](http://github.com/hujiulong))\n\n## v0.18.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.2...v0.18.3)\n\n### 🩹 Fixes\n\n- Automatically add dev server to sandbox CSP ([#663](https://github.com/wxt-dev/wxt/pull/663))\n- Remove `import * as` imports from entrypoints during build ([#671](https://github.com/wxt-dev/wxt/pull/671))\n- **security:** Upgrade tar to 6.2.1 ([215def7](https://github.com/wxt-dev/wxt/commit/215def7))\n\n### 📖 Documentation\n\n- Add YTBlock to homepage ([#666](https://github.com/wxt-dev/wxt/pull/666))\n\n### 🏡 Chore\n\n- Add missing tests for dev mode CSP ([#662](https://github.com/wxt-dev/wxt/pull/662))\n- Upgrade templates to v0.18 ([3b954cc](https://github.com/wxt-dev/wxt/commit/3b954cc))\n\n### 🤖 CI\n\n- Fix sync-releases workflow trigger ([5d8efef](https://github.com/wxt-dev/wxt/commit/5d8efef))\n\n### ❤️ Contributors\n\n- Edoan ([@EdoanR](http://github.com/EdoanR))\n\n## v0.18.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.1...v0.18.2)\n\n### 🚀 Enhancements\n\n- **runner:** Add `keepProfileChanges` option ([#655](https://github.com/wxt-dev/wxt/pull/655))\n\n### 🩹 Fixes\n\n- Automatically detect and add \"sidePanel\" permission ([5fcaf7c](https://github.com/wxt-dev/wxt/commit/5fcaf7c))\n\n### 📖 Documentation\n\n- Fix `wxt-vitest-plugin` reference ([#650](https://github.com/wxt-dev/wxt/pull/650))\n- Fix spelling mistake in remote-code.md ([#652](https://github.com/wxt-dev/wxt/pull/652))\n- Correct event handler name in handling-updates.md ([#653](https://github.com/wxt-dev/wxt/pull/653))\n- Fix iframe typos ([c74e530](https://github.com/wxt-dev/wxt/commit/c74e530))\n\n### ❤️ Contributors\n\n- Edoan ([@EdoanR](http://github.com/EdoanR))\n- Linus Norton ([@linusnorton](http://github.com/linusnorton))\n- Jeffrey Zang ([@jeffrey-zang](http://github.com/jeffrey-zang))\n- Emmanuel Ferdman ([@emmanuel-ferdman](https://github.com/emmanuel-ferdman))\n\n## v0.18.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.18.0...v0.18.1)\n\n### 🩹 Fixes\n\n- `_background` is not defined ([#649](https://github.com/wxt-dev/wxt/pull/649))\n\n### 🏡 Chore\n\n- Add root README back ([ec3dd52](https://github.com/wxt-dev/wxt/commit/ec3dd52))\n\n### 🤖 CI\n\n- Fix sync releases workflow ([dc5b55b](https://github.com/wxt-dev/wxt/commit/dc5b55b))\n\n## v0.18.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.12...v0.18.0)\n\n### 🚀 Enhancements\n\n- Add zip compression settings ([#605](https://github.com/wxt-dev/wxt/pull/605))\n- Support returning values from scripts executed with the scripting API ([#624](https://github.com/wxt-dev/wxt/pull/624))\n- **experimental:** Load entrypoint options with Vite Runtime API ([#648](https://github.com/wxt-dev/wxt/pull/648))\n\n### 🩹 Fixes\n\n- ⚠️ Automatically move `host_permissions` to `permissions` for MV2 ([#626](https://github.com/wxt-dev/wxt/pull/626))\n- **dep:** Upgrade `@webext-core/isolated-element` to v1.1.2 ([#625](https://github.com/wxt-dev/wxt/pull/625))\n\n### 📖 Documentation\n\n- Add Fluent Read to homepage ([#600](https://github.com/wxt-dev/wxt/pull/600))\n- Fix typo on example for wxt.config.ts. ([#609](https://github.com/wxt-dev/wxt/pull/609))\n- Tix typo in `entrypoints.md` ([#614](https://github.com/wxt-dev/wxt/pull/614))\n- Add Facebook Video Controls to homepage ([#615](https://github.com/wxt-dev/wxt/pull/615))\n- Fix typo in assets page ([a94d673](https://github.com/wxt-dev/wxt/commit/a94d673))\n- Add ElemSnap to homepage ([#621](https://github.com/wxt-dev/wxt/pull/621))\n- Update content script registration JSDoc ([e47519f](https://github.com/wxt-dev/wxt/commit/e47519f))\n- Add docs about handling updates ([acb7554](https://github.com/wxt-dev/wxt/commit/acb7554))\n- Add MS Edge TTS to homepage ([#647](https://github.com/wxt-dev/wxt/pull/647))\n- Document required permission for storage API ([#632](https://github.com/wxt-dev/wxt/pull/632))\n\n### 🏡 Chore\n\n- Update vue template config ([#607](https://github.com/wxt-dev/wxt/pull/607))\n- **deps-dev:** Bump lint-staged from 15.2.1 to 15.2.2 ([#637](https://github.com/wxt-dev/wxt/pull/637))\n- **deps-dev:** Bump publint from 0.2.6 to 0.2.7 ([#639](https://github.com/wxt-dev/wxt/pull/639))\n- **deps-dev:** Bump simple-git-hooks from 2.9.0 to 2.11.1 ([#640](https://github.com/wxt-dev/wxt/pull/640))\n- Refactor repo to a standard monorepo ([#646](https://github.com/wxt-dev/wxt/pull/646))\n- Fix formatting after monorepo refactor ([6ca3767](https://github.com/wxt-dev/wxt/commit/6ca3767))\n\n### ❤️ Contributors\n\n- Alegal200 ([@alegal200](https://github.com/alegal200))\n- Yacine-bens ([@yacine-bens](http://github.com/yacine-bens))\n- Ayden ([@AydenGen](https://github.com/AydenGen))\n- Wuzequanyouzi ([@wuzequanyouzi](http://github.com/wuzequanyouzi))\n- Can Rau ([@CanRau](http://github.com/CanRau))\n- 日高 凌 ([@ryohidaka](http://github.com/ryohidaka))\n- Bas Van Zanten ([@Bas950](http://github.com/Bas950))\n- ThinkStu ([@Bistutu](http://github.com/Bistutu))\n\n## v0.17.12\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.11...v0.17.12)\n\n### 🚀 Enhancements\n\n- Add hooks for extending vite config ([#599](https://github.com/wxt-dev/wxt/pull/599))\n\n### 🩹 Fixes\n\n- **content-script-ui:** Properly assign and unassign mounted value ([#598](https://github.com/wxt-dev/wxt/pull/598))\n\n### 📖 Documentation\n\n- Add discord server link ([#593](https://github.com/wxt-dev/wxt/pull/593))\n\n### 🏡 Chore\n\n- Remove unnecssary 'Omit' types ([db57c8e](https://github.com/wxt-dev/wxt/commit/db57c8e))\n- **deps-dev:** Bump @aklinker1/check from 1.1.1 to 1.2.0 ([#588](https://github.com/wxt-dev/wxt/pull/588))\n- **deps-dev:** Bump vue from 3.3.10 to 3.4.21 ([#589](https://github.com/wxt-dev/wxt/pull/589))\n- **deps-dev:** Bump sass from 1.69.5 to 1.72.0 ([#591](https://github.com/wxt-dev/wxt/pull/591))\n\n## v0.17.11\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.10...v0.17.11)\n\n### 🩹 Fixes\n\n- Resolve absolute paths from the public directory properly ([#583](https://github.com/wxt-dev/wxt/pull/583))\n\n### 📖 Documentation\n\n- Fix `zip.includeSources` example ([16fc584](https://github.com/wxt-dev/wxt/commit/16fc584))\n- Add \"GitHub Custom Notifier\" to homepage ([#580](https://github.com/wxt-dev/wxt/pull/580))\n\n### 🏡 Chore\n\n- Simplify virtual module setup ([#581](https://github.com/wxt-dev/wxt/pull/581))\n- Add some array utils ([#582](https://github.com/wxt-dev/wxt/pull/582))\n- Extract helper function ([d3b14af](https://github.com/wxt-dev/wxt/commit/d3b14af))\n\n### ❤️ Contributors\n\n- Qiwei Yang ([@qiweiii](http://github.com/qiweiii))\n\n## v0.17.10\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.9...v0.17.10)\n\n### 🚀 Enhancements\n\n- Add `dev.server.port` config ([#577](https://github.com/wxt-dev/wxt/pull/577))\n\n### 🏡 Chore\n\n- Use `@aklinker1/check` to simplify checks setup ([#550](https://github.com/wxt-dev/wxt/pull/550))\n- Refactor order of config resolution ([#578](https://github.com/wxt-dev/wxt/pull/578))\n\n### ❤️ Contributors\n\n- Zizheng Tai ([@zizhengtai](http://github.com/zizhengtai))\n\n## v0.17.9\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.8...v0.17.9)\n\n### 🚀 Enhancements\n\n- Add `{{mode}}` Template Variable ([#566](https://github.com/wxt-dev/wxt/pull/566))\n\n### 🩹 Fixes\n\n- Don't override `wxt.config.ts` options when CLI flags are not passed ([#567](https://github.com/wxt-dev/wxt/pull/567))\n\n### 🏡 Chore\n\n- Merge user config using `defu` ([#568](https://github.com/wxt-dev/wxt/pull/568))\n\n### ❤️ Contributors\n\n- Guillaume ([@GuiEpi](http://github.com/GuiEpi))\n\n## v0.17.8\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.7...v0.17.8)\n\n### 🚀 Enhancements\n\n- **analysis:** Open `stats.html` file automatically ([#564](https://github.com/wxt-dev/wxt/pull/564))\n\n### 🩹 Fixes\n\n- **init:** Better error logging when templates fail to load ([b47c150](https://github.com/wxt-dev/wxt/commit/b47c150))\n- Remove deprecated extension from Vue template ([#534](https://github.com/wxt-dev/wxt/pull/534))\n- Append option description error ([#546](https://github.com/wxt-dev/wxt/pull/546))\n- **init:** Don't overwrite existing files when initializing a new project ([#556](https://github.com/wxt-dev/wxt/pull/556))\n- **dev:** Don't crash dev mode when rebuild fails ([#565](https://github.com/wxt-dev/wxt/pull/565))\n\n### 📖 Documentation\n\n- Replace `pnpx` with `pnpm dlx` ([#527](https://github.com/wxt-dev/wxt/pull/527))\n- Update homepage demo video ([35269da](https://github.com/wxt-dev/wxt/commit/35269da))\n- Update README demo video ([#539](https://github.com/wxt-dev/wxt/pull/539))\n- Add Plex skipper to \"Using WXT\" section ([#541](https://github.com/wxt-dev/wxt/pull/541))\n- Add documentation for Bun.sh ([#543](https://github.com/wxt-dev/wxt/pull/543))\n- Mention adding unlisted scripts and pages to `web_accessible_resources` ([121b521](https://github.com/wxt-dev/wxt/commit/121b521))\n- Add examples for GitHub Actions ([#540](https://github.com/wxt-dev/wxt/pull/540))\n- Fixed \"Reload the Extension\" section ([#559](https://github.com/wxt-dev/wxt/pull/559))\n\n### 🏡 Chore\n\n- Increase unit test timeout ([d9cba55](https://github.com/wxt-dev/wxt/commit/d9cba55))\n- Increase hook timeout for Windows/NPM tests ([56b7149](https://github.com/wxt-dev/wxt/commit/56b7149))\n- Switch to seed plugin for testing ([#547](https://github.com/wxt-dev/wxt/pull/547))\n- **vue-template:** Upgrade to `vue-tsc` v2 ([#549](https://github.com/wxt-dev/wxt/pull/549))\n- Add a test covering `wxt init` in a non-empty directory ([#563](https://github.com/wxt-dev/wxt/pull/563))\n\n### ❤️ Contributors\n\n- Btea ([@btea](http://github.com/btea))\n- Vlad Fedosov ([@StyleT](http://github.com/StyleT))\n- Lpmvb ([@Lpmvb](http://github.com/Lpmvb))\n- Guillaume ([@GuiEpi](http://github.com/GuiEpi))\n- Sunshio ([@MPB-Tech](http://github.com/MPB-Tech))\n- Luca Dalli ([@lucadalli](http://github.com/lucadalli))\n\n## v0.17.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.6...v0.17.7)\n\n### 🩹 Fixes\n\n- **zip:** List `.wxt/local_modules` overrides relative to the `package.json` they're added to ([#526](https://github.com/wxt-dev/wxt/pull/526))\n\n### 🏡 Chore\n\n- Bump templates to v0.17 ([#524](https://github.com/wxt-dev/wxt/pull/524))\n- Increase test timeout for windows NPM tests ([#525](https://github.com/wxt-dev/wxt/pull/525))\n\n## v0.17.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.5...v0.17.6)\n\n### 🚀 Enhancements\n\n- Add warnings when important directories are missing ([#516](https://github.com/wxt-dev/wxt/pull/516))\n- Automatically remove top-level MV2-only or MV3-only keys ([#518](https://github.com/wxt-dev/wxt/pull/518))\n- Automatically generate `browser_action` based on `action` for MV2 ([#519](https://github.com/wxt-dev/wxt/pull/519))\n\n### 🩹 Fixes\n\n- **zip:** List all private packages correctly in a PNPM workspace ([#520](https://github.com/wxt-dev/wxt/pull/520))\n\n### 📖 Documentation\n\n- Mentions moving folders into `srcDir` ([9cd4e83](https://github.com/wxt-dev/wxt/commit/9cd4e83))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump @types/react from 18.2.34 to 18.2.61 ([#510](https://github.com/wxt-dev/wxt/pull/510))\n\n## v0.17.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.4...v0.17.5)\n\n### 🚀 Enhancements\n\n- Expose package management utils under `wxt.pm` ([#502](https://github.com/wxt-dev/wxt/pull/502))\n- Download and override private packages for Firefox code review ([#507](https://github.com/wxt-dev/wxt/pull/507))\n\n### 📖 Documentation\n\n- Fix typos ([#503](https://github.com/wxt-dev/wxt/pull/503))\n- Add docs about configuring the manifest as a function ([195d2cc](https://github.com/wxt-dev/wxt/commit/195d2cc))\n- Fix CLI generation ([b754435](https://github.com/wxt-dev/wxt/commit/b754435))\n\n### 🏡 Chore\n\n- Use JSZip for `wxt zip`, enabling future features ([#501](https://github.com/wxt-dev/wxt/pull/501))\n\n### ❤️ Contributors\n\n- Btea ([@btea](http://github.com/btea))\n\n## v0.17.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.3...v0.17.4)\n\n### 🚀 Enhancements\n\n- Add basic content script to templates ([#495](https://github.com/wxt-dev/wxt/pull/495))\n- Add `ResolvedConfig.wxtModuleDir`, resolving the directory once ([#497](https://github.com/wxt-dev/wxt/pull/497))\n\n### 🩹 Fixes\n\n- Resolve the path to `node_modules/wxt` correctly ([#498](https://github.com/wxt-dev/wxt/pull/498))\n\n### 📖 Documentation\n\n- Added DocVersionRedirector to \"Using WXT\" section ([#492](https://github.com/wxt-dev/wxt/pull/492))\n- Fix typos ([f80fb42](https://github.com/wxt-dev/wxt/commit/f80fb42))\n- Add CRXJS to comparison page ([cb4f9aa](https://github.com/wxt-dev/wxt/commit/cb4f9aa))\n- Update comparison page ([35778f7](https://github.com/wxt-dev/wxt/commit/35778f7))\n- Update context usage ([012bd7e](https://github.com/wxt-dev/wxt/commit/012bd7e))\n- Add testing example for `ContentScriptContext` ([e1c6020](https://github.com/wxt-dev/wxt/commit/e1c6020))\n\n### 🏡 Chore\n\n- Fix tests after template change ([f9b0aa4](https://github.com/wxt-dev/wxt/commit/f9b0aa4))\n\n### ❤️ Contributors\n\n- Btea ([@btea](http://github.com/btea))\n- Leo Shklovskii <leo@thermopylae.net>\n\n## v0.17.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.2...v0.17.3)\n\n### 🚀 Enhancements\n\n- **storage:** Guarantee `storage.getItems` returns values in the same order as requested ([b5f4d8c](https://github.com/wxt-dev/wxt/commit/b5f4d8c))\n\n### 🩹 Fixes\n\n- Content scripts crash when using `storage.defineItem` ([77e6d1f](https://github.com/wxt-dev/wxt/commit/77e6d1f))\n- **storage:** Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes ([#487](https://github.com/wxt-dev/wxt/pull/487), [#478](https://github.com/wxt-dev/wxt/issues/478))\n\n## v0.17.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.1...v0.17.2)\n\n### 🩹 Fixes\n\n- Don't use sub-dependency binaries directly ([#482](https://github.com/wxt-dev/wxt/pull/482))\n\n## v0.17.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.0...v0.17.1)\n\n### 🩹 Fixes\n\n- Content scripts not loading in dev mode ([3fbbe2c](https://github.com/wxt-dev/wxt/commit/3fbbe2c))\n\n### 📖 Documentation\n\n- Lots of small typo fixes ([#480](https://github.com/wxt-dev/wxt/pull/480))\n\n### ❤️ Contributors\n\n- Leo Shklovskii ([@leos](https://github.com/leos))\n\n## v0.17.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.11...v0.17.0)\n\n### 🚀 Enhancements\n\n- **storage:** ⚠️ Improved support for default values on storage items ([#477](https://github.com/wxt-dev/wxt/pull/477))\n\n### 🩹 Fixes\n\n- **storage:** ⚠️ Only run migrations when the extension is updated ([#478](https://github.com/wxt-dev/wxt/pull/478))\n- Improve dev mode for content scripts registered at runtime ([#474](https://github.com/wxt-dev/wxt/pull/474))\n\n### 📖 Documentation\n\n- **storage:** Update docs ([91fc41c](https://github.com/wxt-dev/wxt/commit/91fc41c))\n\n## v0.16.11\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.10...v0.16.11)\n\n### 🩹 Fixes\n\n- Output main JS file for HTML entrypoints to chunks directory ([#473](https://github.com/wxt-dev/wxt/pull/473))\n\n### 🏡 Chore\n\n- **e2e:** Remove log ([4fda203](https://github.com/wxt-dev/wxt/commit/4fda203))\n\n### 🤖 CI\n\n- Fix codecov warning in release workflow ([7c6973f](https://github.com/wxt-dev/wxt/commit/7c6973f))\n- Upgrade `pnpm/action-setup` to v3 ([905bfc7](https://github.com/wxt-dev/wxt/commit/905bfc7))\n\n## v0.16.10\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.9...v0.16.10)\n\n### 🚀 Enhancements\n\n- Customize when content scripts are registered, in the manifest or at runtime ([#471](https://github.com/wxt-dev/wxt/pull/471))\n\n### 🩹 Fixes\n\n- Don't assume react when importing JSX entrypoints during build ([#470](https://github.com/wxt-dev/wxt/pull/470))\n- Respect `configFile` option ([#472](https://github.com/wxt-dev/wxt/pull/472))\n\n## v0.16.9\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.8...v0.16.9)\n\n### 🚀 Enhancements\n\n- Support setting side panel options in HTML file ([#468](https://github.com/wxt-dev/wxt/pull/468))\n\n### 🩹 Fixes\n\n- Fix order of ShadowRootUI hooks calling ([#459](https://github.com/wxt-dev/wxt/pull/459))\n\n### 📖 Documentation\n\n- Add wrapper div to react's `createShadowRootUi` example ([bc24ea4](https://github.com/wxt-dev/wxt/commit/bc24ea4))\n\n### 🏡 Chore\n\n- Simplify entrypoint types ([#464](https://github.com/wxt-dev/wxt/pull/464))\n\n### ❤️ Contributors\n\n- Okou ([@ookkoouu](https://github.com/ookkoouu))\n\n## v0.16.8\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.7...v0.16.8)\n\n### 🩹 Fixes\n\n- Watch files outside project root during development ([#454](https://github.com/wxt-dev/wxt/pull/454))\n\n### 📖 Documentation\n\n- Add loading and error states for \"Who's using WXT\" section ([447a48f](https://github.com/wxt-dev/wxt/commit/447a48f))\n\n## v0.16.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.6...v0.16.7)\n\n### 🚀 Enhancements\n\n- Generate ESLint globals file for auto-imports ([#450](https://github.com/wxt-dev/wxt/pull/450))\n\n### 🔥 Performance\n\n- Upgrade Vite to 5.1 ([#452](https://github.com/wxt-dev/wxt/pull/452))\n\n### 📖 Documentation\n\n- Add section about dev mode differences ([a0d1643](https://github.com/wxt-dev/wxt/commit/a0d1643))\n- Remove anchor from content script ui examples ([87a62a1](https://github.com/wxt-dev/wxt/commit/87a62a1))\n\n### 🏡 Chore\n\n- **e2e:** Use `wxt prepare` instead of `wxt build` when possible to speed up E2E tests ([#451](https://github.com/wxt-dev/wxt/pull/451))\n\n## v0.16.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.5...v0.16.6)\n\n### 🚀 Enhancements\n\n- Add option to customize the analysis artifacts output ([#431](https://github.com/wxt-dev/wxt/pull/431))\n\n### 🩹 Fixes\n\n- Use `insertBefore` on mounting content script UI ([ba85fdf](https://github.com/wxt-dev/wxt/commit/ba85fdf))\n\n### 💅 Refactors\n\n- Use `Element.prepend` on mounting UI ([295f860](https://github.com/wxt-dev/wxt/commit/295f860))\n\n### 📖 Documentation\n\n- Fix `createShadowRootUi` unmount calls ([946072f](https://github.com/wxt-dev/wxt/commit/946072f))\n\n### 🏡 Chore\n\n- Enable skipped test since it works now ([6b8dfdf](https://github.com/wxt-dev/wxt/commit/6b8dfdf))\n\n### ❤️ Contributors\n\n- Lionelhorn ([@Lionelhorn](https://github.com/Lionelhorn))\n- Okou ([@ookkoouu](https://github.com/ookkoouu))\n\n## v0.16.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.4...v0.16.5)\n\n### 🩹 Fixes\n\n- Support node 20 when running `wxt submit` ([e835502](https://github.com/wxt-dev/wxt/commit/e835502))\n\n### 📖 Documentation\n\n- Remove \"coming soon\" from automated publishing feature ([2b374b9](https://github.com/wxt-dev/wxt/commit/2b374b9))\n\n## v0.16.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.3...v0.16.4)\n\n### 🚀 Enhancements\n\n- Automatically convert MV3 `web_accessible_resources` to MV2 ([#423](https://github.com/wxt-dev/wxt/pull/423))\n- Add option to customize the analysis output filename ([#426](https://github.com/wxt-dev/wxt/pull/426))\n\n### 🩹 Fixes\n\n- Don't use immer for `transformManifest` ([#424](https://github.com/wxt-dev/wxt/pull/424))\n- Exclude analysis files from the build summary ([#425](https://github.com/wxt-dev/wxt/pull/425))\n\n### 🏡 Chore\n\n- Fix fake path in test data generator ([d0f1c70](https://github.com/wxt-dev/wxt/commit/d0f1c70))\n\n## v0.16.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.2...v0.16.3)\n\n### 🚀 Enhancements\n\n- Hooks ([#419](https://github.com/wxt-dev/wxt/pull/419))\n\n### 🩹 Fixes\n\n- **init:** Use `ungh` to prevent rate limits when loading templates ([37ad2c7](https://github.com/wxt-dev/wxt/commit/37ad2c7))\n\n### 📖 Documentation\n\n- Fix typo of intuitive ([#415](https://github.com/wxt-dev/wxt/pull/415))\n- Fix typo of opinionated ([#416](https://github.com/wxt-dev/wxt/pull/416))\n\n### 🏡 Chore\n\n- Add dependabot for github actions ([#404](https://github.com/wxt-dev/wxt/pull/404))\n- **deps-dev:** Bump happy-dom from 12.10.3 to 13.3.8 ([#411](https://github.com/wxt-dev/wxt/pull/411))\n- **deps-dev:** Bump typescript from 5.3.2 to 5.3.3 ([#409](https://github.com/wxt-dev/wxt/pull/409))\n- Register global `wxt` instance ([#418](https://github.com/wxt-dev/wxt/pull/418))\n\n### ❤️ Contributors\n\n- Chen Hua ([@hcljsq](https://github.com/hcljsq))\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v0.16.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.1...v0.16.2)\n\n### 🩹 Fixes\n\n- Don't crash background service worker when using `import.meta.url` ([#402](https://github.com/wxt-dev/wxt/pull/402))\n\n## v0.16.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.0...v0.16.1)\n\n### 🩹 Fixes\n\n- Don't require config to run `wxt submit init` ([9318346](https://github.com/wxt-dev/wxt/commit/9318346))\n\n### 📖 Documentation\n\n- Add premid extension to homepage ([#399](https://github.com/wxt-dev/wxt/pull/399))\n\n### 🏡 Chore\n\n- **templates:** Upgrade to wxt `^0.16.0` ([f0b2a12](https://github.com/wxt-dev/wxt/commit/f0b2a12))\n\n### ❤️ Contributors\n\n- Florian Metz ([@Timeraa](http://github.com/Timeraa))\n\n## v0.16.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.4...v0.16.0)\n\n### 🚀 Enhancements\n\n- ⚠️ ESM background support ([#398](https://github.com/wxt-dev/wxt/pull/398))\n\n### 📖 Documentation\n\n- Document how to opt into ESM ([1e12ce2](https://github.com/wxt-dev/wxt/commit/1e12ce2))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump lint-staged from 15.2.0 to 15.2.1 ([#395](https://github.com/wxt-dev/wxt/pull/395))\n- **deps-dev:** Bump p-map from 7.0.0 to 7.0.1 ([#396](https://github.com/wxt-dev/wxt/pull/396))\n- **deps-dev:** Bump @vitest/coverage-v8 from 1.0.1 to 1.2.2 ([#397](https://github.com/wxt-dev/wxt/pull/397))\n\n## v0.15.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.3...v0.15.4)\n\n### 🩹 Fixes\n\n- **submit:** Load `.env.submit` automatically when running `wxt submit` and `wxt submit init` ([#391](https://github.com/wxt-dev/wxt/pull/391))\n\n## v0.15.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.2...v0.15.3)\n\n### 🩹 Fixes\n\n- **dev:** Reload `<name>/index.html` entrypoints properly on save ([#390](https://github.com/wxt-dev/wxt/pull/390))\n\n## v0.15.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.1...v0.15.2)\n\n### 🚀 Enhancements\n\n- Add `submit` command ([#370](https://github.com/wxt-dev/wxt/pull/370))\n\n### 🩹 Fixes\n\n- **dev:** Resolve `script` and `link` aliases ([#387](https://github.com/wxt-dev/wxt/pull/387))\n\n### ❤️ Contributors\n\n- Nenad Novaković ([@dvlden](https://github.com/dvlden))\n\n## v0.15.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.15.0...v0.15.1)\n\n### 🚀 Enhancements\n\n- Allow passing custom preferences to chrome, enabling dev mode on `chrome://extensions` and allowing content script sourcemaps automatically ([#384](https://github.com/wxt-dev/wxt/pull/384))\n\n### 🩹 Fixes\n\n- **security:** Upgrade to vite@5.0.12 to resolve CVE-2024-23331 ([39b76d3](https://github.com/wxt-dev/wxt/commit/39b76d3))\n\n### 📖 Documentation\n\n- Fixed doc errors on the guide/extension-api page ([#383](https://github.com/wxt-dev/wxt/pull/383))\n\n### 🏡 Chore\n\n- Fix vite version conflicts in demo extension ([98d2792](https://github.com/wxt-dev/wxt/commit/98d2792))\n\n### ❤️ Contributors\n\n- 0x7a7a ([@0x7a7a](https://github.com/0x7a7a))\n\n## v0.15.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.7...v0.15.0)\n\n### 🚀 Enhancements\n\n- **zip:** ⚠️ Add `includeSources` and rename `ignoredSources` to `excludeSources` ([#378](https://github.com/wxt-dev/wxt/pull/378))\n\n### 🩹 Fixes\n\n- Generate missing sourcemap in `wxt:unimport` plugin ([#381](https://github.com/wxt-dev/wxt/pull/381))\n- ⚠️ Move browser constants to `import.meta.env` ([#380](https://github.com/wxt-dev/wxt/pull/380))\n- Enable inline sourcemaps by default during development ([#382](https://github.com/wxt-dev/wxt/pull/382))\n\n### 📖 Documentation\n\n- Fix typo ([f9718a1](https://github.com/wxt-dev/wxt/commit/f9718a1))\n\n### 🏡 Chore\n\n- Update contributor docs ([eb758bd](https://github.com/wxt-dev/wxt/commit/eb758bd))\n\n### ❤️ Contributors\n\n- Nenad Novaković ([@dvlden](https://github.com/dvlden))\n\n## v0.14.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.6...v0.14.7)\n\n### 🩹 Fixes\n\n- Improve error messages when importing and building entrypoints ([3b63a51](https://github.com/wxt-dev/wxt/commit/3b63a51))\n- **storage:** Throw better error message when importing outside a extension environment ([35865ad](https://github.com/wxt-dev/wxt/commit/35865ad))\n- Upgrade `web-ext-run` ([62ecb6f](https://github.com/wxt-dev/wxt/commit/62ecb6f))\n\n### 📖 Documentation\n\n- Add `matches` to content script examples ([dab8efa](https://github.com/wxt-dev/wxt/commit/dab8efa))\n- Fix incorrect sample code ([#372](https://github.com/wxt-dev/wxt/pull/372))\n- Document defined constants for the build target ([68874e6](https://github.com/wxt-dev/wxt/commit/68874e6))\n- Add missing `await` to `createShadowRootUi` examples ([fc45c37](https://github.com/wxt-dev/wxt/commit/fc45c37))\n\n### ❤️ Contributors\n\n- 東奈比 ([@dongnaebi](http://github.com/dongnaebi))\n\n## v0.14.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.5...v0.14.6)\n\n### 🚀 Enhancements\n\n- Restart dev mode when saving config ([#365](https://github.com/wxt-dev/wxt/pull/365))\n- Add basic validation for entrypoint options ([#368](https://github.com/wxt-dev/wxt/pull/368))\n\n### 🩹 Fixes\n\n- Add subdependency bin directory so `wxt build --analyze` works with PNPM ([#363](https://github.com/wxt-dev/wxt/pull/363))\n- Sort build output files naturally ([#364](https://github.com/wxt-dev/wxt/pull/364))\n\n### 🤖 CI\n\n- Check for type errors in demo before building ([4b005b4](https://github.com/wxt-dev/wxt/commit/4b005b4))\n\n## v0.14.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.4...v0.14.5)\n\n### 🚀 Enhancements\n\n- Add `dev.reloadCommand` config ([#362](https://github.com/wxt-dev/wxt/pull/362))\n\n### 🩹 Fixes\n\n- Disable reload dev command when 4 commands are already registered ([#361](https://github.com/wxt-dev/wxt/pull/361))\n\n## v0.14.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.3...v0.14.4)\n\n### 🩹 Fixes\n\n- Allow requiring built-in node modules from ESM CLI ([#356](https://github.com/wxt-dev/wxt/pull/356))\n\n### 🏡 Chore\n\n- Add unit tests for passing flags via the CLI ([#354](https://github.com/wxt-dev/wxt/pull/354))\n\n## v0.14.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.2...v0.14.3)\n\n### 🩹 Fixes\n\n- Make `getArrayFromFlags` result can be undefined ([#352](https://github.com/wxt-dev/wxt/pull/352))\n\n### ❤️ Contributors\n\n- Yuns ([@yunsii](http://github.com/yunsii))\n\n## v0.14.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.1...v0.14.2)\n\n### 🚀 Enhancements\n\n- Add `filterEntrypoints` option to speed up development ([#344](https://github.com/wxt-dev/wxt/pull/344))\n\n### 🔥 Performance\n\n- Only call `findEntrypoint` once per build ([#342](https://github.com/wxt-dev/wxt/pull/342))\n\n### 🩹 Fixes\n\n- Improve error message and document use of imported variables outside an entrypoint's `main` function ([#346](https://github.com/wxt-dev/wxt/pull/346))\n- Allow `browser.runtime.getURL` to include hashes and query params for HTML paths ([#350](https://github.com/wxt-dev/wxt/pull/350))\n\n### 📖 Documentation\n\n- Fix typos and outdated ui function usage ([#347](https://github.com/wxt-dev/wxt/pull/347))\n\n### 🏡 Chore\n\n- Update templates to `^0.14.0` ([70a4961](https://github.com/wxt-dev/wxt/commit/70a4961))\n- Fix typo in function name ([a329e24](https://github.com/wxt-dev/wxt/commit/a329e24))\n\n### ❤️ Contributors\n\n- Yuns ([@yunsii](http://github.com/yunsii))\n- Armin\n\n## v0.14.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.0...v0.14.1)\n\n### 🩹 Fixes\n\n- Use `Alt+R`/`Opt+R` to reload extension during development ([b6ab7a9](https://github.com/wxt-dev/wxt/commit/b6ab7a9))\n\n## v0.14.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.5...v0.14.0)\n\n### 🚀 Enhancements\n\n- ⚠️ Refactor content script UI functions and add helper for \"integrated\" UIs ([#333](https://github.com/wxt-dev/wxt/pull/333))\n\n## v0.13.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.4...v0.13.5)\n\n### 🩹 Fixes\n\n- Strip path from `web_accessible_resources[0].matches` ([#332](https://github.com/wxt-dev/wxt/pull/332))\n\n### 📖 Documentation\n\n- Add section about customizing other browser options during development ([8683bd4](https://github.com/wxt-dev/wxt/commit/8683bd4))\n\n### 🏡 Chore\n\n- Update bug report template ([9a2cc18](https://github.com/wxt-dev/wxt/commit/9a2cc18))\n\n## v0.13.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.3...v0.13.4)\n\n### 🩹 Fixes\n\n- Disable minification during development ([b7cdf15](https://github.com/wxt-dev/wxt/commit/b7cdf15))\n\n### 🏡 Chore\n\n- Use `const` instead of `let` ([2770974](https://github.com/wxt-dev/wxt/commit/2770974))\n\n## v0.13.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.2...v0.13.3)\n\n### 🚀 Enhancements\n\n- **DX:** Add `ctrl+E`/`cmd+E` shortcut to reload extension during development ([#322](https://github.com/wxt-dev/wxt/pull/322))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump tsx from 4.6.2 to 4.7.0 ([#320](https://github.com/wxt-dev/wxt/pull/320))\n- **deps-dev:** Bump prettier from 3.1.0 to 3.1.1 ([#318](https://github.com/wxt-dev/wxt/pull/318))\n- **deps-dev:** Bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 ([#316](https://github.com/wxt-dev/wxt/pull/316))\n- Refactor manifest generation E2E tests to unit tests ([#323](https://github.com/wxt-dev/wxt/pull/323))\n\n## v0.13.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.1...v0.13.2)\n\n### 🚀 Enhancements\n\n- Add `isolateEvents` option to `createContentScripUi` ([#313](https://github.com/wxt-dev/wxt/pull/313))\n\n### 📖 Documentation\n\n- Remove duplicate `entrypoints/` path ([76e63e2](https://github.com/wxt-dev/wxt/commit/76e63e2))\n- Update unlisted pages/scripts description ([c99a281](https://github.com/wxt-dev/wxt/commit/c99a281))\n- Update content script entrypoint docs ([1360eb7](https://github.com/wxt-dev/wxt/commit/1360eb7))\n- Add example for setting up custom panels/panes in devtools ([#308](https://github.com/wxt-dev/wxt/pull/308))\n- Use example tags to automate relevant example lists ([#311](https://github.com/wxt-dev/wxt/pull/311))\n\n### 🏡 Chore\n\n- Update templates to `^0.13.0` ([#309](https://github.com/wxt-dev/wxt/pull/309))\n- Upgrade template dependencies ([#310](https://github.com/wxt-dev/wxt/pull/310))\n- Re-enable coverage ([#312](https://github.com/wxt-dev/wxt/pull/312))\n\n### ❤️ Contributors\n\n- 冯不游\n\n## v0.13.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.0...v0.13.1)\n\n### 🩹 Fixes\n\n- **storage:** Support multiple `:` characters in storage keys ([#303](https://github.com/wxt-dev/wxt/pull/303))\n- Ship `vite/client` types internally for proper resolution using PNPM ([#304](https://github.com/wxt-dev/wxt/pull/304))\n\n### 📖 Documentation\n\n- Reorder guide ([6421ab3](https://github.com/wxt-dev/wxt/commit/6421ab3))\n- General fixes and improvements ([2ad099b](https://github.com/wxt-dev/wxt/commit/2ad099b))\n\n### 🏡 Chore\n\n- Update `scripts/build.ts` show current build step in progress, not completed count ([#306](https://github.com/wxt-dev/wxt/pull/306))\n\n## v0.13.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.5...v0.13.0)\n\n### 🚀 Enhancements\n\n- ⚠️ New `wxt/storage` APIs ([#300](https://github.com/wxt-dev/wxt/pull/300))\n\n## v0.12.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.4...v0.12.5)\n\n### 🩹 Fixes\n\n- Correct import in dev-only, noop background ([#298](https://github.com/wxt-dev/wxt/pull/298))\n\n## v0.12.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.3...v0.12.4)\n\n### 🩹 Fixes\n\n- Disable Vite CJS warnings ([#296](https://github.com/wxt-dev/wxt/pull/296))\n\n## v0.12.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.2...v0.12.3)\n\n### 🩹 Fixes\n\n- Correctly mock `webextension-polyfill` for Vitest ([#294](https://github.com/wxt-dev/wxt/pull/294))\n\n## v0.12.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.1...v0.12.2)\n\n### 🚀 Enhancements\n\n- Support PNPM without hoisting dependencies ([#291](https://github.com/wxt-dev/wxt/pull/291))\n\n## v0.12.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.0...v0.12.1)\n\n### 🩹 Fixes\n\n- Upgrade `@webext-core/match-patterns` to `1.0.3` ([#289](https://github.com/wxt-dev/wxt/pull/289))\n- Fix `package.json` lint errors ([#290](https://github.com/wxt-dev/wxt/pull/290))\n\n### 🏡 Chore\n\n- Upgrade templates to `wxt@^0.12.0` ([#285](https://github.com/wxt-dev/wxt/pull/285))\n\n## v0.12.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.2...v0.12.0)\n\n### 🚀 Enhancements\n\n- ⚠️ Add support for \"main world\" content scripts ([#284](https://github.com/wxt-dev/wxt/pull/284))\n\n### 🩹 Fixes\n\n- Only use type imports for Vite ([#278](https://github.com/wxt-dev/wxt/pull/278))\n- Throw error when no entrypoints are found ([#283](https://github.com/wxt-dev/wxt/pull/283))\n\n### 📖 Documentation\n\n- Improve content script UI guide ([#272](https://github.com/wxt-dev/wxt/pull/272))\n- Fix dead links ([291d25b](https://github.com/wxt-dev/wxt/commit/291d25b))\n\n### 🏡 Chore\n\n- Convert WXT CLI to an ESM binary ([#279](https://github.com/wxt-dev/wxt/pull/279))\n\n## v0.11.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.1...v0.11.2)\n\n### 🩹 Fixes\n\n- Discover `.js`, `.jsx`, and `.tsx` unlisted scripts correctly ([#274](https://github.com/wxt-dev/wxt/pull/274))\n- Improve duplicate entrypoint name detection and catch the error before loading their config ([#276](https://github.com/wxt-dev/wxt/pull/276))\n\n### 📖 Documentation\n\n- Improve content script UI docs ([#268](https://github.com/wxt-dev/wxt/pull/268))\n\n### 🏡 Chore\n\n- Update sSolid template to vite 5 ([#265](https://github.com/wxt-dev/wxt/pull/265))\n- Add missing navigation item ([bcb93af](https://github.com/wxt-dev/wxt/commit/bcb93af))\n\n## v0.11.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.0...v0.11.1)\n\n### 🚀 Enhancements\n\n- Add util for detecting URL changes in content scripts ([#264](https://github.com/wxt-dev/wxt/pull/264))\n\n### 🏡 Chore\n\n- Upgrade templates to `wxt@^0.11.0` ([#263](https://github.com/wxt-dev/wxt/pull/263))\n\n## v0.11.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.4...v0.11.0)\n\n### 🚀 Enhancements\n\n- ⚠️ Vite 5 support ([#261](https://github.com/wxt-dev/wxt/pull/261))\n\n### 📖 Documentation\n\n- Adds tl;dv to homepage ([#260](https://github.com/wxt-dev/wxt/pull/260))\n\n### 🏡 Chore\n\n- Speed up CI using `pnpm` instead of `npm` ([#259](https://github.com/wxt-dev/wxt/pull/259))\n- Abstract vite from WXT's core logic ([#242](https://github.com/wxt-dev/wxt/pull/242))\n\n### ❤️ Contributors\n\n- Ítalo Brasil ([@italodeverdade](http://github.com/italodeverdade))\n\n## v0.10.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.3...v0.10.4)\n\n### 🚀 Enhancements\n\n- Add config to customize `outDir` ([#258](https://github.com/wxt-dev/wxt/pull/258))\n\n### 📖 Documentation\n\n- Add Doozy to homepage ([#249](https://github.com/wxt-dev/wxt/pull/249))\n- Update sidepanel availability ([#250](https://github.com/wxt-dev/wxt/pull/250))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump prettier from 3.0.3 to 3.1.0 ([#254](https://github.com/wxt-dev/wxt/pull/254))\n- **deps-dev:** Bump @types/lodash.merge from 4.6.8 to 4.6.9 ([#255](https://github.com/wxt-dev/wxt/pull/255))\n- **deps-dev:** Bump tsx from 3.14.0 to 4.6.1 ([#252](https://github.com/wxt-dev/wxt/pull/252))\n\n### ❤️ Contributors\n\n- 冯不游\n\n## v0.10.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.2...v0.10.3)\n\n### 🩹 Fixes\n\n- **auto-imports:** Don't add imports to `node_module` dependencies ([#247](https://github.com/wxt-dev/wxt/pull/247))\n\n### 📖 Documentation\n\n- Fix typo ([317b1b6](https://github.com/wxt-dev/wxt/commit/317b1b6))\n\n### 🏡 Chore\n\n- Trigger docs upgrade via webhook ([742b996](https://github.com/wxt-dev/wxt/commit/742b996))\n- Use `normalize-path` instead of `vite.normalizePath` ([#244](https://github.com/wxt-dev/wxt/pull/244))\n- Use `defu` for merging some config objects ([#243](https://github.com/wxt-dev/wxt/pull/243))\n\n### 🤖 CI\n\n- Publish docs on push to main ([1611c1d](https://github.com/wxt-dev/wxt/commit/1611c1d))\n- Only print response headers from docs webhook ([97cbda3](https://github.com/wxt-dev/wxt/commit/97cbda3))\n\n## v0.10.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.1...v0.10.2)\n\n### 🩹 Fixes\n\n- Apply `mode` option to build steps correctly ([82ed821](https://github.com/wxt-dev/wxt/commit/82ed821))\n\n### 🏡 Chore\n\n- Upgrade templates to v0.10 ([#239](https://github.com/wxt-dev/wxt/pull/239))\n\n## v0.10.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.0...v0.10.1)\n\n### 🩹 Fixes\n\n- Remove WXT global to remove unused modules from production builds ([3da3e07](https://github.com/wxt-dev/wxt/commit/3da3e07))\n\n## v0.10.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.2...v0.10.0)\n\n### 🚀 Enhancements\n\n- List `bun` as an experimental option in `wxt init` ([#233](https://github.com/wxt-dev/wxt/pull/233))\n- ⚠️ Allow plural directory and only png's for manifest icons ([#237](https://github.com/wxt-dev/wxt/pull/237))\n- Add `wxt/storage` API ([#234](https://github.com/wxt-dev/wxt/pull/234))\n\n### 🩹 Fixes\n\n- Don't use `bun` to load entrypoint config ([#232](https://github.com/wxt-dev/wxt/pull/232))\n\n### 📖 Documentation\n\n- Update main README links ([207b750](https://github.com/wxt-dev/wxt/commit/207b750))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️ No longer discover icons with extensions other than `.png`. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file ([#237](https://github.com/wxt-dev/wxt/pull/237))\n\n### ❤️ Contributors\n\n- Nenad Novaković ([@dvlden](https://github.com/dvlden))\n\n## v0.9.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.1...v0.9.2)\n\n### 🚀 Enhancements\n\n- Experimental option to exclude `webextension-polyfill` ([#231](https://github.com/wxt-dev/wxt/pull/231))\n\n### 🤖 CI\n\n- Fix sync-release workflow ([d1b5230](https://github.com/wxt-dev/wxt/commit/d1b5230))\n\n## v0.9.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.9.0...v0.9.1)\n\n### 🚀 Enhancements\n\n- Add `alias` config for customizing path aliases ([#216](https://github.com/wxt-dev/wxt/pull/216))\n\n### 🩹 Fixes\n\n- Move `webextension-polyfill` from peer to regular dependencies ([609ae2a](https://github.com/wxt-dev/wxt/commit/609ae2a))\n- Generate valid manifest for Firefox MV3 ([#229](https://github.com/wxt-dev/wxt/pull/229))\n\n### 📖 Documentation\n\n- Add examples ([c81dfff](https://github.com/wxt-dev/wxt/commit/c81dfff))\n- Improve the \"Used By\" section on homepage ([#220](https://github.com/wxt-dev/wxt/pull/220))\n- Add UltraWideo to homepage ([#193](https://github.com/wxt-dev/wxt/pull/193))\n- Add StayFree to homepage ([#221](https://github.com/wxt-dev/wxt/pull/221))\n- Update feature comparison ([67ffa44](https://github.com/wxt-dev/wxt/commit/67ffa44))\n\n### 🏡 Chore\n\n- Remove whitespace from generated `.wxt` files ([#211](https://github.com/wxt-dev/wxt/pull/211))\n- Upgrade templates to `wxt@^0.9.0` ([#214](https://github.com/wxt-dev/wxt/pull/214))\n- Update Vite dependency range to `^4.0.0 || ^5.0.0-0` ([f1e8084](https://github.com/wxt-dev/wxt/commit/f1e8084be89e512dde441b9197a99183c497f67d))\n\n### 🤖 CI\n\n- Automatically sync GitHub releases with `CHANGELOG.md` on push ([#218](https://github.com/wxt-dev/wxt/pull/218))\n\n### ❤️ Contributors\n\n- Aaron Klinker ([@aaronklinker-st](http://github.com/aaronklinker-st))\n\n## v0.9.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.7...v0.9.0)\n\n### 🩹 Fixes\n\n- ⚠️ Remove `lib` from `.wxt/tsconfig.json` ([#209](https://github.com/wxt-dev/wxt/pull/209))\n\n### 📖 Documentation\n\n- Fix heading ([345406f](https://github.com/wxt-dev/wxt/commit/345406f))\n- Add demo video ([#208](https://github.com/wxt-dev/wxt/pull/208))\n\n### 🏡 Chore\n\n- Fix Svelte and React template READMEs ([#207](https://github.com/wxt-dev/wxt/pull/207))\n\n### ❤️ Contributors\n\n- yyyanghj ([@yyyanghj](https://github.com/yyyanghj))\n\n## v0.8.7\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.6...v0.8.7)\n\n### 🚀 Enhancements\n\n- `createContentScriptIframe` utility ([#206](https://github.com/wxt-dev/wxt/pull/206))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump happy-dom from 12.4.0 to 12.10.3 ([#194](https://github.com/wxt-dev/wxt/pull/194))\n- **deps-dev:** Bump tsx from 3.12.8 to 3.14.0 ([#198](https://github.com/wxt-dev/wxt/pull/198))\n- Upgrade types ([f3874da](https://github.com/wxt-dev/wxt/commit/f3874da))\n- **deps-dev:** Upgrade `lint-staged` to `^15.0.2` ([5f74a54](https://github.com/wxt-dev/wxt/commit/5f74a54))\n- **deps-dev:** Upgrade `execa` to `^8.0.1` ([#200](https://github.com/wxt-dev/wxt/pull/200))\n- **deps-dev:** Upgrade `typedoc` to `^0.25.3` ([#201](https://github.com/wxt-dev/wxt/pull/201))\n- **deps-dev:** Upgrade `vue` to `3.3.7` ([0b8d101](https://github.com/wxt-dev/wxt/commit/0b8d101))\n- **deps-dev:** Upgrade `vitepress` to `1.0.0-rc.24` ([5de18e5](https://github.com/wxt-dev/wxt/commit/5de18e5))\n- **deps-dev:** Update `@type/*` packages for demo ([cd4d00e](https://github.com/wxt-dev/wxt/commit/cd4d00e))\n- **deps-dev:** Update `sass` to `1.69.5` ([183bb02](https://github.com/wxt-dev/wxt/commit/183bb02))\n- Improve prettier git hook ([0f09cbe](https://github.com/wxt-dev/wxt/commit/0f09cbe))\n- Run E2E tests in parallel ([#204](https://github.com/wxt-dev/wxt/pull/204))\n\n### 🤖 CI\n\n- Separate validation into multiple jobs ([#203](https://github.com/wxt-dev/wxt/pull/203))\n\n## v0.8.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.5...v0.8.6)\n\n### 🩹 Fixes\n\n- Inline WXT modules inside `WxtVitest` plugin ([b75c553](https://github.com/wxt-dev/wxt/commit/b75c553))\n\n## v0.8.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.4...v0.8.5)\n\n### 🚀 Enhancements\n\n- Refactor project structure to export `initialize`, `prepare`, and `zip` functions ([#182](https://github.com/wxt-dev/wxt/pull/182))\n\n### 🩹 Fixes\n\n- Enable Vue SFC auto-imports in `vue` template ([f8a0fb3](https://github.com/wxt-dev/wxt/commit/f8a0fb3))\n\n### 📖 Documentation\n\n- Improve `runner.binaries` documentation ([d9e9b43](https://github.com/wxt-dev/wxt/commit/d9e9b43))\n- Update auto-imports.md ([#186](https://github.com/wxt-dev/wxt/pull/186))\n- Add `test.server.deps.inline` to Vitest guide ([19756c6](https://github.com/wxt-dev/wxt/commit/19756c6))\n\n### 🏡 Chore\n\n- Update template docs ([2e24b9e](https://github.com/wxt-dev/wxt/commit/2e24b9e))\n- Reduce package size by 70%, 1.92 MB to 590 kB ([#190](https://github.com/wxt-dev/wxt/pull/190))\n\n### ❤️ Contributors\n\n- Nenad Novaković ([@dvlden](https://github.com/dvlden))\n\n## v0.8.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.3...v0.8.4)\n\n### 🩹 Fixes\n\n- Allow actions without a popup ([#181](https://github.com/wxt-dev/wxt/pull/181))\n\n## v0.8.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.2...v0.8.3)\n\n### 🚀 Enhancements\n\n- Add testing utils under `wxt/testing` ([#178](https://github.com/wxt-dev/wxt/pull/178))\n\n## v0.8.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.1...v0.8.2)\n\n### 🩹 Fixes\n\n- **firefox:** Stop extending `AbortController` to fix crash in content scripts ([#176](https://github.com/wxt-dev/wxt/pull/176))\n\n### 🏡 Chore\n\n- Improve output consistency ([#175](https://github.com/wxt-dev/wxt/pull/175))\n\n## v0.8.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.0...v0.8.1)\n\n### 🩹 Fixes\n\n- Output `action.browser_style` correctly ([6a93f20](https://github.com/wxt-dev/wxt/commit/6a93f20))\n\n### 📖 Documentation\n\n- Generate full API docs with typedoc ([#174](https://github.com/wxt-dev/wxt/pull/174))\n\n## v0.8.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.5...v0.8.0)\n\n### 🚀 Enhancements\n\n- ⚠️ Use `defineUnlistedScript` to define unlisted scripts ([#167](https://github.com/wxt-dev/wxt/pull/167))\n\n### 📖 Documentation\n\n- Fix wrong links ([#166](https://github.com/wxt-dev/wxt/pull/166))\n\n### 🌊 Types\n\n- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))\n\n### ❤️ Contributors\n\n- 渣渣120 [@WOSHIZHAZHA120](https://github.com/WOSHIZHAZHA120)\n\n## v0.7.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.4...v0.7.5)\n\n### 🩹 Fixes\n\n- More consistent `version_name` generation between browsers ([#163](https://github.com/wxt-dev/wxt/pull/163))\n- Ignore non-manifest fields when merging content script entries ([#164](https://github.com/wxt-dev/wxt/pull/164))\n- Add `browser_style` to popup options ([#165](https://github.com/wxt-dev/wxt/pull/165))\n\n## v0.7.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.3...v0.7.4)\n\n### 🩹 Fixes\n\n- Support `react-refresh` when pre-rendering HTML pages in dev mode ([#158](https://github.com/wxt-dev/wxt/pull/158))\n\n### 📖 Documentation\n\n- Add migration guides ([b58fb02](https://github.com/wxt-dev/wxt/commit/b58fb02))\n\n### 🏡 Chore\n\n- Upgrade templates to v0.7 ([#156](https://github.com/wxt-dev/wxt/pull/156))\n\n## v0.7.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.2...v0.7.3)\n\n### 🚀 Enhancements\n\n- Support JS entrypoints ([#155](https://github.com/wxt-dev/wxt/pull/155))\n\n## v0.7.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.1...v0.7.2)\n\n### 🚀 Enhancements\n\n- Allow customizing entrypoint options per browser ([#154](https://github.com/wxt-dev/wxt/pull/154))\n\n### 🩹 Fixes\n\n- Default safari to MV2 ([5807931](https://github.com/wxt-dev/wxt/commit/5807931))\n- Add missing `persistent` type to `defineBackgroundScript` ([d9fdcb5](https://github.com/wxt-dev/wxt/commit/d9fdcb5))\n\n### 📖 Documentation\n\n- Restructure website to improve UX ([#149](https://github.com/wxt-dev/wxt/pull/149))\n- Add docs for development and testing ([f58d69d](https://github.com/wxt-dev/wxt/commit/f58d69d))\n\n### 🏡 Chore\n\n- **deps-dev:** Bump @types/fs-extra from 11.0.1 to 11.0.2 ([#144](https://github.com/wxt-dev/wxt/pull/144))\n- **deps-dev:** Bump @faker-js/faker from 8.0.2 to 8.1.0 ([#146](https://github.com/wxt-dev/wxt/pull/146))\n- **deps-dev:** Bump vitest-mock-extended from 1.2.1 to 1.3.0 ([#147](https://github.com/wxt-dev/wxt/pull/147))\n- **deps-dev:** Bump vitest from 0.34.3 to 0.34.6 ([#145](https://github.com/wxt-dev/wxt/pull/145))\n- **deps-dev:** Bump typescript from 5.1 to 5.2 ([#148](https://github.com/wxt-dev/wxt/pull/148))\n\n## v0.7.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.0...v0.7.1)\n\n### 🚀 Enhancements\n\n- `createContentScriptUi` helper ([#143](https://github.com/wxt-dev/wxt/pull/143))\n\n### 📖 Documentation\n\n- Add docs for `createContentScriptUi` ([65fcfc0](https://github.com/wxt-dev/wxt/commit/65fcfc0))\n\n### 🏡 Chore\n\n- **release:** V0.7.1-alpha1 ([2d4983e](https://github.com/wxt-dev/wxt/commit/2d4983e))\n\n## v0.7.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.6...v0.7.0)\n\n### 🚀 Enhancements\n\n- Content script `cssInjectionMode` ([#141](https://github.com/wxt-dev/wxt/pull/141))\n\n### 🩹 Fixes\n\n- Validate transformed manifest correctly ([4b2012c](https://github.com/wxt-dev/wxt/commit/4b2012c))\n- ⚠️ Output content script CSS to `content-scripts/<name>.css` ([#140](https://github.com/wxt-dev/wxt/pull/140))\n- Reorder typescript paths to give priority to `@` and `~` over `@@` and `~~` ([#142](https://github.com/wxt-dev/wxt/pull/142))\n\n### 🏡 Chore\n\n- Store user config metadata in memory ([0591050](https://github.com/wxt-dev/wxt/commit/0591050))\n\n## v0.6.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.5...v0.6.6)\n\n### 🚀 Enhancements\n\n- Disable opening browser automatically during dev mode ([#136](https://github.com/wxt-dev/wxt/pull/136))\n\n## v0.6.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.4...v0.6.5)\n\n### 🩹 Fixes\n\n- Don't crash when `<all_urls>` matches is used in dev mode ([b48cee9](https://github.com/wxt-dev/wxt/commit/b48cee9))\n- Support loading `tsx` entrypoints ([#134](https://github.com/wxt-dev/wxt/pull/134))\n\n### 📖 Documentation\n\n- Add tags for SEO and socials ([96be879](https://github.com/wxt-dev/wxt/commit/96be879))\n- Add more content to the homepage ([5570793](https://github.com/wxt-dev/wxt/commit/5570793))\n- Fix DX section sizing ([41e1549](https://github.com/wxt-dev/wxt/commit/41e1549))\n- Add link to update extensions using WXT ([24e69fe](https://github.com/wxt-dev/wxt/commit/24e69fe))\n\n### 🏡 Chore\n\n- Code coverage improvements ([#131](https://github.com/wxt-dev/wxt/pull/131))\n\n## v0.6.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.3...v0.6.4)\n\n### 🩹 Fixes\n\n- **content-scripts:** Don't throw an error when including `include` or `exclude` options on a content script ([455e7f3](https://github.com/wxt-dev/wxt/commit/455e7f3))\n- Use `execaCommand` instead of `node:child_process` ([#130](https://github.com/wxt-dev/wxt/pull/130))\n\n### 🏡 Chore\n\n- **templates:** Add `.wxt` directory to gitignore ([#129](https://github.com/wxt-dev/wxt/pull/129))\n- Increase E2E test timeout ([5482b2f](https://github.com/wxt-dev/wxt/commit/5482b2f))\n\n## v0.6.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.2...v0.6.3)\n\n### 🚀 Enhancements\n\n- **client:** Add `block` and `addEventListener` utils to `ContentScriptContext` ([#128](https://github.com/wxt-dev/wxt/pull/128))\n\n## v0.6.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.1...v0.6.2)\n\n### 🚀 Enhancements\n\n- `--analyze` build flag ([#125](https://github.com/wxt-dev/wxt/pull/125))\n- Show spinner when building entrypoints ([#126](https://github.com/wxt-dev/wxt/pull/126))\n\n### 📖 Documentation\n\n- Fix import typo ([4c43072](https://github.com/wxt-dev/wxt/commit/4c43072))\n- Update vite docs to use function ([e0929a6](https://github.com/wxt-dev/wxt/commit/e0929a6))\n\n## v0.6.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.0...v0.6.1)\n\n### 🚀 Enhancements\n\n- Add `transformManifest` option ([#124](https://github.com/wxt-dev/wxt/pull/124))\n\n### 🩹 Fixes\n\n- Don't open browser during development when using WSL ([#123](https://github.com/wxt-dev/wxt/pull/123))\n\n### 📖 Documentation\n\n- Load extension details from CWS ([8e0a189](https://github.com/wxt-dev/wxt/commit/8e0a189))\n\n## v0.6.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.6...v0.6.0)\n\n### 🚀 Enhancements\n\n- Export `ContentScriptContext` from `wxt/client` ([1f448d1](https://github.com/wxt-dev/wxt/commit/1f448d1))\n- ⚠️ Require a function for `vite` configuration ([#121](https://github.com/wxt-dev/wxt/pull/121))\n\n### 🩹 Fixes\n\n- Use the same mode for each build step ([1f6a931](https://github.com/wxt-dev/wxt/commit/1f6a931))\n- Disable dev logs in production ([3f260ee](https://github.com/wxt-dev/wxt/commit/3f260ee))\n\n## v0.5.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.5...v0.5.6)\n\n### 🚀 Enhancements\n\n- Add `ContentScriptContext` util for stopping invalidated content scripts ([#120](https://github.com/wxt-dev/wxt/pull/120))\n\n## v0.5.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.4...v0.5.5)\n\n### 🩹 Fixes\n\n- Automatically replace vite's `process.env.NODE_ENV` output in lib mode with the mode ([92039b8](https://github.com/wxt-dev/wxt/commit/92039b8))\n\n## v0.5.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.3...v0.5.4)\n\n### 🩹 Fixes\n\n- Recognize `background/index.ts` as an entrypoint ([419fab8](https://github.com/wxt-dev/wxt/commit/419fab8))\n- Don't warn about deep entrypoint subdirectories not being recognized ([87e8df9](https://github.com/wxt-dev/wxt/commit/87e8df9))\n\n## v0.5.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.2...v0.5.3)\n\n### 🩹 Fixes\n\n- Allow function for vite config ([4ec904e](https://github.com/wxt-dev/wxt/commit/4ec904e))\n\n### 🏡 Chore\n\n- Refactor how config is resolved ([#118](https://github.com/wxt-dev/wxt/pull/118))\n\n## v0.5.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.1...v0.5.2)\n\n### 🩹 Fixes\n\n- Import client utils when getting entrypoint config ([#117](https://github.com/wxt-dev/wxt/pull/117))\n\n## v0.5.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.0...v0.5.1)\n\n### 🚀 Enhancements\n\n- Allow disabling auto-imports ([#114](https://github.com/wxt-dev/wxt/pull/114))\n- Include/exclude entrypoints based on target browser ([#115](https://github.com/wxt-dev/wxt/pull/115))\n\n### 🩹 Fixes\n\n- Allow any string for target browser ([b4de93d](https://github.com/wxt-dev/wxt/commit/b4de93d))\n\n## v0.5.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.1...v0.5.0)\n\n### 🩹 Fixes\n\n- **types:** Don't write to files if nothing changes ([#107](https://github.com/wxt-dev/wxt/pull/107))\n- ⚠️ Change default `publicDir` to `<srcDir>/public` ([5f15f9c](https://github.com/wxt-dev/wxt/commit/5f15f9c))\n\n### 📖 Documentation\n\n- Add link to examples repo ([46a5036](https://github.com/wxt-dev/wxt/commit/46a5036))\n- Fix typos ([beafa6a](https://github.com/wxt-dev/wxt/commit/beafa6a))\n- Make README pretty ([b33b663](https://github.com/wxt-dev/wxt/commit/b33b663))\n- Add migration docs ([e2350fe](https://github.com/wxt-dev/wxt/commit/e2350fe))\n- Add vite customization docs ([fe966b6](https://github.com/wxt-dev/wxt/commit/fe966b6))\n\n### 🏡 Chore\n\n- Move repo to wxt-dev org ([ac7cbfc](https://github.com/wxt-dev/wxt/commit/ac7cbfc))\n- **deps-dev:** Bump prettier from 3.0.1 to 3.0.3 ([#111](https://github.com/wxt-dev/wxt/pull/111))\n- **deps-dev:** Bump tsx from 3.12.7 to 3.12.8 ([#109](https://github.com/wxt-dev/wxt/pull/109))\n- **deps-dev:** Bump @types/node from 20.5.0 to 20.5.9 ([#110](https://github.com/wxt-dev/wxt/pull/110))\n- Add entrypoints debug log ([dbd84c8](https://github.com/wxt-dev/wxt/commit/dbd84c8))\n\n## v0.4.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.0...v0.4.1)\n\n### 🚀 Enhancements\n\n- **cli:** Add `wxt clean` command to delete generated files ([#106](https://github.com/wxt-dev/wxt/pull/106))\n\n### 🩹 Fixes\n\n- **init:** Don't show `cd .` when initializing the current directory ([e086374](https://github.com/wxt-dev/wxt/commit/e086374))\n\n## v0.4.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.2...v0.4.0)\n\n### 🚀 Enhancements\n\n- Add `--debug` flag for printing debug logs for all CLI commands ([#75](https://github.com/wxt-dev/wxt/pull/75))\n- Replace `web-ext` with `web-ext-run` ([#101](https://github.com/wxt-dev/wxt/pull/101))\n- Generate types for `browser.i18n.getMessage` ([#103](https://github.com/wxt-dev/wxt/pull/103))\n\n### 🩹 Fixes\n\n- Allow adding custom content scripts ([b428a62](https://github.com/wxt-dev/wxt/commit/b428a62))\n- Don't overwrite `wxt.config.ts` content scripts, append entrypoints to it ([5f5f1d9](https://github.com/wxt-dev/wxt/commit/5f5f1d9))\n- ⚠️ Use relative path aliases inside `.wxt/tsconfig.json` ([#102](https://github.com/wxt-dev/wxt/pull/102))\n\n### 📖 Documentation\n\n- Add contribution guide ([#76](https://github.com/wxt-dev/wxt/pull/76))\n\n### 🏡 Chore\n\n- Setup dependabot for upgrading dependencies ([d66293c](https://github.com/wxt-dev/wxt/commit/d66293c))\n- Update social preview ([e164bd5](https://github.com/wxt-dev/wxt/commit/e164bd5))\n- Setup bug and feature issue templates ([2bde917](https://github.com/wxt-dev/wxt/commit/2bde917))\n- Upgrade to prettier 3 ([#77](https://github.com/wxt-dev/wxt/pull/77))\n- **deps-dev:** Bump vitest from 0.32.4 to 0.34.1 ([#81](https://github.com/wxt-dev/wxt/pull/81))\n- **deps-dev:** Bump ora from 6.3.1 to 7.0.1 ([#79](https://github.com/wxt-dev/wxt/pull/79))\n- **deps-dev:** Bump @types/node from 20.4.5 to 20.5.0 ([#78](https://github.com/wxt-dev/wxt/pull/78))\n- **deps-dev:** Bump tsup from 7.1.0 to 7.2.0 ([#80](https://github.com/wxt-dev/wxt/pull/80))\n- **deps-dev:** Bump @vitest/coverage-v8 from 0.32.4 to 0.34.1 ([#84](https://github.com/wxt-dev/wxt/pull/84))\n- **deps-dev:** Bump vitepress from 1.0.0-beta.5 to 1.0.0-rc.4 ([#85](https://github.com/wxt-dev/wxt/pull/85))\n- **deps-dev:** Bump vitest-mock-extended from 1.1.4 to 1.2.0 ([#87](https://github.com/wxt-dev/wxt/pull/87))\n- **deps-dev:** Bump lint-staged from 13.3.0 to 14.0.0 ([#89](https://github.com/wxt-dev/wxt/pull/89))\n- Fix remote code E2E test ([83e62a1](https://github.com/wxt-dev/wxt/commit/83e62a1))\n- Fix failing demo build ([b58a15e](https://github.com/wxt-dev/wxt/commit/b58a15e))\n- **deps-dev:** Bump vitest-mock-extended from 1.2.0 to 1.2.1 ([#97](https://github.com/wxt-dev/wxt/pull/97))\n- **deps-dev:** Bump lint-staged from 14.0.0 to 14.0.1 ([#100](https://github.com/wxt-dev/wxt/pull/100))\n- **deps-dev:** Bump vitest from 0.34.1 to 0.34.3 ([#99](https://github.com/wxt-dev/wxt/pull/99))\n- Increase E2E test timeout because GitHub Actions Window runner is slow ([2a0842b](https://github.com/wxt-dev/wxt/commit/2a0842b))\n- **deps-dev:** Bump vitepress from 1.0.0-rc.4 to 1.0.0-rc.10 ([#96](https://github.com/wxt-dev/wxt/pull/96))\n- Fix test watcher restarting indefinitely ([2c7922c](https://github.com/wxt-dev/wxt/commit/2c7922c))\n- Remove explicit icon config from templates ([93bfee0](https://github.com/wxt-dev/wxt/commit/93bfee0))\n- Use import aliases in Vue template ([#104](https://github.com/wxt-dev/wxt/pull/104))\n\n## v0.3.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.1...v0.3.2)\n\n### 🚀 Enhancements\n\n- Discover icons from the public directory ([#72](https://github.com/wxt-dev/wxt/pull/72))\n- Don't allow auto-importing from subdirectories ([d54d611](https://github.com/wxt-dev/wxt/commit/d54d611))\n\n### 📖 Documentation\n\n- Document the `url:` import prefix for remote code ([323045a](https://github.com/wxt-dev/wxt/commit/323045a))\n- Fix typos ([97f0938](https://github.com/wxt-dev/wxt/commit/97f0938))\n- Fix capitalization ([39467d1](https://github.com/wxt-dev/wxt/commit/39467d1))\n- Generate markdown for config reference ([#74](https://github.com/wxt-dev/wxt/pull/74))\n\n### 🏡 Chore\n\n- Upgrade dependencies ([798f02f](https://github.com/wxt-dev/wxt/commit/798f02f))\n- Upgrade vite (`v4.3` &rarr; `v4.4`) ([547c185](https://github.com/wxt-dev/wxt/commit/547c185))\n- Update templates to work with CSS entrypoints ([7f15305](https://github.com/wxt-dev/wxt/commit/7f15305))\n- Improve file list output in CI ([#73](https://github.com/wxt-dev/wxt/pull/73))\n\n### 🤖 CI\n\n- Validate templates against `main` ([#66](https://github.com/wxt-dev/wxt/pull/66))\n- List vite version when validating project templates ([ef140dc](https://github.com/wxt-dev/wxt/commit/ef140dc))\n- Validate templates using tarball to avoid version conflicts within the `wxt/node_modules` directory ([edfa075](https://github.com/wxt-dev/wxt/commit/edfa075))\n\n## v0.3.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.3.0...v0.3.1)\n\n### 🚀 Enhancements\n\n- CSS entrypoints ([#61](https://github.com/wxt-dev/wxt/pull/61))\n- `init` command for bootstrapping new projects ([#65](https://github.com/wxt-dev/wxt/pull/65))\n\n### 📖 Documentation\n\n- Add zip command to installation scripts ([94a1097](https://github.com/wxt-dev/wxt/commit/94a1097))\n- Add output paths to entrypoint docs ([3a336eb](https://github.com/wxt-dev/wxt/commit/3a336eb))\n- Update installation docs ([aea866c](https://github.com/wxt-dev/wxt/commit/aea866c))\n- Add publishing docs ([4184b05](https://github.com/wxt-dev/wxt/commit/4184b05))\n- Add a section for extensions using WXT ([709b61a](https://github.com/wxt-dev/wxt/commit/709b61a))\n- Add a comparison page to compare and contrast against Plasmo ([38d4f9c](https://github.com/wxt-dev/wxt/commit/38d4f9c))\n\n### 🏡 Chore\n\n- Update template projects to v0.3 ([#56](https://github.com/wxt-dev/wxt/pull/56))\n- Branding and logo ([#60](https://github.com/wxt-dev/wxt/pull/60))\n- Simplify binary setup ([#62](https://github.com/wxt-dev/wxt/pull/62))\n- Add Solid template ([#63](https://github.com/wxt-dev/wxt/pull/63))\n- Increase E2E test timeout to fix flakey test ([dfe424f](https://github.com/wxt-dev/wxt/commit/dfe424f))\n\n### 🤖 CI\n\n- Speed up demo validation ([3a9fd39](https://github.com/wxt-dev/wxt/commit/3a9fd39))\n- Fix flakey failure when validating templates ([25677ba](https://github.com/wxt-dev/wxt/commit/25677ba))\n\n### ❤️ Contributors\n\n- BeanWei ([@BeanWei](https://github.com/BeanWei))\n\n## v0.3.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.5...v0.3.0)\n\n### 🚀 Enhancements\n\n- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/wxt-dev/wxt/commit/58a84ec))\n- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/wxt-dev/wxt/commit/19c0948))\n- Windows support ([#50](https://github.com/wxt-dev/wxt/pull/50))\n\n### 🩹 Fixes\n\n- Add `WebWorker` lib to generated tsconfig ([2c70246](https://github.com/wxt-dev/wxt/commit/2c70246))\n\n### 📖 Documentation\n\n- Update entrypoint directory links ([0aebb67](https://github.com/wxt-dev/wxt/commit/0aebb67))\n\n### 🌊 Types\n\n- Allow any string for the `__BROWSER__` global ([6092235](https://github.com/wxt-dev/wxt/commit/6092235))\n\n### 🤖 CI\n\n- Improve checks against `demo/` extension ([9cc464f](https://github.com/wxt-dev/wxt/commit/9cc464f))\n\n## v0.2.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.4...v0.2.5)\n\n### 🚀 Enhancements\n\n- Auto-import from subdirectories ([547fee0](https://github.com/wxt-dev/wxt/commit/547fee0))\n- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/wxt-dev/wxt/commit/ca20a21))\n\n### 🩹 Fixes\n\n- Don't crash when generating types in dev mode ([d8c1903](https://github.com/wxt-dev/wxt/commit/d8c1903))\n- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/wxt-dev/wxt/commit/54b18cc))\n\n### 🏡 Chore\n\n- Update templates to wxt@0.2 ([9d00eb2](https://github.com/wxt-dev/wxt/commit/9d00eb2))\n\n### 🤖 CI\n\n- Validate project templates ([9ac756f](https://github.com/wxt-dev/wxt/commit/9ac756f))\n\n## v0.2.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.3...v0.2.4)\n\n### 🚀 Enhancements\n\n- Add `wxt zip` command ([#47](https://github.com/wxt-dev/wxt/pull/47))\n\n## v0.2.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.2...v0.2.3)\n\n### 🩹 Fixes\n\n- Correctly lookup open port ([#45](https://github.com/wxt-dev/wxt/pull/45))\n- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/wxt-dev/wxt/commit/495c5c8))\n- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/wxt-dev/wxt/pull/46))\n\n## v0.2.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.1...v0.2.2)\n\n### 🩹 Fixes\n\n- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/wxt-dev/wxt/commit/2fb5a54))\n\n## v0.2.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.2.0...v0.2.1)\n\n### 🚀 Enhancements\n\n- Support all content script options ([6f5bf89](https://github.com/wxt-dev/wxt/commit/6f5bf89))\n\n### 🩹 Fixes\n\n- Remove HMR log ([90fa6bf](https://github.com/wxt-dev/wxt/commit/90fa6bf))\n\n## v0.2.0\n\n[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) &bull; [compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.6...v0.2.0)\n\n### 🚀 Enhancements\n\n- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/wxt-dev/wxt/commit/5b48ae9))\n- Recongize unnamed content scripts (`content.ts` and `content/index.ts`) ([3db5cec](https://github.com/wxt-dev/wxt/commit/3db5cec))\n\n### 📖 Documentation\n\n- Update templates ([f28a29e](https://github.com/wxt-dev/wxt/commit/f28a29e))\n- Add docs for each type of entrypoint ([77cbfc1](https://github.com/wxt-dev/wxt/commit/77cbfc1))\n- Add inline JSDoc for public types ([375a2a6](https://github.com/wxt-dev/wxt/commit/375a2a6))\n\n### 🏡 Chore\n\n- Run `wxt prepare` on `postinstall` ([c1ea9ba](https://github.com/wxt-dev/wxt/commit/c1ea9ba))\n- Don't format lockfile ([5c7e041](https://github.com/wxt-dev/wxt/commit/5c7e041))\n\n## v0.1.6\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.5...v0.1.6)\n\n### 🩹 Fixes\n\n- Resolve tsconfig paths in vite ([ea92a27](https://github.com/wxt-dev/wxt/commit/ea92a27))\n- Add logs when a hot reload happens ([977246f](https://github.com/wxt-dev/wxt/commit/977246f))\n\n### 🏡 Chore\n\n- React and Vue starter templates ([#33](https://github.com/wxt-dev/wxt/pull/33))\n- Svelte template ([#34](https://github.com/wxt-dev/wxt/pull/34))\n\n## v0.1.5\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.4...v0.1.5)\n\n### 🩹 Fixes\n\n- Include `vite/client` types ([371be99](https://github.com/wxt-dev/wxt/commit/371be99))\n\n## v0.1.4\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.3...v0.1.4)\n\n### 🩹 Fixes\n\n- Fix regression where manifest was not listed first in build summary ([fa2b656](https://github.com/wxt-dev/wxt/commit/fa2b656))\n- Fix config hook implementations for vite plugins ([49965e7](https://github.com/wxt-dev/wxt/commit/49965e7))\n\n### 📖 Documentation\n\n- Update CLI screenshot ([0a26673](https://github.com/wxt-dev/wxt/commit/0a26673))\n\n### 🏡 Chore\n\n- Update prettier ignore ([68611ae](https://github.com/wxt-dev/wxt/commit/68611ae))\n\n## v0.1.3\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.2...v0.1.3)\n\n### 🚀 Enhancements\n\n- Add tsconfig path aliases ([#32](https://github.com/wxt-dev/wxt/pull/32))\n\n### 🩹 Fixes\n\n- Merge `manifest` option from both inline and user config ([05ca998](https://github.com/wxt-dev/wxt/commit/05ca998))\n- Cleanup build summary with sourcemaps ([ac0b28e](https://github.com/wxt-dev/wxt/commit/ac0b28e))\n\n### 📖 Documentation\n\n- Create documentation site ([#31](https://github.com/wxt-dev/wxt/pull/31))\n\n### 🏡 Chore\n\n- Upgrade to pnpm 8 ([0ce7c9d](https://github.com/wxt-dev/wxt/commit/0ce7c9d))\n\n## v0.1.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.1...v0.1.2)\n\n### 🚀 Enhancements\n\n- Accept a function for `config.manifest` ([ee49837](https://github.com/wxt-dev/wxt/commit/ee49837))\n\n### 🩹 Fixes\n\n- Add missing types for `webextension-polyfill` and the `manifest` option ([636aa48](https://github.com/wxt-dev/wxt/commit/636aa48))\n- Only add imports to JS files ([b29c3c6](https://github.com/wxt-dev/wxt/commit/b29c3c6))\n- Generate valid type for `EntrypointPath` when there are no entrypoints ([6e7184d](https://github.com/wxt-dev/wxt/commit/6e7184d))\n\n### 🌊 Types\n\n- Change `config.vite` to `UserConfig` ([ef6001e](https://github.com/wxt-dev/wxt/commit/ef6001e))\n\n## v0.1.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.1.0...v0.1.1)\n\n### 🩹 Fixes\n\n- Allow dashes in entrypoint names ([2e51e73](https://github.com/wxt-dev/wxt/commit/2e51e73))\n- Unable to read entrypoint options ([#28](https://github.com/wxt-dev/wxt/pull/28))\n\n## v0.1.0\n\nInitial release of WXT. Full support for production builds and initial toolkit for development:\n\n- HMR support when HTML page dependencies change\n- Reload extension when background changes\n- Reload HTML pages when saving them directly\n- Re-register and reload tabs when content scripts change\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.2...v0.1.0)\n\n### 🚀 Enhancements\n\n- Content scripts reloading ([#25](https://github.com/wxt-dev/wxt/pull/25))\n\n### 📖 Documentation\n\n- Update feature list ([0255028](https://github.com/wxt-dev/wxt/commit/0255028))\n\n### 🤖 CI\n\n- Create github release ([b7c078f](https://github.com/wxt-dev/wxt/commit/b7c078f))\n\n## v0.0.2\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.1...v0.0.2)\n\n### 🚀 Enhancements\n\n- Reload extension when source code is changed ([#17](https://github.com/wxt-dev/wxt/pull/17))\n- Setup background script web socket/reload ([#22](https://github.com/wxt-dev/wxt/pull/22))\n- Reload HTML files individually ([#23](https://github.com/wxt-dev/wxt/pull/23))\n\n### 🩹 Fixes\n\n- Output chunks to a chunks directory ([2dd7a99](https://github.com/wxt-dev/wxt/commit/2dd7a99))\n- Remove hash from content script css outputs ([#20](https://github.com/wxt-dev/wxt/pull/20))\n- Overwrite files with the same name when renaming entrypoints in dev mode ([37986bf](https://github.com/wxt-dev/wxt/commit/37986bf))\n- Separate template builds to prevent sharing chunks ([7f3a1e8](https://github.com/wxt-dev/wxt/commit/7f3a1e8))\n- Show Vite warnings and errors ([c51f0e0](https://github.com/wxt-dev/wxt/commit/c51f0e0))\n\n### 📖 Documentation\n\n- Add milestone progress badge to README ([684197d](https://github.com/wxt-dev/wxt/commit/684197d))\n- Fix milestone link in README ([e14f81d](https://github.com/wxt-dev/wxt/commit/e14f81d))\n\n### 🏡 Chore\n\n- Refactor build output type ([#19](https://github.com/wxt-dev/wxt/pull/19))\n- Refactor build outputs to support transpiled templates ([a78aada](https://github.com/wxt-dev/wxt/commit/a78aada))\n- Rename `templates` to `virtual-modules` ([#24](https://github.com/wxt-dev/wxt/pull/24))\n- Update cli screenshot ([54eb118](https://github.com/wxt-dev/wxt/commit/54eb118))\n\n## v0.0.1\n\n[compare changes](https://github.com/wxt-dev/wxt/compare/v0.0.0...v0.0.1)\n\n### 🚀 Enhancements\n\n- Add logger to config ([232ff7a](https://github.com/wxt-dev/wxt/commit/232ff7a))\n- Export and bootstrap the `/client` package ([5b07c95](https://github.com/wxt-dev/wxt/commit/5b07c95))\n- Resolve entrypoints based on filesystem ([a63f061](https://github.com/wxt-dev/wxt/commit/a63f061))\n- Separate output directories for each browser/manifest version ([f09ffbb](https://github.com/wxt-dev/wxt/commit/f09ffbb))\n- Build entrypoints and output `manifest.json` ([1e7c738](https://github.com/wxt-dev/wxt/commit/1e7c738))\n- Automatically add CSS files to content scripts ([047ce04](https://github.com/wxt-dev/wxt/commit/047ce04))\n- Download and bundle remote URL imports ([523c7df](https://github.com/wxt-dev/wxt/commit/523c7df))\n- Generate type declarations and config for project types and auto-imports ([21debad](https://github.com/wxt-dev/wxt/commit/21debad))\n- Good looking console output ([e2cc995](https://github.com/wxt-dev/wxt/commit/e2cc995))\n- Dev server working and a valid extension is built ([505e419](https://github.com/wxt-dev/wxt/commit/505e419))\n- Virtualized content script entrypoint ([ca29537](https://github.com/wxt-dev/wxt/commit/ca29537))\n- Provide custom, typed globals defined by Vite ([8c59a1c](https://github.com/wxt-dev/wxt/commit/8c59a1c))\n- Copy public directory to outputs ([1a25f2b](https://github.com/wxt-dev/wxt/commit/1a25f2b))\n- Support browser and chrome styles for mv2 popups ([7945c94](https://github.com/wxt-dev/wxt/commit/7945c94))\n- Support browser and chrome styles for mv2 popups ([7abb577](https://github.com/wxt-dev/wxt/commit/7abb577))\n- Support more CLI flags for `build` and `dev` ([#9](https://github.com/wxt-dev/wxt/pull/9))\n- Add more supported browser types ([f114c5b](https://github.com/wxt-dev/wxt/commit/f114c5b))\n- Open browser when starting dev server ([#11](https://github.com/wxt-dev/wxt/pull/11))\n\n### 🩹 Fixes\n\n- Support `srcDir` config ([739d19f](https://github.com/wxt-dev/wxt/commit/739d19f))\n- Root path customization now works ([4faa3b3](https://github.com/wxt-dev/wxt/commit/4faa3b3))\n- Print durations as ms/s based on total time ([3e37de9](https://github.com/wxt-dev/wxt/commit/3e37de9))\n- Don't print error twice when background crashes ([407627c](https://github.com/wxt-dev/wxt/commit/407627c))\n- Load package.json from root not cwd ([3ca16ee](https://github.com/wxt-dev/wxt/commit/3ca16ee))\n- Only allow a single entrypoint with a given name ([8eb4e86](https://github.com/wxt-dev/wxt/commit/8eb4e86))\n- Respect the mv2 popup type ([0f37ceb](https://github.com/wxt-dev/wxt/commit/0f37ceb))\n- Respect background type and persistent manifest options ([573ef80](https://github.com/wxt-dev/wxt/commit/573ef80))\n- Make content script array orders consistent ([f380378](https://github.com/wxt-dev/wxt/commit/f380378))\n- Firefox manifest warnings in dev mode ([50bb845](https://github.com/wxt-dev/wxt/commit/50bb845))\n\n### 📖 Documentation\n\n- Update README ([785ea54](https://github.com/wxt-dev/wxt/commit/785ea54))\n- Update README ([99ccadb](https://github.com/wxt-dev/wxt/commit/99ccadb))\n- Update description ([07a262e](https://github.com/wxt-dev/wxt/commit/07a262e))\n- Update README ([58a0ef4](https://github.com/wxt-dev/wxt/commit/58a0ef4))\n- Update README ([23ed6f7](https://github.com/wxt-dev/wxt/commit/23ed6f7))\n- Add initial release milestone link to README ([b400e54](https://github.com/wxt-dev/wxt/commit/b400e54))\n- Fix typo in README ([5590c9d](https://github.com/wxt-dev/wxt/commit/5590c9d))\n\n### 🏡 Chore\n\n- Refactor cli files into their own directory ([e6c0d84](https://github.com/wxt-dev/wxt/commit/e6c0d84))\n- Simplify `BuildOutput` type ([1f6c4a0](https://github.com/wxt-dev/wxt/commit/1f6c4a0))\n- Move `.exvite` directory into `srcDir` instead of `root` ([53fb805](https://github.com/wxt-dev/wxt/commit/53fb805))\n- Refactor CLI commands ([b8952b6](https://github.com/wxt-dev/wxt/commit/b8952b6))\n- Improve build summary sorting ([ec57e8c](https://github.com/wxt-dev/wxt/commit/ec57e8c))\n- Remove comments ([e3e9c0d](https://github.com/wxt-dev/wxt/commit/e3e9c0d))\n- Refactor internal config creation ([7c634f4](https://github.com/wxt-dev/wxt/commit/7c634f4))\n- Check virtual entrypoints feature in README ([70208f4](https://github.com/wxt-dev/wxt/commit/70208f4))\n- Add E2E tests and convert to vitest workspace ([5813302](https://github.com/wxt-dev/wxt/commit/5813302))\n- Rename package to wxt ([51a1072](https://github.com/wxt-dev/wxt/commit/51a1072))\n- Fix header log's timestamp ([8ca5657](https://github.com/wxt-dev/wxt/commit/8ca5657))\n- Fix demo global usage ([1ecfedd](https://github.com/wxt-dev/wxt/commit/1ecfedd))\n- Refactor folder structure ([9ab3953](https://github.com/wxt-dev/wxt/commit/9ab3953))\n- Fix release workflow ([2e94f2a](https://github.com/wxt-dev/wxt/commit/2e94f2a))\n\n### 🤖 CI\n\n- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))\n- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))"
  },
  {
    "path": "packages/wxt/README.md",
    "content": "<!-- DO NOT EDIT, THIS FILE WAS GENERATED BY '../../scripts/generate-readmes.sh' -->\n<div align=\"center\">\n\n# <img align=\"top\" width=\"44\" src=\"https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg\" alt=\"WXT Logo\"> WXT\n\n[![npm version](https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048)](https://www.npmjs.com/package/wxt)\n[![downloads](https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048)](https://www.npmjs.com/package/wxt)\n[![license | MIT](https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048)](https://github.com/wxt-dev/wxt/blob/main/LICENSE)\n[![coverage](https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048)](https://codecov.io/github/wxt-dev/wxt)\n\nNext-gen framework for developing web extensions.<br/>⚡<br/><q><i>It's like Nuxt, but for Web Extensions</i></q>\n\n[Get Started](https://wxt.dev/guide/installation.html) •\n[Configuration](https://wxt.dev/api/config.html) •\n[Examples](https://wxt.dev/examples.html) •\n[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) •\n[Discord](https://discord.gg/ZFsZqGery9)\n\n</div>\n\n![Example CLI Output](https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/assets/cli-output.png)\n\n## Demo\n\n<https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94>\n\n## Quick Start\n\nBootstrap a new project:\n\n```sh\n# npm\nnpx wxt@latest init\n\n# pnpm\npnpm dlx wxt@latest init\n\n# bun\nbunx wxt@latest init\n```\n\nOr see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.\n\n## Features\n\n- 🌐 Supports all browsers\n- ✅ Supports both MV2 and MV3\n- ⚡ Dev mode with HMR & fast reload\n- 📂 File based entrypoints\n- 🚔 TypeScript\n- 🦾 Auto-imports\n- 🤖 Automated publishing\n- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc\n- 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions\n- 🖍️ Quickly bootstrap a new project\n- 📏 Bundle analysis\n- ⬇️ Download and bundle remote URL imports\n\n## Sponsors\n\nWXT 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).\n\n[![WXT Sponsors](https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors.svg)](https://github.com/sponsors/wxt-dev)\n\n## Contributors\n\nPublished under the [MIT](https://github.com/wxt-dev/wxt/blob/main/LICENSE) license.\nMade by [@aklinker1](https://github.com/aklinker1) and [community](https://github.com/wxt-dev/wxt/graphs/contributors) 💛\n\n[![WXT contributors](https://contrib.rocks/image?repo=wxt-dev/wxt)](https://github.com/wxt-dev/wxt/graphs/contributors)\n"
  },
  {
    "path": "packages/wxt/bin/wxt-publish-extension.mjs",
    "content": "#!/usr/bin/env node\n/**\n * A alias around `publish-extension` that is always installed on the path\n * without having to install `publish-browser-extension` as a direct dependency\n * (like for PNPM, which doesn't link sub-dependency binaries to\n * \"node_modules/.bin\")\n */\nawait import('publish-browser-extension/cli');\n"
  },
  {
    "path": "packages/wxt/bin/wxt.mjs",
    "content": "#!/usr/bin/env node\nimport '../dist/cli/index.mjs';\n"
  },
  {
    "path": "packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Auto Imports > eslintrc > \"enabled: 8\" should output a JSON config file compatible with ESlint 8 1`] = `\n\".wxt/eslintrc-auto-import.json\n----------------------------------------\n{\n  \"globals\": {\n    \"AutoMount\": true,\n    \"AutoMountOptions\": true,\n    \"Browser\": true,\n    \"ContentScriptAnchoredOptions\": true,\n    \"ContentScriptAppendMode\": true,\n    \"ContentScriptContext\": true,\n    \"ContentScriptInlinePositioningOptions\": true,\n    \"ContentScriptModalPositioningOptions\": true,\n    \"ContentScriptOverlayAlignment\": true,\n    \"ContentScriptOverlayPositioningOptions\": true,\n    \"ContentScriptPositioningOptions\": true,\n    \"ContentScriptUi\": true,\n    \"ContentScriptUiOptions\": true,\n    \"IframeContentScriptUi\": true,\n    \"IframeContentScriptUiOptions\": true,\n    \"InjectScriptOptions\": true,\n    \"IntegratedContentScriptUi\": true,\n    \"IntegratedContentScriptUiOptions\": true,\n    \"InvalidMatchPattern\": true,\n    \"MatchPattern\": true,\n    \"MigrationError\": true,\n    \"ScriptPublicPath\": true,\n    \"ShadowRootContentScriptUi\": true,\n    \"ShadowRootContentScriptUiOptions\": true,\n    \"StopAutoMount\": true,\n    \"StorageArea\": true,\n    \"StorageAreaChanges\": true,\n    \"StorageItemKey\": true,\n    \"WxtAppConfig\": true,\n    \"WxtStorage\": true,\n    \"WxtStorageItem\": true,\n    \"WxtWindowEventMap\": true,\n    \"browser\": true,\n    \"createIframeUi\": true,\n    \"createIntegratedUi\": true,\n    \"createShadowRootUi\": true,\n    \"defineAppConfig\": true,\n    \"defineBackground\": true,\n    \"defineContentScript\": true,\n    \"defineUnlistedScript\": true,\n    \"defineWxtPlugin\": true,\n    \"fakeBrowser\": true,\n    \"getAppConfig\": true,\n    \"injectScript\": true,\n    \"storage\": true,\n    \"useAppConfig\": true\n  }\n}\n\"\n`;\n\nexports[`Auto Imports > eslintrc > \"enabled: 9\" should output a flat config file compatible with ESlint 9 1`] = `\n\".wxt/eslint-auto-imports.mjs\n----------------------------------------\nconst globals = {\n  \"AutoMount\": true,\n  \"AutoMountOptions\": true,\n  \"Browser\": true,\n  \"ContentScriptAnchoredOptions\": true,\n  \"ContentScriptAppendMode\": true,\n  \"ContentScriptContext\": true,\n  \"ContentScriptInlinePositioningOptions\": true,\n  \"ContentScriptModalPositioningOptions\": true,\n  \"ContentScriptOverlayAlignment\": true,\n  \"ContentScriptOverlayPositioningOptions\": true,\n  \"ContentScriptPositioningOptions\": true,\n  \"ContentScriptUi\": true,\n  \"ContentScriptUiOptions\": true,\n  \"IframeContentScriptUi\": true,\n  \"IframeContentScriptUiOptions\": true,\n  \"InjectScriptOptions\": true,\n  \"IntegratedContentScriptUi\": true,\n  \"IntegratedContentScriptUiOptions\": true,\n  \"InvalidMatchPattern\": true,\n  \"MatchPattern\": true,\n  \"MigrationError\": true,\n  \"ScriptPublicPath\": true,\n  \"ShadowRootContentScriptUi\": true,\n  \"ShadowRootContentScriptUiOptions\": true,\n  \"StopAutoMount\": true,\n  \"StorageArea\": true,\n  \"StorageAreaChanges\": true,\n  \"StorageItemKey\": true,\n  \"WxtAppConfig\": true,\n  \"WxtStorage\": true,\n  \"WxtStorageItem\": true,\n  \"WxtWindowEventMap\": true,\n  \"browser\": true,\n  \"createIframeUi\": true,\n  \"createIntegratedUi\": true,\n  \"createShadowRootUi\": true,\n  \"defineAppConfig\": true,\n  \"defineBackground\": true,\n  \"defineContentScript\": true,\n  \"defineUnlistedScript\": true,\n  \"defineWxtPlugin\": true,\n  \"fakeBrowser\": true,\n  \"getAppConfig\": true,\n  \"injectScript\": true,\n  \"storage\": true,\n  \"useAppConfig\": true\n}\n\nexport default {\n  name: \"wxt/auto-imports\",\n  languageOptions: {\n    globals,\n    /** @type {import('eslint').Linter.SourceType} */\n    sourceType: \"module\",\n  },\n};\n\"\n`;\n\nexports[`Auto Imports > eslintrc > \"enabled: true\" should output a JSON config file compatible with ESlint 8 1`] = `\n\".wxt/eslintrc-auto-import.json\n----------------------------------------\n{\n  \"globals\": {\n    \"AutoMount\": true,\n    \"AutoMountOptions\": true,\n    \"Browser\": true,\n    \"ContentScriptAnchoredOptions\": true,\n    \"ContentScriptAppendMode\": true,\n    \"ContentScriptContext\": true,\n    \"ContentScriptInlinePositioningOptions\": true,\n    \"ContentScriptModalPositioningOptions\": true,\n    \"ContentScriptOverlayAlignment\": true,\n    \"ContentScriptOverlayPositioningOptions\": true,\n    \"ContentScriptPositioningOptions\": true,\n    \"ContentScriptUi\": true,\n    \"ContentScriptUiOptions\": true,\n    \"IframeContentScriptUi\": true,\n    \"IframeContentScriptUiOptions\": true,\n    \"InjectScriptOptions\": true,\n    \"IntegratedContentScriptUi\": true,\n    \"IntegratedContentScriptUiOptions\": true,\n    \"InvalidMatchPattern\": true,\n    \"MatchPattern\": true,\n    \"MigrationError\": true,\n    \"ScriptPublicPath\": true,\n    \"ShadowRootContentScriptUi\": true,\n    \"ShadowRootContentScriptUiOptions\": true,\n    \"StopAutoMount\": true,\n    \"StorageArea\": true,\n    \"StorageAreaChanges\": true,\n    \"StorageItemKey\": true,\n    \"WxtAppConfig\": true,\n    \"WxtStorage\": true,\n    \"WxtStorageItem\": true,\n    \"WxtWindowEventMap\": true,\n    \"browser\": true,\n    \"createIframeUi\": true,\n    \"createIntegratedUi\": true,\n    \"createShadowRootUi\": true,\n    \"defineAppConfig\": true,\n    \"defineBackground\": true,\n    \"defineContentScript\": true,\n    \"defineUnlistedScript\": true,\n    \"defineWxtPlugin\": true,\n    \"fakeBrowser\": true,\n    \"getAppConfig\": true,\n    \"injectScript\": true,\n    \"storage\": true,\n    \"useAppConfig\": true\n  }\n}\n\"\n`;\n\nexports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `\n\"example.json\n----------------------------------------\n{\n  \"globals\": {\n    \"AutoMount\": \"readonly\",\n    \"AutoMountOptions\": \"readonly\",\n    \"Browser\": \"readonly\",\n    \"ContentScriptAnchoredOptions\": \"readonly\",\n    \"ContentScriptAppendMode\": \"readonly\",\n    \"ContentScriptContext\": \"readonly\",\n    \"ContentScriptInlinePositioningOptions\": \"readonly\",\n    \"ContentScriptModalPositioningOptions\": \"readonly\",\n    \"ContentScriptOverlayAlignment\": \"readonly\",\n    \"ContentScriptOverlayPositioningOptions\": \"readonly\",\n    \"ContentScriptPositioningOptions\": \"readonly\",\n    \"ContentScriptUi\": \"readonly\",\n    \"ContentScriptUiOptions\": \"readonly\",\n    \"IframeContentScriptUi\": \"readonly\",\n    \"IframeContentScriptUiOptions\": \"readonly\",\n    \"InjectScriptOptions\": \"readonly\",\n    \"IntegratedContentScriptUi\": \"readonly\",\n    \"IntegratedContentScriptUiOptions\": \"readonly\",\n    \"InvalidMatchPattern\": \"readonly\",\n    \"MatchPattern\": \"readonly\",\n    \"MigrationError\": \"readonly\",\n    \"ScriptPublicPath\": \"readonly\",\n    \"ShadowRootContentScriptUi\": \"readonly\",\n    \"ShadowRootContentScriptUiOptions\": \"readonly\",\n    \"StopAutoMount\": \"readonly\",\n    \"StorageArea\": \"readonly\",\n    \"StorageAreaChanges\": \"readonly\",\n    \"StorageItemKey\": \"readonly\",\n    \"WxtAppConfig\": \"readonly\",\n    \"WxtStorage\": \"readonly\",\n    \"WxtStorageItem\": \"readonly\",\n    \"WxtWindowEventMap\": \"readonly\",\n    \"browser\": \"readonly\",\n    \"createIframeUi\": \"readonly\",\n    \"createIntegratedUi\": \"readonly\",\n    \"createShadowRootUi\": \"readonly\",\n    \"defineAppConfig\": \"readonly\",\n    \"defineBackground\": \"readonly\",\n    \"defineContentScript\": \"readonly\",\n    \"defineUnlistedScript\": \"readonly\",\n    \"defineWxtPlugin\": \"readonly\",\n    \"fakeBrowser\": \"readonly\",\n    \"getAppConfig\": \"readonly\",\n    \"injectScript\": \"readonly\",\n    \"storage\": \"readonly\",\n    \"useAppConfig\": \"readonly\"\n  }\n}\n\"\n`;\n"
  },
  {
    "path": "packages/wxt/e2e/tests/analysis.test.ts",
    "content": "import { describe, it, expect, beforeEach, vi } from 'vitest';\nimport { TestProject } from '../utils';\nimport { resetBundleIncrement } from '../../src/core/builders/vite/plugins';\nimport open from 'open';\n\nvi.mock('open');\nconst openMock = vi.mocked(open);\n\nvi.mock('ci-info', () => ({\n  isCI: false,\n}));\n\ndescribe('Analysis', () => {\n  beforeEach(() => {\n    resetBundleIncrement();\n  });\n\n  it('should output a stats.html with no part files by default', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n\n    await project.build({\n      analysis: {\n        enabled: true,\n      },\n    });\n\n    expect(await project.pathExists('stats.html')).toBe(true);\n    expect(await project.pathExists('.output/chrome-mv3/stats-0.json')).toBe(\n      false,\n    );\n  });\n\n  it('should save part files when requested', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n\n    await project.build({\n      analysis: {\n        enabled: true,\n        keepArtifacts: true,\n      },\n    });\n\n    expect(await project.pathExists('stats.html')).toBe(true);\n    expect(await project.pathExists('stats-0.json')).toBe(true);\n    expect(await project.pathExists('stats-1.json')).toBe(true);\n  });\n\n  it('should support customizing the stats output directory', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n\n    await project.build({\n      analysis: {\n        enabled: true,\n        outputFile: 'stats/bundle.html',\n      },\n    });\n\n    expect(await project.pathExists('stats/bundle.html')).toBe(true);\n  });\n\n  it('should place artifacts next to the custom output file', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n\n    await project.build({\n      analysis: {\n        enabled: true,\n        outputFile: 'stats/bundle.html',\n        keepArtifacts: true,\n      },\n    });\n\n    expect(await project.pathExists('stats/bundle.html')).toBe(true);\n    expect(await project.pathExists('stats/bundle-0.json')).toBe(true);\n    expect(await project.pathExists('stats/bundle-1.json')).toBe(true);\n  });\n\n  it('should open the stats in the browser when requested', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n\n    await project.build({\n      analysis: {\n        enabled: true,\n        open: true,\n      },\n    });\n\n    expect(openMock).toBeCalledTimes(1);\n    expect(openMock).toBeCalledWith(project.resolvePath('stats.html'));\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/auto-imports.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { TestProject } from '../utils';\nimport spawn from 'nano-spawn';\n\ndescribe('Auto Imports', () => {\n  describe('imports: { ... }', () => {\n    it('should generate a declaration file, imports.d.ts, for auto-imports', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare();\n\n      expect(await project.serializeFile('.wxt/types/imports.d.ts'))\n        .toMatchInlineSnapshot(`\n          \".wxt/types/imports.d.ts\n          ----------------------------------------\n          // Generated by wxt\n          export {}\n          declare global {\n            const ContentScriptContext: typeof import('wxt/utils/content-script-context').ContentScriptContext\n            const InvalidMatchPattern: typeof import('wxt/utils/match-patterns').InvalidMatchPattern\n            const MatchPattern: typeof import('wxt/utils/match-patterns').MatchPattern\n            const browser: typeof import('wxt/browser').browser\n            const createIframeUi: typeof import('wxt/utils/content-script-ui/iframe').createIframeUi\n            const createIntegratedUi: typeof import('wxt/utils/content-script-ui/integrated').createIntegratedUi\n            const createShadowRootUi: typeof import('wxt/utils/content-script-ui/shadow-root').createShadowRootUi\n            const defineAppConfig: typeof import('wxt/utils/define-app-config').defineAppConfig\n            const defineBackground: typeof import('wxt/utils/define-background').defineBackground\n            const defineContentScript: typeof import('wxt/utils/define-content-script').defineContentScript\n            const defineUnlistedScript: typeof import('wxt/utils/define-unlisted-script').defineUnlistedScript\n            const defineWxtPlugin: typeof import('wxt/utils/define-wxt-plugin').defineWxtPlugin\n            const fakeBrowser: typeof import('wxt/testing').fakeBrowser\n            const getAppConfig: typeof import('wxt/utils/app-config').getAppConfig\n            const injectScript: typeof import('wxt/utils/inject-script').injectScript\n            const storage: typeof import('wxt/utils/storage').storage\n            const useAppConfig: typeof import('wxt/utils/app-config').useAppConfig\n          }\n          // for type re-export\n          declare global {\n            // @ts-ignore\n            export type { Browser } from 'wxt/browser'\n            import('wxt/browser')\n            // @ts-ignore\n            export type { StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage'\n            import('wxt/utils/storage')\n            // @ts-ignore\n            export type { WxtWindowEventMap } from 'wxt/utils/content-script-context'\n            import('wxt/utils/content-script-context')\n            // @ts-ignore\n            export type { IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe'\n            import('wxt/utils/content-script-ui/iframe')\n            // @ts-ignore\n            export type { IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated'\n            import('wxt/utils/content-script-ui/integrated')\n            // @ts-ignore\n            export type { ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root'\n            import('wxt/utils/content-script-ui/shadow-root')\n            // @ts-ignore\n            export type { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types'\n            import('wxt/utils/content-script-ui/types')\n            // @ts-ignore\n            export type { WxtAppConfig } from 'wxt/utils/define-app-config'\n            import('wxt/utils/define-app-config')\n            // @ts-ignore\n            export type { ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script'\n            import('wxt/utils/inject-script')\n          }\n          \"\n        `);\n    });\n\n    it('should include auto-imports in the project', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare();\n\n      expect(await project.serializeFile('.wxt/wxt.d.ts'))\n        .toMatchInlineSnapshot(`\n          \".wxt/wxt.d.ts\n          ----------------------------------------\n          // Generated by wxt\n          /// <reference types=\"wxt/vite-builder-env\" />\n          /// <reference path=\"./types/paths.d.ts\" />\n          /// <reference path=\"./types/i18n.d.ts\" />\n          /// <reference path=\"./types/globals.d.ts\" />\n          /// <reference path=\"./types/imports-module.d.ts\" />\n          /// <reference path=\"./types/imports.d.ts\" />\n          \"\n        `);\n    });\n\n    it('should generate the #imports module', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n      // Project auto-imports should also be present\n      project.addFile(\n        'utils/time.ts',\n        `export function startOfDay(date: Date): Date {\n          throw Error(\"TODO\")\n        }`,\n      );\n\n      await project.prepare();\n\n      expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))\n        .toMatchInlineSnapshot(`\n          \".wxt/types/imports-module.d.ts\n          ----------------------------------------\n          // Generated by wxt\n          // Types for the #import virtual module\n          declare module '#imports' {\n            export { browser, Browser } from 'wxt/browser';\n            export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';\n            export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';\n            export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';\n            export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';\n            export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';\n            export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root';\n            export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types';\n            export { defineAppConfig, WxtAppConfig } from 'wxt/utils/define-app-config';\n            export { defineBackground } from 'wxt/utils/define-background';\n            export { defineContentScript } from 'wxt/utils/define-content-script';\n            export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';\n            export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';\n            export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';\n            export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';\n            export { fakeBrowser } from 'wxt/testing';\n            export { startOfDay } from '../utils/time';\n          }\n          \"\n        `);\n    });\n  });\n\n  describe('imports: false', () => {\n    it('should not generate a imports.d.ts file', async () => {\n      const project = new TestProject();\n      project.setConfigFileConfig({\n        imports: false,\n      });\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare();\n\n      expect(await project.pathExists('.wxt/types/imports.d.ts')).toBe(false);\n    });\n\n    it('should only include imports-module.d.ts in the the project', async () => {\n      const project = new TestProject();\n      project.setConfigFileConfig({\n        imports: false,\n      });\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare();\n\n      expect(\n        await project.serializeFile('.wxt/wxt.d.ts'),\n      ).toMatchInlineSnapshot(\n        `\n        \".wxt/wxt.d.ts\n        ----------------------------------------\n        // Generated by wxt\n        /// <reference types=\"wxt/vite-builder-env\" />\n        /// <reference path=\"./types/paths.d.ts\" />\n        /// <reference path=\"./types/i18n.d.ts\" />\n        /// <reference path=\"./types/globals.d.ts\" />\n        /// <reference path=\"./types/imports-module.d.ts\" />\n        \"\n      `,\n      );\n    });\n\n    it('should only generate the #imports module', async () => {\n      const project = new TestProject();\n      project.setConfigFileConfig({\n        imports: false,\n      });\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n      // Project auto-imports should also be present\n      project.addFile(\n        'utils/time.ts',\n        `export function startOfDay(date: Date): Date {\n              throw Error(\"TODO\")\n            }`,\n      );\n\n      await project.prepare();\n\n      expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))\n        .toMatchInlineSnapshot(`\n          \".wxt/types/imports-module.d.ts\n          ----------------------------------------\n          // Generated by wxt\n          // Types for the #import virtual module\n          declare module '#imports' {\n            export { browser, Browser } from 'wxt/browser';\n            export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';\n            export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';\n            export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';\n            export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';\n            export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';\n            export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root';\n            export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types';\n            export { defineAppConfig, WxtAppConfig } from 'wxt/utils/define-app-config';\n            export { defineBackground } from 'wxt/utils/define-background';\n            export { defineContentScript } from 'wxt/utils/define-content-script';\n            export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';\n            export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';\n            export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';\n            export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';\n            export { fakeBrowser } from 'wxt/testing';\n          }\n          \"\n        `);\n    });\n  });\n\n  describe('eslintrc', () => {\n    it('\"enabled: true\" should output a JSON config file compatible with ESlint 8', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare({\n        imports: {\n          eslintrc: {\n            enabled: true,\n          },\n        },\n      });\n\n      expect(\n        await project.serializeFile('.wxt/eslintrc-auto-import.json'),\n      ).toMatchSnapshot();\n    });\n\n    it('\"enabled: 8\" should output a JSON config file compatible with ESlint 8', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare({\n        imports: {\n          eslintrc: {\n            enabled: 8,\n          },\n        },\n      });\n\n      expect(\n        await project.serializeFile('.wxt/eslintrc-auto-import.json'),\n      ).toMatchSnapshot();\n    });\n\n    it('\"enabled: 9\" should output a flat config file compatible with ESlint 9', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare({\n        imports: {\n          eslintrc: {\n            enabled: 9,\n          },\n        },\n      });\n\n      expect(\n        await project.serializeFile('.wxt/eslint-auto-imports.mjs'),\n      ).toMatchSnapshot();\n    });\n\n    it('\"enabled: false\" should NOT output an ESlint config file', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare({\n        imports: {\n          eslintrc: {\n            enabled: false,\n          },\n        },\n      });\n\n      expect(await project.pathExists('.wxt/eslint-auto-imports.mjs')).toBe(\n        false,\n      );\n      expect(await project.pathExists('.wxt/eslintrc-auto-import.json')).toBe(\n        false,\n      );\n    });\n\n    it('should allow customizing the output', async () => {\n      const project = new TestProject();\n      project.addFile('entrypoints/popup.html', `<html></html>`);\n\n      await project.prepare({\n        imports: {\n          eslintrc: {\n            enabled: true,\n            filePath: project.resolvePath('example.json'),\n            globalsPropValue: 'readonly',\n          },\n        },\n      });\n\n      expect(await project.serializeFile('example.json')).toMatchSnapshot();\n    });\n\n    describe('Actual linting results', () => {\n      async function runEslint(\n        project: TestProject,\n        version: boolean | 'auto' | 8 | 9,\n      ) {\n        project.addFile(\n          'entrypoints/background.js',\n          `export default defineBackground(() => {})`,\n        );\n        await project.prepare({\n          imports: { eslintrc: { enabled: version } },\n        });\n        return await spawn('pnpm', ['eslint', 'entrypoints/background.js'], {\n          cwd: project.root,\n        });\n      }\n\n      describe('ESLint 9', () => {\n        it('should have lint errors when not extending generated config', async () => {\n          const project = new TestProject({\n            devDependencies: {\n              '@eslint/js': '9.5.0',\n              eslint: '9.5.0',\n            },\n          });\n          project.addFile(\n            'eslint.config.mjs',\n            `\n            import eslint from \"@eslint/js\";\n\n            export default [\n              eslint.configs.recommended,\n            ];\n            `,\n          );\n\n          await expect(runEslint(project, 9)).rejects.toMatchObject({\n            exitCode: 1,\n            stdout: expect.stringContaining(\n              \"'defineBackground' is not defined\",\n            ),\n          });\n        });\n\n        it('should not have any lint errors when configured', async () => {\n          const project = new TestProject({\n            devDependencies: {\n              '@eslint/js': '9.5.0',\n              eslint: '9.5.0',\n            },\n          });\n          project.addFile(\n            'eslint.config.mjs',\n            `\n            import eslint from \"@eslint/js\";\n            import autoImports from \"./.wxt/eslint-auto-imports.mjs\";\n\n            export default [\n              eslint.configs.recommended,\n              autoImports,\n            ];\n            `,\n          );\n          const res = await runEslint(project, 9);\n\n          expect(res).toBeDefined();\n        });\n      });\n\n      describe('ESLint 8', () => {\n        it('should have lint errors when not extending generated config', async () => {\n          const project = new TestProject({\n            devDependencies: {\n              eslint: '8.57.0',\n            },\n          });\n          project.addFile(\n            '.eslintrc',\n            JSON.stringify({\n              parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },\n              env: { es6: true },\n              extends: ['eslint:recommended'],\n            }),\n          );\n\n          await expect(runEslint(project, 8)).rejects.toMatchObject({\n            exitCode: 1,\n            stdout: expect.stringContaining(\n              \"'defineBackground' is not defined\",\n            ),\n          });\n        });\n\n        it('should not have any lint errors when configured', async () => {\n          const project = new TestProject({\n            devDependencies: {\n              eslint: '8.57.0',\n            },\n          });\n          project.addFile(\n            '.eslintrc',\n            JSON.stringify({\n              parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },\n              env: { es6: true },\n              extends: [\n                'eslint:recommended',\n                './.wxt/eslintrc-auto-import.json',\n              ],\n            }),\n          );\n          const res = await runEslint(project, 8);\n\n          expect(res).toBeDefined();\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/dev.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('Dev Mode', () => {\n  it('should not change ports when restarting the server', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {})',\n    );\n\n    const server = await project.startServer({\n      runner: {\n        disabled: true,\n      },\n    });\n    const initialPort = server.port;\n    await server.restart();\n    const finalPort = server.port;\n    await server.stop();\n\n    expect(finalPort).toBe(initialPort);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/encoding.test.ts",
    "content": "import { readFile } from 'node:fs/promises';\nimport { describe, expect, it } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('Encoding', () => {\n  it('should convert unicode characters to ascii escaped chars', async () => {\n    // See more details about this test, see:\n    // https://github.com/wxt-dev/wxt/issues/353#issuecomment-4093271292\n    const KNOWN_BAD_CHAR = '￿';\n    const ESCAPED_BAD_CHAR = '\\\\uFFFF';\n\n    const project = new TestProject();\n\n    // `project.addFile` writes the file as UTF8\n    project.addFile(\n      'entrypoints/example.ts',\n      `export default defineUnlistedScript(() => console.log('${KNOWN_BAD_CHAR}'))`,\n    );\n    await project.build();\n\n    const file = project.resolvePath('.output/chrome-mv3/example.js');\n    const asciiOutput = await readFile(file, 'ascii');\n    expect(asciiOutput).toContain(ESCAPED_BAD_CHAR);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/hooks.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { TestProject } from '../utils';\nimport type { WxtHooks } from '../../src';\n\nconst hooks: WxtHooks = {\n  ready: vi.fn(),\n  'config:resolved': vi.fn(),\n  'prepare:types': vi.fn(),\n  'prepare:publicPaths': vi.fn(),\n  'build:before': vi.fn(),\n  'build:done': vi.fn(),\n  'build:manifestGenerated': vi.fn(),\n  'build:publicAssets': vi.fn(),\n  'entrypoints:found': vi.fn(),\n  'entrypoints:resolved': vi.fn(),\n  'entrypoints:grouped': vi.fn(),\n  'vite:build:extendConfig': vi.fn(),\n  'vite:devServer:extendConfig': vi.fn(),\n  'zip:start': vi.fn(),\n  'zip:extension:start': vi.fn(),\n  'zip:extension:done': vi.fn(),\n  'zip:sources:start': vi.fn(),\n  'zip:sources:done': vi.fn(),\n  'zip:done': vi.fn(),\n  'server:created': vi.fn(),\n  'server:started': vi.fn(),\n  'server:closed': vi.fn(),\n};\n\nfunction expectHooksToBeCalled(\n  called: Record<keyof WxtHooks, boolean | number>,\n) {\n  Object.keys(hooks).forEach((key) => {\n    const hookName = key as keyof WxtHooks;\n    const value = called[hookName];\n    const times = typeof value === 'number' ? value : value ? 1 : 0;\n    expect(\n      hooks[hookName],\n      `Expected \"${hookName}\" to be called ${times} time(s)`,\n    ).toBeCalledTimes(times);\n  });\n}\n\ndescribe('Hooks', () => {\n  beforeEach(() => {\n    Object.values(hooks).forEach((fn) => fn.mockReset());\n  });\n\n  it('prepare should call hooks', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n\n    await project.prepare({ hooks });\n\n    expectHooksToBeCalled({\n      ready: true,\n      'config:resolved': true,\n      'prepare:types': true,\n      'prepare:publicPaths': true,\n      'build:before': false,\n      'build:done': false,\n      'build:publicAssets': false,\n      'build:manifestGenerated': false,\n      'entrypoints:found': true,\n      'entrypoints:grouped': false,\n      'entrypoints:resolved': true,\n      'vite:build:extendConfig': false,\n      'vite:devServer:extendConfig': false,\n      'zip:start': false,\n      'zip:extension:start': false,\n      'zip:extension:done': false,\n      'zip:sources:start': false,\n      'zip:sources:done': false,\n      'zip:done': false,\n      'server:created': false,\n      'server:started': false,\n      'server:closed': false,\n    });\n  });\n\n  it('build should call hooks', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n\n    await project.build({ hooks });\n\n    expectHooksToBeCalled({\n      ready: true,\n      'config:resolved': true,\n      'prepare:types': true,\n      'prepare:publicPaths': true,\n      'build:before': true,\n      'build:done': true,\n      'build:publicAssets': true,\n      'build:manifestGenerated': true,\n      'entrypoints:found': true,\n      'entrypoints:grouped': true,\n      'entrypoints:resolved': true,\n      'vite:build:extendConfig': 1,\n      'vite:devServer:extendConfig': false,\n      'zip:start': false,\n      'zip:extension:start': false,\n      'zip:extension:done': false,\n      'zip:sources:start': false,\n      'zip:sources:done': false,\n      'zip:done': false,\n      'server:created': false,\n      'server:started': false,\n      'server:closed': false,\n    });\n  });\n\n  it('zip should call hooks', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n\n    await project.zip({ hooks });\n\n    expectHooksToBeCalled({\n      ready: true,\n      'config:resolved': true,\n      'prepare:types': true,\n      'prepare:publicPaths': true,\n      'build:before': true,\n      'build:done': true,\n      'build:publicAssets': true,\n      'build:manifestGenerated': true,\n      'entrypoints:found': true,\n      'entrypoints:grouped': true,\n      'entrypoints:resolved': true,\n      'vite:build:extendConfig': 1,\n      'vite:devServer:extendConfig': false,\n      'zip:start': true,\n      'zip:extension:start': true,\n      'zip:extension:done': true,\n      'zip:sources:start': false,\n      'zip:sources:done': false,\n      'zip:done': true,\n      'server:created': false,\n      'server:started': false,\n      'server:closed': false,\n    });\n  });\n\n  it('zip -b firefox should call hooks', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n\n    await project.zip({ hooks, browser: 'firefox' });\n\n    expectHooksToBeCalled({\n      ready: true,\n      'config:resolved': true,\n      'prepare:types': true,\n      'prepare:publicPaths': true,\n      'build:before': true,\n      'build:done': true,\n      'build:publicAssets': true,\n      'build:manifestGenerated': true,\n      'entrypoints:found': 2,\n      'entrypoints:grouped': true,\n      'entrypoints:resolved': 2,\n      'vite:build:extendConfig': 1,\n      'vite:devServer:extendConfig': false,\n      'zip:start': true,\n      'zip:extension:start': true,\n      'zip:extension:done': true,\n      'zip:sources:start': true,\n      'zip:sources:done': true,\n      'zip:done': true,\n      'server:created': false,\n      'server:started': false,\n      'server:closed': false,\n    });\n  });\n\n  it('server.start should call hooks', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n\n    const server = await project.startServer({\n      hooks,\n      webExt: {\n        disabled: true,\n      },\n    });\n    expect(hooks['server:closed']).not.toBeCalled();\n    await server.stop();\n\n    expectHooksToBeCalled({\n      ready: true,\n      'config:resolved': true,\n      'prepare:types': true,\n      'prepare:publicPaths': true,\n      'build:before': true,\n      'build:done': true,\n      'build:publicAssets': true,\n      'build:manifestGenerated': true,\n      'entrypoints:found': true,\n      'entrypoints:grouped': true,\n      'entrypoints:resolved': true,\n      'vite:build:extendConfig': 2,\n      'vite:devServer:extendConfig': 1,\n      'zip:start': false,\n      'zip:extension:start': false,\n      'zip:extension:done': false,\n      'zip:sources:start': false,\n      'zip:sources:done': false,\n      'zip:done': false,\n      'server:created': 1,\n      'server:started': 1,\n      'server:closed': 1,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/init.test.ts",
    "content": "import spawn from 'nano-spawn';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { glob } from 'tinyglobby';\nimport { describe, expect, it } from 'vitest';\nimport { TestProject, WXT_PACKAGE_DIR } from '../utils';\n\ndescribe('Init command', () => {\n  it('should download and create a template', async () => {\n    const project = new TestProject();\n\n    await spawn(\n      'pnpm',\n      ['-s', 'wxt', 'init', project.root, '-t', 'vue', '--pm', 'npm'],\n      {\n        env: { CI: 'true' },\n        stdio: 'ignore',\n        cwd: WXT_PACKAGE_DIR,\n      },\n    );\n    const files = await glob('**/*', {\n      cwd: project.root,\n      onlyFiles: true,\n      dot: true,\n      expandDirectories: false,\n    });\n\n    expect(files.sort()).toMatchInlineSnapshot(`\n      [\n        \".gitignore\",\n        \".vscode/extensions.json\",\n        \"README.md\",\n        \"assets/vue.svg\",\n        \"components/HelloWorld.vue\",\n        \"entrypoints/background.ts\",\n        \"entrypoints/content.ts\",\n        \"entrypoints/popup/App.vue\",\n        \"entrypoints/popup/index.html\",\n        \"entrypoints/popup/main.ts\",\n        \"entrypoints/popup/style.css\",\n        \"package.json\",\n        \"public/icon/128.png\",\n        \"public/icon/16.png\",\n        \"public/icon/32.png\",\n        \"public/icon/48.png\",\n        \"public/icon/96.png\",\n        \"public/wxt.svg\",\n        \"tsconfig.json\",\n        \"wxt.config.ts\",\n      ]\n    `);\n  });\n\n  it('should throw an error if the directory is not empty', async () => {\n    const project = new TestProject();\n    await mkdir(project.root, { recursive: true });\n    await writeFile(project.resolvePath('package.json'), JSON.stringify({}));\n\n    await expect(() =>\n      spawn(\n        'pnpm',\n        ['-s', 'wxt', 'init', project.root, '-t', 'vue', '--pm', 'npm'],\n        {\n          env: { CI: 'true' },\n          stdio: 'ignore',\n          cwd: WXT_PACKAGE_DIR,\n        },\n      ),\n    ).rejects.toThrowError('Command failed with exit code 1:');\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/manifest-content.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('Manifest Content', () => {\n  it.each([\n    { browser: undefined, outDir: 'chrome-mv3', expected: undefined },\n    { browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },\n    { browser: 'firefox', outDir: 'firefox-mv2', expected: true },\n    { browser: 'safari', outDir: 'safari-mv2', expected: false },\n  ])(\n    'should respect the per-browser entrypoint option with %j',\n    async ({ browser, expected, outDir }) => {\n      const project = new TestProject();\n\n      project.addFile(\n        'entrypoints/background.ts',\n        `export default defineBackground({\n          persistent: {\n            firefox: true,\n            safari: false,\n          },\n          main: () => {},\n        })`,\n      );\n      await project.build({ browser });\n\n      const safariManifest = await project.getOutputManifest(\n        `.output/${outDir}/manifest.json`,\n      );\n      expect(safariManifest.background.persistent).toBe(expected);\n    },\n  );\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/modules.test.ts",
    "content": "import { describe, it, expect, vi } from 'vitest';\nimport { TestProject } from '../utils';\nimport type { InlineConfig, UnlistedScriptEntrypoint } from '../../src';\nimport { readFile } from 'node:fs/promises';\nimport { normalizePath } from '../../src';\n\ndescribe('Module Helpers', () => {\n  describe('options', () => {\n    it('should receive the options defined in wxt.config.ts based on the configKey field', async () => {\n      const options = { key: '123' };\n      const reportOptions = vi.fn();\n      vi.stubGlobal('reportOptions', reportOptions);\n      const project = new TestProject();\n\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {})',\n      );\n      project.addFile(\n        'modules/test.ts',\n        `\n          import { defineWxtModule } from 'wxt/modules';\n          import { writeFile, mkdir } from 'node:fs/promises';\n          import { resolve } from 'node:path';\n\n          export default defineWxtModule({\n            configKey: \"example\",\n            setup: async (wxt, options) => {\n              reportOptions(options);\n            },\n          })\n        `,\n      );\n\n      await project.build({\n        // @ts-expect-error: untyped field for testing\n        example: options,\n      });\n\n      expect(reportOptions).toBeCalledWith(options);\n    });\n  });\n\n  describe('addEntrypoint', () => {\n    it('should add a custom entrypoint to be bundled the project', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {})',\n      );\n\n      const entrypoint: UnlistedScriptEntrypoint = {\n        type: 'unlisted-script',\n        inputPath: project.resolvePath('modules/test/injected.ts'),\n        name: 'injected',\n        options: {},\n        outputDir: project.resolvePath('.output/chrome-mv3'),\n        skipped: false,\n      };\n      project.addFile(\n        'modules/test/injected.ts',\n        `export default defineUnlistedScript(() => {})`,\n      );\n      project.addFile(\n        'modules/test/index.ts',\n        `\n          import { defineWxtModule, addEntrypoint } from 'wxt/modules';\n\n          export default defineWxtModule((wxt) => {\n            addEntrypoint(wxt, ${JSON.stringify(entrypoint)})\n          })\n        `,\n      );\n      const config: InlineConfig = {\n        browser: 'chrome',\n      };\n\n      await project.build(config);\n\n      expect(await project.pathExists('.output/chrome-mv3/injected.js')).toBe(\n        true,\n      );\n    });\n  });\n\n  describe('addPublicAssets', () => {\n    it('should add public assets', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {})',\n      );\n\n      project.addFile('modules/test/public/module.txt');\n      const dir = project.resolvePath('modules/test/public');\n      project.addFile(\n        'modules/test/index.ts',\n        `\n          import { defineWxtModule, addPublicAssets } from 'wxt/modules'\n          import { resolve } from 'node:path'\n\n          export default defineWxtModule((wxt) => {\n            addPublicAssets(wxt, \"${normalizePath(dir)}\")\n            wxt.hooks.hook(\"build:publicAssets\", (_, assets) => {\n              assets.push({\n                relativeDest: \"example/generated.txt\",\n                contents: \"\",\n              });\n            });\n          });\n        `,\n      );\n\n      const res = await project.build();\n\n      expect(res.publicAssets).toContainEqual({\n        type: 'asset',\n        fileName: 'module.txt',\n      });\n      await expect(\n        project.pathExists('.output/chrome-mv3/module.txt'),\n      ).resolves.toBe(true);\n      await expect(\n        project.pathExists('.output/chrome-mv3/example/generated.txt'),\n      ).resolves.toBe(true);\n    });\n\n    it(\"should not overwrite the user's public files\", async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {})',\n      );\n\n      project.addFile('public/user.txt', 'from-user');\n      project.addFile('modules/test/public/user.txt', 'from-module');\n      const dir = project.resolvePath('modules/test/public');\n      project.addFile(\n        'modules/test/index.ts',\n        `\n          import { defineWxtModule, addPublicAssets } from 'wxt/modules'\n          import { resolve } from 'node:path'\n\n          export default defineWxtModule((wxt) => {\n            addPublicAssets(wxt, \"${normalizePath(dir)}\")\n          })\n        `,\n      );\n\n      const res = await project.build();\n\n      expect(res.publicAssets).toContainEqual({\n        type: 'asset',\n        fileName: 'user.txt',\n      });\n      await expect(\n        readFile(project.resolvePath('.output/chrome-mv3/user.txt'), 'utf8'),\n      ).resolves.toBe('from-user');\n    });\n  });\n\n  describe('addWxtPlugin', () => {\n    function addPluginModule(project: TestProject) {\n      const expectedText = 'Hello from plugin!';\n      const pluginPath = project.addFile(\n        'modules/test/client-plugin.ts',\n        `\n          export default defineWxtPlugin(() => {\n            console.log(\"${expectedText}\")\n          })\n        `,\n      );\n      project.addFile(\n        'modules/test.ts',\n        `\n          import { defineWxtModule, addWxtPlugin } from 'wxt/modules';\n\n          export default defineWxtModule((wxt) => {\n            addWxtPlugin(wxt, \"${normalizePath(pluginPath)}\");\n          });\n        `,\n      );\n      return expectedText;\n    }\n\n    it('should include the plugin in the background', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {})',\n      );\n      const expectedText = addPluginModule(project);\n\n      await project.build();\n\n      await expect(project.serializeOutput()).resolves.toContain(expectedText);\n    });\n\n    it('should include the plugin in HTML entrypoints', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/popup/index.html',\n        `\n          <html>\n            <body></body>\n          </html>\n        `,\n      );\n      const expectedText = addPluginModule(project);\n\n      await project.build();\n\n      await expect(project.serializeOutput()).resolves.toContain(expectedText);\n    });\n\n    it('should include the plugin in content scripts', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/content.ts',\n        `\n          export default defineContentScript({\n            matches: [\"*://*/*\"],\n            main: () => {},\n          })\n        `,\n      );\n      const expectedText = addPluginModule(project);\n\n      await project.build();\n\n      await expect(project.serializeOutput()).resolves.toContain(expectedText);\n    });\n\n    it('should include the plugin in unlisted scripts', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/unlisted.ts',\n        'export default defineUnlistedScript(() => {})',\n      );\n      const expectedText = addPluginModule(project);\n\n      await project.build();\n\n      await expect(project.serializeOutput()).resolves.toContain(expectedText);\n    });\n  });\n\n  describe('imports', () => {\n    it('should add auto-imports', async () => {\n      const expectedText = 'customImport!';\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/background.ts',\n        `export default defineBackground(() => {\n          customImport();\n        });`,\n      );\n      const utils = project.addFile(\n        'custom.ts',\n        `export function customImport() {\n          console.log(\"${expectedText}\")\n        }`,\n      );\n      project.addFile(\n        'modules/test.ts',\n        `import { defineWxtModule } from 'wxt/modules';\n\n        export default defineWxtModule({\n          imports: [\n            { name: 'customImport', from: '${normalizePath(utils)}' },\n          ],\n        })`,\n      );\n\n      await project.build();\n\n      await expect(project.serializeOutput()).resolves.toContain(expectedText);\n    });\n\n    it('should add preset', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/background.ts',\n        `export default defineBackground(() => {\n            customImport();\n          });`,\n      );\n      project.addFile(\n        'modules/test.ts',\n        `import { defineWxtModule, addImportPreset } from 'wxt/modules';\n\n        export default defineWxtModule((wxt) => {\n          addImportPreset(wxt, \"vue\");\n        })`,\n      );\n\n      await project.build();\n\n      await expect(\n        project.serializeFile('.wxt/types/imports.d.ts'),\n      ).resolves.toContain(\"const ref: typeof import('vue').ref\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/npm-packages.test.ts",
    "content": "import { test, expect } from 'vitest';\nimport spawn from 'nano-spawn';\nimport {\n  NpmListDependency,\n  NpmListProject,\n} from '../../src/core/package-managers/npm';\n\n// Tests to ensure the total size of the WXT module is as small as possible\n// https://pkg-size.dev/wxt\n\ntest('Only one version of esbuild should be installed (each version is ~20mb of node_modules)', async () => {\n  const { stdout } = await spawn('pnpm', [\n    'why',\n    'esbuild',\n    '--prod',\n    '--json',\n  ]);\n  const projects: NpmListProject[] = JSON.parse(stdout);\n  const esbuildVersions = new Set<string>();\n  iterateDependencies(projects, (name, meta) => {\n    if (name === 'esbuild') esbuildVersions.add(meta.version);\n  });\n\n  expect([...esbuildVersions]).toHaveLength(1);\n});\n\nfunction iterateDependencies(\n  projects: NpmListProject[],\n  cb: (name: string, meta: NpmListDependency) => void,\n) {\n  const recurse = (dependencies: Record<string, NpmListDependency>) => {\n    Object.entries(dependencies).forEach(([name, meta]) => {\n      cb(name, meta);\n      if (meta.dependencies) recurse(meta.dependencies);\n    });\n  };\n  projects.forEach((project) => {\n    if (project.dependencies) recurse(project.dependencies);\n  });\n}\n"
  },
  {
    "path": "packages/wxt/e2e/tests/output-structure.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('Output Directory Structure', () => {\n  it('should not output hidden files and directories that start with \".\"', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/.DS_Store');\n    project.addFile('entrypoints/.hidden1/index.html', '<html></html>');\n    project.addFile('entrypoints/.hidden2.html', '<html></html>');\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n\n    await project.build();\n\n    expect(await project.serializeOutput()).toMatchInlineSnapshot(`\n      \".output/chrome-mv3/chunks/unlisted-DPbbfBKe.js\n      ----------------------------------------\n      (function(){const t=document.createElement(\"link\").relList;if(t&&t.supports&&t.supports(\"modulepreload\"))return;for(const e of document.querySelectorAll('link[rel=\"modulepreload\"]'))n(e);new MutationObserver(e=>{for(const r of e)if(r.type===\"childList\")for(const o of r.addedNodes)o.tagName===\"LINK\"&&o.rel===\"modulepreload\"&&n(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin===\"use-credentials\"?r.credentials=\"include\":e.crossOrigin===\"anonymous\"?r.credentials=\"omit\":r.credentials=\"same-origin\",r}function n(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();try{}catch(i){console.error(\"[wxt] Failed to initialize plugins\",i)}\n\n      ================================================================================\n      .output/chrome-mv3/manifest.json\n      ----------------------------------------\n      {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\"}\n      ================================================================================\n      .output/chrome-mv3/unlisted.html\n      ----------------------------------------\n      <html><head>  <script type=\"module\" crossorigin src=\"/chunks/unlisted-DPbbfBKe.js\"></script>\n      </head></html>\"\n    `);\n  });\n\n  it('should output separate CSS files for each content script', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'entrypoints/one.content/index.ts',\n      `import './style.css';\n      export default defineContentScript({\n        matches: [\"*://*/*\"],\n        main: () => {},\n      })`,\n    );\n    project.addFile(\n      'entrypoints/one.content/style.css',\n      `body { color: blue }`,\n    );\n    project.addFile(\n      'entrypoints/two.content/index.ts',\n      `import './style.css';\n      export default defineContentScript({\n        matches: [\"*://*/*\"],\n        main: () => {},\n      })`,\n    );\n    project.addFile('entrypoints/two.content/style.css', `body { color: red }`);\n\n    await project.build();\n\n    expect(\n      await project.serializeOutput([\n        '.output/chrome-mv3/content-scripts/one.js',\n        '.output/chrome-mv3/content-scripts/two.js',\n      ]),\n    ).toMatchInlineSnapshot(`\n      \".output/chrome-mv3/content-scripts/one.css\n      ----------------------------------------\n      body{color:#00f}\n\n      ================================================================================\n      .output/chrome-mv3/content-scripts/one.js\n      ----------------------------------------\n      <contents-ignored>\n      ================================================================================\n      .output/chrome-mv3/content-scripts/two.css\n      ----------------------------------------\n      body{color:red}\n\n      ================================================================================\n      .output/chrome-mv3/content-scripts/two.js\n      ----------------------------------------\n      <contents-ignored>\n      ================================================================================\n      .output/chrome-mv3/manifest.json\n      ----------------------------------------\n      {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"content_scripts\":[{\"matches\":[\"*://*/*\"],\"css\":[\"content-scripts/one.css\",\"content-scripts/two.css\"],\"js\":[\"content-scripts/one.js\",\"content-scripts/two.js\"]}]}\"\n    `);\n  });\n\n  it('should allow inputs with invalid JS variable names, like dashes', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'entrypoints/overlay-one.content.ts',\n      `export default defineContentScript({\n        matches: [\"*://*/*\"],\n        main: () => {},\n      })`,\n    );\n\n    await project.build();\n\n    expect(\n      await project.serializeOutput([\n        '.output/chrome-mv3/content-scripts/overlay-one.js',\n      ]),\n    ).toMatchInlineSnapshot(`\n      \".output/chrome-mv3/content-scripts/overlay-one.js\n      ----------------------------------------\n      <contents-ignored>\n      ================================================================================\n      .output/chrome-mv3/manifest.json\n      ----------------------------------------\n      {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"content_scripts\":[{\"matches\":[\"*://*/*\"],\"js\":[\"content-scripts/overlay-one.js\"]}]}\"\n    `);\n  });\n\n  it('should not include an entrypoint if the target browser is not in the list of included targets', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      `\n          export default defineBackground({\n            include: [\"chrome\"],\n            main() {},\n          })\n        `,\n    );\n\n    await project.build({ browser: 'firefox' });\n\n    expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(\n      false,\n    );\n  });\n\n  it('should not include an entrypoint if the target browser is in the list of excluded targets', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile(\n      'entrypoints/background.ts',\n      `\n          export default defineBackground({\n            exclude: [\"chrome\"],\n            main() {},\n          })\n        `,\n    );\n\n    await project.build({ browser: 'chrome' });\n\n    expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(\n      false,\n    );\n  });\n\n  it('should generate a stats file when analyzing the bundle', async () => {\n    const project = new TestProject();\n    project.setConfigFileConfig({\n      analysis: {\n        enabled: true,\n        template: 'sunburst',\n      },\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      `export default defineBackground(() => {});`,\n    );\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile(\n      'entrypoints/overlay.content.ts',\n      `export default defineContentScript({\n        matches: [],\n        main() {},\n      });`,\n    );\n\n    await project.build();\n\n    expect(await project.pathExists('stats.html')).toBe(true);\n  });\n\n  it('should support JavaScript entrypoints', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'entrypoints/background.js',\n      `export default defineBackground(() => {});`,\n    );\n    project.addFile(\n      'entrypoints/unlisted.js',\n      `export default defineUnlistedScript(() => {})`,\n    );\n    project.addFile(\n      'entrypoints/content.js',\n      `export default defineContentScript({\n        matches: [\"*://*.google.com/*\"],\n        main() {},\n      })`,\n    );\n    project.addFile(\n      'entrypoints/named.content.jsx',\n      `export default defineContentScript({\n        matches: [\"*://*.duckduckgo.com/*\"],\n        main() {},\n      })`,\n    );\n\n    await project.build();\n\n    expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))\n      .toMatchInlineSnapshot(`\n        \".output/chrome-mv3/manifest.json\n        ----------------------------------------\n        {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"background\":{\"service_worker\":\"background.js\"},\"content_scripts\":[{\"matches\":[\"*://*.google.com/*\"],\"js\":[\"content-scripts/content.js\"]},{\"matches\":[\"*://*.duckduckgo.com/*\"],\"js\":[\"content-scripts/named.js\"]}]}\"\n      `);\n    expect(await project.pathExists('.output/chrome-mv3/background.js'));\n    expect(\n      await project.pathExists('.output/chrome-mv3/content-scripts/content.js'),\n    );\n    expect(\n      await project.pathExists('.output/chrome-mv3/content-scripts/named.js'),\n    );\n    expect(await project.pathExists('.output/chrome-mv3/unlisted.js'));\n  });\n\n  it('should support CSS entrypoints', async () => {\n    const project = new TestProject();\n\n    project.addFile(\n      'entrypoints/plain-one.css',\n      `body {\n        font: 100% Helvetica, sans-serif;\n        color: #333;\n      }`,\n    );\n\n    project.addFile(\n      'entrypoints/plain-two.content.css',\n      `body {\n        font: 100% Helvetica, sans-serif;\n        color: #333;\n      }`,\n    );\n\n    project.addFile(\n      'entrypoints/sass-one.scss',\n      `$font-stack: Helvetica, sans-serif;\n      $primary-color: #333;\n\n      body {\n        font: 100% $font-stack;\n        color: $primary-color;\n      }`,\n    );\n\n    project.addFile(\n      'entrypoints/sass-two.content.scss',\n      `$font-stack: Helvetica, sans-serif;\n      $primary-color: #333;\n\n      body {\n        font: 100% $font-stack;\n        color: $primary-color;\n      }`,\n    );\n\n    await project.build();\n\n    expect(await project.serializeOutput(['.output/chrome-mv3/manifest.json']))\n      .toMatchInlineSnapshot(`\n        \".output/chrome-mv3/assets/plain-one.css\n        ----------------------------------------\n        body{font:100% Helvetica,sans-serif;color:#333}\n\n        ================================================================================\n        .output/chrome-mv3/assets/sass-one.css\n        ----------------------------------------\n        body{font:100% Helvetica,sans-serif;color:#333}\n\n        ================================================================================\n        .output/chrome-mv3/content-scripts/plain-two.css\n        ----------------------------------------\n        body{font:100% Helvetica,sans-serif;color:#333}\n\n        ================================================================================\n        .output/chrome-mv3/content-scripts/sass-two.css\n        ----------------------------------------\n        body{font:100% Helvetica,sans-serif;color:#333}\n\n        ================================================================================\n        .output/chrome-mv3/manifest.json\n        ----------------------------------------\n        <contents-ignored>\"\n      `);\n  });\n\n  it(\"should output to a custom directory when overriding 'outDir'\", async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n    project.setConfigFileConfig({\n      outDir: 'dist',\n    });\n\n    await project.build();\n\n    expect(await project.pathExists('dist/chrome-mv3/manifest.json')).toBe(\n      true,\n    );\n  });\n\n  it('should generate ESM background script when type=module', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'utils/log.ts',\n      `export function logHello(name: string) {\n        console.log(\\`Hello \\${name}!\\`);\n      }`,\n    );\n    project.addFile(\n      'entrypoints/background.ts',\n      `export default defineBackground({\n        type: \"module\",\n        main() {\n          logHello(\"background\");\n        },\n      })`,\n    );\n    project.addFile(\n      'entrypoints/popup/index.html',\n      `<html>\n        <head>\n          <script type=\"module\" src=\"./main.ts\"></script>\n        </head>\n      </html>`,\n    );\n    project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);\n\n    await project.build({\n      vite: () => ({\n        build: {\n          // Make output for the snapshot readable\n          minify: false,\n        },\n      }),\n    });\n\n    expect(await project.serializeFile('.output/chrome-mv3/background.js'))\n      .toMatchInlineSnapshot(`\n          \".output/chrome-mv3/background.js\n          ----------------------------------------\n          import { l as logHello, i as initPlugins } from \"./chunks/_virtual_wxt-plugins-OjKtWpmY.js\";\n          function defineBackground(arg) {\n            if (arg == null || typeof arg === \"function\") return { main: arg };\n            return arg;\n          }\n          const definition = defineBackground({\n            type: \"module\",\n            main() {\n              logHello(\"background\");\n            }\n          });\n          globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome;\n          function print(method, ...args) {\n            return;\n          }\n          const logger = {\n            debug: (...args) => print(console.debug, ...args),\n            log: (...args) => print(console.log, ...args),\n            warn: (...args) => print(console.warn, ...args),\n            error: (...args) => print(console.error, ...args)\n          };\n          let result;\n          try {\n            initPlugins();\n            result = definition.main();\n            if (result instanceof Promise) console.warn(\"The background's main() function return a promise, but it must be synchronous\");\n          } catch (err) {\n            logger.error(\"The background crashed on startup!\");\n            throw err;\n          }\n          \"\n        `);\n  });\n\n  it('should generate IIFE background script when type=undefined', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'utils/log.ts',\n      `export function logHello(name: string) {\n          console.log(\\`Hello \\${name}!\\`);\n        }`,\n    );\n    project.addFile(\n      'entrypoints/background.ts',\n      `export default defineBackground({\n          main() {\n            logHello(\"background\");\n          },\n        })`,\n    );\n    project.addFile(\n      'entrypoints/popup/index.html',\n      `<html>\n          <head>\n            <script type=\"module\" src=\"./main.ts\"></script>\n          </head>\n        </html>`,\n    );\n    project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);\n\n    await project.build({\n      vite: () => ({\n        build: {\n          // Make output for the snapshot readable\n          minify: false,\n        },\n      }),\n    });\n\n    expect(await project.serializeFile('.output/chrome-mv3/background.js'))\n      .toMatchInlineSnapshot(`\n      \".output/chrome-mv3/background.js\n      ----------------------------------------\n      var background = (function() {\n        \"use strict\";\n        function defineBackground(arg) {\n          if (arg == null || typeof arg === \"function\") return { main: arg };\n          return arg;\n        }\n        function logHello(name) {\n          console.log(\\`Hello \\${name}!\\`);\n        }\n        const definition = defineBackground({\n          main() {\n            logHello(\"background\");\n          }\n        });\n        function initPlugins() {\n        }\n        globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome;\n        function print(method, ...args) {\n          return;\n        }\n        const logger = {\n          debug: (...args) => print(console.debug, ...args),\n          log: (...args) => print(console.log, ...args),\n          warn: (...args) => print(console.warn, ...args),\n          error: (...args) => print(console.error, ...args)\n        };\n        let result;\n        try {\n          initPlugins();\n          result = definition.main();\n          if (result instanceof Promise) console.warn(\"The background's main() function return a promise, but it must be synchronous\");\n        } catch (err) {\n          logger.error(\"The background crashed on startup!\");\n          throw err;\n        }\n        var background_entrypoint_default = result;\n        return background_entrypoint_default;\n      })();\n      \"\n    `);\n  });\n\n  describe('globalName option', () => {\n    it('generates an IIFE with a default name', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/content.js',\n        `export default defineContentScript({\n          matches: [\"*://*/*\"],\n          main() {},\n        })`,\n      );\n\n      await project.build({ vite: () => ({ build: { minify: false } }) });\n\n      const output = await project.serializeFile(\n        '.output/chrome-mv3/content-scripts/content.js',\n      );\n      expect(output).toMatch(/^var content\\s?=[\\s\\S]*^content;$/gm);\n    });\n\n    it('generates an IIFE with a specific name', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/content.js',\n        `export default defineContentScript({\n          globalName: \"MyContentScript\",\n          matches: [\"*://*/*\"],\n          main() {},\n        })`,\n      );\n\n      await project.build({ vite: () => ({ build: { minify: false } }) });\n\n      const output = await project.serializeFile(\n        '.output/chrome-mv3/content-scripts/content.js',\n      );\n      expect(output).toMatch(\n        /^var MyContentScript =[\\s\\S]*^MyContentScript;$/gm,\n      );\n    });\n\n    it('generates an IIFE with a specific name provided by a function', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/content.js',\n        `export default defineContentScript({\n          globalName: () => \"MyContentScript\",\n          matches: [\"*://*/*\"],\n          main() {},\n        })`,\n      );\n\n      await project.build({ vite: () => ({ build: { minify: false } }) });\n\n      const output = await project.serializeFile(\n        '.output/chrome-mv3/content-scripts/content.js',\n      );\n      expect(output).toMatch(\n        /^var MyContentScript =[\\s\\S]*^MyContentScript;$/gm,\n      );\n    });\n\n    it('generates an anonymous IIFE when not minified', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/content.js',\n        `export default defineContentScript({\n          globalName: false,\n          matches: [\"*://*/*\"],\n          main() {},\n        })`,\n      );\n\n      await project.build({ vite: () => ({ build: { minify: false } }) });\n\n      const output = await project.serializeFile(\n        '.output/chrome-mv3/content-scripts/content.js',\n      );\n      expect(output).toMatch(/^\\(function\\(\\) {[\\s\\S]*^}\\)\\(\\);$/gm);\n    });\n\n    it('generates an anonymous IIFE when minified', async () => {\n      const project = new TestProject();\n      project.addFile(\n        'entrypoints/content.js',\n        `export default defineContentScript({\n          globalName: false,\n          matches: [\"*://*/*\"],\n          main() {},\n        })`,\n      );\n\n      await project.build({ vite: () => ({ build: { minify: true } }) });\n\n      const output = await project.serializeFile(\n        '.output/chrome-mv3/content-scripts/content.js',\n      );\n      expect(output).toMatch(/^\\(function\\(\\){[\\s\\S]*}\\)\\(\\);$/gm);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/react.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('React', () => {\n  it('should prepare and build an project with a tsx entrypoint', async () => {\n    const project = new TestProject({\n      dependencies: {\n        react: '^18.2.0',\n        'react-dom': '^18.2.0',\n      },\n      devDependencies: {\n        '@types/react': '^18.2.14',\n        '@types/react-dom': '^18.2.6',\n      },\n    });\n    project.addFile(\n      'entrypoints/demo.content.tsx',\n      `import ReactDOM from 'react-dom/client';\n      \n      export default defineContentScript({\n        matches: \"<all_urls>\",\n        main() {\n          const container = document.createElement(\"div\");\n          document.body.append(container)\n          const root = ReactDOM.createRoot(container);\n          root.render(<h1>Hello, world!</h1>);\n        }\n      })`,\n    );\n\n    await project.build();\n\n    expect(\n      await project.pathExists('.output/chrome-mv3/content-scripts/demo.js'),\n    ).toBe(true);\n    expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))\n      .toMatchInlineSnapshot(`\n        \".output/chrome-mv3/manifest.json\n        ----------------------------------------\n        {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"content_scripts\":[{\"matches\":\"<all_urls>\",\"js\":[\"content-scripts/demo.js\"]}]}\"\n      `);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/remote-code.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('Remote Code', () => {\n  it('should download \"url:*\" modules and include them in the final bundle', async () => {\n    const url = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js';\n    const project = new TestProject();\n    project.addFile(\n      'entrypoints/popup.ts',\n      `import \"url:${url}\"\n      export default defineUnlistedScript(() => {})`,\n    );\n\n    await project.build();\n\n    const output = await project.serializeFile('.output/chrome-mv3/popup.js');\n    expect(output).toContain(\n      // Some text that will hopefully be in future versions of this script\n      '__lodash_placeholder__',\n    );\n    expect(output).not.toContain(url);\n    expect(\n      await project.pathExists(`.wxt/cache/${encodeURIComponent(url)}`),\n    ).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/typescript-project.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('TypeScript Project', () => {\n  it('should generate defined constants correctly', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/types/globals.d.ts');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/types/globals.d.ts\n      ----------------------------------------\n      // Generated by wxt\n      interface ImportMetaEnv {\n        readonly MANIFEST_VERSION: 2 | 3;\n        readonly BROWSER: string;\n        readonly CHROME: boolean;\n        readonly FIREFOX: boolean;\n        readonly SAFARI: boolean;\n        readonly EDGE: boolean;\n        readonly OPERA: boolean;\n        readonly COMMAND: \"build\" | \"serve\";\n        readonly ENTRYPOINT: string;\n      }\n      interface ImportMeta {\n        readonly env: ImportMetaEnv\n      }\n      \"\n    `);\n  });\n\n  it('should augment the types for browser.runtime.getURL', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/popup.html', '<html></html>');\n    project.addFile('entrypoints/options.html', '<html></html>');\n    project.addFile('entrypoints/sandbox.html', '<html></html>');\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/types/paths.d.ts');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/types/paths.d.ts\n      ----------------------------------------\n      // Generated by wxt\n      import \"wxt/browser\";\n\n      declare module \"wxt/browser\" {\n        export type PublicPath =\n          | \"\"\n          | \"/\"\n          | \"/options.html\"\n          | \"/popup.html\"\n          | \"/sandbox.html\"\n        type HtmlPublicPath = Extract<PublicPath, \\`\\${string}.html\\`>\n        export interface WxtRuntime {\n          getURL(path: PublicPath): string;\n          getURL(path: \\`\\${HtmlPublicPath}\\${string}\\`): string;\n        }\n      }\n      \"\n    `);\n  });\n\n  it('should include CSS entrypoints in browser.runtime.getURL paths', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n    project.addFile(\n      'entrypoints/plain.css',\n      `body {\n        color: red;\n      }`,\n    );\n    project.addFile(\n      'entrypoints/overlay.content.css',\n      `body {\n        color: blue;\n      }`,\n    );\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/types/paths.d.ts');\n    expect(output).toContain('| \"/plain.css\"');\n    expect(output).toContain('| \"/content-scripts/overlay.css\"');\n  });\n\n  it('should augment the types for browser.i18n.getMessage', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n    project.addFile(\n      'public/_locales/en/messages.json',\n      JSON.stringify({\n        prompt_for_name: {\n          message: \"What's your name?\",\n          description: \"Ask for the user's name\",\n        },\n        hello: {\n          message: 'Hello, $USER$',\n          description: 'Greet the user',\n          placeholders: {\n            user: {\n              content: '$1',\n              example: 'Cira',\n            },\n          },\n        },\n        bye: {\n          message: 'Goodbye, $USER$. Come back to $OUR_SITE$ soon!',\n          description: 'Say goodbye to the user',\n          placeholders: {\n            our_site: {\n              content: 'Example.com',\n            },\n            user: {\n              content: '$1',\n              example: 'Cira',\n            },\n          },\n        },\n      }),\n    );\n    project.setConfigFileConfig({\n      manifest: {\n        default_locale: 'en',\n      },\n    });\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/types/i18n.d.ts');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/types/i18n.d.ts\n      ----------------------------------------\n      // Generated by wxt\n      import \"wxt/browser\";\n\n      declare module \"wxt/browser\" {\n        /**\n         * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage\n         */\n        interface GetMessageOptions {\n          /**\n           * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage\n           */\n          escapeLt?: boolean\n        }\n\n        export interface WxtI18n extends I18n.Static {\n          /**\n           * The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\n           * Note: You can't use this message in a manifest file.\n           *\n           * \"<browser.runtime.id>\"\n           */\n          getMessage(\n            messageName: \"@@extension_id\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * \"<browser.i18n.getUiLocale()>\"\n           */\n          getMessage(\n            messageName: \"@@ui_locale\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * The text direction for the current locale, either \"ltr\" for left-to-right languages such as English or \"rtl\" for right-to-left languages such as Japanese.\n           *\n           * \"<ltr|rtl>\"\n           */\n          getMessage(\n            messageName: \"@@bidi_dir\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * If the @@bidi_dir is \"ltr\", then this is \"rtl\"; otherwise, it's \"ltr\".\n           *\n           * \"<rtl|ltr>\"\n           */\n          getMessage(\n            messageName: \"@@bidi_reversed_dir\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * If the @@bidi_dir is \"ltr\", then this is \"left\"; otherwise, it's \"right\".\n           *\n           * \"<left|right>\"\n           */\n          getMessage(\n            messageName: \"@@bidi_start_edge\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * If the @@bidi_dir is \"ltr\", then this is \"right\"; otherwise, it's \"left\".\n           *\n           * \"<right|left>\"\n           */\n          getMessage(\n            messageName: \"@@bidi_end_edge\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * Ask for the user's name\n           *\n           * \"What's your name?\"\n           */\n          getMessage(\n            messageName: \"prompt_for_name\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * Greet the user\n           *\n           * \"Hello, $USER$\"\n           */\n          getMessage(\n            messageName: \"hello\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          /**\n           * Say goodbye to the user\n           *\n           * \"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\"\n           */\n          getMessage(\n            messageName: \"bye\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n          getMessage(\n            messageName: \"@@extension_id\" | \"@@ui_locale\" | \"@@bidi_dir\" | \"@@bidi_reversed_dir\" | \"@@bidi_start_edge\" | \"@@bidi_end_edge\" | \"prompt_for_name\" | \"hello\" | \"bye\",\n            substitutions?: string | string[],\n            options?: GetMessageOptions,\n          ): string;\n        }\n      }\n      \"\n    `);\n  });\n\n  it('should reference all the required types in a single declaration file', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/wxt.d.ts');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/wxt.d.ts\n      ----------------------------------------\n      // Generated by wxt\n      /// <reference types=\"wxt/vite-builder-env\" />\n      /// <reference path=\"./types/paths.d.ts\" />\n      /// <reference path=\"./types/i18n.d.ts\" />\n      /// <reference path=\"./types/globals.d.ts\" />\n      /// <reference path=\"./types/imports-module.d.ts\" />\n      /// <reference path=\"./types/imports.d.ts\" />\n      \"\n    `);\n  });\n\n  it('should generate a TSConfig file for the project', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/tsconfig.json');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/tsconfig.json\n      ----------------------------------------\n      {\n        \"compilerOptions\": {\n          \"target\": \"ESNext\",\n          \"module\": \"ESNext\",\n          \"moduleResolution\": \"Bundler\",\n          \"noEmit\": true,\n          \"esModuleInterop\": true,\n          \"forceConsistentCasingInFileNames\": true,\n          \"resolveJsonModule\": true,\n          \"strict\": true,\n          \"skipLibCheck\": true,\n          \"paths\": {\n            \"@\": [\"..\"],\n            \"@/*\": [\"../*\"],\n            \"~\": [\"..\"],\n            \"~/*\": [\"../*\"],\n            \"@@\": [\"..\"],\n            \"@@/*\": [\"../*\"],\n            \"~~\": [\"..\"],\n            \"~~/*\": [\"../*\"]\n          }\n        },\n        \"include\": [\n          \"../**/*\",\n          \"./wxt.d.ts\"\n        ],\n        \"exclude\": [\"../.output\"]\n      }\"\n    `);\n  });\n\n  it('should generate correct path aliases for a custom srcDir', async () => {\n    const project = new TestProject();\n    project.addFile('src/entrypoints/unlisted.html', '<html></html>');\n    project.setConfigFileConfig({\n      srcDir: 'src',\n    });\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/tsconfig.json');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/tsconfig.json\n      ----------------------------------------\n      {\n        \"compilerOptions\": {\n          \"target\": \"ESNext\",\n          \"module\": \"ESNext\",\n          \"moduleResolution\": \"Bundler\",\n          \"noEmit\": true,\n          \"esModuleInterop\": true,\n          \"forceConsistentCasingInFileNames\": true,\n          \"resolveJsonModule\": true,\n          \"strict\": true,\n          \"skipLibCheck\": true,\n          \"paths\": {\n            \"@\": [\"../src\"],\n            \"@/*\": [\"../src/*\"],\n            \"~\": [\"../src\"],\n            \"~/*\": [\"../src/*\"],\n            \"@@\": [\"..\"],\n            \"@@/*\": [\"../*\"],\n            \"~~\": [\"..\"],\n            \"~~/*\": [\"../*\"]\n          }\n        },\n        \"include\": [\n          \"../**/*\",\n          \"./wxt.d.ts\"\n        ],\n        \"exclude\": [\"../.output\"]\n      }\"\n    `);\n  });\n\n  it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => {\n    const project = new TestProject();\n    project.addFile('src/entrypoints/unlisted.html', '<html></html>');\n    project.setConfigFileConfig({\n      srcDir: 'src',\n      alias: {\n        example: 'example',\n        '@': 'ignored-path',\n      },\n    });\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/tsconfig.json');\n    expect(output).toMatchInlineSnapshot(`\n      \".wxt/tsconfig.json\n      ----------------------------------------\n      {\n        \"compilerOptions\": {\n          \"target\": \"ESNext\",\n          \"module\": \"ESNext\",\n          \"moduleResolution\": \"Bundler\",\n          \"noEmit\": true,\n          \"esModuleInterop\": true,\n          \"forceConsistentCasingInFileNames\": true,\n          \"resolveJsonModule\": true,\n          \"strict\": true,\n          \"skipLibCheck\": true,\n          \"paths\": {\n            \"example\": [\"../example\"],\n            \"example/*\": [\"../example/*\"],\n            \"@\": [\"../src\"],\n            \"@/*\": [\"../src/*\"],\n            \"~\": [\"../src\"],\n            \"~/*\": [\"../src/*\"],\n            \"@@\": [\"..\"],\n            \"@@/*\": [\"../*\"],\n            \"~~\": [\"..\"],\n            \"~~/*\": [\"../*\"]\n          }\n        },\n        \"include\": [\n          \"../**/*\",\n          \"./wxt.d.ts\"\n        ],\n        \"exclude\": [\"../.output\"]\n      }\"\n    `);\n  });\n\n  it('should start path aliases with \"./\" for paths inside the .wxt dir', async () => {\n    const project = new TestProject();\n    project.addFile('src/entrypoints/unlisted.html', '<html></html>');\n    project.setConfigFileConfig({\n      srcDir: 'src',\n      alias: {\n        example: '.wxt/example.ts',\n      },\n    });\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/tsconfig.json');\n    expect(output).toContain('./example.ts');\n  });\n\n  it('should set correct import.meta.env.BROWSER type based on targetBrowsers', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n    project.setConfigFileConfig({\n      targetBrowsers: ['firefox', 'chrome'],\n    });\n\n    await project.prepare();\n\n    const output = await project.serializeFile('.wxt/types/globals.d.ts');\n    expect(output).toContain('readonly BROWSER: \"firefox\" | \"chrome\";');\n  });\n\n  // TODO: Once a module has been published, use it here for testing - local files are never added to the .wxt/wxt.d.ts file\n  it.todo(\n    'should add modules from NPM to the TS project if they have a configKey',\n  );\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/user-config.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { TestProject } from '../utils';\n\ndescribe('User Config', () => {\n  // Root directory is tested with all tests.\n\n  it(\"should respect the 'src' directory\", async () => {\n    const project = new TestProject();\n    project.setConfigFileConfig({\n      srcDir: 'src',\n    });\n    project.addFile(\n      'src/entrypoints/background.ts',\n      `export default defineBackground(\n        () => console.log('Hello background'),\n      );`,\n    );\n\n    await project.build();\n\n    const output = await project.serializeOutput([\n      '.output/chrome-mv3/background.js',\n    ]);\n    expect(output).toMatchInlineSnapshot(`\n      \".output/chrome-mv3/background.js\n      ----------------------------------------\n      <contents-ignored>\n      ================================================================================\n      .output/chrome-mv3/manifest.json\n      ----------------------------------------\n      {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"background\":{\"service_worker\":\"background.js\"}}\"\n    `);\n  });\n\n  it(\"should respect the 'entrypoints' directory\", async () => {\n    const project = new TestProject();\n    project.setConfigFileConfig({\n      entrypointsDir: 'entries',\n    });\n    project.addFile(\n      'entries/background.ts',\n      `export default defineBackground(() => console.log('Hello background'));`,\n    );\n\n    await project.build();\n\n    const output = await project.serializeOutput([\n      '.output/chrome-mv3/background.js',\n    ]);\n    expect(output).toMatchInlineSnapshot(`\n      \".output/chrome-mv3/background.js\n      ----------------------------------------\n      <contents-ignored>\n      ================================================================================\n      .output/chrome-mv3/manifest.json\n      ----------------------------------------\n      {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"background\":{\"service_worker\":\"background.js\"}}\"\n    `);\n  });\n\n  it('should merge inline and user config based manifests', async () => {\n    const project = new TestProject();\n    project.addFile('entrypoints/unlisted.html', '<html></html>');\n    project.addFile(\n      'wxt.config.ts',\n      `import { defineConfig } from 'wxt';\n      export default defineConfig({\n        manifest: ({ mode, browser }) => ({\n          // @ts-expect-error\n          example_customization: [mode, browser],\n        })\n      })`,\n    );\n\n    await project.build({\n      // Specifically setting an invalid field for the test - it should show up in the snapshot\n      manifest: ({ manifestVersion, command }) => ({\n        example_customization: [String(manifestVersion), command],\n      }),\n    });\n\n    expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))\n      .toMatchInlineSnapshot(`\n      \".output/chrome-mv3/manifest.json\n      ----------------------------------------\n      {\"manifest_version\":3,\"name\":\"E2E Extension\",\"description\":\"Example description\",\"version\":\"0.0.0\",\"example_customization\":[\"3\",\"build\",\"production\",\"chrome\"]}\"\n    `);\n  });\n\n  it('should respect changing config files', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'src/entrypoints/background.ts',\n      `export default defineBackground(\n        () => console.log('Hello background'),\n      );`,\n    );\n    project.addFile(\n      'test.config.ts',\n      `import { defineConfig } from 'wxt';\n\n      export default defineConfig({\n        outDir: \".custom-output\",\n        srcDir: \"src\",\n      });`,\n    );\n\n    await project.build({ configFile: 'test.config.ts' });\n\n    expect(\n      await project.pathExists('.custom-output/chrome-mv3/background.js'),\n    ).toBe(true);\n  });\n\n  it('should respect outDirTemplate', async () => {\n    const project = new TestProject();\n    project.setConfigFileConfig({\n      srcDir: 'src',\n      outDirTemplate:\n        'test-{{browser}}-mv{{manifestVersion}}-{{mode}}{{modeSuffix}}-{{command}}',\n    });\n    project.addFile(\n      'src/entrypoints/background.ts',\n      `export default defineBackground(\n        () => console.log('Hello background'),\n      );`,\n    );\n\n    await project.build();\n\n    expect(\n      await project.pathExists('.output/test-chrome-mv3-production-build'),\n    ).toBe(true);\n\n    await project.build({ mode: 'development' });\n\n    expect(\n      await project.pathExists('.output/test-chrome-mv3-development-dev-build'),\n    ).toBe(true);\n  });\n\n  it('should not throw error when config file not exist', async () => {\n    const project = new TestProject();\n    project.addFile(\n      'entrypoints/background.ts',\n      `export default defineBackground(\n        () => console.log('Hello background'),\n      );`,\n    );\n\n    await project.build({ configFile: 'foo.config.ts' });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/tests/zip.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { TestProject } from '../utils';\nimport extract from 'extract-zip';\nimport spawn from 'nano-spawn';\nimport { readFile, writeFile } from 'node:fs/promises';\n\nprocess.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';\n\ndescribe('Zipping', () => {\n  it('should download packages and produce a valid build when zipping sources', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n      dependencies: {\n        flatten: '1.0.3',\n      },\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n    const unzipDir = project.resolvePath('.output/test-1.0.0-sources');\n    const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n    await project.zip({\n      browser: 'firefox',\n      zip: { downloadPackages: ['flatten'] },\n    });\n    expect(await project.pathExists('.output/')).toBe(true);\n\n    await extract(sourcesZip, { dir: unzipDir });\n    // Update package json wxt path\n    const packageJsonPath = project.resolvePath(unzipDir, 'package.json');\n    const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));\n    packageJson.dependencies.wxt = '../../../../..';\n    await writeFile(\n      packageJsonPath,\n      JSON.stringify(packageJson, null, 2),\n      'utf-8',\n    );\n\n    // Build zipped extension\n    await expect(\n      spawn('pnpm', ['i', '--ignore-workspace', '--frozen-lockfile', 'false'], {\n        cwd: unzipDir,\n      }),\n    ).resolves.not.toHaveProperty('exitCode');\n    await expect(\n      spawn('pnpm', ['wxt', 'build', '-b', 'firefox'], {\n        cwd: unzipDir,\n      }),\n    ).resolves.not.toHaveProperty('exitCode');\n\n    await expect(project.pathExists(unzipDir, '.output')).resolves.toBe(true);\n    expect(\n      await project.serializeFile(\n        project.resolvePath(unzipDir, 'package.json'),\n      ),\n    ).toMatchInlineSnapshot(`\n      \".output/test-1.0.0-sources/package.json\n      ----------------------------------------\n      {\n        \"name\": \"test\",\n        \"description\": \"Example description\",\n        \"version\": \"1.0.0\",\n        \"dependencies\": {\n          \"wxt\": \"../../../../..\",\n          \"flatten\": \"1.0.3\"\n        },\n        \"resolutions\": {\n          \"flatten@1.0.3\": \"file://./.wxt/local_modules/flatten-1.0.3.tgz\"\n        }\n      }\"\n    `);\n  });\n\n  it('should correctly apply template variables for zip file names based on provided config', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n    const artifactZip = '.output/test-1.0.0-firefox-development.zip';\n    const sourcesZip = '.output/test-1.0.0-development-sources.zip';\n\n    await project.zip({\n      browser: 'firefox',\n      mode: 'development',\n      zip: {\n        artifactTemplate: '{{name}}-{{version}}-{{browser}}-{{mode}}.zip',\n        sourcesTemplate: '{{name}}-{{version}}-{{mode}}-sources.zip',\n      },\n    });\n\n    expect(await project.pathExists(artifactZip)).toBe(true);\n    expect(await project.pathExists(sourcesZip)).toBe(true);\n  });\n\n  it('should not zip hidden files into sources by default', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n    project.addFile('.env');\n    project.addFile('.hidden-dir/file');\n    const unzipDir = project.resolvePath('.output/test-1.0.0-sources');\n    const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n    await project.zip({\n      browser: 'firefox',\n    });\n    await extract(sourcesZip, { dir: unzipDir });\n    expect(await project.pathExists(unzipDir, '.env')).toBe(false);\n    expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);\n  });\n\n  it('should not zip files inside hidden directories if only the directory is specified', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n    project.addFile('.hidden-dir/file');\n    project.addFile('.hidden-dir/nested/file');\n    const unzipDir = project.resolvePath('.output/test-1.0.0-sources');\n    const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n    await project.zip({\n      browser: 'firefox',\n      zip: {\n        includeSources: ['.hidden-dir'],\n      },\n    });\n    await extract(sourcesZip, { dir: unzipDir });\n    expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);\n    expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file')).toBe(\n      false,\n    );\n  });\n\n  it('should allow zipping hidden files into sources when explicitly listed', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n    project.addFile('.env');\n    project.addFile('.hidden-dir/file');\n    project.addFile('.hidden-dir/nested/file1');\n    project.addFile('.hidden-dir/nested/file2');\n    const unzipDir = project.resolvePath('.output/test-1.0.0-sources');\n    const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n    await project.zip({\n      browser: 'firefox',\n      zip: {\n        includeSources: ['.env', '.hidden-dir/file', '.hidden-dir/nested/**'],\n      },\n    });\n    await extract(sourcesZip, { dir: unzipDir });\n    expect(await project.pathExists(unzipDir, '.env')).toBe(true);\n    expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(true);\n    expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file1')).toBe(\n      true,\n    );\n    expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file2')).toBe(\n      true,\n    );\n  });\n\n  it('should exclude skipped entrypoints from respective browser sources zip', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n    });\n    project.addFile(\n      'entrypoints/not-firefox.content.ts',\n      `export default defineContentScript({\n        matches: ['*://*/*'],\n        exclude: ['firefox'],\n        main() {},\n      });`,\n    );\n    project.addFile(\n      'entrypoints/all.content.ts',\n      `export default defineContentScript({\n        matches: ['*://*/*'],\n        main(ctx) {},\n      });\n`,\n    );\n    const unzipDir = project.resolvePath('.output/test-1.0.0-sources');\n    const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n    await project.zip({\n      browser: 'firefox',\n    });\n    await extract(sourcesZip, { dir: unzipDir });\n    expect(\n      await project.pathExists(unzipDir, 'entrypoints/not-firefox.content.ts'),\n    ).toBe(false);\n    expect(\n      await project.pathExists(unzipDir, 'entrypoints/all.content.ts'),\n    ).toBe(true);\n  });\n\n  it.each(['firefox', 'opera'])(\n    'should create sources zip for \"%s\" browser when sourcesZip is undefined',\n    async (browser) => {\n      const project = new TestProject({\n        name: 'test',\n        version: '1.0.0',\n      });\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {});',\n      );\n      const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n      await project.zip({\n        browser,\n      });\n\n      expect(await project.pathExists(sourcesZip)).toBe(true);\n    },\n  );\n\n  it.each(['firefox', 'chrome'])(\n    'should create sources zip for \"%s\" when sourcesZip is true',\n    async (browser) => {\n      const project = new TestProject({\n        name: 'test',\n        version: '1.0.0',\n      });\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {});',\n      );\n      const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n      await project.zip({\n        browser,\n        zip: {\n          zipSources: true,\n        },\n      });\n\n      expect(await project.pathExists(sourcesZip)).toBe(true);\n    },\n  );\n\n  it.each(['firefox', 'chrome'])(\n    'should not create sources zip for \"%s\" when sourcesZip is false',\n    async (browser) => {\n      const project = new TestProject({\n        name: 'test',\n        version: '1.0.0',\n      });\n      project.addFile(\n        'entrypoints/background.ts',\n        'export default defineBackground(() => {});',\n      );\n      const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');\n\n      await project.zip({\n        browser,\n        zip: {\n          zipSources: false,\n        },\n      });\n\n      expect(await project.pathExists(sourcesZip)).toBe(false);\n    },\n  );\n\n  it('should include files in the zip when negated in zip.exclude', async () => {\n    const project = new TestProject({\n      name: 'test',\n      version: '1.0.0',\n    });\n    project.addFile(\n      'entrypoints/background.ts',\n      'export default defineBackground(() => {});',\n    );\n    const unzipDir = project.resolvePath('.output/test-1.0.0-chrome');\n    const sourcesZip = project.resolvePath('.output/test-1.0.0-chrome.zip');\n\n    await project.zip({\n      zip: {\n        exclude: ['**/*.json', '!manifest.json'],\n      },\n    });\n\n    await extract(sourcesZip, { dir: unzipDir });\n    expect(await project.pathExists(unzipDir, 'manifest.json')).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/e2e/utils.ts",
    "content": "import merge from 'lodash.merge';\nimport spawn from 'nano-spawn';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, relative, resolve } from 'path';\nimport { glob } from 'tinyglobby';\nimport {\n  InlineConfig,\n  UserConfig,\n  build,\n  createServer,\n  prepare,\n  zip,\n} from '../src';\nimport { normalizePath } from '../src/core/utils';\nimport { pathExists, readJson } from '../src/core/utils/fs';\n\n// Run \"pnpm wxt\" to use the \"wxt\" dev script, not the \"wxt\" binary from the\n// wxt package. This uses the TS files instead of the compiled JS package\n// files.\nexport const WXT_PACKAGE_DIR = resolve(__dirname, '..');\n\nexport const E2E_DIR = resolve(WXT_PACKAGE_DIR, 'e2e');\n\nexport class TestProject {\n  files: Array<[string, string]> = [];\n  config: UserConfig | undefined;\n  readonly root: string;\n  readonly hasCustomDependencies: boolean;\n\n  constructor(packageJson: any = {}) {\n    // We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts\n    // file is cached and cannot be different between each test. Instead, we add a random ID to the\n    // end to make each test's path unique.\n    const id = Math.random().toString(32).substring(3);\n    this.root = resolve(E2E_DIR, 'dist', id);\n\n    this.hasCustomDependencies =\n      Object.keys({\n        ...packageJson.dependencies,\n        ...packageJson.devDependencies,\n      }).length > 0;\n\n    this.files.push([\n      'package.json',\n      JSON.stringify(\n        merge(\n          {\n            name: 'E2E Extension',\n            description: 'Example description',\n            version: '0.0.0',\n            dependencies: {\n              wxt: '../../..',\n            },\n          },\n          packageJson,\n        ),\n        null,\n        2,\n      ),\n    ]);\n  }\n\n  /** Add a `wxt.config.ts` to the project with specific contents. */\n  setConfigFileConfig(config: UserConfig = {}) {\n    this.config = config;\n    this.files.push([\n      'wxt.config.ts',\n      `import { defineConfig } from 'wxt'\\n\\nexport default defineConfig(${JSON.stringify(\n        config,\n        null,\n        2,\n      )})`,\n    ]);\n  }\n\n  /**\n   * Adds the file to the project. Stored in memory until `.build` is called.\n   *\n   * @param filename Filename relative to the project's root.\n   * @param content File content.\n   * @returns The absolute path to the file that was added.\n   */\n  addFile(filename: string, content?: string) {\n    this.files.push([filename, content ?? '']);\n    if (filename === 'wxt.config.ts') this.config = {};\n    return this.resolvePath(filename);\n  }\n\n  async prepare(config: InlineConfig = {}) {\n    await this.writeProjectToDisk();\n    await prepare({ ...config, root: this.root });\n  }\n\n  async build(config: InlineConfig = {}) {\n    await this.writeProjectToDisk();\n    return await build({ ...config, root: this.root });\n  }\n\n  async zip(config: InlineConfig = {}) {\n    await this.writeProjectToDisk();\n    await zip({ ...config, root: this.root });\n  }\n\n  async startServer(config: InlineConfig = {}) {\n    await this.writeProjectToDisk();\n    const server = await createServer({ ...config, root: this.root });\n    await server.start();\n    return server;\n  }\n\n  /** Call `path.resolve` relative to the project's root directory. */\n  resolvePath(...path: string[]): string {\n    return resolve(this.root, ...path);\n  }\n\n  private async writeProjectToDisk() {\n    if (this.config == null) this.setConfigFileConfig();\n\n    for (const file of this.files) {\n      const [name, content] = file;\n      const filePath = this.resolvePath(name);\n      const fileDir = dirname(filePath);\n      await mkdir(fileDir, { recursive: true });\n      await writeFile(filePath, content ?? '', 'utf-8');\n    }\n\n    // Only install dependencies if the project has custom ones - otherwise the\n    // project will reuse the ones in `packages/wxt/node_modules`!\n    if (this.hasCustomDependencies) {\n      await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], {\n        cwd: this.root,\n      });\n    }\n\n    await mkdir(resolve(this.root, 'public'), { recursive: true }).catch(\n      () => {},\n    );\n  }\n\n  /**\n   * Read all the files from the test project's `.output` directory and combine\n   * them into a string that can be used in a snapshot.\n   *\n   * Optionally, provide a list of filenames whose content is not printed\n   * (because it's inconsistent or not relevant to a test).\n   */\n  serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> {\n    return this.serializeDir('.output', ignoreContentsOfFilenames);\n  }\n\n  /**\n   * Deeply print the filename and contents of all files in a directory.\n   *\n   * Optionally, provide a list of filenames whose content is not printed\n   * (because it's inconsistent or not relevant to a test).\n   */\n  private async serializeDir(\n    dir: string,\n    ignoreContentsOfFilenames?: string[],\n  ): Promise<string> {\n    const outputFiles = await glob('**/*', {\n      cwd: this.resolvePath(dir),\n      ignore: ['**/node_modules', '**/.output'],\n      expandDirectories: false,\n    });\n    outputFiles.sort();\n    const fileContents = [];\n    for (const file of outputFiles) {\n      const path = this.resolvePath(dir, file);\n      const isContentIgnored = !!ignoreContentsOfFilenames?.find(\n        (ignoredFile) => normalizePath(path).endsWith(ignoredFile),\n      );\n      fileContents.push(await this.serializeFile(path, isContentIgnored));\n    }\n    return fileContents.join(`\\n${''.padEnd(80, '=')}\\n`);\n  }\n\n  /**\n   * @param path An absolute path to a file or a path relative to the root.\n   * @param ignoreContents An optional boolean that, when true, causes this\n   *   function to not print the file contents.\n   */\n  async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {\n    const absolutePath = this.resolvePath(path);\n    return [\n      normalizePath(relative(this.root, absolutePath)),\n      ignoreContents\n        ? '<contents-ignored>'\n        : await readFile(absolutePath, 'utf-8'),\n    ].join(`\\n${''.padEnd(40, '-')}\\n`);\n  }\n\n  pathExists(...path: string[]): Promise<boolean> {\n    return pathExists(this.resolvePath(...path));\n  }\n\n  getOutputManifest(\n    path: string = '.output/chrome-mv3/manifest.json',\n  ): Promise<any> {\n    return readJson(this.resolvePath(path));\n  }\n}\n"
  },
  {
    "path": "packages/wxt/globals.d.ts",
    "content": "// oxlint-disable-next-line eslint(no-shadow-restricted-names)\ndeclare namespace globalThis {\n  var __ENTRYPOINT__: string;\n}\n"
  },
  {
    "path": "packages/wxt/package.json",
    "content": "{\n  \"name\": \"wxt\",\n  \"type\": \"module\",\n  \"version\": \"0.20.20\",\n  \"description\": \"⚡ Next-gen Web Extension Framework\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"wxt\": \"tsx src/cli/index.ts\",\n    \"build\": \"buildc -- tsdown --config-loader unrun\",\n    \"check\": \"pnpm build && pnpm run --reporter-hide-prefix /^check:.*/\",\n    \"check:default\": \"check\",\n    \"check:tsc-virtual\": \"tsc --noEmit -p src/virtual\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"test:coverage\": \"pnpm test run --coverage\",\n    \"sync-releases\": \"pnpx changelogen@latest gh release\",\n    \"prepack\": \"pnpm build\"\n  },\n  \"dependencies\": {\n    \"@1natsu/wait-element\": \"^4.1.2\",\n    \"@aklinker1/rollup-plugin-visualizer\": \"5.12.0\",\n    \"@webext-core/fake-browser\": \"^1.3.4\",\n    \"@webext-core/isolated-element\": \"^1.1.3\",\n    \"@webext-core/match-patterns\": \"^1.0.3\",\n    \"@wxt-dev/browser\": \"workspace:^\",\n    \"@wxt-dev/storage\": \"workspace:^1.0.0\",\n    \"async-mutex\": \"^0.5.0\",\n    \"c12\": \"^3.3.3\",\n    \"cac\": \"^6.7.14 || ^7.0.0\",\n    \"chokidar\": \"^5.0.0\",\n    \"ci-info\": \"^4.4.0\",\n    \"consola\": \"^3.4.2\",\n    \"defu\": \"^6.1.4\",\n    \"dotenv-expand\": \"^12.0.3\",\n    \"esbuild\": \"^0.27.1\",\n    \"filesize\": \"^11.0.13\",\n    \"get-port-please\": \"^3.2.0\",\n    \"giget\": \"^1.2.3 || ^2.0.0 || ^3.0.0\",\n    \"hookable\": \"^6.0.1\",\n    \"import-meta-resolve\": \"^4.2.0\",\n    \"is-wsl\": \"^3.1.1\",\n    \"json5\": \"^2.2.3\",\n    \"jszip\": \"^3.10.1\",\n    \"linkedom\": \"^0.18.12\",\n    \"magicast\": \"^0.5.2\",\n    \"nano-spawn\": \"^2.0.0\",\n    \"nanospinner\": \"^1.2.2\",\n    \"normalize-path\": \"^3.0.0\",\n    \"nypm\": \"^0.6.5\",\n    \"ohash\": \"^2.0.11\",\n    \"open\": \"^11.0.0\",\n    \"perfect-debounce\": \"^2.1.0\",\n    \"picocolors\": \"^1.1.1\",\n    \"picomatch\": \"^4.0.3\",\n    \"prompts\": \"^2.4.2\",\n    \"publish-browser-extension\": \"^2.3.0 || ^3.0.2 || ^4.0.4\",\n    \"scule\": \"^1.3.0\",\n    \"tinyglobby\": \"^0.2.15\",\n    \"unimport\": \"^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0\",\n    \"vite\": \"^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0\",\n    \"vite-node\": \"^3.2.4 || ^5.0.0 || ^6.0.0\",\n    \"web-ext-run\": \"^0.2.4\"\n  },\n  \"peerDependencies\": {\n    \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"eslint\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@faker-js/faker\": \"^10.3.0\",\n    \"@types/lodash.merge\": \"^4.6.9\",\n    \"@types/node\": \"^20.17.6\",\n    \"@types/normalize-path\": \"^3.0.2\",\n    \"@types/picomatch\": \"^4.0.2\",\n    \"@types/prompts\": \"^2.4.9\",\n    \"eslint\": \"^10.0.0\",\n    \"extract-zip\": \"^2.0.1\",\n    \"happy-dom\": \"^20.8.3\",\n    \"lodash.merge\": \"^4.6.2\",\n    \"oxlint\": \"^1.51.0\",\n    \"publint\": \"^0.3.18\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"vitest-plugin-random-seed\": \"^1.1.2\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wxt-dev/wxt.git\"\n  },\n  \"homepage\": \"https://wxt.dev\",\n  \"keywords\": [\n    \"vite\",\n    \"chrome\",\n    \"web\",\n    \"extension\",\n    \"browser\",\n    \"bundler\",\n    \"framework\"\n  ],\n  \"author\": {\n    \"name\": \"Aaron Klinker\",\n    \"email\": \"aaronklinker1+wxt@gmail.com\"\n  },\n  \"funding\": \"https://github.com/sponsors/wxt-dev\",\n  \"files\": [\n    \"bin\",\n    \"dist\"\n  ],\n  \"bin\": {\n    \"wxt\": \"./bin/wxt.mjs\",\n    \"wxt-publish-extension\": \"./bin/wxt-publish-extension.mjs\"\n  },\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./utils/app-config\": {\n      \"types\": \"./dist/utils/app-config.d.mts\",\n      \"default\": \"./dist/utils/app-config.mjs\"\n    },\n    \"./utils/inject-script\": {\n      \"types\": \"./dist/utils/inject-script.d.mts\",\n      \"default\": \"./dist/utils/inject-script.mjs\"\n    },\n    \"./utils/content-script-context\": {\n      \"types\": \"./dist/utils/content-script-context.d.mts\",\n      \"default\": \"./dist/utils/content-script-context.mjs\"\n    },\n    \"./utils/content-script-ui/types\": {\n      \"types\": \"./dist/utils/content-script-ui/types.d.mts\",\n      \"default\": \"./dist/utils/content-script-ui/types.mjs\"\n    },\n    \"./utils/content-script-ui/integrated\": {\n      \"types\": \"./dist/utils/content-script-ui/integrated.d.mts\",\n      \"default\": \"./dist/utils/content-script-ui/integrated.mjs\"\n    },\n    \"./utils/content-script-ui/shadow-root\": {\n      \"types\": \"./dist/utils/content-script-ui/shadow-root.d.mts\",\n      \"default\": \"./dist/utils/content-script-ui/shadow-root.mjs\"\n    },\n    \"./utils/content-script-ui/iframe\": {\n      \"types\": \"./dist/utils/content-script-ui/iframe.d.mts\",\n      \"default\": \"./dist/utils/content-script-ui/iframe.mjs\"\n    },\n    \"./utils/define-app-config\": {\n      \"types\": \"./dist/utils/define-app-config.d.mts\",\n      \"default\": \"./dist/utils/define-app-config.mjs\"\n    },\n    \"./utils/define-background\": {\n      \"types\": \"./dist/utils/define-background.d.mts\",\n      \"default\": \"./dist/utils/define-background.mjs\"\n    },\n    \"./utils/define-content-script\": {\n      \"types\": \"./dist/utils/define-content-script.d.mts\",\n      \"default\": \"./dist/utils/define-content-script.mjs\"\n    },\n    \"./utils/define-unlisted-script\": {\n      \"types\": \"./dist/utils/define-unlisted-script.d.mts\",\n      \"default\": \"./dist/utils/define-unlisted-script.mjs\"\n    },\n    \"./utils/define-wxt-plugin\": {\n      \"types\": \"./dist/utils/define-wxt-plugin.d.mts\",\n      \"default\": \"./dist/utils/define-wxt-plugin.mjs\"\n    },\n    \"./utils/match-patterns\": {\n      \"types\": \"./dist/utils/match-patterns.d.mts\",\n      \"default\": \"./dist/utils/match-patterns.mjs\"\n    },\n    \"./utils/split-shadow-root-css\": {\n      \"types\": \"./dist/utils/split-shadow-root-css.d.mts\",\n      \"default\": \"./dist/utils/split-shadow-root-css.mjs\"\n    },\n    \"./utils/storage\": {\n      \"types\": \"./dist/utils/storage.d.mts\",\n      \"default\": \"./dist/utils/storage.mjs\"\n    },\n    \"./browser\": {\n      \"types\": \"./dist/browser.d.mts\",\n      \"default\": \"./dist/browser.mjs\"\n    },\n    \"./testing/fake-browser\": {\n      \"types\": \"./dist/testing/fake-browser.d.mts\",\n      \"default\": \"./dist/testing/fake-browser.mjs\"\n    },\n    \"./testing/vitest-plugin\": {\n      \"types\": \"./dist/testing/wxt-vitest-plugin.d.mts\",\n      \"default\": \"./dist/testing/wxt-vitest-plugin.mjs\"\n    },\n    \"./testing\": {\n      \"types\": \"./dist/testing/index.d.mts\",\n      \"default\": \"./dist/testing/index.mjs\"\n    },\n    \"./vite-builder-env\": {\n      \"types\": \"./dist/vite-builder-env.d.ts\"\n    },\n    \"./modules\": {\n      \"types\": \"./dist/modules.d.mts\",\n      \"default\": \"./dist/modules.mjs\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.12.0\",\n    \"bun\": \">=1.2.0\"\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/@types/globals.d.ts",
    "content": "declare const __DEV_SERVER_ORIGIN__: string;\n\n// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts\ninterface ImportMetaEnv {\n  readonly COMMAND: WxtCommand;\n  readonly MANIFEST_VERSION: 2 | 3;\n  readonly ENTRYPOINT: string;\n}\n"
  },
  {
    "path": "packages/wxt/src/@types/modules.d.ts",
    "content": "// Custom TS definitions for non-TS packages\n\ndeclare module 'web-ext-run' {\n  export interface WebExtRunInstance {\n    reloadAllExtensions(): Promise<void>;\n    exit(): Promise<void>;\n  }\n\n  const webExt: {\n    cmd: {\n      run(config: any, executeOptions: any): Promise<WebExtRunInstance>;\n    };\n  };\n  export default webExt;\n}\n\ndeclare module 'web-ext-run/util/logger' {\n  // https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L43\n  export interface IConsoleStream {\n    stopCapturing(): void;\n    startCapturing(): void;\n    write(packet: Packet, options: unknown): void;\n    flushCapturedLogs(options: unknown): void;\n  }\n  export interface Packet {\n    name: string;\n    msg: string;\n    level: number;\n  }\n  export class ConsoleStream implements IConsoleStream {\n    constructor(options?: { verbose: false });\n  }\n  export const consoleStream: IConsoleStream;\n}\n"
  },
  {
    "path": "packages/wxt/src/@types/project-types.d.ts",
    "content": "// Types generated in the .wxt directory available after wxt prepare\n\ndeclare type PublicPath = string;\n"
  },
  {
    "path": "packages/wxt/src/__tests__/modules.test.ts",
    "content": "import { fakeWxt } from '../core/utils/testing/fake-objects';\nimport { addImportPreset, addViteConfig } from '../modules';\nimport { describe, it, expect } from 'vitest';\nimport { createHooks } from 'hookable';\n\ndescribe('Module Utilities', () => {\n  describe('addViteConfig', () => {\n    it('should add base vite config', async () => {\n      const wxt = fakeWxt({\n        hooks: createHooks(),\n      });\n      const expected = { build: { sourcemap: true } };\n      const userConfig = {};\n      const moduleConfig = { build: { sourcemap: true } };\n\n      wxt.config.vite = () => Promise.resolve(userConfig);\n      addViteConfig(wxt, () => moduleConfig);\n      await wxt.hooks.callHook('config:resolved', wxt);\n      const actual = await wxt.config.vite(wxt.config.env);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should allow user config to override any changes made', async () => {\n      const wxt = fakeWxt({\n        hooks: createHooks(),\n      });\n      const expected = { build: { sourcemap: true, test: 2 } };\n      const userConfig = { build: { sourcemap: true } };\n      const moduleConfig = { build: { sourcemap: false, test: 2 } };\n\n      wxt.config.vite = () => userConfig;\n      addViteConfig(wxt, () => moduleConfig);\n      await wxt.hooks.callHook('config:resolved', wxt);\n      const actual = await wxt.config.vite(wxt.config.env);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('addImportPreset', () => {\n    it('should add the import to the config', async () => {\n      const preset = 'vue';\n      const wxt = fakeWxt({ hooks: createHooks() });\n\n      addImportPreset(wxt, preset);\n      await wxt.hooks.callHook('config:resolved', wxt);\n\n      expect(wxt.config.imports && wxt.config.imports.presets).toContain(\n        preset,\n      );\n    });\n\n    it('should not add duplicate presets', async () => {\n      const preset = 'vue';\n      const wxt = fakeWxt({\n        hooks: createHooks(),\n        config: {\n          imports: {\n            presets: ['vue', 'react'],\n          },\n        },\n      });\n\n      addImportPreset(wxt, preset);\n      await wxt.hooks.callHook('config:resolved', wxt);\n\n      expect(wxt.config.imports && wxt.config.imports.presets).toHaveLength(2);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/browser.ts",
    "content": "/**\n * Contains the `browser` export which you should use to access the extension\n * APIs in your project:\n *\n * ```ts\n * import { browser } from 'wxt/browser';\n *\n * browser.runtime.onInstalled.addListener(() => {\n *   // ...\n * });\n * ```\n *\n * @module wxt/browser\n */\nimport { browser as _browser, type Browser } from '@wxt-dev/browser';\nimport type { ScriptPublicPath } from './utils/inject-script';\n\n/**\n * This interface is empty because it is generated per-project when running `wxt\n * prepare`. See:\n *\n * - `.wxt/types/paths.d.ts`\n */\nexport interface WxtRuntime {}\n\n/**\n * This interface is empty because it is generated per-project when running `wxt\n * prepare`. See:\n *\n * - `.wxt/types/i18n.d.ts`\n */\nexport interface WxtI18n {}\n\ntype ScriptInjection<Args extends any[], Result> =\n  Browser.scripting.ScriptInjection<Args, Result> extends infer T\n    ? T extends { files: string[] }\n      ? Omit<T, 'files'> & { files: ScriptPublicPath[] }\n      : T\n    : never;\ntype InjectionResult<Result> = Array<\n  Browser.scripting.InjectionResult<Awaited<Result>>\n>;\n\nexport interface WxtScripting {\n  executeScript: {\n    /** @see {@link Browser.scripting.executeScript} */\n    <Args extends any[], Result>(\n      injection: ScriptInjection<Args, Result>,\n    ): Promise<InjectionResult<Result>>;\n    <Args extends any[], Result>(\n      injection: ScriptInjection<Args, Result>,\n      callback: (results: InjectionResult<Result>) => void,\n    ): void;\n  };\n}\n\nexport type WxtBrowser = Omit<\n  typeof _browser,\n  'runtime' | 'i18n' | 'scripting'\n> & {\n  runtime: WxtRuntime & Omit<(typeof _browser)['runtime'], 'getURL'>;\n  i18n: WxtI18n & Omit<(typeof _browser)['i18n'], 'getMessage'>;\n  scripting: WxtScripting &\n    Omit<(typeof _browser)['scripting'], 'executeScript'>;\n};\n\nexport const browser: WxtBrowser = _browser;\n\nexport { Browser };\n"
  },
  {
    "path": "packages/wxt/src/builtin-modules/index.ts",
    "content": "import { WxtModule } from '../types';\nimport unimport from './unimport';\n\nexport const builtinModules: WxtModule<any>[] = [unimport];\n"
  },
  {
    "path": "packages/wxt/src/builtin-modules/unimport.ts",
    "content": "import { addViteConfig, defineWxtModule } from '../modules';\nimport type {\n  EslintGlobalsPropValue,\n  Wxt,\n  WxtDirFileEntry,\n  WxtModule,\n  WxtResolvedUnimportOptions,\n} from '../types';\nimport { type Unimport, createUnimport, toExports } from 'unimport';\nimport UnimportPlugin from 'unimport/unplugin';\n\nexport default defineWxtModule({\n  name: 'wxt:built-in:unimport',\n  setup(wxt) {\n    let unimport: Unimport;\n    const isEnabled = () => !wxt.config.imports.disabled;\n\n    // Add user module imports to config\n    wxt.hooks.hook('config:resolved', () => {\n      const addModuleImports = (module: WxtModule<any>) => {\n        if (!module.imports) return;\n\n        wxt.config.imports.imports ??= [];\n        wxt.config.imports.imports.push(...module.imports);\n      };\n\n      wxt.config.builtinModules.forEach(addModuleImports);\n      wxt.config.userModules.forEach(addModuleImports);\n    });\n\n    // Create unimport instance AFTER \"config:resolved\" so any modifications to the\n    // config inside \"config:resolved\" are applied.\n    wxt.hooks.afterEach((event) => {\n      if (event.name === 'config:resolved') {\n        unimport = createUnimport(wxt.config.imports);\n      }\n    });\n\n    // Generate types\n    wxt.hooks.hook('prepare:types', async (_, entries) => {\n      // Update cache before each rebuild\n      await unimport.init();\n\n      // Always generate the #import module types\n      entries.push(await getImportsModuleEntry(wxt, unimport));\n\n      if (!isEnabled()) return;\n\n      // Only create global types when user has enabled auto-imports\n      entries.push(await getImportsDeclarationEntry(unimport));\n\n      if (!wxt.config.imports.eslintrc.enabled) return;\n\n      // Only generate ESLint config if that feature is enabled\n      entries.push(\n        await getEslintConfigEntry(\n          unimport,\n          wxt.config.imports.eslintrc.enabled,\n          wxt.config.imports,\n        ),\n      );\n    });\n\n    // Add vite plugin\n    addViteConfig(wxt, () => ({\n      plugins: [UnimportPlugin.vite(wxt.config.imports)],\n    }));\n  },\n});\n\nasync function getImportsDeclarationEntry(\n  unimport: Unimport,\n): Promise<WxtDirFileEntry> {\n  return {\n    path: 'types/imports.d.ts',\n    text: [\n      '// Generated by wxt',\n      await unimport.generateTypeDeclarations(),\n      '',\n    ].join('\\n'),\n    tsReference: true,\n  };\n}\n\nasync function getImportsModuleEntry(\n  wxt: Wxt,\n  unimport: Unimport,\n): Promise<WxtDirFileEntry> {\n  const imports = await unimport.getImports();\n  return {\n    path: 'types/imports-module.d.ts',\n    text: [\n      '// Generated by wxt',\n      '// Types for the #import virtual module',\n      \"declare module '#imports' {\",\n      `  ${toExports(imports, wxt.config.wxtDir, true).replaceAll('\\n', '\\n  ')}`,\n      '}',\n      '',\n    ].join('\\n'),\n    tsReference: true,\n  };\n}\n\nasync function getEslintConfigEntry(\n  unimport: Unimport,\n  version: 8 | 9,\n  options: WxtResolvedUnimportOptions,\n): Promise<WxtDirFileEntry> {\n  const globals = (await unimport.getImports())\n    .map((i) => i.as ?? i.name)\n    .filter(Boolean)\n    .sort()\n    .reduce<Record<string, EslintGlobalsPropValue>>((globals, name) => {\n      globals[name] = options.eslintrc.globalsPropValue;\n      return globals;\n    }, {});\n\n  if (version <= 8) return getEslint8ConfigEntry(options, globals);\n  else return getEslint9ConfigEntry(options, globals);\n}\n\nexport function getEslint8ConfigEntry(\n  options: WxtResolvedUnimportOptions,\n  globals: Record<string, EslintGlobalsPropValue>,\n): WxtDirFileEntry {\n  return {\n    path: options.eslintrc.filePath,\n    text: JSON.stringify({ globals }, null, 2) + '\\n',\n  };\n}\n\nexport function getEslint9ConfigEntry(\n  options: WxtResolvedUnimportOptions,\n  globals: Record<string, EslintGlobalsPropValue>,\n): WxtDirFileEntry {\n  return {\n    path: options.eslintrc.filePath,\n    text: `const globals = ${JSON.stringify(globals, null, 2)}\n\nexport default {\n  name: \"wxt/auto-imports\",\n  languageOptions: {\n    globals,\n    /** @type {import('eslint').Linter.SourceType} */\n    sourceType: \"module\",\n  },\n};\n`,\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/cli/__tests__/index.test.ts",
    "content": "import { describe, it, vi, beforeEach, expect } from 'vitest';\nimport {\n  build,\n  createServer,\n  zip,\n  prepare,\n  clean,\n  initialize,\n} from '../../core';\nimport { mock } from 'vitest-mock-extended';\nimport consola, { LogLevels } from 'consola';\n\nvi.mock('../../core/build');\nconst buildMock = vi.mocked(build);\n\nvi.mock('../../core/create-server');\nconst createServerMock = vi.mocked(createServer);\n\nvi.mock('../../core/zip');\nconst zipMock = vi.mocked(zip);\n\nvi.mock('../../core/prepare');\nconst prepareMock = vi.mocked(prepare);\n\nvi.mock('../../core/clean');\nconst cleanMock = vi.mocked(clean);\n\nvi.mock('../../core/initialize');\nconst initializeMock = vi.mocked(initialize);\n\nconsola.wrapConsole();\n\nconst ogArgv = process.argv;\n\nfunction mockArgv(...args: string[]) {\n  process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];\n}\n\nasync function importCli() {\n  await import('../../cli');\n}\n\ndescribe('CLI', () => {\n  beforeEach(() => {\n    vi.resetModules();\n    process.argv = ogArgv;\n    createServerMock.mockResolvedValue(mock());\n  });\n\n  describe('dev', () => {\n    it('should not pass any config when no flags are passed', async () => {\n      mockArgv();\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({});\n    });\n\n    it('should respect passing a custom root', async () => {\n      mockArgv('path/to/root');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        root: 'path/to/root',\n      });\n    });\n\n    it('should respect a custom config file', async () => {\n      mockArgv('-c', './path/to/config.ts');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        configFile: './path/to/config.ts',\n      });\n    });\n\n    it('should respect passing a custom mode', async () => {\n      mockArgv('-m', 'development');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        mode: 'development',\n      });\n    });\n\n    it('should respect passing a custom browser', async () => {\n      mockArgv('-b', 'firefox');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        browser: 'firefox',\n      });\n    });\n\n    it('should pass correct filtered entrypoints', async () => {\n      mockArgv('-e', 'popup', '-e', 'options');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        filterEntrypoints: ['popup', 'options'],\n      });\n    });\n\n    it('should respect passing --mv2', async () => {\n      mockArgv('--mv2');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        manifestVersion: 2,\n      });\n    });\n\n    it('should respect passing --mv3', async () => {\n      mockArgv('--mv3');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        manifestVersion: 3,\n      });\n    });\n\n    it('should respect passing --port', async () => {\n      const expectedPort = 3100;\n      mockArgv('--port', String(expectedPort));\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        dev: {\n          server: {\n            port: expectedPort,\n          },\n        },\n      });\n    });\n\n    it('should respect passing --debug', async () => {\n      mockArgv('--debug');\n      await importCli();\n\n      expect(createServerMock).toBeCalledWith({\n        debug: true,\n      });\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n\n    it('should set log --level', async () => {\n      mockArgv('--level', 'warn');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.warn);\n    });\n\n    it('--debug should override --level', async () => {\n      mockArgv('--debug', '--level', 'silent');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n  });\n\n  describe('build', () => {\n    it('should not pass any config when no flags are passed', async () => {\n      mockArgv('build');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({});\n    });\n\n    it('should respect passing a custom root', async () => {\n      mockArgv('build', 'path/to/root');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        root: 'path/to/root',\n      });\n    });\n\n    it('should respect a custom config file', async () => {\n      mockArgv('build', '-c', './path/to/config.ts');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        configFile: './path/to/config.ts',\n      });\n    });\n\n    it('should respect passing a custom mode', async () => {\n      mockArgv('build', '-m', 'development');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        mode: 'development',\n      });\n    });\n\n    it('should respect passing a custom browser', async () => {\n      mockArgv('build', '-b', 'firefox');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        browser: 'firefox',\n      });\n    });\n\n    it('should pass correct filtered entrypoints', async () => {\n      mockArgv('build', '-e', 'popup', '-e', 'options');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        filterEntrypoints: ['popup', 'options'],\n      });\n    });\n\n    it('should respect passing --mv2', async () => {\n      mockArgv('build', '--mv2');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        manifestVersion: 2,\n      });\n    });\n\n    it('should respect passing --mv3', async () => {\n      mockArgv('build', '--mv3');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        manifestVersion: 3,\n      });\n    });\n\n    it('should include analysis in the build', async () => {\n      mockArgv('build', '--analyze');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        analysis: {\n          enabled: true,\n        },\n      });\n    });\n\n    it('should respect passing --debug', async () => {\n      mockArgv('build', '--debug');\n      await importCli();\n\n      expect(buildMock).toBeCalledWith({\n        debug: true,\n      });\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n\n    it('should set log --level', async () => {\n      mockArgv('build', '--level', 'warn');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.warn);\n    });\n\n    it('--debug should override --level', async () => {\n      mockArgv('build', '--debug', '--level', 'silent');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n  });\n\n  describe('zip', () => {\n    it('should not pass any config when no flags are passed', async () => {\n      mockArgv('zip');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        zip: {},\n      });\n    });\n\n    it('should respect passing a custom root', async () => {\n      mockArgv('zip', 'path/to/root');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        root: 'path/to/root',\n        zip: {},\n      });\n    });\n\n    it('should respect a custom config file', async () => {\n      mockArgv('zip', '-c', './path/to/config.ts');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        configFile: './path/to/config.ts',\n        zip: {},\n      });\n    });\n\n    it('should respect passing a custom mode', async () => {\n      mockArgv('zip', '-m', 'development');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        mode: 'development',\n        zip: {},\n      });\n    });\n\n    it('should respect passing a custom browser', async () => {\n      mockArgv('zip', '-b', 'firefox');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        browser: 'firefox',\n        zip: {},\n      });\n    });\n\n    it('should respect passing --mv2', async () => {\n      mockArgv('zip', '--mv2');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        manifestVersion: 2,\n        zip: {},\n      });\n    });\n\n    it('should respect passing --mv3', async () => {\n      mockArgv('zip', '--mv3');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        manifestVersion: 3,\n        zip: {},\n      });\n    });\n\n    it('should respect passing --debug', async () => {\n      mockArgv('zip', '--debug');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        debug: true,\n        zip: {},\n      });\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n\n    it('should set log --level', async () => {\n      mockArgv('zip', '--level', 'warn');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.warn);\n    });\n\n    it('--debug should override --level', async () => {\n      mockArgv('zip', '--debug', '--level', 'silent');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n\n    it('should pass undefined for zipSources when --sources is not passed', async () => {\n      mockArgv('zip');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        zip: {\n          zipSources: undefined,\n        },\n      });\n    });\n\n    it('should pass true for zipSources when --sources is passed', async () => {\n      mockArgv('zip', '--sources');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        zip: {\n          zipSources: true,\n        },\n      });\n    });\n\n    it('should pass false for zipSources when --sources=false is passed', async () => {\n      mockArgv('zip', '--sources=false');\n      await importCli();\n\n      expect(zipMock).toBeCalledWith({\n        zip: {\n          zipSources: false,\n        },\n      });\n    });\n  });\n\n  describe('prepare', () => {\n    it('should not pass any config when no flags are passed', async () => {\n      mockArgv('prepare');\n      await importCli();\n\n      expect(prepareMock).toBeCalledWith({});\n    });\n\n    it('should respect passing a custom root', async () => {\n      mockArgv('prepare', 'path/to/root');\n      await importCli();\n\n      expect(prepareMock).toBeCalledWith({\n        root: 'path/to/root',\n      });\n    });\n\n    it('should respect a custom config file', async () => {\n      mockArgv('prepare', '-c', './path/to/config.ts');\n      await importCli();\n\n      expect(prepareMock).toBeCalledWith({\n        configFile: './path/to/config.ts',\n      });\n    });\n\n    it('should respect passing --debug', async () => {\n      mockArgv('prepare', '--debug');\n      await importCli();\n\n      expect(prepareMock).toBeCalledWith({\n        debug: true,\n      });\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n\n    it('should set log --level', async () => {\n      mockArgv('prepare', '--level', 'warn');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.warn);\n    });\n\n    it('--debug should override --level', async () => {\n      mockArgv('prepare', '--debug', '--level', 'silent');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n  });\n\n  describe('clean', () => {\n    it('should not pass any config when no flags are passed', async () => {\n      mockArgv('clean');\n      await importCli();\n\n      expect(cleanMock).toBeCalledWith({});\n    });\n\n    it('should respect passing a custom root', async () => {\n      mockArgv('clean', 'path/to/root');\n      await importCli();\n\n      expect(cleanMock).toBeCalledWith({ root: 'path/to/root' });\n    });\n\n    it('should respect a custom config file', async () => {\n      mockArgv('clean', '-c', './path/to/config.ts');\n      await importCli();\n\n      expect(cleanMock).toBeCalledWith({\n        configFile: './path/to/config.ts',\n      });\n    });\n\n    it('should respect passing --debug', async () => {\n      mockArgv('clean', '--debug');\n      await importCli();\n\n      expect(cleanMock).toBeCalledWith({\n        debug: true,\n      });\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n\n    it('should set log --level', async () => {\n      mockArgv('clean', '--level', 'warn');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.warn);\n    });\n\n    it('--debug should override --level', async () => {\n      mockArgv('clean', '--debug', '--level', 'silent');\n      await importCli();\n\n      expect(consola.level).toBe(LogLevels.debug);\n    });\n  });\n\n  describe('init', () => {\n    it('should not pass any options when no flags are passed', async () => {\n      mockArgv('init');\n      await importCli();\n\n      expect(initializeMock).toBeCalledWith({});\n    });\n\n    it('should respect the provided folder', async () => {\n      mockArgv('init', 'path/to/folder');\n      await importCli();\n\n      expect(initializeMock).toBeCalledWith({\n        directory: 'path/to/folder',\n      });\n    });\n\n    it('should respect passing --template', async () => {\n      mockArgv('init', '-t', 'vue');\n      await importCli();\n\n      expect(initializeMock).toBeCalledWith({\n        template: 'vue',\n      });\n    });\n\n    it('should respect passing --pm', async () => {\n      mockArgv('init', '--pm', 'pnpm');\n      await importCli();\n\n      expect(initializeMock).toBeCalledWith({\n        packageManager: 'pnpm',\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/cli/cli-utils.ts",
    "content": "import { CAC, Command } from 'cac';\nimport consola, { LogLevels, LogType } from 'consola';\nimport { filterTruthy, toArray } from '../core/utils/arrays';\nimport { printHeader } from '../core/utils/log';\nimport { formatDuration } from '../core/utils/time';\nimport { ValidationError } from '../core/utils/validation';\nimport { registerWxt } from '../core/wxt';\nimport spawn from 'nano-spawn';\n\n/**\n * Wrap an action handler to add a timer, error handling, and maybe enable debug\n * mode.\n */\nexport function wrapAction(\n  cb: (\n    ...args: any[]\n  ) => void | { isOngoing?: boolean } | Promise<void | { isOngoing?: boolean }>,\n  options?: {\n    disableFinishedLog?: boolean;\n  },\n) {\n  return async (...args: any[]) => {\n    const level: LogType | undefined = args.find((arg) => arg?.level)?.level;\n    if (level && Object.keys(LogLevels).includes(level)) {\n      consola.level = LogLevels[level];\n    }\n\n    // Enable consola's debug mode globally at the start of all commands when\n    // the `--debug` flag is passed\n    const isDebug = !!args.find((arg) => arg?.debug);\n    if (isDebug) {\n      consola.level = LogLevels.debug;\n    }\n\n    const startTime = Date.now();\n    try {\n      printHeader();\n\n      const status = await cb(...args);\n\n      if (!status?.isOngoing && !options?.disableFinishedLog)\n        consola.success(\n          `Finished in ${formatDuration(Date.now() - startTime)}`,\n        );\n    } catch (err) {\n      consola.fail(\n        `Command failed after ${formatDuration(Date.now() - startTime)}`,\n      );\n      if (!(err instanceof ValidationError)) {\n        consola.error(err);\n      }\n      process.exit(1);\n    }\n  };\n}\n\n/**\n * Array flags, when not passed, are either `undefined` or `[undefined]`. This\n * function filters out the\n */\nexport function getArrayFromFlags<T>(\n  flags: any,\n  name: string,\n): T[] | undefined {\n  const array = toArray<T | undefined>(flags[name]);\n  const result = filterTruthy(array);\n  return result.length ? result : undefined;\n}\n\nconst aliasCommandNames = new Set<string>();\n/**\n * @param base Command to add this one to\n * @param name The command name to add\n * @param alias The CLI tool being aliased\n * @param bin The CLI tool binary name. Usually the same as the alias\n * @param docsUrl URL to the docs for the aliased CLI tool\n */\nexport function createAliasedCommand(\n  base: CAC,\n  name: string,\n  alias: string,\n  bin: string,\n  docsUrl: string,\n) {\n  const aliasedCommand = base\n    .command(name, `Alias for ${alias} (${docsUrl})`)\n    .allowUnknownOptions()\n    .action(async () => {\n      try {\n        await registerWxt('build');\n\n        const args = process.argv.slice(\n          process.argv.indexOf(aliasedCommand.name) + 1,\n        );\n        await spawn(bin, args, {\n          stdio: 'inherit',\n        });\n      } catch {\n        // Let the other aliased CLI log errors, just exit\n        process.exit(1);\n      }\n    });\n  aliasCommandNames.add(aliasedCommand.name);\n}\nexport function isAliasedCommand(command: Command | undefined): boolean {\n  return !!command && aliasCommandNames.has(command.name);\n}\n"
  },
  {
    "path": "packages/wxt/src/cli/commands.ts",
    "content": "import cac from 'cac';\nimport { build, clean, createServer, initialize, prepare, zip } from '../core';\nimport {\n  createAliasedCommand,\n  getArrayFromFlags,\n  wrapAction,\n} from './cli-utils';\n\nconst cli = cac('wxt');\n\ncli.option('--debug', 'enable debug mode');\ncli.option(\n  '--level <level>',\n  'specify log level (\"silent\" | \"fatal\" | \"error\" | \"warn\" | \"log\" | \"info\" | \"success\" | \"fail\" | \"ready\" | \"start\" | \"box\" | \"debug\" | \"trace\" | \"verbose\")',\n);\n\n// DEV\ncli\n  .command('[root]', 'start dev server')\n  .option('-c, --config <file>', 'use specified config file')\n  .option('-m, --mode <mode>', 'set env mode')\n  .option('-b, --browser <browser>', 'specify a browser')\n  .option('--host <host>', 'specify a host for the dev server to bind to')\n  .option('-p, --port <port>', 'specify a port for the dev server to bind to')\n  .option(\n    '-e, --filter-entrypoint <entrypoint>',\n    'only build specific entrypoints',\n    {\n      type: [],\n    },\n  )\n  .option('--mv3', 'target manifest v3')\n  .option('--mv2', 'target manifest v2')\n  .action(\n    wrapAction(async (root, flags) => {\n      const serverOptions: NonNullable<\n        NonNullable<Parameters<typeof createServer>[0]>['dev']\n      >['server'] = {};\n      if (flags.host) serverOptions.host = flags.host;\n      if (flags.port) serverOptions.port = parseInt(flags.port);\n\n      const server = await createServer({\n        root,\n        mode: flags.mode,\n        browser: flags.browser,\n        manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,\n        configFile: flags.config,\n        debug: flags.debug,\n        filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),\n        dev:\n          Object.keys(serverOptions).length === 0\n            ? undefined\n            : { server: serverOptions },\n      });\n      await server.start();\n      return { isOngoing: true };\n    }),\n  );\n\n// BUILD\ncli\n  .command('build [root]', 'build for production')\n  .option('-c, --config <file>', 'use specified config file')\n  .option('-m, --mode <mode>', 'set env mode')\n  .option('-b, --browser <browser>', 'specify a browser')\n  .option(\n    '-e, --filter-entrypoint <entrypoint>',\n    'only build specific entrypoints',\n    {\n      type: [],\n    },\n  )\n  .option('--mv3', 'target manifest v3')\n  .option('--mv2', 'target manifest v2')\n  .option('--analyze', 'visualize extension bundle')\n  .option('--analyze-open', 'automatically open stats.html in browser')\n  .action(\n    wrapAction(async (root, flags) => {\n      await build({\n        root,\n        mode: flags.mode,\n        browser: flags.browser,\n        manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,\n        configFile: flags.config,\n        debug: flags.debug,\n        analysis: flags.analyze\n          ? {\n              enabled: true,\n              open: flags.analyzeOpen,\n            }\n          : undefined,\n        filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),\n      });\n    }),\n  );\n\n// ZIP\ncli\n  .command('zip [root]', 'build for production and zip output')\n  .option('-c, --config <file>', 'use specified config file')\n  .option('-m, --mode <mode>', 'set env mode')\n  .option('-b, --browser <browser>', 'specify a browser')\n  .option('--mv3', 'target manifest v3')\n  .option('--mv2', 'target manifest v2')\n  .option('--sources', 'always create sources zip')\n  .action(\n    wrapAction(async (root, flags) => {\n      await zip({\n        root,\n        mode: flags.mode,\n        browser: flags.browser,\n        manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,\n        configFile: flags.config,\n        debug: flags.debug,\n        zip: {\n          zipSources: flags.sources,\n        },\n      });\n    }),\n  );\n\n// PREPARE\ncli\n  .command('prepare [root]', 'prepare typescript project')\n  .option('-c, --config <file>', 'use specified config file')\n  .action(\n    wrapAction(async (root, flags) => {\n      await prepare({\n        root,\n        configFile: flags.config,\n        debug: flags.debug,\n      });\n    }),\n  );\n\n// CLEAN\ncli\n  .command('clean [root]', 'clean generated files and caches')\n  .alias('cleanup')\n  .option('-c, --config <file>', 'use specified config file')\n  .action(\n    wrapAction(async (root, flags) => {\n      await clean({ root, configFile: flags.config, debug: flags.debug });\n    }),\n  );\n\n// INIT\ncli\n  .command('init [directory]', 'initialize a new project')\n  .option('-t, --template <template>', 'template to use')\n  .option('--pm <packageManager>', 'which package manager to use')\n  .action(\n    wrapAction(\n      async (directory, flags) => {\n        await initialize({\n          directory,\n          template: flags.template,\n          packageManager: flags.pm,\n        });\n      },\n      { disableFinishedLog: true },\n    ),\n  );\n\n// SUBMIT\ncreateAliasedCommand(\n  cli,\n  'submit',\n  'publish-extension',\n  'wxt-publish-extension',\n  'https://www.npmjs.com/publish-browser-extension',\n);\n\nexport default cli;\n"
  },
  {
    "path": "packages/wxt/src/cli/index.ts",
    "content": "import cli from './commands';\nimport { version } from '../version';\nimport { isAliasedCommand } from './cli-utils';\n\n// Grab the command that we're trying to run\ncli.parse(process.argv, { run: false });\n\n// If it's not an alias, add the help and version options, then parse again\nif (!isAliasedCommand(cli.matchedCommand)) {\n  cli.help();\n  cli.version(version);\n  cli.parse(process.argv, { run: false });\n}\n\n// Run the alias or command\nawait cli.runMatchedCommand();\n"
  },
  {
    "path": "packages/wxt/src/core/build.ts",
    "content": "import { BuildOutput, InlineConfig } from '../types';\nimport { internalBuild } from './utils/building';\nimport { registerWxt } from './wxt';\n\n/**\n * Bundles the extension for production. Returns a promise of the build result.\n * Discovers the `wxt.config.ts` file in the root directory, and merges that\n * config with what is passed in.\n *\n * @example\n *   // Use config from `wxt.config.ts`\n *   const res = await build();\n *\n *   // or override config `from wxt.config.ts`\n *   const res = await build({\n *     // Override config...\n *   });\n */\nexport async function build(config?: InlineConfig): Promise<BuildOutput> {\n  await registerWxt('build', config);\n\n  return await internalBuild();\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/__tests__/fixtures/module.ts",
    "content": "import { a } from './test';\n\nfunction defineSomething<T>(config: T): T {\n  return config;\n}\n\nexport default defineSomething({\n  option: 'some value',\n  main: () => {\n    console.log('main', a);\n  },\n});\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/__tests__/fixtures/test.ts",
    "content": "console.log('Side-effect in test.ts');\nexport const a = 'a';\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/index.ts",
    "content": "import { Hookable } from 'hookable';\nimport { mkdir, readdir, rename, rmdir, stat } from 'node:fs/promises';\nimport { dirname, extname, join, relative } from 'node:path';\nimport type * as vite from 'vite';\nimport { ViteNodeRunner } from 'vite-node/client';\nimport { ViteNodeServer } from 'vite-node/server';\nimport { installSourcemapsSupport } from 'vite-node/source-map';\nimport {\n  BuildStepOutput,\n  Entrypoint,\n  EntrypointGroup,\n  ResolvedConfig,\n  WxtBuilder,\n  WxtBuilderServer,\n  WxtDevServer,\n  WxtHooks,\n} from '../../../types';\nimport { normalizePath } from '../../utils';\nimport { toArray } from '../../utils/arrays';\nimport {\n  getEntrypointBundlePath,\n  isHtmlEntrypoint,\n} from '../../utils/entrypoints';\nimport { createExtensionEnvironment } from '../../utils/environments';\nimport { safeVarName } from '../../utils/strings';\nimport {\n  VirtualEntrypointType,\n  VirtualModuleId,\n} from '../../utils/virtual-modules';\nimport * as wxtPlugins from './plugins';\n\nexport async function createViteBuilder(\n  wxtConfig: ResolvedConfig,\n  hooks: Hookable<WxtHooks>,\n  getWxtDevServer?: () => WxtDevServer | undefined,\n): Promise<WxtBuilder> {\n  const vite = await import('vite');\n\n  /**\n   * Returns the base vite config shared by all builds based on the inline and\n   * user config.\n   */\n  const getBaseConfig = async (baseConfigOptions?: {\n    excludeAnalysisPlugin?: boolean;\n  }) => {\n    const config: vite.InlineConfig = await wxtConfig.vite(wxtConfig.env);\n\n    config.root = wxtConfig.root;\n    config.configFile = false;\n    config.logLevel = 'warn';\n    config.mode = wxtConfig.mode;\n    config.envPrefix ??= ['VITE_', 'WXT_'];\n\n    config.build ??= {};\n    config.publicDir = wxtConfig.publicDir;\n    config.build.copyPublicDir = false;\n    config.build.outDir = wxtConfig.outDir;\n    config.build.emptyOutDir = false;\n    // Disable minification for the dev command\n    if (config.build.minify == null && wxtConfig.command === 'serve') {\n      config.build.minify = false;\n    }\n    // Enable inline sourcemaps for the dev command (so content scripts have sourcemaps)\n    if (config.build.sourcemap == null && wxtConfig.command === 'serve') {\n      config.build.sourcemap = 'inline';\n    }\n\n    config.server ??= {};\n    config.server.watch = {\n      ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],\n    };\n\n    // TODO: Remove once https://github.com/wxt-dev/wxt/pull/1411 is merged\n    config.legacy ??= {};\n    // @ts-ignore: Untyped option:\n    config.legacy.skipWebSocketTokenCheck = true;\n\n    // Solves https://github.com/wxt-dev/wxt/issues/353\n    config.esbuild ??= {};\n    if (config.esbuild) config.esbuild.charset = 'ascii';\n\n    const server = getWxtDevServer?.();\n\n    config.plugins ??= [];\n    config.plugins.push(\n      wxtPlugins.download(wxtConfig),\n      wxtPlugins.devHtmlPrerender(wxtConfig, server),\n      wxtPlugins.resolveVirtualModules(wxtConfig),\n      wxtPlugins.devServerGlobals(wxtConfig, server),\n      wxtPlugins.tsconfigPaths(wxtConfig),\n      wxtPlugins.noopBackground(),\n      wxtPlugins.globals(wxtConfig),\n      wxtPlugins.defineImportMeta(),\n      wxtPlugins.wxtPluginLoader(wxtConfig),\n      wxtPlugins.resolveAppConfig(wxtConfig),\n    );\n    if (\n      wxtConfig.analysis.enabled &&\n      // If included, vite-node entrypoint loader will increment the\n      // bundleAnalysis's internal build index tracker, which we don't want\n      !baseConfigOptions?.excludeAnalysisPlugin\n    ) {\n      config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));\n    }\n\n    return config;\n  };\n\n  /**\n   * Return the basic config for building an entrypoint in [lib\n   * mode](https://vitejs.dev/guide/build.html#library-mode).\n   */\n  const getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {\n    const entry = getRollupEntry(entrypoint);\n    const plugins: NonNullable<vite.UserConfig['plugins']> = [\n      wxtPlugins.entrypointGroupGlobals(entrypoint),\n    ];\n    let iifeReturnValueName = safeVarName(entrypoint.name);\n\n    if (\n      entrypoint.type === 'content-script-style' ||\n      entrypoint.type === 'unlisted-style'\n    ) {\n      plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));\n    }\n\n    if (\n      entrypoint.type === 'content-script' ||\n      entrypoint.type === 'unlisted-script'\n    ) {\n      if (typeof entrypoint.options.globalName === 'string') {\n        iifeReturnValueName = entrypoint.options.globalName;\n      } else if (typeof entrypoint.options.globalName === 'function') {\n        iifeReturnValueName = entrypoint.options.globalName(entrypoint);\n      }\n\n      if (entrypoint.options.globalName === false) {\n        plugins.push(wxtPlugins.iifeAnonymous(iifeReturnValueName));\n      } else {\n        plugins.push(wxtPlugins.iifeFooter(iifeReturnValueName));\n      }\n    }\n\n    return {\n      mode: wxtConfig.mode,\n      plugins,\n      build: {\n        lib: {\n          entry,\n          formats: ['iife'],\n          name: iifeReturnValueName,\n          fileName: entrypoint.name,\n        },\n        rollupOptions: {\n          output: {\n            // There's only a single output for this build, so we use the desired bundle path for the\n            // entry output (like \"content-scripts/overlay.js\")\n            entryFileNames: getEntrypointBundlePath(\n              entrypoint,\n              wxtConfig.outDir,\n              '.js',\n            ),\n            // Output content script CSS to `content-scripts/`, but all other scripts are written to\n            // `assets/`.\n            assetFileNames: ({ name }) => {\n              if (\n                entrypoint.type === 'content-script' &&\n                name?.endsWith('css')\n              ) {\n                return `content-scripts/${entrypoint.name}.[ext]`;\n              } else {\n                return `assets/${entrypoint.name}.[ext]`;\n              }\n            },\n          },\n        },\n      },\n      define: {\n        // See https://github.com/aklinker1/vite-plugin-web-extension/issues/96\n        'process.env.NODE_ENV': JSON.stringify(wxtConfig.mode),\n      },\n    } satisfies vite.UserConfig;\n  };\n\n  /**\n   * Return the basic config for building multiple entrypoints in [multi-page\n   * mode](https://vitejs.dev/guide/build.html#multi-page-app).\n   */\n  const getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {\n    const htmlEntrypoints = new Set(\n      entrypoints.filter(isHtmlEntrypoint).map((e) => e.name),\n    );\n    return {\n      mode: wxtConfig.mode,\n      plugins: [wxtPlugins.entrypointGroupGlobals(entrypoints)],\n      build: {\n        rollupOptions: {\n          input: entrypoints.reduce<Record<string, string>>((input, entry) => {\n            input[entry.name] = getRollupEntry(entry);\n            return input;\n          }, {}),\n          output: {\n            // Include a hash to prevent conflicts\n            chunkFileNames: 'chunks/[name]-[hash].js',\n            entryFileNames: ({ name }) => {\n              // HTML main JS files go in the chunks folder\n              if (htmlEntrypoints.has(name)) return 'chunks/[name]-[hash].js';\n              // Scripts are output in the root folder\n              return '[name].js';\n            },\n            // We can't control the \"name\", so we need a hash to prevent conflicts\n            assetFileNames: 'assets/[name]-[hash].[ext]',\n          },\n        },\n      },\n    };\n  };\n\n  /**\n   * Return the basic config for building a single CSS entrypoint in [multi-page\n   * mode](https://vitejs.dev/guide/build.html#multi-page-app).\n   */\n  const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {\n    return {\n      mode: wxtConfig.mode,\n      plugins: [wxtPlugins.entrypointGroupGlobals(entrypoint)],\n      build: {\n        rollupOptions: {\n          input: {\n            [entrypoint.name]: entrypoint.inputPath,\n          },\n          output: {\n            assetFileNames: () => {\n              if (entrypoint.type === 'content-script-style') {\n                return `content-scripts/${entrypoint.name}.[ext]`;\n              } else {\n                return `assets/${entrypoint.name}.[ext]`;\n              }\n            },\n          },\n        },\n      },\n    };\n  };\n\n  const createViteNodeImporter = async (paths: string[]) => {\n    const baseConfig = await getBaseConfig({\n      excludeAnalysisPlugin: true,\n    });\n    // Disable dep optimization, as recommended by vite-node's README\n    baseConfig.optimizeDeps ??= {};\n    baseConfig.optimizeDeps.noDiscovery = true;\n    baseConfig.optimizeDeps.include = [];\n    const envConfig: vite.InlineConfig = {\n      plugins: paths.map((path) =>\n        wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),\n      ),\n    };\n    const config = vite.mergeConfig(baseConfig, envConfig);\n    const server = await vite.createServer(config);\n    await server.pluginContainer.buildStart({});\n    const node = new ViteNodeServer(server);\n    installSourcemapsSupport({\n      getSourceMap: (source) => node.getSourceMap(source),\n    });\n    const runner = new ViteNodeRunner({\n      root: server.config.root,\n      base: server.config.base,\n      // when having the server and runner in a different context,\n      // you will need to handle the communication between them\n      // and pass to this function\n      fetchModule(id) {\n        return node.fetchModule(id);\n      },\n      resolveId(id, importer) {\n        return node.resolveId(id, importer);\n      },\n    });\n    return { runner, server };\n  };\n\n  const requireDefaultExport = (path: string, mod: any) => {\n    const relativePath = relative(wxtConfig.root, path);\n    if (mod?.default == null) {\n      const defineFn = relativePath.includes('.content')\n        ? 'defineContentScript'\n        : relativePath.includes('background')\n          ? 'defineBackground'\n          : 'defineUnlistedScript';\n\n      throw Error(\n        `${relativePath}: Default export not found, did you forget to call \"export default ${defineFn}(...)\"?`,\n      );\n    }\n  };\n\n  return {\n    name: 'Vite',\n    version: vite.version,\n    async importEntrypoint(path) {\n      const env = createExtensionEnvironment();\n      const { runner, server } = await createViteNodeImporter([path]);\n      const res = await env.run(() => runner.executeFile(path));\n      await server.close();\n      requireDefaultExport(path, res);\n      return res.default;\n    },\n    async importEntrypoints(paths) {\n      const env = createExtensionEnvironment();\n      const { runner, server } = await createViteNodeImporter(paths);\n      const res = await env.run(() =>\n        Promise.all(\n          paths.map(async (path) => {\n            const mod = await runner.executeFile(path);\n            requireDefaultExport(path, mod);\n            return mod.default;\n          }),\n        ),\n      );\n      await server.close();\n      return res;\n    },\n    async build(group) {\n      let entryConfig;\n      if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);\n      else if (\n        group.type === 'content-script-style' ||\n        group.type === 'unlisted-style'\n      )\n        entryConfig = getCssConfig(group);\n      else entryConfig = getLibModeConfig(group);\n\n      const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);\n      await hooks.callHook(\n        'vite:build:extendConfig',\n        toArray(group),\n        buildConfig,\n      );\n      const result = await vite.build(buildConfig);\n      const chunks = getBuildOutputChunks(result);\n      return {\n        entrypoints: group,\n        chunks: await moveHtmlFiles(wxtConfig, group, chunks),\n      };\n    },\n    async createServer(info) {\n      const serverConfig: vite.InlineConfig = {\n        server: {\n          host: info.host,\n          port: info.port,\n          strictPort: true,\n          origin: info.origin,\n        },\n      };\n      const baseConfig = await getBaseConfig();\n      const finalConfig = vite.mergeConfig(baseConfig, serverConfig);\n      await hooks.callHook('vite:devServer:extendConfig', finalConfig);\n      const viteServer = await vite.createServer(finalConfig);\n\n      const server: WxtBuilderServer = {\n        async listen() {\n          await viteServer.listen(info.port);\n        },\n        async close() {\n          await viteServer.close();\n        },\n        transformHtml(...args) {\n          return viteServer.transformIndexHtml(...args);\n        },\n        ws: {\n          send(message, payload) {\n            return viteServer.ws.send(message, payload);\n          },\n          on(message, cb) {\n            viteServer.ws.on(message, cb);\n          },\n        },\n        watcher: viteServer.watcher,\n        on(event, cb) {\n          viteServer.httpServer?.on(event, cb);\n        },\n      };\n\n      return server;\n    },\n  };\n}\n\nfunction getBuildOutputChunks(\n  result: Awaited<ReturnType<typeof vite.build>>,\n): BuildStepOutput['chunks'] {\n  if ('on' in result) throw Error('wxt does not support vite watch mode.');\n  if (Array.isArray(result)) return result.flatMap(({ output }) => output);\n  return result.output;\n}\n\n/**\n * Returns the input module ID (virtual or real file) for an entrypoint. The\n * returned string should be passed as an input to rollup.\n */\nfunction getRollupEntry(entrypoint: Entrypoint): string {\n  let virtualEntrypointType: VirtualEntrypointType | undefined;\n  switch (entrypoint.type) {\n    case 'background':\n    case 'unlisted-script':\n      virtualEntrypointType = entrypoint.type;\n      break;\n    case 'content-script':\n      virtualEntrypointType =\n        entrypoint.options.world === 'MAIN'\n          ? 'content-script-main-world'\n          : 'content-script-isolated-world';\n      break;\n  }\n\n  if (virtualEntrypointType) {\n    const moduleId: VirtualModuleId = `virtual:wxt-${virtualEntrypointType}-entrypoint`;\n    return `${moduleId}?${entrypoint.inputPath}`;\n  }\n  return entrypoint.inputPath;\n}\n\n/**\n * Ensures the HTML files output by a multipage build are in the correct\n * location. This does two things:\n *\n * 1. Moves the HTML files to their final location at\n *    `<outDir>/<entrypoint.name>.html`.\n * 2. Updates the bundle so it summarizes the files correctly in the returned build\n *    output.\n *\n * Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't\n * need to be modified. HTML files access them via absolute URLs, so we don't\n * need to update any import paths in the HTML files either.\n */\nasync function moveHtmlFiles(\n  config: ResolvedConfig,\n  group: EntrypointGroup,\n  chunks: BuildStepOutput['chunks'],\n): Promise<BuildStepOutput['chunks']> {\n  if (!Array.isArray(group)) return chunks;\n\n  const entryMap = group.reduce<Record<string, Entrypoint>>((map, entry) => {\n    const a = normalizePath(relative(config.root, entry.inputPath));\n    map[a] = entry;\n    return map;\n  }, {});\n\n  const movedChunks = await Promise.all(\n    chunks.map(async (chunk) => {\n      if (!chunk.fileName.endsWith('.html')) return chunk;\n\n      const entry = entryMap[chunk.fileName];\n      const oldBundlePath = chunk.fileName;\n      const newBundlePath = getEntrypointBundlePath(\n        entry,\n        config.outDir,\n        extname(chunk.fileName),\n      );\n      const oldAbsPath = join(config.outDir, oldBundlePath);\n      const newAbsPath = join(config.outDir, newBundlePath);\n      await mkdir(dirname(newAbsPath), { recursive: true });\n      await rename(oldAbsPath, newAbsPath);\n\n      return {\n        ...chunk,\n        fileName: newBundlePath,\n      };\n    }),\n  );\n\n  // TODO: Optimize and only delete old path directories\n  await removeEmptyDirs(config.outDir);\n\n  return movedChunks;\n}\n\n/** Recursively remove all directories that are empty/ */\nexport async function removeEmptyDirs(dir: string): Promise<void> {\n  const files = await readdir(dir);\n  for (const file of files) {\n    const filePath = join(dir, file);\n    const stats = await stat(filePath);\n    if (stats.isDirectory()) {\n      await removeEmptyDirs(filePath);\n    }\n  }\n\n  try {\n    await rmdir(dir);\n  } catch {\n    // noop on failure - this means the directory was not empty.\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/__tests__/devHtmlPrerender.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { pointToDevServer } from '../devHtmlPrerender';\nimport {\n  fakeDevServer,\n  fakeResolvedConfig,\n} from '../../../../utils/testing/fake-objects';\nimport { normalizePath } from '../../../../utils';\nimport { resolve } from 'node:path';\nimport { parseHTML } from 'linkedom';\n\ndescribe('Dev HTML Prerender Plugin', () => {\n  describe('pointToDevServer', () => {\n    it.each([\n      // File paths should be resolved\n      ['style.css', 'http://localhost:5173/entrypoints/popup/style.css'],\n      ['./style.css', 'http://localhost:5173/entrypoints/popup/style.css'],\n      ['../style.css', 'http://localhost:5173/entrypoints/style.css'],\n      ['~/assets/style.css', 'http://localhost:5173/assets/style.css'],\n      ['~~/assets/style.css', 'http://localhost:5173/assets/style.css'],\n      ['~local/style.css', 'http://localhost:5173/style.css'],\n      ['~absolute/style.css', 'http://localhost:5173/assets/style.css'],\n      ['~file', 'http://localhost:5173/example.css'],\n      // Paths outside the project root are loaded with the `/@fs/` base path\n      [\n        '~outside/test.css',\n        `http://localhost:5173/@fs${\n          process.platform === 'win32'\n            ? '/' + normalizePath(resolve('/some/non-root/test.css')) // \"/D:/some/non-root/test.css\"\n            : '/some/non-root/test.css'\n        }`,\n      ],\n      // URLs should not be changed\n      ['https://example.com/style.css', 'https://example.com/style.css'],\n    ])('should transform \"%s\" into \"%s\"', (input, expected) => {\n      const { document } = parseHTML('<html></html>');\n      const root = '/some/root';\n      const config = fakeResolvedConfig({\n        root,\n        alias: {\n          '~local': '.',\n          '~absolute': `${root}/assets`,\n          '~file': `${root}/example.css`,\n          '~outside': `${root}/../non-root`,\n          '~~': root,\n          '~': root,\n        },\n      });\n      const server = fakeDevServer({\n        host: 'localhost',\n        port: 5173,\n        origin: 'http://localhost:5173',\n      });\n      const id = root + '/entrypoints/popup/index.html';\n\n      document.head.innerHTML = `<link rel=\"stylesheet\" href=\"${input}\" />`;\n      pointToDevServer(config, server, id, document, 'link', 'href');\n\n      const actual = document.querySelector('link')!;\n      expect(actual.getAttribute('href')).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/__tests__/iifeFooter.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { iifeFooter } from '../iifeFooter';\n\ninterface OutputChunk {\n  type: 'chunk';\n  code: string;\n  isEntry: boolean;\n}\n\ninterface OutputAsset {\n  type: 'asset';\n  source: string;\n}\n\ntype OutputBundle = Record<string, OutputChunk | OutputAsset>;\n\nfunction dedent(code: string) {\n  const lines = code.trim().split('\\n');\n  return lines.map((line) => line.trimStart()).join('\\n');\n}\n\nfunction createBundle(code: string): OutputBundle {\n  return {\n    'entry.js': {\n      type: 'chunk',\n      code: dedent(code),\n      isEntry: true,\n    },\n  };\n}\n\nfunction getCode(bundle: OutputBundle): string {\n  const entry = bundle['entry.js'];\n\n  if (entry.type !== 'chunk') {\n    throw new Error('expected chunk');\n  }\n\n  return entry.code;\n}\n\nfunction runPlugin(name: string, bundle: OutputBundle) {\n  const plugin = iifeFooter(name);\n  // @ts-expect-error -- calling the hook directly\n  plugin.generateBundle(undefined, bundle);\n}\n\ndescribe('IIFE return value plugin', () => {\n  it('should append return value when no sourcemap comment', () => {\n    const bundle = createBundle(`\n      var foo = (function(){return 1})();\n    `);\n\n    runPlugin('foo', bundle);\n\n    expect(getCode(bundle)).toBe(\n      dedent(`\n        var foo = (function(){return 1})();\n        foo;\n      `),\n    );\n  });\n\n  it('should insert return value before sourcemap comment', () => {\n    const bundle = createBundle(`\n      var foo = (function(){return 1})();\n      //# ${'sourceMappingURL'}=foo.js.map\n    `);\n\n    runPlugin('foo', bundle);\n\n    expect(getCode(bundle)).toBe(\n      dedent(`\n        var foo = (function(){return 1})();\n        foo;\n        //# ${'sourceMappingURL'}=foo.js.map\n      `),\n    );\n  });\n\n  it('should insert return value before inline sourcemap', () => {\n    const bundle = createBundle(`\n      var foo = (function(){return 1})();\n      //# ${'sourceMappingURL'}=data:application/json;base64,abc123\n    `);\n\n    runPlugin('foo', bundle);\n\n    expect(getCode(bundle)).toBe(\n      dedent(`\n        var foo = (function(){return 1})();\n        foo;\n        //# ${'sourceMappingURL'}=data:application/json;base64,abc123\n      `),\n    );\n  });\n\n  it('should skip non-entry chunks', () => {\n    const bundle = createBundle('var x = 1;');\n    (bundle['entry.js'] as OutputChunk).isEntry = false;\n\n    runPlugin('x', bundle);\n\n    expect(getCode(bundle)).toBe('var x = 1;');\n  });\n\n  it('should skip assets', () => {\n    const bundle: OutputBundle = {\n      'style.css': {\n        type: 'asset',\n        source: 'body {}',\n      } satisfies OutputAsset,\n    };\n\n    runPlugin('style', bundle);\n\n    expect((bundle['style.css'] as OutputAsset).source).toBe('body {}');\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/bundleAnalysis.ts",
    "content": "import type * as vite from 'vite';\nimport { visualizer } from '@aklinker1/rollup-plugin-visualizer';\nimport { ResolvedConfig } from '../../../../types';\nimport path from 'node:path';\n\nlet increment = 0;\n\nexport function bundleAnalysis(config: ResolvedConfig): vite.Plugin {\n  return visualizer({\n    template: 'raw-data',\n    filename: path.resolve(\n      config.analysis.outputDir,\n      `${config.analysis.outputName}-${increment++}.json`,\n    ),\n  }) as vite.Plugin;\n}\n\n/** @internal FOR TESTING ONLY. */\nexport function resetBundleIncrement() {\n  increment = 0;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/cssEntrypoints.ts",
    "content": "import type * as vite from 'vite';\nimport { Entrypoint, ResolvedConfig } from '../../../../types';\nimport { getEntrypointBundlePath } from '../../../utils/entrypoints';\n\n/**\n * Rename CSS entrypoint outputs to ensure a JS file is not generated, and that\n * the CSS file is placed in the correct place.\n *\n * It:\n *\n * 1. Renames CSS files to their final paths\n * 2. Removes the JS file that get's output by lib mode\n *\n * THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be\n * added to every build.\n */\nexport function cssEntrypoints(\n  entrypoint: Entrypoint,\n  config: ResolvedConfig,\n): vite.Plugin {\n  return {\n    name: 'wxt:css-entrypoint',\n    config() {\n      return {\n        build: {\n          rollupOptions: {\n            output: {\n              assetFileNames: () =>\n                getEntrypointBundlePath(entrypoint, config.outDir, '.css'),\n            },\n          },\n        },\n      };\n    },\n    generateBundle(_, bundle) {\n      Object.keys(bundle).forEach((file) => {\n        if (file.endsWith('.js')) delete bundle[file];\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/defineImportMeta.ts",
    "content": "/**\n * Overrides definitions for `import.meta.*`\n *\n * - `import.meta.url`: Without this, background service workers crash trying to\n *   access `document.location`, see https://github.com/wxt-dev/wxt/issues/392\n */\nexport function defineImportMeta() {\n  return {\n    name: 'wxt:define',\n    config() {\n      return {\n        define: {\n          // This works for all extension contexts, including background service worker\n          'import.meta.url': 'self.location.href',\n        },\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/devHtmlPrerender.ts",
    "content": "import type * as vite from 'vite';\nimport { ResolvedConfig, WxtDevServer } from '../../../../types';\nimport { getEntrypointName } from '../../../utils/entrypoints';\nimport { parseHTML } from 'linkedom';\nimport { dirname, relative, resolve } from 'node:path';\nimport { normalizePath } from '../../../utils';\nimport { hash } from 'ohash';\n\n// Stored outside the plugin to effect all instances of the devHtmlPrerender plugin.\nconst inlineScriptContents: Record<string, string> = {};\n\n/**\n * Pre-renders the HTML entrypoints when building the extension to connect to\n * the dev server.\n */\nexport function devHtmlPrerender(\n  config: ResolvedConfig,\n  server: WxtDevServer | undefined,\n): vite.PluginOption {\n  const htmlReloadId = '@wxt/reload-html';\n  const resolvedHtmlReloadId = resolve(\n    config.wxtModuleDir,\n    'dist/virtual/reload-html.mjs',\n  );\n\n  const virtualInlineScript = 'virtual:wxt-inline-script';\n  const resolvedVirtualInlineScript = '\\0' + virtualInlineScript;\n\n  return [\n    {\n      apply: 'build',\n      name: 'wxt:dev-html-prerender',\n      config() {\n        return {\n          resolve: {\n            alias: {\n              [htmlReloadId]: resolvedHtmlReloadId,\n            },\n          },\n        };\n      },\n      // Convert scripts like src=\"./main.tsx\" -> src=\"http://localhost:3000/entrypoints/popup/main.tsx\"\n      // before the paths are replaced with their bundled path\n      transform: {\n        filter: {\n          id: /\\.html$/,\n        },\n        handler(code, id) {\n          if (config.command !== 'serve' || server == null) return;\n\n          const { document } = parseHTML(code);\n\n          const _pointToDevServer = (querySelector: string, attr: string) =>\n            pointToDevServer(config, server, id, document, querySelector, attr);\n          _pointToDevServer('script[type=module]', 'src');\n          _pointToDevServer('link[rel=stylesheet]', 'href');\n\n          // Add a script to add page reloading\n          const reloader = document.createElement('script');\n          reloader.src = htmlReloadId;\n          reloader.type = 'module';\n          document.head.appendChild(reloader);\n\n          const newHtml = document.toString();\n          config.logger.debug('transform ' + id);\n          config.logger.debug('Old HTML:\\n' + code);\n          config.logger.debug('New HTML:\\n' + newHtml);\n          return newHtml;\n        },\n      },\n\n      // Pass the HTML through the dev server to add dev-mode specific code\n      async transformIndexHtml(html, ctx) {\n        if (config.command !== 'serve' || server == null) return;\n\n        const originalUrl = `${server.origin}${ctx.path}`;\n        const name = getEntrypointName(config.entrypointsDir, ctx.filename);\n        const url = `${server.origin}/${name}.html`;\n        const serverHtml = await server.transformHtml(url, html, originalUrl);\n        const { document } = parseHTML(serverHtml);\n\n        // Replace inline script with virtual module served via dev server.\n        // Extension CSP blocks inline scripts, so that's why we're pulling them\n        // out.\n        const inlineScripts = document.querySelectorAll('script:not([src])');\n        inlineScripts.forEach((script) => {\n          // Save the text content for later\n          const textContent = script.textContent ?? '';\n          const key = hash(textContent);\n          inlineScriptContents[key] = textContent;\n\n          // Replace unsafe inline script\n          const virtualScript = document.createElement('script');\n          virtualScript.type = 'module';\n          virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${key}`;\n          script.replaceWith(virtualScript);\n        });\n\n        // Change /@vite/client -> http://localhost:3000/@vite/client\n        const viteClientScript = document.querySelector<HTMLScriptElement>(\n          \"script[src='/@vite/client']\",\n        );\n        if (viteClientScript) {\n          viteClientScript.src = `${server.origin}${viteClientScript.src}`;\n        }\n\n        const newHtml = document.toString();\n        config.logger.debug('transformIndexHtml ' + ctx.filename);\n        config.logger.debug('Old HTML:\\n' + html);\n        config.logger.debug('New HTML:\\n' + newHtml);\n        return newHtml;\n      },\n    },\n    {\n      name: 'wxt:virtualize-inline-scripts',\n      apply: 'serve',\n      resolveId: {\n        filter: {\n          id: [new RegExp(`^${virtualInlineScript}`), new RegExp('^/chunks/')],\n        },\n        handler(id) {\n          // Ignore chunks during HTML file pre-rendering\n          if (id.startsWith('/chunks/')) {\n            return '\\0noop';\n          }\n\n          return `\\0${id}`;\n        },\n      },\n      load: {\n        filter: {\n          id: [\n            new RegExp(`^${resolvedVirtualInlineScript}`),\n            //eslint-disable-next-line no-control-regex\n            new RegExp('^\\x00noop'),\n          ],\n        },\n        handler(id) {\n          // Ignore chunks during HTML file pre-rendering\n          if (id === '\\0noop') {\n            return '';\n          }\n\n          // id=\"virtual:wxt-inline-script?<key>\"\n          const key = id.substring(id.indexOf('?') + 1);\n          return inlineScriptContents[key];\n        },\n      },\n    },\n  ];\n}\n\nexport function pointToDevServer(\n  config: ResolvedConfig,\n  server: WxtDevServer,\n  id: string,\n  document: Document,\n  querySelector: string,\n  attr: string,\n) {\n  document.querySelectorAll(querySelector).forEach((element) => {\n    if (\n      element.hasAttribute('vite-ignore') ||\n      element.hasAttribute('wxt-ignore')\n    ) {\n      element.removeAttribute('wxt-ignore');\n      return;\n    }\n    const src = element.getAttribute(attr);\n    if (!src || isUrl(src)) return;\n\n    let resolvedAbsolutePath: string | undefined;\n\n    // Check if src uses a project alias\n    const matchingAlias = Object.entries(config.alias).find(([key]) =>\n      src.startsWith(key),\n    );\n    if (matchingAlias) {\n      // Matches a import alias\n      const [alias, replacement] = matchingAlias;\n      resolvedAbsolutePath = resolve(\n        config.root,\n        src.replace(alias, replacement),\n      );\n    } else {\n      // Some file path relative to the HTML file\n      resolvedAbsolutePath = resolve(dirname(id), src);\n    }\n\n    // Apply the final file path\n    if (resolvedAbsolutePath) {\n      const relativePath = normalizePath(\n        relative(config.root, resolvedAbsolutePath),\n      );\n\n      if (relativePath.startsWith('.')) {\n        // Outside the config.root directory, serve the absolute path\n        let path = normalizePath(resolvedAbsolutePath);\n        // Add \"/\" to start of windows paths (\"D:/some/path\" -> \"/D:/some/path\")\n        if (!path.startsWith('/')) path = '/' + path;\n        element.setAttribute(attr, `${server.origin}/@fs${path}`);\n      } else {\n        // Inside the project, use relative path\n        const url = new URL(relativePath, server.origin);\n        element.setAttribute(attr, url.href);\n      }\n    }\n  });\n}\n\nfunction isUrl(str: string): boolean {\n  try {\n    new URL(str);\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/devServerGlobals.ts",
    "content": "import type { Plugin } from 'vite';\nimport type { ResolvedConfig, WxtDevServer } from '../../../../types';\n\n/**\n * Defines global constants about the dev server. Helps scripts connect to the\n * server's web socket.\n */\nexport function devServerGlobals(\n  config: ResolvedConfig,\n  server: WxtDevServer | undefined,\n): Plugin {\n  return {\n    name: 'wxt:dev-server-globals',\n    config() {\n      if (server == null || config.command == 'build') return;\n\n      return {\n        define: {\n          __DEV_SERVER_ORIGIN__: JSON.stringify(\n            server.origin.replace(/^http(s?):/, 'ws$1:'),\n          ),\n        },\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/download.ts",
    "content": "import type { Plugin } from 'vite';\nimport type { ResolvedConfig } from '../../../../types';\nimport { fetchCached } from '../../../utils/network';\n\n/**\n * Downloads any URL imports, like Google Analytics, into virtual modules so\n * they are bundled with the extension instead of depending on remote code at\n * runtime.\n *\n * @example\n *   import 'url:https://google-tagmanager.com/gtag?id=XYZ';\n */\nexport function download(config: ResolvedConfig): Plugin {\n  return {\n    name: 'wxt:download',\n    resolveId: {\n      filter: {\n        id: /^url:/,\n      },\n      handler(id) {\n        return `\\0${id}`;\n      },\n    },\n    load: {\n      filter: {\n        //eslint-disable-next-line no-control-regex\n        id: /^\\x00url:/,\n      },\n      handler(id) {\n        const url = id.replace('\\0url:', '');\n        return fetchCached(url, config);\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/entrypointGroupGlobals.ts",
    "content": "import type * as vite from 'vite';\nimport { EntrypointGroup } from '../../../../types';\nimport { getEntrypointGlobals } from '../../../utils/globals';\n\n/** Define a set of global variables specific to an entrypoint. */\nexport function entrypointGroupGlobals(\n  entrypointGroup: EntrypointGroup,\n): vite.PluginOption {\n  return {\n    name: 'wxt:entrypoint-group-globals',\n    config() {\n      const define: vite.InlineConfig['define'] = {};\n      let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name;\n      for (const global of getEntrypointGlobals(name)) {\n        define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);\n      }\n      return {\n        define,\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts",
    "content": "import path from 'node:path';\nimport type * as vite from 'vite';\nimport { ResolvedConfig } from '../../../../types';\n\n/**\n * Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake\n * version of the extension APIs\n */\nexport function extensionApiMock(config: ResolvedConfig): vite.PluginOption {\n  const virtualSetupModule = 'virtual:wxt-setup';\n  const resolvedVirtualSetupModule = '\\0' + virtualSetupModule;\n\n  return {\n    name: 'wxt:extension-api-mock',\n    config() {\n      const replacement = path.resolve(\n        config.wxtModuleDir,\n        'dist/virtual/mock-browser',\n      );\n      return {\n        test: {\n          setupFiles: [virtualSetupModule],\n        },\n        resolve: {\n          alias: [\n            // wxt/browser, wxt/browser/...\n            { find: 'wxt/browser', replacement },\n          ],\n        },\n        ssr: {\n          // Inline all WXT modules sub-dependencies can be mocked\n          noExternal: ['wxt'],\n        },\n      };\n    },\n    resolveId: {\n      filter: {\n        id: new RegExp(`${virtualSetupModule}$`),\n      },\n      handler() {\n        return resolvedVirtualSetupModule;\n      },\n    },\n    load: {\n      filter: {\n        id: new RegExp(`^${resolvedVirtualSetupModule}$`),\n      },\n      handler() {\n        return setupTemplate;\n      },\n    },\n  };\n}\n\nconst setupTemplate = `\n  import { vi } from 'vitest';\n  import { fakeBrowser } from 'wxt/testing/fake-browser';\n\n  vi.stubGlobal(\"chrome\", fakeBrowser);\n  vi.stubGlobal(\"browser\", fakeBrowser);\n`;\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/globals.ts",
    "content": "import type * as vite from 'vite';\nimport { ResolvedConfig } from '../../../../types';\nimport { getGlobals } from '../../../utils/globals';\n\nexport function globals(config: ResolvedConfig): vite.PluginOption {\n  return {\n    name: 'wxt:globals',\n    config() {\n      const define: vite.InlineConfig['define'] = {};\n      for (const global of getGlobals(config)) {\n        define[`import.meta.env.${global.name}`] = JSON.stringify(global.value);\n      }\n      return {\n        define,\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/iifeAnonymous.ts",
    "content": "import type { Plugin } from 'vite';\n\nexport function iifeAnonymous(iifeReturnValueName: string): Plugin {\n  return {\n    name: 'wxt:iife-anonymous',\n    generateBundle(_, bundle) {\n      for (const chunk of Object.values(bundle)) {\n        if (chunk.type === 'chunk' && chunk.isEntry) {\n          const namedIIFEPrefix = new RegExp(\n            `^var ${iifeReturnValueName}\\\\s*=\\\\s*(\\\\(function)`,\n          );\n          chunk.code = chunk.code.replace(namedIIFEPrefix, '$1');\n        }\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/iifeFooter.ts",
    "content": "import type { Plugin } from 'vite';\n\n/**\n * Add a footer with the returned value so it can return values to\n * `scripting.executeScript` Footer is added a part of esbuild to make sure it's\n * not minified. It get's removed if added to\n * `build.rollupOptions.output.footer` See\n * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value\n */\nexport function iifeFooter(iifeReturnValueName: string): Plugin {\n  return {\n    name: 'wxt:iife-footer',\n    generateBundle(_, bundle) {\n      for (const chunk of Object.values(bundle)) {\n        if (chunk.type === 'chunk' && chunk.isEntry) {\n          const code = chunk.code;\n          const marker = '\\n//# sourceMappingURL=';\n          const returnValue = `${iifeReturnValueName};`;\n\n          const index = code.indexOf(marker);\n\n          if (index >= 0) {\n            chunk.code =\n              code.slice(0, index + 1) +\n              `${returnValue}\\n` +\n              code.slice(index + 1);\n          } else {\n            chunk.code += `\\n${returnValue}`;\n          }\n        }\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/index.ts",
    "content": "export * from './devHtmlPrerender';\nexport * from './devServerGlobals';\nexport * from './download';\nexport * from './resolveVirtualModules';\nexport * from './tsconfigPaths';\nexport * from './noopBackground';\nexport * from './cssEntrypoints';\nexport * from './bundleAnalysis';\nexport * from './globals';\nexport * from './extensionApiMock';\nexport * from './entrypointGroupGlobals';\nexport * from './defineImportMeta';\nexport * from './removeEntrypointMainFunction';\nexport * from './wxtPluginLoader';\nexport * from './resolveAppConfig';\nexport * from './iifeFooter';\nexport * from './iifeAnonymous';\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/noopBackground.ts",
    "content": "import type { Plugin } from 'vite';\nimport { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../../utils/constants';\n\n/**\n * In dev mode, if there's not a background script listed, we need to add one so\n * that the web socket connection is setup and the extension reloads HTML pages\n * and content scripts correctly.\n */\nexport function noopBackground(): Plugin {\n  const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;\n  const resolvedVirtualModuleId = '\\0' + virtualModuleId;\n  return {\n    name: 'wxt:noop-background',\n    resolveId: {\n      filter: {\n        id: new RegExp(`^${virtualModuleId}$`),\n      },\n      handler() {\n        return resolvedVirtualModuleId;\n      },\n    },\n    load: {\n      filter: {\n        id: new RegExp(`^${resolvedVirtualModuleId}$`),\n      },\n      handler() {\n        return `import { defineBackground } from 'wxt/utils/define-background';\\nexport default defineBackground(() => void 0)`;\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/removeEntrypointMainFunction.ts",
    "content": "import { ResolvedConfig } from '../../../../types';\nimport type * as vite from 'vite';\nimport { normalizePath } from '../../../utils';\nimport { removeMainFunctionCode } from '../../../utils/transform';\nimport { resolve } from 'node:path';\n\n/**\n * Transforms entrypoints, removing the main function from the entrypoint if it\n * exists.\n */\nexport function removeEntrypointMainFunction(\n  config: ResolvedConfig,\n  path: string,\n): vite.Plugin {\n  const absPath = normalizePath(resolve(config.root, path));\n  return {\n    name: 'wxt:remove-entrypoint-main-function',\n    transform: {\n      order: 'pre',\n      filter: {\n        id: new RegExp(`^${absPath}$`),\n      },\n      handler(code) {\n        const newCode = removeMainFunctionCode(code);\n        config.logger.debug('vite-node transformed entrypoint', path);\n        config.logger.debug(`Original:\\n---\\n${code}\\n---`);\n        config.logger.debug(`Transformed:\\n---\\n${newCode.code}\\n---`);\n        return newCode;\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/resolveAppConfig.ts",
    "content": "import { pathExists } from '../../../utils/fs';\nimport { resolve } from 'node:path';\nimport type * as vite from 'vite';\nimport { ResolvedConfig } from '../../../../types';\n\n/**\n * When importing `virtual:app-config`, resolve it to the `app.config.ts` file\n * in the project.\n */\nexport function resolveAppConfig(config: ResolvedConfig): vite.Plugin {\n  const virtualModuleId = 'virtual:app-config';\n  const resolvedVirtualModuleId = '\\0' + virtualModuleId;\n  const appConfigFile = resolve(config.srcDir, 'app.config.ts');\n\n  return {\n    name: 'wxt:resolve-app-config',\n    config() {\n      return {\n        optimizeDeps: {\n          // Prevent ESBuild from attempting to resolve the virtual module\n          // while optimizing WXT.\n          exclude: [virtualModuleId],\n        },\n      };\n    },\n    resolveId: {\n      filter: {\n        id: new RegExp(`^${virtualModuleId}$`),\n      },\n      async handler() {\n        return (await pathExists(appConfigFile))\n          ? appConfigFile\n          : resolvedVirtualModuleId;\n      },\n    },\n    load: {\n      filter: {\n        id: new RegExp(`^${resolvedVirtualModuleId}$`),\n      },\n      handler() {\n        return `export default {}`;\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/resolveVirtualModules.ts",
    "content": "import { readFile } from 'node:fs/promises';\nimport { resolve } from 'path';\nimport type { Plugin } from 'vite';\nimport { ResolvedConfig } from '../../../../types';\nimport { normalizePath } from '../../../utils';\nimport {\n  VirtualModuleId,\n  virtualModuleNames,\n} from '../../../utils/virtual-modules';\n\n/**\n * Resolve all the virtual modules to the `node_modules/wxt/dist/virtual`\n * directory.\n */\nexport function resolveVirtualModules(config: ResolvedConfig): Plugin[] {\n  return virtualModuleNames.map((name) => {\n    const virtualId: `${VirtualModuleId}?` = `virtual:wxt-${name}?`;\n    const resolvedVirtualId = '\\0' + virtualId;\n\n    return {\n      name: `wxt:resolve-virtual-${name}`,\n      resolveId: {\n        filter: {\n          id: new RegExp(virtualId),\n        },\n        handler(id) {\n          const inputPath = normalizePath(\n            id.substring(id.indexOf(virtualId) + virtualId.length),\n          );\n          return resolvedVirtualId + inputPath;\n        },\n      },\n      load: {\n        filter: {\n          id: new RegExp(`^${resolvedVirtualId}`),\n        },\n        async handler(id) {\n          const inputPath = id.replace(resolvedVirtualId, '');\n          const template = await readFile(\n            resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`),\n            'utf-8',\n          );\n          return template.replace(`virtual:user-${name}`, inputPath);\n        },\n      },\n    };\n  });\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/tsconfigPaths.ts",
    "content": "import { ResolvedConfig } from '../../../../types';\nimport type * as vite from 'vite';\n\nexport function tsconfigPaths(config: ResolvedConfig): vite.Plugin {\n  return {\n    name: 'wxt:aliases',\n    async config() {\n      return {\n        resolve: {\n          alias: config.alias,\n        },\n      };\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/builders/vite/plugins/wxtPluginLoader.ts",
    "content": "import { parseHTML } from 'linkedom';\nimport type * as vite from 'vite';\nimport { normalizePath } from '../../../utils';\nimport { ResolvedConfig } from '../../../../types';\n\n/**\n * Resolve and load plugins for each entrypoint. This handles both JS\n * entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding\n * `virtual:wxt-html-plugins` to the document's `<head>`\n */\nexport function wxtPluginLoader(config: ResolvedConfig): vite.Plugin {\n  const virtualModuleId = 'virtual:wxt-plugins';\n  const resolvedVirtualModuleId = '\\0' + virtualModuleId;\n  const virtualHtmlModuleId = 'virtual:wxt-html-plugins';\n  const resolvedVirtualHtmlModuleId = '\\0' + virtualHtmlModuleId;\n\n  return {\n    name: 'wxt:plugin-loader',\n    resolveId: {\n      filter: {\n        id: [\n          new RegExp(`^${virtualModuleId}$`),\n          new RegExp(`^${virtualHtmlModuleId}$`),\n        ],\n      },\n      handler(id) {\n        if (id === virtualModuleId) {\n          return resolvedVirtualModuleId;\n        }\n\n        return resolvedVirtualHtmlModuleId;\n      },\n    },\n    load: {\n      filter: {\n        id: [\n          new RegExp(`^${resolvedVirtualModuleId}$`),\n          new RegExp(`^${resolvedVirtualHtmlModuleId}$`),\n        ],\n      },\n      handler(id) {\n        if (id === resolvedVirtualModuleId) {\n          // Import and init all plugins\n          const imports = config.plugins\n            .map(\n              (plugin, i) =>\n                `import initPlugin${i} from '${normalizePath(plugin)}';`,\n            )\n            .join('\\n');\n          const initCalls = config.plugins\n            .map((_, i) => `  initPlugin${i}();`)\n            .join('\\n');\n          return `${imports}\\n\\nexport function initPlugins() {\\n${initCalls}\\n}`;\n        } else {\n          return `import { initPlugins } from '${virtualModuleId}';\n            try {\n              initPlugins();\n            } catch (err) {\n              console.error(\"[wxt] Failed to initialize plugins\", err);\n            }`;\n        }\n      },\n    },\n    transformIndexHtml: {\n      // Use \"pre\" so the new script is added before vite bundles all the scripts\n      order: 'pre',\n      handler(html, _ctx) {\n        const src =\n          config.command === 'serve'\n            ? `${config.dev.server?.origin}/@id/${virtualHtmlModuleId}`\n            : virtualHtmlModuleId;\n\n        const { document } = parseHTML(html);\n        const existing = document.querySelector(`script[src='${src}']`);\n        if (existing) return;\n\n        const script = document.createElement('script');\n        script.type = 'module';\n        script.src = src;\n\n        if (document.head == null) {\n          const newHead = document.createElement('head');\n          document.documentElement.prepend(newHead);\n        }\n\n        document.head?.prepend(script);\n        return document.toString();\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/clean.ts",
    "content": "import { rm } from 'node:fs/promises';\nimport path from 'node:path';\nimport pc from 'picocolors';\nimport { glob } from 'tinyglobby';\nimport { InlineConfig } from '../types';\nimport { registerWxt, wxt } from './wxt';\n\n/**\n * Remove generated/temp files from the directory.\n *\n * @example\n *   await clean();\n *\n * @param config Optional config that will override your `<root>/wxt.config.ts`.\n */\nexport async function clean(config?: InlineConfig): Promise<void>;\n/**\n * Remove generated/temp files from the directory.\n *\n * @deprecated\n * @example\n *   await clean();\n *\n * @param root The directory to look for generated/temp files in. Defaults to\n *   `process.cwd()`. Can be relative to `process.cwd()` or absolute.\n */\nexport async function clean(root?: string): Promise<void>;\n\nexport async function clean(config?: string | InlineConfig) {\n  if (typeof config === 'string') {\n    config = { root: config };\n  }\n\n  await registerWxt('build', config);\n  wxt.logger.info('Cleaning Project');\n\n  const root = wxt.config.root;\n\n  const tempDirs = [\n    'node_modules/.vite',\n    'node_modules/.cache',\n    '**/.wxt',\n    `${path.relative(root, wxt.config.outBaseDir)}/*`,\n  ];\n  wxt.logger.debug('Looking for:', tempDirs.map(pc.cyan).join(', '));\n  const directories = await glob(tempDirs, {\n    cwd: root,\n    absolute: true,\n    onlyDirectories: true,\n    deep: 2,\n    expandDirectories: false,\n  });\n  if (directories.length === 0) {\n    wxt.logger.debug('No generated files found.');\n    return;\n  }\n\n  wxt.logger.debug(\n    'Found:',\n    directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),\n  );\n  for (const directory of directories) {\n    await rm(directory, { force: true, recursive: true });\n    wxt.logger.debug('Deleted ' + pc.cyan(path.relative(root, directory)));\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/create-server.ts",
    "content": "import { debounce } from 'perfect-debounce';\nimport chokidar from 'chokidar';\nimport {\n  BuildStepOutput,\n  EntrypointGroup,\n  InlineConfig,\n  ServerInfo,\n  WxtDevServer,\n} from '../types';\nimport { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints';\nimport {\n  getContentScriptCssFiles,\n  getContentScriptsCssMap,\n} from './utils/manifest';\nimport {\n  internalBuild,\n  detectDevChanges,\n  rebuild,\n  findEntrypoints,\n} from './utils/building';\nimport { createExtensionRunner } from './runners';\nimport { Mutex } from 'async-mutex';\nimport pc from 'picocolors';\nimport { relative } from 'node:path';\nimport { deinitWxtModules, initWxtModules, registerWxt, wxt } from './wxt';\nimport { unnormalizePath } from './utils/paths';\nimport {\n  getContentScriptJs,\n  mapWxtOptionsToRegisteredContentScript,\n} from './utils/content-scripts';\nimport { createKeyboardShortcuts } from './keyboard-shortcuts';\nimport { isBabelSyntaxError, logBabelSyntaxError } from './utils/syntax-errors';\n\n/**\n * Creates a dev server and pre-builds all the files that need to exist before\n * loading the extension.\n *\n * @example\n *   const server = await wxt.createServer({\n *     // Enter config...\n *   });\n *   await server.start();\n */\nexport async function createServer(\n  inlineConfig?: InlineConfig,\n): Promise<WxtDevServer> {\n  await registerWxt('serve', inlineConfig);\n\n  wxt.server = await createServerInternal();\n  await wxt.hooks.callHook('server:created', wxt, wxt.server);\n  return wxt.server;\n}\n\nasync function createServerInternal(): Promise<WxtDevServer> {\n  const getServerInfo = (): ServerInfo => {\n    const { host, port, origin } = wxt.config.dev.server!;\n    return { host, port, origin };\n  };\n\n  let [runner, builderServer] = await Promise.all([\n    createExtensionRunner(),\n    wxt.builder.createServer(getServerInfo()),\n  ]);\n\n  // Used to track if modules need to be re-initialized\n  let wasStopped = false;\n\n  // Server instance must be created first so its reference can be added to the internal config used\n  // to pre-render entrypoints\n  const server: WxtDevServer = {\n    get host() {\n      return getServerInfo().host;\n    },\n    get port() {\n      return getServerInfo().port;\n    },\n    get origin() {\n      return getServerInfo().origin;\n    },\n    get watcher() {\n      return builderServer.watcher;\n    },\n    get ws() {\n      return builderServer.ws;\n    },\n    currentOutput: undefined,\n    async start() {\n      if (wasStopped) {\n        await wxt.reloadConfig();\n        runner = await createExtensionRunner();\n        builderServer = await wxt.builder.createServer(getServerInfo());\n        await initWxtModules();\n      }\n\n      await builderServer.listen();\n      const hostInfo =\n        server.host === 'localhost' ? '' : ` (listening on ${server.host})`;\n      wxt.logger.success(`Started dev server @ ${server.origin}${hostInfo}`);\n      await wxt.hooks.callHook('server:started', wxt, server);\n\n      // Register content scripts for the first time after the background starts\n      // up since they're not listed in the manifest.\n      // Add listener before opening the browser to guarantee it is present when\n      // the extension sends back the initialization message.\n      server.ws.on('wxt:background-initialized', () => {\n        if (server.currentOutput == null) return;\n        reloadContentScripts(server.currentOutput.steps, server);\n      });\n\n      await buildAndOpenBrowser();\n\n      // Listen for file changes and reload different parts of the extension accordingly\n      const reloadOnChange = createFileReloader(server);\n      server.watcher.on('all', async (...args) => {\n        await reloadOnChange(args[0], args[1]);\n\n        // Restart keyboard shortcuts after file is changed - for some reason they stop working.\n        keyboardShortcuts.start();\n      });\n\n      keyboardShortcuts.printHelp({\n        canReopenBrowser:\n          !wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.(),\n      });\n    },\n\n    async stop() {\n      wasStopped = true;\n      keyboardShortcuts.stop();\n      await runner.closeBrowser?.();\n      await builderServer.close();\n      await wxt.hooks.callHook('server:closed', wxt, server);\n\n      deinitWxtModules();\n      server.currentOutput = undefined;\n    },\n    async restart() {\n      await server.stop();\n      await server.start();\n    },\n    transformHtml(url, html, originalUrl) {\n      return builderServer.transformHtml(url, html, originalUrl);\n    },\n    reloadContentScript(payload) {\n      server.ws.send('wxt:reload-content-script', payload);\n    },\n    reloadPage(path) {\n      server.ws.send('wxt:reload-page', path);\n    },\n    reloadExtension() {\n      server.ws.send('wxt:reload-extension');\n    },\n    async restartBrowser() {\n      await runner.closeBrowser?.();\n      keyboardShortcuts.stop();\n      await wxt.reloadConfig();\n      runner = await createExtensionRunner();\n      await runner.openBrowser();\n      keyboardShortcuts.start();\n    },\n  };\n  const keyboardShortcuts = createKeyboardShortcuts(server);\n\n  const buildAndOpenBrowser = async () => {\n    try {\n      // Build after starting the dev server so it can be used to transform HTML files\n      server.currentOutput = await internalBuild();\n    } catch (err) {\n      if (!isBabelSyntaxError(err)) {\n        throw err;\n      }\n      logBabelSyntaxError(err);\n      wxt.logger.info('Waiting for syntax error to be fixed...');\n      await new Promise<void>((resolve) => {\n        const watcher = chokidar.watch(err.id, { ignoreInitial: true });\n        watcher.on('all', () => {\n          watcher.close();\n          wxt.logger.info('Syntax error resolved, rebuilding...');\n          resolve();\n        });\n      });\n      return buildAndOpenBrowser();\n    }\n\n    // Add file watchers for files not loaded by the dev server. See\n    // https://github.com/wxt-dev/wxt/issues/428#issuecomment-1944731870\n    try {\n      server.watcher.add(getExternalOutputDependencies(server));\n    } catch (err) {\n      wxt.config.logger.warn('Failed to register additional file paths:', err);\n    }\n\n    // Open browser after everything is ready to go.\n    await runner.openBrowser();\n  };\n\n  builderServer.on?.('close', () => keyboardShortcuts.stop());\n\n  return server;\n}\n\n/**\n * Returns a function responsible for reloading different parts of the extension\n * when a file changes.\n */\nfunction createFileReloader(server: WxtDevServer) {\n  const fileChangedMutex = new Mutex();\n  const changeQueue: Array<[string, string]> = [];\n\n  const cb = async (event: string, path: string) => {\n    changeQueue.push([event, path]);\n\n    const reloading = fileChangedMutex.runExclusive(async () => {\n      if (server.currentOutput == null) return;\n\n      const fileChanges = changeQueue\n        .splice(0, changeQueue.length)\n        .map(([_, file]) => file);\n      if (fileChanges.length === 0) return;\n\n      await wxt.reloadConfig();\n\n      const changes = detectDevChanges(fileChanges, server.currentOutput);\n      if (changes.type === 'no-change') return;\n\n      if (changes.type === 'full-restart') {\n        wxt.logger.info('Config changed, restarting server...');\n        server.restart();\n        return;\n      }\n\n      if (changes.type === 'browser-restart') {\n        wxt.logger.info('Runner config changed, restarting browser...');\n        server.restartBrowser();\n        return;\n      }\n\n      // Log the entrypoints that were effected\n      wxt.logger.info(\n        `Changed: ${Array.from(new Set(fileChanges))\n          .map((file) => pc.dim(relative(wxt.config.root, file)))\n          .join(', ')}`,\n      );\n\n      // Rebuild entrypoints on change\n      const allEntrypoints = await findEntrypoints();\n      try {\n        const { output: newOutput } = await rebuild(\n          allEntrypoints,\n          // TODO: this excludes new entrypoints, so they're not built until the dev command is restarted\n          changes.rebuildGroups,\n          changes.cachedOutput,\n        );\n        server.currentOutput = newOutput;\n\n        // Perform reloads\n        switch (changes.type) {\n          case 'extension-reload':\n            server.reloadExtension();\n            wxt.logger.success(`Reloaded extension`);\n            break;\n          case 'html-reload':\n            const { reloadedNames } = reloadHtmlPages(\n              changes.rebuildGroups,\n              server,\n            );\n            wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);\n            break;\n          case 'content-script-reload':\n            reloadContentScripts(changes.changedSteps, server);\n\n            const rebuiltNames = changes.rebuildGroups\n              .flat()\n              .map((entry) => entry.name);\n            wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);\n            break;\n        }\n      } catch {\n        // Catch build errors instead of crashing. Don't log error either, builder should have already logged it\n      }\n    });\n\n    await reloading.catch((error) => {\n      if (!isBabelSyntaxError(error)) {\n        throw error;\n      }\n      // Log syntax errors without crashing the server.\n      logBabelSyntaxError(error);\n    });\n  };\n\n  return debounce(cb, wxt.config.dev.server!.watchDebounce, {\n    leading: true,\n    trailing: false,\n  });\n}\n\n/**\n * From the server, tell the client to reload content scripts from the provided\n * build step outputs.\n */\nfunction reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {\n  if (wxt.config.manifestVersion === 3) {\n    steps.forEach((step) => {\n      if (server.currentOutput == null) return;\n\n      const entry = step.entrypoints;\n      if (Array.isArray(entry) || entry.type !== 'content-script') return;\n\n      const js = getContentScriptJs(wxt.config, entry);\n      const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);\n      const css = getContentScriptCssFiles([entry], cssMap);\n\n      server.reloadContentScript({\n        registration: entry.options.registration,\n        contentScript: mapWxtOptionsToRegisteredContentScript(\n          entry.options,\n          js,\n          css,\n        ),\n      });\n    });\n  } else {\n    server.reloadExtension();\n  }\n}\n\nfunction reloadHtmlPages(\n  groups: EntrypointGroup[],\n  server: WxtDevServer,\n): { reloadedNames: string[] } {\n  // groups might contain other files like background/content scripts, and we only care about the HTMl pages\n  const htmlEntries = groups.flat().filter(isHtmlEntrypoint);\n\n  htmlEntries.forEach((entry) => {\n    const path = getEntrypointBundlePath(entry, wxt.config.outDir, '.html');\n    server.reloadPage(path);\n  });\n\n  return {\n    reloadedNames: htmlEntries.map((entry) => entry.name),\n  };\n}\n\nfunction getFilenameList(names: string[]): string {\n  return names\n    .map((name) => {\n      return pc.cyan(name);\n    })\n    .join(pc.dim(', '));\n}\n\n/**\n * Based on the current build output, return a list of files that are:\n *\n * 1. Not in node_modules\n * 2. Not inside project root\n */\nfunction getExternalOutputDependencies(server: WxtDevServer) {\n  return (\n    server.currentOutput?.steps\n      .flatMap((step, i) => {\n        if (Array.isArray(step.entrypoints) && i === 0) {\n          // Dev server is already watching all HTML/esm files\n          return [];\n        }\n\n        return step.chunks.flatMap((chunk) => {\n          if (chunk.type === 'asset') return [];\n          return chunk.moduleIds;\n        });\n      })\n      .filter(\n        (file) => !file.includes('node_modules') && !file.startsWith('\\x00'),\n      )\n      .map(unnormalizePath)\n      .filter((file) => !file.startsWith(wxt.config.root)) ?? []\n  );\n}\n"
  },
  {
    "path": "packages/wxt/src/core/define-config.ts",
    "content": "import { UserConfig } from '../types';\n\nexport function defineConfig(config: UserConfig): UserConfig {\n  return config;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/define-web-ext-config.ts",
    "content": "import consola from 'consola';\nimport { WebExtConfig } from '../types';\n\n/** @deprecated Use `defineWebExtConfig` instead. Same function, different name. */\nexport function defineRunnerConfig(config: WebExtConfig): WebExtConfig {\n  consola.warn(\n    '`defineRunnerConfig` is deprecated, use `defineWebExtConfig` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0',\n  );\n  return defineWebExtConfig(config);\n}\n\n/**\n * Configure how [`web-ext`](https://github.com/mozilla/web-ext) starts the\n * browser during development.\n */\nexport function defineWebExtConfig(config: WebExtConfig): WebExtConfig {\n  return config;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/generate-wxt-dir.ts",
    "content": "import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '../types';\nimport { mkdir, readFile } from 'node:fs/promises';\nimport { dirname, relative, resolve } from 'node:path';\nimport { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints';\nimport { getEntrypointGlobals, getGlobals } from './utils/globals';\nimport { normalizePath } from './utils';\nimport path from 'node:path';\nimport { Message, parseI18nMessages } from './utils/i18n';\nimport { pathExists, writeFileIfDifferent, getPublicFiles } from './utils/fs';\nimport { wxt } from './wxt';\n\n/**\n * Generate and write all the files inside the `InternalConfig.typesDir`\n * directory.\n */\nexport async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> {\n  await mkdir(wxt.config.typesDir, { recursive: true });\n\n  const entries: WxtDirEntry[] = [\n    // Hard-coded entries\n    { module: 'wxt/vite-builder-env' },\n  ];\n\n  // Add references to modules installed from NPM to the TS project so their\n  // type augmentation can update InlineConfig correctly. Local modules defined\n  // in <root>/modules are already a part of the project, so we don't need to\n  // add them.\n  wxt.config.userModules.forEach((module) => {\n    if (module.type === 'node_module') entries.push({ module: module.id });\n  });\n\n  // browser.runtime.getURL\n  entries.push(await getPathsDeclarationEntry(entrypoints));\n\n  // browser.i18n.getMessage\n  entries.push(await getI18nDeclarationEntry());\n\n  // import.meta.env.*\n  entries.push(await getGlobalsDeclarationEntry());\n\n  // tsconfig.json\n  entries.push(await getTsConfigEntry());\n\n  // Let modules add more entries\n  await wxt.hooks.callHook('prepare:types', wxt, entries);\n\n  // Add main declaration file, not editable\n  entries.push(getMainDeclarationEntry(entries));\n\n  // Write all the files\n  const absoluteFileEntries = (\n    entries.filter((entry) => 'path' in entry) as WxtDirFileEntry[]\n  ).map<WxtDirFileEntry>((entry) => ({\n    ...entry,\n    path: resolve(wxt.config.wxtDir, entry.path),\n  }));\n\n  await Promise.all(\n    absoluteFileEntries.map(async (file) => {\n      await mkdir(dirname(file.path), { recursive: true });\n      await writeFileIfDifferent(file.path, file.text);\n    }),\n  );\n}\n\nasync function getPathsDeclarationEntry(\n  entrypoints: Entrypoint[],\n): Promise<WxtDirFileEntry> {\n  const paths = entrypoints\n    .map((entry) =>\n      getEntrypointBundlePath(\n        entry,\n        wxt.config.outDir,\n        getEntrypointPublicExt(entry),\n      ),\n    )\n    .concat(await getPublicFiles());\n\n  await wxt.hooks.callHook('prepare:publicPaths', wxt, paths);\n\n  const unions = [\n    `    | \"\"`,\n    `    | \"/\"`,\n    ...paths\n      .map(normalizePath)\n      .sort()\n      .map((path) => `    | \"/${path}\"`),\n  ].join('\\n');\n\n  const template = `// Generated by wxt\nimport \"wxt/browser\";\n\ndeclare module \"wxt/browser\" {\n  export type PublicPath =\n{{ union }}\n  type HtmlPublicPath = Extract<PublicPath, \\`\\${string}.html\\`>\n  export interface WxtRuntime {\n    getURL(path: PublicPath): string;\n    getURL(path: \\`\\${HtmlPublicPath}\\${string}\\`): string;\n  }\n}\n`;\n\n  return {\n    path: 'types/paths.d.ts',\n    text: template.replace('{{ union }}', unions || '    | never'),\n    tsReference: true,\n  };\n}\n\nfunction getEntrypointPublicExt(entry: Entrypoint): '.html' | '.js' | '.css' {\n  if (isHtmlEntrypoint(entry)) return '.html';\n\n  switch (entry.type) {\n    case 'content-script-style':\n    case 'unlisted-style':\n      return '.css';\n    default:\n      return '.js';\n  }\n}\n\nasync function getI18nDeclarationEntry(): Promise<WxtDirFileEntry> {\n  const defaultLocale = wxt.config.manifest.default_locale;\n  const template = `// Generated by wxt\nimport \"wxt/browser\";\n\ndeclare module \"wxt/browser\" {\n  /**\n   * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage\n   */\n  interface GetMessageOptions {\n    /**\n     * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage\n     */\n    escapeLt?: boolean\n  }\n\n  export interface WxtI18n extends I18n.Static {\n{{ overrides }}\n  }\n}\n`;\n\n  const defaultLocalePath = path.resolve(\n    wxt.config.publicDir,\n    '_locales',\n    defaultLocale ?? '',\n    'messages.json',\n  );\n  let messages: Message[];\n  if (await pathExists(defaultLocalePath)) {\n    const content = JSON.parse(await readFile(defaultLocalePath, 'utf-8'));\n    messages = parseI18nMessages(content);\n  } else {\n    messages = parseI18nMessages({});\n  }\n\n  const renderGetMessageOverload = (\n    keyType: string,\n    description?: string,\n    translation?: string,\n  ) => {\n    const commentLines: string[] = [];\n    if (description) commentLines.push(...description.split('\\n'));\n    if (translation) {\n      if (commentLines.length > 0) commentLines.push('');\n      commentLines.push(`\"${translation}\"`);\n    }\n    const comment =\n      commentLines.length > 0\n        ? `/**\\n${commentLines.map((line) => `     * ${line}`.trimEnd()).join('\\n')}\\n     */\\n    `\n        : '';\n    return `    ${comment}getMessage(\n      messageName: ${keyType},\n      substitutions?: string | string[],\n      options?: GetMessageOptions,\n    ): string;`;\n  };\n\n  const overrides = [\n    // Generate individual overloads for each message so JSDoc contains\n    // description and base translation.\n    ...messages.map((message) =>\n      renderGetMessageOverload(\n        `\"${message.name}\"`,\n        message.description,\n        message.message,\n      ),\n    ),\n    // Include a final union-based override so TS accepts valid string\n    // templates or concatenations.\n    // ie: browser.i18n.getMessage(`some_enum_${enumValue}`)\n    renderGetMessageOverload(\n      messages.map((message) => `\"${message.name}\"`).join(' | '),\n    ),\n  ];\n\n  return {\n    path: 'types/i18n.d.ts',\n    text: template.replace('{{ overrides }}', overrides.join('\\n')),\n    tsReference: true,\n  };\n}\n\nasync function getGlobalsDeclarationEntry(): Promise<WxtDirFileEntry> {\n  const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];\n  return {\n    path: 'types/globals.d.ts',\n    text: [\n      '// Generated by wxt',\n      'interface ImportMetaEnv {',\n      ...globals.map((global) => `  readonly ${global.name}: ${global.type};`),\n      '}',\n      'interface ImportMeta {',\n      '  readonly env: ImportMetaEnv',\n      '}',\n      '',\n    ].join('\\n'),\n    tsReference: true,\n  };\n}\n\nfunction getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {\n  const lines = ['// Generated by wxt'];\n  references.forEach((ref) => {\n    if ('module' in ref) {\n      return lines.push(`/// <reference types=\"${ref.module}\" />`);\n    } else if (ref.tsReference) {\n      const absolutePath = resolve(wxt.config.wxtDir, ref.path);\n      const relativePath = relative(wxt.config.wxtDir, absolutePath);\n      lines.push(`/// <reference path=\"./${normalizePath(relativePath)}\" />`);\n    }\n  });\n  return {\n    path: 'wxt.d.ts',\n    text: lines.join('\\n') + '\\n',\n  };\n}\n\nasync function getTsConfigEntry(): Promise<WxtDirFileEntry> {\n  const dir = wxt.config.wxtDir;\n  const getTsconfigPath = (path: string) => {\n    const res = normalizePath(relative(dir, path));\n    if (res.startsWith('.') || res.startsWith('/')) return res;\n    return './' + res;\n  };\n  const paths = Object.entries(wxt.config.alias)\n    .flatMap(([alias, absolutePath]) => {\n      const aliasPath = getTsconfigPath(absolutePath);\n      return [\n        `\"${alias}\": [\"${aliasPath}\"]`,\n        `\"${alias}/*\": [\"${aliasPath}/*\"]`,\n      ];\n    })\n    .map((line) => `      ${line}`)\n    .join(',\\n');\n\n  const text = `{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"paths\": {\n${paths}\n    }\n  },\n  \"include\": [\n    \"${getTsconfigPath(wxt.config.root)}/**/*\",\n    \"./wxt.d.ts\"\n  ],\n  \"exclude\": [\"${getTsconfigPath(wxt.config.outBaseDir)}\"]\n}`;\n\n  return {\n    path: 'tsconfig.json',\n    text,\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/index.ts",
    "content": "export * from './build';\nexport * from './clean';\nexport * from './define-config';\nexport * from './define-web-ext-config';\nexport * from './create-server';\nexport * from './initialize';\nexport * from './prepare';\nexport * from './zip';\nexport * from './utils';\n"
  },
  {
    "path": "packages/wxt/src/core/initialize.ts",
    "content": "import prompts from 'prompts';\nimport { consola } from 'consola';\nimport { downloadTemplate } from 'giget';\nimport { readdir, rename } from 'node:fs/promises';\nimport { pathExists } from './utils/fs';\nimport path from 'node:path';\nimport pc from 'picocolors';\nimport { Formatter } from 'picocolors/types';\n\nexport async function initialize(options: {\n  directory: string;\n  template: string;\n  packageManager: string;\n}) {\n  consola.info('Initializing new project');\n\n  const templates = await listTemplates();\n  const defaultTemplate = templates.find(\n    (template) => template.name === options.template?.toLowerCase().trim(),\n  );\n\n  const input = await prompts(\n    [\n      {\n        name: 'directory',\n        type: () => (options.directory == null ? 'text' : undefined),\n        message: 'Project Directory',\n        initial: options.directory,\n      },\n      {\n        name: 'template',\n        type: () => (defaultTemplate == null ? 'select' : undefined),\n        message: 'Choose a template',\n        choices: templates.map((template) => ({\n          title:\n            TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,\n          value: template,\n        })),\n      },\n      {\n        name: 'packageManager',\n        type: () => (options.packageManager == null ? 'select' : undefined),\n        message: 'Package Manager',\n        choices: [\n          { title: pc.red('npm'), value: 'npm' },\n          { title: pc.yellow('pnpm'), value: 'pnpm' },\n          { title: pc.cyan('yarn'), value: 'yarn' },\n          { title: pc.magenta('bun'), value: 'bun' },\n        ],\n      },\n    ],\n    {\n      onCancel: () => process.exit(1),\n    },\n  );\n  input.directory ??= options.directory;\n  input.template ??= defaultTemplate;\n  input.packageManager ??= options.packageManager;\n\n  const isExists = await pathExists(input.directory);\n  if (isExists) {\n    const isEmpty =\n      (await readdir(input.directory)).filter((dir) => dir !== '.git')\n        .length === 0;\n    if (!isEmpty) {\n      consola.error(\n        `The directory ${path.resolve(input.directory)} is not empty. Aborted.`,\n      );\n      process.exit(1);\n    }\n  }\n  await cloneProject(input);\n\n  const cdPath = path.relative(process.cwd(), path.resolve(input.directory));\n  console.log();\n  consola.log(\n    `✨ WXT project created with the ${\n      TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??\n      input.template.name\n    } template.`,\n  );\n  console.log();\n  consola.log('Next steps:');\n  let step = 0;\n  if (cdPath !== '') consola.log(`  ${++step}.`, pc.cyan(`cd ${cdPath}`));\n  consola.log(`  ${++step}.`, pc.cyan(`${input.packageManager} install`));\n  console.log();\n}\n\ninterface Template {\n  /** Template's name. */\n  name: string;\n  /** Path to template directory in github repo. */\n  path: string;\n}\n\nasync function listTemplates(): Promise<Template[]> {\n  const templates = await listTemplatesUngh().catch((err) => {\n    consola.debug('Failed to load templates via ungh:', err);\n    return listTemplatesGithub();\n  });\n  return templates.sort((l, r) => {\n    const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;\n    const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;\n    const diff = lWeight - rWeight;\n    if (diff !== 0) return diff;\n    return l.name.localeCompare(r.name);\n  });\n}\n\nasync function listTemplatesUngh(): Promise<Template[]> {\n  const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');\n  if (res.status !== 200)\n    throw Error(\n      `Request failed with status ${res.status} ${res.statusText}: ${await res.text()}`,\n    );\n\n  const data = (await res.json()) as {\n    meta: {\n      sha: string;\n    };\n    files: Array<{\n      path: string;\n      mode: string;\n      sha: string;\n      size: number;\n    }>;\n  };\n  return data.files\n    .map((item) => item.path.match(/templates\\/(.+)\\/package\\.json/)?.[1])\n    .filter((name) => name != null)\n    .map((name) => ({ name: name!, path: `templates/${name}` }));\n}\n\nasync function listTemplatesGithub(): Promise<Template[]> {\n  const res = await fetch(\n    `https://api.github.com/repos/${REPO}/contents/templates`,\n    { headers: { Accept: 'application/vnd.github+json' } },\n  );\n  if (res.status !== 200)\n    throw Error(\n      `Request failed with status ${res.status} ${res.statusText}: ${await res.text()}`,\n    );\n\n  // Schema is Example4 of https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content\n  return (await res.json()) as Array<{\n    name: string;\n    path: string;\n    sha: string;\n    size: number;\n  }>;\n}\n\nasync function cloneProject({\n  directory,\n  template,\n}: {\n  directory: string;\n  template: Template;\n}) {\n  const { createSpinner } = await import('nanospinner');\n  const spinner = createSpinner('Downloading template').start();\n  try {\n    // 1. Clone repo\n    await downloadTemplate(`gh:${REPO}/${template.path}`, {\n      dir: directory,\n      force: true,\n    });\n\n    // 2. Move _gitignore -> .gitignore\n    await rename(\n      path.join(directory, '_gitignore'),\n      path.join(directory, '.gitignore'),\n    ).catch((err) =>\n      consola.warn('Failed to move _gitignore to .gitignore:', err),\n    );\n\n    spinner.success();\n  } catch (err) {\n    spinner.error();\n    throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);\n  }\n}\n\nconst TEMPLATE_COLORS: Record<string, Formatter> = {\n  vanilla: pc.blue,\n  vue: pc.green,\n  react: pc.cyan,\n  svelte: pc.red,\n  solid: pc.blue,\n};\n\nconst TEMPLATE_SORT_WEIGHT: Record<string, number> = {\n  vanilla: 0,\n  vue: 1,\n  react: 2,\n};\n\nconst REPO = 'wxt-dev/wxt';\n"
  },
  {
    "path": "packages/wxt/src/core/keyboard-shortcuts.ts",
    "content": "import readline from 'node:readline';\nimport { WxtDevServer } from '../types';\nimport { wxt } from './wxt';\nimport pc from 'picocolors';\n\nexport interface KeyboardShortcutWatcher {\n  start(): void;\n  stop(): void;\n  printHelp(flags: { canReopenBrowser: boolean }): void;\n}\n\n/** Function that creates a keyboard shortcut handler for the extension. */\nexport function createKeyboardShortcuts(\n  server: WxtDevServer,\n): KeyboardShortcutWatcher {\n  let rl: readline.Interface | undefined;\n\n  const handleInput = (line: string) => {\n    // Only handle our specific command\n    if (line.trim() === 'o') {\n      server.restartBrowser();\n    }\n  };\n\n  return {\n    start() {\n      this.stop();\n\n      rl ??= readline.createInterface({\n        input: process.stdin,\n        terminal: false, // Don't intercept ctrl+C, ctrl+Z, etc\n      });\n\n      rl.on('line', handleInput);\n    },\n\n    stop() {\n      rl?.removeListener('line', handleInput);\n    },\n\n    printHelp(flags) {\n      if (flags.canReopenBrowser) {\n        wxt.logger.info(\n          `${pc.dim('Press')} ${pc.bold('o + enter')} ${pc.dim('to reopen the browser')}`,\n        );\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/bun.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport path from 'node:path';\nimport { bun } from '../bun';\n\ndescribe.skipIf(() => process.platform === 'win32')(\n  'Bun Package Management Utils',\n  () => {\n    describe('listDependencies', () => {\n      const cwd = path.resolve(__dirname, 'fixtures/simple-bun-project');\n\n      it('should list direct dependencies', async () => {\n        const actual = await bun.listDependencies({ cwd });\n        expect(actual).toEqual([\n          { name: 'flatten', version: '1.0.3' },\n          { name: 'mime-types', version: '2.1.35' },\n        ]);\n      });\n\n      it('should list all dependencies', async () => {\n        const actual = await bun.listDependencies({ cwd, all: true });\n        expect(actual).toEqual([\n          { name: 'flatten', version: '1.0.3' },\n          { name: 'mime-db', version: '1.52.0' },\n          { name: 'mime-types', version: '2.1.35' },\n        ]);\n      });\n    });\n  },\n);\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/fixtures/simple-bun-project/package.json",
    "content": "{\n  \"name\": \"bun-ls\",\n  \"dependencies\": {\n    \"mime-types\": \"2.1.35\"\n  },\n  \"devDependencies\": {\n    \"flatten\": \"1.0.3\"\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/fixtures/simple-npm-project/package.json",
    "content": "{\n  \"name\": \"npm-ls\",\n  \"dependencies\": {\n    \"mime-types\": \"2.1.35\"\n  },\n  \"devDependencies\": {\n    \"flatten\": \"1.0.3\"\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/fixtures/simple-pnpm-project/package.json",
    "content": "{\n  \"name\": \"pnpm-ls\",\n  \"dependencies\": {\n    \"mime-types\": \"2.1.35\"\n  },\n  \"devDependencies\": {\n    \"flatten\": \"1.0.3\"\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/fixtures/simple-yarn-project/package.json",
    "content": "{\n  \"name\": \"yarn-ls\",\n  \"packageManager\": \"yarn@1.22.22\",\n  \"dependencies\": {\n    \"mime-types\": \"2.1.35\"\n  },\n  \"devDependencies\": {\n    \"flatten\": \"1.0.3\"\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/npm.test.ts",
    "content": "import { beforeAll, describe, expect, it } from 'vitest';\nimport path from 'node:path';\nimport { npm } from '../npm';\nimport spawn from 'nano-spawn';\nimport { pathExists } from '../../utils/fs';\n\ndescribe('NPM Package Management Utils', () => {\n  describe('listDependencies', () => {\n    const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');\n    beforeAll(async () => {\n      // NPM needs the modules installed for 'npm ls' to work\n      await spawn('npm', ['i'], { cwd });\n    }, 60e3);\n\n    it('should list direct dependencies', async () => {\n      const actual = await npm.listDependencies({ cwd });\n      expect(actual).toEqual([\n        { name: 'flatten', version: '1.0.3' },\n        { name: 'mime-types', version: '2.1.35' },\n      ]);\n    });\n\n    it('should list all dependencies', async () => {\n      const actual = await npm.listDependencies({ cwd, all: true });\n      expect(actual).toEqual([\n        { name: 'flatten', version: '1.0.3' },\n        { name: 'mime-types', version: '2.1.35' },\n        { name: 'mime-db', version: '1.52.0' },\n      ]);\n    });\n  });\n\n  describe('downloadDependency', () => {\n    const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');\n\n    it('should download the dependency as a tarball', async () => {\n      const downloadDir = path.resolve(cwd, 'dist');\n      const id = 'mime-db@1.52.0';\n      const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz');\n\n      const actual = await npm.downloadDependency(id, downloadDir);\n\n      expect(actual).toEqual(expected);\n      expect(await pathExists(actual)).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/pnpm.test.ts",
    "content": "import { beforeAll, describe, expect, it } from 'vitest';\nimport path from 'node:path';\nimport { pnpm } from '../pnpm';\nimport spawn from 'nano-spawn';\n\nprocess.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';\n\ndescribe('PNPM Package Management Utils', () => {\n  describe('listDependencies', () => {\n    const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');\n    beforeAll(async () => {\n      // PNPM needs the modules installed, or 'pnpm ls' will return a blank list.\n      await spawn('pnpm', ['i', '--ignore-workspace'], { cwd });\n    });\n\n    it('should list direct dependencies', async () => {\n      const actual = await pnpm.listDependencies({ cwd });\n      expect(actual).toEqual([\n        { name: 'flatten', version: '1.0.3' },\n        { name: 'mime-types', version: '2.1.35' },\n      ]);\n    });\n\n    it('should list all dependencies', async () => {\n      const actual = await pnpm.listDependencies({ cwd, all: true });\n      expect(actual).toEqual([\n        { name: 'flatten', version: '1.0.3' },\n        { name: 'mime-types', version: '2.1.35' },\n        { name: 'mime-db', version: '1.52.0' },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/__tests__/yarn.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport path from 'node:path';\nimport { yarn } from '../yarn';\n\ndescribe('Yarn Package Management Utils', () => {\n  describe('listDependencies', () => {\n    const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project');\n\n    it('should list direct dependencies', async () => {\n      const actual = await yarn.listDependencies({ cwd });\n      expect(actual).toEqual([\n        { name: 'mime-db', version: '1.52.0' },\n        { name: 'flatten', version: '1.0.3' },\n        { name: 'mime-types', version: '2.1.35' },\n      ]);\n    });\n\n    it('should list all dependencies', async () => {\n      const actual = await yarn.listDependencies({ cwd, all: true });\n      expect(actual).toEqual([\n        { name: 'mime-db', version: '1.52.0' },\n        { name: 'flatten', version: '1.0.3' },\n        { name: 'mime-types', version: '2.1.35' },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/bun.ts",
    "content": "import { dedupeDependencies, npm } from './npm';\nimport { WxtPackageManagerImpl } from './types';\nimport spawn from 'nano-spawn';\n\nexport const bun: WxtPackageManagerImpl = {\n  overridesKey: 'overrides', // But also supports \"resolutions\"\n  downloadDependency(...args) {\n    return npm.downloadDependency(...args);\n  },\n  async listDependencies(options) {\n    const args = ['pm', 'ls'];\n    if (options?.all) {\n      args.push('--all');\n    }\n    const res = await spawn('bun', args, { cwd: options?.cwd });\n    return dedupeDependencies(\n      res.stdout\n        .split('\\n')\n        .slice(1) // Skip the first line, is not a dependency\n        .map((line) => line.trim())\n        .map((line) => /.* (@?\\S+)@(\\S+)$/.exec(line))\n        .filter((match) => !!match)\n        .map(([_, name, version]) => ({ name, version })),\n    );\n  },\n};\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/deno.ts",
    "content": "import { WxtPackageManagerImpl } from './types';\n\nexport const deno: WxtPackageManagerImpl = {\n  overridesKey: 'na',\n  downloadDependency() {\n    throw Error('Deno not supported');\n  },\n  listDependencies() {\n    throw Error('Deno not supported');\n  },\n};\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/index.ts",
    "content": "import {\n  detectPackageManager,\n  addDependency,\n  addDevDependency,\n  ensureDependencyInstalled,\n  installDependencies,\n  removeDependency,\n  PackageManager,\n  PackageManagerName,\n} from 'nypm';\nimport { WxtPackageManager } from '../../types';\nimport { bun } from './bun';\nimport { WxtPackageManagerImpl } from './types';\nimport { yarn } from './yarn';\nimport { pnpm } from './pnpm';\nimport { npm } from './npm';\nimport { deno } from './deno';\n\nexport async function createWxtPackageManager(\n  root: string,\n): Promise<WxtPackageManager> {\n  const pm = await detectPackageManager(root, {\n    includeParentDirs: true,\n  });\n\n  // Use requirePm to prevent throwing errors before the package manager utils are used.\n  const requirePm = <T>(cb: (pm: PackageManager) => T) => {\n    if (pm == null) throw Error('Could not detect package manager');\n    return cb(pm);\n  };\n\n  return {\n    get name() {\n      return requirePm((pm) => pm.name);\n    },\n    get command() {\n      return requirePm((pm) => pm.command);\n    },\n    get version() {\n      return requirePm((pm) => pm.version);\n    },\n    get majorVersion() {\n      return requirePm((pm) => pm.majorVersion);\n    },\n    get lockFile() {\n      return requirePm((pm) => pm.lockFile);\n    },\n    get files() {\n      return requirePm((pm) => pm.files);\n    },\n    addDependency,\n    addDevDependency,\n    ensureDependencyInstalled,\n    installDependencies,\n    removeDependency,\n    get overridesKey() {\n      return requirePm((pm) => packageManagers[pm.name].overridesKey);\n    },\n    downloadDependency(...args) {\n      return requirePm((pm) =>\n        packageManagers[pm.name].downloadDependency(...args),\n      );\n    },\n    listDependencies(...args) {\n      return requirePm((pm) =>\n        packageManagers[pm.name].listDependencies(...args),\n      );\n    },\n  };\n}\n\nconst packageManagers: Record<PackageManagerName, WxtPackageManagerImpl> = {\n  npm,\n  pnpm,\n  bun,\n  yarn,\n  deno,\n};\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/npm.ts",
    "content": "import { Dependency } from '../../types';\nimport { WxtPackageManagerImpl } from './types';\nimport path from 'node:path';\nimport { mkdir } from 'node:fs/promises';\nimport spawn from 'nano-spawn';\n\nexport const npm: WxtPackageManagerImpl = {\n  overridesKey: 'overrides',\n  async downloadDependency(id, downloadDir) {\n    await mkdir(downloadDir, { recursive: true });\n    const res = await spawn('npm', ['pack', id, '--json'], {\n      cwd: downloadDir,\n    });\n    const packed: PackedDependency[] = JSON.parse(res.stdout);\n    return path.resolve(downloadDir, packed[0].filename);\n  },\n  async listDependencies(options) {\n    const args = ['ls', '--json'];\n    if (options?.all) {\n      args.push('--depth', 'Infinity');\n    }\n    const res = await spawn('npm', args, { cwd: options?.cwd });\n    const project: NpmListProject = JSON.parse(res.stdout);\n\n    return flattenNpmListOutput([project]);\n  },\n};\n\nexport function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] {\n  const queue: Record<string, NpmListDependency>[] = projects.flatMap(\n    (project) => {\n      const acc: Record<string, NpmListDependency>[] = [];\n      if (project.dependencies) acc.push(project.dependencies);\n      if (project.devDependencies) acc.push(project.devDependencies);\n      return acc;\n    },\n  );\n  const dependencies: Dependency[] = [];\n  while (queue.length > 0) {\n    Object.entries(queue.pop()!).forEach(([name, meta]) => {\n      dependencies.push({\n        name,\n        version: meta.version,\n      });\n      if (meta.dependencies) queue.push(meta.dependencies);\n      if (meta.devDependencies) queue.push(meta.devDependencies);\n    });\n  }\n  return dedupeDependencies(dependencies);\n}\n\nexport function dedupeDependencies(dependencies: Dependency[]): Dependency[] {\n  const hashes = new Set<string>();\n  return dependencies.filter((dep) => {\n    const hash = `${dep.name}@${dep.version}`;\n    if (hashes.has(hash)) {\n      return false;\n    } else {\n      hashes.add(hash);\n      return true;\n    }\n  });\n}\n\nexport interface NpmListProject {\n  name: string;\n  dependencies?: Record<string, NpmListDependency>;\n  devDependencies?: Record<string, NpmListDependency>;\n}\n\nexport interface NpmListDependency {\n  version: string;\n  resolved?: string;\n  overridden?: boolean;\n  dependencies?: Record<string, NpmListDependency>;\n  devDependencies?: Record<string, NpmListDependency>;\n}\n\ninterface PackedDependency {\n  id: string;\n  name: string;\n  version: string;\n  filename: string;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/pnpm.ts",
    "content": "import { NpmListProject, flattenNpmListOutput, npm } from './npm';\nimport { WxtPackageManagerImpl } from './types';\nimport spawn from 'nano-spawn';\n\nexport const pnpm: WxtPackageManagerImpl = {\n  overridesKey: 'resolutions', // \"pnpm.overrides\" has a higher priority, but I don't want to deal with nesting\n  downloadDependency(...args) {\n    return npm.downloadDependency(...args);\n  },\n  async listDependencies(options) {\n    const args = ['ls', '-r', '--json'];\n    if (options?.all) {\n      args.push('--depth', 'Infinity');\n    }\n    // Helper for testing - since WXT uses pnpm workspaces, folders inside it don't behave like\n    // standalone projects unless you pass the --ignore-workspace flag.\n    if (\n      typeof process !== 'undefined' &&\n      process.env.WXT_PNPM_IGNORE_WORKSPACE === 'true'\n    ) {\n      args.push('--ignore-workspace');\n    }\n    const res = await spawn('pnpm', args, { cwd: options?.cwd });\n    const projects: NpmListProject[] = JSON.parse(res.stdout);\n\n    return flattenNpmListOutput(projects);\n  },\n};\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/types.ts",
    "content": "import { WxtPackageManager } from '../../types';\n\nexport type WxtPackageManagerImpl = Pick<\n  WxtPackageManager,\n  'downloadDependency' | 'listDependencies' | 'overridesKey'\n>;\n"
  },
  {
    "path": "packages/wxt/src/core/package-managers/yarn.ts",
    "content": "import { Dependency } from '../../types';\nimport { WxtPackageManagerImpl } from './types';\nimport { dedupeDependencies, npm } from './npm';\nimport spawn from 'nano-spawn';\n\nexport const yarn: WxtPackageManagerImpl = {\n  overridesKey: 'resolutions',\n  downloadDependency(...args) {\n    return npm.downloadDependency(...args);\n  },\n  async listDependencies(options) {\n    const args = ['list', '--json'];\n    if (options?.all) {\n      args.push('--depth', 'Infinity');\n    }\n    const res = await spawn('yarn', args, { cwd: options?.cwd });\n    const tree = res.stdout\n      .split('\\n')\n      .map<JsonLine>((line) => JSON.parse(line))\n      .find((line) => line.type === 'tree')?.data as JsonLineTree | undefined;\n    if (tree == null) throw Error(\"'yarn list --json' did not output a tree\");\n\n    const queue = [...tree.trees];\n    const dependencies: Dependency[] = [];\n\n    while (queue.length > 0) {\n      const { name: treeName, children } = queue.pop()!;\n      const match = /(@?\\S+)@(\\S+)$/.exec(treeName);\n      if (match) {\n        const [_, name, version] = match;\n        dependencies.push({ name, version });\n      }\n      if (children != null) {\n        queue.push(...children);\n      }\n    }\n\n    return dedupeDependencies(dependencies);\n  },\n};\n\ntype JsonLine =\n  | { type: unknown; data: unknown }\n  | { type: 'tree'; data: JsonLineTree };\n\ninterface JsonLineTree {\n  type: 'list';\n  trees: Tree[];\n}\n\ninterface Tree {\n  name: string;\n  children?: Tree[];\n}\n"
  },
  {
    "path": "packages/wxt/src/core/prepare.ts",
    "content": "import { InlineConfig } from '../types';\nimport { findEntrypoints } from './utils/building';\nimport { generateWxtDir } from './generate-wxt-dir';\nimport { registerWxt, wxt } from './wxt';\n\nexport async function prepare(config: InlineConfig) {\n  await registerWxt('build', config);\n  wxt.logger.info('Generating types...');\n\n  const entrypoints = await findEntrypoints();\n  await generateWxtDir(entrypoints);\n}\n"
  },
  {
    "path": "packages/wxt/src/core/resolve-config.ts",
    "content": "import { loadConfig } from 'c12';\nimport { resolve as esmResolve } from 'import-meta-resolve';\nimport {\n  InlineConfig,\n  ResolvedConfig,\n  UserConfig,\n  ConfigEnv,\n  UserManifestFn,\n  UserManifest,\n  WebExtConfig,\n  WxtResolvedUnimportOptions,\n  Logger,\n  WxtCommand,\n  WxtModule,\n  WxtModuleWithMetadata,\n  ResolvedEslintrc,\n} from '../types';\nimport path from 'node:path';\nimport { createFsCache } from './utils/cache';\nimport consola, { LogLevels } from 'consola';\nimport defu from 'defu';\nimport { NullablyRequired } from './utils/types';\nimport { pathExists } from './utils/fs';\nimport { normalizePath } from './utils';\nimport { glob } from 'tinyglobby';\nimport { builtinModules } from '../builtin-modules';\nimport { getEslintVersion } from './utils/eslint';\nimport { safeStringToNumber } from './utils/number';\nimport { loadEnv } from './utils/env';\nimport { getPort } from 'get-port-please';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\n\n/**\n * Given an inline config, discover the config file if necessary, merge the\n * results, resolve any relative paths, and apply any defaults.\n *\n * Inline config always has priority over user config. Cli flags are passed as\n * inline config if set. If unset, undefined is passed in, letting this function\n * decide default values.\n */\nexport async function resolveConfig(\n  inlineConfig: InlineConfig,\n  command: WxtCommand,\n): Promise<ResolvedConfig> {\n  // Load user config\n\n  let userConfig: UserConfig = {};\n  let userConfigMetadata: ResolvedConfig['userConfigMetadata'] | undefined;\n  if (inlineConfig.configFile !== false) {\n    const { config: loadedConfig, ...metadata } = await loadConfig<UserConfig>({\n      configFile: inlineConfig.configFile,\n      name: 'wxt',\n      cwd: inlineConfig.root ?? process.cwd(),\n    });\n    userConfig = loadedConfig ?? {};\n    userConfigMetadata = metadata;\n  }\n\n  // Merge it into the inline config\n\n  const mergedConfig = await mergeInlineConfig(inlineConfig, userConfig);\n\n  // Apply defaults to make internal config.\n\n  const debug = mergedConfig.debug ?? false;\n  const logger = mergedConfig.logger ?? consola;\n  if (debug) logger.level = LogLevels.debug;\n\n  const browser = mergedConfig.browser ?? 'chrome';\n  const targetBrowsers = mergedConfig.targetBrowsers ?? [];\n  if (targetBrowsers.length > 0 && !targetBrowsers.includes(browser)) {\n    throw new Error(\n      `Current target browser \\`${browser}\\` is not in your \\`targetBrowsers\\` list!`,\n    );\n  }\n  const manifestVersion =\n    mergedConfig.manifestVersion ??\n    (browser === 'firefox' || browser === 'safari' ? 2 : 3);\n  const mode = mergedConfig.mode ?? COMMAND_MODES[command];\n  const env: ConfigEnv = { browser, command, manifestVersion, mode };\n\n  loadEnv(mode, browser); // Load any environment variables used below\n\n  const root = path.resolve(\n    inlineConfig.root ?? userConfig.root ?? process.cwd(),\n  );\n  const wxtDir = path.resolve(root, '.wxt');\n  const wxtModuleDir = resolveWxtModuleDir();\n  const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);\n  const entrypointsDir = path.resolve(\n    srcDir,\n    mergedConfig.entrypointsDir ?? 'entrypoints',\n  );\n  if (await isDirMissing(entrypointsDir)) {\n    logMissingDir(logger, 'Entrypoints', entrypointsDir);\n  }\n  const modulesDir = path.resolve(root, mergedConfig.modulesDir ?? 'modules');\n  const filterEntrypoints = mergedConfig.filterEntrypoints?.length\n    ? new Set(mergedConfig.filterEntrypoints)\n    : undefined;\n  const publicDir = path.resolve(root, mergedConfig.publicDir ?? 'public');\n  const typesDir = path.resolve(wxtDir, 'types');\n  const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');\n  const modeSuffixes: Record<string, string | undefined> = {\n    production: '',\n    development: '-dev',\n  };\n  const modeSuffix = modeSuffixes[mode] ?? `-${mode}`;\n  const outDirTemplate = (\n    mergedConfig.outDirTemplate ??\n    `${browser}-mv${manifestVersion}${modeSuffix}`\n  )\n    // Resolve all variables in the template\n    .replaceAll('{{browser}}', browser)\n    .replaceAll('{{manifestVersion}}', manifestVersion.toString())\n    .replaceAll('{{modeSuffix}}', modeSuffix)\n    .replaceAll('{{mode}}', mode)\n    .replaceAll('{{command}}', command);\n\n  const outDir = path.resolve(outBaseDir, outDirTemplate);\n  const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';\n\n  if (inlineConfig.runner != null || userConfig.runner != null) {\n    logger.warn(\n      '`InlineConfig#runner` is deprecated, use `InlineConfig#webExt` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0',\n    );\n  }\n  const runnerConfig = await loadConfig<WebExtConfig>({\n    name: 'web-ext',\n    cwd: root,\n    globalRc: true,\n    rcFile: '.webextrc',\n    overrides: inlineConfig.webExt ?? inlineConfig.runner,\n    defaults: userConfig.webExt ?? userConfig.runner,\n  });\n  // Make sure alias are absolute\n  const alias = Object.fromEntries(\n    Object.entries({\n      ...mergedConfig.alias,\n      '@': srcDir,\n      '~': srcDir,\n      '@@': root,\n      '~~': root,\n    }).map(([key, value]) => [key, path.resolve(root, value)]),\n  );\n\n  let devServerConfig: ResolvedConfig['dev']['server'];\n  if (command === 'serve') {\n    if (mergedConfig.dev?.server?.hostname)\n      logger.warn(\n        `The 'hostname' option is deprecated, please use 'host' or 'origin' depending on your circumstances.`,\n      );\n\n    const host =\n      mergedConfig.dev?.server?.host ??\n      mergedConfig.dev?.server?.hostname ??\n      'localhost';\n    let port = mergedConfig.dev?.server?.port;\n    const origin =\n      mergedConfig.dev?.server?.origin ??\n      mergedConfig.dev?.server?.hostname ??\n      'localhost';\n    if (port == null || !isFinite(port)) {\n      port = await getPort({\n        // Passing host required for Mac, unsure of Windows/Linux\n        host,\n        port: 3000,\n        portRange: [3001, 3010],\n      });\n    }\n    const originWithProtocolAndPort = [\n      origin.match(/^https?:\\/\\//) ? '' : 'http://',\n      origin,\n      origin.match(/:[0-9]+$/) ? '' : `:${port}`,\n    ].join('');\n    devServerConfig = {\n      host,\n      port,\n      origin: originWithProtocolAndPort,\n      watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800,\n    };\n  }\n\n  const userModules = await resolveWxtUserModules(\n    root,\n    modulesDir,\n    mergedConfig.modules,\n  );\n  const moduleOptions = userModules.reduce<Record<string, any>>(\n    (map, module) => {\n      if (module.configKey) {\n        map[module.configKey] =\n          // @ts-expect-error\n          mergedConfig[module.configKey];\n      }\n      return map;\n    },\n    {},\n  );\n\n  return {\n    browser,\n    targetBrowsers,\n    command,\n    debug,\n    entrypointsDir,\n    modulesDir,\n    filterEntrypoints,\n    env,\n    fsCache: createFsCache(wxtDir),\n    imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),\n    logger,\n    manifest: await resolveManifestConfig(env, mergedConfig.manifest),\n    manifestVersion,\n    mode,\n    outBaseDir,\n    outDir,\n    publicDir,\n    wxtModuleDir,\n    root,\n    runnerConfig,\n    srcDir,\n    typesDir,\n    wxtDir,\n    zip: resolveZipConfig(root, browser, outBaseDir, mergedConfig),\n    analysis: resolveAnalysisConfig(root, mergedConfig),\n    userConfigMetadata: userConfigMetadata ?? {},\n    alias,\n    experimental: defu(mergedConfig.experimental, {}),\n    dev: {\n      server: devServerConfig,\n      reloadCommand,\n    },\n    hooks: mergedConfig.hooks ?? {},\n    vite: mergedConfig.vite ?? (() => ({})),\n    builtinModules,\n    userModules,\n    plugins: [],\n    ...moduleOptions,\n  };\n}\n\nasync function resolveManifestConfig(\n  env: ConfigEnv,\n  manifest: UserManifest | Promise<UserManifest> | UserManifestFn | undefined,\n): Promise<UserManifest> {\n  return typeof manifest === 'function'\n    ? await manifest(env)\n    : await (manifest ?? {});\n}\n\n/**\n * Merge the inline config and user config. Inline config is given priority.\n * Defaults are not applied here.\n */\nasync function mergeInlineConfig(\n  inlineConfig: InlineConfig,\n  userConfig: UserConfig,\n): Promise<InlineConfig> {\n  // Merge imports option\n  const imports: InlineConfig['imports'] =\n    inlineConfig.imports === false || userConfig.imports === false\n      ? false\n      : userConfig.imports == null && inlineConfig.imports == null\n        ? undefined\n        : defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});\n\n  // Merge manifest option\n  const manifest: UserManifestFn = async (env) => {\n    const user = await resolveManifestConfig(env, userConfig.manifest);\n    const inline = await resolveManifestConfig(env, inlineConfig.manifest);\n    return defu(inline, user);\n  };\n\n  const merged = defu(inlineConfig, userConfig);\n\n  // Builders\n  const builderConfig = await mergeBuilderConfig(\n    merged.logger ?? consola,\n    inlineConfig,\n    userConfig,\n  );\n\n  return {\n    ...merged,\n    // Custom merge values\n    imports,\n    manifest,\n    ...builderConfig,\n  };\n}\n\nfunction resolveZipConfig(\n  root: string,\n  browser: string,\n  outBaseDir: string,\n  mergedConfig: InlineConfig,\n): NullablyRequired<ResolvedConfig['zip']> {\n  const downloadedPackagesDir = path.resolve(root, '.wxt/local_modules');\n  return {\n    name: undefined,\n    sourcesTemplate: '{{name}}-{{version}}-sources.zip',\n    artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',\n    sourcesRoot: root,\n    includeSources: [],\n    compressionLevel: 9,\n    ...mergedConfig.zip,\n    zipSources:\n      mergedConfig.zip?.zipSources ?? ['firefox', 'opera'].includes(browser),\n    exclude: mergedConfig.zip?.exclude ?? [],\n    excludeSources: [\n      '**/node_modules',\n      // WXT files\n      '**/web-ext.config.ts',\n      // Hidden files\n      '**/.*',\n      // Tests\n      '**/__tests__/**',\n      '**/*.+(test|spec).?(c|m)+(j|t)s?(x)',\n      // Output directory\n      `${path.relative(root, outBaseDir)}/**`,\n      // From user\n      ...(mergedConfig.zip?.excludeSources ?? []),\n    ],\n    downloadPackages: mergedConfig.zip?.downloadPackages ?? [],\n    downloadedPackagesDir,\n  };\n}\n\nfunction resolveAnalysisConfig(\n  root: string,\n  mergedConfig: InlineConfig,\n): NullablyRequired<ResolvedConfig['analysis']> {\n  const analysisOutputFile = path.resolve(\n    root,\n    mergedConfig.analysis?.outputFile ?? 'stats.html',\n  );\n  const analysisOutputDir = path.dirname(analysisOutputFile);\n  const analysisOutputName = path.parse(analysisOutputFile).name;\n\n  return {\n    enabled: mergedConfig.analysis?.enabled ?? false,\n    open: mergedConfig.analysis?.open ?? false,\n    template: mergedConfig.analysis?.template ?? 'treemap',\n    outputFile: analysisOutputFile,\n    outputDir: analysisOutputDir,\n    outputName: analysisOutputName,\n    keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false,\n  };\n}\n\nasync function getUnimportOptions(\n  wxtDir: string,\n  srcDir: string,\n  logger: Logger,\n  config: InlineConfig,\n): Promise<WxtResolvedUnimportOptions> {\n  const disabled = config.imports === false;\n  const eslintrc = await getUnimportEslintOptions(wxtDir, config.imports);\n  // mlly sometimes picks up things as exports that aren't. That's what this array contains.\n  const invalidExports = ['options'];\n\n  const defineImportsAndTypes = (imports: string[], typeImports: string[]) => [\n    ...imports,\n    ...typeImports.map((name) => ({ name, type: true })),\n  ];\n\n  const defaultOptions: WxtResolvedUnimportOptions = {\n    imports: [{ name: 'fakeBrowser', from: 'wxt/testing' }],\n    presets: [\n      {\n        from: 'wxt/browser',\n        imports: defineImportsAndTypes(['browser'], ['Browser']),\n      },\n      {\n        from: 'wxt/utils/storage',\n        imports: defineImportsAndTypes(\n          ['storage'],\n          [\n            'StorageArea',\n            'WxtStorage',\n            'WxtStorageItem',\n            'StorageArea',\n            'StorageItemKey',\n            'StorageAreaChanges',\n            'MigrationError',\n          ],\n        ),\n      },\n      {\n        from: 'wxt/utils/app-config',\n        imports: defineImportsAndTypes(['getAppConfig', 'useAppConfig'], []),\n      },\n      {\n        from: 'wxt/utils/content-script-context',\n        imports: defineImportsAndTypes(\n          ['ContentScriptContext'],\n          ['WxtWindowEventMap'],\n        ),\n      },\n      {\n        from: 'wxt/utils/content-script-ui/iframe',\n        imports: defineImportsAndTypes(\n          ['createIframeUi'],\n          ['IframeContentScriptUi', 'IframeContentScriptUiOptions'],\n        ),\n        ignore: invalidExports,\n      },\n      {\n        from: 'wxt/utils/content-script-ui/integrated',\n        imports: defineImportsAndTypes(\n          ['createIntegratedUi'],\n          ['IntegratedContentScriptUi', 'IntegratedContentScriptUiOptions'],\n        ),\n        ignore: invalidExports,\n      },\n      {\n        from: 'wxt/utils/content-script-ui/shadow-root',\n        imports: defineImportsAndTypes(\n          ['createShadowRootUi'],\n          ['ShadowRootContentScriptUi', 'ShadowRootContentScriptUiOptions'],\n        ),\n        ignore: invalidExports,\n      },\n      {\n        from: 'wxt/utils/content-script-ui/types',\n        imports: defineImportsAndTypes(\n          [],\n          [\n            'ContentScriptUi',\n            'ContentScriptUiOptions',\n            'ContentScriptOverlayAlignment',\n            'ContentScriptAppendMode',\n            'ContentScriptInlinePositioningOptions',\n            'ContentScriptOverlayPositioningOptions',\n            'ContentScriptModalPositioningOptions',\n            'ContentScriptPositioningOptions',\n            'ContentScriptAnchoredOptions',\n            'AutoMountOptions',\n            'StopAutoMount',\n            'AutoMount',\n          ],\n        ),\n      },\n      {\n        from: 'wxt/utils/define-app-config',\n        imports: defineImportsAndTypes(['defineAppConfig'], ['WxtAppConfig']),\n      },\n      {\n        from: 'wxt/utils/define-background',\n        imports: defineImportsAndTypes(['defineBackground'], []),\n      },\n      {\n        from: 'wxt/utils/define-content-script',\n        imports: defineImportsAndTypes(['defineContentScript'], []),\n      },\n      {\n        from: 'wxt/utils/define-unlisted-script',\n        imports: defineImportsAndTypes(['defineUnlistedScript'], []),\n      },\n      {\n        from: 'wxt/utils/define-wxt-plugin',\n        imports: defineImportsAndTypes(['defineWxtPlugin'], []),\n      },\n      {\n        from: 'wxt/utils/inject-script',\n        imports: defineImportsAndTypes(\n          ['injectScript'],\n          ['ScriptPublicPath', 'InjectScriptOptions'],\n        ),\n        ignore: invalidExports,\n      },\n      {\n        from: 'wxt/utils/match-patterns',\n        imports: defineImportsAndTypes(\n          ['InvalidMatchPattern', 'MatchPattern'],\n          [],\n        ),\n      },\n    ],\n    virtualImports: ['#imports'],\n    debugLog: logger.debug,\n    warn: logger.warn,\n    dirsScanOptions: {\n      cwd: srcDir,\n    },\n    eslintrc,\n    dirs: disabled ? [] : ['components', 'composables', 'hooks', 'utils'],\n    disabled,\n  };\n\n  return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>(\n    config.imports ?? {},\n    defaultOptions,\n  );\n}\n\nasync function getUnimportEslintOptions(\n  wxtDir: string,\n  options: InlineConfig['imports'],\n): Promise<ResolvedEslintrc> {\n  const inlineEnabled =\n    options === false ? false : (options?.eslintrc?.enabled ?? 'auto');\n\n  let enabled: ResolvedEslintrc['enabled'];\n  switch (inlineEnabled) {\n    case 'auto':\n      const version = await getEslintVersion();\n      let major = parseInt(version[0]);\n      if (isNaN(major)) enabled = false;\n      if (major <= 8) enabled = 8;\n      else if (major >= 9) enabled = 9;\n      // NaN\n      else enabled = false;\n      break;\n    case true:\n      enabled = 8;\n      break;\n    default:\n      enabled = inlineEnabled;\n  }\n\n  return {\n    enabled,\n    filePath: path.resolve(\n      wxtDir,\n      enabled === 9 ? 'eslint-auto-imports.mjs' : 'eslintrc-auto-import.json',\n    ),\n    globalsPropValue: true,\n  };\n}\n\n/** Returns the path to `node_modules/wxt`. */\nfunction resolveWxtModuleDir() {\n  // TODO: Switch to import.meta.resolve() once the parent argument is unflagged\n  // (e.g. --experimental-import-meta-resolve) and all Node.js versions we support\n  // have it.\n  const url = esmResolve('wxt', import.meta.url);\n\n  // esmResolve() returns the \"wxt/dist/index.mjs\" file, not the package's root\n  // directory, which we want to return from this function.\n  return path.resolve(fileURLToPath(url), '../..');\n}\n\nasync function isDirMissing(dir: string) {\n  return !(await pathExists(dir));\n}\n\nfunction logMissingDir(logger: Logger, name: string, expected: string) {\n  logger.warn(\n    `${name} directory not found: ./${normalizePath(\n      path.relative(process.cwd(), expected),\n    )}`,\n  );\n}\n\n/** Map of `ConfigEnv` commands to their default modes. */\nconst COMMAND_MODES: Record<WxtCommand, string> = {\n  build: 'production',\n  serve: 'development',\n};\n\nexport async function mergeBuilderConfig(\n  logger: Logger,\n  inlineConfig: InlineConfig,\n  userConfig: UserConfig,\n): Promise<Pick<InlineConfig, 'vite'>> {\n  const vite = await import('vite').catch((err) => {\n    logger.debug('Failed to import vite:', err);\n  });\n  if (vite) {\n    return {\n      vite: async (env) => {\n        const [resolvedInlineConfig = {}, resolvedUserConfig = {}] =\n          await Promise.all([inlineConfig.vite?.(env), userConfig.vite?.(env)]);\n        return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig);\n      },\n    };\n  }\n\n  throw Error('Builder not found. Make sure vite is installed.');\n}\n\nexport async function resolveWxtUserModules(\n  root: string,\n  modulesDir: string,\n  modules: string[] = [],\n): Promise<WxtModuleWithMetadata<any>[]> {\n  const importer = pathToFileURL(path.join(root, 'index.js')).href;\n\n  // Resolve node_modules modules\n  const npmModules = await Promise.all<WxtModuleWithMetadata<any>>(\n    modules.map(async (moduleId) => {\n      // Resolve before importing to allow for a local WXT clone to be\n      // symlinked into a project.\n      const resolvedModulePath = esmResolve(moduleId, importer);\n      const mod: { default: WxtModule<any> } = await import(\n        /* @vite-ignore */ resolvedModulePath\n      );\n      if (mod.default == null) {\n        throw Error('Module missing default export: ' + moduleId);\n      }\n      return {\n        ...mod.default,\n        type: 'node_module',\n        id: moduleId,\n      };\n    }),\n  );\n\n  // Resolve local file paths\n  const localModulePaths = await glob(['*.[tj]s', '*/index.[tj]s'], {\n    cwd: modulesDir,\n    onlyFiles: true,\n    expandDirectories: false,\n  }).catch(() => []);\n  // Sort modules to ensure a consistent execution order\n  localModulePaths.sort();\n  const localModules = await Promise.all<WxtModuleWithMetadata<any>>(\n    localModulePaths.map(async (file) => {\n      const absolutePath = normalizePath(path.resolve(modulesDir, file));\n      const { config } = await loadConfig<WxtModule<any>>({\n        configFile: absolutePath,\n        globalRc: false,\n        rcFile: false,\n        packageJson: false,\n        envName: false,\n        dotenv: false,\n      });\n      if (config == null)\n        throw Error(\n          `No config found for ${file}. Did you forget to add a default export?`,\n        );\n      // Add name based on filename\n      config.name ??= file;\n      return {\n        ...config,\n        type: 'local',\n        id: absolutePath,\n      };\n    }),\n  );\n  return [...npmModules, ...localModules];\n}\n"
  },
  {
    "path": "packages/wxt/src/core/runners/__tests__/index.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport { createExtensionRunner } from '..';\nimport { setFakeWxt } from '../../utils/testing/fake-objects';\nimport { mock } from 'vitest-mock-extended';\nimport { createSafariRunner } from '../safari';\nimport { createWslRunner } from '../wsl';\nimport { createManualRunner } from '../manual';\nimport { isWsl } from '../../utils/wsl';\nimport { createWebExtRunner } from '../web-ext';\nimport { ExtensionRunner } from '../../../types';\n\nvi.mock('../../utils/wsl');\nconst isWslMock = vi.mocked(isWsl);\n\nvi.mock('../safari');\nconst createSafariRunnerMock = vi.mocked(createSafariRunner);\n\nvi.mock('../wsl');\nconst createWslRunnerMock = vi.mocked(createWslRunner);\n\nvi.mock('../manual');\nconst createManualRunnerMock = vi.mocked(createManualRunner);\n\nvi.mock('../web-ext');\nconst createWebExtRunnerMock = vi.mocked(createWebExtRunner);\n\ndescribe('createExtensionRunner', () => {\n  it('should return a Safari runner when browser is \"safari\"', async () => {\n    setFakeWxt({\n      config: {\n        browser: 'safari',\n      },\n    });\n    const safariRunner = mock<ExtensionRunner>();\n    createSafariRunnerMock.mockReturnValue(safariRunner);\n\n    await expect(createExtensionRunner()).resolves.toBe(safariRunner);\n  });\n\n  it('should return a WSL runner when `is-wsl` is true', async () => {\n    isWslMock.mockReturnValueOnce(true);\n    setFakeWxt({\n      config: {\n        browser: 'chrome',\n      },\n    });\n    const wslRunner = mock<ExtensionRunner>();\n    createWslRunnerMock.mockReturnValue(wslRunner);\n\n    await expect(createExtensionRunner()).resolves.toBe(wslRunner);\n  });\n\n  it('should return a manual runner when `runner.disabled` is true', async () => {\n    isWslMock.mockReturnValueOnce(false);\n    setFakeWxt({\n      config: {\n        browser: 'chrome',\n        runnerConfig: {\n          config: {\n            disabled: true,\n          },\n        },\n      },\n    });\n    const manualRunner = mock<ExtensionRunner>();\n    createManualRunnerMock.mockReturnValue(manualRunner);\n\n    await expect(createExtensionRunner()).resolves.toBe(manualRunner);\n  });\n\n  it('should return a web-ext runner otherwise', async () => {\n    setFakeWxt({\n      config: {\n        browser: 'chrome',\n        runnerConfig: {\n          config: {\n            disabled: undefined,\n          },\n        },\n      },\n    });\n    const manualRunner = mock<ExtensionRunner>();\n    createWebExtRunnerMock.mockReturnValue(manualRunner);\n\n    await expect(createExtensionRunner()).resolves.toBe(manualRunner);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/runners/index.ts",
    "content": "import type { ExtensionRunner } from '../../types';\nimport { isWsl } from '../utils/wsl';\nimport { wxt } from '../wxt';\nimport { createManualRunner } from './manual';\nimport { createSafariRunner } from './safari';\nimport { createWebExtRunner } from './web-ext';\nimport { createWslRunner } from './wsl';\n\nexport async function createExtensionRunner(): Promise<ExtensionRunner> {\n  if (wxt.config.browser === 'safari') return createSafariRunner();\n\n  if (isWsl()) return createWslRunner();\n  if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();\n\n  return createWebExtRunner();\n}\n"
  },
  {
    "path": "packages/wxt/src/core/runners/manual.ts",
    "content": "import { ExtensionRunner } from '../../types';\nimport { relative } from 'node:path';\nimport { wxt } from '../wxt';\n\n/** The manual runner tells the user to load the unpacked extension manually. */\nexport function createManualRunner(): ExtensionRunner {\n  return {\n    async openBrowser() {\n      wxt.logger.info(\n        `Load \"${relative(\n          process.cwd(),\n          wxt.config.outDir,\n        )}\" as an unpacked extension manually`,\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/runners/safari.ts",
    "content": "import { ExtensionRunner } from '../../types';\nimport { relative } from 'node:path';\nimport { wxt } from '../wxt';\n\n/**\n * The Safari runner just logs a warning message because `web-ext` doesn't work\n * with Safari.\n */\nexport function createSafariRunner(): ExtensionRunner {\n  return {\n    async openBrowser() {\n      wxt.logger.warn(\n        `Cannot Safari using web-ext. Load \"${relative(\n          process.cwd(),\n          wxt.config.outDir,\n        )}\" as an unpacked extension manually`,\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/runners/web-ext.ts",
    "content": "import type { WebExtRunInstance } from 'web-ext-run';\nimport { ExtensionRunner } from '../../types';\nimport { formatDuration } from '../utils/time';\nimport defu from 'defu';\nimport { wxt } from '../wxt';\n\n/** Create an `ExtensionRunner` backed by `web-ext`. */\nexport function createWebExtRunner(): ExtensionRunner {\n  let runner: WebExtRunInstance | undefined;\n\n  return {\n    canOpen() {\n      return true;\n    },\n    async openBrowser() {\n      const startTime = Date.now();\n\n      // Use WXT's logger instead of web-ext's built-in one.\n      const webExtLogger = await import('web-ext-run/util/logger');\n      webExtLogger.consoleStream.write = ({ level, msg, name }) => {\n        if (level >= ERROR_LOG_LEVEL) wxt.logger.error(name, msg);\n        if (level >= WARN_LOG_LEVEL) wxt.logger.warn(msg);\n      };\n\n      const wxtUserConfig = wxt.config.runnerConfig.config;\n      const userConfig = {\n        browserConsole: wxtUserConfig?.openConsole,\n        devtools: wxtUserConfig?.openDevtools,\n        startUrl: wxtUserConfig?.startUrls,\n        keepProfileChanges: wxtUserConfig?.keepProfileChanges,\n        chromiumPort: wxtUserConfig?.chromiumPort,\n        ...(wxt.config.browser === 'firefox'\n          ? {\n              firefox: wxtUserConfig?.binaries?.firefox,\n              firefoxProfile: wxtUserConfig?.firefoxProfile,\n              pref: wxtUserConfig?.firefoxPref,\n              args: wxtUserConfig?.firefoxArgs,\n            }\n          : {\n              chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],\n              chromiumProfile: wxtUserConfig?.chromiumProfile,\n              chromiumPref: defu(\n                wxtUserConfig?.chromiumPref,\n                DEFAULT_CHROMIUM_PREFS,\n              ),\n              args: [\n                '--unsafely-disable-devtools-self-xss-warnings',\n                ...(wxtUserConfig?.chromiumArgs ?? []),\n              ],\n            }),\n      };\n\n      const finalConfig = {\n        ...userConfig,\n        target:\n          wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',\n        sourceDir: wxt.config.outDir,\n        // Don't add a \"Reload Manager\" extension alongside dev extension, WXT\n        // already handles reloads intenrally.\n        noReloadManagerExtension: true,\n        // WXT handles reloads, so disable auto-reload behaviors in web-ext\n        noReload: true,\n        noInput: true,\n      };\n      const options = {\n        // Don't call `process.exit(0)` after starting web-ext\n        shouldExitProgram: false,\n      };\n      wxt.logger.debug('web-ext config:', finalConfig);\n      wxt.logger.debug('web-ext options:', options);\n\n      const webExt = await import('web-ext-run');\n      runner = await webExt.default.cmd.run(finalConfig, options);\n\n      const duration = Date.now() - startTime;\n      wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);\n    },\n\n    async closeBrowser() {\n      await runner?.exit();\n    },\n  };\n}\n\n// https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L12\nconst WARN_LOG_LEVEL = 40;\nconst ERROR_LOG_LEVEL = 50;\n\nconst DEFAULT_CHROMIUM_PREFS = {\n  devtools: {\n    synced_preferences_sync_disabled: {\n      // Remove content scripts from sourcemap debugger ignore list so stack traces\n      // and log locations show up properly, see:\n      // https://github.com/wxt-dev/wxt/issues/236#issuecomment-1915364520\n      skipContentScripts: false,\n      // Was renamed at some point, see:\n      // https://github.com/wxt-dev/wxt/issues/912#issuecomment-2284288171\n      'skip-content-scripts': false,\n    },\n  },\n};\n"
  },
  {
    "path": "packages/wxt/src/core/runners/wsl.ts",
    "content": "import { ExtensionRunner } from '../../types';\nimport { relative } from 'node:path';\nimport { wxt } from '../wxt';\n\n/**\n * The WSL runner just logs a warning message because `web-ext` doesn't work in\n * WSL.\n */\nexport function createWslRunner(): ExtensionRunner {\n  return {\n    async openBrowser() {\n      wxt.logger.warn(\n        `Cannot open browser when using WSL. Load \"${relative(\n          process.cwd(),\n          wxt.config.outDir,\n        )}\" as an unpacked extension manually`,\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/arrays.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { every, some } from '../arrays';\n\ndescribe('Array Utils', () => {\n  describe('every', () => {\n    it('should return true when the array is empty', () => {\n      expect(every([], () => false)).toBe(true);\n    });\n\n    it(\"should return true when all item predicate's return true\", () => {\n      expect(every([1, 1, 1], (item) => item === 1)).toBe(true);\n    });\n\n    it(\"should return false when a single item predicate's return false\", () => {\n      expect(every([1, 2, 1], (item) => item === 1)).toBe(false);\n    });\n  });\n\n  describe('some', () => {\n    it('should return true if one value returns true', () => {\n      const array = [1, 2, 3];\n      const predicate = (item: number) => item === 2;\n\n      expect(some(array, predicate)).toBe(true);\n    });\n\n    it('should return false if no values match', () => {\n      const array = [1, 2, 3];\n      const predicate = (item: number) => item === 4;\n\n      expect(some(array, predicate)).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/content-scripts.test.ts",
    "content": "import { describe, expect, it, beforeEach } from 'vitest';\nimport { hashContentScriptOptions } from '../content-scripts';\nimport { setFakeWxt } from '../testing/fake-objects';\n\ndescribe('Content Script Utils', () => {\n  beforeEach(() => {\n    setFakeWxt();\n  });\n\n  describe('hashContentScriptOptions', () => {\n    it('should return a string containing all the options with defaults applied', () => {\n      const hash = hashContentScriptOptions({ matches: [] });\n\n      expect(hash).toEqual(\n        '[[\"all_frames\",false],[\"exclude_globs\",[]],[\"exclude_matches\",[]],[\"include_globs\",[]],[\"match_about_blank\",false],[\"match_origin_as_fallback\",false],[\"matches\",[]],[\"run_at\",\"document_idle\"],[\"world\",\"ISOLATED\"]]',\n      );\n    });\n\n    it('should be consistent regardless of the object ordering and default values', () => {\n      const hash1 = hashContentScriptOptions({\n        allFrames: true,\n        matches: ['*://google.com/*', '*://duckduckgo.com/*'],\n        matchAboutBlank: false,\n      });\n      const hash2 = hashContentScriptOptions({\n        matches: ['*://duckduckgo.com/*', '*://google.com/*'],\n        allFrames: true,\n      });\n\n      expect(hash1).toBe(hash2);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/content-security-policy.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { ContentSecurityPolicy } from '../content-security-policy';\n\ndescribe('Content Security Policy Builder', () => {\n  it('should add values to new directives correctly', () => {\n    const csp = new ContentSecurityPolicy();\n\n    csp.add('default-src', \"'self'\");\n\n    expect(csp.toString()).toEqual(\"default-src 'self';\");\n  });\n\n  it('should add to existing values', () => {\n    const csp = new ContentSecurityPolicy(\"default-src 'self';\");\n\n    csp.add('default-src', 'http://localhost:*');\n\n    expect(csp.toString()).toEqual(\"default-src 'self' http://localhost:*;\");\n  });\n\n  it('should not add duplicates', () => {\n    const csp = new ContentSecurityPolicy(\"default-src 'self';\");\n\n    csp.add('default-src', \"'self'\");\n\n    expect(csp.toString()).toEqual(\"default-src 'self';\");\n  });\n\n  it('should sort the directives in the correct order', () => {\n    const csp = new ContentSecurityPolicy();\n\n    csp.add('object-src', \"'self'\");\n    csp.add('script-src', \"'self'\");\n    csp.add('default-src', \"'self'\");\n\n    expect(csp.toString()).toEqual(\n      \"default-src 'self'; script-src 'self'; object-src 'self';\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/entrypoints.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport {\n  getEntrypointName,\n  getEntrypointOutputFile,\n  resolvePerBrowserOption,\n} from '../entrypoints';\nimport { Entrypoint } from '../../../types';\nimport { resolve } from 'path';\n\ndescribe('Entrypoint Utils', () => {\n  describe('getEntrypointName', () => {\n    const entrypointsDir = '/entrypoints';\n\n    it.each<[string, string]>([\n      [resolve(entrypointsDir, 'popup.html'), 'popup'],\n      [resolve(entrypointsDir, 'options/index.html'), 'options'],\n      [resolve(entrypointsDir, 'example.sandbox/index.html'), 'example'],\n      [resolve(entrypointsDir, 'some.content/index.ts'), 'some'],\n      [resolve(entrypointsDir, 'overlay.content.ts'), 'overlay'],\n    ])('should convert %s to %s', (inputPath, expected) => {\n      const actual = getEntrypointName(entrypointsDir, inputPath);\n      expect(actual).toBe(expected);\n    });\n  });\n\n  describe('getEntrypointOutputFile', () => {\n    const outDir = '/.output';\n    it.each<{ expected: string; name: string; ext: string; outputDir: string }>(\n      [\n        {\n          name: 'popup',\n          ext: '.html',\n          outputDir: outDir,\n          expected: resolve(outDir, 'popup.html'),\n        },\n        {\n          name: 'overlay',\n          ext: '.ts',\n          outputDir: resolve(outDir, 'content-scripts'),\n          expected: resolve(outDir, 'content-scripts', 'overlay.ts'),\n        },\n      ],\n    )('should return %s', ({ name, ext, expected, outputDir }) => {\n      const entrypoint: Entrypoint = {\n        type: 'unlisted-page',\n        inputPath: '...',\n        name,\n        outputDir,\n        options: {},\n        skipped: false,\n      };\n\n      const actual = getEntrypointOutputFile(entrypoint, ext);\n      expect(actual).toBe(expected);\n    });\n  });\n\n  describe('resolvePerBrowserOption', () => {\n    it('should return the value directly', () => {\n      expect(resolvePerBrowserOption('some-string', '')).toEqual('some-string');\n      expect(resolvePerBrowserOption(false, '')).toEqual(false);\n      expect(resolvePerBrowserOption([1], '')).toEqual([1]);\n      expect(resolvePerBrowserOption(['string'], '')).toEqual(['string']);\n    });\n\n    it('should return the value for the specific browser', () => {\n      expect(resolvePerBrowserOption({ a: 'one', b: 'two' }, 'a')).toEqual(\n        'one',\n      );\n      expect(resolvePerBrowserOption({ c: ['one'], d: ['two'] }, 'c')).toEqual([\n        'one',\n      ]);\n      expect(resolvePerBrowserOption({ c: false, d: true }, 'e')).toEqual(\n        undefined,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/manifest.test.ts",
    "content": "import { beforeEach, describe, expect, it } from 'vitest';\nimport { generateManifest, stripPathFromMatchPattern } from '../manifest';\nimport {\n  fakeArray,\n  fakeBackgroundEntrypoint,\n  fakeBuildOutput,\n  fakeContentScriptEntrypoint,\n  fakeEntrypoint,\n  fakeManifestCommand,\n  fakeOptionsEntrypoint,\n  fakePopupEntrypoint,\n  fakeSidepanelEntrypoint,\n  fakeWxtDevServer,\n  setFakeWxt,\n} from '../testing/fake-objects';\nimport {\n  BuildOutput,\n  ContentScriptEntrypoint,\n  Entrypoint,\n  OutputAsset,\n} from '../../../types';\nimport { wxt } from '../../wxt';\nimport { mock } from 'vitest-mock-extended';\nimport type { Browser } from '@wxt-dev/browser';\n\nconst outDir = '/output';\nconst contentScriptOutDir = '/output/content-scripts';\n\ndescribe('Manifest Utils', () => {\n  beforeEach(() => {\n    setFakeWxt();\n  });\n\n  describe('generateManifest', () => {\n    describe('popup', () => {\n      type ActionType = 'browser_action' | 'page_action';\n      const popupEntrypoint = (type?: ActionType) =>\n        fakePopupEntrypoint({\n          options: {\n            // @ts-expect-error: Force this to be undefined instead of inheriting the random value\n            mv2Key: type ?? null,\n            defaultIcon: {\n              '16': '/icon/16.png',\n            },\n            defaultTitle: 'Default Title',\n          },\n          outputDir: outDir,\n          skipped: false,\n        });\n\n      it('should include an action for mv3', async () => {\n        const popup = popupEntrypoint();\n        const buildOutput = fakeBuildOutput();\n\n        setFakeWxt({\n          config: {\n            manifestVersion: 3,\n            outDir,\n          },\n        });\n        const expected: Partial<Browser.runtime.Manifest> = {\n          action: {\n            default_icon: popup.options.defaultIcon,\n            default_title: popup.options.defaultTitle,\n            default_popup: 'popup.html',\n          },\n        };\n\n        const { manifest: actual } = await generateManifest(\n          [popup],\n          buildOutput,\n        );\n\n        expect(actual).toMatchObject(expected);\n      });\n\n      it.each<{\n        inputType: ActionType | undefined;\n        expectedType: ActionType;\n      }>([\n        { inputType: undefined, expectedType: 'browser_action' },\n        { inputType: 'browser_action', expectedType: 'browser_action' },\n        { inputType: 'page_action', expectedType: 'page_action' },\n      ])(\n        'should use the correct action for mv2: %j',\n        async ({ inputType, expectedType }) => {\n          const popup = popupEntrypoint(inputType);\n          const buildOutput = fakeBuildOutput();\n          setFakeWxt({\n            config: {\n              manifestVersion: 2,\n              outDir,\n            },\n          });\n          const expected = {\n            default_icon: popup.options.defaultIcon,\n            default_title: popup.options.defaultTitle,\n            default_popup: 'popup.html',\n          };\n\n          const { manifest: actual } = await generateManifest(\n            [popup],\n            buildOutput,\n          );\n\n          expect(actual[expectedType]).toEqual(expected);\n        },\n      );\n\n      it('should include default_area for Firefox in mv3', async () => {\n        const popup = fakePopupEntrypoint({\n          options: {\n            // @ts-expect-error: Force this to be undefined\n            mv2Key: null,\n            defaultArea: 'navbar',\n          },\n          outputDir: outDir,\n          skipped: false,\n        });\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            manifestVersion: 3,\n            outDir,\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          [popup],\n          buildOutput,\n        );\n\n        expect((actual.action as any).default_area).toBe('navbar');\n      });\n\n      it('should include theme_icons for Firefox in mv3', async () => {\n        const themeIcons = [\n          { light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },\n          { light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 },\n        ];\n        const popup = fakePopupEntrypoint({\n          options: {\n            // @ts-expect-error: Force this to be undefined\n            mv2Key: null,\n            themeIcons,\n          },\n          outputDir: outDir,\n          skipped: false,\n        });\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            manifestVersion: 3,\n            outDir,\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          [popup],\n          buildOutput,\n        );\n\n        expect((actual.action as any).theme_icons).toEqual(themeIcons);\n      });\n\n      it('should include default_area for Firefox in mv2', async () => {\n        const popup = fakePopupEntrypoint({\n          options: {\n            // @ts-expect-error: Force this to be undefined\n            mv2Key: null,\n            defaultArea: 'menupanel',\n          },\n          outputDir: outDir,\n          skipped: false,\n        });\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            manifestVersion: 2,\n            outDir,\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          [popup],\n          buildOutput,\n        );\n\n        expect((actual.browser_action as any).default_area).toBe('menupanel');\n      });\n    });\n\n    describe('action without popup', () => {\n      it('should respect the action field in the manifest without a popup', async () => {\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            outDir,\n            manifestVersion: 3,\n            manifest: {\n              action: {\n                default_icon: 'icon-16.png',\n                default_title: 'Example title',\n              },\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest([], buildOutput);\n\n        expect(actual.action).toEqual(wxt.config.manifest.action);\n        expect(actual.browser_action).toBeUndefined();\n        expect(actual.page_action).toBeUndefined();\n      });\n\n      it('should generate `browser_action` for MV2 when only `action` is defined', async () => {\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            outDir,\n            manifestVersion: 2,\n            manifest: {\n              action: {\n                default_title: 'Action',\n              },\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest([], buildOutput);\n\n        expect(actual.action).toBeUndefined();\n        expect(actual.browser_action).toEqual(wxt.config.manifest.action);\n        expect(actual.page_action).toBeUndefined();\n      });\n\n      it('should keep the `page_action` for MV2 when both `action` and `page_action` are defined', async () => {\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            outDir,\n            manifestVersion: 2,\n            manifest: {\n              action: {\n                default_title: 'Action',\n              },\n              page_action: {\n                default_title: 'Page Action',\n              },\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest([], buildOutput);\n\n        expect(actual.action).toBeUndefined();\n        expect(actual.browser_action).toBeUndefined();\n        expect(actual.page_action).toEqual(wxt.config.manifest.page_action);\n      });\n\n      it('should keep the custom `browser_action` for MV2 when both `action` and `browser_action` are defined', async () => {\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            outDir,\n            manifestVersion: 2,\n            manifest: {\n              action: {\n                default_title: 'Action',\n              },\n              browser_action: {\n                default_title: 'Browser Action',\n              },\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest([], buildOutput);\n\n        expect(actual.action).toBeUndefined();\n        expect(actual.browser_action).toEqual(\n          wxt.config.manifest.browser_action,\n        );\n        expect(actual.page_action).toBeUndefined();\n      });\n    });\n\n    describe('options', () => {\n      const options = fakeOptionsEntrypoint({\n        outputDir: outDir,\n        options: {\n          openInTab: false,\n          chromeStyle: true,\n          browserStyle: true,\n        },\n        skipped: false,\n      });\n\n      it('should include a options_ui and chrome_style for chrome', async () => {\n        setFakeWxt({\n          config: {\n            manifestVersion: 3,\n            outDir,\n            browser: 'chrome',\n          },\n        });\n        const buildOutput = fakeBuildOutput();\n        const expected = {\n          open_in_tab: false,\n          chrome_style: true,\n          page: 'options.html',\n        };\n\n        const { manifest: actual } = await generateManifest(\n          [options],\n          buildOutput,\n        );\n\n        expect(actual.options_ui).toEqual(expected);\n      });\n\n      it('should include a options_ui and browser_style for firefox', async () => {\n        setFakeWxt({\n          config: {\n            manifestVersion: 3,\n            browser: 'firefox',\n            outDir,\n          },\n        });\n        const buildOutput = fakeBuildOutput();\n        const expected = {\n          open_in_tab: false,\n          browser_style: true,\n          page: 'options.html',\n        };\n\n        const { manifest: actual } = await generateManifest(\n          [options],\n          buildOutput,\n        );\n\n        expect(actual.options_ui).toEqual(expected);\n      });\n    });\n\n    describe('background', () => {\n      const background = fakeBackgroundEntrypoint({\n        outputDir: outDir,\n        options: {\n          persistent: true,\n          type: 'module',\n        },\n        skipped: false,\n      });\n\n      describe('MV3', () => {\n        it.each(['chrome', 'safari'])(\n          'should include a service worker and type for %s',\n          async (browser) => {\n            setFakeWxt({\n              config: {\n                outDir,\n                manifestVersion: 3,\n                browser,\n              },\n            });\n            const buildOutput = fakeBuildOutput();\n            const expected = {\n              type: 'module',\n              service_worker: 'background.js',\n            };\n\n            const { manifest: actual } = await generateManifest(\n              [background],\n              buildOutput,\n            );\n\n            expect(actual.background).toEqual(expected);\n          },\n        );\n\n        it('should include a background script and type for firefox', async () => {\n          setFakeWxt({\n            config: {\n              outDir,\n              manifestVersion: 3,\n              browser: 'firefox',\n            },\n          });\n          const buildOutput = fakeBuildOutput();\n          const expected = {\n            type: 'module',\n            scripts: ['background.js'],\n          };\n\n          const { manifest: actual } = await generateManifest(\n            [background],\n            buildOutput,\n          );\n\n          expect(actual.background).toEqual(expected);\n        });\n      });\n\n      describe('MV2', () => {\n        it.each(['chrome', 'safari'])(\n          'should include scripts and persistent for %s',\n          async (browser) => {\n            setFakeWxt({\n              config: {\n                outDir,\n                manifestVersion: 2,\n                browser,\n              },\n            });\n            const buildOutput = fakeBuildOutput();\n            const expected = {\n              persistent: true,\n              scripts: ['background.js'],\n            };\n\n            const { manifest: actual } = await generateManifest(\n              [background],\n              buildOutput,\n            );\n\n            expect(actual.background).toEqual(expected);\n          },\n        );\n\n        it('should include a background script and persistent for firefox mv2', async () => {\n          setFakeWxt({\n            config: {\n              outDir,\n              manifestVersion: 2,\n              browser: 'firefox',\n            },\n          });\n          const buildOutput = fakeBuildOutput();\n          const expected = {\n            persistent: true,\n            scripts: ['background.js'],\n          };\n\n          const { manifest: actual } = await generateManifest(\n            [background],\n            buildOutput,\n          );\n\n          expect(actual.background).toEqual(expected);\n        });\n      });\n    });\n\n    describe('icons', () => {\n      it('should auto-discover icons with the correct name', async () => {\n        const entrypoints = fakeArray(() => fakeEntrypoint({ skipped: false }));\n        const buildOutput = fakeBuildOutput({\n          publicAssets: [\n            { type: 'asset', fileName: 'icon-16.png' },\n            { type: 'asset', fileName: 'icon/32.png' },\n            { type: 'asset', fileName: 'icon@48w.png' },\n            { type: 'asset', fileName: 'icon-64x64.png' },\n            { type: 'asset', fileName: 'icon@96.png' },\n            { type: 'asset', fileName: 'icons/128x128.png' },\n          ],\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.icons).toEqual({\n          16: 'icon-16.png',\n          32: 'icon/32.png',\n          48: 'icon@48w.png',\n          64: 'icon-64x64.png',\n          96: 'icon@96.png',\n          128: 'icons/128x128.png',\n        });\n      });\n\n      it('should return undefined when no icons are found', async () => {\n        const entrypoints = fakeArray(() => fakeEntrypoint({ skipped: false }));\n        const buildOutput = fakeBuildOutput({\n          publicAssets: [\n            { type: 'asset', fileName: 'logo.png' },\n            { type: 'asset', fileName: 'icon-16.jpeg' },\n          ],\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.icons).toBeUndefined();\n      });\n\n      it('should allow icons to be overwritten from the wxt.config.ts file', async () => {\n        const entrypoints = fakeArray(() => fakeEntrypoint({ skipped: false }));\n        const buildOutput = fakeBuildOutput({\n          publicAssets: [\n            { type: 'asset', fileName: 'icon-16.png' },\n            { type: 'asset', fileName: 'icon-32.png' },\n            { type: 'asset', fileName: 'logo-16.png' },\n            { type: 'asset', fileName: 'logo-32.png' },\n            { type: 'asset', fileName: 'logo-48.png' },\n          ],\n        });\n        const expected = {\n          16: 'logo-16.png',\n          32: 'logo-32.png',\n          48: 'logo-48.png',\n        };\n        setFakeWxt({\n          config: {\n            manifest: {\n              icons: expected,\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.icons).toEqual(expected);\n      });\n    });\n\n    describe('content_scripts', () => {\n      it('should group content scripts and styles together based on their manifest properties', async () => {\n        const cs1: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'one',\n          inputPath: 'entrypoints/one.content/index.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n          },\n          skipped: false,\n        };\n        const cs1Styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/one.css',\n        };\n        const cs2: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'two',\n          inputPath: 'entrypoints/two.content/index.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n            runAt: 'document_end',\n          },\n          skipped: false,\n        };\n        const cs2Styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/two.css',\n        };\n        const cs3: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'three',\n          inputPath: 'entrypoints/three.content/index.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n            runAt: 'document_end',\n          },\n          skipped: false,\n        };\n        const cs3Styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/three.css',\n        };\n        const cs4: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'four',\n          inputPath: 'entrypoints/four.content/index.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://duckduckgo.com/*'],\n            runAt: 'document_end',\n          },\n          skipped: false,\n        };\n        const cs4Styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/four.css',\n        };\n        const cs5: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'five',\n          inputPath: 'entrypoints/five.content/index.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n            world: 'MAIN',\n          },\n          skipped: false,\n        };\n        const cs5Styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/five.css',\n        };\n\n        const entrypoints = [cs1, cs2, cs3, cs4, cs5];\n        setFakeWxt({\n          config: {\n            command: 'build',\n            outDir,\n            manifestVersion: 3,\n          },\n        });\n        const buildOutput: Omit<BuildOutput, 'manifest'> = {\n          publicAssets: [],\n          steps: [\n            { entrypoints: cs1, chunks: [cs1Styles] },\n            { entrypoints: cs2, chunks: [cs2Styles] },\n            { entrypoints: cs3, chunks: [cs3Styles] },\n            { entrypoints: cs4, chunks: [cs4Styles] },\n            { entrypoints: cs5, chunks: [cs5Styles] },\n          ],\n        };\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.content_scripts).toContainEqual({\n          matches: ['*://google.com/*'],\n          css: ['content-scripts/one.css'],\n          js: ['content-scripts/one.js'],\n        });\n        expect(actual.content_scripts).toContainEqual({\n          matches: ['*://google.com/*'],\n          run_at: 'document_end',\n          css: ['content-scripts/two.css', 'content-scripts/three.css'],\n          js: ['content-scripts/two.js', 'content-scripts/three.js'],\n        });\n        expect(actual.content_scripts).toContainEqual({\n          matches: ['*://duckduckgo.com/*'],\n          run_at: 'document_end',\n          css: ['content-scripts/four.css'],\n          js: ['content-scripts/four.js'],\n        });\n        expect(actual.content_scripts).toContainEqual({\n          matches: ['*://google.com/*'],\n          css: ['content-scripts/five.css'],\n          js: ['content-scripts/five.js'],\n          world: 'MAIN',\n        });\n      });\n\n      it('should merge any content scripts declared in wxt.config.ts', async () => {\n        const cs: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'one',\n          inputPath: 'entrypoints/one.content.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n          },\n          skipped: false,\n        };\n        const generatedContentScript = {\n          matches: ['*://google.com/*'],\n          js: ['content-scripts/one.js'],\n        };\n        const userContentScript = {\n          css: ['content-scripts/two.css'],\n          matches: ['*://*.google.com/*'],\n        };\n\n        const entrypoints = [cs];\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            outDir,\n            command: 'build',\n            manifest: {\n              content_scripts: [userContentScript],\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.content_scripts).toContainEqual(userContentScript);\n        expect(actual.content_scripts).toContainEqual(generatedContentScript);\n      });\n\n      describe('cssInjectionMode', () => {\n        it.each([undefined, 'manifest'] as const)(\n          'should add a CSS entry when cssInjectionMode is %s',\n          async (cssInjectionMode) => {\n            const cs: ContentScriptEntrypoint = {\n              type: 'content-script',\n              name: 'one',\n              inputPath: 'entrypoints/one.content.ts',\n              outputDir: contentScriptOutDir,\n              options: {\n                matches: ['*://google.com/*'],\n                cssInjectionMode,\n              },\n              skipped: false,\n            };\n            const styles: OutputAsset = {\n              type: 'asset',\n              fileName: 'content-scripts/one.css',\n            };\n\n            const entrypoints = [cs];\n            const buildOutput: Omit<BuildOutput, 'manifest'> = {\n              publicAssets: [],\n              steps: [{ entrypoints: cs, chunks: [styles] }],\n            };\n            setFakeWxt({\n              config: {\n                outDir,\n                command: 'build',\n              },\n            });\n\n            const { manifest: actual } = await generateManifest(\n              entrypoints,\n              buildOutput,\n            );\n\n            expect(actual.content_scripts).toEqual([\n              {\n                js: ['content-scripts/one.js'],\n                css: ['content-scripts/one.css'],\n                matches: ['*://google.com/*'],\n              },\n            ]);\n          },\n        );\n\n        it.each(['manual', 'ui'] as const)(\n          'should not add an entry for CSS when cssInjectionMode is %s',\n          async (cssInjectionMode) => {\n            const cs: ContentScriptEntrypoint = {\n              type: 'content-script',\n              name: 'one',\n              inputPath: 'entrypoints/one.content.ts',\n              outputDir: contentScriptOutDir,\n              options: {\n                matches: ['*://google.com/*'],\n                cssInjectionMode,\n              },\n              skipped: false,\n            };\n            const styles: OutputAsset = {\n              type: 'asset',\n              fileName: 'content-scripts/one.css',\n            };\n\n            const entrypoints = [cs];\n            const buildOutput: Omit<BuildOutput, 'manifest'> = {\n              publicAssets: [],\n              steps: [{ entrypoints: cs, chunks: [styles] }],\n            };\n            setFakeWxt({\n              config: {\n                outDir,\n                command: 'build',\n              },\n            });\n\n            const { manifest: actual } = await generateManifest(\n              entrypoints,\n              buildOutput,\n            );\n\n            expect(actual.content_scripts).toEqual([\n              {\n                js: ['content-scripts/one.js'],\n                matches: ['*://google.com/*'],\n              },\n            ]);\n          },\n        );\n\n        it('should add CSS file to `web_accessible_resources` when cssInjectionMode is \"ui\" for MV3', async () => {\n          const cs: ContentScriptEntrypoint = {\n            type: 'content-script',\n            name: 'one',\n            inputPath: 'entrypoints/one.content.ts',\n            outputDir: contentScriptOutDir,\n            options: {\n              matches: ['*://google.com/*'],\n              cssInjectionMode: 'ui',\n            },\n            skipped: false,\n          };\n          const styles: OutputAsset = {\n            type: 'asset',\n            fileName: 'content-scripts/one.css',\n          };\n\n          const entrypoints = [cs];\n          const buildOutput: Omit<BuildOutput, 'manifest'> = {\n            publicAssets: [],\n            steps: [{ entrypoints: cs, chunks: [styles] }],\n          };\n          setFakeWxt({\n            config: {\n              outDir,\n              command: 'build',\n              manifestVersion: 3,\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.web_accessible_resources).toEqual([\n            {\n              matches: ['*://google.com/*'],\n              resources: ['content-scripts/one.css'],\n              use_dynamic_url: true,\n            },\n          ]);\n        });\n\n        it('should add CSS file to `web_accessible_resources` when cssInjectionMode is \"ui\" for MV2', async () => {\n          const cs: ContentScriptEntrypoint = {\n            type: 'content-script',\n            name: 'one',\n            inputPath: 'entrypoints/one.content.ts',\n            outputDir: contentScriptOutDir,\n            options: {\n              matches: ['*://google.com/*'],\n              cssInjectionMode: 'ui',\n            },\n            skipped: false,\n          };\n          const styles: OutputAsset = {\n            type: 'asset',\n            fileName: 'content-scripts/one.css',\n          };\n\n          const entrypoints = [cs];\n          const buildOutput: Omit<BuildOutput, 'manifest'> = {\n            publicAssets: [],\n            steps: [{ entrypoints: cs, chunks: [styles] }],\n          };\n          setFakeWxt({\n            config: {\n              outDir,\n              command: 'build',\n              manifestVersion: 2,\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.web_accessible_resources).toEqual([\n            'content-scripts/one.css',\n          ]);\n        });\n\n        it('should strip the path off the match pattern so the pattern is valid for `web_accessible_resources`', async () => {\n          const cs: ContentScriptEntrypoint = {\n            type: 'content-script',\n            name: 'one',\n            inputPath: 'entrypoints/one.content.ts',\n            outputDir: contentScriptOutDir,\n            options: {\n              matches: ['*://play.google.com/books/*'],\n              cssInjectionMode: 'ui',\n            },\n            skipped: false,\n          };\n          const styles: OutputAsset = {\n            type: 'asset',\n            fileName: 'content-scripts/one.css',\n          };\n\n          const entrypoints = [cs];\n          const buildOutput: Omit<BuildOutput, 'manifest'> = {\n            publicAssets: [],\n            steps: [{ entrypoints: cs, chunks: [styles] }],\n          };\n          setFakeWxt({\n            config: {\n              outDir,\n              command: 'build',\n              manifestVersion: 3,\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.web_accessible_resources).toEqual([\n            {\n              matches: ['*://play.google.com/*'],\n              resources: ['content-scripts/one.css'],\n              use_dynamic_url: true,\n            },\n          ]);\n        });\n      });\n\n      describe('registration', () => {\n        it('should add host_permissions instead of content_scripts when registration=runtime', async () => {\n          const cs: ContentScriptEntrypoint = {\n            type: 'content-script',\n            name: 'one',\n            inputPath: 'entrypoints/one.content.ts',\n            outputDir: contentScriptOutDir,\n            options: {\n              matches: ['*://google.com/*'],\n              registration: 'runtime',\n            },\n            skipped: false,\n          };\n          const styles: OutputAsset = {\n            type: 'asset',\n            fileName: 'content-scripts/one.css',\n          };\n\n          const entrypoints = [cs];\n          const buildOutput: Omit<BuildOutput, 'manifest'> = {\n            publicAssets: [],\n            steps: [{ entrypoints: cs, chunks: [styles] }],\n          };\n          setFakeWxt({\n            config: {\n              manifestVersion: 3,\n              outDir,\n              command: 'build',\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.content_scripts).toEqual([]);\n          expect(actual.host_permissions).toEqual(['*://google.com/*']);\n        });\n      });\n    });\n\n    describe('sidepanel', () => {\n      it.each(['chrome', 'safari', 'edge'])(\n        'should include the side_panel and permission, ignoring all options for %s',\n        async (browser) => {\n          const sidepanel = fakeSidepanelEntrypoint({\n            outputDir: outDir,\n            skipped: false,\n          });\n          const buildOutput = fakeBuildOutput();\n\n          setFakeWxt({\n            config: {\n              manifestVersion: 3,\n              browser,\n              outDir,\n              command: 'build',\n            },\n          });\n          const expected = {\n            side_panel: {\n              default_path: 'sidepanel.html',\n            },\n            permissions: ['sidePanel'],\n          };\n\n          const { manifest: actual } = await generateManifest(\n            [sidepanel],\n            buildOutput,\n          );\n\n          expect(actual).toMatchObject(expected);\n        },\n      );\n\n      it.each(['firefox'])(\n        'should include a sidebar_action for %s',\n        async (browser) => {\n          const sidepanel = fakeSidepanelEntrypoint({\n            outputDir: outDir,\n            skipped: false,\n          });\n          const buildOutput = fakeBuildOutput();\n\n          setFakeWxt({\n            config: {\n              manifestVersion: 3,\n              browser,\n              outDir,\n            },\n          });\n          const expected = {\n            sidebar_action: {\n              default_panel: 'sidepanel.html',\n              open_at_install: sidepanel.options.openAtInstall,\n              default_title: sidepanel.options.defaultTitle,\n              default_icon: sidepanel.options.defaultIcon,\n              browser_style: sidepanel.options.browserStyle,\n            },\n          };\n\n          const { manifest: actual } = await generateManifest(\n            [sidepanel],\n            buildOutput,\n          );\n\n          expect(actual).toMatchObject(expected);\n        },\n      );\n    });\n\n    describe('web_accessible_resources', () => {\n      it('should combine user defined resources and generated resources for MV3', async () => {\n        const cs: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'one',\n          inputPath: 'entrypoints/one.content.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n            cssInjectionMode: 'ui',\n          },\n          skipped: false,\n        };\n        const styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/one.css',\n        };\n\n        const entrypoints = [cs];\n        const buildOutput: Omit<BuildOutput, 'manifest'> = {\n          publicAssets: [],\n          steps: [{ entrypoints: cs, chunks: [styles] }],\n        };\n        setFakeWxt({\n          config: {\n            outDir,\n            command: 'build',\n            manifestVersion: 3,\n            manifest: {\n              web_accessible_resources: [\n                { resources: ['one.png'], matches: ['*://one.com/*'] },\n              ],\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.web_accessible_resources).toEqual([\n          { resources: ['one.png'], matches: ['*://one.com/*'] },\n          {\n            resources: ['content-scripts/one.css'],\n            matches: ['*://google.com/*'],\n            use_dynamic_url: true,\n          },\n        ]);\n      });\n\n      it('should combine user defined resources and generated resources for MV2', async () => {\n        const cs: ContentScriptEntrypoint = {\n          type: 'content-script',\n          name: 'one',\n          inputPath: 'entrypoints/one.content.ts',\n          outputDir: contentScriptOutDir,\n          options: {\n            matches: ['*://google.com/*'],\n            cssInjectionMode: 'ui',\n          },\n          skipped: false,\n        };\n        const styles: OutputAsset = {\n          type: 'asset',\n          fileName: 'content-scripts/one.css',\n        };\n\n        const entrypoints = [cs];\n        const buildOutput: Omit<BuildOutput, 'manifest'> = {\n          publicAssets: [],\n          steps: [{ entrypoints: cs, chunks: [styles] }],\n        };\n        setFakeWxt({\n          config: {\n            outDir,\n            command: 'build',\n            manifestVersion: 2,\n            manifest: {\n              web_accessible_resources: ['one.png'],\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.web_accessible_resources).toEqual([\n          'one.png',\n          'content-scripts/one.css',\n        ]);\n      });\n\n      it('should convert mv3 items to mv2 strings automatically', async () => {\n        setFakeWxt({\n          config: {\n            outDir,\n            manifestVersion: 2,\n            manifest: {\n              web_accessible_resources: [\n                {\n                  matches: ['*://*/*'],\n                  resources: ['/icon-128.png'],\n                },\n                {\n                  matches: ['https://google.com'],\n                  resources: ['/icon-128.png', '/icon-32.png'],\n                },\n              ],\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          [],\n          fakeBuildOutput(),\n        );\n\n        expect(actual.web_accessible_resources).toEqual([\n          '/icon-128.png',\n          '/icon-32.png',\n        ]);\n      });\n\n      it('should convert mv2 strings to mv3 items with a warning automatically', async () => {\n        setFakeWxt({\n          config: {\n            outDir,\n            manifestVersion: 3,\n            manifest: {\n              web_accessible_resources: ['/icon.svg'],\n            },\n          },\n        });\n\n        await expect(() =>\n          generateManifest([], fakeBuildOutput()),\n        ).rejects.toThrow(\n          'Non-MV3 web_accessible_resources detected: [\"/icon.svg\"]. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.',\n        );\n      });\n    });\n\n    describe('version', () => {\n      it.each(['chrome', 'safari', 'edge'] as const)(\n        'should include version and version_name as is on %s',\n        async (browser) => {\n          const version = '1.0.0';\n          const versionName = '1.0.0-alpha1';\n          const entrypoints: Entrypoint[] = [];\n          const buildOutput = fakeBuildOutput();\n          setFakeWxt({\n            config: {\n              browser,\n              manifest: {\n                version,\n                version_name: versionName,\n              },\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.version).toBe(version);\n          expect(actual.version_name).toBe(versionName);\n        },\n      );\n\n      it.each(['firefox'] as const)(\n        'should not include a version_name on %s because it is unsupported',\n        async (browser) => {\n          const version = '1.0.0';\n          const versionName = '1.0.0-alpha1';\n          const entrypoints: Entrypoint[] = [];\n          const buildOutput = fakeBuildOutput();\n          setFakeWxt({\n            config: {\n              browser,\n              manifest: {\n                version,\n                version_name: versionName,\n              },\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.version).toBe(version);\n          expect(actual.version_name).toBeUndefined();\n        },\n      );\n\n      it.each(['chrome', 'firefox', 'safari', 'edge'])(\n        'should not include the version_name if it is equal to version',\n        async (browser) => {\n          const version = '1.0.0';\n          const entrypoints: Entrypoint[] = [];\n          const buildOutput = fakeBuildOutput();\n          setFakeWxt({\n            config: {\n              browser,\n              manifest: {\n                version,\n                version_name: version,\n              },\n            },\n          });\n\n          const { manifest: actual } = await generateManifest(\n            entrypoints,\n            buildOutput,\n          );\n\n          expect(actual.version).toBe(version);\n          expect(actual.version_name).toBeUndefined();\n        },\n      );\n\n      it('should log a warning if the version could not be detected', async () => {\n        const entrypoints: Entrypoint[] = [];\n        const buildOutput = fakeBuildOutput();\n        setFakeWxt({\n          config: {\n            manifest: {\n              // @ts-ignore: Purposefully removing version from fake object\n              version: null,\n            },\n          },\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.version).toBe('0.0.0');\n        expect(actual.version_name).toBeUndefined();\n        expect(wxt.logger.warn).toBeCalledTimes(1);\n        expect(wxt.logger.warn).toBeCalledWith(\n          expect.stringContaining('Extension version not found'),\n        );\n      });\n    });\n\n    describe('commands', () => {\n      const reloadCommandName = 'wxt:reload-extension';\n      const reloadCommand = {\n        description: expect.any(String),\n        suggested_key: {\n          default: 'Alt+R',\n        },\n      };\n\n      it('should include a command for reloading the extension during development', async () => {\n        setFakeWxt({\n          config: { command: 'serve' },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints = fakeArray(fakeEntrypoint);\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.commands).toEqual({\n          [reloadCommandName]: reloadCommand,\n        });\n      });\n\n      it('should customize the reload commands key binding if passing a custom command', async () => {\n        setFakeWxt({\n          config: {\n            command: 'serve',\n            dev: {\n              reloadCommand: 'Ctrl+E',\n            },\n          },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints = fakeArray(fakeEntrypoint);\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.commands).toEqual({\n          [reloadCommandName]: {\n            ...reloadCommand,\n            suggested_key: {\n              default: 'Ctrl+E',\n            },\n          },\n        });\n      });\n\n      it(\"should not include the reload command when it's been disabled\", async () => {\n        setFakeWxt({\n          config: {\n            command: 'serve',\n            dev: {\n              reloadCommand: false,\n            },\n          },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints = fakeArray(fakeEntrypoint);\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.commands).toBeUndefined();\n      });\n\n      it('should not override any existing commands when adding the one to reload the extension', async () => {\n        const customCommandName = 'custom-command';\n        const customCommand = fakeManifestCommand();\n        setFakeWxt({\n          config: {\n            command: 'serve',\n            manifest: {\n              commands: {\n                [customCommandName]: customCommand,\n              },\n            },\n          },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints = fakeArray(fakeEntrypoint);\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.commands).toEqual({\n          [reloadCommandName]: reloadCommand,\n          [customCommandName]: customCommand,\n        });\n      });\n\n      it('should not include the command if there are already 4 others (the max)', async () => {\n        const commands = {\n          command1: fakeManifestCommand(),\n          command2: fakeManifestCommand(),\n          command3: fakeManifestCommand(),\n          command4: fakeManifestCommand(),\n        };\n        setFakeWxt({\n          config: {\n            command: 'serve',\n            manifest: { commands },\n          },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints = fakeArray(fakeEntrypoint);\n\n        const { manifest: actual, warnings } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.commands).toEqual(commands);\n        expect(warnings).toHaveLength(1);\n      });\n\n      it('should not include the command when building an extension', async () => {\n        setFakeWxt({\n          config: { command: 'build' },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints = fakeArray(fakeEntrypoint);\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.commands).toBeUndefined();\n      });\n    });\n\n    describe('Stripping keys', () => {\n      const mv2Manifest = {\n        page_action: {},\n        browser_action: {},\n        automation: {},\n        content_capabilities: {},\n        converted_from_user_script: {},\n        current_locale: {},\n        differential_fingerprint: {},\n        event_rules: {},\n        file_browser_handlers: {},\n        file_system_provider_capabilities: {},\n        nacl_modules: {},\n        natively_connectable: {},\n        offline_enabled: {},\n        platforms: {},\n        replacement_web_app: {},\n        system_indicator: {},\n        user_scripts: {},\n      };\n      const mv3Manifest = {\n        action: {},\n        export: {},\n        optional_host_permissions: {},\n        side_panel: {},\n      };\n      const hostPermissionsManifest = {\n        host_permissions: {},\n      };\n      const manifest: any = {\n        ...mv2Manifest,\n        ...mv3Manifest,\n        ...hostPermissionsManifest,\n      };\n\n      it.each([\n        ['firefox', 2, mv2Manifest],\n        ['chrome', 2, { ...mv2Manifest, ...hostPermissionsManifest }],\n        ['safari', 2, { ...mv2Manifest, ...hostPermissionsManifest }],\n        ['edge', 2, { ...mv2Manifest, ...hostPermissionsManifest }],\n        ['firefox', 3, { ...mv3Manifest, ...hostPermissionsManifest }],\n        ['chrome', 3, { ...mv3Manifest, ...hostPermissionsManifest }],\n        ['safari', 3, { ...mv3Manifest, ...hostPermissionsManifest }],\n        ['edge', 3, { ...mv3Manifest, ...hostPermissionsManifest }],\n      ] as const)(\n        \"%s MV%s should only include that version's keys\",\n        async (browser, manifestVersion, expected) => {\n          setFakeWxt({\n            config: {\n              browser,\n              manifest,\n              manifestVersion,\n              command: 'build',\n            },\n          });\n          const output = fakeBuildOutput();\n\n          const { manifest: actual } = await generateManifest([], output);\n\n          expect(actual).toEqual({\n            name: expect.any(String),\n            version: expect.any(String),\n            manifest_version: manifestVersion,\n            ...expected,\n          });\n        },\n      );\n    });\n\n    describe('host_permissions', () => {\n      it('should keep host_permissions as-is for MV3', async () => {\n        const expectedHostPermissions = ['https://google.com/*'];\n        const expectedPermissions = ['scripting'];\n        setFakeWxt({\n          config: {\n            manifest: {\n              host_permissions: expectedHostPermissions,\n              permissions: expectedPermissions,\n            },\n            manifestVersion: 3,\n            command: 'build',\n          },\n        });\n        const output = fakeBuildOutput();\n\n        const { manifest: actual } = await generateManifest([], output);\n\n        expect(actual.permissions).toEqual(expectedPermissions);\n        expect(actual.host_permissions).toEqual(expectedHostPermissions);\n      });\n\n      it('should move host_permissions to permissions for MV2, ignoring duplicates', async () => {\n        const expectedPermissions = [\n          'scripting',\n          '*://*.youtube.com/*',\n          'https://google.com/*',\n        ];\n        setFakeWxt({\n          config: {\n            manifest: {\n              host_permissions: ['https://google.com/*', '*://*.youtube.com/*'],\n              permissions: ['scripting', '*://*.youtube.com/*'],\n            },\n            manifestVersion: 2,\n            command: 'build',\n          },\n        });\n        const output = fakeBuildOutput();\n\n        const { manifest: actual } = await generateManifest([], output);\n\n        expect(actual.permissions).toEqual(expectedPermissions);\n        expect(actual.host_permissions).toBeUndefined();\n      });\n    });\n\n    describe('Dev mode', () => {\n      it('should not add any code for production builds', async () => {\n        setFakeWxt({\n          config: {\n            command: 'build',\n          },\n          server: {\n            host: 'localhost',\n            port: 3000,\n            origin: 'http://localhost:3000',\n          },\n        });\n        const output = fakeBuildOutput();\n        const entrypoints: Entrypoint[] = [];\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual.permissions).toBeUndefined();\n        expect(actual.content_security_policy).toBeUndefined();\n      });\n\n      it('should add required permissions for dev mode to function for MV2', async () => {\n        setFakeWxt({\n          config: {\n            command: 'serve',\n            manifestVersion: 2,\n          },\n          server: fakeWxtDevServer({\n            host: 'localhost',\n            port: 3000,\n            origin: 'http://localhost:3000',\n          }),\n        });\n        const output = fakeBuildOutput();\n        const entrypoints: Entrypoint[] = [];\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual).toMatchObject({\n          content_security_policy:\n            \"script-src 'self' http://localhost:3000; object-src 'self';\",\n          permissions: ['http://localhost/*', 'tabs'],\n        });\n      });\n\n      it('should add required permissions for dev mode to function for MV3', async () => {\n        setFakeWxt({\n          config: {\n            command: 'serve',\n            manifestVersion: 3,\n            browser: 'chrome',\n          },\n          server: fakeWxtDevServer({\n            host: 'localhost',\n            port: 3000,\n            origin: 'http://localhost:3000',\n          }),\n        });\n        const output = fakeBuildOutput();\n        const entrypoints: Entrypoint[] = [];\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          output,\n        );\n\n        expect(actual).toMatchObject({\n          content_security_policy: {\n            extension_pages:\n              \"script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';\",\n            sandbox:\n              \"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3000; sandbox allow-scripts allow-forms allow-popups allow-modals; child-src 'self';\",\n          },\n          host_permissions: ['http://localhost/*'],\n          permissions: ['tabs', 'scripting'],\n        });\n      });\n\n      it('should convert MV3 CSP object to MV2 CSP string with localhost for MV2', async () => {\n        const entrypoints: Entrypoint[] = [];\n        const buildOutput = fakeBuildOutput();\n        const inputCsp =\n          \"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';\";\n        const expectedCsp =\n          \"script-src 'self' 'wasm-unsafe-eval' http://localhost:3000; object-src 'self';\";\n\n        // Setup WXT for Firefox and serve command\n        setFakeWxt({\n          config: {\n            browser: 'firefox',\n            command: 'serve',\n            manifestVersion: 2,\n            manifest: {\n              content_security_policy: {\n                extension_pages: inputCsp,\n              },\n            },\n          },\n          server: fakeWxtDevServer({\n            host: 'localhost',\n            port: 3000,\n            origin: 'http://localhost:3000',\n          }),\n        });\n\n        const { manifest: actual } = await generateManifest(\n          entrypoints,\n          buildOutput,\n        );\n\n        expect(actual.content_security_policy).toEqual(expectedCsp);\n      });\n    });\n\n    it('should not add skipped entrypoints to manifest', async () => {\n      const popup = fakePopupEntrypoint({ skipped: true });\n      const content = fakeContentScriptEntrypoint({ skipped: true });\n      const sidePanel = fakeSidepanelEntrypoint({ skipped: true });\n      const buildOutput = fakeBuildOutput();\n\n      setFakeWxt({\n        config: {\n          command: 'build',\n          manifestVersion: 3,\n        },\n      });\n\n      const { manifest } = await generateManifest(\n        [popup, content, sidePanel],\n        buildOutput,\n      );\n\n      expect(manifest.action).toBeUndefined();\n      expect(manifest.sidebar_action).toBeUndefined();\n      expect(manifest.content_scripts).toBeUndefined();\n    });\n\n    describe('manifest_version', () => {\n      it('should ignore and log a warning when someone sets `manifest_version` inside the manifest', async () => {\n        const buildOutput = fakeBuildOutput();\n        const expectedVersion = 2;\n        setFakeWxt({\n          logger: mock(),\n          config: {\n            command: 'build',\n            manifestVersion: expectedVersion,\n            manifest: {\n              manifest_version: 3,\n            },\n          },\n        });\n\n        const { manifest } = await generateManifest([], buildOutput);\n\n        expect(manifest.manifest_version).toBe(expectedVersion);\n        expect(wxt.logger.warn).toBeCalledTimes(1);\n        expect(wxt.logger.warn).toBeCalledWith(\n          expect.stringContaining(\n            '`manifest.manifest_version` config was set, but ignored',\n          ),\n        );\n      });\n    });\n  });\n\n  describe('stripPathFromMatchPattern', () => {\n    it.each([\n      ['<all_urls>', '<all_urls>'],\n      ['*://play.google.com/books/*', '*://play.google.com/*'],\n      ['*://*/*', '*://*/*'],\n      ['https://github.com/wxt-dev/*', 'https://github.com/*'],\n    ])('should convert \"%s\" to \"%s\"', (input, expected) => {\n      const actual = stripPathFromMatchPattern(input);\n      expect(actual).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/network.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport * as dns from 'node:dns';\nimport { isOnline, fetchCached } from '../network';\nimport { ResolvedConfig } from '../../../types';\n\ntype DnsCallback = (err: NodeJS.ErrnoException | null) => void;\ntype MockedFetch = ReturnType<typeof vi.fn>;\n\nvi.mock('node:dns');\n\nglobal.fetch = vi.fn();\n\ndescribe('Network utils', () => {\n  describe('isOnline', () => {\n    it('should return true when online', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(((\n        _: string,\n        callback: DnsCallback,\n      ) => {\n        callback(null);\n      }) as typeof dns.resolve);\n\n      const result = await isOnline();\n      expect(result).toBe(true);\n    });\n\n    it('should return false on DNS error', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(((\n        _: string,\n        callback: DnsCallback,\n      ) => {\n        callback(new Error('ENOTFOUND'));\n      }) as typeof dns.resolve);\n\n      const result = await isOnline();\n      expect(result).toBe(false);\n    });\n\n    it('should return false on timeout', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(() => {\n        // Simulate timeout by not calling callback\n      });\n\n      const result = await isOnline();\n      expect(result).toBe(false);\n    });\n\n    it('should handle dns.resolve errors gracefully', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(() => {\n        throw new Error('DNS resolution failed');\n      });\n\n      const result = await isOnline();\n      expect(result).toBe(false);\n    });\n  });\n\n  describe('fetchCached', () => {\n    it('should fetch from network when online', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(((\n        _: string,\n        callback: DnsCallback,\n      ) => {\n        callback(null);\n      }) as typeof dns.resolve);\n\n      const mockConfig = {\n        fsCache: {\n          set: vi.fn(),\n          get: vi.fn(),\n        },\n        logger: {\n          debug: vi.fn(),\n        },\n      };\n\n      const mockContent = 'cached content';\n      (global.fetch as MockedFetch).mockReturnValueOnce(\n        Promise.resolve({\n          status: 200,\n          text: async () => mockContent,\n        }),\n      );\n\n      const result = await fetchCached(\n        'https://example.com',\n        mockConfig as unknown as ResolvedConfig,\n      );\n\n      expect(result).toBe(mockContent);\n      expect(mockConfig.fsCache.set).toHaveBeenCalledWith(\n        'https://example.com',\n        mockContent,\n      );\n    });\n\n    it('should fall back to cache when network fails', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(((\n        _: string,\n        callback: DnsCallback,\n      ) => {\n        callback(null);\n      }) as typeof dns.resolve);\n\n      const mockConfig = {\n        fsCache: {\n          set: vi.fn(),\n          get: vi.fn().mockResolvedValueOnce('from cache'),\n        },\n        logger: {\n          debug: vi.fn(),\n        },\n      };\n\n      (global.fetch as MockedFetch).mockReturnValueOnce(\n        Promise.resolve({\n          status: 500,\n          text: async () => '',\n        }),\n      );\n\n      const result = await fetchCached(\n        'https://example.com',\n        mockConfig as unknown as ResolvedConfig,\n      );\n\n      expect(result).toBe('from cache');\n      expect(mockConfig.logger.debug).toHaveBeenCalledWith(\n        expect.stringContaining('Failed to download'),\n      );\n    });\n\n    it('should use cache when offline', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(((\n        _: string,\n        callback: DnsCallback,\n      ) => {\n        callback(new Error('ENOTFOUND'));\n      }) as typeof dns.resolve);\n\n      const mockConfig = {\n        fsCache: {\n          set: vi.fn(),\n          get: vi.fn().mockResolvedValueOnce('offline cache'),\n        },\n        logger: {\n          debug: vi.fn(),\n        },\n      };\n\n      const result = await fetchCached(\n        'https://example.com',\n        mockConfig as any,\n      );\n\n      expect(result).toBe('offline cache');\n      expect(global.fetch as MockedFetch).not.toHaveBeenCalled();\n    });\n\n    it('should throw error when offline and no cache available', async () => {\n      vi.mocked(dns.resolve).mockImplementationOnce(((\n        _: string,\n        callback: DnsCallback,\n      ) => {\n        callback(new Error('ENOTFOUND'));\n      }) as typeof dns.resolve);\n\n      const mockConfig = {\n        fsCache: {\n          set: vi.fn(),\n          get: vi.fn().mockResolvedValueOnce(null),\n        },\n        logger: {\n          debug: vi.fn(),\n        },\n      };\n\n      await expect(\n        fetchCached(\n          'https://example.com',\n          mockConfig as unknown as ResolvedConfig,\n        ),\n      ).rejects.toThrow(\n        'Offline and \"https://example.com\" has not been cached. Try again when online.',\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/number.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { safeStringToNumber } from '../number';\n\ndescribe('Number Utils', () => {\n  describe('safeStringToNumber', () => {\n    it.each([\n      { arg: '1000', expected: 1000 },\n      { arg: '', expected: 0 },\n      { arg: '12abc', expected: null },\n      { arg: undefined, expected: null },\n    ])(\n      'should be safely converted from string to number: safeStringToNumber($arg) -> $expected',\n      ({ arg, expected }) => {\n        expect(safeStringToNumber(arg)).toBe(expected);\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/package.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { getPackageJson } from '../package';\nimport { setFakeWxt } from '../testing/fake-objects';\nimport { mock } from 'vitest-mock-extended';\nimport { Logger } from '../../../types';\nimport { WXT_PACKAGE_DIR } from '../../../../e2e/utils';\n\ndescribe('Package JSON Utils', () => {\n  describe('getPackageJson', () => {\n    it('should return the package.json inside <root>/package.json', async () => {\n      setFakeWxt({\n        config: { root: WXT_PACKAGE_DIR },\n      });\n\n      const actual = await getPackageJson();\n\n      expect(actual).toMatchObject({\n        name: 'wxt',\n      });\n    });\n\n    it(\"should return an empty object when <root>/package.json doesn't exist\", async () => {\n      const root = '/some/path/that/does/not/exist';\n      const logger = mock<Logger>();\n      setFakeWxt({\n        config: { root, logger },\n        logger,\n      });\n\n      const actual = await getPackageJson();\n\n      expect(actual).toEqual({});\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/paths.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { normalizePath } from '../paths';\n\ndescribe('Path Utils', () => {\n  describe('normalizePath', () => {\n    it.each([\n      // Relative paths\n      ['../test.sh', '../test.sh'],\n      ['..\\\\test.sh', '../test.sh'],\n      ['test.png', 'test.png'],\n      // Absolute paths\n      ['C:\\\\\\\\path\\\\to\\\\file', 'C:/path/to/file'],\n      ['/path/to/file', '/path/to/file'],\n      // Strip trailing slash\n      ['C:\\\\\\\\path\\\\to\\\\folder\\\\', 'C:/path/to/folder'],\n      ['/path/to/folder/', '/path/to/folder'],\n      // Dedupe slashes\n      ['path\\\\\\\\\\\\file', 'path/file'],\n      ['path//file', 'path/file'],\n    ])('should normalize \"%s\" to \"%s\"', (input, expected) => {\n      expect(normalizePath(input)).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/picomatch-multiple.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { picomatchMultiple } from '../picomatch-multiple';\n\ndescribe('picomatchMultiple', () => {\n  it('should return false if the pattern array is undefined', () => {\n    const patterns = undefined;\n    const search = 'test.json';\n\n    expect(picomatchMultiple(search, patterns)).toBe(false);\n  });\n\n  it('should return false if the pattern array is empty', () => {\n    const patterns: string[] = [];\n    const search = 'test.json';\n\n    expect(picomatchMultiple(search, patterns)).toBe(false);\n  });\n\n  it('should return true if the pattern array contains a match', () => {\n    const patterns = ['test.yml', 'test.json'];\n    const search = 'test.json';\n\n    expect(picomatchMultiple(search, patterns)).toBe(true);\n  });\n\n  it('should return false if the pattern array does not contain a match', () => {\n    const patterns = ['test.yml', 'test.json'];\n    const search = 'test.txt';\n\n    expect(picomatchMultiple(search, patterns)).toBe(false);\n  });\n\n  it('should return false if the pattern matches a negative pattern', () => {\n    const patterns = ['test.*', '!test.json'];\n    const search = 'test.json';\n\n    expect(picomatchMultiple(search, patterns)).toBe(false);\n  });\n\n  it('should return false if the pattern matches a negative pattern, regardless of order', () => {\n    const patterns = ['!test.json', 'test.*'];\n    const search = 'test.json';\n\n    expect(picomatchMultiple(search, patterns)).toBe(false);\n  });\n\n  it('should support extglob-like extension matching', () => {\n    const patterns = ['content.[jt]s?(x)'];\n\n    expect(picomatchMultiple('content.ts', patterns)).toBe(true);\n    expect(picomatchMultiple('content.jsx', patterns)).toBe(true);\n    expect(picomatchMultiple('content.css', patterns)).toBe(false);\n  });\n\n  it('should support nested paths', () => {\n    const patterns = ['foo/**/*.ts'];\n\n    expect(picomatchMultiple('foo/bar/baz.ts', patterns)).toBe(true);\n    expect(picomatchMultiple('foo/bar/baz.js', patterns)).toBe(false);\n  });\n\n  it('should preserve include/exclude interaction used by zip filtering', () => {\n    const include = ['special.txt'];\n    const exclude = ['**/*.txt'];\n    const search = 'special.txt';\n    const shouldInclude =\n      picomatchMultiple(search, include) || !picomatchMultiple(search, exclude);\n\n    expect(shouldInclude).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/strings.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport {\n  kebabCaseAlphanumeric,\n  removeImportStatements,\n  safeVarName,\n} from '../strings';\n\ndescribe('String utils', () => {\n  describe('kebabCaseAlphanumeric', () => {\n    it.each([\n      ['HELLO', 'hello'],\n      ['Hello, World!', 'hello-world'],\n      ['hello123', 'hello123'],\n      ['Hello World This Is A Test', 'hello-world-this-is-a-test'],\n      ['Hello     World', 'hello-world'],\n      ['hello-world', 'hello-world'], // Ensure hyphens are preserved\n    ])('should convert \"%s\" to \"%s\"', (input, expected) => {\n      expect(kebabCaseAlphanumeric(input)).toBe(expected);\n    });\n  });\n\n  describe('safeVarName', () => {\n    it.each([\n      ['Hello world!', 'helloWorld'],\n      ['123', '_123'],\n      ['abc-123', 'abc123'],\n      ['abc-123-xyz', 'abc123Xyz'],\n      ['', '_'],\n      [' ', '_'],\n      ['_', '_'],\n    ])(\n      \"should convert '%s' to '%s', which can be used for a variable name\",\n      (input, expected) => {\n        const actual = safeVarName(input);\n        expect(actual).toBe(expected);\n      },\n    );\n  });\n\n  describe('removeImportStatements', () => {\n    it('should remove all import formats', () => {\n      const imports = `\nimport { registerGithubService, createGithubApi } from \"@/utils/github\";\nimport {\n  registerGithubService,\n  createGithubApi\n} from \"@/utils/github\";\nimport{ registerGithubService, createGithubApi }from \"@/utils/github\";\nimport GitHub from \"@/utils/github\";\nimport \"@/utils/github\";\nimport '@/utils/github';\nimport * as abc from \"@/utils/github\"\nimport\"@/utils/github\"\n import'@/utils/github';\nimport * as abc from \"@/utils/github\"\n    `;\n      expect(removeImportStatements(imports).trim()).toEqual('');\n    });\n\n    it('should not remove import.meta or inline import statements', () => {\n      const imports = `\nimport.meta.env.DEV\nconst a = await import(\"example\");\nimport(\"example\");\n    `;\n      expect(removeImportStatements(imports)).toEqual(imports);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/transform.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { removeMainFunctionCode } from '../transform';\n\ndescribe('Transform Utils', () => {\n  describe('removeMainFunctionCode', () => {\n    it.each(['defineBackground', 'defineUnlistedScript'])(\n      'should remove the first arrow function argument for %s',\n      (def) => {\n        const input = `\n          export default ${def}(() => {\n            console.log();\n          })\n        `;\n        const expected = `export default ${def}();`;\n\n        const actual = removeMainFunctionCode(input).code;\n\n        expect(actual).toEqual(expected);\n      },\n    );\n\n    it.each(['defineBackground', 'defineUnlistedScript'])(\n      'should remove the first function argument for %s',\n      (def) => {\n        const input = `\n          export default ${def}(function () {\n            console.log();\n          })\n        `;\n        const expected = `export default ${def}();`;\n\n        const actual = removeMainFunctionCode(input).code;\n\n        expect(actual).toEqual(expected);\n      },\n    );\n\n    it.each([\n      'defineBackground',\n      'defineContentScript',\n      'defineUnlistedScript',\n    ])('should remove the main field from %s', (def) => {\n      const input = `\n        export default ${def}({\n          asdf: \"asdf\",\n          main: () => {},\n        })\n      `;\n      const expected = `export default ${def}({\n  asdf: \"asdf\"\n})`;\n\n      const actual = removeMainFunctionCode(input).code;\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should remove unused imports', () => {\n      const input = `\n        import { defineBackground } from \"#imports\"\n        import { test1 } from \"somewhere1\"\n        import test2 from \"somewhere2\"\n\n        export default defineBackground(() => {})\n      `;\n      const expected = `import { defineBackground } from \"#imports\"\n\nexport default defineBackground();`;\n\n      const actual = removeMainFunctionCode(input).code;\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should remove explict side-effect imports', () => {\n      const input = `\n        import { defineBackground } from \"#imports\"\n        import \"my-polyfill\"\n        import \"./style.css\"\n\n        export default defineBackground(() => {})\n      `;\n      const expected = `import { defineBackground } from \"#imports\"\n\nexport default defineBackground();`;\n\n      const actual = removeMainFunctionCode(input).code;\n\n      expect(actual).toEqual(expected);\n    });\n\n    it(\"should remove any functions delcared outside the main function that aren't used\", () => {\n      const input = `\n              function getMatches() {\n                return [\"*://*/*\"]\n              }\n              function unused1() {}\n              function unused2() {\n                unused1();\n              }\n\n              export default defineContentScript({\n                matches: getMatches(),\n                main: () => {},\n              })\n            `;\n      const expected = `function getMatches() {\n  return [\"*://*/*\"]\n}\n\nexport default defineContentScript({\n  matches: getMatches()\n})`;\n\n      const actual = removeMainFunctionCode(input).code;\n\n      expect(actual).toEqual(expected);\n    });\n\n    it(\"should remove any variables delcared outside the main function that aren't used\", () => {\n      const input = `\n        const unused1 = \"a\", matches = [\"*://*/*\"];\n        let unused2 = unused1 + \"b\";\n\n        export default defineContentScript({\n          matches,\n          main: () => {}\n        })\n      `;\n      const expected = `const matches = [\"*://*/*\"];\n\nexport default defineContentScript({\n  matches\n})`;\n\n      const actual = removeMainFunctionCode(input).code;\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should not remove any variables delcared outside the main function that are used', () => {\n      const input = `\n        const [ a ] = [ 123, 456 ];\n        const { b } = { b: 123 };\n        const { c: { d } } = { c: { d: 123 } };\n        const { e, ...rest } = { e: 123, f: 456 };\n\n        console.log(a);\n        console.log(b);\n        console.log(d);\n        console.log(e);\n        console.log(rest);\n\n        export default defineBackground(() => {\n          console.log('Hello background!', { id: browser.runtime.id });\n        });`;\n      const expected = `const [ a ] = [ 123, 456 ];\nconst { b } = { b: 123 };\nconst { c: { d } } = { c: { d: 123 } };\nconst { e, ...rest } = { e: 123, f: 456 };\n\nconsole.log(a);\nconsole.log(b);\nconsole.log(d);\nconsole.log(e);\nconsole.log(rest);\n\nexport default defineBackground();`;\n\n      const actual = removeMainFunctionCode(input).code;\n      expect(actual).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/validation.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport {\n  fakeArray,\n  fakeContentScriptEntrypoint,\n  fakeEntrypoint,\n  fakeGenericEntrypoint,\n} from '../testing/fake-objects';\nimport { validateEntrypoints } from '../validation';\n\ndescribe('Validation Utils', () => {\n  describe('validateEntrypoints', () => {\n    it('should return no errors when there are no errors', () => {\n      const entrypoints = fakeArray(fakeEntrypoint);\n      const expected = {\n        errors: [],\n        errorCount: 0,\n        warningCount: 0,\n      };\n\n      const actual = validateEntrypoints(entrypoints);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should return an error when exclude is not an array', () => {\n      const entrypoint = fakeGenericEntrypoint({\n        options: {\n          // @ts-expect-error\n          exclude: 0,\n        },\n      });\n      const expected = {\n        errors: [\n          {\n            type: 'error',\n            message: '`exclude` must be an array of browser names',\n            value: 0,\n            entrypoint,\n          },\n        ],\n        errorCount: 1,\n        warningCount: 0,\n      };\n\n      const actual = validateEntrypoints([entrypoint]);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should return an error when include is not an array', () => {\n      const entrypoint = fakeGenericEntrypoint({\n        options: {\n          // @ts-expect-error\n          include: 0,\n        },\n      });\n      const expected = {\n        errors: [\n          {\n            type: 'error',\n            message: '`include` must be an array of browser names',\n            value: 0,\n            entrypoint,\n          },\n        ],\n        errorCount: 1,\n        warningCount: 0,\n      };\n\n      const actual = validateEntrypoints([entrypoint]);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should return an error when \"registration: manifest\" content scripts don\\'t have matches', () => {\n      const entrypoint = fakeContentScriptEntrypoint({\n        options: {\n          registration: 'manifest',\n          // @ts-expect-error\n          matches: null,\n        },\n      });\n      const expected = {\n        errors: [\n          {\n            type: 'error',\n            message:\n              '`matches` is required for manifest registered content scripts',\n            value: null,\n            entrypoint,\n          },\n        ],\n        errorCount: 1,\n        warningCount: 0,\n      };\n\n      const actual = validateEntrypoints([entrypoint]);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should allow \"registration: runtime\" content scripts to not have matches', () => {\n      const entrypoint = fakeContentScriptEntrypoint({\n        options: {\n          registration: 'runtime',\n          // @ts-expect-error\n          matches: null,\n        },\n      });\n      const expected = {\n        errors: [],\n        errorCount: 0,\n        warningCount: 0,\n      };\n\n      const actual = validateEntrypoints([entrypoint]);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/__tests__/virtual-modules.test.ts",
    "content": "import { describe, it } from 'vitest';\nimport {\n  VirtualModuleId,\n  VirtualModuleName,\n  VirtualEntrypointType,\n  VirtualEntrypointModuleName,\n} from '../virtual-modules';\n\ndescribe('Virtual Modules', () => {\n  it('should resolve types to litteral values, not string', () => {\n    // @ts-expect-error\n    const _c: VirtualEntrypointType = '';\n    // @ts-expect-error\n    const _d: VirtualEntrypointModuleName = '';\n    // @ts-expect-error\n    const _b: VirtualModuleName = '';\n    // @ts-expect-error\n    const _a: VirtualModuleId = '';\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/arrays.ts",
    "content": "/** Checks if `predicate` returns truthy for all elements of the array. */\nexport function every<T>(\n  array: T[],\n  predicate: (item: T, index: number) => boolean,\n): boolean {\n  for (let i = 0; i < array.length; i++)\n    if (!predicate(array[i], i)) return false;\n  return true;\n}\n\n/** Returns true when any of the predicates return true; */\nexport function some<T>(\n  array: T[],\n  predicate: (item: T, index: number) => boolean,\n): boolean {\n  for (let i = 0; i < array.length; i++)\n    if (predicate(array[i], i)) return true;\n  return false;\n}\n\n/** Convert an item or array to an array. */\nexport function toArray<T>(a: T | T[]): T[] {\n  return Array.isArray(a) ? a : [a];\n}\n\nexport function filterTruthy<T>(array: Array<T | undefined>): T[] {\n  return array.filter((item) => !!item) as T[];\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts",
    "content": "import { beforeEach, describe, expect, it } from 'vitest';\nimport { DevModeChange, detectDevChanges } from '../detect-dev-changes';\nimport {\n  fakeBackgroundEntrypoint,\n  fakeContentScriptEntrypoint,\n  fakeFile,\n  fakeGenericEntrypoint,\n  fakeManifest,\n  fakeOptionsEntrypoint,\n  fakePopupEntrypoint,\n  fakeOutputAsset,\n  fakeOutputChunk,\n  fakeWxt,\n  setFakeWxt,\n} from '../../testing/fake-objects';\nimport { BuildOutput, BuildStepOutput } from '../../../../types';\nimport { setWxtForTesting } from '../../../wxt';\n\ndescribe('Detect Dev Changes', () => {\n  beforeEach(() => {\n    setWxtForTesting(fakeWxt());\n  });\n\n  describe('No changes', () => {\n    it(\"should return 'no-change' when the changed file isn't used by any of the entrypoints\", () => {\n      const changes = ['/some/path.ts'];\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [\n          {\n            entrypoints: fakeContentScriptEntrypoint(),\n            chunks: [fakeOutputChunk(), fakeOutputChunk()],\n          },\n          {\n            entrypoints: fakeContentScriptEntrypoint(),\n            chunks: [fakeOutputChunk(), fakeOutputChunk(), fakeOutputChunk()],\n          },\n        ],\n      };\n\n      const actual = detectDevChanges(changes, currentOutput);\n\n      expect(actual).toEqual({ type: 'no-change' });\n    });\n  });\n\n  describe('wxt.config.ts', () => {\n    it(\"should return 'full-restart' when one of the changed files is the config file\", () => {\n      const configFile = '/root/wxt.config.ts';\n      setFakeWxt({\n        config: {\n          userConfigMetadata: {\n            configFile,\n          },\n        },\n      });\n      const changes = ['/root/src/public/image.svg', configFile];\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [],\n      };\n      const expected: DevModeChange = {\n        type: 'full-restart',\n      };\n\n      const actual = detectDevChanges(changes, currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('modules/*', () => {\n    it(\"should return 'full-restart' when one of the changed files is in the WXT modules folder\", () => {\n      const modulesDir = '/root/modules';\n      setFakeWxt({\n        config: {\n          modulesDir,\n        },\n      });\n      const changes = [\n        '/root/src/public/image.svg',\n        `${modulesDir}/example.ts`,\n      ];\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [],\n      };\n      const expected: DevModeChange = {\n        type: 'full-restart',\n      };\n\n      const actual = detectDevChanges(changes, currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('web-ext.config.ts', () => {\n    it(\"should return 'browser-restart' when one of the changed files is the config file\", () => {\n      const runnerFile = '/root/web-ext.config.ts';\n      setFakeWxt({\n        config: {\n          runnerConfig: {\n            configFile: runnerFile,\n          },\n        },\n      });\n      const changes = ['/root/src/public/image.svg', runnerFile];\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [],\n      };\n      const expected: DevModeChange = {\n        type: 'browser-restart',\n      };\n\n      const actual = detectDevChanges(changes, currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('Public Assets', () => {\n    it(\"should return 'extension-reload' without any groups to rebuild when the changed file is a public asset\", () => {\n      const changes = ['/root/src/public/image.svg'];\n      setFakeWxt({\n        config: {\n          publicDir: '/root/src/public',\n        },\n      });\n      const asset1 = fakeOutputAsset({\n        fileName: 'image.svg',\n      });\n      const asset2 = fakeOutputAsset({\n        fileName: 'some-other-image.svg',\n      });\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [asset1, asset2],\n        steps: [],\n      };\n      const expected: DevModeChange = {\n        type: 'extension-reload',\n        rebuildGroups: [],\n        cachedOutput: currentOutput,\n      };\n\n      const actual = detectDevChanges(changes, currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('Background', () => {\n    it(\"should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field\", () => {\n      const changedPath = '/root/utils/shared.ts';\n      const contentScript = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay.content.ts',\n      });\n      const background = fakeBackgroundEntrypoint({\n        inputPath: '/root/background.ts',\n      });\n\n      const step1: BuildStepOutput = {\n        entrypoints: contentScript,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [fakeFile(), fakeFile()],\n          }),\n        ],\n      };\n      const step2: BuildStepOutput = {\n        entrypoints: background,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [fakeFile(), changedPath, fakeFile()],\n          }),\n        ],\n      };\n\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [step1, step2],\n      };\n      const expected: DevModeChange = {\n        type: 'extension-reload',\n        cachedOutput: {\n          ...currentOutput,\n          steps: [step1],\n        },\n        rebuildGroups: [background],\n      };\n\n      const actual = detectDevChanges([changedPath], currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('HTML Pages', () => {\n    it('should detect changes to entrypoints/<name>.html files', async () => {\n      const changedPath = '/root/page1.html';\n      const htmlPage1 = fakePopupEntrypoint({\n        inputPath: changedPath,\n      });\n      const htmlPage2 = fakeOptionsEntrypoint({\n        inputPath: '/root/page2.html',\n      });\n      const htmlPage3 = fakeGenericEntrypoint({\n        type: 'sandbox',\n        inputPath: '/root/page3.html',\n      });\n\n      const step1: BuildStepOutput = {\n        entrypoints: [htmlPage1, htmlPage2],\n        chunks: [\n          fakeOutputAsset({\n            fileName: 'page1.html',\n          }),\n        ],\n      };\n      const step2: BuildStepOutput = {\n        entrypoints: [htmlPage3],\n        chunks: [\n          fakeOutputAsset({\n            fileName: 'page2.html',\n          }),\n        ],\n      };\n\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [step1, step2],\n      };\n      const expected: DevModeChange = {\n        type: 'html-reload',\n        cachedOutput: {\n          ...currentOutput,\n          steps: [step2],\n        },\n        rebuildGroups: [[htmlPage1, htmlPage2]],\n      };\n\n      const actual = detectDevChanges([changedPath], currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should detect changes to entrypoints/<name>/index.html files', async () => {\n      const changedPath = '/root/page1/index.html';\n      const htmlPage1 = fakePopupEntrypoint({\n        inputPath: changedPath,\n      });\n      const htmlPage2 = fakeOptionsEntrypoint({\n        inputPath: '/root/page2/index.html',\n      });\n      const htmlPage3 = fakeGenericEntrypoint({\n        type: 'sandbox',\n        inputPath: '/root/page3/index.html',\n      });\n\n      const step1: BuildStepOutput = {\n        entrypoints: [htmlPage1, htmlPage2],\n        chunks: [\n          fakeOutputAsset({\n            fileName: 'page1.html',\n          }),\n        ],\n      };\n      const step2: BuildStepOutput = {\n        entrypoints: [htmlPage3],\n        chunks: [\n          fakeOutputAsset({\n            fileName: 'page2.html',\n          }),\n        ],\n      };\n\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [step1, step2],\n      };\n      const expected: DevModeChange = {\n        type: 'html-reload',\n        cachedOutput: {\n          ...currentOutput,\n          steps: [step2],\n        },\n        rebuildGroups: [[htmlPage1, htmlPage2]],\n      };\n\n      const actual = detectDevChanges([changedPath], currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n\n  describe('Content Scripts', () => {\n    it('should rebuild then reload only the effected content scripts', async () => {\n      const changedPath = '/root/utils/shared.ts';\n      const script1 = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay1.content/index.ts',\n      });\n      const script2 = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay2.ts',\n      });\n      const script3 = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay3.content/index.ts',\n      });\n\n      const step1: BuildStepOutput = {\n        entrypoints: script1,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [fakeFile(), changedPath],\n          }),\n        ],\n      };\n      const step2: BuildStepOutput = {\n        entrypoints: script2,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [fakeFile(), fakeFile(), fakeFile()],\n          }),\n        ],\n      };\n      const step3: BuildStepOutput = {\n        entrypoints: script3,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [changedPath, fakeFile(), fakeFile()],\n          }),\n        ],\n      };\n\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [step1, step2, step3],\n      };\n      const expected: DevModeChange = {\n        type: 'content-script-reload',\n        cachedOutput: {\n          ...currentOutput,\n          steps: [step2],\n        },\n        changedSteps: [step1, step3],\n        rebuildGroups: [script1, script3],\n      };\n\n      const actual = detectDevChanges([changedPath], currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n\n    it('should detect changes to import files with `?suffix`', () => {\n      const importedPath = '/root/utils/shared.css?inline';\n      const changedPath = '/root/utils/shared.css';\n      const script1 = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay1.content/index.ts',\n      });\n      const script2 = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay2.ts',\n      });\n      const script3 = fakeContentScriptEntrypoint({\n        inputPath: '/root/overlay3.content/index.ts',\n      });\n\n      const step1: BuildStepOutput = {\n        entrypoints: script1,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [fakeFile(), importedPath],\n          }),\n        ],\n      };\n      const step2: BuildStepOutput = {\n        entrypoints: script2,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [fakeFile(), fakeFile(), fakeFile()],\n          }),\n        ],\n      };\n      const step3: BuildStepOutput = {\n        entrypoints: script3,\n        chunks: [\n          fakeOutputChunk({\n            moduleIds: [importedPath, fakeFile(), fakeFile()],\n          }),\n        ],\n      };\n\n      const currentOutput: BuildOutput = {\n        manifest: fakeManifest(),\n        publicAssets: [],\n        steps: [step1, step2, step3],\n      };\n      const expected: DevModeChange = {\n        type: 'content-script-reload',\n        cachedOutput: {\n          ...currentOutput,\n          steps: [step2],\n        },\n        changedSteps: [step1, step3],\n        rebuildGroups: [script1, script3],\n      };\n\n      const actual = detectDevChanges([changedPath], currentOutput);\n\n      expect(actual).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts",
    "content": "import { readFile } from 'node:fs/promises';\nimport { resolve } from 'path';\nimport { glob } from 'tinyglobby';\nimport { beforeEach, describe, expect, it, Mock, vi } from 'vitest';\nimport {\n  BackgroundEntrypoint,\n  BackgroundEntrypointOptions,\n  BaseEntrypointOptions,\n  ContentScriptEntrypoint,\n  GenericEntrypoint,\n  OptionsEntrypoint,\n  PopupEntrypoint,\n  SidepanelEntrypoint,\n} from '../../../../types';\nimport { wxt } from '../../../wxt';\nimport { unnormalizePath } from '../../paths';\nimport { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects';\nimport { findEntrypoints } from '../find-entrypoints';\n\nvi.mock('tinyglobby');\nconst globMock = vi.mocked(glob);\n\nvi.mock('node:fs/promises');\nconst readFileMock = vi.mocked(readFile);\n\ndescribe('findEntrypoints', () => {\n  const config = fakeResolvedConfig({\n    manifestVersion: 3,\n    root: '/',\n    entrypointsDir: resolve('/src/entrypoints'),\n    outDir: resolve('.output'),\n    command: 'build',\n  });\n  let importEntrypointsMock: Mock<typeof wxt.builder.importEntrypoints>;\n\n  beforeEach(() => {\n    setFakeWxt({ config });\n    importEntrypointsMock = vi.mocked(wxt.builder.importEntrypoints);\n    importEntrypointsMock.mockResolvedValue([]);\n  });\n\n  it.each<[string, string, PopupEntrypoint]>([\n    [\n      'popup.html',\n      `\n        <html>\n          <head>\n            <meta name=\"manifest.default_icon\" content=\"{ '16': '/icon/16.png' }\" />\n            <title>Default Title</title>\n          </head>\n        </html>\n      `,\n      {\n        type: 'popup',\n        name: 'popup',\n        inputPath: resolve(config.entrypointsDir, 'popup.html'),\n        outputDir: config.outDir,\n        options: {\n          defaultIcon: { '16': '/icon/16.png' },\n          defaultTitle: 'Default Title',\n        },\n        skipped: false,\n      },\n    ],\n    [\n      'popup/index.html',\n      `\n        <html>\n          <head>\n            <title>Title</title>\n          </head>\n        </html>\n      `,\n      {\n        type: 'popup',\n        name: 'popup',\n        inputPath: resolve(config.entrypointsDir, 'popup/index.html'),\n        outputDir: config.outDir,\n        options: {\n          defaultTitle: 'Title',\n        },\n        skipped: false,\n      },\n    ],\n  ])(\n    'should find and load popup entrypoint config from %s',\n    async (path, content, expected) => {\n      globMock.mockResolvedValueOnce([path]);\n      readFileMock.mockResolvedValueOnce(content);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toHaveLength(1);\n      expect(entrypoints[0]).toEqual(expected);\n    },\n  );\n\n  it.each<[string, string, OptionsEntrypoint]>([\n    [\n      'options.html',\n      `\n        <html>\n          <head>\n            <title>Default Title</title>\n          </head>\n        </html>\n      `,\n      {\n        type: 'options',\n        name: 'options',\n        inputPath: resolve(config.entrypointsDir, 'options.html'),\n        outputDir: config.outDir,\n        options: {\n          title: 'Default Title',\n        },\n        skipped: false,\n      },\n    ],\n    [\n      'options/index.html',\n      `\n        <html>\n          <head>\n            <meta name=\"manifest.open_in_tab\" content=\"true\" />\n            <title>Title</title>\n          </head>\n        </html>\n      `,\n      {\n        type: 'options',\n        name: 'options',\n        inputPath: resolve(config.entrypointsDir, 'options/index.html'),\n        outputDir: config.outDir,\n        options: {\n          openInTab: true,\n          title: 'Title',\n        },\n        skipped: false,\n      },\n    ],\n  ])(\n    'should find and load options entrypoint config from %s',\n    async (path, content, expected) => {\n      globMock.mockResolvedValueOnce([path]);\n      readFileMock.mockResolvedValueOnce(content);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toHaveLength(1);\n      expect(entrypoints[0]).toEqual(expected);\n    },\n  );\n\n  it('should extract wxt.* meta tags from HTML entrypoints', async () => {\n    const path = 'popup.html';\n    const content = `\n      <html>\n        <head>\n          <meta name=\"manifest.default_icon\" content=\"{ '16': '/icon/16.png' }\" />\n          <meta name=\"wxt.custom_option\" content=\"custom_value\" />\n          <meta name=\"wxt.anotherOption\" content=\"true\" />\n          <title>Test Title</title>\n        </head>\n      </html>\n    `;\n\n    globMock.mockResolvedValueOnce([path]);\n    readFileMock.mockResolvedValueOnce(content);\n\n    const entrypoints = await findEntrypoints();\n\n    expect(entrypoints).toHaveLength(1);\n    expect(entrypoints[0].options).toMatchObject({\n      defaultIcon: { '16': '/icon/16.png' },\n      customOption: 'custom_value',\n      anotherOption: true,\n      defaultTitle: 'Test Title',\n    });\n  });\n\n  it.each<[string, Omit<ContentScriptEntrypoint, 'options'>]>([\n    [\n      'content.ts',\n      {\n        type: 'content-script',\n        name: 'content',\n        inputPath: resolve(config.entrypointsDir, 'content.ts'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        skipped: false,\n      },\n    ],\n    [\n      'overlay.content.ts',\n      {\n        type: 'content-script',\n        name: 'overlay',\n        inputPath: resolve(config.entrypointsDir, 'overlay.content.ts'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        skipped: false,\n      },\n    ],\n    [\n      'content/index.ts',\n      {\n        type: 'content-script',\n        name: 'content',\n        inputPath: resolve(config.entrypointsDir, 'content/index.ts'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        skipped: false,\n      },\n    ],\n    [\n      'overlay.content/index.ts',\n      {\n        type: 'content-script',\n        name: 'overlay',\n        inputPath: resolve(config.entrypointsDir, 'overlay.content/index.ts'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        skipped: false,\n      },\n    ],\n    [\n      'overlay.content.tsx',\n      {\n        type: 'content-script',\n        name: 'overlay',\n        inputPath: resolve(config.entrypointsDir, 'overlay.content.tsx'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        skipped: false,\n      },\n    ],\n  ])(\n    'should find and load content script entrypoint config from %s',\n    async (path, expected) => {\n      const options: ContentScriptEntrypoint['options'] = {\n        matches: ['<all_urls>'],\n      };\n      globMock.mockResolvedValueOnce([path]);\n      importEntrypointsMock.mockResolvedValue([options]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toHaveLength(1);\n      expect(entrypoints[0]).toEqual({ ...expected, options });\n      expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]);\n    },\n  );\n\n  it.each<[string, Omit<BackgroundEntrypoint, 'options'>]>([\n    [\n      'background.ts',\n      {\n        type: 'background',\n        name: 'background',\n        inputPath: resolve(config.entrypointsDir, 'background.ts'),\n        outputDir: config.outDir,\n        skipped: false,\n      },\n    ],\n    [\n      'background/index.ts',\n      {\n        type: 'background',\n        name: 'background',\n        inputPath: resolve(config.entrypointsDir, 'background/index.ts'),\n        outputDir: config.outDir,\n        skipped: false,\n      },\n    ],\n  ])(\n    'should find and load background entrypoint config from %s',\n    async (path, expected) => {\n      const options = {\n        type: 'module',\n      } satisfies BackgroundEntrypointOptions;\n      globMock.mockResolvedValueOnce([path]);\n      importEntrypointsMock.mockResolvedValue([options]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toHaveLength(1);\n      expect(entrypoints[0]).toEqual({ ...expected, options });\n      expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]);\n    },\n  );\n\n  it.each<[string, string, SidepanelEntrypoint]>([\n    [\n      'sidepanel.html',\n      `\n        <html>\n          <head>\n            <title>Default Title</title>\n            <meta name=\"manifest.default_icon\" content=\"{ '16': '/icon/16.png' }\" />\n            <meta name=\"manifest.open_at_install\" content=\"true\" />\n          </head>\n        </html>\n      `,\n      {\n        type: 'sidepanel',\n        name: 'sidepanel',\n        inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),\n        outputDir: config.outDir,\n        options: {\n          defaultTitle: 'Default Title',\n          defaultIcon: { '16': '/icon/16.png' },\n          openAtInstall: true,\n        },\n        skipped: false,\n      },\n    ],\n    [\n      'sidepanel/index.html',\n      `<html></html>`,\n      {\n        type: 'sidepanel',\n        name: 'sidepanel',\n        inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),\n        options: {},\n        outputDir: config.outDir,\n        skipped: false,\n      },\n    ],\n    [\n      'named.sidepanel.html',\n      `<html></html>`,\n      {\n        type: 'sidepanel',\n        name: 'named',\n        inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),\n        options: {},\n        outputDir: config.outDir,\n        skipped: false,\n      },\n    ],\n    [\n      'named.sidepanel/index.html',\n      `<html></html>`,\n      {\n        type: 'sidepanel',\n        name: 'named',\n        inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n  ])(\n    'should find and load sidepanel entrypoint config from %s',\n    async (path, content, expected) => {\n      globMock.mockResolvedValueOnce([path]);\n      readFileMock.mockResolvedValueOnce(content);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toHaveLength(1);\n      expect(entrypoints[0]).toEqual(expected);\n    },\n  );\n\n  it('should remove type=module from MV2 background scripts', async () => {\n    setFakeWxt({\n      config: {\n        manifestVersion: 2,\n      },\n      builder: wxt.builder,\n    });\n    const options = {\n      type: 'module',\n    } satisfies BackgroundEntrypointOptions;\n    globMock.mockResolvedValueOnce(['background.ts']);\n    importEntrypointsMock.mockResolvedValue([options]);\n\n    const entrypoints = await findEntrypoints();\n\n    expect(entrypoints[0].options).toEqual({});\n  });\n\n  it('should allow type=module for MV3 background service workers', async () => {\n    setFakeWxt({\n      config: {\n        manifestVersion: 3,\n      },\n      builder: wxt.builder,\n    });\n    const options = {\n      type: 'module',\n    } satisfies BackgroundEntrypointOptions;\n    globMock.mockResolvedValueOnce(['background.ts']);\n    importEntrypointsMock.mockResolvedValue([options]);\n\n    const entrypoints = await findEntrypoints();\n\n    expect(entrypoints[0].options).toEqual(options);\n  });\n\n  it(\"should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user\", async () => {\n    setFakeWxt({\n      config: {\n        ...config,\n        command: 'serve',\n      },\n      builder: wxt.builder,\n    });\n    globMock.mockResolvedValueOnce(['popup.html']);\n\n    const entrypoints = await findEntrypoints();\n\n    expect(entrypoints).toHaveLength(2);\n    expect(entrypoints).toContainEqual({\n      type: 'background',\n      inputPath: 'virtual:user-background',\n      name: 'background',\n      options: {},\n      outputDir: config.outDir,\n      skipped: false,\n    });\n  });\n\n  it.each<string>([\n    'injected.ts',\n    'injected.tsx',\n    'injected.js',\n    'injected.jsx',\n    'injected/index.ts',\n    'injected/index.tsx',\n    'injected/index.js',\n    'injected/index.jsx',\n  ])(\n    'should find and load unlisted-script entrypoint config from %s',\n    async (path) => {\n      const expected = {\n        type: 'unlisted-script',\n        name: 'injected',\n        inputPath: resolve(config.entrypointsDir, path),\n        outputDir: config.outDir,\n        skipped: false,\n      };\n      const options = {} satisfies BaseEntrypointOptions;\n      globMock.mockResolvedValueOnce([path]);\n      importEntrypointsMock.mockResolvedValue([options]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toHaveLength(1);\n      expect(entrypoints[0]).toEqual({ ...expected, options });\n      expect(importEntrypointsMock).toBeCalledWith([expected.inputPath]);\n    },\n  );\n\n  it.each<[string, GenericEntrypoint]>([\n    // Sandbox\n    [\n      'sandbox.html',\n      {\n        type: 'sandbox',\n        name: 'sandbox',\n        inputPath: resolve(config.entrypointsDir, 'sandbox.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'sandbox/index.html',\n      {\n        type: 'sandbox',\n        name: 'sandbox',\n        inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'named.sandbox.html',\n      {\n        type: 'sandbox',\n        name: 'named',\n        inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'named.sandbox/index.html',\n      {\n        type: 'sandbox',\n        name: 'named',\n        inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // bookmarks\n    [\n      'bookmarks.html',\n      {\n        type: 'bookmarks',\n        name: 'bookmarks',\n        inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'bookmarks/index.html',\n      {\n        type: 'bookmarks',\n        name: 'bookmarks',\n        inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // history\n    [\n      'history.html',\n      {\n        type: 'history',\n        name: 'history',\n        inputPath: resolve(config.entrypointsDir, 'history.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'history/index.html',\n      {\n        type: 'history',\n        name: 'history',\n        inputPath: resolve(config.entrypointsDir, 'history/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // newtab\n    [\n      'newtab.html',\n      {\n        type: 'newtab',\n        name: 'newtab',\n        inputPath: resolve(config.entrypointsDir, 'newtab.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'newtab/index.html',\n      {\n        type: 'newtab',\n        name: 'newtab',\n        inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // devtools\n    [\n      'devtools.html',\n      {\n        type: 'devtools',\n        name: 'devtools',\n        inputPath: resolve(config.entrypointsDir, 'devtools.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'devtools/index.html',\n      {\n        type: 'devtools',\n        name: 'devtools',\n        inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // unlisted-page\n    [\n      'onboarding.html',\n      {\n        type: 'unlisted-page',\n        name: 'onboarding',\n        inputPath: resolve(config.entrypointsDir, 'onboarding.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'onboarding/index.html',\n      {\n        type: 'unlisted-page',\n        name: 'onboarding',\n        inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // unlisted-style\n    [\n      'iframe.scss',\n      {\n        type: 'unlisted-style',\n        name: 'iframe',\n        inputPath: resolve(config.entrypointsDir, 'iframe.scss'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'iframe.css',\n      {\n        type: 'unlisted-style',\n        name: 'iframe',\n        inputPath: resolve(config.entrypointsDir, 'iframe.css'),\n        outputDir: config.outDir,\n        options: {},\n        skipped: false,\n      },\n    ],\n\n    // content-script-style\n    [\n      'content.css',\n      {\n        type: 'content-script-style',\n        name: 'content',\n        inputPath: resolve(config.entrypointsDir, 'content.css'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'overlay.content.css',\n      {\n        type: 'content-script-style',\n        name: 'overlay',\n        inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'content/index.css',\n      {\n        type: 'content-script-style',\n        name: 'content',\n        inputPath: resolve(config.entrypointsDir, 'content/index.css'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        options: {},\n        skipped: false,\n      },\n    ],\n    [\n      'overlay.content/index.css',\n      {\n        type: 'content-script-style',\n        name: 'overlay',\n        inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),\n        outputDir: resolve(config.outDir, 'content-scripts'),\n        options: {},\n        skipped: false,\n      },\n    ],\n  ])('should find entrypoint for %s', async (path, expected) => {\n    globMock.mockResolvedValueOnce([path]);\n\n    const entrypoints = await findEntrypoints();\n\n    expect(entrypoints).toHaveLength(1);\n    expect(entrypoints[0]).toEqual(expected);\n  });\n\n  it('should ignore other index files in the same directory when index.html exists', async () => {\n    globMock.mockResolvedValueOnce([\n      'content/index.ts',\n      'popup/index.html',\n      'popup/index.ts',\n      'popup/index.css',\n    ]);\n\n    const entrypoints = await findEntrypoints();\n\n    expect(entrypoints).toHaveLength(2);\n    expect(entrypoints[0]).toMatchObject({\n      type: 'content-script',\n      name: 'content',\n    });\n    expect(entrypoints[1]).toMatchObject({\n      type: 'popup',\n      name: 'popup',\n    });\n  });\n\n  it('should not allow a file entrypoint and directory entrypoint to have the same name', async () => {\n    globMock.mockResolvedValueOnce([\n      'popup.html',\n      'popup/index.html',\n      'popup/index.ts',\n      'other.ts',\n      'other/index.ts',\n    ]);\n\n    await expect(() => findEntrypoints()).rejects.toThrowError(\n      [\n        'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',\n        '',\n        '- other',\n        `  - ${unnormalizePath('src/entrypoints/other.ts')}`,\n        `  - ${unnormalizePath('src/entrypoints/other/index.ts')}`,\n        '- popup',\n        `  - ${unnormalizePath('src/entrypoints/popup.html')}`,\n        `  - ${unnormalizePath('src/entrypoints/popup/index.html')}`,\n      ].join('\\n'),\n    );\n  });\n\n  it('throw an error if there are no entrypoints', async () => {\n    globMock.mockResolvedValueOnce([]);\n\n    await expect(() => findEntrypoints()).rejects.toThrowError(\n      `No entrypoints found in ${unnormalizePath(config.entrypointsDir)}`,\n    );\n  });\n\n  describe('include option', () => {\n    it(\"should mark the background as skipped when include doesn't contain the target browser\", async () => {\n      globMock.mockResolvedValueOnce(['background.ts']);\n      importEntrypointsMock.mockResolvedValue([\n        { include: ['not' + config.browser] },\n      ]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'background',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it(\"should mark content scripts as skipped when include doesn't contain the target browser\", async () => {\n      globMock.mockResolvedValueOnce(['example.content.ts']);\n      importEntrypointsMock.mockResolvedValue([\n        { include: ['not' + config.browser] },\n      ]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'example',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it(\"should mark the popup as skipped when include doesn't contain the target browser\", async () => {\n      globMock.mockResolvedValueOnce(['popup.html']);\n      readFileMock.mockResolvedValueOnce(\n        `<html>\n          <head>\n            <meta name=\"manifest.include\" content=\"['${\n              'not' + config.browser\n            }']\" />\n          </head>\n        </html>`,\n      );\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'popup',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it(\"should mark the options page as skipped when include doesn't contain the target browser\", async () => {\n      globMock.mockResolvedValueOnce(['options.html']);\n      readFileMock.mockResolvedValueOnce(\n        `<html>\n          <head>\n            <meta name=\"manifest.include\" content=\"['${\n              'not' + config.browser\n            }']\" />\n          </head>\n        </html>`,\n      );\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'options',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it(\"should mark unlisted pages as skipped when include doesn't contain the target browser\", async () => {\n      globMock.mockResolvedValueOnce(['unlisted.html']);\n      readFileMock.mockResolvedValueOnce(\n        `<html>\n          <head>\n            <meta name=\"manifest.include\" content=\"['${\n              'not' + config.browser\n            }']\" />\n          </head>\n        </html>`,\n      );\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'unlisted',\n          skipped: true,\n        }),\n      ]);\n    });\n  });\n\n  describe('exclude option', () => {\n    it('should mark the background as skipped when exclude contains the target browser', async () => {\n      globMock.mockResolvedValueOnce(['background.ts']);\n      importEntrypointsMock.mockResolvedValue([{ exclude: [config.browser] }]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'background',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it('should mark content scripts as skipped when exclude contains the target browser', async () => {\n      globMock.mockResolvedValueOnce(['example.content.ts']);\n      importEntrypointsMock.mockResolvedValue([{ exclude: [config.browser] }]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'example',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it('should mark the popup as skipped when exclude contains the target browser', async () => {\n      globMock.mockResolvedValueOnce(['popup.html']);\n      readFileMock.mockResolvedValueOnce(\n        `<html>\n          <head>\n            <meta name=\"manifest.exclude\" content=\"['${config.browser}']\" />\n          </head>\n        </html>`,\n      );\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'popup',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it('should mark the options page as skipped when exclude contains the target browser', async () => {\n      globMock.mockResolvedValueOnce(['options.html']);\n      readFileMock.mockResolvedValueOnce(\n        `<html>\n          <head>\n            <meta name=\"manifest.exclude\" content=\"['${config.browser}']\" />\n          </head>\n        </html>`,\n      );\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'options',\n          skipped: true,\n        }),\n      ]);\n    });\n\n    it('should mark unlisted pages as skipped when exclude contains the target browser', async () => {\n      globMock.mockResolvedValueOnce(['unlisted.html']);\n      readFileMock.mockResolvedValueOnce(\n        `<html>\n          <head>\n            <meta name=\"manifest.exclude\" content=\"['${config.browser}']\" />\n          </head>\n        </html>`,\n      );\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'unlisted',\n          skipped: true,\n        }),\n      ]);\n    });\n  });\n\n  describe('filterEntrypoints option', () => {\n    it('should override include/exclude of individual entrypoint options', async () => {\n      globMock.mockResolvedValue([\n        'options/index.html',\n        'popup/index.html',\n        'ui.content/index.ts',\n        'injected.content/index.ts',\n      ]);\n      const filterEntrypoints = ['popup', 'ui'];\n      setFakeWxt({\n        config: {\n          root: '/',\n          entrypointsDir: resolve('/src/entrypoints'),\n          outDir: resolve('.output'),\n          command: 'build',\n          filterEntrypoints: new Set(filterEntrypoints),\n        },\n        builder: wxt.builder,\n      });\n\n      importEntrypointsMock.mockResolvedValue([{}]);\n\n      const entrypoints = await findEntrypoints();\n\n      expect(entrypoints).toEqual([\n        expect.objectContaining({\n          name: 'injected',\n          skipped: true,\n        }),\n        expect.objectContaining({\n          name: 'options',\n          skipped: true,\n        }),\n        expect.objectContaining({\n          name: 'popup',\n          skipped: false,\n        }),\n        expect.objectContaining({\n          name: 'ui',\n          skipped: false,\n        }),\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/group-entrypoints.test.ts",
    "content": "import { describe, expect, it } from 'vitest';\nimport { Entrypoint } from '../../../../types';\nimport { groupEntrypoints } from '../group-entrypoints';\nimport {\n  fakeBackgroundEntrypoint,\n  fakeGenericEntrypoint,\n  fakePopupEntrypoint,\n} from '../../testing/fake-objects';\n\nconst background: Entrypoint = {\n  type: 'background',\n  name: 'background',\n  inputPath: '/background.ts',\n  outputDir: '/.output/background',\n  options: {},\n  skipped: false,\n};\nconst contentScript: Entrypoint = {\n  type: 'content-script',\n  name: 'overlay',\n  inputPath: '/overlay.content.ts',\n  outputDir: '/.output/content-scripts/overlay',\n  options: {\n    matches: ['<all_urls>'],\n  },\n  skipped: false,\n};\nconst unlistedScript: Entrypoint = {\n  type: 'unlisted-script',\n  name: 'injected',\n  inputPath: '/injected.ts',\n  outputDir: '/.output/injected',\n  options: {},\n  skipped: false,\n};\nconst popup: Entrypoint = {\n  type: 'popup',\n  name: 'popup',\n  inputPath: '/popup.html',\n  outputDir: '/.output/popup',\n  options: {},\n  skipped: false,\n};\nconst unlistedPage: Entrypoint = {\n  type: 'unlisted-page',\n  name: 'onboarding',\n  inputPath: '/onboarding.html',\n  outputDir: '/.output/onboarding',\n  options: {},\n  skipped: false,\n};\nconst options: Entrypoint = {\n  type: 'options',\n  name: 'options',\n  inputPath: '/options.html',\n  outputDir: '/.output/options',\n  options: {},\n  skipped: false,\n};\nconst sandbox1: Entrypoint = {\n  type: 'sandbox',\n  name: 'sandbox',\n  inputPath: '/sandbox1.html',\n  outputDir: '/.output/sandbox1',\n  options: {},\n  skipped: false,\n};\nconst sandbox2: Entrypoint = {\n  type: 'sandbox',\n  name: 'sandbox2',\n  inputPath: '/sandbox2.html',\n  outputDir: '/.output/sandbox2',\n  options: {},\n  skipped: false,\n};\nconst unlistedStyle: Entrypoint = {\n  type: 'unlisted-style',\n  name: 'injected',\n  inputPath: '/injected.scss',\n  outputDir: '/.output',\n  options: {},\n  skipped: false,\n};\nconst contentScriptStyle: Entrypoint = {\n  type: 'content-script-style',\n  name: 'injected',\n  inputPath: '/overlay.content.scss',\n  outputDir: '/.output/content-scripts',\n  options: {},\n  skipped: false,\n};\n\ndescribe('groupEntrypoints', () => {\n  it('should keep scripts separate', () => {\n    const entrypoints: Entrypoint[] = [\n      contentScript,\n      background,\n      unlistedScript,\n      popup,\n    ];\n    const expected = [contentScript, background, unlistedScript, [popup]];\n\n    const actual = groupEntrypoints(entrypoints);\n\n    expect(actual).toEqual(expected);\n  });\n\n  it('should keep styles separate', () => {\n    const entrypoints: Entrypoint[] = [\n      unlistedStyle,\n      contentScriptStyle,\n      popup,\n    ];\n    const expected = [unlistedStyle, contentScriptStyle, [popup]];\n\n    const actual = groupEntrypoints(entrypoints);\n\n    expect(actual).toEqual(expected);\n  });\n\n  it('should group extension pages together', () => {\n    const entrypoints: Entrypoint[] = [\n      popup,\n      background,\n      unlistedPage,\n      options,\n      sandbox1,\n    ];\n    const expected = [[popup, unlistedPage, options], background, [sandbox1]];\n\n    const actual = groupEntrypoints(entrypoints);\n\n    expect(actual).toEqual(expected);\n  });\n\n  it('should group sandbox pages together', () => {\n    const entrypoints: Entrypoint[] = [\n      sandbox1,\n      popup,\n      sandbox2,\n      contentScript,\n    ];\n    const expected = [[sandbox1, sandbox2], [popup], contentScript];\n\n    const actual = groupEntrypoints(entrypoints);\n\n    expect(actual).toEqual(expected);\n  });\n\n  it('should group ESM compatible scripts with extension pages', () => {\n    const background = fakeBackgroundEntrypoint({\n      options: {\n        type: 'module',\n      },\n      skipped: false,\n    });\n    const popup = fakePopupEntrypoint({\n      skipped: false,\n    });\n    const sandbox = fakeGenericEntrypoint({\n      inputPath: '/entrypoints/sandbox.html',\n      name: 'sandbox',\n      type: 'sandbox',\n      skipped: false,\n    });\n\n    const actual = groupEntrypoints([background, popup, sandbox]);\n\n    expect(actual).toEqual([[background, popup], [sandbox]]);\n  });\n\n  it('should exclude skipped entrypoints from the groups to build', () => {\n    const background = fakeBackgroundEntrypoint({\n      options: {\n        type: 'module',\n      },\n      skipped: false,\n    });\n    const popup = fakePopupEntrypoint({\n      skipped: true,\n    });\n\n    const actual = groupEntrypoints([background, popup]);\n\n    expect(actual).toEqual([[background]]);\n  });\n\n  it.todo(\n    'should group ESM compatible sandbox scripts with sandbox pages',\n    () => {\n      // Main world content scripts\n    },\n  );\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts",
    "content": "import { defineBackground } from '../../../../../utils/define-background';\n\nexport default defineBackground({\n  main() {},\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts",
    "content": "import { defineContentScript } from '../../../../../utils/define-content-script';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  main() {},\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts",
    "content": "import { defineContentScript } from '../../../../../utils/define-content-script';\nimport { faker } from '@faker-js/faker';\n\nexport default defineContentScript({\n  matches: [faker.string.nanoid()],\n  main() {},\n});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/no-default-export.ts",
    "content": ""
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx",
    "content": "import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script';\n\nexport default defineUnlistedScript(() => {});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts",
    "content": "import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script';\n\nexport default defineUnlistedScript(() => {});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts",
    "content": "import { defineBackground } from '../../../../../utils/define-background';\n\nexport const a = {};\n\nexport default defineBackground(() => {});\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/build-entrypoints.ts",
    "content": "import {\n  BuildOutput,\n  BuildStepOutput,\n  EntrypointGroup,\n  ResolvedPublicFile,\n} from '../../../types';\nimport { getPublicFiles } from '../fs';\nimport { copyFile, mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, resolve } from 'path';\nimport type { Spinner } from 'nanospinner';\nimport pc from 'picocolors';\nimport { wxt } from '../../wxt';\nimport { toArray } from '../arrays';\n\nexport async function buildEntrypoints(\n  groups: EntrypointGroup[],\n  spinner: Spinner,\n): Promise<Omit<BuildOutput, 'manifest'>> {\n  const steps: BuildStepOutput[] = [];\n  for (let i = 0; i < groups.length; i++) {\n    const group = groups[i];\n    const groupNames = toArray(group).map((e) => e.name);\n    const groupNameColored = groupNames.join(pc.dim(', '));\n    spinner.update({\n      text: pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNameColored}`,\n    });\n    try {\n      steps.push(await wxt.builder.build(group));\n    } catch (err) {\n      spinner.stop();\n      spinner.clear();\n      wxt.logger.error(err);\n      throw Error(`Failed to build ${groupNames.join(', ')}`, { cause: err });\n    }\n  }\n  const publicAssets = await copyPublicDirectory();\n\n  return { publicAssets, steps };\n}\n\nasync function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {\n  const files = (await getPublicFiles()).map<ResolvedPublicFile>((file) => ({\n    absoluteSrc: resolve(wxt.config.publicDir, file),\n    relativeDest: file,\n  }));\n  await wxt.hooks.callHook('build:publicAssets', wxt, files);\n  if (files.length === 0) return [];\n\n  const publicAssets: BuildOutput['publicAssets'] = [];\n  for (const file of files) {\n    const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);\n\n    await mkdir(dirname(absoluteDest), { recursive: true });\n    if ('absoluteSrc' in file) {\n      await copyFile(file.absoluteSrc, absoluteDest);\n    } else {\n      await writeFile(absoluteDest, file.contents, 'utf8');\n    }\n    publicAssets.push({\n      type: 'asset',\n      fileName: file.relativeDest,\n    });\n  }\n\n  return publicAssets;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/detect-dev-changes.ts",
    "content": "import {\n  BuildOutput,\n  BuildStepOutput,\n  EntrypointGroup,\n  OutputFile,\n} from '../../../types';\nimport { every, some } from '../arrays';\nimport { normalizePath } from '../paths';\nimport { wxt } from '../../wxt';\n\n/**\n * Compare the changed files vs the build output and determine what kind of\n * reload needs to happen:\n *\n * - Do nothing\n *\n *   - CSS or JS file associated with an HTML page is changed - this is handled\n *       automatically by the dev server\n *   - Change isn't used by any of the entrypoints\n * - Reload Content script\n *\n *   - CSS or JS file associated with a content script\n *   - Background script will be told to reload the content script\n * - Reload HTML file\n *\n *   - HTML file itself is saved - HMR doesn't handle this because the HTML pages\n *       are pre-rendered\n *   - Chrome is OK reloading the page when the HTML file is changed without\n *       reloading the whole extension. Not sure about firefox, this might need\n *       to change to an extension reload\n * - Reload extension\n *\n *   - Background script is changed\n *   - Manifest is different\n * - Restart browser\n *\n *   - Web-ext.config.ts (runner config changes)\n * - Full dev server restart\n *\n *   - Wxt.config.ts (main config file)\n *   - Modules/* (any file related to WXT modules)\n *   - .env (environment variable changed could effect build)\n */\nexport function detectDevChanges(\n  changedFiles: string[],\n  currentOutput: BuildOutput,\n): DevModeChange {\n  const isConfigChange = some(\n    changedFiles,\n    (file) => file === wxt.config.userConfigMetadata.configFile,\n  );\n  if (isConfigChange) return { type: 'full-restart' };\n\n  const isWxtModuleChange = some(changedFiles, (file) =>\n    file.startsWith(wxt.config.modulesDir),\n  );\n  if (isWxtModuleChange) return { type: 'full-restart' };\n\n  const isRunnerChange = some(\n    changedFiles,\n    (file) => file === wxt.config.runnerConfig.configFile,\n  );\n  if (isRunnerChange) return { type: 'browser-restart' };\n\n  const changedSteps = new Set(\n    changedFiles.flatMap((changedFile) =>\n      findEffectedSteps(changedFile, currentOutput),\n    ),\n  );\n  if (changedSteps.size === 0) {\n    const hasPublicChange = some(changedFiles, (file) =>\n      file.startsWith(wxt.config.publicDir),\n    );\n    if (hasPublicChange) {\n      return {\n        type: 'extension-reload',\n        rebuildGroups: [],\n        cachedOutput: currentOutput,\n      };\n    } else {\n      return { type: 'no-change' };\n    }\n  }\n\n  const unchangedOutput: BuildOutput = {\n    manifest: currentOutput.manifest,\n    steps: [],\n    publicAssets: [...currentOutput.publicAssets],\n  };\n  const changedOutput: BuildOutput = {\n    manifest: currentOutput.manifest,\n    steps: [],\n    publicAssets: [],\n  };\n\n  for (const step of currentOutput.steps) {\n    if (changedSteps.has(step)) {\n      changedOutput.steps.push(step);\n    } else {\n      unchangedOutput.steps.push(step);\n    }\n  }\n\n  const isOnlyHtmlChanges =\n    changedFiles.length > 0 &&\n    every(changedFiles, (file) => file.endsWith('.html'));\n  if (isOnlyHtmlChanges) {\n    return {\n      type: 'html-reload',\n      cachedOutput: unchangedOutput,\n      rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),\n    };\n  }\n\n  const isOnlyContentScripts =\n    changedOutput.steps.length > 0 &&\n    every(\n      changedOutput.steps.flatMap((step) => step.entrypoints),\n      (entry) => entry.type === 'content-script',\n    );\n  if (isOnlyContentScripts) {\n    return {\n      type: 'content-script-reload',\n      cachedOutput: unchangedOutput,\n      changedSteps: changedOutput.steps,\n      rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),\n    };\n  }\n\n  return {\n    type: 'extension-reload',\n    cachedOutput: unchangedOutput,\n    rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),\n  };\n}\n\n/**\n * For a single change, return all the step of the build output that were\n * effected by it.\n */\nfunction findEffectedSteps(\n  changedFile: string,\n  currentOutput: BuildOutput,\n): BuildStepOutput[] {\n  const changes: BuildStepOutput[] = [];\n  const changedPath = normalizePath(changedFile);\n\n  const isChunkEffected = (chunk: OutputFile): boolean => {\n    switch (chunk.type) {\n      // If it's an HTML file with the same path, is is effected because HTML files need to be re-rendered\n      // - fileName is normalized, relative bundle path, \"<entrypoint-name>.html\"\n      case 'asset': {\n        return changedPath\n          .replace('/index.html', '.html')\n          .endsWith(chunk.fileName);\n      }\n      // If it's a chunk that depends on the changed file, it is effected\n      // - moduleIds are absolute, normalized paths\n      case 'chunk': {\n        const modulePaths = chunk.moduleIds.map((path) => path.split('?')[0]);\n        return modulePaths.includes(changedPath);\n      }\n      default: {\n        return false;\n      }\n    }\n  };\n\n  for (const step of currentOutput.steps) {\n    const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));\n    if (effectedChunk) changes.push(step);\n  }\n\n  return changes;\n}\n\n/**\n * Contains information about what files changed, what needs rebuilt, and the\n * type of reload that is required.\n */\nexport type DevModeChange =\n  | NoChange\n  | HtmlReload\n  | ExtensionReload\n  | ContentScriptReload\n  | FullRestart\n  | BrowserRestart;\n\ninterface NoChange {\n  type: 'no-change';\n}\n\ninterface RebuildChange {\n  /** The list of entrypoints that need rebuilt. */\n  rebuildGroups: EntrypointGroup[];\n  /** The previous output stripped of any files are going to change. */\n  cachedOutput: BuildOutput;\n}\n\ninterface FullRestart {\n  type: 'full-restart';\n}\n\ninterface BrowserRestart {\n  type: 'browser-restart';\n}\n\ninterface HtmlReload extends RebuildChange {\n  type: 'html-reload';\n}\n\ninterface ExtensionReload extends RebuildChange {\n  type: 'extension-reload';\n}\n\ninterface ContentScriptReload extends RebuildChange {\n  type: 'content-script-reload';\n  changedSteps: BuildStepOutput[];\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/find-entrypoints.ts",
    "content": "import { relative, resolve } from 'path';\nimport {\n  BackgroundEntrypoint,\n  ContentScriptEntrypoint,\n  Entrypoint,\n  EntrypointInfo,\n  GenericEntrypoint,\n  OptionsEntrypoint,\n  PopupEntrypoint,\n  SidepanelEntrypoint,\n  MainWorldContentScriptEntrypointOptions,\n  IsolatedWorldContentScriptEntrypointOptions,\n  UnlistedScriptEntrypoint,\n} from '../../../types';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport picomatch from 'picomatch';\nimport { parseHTML } from 'linkedom';\nimport JSON5 from 'json5';\nimport { glob } from 'tinyglobby';\nimport {\n  getEntrypointName,\n  isHtmlEntrypoint,\n  isJsEntrypoint,\n  resolvePerBrowserOptions,\n} from '../entrypoints';\nimport { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../constants';\nimport { CSS_EXTENSIONS_PATTERN } from '../paths';\nimport pc from 'picocolors';\nimport { wxt } from '../../wxt';\nimport { camelCase } from 'scule';\n\n/**\n * Return entrypoints and their configuration by looking through the project's\n * files.\n */\nexport async function findEntrypoints(): Promise<Entrypoint[]> {\n  // Make sure required TSConfig file exists to load dependencies\n  await mkdir(wxt.config.wxtDir, { recursive: true });\n  try {\n    await writeFile(\n      resolve(wxt.config.wxtDir, 'tsconfig.json'),\n      JSON.stringify({}),\n      { flag: 'wx' },\n    );\n  } catch (err) {\n    if (!(err instanceof Error) || !('code' in err) || err.code !== 'EEXIST') {\n      throw err;\n    }\n  }\n\n  const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), {\n    cwd: wxt.config.entrypointsDir,\n    expandDirectories: false,\n  });\n  // Ensure consistent output\n  relativePaths.sort();\n\n  const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);\n  const entrypointInfos: EntrypointInfo[] = relativePaths\n    .reduce<EntrypointInfo[]>((results, relativePath) => {\n      const inputPath = resolve(wxt.config.entrypointsDir, relativePath);\n      const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);\n      const matchingGlob = pathGlobs.find((glob) =>\n        picomatch(glob)(relativePath),\n      );\n      if (matchingGlob) {\n        const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];\n        results.push({ name, inputPath, type });\n      }\n      return results;\n    }, [])\n    .filter(({ name, inputPath }, _, entrypointInfos) => {\n      // Remove <name>/index.* if <name>/index.html exists\n\n      if (inputPath.endsWith('.html')) return true;\n      const isIndexFile = /index\\..+$/.test(inputPath);\n      if (!isIndexFile) return true;\n\n      const hasIndexHtml = entrypointInfos.some(\n        (entry) =>\n          entry.name === name && entry.inputPath.endsWith('index.html'),\n      );\n\n      return !hasIndexHtml;\n    });\n\n  await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);\n\n  // Validation\n  preventNoEntrypoints(entrypointInfos);\n  preventDuplicateEntrypointNames(entrypointInfos);\n\n  // Import entrypoints to get their config\n  let hasBackground = false;\n  const entrypointOptions = await importEntrypoints(entrypointInfos);\n  const entrypointsWithoutSkipped: Entrypoint[] = await Promise.all(\n    entrypointInfos.map(async (info): Promise<Entrypoint> => {\n      const { type } = info;\n      const options = entrypointOptions[info.inputPath] ?? {};\n      switch (type) {\n        case 'popup':\n          return await getPopupEntrypoint(info, options);\n        case 'sidepanel':\n          return await getSidepanelEntrypoint(info, options);\n        case 'options':\n          return await getOptionsEntrypoint(info, options);\n        case 'background':\n          hasBackground = true;\n          return await getBackgroundEntrypoint(info, options);\n        case 'content-script':\n          return await getContentScriptEntrypoint(info, options);\n        case 'unlisted-page':\n          return await getUnlistedPageEntrypoint(info, options);\n        case 'unlisted-script':\n          return await getUnlistedScriptEntrypoint(info, options);\n        case 'content-script-style':\n          return {\n            ...info,\n            type,\n            outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),\n            options,\n          };\n        default:\n          return {\n            ...info,\n            type,\n            outputDir: wxt.config.outDir,\n            options,\n          };\n      }\n    }),\n  );\n\n  if (wxt.config.command === 'serve' && !hasBackground) {\n    entrypointsWithoutSkipped.push(\n      await getBackgroundEntrypoint(\n        {\n          inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,\n          name: 'background',\n          type: 'background',\n        },\n        {},\n      ),\n    );\n  }\n\n  // Mark entrypoints as skipped or not\n  const entrypoints = entrypointsWithoutSkipped.map((entry) => ({\n    ...entry,\n    skipped: isEntrypointSkipped(entry),\n  }));\n\n  await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints);\n\n  wxt.logger.debug('All entrypoints:', entrypoints);\n  const skippedEntrypointNames = entrypoints\n    .filter((item) => item.skipped)\n    .map((item) => item.name);\n  if (skippedEntrypointNames.length) {\n    wxt.logger.warn(\n      [\n        'The following entrypoints have been skipped:',\n        ...skippedEntrypointNames.map(\n          (item) => `${pc.dim('-')} ${pc.cyan(item)}`,\n        ),\n      ].join('\\n'),\n    );\n  }\n\n  return entrypoints;\n}\n\n/** Returns a map of input paths to the file's options. */\nasync function importEntrypoints(infos: EntrypointInfo[]) {\n  const resMap: Record<string, Record<string, any> | undefined> = {};\n\n  const htmlInfos = infos.filter((info) => isHtmlEntrypoint(info));\n  const jsInfos = infos.filter((info) => isJsEntrypoint(info));\n\n  await Promise.all([\n    // HTML\n    ...htmlInfos.map(async (info) => {\n      resMap[info.inputPath] = await importHtmlEntrypoint(info);\n    }),\n    // JS\n    (async () => {\n      const res = await wxt.builder.importEntrypoints(\n        jsInfos.map((info) => info.inputPath),\n      );\n      res.forEach((res, i) => {\n        resMap[jsInfos[i].inputPath] = res;\n      });\n    })(),\n    // CSS - never has options\n  ]);\n\n  return resMap;\n}\n\n/**\n * Extract `manifest.` and `wxt.` options from meta tags, converting snake_case\n * keys to camelCase\n */\nasync function importHtmlEntrypoint(\n  info: EntrypointInfo,\n): Promise<Record<string, any>> {\n  const content = await readFile(info.inputPath, 'utf-8');\n  const { document } = parseHTML(content);\n\n  const metaTags = document.querySelectorAll('meta');\n  const res: Record<string, any> = {\n    title: document.querySelector('title')?.textContent || undefined,\n  };\n\n  metaTags.forEach((tag) => {\n    const name = tag.name;\n    let key: string;\n\n    if (name.startsWith('manifest.')) {\n      key = camelCase(name.slice(9));\n    } else if (name.startsWith('wxt.')) {\n      key = camelCase(name.slice(4));\n    } else {\n      return;\n    }\n\n    try {\n      res[key] = JSON5.parse(tag.content);\n    } catch {\n      res[key] = tag.content;\n    }\n  });\n\n  return res;\n}\n\nfunction preventDuplicateEntrypointNames(files: EntrypointInfo[]) {\n  const namesToPaths = files.reduce<Record<string, string[]>>(\n    (map, { name, inputPath }) => {\n      map[name] ??= [];\n      map[name].push(inputPath);\n      return map;\n    },\n    {},\n  );\n  const errorLines = Object.entries(namesToPaths).reduce<string[]>(\n    (lines, [name, absolutePaths]) => {\n      if (absolutePaths.length > 1) {\n        lines.push(`- ${name}`);\n        absolutePaths.forEach((absolutePath) => {\n          lines.push(`  - ${relative(wxt.config.root, absolutePath)}`);\n        });\n      }\n      return lines;\n    },\n    [],\n  );\n  if (errorLines.length > 0) {\n    const errorContent = errorLines.join('\\n');\n    throw Error(\n      `Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.\\n\\n${errorContent}`,\n    );\n  }\n}\n\nfunction preventNoEntrypoints(files: EntrypointInfo[]) {\n  if (files.length === 0) {\n    throw Error(`No entrypoints found in ${wxt.config.entrypointsDir}`);\n  }\n}\n\nasync function getPopupEntrypoint(\n  info: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<PopupEntrypoint> {\n  // Extract non-per-browser options\n  const { themeIcons, title, type, ...perBrowserOptions } = options;\n\n  const strictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions(\n    {\n      ...perBrowserOptions,\n      defaultTitle: title,\n      mv2Key: type,\n    },\n    wxt.config.browser,\n  );\n  if (strictOptions.mv2Key && strictOptions.mv2Key !== 'page_action')\n    strictOptions.mv2Key = 'browser_action';\n\n  return {\n    type: 'popup',\n    name: 'popup',\n    options: { ...strictOptions, themeIcons },\n    inputPath: info.inputPath,\n    outputDir: wxt.config.outDir,\n  };\n}\n\nasync function getOptionsEntrypoint(\n  info: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<OptionsEntrypoint> {\n  return {\n    type: 'options',\n    name: 'options',\n    options: resolvePerBrowserOptions(options, wxt.config.browser),\n    inputPath: info.inputPath,\n    outputDir: wxt.config.outDir,\n  };\n}\n\nasync function getUnlistedPageEntrypoint(\n  info: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<GenericEntrypoint> {\n  return {\n    type: 'unlisted-page',\n    name: info.name,\n    inputPath: info.inputPath,\n    outputDir: wxt.config.outDir,\n    options,\n  };\n}\n\nasync function getUnlistedScriptEntrypoint(\n  { inputPath, name }: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<UnlistedScriptEntrypoint> {\n  return {\n    type: 'unlisted-script',\n    name,\n    inputPath,\n    outputDir: wxt.config.outDir,\n    options: resolvePerBrowserOptions(options, wxt.config.browser),\n  };\n}\n\nasync function getBackgroundEntrypoint(\n  { inputPath, name }: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<BackgroundEntrypoint> {\n  const strictOptions: BackgroundEntrypoint['options'] =\n    resolvePerBrowserOptions(options, wxt.config.browser);\n\n  if (wxt.config.manifestVersion !== 3) {\n    delete strictOptions.type;\n  }\n\n  return {\n    type: 'background',\n    name,\n    inputPath,\n    outputDir: wxt.config.outDir,\n    options: strictOptions,\n  };\n}\n\nasync function getContentScriptEntrypoint(\n  { inputPath, name }: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<ContentScriptEntrypoint> {\n  return {\n    type: 'content-script',\n    name,\n    inputPath,\n    outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),\n    options: resolvePerBrowserOptions(\n      options as\n        | MainWorldContentScriptEntrypointOptions\n        | IsolatedWorldContentScriptEntrypointOptions,\n      wxt.config.browser,\n    ),\n  };\n}\n\nasync function getSidepanelEntrypoint(\n  info: EntrypointInfo,\n  options: Record<string, any>,\n): Promise<SidepanelEntrypoint> {\n  // Extract non-per-browser options and rename title to defaultTitle\n  const { title, ...perBrowserOptions } = options;\n\n  return {\n    type: 'sidepanel',\n    name: info.name,\n    options: resolvePerBrowserOptions(\n      {\n        ...perBrowserOptions,\n        defaultTitle: title,\n      },\n      wxt.config.browser,\n    ),\n    inputPath: info.inputPath,\n    outputDir: wxt.config.outDir,\n  };\n}\n\nfunction isEntrypointSkipped(entry: Omit<Entrypoint, 'skipped'>): boolean {\n  if (wxt.config.filterEntrypoints != null) {\n    return !wxt.config.filterEntrypoints.has(entry.name);\n  }\n\n  const { include, exclude } = entry.options;\n  if (include?.length && exclude?.length) {\n    wxt.logger.warn(\n      `The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint skipped.`,\n    );\n    return true;\n  }\n\n  if (exclude?.length && !include?.length) {\n    return exclude.includes(wxt.config.browser);\n  }\n  if (include?.length && !exclude?.length) {\n    return !include.includes(wxt.config.browser);\n  }\n\n  return false;\n}\n\nconst PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {\n  'sandbox.html': 'sandbox',\n  'sandbox/index.html': 'sandbox',\n  '*.sandbox.html': 'sandbox',\n  '*.sandbox/index.html': 'sandbox',\n\n  'bookmarks.html': 'bookmarks',\n  'bookmarks/index.html': 'bookmarks',\n\n  'history.html': 'history',\n  'history/index.html': 'history',\n\n  'newtab.html': 'newtab',\n  'newtab/index.html': 'newtab',\n\n  'sidepanel.html': 'sidepanel',\n  'sidepanel/index.html': 'sidepanel',\n  '*.sidepanel.html': 'sidepanel',\n  '*.sidepanel/index.html': 'sidepanel',\n\n  'devtools.html': 'devtools',\n  'devtools/index.html': 'devtools',\n\n  'background.[jt]s': 'background',\n  'background/index.[jt]s': 'background',\n  [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',\n\n  'content.[jt]s?(x)': 'content-script',\n  'content/index.[jt]s?(x)': 'content-script',\n  '*.content.[jt]s?(x)': 'content-script',\n  '*.content/index.[jt]s?(x)': 'content-script',\n  [`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',\n  [`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',\n  [`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',\n  [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',\n\n  'popup.html': 'popup',\n  'popup/index.html': 'popup',\n\n  'options.html': 'options',\n  'options/index.html': 'options',\n\n  '*.html': 'unlisted-page',\n  '*/index.html': 'unlisted-page',\n  '*.[jt]s?(x)': 'unlisted-script',\n  '*/index.[jt]s?(x)': 'unlisted-script',\n  [`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',\n  [`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',\n};\n\nconst CONTENT_SCRIPT_OUT_DIR = 'content-scripts';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/group-entrypoints.ts",
    "content": "import { Entrypoint, EntrypointGroup } from '../../../types';\n\n/**\n * Entrypoints are built in groups. HTML pages can all be built together in a\n * single step, content scripts must be build individually, etc.\n *\n * This function returns the entrypoints put into these types of groups.\n */\nexport function groupEntrypoints(entrypoints: Entrypoint[]): EntrypointGroup[] {\n  const groupIndexMap: Partial<Record<Group, number>> = {};\n  const groups: EntrypointGroup[] = [];\n\n  for (const entry of entrypoints) {\n    if (entry.skipped) continue;\n\n    let group = ENTRY_TYPE_TO_GROUP_MAP[entry.type];\n    if (entry.type === 'background' && entry.options.type === 'module') {\n      group = 'esm';\n    }\n    if (group === 'individual') {\n      groups.push(entry);\n    } else {\n      let groupIndex = groupIndexMap[group];\n      if (groupIndex == null) {\n        groupIndex = groups.push([]) - 1;\n        groupIndexMap[group] = groupIndex;\n      }\n      (groups[groupIndex] as Entrypoint[]).push(entry);\n    }\n  }\n\n  return groups;\n}\n\nconst ENTRY_TYPE_TO_GROUP_MAP: Record<Entrypoint['type'], Group> = {\n  sandbox: 'sandboxed-esm',\n\n  popup: 'esm',\n  newtab: 'esm',\n  history: 'esm',\n  options: 'esm',\n  devtools: 'esm',\n  bookmarks: 'esm',\n  sidepanel: 'esm',\n  'unlisted-page': 'esm',\n\n  background: 'individual',\n  'content-script': 'individual',\n  'unlisted-script': 'individual',\n  'unlisted-style': 'individual',\n  'content-script-style': 'individual',\n};\n\ntype Group = 'esm' | 'sandboxed-esm' | 'individual';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/index.ts",
    "content": "export * from './build-entrypoints';\nexport * from './detect-dev-changes';\nexport * from './find-entrypoints';\nexport * from './group-entrypoints';\nexport * from './internal-build';\nexport * from './rebuild';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/internal-build.ts",
    "content": "import { findEntrypoints } from './find-entrypoints';\nimport { BuildOutput, Entrypoint } from '../../../types';\nimport pc from 'picocolors';\nimport { mkdir, rm } from 'node:fs/promises';\nimport { groupEntrypoints } from './group-entrypoints';\nimport { formatDuration } from '../time';\nimport { printBuildSummary } from '../log';\nimport { glob } from 'tinyglobby';\nimport { unnormalizePath } from '../paths';\nimport { rebuild } from './rebuild';\nimport { relative } from 'node:path';\nimport {\n  ValidationError,\n  ValidationResult,\n  ValidationResults,\n  validateEntrypoints,\n} from '../validation';\nimport { wxt } from '../../wxt';\nimport { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';\nimport { isCI } from 'ci-info';\n\n/**\n * Builds the extension based on an internal config. No more config discovery is\n * performed, the build is based on exactly what is passed in.\n *\n * This function:\n *\n * 1. Cleans the output directory\n * 2. Executes the rebuild function with a blank previous output so everything is\n *    built (see `rebuild` for more details)\n * 3. Prints the summary\n */\nexport async function internalBuild(): Promise<BuildOutput> {\n  await wxt.hooks.callHook('build:before', wxt);\n\n  const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';\n  const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;\n  wxt.logger.info(\n    `${verb} ${pc.cyan(target)} for ${pc.cyan(wxt.config.mode)} with ${pc.green(\n      `${wxt.builder.name} ${wxt.builder.version}`,\n    )}`,\n  );\n  const startTime = Date.now();\n\n  // Cleanup\n  await rm(wxt.config.outDir, { recursive: true, force: true });\n  await mkdir(wxt.config.outDir, { recursive: true });\n\n  const entrypoints = await findEntrypoints();\n  wxt.logger.debug('Detected entrypoints:', entrypoints);\n\n  const validationResults = validateEntrypoints(entrypoints);\n  if (validationResults.errorCount + validationResults.warningCount > 0) {\n    printValidationResults(validationResults);\n  }\n  if (validationResults.errorCount > 0) {\n    throw new ValidationError(`Entrypoint validation failed`, {\n      cause: validationResults,\n    });\n  }\n\n  const groups = groupEntrypoints(entrypoints);\n  await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);\n\n  const { output, warnings } = await rebuild(entrypoints, groups, undefined);\n  await wxt.hooks.callHook('build:done', wxt, output);\n\n  // Post-build\n  await printBuildSummary(\n    wxt.logger.success,\n    `Built extension in ${formatDuration(Date.now() - startTime)}`,\n    output,\n  );\n\n  for (const warning of warnings) {\n    wxt.logger.warn(...warning);\n  }\n\n  if (wxt.config.analysis.enabled) {\n    await combineAnalysisStats();\n    const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile);\n    wxt.logger.info(\n      `Analysis complete:\\n  ${pc.gray('└─')} ${pc.yellow(statsPath)}`,\n    );\n    if (wxt.config.analysis.open) {\n      if (isCI) {\n        wxt.logger.debug(`Skipped opening ${pc.yellow(statsPath)} in CI`);\n      } else {\n        wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`);\n        const { default: open } = await import('open');\n        await open(wxt.config.analysis.outputFile);\n      }\n    }\n  }\n\n  return output;\n}\n\nasync function combineAnalysisStats(): Promise<void> {\n  const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {\n    cwd: wxt.config.analysis.outputDir,\n    absolute: true,\n    expandDirectories: false,\n  });\n  const absolutePaths = unixFiles.map(unnormalizePath);\n\n  await mergeJsonOutputs({\n    inputs: absolutePaths,\n    template: wxt.config.analysis.template,\n    filename: wxt.config.analysis.outputFile,\n  });\n\n  if (!wxt.config.analysis.keepArtifacts) {\n    await Promise.all(\n      absolutePaths.map((statsFile) => rm(statsFile, { force: true })),\n    );\n  }\n}\n\nfunction printValidationResults({\n  errorCount,\n  errors,\n  warningCount,\n}: ValidationResults) {\n  (errorCount > 0 ? wxt.logger.error : wxt.logger.warn)(\n    `Entrypoint validation failed: ${errorCount} error${\n      errorCount === 1 ? '' : 's'\n    }, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,\n  );\n\n  const cwd = process.cwd();\n  const entrypointErrors = errors.reduce((map, error) => {\n    const entryErrors = map.get(error.entrypoint) ?? [];\n    entryErrors.push(error);\n    map.set(error.entrypoint, entryErrors);\n    return map;\n  }, new Map<Entrypoint, ValidationResult[]>());\n\n  Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {\n    wxt.logger.log(relative(cwd, entrypoint.inputPath) + '\\n');\n\n    errors.forEach((err) => {\n      const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');\n      const received = pc.dim(`(received: ${JSON.stringify(err.value)})`);\n      wxt.logger.log(`  - ${type} ${err.message} ${received}`);\n    });\n    console.log();\n  });\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/building/rebuild.ts",
    "content": "import type { Browser } from '@wxt-dev/browser';\nimport { createSpinner } from 'nanospinner';\nimport { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types';\nimport { generateWxtDir } from '../../generate-wxt-dir';\nimport { generateManifest, writeManifest } from '../../utils/manifest';\nimport { wxt } from '../../wxt';\nimport { buildEntrypoints } from './build-entrypoints';\n\n/**\n * Given a configuration, list of entrypoints, and an existing, partial output,\n * build the entrypoints and merge the new output with the existing output.\n *\n * This function will:\n *\n * 1. Generate the .wxt directory's types\n * 2. Build the `entrypointGroups` (and copies public files)\n * 3. Generate the latest manifest for all entrypoints\n * 4. Write the new manifest to the file system\n *\n * @param config Internal config containing all the project information.\n * @param allEntrypoints List of entrypoints used to generate the types inside\n *   .wxt directory.\n * @param entrypointGroups The list of entrypoint groups to build.\n * @param existingOutput The previous output to combine the rebuild results\n *   into. An empty array if this is the first build.\n */\nexport async function rebuild(\n  allEntrypoints: Entrypoint[],\n  entrypointGroups: EntrypointGroup[],\n  existingOutput: Omit<BuildOutput, 'manifest'> = {\n    steps: [],\n    publicAssets: [],\n  },\n): Promise<{\n  output: BuildOutput;\n  manifest: Browser.runtime.Manifest;\n  warnings: any[][];\n}> {\n  const spinner = createSpinner('Preparing...').start();\n\n  // Update types directory with new files and types\n  await generateWxtDir(allEntrypoints).catch((err) => {\n    wxt.logger.warn('Failed to update .wxt directory:', err);\n    // Throw the error if doing a regular build, don't for dev mode.\n    if (wxt.config.command === 'build') throw err;\n  });\n\n  // Build and merge the outputs\n  const newOutput = await buildEntrypoints(entrypointGroups, spinner);\n  const mergedOutput: Omit<BuildOutput, 'manifest'> = {\n    steps: [...existingOutput.steps, ...newOutput.steps],\n    // Do not merge existing because all publicAssets copied everytime\n    publicAssets: newOutput.publicAssets,\n  };\n\n  const { manifest: newManifest, warnings: manifestWarnings } =\n    await generateManifest(allEntrypoints, mergedOutput);\n\n  const finalOutput: BuildOutput = {\n    manifest: newManifest,\n    ...mergedOutput,\n  };\n\n  // Write manifest\n  await writeManifest(newManifest, finalOutput);\n\n  // Stop the spinner and remove it from the CLI output\n  spinner.clear().stop();\n\n  return {\n    output: finalOutput,\n    manifest: newManifest,\n    warnings: manifestWarnings,\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/cache.ts",
    "content": "import { mkdir, readFile } from 'node:fs/promises';\nimport { FsCache } from '../../types';\nimport { dirname, resolve } from 'path';\nimport { writeFileIfDifferent } from './fs';\n\n/**\n * A basic file system cache stored at `<srcDir>/.wxt/cache/<key>`. Just caches\n * a string in a file for the given key.\n *\n * @param srcDir Absolute path to source directory. See `InternalConfig.srcDir`\n */\nexport function createFsCache(wxtDir: string): FsCache {\n  const getPath = (key: string) =>\n    resolve(wxtDir, 'cache', encodeURIComponent(key));\n\n  return {\n    async set(key: string, value: string): Promise<void> {\n      const path = getPath(key);\n      await mkdir(dirname(path), { recursive: true });\n      await writeFileIfDifferent(path, value);\n    },\n    async get(key: string): Promise<string | undefined> {\n      const path = getPath(key);\n      try {\n        return await readFile(path, 'utf-8');\n      } catch {\n        return undefined;\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/cli.ts",
    "content": "import { LogLevels, consola } from 'consola';\nimport { printHeader } from './log';\nimport { formatDuration } from './time';\n\nexport function defineCommand<TArgs extends any[]>(\n  cb: (...args: TArgs) => void | boolean | Promise<void | boolean>,\n  options?: {\n    disableFinishedLog?: boolean;\n  },\n) {\n  return async (...args: TArgs) => {\n    // Enable consola's debug mode globally at the start of all commands when\n    // the `--debug` flag is passed\n    const isDebug = !!args.find((arg) => arg?.debug);\n    if (isDebug) {\n      consola.level = LogLevels.debug;\n    }\n\n    const startTime = Date.now();\n    try {\n      printHeader();\n\n      const ongoing = await cb(...args);\n\n      if (!ongoing && !options?.disableFinishedLog)\n        consola.success(\n          `Finished in ${formatDuration(Date.now() - startTime)}`,\n        );\n    } catch (err) {\n      consola.fail(\n        `Command failed after ${formatDuration(Date.now() - startTime)}`,\n      );\n      consola.error(err);\n      process.exit(1);\n    }\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/constants.ts",
    "content": "/**\n * Module ID used to build the background in dev mode if the extension doesn't\n * include a background script/service worker.\n */\nexport const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/content-scripts.ts",
    "content": "import type { Browser } from '@wxt-dev/browser';\nimport { ContentScriptEntrypoint, ResolvedConfig } from '../../types';\nimport { getEntrypointBundlePath } from './entrypoints';\nimport { ManifestContentScript } from './types';\n\n/**\n * Returns a unique and consistent string hash based on a content scripts\n * options.\n *\n * It is able to recognize default values,\n */\nexport function hashContentScriptOptions(\n  options: ContentScriptEntrypoint['options'],\n): string {\n  const simplifiedOptions = mapWxtOptionsToContentScript(\n    options,\n    undefined,\n    undefined,\n  );\n\n  // Remove undefined fields and use defaults to generate hash\n  Object.keys(simplifiedOptions).forEach((key) => {\n    // @ts-expect-error: key not typed as keyof ...\n    if (simplifiedOptions[key] == null) delete simplifiedOptions[key];\n  });\n\n  const withDefaults: ManifestContentScript = {\n    exclude_globs: [],\n    exclude_matches: [],\n    include_globs: [],\n    match_about_blank: false,\n    run_at: 'document_idle',\n    all_frames: false,\n    // @ts-expect-error: Untyped\n    match_origin_as_fallback: false,\n    world: 'ISOLATED',\n    ...simplifiedOptions,\n  };\n  return JSON.stringify(\n    Object.entries(withDefaults)\n      // Sort any arrays so their values are consistent\n      .map<[string, unknown]>(([key, value]) => {\n        if (Array.isArray(value)) return [key, value.sort()];\n        else return [key, value];\n      })\n      // Sort all the fields alphabetically\n      .sort((l, r) => l[0].localeCompare(r[0])),\n  );\n}\n\nexport function mapWxtOptionsToContentScript(\n  options: ContentScriptEntrypoint['options'],\n  js: string[] | undefined,\n  css: string[] | undefined,\n): ManifestContentScript {\n  return {\n    matches: options.matches ?? [],\n    all_frames: options.allFrames,\n    match_about_blank: options.matchAboutBlank,\n    exclude_globs: options.excludeGlobs,\n    exclude_matches: options.excludeMatches,\n    include_globs: options.includeGlobs,\n    run_at: options.runAt,\n    css,\n    js,\n    // @ts-expect-error: Untyped\n    match_origin_as_fallback: options.matchOriginAsFallback,\n    world: options.world,\n  };\n}\n\nexport function mapWxtOptionsToRegisteredContentScript(\n  options: ContentScriptEntrypoint['options'],\n  js: string[] | undefined,\n  css: string[] | undefined,\n): Omit<Browser.scripting.RegisteredContentScript, 'id'> {\n  return {\n    allFrames: options.allFrames,\n    excludeMatches: options.excludeMatches,\n    matches: options.matches,\n    runAt: options.runAt,\n    js,\n    css,\n    world: options.world,\n  };\n}\n\nexport function getContentScriptJs(\n  config: ResolvedConfig,\n  entrypoint: ContentScriptEntrypoint,\n): string[] {\n  return [getEntrypointBundlePath(entrypoint, config.outDir, '.js')];\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/content-security-policy.ts",
    "content": "/**\n * Directive names that make up CSPs. There are more, this is all I need for the\n * plugin.\n */\nexport type CspDirective = 'default-src' | 'script-src' | 'object-src';\n\nexport class ContentSecurityPolicy {\n  private static DIRECTIVE_ORDER: Record<string, number | undefined> = {\n    'default-src': 0,\n    'script-src': 1,\n    'object-src': 2,\n  };\n\n  data: Record<string, string[]>;\n\n  constructor(csp?: string) {\n    if (csp) {\n      const sections = csp.split(';').map((section) => section.trim());\n      this.data = sections.reduce<Record<string, string[]>>((data, section) => {\n        const [key, ...values] = section.split(' ').map((item) => item.trim());\n        if (key) data[key] = values;\n        return data;\n      }, {});\n    } else {\n      this.data = {};\n    }\n  }\n\n  /** Ensure a set of values are listed under a directive. */\n  add(directive: CspDirective, ...newValues: string[]): ContentSecurityPolicy {\n    const values = this.data[directive] ?? [];\n    newValues.forEach((newValue) => {\n      if (!values.includes(newValue)) values.push(newValue);\n    });\n    this.data[directive] = values;\n    return this;\n  }\n\n  toString(): string {\n    const directives = Object.entries(this.data).sort(([l], [r]) => {\n      const lo = ContentSecurityPolicy.DIRECTIVE_ORDER[l] ?? 2;\n      const ro = ContentSecurityPolicy.DIRECTIVE_ORDER[r] ?? 2;\n      return lo - ro;\n    });\n    return directives.map((entry) => entry.flat().join(' ')).join('; ') + ';';\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/entrypoints.ts",
    "content": "import {\n  Entrypoint,\n  PerBrowserOption,\n  ResolvedPerBrowserOptions,\n  TargetBrowser,\n} from '../../types';\nimport path, { relative, resolve, extname } from 'node:path';\nimport { normalizePath } from './paths';\n\nexport function getEntrypointName(\n  entrypointsDir: string,\n  inputPath: string,\n  // type: Entrypoint['type'],\n): string {\n  const relativePath = path.relative(entrypointsDir, inputPath);\n  // Grab the string up to the first . or / or \\\\\n  const name = relativePath.split(/[./\\\\]/, 2)[0];\n\n  return name;\n}\n\nexport function getEntrypointOutputFile(\n  entrypoint: Entrypoint,\n  ext: string,\n): string {\n  return resolve(entrypoint.outputDir, `${entrypoint.name}${ext}`);\n}\n\n/**\n * Return's the entrypoint's output path relative to the output directory. Used\n * for paths in the manifest and rollup's bundle.\n */\nexport function getEntrypointBundlePath(\n  entrypoint: Entrypoint,\n  outDir: string,\n  ext: string,\n): string {\n  return normalizePath(\n    relative(outDir, getEntrypointOutputFile(entrypoint, ext)),\n  );\n}\n\n/** Given an entrypoint option, resolve it's value based on a target browser. */\nexport function resolvePerBrowserOption<T>(\n  option: PerBrowserOption<T>,\n  browser: TargetBrowser,\n): T {\n  if (typeof option === 'object' && !Array.isArray(option))\n    return (option as any)[browser];\n  return option;\n}\n\n/**\n * Given an entrypoint option, resolve it's value based on a target browser.\n *\n * DefaultIcon is special, it's the only key that's a record, which can confuse\n * this function. So it's been manually excluded from resolution.\n */\nexport function resolvePerBrowserOptions<\n  T extends Record<string, any>,\n  TKeys extends keyof T,\n>(options: T, browser: TargetBrowser): ResolvedPerBrowserOptions<T, TKeys> {\n  // @ts-expect-error: Object.entries is untyped.\n  return Object.fromEntries(\n    Object.entries(options).map(([key, value]) => [\n      key,\n      key === 'defaultIcon' ? value : resolvePerBrowserOption(value, browser),\n    ]),\n  );\n}\n\n/**\n * Returns true when the entrypoint is an HTML entrypoint.\n *\n * Naively just checking the file extension of the input path.\n */\nexport function isHtmlEntrypoint(\n  entrypoint: Pick<Entrypoint, 'inputPath'>,\n): boolean {\n  const ext = extname(entrypoint.inputPath);\n  return ['.html'].includes(ext);\n}\n\n/**\n * Returns true when the entrypoint is a JS entrypoint.\n *\n * Naively just checking the file extension of the input path.\n */\nexport function isJsEntrypoint(\n  entrypoint: Pick<Entrypoint, 'inputPath'>,\n): boolean {\n  const ext = extname(entrypoint.inputPath);\n  return ['.js', '.jsx', '.ts', '.tsx'].includes(ext);\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/env.ts",
    "content": "import { readFileSync, existsSync } from 'node:fs';\nimport { parseEnv } from 'node:util';\nimport { expand } from 'dotenv-expand';\nimport type { TargetBrowser } from '../../types';\n\n/** Load environment files based on the current mode and browser. */\nexport function loadEnv(mode: string, browser: TargetBrowser) {\n  const envFiles = [\n    // List is ordered with general files first, specific ones last, so the more\n    // specific files override the more general ones in the loop below.\n    `.env`,\n    `.env.local`,\n    `.env.${mode}`,\n    `.env.${mode}.local`,\n    `.env.${browser}`,\n    `.env.${browser}.local`,\n    `.env.${mode}.${browser}`,\n    `.env.${mode}.${browser}.local`,\n  ];\n\n  const parsed = Object.fromEntries<string>(\n    envFiles.flatMap((filePath) => {\n      if (!existsSync(filePath)) return [];\n\n      try {\n        const content = readFileSync(filePath, 'utf-8');\n        const parsedEnv = parseEnv(content);\n        return Object.entries(parsedEnv) as [string, string][];\n      } catch {\n        return [];\n      }\n    }),\n  );\n\n  // Make a copy of `process.env` so that `dotenv-expand` doesn't re-assign the\n  // expanded values to the global `process.env`.\n  const processEnv = { ...process.env } as Record<string, string>;\n\n  expand({\n    parsed,\n    processEnv,\n  });\n\n  return parsed;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/environments/browser-environment.ts",
    "content": "import { parseHTML } from 'linkedom';\nimport { createEnvironment, Environment, EnvGlobals } from './environment';\n\nexport function createBrowserEnvironment(): Environment {\n  return createEnvironment(getBrowserEnvironmentGlobals);\n}\n\nexport function getBrowserEnvironmentGlobals(): EnvGlobals {\n  const { window, document, global } = parseHTML(`\n    <html>\n      <head></head>\n      <body></body>\n    </html>\n  `);\n  return {\n    ...global,\n    window,\n    document,\n    self: global,\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/environments/environment.ts",
    "content": "export interface Environment {\n  setup: () => () => void;\n  run: <T>(fn: () => Promise<T>) => Promise<T>;\n}\n\nexport function createEnvironment(getGlobals: () => EnvGlobals): Environment {\n  const setup = () => {\n    const envGlobals = getGlobals();\n    const ogGlobals = getOgGlobals(envGlobals);\n    applyGlobals(envGlobals);\n\n    return () => {\n      applyGlobals(ogGlobals);\n    };\n  };\n  const run = async (fn: () => any) => {\n    const teardown = setup();\n    try {\n      return await fn();\n    } finally {\n      teardown();\n    }\n  };\n  return {\n    setup,\n    run,\n  };\n}\n\nexport type EnvGlobals = Record<string, any>;\n\nexport function getOgGlobals(envGlobals: EnvGlobals): EnvGlobals {\n  return Object.keys(envGlobals).reduce<typeof envGlobals>((acc, key) => {\n    // @ts-expect-error: Untyped key on globalThis\n    acc[key] = globalThis[key];\n    return acc;\n  }, {});\n}\n\nexport function applyGlobals(globals: EnvGlobals): void {\n  Object.entries(globals).forEach(([key, envValue]) => {\n    try {\n      // @ts-expect-error: Untyped key on globalThis\n      globalThis[key] = envValue;\n    } catch {\n      // ignore any globals that can't be set\n    }\n  });\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/environments/extension-environment.ts",
    "content": "import { fakeBrowser } from '@webext-core/fake-browser';\nimport { getBrowserEnvironmentGlobals } from './browser-environment';\nimport { createEnvironment, Environment } from './environment';\n\nexport function createExtensionEnvironment(): Environment {\n  return createEnvironment(getExtensionEnvironmentGlobals);\n}\n\nexport function getExtensionEnvironmentGlobals() {\n  return {\n    ...getBrowserEnvironmentGlobals(),\n    chrome: fakeBrowser,\n    browser: fakeBrowser,\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/environments/index.ts",
    "content": "export * from './browser-environment';\nexport * from './extension-environment';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/eslint.ts",
    "content": "export async function getEslintVersion(): Promise<string[]> {\n  try {\n    const { ESLint } = await import('eslint');\n    return ESLint.version?.split('.') ?? [];\n  } catch {\n    // Return an empty version when there's an error importing ESLint\n    return [];\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/fs.ts",
    "content": "import { access, readFile, writeFile } from 'node:fs/promises';\nimport { glob } from 'tinyglobby';\nimport { wxt } from '../wxt';\nimport { unnormalizePath } from './paths';\n\nexport async function pathExists(path: string): Promise<boolean> {\n  try {\n    await access(path);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nexport async function readJson<T = any>(path: string): Promise<T> {\n  return JSON.parse(await readFile(path, 'utf-8'));\n}\n\n/**\n * Only write the contents to a file if it results in a change. This prevents\n * unnecessary file watchers from being triggered, like WXT's dev server or the\n * TS language server in editors.\n *\n * @param file The file to write to.\n * @param newContents The new text content to write.\n */\nexport async function writeFileIfDifferent(\n  file: string,\n  newContents: string,\n): Promise<void> {\n  const existingContents = await readFile(file, 'utf-8').catch(() => undefined);\n\n  if (existingContents !== newContents) {\n    await writeFile(file, newContents);\n  }\n}\n\nexport async function getPublicFiles(): Promise<string[]> {\n  if (!(await pathExists(wxt.config.publicDir))) return [];\n\n  const files = await glob('**/*', {\n    cwd: wxt.config.publicDir,\n    expandDirectories: false,\n  });\n  return files.map(unnormalizePath);\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/globals.ts",
    "content": "import { ResolvedConfig } from '../../types';\n\nexport function getGlobals(\n  config: ResolvedConfig,\n): Array<{ name: string; value: any; type: string }> {\n  return [\n    {\n      name: 'MANIFEST_VERSION',\n      value: config.manifestVersion,\n      type: `2 | 3`,\n    },\n    {\n      name: 'BROWSER',\n      value: config.browser,\n      type:\n        config.targetBrowsers.length === 0\n          ? 'string'\n          : config.targetBrowsers.map((browser) => `\"${browser}\"`).join(' | '),\n    },\n    {\n      name: 'CHROME',\n      value: config.browser === 'chrome',\n      type: `boolean`,\n    },\n    {\n      name: 'FIREFOX',\n      value: config.browser === 'firefox',\n      type: `boolean`,\n    },\n    {\n      name: 'SAFARI',\n      value: config.browser === 'safari',\n      type: `boolean`,\n    },\n    {\n      name: 'EDGE',\n      value: config.browser === 'edge',\n      type: `boolean`,\n    },\n    {\n      name: 'OPERA',\n      value: config.browser === 'opera',\n      type: `boolean`,\n    },\n    {\n      name: 'COMMAND',\n      value: config.command,\n      type: `\"build\" | \"serve\"`,\n    },\n  ];\n}\n\nexport function getEntrypointGlobals(entrypointName: string) {\n  return [\n    {\n      name: 'ENTRYPOINT',\n      value: entrypointName,\n      type: `string`,\n    },\n  ];\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/i18n.ts",
    "content": "export interface Message {\n  name: string;\n  message: string;\n  description?: string;\n}\n\nconst predefinedMessages = {\n  '@@extension_id': {\n    message: '<browser.runtime.id>',\n    description:\n      \"The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\\nNote: You can't use this message in a manifest file.\",\n  },\n  '@@ui_locale': {\n    message: '<browser.i18n.getUiLocale()>',\n    description: '',\n  },\n  '@@bidi_dir': {\n    message: '<ltr|rtl>',\n    description:\n      'The text direction for the current locale, either \"ltr\" for left-to-right languages such as English or \"rtl\" for right-to-left languages such as Japanese.',\n  },\n  '@@bidi_reversed_dir': {\n    message: '<rtl|ltr>',\n    description:\n      'If the @@bidi_dir is \"ltr\", then this is \"rtl\"; otherwise, it\\'s \"ltr\".',\n  },\n  '@@bidi_start_edge': {\n    message: '<left|right>',\n    description:\n      'If the @@bidi_dir is \"ltr\", then this is \"left\"; otherwise, it\\'s \"right\".',\n  },\n  '@@bidi_end_edge': {\n    message: '<right|left>',\n    description:\n      'If the @@bidi_dir is \"ltr\", then this is \"right\"; otherwise, it\\'s \"left\".',\n  },\n};\n\n/**\n * Get a list of all messages and their metadata from JSON file contents.\n *\n * @param messagesJson The contents of a `_locales/en/messages.json` file.\n */\nexport function parseI18nMessages(messagesJson: object): Message[] {\n  return Object.entries({\n    ...predefinedMessages,\n    ...messagesJson,\n  }).map<Message>(([name, details]) => ({\n    name,\n    ...details,\n  }));\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/index.ts",
    "content": "export { normalizePath } from './paths';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/log/index.ts",
    "content": "export * from './printBuildSummary';\nexport * from './printFileList';\nexport * from './printHeader';\nexport * from './printTable';\n"
  },
  {
    "path": "packages/wxt/src/core/utils/log/printBuildSummary.ts",
    "content": "import { resolve } from 'path';\nimport { BuildOutput } from '../../../types';\nimport { printFileList } from './printFileList';\nimport { wxt } from '../../wxt';\n\nexport async function printBuildSummary(\n  log: (...args: any[]) => void,\n  header: string,\n  output: BuildOutput,\n) {\n  const chunks = [\n    ...output.steps.flatMap((step) => step.chunks),\n    ...output.publicAssets,\n  ].sort((l, r) => {\n    const lWeight = getChunkSortWeight(l.fileName);\n    const rWeight = getChunkSortWeight(r.fileName);\n    const diff = lWeight - rWeight;\n    if (diff !== 0) return diff;\n    return l.fileName.localeCompare(r.fileName);\n  });\n\n  const files = chunks.map((chunk) =>\n    resolve(wxt.config.outDir, chunk.fileName),\n  );\n  await printFileList(log, header, wxt.config.outDir, files);\n}\n\nconst DEFAULT_SORT_WEIGHT = 100;\nconst CHUNK_SORT_WEIGHTS: Record<string, number> = {\n  'manifest.json': 0,\n  '.html': 1,\n  '.js.map': 2,\n  '.js': 2,\n  '.css': 3,\n};\nfunction getChunkSortWeight(filename: string) {\n  return (\n    Object.entries(CHUNK_SORT_WEIGHTS).find(([key]) =>\n      filename.endsWith(key),\n    )?.[1] ?? DEFAULT_SORT_WEIGHT\n  );\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/log/printFileList.ts",
    "content": "import path from 'node:path';\nimport pc from 'picocolors';\nimport { lstat } from 'node:fs/promises';\nimport { filesize } from 'filesize';\nimport { printTable } from './printTable';\n\nexport async function printFileList(\n  log: (message: string) => void,\n  header: string,\n  baseDir: string,\n  files: string[],\n): Promise<void> {\n  let totalSize = 0;\n\n  const fileRows: string[][] = await Promise.all(\n    files.map(async (file, i) => {\n      const parts = [\n        path.relative(process.cwd(), baseDir) + path.sep,\n        path.relative(baseDir, file),\n      ];\n      const prefix = i === files.length - 1 ? '  └─' : '  ├─';\n      const color = getChunkColor(file);\n      const stats = await lstat(file);\n      totalSize += stats.size;\n      const size = String(filesize(stats.size));\n      return [\n        `${pc.gray(prefix)} ${pc.dim(parts[0])}${color(parts[1])}`,\n        pc.dim(size),\n      ];\n    }),\n  );\n\n  fileRows.push([`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`]);\n\n  printTable(log, header, fileRows);\n}\n\nconst DEFAULT_COLOR = pc.blue;\nconst CHUNK_COLORS: Record<string, (text: string) => string> = {\n  '.js.map': pc.gray,\n  '.cjs.map': pc.gray,\n  '.mjs.map': pc.gray,\n  '.html': pc.green,\n  '.css': pc.magenta,\n  '.js': pc.cyan,\n  '.cjs': pc.cyan,\n  '.mjs': pc.cyan,\n  '.zip': pc.yellow,\n};\nfunction getChunkColor(filename: string) {\n  return (\n    Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??\n    DEFAULT_COLOR\n  );\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/log/printHeader.ts",
    "content": "import pc from 'picocolors';\nimport { version } from '../../../version';\nimport { consola } from 'consola';\n\nexport function printHeader() {\n  consola.log(`\\n${pc.gray('WXT')} ${pc.gray(pc.bold(version))}`);\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/log/printTable.ts",
    "content": "export function printTable(\n  log: (message: string) => void,\n  header: string,\n  rows: string[][],\n  gap = 2,\n): void {\n  if (rows.length === 0) return;\n\n  const columnWidths = rows.reduce(\n    (widths, row) => {\n      for (let i = 0; i < Math.max(widths.length, row.length); i++) {\n        widths[i] = Math.max(row[i]?.length ?? 0, widths[i] ?? 0);\n      }\n      return widths;\n    },\n    rows[0].map((column) => column.length),\n  );\n\n  let str = '';\n  rows.forEach((row, i) => {\n    row.forEach((col, j) => {\n      str += col.padEnd(columnWidths[j], ' ');\n      if (j !== row.length - 1) str += ''.padEnd(gap, ' ');\n    });\n    if (i !== rows.length - 1) str += '\\n';\n  });\n\n  log(`${header}\\n${str}`);\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/manifest.ts",
    "content": "import {\n  Entrypoint,\n  BackgroundEntrypoint,\n  BuildOutput,\n  ContentScriptEntrypoint,\n  OptionsEntrypoint,\n  PopupEntrypoint,\n  SidepanelEntrypoint,\n} from '../../types';\nimport { mkdir } from 'node:fs/promises';\nimport { resolve } from 'path';\nimport { getEntrypointBundlePath } from './entrypoints';\nimport { ContentSecurityPolicy } from './content-security-policy';\nimport {\n  hashContentScriptOptions,\n  mapWxtOptionsToContentScript,\n} from './content-scripts';\nimport { getPackageJson } from './package';\nimport { normalizePath } from './paths';\nimport { writeFileIfDifferent } from './fs';\nimport defu from 'defu';\nimport { wxt } from '../wxt';\nimport { ManifestV3WebAccessibleResource } from './types';\nimport type { Browser } from '@wxt-dev/browser';\n\n/** Writes the manifest to the output directory and the build output. */\nexport async function writeManifest(\n  manifest: Browser.runtime.Manifest,\n  output: BuildOutput,\n): Promise<void> {\n  const str =\n    wxt.config.mode === 'production'\n      ? JSON.stringify(manifest)\n      : JSON.stringify(manifest, null, 2);\n\n  await mkdir(wxt.config.outDir, { recursive: true });\n  await writeFileIfDifferent(resolve(wxt.config.outDir, 'manifest.json'), str);\n\n  output.publicAssets.unshift({\n    type: 'asset',\n    fileName: 'manifest.json',\n  });\n}\n\n/** Generates the manifest based on the config and entrypoints. */\nexport async function generateManifest(\n  allEntrypoints: Entrypoint[],\n  buildOutput: Omit<BuildOutput, 'manifest'>,\n): Promise<{ manifest: Browser.runtime.Manifest; warnings: any[][] }> {\n  const entrypoints = allEntrypoints.filter((entry) => !entry.skipped);\n\n  const warnings: any[][] = [];\n  const pkg = await getPackageJson();\n\n  let versionName =\n    wxt.config.manifest.version_name ??\n    wxt.config.manifest.version ??\n    pkg?.version;\n  if (versionName == null) {\n    versionName = '0.0.0';\n    wxt.logger.warn(\n      'Extension version not found, defaulting to \"0.0.0\". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/key-concepts/manifest.html#version-and-version-name',\n    );\n  }\n  const version = wxt.config.manifest.version ?? simplifyVersion(versionName);\n\n  const baseManifest: Browser.runtime.Manifest = {\n    manifest_version: wxt.config.manifestVersion,\n    name: pkg?.name,\n    description: pkg?.description,\n    version,\n    short_name: pkg?.shortName,\n    icons: discoverIcons(buildOutput),\n  };\n  const userManifest = wxt.config.manifest;\n  if (userManifest.manifest_version) {\n    delete userManifest.manifest_version;\n    wxt.logger.warn(\n      '`manifest.manifest_version` config was set, but ignored. To change the target manifest version, use the `manifestVersion` option or the `--mv2`/`--mv3` CLI flags.\\nSee https://wxt.dev/guide/essentials/target-different-browsers.html#target-a-manifest-version',\n    );\n  }\n\n  let manifest = defu(userManifest, baseManifest) as Browser.runtime.Manifest;\n\n  // Add reload command in dev mode\n  if (wxt.config.command === 'serve' && wxt.config.dev.reloadCommand) {\n    if (\n      manifest.commands &&\n      // If the following limit is exceeded, Chrome will fail to load the extension.\n      // Error: \"Too many commands specified for 'commands': The maximum is 4.\"\n      Object.values(manifest.commands).filter(\n        (command) => command.suggested_key,\n      ).length >= 4\n    ) {\n      warnings.push([\n        \"Extension already has 4 registered commands with suggested keys, WXT's reload command is disabled\",\n      ]);\n    } else {\n      manifest.commands ??= {};\n      manifest.commands['wxt:reload-extension'] = {\n        description: 'Reload the extension during development',\n        suggested_key: {\n          default: wxt.config.dev.reloadCommand,\n        },\n      };\n    }\n  }\n\n  // Apply the final version fields after merging the user manifest\n  manifest.version = version;\n  manifest.version_name =\n    // Firefox doesn't support version_name\n    wxt.config.browser === 'firefox' || versionName === version\n      ? undefined\n      : versionName;\n\n  addEntrypoints(manifest, entrypoints, buildOutput);\n\n  if (wxt.config.command === 'serve') addDevModeCsp(manifest);\n  if (wxt.config.command === 'serve') addDevModePermissions(manifest);\n\n  await wxt.hooks.callHook('build:manifestGenerated', wxt, manifest);\n\n  if (wxt.config.manifestVersion === 2) {\n    convertWebAccessibleResourcesToMv2(manifest);\n    convertActionToMv2(manifest);\n    convertCspToMv2(manifest);\n    moveHostPermissionsToPermissions(manifest);\n  }\n\n  if (wxt.config.manifestVersion === 3) {\n    validateMv3WebAccessibleResources(manifest);\n  }\n\n  stripKeys(manifest);\n\n  if (manifest.name == null)\n    throw Error(\n      \"Manifest 'name' is missing. Either:\\n1. Set the name in your <rootDir>/package.json\\n2. Set a name via the manifest option in your wxt.config.ts\",\n    );\n  if (manifest.version == null) {\n    throw Error(\n      \"Manifest 'version' is missing. Either:\\n1. Add a version in your <rootDir>/package.json\\n2. Pass the version via the manifest option in your wxt.config.ts\",\n    );\n  }\n\n  return {\n    manifest,\n    warnings,\n  };\n}\n\n/**\n * Removes suffixes from the version, like X.Y.Z-alpha1 (which browsers don't\n * allow), so it's a simple version number, like X or X.Y or X.Y.Z, which\n * browsers allow.\n */\nfunction simplifyVersion(versionName: string): string {\n  // Regex adapted from here: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version#version_format\n  const version = /^((0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}).*$/.exec(\n    versionName,\n  )?.[1];\n\n  if (version == null)\n    throw Error(\n      `Cannot simplify package.json version \"${versionName}\" to a valid extension version, \"X.Y.Z\"`,\n    );\n\n  return version;\n}\n\nfunction addEntrypoints(\n  manifest: Browser.runtime.Manifest,\n  entrypoints: Entrypoint[],\n  buildOutput: Omit<BuildOutput, 'manifest'>,\n): void {\n  const entriesByType = entrypoints.reduce<\n    Partial<Record<Entrypoint['type'], Entrypoint[]>>\n  >((map, entrypoint) => {\n    map[entrypoint.type] ??= [];\n    map[entrypoint.type]?.push(entrypoint);\n    return map;\n  }, {});\n\n  const background = entriesByType['background']?.[0] as\n    | BackgroundEntrypoint\n    | undefined;\n  const bookmarks = entriesByType['bookmarks']?.[0];\n  const contentScripts = entriesByType['content-script'] as\n    | ContentScriptEntrypoint[]\n    | undefined;\n  const devtools = entriesByType['devtools']?.[0];\n  const history = entriesByType['history']?.[0];\n  const newtab = entriesByType['newtab']?.[0];\n  const options = entriesByType['options']?.[0] as\n    | OptionsEntrypoint\n    | undefined;\n  const popup = entriesByType['popup']?.[0] as PopupEntrypoint | undefined;\n  const sandboxes = entriesByType['sandbox'];\n  const sidepanels = entriesByType['sidepanel'] as\n    | SidepanelEntrypoint[]\n    | undefined;\n\n  if (background) {\n    const script = getEntrypointBundlePath(\n      background,\n      wxt.config.outDir,\n      '.js',\n    );\n    if (wxt.config.browser === 'firefox' && wxt.config.manifestVersion === 3) {\n      manifest.background = {\n        type: background.options.type,\n        scripts: [script],\n      };\n    } else if (wxt.config.manifestVersion === 3) {\n      manifest.background = {\n        type: background.options.type,\n        service_worker: script,\n      };\n    } else {\n      manifest.background = {\n        persistent: background.options.persistent,\n        scripts: [script],\n      };\n    }\n  }\n\n  if (bookmarks) {\n    if (wxt.config.browser === 'firefox') {\n      wxt.logger.warn(\n        'Bookmarks are not supported by Firefox. chrome_url_overrides.bookmarks was not added to the manifest',\n      );\n    } else {\n      manifest.chrome_url_overrides ??= {};\n      manifest.chrome_url_overrides.bookmarks = getEntrypointBundlePath(\n        bookmarks,\n        wxt.config.outDir,\n        '.html',\n      );\n    }\n  }\n\n  if (history) {\n    if (wxt.config.browser === 'firefox') {\n      wxt.logger.warn(\n        'Bookmarks are not supported by Firefox. chrome_url_overrides.history was not added to the manifest',\n      );\n    } else {\n      manifest.chrome_url_overrides ??= {};\n      manifest.chrome_url_overrides.history = getEntrypointBundlePath(\n        history,\n        wxt.config.outDir,\n        '.html',\n      );\n    }\n  }\n\n  if (newtab) {\n    manifest.chrome_url_overrides ??= {};\n    manifest.chrome_url_overrides.newtab = getEntrypointBundlePath(\n      newtab,\n      wxt.config.outDir,\n      '.html',\n    );\n  }\n\n  if (popup) {\n    const default_popup = getEntrypointBundlePath(\n      popup,\n      wxt.config.outDir,\n      '.html',\n    );\n    const options: Browser.runtime.ManifestAction = {};\n    if (popup.options.defaultIcon)\n      options.default_icon = popup.options.defaultIcon;\n    if (popup.options.defaultTitle)\n      options.default_title = popup.options.defaultTitle;\n    if (popup.options.browserStyle)\n      // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox\n      options.browser_style = popup.options.browserStyle;\n    if (popup.options.defaultArea)\n      // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox\n      options.default_area = popup.options.defaultArea;\n    if (popup.options.themeIcons)\n      // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox\n      options.theme_icons = popup.options.themeIcons;\n    if (manifest.manifest_version === 3) {\n      manifest.action = {\n        ...manifest.action,\n        ...options,\n        default_popup,\n      };\n    } else {\n      const key = popup.options.mv2Key ?? 'browser_action';\n      manifest[key] = {\n        ...manifest[key],\n        ...options,\n        default_popup,\n      };\n    }\n  }\n\n  if (devtools) {\n    manifest.devtools_page = getEntrypointBundlePath(\n      devtools,\n      wxt.config.outDir,\n      '.html',\n    );\n  }\n\n  if (options) {\n    const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html');\n    manifest.options_ui = {\n      open_in_tab: options.options.openInTab,\n      // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox\n      browser_style:\n        wxt.config.browser === 'firefox'\n          ? options.options.browserStyle\n          : undefined,\n      chrome_style:\n        wxt.config.browser !== 'firefox'\n          ? options.options.chromeStyle\n          : undefined,\n      page,\n    };\n  }\n\n  if (sandboxes?.length) {\n    if (wxt.config.browser === 'firefox') {\n      wxt.logger.warn(\n        'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest',\n      );\n    } else {\n      manifest.sandbox = {\n        pages: sandboxes.map((entry) =>\n          getEntrypointBundlePath(entry, wxt.config.outDir, '.html'),\n        ),\n      };\n    }\n  }\n\n  if (sidepanels?.length) {\n    const defaultSidepanel =\n      sidepanels.find((entry) => entry.name === 'sidepanel') ?? sidepanels[0];\n    const page = getEntrypointBundlePath(\n      defaultSidepanel,\n      wxt.config.outDir,\n      '.html',\n    );\n\n    if (wxt.config.browser === 'firefox') {\n      manifest.sidebar_action = {\n        default_panel: page,\n        browser_style: defaultSidepanel.options.browserStyle,\n        default_icon: defaultSidepanel.options.defaultIcon,\n        default_title: defaultSidepanel.options.defaultTitle,\n        open_at_install: defaultSidepanel.options.openAtInstall,\n      };\n    } else if (wxt.config.manifestVersion === 3) {\n      manifest.side_panel = {\n        default_path: page,\n      };\n      addPermission(manifest, 'sidePanel');\n    } else {\n      wxt.logger.warn(\n        'Side panel not supported by Chromium using MV2. side_panel.default_path was not added to the manifest',\n      );\n    }\n  }\n\n  if (contentScripts?.length) {\n    const cssMap = getContentScriptsCssMap(buildOutput, contentScripts);\n\n    // Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded\n    // at runtime\n    if (wxt.config.command === 'serve' && wxt.config.manifestVersion === 3) {\n      contentScripts.forEach((script) => {\n        script.options.matches?.forEach((matchPattern) => {\n          addHostPermission(manifest, matchPattern);\n        });\n      });\n    } else {\n      // Manifest scripts\n      const hashToEntrypointsMap = contentScripts\n        .filter((cs) => cs.options.registration !== 'runtime')\n        .reduce((map, script) => {\n          const hash = hashContentScriptOptions(script.options);\n          if (map.has(hash)) map.get(hash)?.push(script);\n          else map.set(hash, [script]);\n          return map;\n        }, new Map<string, ContentScriptEntrypoint[]>());\n      const manifestContentScripts = Array.from(\n        hashToEntrypointsMap.values(),\n      ).map((scripts) =>\n        mapWxtOptionsToContentScript(\n          scripts[0].options,\n          scripts.map((entry) =>\n            getEntrypointBundlePath(entry, wxt.config.outDir, '.js'),\n          ),\n          getContentScriptCssFiles(scripts, cssMap),\n        ),\n      );\n      if (manifestContentScripts.length >= 0) {\n        manifest.content_scripts ??= [];\n        manifest.content_scripts.push(...manifestContentScripts);\n      }\n\n      // Runtime content scripts\n      const runtimeContentScripts = contentScripts.filter(\n        (cs) => cs.options.registration === 'runtime',\n      );\n      runtimeContentScripts.forEach((script) => {\n        script.options.matches?.forEach((matchPattern) => {\n          addHostPermission(manifest, matchPattern);\n        });\n      });\n    }\n\n    const contentScriptCssResources = getContentScriptCssWebAccessibleResources(\n      contentScripts,\n      cssMap,\n    );\n    if (contentScriptCssResources.length > 0) {\n      manifest.web_accessible_resources ??= [];\n      manifest.web_accessible_resources.push(...contentScriptCssResources);\n    }\n  }\n}\n\nfunction discoverIcons(\n  buildOutput: Omit<BuildOutput, 'manifest'>,\n): Browser.runtime.Manifest['icons'] {\n  const icons: [string, string][] = [];\n  // prettier-ignore\n  // #region snippet\n  const iconRegex = [\n    /^icon-([0-9]+)\\.png$/,                 // icon-16.png\n    /^icon-([0-9]+)x[0-9]+\\.png$/,          // icon-16x16.png\n    /^icon@([0-9]+)w\\.png$/,                // icon@16w.png\n    /^icon@([0-9]+)h\\.png$/,                // icon@16h.png\n    /^icon@([0-9]+)\\.png$/,                 // icon@16.png\n    /^icons?[/\\\\]([0-9]+)\\.png$/,           // icon/16.png | icons/16.png\n    /^icons?[/\\\\]([0-9]+)x[0-9]+\\.png$/,    // icon/16x16.png | icons/16x16.png\n  ];\n  // #endregion snippet\n\n  buildOutput.publicAssets.forEach((asset) => {\n    let size: string | undefined;\n    for (const regex of iconRegex) {\n      const match = asset.fileName.match(regex);\n      if (match?.[1] != null) {\n        size = match[1];\n        break;\n      }\n    }\n    if (size == null) return;\n\n    icons.push([size, normalizePath(asset.fileName)]);\n  });\n\n  return icons.length > 0 ? Object.fromEntries(icons) : undefined;\n}\n\nfunction addDevModeCsp(manifest: Browser.runtime.Manifest): void {\n  let permissionUrl = wxt.server?.origin;\n  if (permissionUrl) {\n    const permissionUrlInstance = new URL(permissionUrl);\n    permissionUrlInstance.port = '';\n    permissionUrl = permissionUrlInstance.toString();\n  }\n  const permission = `${permissionUrl}*`;\n  const allowedCsp = wxt.server?.origin ?? 'http://localhost:*';\n\n  if (manifest.manifest_version === 3) {\n    addHostPermission(manifest, permission);\n  } else {\n    addPermission(manifest, permission);\n  }\n\n  const extensionPagesCsp = new ContentSecurityPolicy(\n    // @ts-expect-error: extension_pages exists, we convert MV2 CSPs to this earlier in the process\n    manifest.content_security_policy?.extension_pages ??\n      (manifest.manifest_version === 3\n        ? DEFAULT_MV3_EXTENSION_PAGES_CSP\n        : DEFAULT_MV2_CSP),\n  );\n  const sandboxCsp = new ContentSecurityPolicy(\n    // @ts-expect-error: sandbox is not typed\n    manifest.content_security_policy?.sandbox ?? DEFAULT_MV3_SANDBOX_CSP,\n  );\n\n  if (wxt.config.command === 'serve') {\n    extensionPagesCsp.add('script-src', allowedCsp);\n    sandboxCsp.add('script-src', allowedCsp);\n  }\n\n  manifest.content_security_policy ??= {};\n  // @ts-expect-error: extension_pages is not typed\n  manifest.content_security_policy.extension_pages =\n    extensionPagesCsp.toString();\n  // @ts-expect-error: sandbox is not typed\n  manifest.content_security_policy.sandbox = sandboxCsp.toString();\n}\n\nfunction addDevModePermissions(manifest: Browser.runtime.Manifest) {\n  // For reloading the page\n  addPermission(manifest, 'tabs');\n\n  // For registering content scripts\n  if (wxt.config.manifestVersion === 3) addPermission(manifest, 'scripting');\n}\n\n/**\n * Returns the bundle paths to CSS files associated with a list of content\n * scripts, or undefined if there is no associated CSS.\n */\nexport function getContentScriptCssFiles(\n  contentScripts: ContentScriptEntrypoint[],\n  contentScriptCssMap: Record<string, string | undefined>,\n): string[] | undefined {\n  const css: string[] = [];\n\n  contentScripts.forEach((script) => {\n    if (\n      script.options.cssInjectionMode === 'manual' ||\n      script.options.cssInjectionMode === 'ui'\n    )\n      return;\n\n    const cssFile = contentScriptCssMap[script.name];\n    if (cssFile == null) return;\n\n    if (cssFile) css.push(cssFile);\n  });\n\n  if (css.length > 0) return css;\n  return undefined;\n}\n\n/**\n * Content scripts configured with `cssInjectionMode: \"ui\"` need to add their\n * CSS files to web accessible resources so they can be fetched as text and\n * added to shadow roots that the UI is added to.\n */\nexport function getContentScriptCssWebAccessibleResources(\n  contentScripts: ContentScriptEntrypoint[],\n  contentScriptCssMap: Record<string, string | undefined>,\n): any[] {\n  const resources: ManifestV3WebAccessibleResource[] = [];\n\n  contentScripts.forEach((script) => {\n    if (script.options.cssInjectionMode !== 'ui') return;\n\n    const cssFile = contentScriptCssMap[script.name];\n    if (cssFile == null) return;\n\n    resources.push({\n      resources: [cssFile],\n      use_dynamic_url: true,\n      matches:\n        script.options.matches?.map((matchPattern) =>\n          stripPathFromMatchPattern(matchPattern),\n        ) ?? [],\n    });\n  });\n\n  return resources;\n}\n\n/**\n * Based on the build output, return a Record of each content script's name to\n * it CSS file if the script includes one.\n */\nexport function getContentScriptsCssMap(\n  buildOutput: Omit<BuildOutput, 'manifest'>,\n  scripts: ContentScriptEntrypoint[],\n) {\n  const map: Record<string, string | undefined> = {};\n  const allChunks = buildOutput.steps.flatMap((step) => step.chunks);\n  scripts.forEach((script) => {\n    const relatedCss = allChunks.find(\n      (chunk) => chunk.fileName === `content-scripts/${script.name}.css`,\n    );\n    if (relatedCss != null) map[script.name] = relatedCss.fileName;\n  });\n  return map;\n}\n\nfunction addPermission(\n  manifest: Browser.runtime.Manifest,\n  permission: string,\n): void {\n  manifest.permissions ??= [];\n  // @ts-expect-error: Allow using strings for permissions for MV2 support\n  if (manifest.permissions.includes(permission)) return;\n  // @ts-expect-error: Allow using strings for permissions for MV2 support\n  manifest.permissions.push(permission);\n}\n\nfunction addHostPermission(\n  manifest: Browser.runtime.Manifest,\n  hostPermission: string,\n): void {\n  manifest.host_permissions ??= [];\n  if (manifest.host_permissions.includes(hostPermission)) return;\n  manifest.host_permissions.push(hostPermission);\n}\n\n/**\n * - \"<all_urls>\" → \"<all_urls>\"\n * - \"_://play.google.com/books/_\" → \"_://play.google.com/_\"\n */\nexport function stripPathFromMatchPattern(pattern: string) {\n  const protocolSepIndex = pattern.indexOf('://');\n  if (protocolSepIndex === -1) return pattern;\n\n  const startOfPath = pattern.indexOf('/', protocolSepIndex + 3);\n  return pattern.substring(0, startOfPath) + '/*';\n}\n\n/**\n * Converts all MV3 web accessible resources to their MV2 forms. MV3 web\n * accessible resources are generated in this file, and may be defined by the\n * user in their manifest. In both cases, when targeting MV2, automatically\n * convert their definitions down to the basic MV2 array.\n */\nexport function convertWebAccessibleResourcesToMv2(\n  manifest: Browser.runtime.Manifest,\n): void {\n  if (manifest.web_accessible_resources == null) return;\n\n  manifest.web_accessible_resources = Array.from(\n    new Set(\n      manifest.web_accessible_resources.flatMap((item) => {\n        if (typeof item === 'string') return item;\n        return item.resources;\n      }),\n    ),\n  );\n}\n\nfunction moveHostPermissionsToPermissions(\n  manifest: Browser.runtime.Manifest,\n): void {\n  if (!manifest.host_permissions?.length) return;\n\n  manifest.host_permissions.forEach((permission: string) =>\n    addPermission(manifest, permission),\n  );\n  delete manifest.host_permissions;\n}\n\nfunction convertActionToMv2(manifest: Browser.runtime.Manifest): void {\n  if (\n    manifest.action == null ||\n    manifest.browser_action != null ||\n    manifest.page_action != null\n  )\n    return;\n\n  manifest.browser_action = manifest.action;\n}\n\nfunction convertCspToMv2(manifest: Browser.runtime.Manifest): void {\n  if (\n    typeof manifest.content_security_policy === 'string' ||\n    manifest.content_security_policy?.extension_pages == null\n  )\n    return;\n\n  manifest.content_security_policy =\n    manifest.content_security_policy.extension_pages;\n}\n\n/** Make sure all resources are in MV3 format. If not, add a warning. */\nfunction validateMv3WebAccessibleResources(\n  manifest: Browser.runtime.Manifest,\n): void {\n  if (manifest.web_accessible_resources == null) return;\n\n  const stringResources = manifest.web_accessible_resources.filter(\n    (item) => typeof item === 'string',\n  );\n  if (stringResources.length > 0) {\n    throw Error(\n      `Non-MV3 web_accessible_resources detected: ${JSON.stringify(\n        stringResources,\n      )}. When manually defining web_accessible_resources, define them as MV3 objects ({ matches: [...], resources: [...] }), and WXT will automatically convert them to MV2 when necessary.`,\n    );\n  }\n}\n\n/** Remove keys from the manifest based on the build target. */\nfunction stripKeys(manifest: Browser.runtime.Manifest): void {\n  let keysToRemove: string[] = [];\n  if (wxt.config.manifestVersion === 2) {\n    keysToRemove.push(...mv3OnlyKeys);\n    if (wxt.config.browser === 'firefox')\n      keysToRemove.push(...firefoxMv3OnlyKeys);\n  } else {\n    keysToRemove.push(...mv2OnlyKeys);\n  }\n\n  keysToRemove.forEach((key) => {\n    delete manifest[key as keyof Browser.runtime.Manifest];\n  });\n}\n\nconst mv2OnlyKeys = [\n  'page_action',\n  'browser_action',\n  'automation',\n  'content_capabilities',\n  'converted_from_user_script',\n  'current_locale',\n  'differential_fingerprint',\n  'event_rules',\n  'file_browser_handlers',\n  'file_system_provider_capabilities',\n  'nacl_modules',\n  'natively_connectable',\n  'offline_enabled',\n  'platforms',\n  'replacement_web_app',\n  'system_indicator',\n  'user_scripts',\n];\n\nconst mv3OnlyKeys = [\n  'action',\n  'export',\n  'optional_host_permissions',\n  'side_panel',\n];\nconst firefoxMv3OnlyKeys = ['host_permissions'];\n\nconst DEFAULT_MV3_EXTENSION_PAGES_CSP =\n  \"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';\";\nconst DEFAULT_MV3_SANDBOX_CSP =\n  \"sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';\";\nconst DEFAULT_MV2_CSP = \"script-src 'self'; object-src 'self';\";\n"
  },
  {
    "path": "packages/wxt/src/core/utils/network.ts",
    "content": "import dns from 'node:dns';\nimport { ResolvedConfig } from '../../types';\nimport { withTimeout } from './time';\nimport consola from 'consola';\n\nasync function isOffline(): Promise<boolean> {\n  try {\n    const isOffline = new Promise<boolean>((res) => {\n      dns.resolve('google.com', (err) => res(err != null));\n    });\n    return await withTimeout(isOffline, 1e3);\n  } catch (error) {\n    consola.error('Error checking offline status:', error);\n    return true;\n  }\n}\n\nexport async function isOnline(): Promise<boolean> {\n  const offline = await isOffline();\n  return !offline;\n}\n\n/**\n * Fetches a URL with a simple GET request. Grabs it from cache if it doesn't\n * exist, or throws an error if it can't be resolved via the network or cache.\n */\nexport async function fetchCached(\n  url: string,\n  config: ResolvedConfig,\n): Promise<string> {\n  let content: string = '';\n\n  if (await isOnline()) {\n    const res = await fetch(url);\n    if (res.status < 300) {\n      content = await res.text();\n      await config.fsCache.set(url, content);\n    } else {\n      config.logger.debug(\n        `Failed to download \"${url}\", falling back to cache...`,\n      );\n    }\n  }\n\n  if (!content) content = (await config.fsCache.get(url)) ?? '';\n  if (!content)\n    throw Error(\n      `Offline and \"${url}\" has not been cached. Try again when online.`,\n    );\n\n  return content;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/number.ts",
    "content": "export function safeStringToNumber(str: string | undefined): number | null {\n  const num = Number(str);\n  return isNaN(num) ? null : num;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/package.ts",
    "content": "import { resolve } from 'node:path';\nimport { readJson } from './fs';\nimport { wxt } from '../wxt';\n\n/**\n * Read the project's package.json.\n *\n * TODO: look in root and up directories until it's found\n */\nexport async function getPackageJson(): Promise<\n  Partial<Record<string, any>> | undefined\n> {\n  const file = resolve(wxt.config.root, 'package.json');\n  try {\n    return await readJson(file);\n  } catch (err) {\n    wxt.logger.debug(\n      `Failed to read package.json at: ${file}. Returning undefined.`,\n      err,\n    );\n    return {};\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/paths.ts",
    "content": "import systemPath from 'node:path';\nimport normalize from 'normalize-path';\n\n/**\n * Converts system paths to normalized bundler path. On Windows, this returns\n * paths with `/` instead of `\\`.\n */\nexport function normalizePath(path: string): string {\n  return normalize(path);\n}\n\n/**\n * Given a normalized path, convert it to the system path style. On Windows,\n * switch to , otherwise use /.\n */\nexport function unnormalizePath(path: string): string {\n  return systemPath.normalize(path);\n}\n\nexport const CSS_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus'];\n\n// .module.css files are not supported because these are global CSS files, so using CSS modules doesn't make sense.\nexport const CSS_EXTENSIONS_PATTERN = `+(${CSS_EXTENSIONS.join('|')})`;\n"
  },
  {
    "path": "packages/wxt/src/core/utils/picomatch-multiple.ts",
    "content": "import picomatch, { PicomatchOptions } from 'picomatch';\n\n/**\n * Run [`picomatch`](https://npmjs.com/package/picomatch) against multiple\n * patterns.\n *\n * Supports negated patterns, the order does not matter. If your `search` string\n * matches any of the negative patterns, it will return `false`.\n *\n * @example\n *   ```ts\n *   picomatchMultiple('a.json', ['*.json', '!b.json']); // => true\n *   picomatchMultiple('b.json', ['*.json', '!b.json']); // => false\n *   ```;\n */\nexport function picomatchMultiple(\n  search: string,\n  patterns: string[] | undefined,\n  options?: PicomatchOptions,\n): boolean {\n  if (patterns == null) return false;\n\n  const negatePatterns: string[] = [];\n  const positivePatterns: string[] = [];\n  for (const pattern of patterns) {\n    if (pattern[0] === '!') negatePatterns.push(pattern.slice(1));\n    else positivePatterns.push(pattern);\n  }\n\n  if (\n    negatePatterns.some((negatePattern) =>\n      picomatch(negatePattern, options)(search),\n    )\n  )\n    return false;\n\n  return positivePatterns.some((positivePattern) =>\n    picomatch(positivePattern, options)(search),\n  );\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/strings.ts",
    "content": "import { camelCase } from 'scule';\n\nexport function kebabCaseAlphanumeric(str: string): string {\n  return str\n    .toLowerCase()\n    .replace(/[^a-z0-9-\\s]/g, '') // Remove all non-alphanumeric, non-hyphen characters\n    .replace(/\\s+/g, '-'); // Replace spaces with hyphens\n}\n\n/** Return a safe variable name for a given string. */\nexport function safeVarName(str: string): string {\n  const name = camelCase(kebabCaseAlphanumeric(str));\n  if (name.match(/^[a-z]/)) return name;\n  // _ prefix to ensure it doesn't start with a number or other invalid symbol\n  return '_' + name;\n}\n\n/**\n * Converts a string to a valid filename (NOT path), stripping out invalid\n * characters.\n */\nexport function safeFilename(str: string): string {\n  return kebabCaseAlphanumeric(str);\n}\n\n/**\n * Removes import statements from the top of a file. Keeps import.meta and\n * inline, async `import()` calls.\n */\nexport function removeImportStatements(text: string): string {\n  return text.replace(\n    /(import\\s?[\\s\\S]*?from\\s?[\"'][\\s\\S]*?[\"'];?|import\\s?[\"'][\\s\\S]*?[\"'];?)/gm,\n    '',\n  );\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/syntax-errors.ts",
    "content": "import { relative } from 'node:path';\nimport pc from 'picocolors';\nimport { wxt } from '../wxt';\n\nexport interface BabelSyntaxError extends SyntaxError {\n  code: 'BABEL_PARSER_SYNTAX_ERROR';\n  frame?: string;\n  id: string;\n  loc: { line: number; column: number };\n}\n\nexport function isBabelSyntaxError(error: unknown): error is BabelSyntaxError {\n  return (\n    error instanceof SyntaxError &&\n    (error as any).code === 'BABEL_PARSER_SYNTAX_ERROR'\n  );\n}\n\nexport function logBabelSyntaxError(error: BabelSyntaxError) {\n  let filename = relative(wxt.config.root, error.id);\n  if (filename.startsWith('..')) {\n    filename = error.id;\n  }\n  let message = error.message.replace(\n    /\\(\\d+:\\d+\\)$/,\n    `(${filename}:${error.loc.line}:${error.loc.column + 1})`,\n  );\n  if (error.frame) {\n    message += '\\n\\n' + pc.red(error.frame);\n  }\n  wxt.logger.error(message);\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/testing/fake-objects.ts",
    "content": "/** SHOULD ONLY BE IMPORTED IN TESTS. */\nimport { resolve } from 'path';\nimport { faker } from '@faker-js/faker';\nimport merge from 'lodash.merge';\nimport {\n  FsCache,\n  ResolvedConfig,\n  WxtDevServer,\n  BackgroundEntrypoint,\n  ContentScriptEntrypoint,\n  GenericEntrypoint,\n  OptionsEntrypoint,\n  PopupEntrypoint,\n  OutputChunk,\n  OutputAsset,\n  BuildOutput,\n  BuildStepOutput,\n  UserManifest,\n  Wxt,\n  SidepanelEntrypoint,\n  BaseEntrypoint,\n  UnlistedScriptEntrypoint,\n} from '../../../types';\nimport { mock } from 'vitest-mock-extended';\nimport { vi } from 'vitest';\nimport { setWxtForTesting } from '../../wxt';\nimport type { Browser } from '@wxt-dev/browser';\n\nfaker.seed(import.meta.test.SEED);\n\ntype DeepPartial<T> = T extends object\n  ? {\n      [P in keyof T]?: DeepPartial<T[P]>;\n    }\n  : T;\nfunction fakeObjectCreator<T>(base: () => T) {\n  return (overrides?: DeepPartial<T>): T => merge(base(), overrides);\n}\n\nexport function fakeFileName(): string {\n  return faker.string.alphanumeric() + '.' + faker.string.alpha({ length: 3 });\n}\n\nexport function fakeFile(root = process.cwd()): string {\n  return resolve(root, fakeFileName());\n}\n\nexport function fakeDir(root = process.cwd()): string {\n  return resolve(root, faker.string.alphanumeric());\n}\n\nexport const fakeEntrypoint = (options?: DeepPartial<BaseEntrypoint>) =>\n  faker.helpers.arrayElement([\n    fakePopupEntrypoint,\n    fakeGenericEntrypoint,\n    fakeOptionsEntrypoint,\n    fakeBackgroundEntrypoint,\n    fakeContentScriptEntrypoint,\n    fakeUnlistedScriptEntrypoint,\n  ])(options);\n\nexport const fakeContentScriptEntrypoint =\n  fakeObjectCreator<ContentScriptEntrypoint>(() => ({\n    type: 'content-script',\n    inputPath: fakeFile('src'),\n    name: faker.string.alpha(),\n    options: {\n      matches: [],\n      matchAboutBlank: faker.datatype.boolean(),\n      matchOriginAsFallback: faker.helpers.arrayElement([\n        true,\n        false,\n        undefined,\n      ]),\n      runAt: faker.helpers.arrayElement([\n        'document_start',\n        'document_end',\n        'document_idle',\n        undefined,\n      ]),\n    },\n    outputDir: fakeDir('.output'),\n    skipped: faker.datatype.boolean(),\n  }));\n\nexport const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(\n  () => ({\n    type: 'background',\n    inputPath: 'entrypoints/background.ts',\n    name: 'background',\n    options: {\n      persistent: faker.datatype.boolean(),\n      type: faker.helpers.maybe(() => 'module'),\n    },\n    outputDir: fakeDir('.output'),\n    skipped: faker.datatype.boolean(),\n  }),\n);\n\nexport const fakeUnlistedScriptEntrypoint =\n  fakeObjectCreator<UnlistedScriptEntrypoint>(() => ({\n    type: 'unlisted-script',\n    inputPath: fakeFile('src'),\n    name: faker.string.alpha(),\n    outputDir: fakeDir('.output'),\n    options: {},\n    skipped: faker.datatype.boolean(),\n  }));\n\nexport const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(\n  () => ({\n    type: 'options',\n    inputPath: 'entrypoints/options.html',\n    name: 'options',\n    outputDir: fakeDir('.output'),\n    options: {\n      browserStyle: faker.datatype.boolean(),\n      chromeStyle: faker.datatype.boolean(),\n      openInTab: faker.datatype.boolean(),\n    },\n    skipped: faker.datatype.boolean(),\n  }),\n);\n\nexport const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({\n  type: 'popup',\n  inputPath: 'entrypoints/popup.html',\n  name: 'popup',\n  outputDir: fakeDir('.output'),\n  options: {\n    defaultTitle: faker.helpers.arrayElement([\n      faker.person.fullName(),\n      undefined,\n    ]),\n    defaultIcon: faker.helpers.arrayElement([\n      {\n        '16': 'icon/16.png',\n        '24': 'icon/24.png',\n        '64': 'icon/64.png',\n      },\n    ]),\n    mv2Key: faker.helpers.arrayElement([\n      'browser_action',\n      'page_action',\n      undefined,\n    ]),\n    // Firefox-specific options - kept undefined by default to avoid breaking existing tests\n    browserStyle: undefined,\n    defaultArea: undefined,\n    themeIcons: undefined,\n  },\n  skipped: faker.datatype.boolean(),\n}));\n\nexport const fakeSidepanelEntrypoint = fakeObjectCreator<SidepanelEntrypoint>(\n  () => ({\n    type: 'sidepanel',\n    inputPath: 'entrypoints/sidepanel.html',\n    name: 'sidepanel',\n    outputDir: fakeDir('.output'),\n    options: {\n      defaultTitle: faker.helpers.arrayElement([\n        faker.person.fullName(),\n        undefined,\n      ]),\n      defaultIcon: faker.helpers.arrayElement([\n        {\n          '16': 'icon/16.png',\n          '24': 'icon/24.png',\n          '64': 'icon/64.png',\n        },\n      ]),\n      openAtInstall: faker.helpers.arrayElement([true, false, undefined]),\n    },\n    skipped: faker.datatype.boolean(),\n  }),\n);\n\nexport const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(\n  () => ({\n    type: faker.helpers.arrayElement([\n      'sandbox',\n      'bookmarks',\n      'history',\n      'newtab',\n      'devtools',\n      'unlisted-page',\n    ]),\n    inputPath: fakeFile('src'),\n    name: faker.string.alpha(),\n    outputDir: fakeDir('.output'),\n    options: {},\n    skipped: faker.datatype.boolean(),\n  }),\n);\n\nexport const fakeOutputChunk = fakeObjectCreator<OutputChunk>(() => ({\n  type: 'chunk',\n  fileName: faker.string.alphanumeric(),\n  moduleIds: [],\n}));\n\nexport const fakeOutputAsset = fakeObjectCreator<OutputAsset>(() => ({\n  type: 'asset',\n  fileName: fakeFileName(),\n}));\n\nexport const fakeManifest = fakeObjectCreator<Browser.runtime.Manifest>(() => ({\n  manifest_version: faker.helpers.arrayElement([2, 3]),\n  name: faker.string.alphanumeric(),\n  version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,\n}));\n\nexport const fakeUserManifest = fakeObjectCreator<UserManifest>(() => ({\n  name: faker.string.alphanumeric(),\n  version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,\n}));\n\nexport function fakeArray<T>(createItem: () => T, count = 3): T[] {\n  const array: T[] = [];\n  for (let i = 0; i < count; i++) {\n    array.push(createItem());\n  }\n  return array;\n}\n\nexport const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {\n  const browser = faker.helpers.arrayElement(['chrome', 'firefox']);\n  const command = faker.helpers.arrayElement(['build', 'serve'] as const);\n  const manifestVersion = faker.helpers.arrayElement([2, 3] as const);\n  const mode = faker.helpers.arrayElement(['development', 'production']);\n\n  return {\n    browser,\n    targetBrowsers: [],\n    command,\n    entrypointsDir: fakeDir(),\n    modulesDir: fakeDir(),\n    builtinModules: [],\n    userModules: [],\n    env: { browser, command, manifestVersion, mode },\n    fsCache: mock<FsCache>(),\n    imports: {\n      disabled: faker.datatype.boolean(),\n      eslintrc: {\n        enabled: faker.helpers.arrayElement([false, 8, 9]),\n        filePath: fakeFile(),\n        globalsPropValue: faker.helpers.arrayElement([\n          true,\n          false,\n          'readable',\n          'readonly',\n          'writable',\n          'writeable',\n        ] as const),\n      },\n    },\n    logger: mock(),\n    manifest: fakeUserManifest(),\n    manifestVersion,\n    mode,\n    outBaseDir: fakeDir(),\n    outDir: fakeDir(),\n    publicDir: fakeDir(),\n    root: fakeDir(),\n    wxtModuleDir: fakeDir(),\n    runnerConfig: {\n      config: {},\n    },\n    debug: faker.datatype.boolean(),\n    srcDir: fakeDir(),\n    typesDir: fakeDir(),\n    wxtDir: fakeDir(),\n    analysis: {\n      enabled: false,\n      open: false,\n      template: 'treemap',\n      outputFile: fakeFile(),\n      outputDir: fakeDir(),\n      outputName: 'stats',\n      keepArtifacts: false,\n    },\n    zip: {\n      artifactTemplate: '{{name}}-{{version}}.zip',\n      includeSources: [],\n      excludeSources: [],\n      exclude: [],\n      sourcesRoot: fakeDir(),\n      sourcesTemplate: '{{name}}-sources.zip',\n      name: faker.person.firstName().toLowerCase(),\n      downloadedPackagesDir: fakeDir(),\n      downloadPackages: [],\n      compressionLevel: 9,\n      zipSources: false,\n    },\n    userConfigMetadata: {},\n    alias: {},\n    experimental: {},\n    dev: {\n      reloadCommand: 'Alt+R',\n    },\n    hooks: {},\n    vite: () => ({}),\n    plugins: [],\n  };\n});\n\nexport const fakeWxt = fakeObjectCreator<Wxt>(() => ({\n  config: fakeResolvedConfig(),\n  hook: vi.fn(),\n  hooks: mock(),\n  logger: mock(),\n  reloadConfig: vi.fn(),\n  pm: mock(),\n  server: faker.helpers.arrayElement([undefined, fakeWxtDevServer()]),\n  builder: mock(),\n}));\n\nexport const fakeWxtDevServer = fakeObjectCreator<WxtDevServer>(() => ({\n  currentOutput: fakeBuildOutput(),\n  host: 'localhost',\n  port: 3000,\n  origin: 'http://localhost:3000',\n  reloadContentScript: vi.fn(),\n  reloadExtension: vi.fn(),\n  reloadPage: vi.fn(),\n  restart: vi.fn(),\n  restartBrowser: vi.fn(),\n  start: vi.fn(),\n  stop: vi.fn(),\n  transformHtml: vi.fn(),\n  watcher: mock(),\n  ws: mock(),\n}));\n\nexport function setFakeWxt(overrides?: DeepPartial<Wxt>) {\n  const wxt = fakeWxt(overrides);\n  setWxtForTesting(wxt);\n  return wxt;\n}\n\nexport const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({\n  manifest: fakeManifest(),\n  publicAssets: fakeArray(fakeOutputAsset),\n  steps: fakeArray(fakeBuildStepOutput),\n}));\n\nexport const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({\n  chunks: fakeArray(fakeOutputChunk),\n  entrypoints: fakeArray(fakeEntrypoint),\n}));\n\nexport const fakeManifestCommand = fakeObjectCreator<Browser.commands.Command>(\n  () => ({\n    description: faker.string.sample(),\n    suggested_key: {\n      default: `${faker.helpers.arrayElement(['ctrl', 'alt'])}+${faker.number.int(\n        {\n          min: 0,\n          max: 9,\n        },\n      )}`,\n    },\n  }),\n);\n\nexport const fakeDevServer = fakeObjectCreator<WxtDevServer>(() => ({\n  host: 'localhost',\n  port: 5173,\n  origin: 'http://localhost:3000',\n  reloadContentScript: vi.fn(),\n  reloadExtension: vi.fn(),\n  reloadPage: vi.fn(),\n  restart: vi.fn(),\n  restartBrowser: vi.fn(),\n  stop: vi.fn(),\n  start: vi.fn(),\n  watcher: mock(),\n  transformHtml: vi.fn(),\n  ws: mock(),\n  currentOutput: undefined,\n}));\n"
  },
  {
    "path": "packages/wxt/src/core/utils/time.ts",
    "content": "export function formatDuration(duration: number): string {\n  if (duration < 1e3) return `${duration} ms`;\n  if (duration < 10e3) return `${(duration / 1e3).toFixed(3)} s`;\n  if (duration < 60e3) return `${(duration / 1e3).toFixed(1)} s`;\n  return `${(duration / 1e3).toFixed(0)} s`;\n}\n\n/** Add a timeout to a promise. */\nexport function withTimeout<T>(\n  promise: Promise<T>,\n  duration: number,\n): Promise<T> {\n  return new Promise((res, rej) => {\n    const timeout = setTimeout(() => {\n      rej(`Promise timed out after ${duration}ms`);\n    }, duration);\n    promise\n      .then(res)\n      .catch(rej)\n      .finally(() => clearTimeout(timeout));\n  });\n}\n\n/**\n * @deprecated Don't use in production, just for testing and slowing things\n *   down.\n */\nexport function sleep(ms: number): Promise<void> {\n  return new Promise((res) => setTimeout(res, ms));\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/transform.ts",
    "content": "import { ProxifiedModule, parseModule } from 'magicast';\n\n/**\n * Removes any code used at runtime related to an entrypoint's main function.\n *\n * 1. Removes or clears out `main` function from returned object\n * 2. Removes any unused functions/variables outside the definition that aren't\n *    being called/used\n * 3. Removes unused imports\n * 4. Removes value-less, side-effect only imports (like `import \"./styles.css\"` or\n *    `import \"polyfill\"`)\n */\nexport function removeMainFunctionCode(code: string): {\n  code: string;\n  map?: string;\n} {\n  const mod = parseModule(code);\n  emptyMainFunction(mod);\n  let removedCount = 0;\n  let depth = 0;\n  const maxDepth = 10;\n  do {\n    removedCount = 0;\n    removedCount += removeUnusedTopLevelVariables(mod);\n    removedCount += removeUnusedTopLevelFunctions(mod);\n    removedCount += removeUnusedImports(mod);\n  } while (removedCount > 0 && depth++ <= maxDepth);\n  removeSideEffectImports(mod);\n  return mod.generate();\n}\n\nfunction emptyMainFunction(mod: ProxifiedModule): void {\n  if (mod.exports?.default?.$type === 'function-call') {\n    if (mod.exports.default.$ast?.arguments?.[0]?.body) {\n      // Remove body from function\n      // ex: \"fn(() => { ... })\" to \"fn()\"\n      // ex: \"fn(function () { ... })\" to \"fn()\"\n      delete mod.exports.default.$ast.arguments[0];\n    } else if (mod.exports.default.$ast?.arguments?.[0]?.properties) {\n      // Remove main field from object\n      // ex: \"fn({ ..., main: () => {} })\" to \"fn({ ... })\"\n      mod.exports.default.$ast.arguments[0].properties =\n        mod.exports.default.$ast.arguments[0].properties.filter(\n          (prop: any) => prop.key.name !== 'main',\n        );\n    }\n  }\n}\n\nfunction removeUnusedTopLevelVariables(mod: ProxifiedModule): number {\n  const simpleAst = getSimpleAstJson(mod.$ast);\n  const usedMap = findUsedIdentifiers(simpleAst);\n\n  let deletedCount = 0;\n  const ast = mod.$ast as any;\n\n  const isUsed = (id: any) => {\n    return id?.type === 'Identifier' && usedMap.get(id.name);\n  };\n\n  const cleanArrayPattern = (pattern: any): boolean => {\n    const elements = pattern.elements;\n    for (let i = elements.length - 1; i >= 0; i--) {\n      const el = elements[i];\n      if (el?.type === 'Identifier' && !isUsed(el)) {\n        elements.splice(i, 1);\n        deletedCount++;\n      }\n    }\n    return elements.length === 0;\n  };\n\n  const cleanObjectPattern = (pattern: any): boolean => {\n    const properties = pattern.properties;\n    for (let i = properties.length - 1; i >= 0; i--) {\n      const prop = properties[i];\n\n      if (prop.type === 'Property') {\n        const value = prop.value;\n        // support nested object\n        if (value.type === 'ObjectPattern') {\n          const isEmpty = cleanObjectPattern(value);\n          if (isEmpty) {\n            properties.splice(i, 1);\n          }\n        } else if (value.type === 'ArrayPattern') {\n          const isEmpty = cleanArrayPattern(value);\n          if (isEmpty) {\n            properties.splice(i, 1);\n          }\n        } else if (value.type === 'Identifier' && !isUsed(value)) {\n          properties.splice(i, 1);\n          deletedCount++;\n        }\n      } else if (prop.type === 'RestElement') {\n        const arg = prop.argument;\n        if (arg.type === 'Identifier' && !isUsed(arg)) {\n          properties.splice(i, 1);\n          deletedCount++;\n        }\n      }\n    }\n    return properties.length === 0;\n  };\n\n  for (let i = ast.body.length - 1; i >= 0; i--) {\n    if (ast.body[i].type !== 'VariableDeclaration') continue;\n\n    for (let j = ast.body[i].declarations.length - 1; j >= 0; j--) {\n      const id = ast.body[i].declarations[j].id;\n\n      let shouldRemove = false;\n\n      if (id.type === 'Identifier') {\n        shouldRemove = !isUsed(id);\n        if (shouldRemove) deletedCount++;\n      } else if (id.type === 'ArrayPattern') {\n        shouldRemove = cleanArrayPattern(id);\n      } else if (id.type === 'ObjectPattern') {\n        shouldRemove = cleanObjectPattern(id);\n      }\n\n      if (shouldRemove) {\n        ast.body[i].declarations.splice(j, 1);\n      }\n    }\n\n    if (ast.body[i].declarations.length === 0) {\n      ast.body.splice(i, 1);\n    }\n  }\n\n  return deletedCount;\n}\n\nfunction removeUnusedTopLevelFunctions(mod: ProxifiedModule): number {\n  const simpleAst = getSimpleAstJson(mod.$ast);\n  const usedMap = findUsedIdentifiers(simpleAst);\n\n  let deletedCount = 0;\n  const ast = mod.$ast as any;\n  for (let i = ast.body.length - 1; i >= 0; i--) {\n    if (\n      ast.body[i].type === 'FunctionDeclaration' &&\n      !usedMap.get(ast.body[i].id.name)\n    ) {\n      ast.body.splice(i, 1);\n      deletedCount++;\n    }\n  }\n  return deletedCount;\n}\n\nfunction removeUnusedImports(mod: ProxifiedModule): number {\n  const simpleAst = getSimpleAstJson(mod.$ast);\n  const usedMap = findUsedIdentifiers(simpleAst);\n  const importSymbols = Object.keys(mod.imports);\n\n  let deletedCount = 0;\n  importSymbols.forEach((name) => {\n    if (usedMap.get(name)) return;\n\n    delete mod.imports[name];\n    deletedCount++;\n  });\n  return deletedCount;\n}\n\n// TODO: Do a more complex declaration analysis where shadowed variables are detected and ignored.\n// Right now, this code assumes there are no shadowed variables.\nfunction findUsedIdentifiers(simpleAst: any) {\n  const usedMap = new Map<string, boolean>();\n  const queue: any[] = [simpleAst];\n  for (const item of queue) {\n    if (!item) {\n    } else if (Array.isArray(item)) {\n      queue.push(...item);\n    } else if (item.type === 'ImportDeclaration') {\n      // Don't look inside imports, identifiers are only used for declaration\n      continue;\n    } else if (item.type === 'Identifier') {\n      usedMap.set(item.name, true);\n    } else if (typeof item === 'object') {\n      const filterFns: Record<string, (entry: [string, any]) => boolean> = {\n        // Ignore the function declaration's name\n        FunctionDeclaration: ([key]) => key !== 'id',\n        // Ignore object property names\n        ObjectProperty: ([key]) => key !== 'key',\n        // Ignore variable declaration's name\n        VariableDeclarator: ([key]) => key !== 'id',\n      };\n      queue.push(\n        Object.entries(item)\n          .filter(filterFns[item.type] ?? (() => true))\n          .map(([_, value]) => value),\n      );\n    }\n  }\n  return usedMap;\n}\n\nfunction deleteImportAst(\n  mod: ProxifiedModule,\n  shouldDelete: (node: any) => boolean,\n): void {\n  const importIndexesToDelete: number[] = [];\n  (mod.$ast as any).body.forEach((node: any, index: number) => {\n    if (node.type === 'ImportDeclaration' && shouldDelete(node)) {\n      importIndexesToDelete.push(index);\n    }\n  });\n  importIndexesToDelete.reverse().forEach((i) => {\n    delete (mod.$ast as any).body[i];\n  });\n}\n\nfunction removeSideEffectImports(mod: ProxifiedModule): void {\n  deleteImportAst(mod, (node) => node.specifiers.length === 0);\n}\n\n/**\n * Util to get the AST as a simple JSON object, stripping out large objects and\n * file locations to keep it readible\n */\nfunction getSimpleAstJson(ast: any): any {\n  if (!ast) {\n    return ast;\n  } else if (Array.isArray(ast)) {\n    return ast.map(getSimpleAstJson);\n  } else if (typeof ast === 'object') {\n    return Object.fromEntries(\n      Object.entries(ast)\n        .filter(([key]) => key !== 'loc' && key !== 'start' && key !== 'end')\n        .map(([key, value]) => [key, getSimpleAstJson(value)]),\n    );\n  } else {\n    return ast;\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/types.ts",
    "content": "import type { Browser } from '@wxt-dev/browser';\n\n/**\n * Remove optional from key, but keep undefined if present\n *\n * @example\n *   type Test = NullablyRequired<{ a?: string; b: number }>;\n *   // type Test = {a: string | undefined, b: number}\n */\nexport type NullablyRequired<T> = { [K in keyof Required<T>]: T[K] };\n\nexport type ManifestContentScript = NonNullable<\n  Browser.runtime.Manifest['content_scripts']\n>[number];\n\nexport type ManifestV3WebAccessibleResource = NonNullable<\n  Browser.runtime.ManifestV3['web_accessible_resources']\n>[number];\n"
  },
  {
    "path": "packages/wxt/src/core/utils/validation.ts",
    "content": "import { ContentScriptEntrypoint, Entrypoint } from '../../types';\n\nexport function validateEntrypoints(\n  entrypoints: Entrypoint[],\n): ValidationResults {\n  const errors = entrypoints.flatMap((entrypoint) => {\n    switch (entrypoint.type) {\n      case 'content-script':\n        return validateContentScriptEntrypoint(entrypoint);\n      default:\n        return validateBaseEntrypoint(entrypoint);\n    }\n  });\n\n  let errorCount = 0;\n  let warningCount = 0;\n  for (const err of errors) {\n    if (err.type === 'warning') warningCount++;\n    else errorCount++;\n  }\n\n  return {\n    errors,\n    errorCount,\n    warningCount,\n  };\n}\n\nfunction validateContentScriptEntrypoint(\n  definition: ContentScriptEntrypoint,\n): ValidationResult[] {\n  const errors = validateBaseEntrypoint(definition);\n  if (\n    definition.options.registration !== 'runtime' &&\n    definition.options.matches == null\n  ) {\n    errors.push({\n      type: 'error',\n      message: '`matches` is required for manifest registered content scripts',\n      value: definition.options.matches,\n      entrypoint: definition,\n    });\n  }\n  return errors;\n}\n\nfunction validateBaseEntrypoint(definition: Entrypoint): ValidationResult[] {\n  const errors: ValidationResult[] = [];\n\n  if (\n    definition.options.exclude != null &&\n    !Array.isArray(definition.options.exclude)\n  ) {\n    errors.push({\n      type: 'error',\n      message: '`exclude` must be an array of browser names',\n      value: definition.options.exclude,\n      entrypoint: definition,\n    });\n  }\n  if (\n    definition.options.include != null &&\n    !Array.isArray(definition.options.include)\n  ) {\n    errors.push({\n      type: 'error',\n      message: '`include` must be an array of browser names',\n      value: definition.options.include,\n      entrypoint: definition,\n    });\n  }\n\n  return errors;\n}\n\nexport interface ValidationResult {\n  type: 'warning' | 'error';\n  message: string;\n  entrypoint: Entrypoint;\n  value: any;\n}\n\nexport interface ValidationResults {\n  errors: ValidationResult[];\n  errorCount: number;\n  warningCount: number;\n}\n\nexport class ValidationError extends Error {}\n"
  },
  {
    "path": "packages/wxt/src/core/utils/virtual-modules.ts",
    "content": "export const virtualEntrypointTypes = [\n  'content-script-main-world' as const,\n  'content-script-isolated-world' as const,\n  'background' as const,\n  'unlisted-script' as const,\n];\nexport type VirtualEntrypointType = (typeof virtualEntrypointTypes)[0];\n\n/**\n * All the names of entrypoint files in the `src/virtual/` and `dist/virtual/`\n * directories, minus the extension.\n */\nexport const virtualEntrypointModuleNames = virtualEntrypointTypes.map(\n  (name) => `${name}-entrypoint` as const,\n);\n/**\n * Name of entrypoint files in the `src/virtual/` and `dist/virtual/`\n * directories, minus the extension.\n */\nexport type VirtualEntrypointModuleName =\n  (typeof virtualEntrypointModuleNames)[0];\n\n/**\n * All the names of files in the `src/virtual/` and `dist/virtual/` directories,\n * minus the extension.\n */\nexport const virtualModuleNames = [\n  ...virtualEntrypointModuleNames,\n  'mock-browser' as const,\n  'reload-html' as const,\n];\n/**\n * Name of files in the `src/virtual/` and `dist/virtual/` directories, minus\n * the extension.\n */\nexport type VirtualModuleName = (typeof virtualModuleNames)[0];\n\n/** Import alias used for importing a virtual module */\nexport type VirtualModuleId = `virtual:wxt-${VirtualModuleName}`;\n"
  },
  {
    "path": "packages/wxt/src/core/utils/wsl.ts",
    "content": "// TODO: Someone smarter than me should just mock this module instead.\nimport isWsl_ from 'is-wsl';\n\n/** Returns true when running on WSL or WSL2. */\nexport function isWsl(): boolean {\n  return isWsl_;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/wxt.ts",
    "content": "import { InlineConfig, Wxt, WxtCommand, WxtHooks, WxtModule } from '../types';\nimport { resolveConfig } from './resolve-config';\nimport { createHooks } from 'hookable';\nimport { createWxtPackageManager } from './package-managers';\nimport { createViteBuilder } from './builders/vite';\nimport { builtinModules } from '../builtin-modules';\nimport { relative } from 'path';\n\n/**\n * Global variable set once `createWxt` is called once. Since this variable is\n * used everywhere, this global can be used instead of passing the variable as a\n * function parameter everywhere.\n */\nexport let wxt: Wxt;\n\n/**\n * Create and register a global instance of the Wxt interface for use throughout\n * the project.\n */\nexport async function registerWxt(\n  command: WxtCommand,\n  inlineConfig: InlineConfig = {},\n): Promise<void> {\n  // Default NODE_ENV environment variable before other packages, like vite, do it\n  // See https://github.com/wxt-dev/wxt/issues/873#issuecomment-2254555523\n  process.env.NODE_ENV ??=\n    inlineConfig.mode ?? (command === 'serve' ? 'development' : 'production');\n\n  const hooks = createHooks<WxtHooks>();\n  const config = await resolveConfig(inlineConfig, command);\n  const [builder, pm] = await Promise.all([\n    createViteBuilder(config, hooks, () => wxt.server),\n    createWxtPackageManager(config.root),\n  ]);\n\n  wxt = {\n    config,\n    hooks,\n    hook: hooks.hook.bind(hooks),\n    get logger() {\n      return config.logger;\n    },\n    async reloadConfig() {\n      // Prevent changing the server port when resolving config multiple times\n      // get-port-please doesn't always return the same port if it was recently closed.\n      if (wxt.config.dev.server?.port) {\n        inlineConfig.dev ??= {};\n        inlineConfig.dev.server ??= {};\n        inlineConfig.dev.server.port = wxt.config.dev.server.port;\n      }\n\n      wxt.config = await resolveConfig(inlineConfig, command);\n      await wxt.hooks.callHook('config:resolved', wxt);\n    },\n    pm,\n    builder,\n    server: undefined,\n  };\n\n  await initWxtModules();\n}\n\nexport async function initWxtModules() {\n  // Call setup function and add hooks\n  for (const mod of builtinModules) await initWxtModule(mod);\n  for (const mod of wxt.config.userModules) await initWxtModule(mod);\n\n  // Initialize hooks\n  wxt.hooks.addHooks(wxt.config.hooks);\n\n  // Print order for debugging\n  if (wxt.config.debug) {\n    const order = [\n      ...builtinModules.map((module) => module.name),\n      ...wxt.config.userModules.map((module) =>\n        relative(wxt.config.root, module.id),\n      ),\n      'wxt.config.ts > hooks',\n    ];\n    wxt.logger.debug('Hook execution order:');\n    order.forEach((name, i) => {\n      wxt.logger.debug(`  ${i + 1}. ${name}`);\n    });\n  }\n\n  await wxt.hooks.callHook('ready', wxt);\n  await wxt.hooks.callHook('config:resolved', wxt);\n}\n\nasync function initWxtModule(module: WxtModule<any>): Promise<void> {\n  if (module.hooks) wxt.hooks.addHooks(module.hooks);\n  await module.setup?.(\n    wxt,\n    // @ts-expect-error: Untyped configKey field\n    module.configKey ? wxt.config[module.configKey] : undefined,\n  );\n}\n\n/** Unloads WXT modules. */\nexport function deinitWxtModules(): void {\n  wxt.hooks.removeAllHooks();\n}\n\n/**\n * @example\n *   setWxtForTesting(fakeWxt({ ... }));\n *   // Or use the shorthand\n *   setFakeWxt({ ... })\n *\n * @internal ONLY USE FOR TESTING.\n */\nexport function setWxtForTesting(testInstance: Wxt) {\n  wxt = testInstance;\n}\n"
  },
  {
    "path": "packages/wxt/src/core/zip.ts",
    "content": "import { InlineConfig } from '../types';\nimport path from 'node:path';\nimport { mkdir, readFile } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { safeFilename } from './utils/strings';\nimport { getPackageJson } from './utils/package';\nimport { formatDuration } from './utils/time';\nimport { printFileList } from './utils/log';\nimport { findEntrypoints, internalBuild } from './utils/building';\nimport { registerWxt, wxt } from './wxt';\nimport JSZip from 'jszip';\nimport { glob } from 'tinyglobby';\nimport { normalizePath } from './utils';\nimport { picomatchMultiple } from './utils/picomatch-multiple';\n\n/**\n * Build and zip the extension for distribution.\n *\n * @param config Optional config that will override your `<root>/wxt.config.ts`.\n * @returns A list of all files included in the ZIP.\n */\nexport async function zip(config?: InlineConfig): Promise<string[]> {\n  await registerWxt('build', config);\n  const output = await internalBuild();\n  await wxt.hooks.callHook('zip:start', wxt);\n\n  const start = Date.now();\n  wxt.logger.info('Zipping extension...');\n  const zipFiles: string[] = [];\n\n  const packageJson = await getPackageJson();\n  const projectName =\n    wxt.config.zip.name ??\n    safeFilename(packageJson?.name || path.basename(process.cwd()));\n  const applyTemplate = (template: string): string =>\n    template\n      .replaceAll('{{name}}', projectName)\n      .replaceAll('{{browser}}', wxt.config.browser)\n      .replaceAll(\n        '{{version}}',\n        output.manifest.version_name ?? output.manifest.version,\n      )\n      .replaceAll('{{packageVersion}}', packageJson?.version)\n      .replaceAll('{{mode}}', wxt.config.mode)\n      .replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);\n\n  await mkdir(wxt.config.outBaseDir, { recursive: true });\n\n  // ZIP output directory\n  await wxt.hooks.callHook('zip:extension:start', wxt);\n  const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);\n  const outZipPath = path.resolve(wxt.config.outBaseDir, outZipFilename);\n  await zipDir(wxt.config.outDir, outZipPath, {\n    exclude: wxt.config.zip.exclude,\n  });\n  zipFiles.push(outZipPath);\n  await wxt.hooks.callHook('zip:extension:done', wxt, outZipPath);\n\n  if (wxt.config.zip.zipSources) {\n    const entrypoints = await findEntrypoints();\n    const skippedEntrypoints = entrypoints.filter((entry) => entry.skipped);\n    const excludeSources = [\n      ...wxt.config.zip.excludeSources,\n      ...skippedEntrypoints.map((entry) =>\n        path.relative(wxt.config.zip.sourcesRoot, entry.inputPath),\n      ),\n    ].map((paths) => paths.replaceAll('\\\\', '/'));\n    await wxt.hooks.callHook('zip:sources:start', wxt);\n    const { overrides, files: downloadedPackages } =\n      await downloadPrivatePackages();\n    const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate);\n    const sourcesZipPath = path.resolve(\n      wxt.config.outBaseDir,\n      sourcesZipFilename,\n    );\n    await zipDir(wxt.config.zip.sourcesRoot, sourcesZipPath, {\n      include: wxt.config.zip.includeSources,\n      exclude: excludeSources,\n      transform(absolutePath, zipPath, content) {\n        if (zipPath.endsWith('package.json')) {\n          return addOverridesToPackageJson(absolutePath, content, overrides);\n        }\n      },\n      additionalFiles: downloadedPackages,\n    });\n    zipFiles.push(sourcesZipPath);\n    await wxt.hooks.callHook('zip:sources:done', wxt, sourcesZipPath);\n  }\n\n  await printFileList(\n    wxt.logger.success,\n    `Zipped extension in ${formatDuration(Date.now() - start)}`,\n    wxt.config.outBaseDir,\n    zipFiles,\n  );\n\n  await wxt.hooks.callHook('zip:done', wxt, zipFiles);\n\n  return zipFiles;\n}\n\nasync function zipDir(\n  directory: string,\n  outputPath: string,\n  options?: {\n    include?: string[];\n    exclude?: string[];\n    transform?: (\n      absolutePath: string,\n      zipPath: string,\n      content: string,\n    ) => Promise<string | undefined | void> | string | undefined | void;\n    additionalWork?: (archive: JSZip) => Promise<void> | void;\n    additionalFiles?: string[];\n  },\n): Promise<void> {\n  const archive = new JSZip();\n  const files = (\n    await glob(['**/*', ...(options?.include || [])], {\n      cwd: directory,\n      // Ignore node_modules, otherwise this glob step takes forever\n      ignore: ['**/node_modules'],\n      onlyFiles: true,\n      expandDirectories: false,\n    })\n  ).filter((relativePath) => {\n    return (\n      picomatchMultiple(relativePath, options?.include) ||\n      !picomatchMultiple(relativePath, options?.exclude)\n    );\n  });\n  const filesToZip = [\n    ...files,\n    ...(options?.additionalFiles ?? []).map((file) =>\n      path.relative(directory, file),\n    ),\n  ];\n  for (const file of filesToZip) {\n    const absolutePath = path.resolve(directory, file);\n    if (file.endsWith('.json')) {\n      const content = await readFile(absolutePath, 'utf-8');\n      archive.file(\n        file,\n        (await options?.transform?.(absolutePath, file, content)) || content,\n      );\n    } else {\n      const content = await readFile(absolutePath);\n      archive.file(file, content);\n    }\n  }\n  await options?.additionalWork?.(archive);\n\n  await new Promise<void>((resolve, reject) =>\n    archive\n      .generateNodeStream({\n        type: 'nodebuffer',\n        ...(wxt.config.zip.compressionLevel === 0\n          ? { compression: 'STORE' }\n          : {\n              compression: 'DEFLATE',\n              compressionOptions: { level: wxt.config.zip.compressionLevel },\n            }),\n      })\n      .pipe(createWriteStream(outputPath))\n      .on('error', reject)\n      .on('close', resolve),\n  );\n}\n\nasync function downloadPrivatePackages() {\n  const overrides: Record<string, string> = {};\n  const files: string[] = [];\n\n  if (wxt.config.zip.downloadPackages.length > 0) {\n    const _downloadPackages = new Set(wxt.config.zip.downloadPackages);\n    const allPackages = await wxt.pm.listDependencies({\n      all: true,\n      cwd: wxt.config.root,\n    });\n    const downloadPackages = allPackages.filter((pkg) =>\n      _downloadPackages.has(pkg.name),\n    );\n\n    for (const pkg of downloadPackages) {\n      wxt.logger.info(`Downloading package: ${pkg.name}@${pkg.version}`);\n      const id = `${pkg.name}@${pkg.version}`;\n      const tgzPath = await wxt.pm.downloadDependency(\n        id,\n        wxt.config.zip.downloadedPackagesDir,\n      );\n      files.push(tgzPath);\n      overrides[id] = tgzPath;\n    }\n  }\n\n  return { overrides, files };\n}\n\nfunction addOverridesToPackageJson(\n  absolutePackageJsonPath: string,\n  content: string,\n  overrides: Record<string, string>,\n): string {\n  if (Object.keys(overrides).length === 0) return content;\n\n  const packageJsonDir = path.dirname(absolutePackageJsonPath);\n  const oldPackage = JSON.parse(content);\n  const newPackage = {\n    ...oldPackage,\n    [wxt.pm.overridesKey]: { ...oldPackage[wxt.pm.overridesKey] },\n  };\n  Object.entries(overrides).forEach(([key, absolutePath]) => {\n    newPackage[wxt.pm.overridesKey][key] =\n      'file://./' + normalizePath(path.relative(packageJsonDir, absolutePath));\n  });\n  return JSON.stringify(newPackage, null, 2);\n}\n"
  },
  {
    "path": "packages/wxt/src/index.ts",
    "content": "/**\n * This module contains:\n *\n * - JS APIs used by the CLI to build extensions or start dev mode.\n * - Helper functions for defining project config.\n * - Types for building and extension or configuring WXT.\n *\n * @module wxt\n */\nexport * from './core';\nexport * from './types';\nexport * from './version';\n"
  },
  {
    "path": "packages/wxt/src/modules.ts",
    "content": "/**\n * Utilities for creating [WXT\n * Modules](https://wxt.dev/guide/essentials/wxt-modules.html).\n *\n * @module wxt/modules\n */\nimport type {\n  Entrypoint,\n  Wxt,\n  WxtModule,\n  WxtModuleOptions,\n  WxtModuleSetup,\n} from './types';\nimport type * as vite from 'vite';\nimport { glob } from 'tinyglobby';\nimport { resolve } from 'node:path';\nimport type { UnimportOptions } from 'unimport';\n\n// Re-export to prevent TS2742 type errors\nexport { WxtModule };\n\nexport function defineWxtModule<TOptions extends WxtModuleOptions>(\n  module: WxtModule<TOptions> | WxtModuleSetup<TOptions>,\n): WxtModule<TOptions> {\n  if (typeof module === 'function') return { setup: module };\n  return module;\n}\n\n/**\n * Adds a TS/JS file as an entrypoint to the project. This file will be bundled\n * along with the other entrypoints.\n *\n * If you're publishing the module to NPM, you should probably pre-build the\n * entrypoint and use `addPublicAssets` instead to copy pre-bundled assets into\n * the output directory. This will speed up project builds since it just has to\n * copy some files instead of bundling them.\n *\n * To extract entrypoint options from a JS/TS file, use\n * `wxt.builder.importEntrypoint` (see example).\n *\n * @example\n *   export default defineWxtModule(async (wxt, options) => {\n *     const entrypointPath = '/path/to/my-entrypoint.ts';\n *     addEntrypoint(wxt, {\n *       type: 'content-script',\n *       name: 'some-name',\n *       inputPath: entrypointPath,\n *       outputDir: wxt.config.outDir,\n *       options: await wxt.builder.importEntrypoint(entrypointPath),\n *     });\n *   });\n *\n * @param wxt The wxt instance provided by the module's setup function.\n * @param entrypoint The entrypoint to be bundled along with the extension.\n */\nexport function addEntrypoint(wxt: Wxt, entrypoint: Entrypoint): void {\n  wxt.hooks.hook('entrypoints:resolved', (_, entrypoints) => {\n    entrypoints.push(entrypoint);\n  });\n}\n\n/**\n * Copy files inside a directory (as if it were the public directory) into the\n * extension's output directory. The directory itself is not copied, just the\n * files inside it. If a filename matches an existing one, it is ignored.\n *\n * @example\n *   export default defineWxtModule((wxt, options) => {\n *     addPublicAssets(wxt, './dist/prebundled');\n *   });\n *\n * @param wxt The wxt instance provided by the module's setup function.\n * @param dir The directory to copy.\n */\nexport function addPublicAssets(wxt: Wxt, dir: string): void {\n  wxt.hooks.hook('build:publicAssets', async (wxt, files) => {\n    const moreFiles = await glob('**/*', {\n      cwd: dir,\n      expandDirectories: false,\n    });\n    if (moreFiles.length === 0) {\n      wxt.logger.warn('No files to copy in', dir);\n      return;\n    }\n    moreFiles.forEach((file) => {\n      files.unshift({ absoluteSrc: resolve(dir, file), relativeDest: file });\n    });\n  });\n}\n\n/**\n * Merge additional vite config for one or more entrypoint \"groups\" that make up\n * individual builds. Config in the project's `wxt.config.ts` file takes\n * precedence over any config added by this function.\n *\n * @example\n *   export default defineWxtModule((wxt, options) => {\n *   addViteConfig(wxt, () => ({\n *   build: {\n *   sourceMaps: true,\n *   },\n *   });\n *   });\n *\n * @param wxt The wxt instance provided by the module's setup function.\n * @param viteConfig A function that returns the vite config the module is\n *   adding. Same format as `vite` in `wxt.config.ts`.\n */\nexport function addViteConfig(\n  wxt: Wxt,\n  viteConfig: (env: vite.ConfigEnv) => vite.UserConfig | undefined,\n): void {\n  wxt.hooks.hook('config:resolved', (wxt) => {\n    const userVite = wxt.config.vite;\n    wxt.config.vite = async (env) => {\n      const [vite, fromUser] = await Promise.all([\n        import('vite'),\n        userVite(env),\n      ]);\n      const fromModule = viteConfig(env) ?? {};\n      return vite.mergeConfig(fromModule, fromUser);\n    };\n  });\n}\n\n/**\n * Add a runtime plugin to the project. In each entrypoint, before executing the\n * `main` function, plugins are executed.\n *\n * @example\n *   export default defineWxtModule((wxt) => {\n *     addWxtPlugin(wxt, 'wxt-module-analytics/client-plugin');\n *   });\n *\n * @param wxt The wxt instance provided by the module's setup function.\n * @param plugin An import from an NPM module, or an absolute file path to the\n *   file to load at runtime.\n */\nexport function addWxtPlugin(wxt: Wxt, plugin: string): void {\n  wxt.hooks.hook('config:resolved', (wxt) => {\n    wxt.config.plugins.push(plugin);\n  });\n}\n\n/**\n * Add an Unimport preset\n * ([built-in](https://github.com/unjs/unimport?tab=readme-ov-file#built-in-presets),\n * [custom](https://github.com/unjs/unimport?tab=readme-ov-file#custom-presets),\n * or\n * [auto-scanned](https://github.com/unjs/unimport?tab=readme-ov-file#exports-auto-scan)),\n * to the project's list of auto-imported utilities.\n *\n * Some things to note:\n *\n * - This function will only de-duplicate built-in preset names. It will not stop\n *   you adding duplicate custom or auto-scanned presets.\n * - If the project has disabled imports, this function has no effect.\n *\n * @example\n *   export default defineWxtModule((wxt) => {\n *   // Built-in preset:\n *   addImportPreset(wxt, \"vue\");\n *   // Custom preset:\n *   addImportPreset(wxt, {\n *   from: \"vue\",\n *   imports: [\"ref\", \"reactive\", ...],\n *   });\n *   // Auto-scanned preset:\n *   addImportPreset(wxt, { package: \"vue\" });\n *   });\n *\n * @param wxt The wxt instance provided by the module's setup function.\n * @param preset The preset to add to the project.\n */\nexport function addImportPreset(\n  wxt: Wxt,\n  preset: UnimportOptions['presets'][0],\n): void {\n  wxt.hooks.hook('config:resolved', (wxt) => {\n    wxt.config.imports.presets ??= [];\n    // De-duplicate built-in named presets\n    if (wxt.config.imports.presets.includes(preset)) return;\n\n    wxt.config.imports.presets.push(preset);\n  });\n}\n\n/**\n * Adds an import alias to the project's TSConfig paths and bundler. Path can be\n * absolute or relative to the project's root directory.\n *\n * Usually, this is used to provide access to some code generated by your\n * module. In the example below, a `i18n` plugin generates a variable that it\n * wants to provide access to, so it creates the file and adds an import alias\n * to it.\n *\n * @example\n *   import path from 'node:path';\n *\n *   export default defineWxtModule((wxt) => {\n *     const i18nPath = path.resolve(wxt.config.wxtDir, 'i18n.ts');\n *\n *     // Generate the file\n *     wxt.hooks.hook('prepare:types', (_, entries) => {\n *       entries.push({\n *         path: i18nPath,\n *         text: `export const i18n = ...`,\n *       });\n *     });\n *\n *     // Add alias\n *     addAlias(wxt, '#i18n', i18nPath);\n *   });\n */\nexport function addAlias(wxt: Wxt, alias: string, path: string) {\n  wxt.hooks.hook('config:resolved', (wxt) => {\n    const target = resolve(wxt.config.root, path);\n    if (wxt.config.alias[alias] != null && wxt.config.alias[alias] !== target) {\n      wxt.logger.warn(\n        `Skipped adding alias (${alias} => ${target}) because an alias with the same name already exists: ${alias} => ${wxt.config.alias[alias]}`,\n      );\n      return;\n    }\n    wxt.config.alias[alias] = target;\n  });\n}\n"
  },
  {
    "path": "packages/wxt/src/testing/fake-browser.ts",
    "content": "/**\n * The fake browser is automatically used as a mock for the `wxt/browser` import\n * when using `wxt/testing/vitest-plugin` with Vitest. It is also setup to reset\n * all state before each test.\n *\n * This module is just a re-export of\n * [@webext-core/fake-browser](https://webext-core.aklinker1.io/fake-browser/triggering-events).\n *\n * @module wxt/testing/fake-browser\n */\n\nexport { fakeBrowser, type FakeBrowser } from '@webext-core/fake-browser';\n"
  },
  {
    "path": "packages/wxt/src/testing/index.ts",
    "content": "/**\n * Utilities for unit testing WXT extensions.\n *\n * @module wxt/testing\n * @deprecated Use `wxt/testing/*` instead to prevent issues with JSDOM or\n *   HappyDOM environments. Will be removed in the next major version of WXT.\n */\nexport * from './fake-browser';\nexport * from './wxt-vitest-plugin';\n"
  },
  {
    "path": "packages/wxt/src/testing/wxt-vitest-plugin.ts",
    "content": "/**\n * Contains a Vitest plugin that configures your test environment to work with\n * WXT projects.\n *\n * @module wxt/testing/vitest\n */\n\nimport type * as vite from 'vite';\nimport {\n  download,\n  tsconfigPaths,\n  globals,\n  extensionApiMock,\n  resolveAppConfig,\n} from '../core/builders/vite/plugins';\nimport { InlineConfig } from '../types';\nimport UnimportPlugin from 'unimport/unplugin';\nimport { registerWxt, wxt } from '../core/wxt';\n\n/**\n * Vite plugin that configures Vitest with everything required to test a WXT\n * extension, based on the `<root>/wxt.config.ts`\n *\n * ```ts\n * // vitest.config.ts\n * import { defineConfig } from 'vitest/config';\n * import { WxtVitest } from 'wxt/testing/vitest-plugin';\n *\n * export default defineConfig({\n *   plugins: [WxtVitest()],\n * });\n * ```\n *\n * @param inlineConfig Customize WXT's config for testing. Any config specified\n *   here overrides the config from your `wxt.config.ts` file.\n */\nexport async function WxtVitest(\n  inlineConfig?: InlineConfig,\n): Promise<vite.PluginOption[]> {\n  await registerWxt('serve', inlineConfig ?? {});\n\n  const plugins: vite.PluginOption[] = [\n    globals(wxt.config),\n    download(wxt.config),\n    tsconfigPaths(wxt.config),\n    resolveAppConfig(wxt.config),\n    extensionApiMock(wxt.config),\n  ];\n  plugins.push(UnimportPlugin.vite(wxt.config.imports));\n\n  return plugins;\n}\n"
  },
  {
    "path": "packages/wxt/src/types.ts",
    "content": "import type * as vite from 'vite';\nimport { UnimportOptions, Import } from 'unimport';\nimport { LogLevel } from 'consola';\nimport type { ContentScriptContext } from './utils/content-script-context';\nimport type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualizer';\nimport { ResolvedConfig as C12ResolvedConfig } from 'c12';\nimport { Hookable, NestedHooks } from 'hookable';\nimport type * as Nypm from 'nypm';\nimport { ManifestContentScript } from './core/utils/types';\nimport type { Browser } from '@wxt-dev/browser';\n\nexport interface InlineConfig {\n  /**\n   * Your project's root directory containing the `package.json` used to fill\n   * out the `manifest.json`.\n   *\n   * @default process.cwd()\n   */\n  root?: string;\n  /**\n   * Directory containing all source code. Set to `\"src\"` to move all source\n   * code to a `src/` directory.\n   *\n   * After changing, remember to move the `public/` and `entrypoints/`\n   * directories into the new source dir.\n   *\n   * @default config.root\n   */\n  srcDir?: string;\n  /**\n   * Directory containing files that will be copied to the output directory\n   * as-is.\n   *\n   * @default '${config.root}/public'\n   */\n  publicDir?: string;\n  /** @default '${config.srcDir}/entrypoints' */\n  entrypointsDir?: string;\n  /** @default '${config.root}/modules' */\n  modulesDir?: string;\n  /**\n   * A list of entrypoint names (`\"popup\"`, `\"options\"`, etc.) to build. Will\n   * speed up the build if your extension has lots of entrypoints, and you don't\n   * need to build all of them to develop a feature. If specified, this\n   * completely overrides the `include`/`exclude` option provided\n   * per-entrypoint.\n   */\n  filterEntrypoints?: string[];\n  /**\n   * Output directory that stored build folders and ZIPs.\n   *\n   * @default '.output'\n   */\n  outDir?: string;\n  /**\n   * Template string for customizing the output directory structure. Available\n   * variables:\n   *\n   * - <span v-pre>`{{browser}}`</span>: The target browser (e.g., 'chrome',\n   *   'firefox')\n   * - <span v-pre>`{{manifestVersion}}`</span>: The manifest version (e.g., 2 or\n   *   3)\n   * - <span v-pre>`{{mode}}`</span>: The build mode (e.g., 'development',\n   *   'production')\n   * - <span v-pre>`{{modeSuffix}}`</span>: A suffix based on the mode ('-dev' for\n   *   development, '' for production)\n   * - <span v-pre>`{{command}}`</span>: The WXT command being run (e.g., 'build',\n   *   'serve')\n   *\n   * @example\n   *   {{browser}} -mv{{manifestVersion}}\n   *\n   * @default <span v-pre>`\"{{browser}}-mv{{manifestVersion}}{{modeSuffix}}\"`</span>\n   */\n  outDirTemplate?: string;\n  /**\n   * > Only available when using the JS API. Not available in `wxt.config.ts`\n   * > files\n   *\n   * Path to `wxt.config.ts` file or `false` to disable config file discovery.\n   *\n   * @default 'wxt.config.ts'\n   */\n  configFile?: string | false;\n  /**\n   * Set to `true` to show debug logs. Overridden by the command line `--debug`\n   * option.\n   *\n   * @default false\n   */\n  debug?: boolean;\n  /**\n   * Explicitly set a mode to run in. This will override the default mode for\n   * each command, and can be overridden by the command line `--mode` option.\n   */\n  mode?: string;\n  /**\n   * Customize auto-import options. Set to `false` to disable auto-imports.\n   *\n   * For example, to add a directory to auto-import from, you can use:\n   *\n   * ```ts\n   * export default defineConfig({\n   *   imports: {\n   *     dirs: ['some-directory'],\n   *   },\n   * });\n   * ```\n   */\n  imports?: WxtUnimportOptions | false;\n  /**\n   * Explicitly set a browser to build for. This will override the default\n   * browser for each command, and can be overridden by the command line\n   * `--browser` option.\n   *\n   * @default\n   * \"chrome\"\n   */\n  browser?: TargetBrowser;\n  /**\n   * Target browsers to support. When set, `import.meta.env.BROWSER` will be\n   * narrowed to a string literal type containing only the specified browser\n   * names.\n   *\n   * @default [ ]\n   */\n  targetBrowsers?: TargetBrowser[];\n  /**\n   * Explicitly set a manifest version to target. This will override the default\n   * manifest version for each command, and can be overridden by the command\n   * line `--mv2` or `--mv3` option.\n   */\n  manifestVersion?: TargetManifestVersion;\n  /**\n   * Override the logger used.\n   *\n   * @default\n   * consola\n   */\n  logger?: Logger;\n  /**\n   * Customize the `manifest.json` output. Can be an object, promise, or\n   * function that returns an object or promise.\n   */\n  manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;\n  /**\n   * Configure browser startup. Options set here can be overridden in a\n   * `web-ext.config.ts` file.\n   */\n  webExt?: WebExtConfig;\n  /** @deprecated Use `webExt` instead. Same option, just renamed. */\n  runner?: WebExtConfig;\n  zip?: {\n    /**\n     * Configure the filename output when zipping files.\n     *\n     * Available template variables:\n     *\n     * - <span v-pre>`{{name}}`</span> - The project's name converted to\n     *   kebab-case\n     * - <span v-pre>`{{version}}`</span> - The version_name or version from the\n     *   manifest\n     * - <span v-pre>`{{packageVersion}}`</span> - The version from the\n     *   package.json\n     * - <span v-pre>`{{browser}}`</span> - The target browser from the\n     *   `--browser` CLI flag\n     * - <span v-pre>`{{mode}}`</span> - The current mode\n     * - <span v-pre>`{{manifestVersion}}`</span> - Either \"2\" or \"3\"\n     *\n     * @default '{{name}}-{{version}}-{{browser}}.zip'\n     */\n    artifactTemplate?: string;\n    /**\n     * When zipping the extension, also zip sources.\n     *\n     * - `undefined`: zip sources if the target browser is \"firefox\" or \"opera\"\n     * - `true`: always zip sources\n     * - `false`: never zip sources\n     *\n     * @default undefined\n     */\n    zipSources?: boolean;\n    /**\n     * Configure the filename output when zipping files.\n     *\n     * Available template variables:\n     *\n     * - <span v-pre>`{{name}}`</span> - The project's name converted to\n     *   kebab-case\n     * - <span v-pre>`{{version}}`</span> - The version_name or version from the\n     *   manifest\n     * - <span v-pre>`{{packageVersion}}`</span> - The version from the\n     *   package.json\n     * - <span v-pre>`{{browser}}`</span> - The target browser from the\n     *   `--browser` CLI flag\n     * - <span v-pre>`{{mode}}`</span> - The current mode\n     * - <span v-pre>`{{manifestVersion}}`</span> - Either \"2\" or \"3\"\n     *\n     * @default '{{name}}-{{version}}-sources.zip'\n     */\n    sourcesTemplate?: string;\n    /**\n     * Override the artifactTemplate's `{name}` template variable. Defaults to\n     * the `package.json`'s name, or if that doesn't exist, the current working\n     * directories name.\n     */\n    name?: string;\n    /**\n     * Root directory to ZIP when generating the sources ZIP.\n     *\n     * @default config.root\n     */\n    sourcesRoot?: string;\n    /**\n     * [Picomatch](https://www.npmjs.com/package/picomatch) patterns of files to\n     * include when creating a ZIP of all your source code for Firefox. Patterns\n     * are relative to your `config.zip.sourcesRoot`.\n     *\n     * This setting overrides `excludeSources`. So if a file matches both lists,\n     * it is included in the ZIP.\n     *\n     * @example\n     *   [\n     *     'coverage', // Include the coverage directory in the `sourcesRoot`\n     *   ];\n     */\n    includeSources?: string[];\n    /**\n     * [Picomatch](https://www.npmjs.com/package/picomatch) patterns of files to\n     * exclude when creating a ZIP of all your source code for Firefox. Patterns\n     * are relative to your `config.zip.sourcesRoot`.\n     *\n     * Hidden files, node_modules, and tests are ignored by default.\n     *\n     * @example\n     *   [\n     *     'coverage', // Ignore the coverage directory in the `sourcesRoot`\n     *   ];\n     */\n    excludeSources?: string[];\n    /**\n     * [Picomatch](https://www.npmjs.com/package/picomatch) patterns of files to\n     * exclude when zipping the extension.\n     *\n     * @example\n     *   [\n     *     '**\\/*.map', // Exclude all sourcemaps\n     *   ];\n     */\n    exclude?: string[];\n    /**\n     * The Firefox review process requires the extension be buildable from\n     * source to make reviewing easier. This field allows you to use private\n     * packages without exposing your auth tokens.\n     *\n     * Just list the name of all the packages you want to download and include\n     * in the sources zip. Usually, these will be private packages behind auth\n     * tokens, but they don't have to be.\n     *\n     * All packages listed here will be downloaded to in `.wxt/local_modules/`\n     * and an `overrides` or `resolutions` field (depending on your package\n     * manager) will be added to the `package.json`, pointing to the downloaded\n     * packages.\n     *\n     * > _**DO NOT include versions or version filters.**_ Just the package name.\n     * > If multiple versions of a package are present in the project, all\n     * > versions will be downloaded and referenced in the package.json\n     * > correctly.\n     *\n     * @example\n     *   // Correct:\n     *   ['@scope/package-name', 'package-name'][\n     *     // Incorrect, don't include versions!!!\n     *     ('@scope/package-name@1.1.3', 'package-name@^2')\n     *   ];\n     *\n     * @default [ ]\n     */\n    downloadPackages?: string[];\n    /**\n     * Compression level to use when zipping files.\n     *\n     * Levels: 0 (no compression) to 9 (maximum compression).\n     *\n     * @default 9\n     */\n    compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;\n  };\n  analysis?: {\n    /**\n     * Explicitly include bundle analysis when running `wxt build`. This can be\n     * overridden by the command line `--analyze` option.\n     *\n     * @default false\n     */\n    enabled?: boolean;\n    /**\n     * Set to true to automatically open the `stats.html` file when the build is\n     * finished. When building in CI, the browser will never open.\n     *\n     * @default false\n     */\n    open?: boolean;\n    /**\n     * When running `wxt build --analyze` or setting `analysis.enabled` to true,\n     * customize how the bundle will be visualized. See\n     * [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer#how-to-use-generated-files)\n     * for more details.\n     *\n     * @default 'treemap'\n     */\n    template?: PluginVisualizerOptions['template'];\n    /**\n     * Name of the output HTML file. Relative to the project's root directory.\n     *\n     * Changing the filename of the outputFile also effects the names of the\n     * artifacts generated when setting `keepArtifacts` to true:\n     *\n     * - \"stats.html\" => \"stats-*.json\"\n     * - \"stats/bundle.html\" => \"bundle-*.json\"\n     * - \".analysis/index.html\" => \"index-*.json\"\n     *\n     * @default 'stats.html'\n     */\n    outputFile?: string;\n    /**\n     * By default, the `stats-*.json` artifacts generated during bundle analysis\n     * are deleted. Set to `true` to keep them.\n     *\n     * One stats file is output per build step.\n     *\n     * @default false\n     */\n    keepArtifacts?: boolean;\n  };\n  /**\n   * Add additional paths to the `.wxt/tsconfig.json`. Use this instead of\n   * overwriting the `paths` in the root `tsconfig.json` if you want to add new\n   * paths.\n   *\n   * The key is the import alias and the value is either a relative path to the\n   * root directory or an absolute path.\n   *\n   * @example\n   *   { \"testing\": \"src/utils/testing.ts\" }\n   */\n  alias?: Record<string, string>;\n  /** Experimental settings - use with caution. */\n  experimental?: {};\n  /** Config effecting dev mode only. */\n  dev?: {\n    server?: {\n      /**\n       * Host to bind the dev server to.\n       *\n       * @default 'localhost'\n       */\n      host?: string;\n      /**\n       * Port to run the dev server on. Defaults to the first open port from\n       * 3000 to 3010.\n       */\n      port?: number;\n      /**\n       * Origin to use to connect from the extension ui runtime to the dev\n       * server.\n       *\n       * @default 'http://localhost:3000'\n       */\n      origin?: string;\n      /**\n       * Hostname to run the dev server on.\n       *\n       * @deprecated Use `host` to specify the interface to bind to, or use\n       *   `origin` to specify the dev server hostname.\n       */\n      hostname?: string;\n    };\n    /**\n     * Controls whether a custom keyboard shortcut command, `Alt+R`, is added\n     * during dev mode to quickly reload the extension.\n     *\n     * If false, the shortcut is not added during development.\n     *\n     * If set to a custom string, you can override the key combo used. See\n     * [Chrome's command\n     * docs](https://developer.chrome.com/docs/extensions/reference/api/commands)\n     * for available options.\n     *\n     * @default 'Alt+R'\n     */\n    reloadCommand?: string | false;\n  };\n  /** Project hooks for running logic during the build process. */\n  hooks?: NestedHooks<WxtHooks>;\n  /**\n   * List of WXT module names to include. Can be the full package name\n   * (\"wxt-module-analytics\"), or just the suffix (\"analytics\" would resolve to\n   * \"wxt-module-analytics\").\n   */\n  modules?: string[];\n}\n\n// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field\nexport interface InlineConfig {\n  /**\n   * Return custom Vite options from a function. See\n   * [https://vitejs.dev/config/shared-options.html](https://vitejs.dev/config/shared-options.html).\n   *\n   * [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be\n   * set in WXT's config instead of Vite's.\n   *\n   * This is a function because any vite plugins added need to be recreated for\n   * each individual build step, incase they have internal state causing them to\n   * fail when reused.\n   */\n  vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;\n}\n\n// TODO: Move into @wxt/vite-builder\nexport interface ResolvedConfig {\n  vite: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;\n}\n\n// TODO: Move into @wxt/vite-builder\nexport type WxtViteConfig = Omit<\n  vite.UserConfig,\n  'root' | 'configFile' | 'mode'\n>;\n\n// TODO: Move into @wxt/vite-builder\nexport interface WxtHooks {\n  /**\n   * Called when WXT has created Vite's config for a build step. Useful if you\n   * want to add plugins or update the vite config per entrypoint group.\n   *\n   * @param entrypoints The list of entrypoints being built with the provided\n   *   config.\n   * @param viteConfig The config that will be used for the dev server.\n   */\n  'vite:build:extendConfig': (\n    entrypoints: readonly Entrypoint[],\n    viteConfig: vite.InlineConfig,\n  ) => HookResult;\n  /**\n   * Called when WXT has created Vite's config for the dev server. Useful if you\n   * want to add plugins or update the vite config per entrypoint group.\n   *\n   * @param viteConfig The config that will be used to build the entrypoints.\n   *   Can be updated by reference.\n   */\n  'vite:devServer:extendConfig': (config: vite.InlineConfig) => HookResult;\n}\n\nexport interface BuildOutput {\n  manifest: Browser.runtime.Manifest;\n  publicAssets: OutputAsset[];\n  steps: BuildStepOutput[];\n}\n\nexport type OutputFile = OutputChunk | OutputAsset;\n\nexport interface OutputChunk {\n  type: 'chunk';\n  /**\n   * Relative, normalized path relative to the output directory.\n   *\n   * Ex: \"content-scripts/overlay.js\"\n   */\n  fileName: string;\n  /** Absolute, normalized paths to all dependencies this chunk relies on. */\n  moduleIds: string[];\n}\n\nexport interface OutputAsset {\n  type: 'asset';\n  /**\n   * Relative, normalized path relative to the output directory.\n   *\n   * Ex: \"icons/16.png\"\n   */\n  fileName: string;\n}\n\nexport interface BuildStepOutput {\n  entrypoints: EntrypointGroup;\n  chunks: OutputFile[];\n}\n\nexport interface WxtDevServer\n  extends Omit<WxtBuilderServer, 'listen' | 'close'>, ServerInfo {\n  /** Stores the current build output of the server. */\n  currentOutput: BuildOutput | undefined;\n  /** Start the server. */\n  start(): Promise<void>;\n  /** Stop the server. */\n  stop(): Promise<void>;\n  /**\n   * Close the browser, stop the server, rebuild the entire extension, and start\n   * the server again.\n   */\n  restart(): Promise<void>;\n  /** Transform the HTML for dev mode. */\n  transformHtml(\n    url: string,\n    html: string,\n    originalUrl?: string | undefined,\n  ): Promise<string>;\n  /** Tell the extension to reload by running `browser.runtime.reload`. */\n  reloadExtension: () => void;\n  /**\n   * Tell an extension page to reload.\n   *\n   * The path is the bundle path, not the input paths, so if the input paths is\n   * \"src/options/index.html\", you would pass \"options.html\" because that's\n   * where it is written to in the dist directory, and where it's available at\n   * in the actual extension.\n   *\n   * @example\n   *   server.reloadPage('popup.html');\n   *   server.reloadPage('sandbox.html');\n   */\n  reloadPage: (path: string) => void;\n  /**\n   * Tell the extension to restart a content script.\n   *\n   * @param payload Information about the content script to reload.\n   */\n  reloadContentScript: (payload: ReloadContentScriptPayload) => void;\n  /** Grab the latest runner config and restart the browser. */\n  restartBrowser: () => void;\n}\n\nexport interface ReloadContentScriptPayload {\n  registration?: BaseContentScriptEntrypointOptions['registration'];\n  contentScript: Omit<Browser.scripting.RegisteredContentScript, 'id'>;\n}\n\nexport type TargetBrowser = string;\nexport type TargetManifestVersion = 2 | 3;\n\nexport type UserConfig = Omit<InlineConfig, 'configFile'>;\n\nexport interface Logger {\n  debug(...args: any[]): void;\n  log(...args: any[]): void;\n  info(...args: any[]): void;\n  warn(...args: any[]): void;\n  error(...args: any[]): void;\n  fatal(...args: any[]): void;\n  success(...args: any[]): void;\n  level: LogLevel;\n}\n\nexport interface BaseEntrypointOptions {\n  /**\n   * List of target browsers to include this entrypoint in. Defaults to being\n   * included in all builds. Cannot be used with `exclude`. You must choose one\n   * of the two options.\n   *\n   * @default undefined\n   */\n  include?: TargetBrowser[];\n  /**\n   * List of target browsers to exclude this entrypoint from. Cannot be used\n   * with `include`. You must choose one of the two options.\n   *\n   * @default undefined\n   */\n  exclude?: TargetBrowser[];\n}\n\nexport interface BackgroundEntrypointOptions extends BaseEntrypointOptions {\n  persistent?: PerBrowserOption<boolean>;\n  /**\n   * Set to `\"module\"` to output the background entrypoint as ESM. ESM outputs\n   * can share chunks and reduce the overall size of the bundled extension.\n   *\n   * When `undefined`, the background is bundled individually into an IIFE\n   * format.\n   *\n   * @default undefined\n   */\n  type?: PerBrowserOption<'module'>;\n}\n\nexport interface BaseScriptEntrypointOptions extends BaseEntrypointOptions {\n  /**\n   * The variable name for the IIFE in the output bundle.\n   *\n   * This option is relevant for scripts inserted into the page context where\n   * the default IIFE variable name may conflict with an existing variable on\n   * the target page. This applies to content scripts with world=MAIN, and\n   * others, such as unlisted scripts, that could be dynamically injected into\n   * the page with a <script> tag.\n   *\n   * Available options:\n   *\n   * - `true`: automatically generate a name for the IIFE based on the entrypoint\n   *   name\n   * - `false`: Output the IIFE without a variable name, making it anonymous. This\n   *   is the safest option to avoid conflicts with existing variables on the\n   *   page. This will become the default in a future version of WXT.\n   * - `string`: Use the provided string as the global variable name.\n   * - `function`: A function that receives the entrypoint and returns a string to\n   *   use as the variable name.\n   *\n   * @default true\n   */\n  globalName?: string | boolean | ((entrypoint: Entrypoint) => string);\n}\n\nexport interface BaseContentScriptEntrypointOptions extends BaseScriptEntrypointOptions {\n  matches?: PerBrowserOption<NonNullable<ManifestContentScript['matches']>>;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default 'documentIdle'\n   */\n  runAt?: PerBrowserOption<Browser.scripting.RegisteredContentScript['runAt']>;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default false\n   */\n  matchAboutBlank?: PerBrowserOption<\n    ManifestContentScript['match_about_blank']\n  >;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default [ ]\n   */\n  excludeMatches?: PerBrowserOption<ManifestContentScript['exclude_matches']>;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default [ ]\n   */\n  includeGlobs?: PerBrowserOption<ManifestContentScript['include_globs']>;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default [ ]\n   */\n  excludeGlobs?: PerBrowserOption<ManifestContentScript['exclude_globs']>;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default false\n   */\n  allFrames?: PerBrowserOption<ManifestContentScript['all_frames']>;\n  /**\n   * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/\n   *\n   * @default false\n   */\n  matchOriginAsFallback?: PerBrowserOption<boolean>;\n  /**\n   * Customize how imported/generated styles are injected with the content\n   * script. Regardless of the mode selected, CSS will always be built and\n   * included in the output directory.\n   *\n   * - `\"manifest\"` - Include the CSS in the manifest, under the content script's\n   *   `css` array.\n   * - `\"manual\"` - Exclude the CSS from the manifest. You are responsible for\n   *   manually loading it onto the page. Use\n   *   `browser.runtime.getURL(\"content-scripts/<name>.css\")` to get the file's\n   *   URL\n   * - `\"ui\"` - Exclude the CSS from the manifest. CSS will be automatically added\n   *   to your UI when calling `createShadowRootUi`\n   *\n   * @default 'manifest'\n   */\n  cssInjectionMode?: PerBrowserOption<'manifest' | 'manual' | 'ui'>;\n  /**\n   * Specify how the content script is registered.\n   *\n   * - `\"manifest\"`: The content script will be added to the `content_scripts`\n   *   entry in the manifest. This is the normal and most well known way of\n   *   registering a content script.\n   * - `\"runtime\"`: The content script's `matches` is added to `host_permissions`\n   *   and you are responsible for using the scripting API to register/execute\n   *   the content script dynamically at runtime.\n   *\n   * @default 'manifest'\n   */\n  registration?: PerBrowserOption<'manifest' | 'runtime'>;\n}\n\nexport interface MainWorldContentScriptEntrypointOptions extends BaseContentScriptEntrypointOptions {\n  /**\n   * See\n   * https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world\n   */\n  world: 'MAIN';\n}\n\nexport interface IsolatedWorldContentScriptEntrypointOptions extends BaseContentScriptEntrypointOptions {\n  /**\n   * See\n   * https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world\n   *\n   * @default 'ISOLATED'\n   */\n  world?: 'ISOLATED';\n}\n\n/**\n * Firefox theme icon definition for light/dark mode support.\n *\n * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action#theme_icons\n */\nexport interface ThemeIcon {\n  /** Path to the icon shown when the browser uses a light theme. */\n  light: string;\n  /** Path to the icon shown when the browser uses a dark theme. */\n  dark: string;\n  /** Icon size in pixels. */\n  size: number;\n}\n\nexport interface PopupEntrypointOptions extends BaseEntrypointOptions {\n  /** Defaults to \"browser_action\" to be equivalent to MV3's \"action\" key */\n  mv2Key?: PerBrowserOption<'browser_action' | 'page_action'>;\n  defaultIcon?: Record<string, string>;\n  defaultTitle?: PerBrowserOption<string>;\n  browserStyle?: PerBrowserOption<boolean>;\n  /**\n   * Firefox only. Defines the part of the browser in which the button is\n   * initially placed.\n   *\n   * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#default_area\n   */\n  defaultArea?: PerBrowserOption<\n    'navbar' | 'menupanel' | 'tabstrip' | 'personaltoolbar'\n  >;\n  /**\n   * Firefox only. Icons for light and dark themes.\n   *\n   * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#theme_icons\n   */\n  themeIcons?: ThemeIcon[];\n}\n\nexport interface OptionsEntrypointOptions extends BaseEntrypointOptions {\n  title?: string;\n  openInTab?: PerBrowserOption<boolean>;\n  browserStyle?: PerBrowserOption<boolean>;\n  chromeStyle?: PerBrowserOption<boolean>;\n}\n\nexport interface SidepanelEntrypointOptions extends BaseEntrypointOptions {\n  /**\n   * Firefox only. See\n   * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/sidebar_action#syntax\n   *\n   * @default false\n   */\n  openAtInstall?: PerBrowserOption<boolean>;\n  /**\n   * @deprecated See\n   *   https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/sidebar_action#syntax\n   */\n  browserStyle?: PerBrowserOption<boolean>;\n  defaultIcon?: string | Record<string, string>;\n  defaultTitle?: PerBrowserOption<string>;\n}\n\nexport interface BaseEntrypoint {\n  /**\n   * The entrypoint's name. This is the filename or dirname without the type\n   * suffix.\n   *\n   * Examples:\n   *\n   * - `popup.html` → `popup`\n   * - `options/index.html` → `options`\n   * - `named.sandbox.html` → `named`\n   * - `named.sandbox/index.html` → `named`\n   * - `sandbox.html` → `sandbox`\n   * - `sandbox/index.html` → `sandbox`\n   * - `overlay.content.ts` → `overlay`\n   * - `overlay.content/index.ts` → `overlay`\n   *\n   * The name is used when generating an output file:\n   * `<entrypoint.outputDir>/<entrypoint.name>.<ext>`\n   */\n  name: string;\n  /** Absolute path to the entrypoint's input file. */\n  inputPath: string;\n  /**\n   * Absolute path to the entrypoint's output directory. Can be\n   * `wxt.config.outDir` or a subdirectory of it.\n   */\n  outputDir: string;\n  /**\n   * When true, the entrypoint will not be built by WXT. Normally this is set\n   * based on the `filterEntrypoints` config or the entrypoint's\n   * `include`/`exclude` options defined inside the file.\n   *\n   * See\n   * https://wxt.dev/guide/essentials/target-different-browsers.html#filtering-entrypoints\n   */\n  skipped?: boolean;\n}\n\nexport interface GenericEntrypoint extends BaseEntrypoint {\n  type:\n    | 'sandbox'\n    | 'bookmarks'\n    | 'history'\n    | 'newtab'\n    | 'devtools'\n    | 'unlisted-page'\n    | 'unlisted-style'\n    | 'content-script-style';\n  options: ResolvedPerBrowserOptions<BaseEntrypointOptions>;\n}\n\nexport interface UnlistedScriptEntrypoint extends BaseEntrypoint {\n  type: 'unlisted-script';\n  options: ResolvedPerBrowserOptions<BaseScriptEntrypointOptions>;\n}\n\nexport interface BackgroundEntrypoint extends BaseEntrypoint {\n  type: 'background';\n  options: ResolvedPerBrowserOptions<BackgroundEntrypointOptions>;\n}\n\nexport interface ContentScriptEntrypoint extends BaseEntrypoint {\n  type: 'content-script';\n  options: ResolvedPerBrowserOptions<\n    | MainWorldContentScriptEntrypointOptions\n    | IsolatedWorldContentScriptEntrypointOptions\n  >;\n}\n\nexport interface PopupEntrypoint extends BaseEntrypoint {\n  type: 'popup';\n  options: ResolvedPerBrowserOptions<PopupEntrypointOptions, 'defaultIcon'>;\n}\n\nexport interface OptionsEntrypoint extends BaseEntrypoint {\n  type: 'options';\n  options: ResolvedPerBrowserOptions<OptionsEntrypointOptions>;\n}\n\nexport interface SidepanelEntrypoint extends BaseEntrypoint {\n  type: 'sidepanel';\n  options: ResolvedPerBrowserOptions<SidepanelEntrypointOptions, 'defaultIcon'>;\n}\n\nexport type Entrypoint =\n  | GenericEntrypoint\n  | BackgroundEntrypoint\n  | UnlistedScriptEntrypoint\n  | ContentScriptEntrypoint\n  | PopupEntrypoint\n  | OptionsEntrypoint\n  | SidepanelEntrypoint;\n\nexport interface EntrypointInfo {\n  name: string;\n  /** Absolute path to the entrypoint file. */\n  inputPath: string;\n  type: Entrypoint['type'];\n}\n\nexport type EntrypointGroup = Entrypoint | Entrypoint[];\n\nexport type OnContentScriptStopped = (cb: () => void) => void;\n\nexport interface IsolatedWorldContentScriptDefinition extends IsolatedWorldContentScriptEntrypointOptions {\n  /**\n   * Main function executed when the content script is loaded.\n   *\n   * When running a content script with `browser.scripting.executeScript`,\n   * values returned from this function will be returned in the `executeScript`\n   * result as well. Otherwise returning a value does nothing.\n   */\n  main(ctx: ContentScriptContext): any | Promise<any>;\n}\n\nexport interface MainWorldContentScriptDefinition extends MainWorldContentScriptEntrypointOptions {\n  /**\n   * Main function executed when the content script is loaded.\n   *\n   * When running a content script with `browser.scripting.executeScript`,\n   * values returned from this function will be returned in the `executeScript`\n   * result as well. Otherwise returning a value does nothing.\n   */\n  main(): any | Promise<any>;\n}\n\nexport type ContentScriptDefinition =\n  | IsolatedWorldContentScriptDefinition\n  | MainWorldContentScriptDefinition;\n\nexport interface BackgroundDefinition extends BackgroundEntrypointOptions {\n  /**\n   * Main function executed when the background script is started. Cannot be\n   * async.\n   */\n  main(): void;\n}\n\nexport interface UnlistedScriptDefinition extends BaseScriptEntrypointOptions {\n  /**\n   * Main function executed when the unlisted script is ran.\n   *\n   * When running a content script with `browser.scripting.executeScript`,\n   * values returned from this function will be returned in the `executeScript`\n   * result as well. Otherwise returning a value does nothing.\n   */\n  main(): any | Promise<any>;\n}\n\n/**\n * Either a single value or a map of different browsers to the value for that\n * browser.\n */\nexport type PerBrowserOption<T> = T | PerBrowserMap<T>;\nexport type PerBrowserMap<T> = { [browser: TargetBrowser]: T };\n\n/**\n * Convert `{ key: PerBrowserOption<T> }` to just `{ key: T }`, stripping away\n * the `PerBrowserOption` type for all fields inside the object.\n *\n * A optional second list of keys can be passed if a field isn't compatible with\n * `PerBrowserOption`, like `defaultIcon`.\n */\nexport type ResolvedPerBrowserOptions<T, TOmitted extends keyof T = never> = {\n  [key in keyof Omit<T, TOmitted>]: T[key] extends PerBrowserOption<infer U>\n    ? U\n    : T[key];\n} & { [key in TOmitted]: T[key] };\n\n/**\n * Manifest customization available in the `wxt.config.ts` file. You cannot\n * configure entrypoints here, they are configured inline.\n */\nexport type UserManifest = {\n  [key in keyof Browser.runtime.ManifestV3 as key extends\n    | 'action'\n    | 'background'\n    | 'chrome_url_overrides'\n    | 'devtools_page'\n    | 'manifest_version'\n    | 'options_page'\n    | 'options_ui'\n    | 'permissions'\n    | 'sandbox'\n    | 'web_accessible_resources'\n    ? never\n    : key]?: Browser.runtime.ManifestV3[key];\n} & {\n  // Add any Browser-specific or MV2 properties that WXT supports here\n  action?: Browser.runtime.ManifestV3['action'] & {\n    browser_style?: boolean;\n  };\n  browser_action?: Browser.runtime.ManifestV2['browser_action'] & {\n    browser_style?: boolean;\n  };\n  page_action?: Browser.runtime.ManifestV2['page_action'] & {\n    browser_style?: boolean;\n  };\n  browser_specific_settings?: {\n    gecko?: {\n      id?: string;\n      strict_min_version?: string;\n      strict_max_version?: string;\n      update_url?: string;\n    };\n    gecko_android?: {\n      strict_min_version?: string;\n      strict_max_version?: string;\n    };\n    safari?: {\n      strict_min_version?: string;\n      strict_max_version?: string;\n    };\n  };\n  permissions?: (\n    | Browser.runtime.ManifestPermissions\n    | (string & Record<never, never>)\n  )[];\n  web_accessible_resources?:\n    | string[]\n    | Browser.runtime.ManifestV3['web_accessible_resources'];\n};\n\nexport type UserManifestFn = (\n  env: ConfigEnv,\n) => UserManifest | Promise<UserManifest>;\n\nexport interface ConfigEnv {\n  /**\n   * The build mode passed into the CLI. By default, `wxt` uses `\"development\"`\n   * and `wxt build|zip` uses `\"production\"`.\n   */\n  mode: string;\n  /**\n   * The command used to run WXT. `\"serve\"` during development and `\"build\"` for\n   * any other command.\n   */\n  command: WxtCommand;\n  /**\n   * Browser passed in from the CLI via the `-b` or `--browser` flag. Defaults\n   * to `\"chrome\"` when not passed.\n   */\n  browser: TargetBrowser;\n  /**\n   * Manifest version passed in from the CLI via the `--mv2` or `--mv3` flags.\n   * When not passed, it depends on the target browser. See [the\n   * guide](https://wxt.dev/guide/key-concepts/multiple-browsers.html#target-manifest-version)\n   * for more details.\n   */\n  manifestVersion: 2 | 3;\n}\n\nexport type WxtCommand = 'build' | 'serve';\n\n/** @deprecated Use `WebExtConfig` instead. */\nexport type ExtensionRunnerConfig = WebExtConfig;\n\n/**\n * Options for how [`web-ext`](https://github.com/mozilla/web-ext) starts the\n * browser.\n */\nexport interface WebExtConfig {\n  /**\n   * Whether or not to open the browser with the extension installed in dev\n   * mode.\n   *\n   * @default false\n   */\n  disabled?: boolean;\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#browser-console */\n  openConsole?: boolean;\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#devtools */\n  openDevtools?: boolean;\n  /**\n   * List of browser names and the binary that should be used to open the\n   * browser.\n   *\n   * @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary\n   * @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox\n   */\n  binaries?: Record<string, string>;\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-profile */\n  firefoxProfile?: string;\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-profile */\n  chromiumProfile?: string;\n  /**\n   * An map of chrome preferences from\n   * https://chromium.googlesource.com/chromium/src/+/main/chrome/common/pref_names.h\n   *\n   * @example\n   *   // change your downloads directory\n   *   {\n   *   download: {\n   *   default_directory: \"/my/custom/dir\",\n   *   },\n   *   }\n   *\n   * @default\n   * // Enable dev mode and allow content script sourcemaps\n   * {\n   *   devtools: {\n   *     synced_preferences_sync_disabled: {\n   *       skipContentScripts: false,\n   *     },\n   *   }\n   *   extensions: {\n   *     ui: {\n   *       developer_mode: true,\n   *     },\n   *   }\n   * }\n   */\n  chromiumPref?: Record<string, any>;\n  /**\n   * By default, chrome opens a random port for debugging. Set this value to use\n   * a specific port.\n   */\n  chromiumPort?: number;\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#pref */\n  firefoxPref?: Record<string, boolean | number | string>;\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#args */\n  firefoxArgs?: string[];\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#args */\n  chromiumArgs?: string[];\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#start-url */\n  startUrls?: string[];\n  /** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#keep-profile-changes */\n  keepProfileChanges?: boolean;\n}\n\nexport interface WxtBuilder {\n  /** Name of tool used to build. Ex: \"Vite\" or \"Webpack\". */\n  name: string;\n  /** Version of tool used to build. Ex: \"5.0.2\" */\n  version: string;\n  /**\n   * Import a JS entrypoint file, returning the default export containing the\n   * options.\n   */\n  importEntrypoint<T>(path: string): Promise<T>;\n  /** Import a list of JS entrypoint files, returning their options. */\n  importEntrypoints(paths: string[]): Promise<Record<string, unknown>[]>;\n  /**\n   * Build a single entrypoint group. This is effectively one of the multiple\n   * \"steps\" during the build process.\n   */\n  build(group: EntrypointGroup): Promise<BuildStepOutput>;\n  /** Start a dev server at the provided port. */\n  createServer(info: ServerInfo): Promise<WxtBuilderServer>;\n}\n\nexport interface WxtBuilderServer {\n  /** Start the server. */\n  listen(): Promise<void>;\n  /** Stop the server. */\n  close(): Promise<void>;\n  /** Transform the HTML for dev mode. */\n  transformHtml(\n    url: string,\n    html: string,\n    originalUrl?: string | undefined,\n  ): Promise<string>;\n  /** The web socket server used to communicate with the extension. */\n  ws: {\n    /**\n     * Send a message via the server's websocket, with an optional payload.\n     *\n     * @example\n     *   ws.send(\"wxt:reload-extension\");\n     *   ws.send(\"wxt:reload-content-script\", { ... });\n     */\n    send(message: string, payload?: any): void;\n    /** Listen for messages over the server's websocket. */\n    on(message: string, cb: (payload: any) => void): void;\n  };\n  /** Chokidar file watcher instance. */\n  watcher: vite.ViteDevServer['watcher'];\n  on?(event: string, callback: () => void): void;\n}\n\nexport interface ServerInfo {\n  /** Ex: `\"localhost\"` */\n  host: string;\n  /** Ex: `3000` */\n  port: number;\n  /** Ex: `\"http://localhost:3000\"` */\n  origin: string;\n}\n\nexport type HookResult = Promise<void> | void;\n\nexport interface WxtHooks {\n  /**\n   * Called after WXT modules are initialized, when the WXT instance is ready to\n   * be used. `wxt.server` isn't available yet, use `server:created` to get it.\n   *\n   * @param wxt The configured WXT object\n   */\n  ready: (wxt: Wxt) => HookResult;\n  /**\n   * Called whenever config is loaded or reloaded. Use this hook to modify\n   * config by modifying `wxt.config`.\n   *\n   * @param wxt The configured WXT object\n   */\n  'config:resolved': (wxt: Wxt) => HookResult;\n  /**\n   * Called before WXT writes .wxt/tsconfig.json and .wxt/wxt.d.ts, allowing\n   * addition of custom references and declarations in wxt.d.ts, or directly\n   * modifying the options in `tsconfig.json`.\n   *\n   * @example\n   *   wxt.hooks.hook('prepare:types', (wxt, entries) => {\n   *     // Add a file, \".wxt/types/example.d.ts\", that defines a global\n   *     // variable called \"example\" in the TS project.\n   *     entries.push({\n   *       path: 'types/example.d.ts',\n   *       text: 'declare const a: string;',\n   *       tsReference: true,\n   *     });\n   *     // use module to add Triple-Slash Directive in .wxt/wxt.d.ts\n   *     // eg: /// <reference types=\"@types/example\" />\n   *     entries.push({\n   *       module: '@types/example',\n   *     });\n   *   });\n   */\n  'prepare:types': (wxt: Wxt, entries: WxtDirEntry[]) => HookResult;\n  /**\n   * Called before generating the list of public paths inside\n   * `.wxt/types/paths.d.ts`. Use this hook to add additional paths (relative to\n   * output directory) WXT doesn't add automatically.\n   *\n   * @example\n   *   wxt.hooks.hook('prepare:publicPaths', (wxt, paths) => {\n   *     paths.push('/icons/128.png');\n   *   });\n   *\n   * @param wxt The configured WXT object\n   * @param paths This list of paths TypeScript allows `browser.runtime.getURL`\n   *   to be called with.\n   */\n  'prepare:publicPaths': (wxt: Wxt, paths: string[]) => HookResult;\n  /**\n   * Called before the build is started in both dev mode and build mode.\n   *\n   * @param wxt The configured WXT object\n   */\n  'build:before': (wxt: Wxt) => HookResult;\n  /**\n   * Called once the build process has finished. You can add files to the build\n   * summary here by pushing to `output.publicAssets`.\n   *\n   * @param wxt The configured WXT object\n   * @param output The results of the build\n   */\n  'build:done': (wxt: Wxt, output: Readonly<BuildOutput>) => HookResult;\n  /**\n   * Called once the manifest has been generated. Used to transform the manifest\n   * by reference before it is written to the output directory.\n   *\n   * @param wxt The configured WXT object\n   * @param manifest The manifest that was generated\n   */\n  'build:manifestGenerated': (\n    wxt: Wxt,\n    manifest: Browser.runtime.Manifest,\n  ) => HookResult;\n  /**\n   * Called once the names and paths of all entrypoints have been resolved.\n   *\n   * @param wxt The configured WXT object\n   * @param infos List of entrypoints found in the project's `entrypoints`\n   *   directory\n   */\n  'entrypoints:found': (wxt: Wxt, infos: EntrypointInfo[]) => HookResult;\n  /**\n   * Called once all entrypoints have been loaded from the `entrypointsDir`. Use\n   * `wxt.builder.importEntrypoint` to load entrypoint options from the file, or\n   * manually define them.\n   *\n   * @param wxt The configured WXT object\n   * @param entrypoints The list of entrypoints to be built\n   */\n  'entrypoints:resolved': (wxt: Wxt, entrypoints: Entrypoint[]) => HookResult;\n  /**\n   * Called once all entrypoints have been grouped into their build groups.\n   *\n   * @param wxt The configured WXT object\n   * @param entrypoints The list of groups to build in each build step\n   */\n  'entrypoints:grouped': (wxt: Wxt, groups: EntrypointGroup[]) => HookResult;\n  /**\n   * Called when public assets are found. You can modify the `files` list by\n   * reference to add or remove public files.\n   *\n   * @param wxt The configured WXT object\n   * @param entrypoints The list of files that will be copied into the output\n   *   directory\n   */\n  'build:publicAssets': (wxt: Wxt, files: ResolvedPublicFile[]) => HookResult;\n  /**\n   * Called before the zip process starts.\n   *\n   * @param wxt The configured WXT object\n   */\n  'zip:start': (wxt: Wxt) => HookResult;\n  /**\n   * Called before zipping the extension files.\n   *\n   * @param wxt The configured WXT object\n   */\n  'zip:extension:start': (wxt: Wxt) => HookResult;\n  /**\n   * Called after zipping the extension files.\n   *\n   * @param wxt The configured WXT object\n   * @param zipPath The path to the created extension zip file\n   */\n  'zip:extension:done': (wxt: Wxt, zipPath: string) => HookResult;\n  /**\n   * Called before zipping the source files (for Firefox).\n   *\n   * @param wxt The configured WXT object\n   */\n  'zip:sources:start': (wxt: Wxt) => HookResult;\n  /**\n   * Called after zipping the source files (for Firefox).\n   *\n   * @param wxt The configured WXT object\n   * @param zipPath The path to the created sources zip file\n   */\n  'zip:sources:done': (wxt: Wxt, zipPath: string) => HookResult;\n  /**\n   * Called after the entire zip process is complete.\n   *\n   * @param wxt The configured WXT object\n   * @param zipFiles An array of paths to all created zip files\n   */\n  'zip:done': (wxt: Wxt, zipFiles: string[]) => HookResult;\n  /**\n   * Called when the dev server is created (and `wxt.server` is assigned).\n   * Server has not been started yet.\n   *\n   * @param wxt The configured WXT object\n   * @param server Same as `wxt.server`, the object WXT uses to control the dev\n   *   server.\n   */\n  'server:created': (wxt: Wxt, server: WxtDevServer) => HookResult;\n  /**\n   * Called when the dev server is started.\n   *\n   * @param wxt The configured WXT object\n   * @param server Same as `wxt.server`, the object WXT uses to control the dev\n   *   server.\n   */\n  'server:started': (wxt: Wxt, server: WxtDevServer) => HookResult;\n  /**\n   * Called when the dev server is stopped.\n   *\n   * @param wxt The configured WXT object\n   * @param server Same as `wxt.server`, the object WXT uses to control the dev\n   *   server.\n   */\n  'server:closed': (wxt: Wxt, server: WxtDevServer) => HookResult;\n}\n\nexport interface Wxt {\n  config: ResolvedConfig;\n  hooks: Hookable<WxtHooks>;\n  /** Alias for `wxt.hooks.hook(...)`. */\n  hook: Hookable<WxtHooks>['hook'];\n  /** Alias for config.logger */\n  logger: Logger;\n  /** Reload config file and update `wxt.config` with the result. */\n  reloadConfig: () => Promise<void>;\n  /** Package manager utilities. */\n  pm: WxtPackageManager;\n  /** If the dev server was started, it will be available. */\n  server?: WxtDevServer;\n  /** The module in charge of executing all the build steps. */\n  builder: WxtBuilder;\n}\n\nexport interface ResolvedConfig {\n  root: string;\n  srcDir: string;\n  publicDir: string;\n  /**\n   * Absolute path pointing to `.wxt` directory in project root.\n   *\n   * @example\n   *   '/path/to/project/.wxt';\n   */\n  wxtDir: string;\n  typesDir: string;\n  entrypointsDir: string;\n  modulesDir: string;\n  filterEntrypoints?: Set<string>;\n  /**\n   * Absolute path to the `.output` directory\n   *\n   * @example\n   *   '/path/to/project/.output';\n   */\n  outBaseDir: string;\n  /**\n   * Absolute path to the target output directory.\n   *\n   * @example\n   *   '/path/to/project/.output/chrome-mv3';\n   */\n  outDir: string;\n  debug: boolean;\n  /**\n   * Absolute path pointing to the `node_modules/wxt` directory, wherever WXT is\n   * installed.\n   */\n  wxtModuleDir: string;\n  mode: string;\n  command: WxtCommand;\n  browser: TargetBrowser;\n  targetBrowsers: TargetBrowser[];\n  manifestVersion: TargetManifestVersion;\n  env: ConfigEnv;\n  logger: Logger;\n  imports: WxtResolvedUnimportOptions;\n  manifest: UserManifest;\n  fsCache: FsCache;\n  runnerConfig: C12ResolvedConfig<WebExtConfig>;\n  zip: {\n    name?: string;\n    artifactTemplate: string;\n    sourcesTemplate: string;\n    includeSources: string[];\n    excludeSources: string[];\n    sourcesRoot: string;\n    downloadedPackagesDir: string;\n    downloadPackages: string[];\n    compressionLevel: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;\n    exclude: string[];\n    /** If true, when zipping the extension, also zip the sources. */\n    zipSources: boolean;\n  };\n  analysis: {\n    enabled: boolean;\n    open: boolean;\n    template: NonNullable<PluginVisualizerOptions['template']>;\n    /** Absolute file path to the `stats.html` file */\n    outputFile: string;\n    /** The directory where the final `stats.html` file is located */\n    outputDir: string;\n    /** Name of the `stats.html` file, minus \".html\" */\n    outputName: string;\n    keepArtifacts: boolean;\n  };\n  userConfigMetadata: Omit<C12ResolvedConfig<UserConfig>, 'config'>;\n  /** Import aliases to absolute paths. */\n  alias: Record<string, string>;\n  experimental: {};\n  dev: {\n    /** Only defined during dev command */\n    server?: {\n      host: string;\n      port: number;\n      origin: string;\n      /**\n       * The milliseconds to debounce when a file is saved before reloading. The\n       * only way to set this option is to set the `WXT_WATCH_DEBOUNCE`\n       * environment variable, either globally (like in `.bashrc` file) or\n       * per-project (in `.env` file).\n       *\n       * For example:\n       *\n       *     # ~/.zshrc\n       *     export WXT_WATCH_DEBOUNCE=1000\n       *\n       * Or\n       *\n       *     # .env\n       *     WXT_WATCH_DEBOUNCE=1000\n       *\n       * @default 800\n       */\n      watchDebounce: number;\n    };\n    reloadCommand: string | false;\n  };\n  hooks: NestedHooks<WxtHooks>;\n  builtinModules: WxtModule<any>[];\n  userModules: WxtModuleWithMetadata<any>[];\n  /**\n   * An array of string to import plugins from. These paths should be resolvable\n   * by vite, and they should `export default defineWxtPlugin(...)`.\n   *\n   * @example\n   *   ['@wxt-dev/module-vue/plugin', 'wxt-module-google-analytics/plugin'];\n   */\n  plugins: string[];\n}\n\nexport interface FsCache {\n  set(key: string, value: string): Promise<void>;\n  get(key: string): Promise<string | undefined>;\n}\n\nexport interface ExtensionRunner {\n  openBrowser(): Promise<void>;\n\n  closeBrowser?(): Promise<void>;\n\n  /** Whether or not this runner actually opens the browser. */\n  canOpen?(): boolean;\n}\n\nexport type EslintGlobalsPropValue =\n  | boolean\n  | 'readonly'\n  | 'readable'\n  | 'writable'\n  | 'writeable';\n\nexport interface Eslintrc {\n  /**\n   * When true, generates a file that can be used by ESLint to know which\n   * variables are valid globals.\n   *\n   * - `false`: Don't generate the file.\n   * - `'auto'`: Check if eslint is installed, and if it is, generate a compatible\n   *   config file.\n   * - `true`: Same as `8`.\n   * - `8`: Generate a config file compatible with ESLint 8.\n   * - `9`: Generate a config file compatible with ESLint 9.\n   *\n   * @default 'auto'\n   */\n  enabled?: false | true | 'auto' | 8 | 9;\n  /**\n   * File path to save the generated eslint config.\n   *\n   * Default depends on version of ESLint used:\n   *\n   * - 9 and above: './.wxt/eslint-auto-imports.mjs'\n   * - 8 and below: './.wxt/eslintrc-auto-import.json'\n   */\n  filePath?: string;\n  /** @default true */\n  globalsPropValue?: EslintGlobalsPropValue;\n}\n\nexport interface ResolvedEslintrc {\n  /** False if disabled, otherwise the major version of ESLint installed */\n  enabled: false | 8 | 9;\n  /** Absolute path */\n  filePath: string;\n  globalsPropValue: EslintGlobalsPropValue;\n}\n\nexport type WxtUnimportOptions = Partial<UnimportOptions> & {\n  /**\n   * When using ESLint, auto-imported variables are linted as \"undeclared\n   * globals\". This option lets you configure a base eslintrc that, when\n   * extended, fixes these lint errors.\n   *\n   * See\n   * [https://wxt.dev/guide/key-concepts/auto-imports.html#eslint](https://wxt.dev/guide/key-concepts/auto-imports.html#eslint)\n   */\n  eslintrc?: Eslintrc;\n};\n\nexport type WxtResolvedUnimportOptions = Partial<UnimportOptions> & {\n  /**\n   * Set to `true` when the user disabled auto-imports. We still use unimport\n   * for the #imports module, but other features should be disabled.\n   *\n   * You don't need to check this value before modifying the auto-import\n   * options. Even if `disabled` is `true`, there's no harm in adding imports to\n   * the config - they'll just be ignored.\n   */\n  disabled: boolean;\n  eslintrc: ResolvedEslintrc;\n};\n\n/**\n * Package management utils built on top of\n * [`nypm`](https://www.npmjs.com/package/nypm)\n */\nexport interface WxtPackageManager extends Nypm.PackageManager {\n  addDependency: typeof Nypm.addDependency;\n  addDevDependency: typeof Nypm.addDevDependency;\n  ensureDependencyInstalled: typeof Nypm.ensureDependencyInstalled;\n  installDependencies: typeof Nypm.installDependencies;\n  removeDependency: typeof Nypm.removeDependency;\n  /**\n   * Download a package's TGZ file and move it into the `downloadDir`. Use's\n   * `npm pack <name>`, so you must have setup authorization in `.npmrc` file,\n   * regardless of the package manager used.\n   *\n   * @param id Name of the package to download, can include a version (like\n   *   `wxt@0.17.1`)\n   * @param downloadDir Where to store the package.\n   * @returns Absolute path to downloaded file.\n   */\n  downloadDependency: (id: string, downloadDir: string) => Promise<string>;\n  /**\n   * Run `npm ls`, `pnpm ls`, or `bun pm ls`, or `yarn list` and return the\n   * results.\n   *\n   * WARNING: Yarn always returns all dependencies\n   */\n  listDependencies: (options?: {\n    cwd?: string;\n    all?: boolean;\n  }) => Promise<Dependency[]>;\n  /** Key used to override package versions. Sometimes called \"resolutions\". */\n  overridesKey: string;\n}\n\nexport interface Dependency {\n  name: string;\n  version: string;\n}\n\nexport type WxtModuleOptions = Record<string, any>;\n\nexport type WxtModuleSetup<TOptions extends WxtModuleOptions> = (\n  wxt: Wxt,\n  moduleOptions?: TOptions,\n) => void | Promise<void>;\n\nexport interface WxtModule<TOptions extends WxtModuleOptions> {\n  name?: string;\n  /**\n   * Key for users to pass options into your module from their `wxt.config.ts`\n   * file.\n   */\n  configKey?: string;\n  /** Provide a list of imports to add to auto-imports. */\n  imports?: Import[];\n  /**\n   * Alternative to adding hooks in setup function with `wxt.hooks`. Hooks are\n   * added before the `setup` function is called.\n   */\n  hooks?: NestedHooks<WxtHooks>;\n  /**\n   * A custom function that can be used to setup hooks and call module-specific\n   * APIs.\n   */\n  setup?: WxtModuleSetup<TOptions>;\n}\n\nexport interface WxtModuleWithMetadata<\n  TOptions extends WxtModuleOptions,\n> extends WxtModule<TOptions> {\n  type: 'local' | 'node_module';\n  id: string;\n}\n\nexport type ResolvedPublicFile = CopiedPublicFile | GeneratedPublicFile;\n\nexport interface ResolvedBasePublicFile {\n  /**\n   * The relative path in the output directory to copy the file to.\n   *\n   * @example\n   *   'content-scripts/base-styles.css';\n   */\n  relativeDest: string;\n}\n\nexport interface CopiedPublicFile extends ResolvedBasePublicFile {\n  /**\n   * The absolute path to the file that will be copied to the output directory.\n   *\n   * @example\n   *   '/path/to/any/file.css';\n   */\n  absoluteSrc: string;\n}\n\nexport interface GeneratedPublicFile extends ResolvedBasePublicFile {\n  /** Text to write to the file. */\n  contents: string;\n}\n\nexport type WxtPlugin = () => void;\n\nexport type WxtDirEntry = WxtDirTypeReferenceEntry | WxtDirFileEntry;\n\n/**\n * Represents type reference to a node module to be added to `.wxt/wxt.d.ts`\n * file\n */\nexport interface WxtDirTypeReferenceEntry {\n  /**\n   * Specifies the module name that will be used in the `/// <reference\n   * types=\"...\" />` directive. This value will be added to the `.wxt/wxt.d.ts`\n   * file to include type definitions from the specified module.\n   */\n  module: string;\n}\n\n/** Represents a file to be written to the project's `.wxt/` directory. */\nexport interface WxtDirFileEntry {\n  /**\n   * Path relative to the `.wxt/` directory. So \"tsconfig.json\" would resolve to\n   * \".wxt/tsconfig.json\".\n   */\n  path: string;\n  /** The text that will be written to the file. */\n  text: string;\n  /** Set to `true` to add a reference to this file in `.wxt/wxt.d.ts`. */\n  tsReference?: boolean;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/README.md",
    "content": "This folder is for public utils, not internal utils. Put generic helpers and other utils in the core/utils folder.\n"
  },
  {
    "path": "packages/wxt/src/utils/__tests__/__snapshots__/split-shadow-root-css.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Shadow Root Utils > splitShadowRootCss > should extract @property and @font-face declarations from minified tailwindcss v4 1`] = `\n{\n  \"documentCss\": \"@property --tw-rotate-x{syntax:\"*\";inherits:false;initial-value:rotateX(0)}@property --tw-rotate-y{syntax:\"*\";inherits:false;initial-value:rotateY(0)}@property --tw-rotate-z{syntax:\"*\";inherits:false;initial-value:rotateZ(0)}@property --tw-skew-x{syntax:\"*\";inherits:false;initial-value:skewX(0)}@property --tw-skew-y{syntax:\"*\";inherits:false;initial-value:skewY(0)}@property --tw-divide-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@font-face{font-family: \"custom-font\";font-display: swap;font-weight: 500;src: url(\"fonts/custom-font.otf\") format(\"opentype\");}\",\n  \"shadowCss\": \"/*! tailwindcss v4.0.13 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--spacing:.25rem;--container-3xl:48rem;--container-5xl:64rem;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-light:300;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--radius-xl:.75rem;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings);--color-primary:#ffc700;--color-primary-content:#000;--color-secondary:#c00;--color-secondary-content:#fff;--color-base:#000;--color-base-content:#fff;--color-neutral:#1c1c1c;--color-neutral-content:#fff;--color-white:#fff;--color-black:#000;--font-poppins:Poppins,sans-serif;--spacing-main-navigation:calc(20*var(--spacing))}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{min-width:0;min-height:0}body{background-color:var(--color-base);color:var(--color-base-content)}}@layer components{.btn{--btn-bg:var(--color-primary);--btn-text:var(--color-primary-content);background-color:var(--btn-bg);color:var(--btn-text);border-radius:calc(2*var(--spacing));font-weight:500;font-family:var(--font-poppins);padding:calc(3*var(--spacing))calc(6*var(--spacing));justify-content:center;align-items:center;gap:calc(3*var(--spacing));cursor:pointer;transition:transform .2s;display:flex}.btn:hover{transform:scale(1.05)}.btn:active{transform:scale(.97)}.btn-neutral{--btn-bg:var(--color-neutral);--btn-text:var(--color-neutral-content)}.btn-base{--btn-bg:var(--color-base);--btn-text:var(--color-base-content)}.btn-white{--btn-bg:var(--color-white);--btn-text:var(--color-black)}.btn-secondary{--btn-bg:var(--color-secondary);--btn-text:var(--color-secondary-content)}.btn-square{padding:calc(3*var(--spacing))}.nav-link{font-family:var(--font-poppins);font-size:var(--text-xl);transition:color .2s;position:relative}.nav-link:hover,.nav-link.active{color:var(--color-primary)}.nav-link.active:after{content:\"\";bottom:calc(-1*var(--spacing));background-color:var(--color-primary);width:100%;height:2px;position:absolute;left:0}.link{color:color-mix(in srgb,var(--color-secondary),white 30%);font-weight:500;text-decoration:underline}.link-white{color:var(--color-white)}}@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-0{z-index:0}.z-1{z-index:1}.z-10{z-index:10}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-main-navigation{margin-top:var(--spacing-main-navigation)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.i-heroicons-bars-3{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-calendar-days{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12zM12 15h.008v.008H12zm0 2.25h.008v.008H12zM9.75 15h.008v.008H9.75zm0 2.25h.008v.008H9.75zM7.5 15h.008v.008H7.5zm0 2.25h.008v.008H7.5zm6.75-4.5h.008v.008h-.008zm0 2.25h.008v.008h-.008zm0 2.25h.008v.008h-.008zm2.25-4.5h.008v.008H16.5zm0 2.25h.008v.008H16.5z'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-chevron-right{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m8.25 4.5l7.5 7.5l-7.5 7.5'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-x-mark{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 18L18 6M6 6l12 12'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.flex{display:flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.h-12{height:calc(var(--spacing)*12)}.h-20{height:calc(var(--spacing)*20)}.h-\\\\[70vh\\\\]{height:70vh}.h-\\\\[95vh\\\\]{height:95vh}.h-\\\\[100vh\\\\]{height:100vh}.h-full{height:100%}.h-main-navigation{height:var(--spacing-main-navigation)}.w-24{width:calc(var(--spacing)*24)}.w-40{width:calc(var(--spacing)*40)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x)var(--tw-rotate-y)var(--tw-rotate-z)var(--tw-skew-x)var(--tw-skew-y)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-items-center{justify-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-8{gap:calc(var(--spacing)*8)}.gap-16{gap:calc(var(--spacing)*16)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base>:not(:last-child)){border-color:var(--color-base)}.overflow-hidden{overflow:hidden}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-white\\\\/50{border-color:color-mix(in oklab,var(--color-white)50%,transparent)}.bg-base\\\\/0{background-color:color-mix(in oklab,var(--color-base)0%,transparent)}.bg-base\\\\/100{background-color:color-mix(in oklab,var(--color-base)100%,transparent)}.bg-neutral{background-color:var(--color-neutral)}.object-cover{object-fit:cover}.object-center{object-position:center}.p-8{padding:calc(var(--spacing)*8)}.p-16{padding:calc(var(--spacing)*16)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-main-navigation{padding-top:var(--spacing-main-navigation)}.text-center{text-align:center}.font-poppins{font-family:var(--font-poppins)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-\\\\[3\\\\]{--tw-leading:3;line-height:3}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-primary\\\\/50{--tw-shadow-color:color-mix(in oklab,var(--color-primary)50%,transparent)}.ring-primary\\\\/30{--tw-ring-color:color-mix(in oklab,var(--color-primary)30%,transparent)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\\\\:text-primary:hover{color:var(--color-primary)}}@media (width>=48rem){.md\\\\:mx-16{margin-inline:calc(var(--spacing)*16)}.md\\\\:block{display:block}.md\\\\:flex{display:flex}.md\\\\:hidden{display:none}.md\\\\:aspect-square{aspect-ratio:1}.md\\\\:h-56{height:calc(var(--spacing)*56)}.md\\\\:h-full{height:100%}.md\\\\:w-\\\\[unset\\\\]{width:unset}.md\\\\:flex-1{flex:1}.md\\\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\\\\:flex-row-reverse{flex-direction:row-reverse}.md\\\\:items-start{align-items:flex-start}.md\\\\:justify-start{justify-content:flex-start}.md\\\\:justify-items-start{justify-items:start}.md\\\\:gap-12{gap:calc(var(--spacing)*12)}.md\\\\:px-16{padding-inline:calc(var(--spacing)*16)}.md\\\\:pr-\\\\[30vw\\\\]{padding-right:30vw}.md\\\\:text-left{text-align:left}}@media (width>=64rem){.lg\\\\:aspect-\\\\[4\\\\/3\\\\]{aspect-ratio:4/3}.lg\\\\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}\",\n}\n`;\n"
  },
  {
    "path": "packages/wxt/src/utils/__tests__/content-script-context.test.ts",
    "content": "/** @vitest-environment happy-dom */\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { ContentScriptContext } from '../content-script-context';\nimport { fakeBrowser } from '@webext-core/fake-browser';\n\n/**\n * When dispatching events on document/window/etc, they are fired on the next\n * tick of the event loop. So waiting a timeout of 0 will ensure they've been\n * fired.\n */\nfunction waitForEventsToFire() {\n  return new Promise((res) => setTimeout(res));\n}\n\ndescribe('Content Script Context', () => {\n  beforeEach(() => {\n    vi.useRealTimers();\n    fakeBrowser.runtime.id = 'anything';\n  });\n\n  it(\"should recognize when the content script has lost it's connection to the extension API\", () => {\n    const ctx = new ContentScriptContext('test');\n    const onInvalidated = vi.fn();\n\n    ctx.onInvalidated(onInvalidated);\n    // @ts-expect-error Deleting `runtime.id` to simulate disconnection\n    delete fakeBrowser.runtime.id;\n    const isValid = ctx.isValid;\n\n    expect(onInvalidated).toBeCalled();\n    expect(isValid).toBe(false);\n  });\n\n  it('should not throw when browser.runtime is undefined (extension context fully invalidated)', () => {\n    const ctx = new ContentScriptContext('test');\n    const onInvalidated = vi.fn();\n\n    ctx.onInvalidated(onInvalidated);\n    // Simulate complete extension context invalidation where browser.runtime becomes undefined\n    const originalRuntime = fakeBrowser.runtime;\n    // @ts-ignore\n    fakeBrowser.runtime = undefined;\n\n    // Should not throw, and should mark as invalid\n    expect(() => ctx.isInvalid).not.toThrow();\n    expect(ctx.isInvalid).toBe(true);\n    expect(onInvalidated).toBeCalled();\n\n    // Restore for other tests\n    fakeBrowser.runtime = originalRuntime;\n  });\n\n  it('should invalidate the current content script when a new context is created', async () => {\n    const name = 'test';\n    const onInvalidated = vi.fn();\n    const ctx = new ContentScriptContext(name);\n    ctx.onInvalidated(onInvalidated);\n\n    // Wait for events to run before next tick next tick\n    await waitForEventsToFire();\n\n    // Create a new context after first is initialized, and wait for it to initialize\n    new ContentScriptContext(name);\n    await waitForEventsToFire();\n\n    expect(onInvalidated).toBeCalled();\n    expect(ctx.isValid).toBe(false);\n  });\n\n  it('should not invalidate the current content script when a new context is created with a different name', async () => {\n    const onInvalidated = vi.fn();\n    const ctx = new ContentScriptContext('test1');\n    ctx.onInvalidated(onInvalidated);\n\n    // Wait for events to run before next tick next tick\n    await waitForEventsToFire();\n\n    // Create a new context after first is initialized, and wait for it to initialize\n    new ContentScriptContext('test2');\n    await waitForEventsToFire();\n\n    expect(onInvalidated).not.toBeCalled();\n    expect(ctx.isValid).toBe(true);\n  });\n\n  describe('addEventListener', () => {\n    const context = new ContentScriptContext('test');\n    it('should infer types correctly for the window target', () => {\n      context.addEventListener(window, 'DOMContentLoaded', (_) => {});\n      context.addEventListener(window, 'orientationchange', (_) => {});\n      context.addEventListener(window, 'wxt:locationchange', (_) => {});\n      // @ts-expect-error\n      context.addEventListener(window, 'visibilitychange', (_) => {});\n    });\n\n    it('should infer types correctly for the document target', () => {\n      context.addEventListener(document, 'visibilitychange', (_) => {});\n      context.addEventListener(document, 'readystatechange', (_) => {});\n    });\n\n    it('should infer types correctly for HTML element targets', () => {\n      const button = document.createElement('button');\n      context.addEventListener(button, 'click', (_) => {});\n      context.addEventListener(button, 'mouseover', (_) => {});\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/utils/__tests__/define-background.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport { defineBackground } from '../define-background';\nimport { BackgroundDefinition } from '../../types';\n\ndescribe('defineBackground', () => {\n  it('should return the object definition when given an object', () => {\n    const definition: BackgroundDefinition = {\n      include: [''],\n      persistent: false,\n      main: vi.fn(),\n    };\n\n    const actual = defineBackground(definition);\n\n    expect(actual).toEqual(definition);\n  });\n\n  it('should return the object definition when given a main function', () => {\n    const main = vi.fn();\n\n    const actual = defineBackground(main);\n\n    expect(actual).toEqual({ main });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/utils/__tests__/define-content-script.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport { defineContentScript } from '../define-content-script';\nimport { ContentScriptDefinition } from '../../types';\n\ndescribe('defineContentScript', () => {\n  it('should return the object passed in', () => {\n    const definition: ContentScriptDefinition = {\n      matches: [],\n      include: [''],\n      main: vi.fn(),\n    };\n\n    const actual = defineContentScript(definition);\n\n    expect(actual).toEqual(definition);\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/utils/__tests__/define-unlisted-script.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport { defineUnlistedScript } from '../define-unlisted-script';\nimport { UnlistedScriptDefinition } from '../../types';\n\ndescribe('defineUnlistedScript', () => {\n  it('should return the object definition when given an object', () => {\n    const definition: UnlistedScriptDefinition = {\n      include: [''],\n      main: vi.fn(),\n    };\n\n    const actual = defineUnlistedScript(definition);\n\n    expect(actual).toEqual(definition);\n  });\n\n  it('should return the object definition when given a main function', () => {\n    const main = vi.fn();\n\n    const actual = defineUnlistedScript(main);\n\n    expect(actual).toEqual({ main });\n  });\n\n  it('should return the result without awaiting for synchronous main functions', () => {\n    const main = vi.fn(() => 'test');\n\n    const actual = defineUnlistedScript(main);\n\n    expect(actual).toEqual({ main });\n    expect(actual.main()).eq('test');\n  });\n\n  it('should return a promise of a result for async main functions', async () => {\n    const main = vi.fn(() => Promise.resolve('test'));\n\n    const actual = defineUnlistedScript(main);\n\n    expect(actual).toEqual({ main });\n    await expect(actual.main()).resolves.toEqual('test');\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/utils/__tests__/split-shadow-root-css.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { splitShadowRootCss } from '../split-shadow-root-css';\n\ndescribe('Shadow Root Utils', () => {\n  describe('splitShadowRootCss', () => {\n    it('should extract @property and @font-face declarations from minified tailwindcss v4', () => {\n      const css = `/*! tailwindcss v4.0.13 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;--spacing:.25rem;--container-3xl:48rem;--container-5xl:64rem;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-light:300;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--radius-xl:.75rem;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings);--color-primary:#ffc700;--color-primary-content:#000;--color-secondary:#c00;--color-secondary-content:#fff;--color-base:#000;--color-base-content:#fff;--color-neutral:#1c1c1c;--color-neutral-content:#fff;--color-white:#fff;--color-black:#000;--font-poppins:Poppins,sans-serif;--spacing-main-navigation:calc(20*var(--spacing))}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{min-width:0;min-height:0}body{background-color:var(--color-base);color:var(--color-base-content)}}@layer components{.btn{--btn-bg:var(--color-primary);--btn-text:var(--color-primary-content);background-color:var(--btn-bg);color:var(--btn-text);border-radius:calc(2*var(--spacing));font-weight:500;font-family:var(--font-poppins);padding:calc(3*var(--spacing))calc(6*var(--spacing));justify-content:center;align-items:center;gap:calc(3*var(--spacing));cursor:pointer;transition:transform .2s;display:flex}.btn:hover{transform:scale(1.05)}.btn:active{transform:scale(.97)}.btn-neutral{--btn-bg:var(--color-neutral);--btn-text:var(--color-neutral-content)}.btn-base{--btn-bg:var(--color-base);--btn-text:var(--color-base-content)}.btn-white{--btn-bg:var(--color-white);--btn-text:var(--color-black)}.btn-secondary{--btn-bg:var(--color-secondary);--btn-text:var(--color-secondary-content)}.btn-square{padding:calc(3*var(--spacing))}.nav-link{font-family:var(--font-poppins);font-size:var(--text-xl);transition:color .2s;position:relative}.nav-link:hover,.nav-link.active{color:var(--color-primary)}.nav-link.active:after{content:\"\";bottom:calc(-1*var(--spacing));background-color:var(--color-primary);width:100%;height:2px;position:absolute;left:0}.link{color:color-mix(in srgb,var(--color-secondary),white 30%);font-weight:500;text-decoration:underline}.link-white{color:var(--color-white)}}@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-0{z-index:0}.z-1{z-index:1}.z-10{z-index:10}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-main-navigation{margin-top:var(--spacing-main-navigation)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.i-heroicons-bars-3{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-calendar-days{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12zM12 15h.008v.008H12zm0 2.25h.008v.008H12zM9.75 15h.008v.008H9.75zm0 2.25h.008v.008H9.75zM7.5 15h.008v.008H7.5zm0 2.25h.008v.008H7.5zm6.75-4.5h.008v.008h-.008zm0 2.25h.008v.008h-.008zm0 2.25h.008v.008h-.008zm2.25-4.5h.008v.008H16.5zm0 2.25h.008v.008H16.5z'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-chevron-right{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m8.25 4.5l7.5 7.5l-7.5 7.5'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.i-heroicons-x-mark{width:1.5em;height:1.5em;-webkit-mask-image:var(--svg);mask-image:var(--svg);--svg:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 18L18 6M6 6l12 12'/%3E%3C/svg%3E\");background-color:currentColor;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.flex{display:flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.h-12{height:calc(var(--spacing)*12)}.h-20{height:calc(var(--spacing)*20)}.h-\\\\[70vh\\\\]{height:70vh}.h-\\\\[95vh\\\\]{height:95vh}.h-\\\\[100vh\\\\]{height:100vh}.h-full{height:100%}.h-main-navigation{height:var(--spacing-main-navigation)}.w-24{width:calc(var(--spacing)*24)}.w-40{width:calc(var(--spacing)*40)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x)var(--tw-rotate-y)var(--tw-rotate-z)var(--tw-skew-x)var(--tw-skew-y)}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-items-center{justify-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-8{gap:calc(var(--spacing)*8)}.gap-16{gap:calc(var(--spacing)*16)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base>:not(:last-child)){border-color:var(--color-base)}.overflow-hidden{overflow:hidden}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-white\\\\/50{border-color:color-mix(in oklab,var(--color-white)50%,transparent)}.bg-base\\\\/0{background-color:color-mix(in oklab,var(--color-base)0%,transparent)}.bg-base\\\\/100{background-color:color-mix(in oklab,var(--color-base)100%,transparent)}.bg-neutral{background-color:var(--color-neutral)}.object-cover{object-fit:cover}.object-center{object-position:center}.p-8{padding:calc(var(--spacing)*8)}.p-16{padding:calc(var(--spacing)*16)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-main-navigation{padding-top:var(--spacing-main-navigation)}.text-center{text-align:center}.font-poppins{font-family:var(--font-poppins)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-\\\\[3\\\\]{--tw-leading:3;line-height:3}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-primary\\\\/50{--tw-shadow-color:color-mix(in oklab,var(--color-primary)50%,transparent)}.ring-primary\\\\/30{--tw-ring-color:color-mix(in oklab,var(--color-primary)30%,transparent)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\\\\:text-primary:hover{color:var(--color-primary)}}@media (width>=48rem){.md\\\\:mx-16{margin-inline:calc(var(--spacing)*16)}.md\\\\:block{display:block}.md\\\\:flex{display:flex}.md\\\\:hidden{display:none}.md\\\\:aspect-square{aspect-ratio:1}.md\\\\:h-56{height:calc(var(--spacing)*56)}.md\\\\:h-full{height:100%}.md\\\\:w-\\\\[unset\\\\]{width:unset}.md\\\\:flex-1{flex:1}.md\\\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\\\\:flex-row-reverse{flex-direction:row-reverse}.md\\\\:items-start{align-items:flex-start}.md\\\\:justify-start{justify-content:flex-start}.md\\\\:justify-items-start{justify-items:start}.md\\\\:gap-12{gap:calc(var(--spacing)*12)}.md\\\\:px-16{padding-inline:calc(var(--spacing)*16)}.md\\\\:pr-\\\\[30vw\\\\]{padding-right:30vw}.md\\\\:text-left{text-align:left}}@media (width>=64rem){.lg\\\\:aspect-\\\\[4\\\\/3\\\\]{aspect-ratio:4/3}.lg\\\\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}@property --tw-rotate-x{syntax:\"*\";inherits:false;initial-value:rotateX(0)}@property --tw-rotate-y{syntax:\"*\";inherits:false;initial-value:rotateY(0)}@property --tw-rotate-z{syntax:\"*\";inherits:false;initial-value:rotateZ(0)}@property --tw-skew-x{syntax:\"*\";inherits:false;initial-value:skewX(0)}@property --tw-skew-y{syntax:\"*\";inherits:false;initial-value:skewY(0)}@property --tw-divide-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@font-face{font-family: \"custom-font\";font-display: swap;font-weight: 500;src: url(\"fonts/custom-font.otf\") format(\"opentype\");}`;\n\n      const actual = splitShadowRootCss(css);\n\n      expect(actual).toMatchSnapshot();\n    });\n\n    it('should maintain the same rule ordering and spacing as the original', () => {\n      const css = `\n.one {\n  color: blue;\n}\n\n@property --test-1 {\n  initial-value: 0;\n}\n\n.two {\n  color: red;\n}\n\n@property --test-2 {\n  initial-value: 0;\n}\n\n@font-face {\n  font-family: \"custom-font\";\n  font-display: swap;\n  font-weight: 500;\n  src: url(\"~~/assets/fonts/custom-font.otf\") format(\"opentype\");\n}\n      `.trim();\n      const expectedShadowCss = `\n.one {\n  color: blue;\n}\n\n.two {\n  color: red;\n}\n`.trim();\n      const expectedDocumentCss = `\n@property --test-1 {\n  initial-value: 0;\n}\n\n@property --test-2 {\n  initial-value: 0;\n}\n\n@font-face {\n  font-family: \"custom-font\";\n  font-display: swap;\n  font-weight: 500;\n  src: url(\"~~/assets/fonts/custom-font.otf\") format(\"opentype\");\n}\n`.trim();\n\n      const actual = splitShadowRootCss(css);\n\n      expect(actual).toEqual({\n        shadowCss: expectedShadowCss,\n        documentCss: expectedDocumentCss,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/utils/app-config.ts",
    "content": "/** @module wxt/utils/app-config */\n// @ts-expect-error: Untyped virtual module\nimport appConfig from 'virtual:app-config';\nimport type { WxtAppConfig } from './define-app-config';\n\n/**\n * Get runtime config defined in `<srcDir>/app.config.ts`\n *\n * @see https://wxt.dev/guide/essentials/config/runtime.html\n */\nexport function getAppConfig(): WxtAppConfig {\n  return appConfig;\n}\n\n/**\n * Alias for {@link getAppConfig}.\n *\n * @see https://wxt.dev/guide/essentials/config/runtime.html\n */\nexport function useAppConfig(): WxtAppConfig {\n  return getAppConfig();\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-context.ts",
    "content": "/** @module wxt/utils/content-script-context */\nimport { ContentScriptDefinition } from '../types';\nimport { browser } from 'wxt/browser';\nimport { logger } from './internal/logger';\nimport {\n  WxtLocationChangeEvent,\n  getUniqueEventName,\n} from './internal/custom-events';\nimport { createLocationWatcher } from './internal/location-watcher';\n\n/**\n * Implements\n * [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).\n * Used to detect and stop content script code when the script is invalidated.\n *\n * It also provides several utilities like `ctx.setTimeout` and\n * `ctx.setInterval` that should be used in content scripts instead of\n * `window.setTimeout` or `window.setInterval`.\n *\n * To create context for testing, you can use the class's constructor:\n *\n * ```ts\n * import { ContentScriptContext } from 'wxt/utils/content-scripts-context';\n *\n * test('storage listener should be removed when context is invalidated', () => {\n *   const ctx = new ContentScriptContext('test');\n *   const item = storage.defineItem('local:count', { defaultValue: 0 });\n *   const watcher = vi.fn();\n *\n *   const unwatch = item.watch(watcher);\n *   ctx.onInvalidated(unwatch); // Listen for invalidate here\n *\n *   await item.setValue(1);\n *   expect(watcher).toBeCalledTimes(1);\n *   expect(watcher).toBeCalledWith(1, 0);\n *\n *   ctx.notifyInvalidated(); // Use this function to invalidate the context\n *   await item.setValue(2);\n *   expect(watcher).toBeCalledTimes(1);\n * });\n * ```\n */\nexport class ContentScriptContext implements AbortController {\n  private static SCRIPT_STARTED_MESSAGE_TYPE = getUniqueEventName(\n    'wxt:content-script-started',\n  );\n\n  private id: string;\n  private abortController: AbortController;\n  private locationWatcher = createLocationWatcher(this);\n\n  constructor(\n    private readonly contentScriptName: string,\n    public readonly options?: Omit<ContentScriptDefinition, 'main'>,\n  ) {\n    this.id = Math.random().toString(36).slice(2);\n    this.abortController = new AbortController();\n\n    this.stopOldScripts();\n    this.listenForNewerScripts();\n  }\n\n  get signal() {\n    return this.abortController.signal;\n  }\n\n  abort(reason?: any): void {\n    return this.abortController.abort(reason);\n  }\n\n  get isInvalid(): boolean {\n    if (browser.runtime?.id == null) {\n      this.notifyInvalidated(); // Sets `signal.aborted` to true\n    }\n    return this.signal.aborted;\n  }\n\n  get isValid(): boolean {\n    return !this.isInvalid;\n  }\n\n  /**\n   * Add a listener that is called when the content script's context is\n   * invalidated.\n   *\n   * @example\n   *   browser.runtime.onMessage.addListener(cb);\n   *   const removeInvalidatedListener = ctx.onInvalidated(() => {\n   *     browser.runtime.onMessage.removeListener(cb);\n   *   });\n   *   // ...\n   *   removeInvalidatedListener();\n   *\n   * @returns A function to remove the listener.\n   */\n  onInvalidated(cb: () => void): () => void {\n    this.signal.addEventListener('abort', cb);\n    return () => this.signal.removeEventListener('abort', cb);\n  }\n\n  /**\n   * Return a promise that never resolves. Useful if you have an async function\n   * that shouldn't run after the context is expired.\n   *\n   * @example\n   *   const getValueFromStorage = async () => {\n   *     if (ctx.isInvalid) return ctx.block();\n   *\n   *     // ...\n   *   };\n   */\n  block<T>(): Promise<T> {\n    return new Promise(() => {\n      // noop\n    });\n  }\n\n  /**\n   * Wrapper around `window.setInterval` that automatically clears the interval\n   * when invalidated.\n   *\n   * Intervals can be cleared by calling the normal `clearInterval` function.\n   */\n  setInterval(handler: () => void, timeout?: number): number {\n    const id = setInterval(() => {\n      if (this.isValid) handler();\n    }, timeout) as unknown as number;\n    this.onInvalidated(() => clearInterval(id));\n    return id;\n  }\n\n  /**\n   * Wrapper around `window.setTimeout` that automatically clears the interval\n   * when invalidated.\n   *\n   * Timeouts can be cleared by calling the normal `setTimeout` function.\n   */\n  setTimeout(handler: () => void, timeout?: number): number {\n    const id = setTimeout(() => {\n      if (this.isValid) handler();\n    }, timeout) as unknown as number;\n    this.onInvalidated(() => clearTimeout(id));\n    return id;\n  }\n\n  /**\n   * Wrapper around `window.requestAnimationFrame` that automatically cancels\n   * the request when invalidated.\n   *\n   * Callbacks can be canceled by calling the normal `cancelAnimationFrame`\n   * function.\n   */\n  requestAnimationFrame(callback: FrameRequestCallback): number {\n    const id = requestAnimationFrame((...args) => {\n      if (this.isValid) callback(...args);\n    });\n\n    this.onInvalidated(() => cancelAnimationFrame(id));\n    return id;\n  }\n\n  /**\n   * Wrapper around `window.requestIdleCallback` that automatically cancels the\n   * request when invalidated.\n   *\n   * Callbacks can be canceled by calling the normal `cancelIdleCallback`\n   * function.\n   */\n  requestIdleCallback(\n    callback: IdleRequestCallback,\n    options?: IdleRequestOptions,\n  ): number {\n    const id = requestIdleCallback((...args) => {\n      if (!this.signal.aborted) callback(...args);\n    }, options);\n\n    this.onInvalidated(() => cancelIdleCallback(id));\n    return id;\n  }\n\n  /**\n   * Call `target.addEventListener` and remove the event listener when the\n   * context is invalidated.\n   *\n   * Listeners can be canceled by calling the normal `removeEventListener`\n   * function.\n   *\n   * Includes additional events useful for content scripts:\n   *\n   * - `\"wxt:locationchange\"` - Triggered when HTML5 history mode is used to\n   *   change URL. Content scripts are not reloaded when navigating this way, so\n   *   this can be used to reset the content script state on URL change, or run\n   *   custom code.\n   *\n   * @example\n   *   ctx.addEventListener(document, 'visibilitychange', () => {\n   *     // ...\n   *   });\n   *   ctx.addEventListener(window, 'wxt:locationchange', () => {\n   *     // ...\n   *   });\n   */\n  addEventListener<TType extends keyof WxtWindowEventMap>(\n    target: Window,\n    type: TType,\n    handler: (event: WxtWindowEventMap[TType]) => void,\n    options?: AddEventListenerOptions,\n  ): void;\n  addEventListener<TType extends keyof DocumentEventMap>(\n    target: Document,\n    type: TType,\n    handler: (event: DocumentEventMap[TType]) => void,\n    options?: AddEventListenerOptions,\n  ): void;\n  addEventListener<TTarget extends EventTarget>(\n    target: TTarget,\n    ...params: Parameters<TTarget['addEventListener']>\n  ): void;\n  addEventListener(\n    target: EventTarget,\n    type: string,\n    handler: (event: Event) => void,\n    options?: AddEventListenerOptions,\n  ): void {\n    if (type === 'wxt:locationchange') {\n      // Start the location watcher when adding the event for the first time\n      if (this.isValid) this.locationWatcher.run();\n    }\n\n    target.addEventListener?.(\n      type.startsWith('wxt:') ? getUniqueEventName(type) : type,\n      handler,\n      {\n        ...options,\n        signal: this.signal,\n      },\n    );\n  }\n\n  /**\n   * @internal\n   * Abort the abort controller and execute all `onInvalidated` listeners.\n   */\n  notifyInvalidated() {\n    this.abort('Content script context invalidated');\n    logger.debug(\n      `Content script \"${this.contentScriptName}\" context invalidated`,\n    );\n  }\n\n  stopOldScripts() {\n    document.dispatchEvent(\n      new CustomEvent(ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE, {\n        detail: {\n          contentScriptName: this.contentScriptName,\n          messageId: this.id,\n        },\n      }),\n    );\n\n    // Send message using `window.postMessage` for backwards compatibility to invalidate old versions before WXT changed to `document.dispatchEvent`\n    // TODO: Remove this once WXT version using `document.dispatchEvent` has been released for a while\n    window.postMessage(\n      {\n        type: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,\n        contentScriptName: this.contentScriptName,\n        messageId: this.id,\n      },\n      '*',\n    );\n  }\n\n  verifyScriptStartedEvent(event: CustomEvent) {\n    const isSameContentScript =\n      event.detail?.contentScriptName === this.contentScriptName;\n    // Handle case where website dispatches the event again for some reason\n    const isFromSelf = event.detail?.messageId === this.id;\n    return isSameContentScript && !isFromSelf;\n  }\n\n  listenForNewerScripts() {\n    const cb: EventListener = (event) => {\n      if (\n        !(event instanceof CustomEvent) ||\n        !this.verifyScriptStartedEvent(event)\n      ) {\n        return;\n      }\n      this.notifyInvalidated();\n    };\n\n    document.addEventListener(\n      ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,\n      cb,\n    );\n    this.onInvalidated(() =>\n      document.removeEventListener(\n        ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,\n        cb,\n      ),\n    );\n  }\n}\n\nexport interface WxtWindowEventMap extends WindowEventMap {\n  'wxt:locationchange': WxtLocationChangeEvent;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts",
    "content": "/** @vitest-environment happy-dom */\nimport { describe, it, beforeEach, vi, expect, afterEach } from 'vitest';\nimport { createIntegratedUi } from '../integrated';\nimport { createIframeUi } from '../iframe';\nimport { createShadowRootUi } from '../shadow-root';\nimport { ContentScriptContext } from '../../content-script-context';\nimport { ContentScriptUi } from '../types';\n\n/** Util for floating promise. */\nasync function runMicrotasks() {\n  return await new Promise((resolve) => setTimeout(resolve, 0));\n}\n\nfunction appendTestApp(container: HTMLElement) {\n  container.innerHTML = '<app>Hello world</app>';\n}\n\nfunction appendTestElement({\n  tagName = 'div',\n  id,\n}: {\n  tagName?: string;\n  id: string;\n}) {\n  const parent = document.querySelector('#parent');\n  if (!parent) {\n    throw Error(\n      'Parent element not found. Please check the testing environment DOM',\n    );\n  }\n  const element = document.createElement(tagName);\n  element.id = id;\n  parent.append(element);\n  return element;\n}\n\nconst fetch = vi.fn();\n\ndescribe('Content Script UIs', () => {\n  let ctx: ContentScriptContext;\n\n  beforeEach(() => {\n    document.body.innerHTML = `\n      <div id=\"parent\">\n        <p id=\"one\">one</p>\n        <p id=\"two\">two</p>\n        <p id=\"three\"></p>\n      </div>\n    `;\n    window.fetch = fetch;\n    fetch.mockResolvedValue({ text: () => Promise.resolve('') });\n    ctx = new ContentScriptContext('test');\n  });\n\n  describe('type', () => {\n    describe('integrated', () => {\n      it('should add a wrapper and custom UI to the page', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.body.children).toContain(ui.wrapper);\n        expect(document.querySelector('app')).not.toBeNull();\n      });\n\n      it('should allow customizing the wrapper tag', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          tag: 'pre',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.querySelector('pre')).toBe(ui.wrapper);\n        expect(document.querySelector('app')).not.toBeNull();\n      });\n    });\n\n    describe('iframe', () => {\n      it('should add a wrapper and iframe to the page', () => {\n        const ui = createIframeUi(ctx, {\n          page: '/page.html',\n          position: 'inline',\n        });\n        ui.mount();\n\n        expect(document.body.children).toContain(ui.wrapper);\n        expect(document.querySelector('iframe')).toBeDefined();\n      });\n    });\n\n    describe('shadow-root', () => {\n      it('should load a shadow root to the page', async () => {\n        const ui = await createShadowRootUi(ctx, {\n          position: 'inline',\n          name: 'test-component',\n          onMount(uiContainer) {\n            appendTestApp(uiContainer);\n          },\n        });\n        ui.mount();\n\n        expect(document.querySelector('test-component')).toBe(ui.shadowHost);\n        expect(ui.shadow.querySelector('app')).not.toBeNull();\n      });\n\n      it.each([\n        ['open', 'open'],\n        [undefined, 'open'],\n        ['closed', 'closed'],\n      ] as const)(\n        'should respect the shadow root mode (%s -> %s)',\n        async (input, expected) => {\n          const ui = await createShadowRootUi(ctx, {\n            position: 'inline',\n            name: 'test-component',\n            mode: input,\n            onMount: appendTestApp,\n          });\n\n          expect(ui.shadow.mode).toBe(expected);\n        },\n      );\n    });\n  });\n\n  describe('position', () => {\n    describe('inline', () => {\n      it('should wrap the UI in a simple div', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'inline',\n          page: '/page.html',\n        });\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div><iframe src=\"chrome-extension://test-extension-id/page.html\"></iframe></div>\"`,\n        );\n      });\n    });\n\n    describe('overlay', () => {\n      it('should wrap the UI in a positioned div when alignment=undefined', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'overlay',\n          page: '/page.html',\n        });\n        ui.mount();\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div style=\"overflow: visible; position: relative; width: 0px; height: 0px; display: block;\"><iframe src=\"chrome-extension://test-extension-id/page.html\" style=\"position: absolute; top: 0px; left: 0px;\"></iframe></div>\"`,\n        );\n      });\n\n      it('should wrap the UI in a positioned div when alignment=top-left', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'overlay',\n          page: '/page.html',\n          alignment: 'top-left',\n        });\n        ui.mount();\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div style=\"overflow: visible; position: relative; width: 0px; height: 0px; display: block;\"><iframe src=\"chrome-extension://test-extension-id/page.html\" style=\"position: absolute; top: 0px; left: 0px;\"></iframe></div>\"`,\n        );\n      });\n\n      it('should wrap the UI in a positioned div when alignment=top-right', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'overlay',\n          page: '/page.html',\n          alignment: 'top-right',\n        });\n        ui.mount();\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div style=\"overflow: visible; position: relative; width: 0px; height: 0px; display: block;\"><iframe src=\"chrome-extension://test-extension-id/page.html\" style=\"position: absolute; top: 0px; right: 0px;\"></iframe></div>\"`,\n        );\n      });\n\n      it('should wrap the UI in a positioned div when alignment=bottom-right', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'overlay',\n          page: '/page.html',\n          alignment: 'bottom-right',\n        });\n        ui.mount();\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div style=\"overflow: visible; position: relative; width: 0px; height: 0px; display: block;\"><iframe src=\"chrome-extension://test-extension-id/page.html\" style=\"position: absolute; bottom: 0px; right: 0px;\"></iframe></div>\"`,\n        );\n      });\n\n      it('should wrap the UI in a positioned div when alignment=bottom-left', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'overlay',\n          page: '/page.html',\n          alignment: 'bottom-left',\n        });\n        ui.mount();\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div style=\"overflow: visible; position: relative; width: 0px; height: 0px; display: block;\"><iframe src=\"chrome-extension://test-extension-id/page.html\" style=\"position: absolute; bottom: 0px; left: 0px;\"></iframe></div>\"`,\n        );\n      });\n\n      it('should respect the provided zIndex', () => {\n        const zIndex = 123;\n        const ui = createIframeUi(ctx, {\n          position: 'overlay',\n          page: '/page.html',\n          zIndex,\n        });\n        ui.mount();\n\n        expect(ui.wrapper.style.zIndex).toBe(String(zIndex));\n      });\n    });\n\n    describe('modal', () => {\n      it('should wrap the UI in a div with a fixed position', () => {\n        const ui = createIframeUi(ctx, {\n          position: 'modal',\n          page: '/page.html',\n        });\n        ui.mount();\n\n        expect(ui.wrapper.outerHTML).toMatchInlineSnapshot(\n          `\"<div style=\"overflow: visible; position: relative; width: 0px; height: 0px; display: block;\"><iframe src=\"chrome-extension://test-extension-id/page.html\" style=\"position: fixed; top: 0px; bottom: 0px; left: 0px; right: 0px;\"></iframe></div>\"`,\n        );\n      });\n\n      it('should respect the provided zIndex', () => {\n        const zIndex = 123;\n        const ui = createIframeUi(ctx, {\n          position: 'modal',\n          page: '/page.html',\n          zIndex,\n        });\n        ui.mount();\n\n        expect(ui.wrapper.style.zIndex).toBe(String(zIndex));\n      });\n    });\n  });\n\n  describe('anchor', () => {\n    describe('undefined', () => {\n      it('should append the element to the body', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.body.children).toContain(ui.wrapper);\n      });\n    });\n\n    describe('string', () => {\n      it('should append the element using the specified query selector', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          onMount: appendTestApp,\n          anchor: '#parent',\n        });\n        ui.mount();\n\n        expect(document.querySelector('#parent')!.children).toContain(\n          ui.wrapper,\n        );\n      });\n\n      it('should append the element using an XPath string', () => {\n        vi.stubGlobal('XPathResult', { FIRST_ORDERED_NODE_TYPE: 9 });\n        document.evaluate = vi.fn().mockReturnValue({\n          singleNodeValue: document.querySelector('#three'),\n        });\n\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          onMount: appendTestApp,\n          anchor: '//p[@id=\"three\"]',\n        });\n        ui.mount();\n\n        expect(document.querySelector('#three')!.children[0]).toBe(ui.wrapper);\n      });\n    });\n\n    describe('Element', () => {\n      it('should append the element using the specified element', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          onMount: appendTestApp,\n          anchor: document.getElementById('parent'),\n        });\n        ui.mount();\n\n        expect(document.querySelector('#parent')!.children).toContain(\n          ui.wrapper,\n        );\n      });\n    });\n\n    describe('function', () => {\n      it('should append the element using the specified function', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          onMount: appendTestApp,\n          anchor: () => document.getElementById('parent'),\n        });\n        ui.mount();\n\n        expect(document.querySelector('#parent')!.children).toContain(\n          ui.wrapper,\n        );\n      });\n    });\n\n    it('should throw an error when the anchor does not exist', () => {\n      const ui = createIntegratedUi(ctx, {\n        position: 'inline',\n        onMount: appendTestApp,\n        anchor: () => document.getElementById('i-do-not-exist'),\n      });\n\n      expect(ui.mount).toThrow();\n    });\n  });\n\n  describe('append', () => {\n    describe.each([undefined, 'last'] as const)('%s', (append) => {\n      it('should append the element as the last child of the anchor', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          anchor: '#parent',\n          append,\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(\n          document.querySelector('#parent')!.lastElementChild,\n        ).not.toBeNull();\n      });\n    });\n\n    describe('first', () => {\n      it('should append the element as the last child of the anchor', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          anchor: '#parent',\n          append: 'first',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(\n          document.querySelector('#parent')!.children[0].firstElementChild,\n        ).not.toBeNull();\n      });\n    });\n\n    describe('replace', () => {\n      it('should replace the the anchor', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          anchor: '#parent',\n          append: 'replace',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.body.children).toContain(ui.wrapper);\n        expect(document.querySelector('#parent')).toBeNull();\n      });\n    });\n\n    describe('before', () => {\n      it('should append the UI before the anchor', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          anchor: '#one',\n          append: 'before',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.querySelector('#parent')!.firstElementChild).toBe(\n          ui.wrapper,\n        );\n      });\n    });\n\n    describe('after', () => {\n      it('should append the UI after the anchor', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          anchor: '#three',\n          append: 'after',\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.querySelector('#parent')!.lastElementChild).toBe(\n          ui.wrapper,\n        );\n      });\n    });\n\n    describe('function', () => {\n      it('should append the UI using a function', () => {\n        const ui = createIntegratedUi(ctx, {\n          position: 'inline',\n          anchor: '#parent',\n          append: (anchor, ui) => {\n            anchor.replaceWith(ui);\n          },\n          onMount: appendTestApp,\n        });\n        ui.mount();\n\n        expect(document.body.children).toContain(ui.wrapper);\n\n        expect(document.querySelector('#parent')).toBeNull();\n      });\n    });\n  });\n\n  describe('mounted value', () => {\n    describe('integrated', () => {\n      it('should set the mounted value based on the onMounted return value', () => {\n        const expected = Symbol();\n\n        const ui = createIntegratedUi(new ContentScriptContext('test'), {\n          position: 'inline',\n          onMount: () => expected,\n        });\n        expect(ui.mounted).toBeUndefined();\n\n        ui.mount();\n        expect(ui.mounted).toBe(expected);\n\n        ui.remove();\n        expect(ui.mounted).toBeUndefined();\n      });\n    });\n\n    describe('iframe', () => {\n      it('should set the mounted value based on the onMounted return value', async () => {\n        const expected = Symbol();\n\n        const ui = createIframeUi(new ContentScriptContext('test'), {\n          page: '',\n          position: 'inline',\n          onMount: () => expected,\n        });\n        expect(ui.mounted).toBeUndefined();\n\n        ui.mount();\n        expect(ui.mounted).toBe(expected);\n\n        ui.remove();\n        expect(ui.mounted).toBeUndefined();\n      });\n    });\n\n    describe('shadow-root', () => {\n      it('should set the mounted value based on the onMounted return value', async () => {\n        const expected = Symbol();\n\n        const ui = await createShadowRootUi(new ContentScriptContext('test'), {\n          name: 'test-component',\n          position: 'inline',\n          onMount: () => expected,\n        });\n        expect(ui.mounted).toBeUndefined();\n\n        ui.mount();\n        expect(ui.mounted).toBe(expected);\n\n        ui.remove();\n        expect(ui.mounted).toBeUndefined();\n      });\n    });\n  });\n\n  /** Need call runMicrotasks after floating-promise and append/remove dom */\n  describe('auto mount', () => {\n    const DYNAMIC_CHILD_ID = 'dynamic-child';\n    let ui: ContentScriptUi<any>;\n    beforeEach(async () => {\n      ui?.remove();\n      await runMicrotasks();\n    });\n    afterEach(async () => {\n      ui?.remove();\n      await runMicrotasks();\n    });\n\n    describe.each([\n      {\n        name: 'integrated',\n        createUiFunction: createIntegratedUi,\n        uiSelector: '[data-wrapper-name=\"integrated\"]',\n      },\n      {\n        name: 'iframe',\n        createUiFunction: createIframeUi,\n        uiSelector: `[data-wrapper-name=\"iframe\"]`,\n      },\n      {\n        name: 'shadow-root',\n        createUiFunction: createShadowRootUi,\n        uiSelector: 'test-component',\n      },\n    ] as const)(\n      'built-in UI type: $name',\n      ({ name, createUiFunction, uiSelector }) => {\n        const onMount = vi.fn((wrapper: HTMLElement) => {\n          if (name === 'shadow-root') return;\n\n          wrapper.setAttribute('data-wrapper-name', name);\n          appendTestApp(wrapper);\n        });\n\n        it('should mount if an anchor already exists at the initialization', async () => {\n          ui = await createUiFunction(ctx, {\n            position: 'inline',\n            onMount,\n            anchor: `#parent > #${DYNAMIC_CHILD_ID}`,\n            page: name === 'iframe' ? '/page.html' : undefined,\n            name: 'test-component',\n          });\n\n          appendTestElement({ id: DYNAMIC_CHILD_ID });\n\n          ui.autoMount();\n          await runMicrotasks();\n\n          await expect\n            .poll(() => document.querySelector(uiSelector))\n            .not.toBeNull();\n        });\n\n        it('should mount when an anchor is dynamically added and unmount when an anchor is removed', async () => {\n          const onRemove = vi.fn();\n          ui = await createUiFunction(ctx, {\n            position: 'inline',\n            onMount,\n            onRemove,\n            anchor: `#parent > #${DYNAMIC_CHILD_ID}`,\n            page: name === 'iframe' ? '/page.html' : undefined,\n            name: 'test-component',\n          });\n          let dynamicEl;\n          ui.autoMount();\n          await runMicrotasks();\n\n          for (let index = 0; index < 3; index++) {\n            await expect\n              .poll(() => document.querySelector(uiSelector))\n              .toBeNull();\n\n            dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });\n            await runMicrotasks();\n            await expect\n              .poll(() => document.querySelector(uiSelector))\n              .not.toBeNull();\n\n            dynamicEl.remove();\n            await runMicrotasks();\n          }\n\n          expect(onMount).toHaveBeenCalledTimes(3);\n          expect(onRemove).toHaveBeenCalledTimes(3);\n        });\n\n        describe('options', () => {\n          it('should auto-mount only once mount and remove when the `once` option is true', async () => {\n            const onRemove = vi.fn();\n            ui = await createUiFunction(ctx, {\n              position: 'inline',\n              onMount,\n              onRemove,\n              anchor: `#parent > #${DYNAMIC_CHILD_ID}`,\n              page: name === 'iframe' ? '/page.html' : undefined,\n              name: 'test-component',\n            });\n            let dynamicEl;\n            ui.autoMount({ once: true });\n            await runMicrotasks();\n            await expect\n              .poll(() => document.querySelector(uiSelector))\n              .toBeNull();\n\n            dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });\n            await runMicrotasks();\n            await expect\n              .poll(() => document.querySelector(uiSelector))\n              .not.toBeNull();\n\n            dynamicEl.remove();\n            await runMicrotasks();\n            expect(onMount).toHaveBeenCalledTimes(1);\n            expect(onRemove).toHaveBeenCalledTimes(1);\n\n            // re-append after once cycle\n            dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });\n            await runMicrotasks();\n\n            // expect stop automount\n            await expect\n              .poll(() => document.querySelector(uiSelector))\n              .toBeNull();\n            expect(onMount).toHaveBeenCalledTimes(1);\n            expect(onRemove).toHaveBeenCalledTimes(1);\n          });\n        });\n\n        describe('invalid anchors', () => {\n          it('should throw when anchor is set as type Element', async () => {\n            ui = await createUiFunction(ctx, {\n              position: 'inline',\n              onMount,\n              anchor: document.documentElement,\n              page: name === 'iframe' ? '/page.html' : undefined,\n              name: 'test-component',\n            });\n            expect(() => ui.autoMount()).toThrowError(\n              'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.',\n            );\n          });\n\n          it('should throw when anchor is set as type `() => Element`', async () => {\n            ui = await createUiFunction(ctx, {\n              position: 'inline',\n              onMount,\n              anchor: () => document.documentElement,\n              page: name === 'iframe' ? '/page.html' : undefined,\n              name: 'test-component',\n            });\n            expect(() => ui.autoMount()).toThrowError(\n              'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.',\n            );\n          });\n        });\n\n        describe('StopAutoMount', () => {\n          it('should stop auto-mounting and remove ui when `ui.remove` is called', async () => {\n            const onRemove = vi.fn();\n            ui = await createUiFunction(ctx, {\n              position: 'inline',\n              onMount,\n              onRemove,\n              anchor: `#parent > #${DYNAMIC_CHILD_ID}`,\n              page: name === 'iframe' ? '/page.html' : undefined,\n              name: 'test-component',\n            });\n            let dynamicEl;\n            ui.autoMount();\n            await runMicrotasks();\n\n            dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });\n            await runMicrotasks();\n            await expect\n              .poll(() => document.querySelector(uiSelector))\n              .not.toBeNull();\n\n            dynamicEl.remove();\n            await runMicrotasks();\n            expect(onMount).toHaveBeenCalledTimes(1);\n            expect(onRemove).toHaveBeenCalledTimes(1);\n\n            ui.remove();\n\n            dynamicEl = appendTestElement({ id: DYNAMIC_CHILD_ID });\n            dynamicEl.remove();\n            await runMicrotasks();\n            expect(onMount).toHaveBeenCalledTimes(1);\n            expect(onRemove).toHaveBeenCalledTimes(2);\n          });\n\n          it('should call internal StopAutoMount when `ui.remove` is called', async () => {\n            const onRemove = vi.fn();\n            const onStop = vi.fn();\n            ui = await createUiFunction(ctx, {\n              position: 'inline',\n              onMount,\n              onRemove,\n              anchor: `#parent > #${DYNAMIC_CHILD_ID}`,\n              page: name === 'iframe' ? '/page.html' : undefined,\n              name: 'test-component',\n            });\n            ui.autoMount({ onStop });\n            ui.remove();\n            expect(onStop).toHaveBeenCalledTimes(1);\n            expect(onRemove).toHaveBeenCalledTimes(1);\n          });\n\n          it('should allow calling automount again after internal StopAutoMount is called', async () => {\n            ui = await createUiFunction(ctx, {\n              position: 'inline',\n              onMount,\n              anchor: `#parent > #${DYNAMIC_CHILD_ID}`,\n              page: name === 'iframe' ? '/page.html' : undefined,\n              name: 'test-component',\n            });\n            const onStop = vi.fn();\n            ui.autoMount({ onStop });\n            ui.autoMount({ onStop });\n\n            ui.remove();\n            expect(onStop).toBeCalledTimes(1);\n\n            ui.autoMount({ onStop });\n\n            ui.remove();\n            expect(onStop).toBeCalledTimes(2);\n          });\n        });\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-ui/iframe.ts",
    "content": "/** @module wxt/utils/content-script-ui/iframe */\nimport { browser } from 'wxt/browser';\nimport { ContentScriptContext } from '../content-script-context';\nimport type { ContentScriptUi, ContentScriptUiOptions } from './types';\nimport { applyPosition, createMountFunctions, mountUi } from './shared';\n\n/**\n * Create a content script UI using an iframe.\n *\n * @see https://wxt.dev/guide/essentials/content-scripts.html#iframe\n */\nexport function createIframeUi<TMounted>(\n  ctx: ContentScriptContext,\n  options: IframeContentScriptUiOptions<TMounted>,\n): IframeContentScriptUi<TMounted> {\n  const wrapper = document.createElement('div');\n  const iframe = document.createElement('iframe');\n  // @ts-expect-error: getURL is defined per-project, but not inside the package\n  iframe.src = browser.runtime.getURL(options.page);\n  wrapper.appendChild(iframe);\n\n  let mounted: TMounted | undefined;\n  const mount = () => {\n    applyPosition(wrapper, iframe, options);\n    options.onBeforeMount?.(wrapper, iframe);\n    mountUi(wrapper, options);\n    mounted = options.onMount?.(wrapper, iframe);\n  };\n  const remove = () => {\n    options.onRemove?.(mounted);\n    wrapper.remove();\n    mounted = undefined;\n  };\n\n  const mountFunctions = createMountFunctions({ mount, remove }, options);\n\n  ctx.onInvalidated(remove);\n\n  return {\n    get mounted() {\n      return mounted;\n    },\n    iframe,\n    wrapper,\n    ...mountFunctions,\n  };\n}\n\nexport interface IframeContentScriptUi<\n  TMounted,\n> extends ContentScriptUi<TMounted> {\n  /** The iframe added to the DOM. */\n  iframe: HTMLIFrameElement;\n  /** A wrapper div that assists in positioning. */\n  wrapper: HTMLDivElement;\n}\n\nexport type IframeContentScriptUiOptions<TMounted> =\n  ContentScriptUiOptions<TMounted> & {\n    /**\n     * The path to the HTML page that will be shown in the iframe. This string\n     * is passed into `browser.runtime.getURL`.\n     */\n    // @ts-expect-error: HtmlPublicPath is generated per-project\n    page: import('wxt/browser').HtmlPublicPath;\n    /**\n     * Callback executed when mounting the UI. Use this function to customize\n     * the iframe or wrapper element's appearance. It is called every time\n     * `ui.mount()` is called.\n     *\n     * Optionally return a value that can be accessed at `ui.mounted` or in the\n     * `onRemove` callback.\n     */\n    onMount?: (wrapper: HTMLElement, iframe: HTMLIFrameElement) => TMounted;\n    /**\n     * Callback executed before mounting the UI. Use this function to customize\n     * the iframe or wrapper elements before they are injected into the DOM. It\n     * is called every time `ui.mount()` is called.\n     */\n    onBeforeMount?: (wrapper: HTMLElement, iframe: HTMLIFrameElement) => void;\n  };\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-ui/integrated.ts",
    "content": "/** @module wxt/utils/content-script-ui/integrated */\nimport { ContentScriptContext } from '../content-script-context';\nimport type { ContentScriptUi, ContentScriptUiOptions } from './types';\nimport { applyPosition, createMountFunctions, mountUi } from './shared';\n\n/**\n * Create a content script UI without any isolation.\n *\n * @see https://wxt.dev/guide/essentials/content-scripts.html#integrated\n */\nexport function createIntegratedUi<TMounted>(\n  ctx: ContentScriptContext,\n  options: IntegratedContentScriptUiOptions<TMounted>,\n): IntegratedContentScriptUi<TMounted> {\n  const wrapper = document.createElement(options.tag || 'div');\n\n  let mounted: TMounted | undefined;\n  const mount = () => {\n    applyPosition(wrapper, undefined, options);\n    mountUi(wrapper, options);\n    mounted = options.onMount?.(wrapper);\n  };\n  const remove = () => {\n    options.onRemove?.(mounted);\n    wrapper.replaceChildren();\n    wrapper.remove();\n    mounted = undefined;\n  };\n\n  const mountFunctions = createMountFunctions(\n    {\n      mount,\n      remove,\n    },\n    options,\n  );\n\n  ctx.onInvalidated(remove);\n\n  return {\n    get mounted() {\n      return mounted;\n    },\n    wrapper,\n    ...mountFunctions,\n  };\n}\n\n/**\n * Shared types for the different `wxt/utils/content-script-ui/*` modules.\n *\n * @module wxt/utils/content-script-ui/types\n */\nexport interface IntegratedContentScriptUi<\n  TMounted,\n> extends ContentScriptUi<TMounted> {\n  /** A wrapper div that assists in positioning. */\n  wrapper: HTMLElement;\n}\n\nexport type IntegratedContentScriptUiOptions<TMounted> =\n  ContentScriptUiOptions<TMounted> & {\n    /**\n     * Tag used to create the wrapper element.\n     *\n     * @default 'div'\n     */\n    tag?: string;\n    /**\n     * Callback executed when mounting the UI. This function should create and\n     * append the UI to the `wrapper` element. It is called every time\n     * `ui.mount()` is called.\n     *\n     * Optionally return a value that can be accessed at `ui.mounted` or in the\n     * `onRemove` callback.\n     */\n    onMount: (wrapper: HTMLElement) => TMounted;\n  };\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-ui/shadow-root.ts",
    "content": "/** @module wxt/utils/content-script-ui/shadow-root */\nimport { browser } from 'wxt/browser';\nimport { ContentScriptContext } from '../content-script-context';\nimport type { ContentScriptUi, ContentScriptUiOptions } from './types';\nimport { createIsolatedElement } from '@webext-core/isolated-element';\nimport { applyPosition, createMountFunctions, mountUi } from './shared';\nimport { logger } from '../internal/logger';\nimport { splitShadowRootCss } from '../split-shadow-root-css';\n\n/**\n * Create a content script UI inside a\n * [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).\n *\n * > This function is async because it has to load the CSS via a network call.\n *\n * @see https://wxt.dev/guide/essentials/content-scripts.html#shadow-root\n */\nexport async function createShadowRootUi<TMounted>(\n  ctx: ContentScriptContext,\n  options: ShadowRootContentScriptUiOptions<TMounted>,\n): Promise<ShadowRootContentScriptUi<TMounted>> {\n  const instanceId = Math.random().toString(36).substring(2, 15);\n  const css: string[] = [];\n\n  if (!options.inheritStyles) {\n    css.push(`/* WXT Shadow Root Reset */ :host{all:initial !important;}`);\n  }\n  if (options.css) {\n    css.push(options.css);\n  }\n  if (ctx.options?.cssInjectionMode === 'ui') {\n    const entryCss = await loadCss();\n    // Replace :root selectors with :host since we're in a shadow root\n    css.push(entryCss.replaceAll(':root', ':host'));\n  }\n\n  // Some rules must be applied outside the shadow root, so split the CSS apart\n  const { shadowCss, documentCss } = splitShadowRootCss(css.join('\\n').trim());\n\n  const {\n    isolatedElement: uiContainer,\n    parentElement: shadowHost,\n    shadow,\n  } = await createIsolatedElement({\n    name: options.name,\n    css: {\n      textContent: shadowCss,\n    },\n    mode: options.mode ?? 'open',\n    isolateEvents: options.isolateEvents,\n  });\n\n  let mounted: TMounted | undefined;\n\n  const mount = () => {\n    // Add shadow root element to DOM\n    mountUi(shadowHost, options);\n    applyPosition(shadowHost, shadow.querySelector('html'), options);\n\n    // Add document CSS\n    if (\n      documentCss &&\n      !document.querySelector(\n        `style[wxt-shadow-root-document-styles=\"${instanceId}\"]`,\n      )\n    ) {\n      const style = document.createElement('style');\n      style.textContent = documentCss;\n      style.setAttribute('wxt-shadow-root-document-styles', instanceId);\n      (document.head ?? document.body).append(style);\n    }\n\n    // Mount UI inside shadow root\n    mounted = options.onMount(uiContainer, shadow, shadowHost);\n  };\n\n  const remove = () => {\n    // Cleanup mounted state\n    options.onRemove?.(mounted);\n\n    // Detach shadow root from DOM\n    shadowHost.remove();\n\n    // Remove document CSS\n    const documentStyle = document.querySelector(\n      `style[wxt-shadow-root-document-styles=\"${instanceId}\"]`,\n    );\n    documentStyle?.remove();\n\n    // Remove children from uiContainer\n    while (uiContainer.lastChild)\n      uiContainer.removeChild(uiContainer.lastChild);\n\n    // Clear mounted value\n    mounted = undefined;\n  };\n\n  const mountFunctions = createMountFunctions(\n    {\n      mount,\n      remove,\n    },\n    options,\n  );\n\n  ctx.onInvalidated(remove);\n\n  return {\n    shadow,\n    shadowHost,\n    uiContainer,\n    ...mountFunctions,\n    get mounted() {\n      return mounted;\n    },\n  };\n}\n\n/** Load the CSS for the current entrypoint. */\nasync function loadCss(): Promise<string> {\n  const url = browser.runtime\n    // @ts-expect-error: getURL is defined per-project, but not inside the package\n    .getURL(`/content-scripts/${import.meta.env.ENTRYPOINT}.css`);\n  try {\n    const res = await fetch(url);\n    return await res.text();\n  } catch (err) {\n    logger.warn(\n      `Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,\n      err,\n    );\n    return '';\n  }\n}\n\nexport interface ShadowRootContentScriptUi<\n  TMounted,\n> extends ContentScriptUi<TMounted> {\n  /**\n   * The `HTMLElement` hosting the shadow root used to isolate the UI's styles.\n   * This is the element that get's added to the DOM. This element's style is\n   * not isolated from the webpage.\n   */\n  shadowHost: HTMLElement;\n  /**\n   * The container element inside the `ShadowRoot` whose styles are isolated.\n   * The UI is mounted inside this `HTMLElement`.\n   */\n  uiContainer: HTMLElement;\n  /** The shadow root performing the isolation. */\n  shadow: ShadowRoot;\n}\n\nexport type ShadowRootContentScriptUiOptions<TMounted> =\n  ContentScriptUiOptions<TMounted> & {\n    /**\n     * The name of the custom component used to host the ShadowRoot. Must be\n     * kebab-case.\n     */\n    name: string;\n    /**\n     * Custom CSS text to apply to the UI. If your content script\n     * imports/generates CSS and you've set `cssInjectionMode: \"ui\"`, the\n     * imported CSS will be included automatically. You do not need to pass\n     * those styles in here. This is for any additional styles not in the\n     * imported CSS.\n     */\n    css?: string;\n    /**\n     * ShadowRoot's mode.\n     *\n     * @default 'open'\n     * @see https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode\n     */\n    mode?: 'open' | 'closed';\n    /**\n     * When enabled, `event.stopPropagation` will be called on events trying to\n     * bubble out of the shadow root.\n     *\n     * - Set to `true` to stop the propagation of a default set of events,\n     *   `[\"keyup\", \"keydown\", \"keypress\"]`\n     * - Set to an array of event names to stop the propagation of a custom list\n     *   of events\n     */\n    isolateEvents?: boolean | string[];\n    /**\n     * By default, WXT adds `all: initial` to the shadow root before the rest of\n     * your CSS. This resets any inheritable CSS styles that [normally pierce\n     * the Shadow\n     * DOM](https://open-wc.org/guides/knowledge/styling/styles-piercing-shadow-dom/).\n     *\n     * WXT resets everything but:\n     *\n     * - **`rem` Units**: they continue to scale based off the webpage's HTML\n     *   `font-size`.\n     * - **CSS Variables/Custom Properties**: CSS variables defined outside the\n     *   shadow root can be accessed inside it.\n     * - **`@font-face` Definitions**: Fonts defined outside the shadow root can\n     *   be used inside it.\n     *\n     * To disable this behavior and inherit styles from the webpage, set\n     * `inheritStyles: true`.\n     *\n     * @default false\n     */\n    inheritStyles?: boolean;\n    /**\n     * Callback executed when mounting the UI. This function should create and\n     * append the UI to the `uiContainer` element. It is called every time\n     * `ui.mount()` is called.\n     *\n     * Optionally return a value that can be accessed at `ui.mounted` or in the\n     * `onRemove` callback.\n     */\n    onMount: (\n      uiContainer: HTMLElement,\n      shadow: ShadowRoot,\n      shadowHost: HTMLElement,\n    ) => TMounted;\n  };\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-ui/shared.ts",
    "content": "import type {\n  ContentScriptAnchoredOptions,\n  ContentScriptPositioningOptions,\n  AutoMount,\n  AutoMountOptions,\n  BaseMountFunctions,\n  ContentScriptUiOptions,\n  MountFunctions,\n} from './types';\nimport { waitElement } from '@1natsu/wait-element';\nimport {\n  isExist as mountDetector,\n  isNotExist as removeDetector,\n} from '@1natsu/wait-element/detectors';\nimport { logger } from '../internal/logger';\n\nexport function applyPosition(\n  root: HTMLElement,\n  positionedElement: HTMLElement | undefined | null,\n  options: ContentScriptPositioningOptions,\n): void {\n  // No positioning for inline UIs\n  if (options.position === 'inline') return;\n\n  if (options.zIndex != null) root.style.zIndex = String(options.zIndex);\n\n  root.style.overflow = 'visible';\n  root.style.position = 'relative';\n  root.style.width = '0';\n  root.style.height = '0';\n  root.style.display = 'block';\n\n  if (positionedElement) {\n    if (options.position === 'overlay') {\n      positionedElement.style.position = 'absolute';\n      if (options.alignment?.startsWith('bottom-'))\n        positionedElement.style.bottom = '0';\n      else positionedElement.style.top = '0';\n\n      if (options.alignment?.endsWith('-right'))\n        positionedElement.style.right = '0';\n      else positionedElement.style.left = '0';\n    } else {\n      positionedElement.style.position = 'fixed';\n      positionedElement.style.top = '0';\n      positionedElement.style.bottom = '0';\n      positionedElement.style.left = '0';\n      positionedElement.style.right = '0';\n    }\n  }\n}\n\nexport function getAnchor(\n  options: ContentScriptAnchoredOptions,\n): Element | undefined {\n  if (options.anchor == null) return document.body;\n\n  let resolved =\n    typeof options.anchor === 'function' ? options.anchor() : options.anchor;\n\n  if (typeof resolved === 'string') {\n    // If the string is an XPath expression (starts with '//' or '/')\n    if (resolved.startsWith('/')) {\n      // Evaluate the XPath and return the first ordered node\n      const result = document.evaluate(\n        resolved,\n        document,\n        null,\n        XPathResult.FIRST_ORDERED_NODE_TYPE,\n        null,\n      );\n      return (result.singleNodeValue as Element) ?? undefined;\n    } else {\n      // If the string is a CSS selector, query the document and return the element\n      return document.querySelector<Element>(resolved) ?? undefined;\n    }\n  }\n\n  return resolved ?? undefined;\n}\n\nexport function mountUi(\n  root: HTMLElement,\n  options: ContentScriptAnchoredOptions,\n): void {\n  const anchor = getAnchor(options);\n  if (anchor == null)\n    throw Error(\n      'Failed to mount content script UI: could not find anchor element',\n    );\n\n  switch (options.append) {\n    case undefined:\n    case 'last':\n      anchor.append(root);\n      break;\n    case 'first':\n      anchor.prepend(root);\n      break;\n    case 'replace':\n      anchor.replaceWith(root);\n      break;\n    case 'after':\n      anchor.parentElement?.insertBefore(root, anchor.nextElementSibling);\n      break;\n    case 'before':\n      anchor.parentElement?.insertBefore(root, anchor);\n      break;\n    default:\n      options.append(anchor, root);\n  }\n}\n\nexport function createMountFunctions<TMounted>(\n  baseFunctions: BaseMountFunctions,\n  options: ContentScriptUiOptions<TMounted>,\n): MountFunctions {\n  let autoMountInstance: AutoMount | undefined;\n\n  const stopAutoMount = () => {\n    autoMountInstance?.stopAutoMount();\n    autoMountInstance = undefined;\n  };\n\n  const mount = () => {\n    baseFunctions.mount();\n  };\n\n  const unmount = baseFunctions.remove;\n\n  const remove = () => {\n    stopAutoMount();\n    baseFunctions.remove();\n  };\n\n  const autoMount = (autoMountOptions?: AutoMountOptions) => {\n    if (autoMountInstance) {\n      logger.warn('autoMount is already set.');\n    }\n    autoMountInstance = autoMountUi(\n      { mount, unmount, stopAutoMount },\n      {\n        ...options,\n        ...autoMountOptions,\n      },\n    );\n  };\n\n  return {\n    mount,\n    remove,\n    autoMount,\n  };\n}\n\nfunction autoMountUi(\n  uiCallbacks: {\n    mount: () => void;\n    unmount: () => void;\n    stopAutoMount: () => void;\n  },\n  options: ContentScriptAnchoredOptions & AutoMountOptions,\n): AutoMount {\n  const abortController = new AbortController();\n  const EXPLICIT_STOP_REASON = 'explicit_stop_auto_mount';\n  const _stopAutoMount = () => {\n    abortController.abort(EXPLICIT_STOP_REASON);\n    options.onStop?.();\n  };\n\n  let resolvedAnchor =\n    typeof options.anchor === 'function' ? options.anchor() : options.anchor;\n  if (resolvedAnchor instanceof Element) {\n    throw Error(\n      'autoMount and Element anchor option cannot be combined. Avoid passing `Element` directly or `() => Element` to the anchor.',\n    );\n  }\n\n  async function observeElement(selector: string | null | undefined) {\n    let isAnchorExist = !!getAnchor(options);\n\n    // Mount if anchor exists at initialization.\n    if (isAnchorExist) {\n      uiCallbacks.mount();\n    }\n\n    while (!abortController.signal.aborted) {\n      try {\n        const changedAnchor = await waitElement(selector ?? 'body', {\n          customMatcher: () => getAnchor(options) ?? null,\n          detector: isAnchorExist ? removeDetector : mountDetector,\n          signal: abortController.signal,\n        });\n        isAnchorExist = !!changedAnchor;\n        if (isAnchorExist) {\n          uiCallbacks.mount();\n        } else {\n          uiCallbacks.unmount();\n          if (options.once) {\n            uiCallbacks.stopAutoMount();\n          }\n        }\n      } catch (error) {\n        if (\n          abortController.signal.aborted &&\n          abortController.signal.reason === EXPLICIT_STOP_REASON\n        ) {\n          break;\n        } else {\n          throw error;\n        }\n      }\n    }\n  }\n  void observeElement(resolvedAnchor);\n\n  return { stopAutoMount: _stopAutoMount };\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/content-script-ui/types.ts",
    "content": "/** @module wxt/utils/content-script-ui/types */\n\nexport interface ContentScriptUi<TMounted> extends MountFunctions {\n  mounted: TMounted | undefined;\n}\n\nexport type ContentScriptUiOptions<TMounted> = ContentScriptPositioningOptions &\n  ContentScriptAnchoredOptions & {\n    /**\n     * Callback called before the UI is removed from the webpage. Use to cleanup\n     * your UI, like unmounting your Vue or React apps.\n     *\n     * Note that this callback is called only when `ui.remove` is called - that\n     * means it is not called automatically when the anchor is removed, unless\n     * you use `autoMount`.\n     */\n    onRemove?: (mounted: TMounted | undefined) => void;\n  };\n\nexport type ContentScriptOverlayAlignment =\n  | 'top-left'\n  | 'top-right'\n  | 'bottom-left'\n  | 'bottom-right';\n\n/**\n * [Visualization of different append\n * modes](https://wxt.dev/content-script-ui-append.png)\n */\nexport type ContentScriptAppendMode =\n  | 'last'\n  | 'first'\n  | 'replace'\n  | 'before'\n  | 'after'\n  | ((anchor: Element, ui: Element) => void);\n\nexport interface ContentScriptInlinePositioningOptions {\n  position: 'inline';\n}\n\nexport interface ContentScriptOverlayPositioningOptions {\n  position: 'overlay';\n  /**\n   * The `z-index` used on the `wrapper` element. Set to a positive number to\n   * show your UI over website content.\n   */\n  zIndex?: number;\n  /**\n   * When using `type: \"overlay\"`, the mounted element is 0px by 0px in size.\n   * Alignment specifies which corner is aligned with that 0x0 pixel space.\n   *\n   * [Visualization of alignment\n   * options](https://wxt.dev/content-script-ui-alignment.png)\n   *\n   * @default 'top-left'\n   */\n  alignment?: ContentScriptOverlayAlignment;\n}\n\nexport interface ContentScriptModalPositioningOptions {\n  position: 'modal';\n  /**\n   * The `z-index` used on the `shadowHost`. Set to a positive number to show\n   * your UI over website content.\n   */\n  zIndex?: number;\n}\n\n/**\n * Choose between `\"inline\"`, `\"overlay\"`, or `\"modal\"` positions.\n *\n * [Visualization of different\n * types](https://wxt.dev/content-script-ui-position.png)\n */\nexport type ContentScriptPositioningOptions =\n  | ContentScriptInlinePositioningOptions\n  | ContentScriptOverlayPositioningOptions\n  | ContentScriptModalPositioningOptions;\n\nexport interface ContentScriptAnchoredOptions {\n  /**\n   * A CSS selector, XPath expression, element, or function that returns one of\n   * the three. Along with `append`, the `anchor` dictates where in the page the\n   * UI will be added.\n   */\n  anchor?:\n    | string\n    | Element\n    | null\n    | undefined\n    | (() => string | Element | null | undefined);\n  /**\n   * In combination with `anchor`, decide how to add the UI to the DOM.\n   *\n   * - `\"last\"` (default) - Add the UI as the last child of the `anchor` element\n   * - `\"first\"` - Add the UI as the first child of the `anchor` element\n   * - `\"replace\"` - Replace the `anchor` element with the UI.\n   * - `\"before\"` - Add the UI as the sibling before the `anchor` element\n   * - `\"after\"` - Add the UI as the sibling after the `anchor` element\n   * - `(anchor, ui) => void` - Customizable function that let's you add the UI to\n   *   the DOM\n   */\n  append?: ContentScriptAppendMode | ((anchor: Element, ui: Element) => void);\n}\n\nexport interface BaseMountFunctions {\n  /** Function that mounts or remounts the UI on the page. */\n  mount: () => void;\n\n  /** Function that removes the UI from the webpage. */\n  remove: () => void;\n}\n\nexport interface MountFunctions extends BaseMountFunctions {\n  /**\n   * Call `ui.autoMount()` to automatically mount and remove the UI as the\n   * anchor is dynamically added/removed by the webpage.\n   */\n  autoMount: (options?: AutoMountOptions) => void;\n}\n\nexport type AutoMountOptions = {\n  /** When true, only mount and unmount a UI once. */\n  once?: boolean;\n  /** The callback triggered when `StopAutoMount` is called. */\n  onStop?: () => void;\n};\nexport type StopAutoMount = () => void;\nexport interface AutoMount {\n  /** Stop watching the anchor element for changes, but keep the UI mounted. */\n  stopAutoMount: StopAutoMount;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/define-app-config.ts",
    "content": "/** @module wxt/utils/define-app-config */\nexport interface WxtAppConfig {}\n\n/**\n * Runtime app config defined in `<srcDir>/app.config.ts`.\n *\n * You can add fields to this interface via [\"Module\n * Augmentation\"](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation):\n *\n * ```ts\n * // app.config.ts\n * import 'wxt/utils/define-app-config';\n *\n * declare module 'wxt/utils/define-app-config' {\n *   export interface WxtAppConfig {\n *     analytics: AnalyticsConfig;\n *   }\n * }\n * ```\n */\nexport function defineAppConfig(config: WxtAppConfig): WxtAppConfig {\n  return config;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/define-background.ts",
    "content": "/** @module wxt/utils/define-background */\nimport type { BackgroundDefinition } from '../types';\n\nexport function defineBackground(main: () => void): BackgroundDefinition;\nexport function defineBackground(\n  definition: BackgroundDefinition,\n): BackgroundDefinition;\nexport function defineBackground(\n  arg: (() => void) | BackgroundDefinition,\n): BackgroundDefinition {\n  if (arg == null || typeof arg === 'function') return { main: arg };\n  return arg;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/define-content-script.ts",
    "content": "/** @module wxt/utils/define-content-script */\nimport type { ContentScriptDefinition } from '../types';\n\nexport function defineContentScript(\n  definition: ContentScriptDefinition,\n): ContentScriptDefinition {\n  return definition;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/define-unlisted-script.ts",
    "content": "/** @module wxt/utils/define-unlisted-script */\nimport type { UnlistedScriptDefinition } from '../types';\n\nexport function defineUnlistedScript(\n  main: () => void,\n): UnlistedScriptDefinition;\nexport function defineUnlistedScript(\n  definition: UnlistedScriptDefinition,\n): UnlistedScriptDefinition;\nexport function defineUnlistedScript(\n  arg: (() => void) | UnlistedScriptDefinition,\n): UnlistedScriptDefinition {\n  if (arg == null || typeof arg === 'function') return { main: arg };\n  return arg;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/define-wxt-plugin.ts",
    "content": "/** @module wxt/utils/define-wxt-plugin */\nimport type { WxtPlugin } from '../types';\n\nexport function defineWxtPlugin(plugin: WxtPlugin): WxtPlugin {\n  return plugin;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/inject-script.ts",
    "content": "/** @module wxt/utils/inject-script */\nimport { browser } from 'wxt/browser';\n\nexport type ScriptPublicPath = Extract<\n  // @ts-expect-error: PublicPath is generated per-project\n  import('wxt/browser').PublicPath,\n  `${string}.js`\n>;\n\n/**\n * This function can only be called inside content scripts.\n *\n * Inject an unlisted script into the page. Scripts are added to the `<head>`\n * element or `document.documentElement` if there is no head.\n *\n * Make sure to add the injected script to your manifest's\n * `web_accessible_resources`.\n *\n * @returns A result object containing the created script element.\n */\nexport async function injectScript(\n  path: ScriptPublicPath,\n  options?: InjectScriptOptions,\n): Promise<InjectScriptResult> {\n  // @ts-expect-error: getURL is defined per-project, but not inside the package\n  const url = browser.runtime.getURL(path);\n  const script = document.createElement('script');\n\n  if (browser.runtime.getManifest().manifest_version === 2) {\n    // MV2 requires using an inline script\n    script.text = await fetch(url).then((res) => res.text());\n  } else {\n    // MV3 requires using src\n    script.src = url;\n  }\n\n  const loadedPromise = makeLoadedPromise(script);\n\n  await options?.modifyScript?.(script);\n\n  (document.head ?? document.documentElement).append(script);\n\n  if (!options?.keepInDom) {\n    script.remove();\n  }\n\n  await loadedPromise;\n\n  return {\n    script,\n  };\n}\n\nfunction makeLoadedPromise(script: HTMLScriptElement): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const onload = () => {\n      resolve();\n      cleanup();\n    };\n\n    const onerror = () => {\n      reject(new Error(`Failed to load script: ${script.src}`));\n      cleanup();\n    };\n\n    const cleanup = () => {\n      script.removeEventListener('load', onload);\n      script.removeEventListener('error', onerror);\n    };\n\n    script.addEventListener('load', onload);\n    script.addEventListener('error', onerror);\n  });\n}\n\nexport interface InjectScriptOptions {\n  /**\n   * By default, the injected script is removed from the DOM after being\n   * injected. To disable this behavior, set this flag to true.\n   */\n  keepInDom?: boolean;\n  /**\n   * Modify the script element just before it is added to the DOM.\n   *\n   * It can be used to e.g. modify `script.async`/`script.defer`, add event\n   * listeners to the element, or pass data to the script via `script.dataset`\n   * (which can be accessed by the script via `document.currentScript`).\n   */\n  modifyScript?: (script: HTMLScriptElement) => Promise<void> | void;\n}\n\nexport interface InjectScriptResult {\n  /**\n   * The created script element. It can be used to e.g. send messages to the\n   * script in the form of custom events. The script can add an event listener\n   * for them via `document.currentScript`.\n   */\n  script: HTMLScriptElement;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/internal/custom-events.ts",
    "content": "import { browser } from 'wxt/browser';\n\nexport class WxtLocationChangeEvent extends Event {\n  static EVENT_NAME = getUniqueEventName('wxt:locationchange');\n\n  constructor(\n    readonly newUrl: URL,\n    readonly oldUrl: URL,\n  ) {\n    super(WxtLocationChangeEvent.EVENT_NAME, {});\n  }\n}\n\n/**\n * Returns an event name unique to the extension and content script that's\n * running.\n */\nexport function getUniqueEventName(eventName: string): string {\n  return `${browser?.runtime?.id}:${import.meta.env.ENTRYPOINT}:${eventName}`;\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/internal/dev-server-websocket.ts",
    "content": "import { logger } from './logger';\n\ninterface WebSocketMessage {\n  type: string;\n  event: string;\n  data?: any;\n}\n\nexport interface WxtWebSocket extends WebSocket {\n  addWxtEventListener(\n    type: 'wxt:reload-extension',\n    callback: (event: CustomEvent<undefined>) => void,\n    options?: AddEventListenerOptions | boolean,\n  ): void;\n  addWxtEventListener(\n    type: 'wxt:reload-content-script',\n    callback?: (event: CustomEvent<ReloadContentScriptPayload>) => void,\n    options?: AddEventListenerOptions | boolean,\n  ): void;\n  addWxtEventListener(\n    type: 'wxt:reload-page',\n    callback?: (event: CustomEvent<string>) => void,\n    options?: AddEventListenerOptions | boolean,\n  ): void;\n  sendCustom(event: string, payload?: any): void;\n}\n\nlet ws: WxtWebSocket | undefined;\n\n/** Connect to the websocket and listen for messages. */\nexport function getDevServerWebSocket(): WxtWebSocket {\n  if (import.meta.env.COMMAND !== 'serve')\n    throw Error(\n      'Must be running WXT dev command to connect to call getDevServerWebSocket()',\n    );\n\n  if (ws == null) {\n    const serverUrl = __DEV_SERVER_ORIGIN__;\n    logger.debug('Connecting to dev server @', serverUrl);\n\n    ws = new WebSocket(serverUrl, 'vite-hmr') as WxtWebSocket;\n    ws.addWxtEventListener = ws.addEventListener.bind(ws);\n    ws.sendCustom = (event, payload) =>\n      ws?.send(JSON.stringify({ type: 'custom', event, payload }));\n\n    ws.addEventListener('open', () => {\n      logger.debug('Connected to dev server');\n    });\n    ws.addEventListener('close', () => {\n      logger.debug('Disconnected from dev server');\n    });\n    ws.addEventListener('error', (event) => {\n      logger.error('Failed to connect to dev server', event);\n    });\n\n    ws.addEventListener('message', (e) => {\n      try {\n        const message = JSON.parse(e.data) as WebSocketMessage;\n        if (message.type === 'custom') {\n          ws?.dispatchEvent(\n            new CustomEvent(message.event, { detail: message.data }),\n          );\n        }\n      } catch (err) {\n        logger.error('Failed to handle message', err);\n      }\n    });\n  }\n\n  return ws;\n}\n\nexport interface ReloadContentScriptPayload {\n  registration?: 'manifest' | 'runtime';\n  contentScript: {\n    matches: string[];\n    js?: string[];\n    css?: string[];\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/internal/location-watcher.ts",
    "content": "import { ContentScriptContext } from '../content-script-context';\nimport { WxtLocationChangeEvent } from './custom-events';\n\nconst supportsNavigationApi =\n  typeof (globalThis as any).navigation?.addEventListener === 'function';\n\n/**\n * Create a util that watches for URL changes, dispatching the custom event when\n * detected. Stops watching when content script is invalidated. Uses Navigation\n * API when available, otherwise falls back to polling.\n */\nexport function createLocationWatcher(ctx: ContentScriptContext) {\n  let lastUrl: URL;\n  let watching = false;\n\n  return {\n    /**\n     * Ensure the location watcher is actively looking for URL changes. If it's\n     * already watching, this is a noop.\n     */\n    run() {\n      if (watching) return;\n      watching = true;\n      lastUrl = new URL(location.href);\n\n      if (supportsNavigationApi) {\n        (globalThis as any).navigation.addEventListener(\n          'navigate',\n          (event: any) => {\n            const newUrl = new URL(event.destination.url);\n            if (newUrl.href === lastUrl.href) return;\n            window.dispatchEvent(new WxtLocationChangeEvent(newUrl, lastUrl));\n            lastUrl = newUrl;\n          },\n          { signal: ctx.signal },\n        );\n      } else {\n        ctx.setInterval(() => {\n          const newUrl = new URL(location.href);\n          if (newUrl.href !== lastUrl.href) {\n            window.dispatchEvent(new WxtLocationChangeEvent(newUrl, lastUrl));\n            lastUrl = newUrl;\n          }\n        }, 1e3);\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/internal/logger.ts",
    "content": "/// <reference types=\"vite/client\" />\n\nfunction print(method: (...args: any[]) => void, ...args: any[]) {\n  if (import.meta.env.MODE === 'production') return;\n\n  if (typeof args[0] === 'string') {\n    const message = args.shift();\n    method(`[wxt] ${message}`, ...args);\n  } else {\n    method('[wxt]', ...args);\n  }\n}\n\n/** Wrapper around `console` with a \"[wxt]\" prefix */\nexport const logger = {\n  debug: (...args: any[]) => print(console.debug, ...args),\n  log: (...args: any[]) => print(console.log, ...args),\n  warn: (...args: any[]) => print(console.warn, ...args),\n  error: (...args: any[]) => print(console.error, ...args),\n};\n"
  },
  {
    "path": "packages/wxt/src/utils/match-patterns.ts",
    "content": "/**\n * Re-export the [`@webext-core/match-patterns`\n * package](https://www.npmjs.com/package/@webext-core/match-patterns).\n *\n * @module wxt/utils/match-patterns\n */\nexport * from '@webext-core/match-patterns';\n"
  },
  {
    "path": "packages/wxt/src/utils/split-shadow-root-css.ts",
    "content": "/** @module wxt/utils/split-shadow-root-css */\n\nconst AT_RULE_BLOCKS = /(\\s*@(property|font-face)[\\s\\S]*?{[\\s\\S]*?})/gm;\n\n/**\n * Given a CSS string that will be loaded into a shadow root, split it into two\n * parts:\n *\n * - `documentCss`: CSS that needs to be applied to the document (like\n *   `@property`)\n * - `shadowCss`: CSS that needs to be applied to the shadow root\n *\n * @param css\n */\nexport function splitShadowRootCss(css: string): {\n  documentCss: string;\n  shadowCss: string;\n} {\n  const documentCss = Array.from(css.matchAll(AT_RULE_BLOCKS), (m) => m[0])\n    .join('')\n    .trim();\n  const shadowCss = css.replace(AT_RULE_BLOCKS, '').trim();\n\n  return {\n    documentCss: documentCss,\n    shadowCss: shadowCss,\n  };\n}\n"
  },
  {
    "path": "packages/wxt/src/utils/storage.ts",
    "content": "/**\n * Re-export the [`@wxt-dev/storage`\n * package](https://www.npmjs.com/package/@wxt-dev/storage).\n *\n * @module wxt/utils/storage\n */\nexport * from '@wxt-dev/storage';\n"
  },
  {
    "path": "packages/wxt/src/version.ts",
    "content": "export const version = '{{version}}';\n"
  },
  {
    "path": "packages/wxt/src/virtual/README.md",
    "content": "# WXT Virtual Entrypoints\n\nThis folder contains scripts that are either loaded as entrypoints to JS files or included in HTML files, just like a project using WXT might load their own scripts.\n\nWhile they are bundled and shipped inside WXT, Vite considers them a part of your project's source code, not WXT's. This means they cannot import 3rd party modules directly, otherwise `pnpm i --shamefully-hoist=false` and Yarn PnP will fail.\n\nFor this reason, the virtual entrypoints get their own TS project to isolate them from the rest of the project. They can only import from `wxt/*` or utils that don't have any imports from node_modules, like the logger.\n\nWhen bundling WXT for publishing to NPM, all the `wxt/*` imports are marked as external and resolved when building your application. Other imports are added inline.\n\nSee <https://github.com/wxt-dev/wxt/issues/286#issuecomment-1858888390> for more details.\n"
  },
  {
    "path": "packages/wxt/src/virtual/background-entrypoint.ts",
    "content": "import definition from 'virtual:user-background-entrypoint';\nimport { initPlugins } from 'virtual:wxt-plugins';\nimport { getDevServerWebSocket } from '../utils/internal/dev-server-websocket';\nimport { logger } from '../utils/internal/logger';\nimport { browser } from 'wxt/browser';\nimport { keepServiceWorkerAlive } from './utils/keep-service-worker-alive';\nimport { reloadContentScript } from './utils/reload-content-scripts';\n\nif (import.meta.env.COMMAND === 'serve') {\n  try {\n    const ws = getDevServerWebSocket();\n    ws.addWxtEventListener('wxt:reload-extension', () => {\n      browser.runtime.reload();\n    });\n    ws.addWxtEventListener('wxt:reload-content-script', (event) => {\n      reloadContentScript(event.detail);\n    });\n\n    if (import.meta.env.MANIFEST_VERSION === 3) {\n      // Tell the server the background script is loaded and ready to go\n      ws.addEventListener('open', () =>\n        ws.sendCustom('wxt:background-initialized'),\n      );\n\n      // Web Socket will disconnect if the service worker is killed\n      keepServiceWorkerAlive();\n    }\n  } catch (err) {\n    logger.error('Failed to setup web socket connection with dev server', err);\n  }\n\n  browser.commands.onCommand.addListener((command) => {\n    if (command === 'wxt:reload-extension') {\n      browser.runtime.reload();\n    }\n  });\n}\n\nlet result;\n\ntry {\n  initPlugins();\n  result = definition.main();\n  // @ts-expect-error: Res shouldn't be a promise, but we're checking it anyways\n  if (result instanceof Promise) {\n    console.warn(\n      \"The background's main() function return a promise, but it must be synchronous\",\n    );\n  }\n} catch (err) {\n  logger.error('The background crashed on startup!');\n  throw err;\n}\n\n// Return the main function's result to the background when executed via the\n// scripting API. Default export causes the IIFE to return a value.\n// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value\n// Tested on both Chrome and Firefox\nexport default result;\n"
  },
  {
    "path": "packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts",
    "content": "import definition from 'virtual:user-content-script-isolated-world-entrypoint';\nimport { logger } from '../utils/internal/logger';\nimport { ContentScriptContext } from 'wxt/utils/content-script-context';\nimport { initPlugins } from 'virtual:wxt-plugins';\n\nconst result = (async () => {\n  try {\n    initPlugins();\n    const { main, ...options } = definition;\n    const ctx = new ContentScriptContext(import.meta.env.ENTRYPOINT, options);\n\n    return await main(ctx);\n  } catch (err) {\n    logger.error(\n      `The content script \"${import.meta.env.ENTRYPOINT}\" crashed on startup!`,\n      err,\n    );\n    throw err;\n  }\n})();\n\n// Return the main function's result to the background when executed via the\n// scripting API. Default export causes the IIFE to return a value.\n// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value\n// Tested on both Chrome and Firefox\nexport default result;\n"
  },
  {
    "path": "packages/wxt/src/virtual/content-script-main-world-entrypoint.ts",
    "content": "import definition from 'virtual:user-content-script-main-world-entrypoint';\nimport { logger } from '../utils/internal/logger';\nimport { initPlugins } from 'virtual:wxt-plugins';\n\nconst result = (async () => {\n  try {\n    initPlugins();\n    return await definition.main();\n  } catch (err) {\n    logger.error(\n      `The content script \"${import.meta.env.ENTRYPOINT}\" crashed on startup!`,\n      err,\n    );\n    throw err;\n  }\n})();\n\n// Return the main function's result to the background when executed via the\n// scripting API. Default export causes the IIFE to return a value.\n// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value\n// Tested on both Chrome and Firefox\nexport default result;\n"
  },
  {
    "path": "packages/wxt/src/virtual/mock-browser.ts",
    "content": "import { fakeBrowser } from 'wxt/testing';\n\nexport const browser = fakeBrowser;\nexport default fakeBrowser;\n"
  },
  {
    "path": "packages/wxt/src/virtual/reload-html.ts",
    "content": "import { logger } from '../utils/internal/logger';\nimport { getDevServerWebSocket } from '../utils/internal/dev-server-websocket';\n\nif (import.meta.env.COMMAND === 'serve') {\n  try {\n    const ws = getDevServerWebSocket();\n    ws.addWxtEventListener('wxt:reload-page', (event) => {\n      // \"popup.html\" === \"/popup.html\".substring(1)\n      if (event.detail === location.pathname.substring(1)) location.reload();\n    });\n  } catch (err) {\n    logger.error('Failed to setup web socket connection with dev server', err);\n  }\n}\n"
  },
  {
    "path": "packages/wxt/src/virtual/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vite/client\", \"../@types/globals.d.ts\"]\n  },\n  \"include\": [\"./*\"]\n}\n"
  },
  {
    "path": "packages/wxt/src/virtual/unlisted-script-entrypoint.ts",
    "content": "import definition from 'virtual:user-unlisted-script-entrypoint';\nimport { logger } from '../utils/internal/logger';\nimport { initPlugins } from 'virtual:wxt-plugins';\n\nconst result = (() => {\n  try {\n    initPlugins();\n  } catch (err) {\n    logger.error(\n      `Failed to initialize plugins for \"${import.meta.env.ENTRYPOINT}\"`,\n      err,\n    );\n    throw err;\n  }\n  let result;\n  try {\n    result = definition.main();\n\n    if (result instanceof Promise) {\n      result = (result as Promise<any>).catch((err) => {\n        logger.error(\n          `The unlisted script \"${import.meta.env.ENTRYPOINT}\" crashed on startup!`,\n          err,\n        );\n        throw err;\n      });\n    }\n  } catch (err) {\n    logger.error(\n      `The unlisted script \"${import.meta.env.ENTRYPOINT}\" crashed on startup!`,\n      err,\n    );\n    throw err;\n  }\n  return result;\n})();\n\n// Return the main function's result to the background when executed via the\n// scripting API. Default export causes the IIFE to return a value.\n// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript#return_value\n// Tested on both Chrome and Firefox\nexport default result;\n"
  },
  {
    "path": "packages/wxt/src/virtual/utils/keep-service-worker-alive.ts",
    "content": "import { browser } from 'wxt/browser';\n\n/** https://developer.chrome.com/blog/longer-esw-lifetimes/ */\nexport function keepServiceWorkerAlive() {\n  setInterval(async () => {\n    // Calling an async browser API resets the service worker's timeout\n    await browser.runtime.getPlatformInfo();\n  }, 5e3);\n}\n"
  },
  {
    "path": "packages/wxt/src/virtual/utils/reload-content-scripts.ts",
    "content": "import { browser } from 'wxt/browser';\nimport { logger } from '../../utils/internal/logger';\nimport { MatchPattern } from 'wxt/utils/match-patterns';\nimport type { ReloadContentScriptPayload } from '../../utils/internal/dev-server-websocket';\n\nexport function reloadContentScript(payload: ReloadContentScriptPayload) {\n  const manifest = browser.runtime.getManifest();\n  if (manifest.manifest_version == 2) {\n    void reloadContentScriptMv2(payload);\n  } else {\n    void reloadContentScriptMv3(payload);\n  }\n}\n\nexport async function reloadContentScriptMv3({\n  registration,\n  contentScript,\n}: ReloadContentScriptPayload) {\n  if (registration === 'runtime') {\n    await reloadRuntimeContentScriptMv3(contentScript);\n  } else {\n    await reloadManifestContentScriptMv3(contentScript);\n  }\n}\n\ntype ContentScript = ReloadContentScriptPayload['contentScript'];\n\nexport async function reloadManifestContentScriptMv3(\n  contentScript: ContentScript,\n) {\n  const id = `wxt:${contentScript.js![0]}`;\n  logger.log('Reloading content script:', contentScript);\n  const registered = await browser.scripting.getRegisteredContentScripts();\n  logger.debug('Existing scripts:', registered);\n\n  const existing = registered.find((cs) => cs.id === id);\n\n  if (existing) {\n    logger.debug('Updating content script', existing);\n    await browser.scripting.updateContentScripts([\n      {\n        ...contentScript,\n        id,\n        css: contentScript.css ?? [],\n      },\n    ]);\n  } else {\n    logger.debug('Registering new content script...');\n    await browser.scripting.registerContentScripts([\n      {\n        ...contentScript,\n        id,\n        css: contentScript.css ?? [],\n      },\n    ]);\n  }\n\n  await reloadTabsForContentScript(contentScript);\n}\n\nexport async function reloadRuntimeContentScriptMv3(\n  contentScript: ContentScript,\n) {\n  logger.log('Reloading content script:', contentScript);\n  const registered = await browser.scripting.getRegisteredContentScripts();\n  logger.debug('Existing scripts:', registered);\n\n  const matches = registered.filter((cs) => {\n    const hasJs = contentScript.js?.find((js) => cs.js?.includes(js));\n    const hasCss = contentScript.css?.find((css) => cs.css?.includes(css));\n    return hasJs || hasCss;\n  });\n\n  if (matches.length === 0) {\n    logger.log(\n      'Content script is not registered yet, nothing to reload',\n      contentScript,\n    );\n    return;\n  }\n\n  await browser.scripting.updateContentScripts(matches);\n  await reloadTabsForContentScript(contentScript);\n}\n\nasync function reloadTabsForContentScript(contentScript: ContentScript) {\n  const allTabs = await browser.tabs.query({});\n  const matchPatterns = contentScript.matches.map(\n    (match) => new MatchPattern(match),\n  );\n  const matchingTabs = allTabs.filter((tab) => {\n    const url = tab.url;\n    if (!url) return false;\n    return !!matchPatterns.find((pattern) => pattern.includes(url));\n  });\n  await Promise.all(\n    matchingTabs.map(async (tab) => {\n      try {\n        await browser.tabs.reload(tab.id!);\n      } catch (err) {\n        logger.warn('Failed to reload tab:', err);\n      }\n    }),\n  );\n}\n\nexport async function reloadContentScriptMv2(\n  _payload: ReloadContentScriptPayload,\n) {\n  throw Error('TODO: reloadContentScriptMv2');\n}\n"
  },
  {
    "path": "packages/wxt/src/virtual/virtual-module-globals.d.ts",
    "content": "// Types required to make the virtual modules happy.\n\ndeclare module 'virtual:user-background-entrypoint' {\n  const definition: import('wxt').BackgroundDefinition;\n  export default definition;\n}\n\ndeclare module 'virtual:user-content-script-isolated-world-entrypoint' {\n  const definition: import('wxt').IsolatedWorldContentScriptDefinition;\n  export default definition;\n}\n\ndeclare module 'virtual:user-content-script-main-world-entrypoint' {\n  const definition: import('wxt').MainWorldContentScriptDefinition;\n  export default definition;\n}\n\ndeclare module 'virtual:user-unlisted-script-entrypoint' {\n  const definition: import('wxt').UnlistedScriptDefinition;\n  export default definition;\n}\n\ndeclare module 'wxt/browser' {\n  export { browser, Browser } from '@wxt-dev/browser';\n}\n\ndeclare module 'virtual:wxt-plugins' {\n  export function initPlugins(): void;\n}\n"
  },
  {
    "path": "packages/wxt/src/vite-builder-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "packages/wxt/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest-plugin-random-seed/types\"]\n  },\n  \"exclude\": [\"node_modules\", \"src/virtual\", \"e2e/dist\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/wxt/tsdown.config.ts",
    "content": "import { defineConfig, UserConfig } from 'tsdown';\nimport pkgJson from './package.json' with { type: 'json' };\nimport { readFile, writeFile } from 'node:fs/promises';\nimport {\n  virtualEntrypointModuleNames,\n  virtualModuleNames,\n} from './src/core/utils/virtual-modules';\n\nexport default defineConfig([\n  // Non-virtual modules can be transpiled in-place to make debugging in node_modules easier\n  {\n    entry: [\n      // Exports\n      ...Object.values(pkgJson.exports)\n        .filter((ex: any) => ex.default)\n        .map((ex: any) =>\n          ex.default.replace('./dist', 'src').replace('.mjs', '.ts'),\n        ),\n\n      // CLI\n      'src/cli/index.ts',\n    ],\n    unbundle: true,\n    external: ['wxt/browser', 'virtual:app-config'],\n    copy: [\n      // If tsdown bundles this file, it removes the triple-slash reference, so\n      // we need to copy it into the out dir manually instead of building it.\n      'src/vite-builder-env.d.ts',\n    ],\n    onSuccess: async () => {\n      // Don't rely on importing the package.json file at runtime, hardcode the\n      // version to avoid issues with different runtimes handling JSON imports\n      // differently.\n      await replaceVars('dist/version.mjs', { version: pkgJson.version });\n    },\n  },\n\n  // Virtual modules must be bundled individually\n  ...virtualModuleNames.map(\n    (moduleName): UserConfig => ({\n      entry: `src/virtual/${moduleName}.ts`,\n      outDir: 'dist/virtual',\n      external: [\n        ...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),\n        'virtual:wxt-plugins',\n        'virtual:app-config',\n        ...Object.keys(pkgJson.exports).map((path) => 'wxt' + path.slice(1)), // ./utils/storage => wxt/utils/storage\n      ],\n    }),\n  ),\n]);\n\nasync function replaceVars(\n  file: string,\n  vars: Record<string, string>,\n): Promise<void> {\n  let text = await readFile(file, 'utf8');\n  Object.entries(vars).forEach(([name, value]) => {\n    text = text.replaceAll(`{{${name}}}`, value);\n  });\n  await writeFile(file, text, 'utf8');\n}\n"
  },
  {
    "path": "packages/wxt/typedoc.json",
    "content": "{\n  \"entryPoints\": [\n    \"src/index.ts\",\n    \"src/utils/app-config.ts\",\n    \"src/utils/inject-script.ts\",\n    \"src/utils/content-script-context.ts\",\n    \"src/utils/content-script-ui/types.ts\",\n    \"src/utils/content-script-ui/integrated.ts\",\n    \"src/utils/content-script-ui/shadow-root.ts\",\n    \"src/utils/content-script-ui/iframe.ts\",\n    \"src/utils/define-app-config.ts\",\n    \"src/utils/define-background.ts\",\n    \"src/utils/define-content-script.ts\",\n    \"src/utils/define-unlisted-script.ts\",\n    \"src/utils/define-wxt-plugin.ts\",\n    \"src/utils/match-patterns.ts\",\n    \"src/utils/split-shadow-root-css.ts\",\n    \"src/utils/storage.ts\",\n    \"src/testing/index.ts\",\n    \"src/testing/fake-browser.ts\",\n    \"src/testing/wxt-vitest-plugin.ts\",\n    \"src/modules.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/wxt/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\nimport path from 'node:path';\nimport RandomSeed from 'vitest-plugin-random-seed';\n\nexport default defineConfig({\n  test: {\n    mockReset: true,\n    restoreMocks: true,\n    testTimeout: 120e3,\n    coverage: {\n      include: ['src/**'],\n      exclude: ['**/dist', '**/__tests__', 'src/utils/testing'],\n    },\n    setupFiles: ['./vitest.setup.ts'],\n    globalSetup: ['./vitest.globalSetup.ts'],\n  },\n  server: {\n    watch: {\n      ignored: '**/dist/**',\n    },\n  },\n  plugins: [RandomSeed()],\n  resolve: {\n    alias: {\n      'wxt/testing': path.resolve('src/testing'),\n    },\n  },\n});\n"
  },
  {
    "path": "packages/wxt/vitest.globalSetup.ts",
    "content": "import { access, rm } from 'node:fs/promises';\n\nlet setupHappened = false;\n\nexport async function setup() {\n  if (setupHappened) {\n    throw new Error('setup called twice');\n  }\n\n  setupHappened = true;\n\n  globalThis.__ENTRYPOINT__ = 'test';\n\n  const e2eDistPath = './e2e/dist/';\n  try {\n    await access(e2eDistPath);\n    await rm(e2eDistPath, { recursive: true, force: true });\n  } catch {\n    // Directory doesn't exist, nothing to clean up\n  }\n}\n"
  },
  {
    "path": "packages/wxt/vitest.setup.ts",
    "content": "import { fakeBrowser } from '@webext-core/fake-browser';\nimport { vi } from 'vitest';\n\nvi.stubGlobal('chrome', fakeBrowser);\nvi.stubGlobal('browser', fakeBrowser);\n"
  },
  {
    "path": "packages/wxt-demo/eslint.config.js",
    "content": "import autoImports from './.wxt/eslintrc-auto-import.js';\n\nexport default [\n  {\n    languageOptions: {\n      globals: {\n        ...autoImports.globals,\n      },\n      sourceType: 'module',\n    },\n  },\n];\n"
  },
  {
    "path": "packages/wxt-demo/modules/auto-icons.ts",
    "content": "import autoIcons from '@wxt-dev/auto-icons';\n\nexport default autoIcons;\n"
  },
  {
    "path": "packages/wxt-demo/modules/example.ts",
    "content": "import { defineWxtModule } from 'wxt/modules';\n\n// Example of adding option types to wxt config file\nexport interface ExampleModuleOptions {\n  a: string;\n  b?: string;\n}\ndeclare module 'wxt' {\n  interface InlineConfig {\n    example?: ExampleModuleOptions;\n  }\n}\n\nexport default defineWxtModule<ExampleModuleOptions>({\n  configKey: 'example',\n  setup(wxt, options) {\n    wxt.logger.info('Example module with options:', options);\n  },\n});\n"
  },
  {
    "path": "packages/wxt-demo/modules/i18n.ts",
    "content": "import module from '@wxt-dev/i18n/module';\n\nexport default module;\n"
  },
  {
    "path": "packages/wxt-demo/modules/unocss.ts",
    "content": "export { default } from '@wxt-dev/unocss';\n"
  },
  {
    "path": "packages/wxt-demo/package.json",
    "content": "{\n  \"name\": \"wxt-demo\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"buildc --deps-only -- wxt\",\n    \"build\": \"buildc --deps-only -- wxt build\",\n    \"build:all\": \"buildc --deps-only -- pnpm run --reporter-hide-prefix /^build:all:.*/\",\n    \"build:all:chrome-mv3\": \"wxt build\",\n    \"build:all:chrome-mv2\": \"wxt build --mv2\",\n    \"build:all:firefox-mv3\": \"wxt build -b firefox --mv3\",\n    \"build:all:firefox-mv2\": \"wxt build -b firefox\",\n    \"test\": \"buildc --deps-only -- vitest\",\n    \"zip\": \"buildc --deps-only -- wxt zip\",\n    \"check\": \"buildc --deps-only -- check\",\n    \"postinstall\": \"buildc --deps-only -- wxt prepare\"\n  },\n  \"dependencies\": {\n    \"@wxt-dev/i18n\": \"workspace:*\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@wxt-dev/auto-icons\": \"workspace:*\",\n    \"@wxt-dev/unocss\": \"workspace:*\",\n    \"sass\": \"^1.97.3\",\n    \"typescript\": \"^5.9.3\",\n    \"unocss\": \"^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0\",\n    \"vitest\": \"^4.0.18\",\n    \"vitest-plugin-random-seed\": \"^1.1.2\",\n    \"wxt\": \"workspace:*\"\n  },\n  \"buildc\": {\n    \"cachable\": false\n  }\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/app.config.ts",
    "content": "import { defineAppConfig } from '#imports';\n\ndeclare module 'wxt/utils/define-app-config' {\n  export interface WxtAppConfig {\n    example: string;\n  }\n}\n\nexport default defineAppConfig({\n  example: 'value',\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/__tests__/background.test.ts",
    "content": "import { beforeEach, describe, expect, it, vi } from 'vitest';\nimport background from '../background';\n\nbrowser.i18n.getMessage = () => 'fake-message';\n\nconst logMock = vi.fn();\nconsole.log = logMock;\n\ndescribe('Background Entrypoint', () => {\n  beforeEach(() => {\n    fakeBrowser.reset();\n  });\n\n  it(\"should log the extension's runtime ID\", () => {\n    const id = 'some-id';\n    fakeBrowser.runtime.id = id;\n\n    background.main();\n\n    expect(logMock).toBeCalledWith(id);\n  });\n\n  it('should set the start time in storage', async () => {\n    background.main();\n    await new Promise((res) => setTimeout(res));\n\n    expect(await storage.getItem('session:startTime')).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/automount.content/index.ts",
    "content": "import 'uno.css';\nimport './style.css';\n\nexport default defineContentScript({\n  matches: ['https://*.duckduckgo.com/*'],\n  cssInjectionMode: 'manifest',\n\n  async main(ctx) {\n    const dynamicUI = createIntegratedUi(ctx, {\n      position: 'inline',\n      append: 'after',\n      anchor: 'form[role=search]',\n      onMount: (container) => {\n        const app = document.createElement('div');\n        container.id = 'automount-anchor';\n        app.classList.add('m-4', 'text-center', 'text-red-500');\n        app.textContent = i18n.t('prompt_for_name');\n        container.append(app);\n        return { container, app };\n      },\n      onRemove() {\n        console.log('dynamicUI removed');\n      },\n    });\n\n    const autoMountUi = createIntegratedUi(ctx, {\n      position: 'inline',\n      append: 'after',\n      anchor: '#automount-anchor',\n      onMount: (container) => {\n        const app = document.createElement('div');\n        app.id = 'automount-ui';\n        app.classList.add('m-0', 'text-center', 'text-blue-500');\n        app.textContent = `Hello, I'm automount UI.`;\n        container.append(app);\n      },\n      onRemove() {\n        console.log('autoMountUi removed');\n      },\n    });\n\n    autoMountUi.autoMount({\n      onStop: () => {\n        console.log('Auto mount stopped.');\n      },\n    });\n\n    const stopAutoMountButton = createIntegratedUi(ctx, {\n      position: 'inline',\n      append: 'last',\n      anchor: 'form[role=search]',\n      onMount: (container) => {\n        const app = document.createElement('button');\n        container.classList.add('flex', 'flex-justify-center');\n        app.classList.add('mt-4', 'p-2');\n        app.textContent = 'Stop auto-mount';\n        app.onclick = (e) => {\n          e.preventDefault();\n          autoMountUi.remove();\n        };\n        container.append(app);\n        return { container, app };\n      },\n    });\n\n    stopAutoMountButton.mount();\n\n    setInterval(() => {\n      if (dynamicUI.mounted) {\n        dynamicUI.remove();\n      } else {\n        dynamicUI.mount();\n      }\n    }, 2000);\n  },\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/automount.content/style.css",
    "content": ":root {\n  color-scheme: dark;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/background.ts",
    "content": "export default defineBackground({\n  // type: 'module',\n\n  main() {\n    console.log(browser.runtime.id);\n    logId();\n    console.log({\n      url: import.meta.url,\n      browser: import.meta.env.BROWSER,\n      chrome: import.meta.env.CHROME,\n      firefox: import.meta.env.FIREFOX,\n      manifestVersion: import.meta.env.MANIFEST_VERSION,\n    });\n\n    console.log(getAppConfig());\n\n    browser.runtime.getURL('/');\n    browser.runtime.getURL('/background.js');\n    browser.runtime.getURL('/icons/128.png');\n    browser.runtime.getURL('/example.html#hash');\n    browser.runtime.getURL('/example.html?query=param');\n    // @ts-expect-error: should only accept entrypoints or public assets\n    browser.runtime.getURL('/unknown');\n    // @ts-expect-error: should only allow hashes/query params on HTML files\n    browser.runtime.getURL('/icon-128.png?query=param');\n\n    // @ts-expect-error: should only accept known message names\n    i18n.t('test');\n    i18n.t('prompt_for_name');\n    i18n.t('hello', ['test']);\n    i18n.t('bye', ['Aaron']);\n    i18n.t('@@extension_id');\n    i18n.t('deep.example');\n    i18n.t('items', 0);\n    i18n.t('items', 0, ['one']);\n\n    console.log('WXT MODE:', {\n      MODE: import.meta.env.MODE,\n      DEV: import.meta.env.DEV,\n      PROD: import.meta.env.PROD,\n    });\n\n    storage.setItem('session:startTime', Date.now());\n  },\n});\n\nfunction _otherTypeChecksNotEvaluated() {\n  browser.scripting.executeScript({\n    target: { tabId: 1 },\n    files: [\n      '/background.js',\n      // @ts-expect-error: Should error for non-existing paths\n      '/other.js',\n    ],\n  });\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/example-2.scss",
    "content": "body {\n  background-color: red;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/example-tsx.content.tsx",
    "content": "import ReactDOM from 'react-dom/client';\n\nexport default defineContentScript({\n  matches: ['<all_urls>'],\n  async main(ctx) {\n    console.log(browser.runtime.id);\n    logId();\n\n    console.log('WXT MODE:', {\n      MODE: import.meta.env.MODE,\n      DEV: import.meta.env.DEV,\n      PROD: import.meta.env.PROD,\n    });\n\n    const n = (Math.random() * 100).toFixed(1);\n    ctx.setInterval(() => {\n      console.log(n, browser.runtime.id);\n    }, 1e3);\n\n    const container = document.createElement('div');\n    document.body.append(container);\n\n    ReactDOM.createRoot(container).render(<SomeComponent />);\n  },\n});\n\nfunction SomeComponent() {\n  return <div>Some component</div>;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/example.sandbox/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n    <script type=\"module\" src=\"../../utils/logger.ts\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/iframe-src/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Iframe Example</title>\n  </head>\n  <body>\n    <p>Hello iframe page!</p>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/iframe-src/main.ts",
    "content": "console.log('iframe 2');\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/iframe.content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*.google.com/*'],\n\n  main(ctx) {\n    const ui = createIframeUi(ctx, {\n      page: '/iframe-src.html',\n      position: 'overlay',\n      anchor: 'form[action=\"/search\"]',\n    });\n    ui.mount();\n    console.log('Mounted iframe');\n  },\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/injected.content/index.css",
    "content": "body {\n  color: blue;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/location-change.content.ts",
    "content": "export default defineContentScript({\n  // Site that uses HTML5 history\n  matches: ['*://*.crunchyroll.com/*'],\n\n  main(ctx) {\n    ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl, oldUrl }) => {\n      console.log('Location changed:', newUrl.href, oldUrl.href);\n    });\n    console.log('Watching for location change...');\n  },\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/main-world.content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*/*'],\n  world: 'MAIN',\n\n  main() {\n    console.log(`Hello from ${location.hostname}!`);\n  },\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/options/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Extension Settings</title>\n    <link rel=\"stylesheet\" href=\"./style.css\" />\n  </head>\n  <body>\n    <p>Hello options page!</p>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/options/main.ts",
    "content": "import 'url:https://code.jquery.com/jquery-3.7.1.slim.min.js';\n\nconsole.log(browser.runtime.id);\nlogId();\nconsole.log(2);\n\nconsole.log('WXT MODE:', {\n  MODE: import.meta.env.MODE,\n  DEV: import.meta.env.DEV,\n  PROD: import.meta.env.PROD,\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/options/style.css",
    "content": "html {\n  background: black;\n  color: white;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/popup.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Popup</title>\n    <link rel=\"stylesheet\" href=\"uno.css\" />\n  </head>\n  <body>\n    <p>Hello popup!</p>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/sandbox.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n    <script type=\"module\" src=\"../utils/logger.ts\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/sidepanel.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Sidebar</title>\n  </head>\n  <body>\n    <p>Example</p>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/ui.content/index.ts",
    "content": "import 'uno.css';\nimport './style.css';\nimport manualStyle from './manual-style.css?inline';\n\nexport default defineContentScript({\n  matches: ['https://*.duckduckgo.com/*'],\n  cssInjectionMode: 'ui',\n\n  async main(ctx) {\n    const style = document.createElement('style');\n    style.textContent = manualStyle;\n    document.head.append(style);\n\n    const ui = await createShadowRootUi(ctx, {\n      name: 'demo-ui',\n      position: 'inline',\n      append: 'before',\n      anchor: 'form[role=search]',\n      onMount: (container) => {\n        const app = document.createElement('div');\n        app.classList.add('m-4', 'text-red-500');\n        app.textContent = i18n.t('prompt_for_name');\n        container.append(app);\n      },\n    });\n    ui.mount();\n\n    setTimeout(ui.remove, 5000);\n  },\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/ui.content/manual-style.css",
    "content": "body {\n  padding: 2rem;\n  background-color: blanchedalmond;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/ui.content/style.css",
    "content": ":root {\n  color-scheme: dark;\n}\nhtml {\n  background-color: black;\n}\n"
  },
  {
    "path": "packages/wxt-demo/src/entrypoints/unlisted.ts",
    "content": "export default defineUnlistedScript(() => {\n  console.log('injected');\n});\n"
  },
  {
    "path": "packages/wxt-demo/src/locales/en.yml",
    "content": "prompt_for_name:\n  message: What's your name?\n  description: Ask for the user's name\nhello:\n  message: Hello, $USER$\n  description: Greet the user\n  placeholders:\n    user:\n      content: $1\n      example: Cira\nbye:\n  message: Goodbye, $USER$. Come back to $OUR_SITE$ soon!\n  description: Say goodbye to the user\n  placeholders:\n    our_site:\n      content: Example.com\n    user:\n      content: $1\n      example: Cira\ndeep:\n  example: 'this is deep'\nitems:\n  0: Zero items\n  1: 1 item\n  n: $1 items\n"
  },
  {
    "path": "packages/wxt-demo/src/utils/logger.ts",
    "content": "export default console;\n\nexport function logId() {\n  console.log('logId', browser.runtime.id);\n}\n"
  },
  {
    "path": "packages/wxt-demo/tsconfig.json",
    "content": "{\n  \"extends\": [\"../../tsconfig.base.json\", \"./.wxt/tsconfig.json\"],\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"jsx\": \"react-jsx\",\n    \"types\": [\"vitest-plugin-random-seed/types\"]\n  }\n}\n"
  },
  {
    "path": "packages/wxt-demo/vitest.config.ts",
    "content": "import { defineProject } from 'vitest/config';\nimport { WxtVitest } from 'wxt/testing';\n\nexport default defineProject({\n  test: {\n    mockReset: true,\n    restoreMocks: true,\n  },\n  plugins: [WxtVitest()],\n});\n"
  },
  {
    "path": "packages/wxt-demo/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\nimport { presetWind3 } from 'unocss';\n\nexport default defineConfig({\n  srcDir: 'src',\n  targetBrowsers: ['chrome', 'firefox', 'safari'],\n  manifest: {\n    permissions: ['storage'],\n    default_locale: 'en',\n    web_accessible_resources: [\n      {\n        resources: ['/iframe-src.html'],\n        matches: ['*://*.google.com/*'],\n      },\n    ],\n  },\n  zip: {\n    downloadPackages: ['sass'],\n  },\n  analysis: {\n    open: true,\n  },\n  webExt: {\n    startUrls: ['https://duckduckgo.com'],\n  },\n  example: {\n    a: 'a',\n    // @ts-expect-error: c is not defined, this should be a type error, but it should show up in the module\n    c: 'c',\n  },\n  unocss: {\n    excludeEntrypoints: [\n      'example',\n      'iframe-src',\n      'injected',\n      'example-tsx',\n      'example-2',\n      'iframe',\n      'location-change',\n      'main-world',\n      'sandbox',\n      'sidepanel',\n      'unlisted',\n    ],\n    configOrPath: {\n      content: {\n        pipeline: {\n          include: [\n            // the default\n            /\\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\\?)/,\n            // include js/ts files\n            'src/entrypoints/**/*.{js,ts}',\n          ],\n        },\n      },\n      presets: [presetWind3()],\n    },\n  },\n});\n"
  },
  {
    "path": "patches/markdown-it-footnote.md",
    "content": "Removed sub-ids from rendered links. When you link to the same footnote multiple times, the link would look like `[3.2]` instead of just `[3]`. Didn't like how that looked, so this patch removes that function.\n"
  },
  {
    "path": "patches/markdown-it-footnote.patch",
    "content": "diff --git a/dist/index.cjs.js b/dist/index.cjs.js\nindex 806448c967261a61288f0faa0633a91f77d35968..6812dd84bbbb7176af4115a287677454fa562883 100644\n--- a/dist/index.cjs.js\n+++ b/dist/index.cjs.js\n@@ -13,14 +13,14 @@ function render_footnote_anchor_name(tokens, idx, options, env /*, slf */) {\n }\n function render_footnote_caption(tokens, idx /*, options, env, slf */) {\n   let n = Number(tokens[idx].meta.id + 1).toString();\n-  if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;\n+  // if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;\n   return `[${n}]`;\n }\n function render_footnote_ref(tokens, idx, options, env, slf) {\n   const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);\n   const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);\n   let refid = id;\n-  if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;\n+  // if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;\n   return `<sup class=\"footnote-ref\"><a href=\"#fn${id}\" id=\"fnref${refid}\">${caption}</a></sup>`;\n }\n function render_footnote_block_open(tokens, idx, options) {\n@@ -31,7 +31,7 @@ function render_footnote_block_close() {\n }\n function render_footnote_open(tokens, idx, options, env, slf) {\n   let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);\n-  if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;\n+  // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;\n   return `<li id=\"fn${id}\" class=\"footnote-item\">`;\n }\n function render_footnote_close() {\n@@ -39,7 +39,7 @@ function render_footnote_close() {\n }\n function render_footnote_anchor(tokens, idx, options, env, slf) {\n   let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);\n-  if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;\n+  // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;\n \n   /* ↩ with escape code to prevent display as Apple Emoji on iOS */\n   return ` <a href=\"#fnref${id}\" class=\"footnote-backref\">\\u21a9\\uFE0E</a>`;\ndiff --git a/index.mjs b/index.mjs\nindex 48277ca67206f248b9deb0058e9a7d69dbc07702..718e3e527b2513e4f6f59cba9d4526a36d58f6bc 100644\n--- a/index.mjs\n+++ b/index.mjs\n@@ -17,7 +17,7 @@ function render_footnote_anchor_name (tokens, idx, options, env/*, slf */) {\n function render_footnote_caption (tokens, idx/*, options, env, slf */) {\n   let n = Number(tokens[idx].meta.id + 1).toString()\n \n-  if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`\n+  // if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`\n \n   return `[${n}]`\n }\n@@ -27,7 +27,7 @@ function render_footnote_ref (tokens, idx, options, env, slf) {\n   const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf)\n   let refid = id\n \n-  if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`\n+  // if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`\n \n   return `<sup class=\"footnote-ref\"><a href=\"#fn${id}\" id=\"fnref${refid}\">${caption}</a></sup>`\n }\n@@ -45,7 +45,7 @@ function render_footnote_block_close () {\n function render_footnote_open (tokens, idx, options, env, slf) {\n   let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)\n \n-  if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`\n+  // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`\n \n   return `<li id=\"fn${id}\" class=\"footnote-item\">`\n }\n@@ -57,7 +57,7 @@ function render_footnote_close () {\n function render_footnote_anchor (tokens, idx, options, env, slf) {\n   let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)\n \n-  if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`\n+  // if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`\n \n   /* ↩ with escape code to prevent display as Apple Emoji on iOS */\n   return ` <a href=\"#fnref${id}\" class=\"footnote-backref\">\\u21a9\\uFE0E</a>`\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - docs\n  - packages/*\nonlyBuiltDependencies:\n  - '@parcel/watcher'\n  - dtrace-provider\n  - esbuild\n  - sharp\n  - simple-git-hooks\n  - spawn-sync\nautoInstallPeers: false\npatchedDependencies:\n  markdown-it-footnote: patches/markdown-it-footnote.patch\npeerDependencyRules:\n  ignoreMissing:\n    - '@algolia/client-search'\n    - search-insights\n    - svelte\n"
  },
  {
    "path": "scripts/benchmarks/browser-startup.patch",
    "content": "diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts\nindex 09819c3..a0f0df2 100644\n--- a/packages/wxt/src/core/runners/web-ext.ts\n+++ b/packages/wxt/src/core/runners/web-ext.ts\n@@ -3,6 +3,8 @@ import { ExtensionRunner } from '../../types';\n import { formatDuration } from '../utils/time';\n import defu from 'defu';\n import { wxt } from '../wxt';\n+import fs from 'node:fs';\n+import stream from 'node:stream/promises';\n \n /**\n  * Create an `ExtensionRunner` backed by `web-ext`.\n@@ -78,6 +80,12 @@ export function createWebExtRunner(): ExtensionRunner {\n \n       const duration = Date.now() - startTime;\n       wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);\n+      await runner.exit();\n+      const s = fs.createWriteStream('stats.txt', { flags: 'a' });\n+      s.write(duration + ' ');\n+      s.end();\n+      await stream.finished(s);\n+      process.exit(0);\n     },\n \n     async closeBrowser() {\n"
  },
  {
    "path": "scripts/benchmarks/browser-startup.sh",
    "content": "#!/bin/bash\nset -e\n\n#\n# Benchmark how long it takes for the browser to open in dev mode.\n#\n# To run:\n#   cd <root>\n#   ./scripts/benchmarks/browser-startup.sh\n#\n# You can set N below to change the number of samples per git ref.\n#\n\nN=20\n\nfunction benchmark_ref() {\n    # Prep\n    git checkout $1\n    pnpm buildc clean\n    git apply scripts/benchmarks/browser-startup.patch\n    pnpm i --ignore-scripts\n    pnpm -r --filter wxt build\n    echo -n \"$1 \" >> stats.txt\n\n    # Run benchmark\n    for i in $(seq $N); do\n        pnpm wxt packages/wxt-demo\n    done\n    git checkout HEAD -- packages/wxt/src/core/runners/web-ext.ts pnpm-lock.yaml\n    echo \"\" >> stats.txt\n}\n\nrm -f stats.txt\n\nbenchmark_ref \"HEAD\"\n\n# Benchmark a commit:\n#   benchmark_ref \"3109bba\"\n#\n# Benchmark a version:\n#   benchmark_ref \"v0.19.0\"\n"
  },
  {
    "path": "scripts/bump-package-version.ts",
    "content": "import {\n  determineSemverChange,\n  generateMarkDown,\n  loadChangelogConfig,\n  parseChangelogMarkdown,\n  parseCommits,\n} from 'changelogen';\nimport { consola } from 'consola';\nimport { readdir, readFile, writeFile } from 'node:fs/promises';\nimport spawn from 'nano-spawn';\nimport path from 'node:path';\nimport { getPkgTag, grabPackageDetails, listCommitsInDir } from './git';\n\nconst pkg = process.argv[2];\nif (!pkg) {\n  throw Error(\n    'Package name missing. Usage: tsx bump-package-version.ts <package-name>',\n  );\n}\nconst { pkgDir, pkgName, currentVersion, prevTag, changelogPath, pkgJsonPath } =\n  await grabPackageDetails(pkg);\nconsola.info('Bumping:', { pkg, pkgDir, pkgName, currentVersion });\n\n// Get commits\nconst config = await loadChangelogConfig(process.cwd());\nconsola.info('Config:', config);\nconst additionalDirs = pkg === 'wxt' ? ['docs'] : [];\nconst rawCommits = await listCommitsInDir(pkgDir, prevTag, additionalDirs);\nconst commits = parseCommits(rawCommits, config);\n\n// Bump version\nconst originalBumpType = determineSemverChange(commits, config) ?? 'patch';\nlet bumpType = originalBumpType;\nif (currentVersion.startsWith('0.')) {\n  if (bumpType === 'major') {\n    bumpType = 'minor';\n  } else if (bumpType === 'minor') {\n    bumpType = 'patch';\n  }\n}\nawait spawn('pnpm', ['version', bumpType], {\n  cwd: pkgDir,\n});\nconst updatedPkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));\nconst newVersion: string = updatedPkgJson.version;\nconst newTag = getPkgTag(pkg, newVersion);\nconsola.info('Bump:', { currentVersion, bumpType, newVersion });\n\n// Generate changelog\nconst versionChangelog = await generateMarkDown(commits, {\n  ...config,\n  from: prevTag,\n  to: newTag,\n});\nlet versionChangelogBody = versionChangelog\n  .split('\\n')\n  .slice(1)\n  .join('\\n')\n  .trim();\nif (originalBumpType === 'major') {\n  versionChangelogBody = versionChangelogBody.replace(\n    '[compare changes]',\n    `[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) &bull; [compare changes]`,\n  );\n}\nconst { releases: prevReleases } = await readFile(changelogPath, 'utf8')\n  .then(parseChangelogMarkdown)\n  .catch(() => ({ releases: [] }));\nconst allReleases = [\n  {\n    version: newVersion,\n    body: versionChangelogBody,\n  },\n  ...prevReleases,\n];\n\nconst newChangelog =\n  '# Changelog\\n\\n' +\n  allReleases\n    .map((release) => [`## v${release.version}`, release.body].join('\\n\\n'))\n    .join('\\n\\n');\nawait writeFile(changelogPath, newChangelog, 'utf8');\nconsola.success('Updated changelog');\n\n// Update WXT version in templates when releasing wxt package\nconst templatePkgJsonPaths: string[] = [];\nif (pkg === 'wxt') {\n  const templatesDir = 'templates';\n  const templateDirs = await readdir(templatesDir);\n  for (const templateDir of templateDirs) {\n    const templatePkgJsonPath = path.join(\n      templatesDir,\n      templateDir,\n      'package.json',\n    );\n    try {\n      const templatePkgJson = JSON.parse(\n        await readFile(templatePkgJsonPath, 'utf-8'),\n      );\n      if (templatePkgJson.devDependencies?.wxt) {\n        templatePkgJson.devDependencies.wxt = `^${newVersion}`;\n        await writeFile(\n          templatePkgJsonPath,\n          JSON.stringify(templatePkgJson, null, 2),\n        );\n        templatePkgJsonPaths.push(templatePkgJsonPath);\n        consola.success(`Updated wxt version in ${templatePkgJsonPath}`);\n      }\n    } catch {}\n  }\n}\n\n// Commit changes\nawait spawn('git', [\n  'add',\n  pkgJsonPath,\n  changelogPath,\n  ...templatePkgJsonPaths,\n]);\nawait spawn('git', [\n  'commit',\n  '-m',\n  `chore(release): ${pkgName} v${newVersion}`,\n]);\nawait spawn('git', ['tag', newTag]);\nconsola.success('Committed version and changelog');\n"
  },
  {
    "path": "scripts/create-github-release.ts",
    "content": "import {\n  createGithubRelease,\n  GithubRelease,\n  loadChangelogConfig,\n  parseChangelogMarkdown,\n} from 'changelogen';\nimport { readFile } from 'node:fs/promises';\nimport { grabPackageDetails } from './git';\nimport consola from 'consola';\n\nconst pkg = process.argv[2];\nif (!pkg) {\n  throw Error(\n    'Package name missing. Usage: tsx create-github-release.ts <package-name>',\n  );\n}\n\nconst { pkgName, prevTag, currentVersion, changelogPath } =\n  await grabPackageDetails(pkg);\nconsola.info('Creating release for:', { pkg, pkgName, prevTag });\n\nconst { releases } = await readFile(changelogPath, 'utf8')\n  .then(parseChangelogMarkdown)\n  .catch(() => ({ releases: [] }));\n\nconst config = await loadChangelogConfig(process.cwd());\nconfig.tokens.github = process.env.GITHUB_TOKEN;\nawait createGithubRelease(config, {\n  tag_name: prevTag,\n  name: `${pkgName} v${currentVersion}`,\n  body: releases[0].body,\n  make_latest: String(pkg === 'wxt'),\n} as GithubRelease & { make_latest: string });\nconsola.success('Created release');\n"
  },
  {
    "path": "scripts/generate-readmes.sh",
    "content": "#!/bin/bash\n\n#\n# Sync wxt/packages/README.md with README.md for NPM\n#\n\necho \"<!-- DO NOT EDIT, THIS FILE WAS GENERATED BY '../../scripts/generate-readmes.sh' -->\" > packages/wxt/README.md\ncat README.md >> packages/wxt/README.md\n"
  },
  {
    "path": "scripts/git.ts",
    "content": "import { RawGitCommit, getGitDiff } from 'changelogen';\nimport { consola } from 'consola';\nimport { readFile } from 'node:fs/promises';\n\nexport async function grabPackageDetails(pkg: string) {\n  const pkgDir = `packages/${pkg}`;\n  const pkgJsonPath = `${pkgDir}/package.json`;\n  const pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));\n  const currentVersion: string = pkgJson.version;\n  return {\n    pkgDir,\n    pkgJsonPath,\n    changelogPath: `${pkgDir}/CHANGELOG.md`,\n    pkgJson,\n    pkgName: pkgJson.name,\n    currentVersion,\n    prevTag: getPkgTag(pkg, currentVersion),\n  };\n}\n\nexport function getPkgTag(pkg: string, version: string | undefined) {\n  return `${pkg}-v${version}`;\n}\n\nexport async function listCommitsInDir(\n  dir: string,\n  lastTag: string,\n  additionalDirs: string[] = [],\n): Promise<RawGitCommit[]> {\n  const allDirs = [dir, ...additionalDirs];\n  consola.info('Listing commits:', { lastTag, dirs: allDirs });\n  const commits = await getGitDiff(lastTag);\n  consola.info('All commits:', commits.length);\n  consola.debug(commits);\n  // commit.body contains all the files that were modified/added. So just check to make sure \"\\t\" + \"packages/storage\" + \"/\" is in the body to include\n  // '\"\\n\\nM\\tpackages/wxt/vitest.config.ts\\n\"'\n  const filtered = commits.filter((commit) =>\n    allDirs.some((dir) => commit.body.includes(`\\t${dir}/`)),\n  );\n  consola.info('Filtered:', filtered.length);\n  consola.debug(filtered);\n  return filtered;\n}\n"
  },
  {
    "path": "scripts/list-unreleased-commits.sh",
    "content": "#!/bin/bash\n\nset -e\n\nPACKAGES_DIR=\"./packages\"\nexport GIT_PAGER=cat\nIGNORED_DIRS=(\"browser\" \"wxt-demo\")\n\nif [ ! -d \"$PACKAGES_DIR\" ]; then\n    echo \"Error: Directory '$PACKAGES_DIR' not found.\"\n    exit 1\nfi\n\necho \"Checking for changes in packages since their last tag...\"\necho \"\"\n\nfor dir in \"$PACKAGES_DIR\"/*; do\n    if [ -d \"$dir\" ]; then\n        pkg_name=$(basename \"$dir\")\n\n        # Check if the package name is in the ignored directories list\n        if [[ \" ${IGNORED_DIRS[*]} \" =~ \" $pkg_name \" ]]; then\n            echo \"----------------------------------------\"\n            echo \"Skipping ignored package: $pkg_name\"\n            continue # Skip to the next directory\n        fi\n\n        echo \"----------------------------------------\"\n        echo \"Package: $pkg_name\"\n\n        # Find the latest tag for the package, e.g., \"my-package-v1.2.3\"\n        # Sorts tags by version and picks the most recent one.\n        last_tag=$(git tag --list \"${pkg_name}-v*\" --sort=-v:refname | head -n 1)\n\n        if [ -n \"$last_tag\" ]; then\n            # If a tag is found, show commits since that tag for the specific package directory\n            echo \"Commits since last tag ($last_tag):\"\n            git log \"${last_tag}..HEAD\" --oneline -- \"$dir\" | grep -v -E \"^[a-f0-9]* (chore|docs|refactor)\" || true\n        else\n            # If no tag is found, show all commits for that package directory\n            echo \"No tags found for this package. Listing all commits:\"\n            git log --oneline -- \"$dir\" | grep -v -E \"^[a-f0-9]* (chore|docs|refactor)\" || true\n        fi\n    fi\ndone\n"
  },
  {
    "path": "scripts/sync-releases.ts",
    "content": "import {\n  getGithubReleaseByTag,\n  loadChangelogConfig,\n  parseChangelogMarkdown,\n  updateGithubRelease,\n} from 'changelogen';\nimport { getPkgTag, grabPackageDetails } from './git';\nimport { readFile } from 'node:fs/promises';\nimport consola from 'consola';\n\nconst pkg = process.argv[2];\nif (!pkg) {\n  throw Error(\n    'Package name missing. Usage: tsx sync-releases.ts <package-name>',\n  );\n}\n\n// Update\nconst { changelogPath, pkgName } = await grabPackageDetails(pkg);\nconst { releases } = await readFile(changelogPath, 'utf8')\n  .then(parseChangelogMarkdown)\n  .catch(() => ({ releases: [] }));\nconst config = await loadChangelogConfig(process.cwd());\nconfig.tokens.github = process.env.GITHUB_TOKEN;\n\n// Update releases\nfor (const release of releases) {\n  const tag = getPkgTag(pkg, release.version);\n  try {\n    const existing = await getGithubReleaseByTag(config, tag);\n    if (existing.body !== release.body) {\n      await updateGithubRelease(config, existing.id!, {\n        tag_name: tag,\n        name: `${pkgName} v${release.version}`,\n        body: release.body,\n      });\n    }\n    consola.success(`Synced \\`${tag}\\``);\n  } catch (err) {\n    consola.fail(`\\`${tag}\\``, err);\n  }\n}\nconsola.success('Done');\n"
  },
  {
    "path": "scripts/upgrade-deps.ts",
    "content": "import consola from 'consola';\nimport spawn from 'nano-spawn';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport pMap from 'p-map';\nimport * as semver from 'semver';\nimport { glob } from 'tinyglobby';\n\nconst HELP_MESSAGE = `\nUpgrades dependencies throughout WXT using custom rules.\n\nUsage:\n  pnpm tsx scripts/upgrade-deps.ts [options]\n\nOptions:\n  --write, -w       Write changes to package.json files\n  --major, -m       Allow major version upgrades\n  --dev-only, -d    Only check/upgrade dev dependencies\n  --prod-only, -p   Only check/upgrade production dependencies\n  --help, -h        Display this help message\n`.slice(1, -1);\n\nconst IGNORED_PACKAGES = [\n  // Very touchy, don't change:\n  'typedoc',\n  'typedoc-plugin-markdown',\n  'typedoc-vitepress-theme',\n  // Manually manage version so a single version is used:\n  'esbuild',\n  // Maintained manually to match min-node version\n  '@types/node',\n  // License changed in newer versions\n  'ua-parser-js',\n];\n\nawait main();\n\nasync function main(): Promise<never> {\n  const args = process.argv.slice(2);\n  const isHelp = ['-h', '--help'].some((arg) => args.includes(arg));\n  const isWrite = ['-w', '--write'].some((arg) => args.includes(arg));\n  const isDevOnly = ['-d', '--dev-only'].some((arg) => args.includes(arg));\n  const isProdOnly = ['-p', '--prod-only'].some((arg) => args.includes(arg));\n\n  if (isHelp) {\n    console.log(HELP_MESSAGE);\n    process.exit(0);\n  }\n\n  const { packageJsonFiles, dependencyVersionsMap } =\n    await getPackageJsonDependencies(isDevOnly, isProdOnly);\n  const dependencyVersionMap = validateNoMultipleVersions(\n    dependencyVersionsMap,\n  );\n\n  consola.start(\n    `Fetching ${Object.keys(dependencyVersionsMap).length} dependencies...`,\n  );\n  const dependencies = await fetchAllPackageInfos(dependencyVersionMap);\n  consola.success('Done!');\n\n  const isMajor = ['-m', '--major'].some((arg) => args.includes(arg));\n  const upgrades = await detectUpgrades(dependencies, isMajor);\n\n  if (!upgrades.length) {\n    consola.info(\"\\nNo upgrades found, you're up to date!\\n\");\n    process.exit(0);\n  }\n\n  printUpgrades(upgrades);\n\n  if (!isWrite) {\n    consola.info('Run with `-w` to write changes to package.json files\\n');\n    process.exit(0);\n  }\n\n  consola.start('Writing new versions to package.json files...');\n  await writeUpgrades(packageJsonFiles, upgrades);\n  consola.success('Done!');\n  consola.info('\\nRun `pnpm i` to install new dependencies\\n');\n  process.exit(0);\n}\n\ntype DependencyVersionsMap = Record<string, Set<string>>;\ntype PackageJsonData = {\n  content: any;\n  path: string;\n  folder: string;\n};\n\nasync function getPackageJsonDependencies(\n  isDevOnly: boolean,\n  isProdOnly: boolean,\n): Promise<{\n  packageJsonFiles: string[];\n  dependencyVersionsMap: DependencyVersionsMap;\n}> {\n  const packageJsonFiles = await glob(\n    ['package.json', '*/*/package.json', '!**/node_modules'],\n    { onlyFiles: true, expandDirectories: false },\n  );\n  const packageJsons: PackageJsonData[] = await Promise.all(\n    packageJsonFiles.map(async (path) => ({\n      content: JSON.parse(await readFile(path, 'utf-8')),\n      path,\n      folder: dirname(path),\n    })),\n  );\n\n  const dependencyVersionsMap = packageJsons.reduce<DependencyVersionsMap>(\n    (map, { content }) => {\n      const addToMap = ([name, version]: [string, any]) => {\n        if (\n          name === 'wxt' ||\n          version.startsWith('workspace:') ||\n          IGNORED_PACKAGES.includes(name)\n        )\n          return;\n\n        map[name] ||= new Set();\n        map[name].add(version as string);\n      };\n      if (!isDevOnly)\n        Object.entries(content.dependencies || {}).forEach(addToMap);\n      if (!isProdOnly)\n        Object.entries(content.devDependencies || {}).forEach(addToMap);\n      return map;\n    },\n    {},\n  );\n\n  return {\n    packageJsonFiles,\n    dependencyVersionsMap,\n  };\n}\n\ntype DependencyVersionMap = Record<string, string>;\n\nfunction validateNoMultipleVersions(\n  dependencyVersionsMap: DependencyVersionsMap,\n): DependencyVersionMap {\n  const depsWithMultipleVersions = Object.entries(dependencyVersionsMap).filter(\n    ([_, versions]) => versions.size > 1,\n  );\n  if (depsWithMultipleVersions.length === 0) {\n    return Object.fromEntries(\n      Object.entries(dependencyVersionsMap).map(([name, versions]) => [\n        name,\n        [...versions][0],\n      ]),\n    );\n  }\n\n  const maxWidth = Math.max(\n    ...depsWithMultipleVersions.map(([name]) => name.length),\n  );\n  console.log(maxWidth);\n  const addCyan = (text: string) => `\\x1b[36m${text}\\x1b[0m`;\n\n  consola.info('Found multiple versions of:');\n  for (const [name, versions] of depsWithMultipleVersions) {\n    console.log(\n      `    \\x1b[35m${name.padEnd(maxWidth)}\\x1b[0m  ${Array.from(versions)\n        .map(addCyan)\n        .join('\\t')}`,\n    );\n  }\n  consola.error(`${depsWithMultipleVersions.length} problem(s) found`);\n  process.exit(1);\n}\n\nasync function fetchPackageInfo(name: string): Promise<PackageInfo> {\n  // Use PNPM instead of API in case dependencies don't come from NPM\n  const res = await spawn('pnpm', ['view', name, '--json']);\n  return JSON.parse(res.stdout);\n}\n\ntype PackageInfo = {\n  name: string;\n  'dist-tags': {\n    latest: string;\n    [tag: string]: string;\n  };\n  versions: string[];\n  time: {\n    created: string;\n    modified: string;\n    [version: string]: string;\n  };\n};\n\ntype DependencyInfo = {\n  name: string;\n  currentVersionRange: string;\n  info: PackageInfo;\n};\n\nasync function fetchAllPackageInfos(\n  deps: DependencyVersionMap,\n): Promise<DependencyInfo[]> {\n  const infos: DependencyInfo[] = await pMap(\n    Object.entries(deps),\n    async ([name, currentVersionRange]) => {\n      const info = await fetchPackageInfo(name);\n      return { name, currentVersionRange, info };\n    },\n    { concurrency: 20 },\n  );\n  return infos.toSorted((a, b) => a.name.localeCompare(b.name));\n}\n\ntype UpgradeDetails = {\n  name: string;\n  rangePrefix: string;\n  currentVersion: string;\n  currentVersionReleasedAt: Date;\n  currentRange: string;\n  upgradeToVersion: string;\n  upgradeToVersionReleasedAt: Date;\n  upgradeToRange: string;\n  diff: semver.ReleaseType | null;\n  latestVersion: string;\n  latestVersionReleasedAt: Date;\n};\n\nasync function detectUpgrades(\n  deps: DependencyInfo[],\n  isMajor: boolean,\n): Promise<UpgradeDetails[]> {\n  const results: UpgradeDetails[] = [];\n\n  for (const dep of deps) {\n    const currentRange = dep.currentVersionRange;\n    if (currentRange === '*') continue;\n\n    const parts = currentRange.split(' || ').map((part) => part.trim());\n    const lastRange = parts[parts.length - 1];\n    const isUnion = parts.length > 1;\n\n    const rangePrefix = lastRange.match(/^(.?)(\\d+\\.\\d+\\.\\d+)/)?.[1] ?? '';\n\n    const currentVersion = semver.minVersion(lastRange)?.version;\n    if (currentVersion == null)\n      throw Error(`Invalid version specifier: ${dep.name}@${currentRange}`);\n\n    const currentVersionReleasedAt = new Date(dep.info.time[currentVersion]);\n    const isPre1 = currentVersion.startsWith('0.');\n\n    const latestVersion = dep.info['dist-tags'].latest;\n    const latestVersionReleasedAt = new Date(dep.info.time[latestVersion]);\n\n    const upgradeToVersion = isMajor\n      ? // Always use the latest version for major upgrades\n        latestVersion\n      : // Otherwise use the last stable version greater than the current version that is not a major release\n        (dep.info.versions.findLast(\n          (v) =>\n            semver.gt(v, currentVersion) &&\n            (isPre1 ? ['patch'] : ['patch', 'minor']).includes(\n              semver.diff(v, currentVersion)!,\n            ) &&\n            semver.prerelease(v) == null,\n        ) ?? currentVersion);\n    const upgradeToVersionReleasedAt = new Date(\n      dep.info.time[upgradeToVersion],\n    );\n\n    let upgradeToRange = `${rangePrefix}${upgradeToVersion}`;\n    upgradeToRange = isUnion\n      ? semver.satisfies(upgradeToVersion, currentRange)\n        ? currentRange\n        : `${currentRange} || ${upgradeToRange}`\n      : upgradeToRange;\n\n    let diff = semver.diff(currentVersion, upgradeToVersion);\n    if (isPre1) {\n      if (diff === 'minor') diff = 'major';\n      if (diff === 'patch') diff = 'minor';\n    }\n\n    if (upgradeToRange === currentRange) continue;\n\n    results.push({\n      name: dep.name,\n      rangePrefix,\n      diff,\n      currentVersion,\n      currentVersionReleasedAt,\n      currentRange,\n      upgradeToVersion,\n      upgradeToVersionReleasedAt,\n      upgradeToRange,\n      latestVersion,\n      latestVersionReleasedAt,\n    });\n  }\n\n  return results;\n}\n\nfunction printUpgrades(upgrades: UpgradeDetails[]): void {\n  const namePadding = Math.max(...upgrades.map((u) => u.name.length));\n  const currentVersionPadding = Math.max(\n    ...upgrades.map((u) => u.currentRange.length),\n  );\n  const upgradeToVersionPadding = Math.max(\n    ...upgrades.map((u) => u.upgradeToRange.length),\n  );\n  const numberPadding = String(upgrades.length + 1).length + 1;\n\n  consola.info(`Found ${upgrades.length} upgrades:\\n`);\n\n  for (let i = 0; i < upgrades.length; i++) {\n    const upgrade = upgrades[i];\n    const num = `\\x1b[2m${(i + 1).toString().padStart(numberPadding)}.\\x1b[0m`;\n    const name = `\\x1b[35m${upgrade.name.padEnd(namePadding)}\\x1b[0m`;\n    const color =\n      upgrade.diff == null\n        ? '\\x1b[2m'\n        : upgrade.diff === 'patch'\n          ? '\\x1b[32m'\n          : upgrade.diff === 'minor'\n            ? '\\x1b[33m'\n            : '\\x1b[31m';\n    const currentVersion = `\\x1b[2m${upgrade.currentRange.padEnd(currentVersionPadding)}\\x1b[0m`;\n    const upgradeToVersion = `${color}${upgrade.upgradeToRange.padEnd(upgradeToVersionPadding)}\\x1b[0m`;\n    const latest =\n      upgrade.latestVersion !== upgrade.upgradeToVersion\n        ? ` \\x1b[2m\\x1b[31m(${upgrade.latestVersion} available)\\x1b[0m`\n        : '';\n    console.log(\n      `  ${num} ${name}  ${currentVersion}  \\x1b[2m→\\x1b[0m  ${upgradeToVersion}${latest}`,\n    );\n  }\n  console.log();\n}\n\nasync function writeUpgrades(\n  packageJsonFiles: string[],\n  upgrades: UpgradeDetails[],\n) {\n  for (const packageJsonFile of packageJsonFiles) {\n    const oldText = await readFile(packageJsonFile, 'utf8');\n    let newText = oldText;\n    for (const upgrade of upgrades) {\n      const search = `\"${upgrade.name}\": \"${upgrade.currentRange}\"`;\n      const replace = `\"${upgrade.name}\": \"${upgrade.upgradeToRange}\"`;\n      newText = newText.replaceAll(search, replace);\n    }\n    if (newText !== oldText) {\n      await writeFile(packageJsonFile, newText, 'utf8');\n    }\n  }\n}\n"
  },
  {
    "path": "templates/react/README.md",
    "content": "# WXT + React\n\nThis template should help get you started developing with React in WXT.\n"
  },
  {
    "path": "templates/react/_gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "templates/react/entrypoints/background.ts",
    "content": "export default defineBackground(() => {\n  console.log('Hello background!', { id: browser.runtime.id });\n});\n"
  },
  {
    "path": "templates/react/entrypoints/content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*.google.com/*'],\n  main() {\n    console.log('Hello content.');\n  },\n});\n"
  },
  {
    "path": "templates/react/entrypoints/popup/App.css",
    "content": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #54bc4ae0);\n}\n.logo.react:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  a:nth-of-type(2) .logo {\n    animation: logo-spin infinite 20s linear;\n  }\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "templates/react/entrypoints/popup/App.tsx",
    "content": "import { useState } from 'react';\nimport reactLogo from '@/assets/react.svg';\nimport wxtLogo from '/wxt.svg';\nimport './App.css';\n\nfunction App() {\n  const [count, setCount] = useState(0);\n\n  return (\n    <>\n      <div>\n        <a href=\"https://wxt.dev\" target=\"_blank\">\n          <img src={wxtLogo} className=\"logo\" alt=\"WXT logo\" />\n        </a>\n        <a href=\"https://react.dev\" target=\"_blank\">\n          <img src={reactLogo} className=\"logo react\" alt=\"React logo\" />\n        </a>\n      </div>\n      <h1>WXT + React</h1>\n      <div className=\"card\">\n        <button onClick={() => setCount((count) => count + 1)}>\n          count is {count}\n        </button>\n        <p>\n          Edit <code>src/App.tsx</code> and save to test HMR\n        </p>\n      </div>\n      <p className=\"read-the-docs\">\n        Click on the WXT and React logos to learn more\n      </p>\n    </>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "templates/react/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Popup Title</title>\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/react/entrypoints/popup/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App.tsx';\nimport './style.css';\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "templates/react/entrypoints/popup/style.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-text-size-adjust: 100%;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "templates/react/package.json",
    "content": "{\n  \"name\": \"wxt-react-starter\",\n  \"description\": \"manifest.json description\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"dev:firefox\": \"wxt -b firefox\",\n    \"build\": \"wxt build\",\n    \"build:firefox\": \"wxt build -b firefox\",\n    \"zip\": \"wxt zip\",\n    \"zip:firefox\": \"wxt zip -b firefox\",\n    \"compile\": \"tsc --noEmit\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@wxt-dev/module-react\": \"^1.1.5\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"^0.20.20\"\n  }\n}\n"
  },
  {
    "path": "templates/react/tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\",\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"jsx\": \"react-jsx\"\n  }\n}\n"
  },
  {
    "path": "templates/react/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n  modules: ['@wxt-dev/module-react'],\n});\n"
  },
  {
    "path": "templates/solid/README.md",
    "content": "# WXT + SolidJS\n\nThis template should help get you started developing with SolidJS in WXT.\n"
  },
  {
    "path": "templates/solid/_gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "templates/solid/entrypoints/background.ts",
    "content": "export default defineBackground(() => {\n  console.log('Hello background!', { id: browser.runtime.id });\n});\n"
  },
  {
    "path": "templates/solid/entrypoints/content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*.google.com/*'],\n  main() {\n    console.log('Hello content.');\n  },\n});\n"
  },
  {
    "path": "templates/solid/entrypoints/popup/App.css",
    "content": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #54bc4ae0);\n}\n.logo.solid:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "templates/solid/entrypoints/popup/App.tsx",
    "content": "import { createSignal } from 'solid-js';\nimport solidLogo from '@/assets/solid.svg';\nimport wxtLogo from '/wxt.svg';\nimport './App.css';\n\nfunction App() {\n  const [count, setCount] = createSignal(0);\n\n  return (\n    <>\n      <div>\n        <a href=\"https://wxt.dev\" target=\"_blank\">\n          <img src={wxtLogo} class=\"logo\" alt=\"WXT logo\" />\n        </a>\n        <a href=\"https://solidjs.com\" target=\"_blank\">\n          <img src={solidLogo} class=\"logo solid\" alt=\"Solid logo\" />\n        </a>\n      </div>\n      <h1>WXT + Solid</h1>\n      <div class=\"card\">\n        <button onClick={() => setCount((count) => count + 1)}>\n          count is {count()}\n        </button>\n        <p>\n          Edit <code>popup/App.tsx</code> and save to test HMR\n        </p>\n      </div>\n      <p class=\"read-the-docs\">\n        Click on the WXT and Solid logos to learn more\n      </p>\n    </>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "templates/solid/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Popup Title</title>\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/solid/entrypoints/popup/main.tsx",
    "content": "import { render } from 'solid-js/web';\n\nimport './style.css';\nimport App from './App';\n\nrender(() => <App />, document.getElementById('root')!);\n"
  },
  {
    "path": "templates/solid/entrypoints/popup/style.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-text-size-adjust: 100%;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "templates/solid/package.json",
    "content": "{\n  \"name\": \"wxt-solid-starter\",\n  \"description\": \"manifest.json description\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"dev:firefox\": \"wxt -b firefox\",\n    \"build\": \"wxt build\",\n    \"build:firefox\": \"wxt build -b firefox\",\n    \"zip\": \"wxt zip\",\n    \"zip:firefox\": \"wxt zip -b firefox\",\n    \"compile\": \"tsc --noEmit\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"dependencies\": {\n    \"solid-js\": \"^1.9.11\"\n  },\n  \"devDependencies\": {\n    \"@wxt-dev/module-solid\": \"^1.1.4\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"^0.20.20\"\n  }\n}\n"
  },
  {
    "path": "templates/solid/tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\"\n  }\n}\n"
  },
  {
    "path": "templates/solid/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n  modules: ['@wxt-dev/module-solid'],\n});\n"
  },
  {
    "path": "templates/svelte/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"svelte.svelte-vscode\"]\n}\n"
  },
  {
    "path": "templates/svelte/README.md",
    "content": "# WXT + Svelte\n\nThis template should help get you started developing with Svelte in WXT.\n\n## Recommended IDE Setup\n\n[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).\n"
  },
  {
    "path": "templates/svelte/_gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "templates/svelte/package.json",
    "content": "{\n  \"name\": \"wxt-svelte-starter\",\n  \"description\": \"manifest.json description\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"dev:firefox\": \"wxt -b firefox\",\n    \"build\": \"wxt build\",\n    \"build:firefox\": \"wxt build -b firefox\",\n    \"zip\": \"wxt zip\",\n    \"zip:firefox\": \"wxt zip -b firefox\",\n    \"check\": \"svelte-check --tsconfig ./tsconfig.json\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"devDependencies\": {\n    \"@tsconfig/svelte\": \"^5.0.8\",\n    \"@wxt-dev/module-svelte\": \"^2.0.4\",\n    \"svelte\": \"^5.53.7\",\n    \"svelte-check\": \"^4.4.4\",\n    \"tslib\": \"^2.8.1\",\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"^0.20.20\"\n  }\n}\n"
  },
  {
    "path": "templates/svelte/src/entrypoints/background.ts",
    "content": "export default defineBackground(() => {\n  console.log('Hello background!', { id: browser.runtime.id });\n});\n"
  },
  {
    "path": "templates/svelte/src/entrypoints/content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*.google.com/*'],\n  main() {\n    console.log('Hello content.');\n  },\n});\n"
  },
  {
    "path": "templates/svelte/src/entrypoints/popup/App.svelte",
    "content": "<script lang=\"ts\">\n  import svelteLogo from '../../assets/svelte.svg'\n  import Counter from '../../lib/Counter.svelte'\n</script>\n\n<main>\n  <div>\n    <a href=\"https://wxt.dev\" target=\"_blank\" rel=\"noreferrer\">\n      <img src=\"/wxt.svg\" class=\"logo\" alt=\"WXT Logo\" />\n    </a>\n    <a href=\"https://svelte.dev\" target=\"_blank\" rel=\"noreferrer\">\n      <img src={svelteLogo} class=\"logo svelte\" alt=\"Svelte Logo\" />\n    </a>\n  </div>\n  <h1>WXT + Svelte</h1>\n\n  <div class=\"card\">\n    <Counter />\n  </div>\n\n  <p class=\"read-the-docs\">\n    Click on the WXT and Svelte logos to learn more\n  </p>\n</main>\n\n<style>\n  .logo {\n    height: 6em;\n    padding: 1.5em;\n    will-change: filter;\n    transition: filter 300ms;\n  }\n  .logo:hover {\n    filter: drop-shadow(0 0 2em #54bc4ae0);\n  }\n  .logo.svelte:hover {\n    filter: drop-shadow(0 0 2em #ff3e00aa);\n  }\n  .read-the-docs {\n    color: #888;\n  }\n</style>\n"
  },
  {
    "path": "templates/svelte/src/entrypoints/popup/app.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-text-size-adjust: 100%;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\n.card {\n  padding: 2em;\n}\n\n#app {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "templates/svelte/src/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Popup Title</title>\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/svelte/src/entrypoints/popup/main.ts",
    "content": "import { mount } from 'svelte';\nimport App from './App.svelte';\nimport './app.css';\n\nconst app = mount(App, {\n  target: document.getElementById('app')!,\n});\n\nexport default app;\n"
  },
  {
    "path": "templates/svelte/src/lib/Counter.svelte",
    "content": "<script lang=\"ts\">\n  let count: number = 0\n  const increment = () => {\n    count += 1\n  }\n</script>\n\n<button on:click={increment}>\n  count is {count}\n</button>\n"
  },
  {
    "path": "templates/svelte/tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "templates/svelte/wxt-env.d.ts",
    "content": "/// <reference types=\"svelte\" />\n"
  },
  {
    "path": "templates/svelte/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n  srcDir: 'src',\n  modules: ['@wxt-dev/module-svelte'],\n});\n"
  },
  {
    "path": "templates/vanilla/_gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "templates/vanilla/components/counter.ts",
    "content": "export function setupCounter(element: HTMLButtonElement) {\n  let counter = 0;\n  const setCounter = (count: number) => {\n    counter = count;\n    element.innerHTML = `count is ${counter}`;\n  };\n  element.addEventListener('click', () => setCounter(counter + 1));\n  setCounter(0);\n}\n"
  },
  {
    "path": "templates/vanilla/entrypoints/background.ts",
    "content": "export default defineBackground(() => {\n  console.log('Hello background!', { id: browser.runtime.id });\n});\n"
  },
  {
    "path": "templates/vanilla/entrypoints/content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*.google.com/*'],\n  main() {\n    console.log('Hello content.');\n  },\n});\n"
  },
  {
    "path": "templates/vanilla/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Popup Title</title>\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/vanilla/entrypoints/popup/main.ts",
    "content": "import './style.css';\nimport typescriptLogo from '@/assets/typescript.svg';\nimport wxtLogo from '/wxt.svg';\nimport { setupCounter } from '@/components/counter';\n\ndocument.querySelector<HTMLDivElement>('#app')!.innerHTML = `\n  <div>\n    <a href=\"https://wxt.dev\" target=\"_blank\">\n      <img src=\"${wxtLogo}\" class=\"logo\" alt=\"WXT logo\" />\n    </a>\n    <a href=\"https://www.typescriptlang.org/\" target=\"_blank\">\n      <img src=\"${typescriptLogo}\" class=\"logo vanilla\" alt=\"TypeScript logo\" />\n    </a>\n    <h1>WXT + TypeScript</h1>\n    <div class=\"card\">\n      <button id=\"counter\" type=\"button\"></button>\n    </div>\n    <p class=\"read-the-docs\">\n      Click on the WXT and TypeScript logos to learn more\n    </p>\n  </div>\n`;\n\nsetupCounter(document.querySelector<HTMLButtonElement>('#counter')!);\n"
  },
  {
    "path": "templates/vanilla/entrypoints/popup/style.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-text-size-adjust: 100%;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\n#app {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #54bc4ae0);\n}\n.logo.vanilla:hover {\n  filter: drop-shadow(0 0 2em #3178c6aa);\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "templates/vanilla/package.json",
    "content": "{\n  \"name\": \"wxt-starter\",\n  \"description\": \"manifest.json description\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"dev:firefox\": \"wxt -b firefox\",\n    \"build\": \"wxt build\",\n    \"build:firefox\": \"wxt build -b firefox\",\n    \"zip\": \"wxt zip\",\n    \"zip:firefox\": \"wxt zip -b firefox\",\n    \"compile\": \"tsc --noEmit\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.9.3\",\n    \"wxt\": \"^0.20.20\"\n  }\n}\n"
  },
  {
    "path": "templates/vanilla/tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "templates/vanilla/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({});\n"
  },
  {
    "path": "templates/vue/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"Vue.volar\"]\n}\n"
  },
  {
    "path": "templates/vue/README.md",
    "content": "# WXT + Vue 3\n\nThis template should help get you started developing with Vue 3 in WXT.\n\n## Recommended IDE Setup\n\n- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar).\n"
  },
  {
    "path": "templates/vue/_gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.output\nstats.html\nstats-*.json\n.wxt\nweb-ext.config.ts\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "templates/vue/components/HelloWorld.vue",
    "content": "<script lang=\"ts\" setup>\nimport { ref } from 'vue';\n\ndefineProps({\n  msg: String,\n});\n\nconst count = ref(0);\n</script>\n\n<template>\n  <h1>{{ msg }}</h1>\n\n  <div class=\"card\">\n    <button type=\"button\" @click=\"count++\">count is {{ count }}</button>\n    <p>\n      Edit\n      <code>components/HelloWorld.vue</code> to test HMR\n    </p>\n  </div>\n\n  <p>\n    Install\n    <a href=\"https://github.com/vuejs/language-tools\" target=\"_blank\">Volar</a>\n    in your IDE for a better DX\n  </p>\n  <p class=\"read-the-docs\">Click on the WXT and Vue logos to learn more</p>\n</template>\n\n<style scoped>\n.read-the-docs {\n  color: #888;\n}\n</style>\n"
  },
  {
    "path": "templates/vue/entrypoints/background.ts",
    "content": "export default defineBackground(() => {\n  console.log('Hello background!', { id: browser.runtime.id });\n});\n"
  },
  {
    "path": "templates/vue/entrypoints/content.ts",
    "content": "export default defineContentScript({\n  matches: ['*://*.google.com/*'],\n  main() {\n    console.log('Hello content.');\n  },\n});\n"
  },
  {
    "path": "templates/vue/entrypoints/popup/App.vue",
    "content": "<script lang=\"ts\" setup>\nimport HelloWorld from '@/components/HelloWorld.vue';\n</script>\n\n<template>\n  <div>\n    <a href=\"https://wxt.dev\" target=\"_blank\">\n      <img src=\"/wxt.svg\" class=\"logo\" alt=\"WXT logo\" />\n    </a>\n    <a href=\"https://vuejs.org/\" target=\"_blank\">\n      <img src=\"@/assets/vue.svg\" class=\"logo vue\" alt=\"Vue logo\" />\n    </a>\n  </div>\n  <HelloWorld msg=\"WXT + Vue\" />\n</template>\n\n<style scoped>\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #54bc4ae0);\n}\n.logo.vue:hover {\n  filter: drop-shadow(0 0 2em #42b883aa);\n}\n</style>\n"
  },
  {
    "path": "templates/vue/entrypoints/popup/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Default Popup Title</title>\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/vue/entrypoints/popup/main.ts",
    "content": "import { createApp } from 'vue';\nimport './style.css';\nimport App from './App.vue';\n\ncreateApp(App).mount('#app');\n"
  },
  {
    "path": "templates/vue/entrypoints/popup/style.css",
    "content": ":root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-text-size-adjust: 100%;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n.card {\n  padding: 2em;\n}\n\n#app {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n"
  },
  {
    "path": "templates/vue/package.json",
    "content": "{\n  \"name\": \"wxt-vue-starter\",\n  \"description\": \"manifest.json description\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"wxt\",\n    \"dev:firefox\": \"wxt -b firefox\",\n    \"build\": \"wxt build\",\n    \"build:firefox\": \"wxt build -b firefox\",\n    \"zip\": \"wxt zip\",\n    \"zip:firefox\": \"wxt zip -b firefox\",\n    \"compile\": \"vue-tsc --noEmit\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"dependencies\": {\n    \"vue\": \"^3.5.29\"\n  },\n  \"devDependencies\": {\n    \"@wxt-dev/module-vue\": \"^1.0.3\",\n    \"typescript\": \"^5.9.3\",\n    \"vue-tsc\": \"^3.2.5\",\n    \"wxt\": \"^0.20.20\"\n  }\n}\n"
  },
  {
    "path": "templates/vue/tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "templates/vue/wxt.config.ts",
    "content": "import { defineConfig } from 'wxt';\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n  modules: ['@wxt-dev/module-vue'],\n});\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n\n    /* Type Checking */\n    \"strict\": true,\n    \"lib\": [\"DOM\", \"WebWorker\", \"ESNext\"],\n\n    /* Completeness */\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"exclude\": [\"packages\", \"templates\", \"node_modules\"]\n}\n"
  }
]