Compare commits

...

41 Commits

Author SHA1 Message Date
github-actions[bot] cb851b1738 chore(release): @wxt-dev/analytics v0.5.5
📼 VHS / Create VHS (push) Waiting to run
2026-06-30 20:56:54 +00:00
ZerGo0 82825a5b35 fix: file reloading mechanism and add profile watch ignores (#2112)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-06-30 20:12:01 +00:00
Aaron a63f2d2a33 chore(dev-deps): Upgrade commitlint to v21 (#2471) 2026-06-30 15:05:18 -05:00
SmAsHeD d3983a603f docs: Revert #2380 - locationWatcher (#2416) 2026-06-30 19:55:39 +00:00
dependabot[bot] 9304319f55 chore(deps-dev): bump markdownlint-cli from 0.48.0 to 0.49.0 (#2466)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 14:45:38 -05:00
dependabot[bot] e778449377 chore(deps-dev): bump feed from 5.2.0 to 5.2.1 (#2469)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 14:43:51 -05:00
Patryk Kuniczak 3ecafbe0f3 docs: Update BlogLayout.vue useData to correctly type (#2369) 2026-06-30 08:02:05 -05:00
dependabot[bot] 9ab345b6dc chore(deps-dev): bump semver from 7.7.4 to 7.8.0 (#2377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 07:59:47 -05:00
dependabot[bot] ade1a6b670 chore(deps): bump codecov/codecov-action from 6.0.0 to 7.0.0 (#2407)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 07:58:15 -05:00
silverfish2525 af840d7169 feat: export getEntrypointBundlePath from wxt (#2463) 2026-06-30 12:55:09 +00:00
Gliches f2ced3f0e4 chore: Update code owners (#2462) 2026-06-30 07:51:19 -05:00
Muhammed Mustafa AKŞAM eb2e151520 fix(analytics): change method return types from void to Promise<void> (#2409)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-06-29 22:13:08 +00:00
dependabot[bot] b235c8591e chore(deps): bump actions/checkout from 6.0.2 to 7.0.0 (#2424)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-29 22:08:20 +00:00
dependabot[bot] 4c11f00176 chore(deps): bump actions/setup-go from 6.4.0 to 6.5.0 (#2432)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-29 21:58:26 +00:00
Gliches f75d1e1afc fix: opening the browser issue in windows. (#2436) 2026-06-29 21:05:04 +00:00
dependabot[bot] 4f419da1dc chore(deps): bump stefanzweifel/git-auto-commit-action from 7.1.0 to 7.2.0 (#2460)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 15:56:01 -05:00
Aaron d9c25ac4ce ci: Fix VHS action (#2459)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: ded-furby <190979964+ded-furby@users.noreply.github.com>
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-06-29 20:52:49 +00:00
T 8dfedf2a5a feat: add watch options config (#2439) 2026-06-29 15:32:46 +00:00
ded-furby 19f94e1e6a docs: prioritize Bun in package manager options (#2425)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-06-29 10:31:47 -05:00
T fc269ed78d chore: skip package manager tests when binaries are missing (#2441)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-06-29 14:44:35 +00:00
Aaron cfc633202b feat: Add prepare:tsconfig hook for updating .wxt/tsconfig.json file (#2450) 2026-06-29 09:42:19 -05:00
Aaron 6f1790deef fix(init): Allow entering an empty directory for project path (#2458) 2026-06-29 09:17:30 -05:00
T 331ce14170 fix: avoid deprecated Rollup asset name access (#2453) 2026-06-29 13:43:29 +00:00
Peter Wong a4c6cd1cd0 docs: Added "NiceTab" Edge Addons (edgeId) to extension showcase (#2455)
Co-authored-by: wangpeng618 <wangpeng618@jd.com>
2026-06-29 13:42:40 +00:00
T cb0e336812 fix: handle spread content script configs (#2448) 2026-06-29 08:37:59 -05:00
Shubhrajit Sadhukhan ba7c9cef60 docs: Update extension-showcase.yml (#2457) 2026-06-29 08:35:24 -05:00
dependabot[bot] 70b30ae431 chore(deps): bump pnpm/action-setup from 6.0.7 to 6.0.9 (#2418)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 13:31:21 +00:00
Patryk Kuniczak 12c267a5d1 docs: Fix undefined issue for ExampleSearch.vue (#2370) 2026-06-29 08:27:59 -05:00
Suvesh Moza 38625a8cd3 ci: use latest pnpm in validate workflow (#2390) 2026-06-29 08:25:47 -05:00
Aaron 700cd8c3e8 docs: Don't underline the comma when hovering over showcase item store links (#2454) 2026-06-29 01:29:06 -05:00
T c272fbe0ae docs: show Edge Add-ons in showcase (#2452)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-06-29 06:24:10 +00:00
Kral f40005da36 docs: document PerBrowser options for entrypoints (#2304) (#2403)
Co-authored-by: dashitongzhi <dashitongzhi@users.noreply.github.com>
Co-authored-by: ded-furby <ded-furby@users.noreply.github.com>
2026-06-28 20:51:14 +00:00
Gliches 34afac0165 chore: updating the alignment of the blue text in auto-mount-ui. (#2435) 2026-06-28 20:25:34 +00:00
Vaughn Bosu 7cbe9a477b feat(analytics): add Moderok analytics provider (#2393) 2026-06-28 15:16:31 -05:00
T 081d472176 feat: Add named substitutions to i18n (#2440) 2026-06-28 19:13:11 +00:00
Aaron d7f8ebec39 docs: Use bright green for example hover and link colors 2026-06-28 13:54:08 -05:00
Patryk Kuniczak bde65e2066 docs: Make vitepress components css files more readable and simply (#2272) 2026-06-28 13:53:25 -05:00
BlazerYoo 4896cd208e docs: Added package manager tab group synchronization across the entire installation page (#2421)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-06-27 19:03:26 +00:00
李超源 42a5b3cd67 Update extension-showcase.yml (#2433) 2026-06-25 19:26:27 +00:00
LUIDevo d60fd2516f fix: wxt clean removes .wxt/ directory at project root (#2415) 2026-06-24 09:09:13 -05:00
ded-furby 6b2c27cf45 docs: move Bun tabs to first position in installation (#2428) 2026-06-23 18:53:01 +00:00
75 changed files with 2136 additions and 661 deletions
+1 -1
View File
@@ -9,4 +9,4 @@
/packages/unocss/ @Timeraa
# Other Directories
/docs @aklinker1 @PatrykKuniczak
/docs @aklinker1 @PatrykKuniczak @nishu-murmu
@@ -10,7 +10,7 @@ jobs:
if: github.repository_owner == 'wxt-dev'
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: ${{ github.repository == 'wxt-dev/wxt' }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Only fetch the config file from the repository
sparse-checkout-cone-mode: false
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- validate
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
id-token: write # OIDC for NPM publishing
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
@@ -36,7 +36,7 @@ jobs:
- name: Commit Changes
id: commit
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+8 -8
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
@@ -68,15 +68,15 @@ jobs:
coverage: false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Setup PNPM
uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.0
version: latest
- name: Run Tests
if: ${{ ! matrix.coverage }}
@@ -88,7 +88,7 @@ jobs:
- name: Upload Coverage
if: matrix.coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -106,7 +106,7 @@ jobs:
- vue
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup
uses: ./.github/actions/setup
+9 -16
View File
@@ -17,27 +17,20 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref_name }}
- name: Setup PNPM
uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup NodeJS
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
cache: pnpm
- name: Setup
uses: ./.github/actions/setup
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
with:
go-version: '1.25.1'
# 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
# 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
- name: Record VHS
run: |
@@ -46,7 +39,7 @@ jobs:
vhs docs/tapes/init-demo.tape
- name: Save recorded GIF
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
Generated
+69 -36
View File
@@ -6,23 +6,23 @@
"devDependencies": {
"@aklinker1/buildc": "catalog:",
"@aklinker1/check": "catalog:",
"@commitlint/config-conventional": "^20.4.3",
"@commitlint/types": "^20.4.3",
"@commitlint/config-conventional": "^21.2.0",
"@commitlint/types": "^21.2.0",
"@types/bun": "catalog:",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.1.6",
"changelogen": "^0.6.2",
"consola": "^3.4.2",
"cspell": "^9.7.0",
"feed": "^5.2.0",
"feed": "^5.2.1",
"markdown-it-footnote": "^4.0.0",
"markdownlint-cli": "^0.48.0",
"markdownlint-cli": "^0.49.0",
"nano-spawn": "catalog:",
"nano-staged": "^1.0.2",
"p-map": "^7.0.4",
"prettier": "^3.8.1",
"prettier-plugin-jsdoc": "^1.8.0",
"semver": "^7.7.4",
"semver": "^7.8.0",
"simple-git-hooks": "^2.13.1",
"tinyglobby": "^0.2.16",
"tsdown": "catalog:",
@@ -43,7 +43,7 @@
},
"packages/analytics": {
"name": "@wxt-dev/analytics",
"version": "0.5.4",
"version": "0.5.5",
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/is-background": "workspace:^",
@@ -325,7 +325,6 @@
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^11.0.0",
"perfect-debounce": "^2.1.0",
"picomatch": "^4.0.3",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.5",
@@ -499,9 +498,11 @@
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
"@commitlint/config-conventional": ["@commitlint/config-conventional@20.5.0", "", { "dependencies": { "@commitlint/types": "^20.5.0", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA=="],
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.2.0", "", { "dependencies": { "@commitlint/types": "^21.2.0", "conventional-changelog-conventionalcommits": "^10.0.0" } }, "sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ=="],
"@commitlint/types": ["@commitlint/types@20.5.0", "", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA=="],
"@commitlint/types": ["@commitlint/types@21.2.0", "", { "dependencies": { "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" } }, "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw=="],
"@conventional-changelog/template": ["@conventional-changelog/template@1.2.0", "", {}, "sha512-12qHxvlKjHmP0PQ+17EREgC7lWyLwbph1RKcZQZ7k7ZWGmrxfxC9gadHGfvzr0g0u8BhiBGg3tks93txodlyRQ=="],
"@cspell/cspell-bundled-dicts": ["@cspell/cspell-bundled-dicts@9.8.0", "", { "dependencies": { "@cspell/dict-ada": "^4.1.1", "@cspell/dict-al": "^1.1.1", "@cspell/dict-aws": "^4.0.17", "@cspell/dict-bash": "^4.2.2", "@cspell/dict-companies": "^3.2.11", "@cspell/dict-cpp": "^7.0.2", "@cspell/dict-cryptocurrencies": "^5.0.5", "@cspell/dict-csharp": "^4.0.8", "@cspell/dict-css": "^4.1.1", "@cspell/dict-dart": "^2.3.2", "@cspell/dict-data-science": "^2.0.13", "@cspell/dict-django": "^4.1.6", "@cspell/dict-docker": "^1.1.17", "@cspell/dict-dotnet": "^5.0.13", "@cspell/dict-elixir": "^4.0.8", "@cspell/dict-en-common-misspellings": "^2.1.12", "@cspell/dict-en-gb-mit": "^3.1.22", "@cspell/dict-en_us": "^4.4.33", "@cspell/dict-filetypes": "^3.0.18", "@cspell/dict-flutter": "^1.1.1", "@cspell/dict-fonts": "^4.0.6", "@cspell/dict-fsharp": "^1.1.1", "@cspell/dict-fullstack": "^3.2.9", "@cspell/dict-gaming-terms": "^1.1.2", "@cspell/dict-git": "^3.1.0", "@cspell/dict-golang": "^6.0.26", "@cspell/dict-google": "^1.0.9", "@cspell/dict-haskell": "^4.0.6", "@cspell/dict-html": "^4.0.15", "@cspell/dict-html-symbol-entities": "^4.0.5", "@cspell/dict-java": "^5.0.12", "@cspell/dict-julia": "^1.1.1", "@cspell/dict-k8s": "^1.0.12", "@cspell/dict-kotlin": "^1.1.1", "@cspell/dict-latex": "^5.1.0", "@cspell/dict-lorem-ipsum": "^4.0.5", "@cspell/dict-lua": "^4.0.8", "@cspell/dict-makefile": "^1.0.5", "@cspell/dict-markdown": "^2.0.16", "@cspell/dict-monkeyc": "^1.0.12", "@cspell/dict-node": "^5.0.9", "@cspell/dict-npm": "^5.2.38", "@cspell/dict-php": "^4.1.1", "@cspell/dict-powershell": "^5.0.15", "@cspell/dict-public-licenses": "^2.0.16", "@cspell/dict-python": "^4.2.26", "@cspell/dict-r": "^2.1.1", "@cspell/dict-ruby": "^5.1.1", "@cspell/dict-rust": "^4.1.2", "@cspell/dict-scala": "^5.0.9", "@cspell/dict-shell": "^1.1.2", "@cspell/dict-software-terms": "^5.2.2", "@cspell/dict-sql": "^2.2.1", "@cspell/dict-svelte": "^1.0.7", "@cspell/dict-swift": "^2.0.6", "@cspell/dict-terraform": "^1.1.3", "@cspell/dict-typescript": "^3.2.3", "@cspell/dict-vue": "^3.0.5", "@cspell/dict-zig": "^1.0.0" } }, "sha512-MpXFpVyBPfJQ1YuVotljqUaGf6lWuf+fuWBBgs0PHFYTSjRPWuIxviAaCDnup/CJLLH60xQL4IlcQe4TOjzljw=="],
@@ -1037,7 +1038,7 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@simple-libs/stream-utils": ["@simple-libs/stream-utils@1.2.0", "", {}, "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA=="],
"@simple-libs/stream-utils": ["@simple-libs/stream-utils@2.0.0", "", {}, "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -1285,8 +1286,6 @@
"array-differ": ["array-differ@4.0.0", "", {}, "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw=="],
"array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="],
"array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="],
"array-union": ["array-union@3.0.1", "", {}, "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw=="],
@@ -1405,8 +1404,6 @@
"comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="],
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
@@ -1419,9 +1416,9 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@9.3.1", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw=="],
"conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@10.2.0", "", { "dependencies": { "@conventional-changelog/template": "^1.2.0" } }, "sha512-UtlM9GqolY7OmlQh5L/UEVoKsTUpTgUVy1PU8JN5gl5Ydaejb7WRklGliG1SKPxxj7hzA173eG3Kt5fYWE2pmg=="],
"conventional-commits-parser": ["conventional-commits-parser@6.4.0", "", { "dependencies": { "@simple-libs/stream-utils": "^1.2.0", "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw=="],
"conventional-commits-parser": ["conventional-commits-parser@7.0.0", "", { "dependencies": { "@simple-libs/stream-utils": "^2.0.0", "meow": "^14.0.0" }, "bin": { "conventional-commits-parser": "./dist/cli/index.js" } }, "sha512-dZe/p+FgGQLNJFqaCgNdl8j6a7gI8xuaN30Wy5g7nvyK3jqOpNUEUZ3pGJ5D68h89uRh038FtkeOk/bnGmYkmg=="],
"convert-gitmoji": ["convert-gitmoji@0.1.5", "", {}, "sha512-4wqOafJdk2tqZC++cjcbGcaJ13BZ3kwldf06PTiAQRAB76Z1KJwZNL1SaRZMi2w1FM9RYTgZ6QErS8NUl/GBmQ=="],
@@ -1501,7 +1498,7 @@
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
"dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
@@ -1599,7 +1596,7 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"feed": ["feed@5.2.0", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-hgH6CCb+7+0c8PBlakI2KubG6R+Rb1MhpNcdvqUXZTBwBHf32piwY255diAkAmkGZ6AWlywOU88AkOgP9q8Rdw=="],
"feed": ["feed@5.2.1", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-jTynzYPWs9ALjro0GW8j7sv9y7cJBeOdD4Y88kVqYy/eyusIX3g+499JiTDIlD9Ge/unebx57T4Uzo6vpYvMtA=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -1635,7 +1632,7 @@
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
@@ -1733,8 +1730,6 @@
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
"is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
@@ -1771,7 +1766,7 @@
"js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -1821,7 +1816,7 @@
"linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="],
"listr2": ["listr2@10.1.0", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^7.0.2", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-/6t2KgDYIcCjhELwvrRxi1gaJ4xCGLTjNvh6mSjYenBkrZxggek8EwCbwBU33GMUCpyyrOzz2TzylrO5mTiI1w=="],
@@ -1869,15 +1864,15 @@
"mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="],
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
"markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="],
"markdown-it-footnote": ["markdown-it-footnote@4.0.0", "", {}, "sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ=="],
"markdown-title": ["markdown-title@1.0.2", "", {}, "sha512-MqIQVVkz+uGEHi3TsHx/czcxxCbRIL7sv5K5DnYw/tI+apY54IbPefV/cmgxp6LoJSEx/TqcHdLs/298afG5QQ=="],
"markdownlint": ["markdownlint@0.40.0", "", { "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2", "string-width": "8.1.0" } }, "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg=="],
"markdownlint": ["markdownlint@0.41.0", "", { "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2", "string-width": "8.2.1" } }, "sha512-xMUI3ChBuRuxuLF4ENvCZyS8z/+Jly1coUcZwErKLIB3sDj7ojpaTBa1e9YVPhSN4jGEIjYGQCldbTJS/hqS+A=="],
"markdownlint-cli": ["markdownlint-cli@0.48.0", "", { "dependencies": { "commander": "~14.0.3", "deep-extend": "~0.6.0", "ignore": "~7.0.5", "js-yaml": "~4.1.1", "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", "markdown-it": "~14.1.1", "markdownlint": "~0.40.0", "minimatch": "~10.2.4", "run-con": "~1.3.2", "smol-toml": "~1.6.0", "tinyglobby": "~0.2.15" }, "bin": { "markdownlint": "markdownlint.js" } }, "sha512-NkZQNu2E0Q5qLEEHwWj674eYISTLD4jMHkBzDobujXd1kv+yCxi8jOaD/rZoQNW1FBBMMGQpuW5So8B51N/e0A=="],
"markdownlint-cli": ["markdownlint-cli@0.49.0", "", { "dependencies": { "commander": "~15.0.0", "deep-extend": "~0.6.0", "ignore": "~7.0.5", "js-yaml": "~4.2.0", "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", "markdown-it": "~14.2.0", "markdownlint": "~0.41.0", "minimatch": "~10.2.5", "run-con": "~1.3.2", "smol-toml": "~1.6.1", "tinyglobby": "~0.2.17" }, "bin": { "markdownlint": "markdownlint.js" } }, "sha512-vS5tWq5W91Gg33LD4pyAaXPclnz/sRvo6/RGOyDQjQ3eds2DkK6H4szUuE0M9TiRB/u/VBx1gtd9Ktrtx5WlSA=="],
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
@@ -1899,7 +1894,7 @@
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
"meow": ["meow@14.1.0", "", {}, "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw=="],
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
@@ -2209,7 +2204,7 @@
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
@@ -2273,7 +2268,7 @@
"std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
"string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
"string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
@@ -2605,6 +2600,10 @@
"@vueuse/shared/vue": ["vue@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="],
"@wxt-dev/i18n/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"@wxt-dev/unocss/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"ast-kit/@babel/parser": ["@babel/parser@8.0.0-rc.1", "", { "dependencies": { "@babel/types": "^8.0.0-rc.1" }, "bin": "./bin/babel-parser.js" }, "sha512-6HyyU5l1yK/7h9Ki52i5h6mDAx4qJdiLQO4FdCyJNoB/gy3T3GGJdhQzzbZgvgZCugYBvwtQiWRt94QKedHnkA=="],
@@ -2625,6 +2624,8 @@
"changelogen/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"changelogen/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"changelogen/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
@@ -2639,16 +2640,18 @@
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"configstore/dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
"copy-anything/is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
"cspell/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"cspell/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"cspell-config-lib/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -2677,13 +2680,19 @@
"istanbul-reports/html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
"jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"local-pkg/quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"markdownlint-cli/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"markdownlint-cli/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
"markdownlint-cli/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@@ -2697,12 +2706,16 @@
"node-notifier/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"node-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="],
"package-json/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -2731,6 +2744,8 @@
"sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"solid-refresh/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
@@ -2741,6 +2756,8 @@
"tsdown/defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"tsdown/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"tsdown/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"typedoc/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -2761,6 +2778,8 @@
"unplugin-utils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"update-notifier/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -2777,6 +2796,8 @@
"vitepress/vue": ["vue@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="],
"vitepress-plugin-llms/markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
"vitepress-plugin-llms/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"vitest/@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="],
@@ -2793,6 +2814,8 @@
"wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"wxt/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@aklinker1/rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
@@ -2831,6 +2854,8 @@
"boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"boxen/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"changelogen/c12/defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"changelogen/c12/dotenv": ["dotenv@17.2.4", "", {}, "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw=="],
@@ -2841,12 +2866,12 @@
"changelogen/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"cli-truncate/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"configstore/dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"cspell/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="],
@@ -2855,8 +2880,6 @@
"is-installed-globally/global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="],
"markdownlint-cli/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"multimatch/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -2865,10 +2888,16 @@
"sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"slice-ansi/is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"tsdown/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"typedoc/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"vitepress-plugin-llms/markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"vitepress-plugin-llms/markdown-it/linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"vitepress/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"vitepress/vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
@@ -2881,8 +2910,12 @@
"widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"widest-line/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"wrap-ansi/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@vueuse/core/vue/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
+2
View File
@@ -32,6 +32,7 @@ words:
- consolas
- Cpath
- crbug
- cros
- crunchyroll
- cssinjs
- CSUI
@@ -69,6 +70,7 @@ words:
- Menlo
- menupanel
- metas
- moderok
- mktemp
- mycompany
- nacl
+27 -33
View File
@@ -18,10 +18,8 @@ const posts = computed<Post[]>(() =>
<template>
<div class="container">
<div>
<div class="vp-doc">
<h1>Blog</h1>
</div>
<div class="inner-container">
<h1>Blog</h1>
<ul>
<BlogPostPreview v-for="post of posts" :key="post.url" :post />
@@ -35,37 +33,33 @@ const posts = computed<Post[]>(() =>
display: flex;
flex-direction: column;
align-items: center;
}
.container > div {
padding: 32px;
max-width: 900px;
width: 100%;
min-width: 0;
}
h1 {
padding-bottom: 16px;
}
.inner-container {
padding: 32px;
max-width: 900px;
width: 100%;
ul {
display: flex;
flex-direction: column;
list-style: none;
}
ul,
li {
padding: 0;
margin: 0;
}
h1 {
font-size: 32px;
font-weight: 600;
margin-top: 10px;
padding-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);
ul {
display: flex;
flex-direction: column;
li {
padding-top: 16px;
margin-top: 16px;
&:last-child {
padding-bottom: 16px;
margin-bottom: 16px;
}
}
}
}
}
</style>
+46 -44
View File
@@ -1,15 +1,19 @@
<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<PostFrontmatter>();
const { frontmatter } = useData() as unknown as {
frontmatter: Ref<PostFrontmatter>;
};
const date = useBlogDate(() => frontmatter.value.date);
</script>
<template>
<div class="vp-doc">
<main class="container-content">
<main class="container">
<h1 v-html="frontmatter.title" />
<p class="meta-row">
<a
@@ -18,7 +22,10 @@ const date = useBlogDate(() => frontmatter.value.date);
:href="`https://github.com/${author.github}`"
class="author"
>
<img :src="`https://github.com/${author.github}.png?size=96`" />
<img
:src="`https://github.com/${author.github}.png?size=96`"
alt="Author's avatar"
/>
<span>{{ author.name }}</span>
</a>
<span>&bull;</span>
@@ -30,48 +37,43 @@ const date = useBlogDate(() => frontmatter.value.date);
</template>
<style scoped>
vp-doc {
.vp-doc {
display: flex;
}
main {
max-width: 1080px;
padding: 32px;
margin: auto;
}
@media (min-width: 768px) {
main {
padding: 64px;
.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%;
}
}
}
}
.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>
+16 -35
View File
@@ -31,42 +31,23 @@ const date = useBlogDate(() => props.post.date);
</template>
<style scoped>
li {
padding: 0;
margin: 0;
}
.vp-doc {
* {
margin-block: 10px;
}
p {
margin: 0;
}
h3 {
margin: 0;
padding: 0;
border: none;
}
.title {
color: var(--vp-c-text-1);
}
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);
.description {
color: var(--vp-c-text-2);
}
.meta {
font-weight: 400;
font-size: 12px;
color: var(--vp-c-text-2);
}
}
</style>
@@ -5,36 +5,61 @@ defineProps<{
</script>
<template>
<table class="no-vertical-dividers">
<table class="no-vertical-dividers entrypoint-patterns">
<thead>
<tr>
<th style="width: 100%">Filename</th>
<th class="col-filename">Filename</th>
<th></th>
<th>Output Path</th>
</tr>
</thead>
<tbody>
<tr v-for="pattern of patterns">
<td style="white-space: nowrap; padding-right: 8px">
<tr v-for="pattern of patterns" :key="pattern[0]">
<td class="cell-input">
<code>entrypoints/{{ pattern[0] }}</code>
</td>
<td style="padding: 6px; opacity: 50%">
<td class="cell-arrow">
<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
fill="currentColor"
d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z"
fill="currentColor"
/>
</svg>
</td>
<td style="white-space: nowrap; padding-left: 8px">
<td class="cell-output">
<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>
+49 -107
View File
@@ -8,7 +8,12 @@ defineProps<{
tag?: string;
}>();
const exampleMetadata = ref<ExamplesMetadata>();
const exampleMetadata = ref<ExamplesMetadata>({
allApis: [],
allPackages: [],
allPermissions: [],
examples: [],
});
onMounted(async () => {
const res = await fetch(
'https://raw.githubusercontent.com/wxt-dev/examples/main/metadata.json',
@@ -72,19 +77,19 @@ const filteredExamples = computed(() => {
<div class="filters">
<ExampleSearchFilterByItem
label="APIs"
:items="exampleMetadata?.allApis"
v-model="selectedApis"
:items="exampleMetadata?.allApis"
label="APIs"
/>
<ExampleSearchFilterByItem
label="Permissions"
:items="exampleMetadata?.allPermissions"
v-model="selectedPermissions"
:items="exampleMetadata?.allPermissions"
label="Permissions"
/>
<ExampleSearchFilterByItem
label="Packages"
:items="exampleMetadata?.allPackages"
v-model="selectedPackages"
:items="exampleMetadata?.allPackages"
label="Packages"
/>
</div>
@@ -107,120 +112,57 @@ const filteredExamples = computed(() => {
<style scoped>
.example-layout {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas:
'search'
'results';
'filters search'
'filters results';
gap: 16px;
}
@media only screen and (min-width: 720px) {
.example-layout {
@media only screen and (min-width: 710px) {
grid-template-columns: 256px 1fr;
grid-template-rows: auto 1fr;
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) {
.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: 2px;
border-radius: 16px;
overflow: hidden;
align-self: flex-start;
gap: 10px;
@media only screen and (min-width: 710px) {
border-radius: 16px;
}
}
}
.results {
grid-area: results;
}
.box {
border-radius: 16px;
overflow: hidden;
}
.results {
grid-area: results;
.search input {
min-width: 0;
flex: 1;
font-size: 16px;
}
.search-results {
display: grid;
gap: 16px;
.checkbox-col {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: 200px;
font-size: 14px;
gap: 4px;
}
@media only screen and (min-width: 800px) {
grid-template-columns: repeat(2, 1fr);
}
.filter-btn {
color: var(--vp-c-brand-1);
}
@media only screen and (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}
.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);
a {
background-color: red;
}
}
}
}
@media only screen and (min-width: 1024px) {
.search-results {
grid-template-columns: repeat(3, 1fr);
}
}
a {
background-color: red;
}
</style>
@@ -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>
<ul class="items">
<li v-for="item in items">
<label :title="item">
<label :title="item" class="item-label">
<input
type="checkbox"
:checked="selectedItems[item]"
type="checkbox"
@input="toggleItem(item)"
/>
<span>{{ item }}</span>
@@ -51,50 +51,45 @@ 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;
.scroll-container {
flex: 1;
overflow: hidden;
position: relative;
}
.header {
padding: 8px 16px;
font-size: 12px;
font-weight: bold;
opacity: 50%;
}
.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;
}
.scroll-container {
flex: 1;
position: relative;
.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;
.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;
}
}
}
}
}
</style>
@@ -17,56 +17,45 @@ 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;
}
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;
&:hover {
outline: 2px solid var(--vp-c-brand-2);
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;
.link {
opacity: 100%;
}
}
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.name {
font-weight: 500;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.link {
opacity: 0;
transition: 250ms;
color: var(--vp-c-brand-2);
font-weight: bold;
text-align: right;
}
a:hover .link {
opacity: 100%;
.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;
}
}
</style>
+39 -22
View File
@@ -21,6 +21,7 @@ interface ListedExtension {
const extensionEntries = _extensionEntries as Array<{
chromeId?: string;
firefoxSlug?: string;
edgeId?: string;
}>;
const chromeIds = extensionEntries.flatMap((e) =>
@@ -31,8 +32,13 @@ const firefoxSlugs = [
extensionEntries.flatMap((e) => (e.firefoxSlug ? [e.firefoxSlug] : [])),
),
];
const edgeIds = extensionEntries.flatMap((e) => (e.edgeId ? [e.edgeId] : []));
const { data, isLoading } = useListExtensionDetails(chromeIds, firefoxSlugs);
const { data, isLoading } = useListExtensionDetails(
chromeIds,
firefoxSlugs,
edgeIds,
);
function addUtmSource(storeUrl: string): string {
const url = new URL(storeUrl);
@@ -49,6 +55,9 @@ const sortedExtensions = computed((): ListedExtension[] => {
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[] = [];
@@ -57,12 +66,14 @@ const sortedExtensions = computed((): ListedExtension[] => {
const firefox = entry.firefoxSlug
? firefoxBySlug.get(entry.firefoxSlug)
: undefined;
const edge = entry.edgeId ? edgeById.get(entry.edgeId) : undefined;
if (!chrome && !firefox) continue;
if (!chrome && !firefox && !edge) continue;
const primary = chrome ?? firefox!;
const users = (chrome?.users ?? 0) + (firefox?.users ?? 0);
const rating = chrome?.rating ?? firefox?.rating;
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) {
@@ -71,9 +82,16 @@ const sortedExtensions = computed((): ListedExtension[] => {
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 ?? `firefox:${entry.firefoxSlug}`,
id:
entry.chromeId ??
(entry.firefoxSlug
? `firefox:${entry.firefoxSlug}`
: `edge:${entry.edgeId}`),
name: primary.name,
iconUrl: primary.iconUrl,
shortDescription: primary.shortDescription,
@@ -124,32 +142,31 @@ function formatStars(r: number): string {
{{ extension.shortDescription }}
</p>
<div v-if="extension.stores.length > 0" class="store-stats">
<span class="store-stats-info">
<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>
</span>
<span class="store-stats-sep" aria-hidden="true">&middot;</span>
<span class="store-links">
<a
v-for="(store, i) of extension.stores"
:key="store.label"
:href="store.url"
target="_blank"
class="store-link"
:title="store.label"
>
{{ store.label
}}<span
</p>
<p class="store-stats-sep" aria-hidden="true">&middot;</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"
>,&nbsp;</span
>
</a>
</span>
</template>
</p>
</div>
</div>
</li>
@@ -13,10 +13,11 @@ export interface Extension {
export interface ExtensionResults {
chrome: Extension[];
firefox: Extension[];
edge: Extension[];
}
const operationName = 'WxtDocsUsedBy';
const query = `query ${operationName}($chromeIds: [String!]!, $firefoxIds: [String!]!) {
const query = `query ${operationName}($chromeIds: [String!]!, $firefoxIds: [String!]!, $edgeIds: [String!]!) {
chromeExtensions(ids: $chromeIds) {
id
...ExtensionData
@@ -25,6 +26,10 @@ const query = `query ${operationName}($chromeIds: [String!]!, $firefoxIds: [Stri
id: slug
...ExtensionData
}
edgeAddons(ids: $edgeIds) {
id
...ExtensionData
}
}
fragment ExtensionData on Extension {
@@ -36,13 +41,21 @@ fragment ExtensionData on Extension {
users
}`;
export default function (chromeIds: string[], firefoxSlugs: string[]) {
export default function (
chromeIds: string[],
firefoxSlugs: string[],
edgeIds: string[],
) {
const data = ref<ExtensionResults>();
const err = ref<unknown>();
const isLoading = ref(true);
if (chromeIds.length === 0 && firefoxSlugs.length === 0) {
data.value = { chrome: [], firefox: [] };
if (
chromeIds.length === 0 &&
firefoxSlugs.length === 0 &&
edgeIds.length === 0
) {
data.value = { chrome: [], firefox: [], edge: [] };
isLoading.value = false;
return { data, err, isLoading };
}
@@ -52,7 +65,7 @@ export default function (chromeIds: string[], firefoxSlugs: string[]) {
body: JSON.stringify({
operationName,
query,
variables: { chromeIds, firefoxIds: firefoxSlugs },
variables: { chromeIds, firefoxIds: firefoxSlugs, edgeIds },
}),
headers: {
'Content-Type': 'application/json',
@@ -64,6 +77,7 @@ export default function (chromeIds: string[], firefoxSlugs: string[]) {
data.value = {
chrome: responseData.chromeExtensions ?? [],
firefox: responseData.firefoxAddons ?? [],
edge: responseData.edgeAddons ?? [],
};
err.value = undefined;
})
+25 -46
View File
@@ -1,9 +1,15 @@
/* 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 {
@@ -13,66 +19,40 @@
--wxt-c-green-soft: rgba(103, 212, 94, 0.14);
}
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
:root {
--vp-c-brand-1: var(--wxt-c-green-1);
--vp-c-brand-2: var(--wxt-c-green-2);
--vp-c-brand-3: var(--wxt-c-green-3);
--vp-c-brand-soft: var(--wxt-c-green-soft);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Customize Individual Components */
.vp-doc .no-vertical-dividers {
th,
td {
border: none;
}
.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);
tr {
border: 1px solid var(--vp-c-divider);
}
}
body {
overflow-y: scroll;
}
.VPSidebar {
user-select: none;
}
.VPSidebarItem .badge {
display: inline-block;
min-width: 1.6em;
padding: 0.3em 0.4em 0.2em;
border-radius: 1rem;
font-size: 0.75em;
line-height: 1;
margin-left: 0.5rem;
text-align: center;
vertical-align: middle;
background-color: var(--vp-c-default-2);
}
.netlify-badge {
display: inline-flex;
line-height: 0;
vertical-align: middle;
}
.netlify-badge img {
display: block;
}
.light-netlify {
display: inline-flex;
}
.dark .light-netlify {
display: none;
}
.dark-netlify {
display: none;
}
.dark .dark-netlify {
display: inline-flex;
}
@@ -80,15 +60,15 @@ body {
/* Add Aaron's favorite pic from Artemis II to the hero section because it's cool. */
.dark .VPHome::before {
content: '';
background-image: url('/assets/art002e009298.jpg');
opacity: 50%;
position: absolute;
background-image: url('../../assets/art002e009298.jpg');
opacity: 50%;
width: 100%;
height: 110vh;
background-size: cover;
background-position: 90%;
pointer-events: none;
}
.dark .VPHome::after {
content: '';
position: absolute;
@@ -96,9 +76,8 @@ body {
height: 10vh;
top: 100vh;
background: linear-gradient(to bottom, transparent, var(--vp-c-bg));
pointer-events: none;
z-index: 0;
}
.item {
z-index: 1;
}
+3
View File
@@ -7,6 +7,7 @@ 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,
@@ -17,5 +18,7 @@ export default {
.component('UsingWxtSection', UsingWxtSection)
.component('ExampleSearch', ExampleSearch)
.component('blog', BlogLayout);
setupCodeGroupSync();
},
};
+73
View File
@@ -0,0 +1,73 @@
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);
}
+10 -1
View File
@@ -7,7 +7,8 @@
# Use:
#
# - `chromeId` for the Chrome Web Store listing,
# - `firefoxSlug` for Firefox Add-ons (slug from https://addons.mozilla.org/firefox/addon/{slug}/)
# - `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.
@@ -30,6 +31,7 @@
- # StayFree - Website Blocker & Web Analytics
chromeId: elfaihghhjjoknimpccccmkioofjjfkf
firefoxSlug: stayfree
edgeId: aghmglalnfmjbipodadgbgpakneabgnp
- # Doozy: Ai Made Easy
chromeId: okifoaikfmpfcamplcfjkpdnhfodpkil
@@ -110,6 +112,7 @@
- # NiceTab - https://github.com/web-dahuyou/NiceTab
chromeId: fonflmjnjbkigocpoommgmhljdpljain
firefoxSlug: nice-tab-manager
edgeId: ompjiaelpibiggcnanhbdblkhfdmkgnl
- # Draftly for LinkedIn
chromeId: fcffekbnfcfdemeekijbbmgmkognnmkd
@@ -509,3 +512,9 @@
- # Hayami: Anime comments & discussions
chromeId: nhkggpiaeaeeeimohfpchnjobbamfcbg
firefoxSlug: hayami
- # CopyApes Assistant
chromeId: affmjifigldmicnbgpghddaneomejmfo
- # Leetcode Enhancer
chromeId: cpoclfijojgjiafnlgnhjalkaiabcjch
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 132 KiB

+33 -16
View File
@@ -17,28 +17,14 @@ 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 "~~/scripts"` |
| `@@` | `<rootDir>/*` | `import "@@/scripts"` |
| `~~` | `<rootDir>/*` | `import "~~/package.json"` |
| `@@` | `<rootDir>/*` | `import "@@/package.json"` |
| `~` | `<srcDir>/*` | `import { toLowerCase } from "~/utils/strings"` |
| `@` | `<srcDir>/*` | `import { toLowerCase } from "@/utils/strings"` |
@@ -63,3 +49,34 @@ 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.
-1
View File
@@ -734,7 +734,6 @@ const watchPattern = new MatchPattern('*://*.youtube.com/watch*');
export default defineContentScript({
matches: ['*://*.youtube.com/*'],
main(ctx) {
ctx.locationWatcher.run();
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {
if (watchPattern.includes(newUrl)) mainWatch(ctx);
});
@@ -78,3 +78,28 @@ 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) {
// ...
},
});
```
+17 -17
View File
@@ -10,14 +10,14 @@ Run the [`init` command](/api/cli/wxt-init), and follow the instructions.
:::code-group
```sh [PNPM]
pnpm dlx wxt@latest init
```
```sh [Bun]
bunx wxt@latest init
```
```sh [PNPM]
pnpm dlx 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 [PNPM]
cd my-project
pnpm init
```
```sh [Bun]
cd my-project
bun init
```
```sh [PNPM]
cd my-project
pnpm 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 [PNPM]
pnpm i -D wxt
```
```sh [Bun]
bun add -D wxt
```
```sh [PNPM]
pnpm 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 [PNPM]
pnpm dev
```
```sh [Bun]
bun run dev
```
```sh [PNPM]
pnpm dev
```
```sh [NPM]
npm run dev
```
+12
View File
@@ -221,6 +221,18 @@ 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".
+7 -6
View File
@@ -7,7 +7,7 @@
Output docs/assets/init-demo.gif
# The tools we will use
Require pnpm
Require bun
# === Scene ====
@@ -35,19 +35,19 @@ Env PS1 "\e[0;32m●\e[0m "
Hide
# Create a temporary folder for demo
Type 'vhs_sandbox="$(mktemp -d)"' Enter
Type 'cd "$vhs_sandbox"' Enter
Type 'mkdir packages/wxt/init-demo' Enter
Type 'cd packages/wxt/init-demo' Enter
Type 'clear' Enter
Show
# ===== Actions =====
Type 'pnpm dlx wxt@latest init .' Sleep 1s Enter
Type 'bunx wxt init .' Sleep 1s Enter
Sleep 3.25s
Down@750ms Enter@750ms # Select `vue` template
Sleep 1.25s
Down@750ms Enter@750ms # Select `pnpm` as a package manager
Down@500ms Down@500ms Enter@750ms # Select `pnpm` as a package manager
Sleep 5s
@@ -55,5 +55,6 @@ Sleep 5s
Hide
# Delete the temporary folder
Type 'rm -rf "$vhs_sandbox"' Enter
Type 'cd .."' Enter
Type 'rm -rf init-demo' Enter
Show
+5 -5
View File
@@ -37,23 +37,23 @@
"devDependencies": {
"@aklinker1/buildc": "catalog:",
"@aklinker1/check": "catalog:",
"@commitlint/config-conventional": "^20.4.3",
"@commitlint/types": "^20.4.3",
"@commitlint/config-conventional": "^21.2.0",
"@commitlint/types": "^21.2.0",
"@types/bun": "catalog:",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.1.6",
"changelogen": "^0.6.2",
"consola": "^3.4.2",
"cspell": "^9.7.0",
"feed": "^5.2.0",
"feed": "^5.2.1",
"markdown-it-footnote": "^4.0.0",
"markdownlint-cli": "^0.48.0",
"markdownlint-cli": "^0.49.0",
"nano-spawn": "catalog:",
"nano-staged": "^1.0.2",
"p-map": "^7.0.4",
"prettier": "^3.8.1",
"prettier-plugin-jsdoc": "^1.8.0",
"semver": "^7.7.4",
"semver": "^7.8.0",
"simple-git-hooks": "^2.13.1",
"tinyglobby": "^0.2.16",
"tsdown": "catalog:",
+28
View File
@@ -1,5 +1,33 @@
# 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)
+36
View File
@@ -5,6 +5,7 @@ Report analytics events from your web extension extension.
## Supported Analytics Providers
- [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol)
- [Moderok](#moderok)
- [Umami](#umami)
## Install With WXT
@@ -120,6 +121,41 @@ 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).
### Umami
[Umami](https://umami.is/) is a privacy-first, open source analytics platform.
@@ -25,7 +25,7 @@ type AnalyticsMethod =
type MethodForwarder = <K extends keyof Analytics>(
fn: K,
) => (...args: Parameters<Analytics[K]>) => void;
) => (...args: Parameters<Analytics[K]>) => Promise<void>;
const ANALYTICS_PORT = '@wxt-dev/analytics';
@@ -84,9 +84,13 @@ function createBackgroundAnalytics(
// Cached values
const platformInfo = browser.runtime.getPlatformInfo();
const userAgent = UAParser();
let userId = Promise.resolve(userIdStorage.getValue()).then(
(id) => id ?? globalThis.crypto.randomUUID(),
);
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 userProperties = userPropertiesStorage.getValue();
const manifest = browser.runtime.getManifest();
@@ -238,6 +242,7 @@ function createFrontendAnalytics(): Analytics {
(fn) =>
(...args) => {
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
return Promise.resolve();
};
const analytics: Analytics = {
@@ -0,0 +1,267 @@
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,20 +1,23 @@
export interface Analytics {
/** Report a page change. */
page: (url: string) => void;
page: (url: string) => Promise<void>;
/** Report a custom event. */
track: (
eventName: string,
eventProperties?: Record<string, string | undefined>,
) => void;
) => Promise<void>;
/** Save information about the user. */
identify: (userId: string, userProperties?: Record<string, string>) => void;
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.
*/
autoTrack: (root: Document | ShadowRoot | Element) => () => void;
/** Calls `config.enabled.setValue`. */
setEnabled: (enabled: boolean) => void;
setEnabled: (enabled: boolean) => Promise<void>;
}
export interface AnalyticsConfig {
+5 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wxt-dev/analytics",
"version": "0.5.4",
"version": "0.5.5",
"description": "Add analytics to your web extension",
"type": "module",
"scripts": {
@@ -55,6 +55,10 @@
"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"
+1
View File
@@ -9,6 +9,7 @@ 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',
},
deps: {
neverBundle: ['#analytics'],
+17
View File
@@ -173,6 +173,23 @@ 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]
@@ -5,6 +5,9 @@
"substitution": {
"message": "Includes one substitution: $1"
},
"namedSubstitution": {
"message": "Includes one named substitution: {value}"
},
"singular": {
"message": "There is $1 item"
},
+9
View File
@@ -36,6 +36,15 @@ setTimeout(() => {
await runBench(substitution);
}
{
const key = 'namedSubstitution';
const value = 'test';
const namedSubstitution = new Bench({ name: 'Named substitution' })
.add('Vanilla', () => void chrome.i18n.getMessage(key))
.add('WXT I18n', () => void i18n.t(key, { value }));
await runBench(namedSubstitution);
}
{
const vanillaPluralKey = 'plural';
const wxtKey = 'wxtPlural';
+22
View File
@@ -22,6 +22,7 @@ describe('Built Tools', () => {
const fileText = stringifyYAML({
simple: 'example',
sub: 'Hello $1',
named: 'Hello {name} from {tool}',
nested: {
example: 'This is nested',
array: ['One', 'Two'],
@@ -33,11 +34,19 @@ describe('Built Tools', () => {
message: 'test 2',
description: 'test',
},
chromeNamed: {
message: 'test {value}',
description: 'test',
},
plural0: {
0: 'Zero items',
1: 'One item',
n: '$1 items',
},
pluralNamed: {
1: 'One {item}',
n: '$1 {item}',
},
plural1: {
1: 'One item',
n: '$1 items',
@@ -68,6 +77,9 @@ describe('Built Tools', () => {
"sub": {
"message": "Hello $1"
},
"named": {
"message": "Hello {name} from {tool}"
},
"nested_example": {
"message": "This is nested"
},
@@ -84,9 +96,16 @@ describe('Built Tools', () => {
"message": "test 2",
"description": "test"
},
"chromeNamed": {
"message": "test {value}",
"description": "test"
},
"plural0": {
"message": "Zero items | One item | $1 items"
},
"pluralNamed": {
"message": "One {item} | $1 {item}"
},
"plural1": {
"message": "One item | $1 items"
},
@@ -103,12 +122,15 @@ describe('Built Tools', () => {
"export type GeneratedI18nStructure = {
"simple": { substitutions: 0, plural: false };
"sub": { substitutions: 1, plural: false };
"named": { substitutions: 0, plural: false, namedSubstitutions: ["name","tool"] };
"nested.example": { substitutions: 0, plural: false };
"nested.array.0": { substitutions: 0, plural: false };
"nested.array.1": { substitutions: 0, plural: false };
"nested.notChrome.message": { substitutions: 0, plural: false };
"chrome": { substitutions: 0, plural: false };
"chromeNamed": { substitutions: 0, plural: false, namedSubstitutions: ["value"] };
"plural0": { substitutions: 1, plural: true };
"pluralNamed": { substitutions: 1, plural: true, namedSubstitutions: ["item"] };
"plural1": { substitutions: 1, plural: true };
"pluralN": { substitutions: 1, plural: true };
"pluralSub": { substitutions: 2, plural: true };
+43
View File
@@ -68,4 +68,47 @@ describe('createI18n', () => {
i18n.t('key', 3, ['custom']);
expect(getMessageMock).toBeCalledWith('key', ['custom']);
});
it('should replace named substitutions', () => {
const i18n = createI18n();
getMessageMock.mockReturnValue('My name is {name}, and I use {tool}.');
const actual = i18n.t('key', {
name: 'Ada',
tool: 'WXT',
});
expect(actual).toBe('My name is Ada, and I use WXT.');
expect(getMessageMock).toBeCalledTimes(1);
expect(getMessageMock).toBeCalledWith('key');
});
it('should leave unknown named substitutions unchanged', () => {
const i18n = createI18n();
getMessageMock.mockReturnValue('Hello {name} from {team}.');
const actual = i18n.t('key', { name: 'Ada' });
expect(actual).toBe('Hello Ada from {team}.');
});
it('should combine positional and named substitutions', () => {
const i18n = createI18n();
getMessageMock.mockReturnValue('Hello Ada from {team}.');
const actual = i18n.t('key', ['Ada'], { team: 'WXT' });
expect(actual).toBe('Hello Ada from WXT.');
expect(getMessageMock).toBeCalledWith('key', ['Ada']);
});
it('should replace named substitutions after pluralization', () => {
const i18n = createI18n();
getMessageMock.mockReturnValue('One {item} | Multiple {item}');
const actual = i18n.t('key', 2, { item: 'files' });
expect(actual).toBe('Multiple files');
expect(getMessageMock).toBeCalledWith('key', ['2']);
});
});
+55
View File
@@ -39,9 +39,29 @@ describe('I18n Types', () => {
simple: { plural: false; substitutions: 0 };
simpleSub1: { plural: false; substitutions: 1 };
simpleSub2: { plural: false; substitutions: 2 };
named: {
plural: false;
substitutions: 0;
namedSubstitutions: ['name', 'tool'];
};
mixed: {
plural: false;
substitutions: 1;
namedSubstitutions: ['team'];
};
plural: { plural: true; substitutions: 0 };
pluralSub1: { plural: true; substitutions: 1 };
pluralSub2: { plural: true; substitutions: 2 };
pluralNamed: {
plural: true;
substitutions: 1;
namedSubstitutions: ['item'];
};
pluralMixed: {
plural: true;
substitutions: 2;
namedSubstitutions: ['item'];
};
};
const i18n = createI18n<MyStructure>();
@@ -76,6 +96,22 @@ describe('I18n Types', () => {
// @ts-expect-error
i18n.t('simpleSub2', n);
i18n.t('named', { name: 'Ada', tool: 'WXT' });
// @ts-expect-error
i18n.t('named');
// @ts-expect-error
i18n.t('named', { name: 'Ada' });
// @ts-expect-error
i18n.t('named', ['Ada', 'WXT']);
i18n.t('mixed', ['Ada'], { team: 'WXT' });
// @ts-expect-error
i18n.t('mixed', { team: 'WXT' });
// @ts-expect-error
i18n.t('mixed', ['Ada']);
// @ts-expect-error
i18n.t('mixed', ['Ada'], {});
i18n.t('plural', n);
// @ts-expect-error
i18n.t('plural');
@@ -109,6 +145,25 @@ describe('I18n Types', () => {
i18n.t('pluralSub2', n, ['one', 'two', 'three']);
// @ts-expect-error
i18n.t('pluralSub2', n);
i18n.t('pluralNamed', n, { item: 'files' });
i18n.t('pluralNamed', n, ['files'], { item: 'files' });
// @ts-expect-error
i18n.t('pluralNamed', n);
// @ts-expect-error
i18n.t('pluralNamed', n, {});
// @ts-expect-error
i18n.t('pluralNamed', n, ['files']);
i18n.t('pluralMixed', n, ['1', 'files'], { item: 'reports' });
// @ts-expect-error
i18n.t('pluralMixed', n, { item: 'reports' });
// @ts-expect-error
i18n.t('pluralMixed', n, ['1', 'files']);
// @ts-expect-error
i18n.t('pluralMixed', n, ['1'], { item: 'reports' });
// @ts-expect-error
i18n.t('pluralMixed', n, ['1', 'files'], {});
});
});
});
+30
View File
@@ -1,7 +1,9 @@
import { describe, it, expect } from 'vitest';
import { ChromeMessage } from '../build';
import {
applyNamedSubstitutions,
applyChromeMessagePlaceholders,
getNamedSubstitutionNames,
getSubstitutionCount,
standardizeLocale,
} from '../utils';
@@ -42,6 +44,34 @@ describe('Utils', () => {
});
});
describe('applyNamedSubstitutions', () => {
it('should replace named substitutions', () => {
expect(
applyNamedSubstitutions('Hello {name}, welcome to {tool}', {
name: 'Ada',
tool: 'WXT',
}),
).toBe('Hello Ada, welcome to WXT');
});
it('should leave missing substitutions unchanged', () => {
expect(applyNamedSubstitutions('Hello {name}', {})).toBe('Hello {name}');
});
});
describe('getNamedSubstitutionNames', () => {
it('should return unique substitution names in order', () => {
expect(getNamedSubstitutionNames('{name} uses {tool}: {name}')).toEqual([
'name',
'tool',
]);
});
it('should return an empty array when no named substitutions are present', () => {
expect(getNamedSubstitutionNames('Hello $1')).toEqual([]);
});
});
describe('getSubstitutionCount', () => {
it('should return the last substitution present in the message', () => {
expect(getSubstitutionCount('I like $1, but I like $2 better')).toBe(2);
+17 -1
View File
@@ -8,7 +8,11 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { parseYAML, parseJSON5, parseTOML } from 'confbox';
import { dirname, extname } from 'node:path';
import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';
import {
applyChromeMessagePlaceholders,
getNamedSubstitutionNames,
getSubstitutionCount,
} from './utils';
export { SUPPORTED_LOCALES } from './supported-locales';
@@ -38,6 +42,7 @@ export type MessagesObject = Record<string, Message>;
export interface ParsedBaseMessage {
key: string[];
substitutions: number;
namedSubstitutions: string[];
}
export interface ParsedChromeMessage extends ParsedBaseMessage, ChromeMessage {
@@ -167,11 +172,13 @@ function _parseMessagesObject(
case 'symbol': {
const message = String(object);
const substitutions = getSubstitutionCount(message);
const namedSubstitutions = getNamedSubstitutionNames(message);
return [
{
type: 'simple',
key: path,
substitutions,
namedSubstitutions,
message,
},
];
@@ -191,11 +198,13 @@ function _parseMessagesObject(
if (isPluralMessage(object)) {
const message = Object.values(object).join('|');
const substitutions = getSubstitutionCount(message);
const namedSubstitutions = getNamedSubstitutionNames(message);
return [
{
type: 'plural',
key: path,
substitutions,
namedSubstitutions,
plurals: object,
},
];
@@ -204,11 +213,13 @@ function _parseMessagesObject(
if (depth === 1 && isChromeMessage(object)) {
const message = applyChromeMessagePlaceholders(object);
const substitutions = getSubstitutionCount(message);
const namedSubstitutions = getNamedSubstitutionNames(message);
return [
{
type: 'chrome',
key: path,
substitutions,
namedSubstitutions,
...object,
},
];
@@ -247,6 +258,11 @@ export function generateTypeText(messages: ParsedMessage[]): string {
`substitutions: ${message.substitutions}`,
`plural: ${message.type === 'plural'}`,
];
if (message.namedSubstitutions.length > 0) {
features.push(
`namedSubstitutions: ${JSON.stringify(message.namedSubstitutions)}`,
);
}
return ` "${key}": { ${features.join(', ')} };`;
};
+39 -17
View File
@@ -1,6 +1,13 @@
/** @module @wxt-dev/i18n */
import { I18nStructure, I18n, Substitution, UntypedI18n } from './types';
import type {
I18nStructure,
I18n,
NamedSubstitutions,
Substitution,
UntypedI18n,
} from './types';
import { browser } from '@wxt-dev/browser';
import { applyNamedSubstitutions } from './utils';
export function createI18n(): UntypedI18n;
export function createI18n<T extends I18nStructure>(): I18n<T>;
@@ -8,6 +15,7 @@ export function createI18n(): UntypedI18n {
const t: UntypedI18n['t'] = (key: string, ...args: unknown[]) => {
// Resolve args
let sub: Substitution[] | undefined;
let namedSub: NamedSubstitutions | undefined;
let count: number | undefined;
args.forEach((arg, i) => {
if (arg == null) {
@@ -16,9 +24,11 @@ export function createI18n(): UntypedI18n {
count = arg;
} else if (Array.isArray(arg)) {
sub = arg;
} else if (isNamedSubstitutions(arg)) {
namedSub = arg;
} else {
throw Error(
`Unknown argument at index ${i}. Must be a number for pluralization, substitution array, or options object.`,
`Unknown argument at index ${i}. Must be a number for pluralization, substitution array, or named substitution object.`,
);
}
});
@@ -40,24 +50,36 @@ export function createI18n(): UntypedI18n {
if (!message) {
console.warn(`[i18n] Message not found: "${key}"`);
}
if (count == null) return message;
// Apply pluralization
const plural = message.split(' | ');
switch (plural.length) {
// "n items"
case 1:
return plural[0];
// "1 item | n items"
case 2:
return plural[count === 1 ? 0 : 1];
// "0 items | 1 item | n items"
case 3:
return plural[count === 0 || count === 1 ? count : 2];
default:
throw Error('Unknown plural formatting');
if (count != null) {
// Apply pluralization
const plural = message.split(' | ');
switch (plural.length) {
// "n items"
case 1:
message = plural[0];
break;
// "1 item | n items"
case 2:
message = plural[count === 1 ? 0 : 1];
break;
// "0 items | 1 item | n items"
case 3:
message = plural[count === 0 || count === 1 ? count : 2];
break;
default:
throw Error('Unknown plural formatting');
}
}
return namedSub == null
? message
: applyNamedSubstitutions(message, namedSub);
};
return { t };
}
function isNamedSubstitutions(value: unknown): value is NamedSubstitutions {
return typeof value === 'object' && value != null;
}
+77 -5
View File
@@ -1,6 +1,7 @@
export interface I18nFeatures {
plural: boolean;
substitutions: SubstitutionCount;
namedSubstitutions?: readonly string[];
}
export interface UntypedI18n {
@@ -10,8 +11,21 @@ export interface UntypedI18n {
export type UntypedTFunction = {
(key: string): string;
(key: string, substitutions: Substitution[]): string;
(key: string, substitutions: NamedSubstitutions): string;
(
key: string,
substitutions: Substitution[],
namedSubstitutions: NamedSubstitutions,
): string;
(key: string, n: number): string;
(key: string, n: number, substitutions: Substitution[]): string;
(key: string, n: number, substitutions: NamedSubstitutions): string;
(
key: string,
n: number,
substitutions: Substitution[],
namedSubstitutions: NamedSubstitutions,
): string;
};
export type I18nStructure = Record<string, I18nFeatures>;
@@ -20,6 +34,12 @@ export interface I18n<T extends I18nStructure> {
t: TFunction<T>;
}
type HasNamedSubstitutions<T extends I18nFeatures> = T extends {
namedSubstitutions: readonly string[];
}
? true
: false;
// prettier-ignore
export type SubstitutionTuple<T extends SubstitutionCount> =
T extends 1 ? [$1: Substitution]
@@ -37,22 +57,43 @@ export type TFunction<T extends I18nStructure> = {
// Non-plural, no substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0 } ? P : never; }[keyof T],
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0 } ? HasNamedSubstitutions<T[P]> extends true ? never : P : never; }[keyof T],
): string;
// Non-plural with substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? P : never; }[keyof T],
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? HasNamedSubstitutions<T[P]> extends true ? never : P : never; }[keyof T],
substitutions: T[K] extends I18nFeatures
? SubstitutionTuple<T[K]['substitutions']>
: never,
): string;
// Non-plural with named substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0; namedSubstitutions: readonly string[] } ? P : never; }[keyof T],
substitutions: T[K] extends I18nFeatures
? NamedSubstitutionsFor<T[K]>
: never,
): string;
// Non-plural with positional and named substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: PositiveSubstitutionCount; namedSubstitutions: readonly string[] } ? P : never; }[keyof T],
substitutions: T[K] extends I18nFeatures
? SubstitutionTuple<T[K]['substitutions']>
: never,
namedSubstitutions: T[K] extends I18nFeatures
? NamedSubstitutionsFor<T[K]>
: never,
): string;
// Plural with 1 substitution
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 1 } ? P : never; }[keyof T],
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 1 } ? HasNamedSubstitutions<T[P]> extends true ? never : P : never; }[keyof T],
n: number,
substitutions?: SubstitutionTuple<1>,
): string;
@@ -60,21 +101,52 @@ export type TFunction<T extends I18nStructure> = {
// Plural without substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 0 | 1 } ? P : never; }[keyof T],
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 0 | 1 } ? HasNamedSubstitutions<T[P]> extends true ? never : P : never; }[keyof T],
n: number,
): string;
// Plural with substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: SubstitutionCount } ? P : never; }[keyof T],
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: SubstitutionCount } ? HasNamedSubstitutions<T[P]> extends true ? never : P : never; }[keyof T],
n: number,
substitutions: T[K] extends I18nFeatures
? SubstitutionTuple<T[K]['substitutions']>
: never,
): string;
// Plural with named substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 0 | 1; namedSubstitutions: readonly string[] } ? P : never; }[keyof T],
n: number,
substitutions: T[K] extends I18nFeatures
? NamedSubstitutionsFor<T[K]>
: never,
): string;
// Plural with positional and named substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: PositiveSubstitutionCount; namedSubstitutions: readonly string[] } ? P : never; }[keyof T],
n: number,
substitutions: T[K] extends I18nFeatures
? SubstitutionTuple<T[K]['substitutions']>
: never,
namedSubstitutions: T[K] extends I18nFeatures
? NamedSubstitutionsFor<T[K]>
: never,
): string;
};
export type Substitution = string | number;
export type NamedSubstitutions = Record<string, Substitution>;
type SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type PositiveSubstitutionCount = Exclude<SubstitutionCount, 0>;
type NamedSubstitutionsFor<T extends I18nFeatures> = T extends {
namedSubstitutions: readonly string[];
}
? { [K in T['namedSubstitutions'][number]]: Substitution }
: never;
+22 -1
View File
@@ -1,4 +1,7 @@
import { ChromeMessage } from './build';
import type { ChromeMessage } from './build';
import type { NamedSubstitutions } from './types';
const NAMED_SUBSTITUTION_RE = /\{([A-Za-z0-9_]+)\}/g;
export function applyChromeMessagePlaceholders(message: ChromeMessage): string {
if (message.placeholders == null) return message.message;
@@ -11,6 +14,24 @@ export function applyChromeMessagePlaceholders(message: ChromeMessage): string {
);
}
export function applyNamedSubstitutions(
message: string,
substitutions: NamedSubstitutions,
): string {
return message.replace(NAMED_SUBSTITUTION_RE, (match, key: string) => {
return Object.prototype.hasOwnProperty.call(substitutions, key)
? String(substitutions[key])
: match;
});
}
export function getNamedSubstitutionNames(message: string): string[] {
return Array.from(
message.matchAll(NAMED_SUBSTITUTION_RE),
([, name]) => name,
).filter((name, i, names) => names.indexOf(name) === i);
}
export function getSubstitutionCount(message: string): number {
return (
1 +
@@ -28,9 +28,15 @@ export default defineContentScript({
append: 'after',
anchor: '#automount-anchor',
onMount: (container) => {
container.classList.add(
'flex',
'-ml-9',
'items-start',
'justify-start',
);
const app = document.createElement('div');
app.id = 'automount-ui';
app.classList.add('m-0', 'text-center', 'text-blue-500');
app.classList.add('mt-9', 'text-center', 'text-blue-500');
app.textContent = `Hello, I'm automount UI.`;
container.append(app);
},
+6
View File
@@ -6,6 +6,7 @@ const hooks: WxtHooks = {
ready: vi.fn(),
'config:resolved': vi.fn(),
'prepare:types': vi.fn(),
'prepare:tsconfig': vi.fn(),
'prepare:publicPaths': vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
@@ -56,6 +57,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': false,
'build:done': false,
@@ -88,6 +90,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -120,6 +123,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -152,6 +156,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -191,6 +196,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -84,6 +84,51 @@ describe('Output Directory Structure', () => {
`);
});
it('should route content script CSS with Rollup strict deprecations enabled', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.content/index.ts',
`import './style.css';
export default defineContentScript({
matches: ["*://*/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/content.content/style.css',
`body { color: purple }`,
);
await project.build({
vite: () => ({
build: {
rollupOptions: {
strictDeprecations: true,
},
},
}),
});
expect(
await project.serializeOutput([
'.output/chrome-mv3/content-scripts/content.js',
]),
).toMatchInlineSnapshot(`
".output/chrome-mv3/content-scripts/content.css
----------------------------------------
body{color:purple}
================================================================================
.output/chrome-mv3/content-scripts/content.js
----------------------------------------
<contents-ignored>
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should allow inputs with invalid JS variable names, like dashes', async () => {
const project = new TestProject();
project.addFile(
+110 -29
View File
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { readFile } from 'node:fs/promises';
describe('TypeScript Project', () => {
it('should generate defined constants correctly', async () => {
@@ -333,25 +334,63 @@ describe('TypeScript Project', () => {
"strict": true,
"skipLibCheck": true,
"paths": {
"@": [".."],
"@/*": ["../*"],
"~": [".."],
"~/*": ["../*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"@": [
".."
],
"@/*": [
"../*"
],
"~": [
".."
],
"~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
it("should allow updating the project's tsconfig via hooks", async () => {
const project = new TestProject();
project.setConfigFileConfig({});
project.addFile('entrypoints/unlisted.html', '<html></html>');
await project.prepare({
hooks: {
'prepare:tsconfig': (_, { tsconfig }) => {
tsconfig.test = 'test';
},
},
});
const actual = JSON.parse(
await readFile(project.resolvePath('.wxt/tsconfig.json'), 'utf-8'),
);
expect(actual).toMatchObject({ test: 'test' });
});
it('should generate correct path aliases for a custom srcDir', async () => {
const project = new TestProject();
project.addFile('src/entrypoints/unlisted.html', '<html></html>');
@@ -377,21 +416,40 @@ describe('TypeScript Project', () => {
"strict": true,
"skipLibCheck": true,
"paths": {
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"@": [
"../src"
],
"@/*": [
"../src/*"
],
"~": [
"../src"
],
"~/*": [
"../src/*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
@@ -425,23 +483,46 @@ describe('TypeScript Project', () => {
"strict": true,
"skipLibCheck": true,
"paths": {
"example": ["../example"],
"example/*": ["../example/*"],
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"example": [
"../example"
],
"example/*": [
"../example/*"
],
"@": [
"../src"
],
"@/*": [
"../src/*"
],
"~": [
"../src"
],
"~/*": [
"../src/*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
+38 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { TestProject } from '../utils';
describe('User Config', () => {
@@ -150,4 +150,41 @@ describe('User Config', () => {
await project.build({ configFile: 'foo.config.ts' });
});
it('should pass watch options to the dev server', async () => {
const project = new TestProject();
const extendViteDevServerConfig = vi.fn();
project.addFile('entrypoints/popup.html', '<html></html>');
const server = await project.startServer({
watchOptions: {
usePolling: true,
interval: 1000,
ignored: ['**/generated/**'],
},
hooks: {
'vite:devServer:extendConfig': extendViteDevServerConfig,
},
webExt: {
disabled: true,
},
});
try {
const viteConfig = extendViteDevServerConfig.mock.calls[0]?.[0];
expect(viteConfig?.server?.watch).toMatchObject({
usePolling: true,
interval: 1000,
});
expect(viteConfig?.server?.watch?.ignored).toEqual(
expect.arrayContaining([
expect.stringContaining('.output/**'),
expect.stringContaining('.wxt/**'),
'**/generated/**',
]),
);
} finally {
await server.stop();
}
});
});
-1
View File
@@ -48,7 +48,6 @@
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^11.0.0",
"perfect-debounce": "^2.1.0",
"picomatch": "^4.0.3",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.5",
+75 -4
View File
@@ -1,6 +1,6 @@
import { Hookable } from 'hookable';
import { mkdir, readdir, rename, rmdir, stat } from 'node:fs/promises';
import { dirname, extname, join, relative } from 'node:path';
import { dirname, extname, join, relative, resolve } from 'node:path';
import type * as vite from 'vite';
import { ViteNodeRunner } from 'vite-node/client';
import { ViteNodeServer } from 'vite-node/server';
@@ -29,6 +29,11 @@ import {
} from '../../utils/virtual-modules';
import * as wxtPlugins from './plugins';
interface RollupAssetNameInfo {
name?: string;
names?: string[];
}
export async function createViteBuilder(
wxtConfig: ResolvedConfig,
hooks: Hookable<WxtHooks>,
@@ -67,7 +72,13 @@ export async function createViteBuilder(
config.server ??= {};
config.server.watch = {
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],
...wxtConfig.watchOptions,
ignored: [
`${wxtConfig.outBaseDir}/**`,
`${wxtConfig.wxtDir}/**`,
...getRunnerProfileWatchIgnores(wxtConfig),
...toArray(wxtConfig.watchOptions.ignored ?? []),
],
};
// TODO: Remove once https://github.com/wxt-dev/wxt/pull/1411 is merged
@@ -166,10 +177,12 @@ export async function createViteBuilder(
),
// Output content script CSS to `content-scripts/`, but all other scripts are written to
// `assets/`.
assetFileNames: ({ name }) => {
assetFileNames: (assetInfo) => {
if (
entrypoint.type === 'content-script' &&
name?.endsWith('css')
getRollupAssetNames(assetInfo).some((name) =>
name.endsWith('css'),
)
) {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
@@ -392,6 +405,64 @@ export async function createViteBuilder(
};
}
export function getRunnerProfileWatchIgnores(
wxtConfig: ResolvedConfig,
): string[] {
const root = normalizePath(wxtConfig.root);
const chromiumArgProfiles = extractPathArgs(
wxtConfig.runnerConfig.config?.chromiumArgs,
'--user-data-dir',
);
const firefoxArgProfiles = extractPathArgs(
wxtConfig.runnerConfig.config?.firefoxArgs,
'-profile',
);
const profiles = [
wxtConfig.runnerConfig.config?.chromiumProfile,
wxtConfig.runnerConfig.config?.firefoxProfile,
...chromiumArgProfiles,
...firefoxArgProfiles,
].filter((profile): profile is string => typeof profile === 'string');
return Array.from(
new Set(
profiles
.map((profile) => normalizePath(resolve(wxtConfig.root, profile)))
// Avoid accidentally disabling all file watching.
.filter((profilePath) => profilePath !== root)
.map((profilePath) => `${profilePath}/**`),
),
);
}
function extractPathArgs(args: string[] | undefined, flag: string): string[] {
if (!args?.length) return [];
const paths: string[] = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith(`${flag}=`)) {
const value = arg.slice(flag.length + 1).trim();
if (value) paths.push(value);
continue;
}
if (arg === flag) {
const nextValue = args[i + 1]?.trim();
if (nextValue) paths.push(nextValue);
i += 1;
}
}
return paths;
}
function getRollupAssetNames(assetInfo: RollupAssetNameInfo): string[] {
if (Array.isArray(assetInfo.names)) return assetInfo.names;
return assetInfo.name ? [assetInfo.name] : [];
}
function getBuildOutputChunks(
result: Awaited<ReturnType<typeof vite.build>>,
): BuildStepOutput['chunks'] {
+1 -1
View File
@@ -39,7 +39,7 @@ export async function clean(config?: string | InlineConfig) {
const tempDirs = [
'node_modules/.vite',
'node_modules/.cache',
'**/.wxt',
'{,**/}.wxt',
`${path.relative(root, wxt.config.outBaseDir)}/*`,
];
wxt.logger.debug(
+13 -9
View File
@@ -93,14 +93,6 @@ async function createServerInternal(): Promise<WxtDevServer> {
const reloadOnChange = createFileReloader(server);
server.watcher.on('all', async (...args) => {
await reloadOnChange(args[0], args[1]);
// Restart keyboard shortcuts after file is changed - for some reason they stop working.
keyboardShortcuts.start();
});
keyboardShortcuts.printHelp({
canReopenBrowser:
!wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.(),
});
},
@@ -137,6 +129,10 @@ async function createServerInternal(): Promise<WxtDevServer> {
runner = await createExtensionRunner();
await runner.openBrowser();
keyboardShortcuts.start();
keyboardShortcuts.printHelp({
canReopenBrowser:
!wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.(),
});
},
};
const keyboardShortcuts = createKeyboardShortcuts(server);
@@ -152,7 +148,10 @@ async function createServerInternal(): Promise<WxtDevServer> {
logBabelSyntaxError(err);
wxt.logger.info('Waiting for syntax error to be fixed...');
await new Promise<void>((resolve) => {
const watcher = chokidar.watch(err.id, { ignoreInitial: true });
const watcher = chokidar.watch(err.id, {
...wxt.config.watchOptions,
ignoreInitial: true,
});
watcher.on('all', () => {
watcher.close();
wxt.logger.info('Syntax error resolved, rebuilding...');
@@ -172,6 +171,11 @@ async function createServerInternal(): Promise<WxtDevServer> {
// Open browser after everything is ready to go.
await runner.openBrowser();
keyboardShortcuts.start();
keyboardShortcuts.printHelp({
canReopenBrowser:
!wxt.config.runnerConfig.config.disabled && !!runner.canOpen?.(),
});
};
builderServer.on?.('close', () => keyboardShortcuts.stop());
+33 -32
View File
@@ -283,41 +283,42 @@ async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
if (res.startsWith('.') || res.startsWith('/')) return res;
return './' + res;
};
const paths = Object.entries(wxt.config.alias)
.flatMap(([alias, absolutePath]) => {
const aliasPath = getTsconfigPath(absolutePath);
return [
`"${alias}": ["${aliasPath}"]`,
`"${alias}/*": ["${aliasPath}/*"]`,
];
})
.map((line) => ` ${line}`)
.join(',\n');
const text = `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
${paths}
}
},
"include": [
"${getTsconfigPath(wxt.config.root)}/**/*",
"./wxt.d.ts"
],
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
}`;
const paths = Object.fromEntries(
Object.entries(wxt.config.alias)
.map(([alias, absolutePath]) => [alias, getTsconfigPath(absolutePath)])
.flatMap(([alias, path]) => [
// Add alias for files
[alias, [path]],
// Add alias for all files in a directory
[alias + '/*', [path + '/*']],
]),
);
const tsconfig = {
compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'Bundler',
noEmit: true,
esModuleInterop: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
strict: true,
skipLibCheck: true,
paths,
},
include: [`${getTsconfigPath(wxt.config.root)}/**/*`, './wxt.d.ts'],
exclude: [
getTsconfigPath(wxt.config.root) + '/**/node_modules',
getTsconfigPath(wxt.config.outBaseDir),
],
};
await wxt.hooks.callHook('prepare:tsconfig', wxt, { tsconfig });
return {
path: 'tsconfig.json',
text,
text: JSON.stringify(tsconfig, null, 2),
};
}
+2 -2
View File
@@ -43,10 +43,10 @@ export async function initialize(options: {
type: () => (options.packageManager == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ title: styleText('magenta', 'bun'), value: 'bun' },
{ title: styleText('red', 'npm'), value: 'npm' },
{ title: styleText('yellow', 'pnpm'), value: 'pnpm' },
{ title: styleText('cyan', 'yarn'), value: 'yarn' },
{ title: styleText('magenta', 'bun'), value: 'bun' },
],
},
],
@@ -54,7 +54,7 @@ export async function initialize(options: {
onCancel: () => process.exit(1),
},
);
input.directory ??= options.directory;
input.directory ||= options.directory || '.';
input.template ??= defaultTemplate;
input.packageManager ??= options.packageManager;
@@ -2,10 +2,11 @@ import spawn from 'nano-spawn';
import path from 'node:path';
import { beforeAll, describe, expect, it } from 'vitest';
import { pnpm } from '../pnpm';
import { describeWithBin } from '../../utils/testing/fixtures';
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('PNPM Package Management Utils', () => {
describeWithBin('pnpm', 'PNPM Package Management Utils', () => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');
@@ -1,8 +1,9 @@
import { describe, expect, it } from 'vitest';
import path from 'node:path';
import { yarn } from '../yarn';
import { describeWithBin } from '../../utils/testing/fixtures';
describe('Yarn Package Management Utils', () => {
describeWithBin('yarn', 'Yarn Package Management Utils', () => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project');
+1
View File
@@ -235,6 +235,7 @@ export async function resolveConfig(
alias,
experimental: defu(mergedConfig.experimental, {}),
suppressWarnings: mergedConfig.suppressWarnings ?? {},
watchOptions: mergedConfig.watchOptions ?? {},
dev: {
server: devServerConfig,
reloadCommand,
@@ -0,0 +1,180 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFileReloader } from '../create-file-reloader';
import { findEntrypoints, rebuild } from '../building';
import {
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeDevServer,
fakeOutputChunk,
fakePopupEntrypoint,
setFakeWxt,
} from '../testing/fake-objects';
vi.mock('../building', async () => {
const actual =
await vi.importActual<typeof import('../building')>('../building');
return {
...actual,
findEntrypoints: vi.fn(),
rebuild: vi.fn(),
};
});
describe('createFileReloader', () => {
beforeEach(() => {
vi.useFakeTimers();
setFakeWxt({
config: {
root: '/root',
entrypointsDir: '/root/src/entrypoints',
dev: {
server: {
watchDebounce: 100,
},
},
},
});
vi.mocked(findEntrypoints).mockResolvedValue([]);
});
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});
it('should detect relevant file changes even when noisy file events happen first', async () => {
const relevantFile = '/root/src/entrypoints/background.ts';
const noisyProfileFile =
'/root/private/.dev-profile/Default/Cache/Cache_Data/d573fa6484e43cf9_0';
const backgroundEntrypoint = fakeBackgroundEntrypoint({
inputPath: relevantFile,
skipped: false,
});
const currentOutput = fakeBuildOutput({
steps: [
{
entrypoints: backgroundEntrypoint,
chunks: [fakeOutputChunk({ moduleIds: [relevantFile] })],
},
],
publicAssets: [],
});
const server = fakeDevServer({
currentOutput,
reloadExtension: vi.fn(),
});
vi.mocked(rebuild).mockResolvedValue({
output: currentOutput,
manifest: currentOutput.manifest,
warnings: [],
});
vi.mocked(findEntrypoints).mockResolvedValue([backgroundEntrypoint]);
const reloadOnChange = createFileReloader(server);
const fixedFirst = reloadOnChange('change', noisyProfileFile);
await vi.advanceTimersByTimeAsync(50);
const fixedSecond = reloadOnChange('change', relevantFile);
await vi.advanceTimersByTimeAsync(500);
await Promise.all([fixedFirst, fixedSecond]);
expect(rebuild).toBeCalledTimes(1);
const [allEntrypoints, rebuiltGroups] = vi.mocked(rebuild).mock.calls[0];
expect(
allEntrypoints.some((entry) => entry.inputPath === relevantFile),
).toBe(true);
expect(
rebuiltGroups.flat().some((entry) => entry.inputPath === relevantFile),
).toBe(true);
expect(server.reloadExtension).toBeCalledTimes(1);
});
it('should rebuild and reload extension when a new entrypoint is added', async () => {
const backgroundFile = '/root/src/entrypoints/background.ts';
const newEntrypointFile = '/root/src/entrypoints/popup.html';
const backgroundEntrypoint = fakeBackgroundEntrypoint({
inputPath: backgroundFile,
skipped: false,
});
const popupEntrypoint = fakePopupEntrypoint({
inputPath: newEntrypointFile,
skipped: false,
});
const currentOutput = fakeBuildOutput({
steps: [
{
entrypoints: backgroundEntrypoint,
chunks: [fakeOutputChunk({ moduleIds: [backgroundFile] })],
},
],
publicAssets: [],
});
const server = fakeDevServer({
currentOutput,
reloadExtension: vi.fn(),
});
vi.mocked(findEntrypoints).mockResolvedValue([
backgroundEntrypoint,
popupEntrypoint,
]);
vi.mocked(rebuild).mockResolvedValue({
output: currentOutput,
manifest: currentOutput.manifest,
warnings: [],
});
const reloadOnChange = createFileReloader(server);
const trigger = reloadOnChange('add', newEntrypointFile);
await vi.advanceTimersByTimeAsync(500);
await trigger;
expect(rebuild).toBeCalledTimes(1);
const [allEntrypoints, rebuiltGroups, cachedOutput] =
vi.mocked(rebuild).mock.calls[0];
expect(allEntrypoints).toEqual([backgroundEntrypoint, popupEntrypoint]);
expect(
rebuiltGroups
.flat()
.some((entry) => entry.inputPath === newEntrypointFile),
).toBe(true);
expect(cachedOutput).toEqual(currentOutput);
expect(server.reloadExtension).toBeCalledTimes(1);
expect(server.reloadContentScript).not.toBeCalled();
});
it('should ignore entrypoint directory changes when no new entrypoints are found', async () => {
const backgroundFile = '/root/src/entrypoints/background.ts';
const ignoredEntrypointDirFile = '/root/src/entrypoints/.DS_Store';
const backgroundEntrypoint = fakeBackgroundEntrypoint({
inputPath: backgroundFile,
skipped: false,
});
const currentOutput = fakeBuildOutput({
steps: [
{
entrypoints: backgroundEntrypoint,
chunks: [fakeOutputChunk({ moduleIds: [backgroundFile] })],
},
],
publicAssets: [],
});
const server = fakeDevServer({
currentOutput,
reloadContentScript: vi.fn(),
reloadExtension: vi.fn(),
});
vi.mocked(findEntrypoints).mockResolvedValue([backgroundEntrypoint]);
const reloadOnChange = createFileReloader(server);
const trigger = reloadOnChange('add', ignoredEntrypointDirFile);
await vi.advanceTimersByTimeAsync(500);
await trigger;
expect(rebuild).not.toBeCalled();
expect(server.reloadContentScript).not.toBeCalled();
expect(server.reloadExtension).not.toBeCalled();
});
});
@@ -55,6 +55,34 @@ describe('Transform Utils', () => {
expect(actual).toEqual(expected);
});
it('should preserve spread config when removing the main field', () => {
const input = `
const sharedConfig = {
matches: ['*://*/*'],
};
export default defineContentScript({
...sharedConfig,
runAt: 'document_idle',
main(ctx) {
console.log(ctx);
},
})
`;
const expected = `const sharedConfig = {
matches: ['*://*/*'],
};
export default defineContentScript({
...sharedConfig,
runAt: 'document_idle'
})`;
const actual = removeMainFunctionCode(input).code;
expect(actual).toEqual(expected);
});
it('should remove unused imports', () => {
const input = `
import { defineBackground } from "#imports"
@@ -252,6 +252,60 @@ describe('Detect Dev Changes', () => {
expect(actual).toEqual(expected);
});
it('should ignore unrelated changed files when checking html-only reloads', async () => {
const changedPath = '/root/page1.html';
const unrelatedPath =
'/root/private/.dev-profile/Default/Cache/Cache_Data/1004_0';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
});
const htmlPage2 = fakeOptionsEntrypoint({
inputPath: '/root/page2.html',
});
const htmlPage3 = fakeGenericEntrypoint({
type: 'sandbox',
inputPath: '/root/page3.html',
});
const step1: BuildStepOutput = {
entrypoints: [htmlPage1, htmlPage2],
chunks: [
fakeOutputAsset({
fileName: 'page1.html',
}),
],
};
const step2: BuildStepOutput = {
entrypoints: [htmlPage3],
chunks: [
fakeOutputAsset({
fileName: 'page2.html',
}),
],
};
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [step1, step2],
};
const expected: DevModeChange = {
type: 'html-reload',
cachedOutput: {
...currentOutput,
steps: [step2],
},
rebuildGroups: [[htmlPage1, htmlPage2]],
};
const actual = detectDevChanges(
[unrelatedPath, changedPath],
currentOutput,
);
expect(actual).toEqual(expected);
});
it('should detect changes to entrypoints/<name>/index.html files', async () => {
const changedPath = '/root/page1/index.html';
const htmlPage1 = fakePopupEntrypoint({
@@ -45,30 +45,35 @@ export function detectDevChanges(
changedFiles: string[],
currentOutput: BuildOutput,
): DevModeChange {
const isConfigChange = some(
const relevantChangedFiles = getRelevantDevChangedFiles(
changedFiles,
currentOutput,
);
const isConfigChange = some(
relevantChangedFiles,
(file) => file === wxt.config.userConfigMetadata.configFile,
);
if (isConfigChange) return { type: 'full-restart' };
const isWxtModuleChange = some(changedFiles, (file) =>
const isWxtModuleChange = some(relevantChangedFiles, (file) =>
file.startsWith(wxt.config.modulesDir),
);
if (isWxtModuleChange) return { type: 'full-restart' };
const isRunnerChange = some(
changedFiles,
relevantChangedFiles,
(file) => file === wxt.config.runnerConfig.configFile,
);
if (isRunnerChange) return { type: 'browser-restart' };
const changedSteps = new Set(
changedFiles.flatMap((changedFile) =>
relevantChangedFiles.flatMap((changedFile) =>
findEffectedSteps(changedFile, currentOutput),
),
);
if (changedSteps.size === 0) {
const hasPublicChange = some(changedFiles, (file) =>
const hasPublicChange = some(relevantChangedFiles, (file) =>
file.startsWith(wxt.config.publicDir),
);
if (hasPublicChange) {
@@ -102,8 +107,8 @@ export function detectDevChanges(
}
const isOnlyHtmlChanges =
changedFiles.length > 0 &&
every(changedFiles, (file) => file.endsWith('.html'));
relevantChangedFiles.length > 0 &&
every(relevantChangedFiles, (file) => file.endsWith('.html'));
if (isOnlyHtmlChanges) {
return {
type: 'html-reload',
@@ -134,6 +139,19 @@ export function detectDevChanges(
};
}
export function getRelevantDevChangedFiles(
changedFiles: string[],
currentOutput: BuildOutput,
): string[] {
return Array.from(new Set(changedFiles)).filter((changedFile) => {
if (changedFile === wxt.config.userConfigMetadata.configFile) return true;
if (changedFile.startsWith(wxt.config.modulesDir)) return true;
if (changedFile === wxt.config.runnerConfig.configFile) return true;
if (changedFile.startsWith(wxt.config.publicDir)) return true;
return findEffectedSteps(changedFile, currentOutput).length > 0;
});
}
/**
* For a single change, return all the step of the build output that were
* effected by it.
@@ -1,9 +1,19 @@
import { debounce } from 'perfect-debounce';
import { Mutex } from 'async-mutex';
import { relative } from 'node:path';
import { BuildStepOutput, EntrypointGroup, WxtDevServer } from '../../types';
import {
BuildOutput,
BuildStepOutput,
EntrypointGroup,
WxtDevServer,
} from '../../types';
import { wxt } from '../wxt';
import { detectDevChanges, findEntrypoints, rebuild } from './building';
import {
detectDevChanges,
findEntrypoints,
getRelevantDevChangedFiles,
groupEntrypoints,
rebuild,
} from './building';
import { getEntrypointBundlePath, isHtmlEntrypoint } from './entrypoints';
import { getContentScriptCssFiles, getContentScriptsCssMap } from './manifest';
import {
@@ -12,6 +22,8 @@ import {
} from './content-scripts';
import { isBabelSyntaxError, logBabelSyntaxError } from './syntax-errors';
import { styleText } from 'node:util';
import { filterTruthy, toArray } from './arrays';
import { normalizePath } from './paths';
/**
* Returns a function responsible for reloading different parts of the extension
@@ -20,22 +32,44 @@ import { styleText } from 'node:util';
export function createFileReloader(server: WxtDevServer) {
const fileChangedMutex = new Mutex();
const changeQueue: Array<[string, string]> = [];
let processLoop: Promise<void> | undefined;
const cb = async (event: string, path: string) => {
changeQueue.push([event, path]);
const processQueue = async () => {
const reloading = fileChangedMutex.runExclusive(async () => {
if (server.currentOutput == null) return;
const fileChanges = changeQueue
.splice(0, changeQueue.length)
.map(([_, file]) => file);
if (fileChanges.length === 0) return;
if (server.currentOutput == null) return;
const normalizedFileChanges = fileChanges.map(normalizePath);
const relevantFileChanges = getRelevantDevChangedFiles(
fileChanges,
server.currentOutput,
);
const normalizedEntrypointsDir = normalizePath(wxt.config.entrypointsDir);
const hasEntrypointDirChange = normalizedFileChanges.some(
(file) =>
file === normalizedEntrypointsDir ||
file.startsWith(`${normalizedEntrypointsDir}/`),
);
if (relevantFileChanges.length === 0 && !hasEntrypointDirChange) return;
await wxt.reloadConfig();
const allEntrypoints = await findEntrypoints();
const allEntrypointGroups = groupEntrypoints(allEntrypoints);
const newEntrypointGroups = getNewEntrypointGroups(
normalizedFileChanges,
allEntrypointGroups,
server.currentOutput,
);
if (relevantFileChanges.length === 0 && newEntrypointGroups.length === 0)
return;
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
const changes = detectDevChanges(
relevantFileChanges,
server.currentOutput,
);
if (changes.type === 'full-restart') {
wxt.logger.info('Config changed, restarting server...');
@@ -48,46 +82,58 @@ export function createFileReloader(server: WxtDevServer) {
server.restartBrowser();
return;
}
const hasNewEntrypoints = newEntrypointGroups.length > 0;
if (changes.type === 'no-change' && !hasNewEntrypoints) return;
const changedFilesToLog = Array.from(
new Set([
...relevantFileChanges,
...newEntrypointGroups
.flatMap((group) => toArray(group))
.map((entry) => entry.inputPath),
]),
);
// Log the entrypoints that were effected
wxt.logger.info(
`Changed: ${Array.from(new Set(fileChanges))
`Changed: ${changedFilesToLog
.map((file) => styleText('dim', relative(wxt.config.root, file)))
.join(', ')}`,
);
// Rebuild entrypoints on change
const allEntrypoints = await findEntrypoints();
const rebuildGroups =
changes.type === 'no-change'
? newEntrypointGroups
: mergeEntrypointGroups(
getLatestRebuildGroups(
changes.rebuildGroups,
allEntrypointGroups,
),
newEntrypointGroups,
);
try {
const { output: newOutput } = await rebuild(
allEntrypoints,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
changes.cachedOutput,
rebuildGroups,
changes.type === 'no-change'
? server.currentOutput
: changes.cachedOutput,
);
server.currentOutput = newOutput;
// Perform reloads
switch (changes.type) {
case 'extension-reload':
server.reloadExtension();
wxt.logger.success(`Reloaded extension`);
break;
case 'html-reload':
const { reloadedNames } = reloadHtmlPages(
changes.rebuildGroups,
server,
);
wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, server);
if (hasNewEntrypoints || changes.type === 'extension-reload') {
server.reloadExtension();
wxt.logger.success(`Reloaded extension`);
} else if (changes.type === 'html-reload') {
const { reloadedNames } = reloadHtmlPages(rebuildGroups, server);
wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
} else if (changes.type === 'content-script-reload') {
reloadContentScripts(changes.changedSteps, server);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => entry.name);
wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
break;
const rebuiltNames = rebuildGroups.flat().map((entry) => entry.name);
wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
}
} catch {
// Catch build errors instead of crashing. Don't log error either, builder should have already logged it
@@ -103,12 +149,98 @@ export function createFileReloader(server: WxtDevServer) {
});
};
return debounce(cb, wxt.config.dev.server!.watchDebounce, {
leading: true,
trailing: false,
const waitForDebounceWindow = async () => {
await new Promise((resolve) => {
setTimeout(resolve, wxt.config.dev.server!.watchDebounce);
});
};
const queueWorker = async () => {
while (true) {
await processQueue();
await waitForDebounceWindow();
if (changeQueue.length === 0) break;
}
};
return async (event: string, path: string) => {
// Queue every event before debouncing so we never drop changes.
changeQueue.push([event, path]);
processLoop ??= queueWorker().finally(() => {
processLoop = undefined;
});
await processLoop;
};
}
function getNewEntrypointGroups(
normalizedFileChanges: string[],
allEntrypointGroups: EntrypointGroup[],
currentOutput: BuildOutput,
): EntrypointGroup[] {
const changedFiles = new Set(normalizedFileChanges);
const builtEntrypointPaths = new Set(
currentOutput.steps.flatMap((step) =>
toArray(step.entrypoints).map((entry) => normalizePath(entry.inputPath)),
),
);
return allEntrypointGroups.filter((group) => {
const groupEntrypoints = toArray(group);
const hasNewEntrypoint = groupEntrypoints.some(
(entry) => !builtEntrypointPaths.has(normalizePath(entry.inputPath)),
);
const changedEntrypoint = groupEntrypoints.some((entry) =>
changedFiles.has(normalizePath(entry.inputPath)),
);
return hasNewEntrypoint && changedEntrypoint;
});
}
function getLatestRebuildGroups(
rebuildGroups: EntrypointGroup[],
allEntrypointGroups: EntrypointGroup[],
): EntrypointGroup[] {
const groupByEntrypointPath = new Map<string, EntrypointGroup>();
allEntrypointGroups.forEach((group) => {
toArray(group).forEach((entry) => {
groupByEntrypointPath.set(normalizePath(entry.inputPath), group);
});
});
return mergeEntrypointGroups(
rebuildGroups.flatMap((group) => {
return filterTruthy(
toArray(group).map((entry) =>
groupByEntrypointPath.get(normalizePath(entry.inputPath)),
),
);
}),
);
}
function mergeEntrypointGroups(
...groups: EntrypointGroup[][]
): EntrypointGroup[] {
const deduped = new Map<string, EntrypointGroup>();
groups.flat().forEach((group) => {
deduped.set(getEntrypointGroupKey(group), group);
});
return [...deduped.values()];
}
function getEntrypointGroupKey(group: EntrypointGroup): string {
return toArray(group)
.map((entry) => normalizePath(entry.inputPath))
.sort()
.join('|');
}
/**
* From the server, tell the client to reload content scripts from the provided
* build step outputs.
+1
View File
@@ -1 +1,2 @@
export { normalizePath } from './paths';
export { getEntrypointBundlePath } from './entrypoints';
@@ -296,6 +296,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
userConfigMetadata: {},
alias: {},
experimental: {},
watchOptions: {},
dev: {
reloadCommand: 'Alt+R',
},
@@ -0,0 +1,17 @@
import { spawnSync } from 'node:child_process';
import { describe } from 'vitest';
export function describeWithBin(
bin: string,
title: string,
callback: () => void,
) {
if (process.env.CI === 'true') return describe(title, callback);
const result = spawnSync(bin, ['--version'], {
stdio: 'ignore',
shell: true,
});
if (result.status !== 0) return describe.skip(title, callback);
return describe(title, callback);
}
+1 -1
View File
@@ -41,7 +41,7 @@ function emptyMainFunction(mod: ProxifiedModule): void {
// ex: "fn({ ..., main: () => {} })" to "fn({ ... })"
mod.exports.default.$ast.arguments[0].properties =
mod.exports.default.$ast.arguments[0].properties.filter(
(prop: any) => prop.key.name !== 'main',
(prop: any) => prop.key?.name !== 'main',
);
}
}
+32
View File
@@ -131,6 +131,20 @@ export interface InlineConfig {
* line `--mv2` or `--mv3` option.
*/
manifestVersion?: TargetManifestVersion;
/**
* Chokidar options used by dev-mode file watchers. This is useful in
* containers, WSL, and network file systems where native file events can be
* unreliable.
*
* @example
* export default defineConfig({
* watchOptions: {
* usePolling: true,
* interval: 1000,
* },
* });
*/
watchOptions?: vite.WatchOptions;
/**
* Override the logger used.
*
@@ -1259,6 +1273,11 @@ export interface ServerInfo {
origin: string;
}
export type PrepareTsconfigs = {
/** The JSON contents of the `.wxt/tsconfig.json` file. */
tsconfig: any;
};
export type HookResult = Promise<void> | void;
export interface WxtHooks {
@@ -1298,6 +1317,17 @@ export interface WxtHooks {
* });
*/
'prepare:types': (wxt: Wxt, entries: WxtDirEntry[]) => HookResult;
/**
* Called before WXT writes your tsconfig to the disk, allowing full
* customization by modifying the object by reference.
*
* @since 0.20.28
* @example
* wxt.hooks.hook('prepare:tsconfig', (wxt, { tsconfig }) => {
* tsconfig.compilerOptions.lib.push('WebWorker');
* });
*/
'prepare:tsconfig': (wxt: Wxt, configs: PrepareTsconfigs) => HookResult;
/**
* Called before generating the list of public paths inside
* `.wxt/types/paths.d.ts`. Use this hook to add additional paths (relative to
@@ -1539,6 +1569,8 @@ export interface ResolvedConfig {
firefoxDataCollection?: boolean;
firefoxId?: boolean;
};
/** Chokidar options used by dev-mode file watchers. */
watchOptions: vite.WatchOptions;
dev: {
/** Only defined during dev command */
server?: {