Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9205bb08f | |||
| 05e23e70b4 | |||
| 68fa36ef2b | |||
| 182d575205 | |||
| 09eadf6796 | |||
| a6904b6319 | |||
| 42d65cb10b | |||
| 636cf1f841 | |||
| 3b154508bd | |||
| d34eac9a4c | |||
| 54c6fcca10 | |||
| 0114ee6a7a |
@@ -7,7 +7,6 @@
|
||||
pnpm-lock.yaml linguist-generated
|
||||
package-lock.json linguist-generated
|
||||
bun.lockb linguist-generated
|
||||
bun.lock linguist-generated
|
||||
yarn.lock linguist-generated
|
||||
|
||||
# Exclude templates from language statistics
|
||||
|
||||
+1
-4
@@ -1,5 +1,5 @@
|
||||
# Set default
|
||||
* @aklinker1
|
||||
* @aklinker1 @Timeraa
|
||||
|
||||
# Secure Directories
|
||||
/.github/ @aklinker1
|
||||
@@ -7,6 +7,3 @@
|
||||
# Creator of specific wxt modules
|
||||
/packages/auto-icons/ @Timeraa
|
||||
/packages/unocss/ @Timeraa
|
||||
|
||||
# Other Directories
|
||||
/docs @aklinker1 @PatrykKuniczak @nishu-murmu
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
name: Basic Setup
|
||||
description: Install Bun and dependencies
|
||||
description: Install PNPM, Node, and dependencies
|
||||
|
||||
inputs:
|
||||
install:
|
||||
default: 'true'
|
||||
description: Whether or not to run 'bun install'
|
||||
description: Whether or not to run 'pnpm install'
|
||||
|
||||
installArgs:
|
||||
default: ''
|
||||
description: Additional args to append to "pnpm install"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
steps:
|
||||
- name: 🛠️ Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
- name: 🛠️ Setup PNPM
|
||||
uses: pnpm/action-setup@f2b2b233b538f500472c7274c7012f57857d8ce0 # v4.1.0
|
||||
|
||||
- name: 🛠️ Setup NodeJS
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
bun-version-file: '.tool-versions'
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
if: ${{ inputs.install == 'true' }}
|
||||
shell: bash
|
||||
run: bun ci ${{ inputs.installArgs }}
|
||||
run: pnpm install ${{ inputs.installArgs }}
|
||||
|
||||
+7
-12
@@ -1,20 +1,15 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: bun
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
groups:
|
||||
production-updates:
|
||||
applies-to: version-updates
|
||||
dependency-type: production
|
||||
update-types: [minor, patch]
|
||||
dev-updates:
|
||||
applies-to: version-updates
|
||||
dependency-type: development
|
||||
update-types: [minor, patch]
|
||||
interval: 'monthly'
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'monthly'
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- name: Gather Info
|
||||
id: check
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
@@ -59,24 +59,20 @@ jobs:
|
||||
|
||||
- name: Sync Author
|
||||
if: steps.check.outputs.has_assignees == 'false'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
assignees: ['${{ steps.check.outputs.author }}']
|
||||
});
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
assignees: ['${{ steps.check.outputs.author }}']
|
||||
});
|
||||
|
||||
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
|
||||
} catch (err) {
|
||||
console.warn("Error assigning author:", err);
|
||||
}
|
||||
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
|
||||
|
||||
- name: Sync Labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const newLabels = ${{ steps.check.outputs.labels }};
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
if: github.repository_owner == 'wxt-dev'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -17,18 +17,13 @@ jobs:
|
||||
if: ${{ github.repository == 'wxt-dev/wxt' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build All Packages
|
||||
run: bun run buildc all
|
||||
run: pnpm buildc all
|
||||
|
||||
- name: Publish
|
||||
run: bunx pkg-pr-new publish --compact --bun './packages/*'
|
||||
|
||||
- name: DEBUG
|
||||
if: always()
|
||||
run: ls -lhA
|
||||
working-directory: packages/analytics
|
||||
run: pnpx pkg-pr-new publish --compact --pnpm './packages/*'
|
||||
|
||||
@@ -15,22 +15,8 @@ jobs:
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
steps:
|
||||
- name: Log PR Author Info
|
||||
run: |
|
||||
echo "PR Author Login: ${{ github.event.pull_request.user.login }}"
|
||||
echo "PR Author ID: ${{ github.event.pull_request.user.id }}"
|
||||
echo "PR Author Type: ${{ github.event.pull_request.user.type }}"
|
||||
echo "PR Author Node ID: ${{ github.event.pull_request.user.node_id }}"
|
||||
|
||||
- name: Post Thank You Comment
|
||||
if: >-
|
||||
github.event.pull_request.user.login != 'aklinker1' &&
|
||||
github.event.pull_request.user.login != 'Timeraa' &&
|
||||
github.event.pull_request.user.login != 'PatrykKuniczak' &&
|
||||
github.event.pull_request.user.login != 'nishu-murmu' &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
||||
github.event.pull_request.user.login != 'copilot-swe-agent[bot]'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
comment: Thanks for helping make WXT better!
|
||||
with:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Only fetch the config file from the repository
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
@@ -23,13 +23,13 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||
@@ -37,11 +37,11 @@ jobs:
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
bun run docs:build
|
||||
pnpm docs:build
|
||||
docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
|
||||
|
||||
- name: Push Image
|
||||
run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
|
||||
|
||||
- name: Deploy
|
||||
run: 'curl -X POST -i -H "X-API-Key: ${{ secrets.DOCS_WEBHOOK_API_KEY }}" https://${{ secrets.DOCS_WEBHOOK_DOMAIN }}/api/webhook/stacks/${{ secrets.DOCS_WEBHOOK_STACK_ID }}'
|
||||
run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}
|
||||
|
||||
@@ -20,13 +20,6 @@ on:
|
||||
- unocss
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
bump:
|
||||
description: Blank (auto), a version (1.4.1), or major/minor/patch
|
||||
type: string
|
||||
dryRun:
|
||||
description: Do a dry-run and don't publish anything
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -36,23 +29,18 @@ jobs:
|
||||
name: Validate
|
||||
uses: './.github/workflows/validate.yml'
|
||||
secrets: inherit
|
||||
if: ${{ inputs.dryRun != true }}
|
||||
|
||||
publish:
|
||||
name: Publish ${{ case(inputs.dryRun, ' (dry-run)', '') }}
|
||||
name: Publish
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write # Push version changes
|
||||
id-token: write # OIDC for NPM publishing
|
||||
needs:
|
||||
- validate
|
||||
# Allow dry runs to skip validation but still run this job
|
||||
if: |
|
||||
always() &&
|
||||
(needs.validate.result == 'success' || needs.validate.result == 'skipped')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example
|
||||
@@ -64,42 +52,22 @@ jobs:
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git config --global push.followTags true
|
||||
|
||||
- name: Upgrade NPM for Provence
|
||||
run: sudo npm i -g npm@latest
|
||||
- name: Bump and Tag
|
||||
run: |
|
||||
pnpm tsx scripts/bump-package-version.ts ${{ inputs.package }}
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
- name: Bump and Release
|
||||
uses: aklinker1/zero-changelog/actions/release@b6fcca17cebfc45f996b80fe5926da924d8441f8 # 0.2.1
|
||||
with:
|
||||
bump: ${{ inputs.bump }}
|
||||
path: packages/${{ inputs.package }}
|
||||
# Include documentation updates in WXT's releases
|
||||
additionalDirs: |-
|
||||
${{ case(
|
||||
inputs.package == 'wxt', 'docs',
|
||||
''
|
||||
) }}
|
||||
# Use package name in commit message
|
||||
commitTemplate: |-
|
||||
${{ case(
|
||||
inputs.package == 'wxt', 'chore(release): wxt v{{version}}',
|
||||
'chore(release): @wxt-dev/{{dirname}} v{{version}}'
|
||||
) }}
|
||||
# Use package name in release name
|
||||
releaseNameTemplate: |-
|
||||
${{ case(
|
||||
inputs.package == 'wxt', 'wxt v{{version}}',
|
||||
'@wxt-dev/{{dirname}} v{{version}}'
|
||||
) }}
|
||||
# Update lockfile with new versions since it includes the package versions
|
||||
preCommitCommands: |-
|
||||
bun i
|
||||
publishCommands: |-
|
||||
bun pm pack
|
||||
npm publish *.tgz
|
||||
dryRun: ${{ inputs.dryRun }}
|
||||
dryRunPublishCommands: |-
|
||||
bun pm pack
|
||||
npm publish *.tgz --dry-run
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
latestRelease: ${{ inputs.package == 'wxt' }}
|
||||
- name: Publish to NPM
|
||||
working-directory: packages/${{ inputs.package }}
|
||||
run: |
|
||||
pnpm pack
|
||||
sudo npm i -g npm@latest
|
||||
/usr/local/bin/npm publish *.tgz
|
||||
|
||||
- name: Create GitHub release
|
||||
run: pnpm tsx scripts/create-github-release.ts ${{ inputs.package }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: 🔄 Sync Releases
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: Package to sync
|
||||
default: wxt
|
||||
type: choice
|
||||
options:
|
||||
- analytics
|
||||
- auto-icons
|
||||
- i18n
|
||||
- is-background
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync Releases
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
installArgs: --ignore-scripts
|
||||
|
||||
- name: Sync Releases
|
||||
run: pnpm tsx scripts/sync-releases.ts ${{ inputs.package }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
id-token: write # OIDC for NPM publishing
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
@@ -28,15 +28,15 @@ jobs:
|
||||
|
||||
- name: Generate Latest Code
|
||||
working-directory: packages/browser
|
||||
run: bun run gen
|
||||
run: pnpm gen
|
||||
|
||||
- name: Run Checks
|
||||
working-directory: packages/browser
|
||||
run: bun run check
|
||||
run: pnpm check
|
||||
|
||||
- name: Commit Changes
|
||||
id: commit
|
||||
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -46,6 +46,6 @@ jobs:
|
||||
if: steps.commit.outputs.changes_detected == 'true'
|
||||
working-directory: packages/browser
|
||||
run: |
|
||||
bun pm pack
|
||||
pnpm pack
|
||||
sudo npm i -g npm@latest
|
||||
npm publish *.tgz
|
||||
/usr/local/bin/npm publish *.tgz
|
||||
|
||||
@@ -15,43 +15,43 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Basic Checks
|
||||
run: bun run check
|
||||
run: pnpm check
|
||||
|
||||
builds:
|
||||
name: Builds
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build All Packages
|
||||
run: bun run buildc all
|
||||
run: pnpm buildc all
|
||||
|
||||
build-demo:
|
||||
name: Build Demo
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build
|
||||
run: bun run build:all
|
||||
run: pnpm build:all
|
||||
working-directory: packages/wxt-demo
|
||||
|
||||
- name: ZIP
|
||||
run: bun run wxt zip
|
||||
run: pnpm wxt zip
|
||||
working-directory: packages/wxt-demo
|
||||
|
||||
tests:
|
||||
@@ -68,27 +68,25 @@ jobs:
|
||||
coverage: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: latest
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ ! matrix.coverage }}
|
||||
run: bun run test
|
||||
run: pnpm test
|
||||
|
||||
- name: Run Tests (Coverage)
|
||||
if: matrix.coverage
|
||||
run: bun run test:coverage --reporter=default --reporter=hanging-process
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
- name: Upload Coverage
|
||||
if: matrix.coverage
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -106,13 +104,13 @@ jobs:
|
||||
- vue
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Pack WXT package
|
||||
run: bun pm pack
|
||||
run: pnpm pack
|
||||
working-directory: packages/wxt
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -124,15 +122,15 @@ jobs:
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
- name: Type Check Template
|
||||
run: bun run compile
|
||||
run: pnpm compile
|
||||
if: matrix.template != 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
- name: Type Check Template
|
||||
run: bun run check
|
||||
run: pnpm check
|
||||
if: matrix.template == 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
- name: Build Template
|
||||
run: bun run build
|
||||
run: pnpm build
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
+13
-26
@@ -2,12 +2,7 @@ name: 📼 VHS
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- docs/tapes/*.tape
|
||||
- .github/workflows/vhs.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- docs/tapes/*.tape
|
||||
- .github/workflows/vhs.yml
|
||||
- 'docs/tapes/*.tape'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -16,43 +11,35 @@ permissions:
|
||||
jobs:
|
||||
vhs:
|
||||
name: Create VHS
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.repository == 'wxt-dev/wxt' }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
|
||||
with:
|
||||
go-version: '1.25.1'
|
||||
install: false
|
||||
|
||||
# This prevents bunx from downloading WXT in the video, and it uses the checked-out version
|
||||
- name: Build WXT
|
||||
working-directory: packages/wxt
|
||||
run: bun run build
|
||||
# This prevents pnpm dlx from downloading WXT in the video
|
||||
- name: Pre-install WXT
|
||||
run: |
|
||||
pnpm store add wxt@latest
|
||||
pnpm dlx wxt@latest --version
|
||||
|
||||
- name: Record VHS
|
||||
run: |
|
||||
brew install ttyd ffmpeg
|
||||
go install github.com/charmbracelet/vhs@517bcda0faf416728bcf6b7fe489eb0e2469d9b5 # v0.10.0
|
||||
vhs docs/tapes/init-demo.tape
|
||||
uses: charmbracelet/vhs-action@59641cdc7fadf3978db65eb8c6937ea2752f4ec3 # v2.1.0
|
||||
with:
|
||||
path: 'docs/tapes/init-demo.tape'
|
||||
|
||||
- name: Save recorded GIF
|
||||
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
commit_message: 'docs: Update `wxt init` GIF'
|
||||
commit_user_name: github-actions[bot]
|
||||
commit_user_email: github-actions[bot]@users.noreply.github.com
|
||||
commit_author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
# https://github.com/charmbracelet/vhs#output
|
||||
file_pattern: 'docs/assets/*.gif'
|
||||
|
||||
+1
-2
@@ -1,14 +1,12 @@
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!packages/wxt/src/core/utils/__tests__/fixtures/.env
|
||||
.idea
|
||||
.output
|
||||
.webextrc
|
||||
.wxt
|
||||
.wxt-runner
|
||||
*.log
|
||||
*.tgz
|
||||
/docs/.vitepress/cache
|
||||
docs/.vitepress/.temp
|
||||
coverage
|
||||
@@ -22,5 +20,6 @@ templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/api/reference
|
||||
stats.html
|
||||
.tool-versions
|
||||
.cache
|
||||
*-stats.txt
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
singleQuote: true
|
||||
endOfLine: lf
|
||||
plugins:
|
||||
- prettier-plugin-jsdoc
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
bun 1.3.13
|
||||
nodejs 24.14.1
|
||||
+35
-58
@@ -15,7 +15,7 @@ WXT is two things:
|
||||
|
||||
The 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.
|
||||
|
||||
I 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 available, 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.
|
||||
I 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.
|
||||
|
||||
> 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.
|
||||
|
||||
@@ -48,12 +48,16 @@ To make a breaking change:
|
||||
|
||||
## Setup
|
||||
|
||||
WXT uses Bun for package management and development. Install it from: <https://bun.com/>
|
||||
|
||||
Then install dependencies:
|
||||
WXT uses `pnpm`, so make sure you have it installed.
|
||||
|
||||
```sh
|
||||
bun install
|
||||
corepack enable
|
||||
```
|
||||
|
||||
Then, simply run the install command:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Development
|
||||
@@ -61,68 +65,48 @@ bun install
|
||||
Here are some helpful commands:
|
||||
|
||||
```sh
|
||||
# Build WXT package and workspace dependencies
|
||||
bun run --filter wxt build
|
||||
# Build WXT package
|
||||
cd packages/wxt
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build workspace dependencies, then start the demo extension in dev mode
|
||||
bun run --filter wxt-demo dev
|
||||
# Build WXT package, then build demo extension
|
||||
cd packages/wxt-demo
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then start the demo extension in dev mode
|
||||
cd packages/wxt-demo
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh
|
||||
# Run unit and E2E tests
|
||||
bun run test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
```sh
|
||||
# Start the docs website locally
|
||||
bun run docs:dev
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
> Above, we used bun's `--filter` flag to choose which package to run a command in, but there are other ways:
|
||||
>
|
||||
> ```sh
|
||||
> bun run -F @wxt-dev/i18n build
|
||||
> # or
|
||||
> bun run --cwd packages/i18n build
|
||||
> # or
|
||||
> cd packages/i18n
|
||||
> bun run build
|
||||
> ```
|
||||
>
|
||||
> Pick your poison!
|
||||
|
||||
## Profiling
|
||||
|
||||
```sh
|
||||
# Build the latest version
|
||||
bun run --filter wxt build
|
||||
pnpm --filter wxt build
|
||||
|
||||
# CD to the demo directory
|
||||
cd packages/wxt-demo
|
||||
|
||||
# 1. Generate a flamechart with 0x
|
||||
pnpm dlx 0x node_modules/wxt/bin/wxt.mjs build
|
||||
# 2. Inspect the process with chrome @ chrome://inspect
|
||||
pnpm node --inspect node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
Then there are a few different ways to profile WXT commands:
|
||||
|
||||
- Generate a flamechart with 0x:
|
||||
|
||||
```sh
|
||||
bunx 0x node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
- Create a CPU profile:
|
||||
|
||||
```sh
|
||||
bun run --cpu-prof node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
- Debug the process:
|
||||
|
||||
```sh
|
||||
bun run --inspect node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
## Updating Docs
|
||||
|
||||
Documentation is written with VitePress, and is located in the `docs/` directory.
|
||||
@@ -138,7 +122,7 @@ WXT has unit and E2E tests. When making a change or adding a feature, make sure
|
||||
To run tests for a specific file, add the filename at the end of the test command:
|
||||
|
||||
```sh
|
||||
bun run --filter wxt test manifest-contents
|
||||
pnpm test manifest-contents
|
||||
```
|
||||
|
||||
All test (unit and E2E) for all packages are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
|
||||
@@ -147,7 +131,7 @@ If you want to manually test a change, you can modify the demo project for your
|
||||
|
||||
## Templates
|
||||
|
||||
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `bun`), and run the relevant commands
|
||||
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands
|
||||
|
||||
```sh
|
||||
cd templates/vue
|
||||
@@ -163,7 +147,7 @@ Note that templates are hardcoded to a specific version of `wxt` from NPM, they
|
||||
"typescript": "^5.3.2",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
- "wxt": "^0.8.0"
|
||||
+ "wxt": "../../packages/wxt"
|
||||
+ "wxt": "../.."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -179,25 +163,18 @@ cp -r templates/vanilla templates/<new-template-name>
|
||||
|
||||
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
|
||||
|
||||
## Releasing Updates
|
||||
|
||||
Releases are done with GitHub actions:
|
||||
|
||||
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
|
||||
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
|
||||
|
||||
## Upgrading Dependencies
|
||||
|
||||
WXT has custom rules around what dependencies can be upgraded. Use the `scripts/upgrade-deps.ts` script to upgrade dependencies and follow these rules.
|
||||
|
||||
```sh
|
||||
bun run scripts/upgrade-deps.ts
|
||||
pnpm tsx scripts/upgrade-deps.ts
|
||||
```
|
||||
|
||||
To see all the options, run:
|
||||
|
||||
```sh
|
||||
bun run scripts/upgrade-deps.ts --help
|
||||
pnpm tsx scripts/upgrade-deps.ts --help
|
||||
```
|
||||
|
||||
## Install Unreleased Versions
|
||||
@@ -235,4 +212,4 @@ Anyone is welcome to submit a blog post on <https://wxt.dev/blog>!
|
||||
|
||||
If 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!
|
||||
|
||||
Maintainers don't have to just write code - they can manage issues, answer questions, review PRs, organize and prioritize work - there's lots of ways for you to help out.
|
||||
Maintainers 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.
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ Example PR: <https://github.com/wxt-dev/wxt/pull/2152>
|
||||
|
||||
```sh
|
||||
cd packages/<dir-name>
|
||||
bun publish --access public
|
||||
pnpm publish --access public
|
||||
```
|
||||
|
||||
7. Create a basic release on GitHub mentioning the new package is available.
|
||||
|
||||
@@ -53,6 +53,7 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
|
||||
## Sponsors
|
||||
|
||||
|
||||
+6
-2
@@ -4,8 +4,12 @@ While WXT is in prerelease, only the latest version will receive security update
|
||||
|
||||
<img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
|
||||
|
||||
<!--
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If 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.
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
If 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.
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
-->
|
||||
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
version: '0.2'
|
||||
ignorePaths:
|
||||
- pnpm-lock.yaml
|
||||
- '**/node_modules/**'
|
||||
- '**/dist/**'
|
||||
- '**/coverage/**'
|
||||
- docs/api/**
|
||||
- '**/CHANGELOG.md'
|
||||
- docs/public/_redirects
|
||||
- packages/browser/src/gen/**
|
||||
- patches/**
|
||||
- '**/*.snap'
|
||||
- docs/assets/extension-showcase.yml
|
||||
- packages/*/stats.html
|
||||
words:
|
||||
- Aabid
|
||||
- aabidk
|
||||
- aklinker
|
||||
- automount
|
||||
- avenir
|
||||
- bidi
|
||||
- blocklists
|
||||
- buildc
|
||||
- bunx
|
||||
- cachable
|
||||
- cacjs
|
||||
- charmbracelet
|
||||
- chromedriver
|
||||
- Cira
|
||||
- comctx
|
||||
- confbox
|
||||
- configfile
|
||||
- consola
|
||||
- consolas
|
||||
- Cpath
|
||||
- crbug
|
||||
- cros
|
||||
- crunchyroll
|
||||
- cssinjs
|
||||
- CSUI
|
||||
- Csvg
|
||||
- defu
|
||||
- deinit
|
||||
- dequal
|
||||
- dtrace
|
||||
- duckduckgo
|
||||
- esbuild
|
||||
- esque
|
||||
- extglob
|
||||
- favicons
|
||||
- fieldtrials
|
||||
- firefoxdeveloperedition
|
||||
- flamechart
|
||||
- Florian
|
||||
- focusring
|
||||
- frontmatter
|
||||
- giget
|
||||
- grayscale
|
||||
- greyscale
|
||||
- gtag
|
||||
- hckhakegfgenefhikdcfkaaonnclljmf
|
||||
- hookable
|
||||
- iconify
|
||||
- jsons
|
||||
- Kenobi
|
||||
- keystyle
|
||||
- klinker
|
||||
- linkedom
|
||||
- llms
|
||||
- locationchange
|
||||
- magicast
|
||||
- Menlo
|
||||
- menupanel
|
||||
- metas
|
||||
- moderok
|
||||
- mktemp
|
||||
- mycompany
|
||||
- nacl
|
||||
- nanospinner
|
||||
- natsu
|
||||
- newtab
|
||||
- njsproj
|
||||
- nodebuffer
|
||||
- Noto
|
||||
- ntvs
|
||||
- nypm
|
||||
- obug
|
||||
- ohash
|
||||
- oklab
|
||||
- oxlint
|
||||
- pbbf
|
||||
- personaltoolbar
|
||||
- portaling
|
||||
- posthog
|
||||
- prebundled
|
||||
- prefs
|
||||
- proxified
|
||||
- pWXLO
|
||||
- repost
|
||||
- rxliuli
|
||||
- sandboxed
|
||||
- scule
|
||||
- Segoe
|
||||
- sidepanel
|
||||
- sidepanels
|
||||
- solidjs
|
||||
- styl
|
||||
- subdependency
|
||||
- superlock
|
||||
- tabstrip
|
||||
- teleporting
|
||||
- topcli
|
||||
- treemap
|
||||
- triaging
|
||||
- umami
|
||||
- ungh
|
||||
- unimport
|
||||
- unocss
|
||||
- unrun
|
||||
- unstub
|
||||
- unwatch
|
||||
- USSS
|
||||
- vitepress
|
||||
- webext
|
||||
- webextension
|
||||
- webextrc
|
||||
- xcrun
|
||||
- xlink
|
||||
@@ -3,9 +3,8 @@ import { computed } from 'vue';
|
||||
// @ts-expect-error: Vitepress data-loader magic, this import is correct
|
||||
import { data } from '../loaders/blog.data';
|
||||
import BlogPostPreview from './BlogPostPreview.vue';
|
||||
import { Post } from '../utils/types';
|
||||
|
||||
const posts = computed<Post[]>(() =>
|
||||
const posts = computed(() =>
|
||||
data
|
||||
.map((post) => ({
|
||||
...post,
|
||||
@@ -18,8 +17,10 @@ const posts = computed<Post[]>(() =>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="inner-container">
|
||||
<h1>Blog</h1>
|
||||
<div>
|
||||
<div class="vp-doc">
|
||||
<h1>Blog</h1>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<BlogPostPreview v-for="post of posts" :key="post.url" :post />
|
||||
@@ -33,33 +34,37 @@ const posts = computed<Post[]>(() =>
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.container > div {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.inner-container {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
h1 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ul li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid var(--vp-c-default);
|
||||
}
|
||||
ul li:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-default);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { Ref } from 'vue';
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
import { Content, useData } from 'vitepress';
|
||||
import { PostFrontmatter } from '../utils/types';
|
||||
|
||||
const { frontmatter } = useData() as unknown as {
|
||||
frontmatter: Ref<PostFrontmatter>;
|
||||
};
|
||||
import { useData } from 'vitepress';
|
||||
|
||||
const { frontmatter } = useData();
|
||||
const date = useBlogDate(() => frontmatter.value.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-doc">
|
||||
<main class="container">
|
||||
<h1 v-html="frontmatter.title" />
|
||||
<main class="container-content">
|
||||
<h1 v-html="$frontmatter.title" />
|
||||
<p class="meta-row">
|
||||
<a
|
||||
v-for="author of frontmatter.authors"
|
||||
class="author"
|
||||
v-for="author of $frontmatter.authors"
|
||||
:key="author.github"
|
||||
:href="`https://github.com/${author.github}`"
|
||||
class="author"
|
||||
>
|
||||
<img
|
||||
:src="`https://github.com/${author.github}.png?size=96`"
|
||||
alt="Author's avatar"
|
||||
/>
|
||||
<img :src="`https://github.com/${author.github}.png?size=96`" />
|
||||
<span>{{ author.name }}</span>
|
||||
</a>
|
||||
<span>•</span>
|
||||
@@ -37,43 +29,48 @@ const date = useBlogDate(() => frontmatter.value.date);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-doc {
|
||||
vp-doc {
|
||||
display: flex;
|
||||
|
||||
.container {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
main {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding: 64px;
|
||||
}
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.meta-row > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
.author img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.author span {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.author:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,23 +31,42 @@ const date = useBlogDate(() => props.post.date);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vp-doc {
|
||||
* {
|
||||
margin-block: 10px;
|
||||
}
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
li > a > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
li > a > div:hover {
|
||||
background: var(--vp-c-default);
|
||||
}
|
||||
li .title {
|
||||
color: var(--vp-c-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
li .description {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
li .meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,65 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
patterns: Array<[intput: string, output: string]>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="no-vertical-dividers entrypoint-patterns">
|
||||
<table class="no-vertical-dividers">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-filename">Filename</th>
|
||||
<th style="width: 100%">Filename</th>
|
||||
<th></th>
|
||||
<th>Output Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pattern of patterns" :key="pattern[0]">
|
||||
<td class="cell-input">
|
||||
<tr v-for="pattern of patterns">
|
||||
<td style="white-space: nowrap; padding-right: 8px">
|
||||
<code>entrypoints/{{ pattern[0] }}</code>
|
||||
</td>
|
||||
<td class="cell-arrow">
|
||||
<td style="padding: 6px; opacity: 50%">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z"
|
||||
fill="currentColor"
|
||||
d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
<td class="cell-output">
|
||||
<td style="white-space: nowrap; padding-left: 8px">
|
||||
<code>/{{ pattern[1] }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entrypoint-patterns {
|
||||
width: 100%;
|
||||
|
||||
.col-filename {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cell-input {
|
||||
white-space: nowrap;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.cell-arrow {
|
||||
padding: 6px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cell-output {
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, Ref, toRaw } from 'vue';
|
||||
import { ref, onMounted, computed, toRaw, Ref } from 'vue';
|
||||
import ExampleSearchFilterByItem from './ExampleSearchFilterByItem.vue';
|
||||
import ExampleSearchResult from './ExampleSearchResult.vue';
|
||||
import { ExamplesMetadata, KeySelectedObject } from '../utils/types';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
const exampleMetadata = ref<ExamplesMetadata>({
|
||||
allApis: [],
|
||||
allPackages: [],
|
||||
allPermissions: [],
|
||||
examples: [],
|
||||
});
|
||||
const exampleMetadata = ref<ExamplesMetadata>();
|
||||
onMounted(async () => {
|
||||
const res = await fetch(
|
||||
'https://raw.githubusercontent.com/wxt-dev/examples/main/metadata.json',
|
||||
@@ -53,7 +48,7 @@ function doesExampleMatchSelected(
|
||||
|
||||
const filteredExamples = computed(() => {
|
||||
const text = searchText.value.toLowerCase();
|
||||
return exampleMetadata.value?.examples.filter((example) => {
|
||||
return exampleMetadata.value.examples.filter((example) => {
|
||||
const matchesText = example.searchText.toLowerCase().includes(text);
|
||||
const matchesApis = doesExampleMatchSelected(example.apis, requiredApis);
|
||||
const matchesPermissions = doesExampleMatchSelected(
|
||||
@@ -77,19 +72,19 @@ const filteredExamples = computed(() => {
|
||||
|
||||
<div class="filters">
|
||||
<ExampleSearchFilterByItem
|
||||
v-model="selectedApis"
|
||||
:items="exampleMetadata?.allApis"
|
||||
label="APIs"
|
||||
:items="exampleMetadata?.allApis"
|
||||
v-model="selectedApis"
|
||||
/>
|
||||
<ExampleSearchFilterByItem
|
||||
v-model="selectedPermissions"
|
||||
:items="exampleMetadata?.allPermissions"
|
||||
label="Permissions"
|
||||
:items="exampleMetadata?.allPermissions"
|
||||
v-model="selectedPermissions"
|
||||
/>
|
||||
<ExampleSearchFilterByItem
|
||||
v-model="selectedPackages"
|
||||
:items="exampleMetadata?.allPackages"
|
||||
label="Packages"
|
||||
:items="exampleMetadata?.allPackages"
|
||||
v-model="selectedPackages"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -112,57 +107,120 @@ const filteredExamples = computed(() => {
|
||||
<style scoped>
|
||||
.example-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas:
|
||||
'filters search'
|
||||
'filters results';
|
||||
'search'
|
||||
'results';
|
||||
gap: 16px;
|
||||
|
||||
@media only screen and (min-width: 710px) {
|
||||
}
|
||||
@media only screen and (min-width: 720px) {
|
||||
.example-layout {
|
||||
grid-template-columns: 256px 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.search {
|
||||
grid-area: search;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
grid-area: filters;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
@media only screen and (min-width: 710px) {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
grid-area: results;
|
||||
|
||||
.search-results {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
@media only screen and (min-width: 800px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1024px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
grid-template-areas:
|
||||
'filters search'
|
||||
'filters results';
|
||||
}
|
||||
}
|
||||
.search {
|
||||
grid-area: search;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.filters {
|
||||
display: none;
|
||||
grid-area: filters;
|
||||
}
|
||||
@media only screen and (min-width: 720px) {
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
.results {
|
||||
grid-area: results;
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.checkbox-col {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.checkbox-col .header {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.checkbox-col p {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
span {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.checkbox-col input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-col-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media only screen and (min-width: 800px) {
|
||||
.search-results {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1024px) {
|
||||
.search-results {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, toRaw } from 'vue';
|
||||
import { KeySelectedObject } from '../utils/types';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
items?: string[];
|
||||
}>();
|
||||
@@ -29,12 +29,12 @@ function toggleItem(pkg: string) {
|
||||
<span>Filter by {{ label }}</span> <span v-if="count">({{ count }})</span>
|
||||
</p>
|
||||
<div class="scroll-container">
|
||||
<ul class="items">
|
||||
<ul>
|
||||
<li v-for="item in items">
|
||||
<label :title="item" class="item-label">
|
||||
<label :title="item">
|
||||
<input
|
||||
:checked="selectedItems[item]"
|
||||
type="checkbox"
|
||||
:checked="selectedItems[item]"
|
||||
@input="toggleItem(item)"
|
||||
/>
|
||||
<span>{{ item }}</span>
|
||||
@@ -51,45 +51,50 @@ function toggleItem(pkg: string) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 20px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--vp-c-brand-3) transparent;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
.scroll-container ul {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: small;
|
||||
padding: 8px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.items {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: small;
|
||||
padding: 8px 16px 16px;
|
||||
|
||||
.item-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
span {
|
||||
padding-top: 1px;
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Example } from '../utils/types';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
example: Example;
|
||||
}>();
|
||||
</script>
|
||||
@@ -17,45 +17,56 @@ defineProps<{
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
min-width: 0;
|
||||
}
|
||||
a {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 2px solid var(--vp-c-bg-soft);
|
||||
border-radius: 16px;
|
||||
color: var(--vp-c-text-1) !important;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
outline: 2px solid var(--vp-c-brand-2);
|
||||
a:hover {
|
||||
outline: 2px solid var(--vp-c-brand-2);
|
||||
}
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
|
||||
.link {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.description {
|
||||
height: 53px;
|
||||
opacity: 70%;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 120%;
|
||||
min-height: 0;
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 70%;
|
||||
font-size: 14px;
|
||||
line-height: 130%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
color: var(--vp-c-brand-2);
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
.link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
color: var(--vp-c-brand-2);
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
a:hover .link {
|
||||
opacity: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,138 +1,170 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import useListExtensionDetails from '../composables/useListExtensionDetails';
|
||||
import _extensionEntries from '../../assets/extension-showcase.yml';
|
||||
import useListExtensionDetails, {
|
||||
ChromeExtension,
|
||||
} from '../composables/useListExtensionDetails';
|
||||
|
||||
interface StoreLink {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ListedExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
shortDescription: string;
|
||||
users: number;
|
||||
rating: number | undefined;
|
||||
stores: StoreLink[];
|
||||
}
|
||||
|
||||
const extensionEntries = _extensionEntries as Array<{
|
||||
chromeId?: string;
|
||||
firefoxSlug?: string;
|
||||
edgeId?: string;
|
||||
}>;
|
||||
|
||||
const chromeIds = extensionEntries.flatMap((e) =>
|
||||
e.chromeId ? [e.chromeId] : [],
|
||||
);
|
||||
const firefoxSlugs = [
|
||||
...new Set(
|
||||
extensionEntries.flatMap((e) => (e.firefoxSlug ? [e.firefoxSlug] : [])),
|
||||
),
|
||||
// Add extension IDs to end of the list. On the website, extensions will be sorted by a combination of weekly active users and rating.
|
||||
// Change the commit message or PR title to: "docs: Added "[extension name]" to the homepage"
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo
|
||||
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
|
||||
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
|
||||
'ceicccfeikoipigeghddpocceifjelph', // Plex Skipper
|
||||
'aelkipgppclpfimeamgmlonimflbhlgf', // GitHub Custom Notifier
|
||||
'djnlaiohfaaifbibleebjggkghlmcpcj', // Fluent Read
|
||||
'nhclljcpfmmaiojbhhnkpjcfmacfcian', // Facebook Video Controls
|
||||
'mblkhbaakhbhiimkbcnmeciblfhmafna', // ElemSnap - Quick capture of webpage elements and conversion to images,
|
||||
'oajalfneblkfiejoadecnmodfpnaeblh', // MS Edge TTS (Text to Speech)
|
||||
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
'fencadnndhdeggodopebjgdfdlhcimfk', // 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
'bnacincmbaknlbegecpioobkfgejlojp', // MaxFocus: Link Preview
|
||||
'bcpgdpedphodjcjlminjbdeejccjbimp', // 汇率转换-中文版本
|
||||
'loeilaonggnalkaiiaepbegccilkmjjp', // Currency Converter Plus
|
||||
'npcnninnjghigjfiecefheeibomjpkak', // Respond Easy
|
||||
'cfkdcideecefncbglkhneoflfnmhoicc', // mindful - stay focused on your goals
|
||||
'lnhejcpclabmbgpiiomjbhalblnnbffg', // 1Proompt
|
||||
'fonflmjnjbkigocpoommgmhljdpljain', // NiceTab - https://github.com/web-dahuyou/NiceTab
|
||||
'fcffekbnfcfdemeekijbbmgmkognnmkd', // Draftly for LinkedIn
|
||||
'nkndldfehcidpejfkokbeghpnlbppdmo', // YouTube Summarized - Summarize any YouTube video
|
||||
'dbichmdlbjdeplpkhcejgkakobjbjalc', // 社媒助手 - https://github.com/iszhouhua/social-media-copilot
|
||||
'opepfpjeogkbgeigkbepobceinnfmjdd', // Dofollow Links for SEO
|
||||
'pdnenlnelpdomajfejgapbdpmjkfpjkp', // ChatGPT Writer: Use AI on Any Site (GPT-4o, Claude, Gemini, and More)
|
||||
'jobnhifpphkgoelnhnopgkdhbdkiadmj', // discord message translator
|
||||
'ncokhechhpjgjonhjnlaneglmdkfkcbj', // Habit Tracker app widget for daily habit tracking
|
||||
'lnjaiaapbakfhlbjenjkhffcdpoompki', // Catppuccin for GitHub File Explorer Icons
|
||||
'cpaedhbidlpnbdfegakhiamfpndhjpgf', // WebChat: Chat with anyone on any website
|
||||
'fcphghnknhkimeagdglkljinmpbagone', // YouTube Auto HD + FPS
|
||||
'lpomjgbicdemjkgmbnkjncgdebogkhlb', // MultiViewer Companion
|
||||
'ggiafipgeeaaahnjamgpjcgkdpanhddg', // Sync Watch - Watch videos together on any site
|
||||
'nmldnjcblcihmegipecakhmnieiofmgl', // Keyword Rank Checker
|
||||
'gppllamhaciichleihemgilcpledblpn', // YouTube Simple View - Hide distractions & more
|
||||
'pccbghdfdnnkkbcdcibchpbffdgednkf', // Propbar - Property Data Enhancer
|
||||
'lfknakglefggmdkjdfhhofkjnnolffkh', // Text Search Pro - Search by case and whole-word match!
|
||||
'mbenhbocjckkbaojacmaepiameldglij', // Invoice Generator
|
||||
'phlfhkmdofajnbhgmbmjkbkdgppgoppb', // Monthly Bill Tracker
|
||||
'macmkmchfoclhpbncclinhjflmdkaoom', // Wandpen - Instantly improve your writing with AI
|
||||
'lhmgechokhmdekdpgkkemoeecelcaonm', // YouTube Hider - Remove Comments By Keywords, Usernames & Tools
|
||||
'imgheieooppmahcgniieddodaliodeeg', // QA Compass - Record standardized bug reports easily
|
||||
'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled
|
||||
'hmdcmlfkchdmnmnmheododdhjedfccka', // Eye Dropper
|
||||
'eihpmapodnppeemkhkbhikmggfojdkjd', // Cursorful - Screen Recorder with Auto Zoom
|
||||
'hjjkgbibknbahijglkffklflidncplkn', // Show IP – Live View of Website IPs for Developers
|
||||
'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator
|
||||
'ocllfkhcdopiafndigclebelbecaiocp', // ZenGram: Mindful Instagram, Your Way
|
||||
'odffpjnpocjfcaclnenaaaddghkgijdb', // Blync: Preview Links, Selection Search, AI Assistant
|
||||
'kofbbilhmnkcmibjbioafflgmpkbnmme', // HTML to Markdown - Convert webpages to markdown
|
||||
'boecmgggeigllcdocgioijmleimjbfkg', // Walmart WFS Profit Calculator
|
||||
'dlnjcbkmomenmieechnmgglgcljhoepd', // Youtube Live Chat Fullscreen
|
||||
'keiealdacakpnbbljlmhfgcebmaadieg', // Python Code Runner
|
||||
'hafcajcllbjnoolpfngclfmmgpikdhlm', // Monochromate
|
||||
'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager
|
||||
'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder
|
||||
'gdjampjdgjmbifnhldgcnccdjkcoicmg', // radiofrance - news & broadcasts (French), music (international)
|
||||
'jlnhphlghikichhgbnkepenehbmloenb', // Blens - Time Tracker and AI Insight
|
||||
'njnammmpdodmfkodnfpammnpdcbhnlcm', // Always Light Mode - Setting website always in light mode
|
||||
'lblmfclcfniabobmamfkdogcgdagbhhb', // DesignPicker - Color Picker & Font Detector
|
||||
'pamnlaoeobcmhkliljfaofekeddpmfoh', // Web to PDF
|
||||
'jmbcbeepjfenihlocplnbmbhimcoooka', // Online CSV Viewer
|
||||
'nkjcoophmpcmmgadnljnlpbpfdfacgbo', // YouTube Video Transcript
|
||||
'lcaieahkjgeggeiihblhcjbbjlppgieh', // NetSuite Record Scripts
|
||||
'gmocfknjllodfiomnljmaehcplnekhlo', // VueTracker
|
||||
'ggcfemmoabhhelfkhknhbnkmeahloiod', // CanCopy - A web extension that allow you to copy any content from website
|
||||
'modkelfkcfjpgbfmnbnllalkiogfofhb', // Language Learning with AI
|
||||
'npfopljnjbamegincfjelhjhnonnjloo', // Bilibili Feed History Helper
|
||||
'edkhpdceeinkcacjdgebjehipmnbomce', // NZBDonkey - The ultimate NZB file download tool
|
||||
'cckggnbnimdbbpmdinkkgbbncopbloob', // WeChat Markdown Editor(微信 Markdown 编辑器)
|
||||
'jcblcjolcojmfopefcighfmkkefbaofg', // Tab Grab
|
||||
'eehmoikadcijkapfjocnhjclpbaindlb', // BrowserLens - https://browserlens.com/
|
||||
'hfhellofkjebbchcdffmicekjdomkcmc', // Epic Games Library Extension
|
||||
'gknigcbhlammoakmmdddkblknanpjiac', // Zen Analytics Pixel Tracker - zapt.web.app
|
||||
'cnklededohhcbmjjdlbjdkkihkgoggol', // Crypto Pulse - Compose your newtab with nature images, widgets & realtime Crypto Price & Bitcoin RSS.
|
||||
'miponnamafdenpgjemkknimgjfibicdc', // Youtube Video Scheduler
|
||||
'nhmbcmalgpkjbomhlhgdicanmkkaajmg', // Chatslator: Livestream Chat Translator
|
||||
'mbamjfdjbcdgpopfnkkmlohadbbnplhm', // 公众号阅读增强器 - https://wxreader.honwhy.wang
|
||||
'hannhecbnjnnbbafffmogdlnajpcomek', // 토탐정
|
||||
'ehboaofjncodknjkngdggmpdinhdoijp', // 2FAS Pass - https://2fas.com/
|
||||
'hnjamiaoicaepbkhdoknhhcedjdocpkd', // Quick Prompt - https://github.com/wenyuanw/quick-prompt
|
||||
'kacblhilkacgfnkjfodalohcnllcgmjd', // Add QR Code Generator Icon Back To Address Bar
|
||||
'fkbdlogfdjmpfepbbbjcgcfbgbcfcnne', // Piwik PRO Tracking Helper
|
||||
'nkbikckldmljjiiajklecmgmajgapbfl', // PIPX - Take Control of Picture-in-Picture, Automatically
|
||||
'hgppdobcpkfkmiegekaglonjajeojmdd', // Browsely - AI-powered browser extension
|
||||
'ehmoihnjgkdimihkhokkmfjdgomohjgm', // Filmbudd Pro - Simple, private – and synced ratings and watch notes across all your devices
|
||||
'alglchohmdikgdjhafiicilegegieafa', // MultiField CopyCat - Copy, Paste & Autofill Web Forms Instantly
|
||||
'aamihahiiogceidpbnfgehacgiecephe', // ChatSight - Add Table of Contents to ChatGPT
|
||||
'cndibmoanboadcifjkjbdpjgfedanolh', // BetterCampus (prev. BetterCanvas)
|
||||
'hinfimgacobnellbncbcpdlpaapcofaa', // Leetcode Fonts - Change fonts in leetcode effortlessly
|
||||
'kbkbfefhhabpkibojinapkkgciiacggg', // TranslateManga - Manga Translator & Manga Tracker
|
||||
'emeakbgdecgmdjgegnejpppcnkcnoaen', // SiteData - Free Website Traffic Checker & Reverse AdSense Tool
|
||||
'gpnckbhgpnjciklpoehkmligeaebigaa', // Livestream Chat Reader - Text-to-Speech for YouTube/Twitch chat
|
||||
'fjlalaedpfcojcfpkgkglbjjbbkofgnl', // ChatGPT Token Counter - Count tokens in real time on chatgpt conversation
|
||||
'fbgblmjbeebanackldpbmpacppflgmlj', // LinuxDo Scripts - 为 linux.do 用户提供了一些增强功能
|
||||
'dfacnjidgbagicaekenjgclfnhdnjjdi', // Zen Virtual Piano - https://zen-piano.web.app/
|
||||
'naeibcegmgpofimedkmfgjgphfhfhlab', // Crypto Pulse price tracker - https://get-crypto-pulse.web.app/
|
||||
'ffglckbhfbfmdkefdmjbhpnffkcmlhdh', // Redirect Web - Automatically redirect pages or open them in another app
|
||||
'eglpfhbhmelampoihamjomgkeobgdofl', // Capture It - Capture & Edit Screenshots
|
||||
'jmghclbfbbapimhbgnpffbimphlpolnm', // Teams Chat Exporter
|
||||
'jdcppdokgfbnhiacbeplahgnciahnhck', // Lofi BGM Player - Free lofi focus music for work & study
|
||||
'cgpmbiiagnehkikhcbnhiagfomajncpa', // Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.
|
||||
'mfjdonmgmgcijagclnkfhmjiblbfjaid', // KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows
|
||||
'dhiekgdaipindoapjmcnpompdknjeijf', // Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers
|
||||
'lapnciffpekdengooeolaienkeoilfeo', // All API Hub – AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub
|
||||
];
|
||||
const edgeIds = extensionEntries.flatMap((e) => (e.edgeId ? [e.edgeId] : []));
|
||||
|
||||
const { data, isLoading } = useListExtensionDetails(
|
||||
chromeIds,
|
||||
firefoxSlugs,
|
||||
edgeIds,
|
||||
);
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
const sortedExtensions = computed(() => {
|
||||
if (!data.value?.length) return [];
|
||||
|
||||
function addUtmSource(storeUrl: string): string {
|
||||
const url = new URL(storeUrl);
|
||||
url.searchParams.set('utm_source', 'wxt.dev');
|
||||
return url.href;
|
||||
}
|
||||
|
||||
const sortedExtensions = computed((): ListedExtension[] => {
|
||||
if (!data.value) return [];
|
||||
|
||||
const chromeById = new Map(
|
||||
(data.value.chrome ?? []).filter(Boolean).map((e) => [e.id, e]),
|
||||
);
|
||||
const firefoxBySlug = new Map(
|
||||
(data.value.firefox ?? []).filter(Boolean).map((e) => [e.id, e]),
|
||||
);
|
||||
const edgeById = new Map(
|
||||
(data.value.edge ?? []).filter(Boolean).map((e) => [e.id, e]),
|
||||
);
|
||||
|
||||
const results: ListedExtension[] = [];
|
||||
|
||||
for (const entry of extensionEntries) {
|
||||
const chrome = entry.chromeId ? chromeById.get(entry.chromeId) : undefined;
|
||||
const firefox = entry.firefoxSlug
|
||||
? firefoxBySlug.get(entry.firefoxSlug)
|
||||
: undefined;
|
||||
const edge = entry.edgeId ? edgeById.get(entry.edgeId) : undefined;
|
||||
|
||||
if (!chrome && !firefox && !edge) continue;
|
||||
|
||||
const primary = chrome ?? firefox ?? edge!;
|
||||
const users =
|
||||
(chrome?.users ?? 0) + (firefox?.users ?? 0) + (edge?.users ?? 0);
|
||||
const rating = chrome?.rating ?? firefox?.rating ?? edge?.rating;
|
||||
|
||||
const stores: StoreLink[] = [];
|
||||
if (chrome) {
|
||||
stores.push({ label: 'Chrome', url: addUtmSource(chrome.storeUrl) });
|
||||
}
|
||||
if (firefox) {
|
||||
stores.push({ label: 'Firefox', url: addUtmSource(firefox.storeUrl) });
|
||||
}
|
||||
if (edge) {
|
||||
stores.push({ label: 'Edge', url: addUtmSource(edge.storeUrl) });
|
||||
}
|
||||
|
||||
results.push({
|
||||
id:
|
||||
entry.chromeId ??
|
||||
(entry.firefoxSlug
|
||||
? `firefox:${entry.firefoxSlug}`
|
||||
: `edge:${entry.edgeId}`),
|
||||
name: primary.name,
|
||||
iconUrl: primary.iconUrl,
|
||||
shortDescription: primary.shortDescription,
|
||||
users,
|
||||
rating,
|
||||
stores,
|
||||
});
|
||||
}
|
||||
|
||||
const score = (e: ListedExtension) => ((e.rating ?? 5) / 5) * e.users;
|
||||
return results.sort((a, b) => score(b) - score(a));
|
||||
return [...data.value]
|
||||
.filter((item) => item != null)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
// Sort based on the user count weighted by the rating
|
||||
sortKey: ((item.rating ?? 5) / 5) * item.weeklyActiveUsers,
|
||||
}))
|
||||
.filter((item) => !!item)
|
||||
.sort((l, r) => r.sortKey - l.sortKey);
|
||||
});
|
||||
|
||||
function formatUsers(n: number): string {
|
||||
return `${n.toLocaleString()} users`;
|
||||
}
|
||||
|
||||
function formatStars(r: number): string {
|
||||
const rounded = Math.round(r * 10) / 10;
|
||||
return `${rounded % 1 === 0 ? rounded.toFixed(0) : rounded.toFixed(1)} stars`;
|
||||
function getStoreUrl(extension: ChromeExtension) {
|
||||
const url = new URL(extension.storeUrl);
|
||||
url.searchParams.set('utm_source', 'wxt.dev');
|
||||
return url.href;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-if="isLoading" style="text-align: center; opacity: 50%">Loading...</p>
|
||||
<p
|
||||
v-else-if="sortedExtensions.length === 0"
|
||||
v-else-if="err || sortedExtensions.length === 0"
|
||||
style="text-align: center; opacity: 50%"
|
||||
>
|
||||
Failed to load extension details.
|
||||
</p>
|
||||
<ul v-else class="extension-grid">
|
||||
<li v-for="extension of sortedExtensions" :key="extension.id">
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="extension of sortedExtensions"
|
||||
:key="extension.id"
|
||||
class="relative"
|
||||
>
|
||||
<img
|
||||
:src="extension.iconUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div class="card-content">
|
||||
<div class="relative">
|
||||
<a
|
||||
:href="extension.stores[0]?.url ?? '#'"
|
||||
:href="getStoreUrl(extension)"
|
||||
target="_blank"
|
||||
:title="extension.name"
|
||||
class="extension-name"
|
||||
@@ -141,39 +173,19 @@ function formatStars(r: number): string {
|
||||
<p class="description" :title="extension.shortDescription">
|
||||
{{ extension.shortDescription }}
|
||||
</p>
|
||||
<div v-if="extension.stores.length > 0" class="store-stats">
|
||||
<p class="store-stats-info">
|
||||
<span>{{ formatUsers(extension.users) }}</span>
|
||||
<template v-if="extension.rating">
|
||||
<span class="store-stats-sep" aria-hidden="true">,</span>
|
||||
<span>{{ formatStars(extension.rating) }}</span>
|
||||
</template>
|
||||
</p>
|
||||
<p class="store-stats-sep" aria-hidden="true">·</p>
|
||||
<p class="store-links">
|
||||
<template v-for="(store, i) of extension.stores" :key="store.label">
|
||||
<a
|
||||
:href="store.url"
|
||||
target="_blank"
|
||||
class="store-link"
|
||||
:title="store.label"
|
||||
>{{ store.label }}</a
|
||||
>
|
||||
<span
|
||||
v-if="i < extension.stores.length - 1"
|
||||
class="store-stats-sep"
|
||||
aria-hidden="true"
|
||||
>, </span
|
||||
>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="user-count">
|
||||
<span>{{ extension.weeklyActiveUsers.toLocaleString() }} users</span
|
||||
><template v-if="extension.rating != null"
|
||||
>,
|
||||
<span>{{ extension.rating }} stars</span>
|
||||
</template>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="centered pr">
|
||||
<a
|
||||
href="https://github.com/wxt-dev/wxt/edit/main/docs/assets/extension-showcase.yml"
|
||||
href="https://github.com/wxt-dev/wxt/edit/main/docs/.vitepress/components/UsingWxtSection.vue"
|
||||
target="_blank"
|
||||
>Open a PR</a
|
||||
>
|
||||
@@ -182,7 +194,7 @@ function formatStars(r: number): string {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.extension-grid > li > img {
|
||||
li img {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
padding: 16px;
|
||||
@@ -190,7 +202,7 @@ function formatStars(r: number): string {
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
.extension-grid {
|
||||
ul {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
align-items: stretch;
|
||||
@@ -200,12 +212,12 @@ function formatStars(r: number): string {
|
||||
padding: 0;
|
||||
}
|
||||
@media (min-width: 960px) {
|
||||
.extension-grid {
|
||||
ul {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.extension-grid > li {
|
||||
li {
|
||||
margin: 0 !important;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
@@ -213,68 +225,28 @@ function formatStars(r: number): string {
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.extension-grid > li .description {
|
||||
li a,
|
||||
li .user-count,
|
||||
li .description {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.extension-grid > li .card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-right: 0;
|
||||
li .user-count {
|
||||
opacity: 70%;
|
||||
font-size: small;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.store-stats {
|
||||
margin-top: auto;
|
||||
padding-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: baseline;
|
||||
column-gap: 6px;
|
||||
row-gap: 4px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
opacity: 0.72;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.store-stats-info {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.store-stats-sep {
|
||||
opacity: 0.45;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.store-links {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.store-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-link:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.extension-grid > li .extension-name {
|
||||
li a {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -283,23 +255,33 @@ function formatStars(r: number): string {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.extension-grid > li .extension-name:hover {
|
||||
li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.extension-grid > li .description {
|
||||
li div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
li .description {
|
||||
opacity: 90%;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
li .extension-name {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.pr {
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,71 +1,39 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface Extension {
|
||||
export interface ChromeExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
weeklyActiveUsers: number;
|
||||
shortDescription: string;
|
||||
storeUrl: string;
|
||||
rating: number | undefined;
|
||||
users: number;
|
||||
}
|
||||
|
||||
export interface ExtensionResults {
|
||||
chrome: Extension[];
|
||||
firefox: Extension[];
|
||||
edge: Extension[];
|
||||
}
|
||||
|
||||
const operationName = 'WxtDocsUsedBy';
|
||||
const query = `query ${operationName}($chromeIds: [String!]!, $firefoxIds: [String!]!, $edgeIds: [String!]!) {
|
||||
chromeExtensions(ids: $chromeIds) {
|
||||
const query = `query ${operationName}($ids:[String!]!) {
|
||||
chromeExtensions(ids: $ids) {
|
||||
id
|
||||
...ExtensionData
|
||||
name
|
||||
iconUrl
|
||||
weeklyActiveUsers
|
||||
shortDescription
|
||||
storeUrl
|
||||
rating
|
||||
}
|
||||
firefoxAddons(ids: $firefoxIds) {
|
||||
id: slug
|
||||
...ExtensionData
|
||||
}
|
||||
edgeAddons(ids: $edgeIds) {
|
||||
id
|
||||
...ExtensionData
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExtensionData on Extension {
|
||||
name
|
||||
iconUrl
|
||||
shortDescription
|
||||
storeUrl
|
||||
rating
|
||||
users
|
||||
}`;
|
||||
|
||||
export default function (
|
||||
chromeIds: string[],
|
||||
firefoxSlugs: string[],
|
||||
edgeIds: string[],
|
||||
) {
|
||||
const data = ref<ExtensionResults>();
|
||||
export default function (ids: string[]) {
|
||||
const data = ref<ChromeExtension[]>();
|
||||
const err = ref<unknown>();
|
||||
const isLoading = ref(true);
|
||||
|
||||
if (
|
||||
chromeIds.length === 0 &&
|
||||
firefoxSlugs.length === 0 &&
|
||||
edgeIds.length === 0
|
||||
) {
|
||||
data.value = { chrome: [], firefox: [], edge: [] };
|
||||
isLoading.value = false;
|
||||
return { data, err, isLoading };
|
||||
}
|
||||
|
||||
fetch('https://queue.wxt.dev/api', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName,
|
||||
query,
|
||||
variables: { chromeIds, firefoxIds: firefoxSlugs, edgeIds },
|
||||
variables: { ids },
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -73,12 +41,10 @@ export default function (
|
||||
})
|
||||
.then(async (res) => {
|
||||
isLoading.value = false;
|
||||
const { data: responseData } = await res.json();
|
||||
data.value = {
|
||||
chrome: responseData.chromeExtensions ?? [],
|
||||
firefox: responseData.firefoxAddons ?? [],
|
||||
edge: responseData.edgeAddons ?? [],
|
||||
};
|
||||
const {
|
||||
data: { chromeExtensions },
|
||||
} = await res.json();
|
||||
data.value = chromeExtensions;
|
||||
err.value = undefined;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -88,5 +54,9 @@ export default function (
|
||||
err.value = error;
|
||||
});
|
||||
|
||||
return { data, err, isLoading };
|
||||
return {
|
||||
data,
|
||||
err,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
+24
-38
@@ -1,26 +1,5 @@
|
||||
import { Feed } from 'feed';
|
||||
// @ts-expect-error; It isn't TypeScript lib
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import fs, { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import addKnowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
localIconLoader,
|
||||
} from 'vitepress-plugin-group-icons';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { version as analyticsVersion } from '../../packages/analytics/package.json';
|
||||
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
|
||||
import { version as i18nVersion } from '../../packages/i18n/package.json';
|
||||
import { version as runnerVersion } from '../../packages/runner/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as wxtVersion } from '../../packages/wxt/package.json';
|
||||
import { version as isBackgroundVersion } from '../../packages/is-background/package.json';
|
||||
import typedocSidebar from '../api/reference/typedoc-sidebar.json';
|
||||
import { meta, script } from './utils/head';
|
||||
import {
|
||||
menuGroup,
|
||||
menuItem,
|
||||
@@ -28,6 +7,26 @@ import {
|
||||
navItem,
|
||||
prepareTypedocSidebar,
|
||||
} from './utils/menus';
|
||||
import { meta, script } from './utils/head';
|
||||
import footnote from 'markdown-it-footnote';
|
||||
import { version as wxtVersion } from '../../packages/wxt/package.json';
|
||||
import { version as i18nVersion } from '../../packages/i18n/package.json';
|
||||
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
|
||||
import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { version as analyticsVersion } from '../../packages/analytics/package.json';
|
||||
import { version as runnerVersion } from '../../packages/runner/package.json';
|
||||
import { version as isBackgroundVersion } from '../../packages/is-background/package.json';
|
||||
import addKnowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
localIconLoader,
|
||||
} from 'vitepress-plugin-group-icons';
|
||||
import { Feed } from 'feed';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
|
||||
const origin = 'https://wxt.dev';
|
||||
|
||||
@@ -74,11 +73,8 @@ export default defineConfig({
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
//TODO: REMOVE THIS @TS-EXPECT-ERROR AFTER BUMP VITEPRESS TO V2.0
|
||||
plugins: [
|
||||
// @ts-expect-error: Vite version mismatch between this project and the plugin
|
||||
llmstxt(),
|
||||
// @ts-expect-error: Vite version mismatch between this project and the plugin
|
||||
groupIconVitePlugin({
|
||||
customIcon: {
|
||||
'wxt.config.ts': localIconLoader(
|
||||
@@ -87,17 +83,6 @@ export default defineConfig({
|
||||
),
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'yaml-loader',
|
||||
load: {
|
||||
handler: async (id) => {
|
||||
if (id.endsWith('.yml') || id.endsWith('.yaml')) {
|
||||
const obj = Bun.YAML.parse(await readFile(id, 'utf8'));
|
||||
return `export default ${JSON.stringify(obj)}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
lastUpdated: true,
|
||||
@@ -128,7 +113,7 @@ export default defineConfig({
|
||||
});
|
||||
// console.log('rss.xml:');
|
||||
// console.log(feed.rss2());
|
||||
await fs.writeFile(join(site.outDir, 'rss.xml'), feed.rss2());
|
||||
await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');
|
||||
},
|
||||
|
||||
head: [
|
||||
@@ -163,7 +148,8 @@ export default defineConfig({
|
||||
|
||||
footer: {
|
||||
message: [
|
||||
'<span class="netlify-badge"><a class="light-netlify" href="https://www.netlify.com"><img src="/netlify-badge-light.svg" alt="Deploys by Netlify" /></a><a class="dark-netlify" href="https://www.netlify.com"><img src="/netlify-badge-dark.svg" alt="Deploys by Netlify" /></a></span>',
|
||||
'<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>',
|
||||
'<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>',
|
||||
'Released under the <a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE">MIT License</a>.',
|
||||
].join('<br/>'),
|
||||
copyright:
|
||||
@@ -238,11 +224,11 @@ export default defineConfig({
|
||||
menuItem('Storage', 'storage.md'),
|
||||
menuItem('Messaging', 'messaging.md'),
|
||||
menuItem('I18n', 'i18n.md'),
|
||||
menuItem('Favicons', 'favicons.md'),
|
||||
menuItem('Scripting', 'scripting.md'),
|
||||
menuItem('WXT Modules', 'wxt-modules.md'),
|
||||
menuItem('Frontend Frameworks', 'frontend-frameworks.md'),
|
||||
menuItem('ES Modules', 'es-modules.md'),
|
||||
menuItem('Remote Code', 'remote-code.md'),
|
||||
menuItem('Unit Testing', 'unit-testing.md'),
|
||||
menuItem('E2E Testing', 'e2e-testing.md'),
|
||||
menuItem('Publishing', 'publishing.md'),
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { type ContentData, createContentLoader } from 'vitepress';
|
||||
import { PostFrontmatter } from '../utils/types';
|
||||
import { createContentLoader } from 'vitepress';
|
||||
|
||||
declare const data: Array<ContentData & { frontmatter: PostFrontmatter }>;
|
||||
export { data };
|
||||
|
||||
export default createContentLoader<
|
||||
Array<ContentData & { frontmatter: PostFrontmatter }>
|
||||
>('blog/*.md');
|
||||
export default createContentLoader('blog/*.md');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { resolve } from 'node:path';
|
||||
import consola from 'consola';
|
||||
import spawn from 'nano-spawn';
|
||||
import { resolve } from 'node:path';
|
||||
import { version } from '../../../packages/wxt/package.json';
|
||||
|
||||
const cliDir = resolve('packages/wxt/src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
@@ -39,20 +38,24 @@ export default {
|
||||
|
||||
async function getHelp(command: string): Promise<string> {
|
||||
const args = command.split(' ');
|
||||
const result = await spawn(args[0], [...args.slice(1), '--help'], {
|
||||
const res = await spawn(args[0], [...args.slice(1), '--help'], {
|
||||
cwd: 'packages/wxt',
|
||||
});
|
||||
return result.stdout;
|
||||
return res.stdout;
|
||||
}
|
||||
|
||||
async function getWxtHelp(command: string): Promise<string> {
|
||||
const res = await getHelp(`bun run --silent wxt ${command}`.trim());
|
||||
return res.replaceAll('{{version}}', version);
|
||||
function getWxtHelp(command: string): Promise<string> {
|
||||
return getHelp(`pnpm -s wxt ${command}`.trim());
|
||||
}
|
||||
|
||||
async function getPublishExtensionHelp(command: string): Promise<string> {
|
||||
const res = await getHelp(
|
||||
`bun run --silent publish-extension ${command}`.trim(),
|
||||
`./node_modules/.bin/publish-extension ${command}`.trim(),
|
||||
);
|
||||
return res.replace(/\$ publish-extension/g, '$ wxt submit');
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
docs: string;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
/* Colors */
|
||||
:root {
|
||||
--wxt-c-green-1: #0b8a00;
|
||||
--wxt-c-green-2: #096600;
|
||||
--wxt-c-green-3: #096600;
|
||||
--wxt-c-green-soft: rgba(11, 138, 0, 0.14);
|
||||
|
||||
--vp-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
--vp-c-brand-soft: var(--wxt-c-green-soft);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -19,69 +13,56 @@
|
||||
--wxt-c-green-soft: rgba(103, 212, 94, 0.14);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
|
||||
:root {
|
||||
--vp-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
--vp-c-brand-soft: var(--wxt-c-green-soft);
|
||||
}
|
||||
|
||||
/* Customize Individual Components */
|
||||
.vp-doc .no-vertical-dividers {
|
||||
th,
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.vp-doc .no-vertical-dividers th,
|
||||
.vp-doc .no-vertical-dividers td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vp-doc .no-vertical-dividers tr {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.light-netlify {
|
||||
display: inline-flex;
|
||||
.VPSidebar {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.VPSidebarItem .badge {
|
||||
display: inline-block;
|
||||
min-width: 1.6em;
|
||||
padding: 0.3em 0.4em 0.2em;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75em;
|
||||
line-height: 1;
|
||||
margin-left: 0.5rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background-color: var(--vp-c-default-2);
|
||||
}
|
||||
|
||||
.light-netlify {
|
||||
display: inline;
|
||||
}
|
||||
.dark .light-netlify {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark-netlify {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark .dark-netlify {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Add Aaron's favorite pic from Artemis II to the hero section because it's cool. */
|
||||
.dark .VPHome::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-image: url('../../assets/art002e009298.jpg');
|
||||
opacity: 50%;
|
||||
width: 100%;
|
||||
height: 110vh;
|
||||
background-size: cover;
|
||||
background-position: 90%;
|
||||
}
|
||||
|
||||
.dark .VPHome::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 10vh;
|
||||
top: 100vh;
|
||||
background: linear-gradient(to bottom, transparent, var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.item {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.VPSidebar .curtain {
|
||||
z-index: 2;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,15 @@ import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import BlogLayout from '../components/BlogLayout.vue';
|
||||
import './custom.css';
|
||||
import 'virtual:group-icons.css';
|
||||
import type { EnhanceAppContext } from 'vitepress/client';
|
||||
import { setupCodeGroupSync } from '../utils/code-groups-sync';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx: EnhanceAppContext) {
|
||||
enhanceApp(ctx) {
|
||||
ctx.app
|
||||
.component('Icon', Icon)
|
||||
.component('EntrypointPatterns', EntrypointPatterns)
|
||||
.component('UsingWxtSection', UsingWxtSection)
|
||||
.component('ExampleSearch', ExampleSearch)
|
||||
.component('blog', BlogLayout);
|
||||
|
||||
setupCodeGroupSync();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
const CODE_GROUP_SELECTORS = {
|
||||
root: '.vp-code-group',
|
||||
input: '.vp-code-group .tabs input',
|
||||
label: '.tabs label',
|
||||
} as const;
|
||||
|
||||
function getTabTitle(label: HTMLLabelElement) {
|
||||
return label.dataset.title?.trim() || label.textContent?.trim();
|
||||
}
|
||||
|
||||
function getCodeGroupTitle(input: HTMLInputElement) {
|
||||
const label = input.parentElement?.querySelector<HTMLLabelElement>(
|
||||
`label[for="${input.id}"]`,
|
||||
);
|
||||
|
||||
return label ? getTabTitle(label) : undefined;
|
||||
}
|
||||
|
||||
function findCodeGroupTabIndex(group: HTMLElement, title: string) {
|
||||
return Array.from(
|
||||
group.querySelectorAll<HTMLLabelElement>(CODE_GROUP_SELECTORS.label),
|
||||
).findIndex((label) => getTabTitle(label) === title);
|
||||
}
|
||||
|
||||
function syncCodeGroupSelection(group: HTMLElement, title: string) {
|
||||
const targetIndex = findCodeGroupTabIndex(group, title);
|
||||
|
||||
if (targetIndex < 0) return;
|
||||
|
||||
const blocks = group.querySelector('.blocks');
|
||||
const targetBlock = blocks?.children[targetIndex];
|
||||
if (!blocks || !targetBlock || targetBlock.classList.contains('active'))
|
||||
return;
|
||||
|
||||
blocks.querySelector('.active')?.classList.remove('active');
|
||||
targetBlock.classList.add('active');
|
||||
|
||||
const input = group.querySelectorAll<HTMLInputElement>(
|
||||
CODE_GROUP_SELECTORS.input,
|
||||
)[targetIndex];
|
||||
if (input) input.checked = true;
|
||||
}
|
||||
|
||||
function syncCodeGroupTabs(event: Event) {
|
||||
const target = event.target;
|
||||
if (
|
||||
!(target instanceof HTMLInputElement) ||
|
||||
!target.matches(CODE_GROUP_SELECTORS.input)
|
||||
)
|
||||
return;
|
||||
|
||||
const activeGroup = target.closest(CODE_GROUP_SELECTORS.root);
|
||||
const activeTitle = getCodeGroupTitle(target);
|
||||
if (!activeTitle) return;
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(CODE_GROUP_SELECTORS.root)
|
||||
.forEach((group) => {
|
||||
if (group === activeGroup) return;
|
||||
|
||||
syncCodeGroupSelection(group, activeTitle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps every code group on the page in sync: selecting a package manager (or
|
||||
* any tab) in one group switches the matching tab in all the other groups.
|
||||
*/
|
||||
export function setupCodeGroupSync() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
window.addEventListener('change', syncCodeGroupTabs, true);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HeadConfig } from 'vitepress';
|
||||
import { HeadConfig } from 'vitepress/types/shared';
|
||||
|
||||
export function meta(
|
||||
property: string,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DefaultTheme } from 'vitepress';
|
||||
|
||||
type SidebarItem = DefaultTheme.SidebarItem;
|
||||
type NavItem = DefaultTheme.NavItem;
|
||||
type NavItemWithLink = DefaultTheme.NavItemWithLink;
|
||||
type NavItemWithChildren = DefaultTheme.NavItemWithChildren;
|
||||
type NavItemChildren = DefaultTheme.NavItemChildren;
|
||||
|
||||
@@ -17,7 +19,7 @@ export function navItem(text: string, arg2?: unknown): any {
|
||||
}
|
||||
|
||||
export function menuRoot(items: SidebarItem[]) {
|
||||
return items.map((item) => {
|
||||
return items.map((item, index) => {
|
||||
// item.collapsed = false; // uncomment to expand all level-0 items
|
||||
return item;
|
||||
});
|
||||
@@ -58,6 +60,12 @@ export function menuGroup(
|
||||
throw Error('Unknown overload');
|
||||
}
|
||||
|
||||
export function menuItems(items: SidebarItem[]) {
|
||||
return {
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
export function menuItem(
|
||||
text: string,
|
||||
link: string,
|
||||
@@ -69,7 +77,9 @@ export function menuItem(
|
||||
return { text, link };
|
||||
}
|
||||
|
||||
/** Clean up and add badges to typedoc leaf sections */
|
||||
/**
|
||||
* Clean up and add badges to typedoc leaf sections
|
||||
*/
|
||||
export function prepareTypedocSidebar(items: SidebarItem[]) {
|
||||
// skip contents file
|
||||
const filtered = items.slice(1);
|
||||
@@ -89,7 +99,9 @@ export function prepareTypedocSidebar(items: SidebarItem[]) {
|
||||
}
|
||||
};
|
||||
|
||||
// process
|
||||
prepareItems(filtered);
|
||||
|
||||
// return
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ContentData } from 'vitepress';
|
||||
|
||||
export interface Example {
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -18,15 +16,3 @@ export type ExamplesMetadata = {
|
||||
};
|
||||
|
||||
export type KeySelectedObject = Record<string, boolean | undefined>;
|
||||
|
||||
export interface PostFrontmatter {
|
||||
title: string;
|
||||
description?: string;
|
||||
date: Date;
|
||||
authors: { name: string; github: string }[];
|
||||
}
|
||||
|
||||
export interface Post
|
||||
extends Omit<ContentData, 'frontmatter'>, Omit<PostFrontmatter, 'date'> {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 237 KiB |
@@ -1,533 +0,0 @@
|
||||
# Add extension entries to end of the list. On the website, extensions will be sorted by a combination of users and rating.
|
||||
#
|
||||
# Change the commit message or PR title to:
|
||||
#
|
||||
# > "docs: Added "[extension name]" to showcase"
|
||||
#
|
||||
# Use:
|
||||
#
|
||||
# - `chromeId` for the Chrome Web Store listing,
|
||||
# - `firefoxSlug` for Firefox Add-ons (slug from https://addons.mozilla.org/firefox/addon/{slug}/),
|
||||
# - `edgeId` for the Edge Add-ons listing (ID from https://microsoftedge.microsoft.com/addons/detail/{slug}/{id})
|
||||
#
|
||||
# You may include one or more of these fields for your extension.
|
||||
|
||||
- # Anylang Translator
|
||||
chromeId: injdenmpjncknciccmjjmjlgbmnkince
|
||||
firefoxSlug: anylang-translator
|
||||
|
||||
- # GitHub: Better Line Counts
|
||||
chromeId: ocfdgncpifmegplaglcnglhioflaimkd
|
||||
firefoxSlug: github-better-line-counts
|
||||
|
||||
- # Anime Skip Player
|
||||
chromeId: mgmdkjcljneegjfajchedjpdhbadklcf
|
||||
firefoxSlug: anime-skip
|
||||
|
||||
- # UltraWideo
|
||||
chromeId: bfbnagnphiehemkdgmmficmjfddgfhpl
|
||||
firefoxSlug: ultrawideo
|
||||
|
||||
- # StayFree - Website Blocker & Web Analytics
|
||||
chromeId: elfaihghhjjoknimpccccmkioofjjfkf
|
||||
firefoxSlug: stayfree
|
||||
edgeId: aghmglalnfmjbipodadgbgpakneabgnp
|
||||
|
||||
- # Doozy: Ai Made Easy
|
||||
chromeId: okifoaikfmpfcamplcfjkpdnhfodpkil
|
||||
|
||||
- # tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
chromeId: lknmjhcajhfbbglglccadlfdjbaiifig
|
||||
|
||||
- # Youtube中文配音
|
||||
chromeId: oglffgiaiekgeicdgkdlnlkhliajdlja
|
||||
|
||||
- # PreMiD
|
||||
chromeId: agjnjboanicjcpenljmaaigopkgdnihi
|
||||
|
||||
- # Markdown Sticky Notes
|
||||
chromeId: aiakblgmlabokilgljkglggnpflljdgp
|
||||
firefoxSlug: markdown-sticky-notes
|
||||
|
||||
- # DocVersionRedirector
|
||||
chromeId: nomnkbngkijpffepcgbbofhcnafpkiep
|
||||
|
||||
- # Plex Skipper
|
||||
chromeId: ceicccfeikoipigeghddpocceifjelph
|
||||
firefoxSlug: plex-skipper
|
||||
|
||||
- # GitHub Custom Notifier
|
||||
chromeId: aelkipgppclpfimeamgmlonimflbhlgf
|
||||
firefoxSlug: github-custom-notifier
|
||||
|
||||
- # Fluent Read
|
||||
chromeId: djnlaiohfaaifbibleebjggkghlmcpcj
|
||||
|
||||
- # Facebook Video Controls
|
||||
chromeId: nhclljcpfmmaiojbhhnkpjcfmacfcian
|
||||
firefoxSlug: facebook-video-controls
|
||||
|
||||
- # ElemSnap - Quick capture of webpage elements and conversion to images,
|
||||
chromeId: mblkhbaakhbhiimkbcnmeciblfhmafna
|
||||
|
||||
- # MS Edge TTS (Text to Speech)
|
||||
chromeId: oajalfneblkfiejoadecnmodfpnaeblh
|
||||
firefoxSlug: ms-edge-tts-text-to-speech
|
||||
|
||||
- # YTBlock - Block any content from YouTube™
|
||||
chromeId: nedcanggplmbbgmlpcjiafgjcpdimpea
|
||||
|
||||
- # demo.fun - Interactive product demos that convert
|
||||
chromeId: oadbjpccljkplmhnjekgjamejnbadlne
|
||||
|
||||
- # SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
chromeId: iopdafdcollfgaoffingmahpffckmjni
|
||||
|
||||
- # WorkFlowy MultiFlow
|
||||
chromeId: khjdmjcmpolknpccmaaipmidphjokhdf
|
||||
|
||||
- # 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
chromeId: fencadnndhdeggodopebjgdfdlhcimfk
|
||||
|
||||
- # MaxFocus: Link Preview
|
||||
chromeId: bnacincmbaknlbegecpioobkfgejlojp
|
||||
firefoxSlug: maxfocus-link-preview
|
||||
|
||||
- # 汇率转换-中文版本
|
||||
chromeId: bcpgdpedphodjcjlminjbdeejccjbimp
|
||||
|
||||
- # Currency Converter Plus
|
||||
chromeId: loeilaonggnalkaiiaepbegccilkmjjp
|
||||
firefoxSlug: currency-converter-plus
|
||||
|
||||
- # Respond Easy
|
||||
chromeId: npcnninnjghigjfiecefheeibomjpkak
|
||||
|
||||
- # mindful - stay focused on your goals
|
||||
chromeId: cfkdcideecefncbglkhneoflfnmhoicc
|
||||
|
||||
- # 1Proompt
|
||||
chromeId: lnhejcpclabmbgpiiomjbhalblnnbffg
|
||||
|
||||
- # NiceTab - https://github.com/web-dahuyou/NiceTab
|
||||
chromeId: fonflmjnjbkigocpoommgmhljdpljain
|
||||
firefoxSlug: nice-tab-manager
|
||||
edgeId: ompjiaelpibiggcnanhbdblkhfdmkgnl
|
||||
|
||||
- # Draftly for LinkedIn
|
||||
chromeId: fcffekbnfcfdemeekijbbmgmkognnmkd
|
||||
|
||||
- # YouTube Summarized - Summarize any YouTube video
|
||||
chromeId: nkndldfehcidpejfkokbeghpnlbppdmo
|
||||
|
||||
- # 社媒助手 - https://github.com/iszhouhua/social-media-copilot
|
||||
chromeId: dbichmdlbjdeplpkhcejgkakobjbjalc
|
||||
|
||||
- # Dofollow Links for SEO
|
||||
chromeId: opepfpjeogkbgeigkbepobceinnfmjdd
|
||||
|
||||
- # ChatGPT Writer: Use AI on Any Site (GPT-4o, Claude, Gemini, and More)
|
||||
chromeId: pdnenlnelpdomajfejgapbdpmjkfpjkp
|
||||
|
||||
- # discord message translator
|
||||
chromeId: jobnhifpphkgoelnhnopgkdhbdkiadmj
|
||||
|
||||
- # Habit Tracker app widget for daily habit tracking
|
||||
chromeId: ncokhechhpjgjonhjnlaneglmdkfkcbj
|
||||
|
||||
- # Catppuccin for GitHub File Explorer Icons
|
||||
chromeId: lnjaiaapbakfhlbjenjkhffcdpoompki
|
||||
firefoxSlug: catppuccin-web-file-icons
|
||||
|
||||
- # WebChat: Chat with anyone on any website
|
||||
chromeId: cpaedhbidlpnbdfegakhiamfpndhjpgf
|
||||
|
||||
- # YouTube Auto HD + FPS
|
||||
chromeId: fcphghnknhkimeagdglkljinmpbagone
|
||||
firefoxSlug: youtube-auto-hd-fps
|
||||
|
||||
- # YouTube Auto Feed
|
||||
chromeId: jcdebdlnakhdinkindpogcehnhggbfad
|
||||
firefoxSlug: youtube-auto-feed
|
||||
|
||||
- # YouTube Like-Dislike Shortcut
|
||||
chromeId: fdkpkpelkkdkjhpacficichkfifijipc
|
||||
firefoxSlug: youtube-like-dislike-shortcut
|
||||
|
||||
- # Twitch Channel Points Bonus Collector
|
||||
chromeId: kbbdnbaghpcjpdhbjbccadodjejlkkgg
|
||||
firefoxSlug: twitch-cp-bonus-collector
|
||||
|
||||
- # Skillshare Player Control
|
||||
chromeId: agbhgcomfpcfboebbfmefbicfkpnlfeg
|
||||
firefoxSlug: skillshare-player-control
|
||||
|
||||
- # MultiViewer Companion
|
||||
chromeId: lpomjgbicdemjkgmbnkjncgdebogkhlb
|
||||
firefoxSlug: multiviewer-companion
|
||||
|
||||
- # Sync Watch - Watch videos together on any site
|
||||
chromeId: ggiafipgeeaaahnjamgpjcgkdpanhddg
|
||||
firefoxSlug: syncwatch
|
||||
|
||||
- # Keyword Rank Checker
|
||||
chromeId: nmldnjcblcihmegipecakhmnieiofmgl
|
||||
firefoxSlug: keyword-rank-checker-sa
|
||||
|
||||
- # YouTube Simple View - Hide distractions & more
|
||||
chromeId: gppllamhaciichleihemgilcpledblpn
|
||||
firefoxSlug: youtube-simple-view
|
||||
|
||||
- # Propbar - Property Data Enhancer
|
||||
chromeId: pccbghdfdnnkkbcdcibchpbffdgednkf
|
||||
|
||||
- # Text Search Pro - Search by case and whole-word match!
|
||||
chromeId: lfknakglefggmdkjdfhhofkjnnolffkh
|
||||
firefoxSlug: text-search-pro-ext
|
||||
|
||||
- # Invoice Generator
|
||||
chromeId: mbenhbocjckkbaojacmaepiameldglij
|
||||
firefoxSlug: quick-invoice-generator
|
||||
|
||||
- # Monthly Bill Tracker
|
||||
chromeId: phlfhkmdofajnbhgmbmjkbkdgppgoppb
|
||||
|
||||
- # Wandpen - Instantly improve your writing with AI
|
||||
chromeId: macmkmchfoclhpbncclinhjflmdkaoom
|
||||
|
||||
- # YouTube Hider - Remove Comments By Keywords, Usernames & Tools
|
||||
chromeId: lhmgechokhmdekdpgkkemoeecelcaonm
|
||||
|
||||
- # QA Compass - Record standardized bug reports easily
|
||||
chromeId: imgheieooppmahcgniieddodaliodeeg
|
||||
|
||||
- # aesthetic Notion, styled
|
||||
chromeId: npgghjedpchajflknnbngajkjkdhncdo
|
||||
|
||||
- # Eye Dropper
|
||||
chromeId: hmdcmlfkchdmnmnmheododdhjedfccka
|
||||
firefoxSlug: eye_dropper
|
||||
|
||||
- # Cursorful - Screen Recorder with Auto Zoom
|
||||
chromeId: eihpmapodnppeemkhkbhikmggfojdkjd
|
||||
|
||||
- # Show IP – Live View of Website IPs for Developers
|
||||
chromeId: hjjkgbibknbahijglkffklflidncplkn
|
||||
|
||||
- # Strong Password Generator
|
||||
chromeId: ilbikcehnpkmldojkcmlldkoelofnbde
|
||||
firefoxSlug: strongpasswordgenerator
|
||||
|
||||
- # ZenGram: Mindful Instagram, Your Way
|
||||
chromeId: ocllfkhcdopiafndigclebelbecaiocp
|
||||
|
||||
- # Blync: Preview Links, Selection Search, AI Assistant
|
||||
chromeId: odffpjnpocjfcaclnenaaaddghkgijdb
|
||||
|
||||
- # HTML to Markdown - Convert webpages to markdown
|
||||
chromeId: kofbbilhmnkcmibjbioafflgmpkbnmme
|
||||
|
||||
- # Walmart WFS Profit Calculator
|
||||
chromeId: boecmgggeigllcdocgioijmleimjbfkg
|
||||
firefoxSlug: walmart-wfs-profit-calculator
|
||||
|
||||
- # Youtube Live Chat Fullscreen
|
||||
chromeId: dlnjcbkmomenmieechnmgglgcljhoepd
|
||||
firefoxSlug: youtube-live-chat-fullscreen
|
||||
|
||||
- # Python Code Runner
|
||||
chromeId: keiealdacakpnbbljlmhfgcebmaadieg
|
||||
firefoxSlug: code-runner-manager
|
||||
|
||||
- # Monochromate
|
||||
chromeId: hafcajcllbjnoolpfngclfmmgpikdhlm
|
||||
firefoxSlug: monochromate
|
||||
|
||||
- # AliasVault - Open-Source Password & (Email) Alias Manager
|
||||
chromeId: bmoggiinmnodjphdjnmpcnlleamkfedj
|
||||
|
||||
- # SnapThePrice: AI-Powered Real-time Lowest Price Finder
|
||||
chromeId: hlnhhamckimoaiekbglafiebkfimhapb
|
||||
|
||||
- # radiofrance - news & broadcasts (French), music (international)
|
||||
chromeId: gdjampjdgjmbifnhldgcnccdjkcoicmg
|
||||
|
||||
- # Blens - Time Tracker and AI Insight
|
||||
chromeId: jlnhphlghikichhgbnkepenehbmloenb
|
||||
firefoxSlug: blens-timetracker-ai-insight
|
||||
|
||||
- # Always Light Mode - Setting website always in light mode
|
||||
chromeId: njnammmpdodmfkodnfpammnpdcbhnlcm
|
||||
|
||||
- # DesignPicker - Color Picker & Font Detector
|
||||
chromeId: lblmfclcfniabobmamfkdogcgdagbhhb
|
||||
firefoxSlug: design-colorpicker-fontdetect
|
||||
|
||||
- # Web to PDF
|
||||
chromeId: pamnlaoeobcmhkliljfaofekeddpmfoh
|
||||
firefoxSlug: export-web-to-pdf
|
||||
|
||||
- # Online CSV Viewer
|
||||
chromeId: jmbcbeepjfenihlocplnbmbhimcoooka
|
||||
firefoxSlug: csv-viewer
|
||||
|
||||
- # YouTube Video Transcript
|
||||
chromeId: nkjcoophmpcmmgadnljnlpbpfdfacgbo
|
||||
firefoxSlug: youtube-transcript-copy
|
||||
|
||||
- # NetSuite Record Scripts
|
||||
chromeId: lcaieahkjgeggeiihblhcjbbjlppgieh
|
||||
firefoxSlug: netsuite-scripts-manager
|
||||
|
||||
- # VueTracker
|
||||
chromeId: gmocfknjllodfiomnljmaehcplnekhlo
|
||||
|
||||
- # CanCopy - A web extension that allow you to copy any content from website
|
||||
chromeId: ggcfemmoabhhelfkhknhbnkmeahloiod
|
||||
|
||||
- # Read Frog
|
||||
chromeId: modkelfkcfjpgbfmnbnllalkiogfofhb
|
||||
firefoxSlug: read-frog-open-ai-translator
|
||||
|
||||
- # Bilibili Feed History Helper
|
||||
chromeId: npfopljnjbamegincfjelhjhnonnjloo
|
||||
|
||||
- # NZBDonkey - The ultimate NZB file download tool
|
||||
chromeId: edkhpdceeinkcacjdgebjehipmnbomce
|
||||
|
||||
- # WeChat Markdown Editor(微信 Markdown 编辑器)
|
||||
chromeId: cckggnbnimdbbpmdinkkgbbncopbloob
|
||||
|
||||
- # Tab Grab
|
||||
chromeId: jcblcjolcojmfopefcighfmkkefbaofg
|
||||
firefoxSlug: tab-grab
|
||||
|
||||
- # BrowserLens - https://browserlens.com/
|
||||
chromeId: eehmoikadcijkapfjocnhjclpbaindlb
|
||||
firefoxSlug: browserlens
|
||||
|
||||
- # Epic Games Library Extension
|
||||
chromeId: hfhellofkjebbchcdffmicekjdomkcmc
|
||||
firefoxSlug: epic-games-library-extension
|
||||
|
||||
- # Zen Analytics Pixel Tracker - zapt.web.app
|
||||
chromeId: gknigcbhlammoakmmdddkblknanpjiac
|
||||
|
||||
- # Crypto Pulse - Compose your newtab with nature images, widgets & realtime Crypto Price & Bitcoin RSS.
|
||||
chromeId: cnklededohhcbmjjdlbjdkkihkgoggol
|
||||
|
||||
- # Youtube Video Scheduler
|
||||
chromeId: miponnamafdenpgjemkknimgjfibicdc
|
||||
firefoxSlug: youtube-video-scheduler
|
||||
|
||||
- # Chatslator: Livestream Chat Translator
|
||||
chromeId: nhmbcmalgpkjbomhlhgdicanmkkaajmg
|
||||
firefoxSlug: chatslator
|
||||
|
||||
- # 公众号阅读增强器 - https://wxreader.honwhy.wang
|
||||
chromeId: mbamjfdjbcdgpopfnkkmlohadbbnplhm
|
||||
|
||||
- # 토탐정
|
||||
chromeId: hannhecbnjnnbbafffmogdlnajpcomek
|
||||
firefoxSlug: 토탐정
|
||||
|
||||
- # 2FAS Pass - https://2fas.com/
|
||||
chromeId: ehboaofjncodknjkngdggmpdinhdoijp
|
||||
firefoxSlug: 2fas-pass-browser-extension
|
||||
|
||||
- # Quick Prompt - https://github.com/wenyuanw/quick-prompt
|
||||
chromeId: hnjamiaoicaepbkhdoknhhcedjdocpkd
|
||||
firefoxSlug: quick-prompt
|
||||
|
||||
- # Add QR Code Generator Icon Back To Address Bar
|
||||
chromeId: kacblhilkacgfnkjfodalohcnllcgmjd
|
||||
|
||||
- # Piwik PRO Tracking Helper
|
||||
chromeId: fkbdlogfdjmpfepbbbjcgcfbgbcfcnne
|
||||
firefoxSlug: piwik-pro-tracking-helper
|
||||
|
||||
- # PIPX - Take Control of Picture-in-Picture, Automatically
|
||||
chromeId: nkbikckldmljjiiajklecmgmajgapbfl
|
||||
|
||||
- # Browsely - AI-powered browser extension
|
||||
chromeId: hgppdobcpkfkmiegekaglonjajeojmdd
|
||||
|
||||
- # Filmbudd Pro - Simple, private – and synced ratings and watch notes across all your devices
|
||||
chromeId: ehmoihnjgkdimihkhokkmfjdgomohjgm
|
||||
|
||||
- # MultiField CopyCat - Copy, Paste & Autofill Web Forms Instantly
|
||||
chromeId: alglchohmdikgdjhafiicilegegieafa
|
||||
|
||||
- # ChatSight - Add Table of Contents to ChatGPT
|
||||
chromeId: aamihahiiogceidpbnfgehacgiecephe
|
||||
|
||||
- # BetterCampus (prev. BetterCanvas)
|
||||
chromeId: cndibmoanboadcifjkjbdpjgfedanolh
|
||||
firefoxSlug: better-canvas
|
||||
|
||||
- # Leetcode Fonts - Change fonts in leetcode effortlessly
|
||||
chromeId: hinfimgacobnellbncbcpdlpaapcofaa
|
||||
|
||||
- # TranslateManga - Manga Translator & Manga Tracker
|
||||
chromeId: kbkbfefhhabpkibojinapkkgciiacggg
|
||||
|
||||
- # SiteData - Free Website Traffic Checker & Reverse AdSense Tool
|
||||
chromeId: emeakbgdecgmdjgegnejpppcnkcnoaen
|
||||
firefoxSlug: sitedata
|
||||
|
||||
- # Livestream Chat Reader - Text-to-Speech for YouTube/Twitch chat
|
||||
chromeId: gpnckbhgpnjciklpoehkmligeaebigaa
|
||||
|
||||
- # ChatGPT Token Counter - Count tokens in real time on chatgpt conversation
|
||||
chromeId: fjlalaedpfcojcfpkgkglbjjbbkofgnl
|
||||
firefoxSlug: chatgpt-token-counter
|
||||
|
||||
- # LinuxDo Scripts - 为 linux.do 用户提供了一些增强功能
|
||||
chromeId: fbgblmjbeebanackldpbmpacppflgmlj
|
||||
firefoxSlug: linux_do-scripts
|
||||
|
||||
- # Zen Virtual Piano - https://zen-piano.web.app/
|
||||
chromeId: dfacnjidgbagicaekenjgclfnhdnjjdi
|
||||
|
||||
- # Crypto Pulse price tracker - https://get-crypto-pulse.web.app/
|
||||
chromeId: naeibcegmgpofimedkmfgjgphfhfhlab
|
||||
|
||||
- # Redirect Web - Automatically redirect pages or open them in another app
|
||||
chromeId: ffglckbhfbfmdkefdmjbhpnffkcmlhdh
|
||||
|
||||
- # Capture It - Capture & Edit Screenshots
|
||||
chromeId: eglpfhbhmelampoihamjomgkeobgdofl
|
||||
|
||||
- # Teams Chat Exporter
|
||||
chromeId: jmghclbfbbapimhbgnpffbimphlpolnm
|
||||
firefoxSlug: teams-chat-exporter
|
||||
edgeId: phlomfiieaggnbfpacmjmidcjdlaiplp
|
||||
|
||||
- # Lofi BGM Player - Free lofi focus music for work & study
|
||||
chromeId: jdcppdokgfbnhiacbeplahgnciahnhck
|
||||
firefoxSlug: lofi-bgm-player
|
||||
|
||||
- # Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.
|
||||
chromeId: cgpmbiiagnehkikhcbnhiagfomajncpa
|
||||
|
||||
- # KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows
|
||||
chromeId: mfjdonmgmgcijagclnkfhmjiblbfjaid
|
||||
|
||||
- # Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers
|
||||
chromeId: dhiekgdaipindoapjmcnpompdknjeijf
|
||||
firefoxSlug: glossy-new-tab
|
||||
|
||||
- # All API Hub – AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub
|
||||
chromeId: lapnciffpekdengooeolaienkeoilfeo
|
||||
firefoxSlug: all-api-hub
|
||||
|
||||
- # Scrape Similar - Extract data from websites into spreadsheets - https://github.com/zizzfizzix/scrape-similar
|
||||
chromeId: bhgobenflkkhfcgkikejaaejenoddcmo
|
||||
|
||||
- # isTrust - https://github.com/Internet-Society-Belgium/isTrust/
|
||||
chromeId: kinlknncggaihnhdcalijdmpbhbflalm
|
||||
|
||||
- # Dymo
|
||||
chromeId: ojpakgiekphppgkcdihbjpafobhnhlkp
|
||||
|
||||
- # Extension Rank Checker - Extension Ranker
|
||||
chromeId: pmgehhllikbjmadpenhabejhpemplhmd
|
||||
firefoxSlug: rank-checker
|
||||
|
||||
- # PlayFaster - Enhanced playback speed control for online videos and audio
|
||||
chromeId: fppcbkhpahkbgijkdcpjgjmhpfbmfiih
|
||||
firefoxSlug: playfaster
|
||||
|
||||
- # LatTab - Learn Latin With Every New Tab
|
||||
chromeId: eiocjaocpmackhbaffoejkcmnfbdpgpj
|
||||
firefoxSlug: lattab-learn-latin-new-tabs
|
||||
|
||||
- # Vim What? - Interactive Vim command reference
|
||||
chromeId: ngbehgnlcdjkbnihgpkgdangbhemidge
|
||||
firefoxSlug: vim-what
|
||||
|
||||
- # FRED - Fraud Recognition Easy Detection
|
||||
chromeId: bjdbcabacnlmbpcmiapcdfancfgcakfn
|
||||
firefoxSlug: fred
|
||||
|
||||
- # Clear Wisdom - Gems of wisdom from James Clear's 3-2-1 newsletter
|
||||
chromeId: jijfmgoijddfmlcdghopbkdpelbpmjdm
|
||||
firefoxSlug: clear-wisdom
|
||||
|
||||
- # Soundcloud Enhanced Pro
|
||||
chromeId: ggplcohodggmdfpopelnpplhgfjclomi
|
||||
|
||||
- # AlarmBot: ULTIMATE Web Monitoring & Smart Price Alerts
|
||||
chromeId: mpckalcodookackleecihhnngdibelif
|
||||
|
||||
- # TF2 Trader - TF2 & Steam Trading Extension - https://github.com/offish/tf2-trader
|
||||
chromeId: gmicpekfpbikhibodgokfpghadkclhoe
|
||||
|
||||
- # Mimik
|
||||
chromeId: jmfohdaflahliammccpiadmkcibohgha
|
||||
firefoxSlug: mimik
|
||||
|
||||
- # Modrinth Extras - https://github.com/creeperkatze/modrinth-extras
|
||||
chromeId: ajmkilipadfpaefpcjfgnkejalmhdlcj
|
||||
firefoxSlug: modrinth-extras
|
||||
|
||||
- # Pi-hole In One - https://github.com/creeperkatze/pihole-in-one
|
||||
chromeId: gaaobidjebianpcngcfpkniaocibidhe
|
||||
firefoxSlug: pihole-in-one
|
||||
|
||||
- # QIE Wallet – Non-Custodial Multi-Chain Crypto Wallet
|
||||
chromeId: oljchdcgmibnjbbopolafbjncfhdacjb
|
||||
|
||||
- # Headstart – Supercharge your browsing efficiency https://useheadstart.app/
|
||||
chromeId: fhhldbfombjdnjnpeojjohloohmbanda
|
||||
|
||||
- # Redmine Time Tracking
|
||||
chromeId: ldcanhhkffokndenejhafhlkapflgcjg
|
||||
firefoxSlug: redmine-time-tracking
|
||||
|
||||
- # Telsia: Eye Strain Reduction
|
||||
chromeId: iakkmikmbknfcffhalijnhijblikoldc
|
||||
firefoxSlug: telsia-eye-strain-reduction
|
||||
|
||||
- # Sound Booster & Equalizer
|
||||
chromeId: podnfipggfpfcpglhjebcefinglohbhn
|
||||
|
||||
- # Link Later
|
||||
chromeId: gpehbbegbcpjmipphokcmfhkchhcpfam
|
||||
|
||||
- # Google Cloud Skills Boost - Helper
|
||||
chromeId: lmbhjioadhcoebhgapaidogodllonbgg
|
||||
firefoxSlug: cloud-skills-boost-helper
|
||||
|
||||
- # Property Track - https://www.propertytrack.co
|
||||
chromeId: ahknifalnhmbanoclebllcolcdomfdop
|
||||
firefoxSlug: propertytrack
|
||||
|
||||
- # WageSlav3
|
||||
chromeId: anhkcmapjhiiofpcgglfljphdgppilof
|
||||
|
||||
- # LinkedIn Games Solver
|
||||
chromeId: jnhgapnkejaijibcdhcldhdfikjmdaph
|
||||
firefoxSlug: linkedin-games-solver-addon
|
||||
|
||||
- # Hayami: Anime comments & discussions
|
||||
chromeId: nhkggpiaeaeeeimohfpchnjobbamfcbg
|
||||
firefoxSlug: hayami
|
||||
|
||||
- # CopyApes Assistant
|
||||
chromeId: affmjifigldmicnbgpghddaneomejmfo
|
||||
|
||||
- # Leetcode Enhancer
|
||||
chromeId: cpoclfijojgjiafnlgnhjalkaiabcjch
|
||||
|
||||
- # Adaptive Tab Bar Colour
|
||||
firefoxSlug: adaptive-tab-bar-colour
|
||||
|
||||
- #ArcPort - Tab Checkpoint
|
||||
chromeId: kajmnfhpmkimleehomeioondgfcjjnbp
|
||||
|
||||
- #ArcPort - Tab Rebrand
|
||||
chromeId: pgmohcikgaeddapienpncpbiklbmfkgp
|
||||
|
||||
- #MindSnap
|
||||
chromeId: mdcpckfcphpicfpiopfcpeambjknjabl
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 131 KiB |
@@ -43,7 +43,7 @@ ESLint doesn't know about the auto-imported variables unless they are explicitly
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [ESLint >=9]
|
||||
```ts [ESLint 9]
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
@@ -53,7 +53,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
```ts [ESLint <=8]
|
||||
```ts [ESLint 8]
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
@@ -69,7 +69,7 @@ Then in your ESLint config, import and use the generated file:
|
||||
|
||||
:::code-group
|
||||
|
||||
```js [ESLint >=9]
|
||||
```js [ESLint 9]
|
||||
// eslint.config.mjs
|
||||
import autoImports from './.wxt/eslint-auto-imports.mjs';
|
||||
|
||||
@@ -81,7 +81,7 @@ export default [
|
||||
];
|
||||
```
|
||||
|
||||
```js [ESLint <=8]
|
||||
```js [ESLint 8]
|
||||
// .eslintrc.mjs
|
||||
export default {
|
||||
extends: ['./.wxt/eslintrc-auto-import.json'],
|
||||
|
||||
@@ -4,17 +4,11 @@ outline: deep
|
||||
|
||||
# Browser Startup
|
||||
|
||||
During development, WXT will use any of the below packages to automatically open a browser with your extension installed.
|
||||
|
||||
- [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext)
|
||||
|
||||
Just install the dependency you want WXT to use to open the browser.
|
||||
|
||||
## `web-ext` Usage
|
||||
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.
|
||||
|
||||
### Config Files
|
||||
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
|
||||
|
||||
## Config Files
|
||||
|
||||
You can configure browser startup in 3 places:
|
||||
|
||||
@@ -31,9 +25,9 @@ You can configure browser startup in 3 places:
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control
|
||||
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
|
||||
|
||||
### Recipes
|
||||
## Recipes
|
||||
|
||||
#### Set Browser Binaries
|
||||
### Set Browser Binaries
|
||||
|
||||
To set or customize the browser opened during development:
|
||||
|
||||
@@ -63,7 +57,7 @@ export default defineConfig({
|
||||
|
||||
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
|
||||
|
||||
#### Persist Data
|
||||
### Persist Data
|
||||
|
||||
By default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script.
|
||||
|
||||
@@ -100,9 +94,9 @@ Now, next time you run the `dev` script, a persistent profile will be created in
|
||||
You can use any directory you'd like for `--user-data-dir`, the examples above create a persistent profile for each WXT project. To create a profile for all WXT projects, you can put the `chrome-data` directory inside your user's home directory.
|
||||
:::
|
||||
|
||||
#### Disable Opening Browser
|
||||
### Disable Opening Browser
|
||||
|
||||
If you don't want to uninstall `web-ext`, like to test in your normal profile, you can do so via `disabled: true`:
|
||||
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
|
||||
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
@@ -159,30 +159,6 @@ export default defineConfig({
|
||||
|
||||
Alternatively, you can use [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) to let WXT generate your icon at the required sizes.
|
||||
|
||||
### Firefox `theme_icons`
|
||||
|
||||
Firefox supports a [`theme_icons`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#theme_icons) field on the toolbar action that swaps between a light and dark variant based on the current browser theme.
|
||||
|
||||
When [targeting Firefox](/guide/essentials/target-different-browsers.md), WXT auto-discovers paired light/dark icons in the `public/` directory and attaches them to `action` (MV3) or `browser_action` (MV2). You only need to drop both variants next to your regular icons:
|
||||
|
||||
```plaintext
|
||||
public/
|
||||
├─ icon-16.png # regular (default_icon)
|
||||
├─ icon-light-16.png # Firefox light theme
|
||||
├─ icon-dark-16.png # Firefox dark theme
|
||||
├─ icon-32.png
|
||||
├─ icon-light-32.png
|
||||
└─ icon-dark-32.png
|
||||
```
|
||||
|
||||
A size is only included in `theme_icons` if **both** a light and a dark file are present. The following filename patterns are discovered:
|
||||
|
||||
<<< @/../packages/wxt/src/core/utils/theme-icons.ts#snippet
|
||||
|
||||
Only `.png` files are discovered today, even though Firefox supports `.svg` - follow [#1120](https://github.com/wxt-dev/wxt/issues/1120) for updates.
|
||||
|
||||
If you set `manifest.action.theme_icons` (or `manifest.browser_action.theme_icons`) explicitly in `wxt.config.ts`, WXT will not overwrite it.
|
||||
|
||||
## Permissions
|
||||
|
||||
> [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/)
|
||||
@@ -200,23 +176,6 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
Different browsers support different permissions. You are responsible for passing only the permissions required for each browser:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: ({ browser }) => ({
|
||||
permissions:
|
||||
browser === 'chrome'
|
||||
? ['storage', 'favicon', 'declarativeNetRequest']
|
||||
: ['storage', 'webRequest'],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Host Permissions
|
||||
|
||||
> [Chrome docs](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions)
|
||||
|
||||
@@ -17,14 +17,28 @@ Or if you're in a monorepo, you may not want to extend the config. If you don't
|
||||
/// <reference path="./.wxt/wxt.d.ts" />
|
||||
```
|
||||
|
||||
## Compiler Options
|
||||
|
||||
To specify custom compiler options, add them in `<rootDir>/tsconfig.json`:
|
||||
|
||||
```jsonc
|
||||
// <rootDir>/tsconfig.json
|
||||
{
|
||||
"extends": ".wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## TSConfig Paths
|
||||
|
||||
WXT provides a default set of path aliases.
|
||||
|
||||
| Alias | To | Example |
|
||||
| ----- | ------------- | ----------------------------------------------- |
|
||||
| `~~` | `<rootDir>/*` | `import "~~/package.json"` |
|
||||
| `@@` | `<rootDir>/*` | `import "@@/package.json"` |
|
||||
| `~~` | `<rootDir>/*` | `import "~~/scripts"` |
|
||||
| `@@` | `<rootDir>/*` | `import "@@/scripts"` |
|
||||
| `~` | `<srcDir>/*` | `import { toLowerCase } from "~/utils/strings"` |
|
||||
| `@` | `<srcDir>/*` | `import { toLowerCase } from "@/utils/strings"` |
|
||||
|
||||
@@ -49,34 +63,3 @@ export default defineConfig({
|
||||
import { fakeTab } from 'testing/fake-objects';
|
||||
import { toLowerCase } from 'strings';
|
||||
```
|
||||
|
||||
## Custom Options
|
||||
|
||||
To specify custom compiler or top-level options, you have two options:
|
||||
|
||||
1. Override WXT's value completely by setting it in your root `tsconfig.json`:
|
||||
|
||||
```jsonc
|
||||
// <rootDir>/tsconfig.json
|
||||
{
|
||||
"extends": ".wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Merge custom values into values generated by WXT via the `prepare:tsconfig` hook:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
hooks: {
|
||||
'prepare:tsconfig': (wxt, { tsconfig }) => {
|
||||
tsconfig.compilerOptions.lib.push('WebWorker');
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Overriding is great for simple boolean values, but not for complex options like `paths` or `libs`, where you can't just "add" a value to the object from your `tsconfig.json` file. So WXT provides the hook that let's you add or merge or delete or do whatever you want to the config file before it's written to the `.wxt/` directory.
|
||||
|
||||
@@ -449,15 +449,6 @@ Full examples:
|
||||
- [react-content-script-ui](https://github.com/wxt-dev/examples/tree/main/examples/react-content-script-ui)
|
||||
- [tailwindcss](https://github.com/wxt-dev/examples/tree/main/examples/tailwindcss)
|
||||
|
||||
:::warning `rem` Units Are Not Fully Isolated
|
||||
|
||||
WXT resets most inherited styles via `all: initial`. This doesn't reset the `<html>` element's font size, which determines the relative size of `rem` units.
|
||||
|
||||
If your CSS framework uses `rem` units, like Tailwind CSS, you may notice your UI's scale changing on different websites.
|
||||
|
||||
See the [FAQ](/guide/resources/faq#my-content-script-ui-looks-different-on-certain-websites) for a fix.
|
||||
:::
|
||||
|
||||
### IFrame
|
||||
|
||||
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
|
||||
|
||||
@@ -463,8 +463,8 @@ When you define a Newtab entrypoint, WXT will automatically update the manifest
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
'16': '/icon-16.png',
|
||||
'24': '/icon-24.png',
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
@@ -554,8 +554,8 @@ Firefox does not support sandboxed pages.
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
'16': '/icon-16.png',
|
||||
'24': '/icon-24.png',
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineBackground({
|
||||
});
|
||||
```
|
||||
|
||||
This will change the output format to ESM, enable code-splitting between your background script and HTML pages, and set `"type": "module"` in your manifest.
|
||||
This will change the output format to ESM, enable code-spliting between your background script and HTML pages, and set `"type": "module"` in your manifest.
|
||||
|
||||
:::warning
|
||||
Only MV3 supports ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
|
||||
|
||||
@@ -76,34 +76,6 @@ Alternatively, if you're trying to use similar APIs under different names (to su
|
||||
});
|
||||
```
|
||||
|
||||
## Add Firefox Types
|
||||
|
||||
WXT'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 by:
|
||||
|
||||
1. Installing `@wxt-dev/browser` as a direct dependency
|
||||
|
||||
```sh
|
||||
pnpm add @wxt-dev/browser
|
||||
```
|
||||
|
||||
2. Augmenting the `Browser` type:
|
||||
|
||||
```ts
|
||||
// <srcDir>/browser-types.d.ts
|
||||
import '@wxt-dev/browser';
|
||||
import type { SidebarAction } from 'webextension-polyfill';
|
||||
|
||||
declare module '@wxt-dev/browser' {
|
||||
namespace Browser {
|
||||
export const sidebarAction: SidebarAction.Static;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can add types from any source: `webextension-polyfill`, `@types/firefox-webext-browser`, or your own custom types.
|
||||
|
||||
See TypeScript's documentation on [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) for more information.
|
||||
|
||||
## Entrypoint Limitations
|
||||
|
||||
Because 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**.
|
||||
@@ -122,32 +94,41 @@ The fix is simple, just move your API usage into the entrypoint's main function:
|
||||
|
||||
:::code-group
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts [background.ts]
|
||||
browser.action.onClicked.addListener(() => { /* ... */ }); // [!code --]
|
||||
browser.action.onClicked.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.action.onClicked.addListener(() => { /* ... */ }); // [!code ++]
|
||||
browser.action.onClicked.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts [content.ts]
|
||||
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code --]
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
main() {
|
||||
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code ++]
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts [unlisted.ts]
|
||||
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code --]
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code --]
|
||||
|
||||
export default defineUnlistedScript(() => {
|
||||
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code ++]
|
||||
browser.runtime.onMessage.addListener(() => {
|
||||
/* ... */
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# Favicons
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/how-to/ui/favicons)
|
||||
|
||||
Chromium-based browsers expose cached favicons through the `_favicon/` URL served from your extension. To use it, declare the `favicon` permission in your manifest and call `browser.runtime.getURL` with a `_favicon/` path:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['favicon'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// Any entrypoint
|
||||
function getFaviconUrl(pageUrl: string, size = 16) {
|
||||
const url = new URL(browser.runtime.getURL('/_favicon/'));
|
||||
url.searchParams.set('pageUrl', pageUrl);
|
||||
url.searchParams.set('size', String(size));
|
||||
return url.toString();
|
||||
}
|
||||
```
|
||||
|
||||
::: warning Chromium only
|
||||
The favicon API is only available on Chromium-based browsers. Firefox has no equivalent, so `/_favicon/` URLs will not resolve at runtime there. If your extension supports both browsers, gate favicon usage behind `import.meta.env.CHROME` (or similar) and declare the permission per-browser.
|
||||
:::
|
||||
|
||||
## Usage from a content script
|
||||
|
||||
WXT does **not** add a `web_accessible_resources` entry for `_favicon/*` — not every extension needs one, and adding it unconditionally would expose internals to sites that don't need them. If you want to load a favicon inside a content script (for example, as an `<img src>`), add your own entry in `wxt.config.ts`:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['favicon'],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['_favicon/*'],
|
||||
matches: ['<all_urls>'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -97,7 +97,7 @@ Instead, you need to configure the router to run in "hash" mode, where the routi
|
||||
|
||||
Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
|
||||
|
||||
- [`react-router`](https://reactrouter.com/api/data-routers/createHashRouter#createhashrouter)
|
||||
- [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)
|
||||
- [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
|
||||
- [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
|
||||
- [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
|
||||
|
||||
@@ -163,8 +163,8 @@ yarn zip:firefox
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bun install
|
||||
bun run zip:firefox
|
||||
bun i
|
||||
bun zip:firefox
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -206,13 +206,13 @@ WXT uses the command `npm pack <package-name>` to download the package. That mea
|
||||
|
||||
WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide:
|
||||
|
||||
- [Packaging a web extension for Safari](https://developer.apple.com/documentation/safariservices/packaging-a-web-extension-for-safari) - "Package your existing extension as a Safari web extension using Xcode’s command-line tool."
|
||||
- [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari) - "Convert your existing extension to a Safari web extension using Xcode’s command-line tool."
|
||||
|
||||
When running the `safari-web-extension-packager` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
|
||||
When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
|
||||
|
||||
```sh
|
||||
pnpm wxt build -b safari
|
||||
xcrun safari-web-extension-packager .output/safari-mv2
|
||||
xcrun safari-web-extension-converter .output/safari-mv2
|
||||
```
|
||||
|
||||
### Edge Addons
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Remote Code
|
||||
|
||||
WXT will automatically download and bundle imports with the `url:` prefix so the extension does not depend on remote code, [a requirement from Google for MV3](https://developer.chrome.com/docs/extensions/migrating/improve-security/#remove-remote-code).
|
||||
|
||||
## Google Analytics
|
||||
|
||||
For example, you can import Google Analytics:
|
||||
|
||||
```ts
|
||||
// utils/google-analytics.ts
|
||||
import 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
// NOTE: This line is different from Google's documentation
|
||||
window.gtag = function () {
|
||||
dataLayer.push(arguments);
|
||||
};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-XXXXXX');
|
||||
```
|
||||
|
||||
Then you can import this in your HTML files to enable Google Analytics:
|
||||
|
||||
```ts
|
||||
// popup/main.ts
|
||||
import '~/utils/google-analytics';
|
||||
|
||||
gtag('event', 'event_name', {
|
||||
key: 'value',
|
||||
});
|
||||
```
|
||||
@@ -78,28 +78,3 @@ Here are some examples:
|
||||
```
|
||||
|
||||
Alternatively, you can use the [`filterEntrypoints` config](/api/reference/wxt/interfaces/InlineConfig#filterentrypoints) to list all the entrypoints you want to build.
|
||||
|
||||
## Per-Browser Options
|
||||
|
||||
Some entrypoint options can be customized per build target by passing an object keyed by the [target browser](/guide/essentials/target-different-browsers#target-a-browser) instead of a single value. This is useful when different browsers need different match patterns, run timings, or other entrypoint behavior:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: {
|
||||
chrome: ['*://chrome.example.com/*'],
|
||||
firefox: ['*://firefox.example.com/*'],
|
||||
},
|
||||
runAt: {
|
||||
chrome: 'document_start',
|
||||
firefox: 'document_end',
|
||||
},
|
||||
|
||||
world: {
|
||||
firefox: 'MAIN',
|
||||
},
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -21,6 +21,7 @@ This plugin does several things:
|
||||
- Polyfills the extension API, `browser`, with an in-memory implementation using [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation)
|
||||
- Adds all vite config or plugins in `wxt.config.ts`
|
||||
- Configures auto-imports (if enabled)
|
||||
- Applies internal WXT vite plugins for things like [bundling remote code](/guide/essentials/remote-code)
|
||||
- Sets up global variables provided by WXT (`import.meta.env.BROWSER`, `import.meta.env.MANIFEST_VERSION`, `import.meta.env.IS_CHROME`, etc)
|
||||
- Configures aliases (`@/*`, `@@/*`, etc) so imports can be resolved
|
||||
|
||||
|
||||
@@ -4,21 +4,33 @@ outline: deep
|
||||
|
||||
# WXT Modules
|
||||
|
||||
WXT provides a "module system" that lets you run code at different steps in the build process to modify it.
|
||||
WXT provides a "module system" that let's you run code at different steps in the build process to modify it.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## Installing a Module
|
||||
## Adding a Module
|
||||
|
||||
To use a published module from NPM, install the package and add it to your config:
|
||||
There are two ways to add a module to your project:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
```
|
||||
1. **NPM**: install an NPM package, like [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) and add it to your config:
|
||||
|
||||
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
```
|
||||
|
||||
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
|
||||
|
||||
2. **Local**: add a file to your project's `modules/` directory:
|
||||
|
||||
```plaintext
|
||||
<rootDir>/
|
||||
modules/
|
||||
my-module.ts
|
||||
```
|
||||
|
||||
> To learn more about writing your own modules, read the [Writing Modules](/guide/essentials/wxt-modules) docs.
|
||||
|
||||
## Module Options
|
||||
|
||||
@@ -37,7 +49,7 @@ Modules are loaded in the same order as hooks are executed. Refer to the [Hooks
|
||||
|
||||
## Writing Modules
|
||||
|
||||
If you need custom build logic for your project, you can write your own local module. Here's what a basic WXT module looks like:
|
||||
Here's what a basic WXT module looks like:
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
@@ -49,14 +61,6 @@ export default defineWxtModule({
|
||||
});
|
||||
```
|
||||
|
||||
To add it to your project, place the file in the `modules/` directory at the root of your project. Any module file in this directory is **automatically discovered and loaded** — no additional configuration is needed:
|
||||
|
||||
```plaintext
|
||||
<rootDir>/
|
||||
modules/
|
||||
my-module.ts ← loaded automatically
|
||||
```
|
||||
|
||||
Each module's setup function is executed after the `wxt.config.ts` file is loaded. The `wxt` object provides everything you need to write a module:
|
||||
|
||||
- Use `wxt.hook(...)` to hook into the build's lifecycle and make changes
|
||||
@@ -137,35 +141,6 @@ console.log(config.myModule);
|
||||
|
||||
This is very useful when [generating runtime code](#generate-runtime-module).
|
||||
|
||||
#### Logging
|
||||
|
||||
There are two "correct" ways to add console logs to your modules:
|
||||
|
||||
1. Use `wxt.logger` for info, warnings, and errors
|
||||
2. Install [`obug`](https://www.npmjs.com/package/obug) for debug messages
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import { createDebug } from 'obug';
|
||||
|
||||
const debug = createDebug('my-module');
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.logger.info('Module loaded');
|
||||
debug('Debug details');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`wxt.logger` is great for formatted, pretty messages that match the rest of WXT's logs.
|
||||
|
||||
`obug` makes it easy for devs to enable and filter debug logs when necessary:
|
||||
|
||||
```sh
|
||||
DEBUG=my-module wxt dev
|
||||
```
|
||||
|
||||
#### Add custom entrypoint options
|
||||
|
||||
Modules 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.
|
||||
|
||||
+17
-17
@@ -10,14 +10,14 @@ Run the [`init` command](/api/cli/wxt-init), and follow the instructions.
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [Bun]
|
||||
bunx wxt@latest init
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm dlx wxt@latest init
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
bunx wxt@latest init
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
npx wxt@latest init
|
||||
```
|
||||
@@ -46,16 +46,16 @@ Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
|
||||
1. Create a new project
|
||||
:::code-group
|
||||
|
||||
```sh [Bun]
|
||||
cd my-project
|
||||
bun init
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
cd my-project
|
||||
pnpm init
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
cd my-project
|
||||
bun init
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
cd my-project
|
||||
npm init
|
||||
@@ -71,14 +71,14 @@ Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
|
||||
2. Install WXT:
|
||||
:::code-group
|
||||
|
||||
```sh [Bun]
|
||||
bun add -D wxt
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm i -D wxt
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
bun i -D wxt
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
npm i -D wxt
|
||||
```
|
||||
@@ -119,14 +119,14 @@ Once you've run the `dev` command, continue to [Next Steps](#next-steps)!
|
||||
5. Run your extension in dev mode
|
||||
:::code-group
|
||||
|
||||
```sh [Bun]
|
||||
bun run dev
|
||||
```
|
||||
|
||||
```sh [PNPM]
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh [Bun]
|
||||
bun run dev
|
||||
```
|
||||
|
||||
```sh [NPM]
|
||||
npm run dev
|
||||
```
|
||||
|
||||
@@ -24,6 +24,7 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
|
||||
| Supports all frontend frameworks | ✅ | 🟡 [^c] | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 [^d] | ✅ [^e] | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| Unlisted HTML Pages | ✅ | ✅ | ✅ |
|
||||
| Unlisted Scripts | ✅ | ❌ | ❌ |
|
||||
| ESM Content Scripts | ❌ [^l] | ❌ | ✅ |
|
||||
|
||||
@@ -164,36 +164,6 @@ Both issues have the same cause: the library puts something outside the `ShadowR
|
||||
|
||||
Both 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.
|
||||
|
||||
## My content script UI looks different on certain websites
|
||||
|
||||
If your `createShadowRootUi` looks correct on most sites but appears at the wrong size on others (e.g., Reddit), it's because `rem` unit is relative to the `<html>` element's `font-size`, which lives outside the Shadow DOM. When a website overrides it, your UI scales incorrectly.
|
||||
|
||||
The fix is to convert `rem` units to `px` at build time using [`postcss-rem-to-responsive-pixel`](https://www.npmjs.com/package/postcss-rem-to-responsive-pixel). This eliminates the dependency on the host page's root font-size.
|
||||
|
||||
1. Install the package:
|
||||
|
||||
```sh
|
||||
bun i -D postcss-rem-to-responsive-pixel
|
||||
```
|
||||
|
||||
2. Configure your PostCSS config:
|
||||
|
||||
```js [postcss.config.mjs]
|
||||
import remToPx from 'postcss-rem-to-responsive-pixel';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
remToPx({
|
||||
rootValue: 16,
|
||||
propList: ['*'],
|
||||
transformUnit: 'px',
|
||||
}),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
See [Issue #678](https://github.com/wxt-dev/wxt/issues/678) for additional context on this behavior.
|
||||
|
||||
## Does WXT provide docs for LLMs?
|
||||
|
||||
Yes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).
|
||||
@@ -221,18 +191,6 @@ To run the WXT dev server in a devcontainer, but load the dev build of your exte
|
||||
3. **Tell WXT to listen on all network interfaces**
|
||||
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`.
|
||||
|
||||
4. **Enable polling if file changes are not detected**
|
||||
Some container, WSL, and network file system setups do not emit native file events reliably. Configure Chokidar polling in `wxt.config.ts`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
watchOptions: {
|
||||
usePolling: true,
|
||||
interval: 1000,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## How do I use the new Prompt API in Chrome?
|
||||
|
||||
The 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".
|
||||
|
||||
@@ -51,7 +51,7 @@ Here's specific steps for other popular frameworks/build tools.
|
||||
3. Move public `assets/*` into the `public/` directory
|
||||
4. If you use CSUI, migrate to WXT's `createContentScriptUi`
|
||||
5. Convert Plasmo's custom import resolutions to Vite's
|
||||
6. Importing remote code via a URL is not supported in WXT, see [this issue](https://github.com/wxt-dev/wxt/issues/2262) for alternatives
|
||||
6. If importing remote code via a URL, add a `url:` prefix so it works with WXT
|
||||
7. 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`)
|
||||
8. ⚠️ 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.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ To upgrade WXT to the latest major version:
|
||||
```
|
||||
|
||||
2. Follow the upgrade steps below to fix any breaking changes.
|
||||
3. Run `wxt prepare`. It should succeed and type errors will go away afterwards.
|
||||
3. Run `wxt prepare`. It should succeed and type errors will go away afterwords.
|
||||
|
||||
```sh
|
||||
pnpm wxt prepare
|
||||
@@ -35,340 +35,6 @@ Listed below are all the breaking changes you should address when upgrading to a
|
||||
|
||||
Currently, 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.
|
||||
|
||||
## v0.20.0 → v0.21.0
|
||||
|
||||
This release focuses on trimming WXT's install footprint and simplifying project configuration.
|
||||
|
||||
:::tip
|
||||
Read through all the changes once before updating your code.
|
||||
:::
|
||||
|
||||
### Install Footprint
|
||||
|
||||
When installing `wxt` v0.20, it downloaded 98 MB and 366 packages to your `node_modules`. v0.21 cuts this down to 22MB / 156 packages - that's 22.4% the previous size! This is a good improvement, but frankly, not good enough. Why?
|
||||
|
||||
- `wxt init` is slow for new developers trying it out
|
||||
- Less exposure to supply chain attacks
|
||||
- More compute and storage used by servers hosting the package and clients downloading it degrades hardware and shortens their life-cycle
|
||||
|
||||
If you want to explore WXT's dependencies and why it's so large, here's a few links:
|
||||
|
||||
- <https://pkg-size.dev/wxt@%5E0.21?no-peers>
|
||||
- <https://npmgraph.js.org/?q=wxt@^0.21>
|
||||
- <https://node-modules.dev/grid/depth#install=wxt@^0.21>
|
||||
|
||||
We'll continue working on minimizing WXT's install footprint.
|
||||
|
||||
### Minimum Node.js, Vite, and TypeScript Versions
|
||||
|
||||
| Tool | Old requirement | New requirement |
|
||||
| ---------- | --------------------------------- | ---------------------------------- |
|
||||
| Node.js | `>=20.12.0` | `>=22` |
|
||||
| Vite | `^5.4.19 \|\| ^6.3.4 \|\| ^7.0.0` | `^6.3.4 \|\| ^7.0.0 \|\| ^8.0.0-0` |
|
||||
| TypeScript | Not enforced | `>=5.4` |
|
||||
|
||||
Vite 5 support was dropped as part of moving WXT's dev server off `vite-node` and onto Vite's native `ModuleRunner`/Environment API, which requires Vite ≥6.3.4.
|
||||
|
||||
### `vite`, `web-ext`, and `typescript` Are Now Peer Dependencies
|
||||
|
||||
Previously, WXT installed its own copies of `vite` and `web-ext` for you (you never had to add them to your `package.json`). Now, all three are declared as [`peerDependencies`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies) instead of regular dependencies, so **your project controls the exact version that gets used**, and WXT's install size shrinks.
|
||||
|
||||
- `vite` is a **required** peer, add it to your `devDependencies`.
|
||||
- `web-ext` and `typescript` are **optional** peers, only needed if you use the corresponding features (see [below](#opt-into-opening-the-browser-during-dev-mode) for `web-ext`).
|
||||
|
||||
```sh
|
||||
pnpm add -D vite typescript
|
||||
```
|
||||
|
||||
### Opt Into Opening the Browser During Dev Mode
|
||||
|
||||
In v0.21, `web-ext` is now an optional peer dependency - if it's installed, the browser will be opened automatically. If not, the feature is disabled.
|
||||
|
||||
- **To continue opening the browser automatically**: Install `web-ext` as a dependency
|
||||
|
||||
```sh
|
||||
pnpm add -D web-ext
|
||||
```
|
||||
|
||||
- **If you don't use this feature**: Don't install it and you can delete any `web-ext.config.ts` options and/or remove any `webExt.enabled: false` config
|
||||
|
||||
You can still disable this behavior by setting `webExt.enabled: false` in your `wxt.config.ts` file or `web-ext.config.ts` file if the package is present.
|
||||
|
||||
> Projects created with `wxt init` will include `web-ext` in their `package.json`, opting new projects into this feature by default.
|
||||
|
||||
### `.wxt/tsconfig.json` Compiler Option Changes
|
||||
|
||||
The generated `.wxt/tsconfig.json` was updated to match Vite's current recommended TypeScript settings, and now requires **TypeScript ≥5.4**.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"], // [!code ++]
|
||||
"target": "ESNext",
|
||||
"module": "ESNext", // [!code --]
|
||||
"module": "Preserve", // [!code ++]
|
||||
"moduleDetection": "force", // [!code ++]
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true, // [!code ++]
|
||||
"verbatimModuleSyntax": true, // [!code ++]
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true, // [!code --]
|
||||
"forceConsistentCasingInFileNames": true, // [!code --]
|
||||
"resolveJsonModule": true, // [!code --]
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true, // [!code ++]
|
||||
"noUncheckedIndexedAccess": true, // [!code ++]
|
||||
"noImplicitOverride": true, // [!code ++]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If these changes do cause problems, `verbatimModuleSyntax` and `noUncheckedIndexedAccess` are the two most likely culprits. It's recommended you fix the issues and leave the new settings enabled.
|
||||
|
||||
However, if you want to revert the config back and upgrade to the recommended options at a later time, you can add the following to your `wxt.config.ts` file:
|
||||
|
||||
<details>
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
hooks: {
|
||||
'prepare:tsconfig': (_, { tsconfig }) => {
|
||||
// Reverts TSConfig back to WXT v0.20
|
||||
// - https://wxt.dev/guide/resources/upgrading.html#wxttsconfigjson-compiler-option-changes
|
||||
delete tsconfig.lib;
|
||||
tsconfig.module = 'ESNext';
|
||||
delete tsconfig.moduleDetection;
|
||||
delete tsconfig.allowImportingTsExtensions;
|
||||
delete tsconfig.verbatimModuleSyntax;
|
||||
tsconfig.esModuleInterop = true;
|
||||
tsconfig.forceConsistentCasingInFileNames = true;
|
||||
tsconfig.resolveJsonModule = true;
|
||||
delete tsconfig.noFallthroughCasesInSwitch;
|
||||
delete tsconfig.noUncheckedIndexedAccess;
|
||||
delete tsconfig.noImplicitOverride;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### `url:` Imports Removed
|
||||
|
||||
The `url:` import feature (importing and bundling remote code by URL, e.g. `import 'url:https://code.jquery.com/jquery.js'`) has been removed due to the supply-chain risk of a compromised CDN silently injecting code into your build.
|
||||
|
||||
There is no config flag to restore this behavior. If you relied on it:
|
||||
|
||||
- **Prefer an NPM package** if one exists for the library you need.
|
||||
- **Vendor the file**: download it once, commit it to your repo, and import it locally like any other file. This lets you review the code and track changes via version control, instead of trusting a URL to never change.
|
||||
|
||||
:::details Why was this removed?
|
||||
See <https://github.com/wxt-dev/wxt/issues/2262> and <https://github.com/wxt-dev/wxt/issues/2229> for the full discussion. A proposal to restore this behind a mandatory integrity hash is being tracked in [#2514](https://github.com/wxt-dev/wxt/pull/2514); if there's enough demand, it may come back as a separate, opt-in WXT module instead of core.
|
||||
:::
|
||||
|
||||
### Zip Filename Template Changes
|
||||
|
||||
The default filename templates for `zip.artifactTemplate` and `zip.sourcesTemplate` changed, and the <span v-pre>`{{version}}`</span> template variable's meaning changed:
|
||||
|
||||
- <span v-pre>`{{version}}`</span> now always resolves to `manifest.version` (previously it resolved to `manifest.version_name ?? manifest.version`).
|
||||
- A new <span v-pre>`{{versionName}}`</span> variable was added with the old <span v-pre>`{{version}}`</span> behavior (`manifest.version_name ?? manifest.version`).
|
||||
- A new <span v-pre>`{{packageVersion}}`</span> variable was added, resolving to your `package.json`'s version.
|
||||
- A new <span v-pre>`{{modeSuffix}}`</span> variable was added (`-dev` for development builds, `''` for production).
|
||||
|
||||
The defaults themselves changed to use these new variables:
|
||||
|
||||
```diff
|
||||
- artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip'
|
||||
+ artifactTemplate: '{{name}}-{{packageVersion}}-{{browser}}{{modeSuffix}}.zip'
|
||||
|
||||
- sourcesTemplate: '{{name}}-{{version}}-sources.zip'
|
||||
+ sourcesTemplate: '{{name}}-{{packageVersion}}-sources{{modeSuffix}}.zip'
|
||||
```
|
||||
|
||||
- **If you don't customize `artifactTemplate`/`sourcesTemplate`**: your production zip filenames will not change, but dev and other build modes will be output to separate directories.
|
||||
|
||||
> If you previously install the dev version of your extension in your browser, note that the output directory has changed and you'll need to re-install it next time you run the dev command.
|
||||
|
||||
- **If you have a custom template**: replace any <span v-pre>`{{version}}`</span> usage with <span v-pre>`{{versionName}}`</span> to keep the old value, or explicitly opt back into the old defaults:
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{versionName}}-{{browser}}.zip',
|
||||
sourcesTemplate: '{{name}}-{{versionName}}-sources.zip',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For more info on the difference between `manifest.version` and `manifest.version_name`, see the [Manifest Config docs](/guide/essentials/config/manifest#version-and-version-name).
|
||||
|
||||
### Sources ZIP: `includeSources`/`excludeSources` Now Behave Like a Standard Allowlist
|
||||
|
||||
Previously, `includeSources`/`excludeSources` behaved counterintuitively: `all files - excludeSources + includedSources = zipped files`. This is not the standard behavior for allowlist/blocklists.
|
||||
|
||||
As of v0.21, these options behave as expected: `includedSources - excludeSources = zipped files`.
|
||||
|
||||
Additionally, a new `zip.dotSources` option (default `false`) was added to control whether hidden files/directories can be included in the zip.
|
||||
|
||||
This will likely be the biggest pain point of the upgrade. You will need to rebuild your included and excluded sources lists from scratch. Luckily, it's simpler than before!
|
||||
|
||||
1. Do you need any hidden files? If so, set `zip.dotSources` to `true`
|
||||
1. Add base files and directories that need to be in the ZIP to `zip.includeSources`
|
||||
1. Run `wxt zip -b firefox` and confirm all the files you need are included
|
||||
1. Remove files from the zip by adding them to `zip.excludeSources`
|
||||
1. Run `wxt zip -b firefox` and confirm only the files you need are in the zip
|
||||
|
||||
:::tip
|
||||
`wxt zip -b firefox` now prints every file included in the sources ZIP. This will help you identify missing or unwanted files more quickly.
|
||||
:::
|
||||
|
||||
Here's an example configuration:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
zip: {
|
||||
// Say I have a .tool-versions file I want to include
|
||||
dotSources: true,
|
||||
// Specify base folders and files to include
|
||||
includeSources: [
|
||||
'src',
|
||||
'public',
|
||||
'package.json',
|
||||
'bun.lock',
|
||||
'README.md',
|
||||
'.tool-versions',
|
||||
],
|
||||
// Exclude any folders inside the base folders that are not needed, like test data
|
||||
excludeSources: ['**/__fixtures__'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `createShadowRootUi` DOM Changes
|
||||
|
||||
`@webext-core/isolated-element` was upgraded from v1 to v3. The only breaking change relevant to WXT projects is that the internal structure of the shadow root's is simpler:
|
||||
|
||||
:::code-group
|
||||
|
||||
```html [Before]
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
...
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
...your app
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```html [After]
|
||||
<style>
|
||||
...
|
||||
</style>
|
||||
<div>...your app</div>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Historically, CSS frameworks haven't had good support for shadow roots, so we needed a full `<html>` structure for styles to be applied correctly. But that has change this past few years, frameworks have started to include the `host:` selector alongside `root:` (which is required for base styles to be applied to the shadow root's host element):
|
||||
|
||||
```css
|
||||
:root { /* [!code --] */
|
||||
:root, :host { /* [!code ++] */
|
||||
/* base styles... */
|
||||
}
|
||||
```
|
||||
|
||||
If you use `createShadowRootUi`, see if your UI looks the same after this update without any changes.
|
||||
|
||||
If the styles are broken, you can continue using the full `<html>` structure by installing the v1 of `@webext-core/isolated-element` - WXT will respect whatever version of the package is listed in your `package.json`.
|
||||
|
||||
```sh
|
||||
pnpm add @webext-core/isolated-element@^1
|
||||
```
|
||||
|
||||
In the next major version, support for `@webext-core/isolated-element` v1 will be dropped, so you have some time to migrate.
|
||||
|
||||
### Content & Unlisted Script `globalName` Now Defaults to `false`
|
||||
|
||||
Content scripts and unlisted scripts are built as an IIFE. Previously, WXT generated a global variable to hold the IIFE's return value for all scripts by default. Now, no global variable is generated by default, producing a smaller, anonymous IIFE that prevents variable collisions with the page.
|
||||
|
||||
If you rely on the return value of your script, for example when injecting it with `browser.scripting.executeScript` and reading `InjectionResult.result`, set `globalName: true` to restore the old behavior:
|
||||
|
||||
```ts
|
||||
export default defineUnlistedScript({
|
||||
globalName: true, // [!code ++]
|
||||
main() {
|
||||
return 'some value';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This applies per-entrypoint, to both `defineContentScript` and `defineUnlistedScript`.
|
||||
|
||||
### ESLint Auto-Import Config Now Detects Your Installed ESLint Version
|
||||
|
||||
Setting `imports.eslintrc.enabled: true` used to always assume you wanted the legacy ESLint 8 config format, regardless of which ESLint version you actually had installed.
|
||||
|
||||
Now, `true` behaves the same as `'auto'`: WXT detects your installed ESLint version and generates the matching file automatically (ESLint 9+ → `eslint-auto-imports.mjs`, ESLint ≤8 → `eslintrc-auto-import.json`).
|
||||
|
||||
- If you're on ESLint 9+ but never explicitly set `enabled: 9`, you'll now get the new flat-config file instead of the legacy one.
|
||||
- To force the old ESLint 8 format regardless of your installed version, set `enabled: 8` explicitly.
|
||||
|
||||
See the [ESLint config docs](/guide/essentials/config/auto-imports#eslint) for the full set of options and how to wire up the generated file.
|
||||
|
||||
### Deprecated APIs Removed From v0.21
|
||||
|
||||
APIs deprecated in v0.20 have been removed:
|
||||
|
||||
- `wxt.config.runner` / `ExtensionRunnerConfig` / `defineRunnerConfig` → use `wxt.config.webExt` / `defineWebExtConfig` instead.
|
||||
- `dev.server.hostname` → use `dev.server.host` instead.
|
||||
- The `wxt/testing` barrel export → import from the specific submodule instead:
|
||||
|
||||
```ts
|
||||
import { fakeBrowser, WxtVitest } from 'wxt/testing'; // [!code --]
|
||||
import { fakeBrowser } from 'wxt/testing/fake-browser'; // [!code ++]
|
||||
import { WxtVitest } from 'wxt/testing/vitest-plugin'; // [!code ++]
|
||||
```
|
||||
|
||||
- The `clean(root: string)` JS API overload → Pass a root like this instead `clean({ root: './my-extension' })`
|
||||
|
||||
### CWS v2 API Support
|
||||
|
||||
`publish-browser-extension` was upgraded to v5, which adds support for the new Chrome Web Store v2 API to `wxt submit`! Instead of using a refresh token for auth, it uses a service account, which is much easier to setup and never expires. It also supports a few other features: <https://developer.chrome.com/blog/cws-api-v2>
|
||||
|
||||
To setup your project to use v2, run `wxt submit init` and go through the setup process for the CWS, selecting v2 when prompted.
|
||||
|
||||
v1 will stop working **15th October 2026**, so you have a few months to migrate.
|
||||
|
||||
### `@webext-core/fake-browser` v2
|
||||
|
||||
If you use `wxt/testing/fake-browser` (or `@webext-core/fake-browser` directly) in unit tests, note that its types now match `@wxt-dev/browser` instead of `webextension-polyfill`. This is the same change WXT's own `browser` went through in [v0.20](#webextension-polyfill-removed).
|
||||
|
||||
Most notably, **mocking `browser.runtime.onMessage` listeners that return a promise no longer works**, use `sendResponse` instead:
|
||||
|
||||
```ts
|
||||
fakeBrowser.runtime.onMessage.addListener(async () => { // [!code --]
|
||||
return await someAsyncWork(); // [!code --]
|
||||
fakeBrowser.runtime.onMessage.addListener((_message, _sender, sendResponse) => { // [!code ++]
|
||||
someAsyncWork().then(sendResponse); // [!code ++]
|
||||
return true; // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
If you use use a messaging library, it will likely continue working as-is.
|
||||
|
||||
### New Deprecations in v0.20
|
||||
|
||||
Deprecated APIs will be removed in the next major release.
|
||||
|
||||
- `useAppConfig` deprecated in favor of `getAppConfig` - same function, just renamed. This avoids react linters mistaking it as a hook.
|
||||
|
||||
## v0.19.0 → v0.20.0
|
||||
|
||||
v0.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.
|
||||
@@ -810,7 +476,7 @@ export default defineConfig({
|
||||
|
||||
### Renamed Undocumented Constants
|
||||
|
||||
Renamed 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/essentials/config/environment-variables.html#built-in-environment-variables>
|
||||
Renamed 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>
|
||||
|
||||
- `__BROWSER__` → `import.meta.env.BROWSER`
|
||||
- `__COMMAND__` → `import.meta.env.COMMAND`
|
||||
@@ -891,8 +557,6 @@ Unlisted scripts must now `export default defineUnlistedScript(...)`.
|
||||
|
||||
### `BackgroundDefinition` Type
|
||||
|
||||
<!-- cspell:disable -->
|
||||
|
||||
Rename `BackgroundScriptDefintition` to `BackgroundDefinition`.
|
||||
|
||||
## v0.6.0 → v0.7.0
|
||||
|
||||
@@ -66,6 +66,11 @@ features:
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: Tools for analyzing the final extension bundle and minimizing your extension's size.
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/essentials/remote-code
|
||||
linkText: Read docs
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
@@ -38,10 +38,6 @@
|
||||
# 0.19.0
|
||||
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
|
||||
|
||||
# 0.21.0
|
||||
# The `url:` import feature was removed, along with its docs page.
|
||||
/guide/essentials/remote-code.html /guide/resources/upgrading.html#url-imports-removed
|
||||
|
||||
# https://github.com/wxt-dev/wxt/issues/704
|
||||
# Generated via `pnpm docs:build && find docs/.vitepress/dist -type f -name "*.html"`
|
||||
|
||||
@@ -63,7 +59,7 @@
|
||||
/guide/go-further/handling-updates.html /guide/essentials/testing-updates.html
|
||||
/guide/go-further/custom-events.html /guide/essentials/content-scripts.html#dealing-with-spas
|
||||
/guide/go-further/debugging.html /TODO
|
||||
/guide/go-further/remote-code.html /guide/resources/upgrading.html#url-imports-removed
|
||||
/guide/go-further/remote-code.html /guide/essentials/remote-code.html
|
||||
/guide/go-further/vite.html /guide/essentials/config/vite.html
|
||||
/guide/go-further/testing.html /guide/essentials/unit-testing.html
|
||||
/guide/go-further/how-wxt-works.html /guide/resources/how-wxt-works.html
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="114" height="50" viewBox="0 0 114 50" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path fill="#060B10" d="M0 0h114v51H0z"/><g clip-path="url(#b)" fill="#fff"><path d="M44.445 40.302v-7.695l.153-.16h1.853l.154.16v7.695l-.154.16h-1.853l-.153-.16ZM44.445 17.855v-7.696l.153-.159h1.853l.154.16v7.695l-.154.16h-1.853l-.153-.16ZM38.068 34.892h-.254l-1.275-1.323v-.264l2.388-2.482 1.349.002.18.185v1.4l-2.388 2.482ZM36.54 17.227v-.267l1.274-1.32h.254l2.388 2.479v1.398l-.18.19h-1.349l-2.388-2.48ZM28.154 24.107h10.5l.153.16v1.926l-.153.16h-10.5l-.154-.16v-1.927l.154-.16ZM99.844 24.11l.154.159v1.924l-.154.16H89.19l-.153-.16.77-1.924.154-.16h9.883ZM49 29.24h-1.853l-.153-.16v-4.502c0-.802-.303-1.423-1.234-1.443a24.2 24.2 0 0 0-1.613.025l-.089.092v5.826l-.153.16h-1.853l-.154-.16v-7.695l.154-.16h4.169c1.62 0 2.933 1.364 2.933 3.046v4.81l-.154.159v.002ZM57.974 25.871l-.154.16h-4.79l-.154.16c0 .32.31 1.283 1.546 1.283.463 0 .926-.16 1.082-.481l.154-.16h1.852l.154.16c-.154.962-.926 2.407-3.245 2.407-2.625 0-3.861-1.924-3.861-4.172 0-2.248 1.236-4.171 3.708-4.171 2.472 0 3.708 1.924 3.708 4.171v.643Zm-2.319-1.605c0-.16-.153-1.283-1.39-1.283-1.235 0-1.389 1.124-1.389 1.283l.154.16h2.472l.153-.16ZM62.298 26.833c0 .322.154.481.464.481h1.39l.153.16v1.604l-.154.16h-1.39c-1.389 0-2.625-.643-2.625-2.407v-3.529l-.154-.16H58.9l-.154-.159v-1.605l.154-.16h1.082l.154-.159v-1.443l.154-.16h1.852l.154.16v1.443l.154.16h1.699l.153.16v1.604l-.153.16h-1.7l-.153.159v3.529l.002.002ZM68.015 29.24h-1.853l-.153-.16V18.172l.153-.16h1.853l.154.16v10.907l-.154.16v.002ZM72.186 19.935h-1.852l-.154-.16v-1.604l.154-.16h1.852l.154.16v1.605l-.154.16Zm0 9.305h-1.852l-.154-.16v-7.7l.154-.159h1.852l.154.16v7.7l-.154.16ZM79.449 18.171v1.605l-.154.16h-1.39c-.309 0-.463.159-.463.48v.643l.154.16h1.546l.153.16v1.604l-.153.16h-1.546l-.154.159v5.774l-.153.16h-1.853l-.154-.16v-5.774l-.153-.16h-1.083l-.153-.159v-1.605l.153-.16h1.083l.153-.159v-.643c0-1.764 1.236-2.407 2.626-2.407h1.39l.153.16-.002.002ZM85.163 29.4c-.619 1.605-1.236 2.567-3.398 2.567h-.773l-.154-.16v-1.605l.154-.16h.773c.773 0 .926-.159 1.082-.64v-.16l-2.472-6.257v-1.604l.154-.16h1.39l.153.16 1.853 5.455h.153l1.853-5.455.154-.16h1.39l.153.16v1.604l-2.472 6.417.007-.002Z"/></g><path fill="#fff" d="M0 0v50h15V0z"/><path d="M6.518 45.367v-.774H11v.774H6.518Zm0-1.746c0-.44.098-.784.294-1.032.196-.248.474-.372.834-.372.348 0 .622.13.822.39.2.26.3.628.3 1.104l-.336.018c.004-.608.114-1.062.33-1.362.212-.3.524-.45.936-.45.424 0 .748.146.972.438.22.288.33.686.33 1.194v1.242h-.738v-1.152c0-.268-.05-.488-.15-.66a.498.498 0 0 0-.462-.258c-.224 0-.39.088-.498.264-.108.172-.162.39-.162.654v1.146h-.738V43.68c0-.192-.04-.36-.12-.504a.426.426 0 0 0-.396-.216c-.192 0-.33.068-.414.204a.955.955 0 0 0-.126.51v1.116h-.678v-1.17Zm0-2.925v-.774H9.17c.256 0 .472-.04.648-.12a.88.88 0 0 0 .39-.336.926.926 0 0 0 .126-.486.926.926 0 0 0-.126-.486.845.845 0 0 0-.39-.33 1.497 1.497 0 0 0-.648-.126H6.518v-.75h2.664c.604 0 1.068.146 1.392.438.324.288.486.71.486 1.266 0 .56-.162.984-.486 1.272-.324.288-.788.432-1.392.432H6.518Zm0-5.048v-.774H11v.774H6.518Zm0-2.475V32.4H11v.774H6.518Zm3.75-.546v-2.412H11v2.412h-.732Zm-3.69-3.903v-.773H11v.773H6.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-6.98v-.827l3.684-1.056v.318L6.518 20.43v-.888l3.684-1.122v.318L6.518 17.64v-.798L11 18.18v.81l-4.086 1.254v-.438L11 21.097v.756l-4.482 1.302Zm0-7.385v-.774H11v.774H6.518Zm.06-3.283v-.774H11v.774H6.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-4.7v-.773H11v.774H6.518Zm1.572-.545V6.179h.732V8.65H8.09ZM6.518 6.407v-.774H11v.774H6.518Z" fill="#060B10"/></g><rect x="1" y="1" width="112" height="48" rx="3" stroke="#060B10" stroke-width="2"/><defs><clipPath id="a"><rect width="114" height="50" rx="4" fill="#fff"/></clipPath><clipPath id="b"><path fill="#fff" d="M28 10h72v30.462H28z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="114" height="50" viewBox="0 0 114 50" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><rect width="114" height="50" rx="4" fill="#181A1C"/><path fill="#fff" d="M0 0h114v51H0z"/><g clip-path="url(#b)" fill="#181A1C"><path d="M44.445 40.3v-7.694l.153-.16h1.853l.154.16V40.3l-.154.16h-1.853l-.153-.16ZM44.445 17.854V10.16l.153-.16h1.853l.154.16v7.694l-.154.16h-1.853l-.153-.16ZM38.068 34.89h-.254l-1.275-1.322v-.264l2.388-2.482 1.349.002.18.184v1.4l-2.388 2.483ZM36.54 17.226v-.266l1.274-1.321h.254l2.388 2.48v1.397l-.18.19h-1.349l-2.388-2.48ZM28.154 24.106h10.5l.153.16v1.926l-.153.16h-10.5l-.154-.16v-1.926l.154-.16ZM99.844 24.109l.154.16v1.923l-.154.16H89.19l-.153-.16.77-1.924.154-.16h9.883ZM49 29.24h-1.853l-.153-.16v-4.503c0-.802-.303-1.423-1.234-1.443-.48-.012-1.027 0-1.613.025l-.089.093v5.825l-.153.16h-1.853l-.154-.16v-7.694l.154-.16h4.169c1.62 0 2.933 1.363 2.933 3.045v4.81l-.154.159v.002ZM57.974 25.87l-.154.16h-4.79l-.154.16c0 .32.31 1.283 1.546 1.283.463 0 .926-.16 1.082-.481l.154-.16h1.852l.154.16c-.154.962-.926 2.407-3.245 2.407-2.625 0-3.861-1.924-3.861-4.172 0-2.247 1.236-4.17 3.708-4.17 2.472 0 3.708 1.923 3.708 4.17v.643Zm-2.319-1.604c0-.16-.153-1.284-1.39-1.284-1.235 0-1.389 1.124-1.389 1.284l.154.16h2.472l.153-.16ZM62.298 26.832c0 .322.154.481.464.481h1.39l.153.16v1.604l-.154.16h-1.39c-1.389 0-2.625-.643-2.625-2.407V23.3l-.154-.16H58.9l-.154-.159v-1.604l.154-.16h1.082l.154-.16v-1.442l.154-.16h1.852l.154.16v1.443l.154.16h1.699l.153.159v1.604l-.153.16h-1.7l-.153.16v3.528l.002.002ZM68.015 29.24h-1.853l-.153-.16V18.17l.153-.159h1.853l.154.16v10.906l-.154.16v.002ZM72.186 19.935h-1.852l-.154-.16v-1.604l.154-.16h1.852l.154.16v1.604l-.154.16Zm0 9.304h-1.852l-.154-.16V21.38l.154-.16h1.852l.154.16v7.7l-.154.16ZM79.449 18.17v1.605l-.154.16h-1.39c-.309 0-.463.16-.463.48v.644l.154.16h1.546l.153.159v1.604l-.153.16h-1.546l-.154.16v5.773l-.153.16h-1.853l-.154-.16V23.3l-.153-.16h-1.083l-.153-.159v-1.604l.153-.16h1.083l.153-.16v-.642c0-1.764 1.236-2.407 2.626-2.407h1.39l.153.16-.002.002ZM85.163 29.399c-.619 1.604-1.236 2.566-3.398 2.566h-.773l-.154-.16v-1.604l.154-.16h.773c.773 0 .926-.159 1.082-.64v-.16l-2.472-6.256V21.38l.154-.16h1.39l.153.16 1.853 5.455h.153l1.853-5.455.154-.16h1.39l.153.16v1.605L85.156 29.4l.007-.002Z"/></g><path fill="#181A1C" d="M0 0h15v50H0z"/><path d="M5.518 45.367v-.774H10v.774H5.518Zm0-1.746c0-.44.098-.784.294-1.032.196-.248.474-.372.834-.372.348 0 .622.13.822.39.2.26.3.628.3 1.104l-.336.018c.004-.608.114-1.062.33-1.362.212-.3.524-.45.936-.45.424 0 .748.146.972.438.22.288.33.686.33 1.194v1.242h-.738v-1.152c0-.268-.05-.488-.15-.66a.498.498 0 0 0-.462-.258c-.224 0-.39.088-.498.264-.108.172-.162.39-.162.654v1.146h-.738V43.68c0-.192-.04-.36-.12-.504a.426.426 0 0 0-.396-.216c-.192 0-.33.068-.414.204a.955.955 0 0 0-.126.51v1.116h-.678v-1.17Zm0-2.925v-.774H8.17c.256 0 .472-.04.648-.12a.88.88 0 0 0 .39-.336.926.926 0 0 0 .126-.486.926.926 0 0 0-.126-.486.845.845 0 0 0-.39-.33 1.497 1.497 0 0 0-.648-.126H5.518v-.75h2.664c.604 0 1.068.146 1.392.438.324.288.486.71.486 1.266 0 .56-.162.984-.486 1.272-.324.288-.788.432-1.392.432H5.518Zm0-5.048v-.774H10v.774H5.518Zm0-2.475V32.4H10v.774H5.518Zm3.75-.546v-2.412H10v2.412h-.732Zm-3.69-3.903v-.773H10v.773H5.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-6.98v-.827l3.684-1.056v.318L5.518 20.43v-.888l3.684-1.122v.318L5.518 17.64v-.798L10 18.18v.81l-4.086 1.254v-.438L10 21.097v.756l-4.482 1.302Zm0-7.385v-.774H10v.774H5.518Zm.06-3.283v-.774H10v.774H5.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-4.7v-.773H10v.774H5.518Zm1.572-.545V6.179h.732V8.65H7.09ZM5.518 6.407v-.774H10v.774H5.518Z" fill="#fff"/></g><rect x="1" y="1" width="112" height="48" rx="3" stroke="#181A1C" stroke-width="2"/><defs><clipPath id="a"><rect width="114" height="50" rx="4" fill="#fff"/></clipPath><clipPath id="b"><path fill="#fff" d="M28 10h72v30.46H28z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
+5
-3
@@ -4,7 +4,7 @@ outline: deep
|
||||
|
||||
# WXT Storage
|
||||
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md) • [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage)
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md)
|
||||
|
||||
A simplified wrapper around the extension storage APIs.
|
||||
|
||||
@@ -74,7 +74,7 @@ await storage.watch<number>(
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
> 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.
|
||||
For a full list of methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage).
|
||||
|
||||
## Watchers
|
||||
|
||||
@@ -146,7 +146,7 @@ const showChangelogOnUpdate = storage.defineItem<boolean>(
|
||||
);
|
||||
```
|
||||
|
||||
Now, instead of using the `storage` variable, you can use the storage item instead:
|
||||
Now, instead of using the `storage` variable, you can use the helper functions on the storage item you created:
|
||||
|
||||
```ts
|
||||
await showChangelogOnUpdate.getValue();
|
||||
@@ -157,6 +157,8 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => {
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
You 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.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Output docs/assets/init-demo.gif
|
||||
|
||||
# The tools we will use
|
||||
Require bun
|
||||
Require pnpm
|
||||
|
||||
|
||||
# === Scene ====
|
||||
@@ -26,7 +26,7 @@ Set FontSize 32
|
||||
# Terminal settings
|
||||
Set Shell "bash"
|
||||
|
||||
# Terminal prompt. It looks like "● command ..."
|
||||
# Terminal prompt. It looks like "● mycommand ..."
|
||||
Env PS1 "\e[0;32m●\e[0m "
|
||||
|
||||
|
||||
@@ -35,22 +35,19 @@ Env PS1 "\e[0;32m●\e[0m "
|
||||
|
||||
Hide
|
||||
# Create a temporary folder for demo
|
||||
Type 'mkdir packages/wxt/init-demo' Enter
|
||||
Type 'cd packages/wxt/init-demo' Enter
|
||||
# Unset CI variables inherited from the GitHub Actions runner so consola
|
||||
# picks its fancy reporter instead of the basic one (#2461)
|
||||
Type 'unset CI GITHUB_ACTIONS' Enter
|
||||
Type 'vhs_sandbox="$(mktemp -d)"' Enter
|
||||
Type 'cd "$vhs_sandbox"' Enter
|
||||
Type 'clear' Enter
|
||||
Show
|
||||
|
||||
|
||||
# ===== Actions =====
|
||||
|
||||
Type 'bunx wxt init .' Sleep 1s Enter
|
||||
Type 'pnpm dlx wxt@latest init .' Sleep 1s Enter
|
||||
Sleep 3.25s
|
||||
Down@750ms Enter@750ms # Select `vue` template
|
||||
Sleep 1.25s
|
||||
Down@500ms Down@500ms Enter@750ms # Select `pnpm` as a package manager
|
||||
Down@750ms Enter@750ms # Select `pnpm` as a package manager
|
||||
Sleep 5s
|
||||
|
||||
|
||||
@@ -58,6 +55,5 @@ Sleep 5s
|
||||
|
||||
Hide
|
||||
# Delete the temporary folder
|
||||
Type 'cd .."' Enter
|
||||
Type 'rm -rf init-demo' Enter
|
||||
Type 'rm -rf "$vhs_sandbox"' Enter
|
||||
Show
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[build]
|
||||
command = "bun run docs:build"
|
||||
publish = "docs/.vitepress/dist"
|
||||
|
||||
[build.environment]
|
||||
BUN_VERSION = "1.3.5"
|
||||
+30
-54
@@ -2,83 +2,59 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": ">=1.3.5"
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
|
||||
"scripts": {
|
||||
"check": "check && bun run --workspaces check",
|
||||
"test": "bun run --workspaces test run",
|
||||
"test:coverage": "bun run --workspaces test:coverage",
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
"test:coverage": "pnpm -r --sequential run test:coverage",
|
||||
"prepare": "simple-git-hooks",
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "bun run --silent docs:gen && bun run --bun vitepress dev docs",
|
||||
"docs:build": "bun run --silent docs:gen && bun run --bun vitepress build docs",
|
||||
"docs:preview": "bun run --silent docs:gen && bun run --bun vitepress preview docs"
|
||||
},
|
||||
"catalog": {
|
||||
"@aklinker1/buildc": "^1.1.7",
|
||||
"@aklinker1/check": "^2.4.0",
|
||||
"@types/bun": "^1.3.5",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@webext-core/fake-browser": "^1.3.4",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"oxlint": "^1.63.0",
|
||||
"publint": "^0.3.18",
|
||||
"tsdown": "^0.21.0",
|
||||
"typescript": "^6.0.3",
|
||||
"unocss": "^66.6.8",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.1.5",
|
||||
"vitest-plugin-random-seed": "^1.1.2"
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@commitlint/config-conventional": "^21.2.0",
|
||||
"@commitlint/types": "^21.2.0",
|
||||
"@types/bun": "catalog:",
|
||||
"@aklinker1/buildc": "^1.1.6",
|
||||
"@aklinker1/check": "^2.2.0",
|
||||
"@commitlint/config-conventional": "^20.4.1",
|
||||
"@commitlint/types": "^20.4.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"changelogen": "^0.6.2",
|
||||
"consola": "^3.4.2",
|
||||
"cspell": "^9.7.0",
|
||||
"feed": "^5.2.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"feed": "^5.2.0",
|
||||
"fs-extra": "^11.3.3",
|
||||
"lint-staged": "^16.2.7",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"markdownlint-cli": "^0.49.0",
|
||||
"nano-spawn": "catalog:",
|
||||
"nano-staged": "^1.0.2",
|
||||
"markdownlint-cli": "^0.47.0",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"p-map": "^7.0.4",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-jsdoc": "^1.8.0",
|
||||
"semver": "^7.8.0",
|
||||
"semver": "^7.7.4",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"tinyglobby": "^0.2.16",
|
||||
"tsdown": "catalog:",
|
||||
"tsdown": "^0.20.3",
|
||||
"tsx": "4.21.0",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-frontmatter": "^1.3.1",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "catalog:",
|
||||
"typescript": "^5.9.3",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-knowledge": "^0.4.1",
|
||||
"vitepress-plugin-group-icons": "^1.7.3",
|
||||
"vitepress-plugin-llms": "^1.12.2",
|
||||
"vitest-mock-extended": "^4.0.0",
|
||||
"vue": "^3.5.34",
|
||||
"vitepress-plugin-group-icons": "^1.7.1",
|
||||
"vitepress-plugin-llms": "^1.11.0",
|
||||
"vitest-mock-extended": "^3.1.0",
|
||||
"vue": "^3.5.27",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"markdown-it-footnote@4.0.0": "patches/markdown-it-footnote.patch",
|
||||
"simple-git-hooks@2.13.1": "patches/simple-git-hooks@2.13.1.patch"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "./node_modules/.bin/nano-staged"
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"nano-staged": {
|
||||
"lint-staged": {
|
||||
"*": "prettier --ignore-unknown --write"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.4...analytics-v0.5.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **analytics:** Add Moderok analytics provider ([#2393](https://github.com/wxt-dev/wxt/pull/2393))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **analytics:** Change method return types from void to Promise<void> ([#2409](https://github.com/wxt-dev/wxt/pull/2409))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `prettier-plugin-jsdoc` to project ([#2171](https://github.com/wxt-dev/wxt/pull/2171))
|
||||
- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))
|
||||
- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))
|
||||
- Use `deps.neverBundle` instead of `external` ([8f71e174](https://github.com/wxt-dev/wxt/commit/8f71e174))
|
||||
- Migrate monorepo to use Bun instead of PNPM ([#2009](https://github.com/wxt-dev/wxt/pull/2009))
|
||||
- **deps-dev:** Bump typescript from 5.9.3 to 6.0.3 ([#2325](https://github.com/wxt-dev/wxt/pull/2325))
|
||||
- Use `catalog:` for dev dependencies ([#2357](https://github.com/wxt-dev/wxt/pull/2357))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Muhammed Mustafa AKŞAM ([@muhammedaksam](https://github.com/muhammedaksam))
|
||||
- Vaughn Bosu ([@VaughnBosu](https://github.com/VaughnBosu))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.5.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.3...analytics-v0.5.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Continue using `useAppConfig` to support older versions of WXT ([bfd94556](https://github.com/wxt-dev/wxt/commit/bfd94556))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.5.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))
|
||||
- Allow `userId` option to return `undefined` ([636cf1f8](https://github.com/wxt-dev/wxt/commit/636cf1f8))
|
||||
- Improve background script detection logic for analytics package ([#1808](https://github.com/wxt-dev/wxt/pull/1808))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Created new types, instead of `any` for `analytics` ([#2119](https://github.com/wxt-dev/wxt/pull/2119))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Smit ([@sm17p](https://github.com/sm17p))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))
|
||||
|
||||
## v0.5.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.1...analytics-v0.5.2)
|
||||
|
||||
@@ -5,8 +5,6 @@ Report analytics events from your web extension extension.
|
||||
## Supported Analytics Providers
|
||||
|
||||
- [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol)
|
||||
- [Moderok](#moderok)
|
||||
- [PostHog](#posthog)
|
||||
- [Umami](#umami)
|
||||
|
||||
## Install With WXT
|
||||
@@ -122,70 +120,6 @@ export default defineAppConfig({
|
||||
});
|
||||
```
|
||||
|
||||
### Moderok
|
||||
|
||||
[Moderok](https://moderok.dev) is an analytics platform built specifically for browser extensions. It requires no `host_permissions`, works in Manifest V3 service workers, and collects only anonymous usage data.
|
||||
|
||||
Sign up at [moderok.dev](https://moderok.dev) to get your app key, then save it to your `.env` file:
|
||||
|
||||
```dotenv
|
||||
WXT_MODEROK_APP_KEY='mk_...'
|
||||
```
|
||||
|
||||
Then add the `moderok` provider to your `<srcDir>/app.config.ts` file:
|
||||
|
||||
```ts
|
||||
import { moderok } from '@wxt-dev/analytics/providers/moderok';
|
||||
|
||||
export default defineAppConfig({
|
||||
analytics: {
|
||||
providers: [
|
||||
moderok({
|
||||
appKey: import.meta.env.WXT_MODEROK_APP_KEY,
|
||||
// Automatically track first open, install, update, and daily ping events (default: true)
|
||||
trackLifecycle: true,
|
||||
// Track when users uninstall the extension (default: false)
|
||||
trackUninstalls: false,
|
||||
// Optional: when trackUninstalls is on, redirect users to this page
|
||||
// after they uninstall (e.g. a feedback survey)
|
||||
uninstallUrl: 'https://example.com/uninstall',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For a full walkthrough — module setup, sending events, and all provider options — see the [Moderok WXT guide](https://docs.moderok.dev/guide/wxt).
|
||||
|
||||
### PostHog
|
||||
|
||||
[PostHog](https://posthog.com/) is an open source product analytics platform. It supports event tracking, session recording, feature flags, surveys, and more.
|
||||
|
||||
In your PostHog project settings, find your **Project API key** and save it to your `.env` file:
|
||||
|
||||
```dotenv
|
||||
WXT_POSTHOG_API_KEY='phc_...'
|
||||
```
|
||||
|
||||
Then add the `posthog` provider to your `<srcDir>/app.config.ts` file:
|
||||
|
||||
```ts
|
||||
import { posthog } from '@wxt-dev/analytics/providers/posthog';
|
||||
|
||||
export default defineAppConfig({
|
||||
analytics: {
|
||||
providers: [
|
||||
posthog({
|
||||
apiKey: import.meta.env.WXT_POSTHOG_API_KEY,
|
||||
// apiHost defaults to 'https://us.i.posthog.com'.
|
||||
// Change to 'https://eu.i.posthog.com' for EU Cloud, or your self-hosted URL.
|
||||
apiHost: 'https://eu.i.posthog.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Umami
|
||||
|
||||
[Umami](https://umami.is/) is a privacy-first, open source analytics platform.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineAppConfig } from 'wxt/utils/define-app-config';
|
||||
import { googleAnalytics4 } from './modules/analytics/providers/google-analytics-4';
|
||||
import { posthog } from './modules/analytics/providers/posthog';
|
||||
import { umami } from './modules/analytics/providers/umami';
|
||||
|
||||
export default defineAppConfig({
|
||||
@@ -11,9 +10,6 @@ export default defineAppConfig({
|
||||
apiSecret: '...',
|
||||
measurementId: '...',
|
||||
}),
|
||||
posthog({
|
||||
apiKey: '...',
|
||||
}),
|
||||
umami({
|
||||
apiUrl: 'https://umami.aklinker1.io/api',
|
||||
domain: 'analytics.wxt.dev',
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
BaseAnalyticsEvent,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
import { isBackground } from '@wxt-dev/is-background';
|
||||
|
||||
type AnalyticsMessage = {
|
||||
[K in keyof Analytics]: {
|
||||
@@ -25,7 +24,7 @@ type AnalyticsMethod =
|
||||
|
||||
type MethodForwarder = <K extends keyof Analytics>(
|
||||
fn: K,
|
||||
) => (...args: Parameters<Analytics[K]>) => Promise<void>;
|
||||
) => (...args: Parameters<Analytics[K]>) => void;
|
||||
|
||||
const ANALYTICS_PORT = '@wxt-dev/analytics';
|
||||
|
||||
@@ -56,14 +55,16 @@ export function createAnalytics(config?: AnalyticsConfig): Analytics {
|
||||
);
|
||||
}
|
||||
|
||||
if (isBackground()) return createBackgroundAnalytics(config);
|
||||
// TODO: This only works for standard WXT extensions, add a more generic
|
||||
// background script detector that works with non-WXT projects.
|
||||
if (location.pathname === '/background.js')
|
||||
return createBackgroundAnalytics(config);
|
||||
|
||||
return createFrontendAnalytics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analytics client in the background responsible for uploading
|
||||
* events to the server to avoid CORS errors.
|
||||
* Creates an analytics client in the background responsible for uploading events to the server to avoid CORS errors.
|
||||
*/
|
||||
function createBackgroundAnalytics(
|
||||
config: AnalyticsConfig | undefined,
|
||||
@@ -84,13 +85,9 @@ function createBackgroundAnalytics(
|
||||
// Cached values
|
||||
const platformInfo = browser.runtime.getPlatformInfo();
|
||||
const userAgent = UAParser();
|
||||
let userId = Promise.resolve(userIdStorage.getValue()).then(async (id) => {
|
||||
if (id != null) return id;
|
||||
// Persist the generated ID so it's stable across service worker restarts.
|
||||
const generatedId = globalThis.crypto.randomUUID();
|
||||
await userIdStorage.setValue?.(generatedId);
|
||||
return generatedId;
|
||||
});
|
||||
let userId = Promise.resolve(userIdStorage.getValue()).then(
|
||||
(id) => id ?? globalThis.crypto.randomUUID(),
|
||||
);
|
||||
let userProperties = userPropertiesStorage.getValue();
|
||||
const manifest = browser.runtime.getManifest();
|
||||
|
||||
@@ -224,7 +221,9 @@ function createBackgroundAnalytics(
|
||||
return analytics;
|
||||
}
|
||||
|
||||
/** Creates an analytics client for non-background contexts. */
|
||||
/**
|
||||
* Creates an analytics client for non-background contexts.
|
||||
*/
|
||||
function createFrontendAnalytics(): Analytics {
|
||||
const port = browser.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const sessionId = Date.now();
|
||||
@@ -242,7 +241,6 @@ function createFrontendAnalytics(): Analytics {
|
||||
(fn) =>
|
||||
(...args) => {
|
||||
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const analytics: Analytics = {
|
||||
|
||||
@@ -44,9 +44,9 @@ export default defineWxtModule({
|
||||
? clientModuleId
|
||||
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
|
||||
}';
|
||||
import { useAppConfig } from '#imports';
|
||||
import { getAppConfig } from '#imports';
|
||||
|
||||
export const analytics = createAnalytics(useAppConfig().analytics);
|
||||
export const analytics = createAnalytics(getAppConfig().analytics);
|
||||
`;
|
||||
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
|
||||
wxt.hook('prepare:types', async (_, entries) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const googleAnalytics4 =
|
||||
eventProperties: Record<string, string | undefined> | undefined,
|
||||
): Promise<void> => {
|
||||
const url = new URL(
|
||||
config.debug ? '/debug/mp/collect' : '/mp/collect',
|
||||
config?.debug ? '/debug/mp/collect' : '/mp/collect',
|
||||
options.apiUrl ?? 'https://www.google-analytics.com',
|
||||
);
|
||||
if (options.apiSecret)
|
||||
@@ -38,12 +38,6 @@ export const googleAnalytics4 =
|
||||
]),
|
||||
);
|
||||
|
||||
if (config.debug) {
|
||||
console.debug(
|
||||
'[@wxt-dev/analytics] Sending event to Google Analytics 4:',
|
||||
{ eventName, eventProperties },
|
||||
);
|
||||
}
|
||||
await fetch(url.href, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import { defineAnalyticsProvider } from '../client';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
import type { BaseAnalyticsEvent } from '../types';
|
||||
|
||||
const SDK_VERSION = 'wxt/0.1.0';
|
||||
const DEFAULT_ENDPOINT = 'https://api.moderok.dev/v1/events';
|
||||
const PING_STORAGE_KEY = 'moderok:last-ping-date';
|
||||
const FIRST_OPEN_STORAGE_KEY = 'moderok:first-open';
|
||||
|
||||
export interface ModerokProviderOptions {
|
||||
appKey: string;
|
||||
endpoint?: string;
|
||||
trackLifecycle?: boolean;
|
||||
trackUninstalls?: boolean;
|
||||
uninstallUrl?: string;
|
||||
}
|
||||
|
||||
const OS_MAP: Record<string, string> = {
|
||||
mac: 'MacOS',
|
||||
win: 'Windows',
|
||||
linux: 'Linux',
|
||||
cros: 'ChromeOS',
|
||||
android: 'Android',
|
||||
};
|
||||
|
||||
const BROWSER_MAP: Record<string, string> = {
|
||||
chrome: 'chrome',
|
||||
edge: 'edge',
|
||||
firefox: 'firefox',
|
||||
chromium: 'other_chromium',
|
||||
};
|
||||
|
||||
function mapOs(wxtOs: string | undefined): string {
|
||||
if (!wxtOs) return 'unknown';
|
||||
return OS_MAP[wxtOs.toLowerCase()] ?? 'unknown';
|
||||
}
|
||||
|
||||
function mapBrowser(wxtBrowser: string | undefined): string {
|
||||
if (!wxtBrowser) return 'unknown';
|
||||
return BROWSER_MAP[wxtBrowser.toLowerCase()] ?? 'unknown';
|
||||
}
|
||||
|
||||
function detectSource(meta: BaseAnalyticsEvent['meta']): string {
|
||||
if (meta.sessionId == null) return 'background';
|
||||
|
||||
const url = meta.url;
|
||||
if (!url) return 'unknown';
|
||||
const isExtensionUrl =
|
||||
url.startsWith('chrome-extension://') || url.startsWith('moz-extension://');
|
||||
if (!isExtensionUrl) return 'content_script';
|
||||
|
||||
const path = url.toLowerCase();
|
||||
if (path.includes('popup')) return 'popup';
|
||||
if (path.includes('option')) return 'options';
|
||||
if (
|
||||
path.includes('sidepanel') ||
|
||||
path.includes('side-panel') ||
|
||||
path.includes('side_panel')
|
||||
)
|
||||
return 'side_panel';
|
||||
|
||||
return 'extension_page';
|
||||
}
|
||||
|
||||
function buildContext(event: BaseAnalyticsEvent, extensionId: string) {
|
||||
return {
|
||||
sdkVersion: SDK_VERSION,
|
||||
extensionId: extensionId || 'unknown',
|
||||
extensionVersion: event.user.properties.version || 'unknown',
|
||||
browser: mapBrowser(event.user.properties.browser),
|
||||
browserVersion: event.user.properties.browserVersion || 'unknown',
|
||||
os: mapOs(event.user.properties.os),
|
||||
locale: event.meta.language || 'unknown',
|
||||
source: detectSource(event.meta),
|
||||
};
|
||||
}
|
||||
|
||||
function sendEvent(
|
||||
endpoint: string,
|
||||
appKey: string,
|
||||
event: {
|
||||
name: string;
|
||||
userId: string;
|
||||
timestamp: number;
|
||||
context: ReturnType<typeof buildContext>;
|
||||
properties?: Record<string, string | number | boolean>;
|
||||
},
|
||||
debug: boolean,
|
||||
) {
|
||||
const payload = {
|
||||
appKey,
|
||||
sentAt: Date.now(),
|
||||
events: [
|
||||
{
|
||||
id: globalThis.crypto.randomUUID(),
|
||||
name: event.name,
|
||||
timestamp: event.timestamp,
|
||||
userId: event.userId,
|
||||
context: event.context,
|
||||
...(event.properties && Object.keys(event.properties).length > 0
|
||||
? { properties: event.properties }
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (debug) {
|
||||
console.debug('[@wxt-dev/analytics][moderok] Sending:', payload);
|
||||
}
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
function cleanProperties(
|
||||
raw: Record<string, string | undefined> | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
if (!raw) return undefined;
|
||||
const clean: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (value != null) clean[key] = value;
|
||||
}
|
||||
return Object.keys(clean).length > 0 ? clean : undefined;
|
||||
}
|
||||
|
||||
function utcDateStamp(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function buildUninstallUrl(
|
||||
endpoint: string,
|
||||
appKey: string,
|
||||
userId: string,
|
||||
redirectUrl?: string,
|
||||
): string {
|
||||
const url = new URL(endpoint);
|
||||
url.pathname = url.pathname.replace(/\/[^/]*$/, '/uninstall');
|
||||
url.searchParams.set('app', appKey);
|
||||
url.searchParams.set('uid', userId);
|
||||
if (redirectUrl) url.searchParams.set('redirect', redirectUrl);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export const moderok = defineAnalyticsProvider<ModerokProviderOptions>(
|
||||
(analytics, config, options) => {
|
||||
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
||||
const debug = config.debug ?? false;
|
||||
const trackLifecycle = options.trackLifecycle ?? true;
|
||||
const extensionId = browser.runtime.id ?? '';
|
||||
let firstOpenPromise: Promise<void> | undefined;
|
||||
|
||||
const maybeTrackFirstOpen = async (event: BaseAnalyticsEvent) => {
|
||||
if (!trackLifecycle) return;
|
||||
if (firstOpenPromise) return firstOpenPromise;
|
||||
|
||||
firstOpenPromise = (async () => {
|
||||
const stored = await browser.storage.local.get(FIRST_OPEN_STORAGE_KEY);
|
||||
if (stored[FIRST_OPEN_STORAGE_KEY]) return;
|
||||
|
||||
const response = await sendEvent(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
{
|
||||
name: '__first_open',
|
||||
userId: event.user.id,
|
||||
timestamp: event.meta.timestamp,
|
||||
context: buildContext(event, extensionId),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
await browser.storage.local.set({ [FIRST_OPEN_STORAGE_KEY]: true });
|
||||
} else {
|
||||
firstOpenPromise = undefined;
|
||||
}
|
||||
})().catch((error) => {
|
||||
firstOpenPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return firstOpenPromise;
|
||||
};
|
||||
|
||||
if (trackLifecycle) {
|
||||
browser.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === 'install') {
|
||||
analytics.track('__install');
|
||||
} else if (details.reason === 'update') {
|
||||
analytics.track('__update', {
|
||||
previousVersion: (details as { previousVersion?: string })
|
||||
.previousVersion,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
const today = utcDateStamp();
|
||||
const stored = await browser.storage.local.get(PING_STORAGE_KEY);
|
||||
if (stored[PING_STORAGE_KEY] === today) return;
|
||||
await browser.storage.local.set({ [PING_STORAGE_KEY]: today });
|
||||
analytics.track('__daily_ping');
|
||||
})();
|
||||
}
|
||||
|
||||
let uninstallUrlSet = false;
|
||||
function maybeSetUninstallUrl(userId: string) {
|
||||
if (!options.trackUninstalls || uninstallUrlSet) return;
|
||||
uninstallUrlSet = true;
|
||||
|
||||
const url = buildUninstallUrl(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
userId,
|
||||
options.uninstallUrl,
|
||||
);
|
||||
if (url.length <= 1023) {
|
||||
browser.runtime.setUninstallURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
identify: () => Promise.resolve(),
|
||||
|
||||
page: async (event) => {
|
||||
maybeSetUninstallUrl(event.user.id);
|
||||
await maybeTrackFirstOpen(event);
|
||||
await sendEvent(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
{
|
||||
name: '__page_view',
|
||||
userId: event.user.id,
|
||||
timestamp: event.meta.timestamp,
|
||||
context: buildContext(event, extensionId),
|
||||
properties: {
|
||||
url: event.page.url,
|
||||
...(event.page.title ? { title: event.page.title } : {}),
|
||||
...(event.page.location ? { location: event.page.location } : {}),
|
||||
},
|
||||
},
|
||||
debug,
|
||||
);
|
||||
},
|
||||
|
||||
track: async (event) => {
|
||||
maybeSetUninstallUrl(event.user.id);
|
||||
await maybeTrackFirstOpen(event);
|
||||
await sendEvent(
|
||||
endpoint,
|
||||
options.appKey,
|
||||
{
|
||||
name: event.event.name.trim(),
|
||||
userId: event.user.id,
|
||||
timestamp: event.meta.timestamp,
|
||||
context: buildContext(event, extensionId),
|
||||
properties: cleanProperties(event.event.properties),
|
||||
},
|
||||
debug,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1,83 +0,0 @@
|
||||
import { defineAnalyticsProvider } from '../client';
|
||||
|
||||
export interface PostHogProviderOptions {
|
||||
/** Your PostHog project API key. */
|
||||
apiKey: string;
|
||||
/**
|
||||
* PostHog API host URL.
|
||||
*
|
||||
* @default 'https://us.i.posthog.com'
|
||||
*/
|
||||
apiHost?: string;
|
||||
}
|
||||
|
||||
export const posthog = defineAnalyticsProvider<PostHogProviderOptions>(
|
||||
(_, config, options) => {
|
||||
const apiHost = (options.apiHost ?? 'https://us.i.posthog.com').replace(
|
||||
/\/$/,
|
||||
'',
|
||||
);
|
||||
|
||||
const capture = async (
|
||||
distinctId: string,
|
||||
event: string,
|
||||
properties: Record<string, unknown>,
|
||||
): Promise<void> => {
|
||||
if (config.debug) {
|
||||
console.debug('[@wxt-dev/analytics] Sending event to PostHog:', {
|
||||
event,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
const body: PostHogCaptureBody = {
|
||||
api_key: options.apiKey,
|
||||
distinct_id: distinctId,
|
||||
event,
|
||||
properties,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
await fetch(`${apiHost}/i/v0/e/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
identify: async (event) => {
|
||||
await capture(event.user.id, '$identify', {
|
||||
$set: event.user.properties,
|
||||
});
|
||||
},
|
||||
page: async (event) => {
|
||||
await capture(event.user.id, '$pageview', {
|
||||
$current_url: event.page.url,
|
||||
$title: event.page.title,
|
||||
$session_id: event.meta.sessionId,
|
||||
$screen: event.meta.screen,
|
||||
$language: event.meta.language,
|
||||
$referrer: event.meta.referrer,
|
||||
$set: event.user.properties,
|
||||
});
|
||||
},
|
||||
track: async (event) => {
|
||||
await capture(event.user.id, event.event.name, {
|
||||
...event.event.properties,
|
||||
$screen: event.meta.screen,
|
||||
$language: event.meta.language,
|
||||
$referrer: event.meta.referrer,
|
||||
$set: event.user.properties,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/** @see https://posthog.com/docs/api/capture */
|
||||
interface PostHogCaptureBody {
|
||||
api_key: string;
|
||||
distinct_id: string;
|
||||
event: string;
|
||||
properties: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -1,49 +1,43 @@
|
||||
export interface Analytics {
|
||||
/** Report a page change. */
|
||||
page: (url: string) => Promise<void>;
|
||||
page: (url: string) => void;
|
||||
/** Report a custom event. */
|
||||
track: (
|
||||
eventName: string,
|
||||
eventProperties?: Record<string, string | undefined>,
|
||||
) => Promise<void>;
|
||||
) => void;
|
||||
/** Save information about the user. */
|
||||
identify: (
|
||||
userId: string,
|
||||
userProperties?: Record<string, string>,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Automatically setup and track user interactions, returning a function to
|
||||
* remove any listeners that were setup.
|
||||
*/
|
||||
identify: (userId: string, userProperties?: Record<string, string>) => void;
|
||||
/** Automatically setup and track user interactions, returning a function to remove any listeners that were setup. */
|
||||
autoTrack: (root: Document | ShadowRoot | Element) => () => void;
|
||||
/** Calls `config.enabled.setValue`. */
|
||||
setEnabled: (enabled: boolean) => Promise<void>;
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export interface AnalyticsConfig {
|
||||
/** Array of providers to send analytics to. */
|
||||
/**
|
||||
* Array of providers to send analytics to.
|
||||
*/
|
||||
providers: AnalyticsProvider[];
|
||||
/** Enable debug logs and other provider-specific debugging features. */
|
||||
/**
|
||||
* Enable debug logs and other provider-specific debugging features.
|
||||
*/
|
||||
debug?: boolean;
|
||||
/**
|
||||
* Your extension's version, reported alongside events.
|
||||
*
|
||||
* @default browser.runtime.getManifest().version`.
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* Configure how the enabled flag is persisted. Defaults to using
|
||||
* `browser.storage.local`.
|
||||
* Configure how the enabled flag is persisted. Defaults to using `browser.storage.local`.
|
||||
*/
|
||||
enabled?: AnalyticsStorageItem<boolean>;
|
||||
/**
|
||||
* Configure how the user Id is persisted. Defaults to using
|
||||
* `browser.storage.local`.
|
||||
* Configure how the user Id is persisted. Defaults to using `browser.storage.local`.
|
||||
*/
|
||||
userId?: AnalyticsStorageItem<string | undefined>;
|
||||
/**
|
||||
* Configure how user properties are persisted. Defaults to using
|
||||
* `browser.storage.local`.
|
||||
* Configure how user properties are persisted. Defaults to using `browser.storage.local`.
|
||||
*/
|
||||
userProperties?: AnalyticsStorageItem<Record<string, string>>;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,14 @@
|
||||
{
|
||||
"name": "@wxt-dev/analytics",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.2",
|
||||
"description": "Add analytics to your web extension",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"dev:build": "buildc --deps-only -- wxt build",
|
||||
"check": "bun run build && check",
|
||||
"build": "buildc -- tsdown",
|
||||
"prepack": "bun run build",
|
||||
"postinstall": "buildc --deps-only -- wxt prepare",
|
||||
"test": "echo 'noop'",
|
||||
"test:coverage": "echo 'noop'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"@wxt-dev/is-background": "workspace:^",
|
||||
"ua-parser-js": "^1.0.40"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"publint": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/analytics"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
@@ -55,22 +29,35 @@
|
||||
"types": "./dist/providers/google-analytics-4.d.mts",
|
||||
"default": "./dist/providers/google-analytics-4.mjs"
|
||||
},
|
||||
"./providers/moderok": {
|
||||
"types": "./dist/providers/moderok.d.mts",
|
||||
"default": "./dist/providers/moderok.mjs"
|
||||
},
|
||||
"./providers/umami": {
|
||||
"types": "./dist/providers/umami.d.mts",
|
||||
"default": "./dist/providers/umami.mjs"
|
||||
},
|
||||
"./providers/posthog": {
|
||||
"types": "./dist/providers/posthog.d.mts",
|
||||
"default": "./dist/providers/posthog.mjs"
|
||||
}
|
||||
},
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"dev:build": "buildc --deps-only -- wxt build",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- tsdown",
|
||||
"prepack": "pnpm -s build",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"publint": "^0.3.17",
|
||||
"typescript": "^5.9.3",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"ua-parser-js": "^1.0.40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,8 @@ export default defineConfig({
|
||||
'providers/google-analytics-4':
|
||||
'./modules/analytics/providers/google-analytics-4.ts',
|
||||
'providers/umami': './modules/analytics/providers/umami.ts',
|
||||
'providers/moderok': './modules/analytics/providers/moderok.ts',
|
||||
'providers/posthog': './modules/analytics/providers/posthog.ts',
|
||||
},
|
||||
deps: {
|
||||
neverBundle: ['#analytics'],
|
||||
},
|
||||
external: ['#analytics'],
|
||||
define: {
|
||||
'process.env.NPM': 'true',
|
||||
},
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.1.0...auto-icons-v1.1.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Auto icons override default icons ([#1616](https://github.com/wxt-dev/wxt/pull/1616))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
|
||||
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
|
||||
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
|
||||
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
|
||||
- **deps:** Upgrade non-breaking production dependencies ([#1877](https://github.com/wxt-dev/wxt/pull/1877))
|
||||
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
|
||||
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
|
||||
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
|
||||
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Omerfardemir <od080624@gmail.com>
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v1.1.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.2...auto-icons-v1.1.0)
|
||||
|
||||
@@ -16,7 +16,7 @@ Install the package:
|
||||
npm i --save-dev @wxt-dev/auto-icons
|
||||
pnpm i -D @wxt-dev/auto-icons
|
||||
yarn add --dev @wxt-dev/auto-icons
|
||||
bun add -D @wxt-dev/auto-icons
|
||||
bun i -D @wxt-dev/auto-icons
|
||||
```
|
||||
|
||||
Add the module to `wxt.config.ts`:
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
@@ -39,25 +39,20 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- tsdown",
|
||||
"check": "bun run build && check",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"test:coverage": "bun run test run --coverage",
|
||||
"prepack": "bun run build"
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"fs-extra": "^11.3.3",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"oxlint": "^1.43.0",
|
||||
"publint": "^0.3.17",
|
||||
"typescript": "^5.9.3",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||
import { resolve } from 'node:path';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import type { Wxt, UserManifest } from 'wxt';
|
||||
|
||||
// Import the actual module
|
||||
import autoIconsModule from '../index';
|
||||
import type { AutoIconsOptions } from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
mkdir: vi.fn(),
|
||||
access: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('sharp', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
// Type definitions for better type safety
|
||||
interface MockWxt {
|
||||
config: {
|
||||
srcDir: string;
|
||||
outDir: string;
|
||||
mode: 'development' | 'production';
|
||||
};
|
||||
logger: {
|
||||
warn: Mock;
|
||||
};
|
||||
hooks: {
|
||||
hook: Mock;
|
||||
};
|
||||
}
|
||||
|
||||
interface PublicAsset {
|
||||
type: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
interface BuildOutput {
|
||||
publicAssets: PublicAsset[];
|
||||
}
|
||||
|
||||
describe('auto-icons module', () => {
|
||||
const mockWxt: MockWxt = {
|
||||
config: {
|
||||
srcDir: '/mock/src',
|
||||
outDir: '/mock/dist',
|
||||
mode: 'development',
|
||||
},
|
||||
logger: {
|
||||
warn: vi.fn(),
|
||||
},
|
||||
hooks: {
|
||||
hook: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const createMockSharpInstance = () => {
|
||||
const instance = {
|
||||
png: vi.fn(),
|
||||
grayscale: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
toFile: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
// Make methods chainable
|
||||
instance.png.mockReturnValue(instance);
|
||||
instance.grayscale.mockReturnValue(instance);
|
||||
instance.resize.mockImplementation(() => {
|
||||
// Create a new instance for each resize to simulate real sharp behavior
|
||||
const resizedInstance = { ...instance };
|
||||
resizedInstance.toFile = vi.fn().mockResolvedValue(undefined);
|
||||
return resizedInstance;
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
let mockSharpInstance: ReturnType<typeof createMockSharpInstance>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSharpInstance = createMockSharpInstance();
|
||||
vi.mocked(sharp).mockReturnValue(
|
||||
mockSharpInstance as unknown as sharp.Sharp,
|
||||
);
|
||||
vi.mocked(fsPromises.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fsPromises.mkdir).mockResolvedValue(undefined as any);
|
||||
});
|
||||
|
||||
describe('module setup', () => {
|
||||
it('should have correct module metadata', () => {
|
||||
expect(autoIconsModule.name).toBe('@wxt-dev/auto-icons');
|
||||
expect(autoIconsModule.configKey).toBe('autoIcons');
|
||||
expect(typeof autoIconsModule.setup).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('options handling', () => {
|
||||
it('should use default options when not provided', async () => {
|
||||
const options: AutoIconsOptions = {};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
// Verify that the module was set up (hooks were registered)
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:manifestGenerated',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:done',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'prepare:publicPaths',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge custom options with defaults', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
sizes: [64, 32],
|
||||
grayscaleOnDevelopment: false,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
// Verify that the module was set up with custom options
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:manifestGenerated',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'build:done',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
|
||||
'prepare:publicPaths',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should warn when disabled', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
|
||||
'`[auto-icons]` @wxt-dev/auto-icons disabled',
|
||||
);
|
||||
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn when base icon not found', async () => {
|
||||
vi.mocked(fsPromises.access).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
baseIconPath: 'assets/missing-icon.png',
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Skipping icon generation, no base icon found at',
|
||||
),
|
||||
);
|
||||
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('manifest generation hook', () => {
|
||||
it('should update manifest with default icons when no custom sizes provided', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
expect(manifestHook).toBeDefined();
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
// Should use default sizes: [128, 48, 32, 16]
|
||||
expect(manifest.icons).toEqual({
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge custom sizes with defaults', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
sizes: [96, 64], // These will be merged with defaults
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
expect(manifestHook).toBeDefined();
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
// defu merges arrays, so we get both custom and default sizes
|
||||
expect(manifest.icons).toEqual({
|
||||
96: 'icons/96.png',
|
||||
64: 'icons/64.png',
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn when overwriting existing icons in manifest', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
const manifest: UserManifest = {
|
||||
icons: {
|
||||
128: 'existing-icon.png',
|
||||
},
|
||||
};
|
||||
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
|
||||
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('icon generation hook', () => {
|
||||
it('should generate icons with default sizes', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const output: BuildOutput = {
|
||||
publicAssets: [],
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
expect(buildHook).toBeDefined();
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(sharp).toHaveBeenCalledWith(
|
||||
resolve('/mock/src', 'assets/icon.png'),
|
||||
);
|
||||
expect(mockSharpInstance.png).toHaveBeenCalled();
|
||||
|
||||
// Should resize to default sizes
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
|
||||
|
||||
expect(fsPromises.mkdir).toHaveBeenCalledWith(
|
||||
resolve('/mock/dist', 'icons'),
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
expect(output.publicAssets).toEqual([
|
||||
{ type: 'asset', fileName: 'icons/128.png' },
|
||||
{ type: 'asset', fileName: 'icons/48.png' },
|
||||
{ type: 'asset', fileName: 'icons/32.png' },
|
||||
{ type: 'asset', fileName: 'icons/16.png' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate icons with custom sizes merged with defaults', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
sizes: [96, 64],
|
||||
};
|
||||
|
||||
const output: BuildOutput = {
|
||||
publicAssets: [],
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
expect(buildHook).toBeDefined();
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
// Should include both custom and default sizes
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(96);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(64);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
|
||||
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
|
||||
|
||||
expect(output.publicAssets).toEqual([
|
||||
{ type: 'asset', fileName: 'icons/96.png' },
|
||||
{ type: 'asset', fileName: 'icons/64.png' },
|
||||
{ type: 'asset', fileName: 'icons/128.png' },
|
||||
{ type: 'asset', fileName: 'icons/48.png' },
|
||||
{ type: 'asset', fileName: 'icons/32.png' },
|
||||
{ type: 'asset', fileName: 'icons/16.png' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should apply grayscale in development mode', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
grayscaleOnDevelopment: true,
|
||||
sizes: [128],
|
||||
};
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(mockSharpInstance.grayscale).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not apply grayscale in production mode', async () => {
|
||||
const prodMockWxt = {
|
||||
...mockWxt,
|
||||
config: {
|
||||
...mockWxt.config,
|
||||
mode: 'production' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
grayscaleOnDevelopment: true,
|
||||
sizes: [128],
|
||||
};
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
await autoIconsModule.setup!(prodMockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(prodMockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(prodMockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not apply grayscale when disabled', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
grayscaleOnDevelopment: false,
|
||||
sizes: [128],
|
||||
};
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('public paths hook', () => {
|
||||
it('should add default icon paths to public paths', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const pathsHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
|
||||
|
||||
expect(pathsHook).toBeDefined();
|
||||
|
||||
const paths: string[] = [];
|
||||
if (pathsHook) {
|
||||
pathsHook(mockWxt as unknown as Wxt, paths);
|
||||
}
|
||||
|
||||
expect(paths).toEqual([
|
||||
'icons/128.png',
|
||||
'icons/48.png',
|
||||
'icons/32.png',
|
||||
'icons/16.png',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle empty sizes array', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
sizes: [], // Empty array should still merge with defaults
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
|
||||
// Should still have default sizes due to defu merge
|
||||
expect(manifest.icons).toEqual({
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle sharp processing errors gracefully', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Make toFile throw an error - need to properly chain resize -> png -> toFile
|
||||
const errorInstance = {
|
||||
toFile: vi.fn().mockRejectedValue(new Error('Sharp processing failed')),
|
||||
grayscale: vi.fn(),
|
||||
composite: vi.fn(),
|
||||
};
|
||||
errorInstance.grayscale.mockReturnValue(errorInstance);
|
||||
errorInstance.composite.mockReturnValue(errorInstance);
|
||||
|
||||
mockSharpInstance.resize = vi.fn().mockImplementation(() => ({
|
||||
png: vi.fn().mockReturnValue(errorInstance),
|
||||
}));
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
// Should throw the sharp error
|
||||
if (buildHook) {
|
||||
await expect(
|
||||
buildHook(mockWxt as unknown as Wxt, output),
|
||||
).rejects.toThrow('Sharp processing failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle file system errors during directory creation', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Make ensureDir throw an error
|
||||
vi.mocked(fsPromises.mkdir).mockRejectedValue(
|
||||
new Error('Directory creation failed'),
|
||||
);
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
// The module doesn't await ensureDir, so it won't throw
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
// But ensureDir should have been called
|
||||
expect(fsPromises.mkdir).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle custom base icon path correctly', async () => {
|
||||
const customPath = 'custom/icon.png';
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
baseIconPath: customPath,
|
||||
};
|
||||
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
|
||||
// Should resolve the path relative to srcDir
|
||||
expect(sharp).toHaveBeenCalledWith(resolve('/mock/src', customPath));
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test', () => {
|
||||
it('should handle full workflow correctly', async () => {
|
||||
const options: AutoIconsOptions = {
|
||||
enabled: true,
|
||||
baseIconPath: 'assets/custom-icon.png',
|
||||
sizes: [96], // Will be merged with defaults
|
||||
grayscaleOnDevelopment: false,
|
||||
};
|
||||
|
||||
const manifest: UserManifest = {};
|
||||
const output: BuildOutput = { publicAssets: [] };
|
||||
const paths: string[] = [];
|
||||
|
||||
// Setup the module
|
||||
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
|
||||
|
||||
// Execute all hooks
|
||||
const manifestHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
|
||||
const buildHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
|
||||
const pathsHook = vi
|
||||
.mocked(mockWxt.hooks.hook)
|
||||
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
|
||||
|
||||
if (manifestHook) {
|
||||
await manifestHook(mockWxt as unknown as Wxt, manifest);
|
||||
}
|
||||
if (buildHook) {
|
||||
await buildHook(mockWxt as unknown as Wxt, output);
|
||||
}
|
||||
if (pathsHook) {
|
||||
pathsHook(mockWxt as unknown as Wxt, paths);
|
||||
}
|
||||
|
||||
// Verify results - defu merges arrays
|
||||
expect(manifest.icons).toEqual({
|
||||
96: 'icons/96.png',
|
||||
128: 'icons/128.png',
|
||||
48: 'icons/48.png',
|
||||
32: 'icons/32.png',
|
||||
16: 'icons/16.png',
|
||||
});
|
||||
|
||||
expect(output.publicAssets).toEqual([
|
||||
{ type: 'asset', fileName: 'icons/96.png' },
|
||||
{ type: 'asset', fileName: 'icons/128.png' },
|
||||
{ type: 'asset', fileName: 'icons/48.png' },
|
||||
{ type: 'asset', fileName: 'icons/32.png' },
|
||||
{ type: 'asset', fileName: 'icons/16.png' },
|
||||
]);
|
||||
|
||||
expect(paths).toEqual([
|
||||
'icons/96.png',
|
||||
'icons/128.png',
|
||||
'icons/48.png',
|
||||
'icons/32.png',
|
||||
'icons/16.png',
|
||||
]);
|
||||
|
||||
expect(sharp).toHaveBeenCalledWith(
|
||||
resolve('/mock/src', 'assets/custom-icon.png'),
|
||||
);
|
||||
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import defu from 'defu';
|
||||
import sharp from 'sharp';
|
||||
import { access, mkdir } from 'node:fs/promises';
|
||||
import { ensureDir, pathExists } from 'fs-extra';
|
||||
|
||||
export default defineWxtModule<AutoIconsOptions>({
|
||||
name: '@wxt-dev/auto-icons',
|
||||
@@ -37,11 +37,7 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
if (!parsedOptions.enabled)
|
||||
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
|
||||
|
||||
const iconExists = await access(resolvedPath).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
if (!iconExists) {
|
||||
if (!(await pathExists(resolvedPath))) {
|
||||
return wxt.logger.warn(
|
||||
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
|
||||
);
|
||||
@@ -49,7 +45,7 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
|
||||
wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {
|
||||
if (manifest.icons)
|
||||
wxt.logger.warn(
|
||||
return wxt.logger.warn(
|
||||
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
|
||||
);
|
||||
|
||||
@@ -95,7 +91,7 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
}
|
||||
}
|
||||
|
||||
mkdir(resolve(outputFolder, 'icons'), { recursive: true });
|
||||
ensureDir(resolve(outputFolder, 'icons'));
|
||||
await resizedImage.toFile(resolve(outputFolder, `icons/${size}.png`));
|
||||
|
||||
output.publicAssets.push({
|
||||
@@ -113,11 +109,12 @@ export default defineWxtModule<AutoIconsOptions>({
|
||||
},
|
||||
});
|
||||
|
||||
/** Options for the auto-icons module */
|
||||
/**
|
||||
* Options for the auto-icons module
|
||||
*/
|
||||
export interface AutoIconsOptions {
|
||||
/**
|
||||
* Enable auto-icons generation
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
@@ -125,30 +122,27 @@ export interface AutoIconsOptions {
|
||||
* Path to the image to use.
|
||||
*
|
||||
* Path is relative to the project's src directory.
|
||||
*
|
||||
* @default '<srcDir>/assets/icon.png'
|
||||
* @default "<srcDir>/assets/icon.png"
|
||||
*/
|
||||
baseIconPath?: string;
|
||||
/**
|
||||
* Apply a visual indicator to the icon when running in development mode.
|
||||
*
|
||||
* "grayscale" converts the icon to grayscale. "overlay" covers the bottom
|
||||
* half with a yellow rectangle and writes "DEV" in black text. Set to `false`
|
||||
* to disable any indicator.
|
||||
* "grayscale" converts the icon to grayscale.
|
||||
* "overlay" covers the bottom half with a yellow rectangle and writes "DEV" in black text.
|
||||
* Set to `false` to disable any indicator.
|
||||
*
|
||||
* @default 'grayscale'
|
||||
* @default "grayscale"
|
||||
*/
|
||||
developmentIndicator?: 'grayscale' | 'overlay' | false;
|
||||
/**
|
||||
* Grayscale the image when in development mode to indicate development
|
||||
*
|
||||
* @deprecated Use `developmentIndicator` instead
|
||||
* @default true
|
||||
* @deprecated Use `developmentIndicator` instead
|
||||
*/
|
||||
grayscaleOnDevelopment?: boolean;
|
||||
/**
|
||||
* Sizes to generate icons for
|
||||
*
|
||||
* @default [128, 48, 32, 16]
|
||||
*/
|
||||
sizes?: number[];
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/browser",
|
||||
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
|
||||
"version": "0.2.2",
|
||||
"version": "0.1.37",
|
||||
"type": "module",
|
||||
"main": "src/index.mjs",
|
||||
"types": "src/index.d.ts",
|
||||
@@ -12,9 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"check": "check",
|
||||
"gen": "bun run scripts/generate.ts",
|
||||
"test": "echo 'noop'",
|
||||
"test:coverage": "echo 'noop'"
|
||||
"gen": "tsx scripts/generate.ts"
|
||||
},
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
@@ -25,11 +23,11 @@
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.2.2",
|
||||
"@types/node": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"@types/chrome": "0.1.37",
|
||||
"fs-extra": "^11.3.3",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/filesystem": "*",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import spawn from 'nano-spawn';
|
||||
import fs from 'fs-extra';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve, sep } from 'node:path';
|
||||
import { sep as posixSep } from 'node:path/posix';
|
||||
import { readdir, mkdir } from 'node:fs/promises';
|
||||
import { styleText } from 'node:util';
|
||||
|
||||
// Fetch latest version
|
||||
|
||||
console.log(`Getting latest version of ${styleText('cyan', '@types/chrome')}`);
|
||||
await Bun.$`bun install --ignore-scripts -D @types/chrome@latest`;
|
||||
console.log('Getting latest version of \x1b[36m@types/chrome\x1b[0m');
|
||||
await spawn('pnpm', ['i', '--ignore-scripts', '-D', '@types/chrome@latest']);
|
||||
|
||||
// Generate new package.json
|
||||
|
||||
console.log(`Generating new ${styleText('cyan', 'package.json')}`);
|
||||
console.log('Generating new \x1b[36mpackage.json\x1b[0m');
|
||||
|
||||
const pkgJsonPath = fileURLToPath(
|
||||
import.meta.resolve('@types/chrome/package.json'),
|
||||
);
|
||||
const pkgDir = dirname(pkgJsonPath);
|
||||
const pkgJson = await Bun.file(pkgJsonPath).json();
|
||||
const pkgJsonTemplate = await Bun.file('templates/package.json').text();
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
const pkgJsonTemplate = await fs.readFile('templates/package.json', 'utf8');
|
||||
const newPkgJson = JSON.parse(
|
||||
pkgJsonTemplate.replaceAll('{{chromeTypesVersion}}', pkgJson.version),
|
||||
);
|
||||
@@ -27,15 +27,15 @@ newPkgJson.peerDependencies = pkgJson.peerDependencies;
|
||||
newPkgJson.peerDependenciesMeta = pkgJson.peerDependenciesMeta;
|
||||
|
||||
const outPkgJsonPath = resolve('package.json');
|
||||
await Bun.write(outPkgJsonPath, JSON.stringify(newPkgJson));
|
||||
await Bun.$`bun run --cwd ../.. prettier --write "${outPkgJsonPath}"`;
|
||||
await fs.writeJson(outPkgJsonPath, newPkgJson);
|
||||
await spawn('pnpm', ['-w', 'prettier', '--write', outPkgJsonPath]);
|
||||
|
||||
// Generate declaration files
|
||||
|
||||
console.log('Generating declaration files');
|
||||
const outDir = resolve('src/gen');
|
||||
const declarationFileMapping = (
|
||||
await readdir(pkgDir, {
|
||||
await fs.readdir(pkgDir, {
|
||||
recursive: true,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
@@ -50,21 +50,17 @@ const declarationFileMapping = (
|
||||
}));
|
||||
|
||||
for (const { file, srcPath, destPath } of declarationFileMapping) {
|
||||
const content = await Bun.file(srcPath).text();
|
||||
const content = await fs.readFile(srcPath, 'utf8');
|
||||
const transformedContent = transformFile(file, content);
|
||||
const destDir = dirname(destPath);
|
||||
await mkdir(destDir, { recursive: true });
|
||||
await Bun.write(destPath, transformedContent);
|
||||
console.log(` ${styleText('dim', '-')} ${styleText('cyan', file)}`);
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
await fs.writeFile(destPath, transformedContent);
|
||||
console.log(` \x1b[2m-\x1b[0m \x1b[36m${file}\x1b[0m`);
|
||||
}
|
||||
|
||||
await Bun.$`bun install --ignore-scripts`;
|
||||
|
||||
// Done!
|
||||
|
||||
console.log(
|
||||
`${styleText('green', '✔')} Done in ${performance.now().toFixed(0)} ms`,
|
||||
);
|
||||
console.log('\x1b[32m✔\x1b[0m Done in ' + performance.now().toFixed(0) + ' ms');
|
||||
|
||||
// Transformations
|
||||
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
/* DO NOT EDIT - generated by scripts/generate.ts */
|
||||
|
||||
/* eslint-disable @definitelytyped/strict-export-declare-modifiers */
|
||||
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
|
||||
export namespace Browser {
|
||||
////////////////////
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
import { Entry, Log } from "har-format";
|
||||
|
||||
declare global {
|
||||
type HARFormatEntry = Entry;
|
||||
type HARFormatLog = Log;
|
||||
export type HARFormatEntry = Entry;
|
||||
export type HARFormatLog = Log;
|
||||
}
|
||||
|
||||
|
||||
+1824
-1896
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"check": "check",
|
||||
"gen": "bun run scripts/generate.ts",
|
||||
"test": "echo 'noop'",
|
||||
"test:coverage": "echo 'noop'"
|
||||
"gen": "tsx scripts/generate.ts"
|
||||
},
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
@@ -26,9 +24,9 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "{{chromeTypesVersion}}",
|
||||
"@types/node": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"fs-extra": "^11.3.3",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.5...i18n-v0.2.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add named substitutions to i18n ([#2440](https://github.com/wxt-dev/wxt/pull/2440))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `prettier-plugin-jsdoc` to project ([#2171](https://github.com/wxt-dev/wxt/pull/2171))
|
||||
- Replace fast-glob and ora with lighter alternatives ([#2173](https://github.com/wxt-dev/wxt/pull/2173))
|
||||
- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))
|
||||
- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))
|
||||
- Remove `any` from `i18n`'s internal types ([#2201](https://github.com/wxt-dev/wxt/pull/2201))
|
||||
- Add cspell and fix all typos ([6621aaf8](https://github.com/wxt-dev/wxt/commit/6621aaf8))
|
||||
- Migrate monorepo to use Bun instead of PNPM ([#2009](https://github.com/wxt-dev/wxt/pull/2009))
|
||||
- Add new types for i18n ([#2121](https://github.com/wxt-dev/wxt/pull/2121))
|
||||
- **deps-dev:** Bump typescript from 5.9.3 to 6.0.3 ([#2325](https://github.com/wxt-dev/wxt/pull/2325))
|
||||
- **deps-dev:** Bump oxlint from 1.59.0 to 1.63.0 ([#2356](https://github.com/wxt-dev/wxt/pull/2356))
|
||||
- Use `catalog:` for dev dependencies ([#2357](https://github.com/wxt-dev/wxt/pull/2357))
|
||||
- **deps:** Bump tinyglobby from 0.2.15 to 0.2.16 ([#2366](https://github.com/wxt-dev/wxt/pull/2366))
|
||||
- Add benchmarks for i18n vs vanilla usage ([#2372](https://github.com/wxt-dev/wxt/pull/2372))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- T ([@cookesan](https://github.com/cookesan))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
|
||||
- Florian Metz ([@Timeraa](https://github.com/Timeraa))
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.4...i18n-v0.2.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `.jsonc` support for locale files to match docs ([#2066](https://github.com/wxt-dev/wxt/pull/2066))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Manually fix markdownlint errors ([#1711](https://github.com/wxt-dev/wxt/pull/1711))
|
||||
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
|
||||
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
|
||||
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
|
||||
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
|
||||
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
|
||||
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
|
||||
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
|
||||
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
|
||||
- Upgrade major deps ([#2070](https://github.com/wxt-dev/wxt/pull/2070))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Ilya Kubariev ([@gymnasy55](https://github.com/gymnasy55))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)
|
||||
|
||||
+1
-18
@@ -161,7 +161,7 @@ hello: Hello $1!
|
||||
order: Thanks for ordering your $1
|
||||
```
|
||||
|
||||
#### Escaping `$`
|
||||
#### Escapting `$`
|
||||
|
||||
To escape the dollar sign, put another `$` in front of it:
|
||||
|
||||
@@ -173,23 +173,6 @@ dollars: $$$1
|
||||
i18n.t('dollars', ['1.00']); // "$1.00"
|
||||
```
|
||||
|
||||
#### Named Substitutions
|
||||
|
||||
You can also use named substitutions with `{key}` placeholders:
|
||||
|
||||
```yml
|
||||
welcome: Hello {name}, welcome to {appName}!
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('welcome', {
|
||||
name: 'Ada',
|
||||
appName: 'WXT',
|
||||
}); // "Hello Ada, welcome to WXT!"
|
||||
```
|
||||
|
||||
Missing keys are left unchanged in the returned message.
|
||||
|
||||
### Plural Forms
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# I18n Benchmarks
|
||||
|
||||
These benchmarks must be ran in an actual browser extension environment.
|
||||
|
||||
To run them, run `bench/run.ts` and load the `bench/` directory into a browser as an extension. Or just:
|
||||
|
||||
```sh
|
||||
bun --cwd packages/i18n bench
|
||||
```
|
||||
|
||||
Benchmarks are defined in `bench/background.ts`. Add or edit them there.
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"simple": {
|
||||
"message": "Just plain text"
|
||||
},
|
||||
"substitution": {
|
||||
"message": "Includes one substitution: $1"
|
||||
},
|
||||
"namedSubstitution": {
|
||||
"message": "Includes one named substitution: {value}"
|
||||
},
|
||||
"singular": {
|
||||
"message": "There is $1 item"
|
||||
},
|
||||
"plural": {
|
||||
"message": "There are $1 items"
|
||||
},
|
||||
"wxtPlural": {
|
||||
"message": "There is $1 item | There are $1 items"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user