Compare commits

..

127 Commits

Author SHA1 Message Date
rxliuli ebe1f9da7e docs: Remove dangling "Remote Code" sidebar link (#2532)
✨ pkg.pr.new / Publish Test Packages (push) Waiting to run
📝 Publish Docs / Publish Docs (push) Waiting to run
🛡️ Validate / Checks (push) Waiting to run
🛡️ Validate / Builds (push) Waiting to run
🛡️ Validate / Build Demo (push) Waiting to run
🛡️ Validate / Tests (Windows) (push) Waiting to run
🛡️ Validate / Tests (Linux) (push) Waiting to run
🛡️ Validate / Template (react) (push) Waiting to run
🛡️ Validate / Template (solid) (push) Waiting to run
🛡️ Validate / Template (svelte) (push) Waiting to run
🛡️ Validate / Template (vanilla) (push) Waiting to run
🛡️ Validate / Template (vue) (push) Waiting to run
2026-07-30 16:38:23 +05:30
Aaron acc4cdec07 chore(deps): Replace async-mutex with superlock (#2529) 2026-07-29 09:12:36 -05:00
Aaron 8570ce29b0 chore(deps): Replace prompts with @topcli/prompts (#2528) 2026-07-28 17:38:45 -05:00
Aaron f7cce3e269 chore(deps): Replace open with tiny-open (#2527) 2026-07-28 17:10:22 -05:00
Aaron 315912ebf3 chore(deps): Drop import-meta-resolve (#2526) 2026-07-28 17:10:12 -05:00
Aaron 574c3bd90a docs: Update bundle size for 0.21 2026-07-28 13:57:00 -05:00
Aaron 8dc8fa6f51 chore: Add support for publish-browser-extension v6 2026-07-28 13:57:00 -05:00
github-actions[bot] 1e841ebad7 chore(release): wxt v0.21.2
📼 VHS / Create VHS (push) Waiting to run
2026-07-28 13:01:06 +00:00
Aaron 925e97d4fc docs: Update package size links 2026-07-28 07:56:20 -05:00
Aaron 859d654611 fix: Remove web-ext-run, it's unused now 2026-07-28 07:46:01 -05:00
Aaron 466ae8a33c docs: Fix heading level 2026-07-26 18:23:51 -05:00
Aaron d1ab2d923d docs: Fix typos and wording of upgrade guide 2026-07-26 18:22:54 -05:00
github-actions[bot] 718b063ed7 chore(release): wxt v0.21.1
📼 VHS / Create VHS (push) Waiting to run
2026-07-26 23:13:13 +00:00
Aaron 87a96d4661 chore: Add vite to template projects (#2517) 2026-07-26 18:02:38 -05:00
Aaron d8a86e7633 docs: Add section about install footprint 2026-07-26 18:02:38 -05:00
Aaron c3eaef6fb4 docs: Finalize migration guide for wxt v0.21 (#2516) 2026-07-26 18:02:38 -05:00
Aaron d1a956342d ci: Use @aklinker1/zero-changelog for release notes (#2515) 2026-07-26 18:02:38 -05:00
Aaron 0fdac0d4e4 feat(Firefox): List files in sources zip (#2513) 2026-07-26 18:02:38 -05:00
Aaron 7965ab0592 chore: Add type to type imports 2026-07-26 18:02:38 -05:00
Aaron 54d10af1fd fix(TS)!: Update .wxt/tsconfig.json compiler options, require TS >=5.4 (#2512)
BREAKING CHANGE: Updates `.wxt/tsconfig.json` defaults to latest recommended settings for vite projects and requires Typescript 5.4 or later. See #2512 for details on what compiler options were changed.

Co-authored-by: cookesan <cookesan@users.noreply.github.com>
2026-07-26 18:02:38 -05:00
Aaron 7dbe18ba0c fix!: Update content and unlisted script globalName default to false (#2511)
BREAKING CHANGE: If your script needs to return a value, like for `browser.scripting.executeScript`, set `globalName: true` to maintain the old behavior.
2026-07-26 18:02:38 -05:00
Aaron d4f78b623e Fix markdownlint 2026-07-26 18:02:38 -05:00
Aaron 7d6caad9b5 fix: Loosen version of @webext-core/isolated-element
Allows reverting to v1 if styles break
2026-07-26 18:02:38 -05:00
Aaron 0b352f6eff fix: Remove console.log 2026-07-26 18:02:38 -05:00
Aaron 885f8f0b76 docs: Cleanup v0.21 migration guide 2026-07-26 18:02:38 -05:00
ТΞNSΛI eae835fa80 feat!: new template variable {{versionName}} and change of the behaviour of {{version}} (#1612)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-07-26 18:02:38 -05:00
Kai Gritun 782a5b85f1 fix(zip)!: make includeSources/excludeSources follow standard allowlist behavior (#2114)
Co-authored-by: Eli <88557639+lishaduck@users.noreply.github.com>
Co-authored-by: ТΞNSΛI <tensai@gmx.net>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
Co-authored-by: Kai Gritun <kai@kaigritun.com>
2026-07-26 18:02:38 -05:00
Aaron 961cfa5f2a chore(deps)!: Upgrade major dependencies (#2508) 2026-07-26 18:02:38 -05:00
fkatsuhiro ada0c86877 chore!: Remove deprecated APIs (#2488)
Co-authored-by: Gliches <nishumurmu017@gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-07-26 18:02:38 -05:00
Gliches 13151509b6 fix!: Remove url: import feature (#2456)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-07-26 18:02:38 -05:00
Aaron b7bdb11a15 chore!: Upgrade @webext-core/isolated-element v1 → v3 (#2098)
Co-authored-by: Eli <88557639+lishaduck@users.noreply.github.com>
Co-authored-by: ТΞNSΛI <tensai@gmx.net>
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-07-26 18:02:38 -05:00
Aaron ab679ba413 fix: Handle edge case where eslint version cannot be found 2026-07-26 18:02:38 -05:00
Patryk Kuniczak f69b594401 feat!: Change default behavior of getUnimportEslintOptions true flag (#2296) 2026-07-26 18:02:38 -05:00
Aaron be3af14f9d chore(deps): Remove esbuild dependency (#2506) 2026-07-26 18:02:38 -05:00
Aaron 3679bc772e chore(deps): Upgrade to @types/node 22 2026-07-26 18:02:38 -05:00
Aaron 9da43b6b75 Fix formatting 2026-07-26 18:02:38 -05:00
Aaron 008f5a66c9 chore!: Bump minimum supported Node version to >=22 2026-07-26 18:02:38 -05:00
Aaron cffacffe34 fix!: Make web-ext a peer dependency (#2079)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-07-26 18:02:38 -05:00
ТΞNSΛI 3bd7aea9ee feat!: add {{modeSuffix}} to zip templates and update defaults (#1623)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-07-26 18:02:38 -05:00
Eli 4b26d872ec refactor!: move to module-runner (#1946) 2026-07-26 18:02:38 -05:00
Eli c10d09fbdb refactor: make vite a default peer (#1945) 2026-07-26 18:02:38 -05:00
fkatsuhiro 9698f2fdd9 fix: Deprecated useAppConfig in favour of getAppConfig (#2487)
Co-authored-by: Gliches <nishumurmu017@gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-07-26 11:44:52 -05:00
github-actions[bot] 299b0add80 docs: Update wxt init GIF 2026-07-26 15:04:13 +00:00
Aaron ec3d3196a9 ci: Run VHS workflow on PRs that touch relevant files 2026-07-26 10:01:54 -05:00
github-actions[bot] 13be7c5bfd docs: Update wxt init GIF 2026-07-26 15:01:02 +00:00
Aaron 575240aaf1 ci: Allow the VHS action to commit to main 2026-07-26 09:58:27 -05:00
Thribhuvan b124c82ed4 docs: use consola's fancy reporter in init demo GIF (#2485) 2026-07-26 09:50:44 -05:00
Gliches 6c5a1b0d2e fix: add user exclusion for GitHub Actions in PR closed workflow (#2509) 2026-07-26 09:32:34 -05:00
Gliches eb93fa4535 fix: docs sidebar header, background color issue (#2504) 2026-07-26 10:37:37 +02:00
nnthanh01061999 5bd36e90bf docs: add MindSnap to showcase (#2499) 2026-07-21 17:57:14 +00:00
Hugo Villanueva Jimenez 172d37193e Docs: add ArcPort extensions entries to showcase (#2495) 2026-07-17 18:31:58 +00:00
Eason d92d5a4892 Add Adaptive Tab Bar Colour to extension showcase (#2490) 2026-07-11 17:42:31 +00:00
aklinker1 40c6c19802 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-07-02 01:05:26 +00:00
Aaron 653fccc8eb docs: Cleanup the "extension APIs" page (#2473) 2026-07-01 21:02:23 +05:30
Nazım Gediz Aydındoğmuş 95657939a8 docs: Added "Teams Chat Exporter" Edge Addons (edgeId) to extension showcase (#2472) 2026-06-30 22:12:08 +00:00
Arthur a578dbf633 feat(analytics): add posthog as an analytics provider (#2414)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-06-30 21:20:04 +00:00
github-actions[bot] f14ec35889 chore(release): @wxt-dev/i18n v0.2.6
📼 VHS / Create VHS (push) Waiting to run
2026-06-30 21:02:33 +00:00
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
github-actions[bot] 1a3f4f64e5 chore(release): wxt v0.20.27
📼 VHS / Create VHS (push) Waiting to run
2026-06-23 18:08:15 +00:00
Patryk Kuniczak 492cbb4644 fix: Rename ManifestPermissions to ManifestPermission of types.ts in wxt package (#2430) 2026-06-23 12:57:50 -05:00
aklinker1 c8e93b15db fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-06-21 01:16:16 +00:00
RenovZ e3f4fe3e13 docs: added anylang translator to showcase (#2426) 2026-06-19 20:01:03 +00:00
Nicholas Yoannou 8cb973cf3c docs: Added Hayami to showcase (#2422) 2026-06-18 17:39:17 +00:00
Avi 22d1a72752 docs: added 4 extensions to the homepage (#2419) 2026-06-16 19:32:40 +00:00
Peter Wong 8f90331229 docs: Added NiceTab firefoxslug to extension showcase (#2417)
Co-authored-by: wangpeng618 <wangpeng618@jd.com>
2026-06-16 19:01:39 +00:00
ChamikaUluwatta 9a638449d5 fix: correct broken link at react router in createhashrouter (#2411) 2026-06-09 18:00:21 +00:00
Muhammed Mustafa AKŞAM 9ce9b714f6 docs: Added "LinkedIn Games Solver" to showcase (#2410) 2026-06-09 14:35:31 +00:00
aklinker1 22b7aba035 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-06-06 01:04:14 +00:00
Slovakian 4cbcf282f7 docs: Added "WageSlav3" to showcase (#2405)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-06-05 20:56:03 +00:00
Oliver Benns d8b1ddfb59 docs: Added Property Track to showcase (#2404) 2026-06-05 09:08:58 +00:00
David Nguyen 7905cb379b chore: Add Google Cloud Skills Boost - Helper extension to showcase (#2401) 2026-06-02 14:35:05 +00:00
Esteban Vera 1b2463d52e "docs: Added "Link Later" to showcase" (#2392) 2026-05-22 15:09:42 +00:00
Saksham Kushwaha b567f9957a Add firefoxSlug for Monochromate extension (#2389) 2026-05-20 14:17:28 +00:00
Sean Kenneth Doherty c2fb8c786d docs: self-host Netlify footer badge (#2386) 2026-05-18 06:08:24 +00:00
Muhammad Umer Farooq 4320d8e1fa docs: clarify SPA navigation setup with locationWatcher.run() (#2380)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-05-16 17:37:56 -07:00
Muzammil 344d14dafc docs: Added "Sound Booster & Equalizer" to showcase (#2381) 2026-05-15 16:21:59 +02:00
Aaron 3cb894c4e5 chore: Add benchmarks for i18n vs vanilla usage (#2372) 2026-05-14 06:41:42 -07:00
Aniket lodh dbbe14ddd6 docs: add warning about rem unit inheritance in Shadow Root UI (#2330)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-05-12 18:19:51 +00:00
Aaron 9ee07f431a chore: Fix lockfile 2026-05-12 07:34:44 -05:00
dependabot[bot] d517e9a0b9 chore(deps): bump tinyglobby from 0.2.15 to 0.2.16 (#2366)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 07:27:17 -05:00
dependabot[bot] d9adbb6f09 chore(deps): bump pnpm/action-setup from 6.0.6 to 6.0.7 (#2362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 07:26:48 -05:00
dependabot[bot] 6b77277cea chore(deps): bump filesize from 11.0.15 to 11.0.17 (#2363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 07:26:30 -05:00
dependabot[bot] bad24392fc chore(deps): bump c12 from 3.3.3 to 3.3.4 (#2364)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-05-12 07:26:13 -05:00
George Forse f24e62129b docs: Add Telsia extension to Showcase (#2368) 2026-05-12 07:13:58 -05:00
Aaron f38cb855ea ci: Don't error PR when author assignment fails 2026-05-12 07:12:44 -05:00
dependabot[bot] 891a0d50a6 chore(deps-dev): bump @vitest/coverage-v8 from 4.1.5 to 4.1.6 (#2367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 07:09:42 -05:00
Shubhrajit Sadhukhan cfb384e618 feat: Add warning for missing Firefox extension ID in manifest (#2293)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-05-12 12:08:38 +00:00
152 changed files with 4291 additions and 1961 deletions
+1 -1
View File
@@ -9,4 +9,4 @@
/packages/unocss/ @Timeraa
# Other Directories
/docs @aklinker1 @PatrykKuniczak
/docs @aklinker1 @PatrykKuniczak @nishu-murmu
+11 -7
View File
@@ -62,14 +62,18 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
assignees: ['${{ steps.check.outputs.author }}']
});
try {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
assignees: ['${{ steps.check.outputs.author }}']
});
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
} catch (err) {
console.warn("Error assigning author:", err);
}
- name: Sync Labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -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
View File
@@ -27,6 +27,7 @@ jobs:
github.event.pull_request.user.login != 'aklinker1' &&
github.event.pull_request.user.login != 'Timeraa' &&
github.event.pull_request.user.login != 'PatrykKuniczak' &&
github.event.pull_request.user.login != 'nishu-murmu' &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
github.event.pull_request.user.login != 'copilot-swe-agent[bot]'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+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
+51 -19
View File
@@ -20,6 +20,13 @@ on:
- unocss
- webextension-polyfill
- wxt
bump:
description: Blank (auto), a version (1.4.1), or major/minor/patch
type: string
dryRun:
description: Do a dry-run and don't publish anything
type: boolean
default: false
permissions:
contents: read
@@ -29,18 +36,23 @@ jobs:
name: Validate
uses: './.github/workflows/validate.yml'
secrets: inherit
if: ${{ inputs.dryRun != true }}
publish:
name: Publish
name: Publish ${{ case(inputs.dryRun, ' (dry-run)', '') }}
runs-on: ubuntu-24.04
permissions:
contents: write # Push version changes
id-token: write # OIDC for NPM publishing
needs:
- validate
# Allow dry runs to skip validation but still run this job
if: |
always() &&
(needs.validate.result == 'success' || needs.validate.result == 'skipped')
steps:
- name: Checkout
uses: actions/checkout@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
@@ -52,22 +64,42 @@ jobs:
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git config --global push.followTags true
- name: Bump and Tag
run: |
bun run scripts/bump-package-version.ts ${{ inputs.package }}
git push
git push --tags
- name: Upgrade NPM for Provence
run: sudo npm i -g npm@latest
- name: Publish to NPM
working-directory: packages/${{ inputs.package }}
run: |
bun pm pack
sudo npm i -g npm@latest
npm publish *.tgz
- name: Create GitHub release
run: bun run scripts/create-github-release.ts ${{ inputs.package }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Bump and Release
uses: aklinker1/zero-changelog/actions/release@b6fcca17cebfc45f996b80fe5926da924d8441f8 # 0.2.1
with:
bump: ${{ inputs.bump }}
path: packages/${{ inputs.package }}
# Include documentation updates in WXT's releases
additionalDirs: |-
${{ case(
inputs.package == 'wxt', 'docs',
''
) }}
# Use package name in commit message
commitTemplate: |-
${{ case(
inputs.package == 'wxt', 'chore(release): wxt v{{version}}',
'chore(release): @wxt-dev/{{dirname}} v{{version}}'
) }}
# Use package name in release name
releaseNameTemplate: |-
${{ case(
inputs.package == 'wxt', 'wxt v{{version}}',
'@wxt-dev/{{dirname}} v{{version}}'
) }}
# Update lockfile with new versions since it includes the package versions
preCommitCommands: |-
bun i
publishCommands: |-
bun pm pack
npm publish *.tgz
dryRun: ${{ inputs.dryRun }}
dryRunPublishCommands: |-
bun pm pack
npm publish *.tgz --dry-run
githubToken: ${{ secrets.GITHUB_TOKEN }}
latestRelease: ${{ inputs.package == 'wxt' }}
-44
View File
@@ -1,44 +0,0 @@
name: 🔄 Sync Releases
on:
workflow_dispatch:
inputs:
package:
description: Package to sync
default: wxt
type: choice
options:
- analytics
- auto-icons
- i18n
- is-background
- module-react
- module-solid
- module-svelte
- module-vue
- runner
- storage
- webextension-polyfill
- wxt
permissions:
contents: read
jobs:
sync:
name: Sync Releases
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
with:
installArgs: --ignore-scripts
- name: Sync Releases
run: bun run scripts/sync-releases.ts ${{ inputs.package }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+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@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
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
+16 -16
View File
@@ -2,7 +2,12 @@ name: 📼 VHS
on:
push:
paths:
- 'docs/tapes/*.tape'
- docs/tapes/*.tape
- .github/workflows/vhs.yml
pull_request:
paths:
- docs/tapes/*.tape
- .github/workflows/vhs.yml
workflow_dispatch:
permissions:
@@ -17,27 +22,22 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.ref_name }}
- name: Setup PNPM
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
ssh-key: ${{ secrets.DEPLOY_KEY }}
- 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 +46,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:
-1
View File
@@ -53,7 +53,6 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
- 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
- ⬇️ Download and bundle remote URL imports
## Sponsors
Generated
+308 -202
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -19,6 +19,7 @@ words:
- automount
- avenir
- bidi
- blocklists
- buildc
- bunx
- cachable
@@ -29,9 +30,11 @@ words:
- comctx
- confbox
- configfile
- consola
- consolas
- Cpath
- crbug
- cros
- crunchyroll
- cssinjs
- CSUI
@@ -69,6 +72,7 @@ words:
- Menlo
- menupanel
- metas
- moderok
- mktemp
- mycompany
- nacl
@@ -87,6 +91,7 @@ words:
- pbbf
- personaltoolbar
- portaling
- posthog
- prebundled
- prefs
- proxified
@@ -101,8 +106,10 @@ words:
- solidjs
- styl
- subdependency
- superlock
- tabstrip
- teleporting
- topcli
- treemap
- triaging
- umami
+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;
})
+1 -3
View File
@@ -163,8 +163,7 @@ export default defineConfig({
footer: {
message: [
'<a class="light-netlify" href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-bg.svg" alt="Deploys by Netlify" style="display: inline;" /></a>',
'<a class="dark-netlify" href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" style="display: inline;" /></a>',
'<span class="netlify-badge"><a class="light-netlify" href="https://www.netlify.com"><img src="/netlify-badge-light.svg" alt="Deploys by Netlify" /></a><a class="dark-netlify" href="https://www.netlify.com"><img src="/netlify-badge-dark.svg" alt="Deploys by Netlify" /></a></span>',
'Released under the <a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE">MIT License</a>.',
].join('<br/>'),
copyright:
@@ -244,7 +243,6 @@ export default defineConfig({
menuItem('WXT Modules', 'wxt-modules.md'),
menuItem('Frontend Frameworks', 'frontend-frameworks.md'),
menuItem('ES Modules', 'es-modules.md'),
menuItem('Remote Code', 'remote-code.md'),
menuItem('Unit Testing', 'unit-testing.md'),
menuItem('E2E Testing', 'e2e-testing.md'),
menuItem('Publishing', 'publishing.md'),
+31 -38
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,72 +19,56 @@
--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);
}
.light-netlify {
display: inline;
display: inline-flex;
}
.dark .light-netlify {
display: none;
}
.dark-netlify {
display: none;
}
.dark .dark-netlify {
display: inline;
display: inline-flex;
}
/* 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;
@@ -86,9 +76,12 @@ 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;
}
.VPSidebar .curtain {
z-index: 2;
}
+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);
}
+74 -1
View File
@@ -7,10 +7,15 @@
# 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.
- # Anylang Translator
chromeId: injdenmpjncknciccmjjmjlgbmnkince
firefoxSlug: anylang-translator
- # GitHub: Better Line Counts
chromeId: ocfdgncpifmegplaglcnglhioflaimkd
firefoxSlug: github-better-line-counts
@@ -26,6 +31,7 @@
- # StayFree - Website Blocker & Web Analytics
chromeId: elfaihghhjjoknimpccccmkioofjjfkf
firefoxSlug: stayfree
edgeId: aghmglalnfmjbipodadgbgpakneabgnp
- # Doozy: Ai Made Easy
chromeId: okifoaikfmpfcamplcfjkpdnhfodpkil
@@ -105,6 +111,8 @@
- # NiceTab - https://github.com/web-dahuyou/NiceTab
chromeId: fonflmjnjbkigocpoommgmhljdpljain
firefoxSlug: nice-tab-manager
edgeId: ompjiaelpibiggcnanhbdblkhfdmkgnl
- # Draftly for LinkedIn
chromeId: fcffekbnfcfdemeekijbbmgmkognnmkd
@@ -138,6 +146,22 @@
chromeId: fcphghnknhkimeagdglkljinmpbagone
firefoxSlug: youtube-auto-hd-fps
- # YouTube Auto Feed
chromeId: jcdebdlnakhdinkindpogcehnhggbfad
firefoxSlug: youtube-auto-feed
- # YouTube Like-Dislike Shortcut
chromeId: fdkpkpelkkdkjhpacficichkfifijipc
firefoxSlug: youtube-like-dislike-shortcut
- # Twitch Channel Points Bonus Collector
chromeId: kbbdnbaghpcjpdhbjbccadodjejlkkgg
firefoxSlug: twitch-cp-bonus-collector
- # Skillshare Player Control
chromeId: agbhgcomfpcfboebbfmefbicfkpnlfeg
firefoxSlug: skillshare-player-control
- # MultiViewer Companion
chromeId: lpomjgbicdemjkgmbnkjncgdebogkhlb
firefoxSlug: multiviewer-companion
@@ -217,6 +241,7 @@
- # Monochromate
chromeId: hafcajcllbjnoolpfngclfmmgpikdhlm
firefoxSlug: monochromate
- # AliasVault - Open-Source Password & (Email) Alias Manager
chromeId: bmoggiinmnodjphdjnmpcnlleamkfedj
@@ -376,6 +401,7 @@
- # Teams Chat Exporter
chromeId: jmghclbfbbapimhbgnpffbimphlpolnm
firefoxSlug: teams-chat-exporter
edgeId: phlomfiieaggnbfpacmjmidcjdlaiplp
- # Lofi BGM Player - Free lofi focus music for work & study
chromeId: jdcppdokgfbnhiacbeplahgnciahnhck
@@ -458,3 +484,50 @@
- # Redmine Time Tracking
chromeId: ldcanhhkffokndenejhafhlkapflgcjg
firefoxSlug: redmine-time-tracking
- # Telsia: Eye Strain Reduction
chromeId: iakkmikmbknfcffhalijnhijblikoldc
firefoxSlug: telsia-eye-strain-reduction
- # Sound Booster & Equalizer
chromeId: podnfipggfpfcpglhjebcefinglohbhn
- # Link Later
chromeId: gpehbbegbcpjmipphokcmfhkchhcpfam
- # Google Cloud Skills Boost - Helper
chromeId: lmbhjioadhcoebhgapaidogodllonbgg
firefoxSlug: cloud-skills-boost-helper
- # Property Track - https://www.propertytrack.co
chromeId: ahknifalnhmbanoclebllcolcdomfdop
firefoxSlug: propertytrack
- # WageSlav3
chromeId: anhkcmapjhiiofpcgglfljphdgppilof
- # LinkedIn Games Solver
chromeId: jnhgapnkejaijibcdhcldhdfikjmdaph
firefoxSlug: linkedin-games-solver-addon
- # Hayami: Anime comments & discussions
chromeId: nhkggpiaeaeeeimohfpchnjobbamfcbg
firefoxSlug: hayami
- # CopyApes Assistant
chromeId: affmjifigldmicnbgpghddaneomejmfo
- # Leetcode Enhancer
chromeId: cpoclfijojgjiafnlgnhjalkaiabcjch
- # Adaptive Tab Bar Colour
firefoxSlug: adaptive-tab-bar-colour
- #ArcPort - Tab Checkpoint
chromeId: kajmnfhpmkimleehomeioondgfcjjnbp
- #ArcPort - Tab Rebrand
chromeId: pgmohcikgaeddapienpncpbiklbmfkgp
- #MindSnap
chromeId: mdcpckfcphpicfpiopfcpeambjknjabl
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 138 KiB

+4 -4
View File
@@ -43,7 +43,7 @@ ESLint doesn't know about the auto-imported variables unless they are explicitly
:::code-group
```ts [ESLint 9]
```ts [ESLint >=9]
export default defineConfig({
imports: {
eslintrc: {
@@ -53,7 +53,7 @@ export default defineConfig({
});
```
```ts [ESLint 8]
```ts [ESLint <=8]
export default defineConfig({
imports: {
eslintrc: {
@@ -69,7 +69,7 @@ Then in your ESLint config, import and use the generated file:
:::code-group
```js [ESLint 9]
```js [ESLint >=9]
// eslint.config.mjs
import autoImports from './.wxt/eslint-auto-imports.mjs';
@@ -81,7 +81,7 @@ export default [
];
```
```js [ESLint 8]
```js [ESLint <=8]
// .eslintrc.mjs
export default {
extends: ['./.wxt/eslintrc-auto-import.json'],
@@ -4,11 +4,17 @@ outline: deep
# Browser Startup
During development, WXT will use any of the below packages to automatically open a browser with your extension installed.
- [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext)
Just install the dependency you want WXT to use to open the browser.
## `web-ext` Usage
> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
## Config Files
### Config Files
You can configure browser startup in 3 places:
@@ -25,9 +31,9 @@ You can configure browser startup in 3 places:
2. `<rootDir>/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
## Recipes
### Recipes
### Set Browser Binaries
#### Set Browser Binaries
To set or customize the browser opened during development:
@@ -57,7 +63,7 @@ export default defineConfig({
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
### Persist Data
#### Persist Data
By default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script.
@@ -94,9 +100,9 @@ Now, next time you run the `dev` script, a persistent profile will be created in
You can use any directory you'd like for `--user-data-dir`, the examples above create a persistent profile for each WXT project. To create a profile for all WXT projects, you can put the `chrome-data` directory inside your user's home directory.
:::
### Disable Opening Browser
#### Disable Opening Browser
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
If you don't want to uninstall `web-ext`, like to test in your normal profile, you can do so via `disabled: true`:
```ts [web-ext.config.ts]
import { defineWebExtConfig } from 'wxt';
+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.
+9
View File
@@ -449,6 +449,15 @@ Full examples:
- [react-content-script-ui](https://github.com/wxt-dev/examples/tree/main/examples/react-content-script-ui)
- [tailwindcss](https://github.com/wxt-dev/examples/tree/main/examples/tailwindcss)
:::warning `rem` Units Are Not Fully Isolated
WXT resets most inherited styles via `all: initial`. This doesn't reset the `<html>` element's font size, which determines the relative size of `rem` units.
If your CSS framework uses `rem` units, like Tailwind CSS, you may notice your UI's scale changing on different websites.
See the [FAQ](/guide/resources/faq#my-content-script-ui-looks-different-on-certain-websites) for a fix.
:::
### IFrame
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
+32 -35
View File
@@ -76,27 +76,33 @@ Alternatively, if you're trying to use similar APIs under different names (to su
});
```
### Augmenting the Browser Type
## Add Firefox Types
WXT's `browser` types are based on the `@types/chrome` package. That means some Firefox-specific APIs may not be typed, like `browser.sidebarAction`. If you want to add types for these APIs, you can augment the browser type to add them yourself:
WXT's `browser` types are based on the `@types/chrome` package. That means some Firefox-specific APIs may not be typed, like `browser.sidebarAction`. If you want to add types for these APIs, you can augment the browser type by:
```ts
// <srcDir>/browser-types.d.ts
import '@wxt-dev/browser';
import type { SidebarAction } from 'webextension-polyfill';
1. Installing `@wxt-dev/browser` as a direct dependency
declare module '@wxt-dev/browser' {
namespace Browser {
export const sidebarAction: SidebarAction.Static;
}
}
```
```sh
pnpm add @wxt-dev/browser
```
> For this to work, you may need to install `@wxt-dev/browser` as a direct dependency.
>
> ```sh
> pnpm add @wxt-dev/browser
> ```
2. Augmenting the `Browser` type:
```ts
// <srcDir>/browser-types.d.ts
import '@wxt-dev/browser';
import type { SidebarAction } from 'webextension-polyfill';
declare module '@wxt-dev/browser' {
namespace Browser {
export const sidebarAction: SidebarAction.Static;
}
}
```
You can add types from any source: `webextension-polyfill`, `@types/firefox-webext-browser`, or your own custom types.
See TypeScript's documentation on [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) for more information.
## Entrypoint Limitations
@@ -116,41 +122,32 @@ The fix is simple, just move your API usage into the entrypoint's main function:
:::code-group
<!-- prettier-ignore -->
```ts [background.ts]
browser.action.onClicked.addListener(() => {
/* ... */
}); // [!code --]
browser.action.onClicked.addListener(() => { /* ... */ }); // [!code --]
export default defineBackground(() => {
browser.action.onClicked.addListener(() => {
/* ... */
}); // [!code ++]
browser.action.onClicked.addListener(() => { /* ... */ }); // [!code ++]
});
```
<!-- prettier-ignore -->
```ts [content.ts]
browser.runtime.onMessage.addListener(() => {
/* ... */
}); // [!code --]
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code --]
export default defineContentScript({
main() {
browser.runtime.onMessage.addListener(() => {
/* ... */
}); // [!code ++]
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code ++]
},
});
```
<!-- prettier-ignore -->
```ts [unlisted.ts]
browser.runtime.onMessage.addListener(() => {
/* ... */
}); // [!code --]
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code --]
export default defineUnlistedScript(() => {
browser.runtime.onMessage.addListener(() => {
/* ... */
}); // [!code ++]
browser.runtime.onMessage.addListener(() => { /* ... */ }); // [!code ++]
});
```
+1 -1
View File
@@ -97,7 +97,7 @@ Instead, you need to configure the router to run in "hash" mode, where the routi
Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
- [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)
- [`react-router`](https://reactrouter.com/api/data-routers/createHashRouter#createhashrouter)
- [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
- [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
- [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
-31
View File
@@ -1,31 +0,0 @@
# Remote Code
WXT will automatically download and bundle imports with the `url:` prefix so the extension does not depend on remote code, [a requirement from Google for MV3](https://developer.chrome.com/docs/extensions/migrating/improve-security/#remove-remote-code).
## Google Analytics
For example, you can import Google Analytics:
```ts
// utils/google-analytics.ts
import 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';
window.dataLayer = window.dataLayer || [];
// NOTE: This line is different from Google's documentation
window.gtag = function () {
dataLayer.push(arguments);
};
gtag('js', new Date());
gtag('config', 'G-XXXXXX');
```
Then you can import this in your HTML files to enable Google Analytics:
```ts
// popup/main.ts
import '~/utils/google-analytics';
gtag('event', 'event_name', {
key: 'value',
});
```
@@ -78,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) {
// ...
},
});
```
-1
View File
@@ -21,7 +21,6 @@ This plugin does several things:
- Polyfills the extension API, `browser`, with an in-memory implementation using [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation)
- Adds all vite config or plugins in `wxt.config.ts`
- Configures auto-imports (if enabled)
- Applies internal WXT vite plugins for things like [bundling remote code](/guide/essentials/remote-code)
- Sets up global variables provided by WXT (`import.meta.env.BROWSER`, `import.meta.env.MANIFEST_VERSION`, `import.meta.env.IS_CHROME`, etc)
- Configures aliases (`@/*`, `@@/*`, etc) so imports can be resolved
+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
```
-1
View File
@@ -24,7 +24,6 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
| Supports all frontend frameworks | ✅ | 🟡 [^c] | ✅ |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 [^d] | ✅ [^e] | ❌ |
| Automated publishing | ✅ | ✅ | ❌ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
| Unlisted HTML Pages | ✅ | ✅ | ✅ |
| Unlisted Scripts | ✅ | ❌ | ❌ |
| ESM Content Scripts | ❌ [^l] | ❌ | ✅ |
+42
View File
@@ -164,6 +164,36 @@ Both issues have the same cause: the library puts something outside the `ShadowR
Both issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.
## My content script UI looks different on certain websites
If your `createShadowRootUi` looks correct on most sites but appears at the wrong size on others (e.g., Reddit), it's because `rem` unit is relative to the `<html>` element's `font-size`, which lives outside the Shadow DOM. When a website overrides it, your UI scales incorrectly.
The fix is to convert `rem` units to `px` at build time using [`postcss-rem-to-responsive-pixel`](https://www.npmjs.com/package/postcss-rem-to-responsive-pixel). This eliminates the dependency on the host page's root font-size.
1. Install the package:
```sh
bun i -D postcss-rem-to-responsive-pixel
```
2. Configure your PostCSS config:
```js [postcss.config.mjs]
import remToPx from 'postcss-rem-to-responsive-pixel';
export default {
plugins: [
remToPx({
rootValue: 16,
propList: ['*'],
transformUnit: 'px',
}),
],
};
```
See [Issue #678](https://github.com/wxt-dev/wxt/issues/678) for additional context on this behavior.
## Does WXT provide docs for LLMs?
Yes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).
@@ -191,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".
+1 -1
View File
@@ -51,7 +51,7 @@ Here's specific steps for other popular frameworks/build tools.
3. Move public `assets/*` into the `public/` directory
4. If you use CSUI, migrate to WXT's `createContentScriptUi`
5. Convert Plasmo's custom import resolutions to Vite's
6. If importing remote code via a URL, add a `url:` prefix so it works with WXT
6. Importing remote code via a URL is not supported in WXT, see [this issue](https://github.com/wxt-dev/wxt/issues/2262) for alternatives
7. Replace your [Plasmo tags](https://docs.plasmo.com/framework/workflows/build#with-a-custom-tag) (`--tag`) with [WXT build modes](/guide/essentials/config/build-mode) (`--mode`)
8. ⚠️ Compare the old production manifest to `.output/*/manifest.json`. They should have the same content as before. If not, tweak your entrypoints and config until they are the same.
+335 -1
View File
@@ -15,7 +15,7 @@ To upgrade WXT to the latest major version:
```
2. Follow the upgrade steps below to fix any breaking changes.
3. Run `wxt prepare`. It should succeed and type errors will go away afterwords.
3. Run `wxt prepare`. It should succeed and type errors will go away afterwards.
```sh
pnpm wxt prepare
@@ -35,6 +35,340 @@ Listed below are all the breaking changes you should address when upgrading to a
Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.
## v0.20.0 &rarr; v0.21.0
This release focuses on trimming WXT's install footprint and simplifying project configuration.
:::tip
Read through all the changes once before updating your code.
:::
### Install Footprint
When installing `wxt` v0.20, it downloaded 98 MB and 366 packages to your `node_modules`. v0.21 cuts this down to 22MB / 156 packages - that's 22.4% the previous size! This is a good improvement, but frankly, not good enough. Why?
- `wxt init` is slow for new developers trying it out
- Less exposure to supply chain attacks
- More compute and storage used by servers hosting the package and clients downloading it degrades hardware and shortens their life-cycle
If you want to explore WXT's dependencies and why it's so large, here's a few links:
- <https://pkg-size.dev/wxt@%5E0.21?no-peers>
- <https://npmgraph.js.org/?q=wxt@^0.21>
- <https://node-modules.dev/grid/depth#install=wxt@^0.21>
We'll continue working on minimizing WXT's install footprint.
### Minimum Node.js, Vite, and TypeScript Versions
| Tool | Old requirement | New requirement |
| ---------- | --------------------------------- | ---------------------------------- |
| Node.js | `>=20.12.0` | `>=22` |
| Vite | `^5.4.19 \|\| ^6.3.4 \|\| ^7.0.0` | `^6.3.4 \|\| ^7.0.0 \|\| ^8.0.0-0` |
| TypeScript | Not enforced | `>=5.4` |
Vite 5 support was dropped as part of moving WXT's dev server off `vite-node` and onto Vite's native `ModuleRunner`/Environment API, which requires Vite &ge;6.3.4.
### `vite`, `web-ext`, and `typescript` Are Now Peer Dependencies
Previously, WXT installed its own copies of `vite` and `web-ext` for you (you never had to add them to your `package.json`). Now, all three are declared as [`peerDependencies`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies) instead of regular dependencies, so **your project controls the exact version that gets used**, and WXT's install size shrinks.
- `vite` is a **required** peer, add it to your `devDependencies`.
- `web-ext` and `typescript` are **optional** peers, only needed if you use the corresponding features (see [below](#opt-into-opening-the-browser-during-dev-mode) for `web-ext`).
```sh
pnpm add -D vite typescript
```
### Opt Into Opening the Browser During Dev Mode
In v0.21, `web-ext` is now an optional peer dependency - if it's installed, the browser will be opened automatically. If not, the feature is disabled.
- **To continue opening the browser automatically**: Install `web-ext` as a dependency
```sh
pnpm add -D web-ext
```
- **If you don't use this feature**: Don't install it and you can delete any `web-ext.config.ts` options and/or remove any `webExt.enabled: false` config
You can still disable this behavior by setting `webExt.enabled: false` in your `wxt.config.ts` file or `web-ext.config.ts` file if the package is present.
> Projects created with `wxt init` will include `web-ext` in their `package.json`, opting new projects into this feature by default.
### `.wxt/tsconfig.json` Compiler Option Changes
The generated `.wxt/tsconfig.json` was updated to match Vite's current recommended TypeScript settings, and now requires **TypeScript &ge;5.4**.
```jsonc
{
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"], // [!code ++]
"target": "ESNext",
"module": "ESNext", // [!code --]
"module": "Preserve", // [!code ++]
"moduleDetection": "force", // [!code ++]
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true, // [!code ++]
"verbatimModuleSyntax": true, // [!code ++]
"noEmit": true,
"esModuleInterop": true, // [!code --]
"forceConsistentCasingInFileNames": true, // [!code --]
"resolveJsonModule": true, // [!code --]
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true, // [!code ++]
"noUncheckedIndexedAccess": true, // [!code ++]
"noImplicitOverride": true, // [!code ++]
},
}
```
If these changes do cause problems, `verbatimModuleSyntax` and `noUncheckedIndexedAccess` are the two most likely culprits. It's recommended you fix the issues and leave the new settings enabled.
However, if you want to revert the config back and upgrade to the recommended options at a later time, you can add the following to your `wxt.config.ts` file:
<details>
```ts
export default defineConfig({
hooks: {
'prepare:tsconfig': (_, { tsconfig }) => {
// Reverts TSConfig back to WXT v0.20
// - https://wxt.dev/guide/resources/upgrading.html#wxttsconfigjson-compiler-option-changes
delete tsconfig.lib;
tsconfig.module = 'ESNext';
delete tsconfig.moduleDetection;
delete tsconfig.allowImportingTsExtensions;
delete tsconfig.verbatimModuleSyntax;
tsconfig.esModuleInterop = true;
tsconfig.forceConsistentCasingInFileNames = true;
tsconfig.resolveJsonModule = true;
delete tsconfig.noFallthroughCasesInSwitch;
delete tsconfig.noUncheckedIndexedAccess;
delete tsconfig.noImplicitOverride;
},
},
});
```
</details>
### `url:` Imports Removed
The `url:` import feature (importing and bundling remote code by URL, e.g. `import 'url:https://code.jquery.com/jquery.js'`) has been removed due to the supply-chain risk of a compromised CDN silently injecting code into your build.
There is no config flag to restore this behavior. If you relied on it:
- **Prefer an NPM package** if one exists for the library you need.
- **Vendor the file**: download it once, commit it to your repo, and import it locally like any other file. This lets you review the code and track changes via version control, instead of trusting a URL to never change.
:::details Why was this removed?
See <https://github.com/wxt-dev/wxt/issues/2262> and <https://github.com/wxt-dev/wxt/issues/2229> for the full discussion. A proposal to restore this behind a mandatory integrity hash is being tracked in [#2514](https://github.com/wxt-dev/wxt/pull/2514); if there's enough demand, it may come back as a separate, opt-in WXT module instead of core.
:::
### Zip Filename Template Changes
The default filename templates for `zip.artifactTemplate` and `zip.sourcesTemplate` changed, and the <span v-pre>`{{version}}`</span> template variable's meaning changed:
- <span v-pre>`{{version}}`</span> now always resolves to `manifest.version` (previously it resolved to `manifest.version_name ?? manifest.version`).
- A new <span v-pre>`{{versionName}}`</span> variable was added with the old <span v-pre>`{{version}}`</span> behavior (`manifest.version_name ?? manifest.version`).
- A new <span v-pre>`{{packageVersion}}`</span> variable was added, resolving to your `package.json`'s version.
- A new <span v-pre>`{{modeSuffix}}`</span> variable was added (`-dev` for development builds, `''` for production).
The defaults themselves changed to use these new variables:
```diff
- artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip'
+ artifactTemplate: '{{name}}-{{packageVersion}}-{{browser}}{{modeSuffix}}.zip'
- sourcesTemplate: '{{name}}-{{version}}-sources.zip'
+ sourcesTemplate: '{{name}}-{{packageVersion}}-sources{{modeSuffix}}.zip'
```
- **If you don't customize `artifactTemplate`/`sourcesTemplate`**: your production zip filenames will not change, but dev and other build modes will be output to separate directories.
> If you previously install the dev version of your extension in your browser, note that the output directory has changed and you'll need to re-install it next time you run the dev command.
- **If you have a custom template**: replace any <span v-pre>`{{version}}`</span> usage with <span v-pre>`{{versionName}}`</span> to keep the old value, or explicitly opt back into the old defaults:
```ts [wxt.config.ts]
export default defineConfig({
zip: {
artifactTemplate: '{{name}}-{{versionName}}-{{browser}}.zip',
sourcesTemplate: '{{name}}-{{versionName}}-sources.zip',
},
});
```
For more info on the difference between `manifest.version` and `manifest.version_name`, see the [Manifest Config docs](/guide/essentials/config/manifest#version-and-version-name).
### Sources ZIP: `includeSources`/`excludeSources` Now Behave Like a Standard Allowlist
Previously, `includeSources`/`excludeSources` behaved counterintuitively: `all files - excludeSources + includedSources = zipped files`. This is not the standard behavior for allowlist/blocklists.
As of v0.21, these options behave as expected: `includedSources - excludeSources = zipped files`.
Additionally, a new `zip.dotSources` option (default `false`) was added to control whether hidden files/directories can be included in the zip.
This will likely be the biggest pain point of the upgrade. You will need to rebuild your included and excluded sources lists from scratch. Luckily, it's simpler than before!
1. Do you need any hidden files? If so, set `zip.dotSources` to `true`
1. Add base files and directories that need to be in the ZIP to `zip.includeSources`
1. Run `wxt zip -b firefox` and confirm all the files you need are included
1. Remove files from the zip by adding them to `zip.excludeSources`
1. Run `wxt zip -b firefox` and confirm only the files you need are in the zip
:::tip
`wxt zip -b firefox` now prints every file included in the sources ZIP. This will help you identify missing or unwanted files more quickly.
:::
Here's an example configuration:
```ts
// wxt.config.ts
export default defineConfig({
srcDir: 'src',
zip: {
// Say I have a .tool-versions file I want to include
dotSources: true,
// Specify base folders and files to include
includeSources: [
'src',
'public',
'package.json',
'bun.lock',
'README.md',
'.tool-versions',
],
// Exclude any folders inside the base folders that are not needed, like test data
excludeSources: ['**/__fixtures__'],
},
});
```
### `createShadowRootUi` DOM Changes
`@webext-core/isolated-element` was upgraded from v1 to v3. The only breaking change relevant to WXT projects is that the internal structure of the shadow root's is simpler:
:::code-group
```html [Before]
<html>
<head>
<style>
...
</style>
</head>
<body>
...your app
</body>
</html>
```
```html [After]
<style>
...
</style>
<div>...your app</div>
```
:::
Historically, CSS frameworks haven't had good support for shadow roots, so we needed a full `<html>` structure for styles to be applied correctly. But that has change this past few years, frameworks have started to include the `host:` selector alongside `root:` (which is required for base styles to be applied to the shadow root's host element):
```css
:root { /* [!code --] */
:root, :host { /* [!code ++] */
/* base styles... */
}
```
If you use `createShadowRootUi`, see if your UI looks the same after this update without any changes.
If the styles are broken, you can continue using the full `<html>` structure by installing the v1 of `@webext-core/isolated-element` - WXT will respect whatever version of the package is listed in your `package.json`.
```sh
pnpm add @webext-core/isolated-element@^1
```
In the next major version, support for `@webext-core/isolated-element` v1 will be dropped, so you have some time to migrate.
### Content & Unlisted Script `globalName` Now Defaults to `false`
Content scripts and unlisted scripts are built as an IIFE. Previously, WXT generated a global variable to hold the IIFE's return value for all scripts by default. Now, no global variable is generated by default, producing a smaller, anonymous IIFE that prevents variable collisions with the page.
If you rely on the return value of your script, for example when injecting it with `browser.scripting.executeScript` and reading `InjectionResult.result`, set `globalName: true` to restore the old behavior:
```ts
export default defineUnlistedScript({
globalName: true, // [!code ++]
main() {
return 'some value';
},
});
```
This applies per-entrypoint, to both `defineContentScript` and `defineUnlistedScript`.
### ESLint Auto-Import Config Now Detects Your Installed ESLint Version
Setting `imports.eslintrc.enabled: true` used to always assume you wanted the legacy ESLint 8 config format, regardless of which ESLint version you actually had installed.
Now, `true` behaves the same as `'auto'`: WXT detects your installed ESLint version and generates the matching file automatically (ESLint 9+ &rarr; `eslint-auto-imports.mjs`, ESLint &le;8 &rarr; `eslintrc-auto-import.json`).
- If you're on ESLint 9+ but never explicitly set `enabled: 9`, you'll now get the new flat-config file instead of the legacy one.
- To force the old ESLint 8 format regardless of your installed version, set `enabled: 8` explicitly.
See the [ESLint config docs](/guide/essentials/config/auto-imports#eslint) for the full set of options and how to wire up the generated file.
### Deprecated APIs Removed From v0.21
APIs deprecated in v0.20 have been removed:
- `wxt.config.runner` / `ExtensionRunnerConfig` / `defineRunnerConfig` &rarr; use `wxt.config.webExt` / `defineWebExtConfig` instead.
- `dev.server.hostname` &rarr; use `dev.server.host` instead.
- The `wxt/testing` barrel export &rarr; import from the specific submodule instead:
```ts
import { fakeBrowser, WxtVitest } from 'wxt/testing'; // [!code --]
import { fakeBrowser } from 'wxt/testing/fake-browser'; // [!code ++]
import { WxtVitest } from 'wxt/testing/vitest-plugin'; // [!code ++]
```
- The `clean(root: string)` JS API overload &rarr; Pass a root like this instead `clean({ root: './my-extension' })`
### CWS v2 API Support
`publish-browser-extension` was upgraded to v5, which adds support for the new Chrome Web Store v2 API to `wxt submit`! Instead of using a refresh token for auth, it uses a service account, which is much easier to setup and never expires. It also supports a few other features: <https://developer.chrome.com/blog/cws-api-v2>
To setup your project to use v2, run `wxt submit init` and go through the setup process for the CWS, selecting v2 when prompted.
v1 will stop working **15th October 2026**, so you have a few months to migrate.
### `@webext-core/fake-browser` v2
If you use `wxt/testing/fake-browser` (or `@webext-core/fake-browser` directly) in unit tests, note that its types now match `@wxt-dev/browser` instead of `webextension-polyfill`. This is the same change WXT's own `browser` went through in [v0.20](#webextension-polyfill-removed).
Most notably, **mocking `browser.runtime.onMessage` listeners that return a promise no longer works**, use `sendResponse` instead:
```ts
fakeBrowser.runtime.onMessage.addListener(async () => { // [!code --]
return await someAsyncWork(); // [!code --]
fakeBrowser.runtime.onMessage.addListener((_message, _sender, sendResponse) => { // [!code ++]
someAsyncWork().then(sendResponse); // [!code ++]
return true; // [!code ++]
});
```
If you use use a messaging library, it will likely continue working as-is.
### New Deprecations in v0.20
Deprecated APIs will be removed in the next major release.
- `useAppConfig` deprecated in favor of `getAppConfig` - same function, just renamed. This avoids react linters mistaking it as a hook.
## v0.19.0 &rarr; v0.20.0
v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.
-5
View File
@@ -66,11 +66,6 @@ features:
- icon: 📏
title: Bundle Analysis
details: Tools for analyzing the final extension bundle and minimizing your extension's size.
- icon: ⬇️
title: Bundle Remote Code
details: Downloads and bundles remote code imported from URLs.
link: /guide/essentials/remote-code
linkText: Read docs
---
## Sponsors
+5 -1
View File
@@ -38,6 +38,10 @@
# 0.19.0
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
# 0.21.0
# The `url:` import feature was removed, along with its docs page.
/guide/essentials/remote-code.html /guide/resources/upgrading.html#url-imports-removed
# https://github.com/wxt-dev/wxt/issues/704
# Generated via `pnpm docs:build && find docs/.vitepress/dist -type f -name "*.html"`
@@ -59,7 +63,7 @@
/guide/go-further/handling-updates.html /guide/essentials/testing-updates.html
/guide/go-further/custom-events.html /guide/essentials/content-scripts.html#dealing-with-spas
/guide/go-further/debugging.html /TODO
/guide/go-further/remote-code.html /guide/essentials/remote-code.html
/guide/go-further/remote-code.html /guide/resources/upgrading.html#url-imports-removed
/guide/go-further/vite.html /guide/essentials/config/vite.html
/guide/go-further/testing.html /guide/essentials/unit-testing.html
/guide/go-further/how-wxt-works.html /guide/resources/how-wxt-works.html
+1
View File
@@ -0,0 +1 @@
<svg width="114" height="50" viewBox="0 0 114 50" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path fill="#060B10" d="M0 0h114v51H0z"/><g clip-path="url(#b)" fill="#fff"><path d="M44.445 40.302v-7.695l.153-.16h1.853l.154.16v7.695l-.154.16h-1.853l-.153-.16ZM44.445 17.855v-7.696l.153-.159h1.853l.154.16v7.695l-.154.16h-1.853l-.153-.16ZM38.068 34.892h-.254l-1.275-1.323v-.264l2.388-2.482 1.349.002.18.185v1.4l-2.388 2.482ZM36.54 17.227v-.267l1.274-1.32h.254l2.388 2.479v1.398l-.18.19h-1.349l-2.388-2.48ZM28.154 24.107h10.5l.153.16v1.926l-.153.16h-10.5l-.154-.16v-1.927l.154-.16ZM99.844 24.11l.154.159v1.924l-.154.16H89.19l-.153-.16.77-1.924.154-.16h9.883ZM49 29.24h-1.853l-.153-.16v-4.502c0-.802-.303-1.423-1.234-1.443a24.2 24.2 0 0 0-1.613.025l-.089.092v5.826l-.153.16h-1.853l-.154-.16v-7.695l.154-.16h4.169c1.62 0 2.933 1.364 2.933 3.046v4.81l-.154.159v.002ZM57.974 25.871l-.154.16h-4.79l-.154.16c0 .32.31 1.283 1.546 1.283.463 0 .926-.16 1.082-.481l.154-.16h1.852l.154.16c-.154.962-.926 2.407-3.245 2.407-2.625 0-3.861-1.924-3.861-4.172 0-2.248 1.236-4.171 3.708-4.171 2.472 0 3.708 1.924 3.708 4.171v.643Zm-2.319-1.605c0-.16-.153-1.283-1.39-1.283-1.235 0-1.389 1.124-1.389 1.283l.154.16h2.472l.153-.16ZM62.298 26.833c0 .322.154.481.464.481h1.39l.153.16v1.604l-.154.16h-1.39c-1.389 0-2.625-.643-2.625-2.407v-3.529l-.154-.16H58.9l-.154-.159v-1.605l.154-.16h1.082l.154-.159v-1.443l.154-.16h1.852l.154.16v1.443l.154.16h1.699l.153.16v1.604l-.153.16h-1.7l-.153.159v3.529l.002.002ZM68.015 29.24h-1.853l-.153-.16V18.172l.153-.16h1.853l.154.16v10.907l-.154.16v.002ZM72.186 19.935h-1.852l-.154-.16v-1.604l.154-.16h1.852l.154.16v1.605l-.154.16Zm0 9.305h-1.852l-.154-.16v-7.7l.154-.159h1.852l.154.16v7.7l-.154.16ZM79.449 18.171v1.605l-.154.16h-1.39c-.309 0-.463.159-.463.48v.643l.154.16h1.546l.153.16v1.604l-.153.16h-1.546l-.154.159v5.774l-.153.16h-1.853l-.154-.16v-5.774l-.153-.16h-1.083l-.153-.159v-1.605l.153-.16h1.083l.153-.159v-.643c0-1.764 1.236-2.407 2.626-2.407h1.39l.153.16-.002.002ZM85.163 29.4c-.619 1.605-1.236 2.567-3.398 2.567h-.773l-.154-.16v-1.605l.154-.16h.773c.773 0 .926-.159 1.082-.64v-.16l-2.472-6.257v-1.604l.154-.16h1.39l.153.16 1.853 5.455h.153l1.853-5.455.154-.16h1.39l.153.16v1.604l-2.472 6.417.007-.002Z"/></g><path fill="#fff" d="M0 0v50h15V0z"/><path d="M6.518 45.367v-.774H11v.774H6.518Zm0-1.746c0-.44.098-.784.294-1.032.196-.248.474-.372.834-.372.348 0 .622.13.822.39.2.26.3.628.3 1.104l-.336.018c.004-.608.114-1.062.33-1.362.212-.3.524-.45.936-.45.424 0 .748.146.972.438.22.288.33.686.33 1.194v1.242h-.738v-1.152c0-.268-.05-.488-.15-.66a.498.498 0 0 0-.462-.258c-.224 0-.39.088-.498.264-.108.172-.162.39-.162.654v1.146h-.738V43.68c0-.192-.04-.36-.12-.504a.426.426 0 0 0-.396-.216c-.192 0-.33.068-.414.204a.955.955 0 0 0-.126.51v1.116h-.678v-1.17Zm0-2.925v-.774H9.17c.256 0 .472-.04.648-.12a.88.88 0 0 0 .39-.336.926.926 0 0 0 .126-.486.926.926 0 0 0-.126-.486.845.845 0 0 0-.39-.33 1.497 1.497 0 0 0-.648-.126H6.518v-.75h2.664c.604 0 1.068.146 1.392.438.324.288.486.71.486 1.266 0 .56-.162.984-.486 1.272-.324.288-.788.432-1.392.432H6.518Zm0-5.048v-.774H11v.774H6.518Zm0-2.475V32.4H11v.774H6.518Zm3.75-.546v-2.412H11v2.412h-.732Zm-3.69-3.903v-.773H11v.773H6.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-6.98v-.827l3.684-1.056v.318L6.518 20.43v-.888l3.684-1.122v.318L6.518 17.64v-.798L11 18.18v.81l-4.086 1.254v-.438L11 21.097v.756l-4.482 1.302Zm0-7.385v-.774H11v.774H6.518Zm.06-3.283v-.774H11v.774H6.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-4.7v-.773H11v.774H6.518Zm1.572-.545V6.179h.732V8.65H8.09ZM6.518 6.407v-.774H11v.774H6.518Z" fill="#060B10"/></g><rect x="1" y="1" width="112" height="48" rx="3" stroke="#060B10" stroke-width="2"/><defs><clipPath id="a"><rect width="114" height="50" rx="4" fill="#fff"/></clipPath><clipPath id="b"><path fill="#fff" d="M28 10h72v30.462H28z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="114" height="50" viewBox="0 0 114 50" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><rect width="114" height="50" rx="4" fill="#181A1C"/><path fill="#fff" d="M0 0h114v51H0z"/><g clip-path="url(#b)" fill="#181A1C"><path d="M44.445 40.3v-7.694l.153-.16h1.853l.154.16V40.3l-.154.16h-1.853l-.153-.16ZM44.445 17.854V10.16l.153-.16h1.853l.154.16v7.694l-.154.16h-1.853l-.153-.16ZM38.068 34.89h-.254l-1.275-1.322v-.264l2.388-2.482 1.349.002.18.184v1.4l-2.388 2.483ZM36.54 17.226v-.266l1.274-1.321h.254l2.388 2.48v1.397l-.18.19h-1.349l-2.388-2.48ZM28.154 24.106h10.5l.153.16v1.926l-.153.16h-10.5l-.154-.16v-1.926l.154-.16ZM99.844 24.109l.154.16v1.923l-.154.16H89.19l-.153-.16.77-1.924.154-.16h9.883ZM49 29.24h-1.853l-.153-.16v-4.503c0-.802-.303-1.423-1.234-1.443-.48-.012-1.027 0-1.613.025l-.089.093v5.825l-.153.16h-1.853l-.154-.16v-7.694l.154-.16h4.169c1.62 0 2.933 1.363 2.933 3.045v4.81l-.154.159v.002ZM57.974 25.87l-.154.16h-4.79l-.154.16c0 .32.31 1.283 1.546 1.283.463 0 .926-.16 1.082-.481l.154-.16h1.852l.154.16c-.154.962-.926 2.407-3.245 2.407-2.625 0-3.861-1.924-3.861-4.172 0-2.247 1.236-4.17 3.708-4.17 2.472 0 3.708 1.923 3.708 4.17v.643Zm-2.319-1.604c0-.16-.153-1.284-1.39-1.284-1.235 0-1.389 1.124-1.389 1.284l.154.16h2.472l.153-.16ZM62.298 26.832c0 .322.154.481.464.481h1.39l.153.16v1.604l-.154.16h-1.39c-1.389 0-2.625-.643-2.625-2.407V23.3l-.154-.16H58.9l-.154-.159v-1.604l.154-.16h1.082l.154-.16v-1.442l.154-.16h1.852l.154.16v1.443l.154.16h1.699l.153.159v1.604l-.153.16h-1.7l-.153.16v3.528l.002.002ZM68.015 29.24h-1.853l-.153-.16V18.17l.153-.159h1.853l.154.16v10.906l-.154.16v.002ZM72.186 19.935h-1.852l-.154-.16v-1.604l.154-.16h1.852l.154.16v1.604l-.154.16Zm0 9.304h-1.852l-.154-.16V21.38l.154-.16h1.852l.154.16v7.7l-.154.16ZM79.449 18.17v1.605l-.154.16h-1.39c-.309 0-.463.16-.463.48v.644l.154.16h1.546l.153.159v1.604l-.153.16h-1.546l-.154.16v5.773l-.153.16h-1.853l-.154-.16V23.3l-.153-.16h-1.083l-.153-.159v-1.604l.153-.16h1.083l.153-.16v-.642c0-1.764 1.236-2.407 2.626-2.407h1.39l.153.16-.002.002ZM85.163 29.399c-.619 1.604-1.236 2.566-3.398 2.566h-.773l-.154-.16v-1.604l.154-.16h.773c.773 0 .926-.159 1.082-.64v-.16l-2.472-6.256V21.38l.154-.16h1.39l.153.16 1.853 5.455h.153l1.853-5.455.154-.16h1.39l.153.16v1.605L85.156 29.4l.007-.002Z"/></g><path fill="#181A1C" d="M0 0h15v50H0z"/><path d="M5.518 45.367v-.774H10v.774H5.518Zm0-1.746c0-.44.098-.784.294-1.032.196-.248.474-.372.834-.372.348 0 .622.13.822.39.2.26.3.628.3 1.104l-.336.018c.004-.608.114-1.062.33-1.362.212-.3.524-.45.936-.45.424 0 .748.146.972.438.22.288.33.686.33 1.194v1.242h-.738v-1.152c0-.268-.05-.488-.15-.66a.498.498 0 0 0-.462-.258c-.224 0-.39.088-.498.264-.108.172-.162.39-.162.654v1.146h-.738V43.68c0-.192-.04-.36-.12-.504a.426.426 0 0 0-.396-.216c-.192 0-.33.068-.414.204a.955.955 0 0 0-.126.51v1.116h-.678v-1.17Zm0-2.925v-.774H8.17c.256 0 .472-.04.648-.12a.88.88 0 0 0 .39-.336.926.926 0 0 0 .126-.486.926.926 0 0 0-.126-.486.845.845 0 0 0-.39-.33 1.497 1.497 0 0 0-.648-.126H5.518v-.75h2.664c.604 0 1.068.146 1.392.438.324.288.486.71.486 1.266 0 .56-.162.984-.486 1.272-.324.288-.788.432-1.392.432H5.518Zm0-5.048v-.774H10v.774H5.518Zm0-2.475V32.4H10v.774H5.518Zm3.75-.546v-2.412H10v2.412h-.732Zm-3.69-3.903v-.773H10v.773H5.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-6.98v-.827l3.684-1.056v.318L5.518 20.43v-.888l3.684-1.122v.318L5.518 17.64v-.798L10 18.18v.81l-4.086 1.254v-.438L10 21.097v.756l-4.482 1.302Zm0-7.385v-.774H10v.774H5.518Zm.06-3.283v-.774H10v.774H5.578Zm-.06 1.41v-3.6h.732v3.6h-.732Zm0-4.7v-.773H10v.774H5.518Zm1.572-.545V6.179h.732V8.65H7.09ZM5.518 6.407v-.774H10v.774H5.518Z" fill="#fff"/></g><rect x="1" y="1" width="112" height="48" rx="3" stroke="#181A1C" stroke-width="2"/><defs><clipPath id="a"><rect width="114" height="50" rx="4" fill="#fff"/></clipPath><clipPath id="b"><path fill="#fff" d="M28 10h72v30.46H28z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+10 -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,22 @@ 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
# Unset CI variables inherited from the GitHub Actions runner so consola
# picks its fancy reporter instead of the basic one (#2461)
Type 'unset CI GITHUB_ACTIONS' Enter
Type '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 +58,6 @@ Sleep 5s
Hide
# Delete the temporary folder
Type 'rm -rf "$vhs_sandbox"' Enter
Type 'cd .."' Enter
Type 'rm -rf init-demo' Enter
Show
+9 -9
View File
@@ -21,7 +21,7 @@
"@aklinker1/buildc": "^1.1.7",
"@aklinker1/check": "^2.4.0",
"@types/bun": "^1.3.5",
"@types/node": "^20.17.6",
"@types/node": "^22",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@webext-core/fake-browser": "^1.3.4",
@@ -31,31 +31,31 @@
"tsdown": "^0.21.0",
"typescript": "^6.0.3",
"unocss": "^66.6.8",
"vite": "^7.3.1",
"vitest": "^4.1.5",
"vitest-plugin-random-seed": "^1.1.2"
},
"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.5",
"changelogen": "^0.6.2",
"@vitest/coverage-v8": "^4.1.6",
"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.15",
"tinyglobby": "^0.2.16",
"tsdown": "catalog:",
"tsx": "4.21.0",
"typedoc": "^0.25.4",
+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)
+66
View File
@@ -5,6 +5,8 @@ Report analytics events from your web extension extension.
## Supported Analytics Providers
- [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol)
- [Moderok](#moderok)
- [PostHog](#posthog)
- [Umami](#umami)
## Install With WXT
@@ -120,6 +122,70 @@ export default defineAppConfig({
});
```
### Moderok
[Moderok](https://moderok.dev) is an analytics platform built specifically for browser extensions. It requires no `host_permissions`, works in Manifest V3 service workers, and collects only anonymous usage data.
Sign up at [moderok.dev](https://moderok.dev) to get your app key, then save it to your `.env` file:
```dotenv
WXT_MODEROK_APP_KEY='mk_...'
```
Then add the `moderok` provider to your `<srcDir>/app.config.ts` file:
```ts
import { moderok } from '@wxt-dev/analytics/providers/moderok';
export default defineAppConfig({
analytics: {
providers: [
moderok({
appKey: import.meta.env.WXT_MODEROK_APP_KEY,
// Automatically track first open, install, update, and daily ping events (default: true)
trackLifecycle: true,
// Track when users uninstall the extension (default: false)
trackUninstalls: false,
// Optional: when trackUninstalls is on, redirect users to this page
// after they uninstall (e.g. a feedback survey)
uninstallUrl: 'https://example.com/uninstall',
}),
],
},
});
```
For a full walkthrough — module setup, sending events, and all provider options — see the [Moderok WXT guide](https://docs.moderok.dev/guide/wxt).
### PostHog
[PostHog](https://posthog.com/) is an open source product analytics platform. It supports event tracking, session recording, feature flags, surveys, and more.
In your PostHog project settings, find your **Project API key** and save it to your `.env` file:
```dotenv
WXT_POSTHOG_API_KEY='phc_...'
```
Then add the `posthog` provider to your `<srcDir>/app.config.ts` file:
```ts
import { posthog } from '@wxt-dev/analytics/providers/posthog';
export default defineAppConfig({
analytics: {
providers: [
posthog({
apiKey: import.meta.env.WXT_POSTHOG_API_KEY,
// apiHost defaults to 'https://us.i.posthog.com'.
// Change to 'https://eu.i.posthog.com' for EU Cloud, or your self-hosted URL.
apiHost: 'https://eu.i.posthog.com',
}),
],
},
});
```
### Umami
[Umami](https://umami.is/) is a privacy-first, open source analytics platform.
+4
View File
@@ -1,5 +1,6 @@
import { defineAppConfig } from 'wxt/utils/define-app-config';
import { googleAnalytics4 } from './modules/analytics/providers/google-analytics-4';
import { posthog } from './modules/analytics/providers/posthog';
import { umami } from './modules/analytics/providers/umami';
export default defineAppConfig({
@@ -10,6 +11,9 @@ export default defineAppConfig({
apiSecret: '...',
measurementId: '...',
}),
posthog({
apiKey: '...',
}),
umami({
apiUrl: 'https://umami.aklinker1.io/api',
domain: 'analytics.wxt.dev',
@@ -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 = {
@@ -18,7 +18,7 @@ export const googleAnalytics4 =
eventProperties: Record<string, string | undefined> | undefined,
): Promise<void> => {
const url = new URL(
config?.debug ? '/debug/mp/collect' : '/mp/collect',
config.debug ? '/debug/mp/collect' : '/mp/collect',
options.apiUrl ?? 'https://www.google-analytics.com',
);
if (options.apiSecret)
@@ -38,6 +38,12 @@ export const googleAnalytics4 =
]),
);
if (config.debug) {
console.debug(
'[@wxt-dev/analytics] Sending event to Google Analytics 4:',
{ eventName, eventProperties },
);
}
await fetch(url.href, {
method: 'POST',
body: JSON.stringify({
@@ -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,
);
},
};
},
);
@@ -0,0 +1,83 @@
import { defineAnalyticsProvider } from '../client';
export interface PostHogProviderOptions {
/** Your PostHog project API key. */
apiKey: string;
/**
* PostHog API host URL.
*
* @default 'https://us.i.posthog.com'
*/
apiHost?: string;
}
export const posthog = defineAnalyticsProvider<PostHogProviderOptions>(
(_, config, options) => {
const apiHost = (options.apiHost ?? 'https://us.i.posthog.com').replace(
/\/$/,
'',
);
const capture = async (
distinctId: string,
event: string,
properties: Record<string, unknown>,
): Promise<void> => {
if (config.debug) {
console.debug('[@wxt-dev/analytics] Sending event to PostHog:', {
event,
properties,
});
}
const body: PostHogCaptureBody = {
api_key: options.apiKey,
distinct_id: distinctId,
event,
properties,
timestamp: new Date().toISOString(),
};
await fetch(`${apiHost}/i/v0/e/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
};
return {
identify: async (event) => {
await capture(event.user.id, '$identify', {
$set: event.user.properties,
});
},
page: async (event) => {
await capture(event.user.id, '$pageview', {
$current_url: event.page.url,
$title: event.page.title,
$session_id: event.meta.sessionId,
$screen: event.meta.screen,
$language: event.meta.language,
$referrer: event.meta.referrer,
$set: event.user.properties,
});
},
track: async (event) => {
await capture(event.user.id, event.event.name, {
...event.event.properties,
$screen: event.meta.screen,
$language: event.meta.language,
$referrer: event.meta.referrer,
$set: event.user.properties,
});
},
};
},
);
/** @see https://posthog.com/docs/api/capture */
interface PostHogCaptureBody {
api_key: string;
distinct_id: string;
event: string;
properties: Record<string, unknown>;
timestamp: string;
}
@@ -1,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 {
+9 -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,9 +55,17 @@
"types": "./dist/providers/google-analytics-4.d.mts",
"default": "./dist/providers/google-analytics-4.mjs"
},
"./providers/moderok": {
"types": "./dist/providers/moderok.d.mts",
"default": "./dist/providers/moderok.mjs"
},
"./providers/umami": {
"types": "./dist/providers/umami.d.mts",
"default": "./dist/providers/umami.mjs"
},
"./providers/posthog": {
"types": "./dist/providers/posthog.d.mts",
"default": "./dist/providers/posthog.mjs"
}
},
"module": "./dist/index.mjs",
+2
View File
@@ -9,6 +9,8 @@ export default defineConfig({
'providers/google-analytics-4':
'./modules/analytics/providers/google-analytics-4.ts',
'providers/umami': './modules/analytics/providers/umami.ts',
'providers/moderok': './modules/analytics/providers/moderok.ts',
'providers/posthog': './modules/analytics/providers/posthog.ts',
},
deps: {
neverBundle: ['#analytics'],
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/browser",
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
"version": "0.1.42",
"version": "0.2.2",
"type": "module",
"main": "src/index.mjs",
"types": "src/index.d.ts",
@@ -25,7 +25,7 @@
"src"
],
"devDependencies": {
"@types/chrome": "0.1.42",
"@types/chrome": "0.2.2",
"@types/node": "catalog:",
"nano-spawn": "catalog:",
"typescript": "catalog:",
+74 -59
View File
@@ -383,22 +383,53 @@ export namespace Browser {
* Permissions: "alarms"
*/
export namespace alarms {
interface AlarmCreateInfo {
/** Length of time in minutes after which the {@link onAlarm} event should fire. */
delayInMinutes?: number | undefined;
/** If set, the onAlarm event should fire every `periodInMinutes` minutes after the initial event specified by `when` or `delayInMinutes`. If not set, the alarm will only fire once. */
periodInMinutes?: number | undefined;
/** Time at which the alarm should fire, in milliseconds past the epoch (e.g. `Date.now() + n`). */
when?: number | undefined;
}
type AlarmCreateInfo =
& {
/**
* Whether the alarm should persist across sessions (browser restarts). In Chrome, this defaults to true to match historical behavior, but you should set this explicitly to maximize compatibility across browsers.
* @since Chrome 150
*/
persistAcrossSessions?: boolean | undefined;
}
& (
| {
/** Length of time in minutes after which the {@link onAlarm} event should fire. */
delayInMinutes: number;
/** If set, the onAlarm event should fire every `periodInMinutes` minutes after the initial event specified by `when` or `delayInMinutes`. If not set, the alarm will only fire once. */
periodInMinutes?: number | undefined;
/** Time at which the alarm should fire, in milliseconds past the epoch (e.g. `Date.now() + n`). */
when?: never | undefined;
}
| {
/** Length of time in minutes after which the {@link onAlarm} event should fire. */
delayInMinutes?: number | undefined;
/** If set, the onAlarm event should fire every `periodInMinutes` minutes after the initial event specified by `when` or `delayInMinutes`. If not set, the alarm will only fire once. */
periodInMinutes: number;
/** Time at which the alarm should fire, in milliseconds past the epoch (e.g. `Date.now() + n`). */
when?: number | undefined;
}
| {
/** Length of time in minutes after which the {@link onAlarm} event should fire. */
delayInMinutes?: never | undefined;
/** If set, the onAlarm event should fire every `periodInMinutes` minutes after the initial event specified by `when` or `delayInMinutes`. If not set, the alarm will only fire once. */
periodInMinutes?: number | undefined;
/** Time at which the alarm should fire, in milliseconds past the epoch (e.g. `Date.now() + n`). */
when: number;
}
);
interface Alarm {
/** If not null, the alarm is a repeating alarm and will fire again in `periodInMinutes` minutes. */
periodInMinutes?: number;
/** Time at which this alarm was scheduled to fire, in milliseconds past the epoch (e.g. `Date.now() + n`). For performance reasons, the alarm may have been delayed an arbitrary amount beyond this. */
scheduledTime: number;
/** Name of this alarm. */
name: string;
/** If not null, the alarm is a repeating alarm and will fire again in `periodInMinutes` minutes. */
periodInMinutes?: number;
/**
* Whether the alarm should persist across sessions (browser restarts).
* @since Chrome 150
*/
persistAcrossSessions: boolean;
/** Time at which this alarm was scheduled to fire, in milliseconds past the epoch (e.g. `Date.now() + n`). For performance reasons, the alarm may have been delayed an arbitrary amount beyond this. */
scheduledTime: number;
}
/**
@@ -1727,7 +1758,7 @@ export namespace Browser {
*
* `allow`: Allow sites to use advanced clipboard capabilities,
*
* `block`: Don't allow sites to use advanced clipboard capabilties,
* `block`: Don't allow sites to use advanced clipboard capabilities,
*
* `ask`: Ask when a site wants to use advanced clipboard capabilities.
*
@@ -1878,6 +1909,8 @@ export namespace Browser {
BROWSER_ACTION = "browser_action",
PAGE_ACTION = "page_action",
ACTION = "action",
/** @since Chrome 149 */
TAB = "tab",
}
/**
@@ -2248,7 +2281,7 @@ export namespace Browser {
*
* Permissions: "debugger"
*/
export namespace _debugger {
namespace _debugger {
/** Debuggee identifier. Either tabId, extensionId or targetId must be specified */
interface Debuggee {
/** The id of the tab which you intend to debug. */
@@ -2587,7 +2620,7 @@ export namespace Browser {
interface EditResponseCookie {
/** Filter for cookies that will be modified. All empty entries are ignored. */
filter: ResponseCookie;
/** Attributes that shall be overridden in cookies that machted the filter. Attributes that are set to an empty string are removed. */
/** Attributes that shall be overridden in cookies that matched the filter. Attributes that are set to an empty string are removed. */
modification: ResponseCookie;
}
@@ -2605,7 +2638,7 @@ export namespace Browser {
interface EditRequestCookie {
/** Filter for cookies that will be modified. All empty entries are ignored. */
filter: RequestCookie;
/** Attributes that shall be overridden in cookies that machted the filter. Attributes that are set to an empty string are removed. */
/** Attributes that shall be overridden in cookies that matched the filter. Attributes that are set to an empty string are removed. */
modification: RequestCookie;
}
@@ -4547,7 +4580,7 @@ export namespace Browser {
const inIncognitoContext: boolean;
/**
* Set for the lifetime of a callback if an ansychronous extension api has resulted in an error. If no error has occurred lastError will be `undefined`.
* Set for the lifetime of a callback if an asynchronous extension api has resulted in an error. If no error has occurred lastError will be `undefined`.
* @deprecated since Chrome 58. Please use {@link runtime.lastError}
*/
const lastError: runtime.LastError | undefined;
@@ -7604,7 +7637,7 @@ export namespace Browser {
* Creates a new offscreen document for the extension.
* @param parameters The parameters describing the offscreen document to create.
*
* Can return its result via Promise in Manifest V3.
* Can return its result via Promise.
*/
function createDocument(parameters: CreateParameters): Promise<void>;
function createDocument(parameters: CreateParameters, callback: () => void): void;
@@ -7612,7 +7645,7 @@ export namespace Browser {
/**
* Closes the currently-open offscreen document for the extension.
*
* Can return its result via Promise in Manifest V3.
* Can return its result via Promise.
*/
function closeDocument(): Promise<void>;
function closeDocument(callback: () => void): void;
@@ -7620,7 +7653,8 @@ export namespace Browser {
/**
* Determines whether the extension has an active document.
*
* Can return its result via Promise in Manifest V3.
* Can return its result via Promise.
* @since Chrome 150
*/
function hasDocument(): Promise<boolean>;
function hasDocument(callback: (result: boolean) => void): void;
@@ -8914,6 +8948,7 @@ export namespace Browser {
/**
* The native client architecture. This may be different from arch on some platforms.
* @since Chrome 44
* @deprecated since Chrome 149. This enum is deprecated following complete removal of Native Client.
*/
enum PlatformNaclArch {
/** Specifies the native client architecture as arm. */
@@ -9074,7 +9109,10 @@ export namespace Browser {
os: `${PlatformOs}`;
/** The machine's processor architecture. */
arch: `${PlatformArch}`;
/** The native client architecture. This may be different from arch on some platforms. */
/**
* The native client architecture. This may be different from arch on some platforms.
* @deprecated since Chrome 149. This attribute is deprecated following complete removal of Native Client.
*/
nacl_arch?: `${PlatformNaclArch}`;
}
@@ -9178,6 +9216,7 @@ export namespace Browser {
| "identity"
| "identity.email"
| "idle"
| "input"
| "loginState"
| "management"
| "nativeMessaging"
@@ -9219,11 +9258,6 @@ export namespace Browser {
| "webRequestBlocking"
| "webRequestAuthProvider";
/**
* @deprecated Use `ManifestPermission` instead.
*/
type ManifestPermissions = ManifestPermission;
/** Source : https://developer.chrome.com/docs/extensions/reference/api/permissions */
type ManifestOptionalPermission = Exclude<
ManifestPermission,
@@ -9241,11 +9275,6 @@ export namespace Browser {
| "webAuthenticationProxy"
>;
/**
* @deprecated Use `ManifestOptionalPermission` instead.
*/
type ManifestOptionalPermissions = ManifestOptionalPermission;
interface SearchProvider {
name?: string | undefined;
keyword?: string | undefined;
@@ -10114,12 +10143,7 @@ export namespace Browser {
* Permissions: "storage"
*/
export namespace storage {
/** NoInfer for old TypeScript versions (Required TS 5.4+) */
type NoInferX<T> = T[][T extends any ? 0 : never];
// The next line prevents things without the export keyword from being automatically exported (like NoInferX)
export {};
export interface StorageArea {
interface StorageArea {
/**
* Gets the amount of space (in bytes) being used by one or more items.
* @param keys A single key or list of keys to get the total usage for. An empty list will return 0. Pass in `null` to get the total usage of all of storage.
@@ -10166,11 +10190,11 @@ export namespace Browser {
* Can return its result via Promise in Manifest V3 or later since Chrome 95.
*/
get<T = { [key: string]: unknown }>(
keys?: NoInferX<keyof T> | Array<NoInferX<keyof T>> | Partial<NoInferX<T>> | null,
keys?: NoInfer<keyof T> | Array<NoInfer<keyof T>> | Partial<NoInfer<T>> | null | undefined,
): Promise<T>;
get<T = { [key: string]: unknown }>(callback: (items: T) => void): void;
get<T = { [key: string]: unknown }>(
keys: NoInferX<keyof T> | Array<NoInferX<keyof T>> | Partial<NoInferX<T>> | null | undefined,
keys: NoInfer<keyof T> | Array<NoInfer<keyof T>> | Partial<NoInfer<T>> | null | undefined,
callback: (items: T) => void,
): void;
@@ -10197,19 +10221,19 @@ export namespace Browser {
getKeys(callback: (keys: string[]) => void): void;
}
export interface StorageChange {
interface StorageChange {
/** The new value of the item, if there is a new value. */
newValue?: unknown;
/** The old value of the item, if there was an old value. */
oldValue?: unknown;
}
export interface LocalStorageArea extends StorageArea {
interface LocalStorageArea extends StorageArea {
/** The maximum amount (in bytes) of data that can be stored in local storage, as measured by the JSON stringification of every value plus every key's length. This value will be ignored if the extension has the unlimitedStorage permission. Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or a rejected Promise if using async/await. */
QUOTA_BYTES: 10485760;
}
export interface SyncStorageArea extends StorageArea {
interface SyncStorageArea extends StorageArea {
/** @deprecated The storage.sync API no longer has a sustained write operation quota. */
MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE: 1000000;
/** The maximum total amount (in bytes) of data that can be stored in sync storage, as measured by the JSON stringification of every value plus every key's length. Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected. */
@@ -10232,18 +10256,18 @@ export namespace Browser {
MAX_WRITE_OPERATIONS_PER_MINUTE: 120;
}
export interface SessionStorageArea extends StorageArea {
interface SessionStorageArea extends StorageArea {
/** The maximum amount (in bytes) of data that can be stored in memory, as measured by estimating the dynamically allocated memory usage of every value and key. Updates that would cause this limit to be exceeded fail immediately and set runtime.lastError when using a callback, or when a Promise is rejected. */
QUOTA_BYTES: 10485760;
}
export type AreaName = "sync" | "local" | "managed" | "session";
type AreaName = "sync" | "local" | "managed" | "session";
/**
* The storage area's access level.
* @since Chrome 102
*/
export enum AccessLevel {
enum AccessLevel {
/** Specifies contexts originating from the extension itself. */
TRUSTED_CONTEXTS = "TRUSTED_CONTEXTS",
/** Specifies contexts originating from outside the extension. */
@@ -10251,13 +10275,13 @@ export namespace Browser {
}
/** Items in the `local` storage area are local to each machine. */
export const local: LocalStorageArea;
const local: LocalStorageArea;
/** Items in the `sync` storage area are synced using Chrome Sync. */
export const sync: SyncStorageArea;
const sync: SyncStorageArea;
/** Items in the `managed` storage area are set by an enterprise policy configured by the domain administrator, and are read-only for the extension; trying to modify this namespace results in an error. For information on configuring a policy, see Manifest for storage areas. */
export const managed: StorageArea;
const managed: StorageArea;
/**
* Items in the `session` storage area are stored in-memory and will not be persisted to disk.
@@ -10265,10 +10289,10 @@ export namespace Browser {
* MV3 only
* @since Chrome 102
*/
export const session: SessionStorageArea;
const session: SessionStorageArea;
/** Fired when one or more items change. */
export const onChanged: events.Event<(changes: { [key: string]: StorageChange }, areaName: AreaName) => void>;
const onChanged: events.Event<(changes: { [key: string]: StorageChange }, areaName: AreaName) => void>;
}
////////////////////
@@ -10291,10 +10315,6 @@ export namespace Browser {
total: number;
}
/** @deprecated Use {@link CpuTime} instead. */
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface ProcessorUsage extends CpuTime {}
interface ProcessorInfo {
/** Cumulative usage info for this logical processor. */
usage: CpuTime;
@@ -12527,10 +12547,6 @@ export namespace Browser {
reconnect?: string | undefined;
}
/** @deprecated Use {@link Parameters} instead */
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface VpnSessionParameters extends Parameters {}
/** The enum is used by the platform to notify the client of the VPN session status. */
enum PlatformMessage {
/** Indicates that the VPN configuration connected. */
@@ -13435,7 +13451,6 @@ export namespace Browser {
/** Fired before sending an HTTP request, once the request headers are available. This may occur after a TCP connection is made to the server, but before any HTTP data is sent. */
const onBeforeSendHeaders: WebRequestEvent<
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
(details: OnBeforeSendHeadersDetails) => BlockingResponse | undefined,
`${OnBeforeSendHeadersOptions}`[]
>;
+31
View File
@@ -1,5 +1,36 @@
# Changelog
## v0.2.6
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.5...i18n-v0.2.6)
### 🚀 Enhancements
- Add named substitutions to i18n ([#2440](https://github.com/wxt-dev/wxt/pull/2440))
### 🏡 Chore
- Add `prettier-plugin-jsdoc` to project ([#2171](https://github.com/wxt-dev/wxt/pull/2171))
- Replace fast-glob and ora with lighter alternatives ([#2173](https://github.com/wxt-dev/wxt/pull/2173))
- **deps:** Upgrade deps ([#2175](https://github.com/wxt-dev/wxt/pull/2175))
- Add prepack script to all packages ([032f7931](https://github.com/wxt-dev/wxt/commit/032f7931))
- Remove `any` from `i18n`'s internal types ([#2201](https://github.com/wxt-dev/wxt/pull/2201))
- Add cspell and fix all typos ([6621aaf8](https://github.com/wxt-dev/wxt/commit/6621aaf8))
- Migrate monorepo to use Bun instead of PNPM ([#2009](https://github.com/wxt-dev/wxt/pull/2009))
- Add new types for i18n ([#2121](https://github.com/wxt-dev/wxt/pull/2121))
- **deps-dev:** Bump typescript from 5.9.3 to 6.0.3 ([#2325](https://github.com/wxt-dev/wxt/pull/2325))
- **deps-dev:** Bump oxlint from 1.59.0 to 1.63.0 ([#2356](https://github.com/wxt-dev/wxt/pull/2356))
- Use `catalog:` for dev dependencies ([#2357](https://github.com/wxt-dev/wxt/pull/2357))
- **deps:** Bump tinyglobby from 0.2.15 to 0.2.16 ([#2366](https://github.com/wxt-dev/wxt/pull/2366))
- Add benchmarks for i18n vs vanilla usage ([#2372](https://github.com/wxt-dev/wxt/pull/2372))
### ❤️ Contributors
- T ([@cookesan](https://github.com/cookesan))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Florian Metz ([@Timeraa](https://github.com/Timeraa))
## v0.2.5
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.4...i18n-v0.2.5)
+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]
+11
View File
@@ -0,0 +1,11 @@
# I18n Benchmarks
These benchmarks must be ran in an actual browser extension environment.
To run them, run `bench/run.ts` and load the `bench/` directory into a browser as an extension. Or just:
```sh
bun --cwd packages/i18n bench
```
Benchmarks are defined in `bench/background.ts`. Add or edit them there.
@@ -0,0 +1,20 @@
{
"simple": {
"message": "Just plain text"
},
"substitution": {
"message": "Includes one substitution: $1"
},
"namedSubstitution": {
"message": "Includes one named substitution: {value}"
},
"singular": {
"message": "There is $1 item"
},
"plural": {
"message": "There are $1 items"
},
"wxtPlural": {
"message": "There is $1 item | There are $1 items"
}
}
+94
View File
@@ -0,0 +1,94 @@
import { Bench } from 'tinybench';
import { createI18n } from '../src';
declare const chrome: any;
export const i18n = createI18n();
// Keep service worker alive
setTimeout(() => {
chrome.runtime.getPlatformInfo();
}, 20e3);
(async () => {
const results: Record<string, any> = {};
const runBench = async (bench: Bench) => {
await reportProgress(`[start] ${bench.name}`);
await bench.run();
results[bench.name!] = bench.table();
await reportProgress(`[done] ${bench.name}`);
};
{
const key = 'simple';
const simple = new Bench({ name: 'Simple' })
.add('Vanilla', () => void chrome.i18n.getMessage(key))
.add('WXT I18n', () => void i18n.t(key));
await runBench(simple);
}
{
const key = 'substitution';
const value = 'test';
const substitution = new Bench({ name: 'Substitution' })
.add('Vanilla', () => void chrome.i18n.getMessage(key, [value]))
.add('WXT I18n', () => void i18n.t(key, [value]));
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';
const one = 1;
const pluralSingular = new Bench({
name: 'Plural (singular form)',
})
.add(
'Vanilla',
() => void chrome.i18n.getMessage(vanillaPluralKey, [one]),
)
.add('WXT I18n', () => void i18n.t(wxtKey, one));
await runBench(pluralSingular);
const two = 2;
const vanillaSingularKey = 'singular';
const pluralPlural = new Bench({
name: 'Plural (plural form)',
time: 1000,
})
.add(
'Vanilla',
() => void chrome.i18n.getMessage(vanillaSingularKey, [two]),
)
.add('WXT I18n', () => void i18n.t(wxtKey, two));
await runBench(pluralPlural);
}
// Report results
await reportResults(results);
})();
async function reportProgress(message: string) {
const res = await fetch('http://localhost:3000/progress', {
method: 'POST',
body: message,
});
if (!res.ok) throw Error('Progress report failed...');
}
async function reportResults(results: any) {
const res = await fetch('http://localhost:3000/results', {
method: 'POST',
body: JSON.stringify(results, null, 2),
});
if (!res.ok) throw Error('Results report failed...');
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/chrome-manifest",
"name": "WXT I18n Benchmark",
"version": "1.0.0",
"manifest_version": 3,
"default_locale": "en",
"background": {
"type": "module",
"service_worker": "dist/background.js"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { join, relative } from 'node:path';
const distDir = join(import.meta.dir, 'dist');
const backgroundSrc = join(import.meta.dir, 'background.ts');
console.log('\nBuilding benchmark extension...');
await Bun.build({
entrypoints: [backgroundSrc],
outdir: distDir,
format: 'esm',
});
console.log('Done!');
console.log(
`\nInstall the extension from ./${relative(process.cwd(), import.meta.dir)} (or reload it) to run benchmarks.`,
);
console.log('Waiting for benchmark results...');
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
};
Bun.serve({
fetch: async (request) => {
// CORS support
if (request.method === 'OPTIONS') {
return new Response(undefined, { headers: CORS_HEADERS });
}
const url = new URL(request.url);
if (url.pathname === '/progress') {
console.log(`\x1b[2m← ${await request.text()}\x1b[0m`);
return new Response(undefined, { status: 202, headers: CORS_HEADERS });
}
if (url.pathname === '/results') {
const body = await request.json();
for (const [name, table] of Object.entries(body)) {
console.log('\n' + name);
console.table(table);
}
setTimeout(() => void process.exit(0));
return new Response(undefined, { status: 202, headers: CORS_HEADERS });
}
return new Response(undefined, { status: 404, headers: CORS_HEADERS });
},
});
+4 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/i18n",
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
"version": "0.2.5",
"version": "0.2.6",
"type": "module",
"repository": {
"type": "git",
@@ -24,6 +24,7 @@
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest",
"bench": "buildc --deps-only -- bun run bench/run.ts",
"test:coverage": "bun run test run --coverage",
"prepack": "bun run build"
},
@@ -31,7 +32,7 @@
"@wxt-dev/browser": "workspace:^",
"chokidar": "^5.0.0",
"confbox": "^0.2.4",
"tinyglobby": "^0.2.15"
"tinyglobby": "^0.2.16"
},
"peerDependencies": {
"wxt": ">=0.19.7"
@@ -46,6 +47,7 @@
"@types/node": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"tinybench": "^6.0.1",
"tsdown": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
+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 +
+1 -1
View File
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node"]
"types": ["bun"]
},
"exclude": ["node_modules/**", "dist/**"]
}
+1 -1
View File
@@ -1,6 +1,6 @@
import 'wxt';
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
import react, { Options as PluginOptions } from '@vitejs/plugin-react';
import react, { type Options as PluginOptions } from '@vitejs/plugin-react';
import type { PluginOption } from 'vite';
export default defineWxtModule<ReactModuleOptions>({
+1 -1
View File
@@ -1,4 +1,4 @@
import { Component } from 'solid-js';
import type { Component } from 'solid-js';
export const App: Component = () => {
const [count, setCount] = createSignal(0);
+1 -1
View File
@@ -1,6 +1,6 @@
import 'wxt';
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
import solid, { Options as PluginOptions } from 'vite-plugin-solid';
import solid, { type Options as PluginOptions } from 'vite-plugin-solid';
export default defineWxtModule<SolidModuleOptions>({
name: '@wxt-dev/module-solid',
+2 -2
View File
@@ -33,8 +33,8 @@
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"async-mutex": "^0.5.0",
"dequal": "^2.0.3"
"dequal": "^2.0.3",
"superlock": "^1.3.2"
},
"devDependencies": {
"@aklinker1/buildc": "catalog:",
+3 -3
View File
@@ -7,7 +7,7 @@
* @module @wxt-dev/storage
*/
import { browser, type Browser } from '@wxt-dev/browser';
import { Mutex } from 'async-mutex';
import { withLock } from 'superlock';
import { dequal } from 'dequal/lite';
export const storage = createStorage();
@@ -487,12 +487,12 @@ function createStorage(): WxtStorage {
);
});
const initMutex = new Mutex();
const initLock = withLock();
const getFallback = () => opts?.fallback ?? opts?.defaultValue ?? null;
const getOrInitValue = () =>
initMutex.runExclusive(async () => {
initLock(async () => {
const value = await driver.getItem<any>(driverKey);
// Don't init value if it already exists or the init function isn't provided
if (value != null || opts?.init == null) return value;
+1 -1
View File
@@ -53,6 +53,6 @@
},
"dependencies": {
"defu": "^6.1.4",
"tinyglobby": "^0.2.15"
"tinyglobby": "^0.2.16"
}
}
@@ -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);
},
@@ -1,6 +1,3 @@
// @ts-expect-error: URL imports not typed
import 'url:https://code.jquery.com/jquery-3.7.1.slim.min.js';
console.log(browser.runtime.id);
logId();
console.log(2);
+1 -1
View File
@@ -1,5 +1,5 @@
import { defineProject } from 'vitest/config';
import { WxtVitest } from 'wxt/testing';
import { WxtVitest } from 'wxt/testing/vitest-plugin';
export default defineProject({
test: {
+201 -1
View File
@@ -1,5 +1,205 @@
# Changelog
## v0.21.2
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.21.1...wxt-v0.21.2)
### 🩹 Fixes
- Remove `web-ext-run`, it's unused now ([`859d654`](https://github.com/wxt-dev/wxt/commit/859d654611571941ee0a7aa4011f3ce011787a30))
### 📖 Documentation
- Fix typos and wording of upgrade guide ([`d1ab2d9`](https://github.com/wxt-dev/wxt/commit/d1ab2d923dada60ac4a3ece7d85001ee611f9731))
- Fix heading level ([`466ae8a`](https://github.com/wxt-dev/wxt/commit/466ae8a33c73436ac2dc06e0046ea949e1982a7f))
- Update package size links ([`925e97d`](https://github.com/wxt-dev/wxt/commit/925e97d4fc83ecfcc93bbbb357247373b594804f))
### ❤️ Contributors
- [@aklinker1](https://github.com/aklinker1)
## v0.21.1
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.27...wxt-v0.21.1)
### 🚀 Features
- Add `prepare:tsconfig` hook for updating `.wxt/tsconfig.json` file ([#2450](https://github.com/wxt-dev/wxt/pull/2450))
- Add watch options config ([#2439](https://github.com/wxt-dev/wxt/pull/2439))
- Export `getEntrypointBundlePath` from `wxt` ([#2463](https://github.com/wxt-dev/wxt/pull/2463))
- Add `{{modeSuffix}}` to zip templates and update defaults ([#1623](https://github.com/wxt-dev/wxt/pull/1623))
- Change default behavior of `getUnimportEslintOptions` true flag ([#2296](https://github.com/wxt-dev/wxt/pull/2296))
- New template variable `{{versionName}}` and change of the behaviour of `{{version}}` ([#1612](https://github.com/wxt-dev/wxt/pull/1612))
- **Firefox**: List files in sources zip ([#2513](https://github.com/wxt-dev/wxt/pull/2513))
### 🩹 Fixes
- `wxt clean` removes `.wxt/` directory at project root ([#2415](https://github.com/wxt-dev/wxt/pull/2415))
- Handle spread content script configs ([#2448](https://github.com/wxt-dev/wxt/pull/2448))
- Avoid deprecated Rollup asset name access ([#2453](https://github.com/wxt-dev/wxt/pull/2453))
- **init**: Allow entering an empty directory for project path ([#2458](https://github.com/wxt-dev/wxt/pull/2458))
- Opening the browser issue in windows. ([#2436](https://github.com/wxt-dev/wxt/pull/2436))
- File reloading mechanism and add profile watch ignores ([#2112](https://github.com/wxt-dev/wxt/pull/2112))
- Docs sidebar header, background color issue ([#2504](https://github.com/wxt-dev/wxt/pull/2504))
- Deprecated `useAppConfig` in favour of `getAppConfig` ([#2487](https://github.com/wxt-dev/wxt/pull/2487))
- Make `web-ext` a peer dependency ([#2079](https://github.com/wxt-dev/wxt/pull/2079))
- Handle edge case where eslint version cannot be found ([`ab679ba`](https://github.com/wxt-dev/wxt/commit/ab679ba413772849e0c263315587334b42c25ed0))
- Remove `url:` import feature ([#2456](https://github.com/wxt-dev/wxt/pull/2456))
- **zip**: Make `includeSources`/`excludeSources` follow standard allowlist behavior ([#2114](https://github.com/wxt-dev/wxt/pull/2114))
- Remove `console.log` ([`0b352f6`](https://github.com/wxt-dev/wxt/commit/0b352f6eff1fbb45034b788a134742d03db81816))
- Loosen version of `@webext-core/isolated-element` ([`7d6caad`](https://github.com/wxt-dev/wxt/commit/7d6caad9b52bc263d0acb4263153fe0e05e24b76))
- Update content and unlisted script `globalName` default to `false` ([#2511](https://github.com/wxt-dev/wxt/pull/2511))
- **TS**: Update `.wxt/tsconfig.json` compiler options, require TS >=5.4 ([#2512](https://github.com/wxt-dev/wxt/pull/2512))
### 💅 Refactors
- Make vite a default peer ([#1945](https://github.com/wxt-dev/wxt/pull/1945))
- Move to module-runner ([#1946](https://github.com/wxt-dev/wxt/pull/1946))
### 📖 Documentation
- Move Bun tabs to first position in installation ([#2428](https://github.com/wxt-dev/wxt/pull/2428))
- Added package manager tab group synchronization across the entire installation page ([#2421](https://github.com/wxt-dev/wxt/pull/2421))
- Make vitepress components css files more readable and simply ([#2272](https://github.com/wxt-dev/wxt/pull/2272))
- Use bright green for example hover and link colors ([`d7f8ebe`](https://github.com/wxt-dev/wxt/commit/d7f8ebec3904ea6efa803c9d7b5dcd9b3ea59a01))
- Document PerBrowser options for entrypoints (#2304) ([#2403](https://github.com/wxt-dev/wxt/pull/2403))
- Show Edge Add-ons in showcase ([#2452](https://github.com/wxt-dev/wxt/pull/2452))
- Don't underline the comma when hovering over showcase item store links ([#2454](https://github.com/wxt-dev/wxt/pull/2454))
- Fix `undefined` issue for `ExampleSearch.vue` ([#2370](https://github.com/wxt-dev/wxt/pull/2370))
- Update extension-showcase.yml ([#2457](https://github.com/wxt-dev/wxt/pull/2457))
- Added "NiceTab" Edge Addons (edgeId) to extension showcase ([#2455](https://github.com/wxt-dev/wxt/pull/2455))
- Prioritize Bun in package manager options ([#2425](https://github.com/wxt-dev/wxt/pull/2425))
- Update BlogLayout.vue `useData` to correctly type ([#2369](https://github.com/wxt-dev/wxt/pull/2369))
- Revert #2380 - locationWatcher ([#2416](https://github.com/wxt-dev/wxt/pull/2416))
- Added "Teams Chat Exporter" Edge Addons (edgeId) to extension showcase ([#2472](https://github.com/wxt-dev/wxt/pull/2472))
- Cleanup the "extension APIs" page ([#2473](https://github.com/wxt-dev/wxt/pull/2473))
- Add MindSnap to showcase ([#2499](https://github.com/wxt-dev/wxt/pull/2499))
- Use consola's fancy reporter in init demo GIF ([#2485](https://github.com/wxt-dev/wxt/pull/2485))
- Update `wxt init` GIF ([`13be7c5`](https://github.com/wxt-dev/wxt/commit/13be7c5bfdedc6660fc799ded6d868736dffcb86))
- Update `wxt init` GIF ([`299b0ad`](https://github.com/wxt-dev/wxt/commit/299b0add803caab65f707ab73a677fc92f8e3379))
- Cleanup v0.21 migration guide ([`885f8f0`](https://github.com/wxt-dev/wxt/commit/885f8f0b76d8daa635957edcd0783cb3b2a41ade))
- Finalize migration guide for wxt v0.21 ([#2516](https://github.com/wxt-dev/wxt/pull/2516))
- Add section about install footprint ([`d8a86e7`](https://github.com/wxt-dev/wxt/commit/d8a86e76336be263271673028ee7677efeecf905))
### 🏡 Chore
- Skip package manager tests when binaries are missing ([#2441](https://github.com/wxt-dev/wxt/pull/2441))
- **deps**: Bump actions/setup-go from 6.4.0 to 6.5.0 ([#2432](https://github.com/wxt-dev/wxt/pull/2432))
- Bump minimum supported Node version to >=22 ([`008f5a6`](https://github.com/wxt-dev/wxt/commit/008f5a66c9eb9e01a3dd1db0545e362254641089))
- **deps**: Remove `esbuild` dependency ([#2506](https://github.com/wxt-dev/wxt/pull/2506))
- Upgrade `@webext-core/isolated-element` v1 → v3 ([#2098](https://github.com/wxt-dev/wxt/pull/2098))
- Remove deprecated APIs ([#2488](https://github.com/wxt-dev/wxt/pull/2488))
- **deps**: Upgrade major dependencies ([#2508](https://github.com/wxt-dev/wxt/pull/2508))
### 🤖 CI
- Fix VHS action ([#2459](https://github.com/wxt-dev/wxt/pull/2459))
- Use `@aklinker1/zero-changelog` for release notes ([#2515](https://github.com/wxt-dev/wxt/pull/2515))
### ⚠️ Breaking Changes
- Add `{{modeSuffix}}` to zip templates and update defaults
- Change default behavior of `getUnimportEslintOptions` true flag
- New template variable `{{versionName}}` and change of the behaviour of `{{version}}`
- Make `web-ext` a peer dependency
- Remove `url:` import feature
- **zip**: Make `includeSources`/`excludeSources` follow standard allowlist behavior
- [#2511](https://github.com/wxt-dev/wxt/pull/2511): If your script needs to return a value, like for `browser.scripting.executeScript`, set `globalName: true` to maintain the old behavior.
- [#2512](https://github.com/wxt-dev/wxt/pull/2512): Updates `.wxt/tsconfig.json` defaults to latest recommended settings for vite projects and requires Typescript 5.4 or later. See #2512 for details on what compiler options were changed.
- Move to module-runner
- Bump minimum supported Node version to >=22
- Upgrade `@webext-core/isolated-element` v1 → v3
- Remove deprecated APIs
- **deps**: Upgrade major dependencies
### ❤️ Contributors
- [@z1lV3r](https://github.com/z1lV3r)
- Peter Wong <www121ppp@gmail.com>
- wangpeng618 <wangpeng618@jd.com>
- Thribhuvan <thribhuvan003@gmail.com>
- ТΞNSΛI <tensai@gmx.net>
- silverfish2525 <silverfish25@proton.me>
- [@PatrykKuniczak](https://github.com/PatrykKuniczak)
- [@nishu-murmu](https://github.com/nishu-murmu)
- Nazım Gediz Aydındoğmuş <nga@hotmail.com.tr>
- Kai Gritun <kaigritun@gmail.com>
- Kai Gritun <kai@kaigritun.com>
- [@github-actions[bot]](https://github.com/github-actions[bot])
- [@ded-furby](https://github.com/ded-furby)
- [@dashitongzhi](https://github.com/dashitongzhi)
- [@cookesan](https://github.com/cookesan)
- Shubhrajit Sadhukhan <champa.281982@gmail.com>
- BlazerYoo <blazeryoo1921@gmail.com>
- [@aklinker1](https://github.com/aklinker1)
- [@lishaduck](https://github.com/lishaduck)
- [@nnthanh01061999](https://github.com/nnthanh01061999)
- [@cookesan](https://github.com/cookesan)
- [@smashedr](https://github.com/smashedr)
- [@dependabot[bot]](https://github.com/dependabot[bot])
- [@ded-furby](https://github.com/ded-furby)
- [@ZerGo0](https://github.com/ZerGo0)
- [@LUIDevo](https://github.com/LUIDevo)
- [@dashitongzhi](https://github.com/dashitongzhi)
- [@fkatsuhiro](https://github.com/fkatsuhiro)
## v0.20.27
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.26...wxt-v0.20.27)
### 🚀 Enhancements
- Add warning for missing Firefox extension ID in manifest ([#2293](https://github.com/wxt-dev/wxt/pull/2293))
### 🩹 Fixes
- Correct broken link at react router in createhashrouter ([#2411](https://github.com/wxt-dev/wxt/pull/2411))
- Rename `ManifestPermissions` to `ManifestPermission` of types.ts in `wxt` package ([#2430](https://github.com/wxt-dev/wxt/pull/2430))
### 📖 Documentation
- Add Telsia extension to Showcase ([#2368](https://github.com/wxt-dev/wxt/pull/2368))
- Add warning about rem unit inheritance in Shadow Root UI ([#2330](https://github.com/wxt-dev/wxt/pull/2330))
- Added "Sound Booster & Equalizer" to showcase ([#2381](https://github.com/wxt-dev/wxt/pull/2381))
- Clarify SPA navigation setup with locationWatcher.run() ([#2380](https://github.com/wxt-dev/wxt/pull/2380))
- Self-host Netlify footer badge ([#2386](https://github.com/wxt-dev/wxt/pull/2386))
- Added "Link Later" to showcase" ([#2392](https://github.com/wxt-dev/wxt/pull/2392))
- Added Property Track to showcase ([#2404](https://github.com/wxt-dev/wxt/pull/2404))
- Added "WageSlav3" to showcase ([#2405](https://github.com/wxt-dev/wxt/pull/2405))
- Added "LinkedIn Games Solver" to showcase ([#2410](https://github.com/wxt-dev/wxt/pull/2410))
- Added NiceTab firefoxslug to extension showcase ([#2417](https://github.com/wxt-dev/wxt/pull/2417))
- Added 4 extensions to the homepage ([#2419](https://github.com/wxt-dev/wxt/pull/2419))
- Added Hayami to showcase ([#2422](https://github.com/wxt-dev/wxt/pull/2422))
- Added anylang translator to showcase ([#2426](https://github.com/wxt-dev/wxt/pull/2426))
### 🏡 Chore
- **deps:** Bump c12 from 3.3.3 to 3.3.4 ([#2364](https://github.com/wxt-dev/wxt/pull/2364))
- **deps:** Bump filesize from 11.0.15 to 11.0.17 ([#2363](https://github.com/wxt-dev/wxt/pull/2363))
- **deps:** Bump tinyglobby from 0.2.15 to 0.2.16 ([#2366](https://github.com/wxt-dev/wxt/pull/2366))
- Add Google Cloud Skills Boost - Helper extension to showcase ([#2401](https://github.com/wxt-dev/wxt/pull/2401))
### ❤️ Contributors
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- RenovZ ([@RenovZ](https://github.com/RenovZ))
- Nicholas Yoannou ([@nicholasyoannou](https://github.com/nicholasyoannou))
- Avi ([@avi12](https://github.com/avi12))
- Peter Wong <www121ppp@gmail.com>
- ChamikaUluwatta ([@ChamikaUluwatta](https://github.com/ChamikaUluwatta))
- Muhammed Mustafa AKŞAM ([@muhammedaksam](https://github.com/muhammedaksam))
- Slovakian ([@slovakian](https://github.com/slovakian))
- Oliver Benns ([@oliverbenns](https://github.com/oliverbenns))
- David Nguyen <h250694@gmail.com>
- Esteban Vera <minos_93105@hotmail.com>
- Sean Kenneth Doherty <Smaster7772@gmail.com>
- Muhammad Umer Farooq ([@mrumerf](https://github.com/mrumerf))
- Muzammil ([@oyzamil](https://github.com/oyzamil))
- Aniket Lodh ([@Aniket-lodh](https://github.com/Aniket-lodh))
- George Forse <george@gforse.uk>
- Shubhrajit Sadhukhan <champa.281982@gmail.com>
## v0.20.26
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.25...wxt-v0.20.26)
@@ -3641,4 +3841,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
### 🤖 CI
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
-1
View File
@@ -54,7 +54,6 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
- 📦 [Module system](https://wxt.dev/guide/essentials/wxt-modules.html#overview) for reusing code between extensions
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
- ⬇️ Download and bundle remote URL imports
## Sponsors
@@ -119,8 +119,8 @@ export default {
"
`;
exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config file compatible with ESlint 8 1`] = `
".wxt/eslintrc-auto-import.json
exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config file compatible with ESlint of package.json 1`] = `
".wxt/eslint-auto-imports.mjs
----------------------------------------
{
"globals": {
+2 -2
View File
@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TestProject } from '../utils';
import { resetBundleIncrement } from '../../src/core/builders/vite/plugins';
import open from 'open';
import open from 'tiny-open';
vi.mock('open');
vi.mock('tiny-open');
const openMock = vi.mocked(open);
vi.mock('ci-info', () => ({
+5 -5
View File
@@ -28,7 +28,7 @@ describe('Auto Imports', () => {
const defineContentScript: typeof import('wxt/utils/define-content-script').defineContentScript
const defineUnlistedScript: typeof import('wxt/utils/define-unlisted-script').defineUnlistedScript
const defineWxtPlugin: typeof import('wxt/utils/define-wxt-plugin').defineWxtPlugin
const fakeBrowser: typeof import('wxt/testing').fakeBrowser
const fakeBrowser: typeof import('wxt/testing/fake-browser').fakeBrowser
const getAppConfig: typeof import('wxt/utils/app-config').getAppConfig
const injectScript: typeof import('wxt/utils/inject-script').injectScript
const storage: typeof import('wxt/utils/storage').storage
@@ -124,7 +124,7 @@ describe('Auto Imports', () => {
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
export { fakeBrowser } from 'wxt/testing/fake-browser';
export { startOfDay } from '../utils/time';
}
"
@@ -209,7 +209,7 @@ describe('Auto Imports', () => {
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
export { fakeBrowser } from 'wxt/testing/fake-browser';
}
"
`);
@@ -217,7 +217,7 @@ describe('Auto Imports', () => {
});
describe('eslintrc', () => {
it('"enabled: true" should output a JSON config file compatible with ESlint 8', async () => {
it('"enabled: true" should output a JSON config file compatible with ESlint of package.json', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
@@ -230,7 +230,7 @@ describe('Auto Imports', () => {
});
expect(
await project.serializeFile('.wxt/eslintrc-auto-import.json'),
await project.serializeFile('.wxt/eslint-auto-imports.mjs'),
).toMatchSnapshot();
});
+4 -4
View File
@@ -10,7 +10,7 @@ describe('Dev Mode', () => {
);
const server = await project.startServer({
runner: {
webExt: {
disabled: true,
},
});
@@ -30,7 +30,7 @@ describe('Dev Mode', () => {
);
const server = await project.startServer({
runner: { disabled: true },
webExt: { disabled: true },
dev: { server: { port: 4400 } },
});
try {
@@ -51,7 +51,7 @@ describe('Dev Mode', () => {
);
const server = await project.startServer({
runner: { disabled: true },
webExt: { disabled: true },
dev: { server: { port } },
});
try {
@@ -76,7 +76,7 @@ describe('Dev Mode', () => {
try {
await expect(
project.startServer({
runner: { disabled: true },
webExt: { disabled: true },
dev: { server: { port, strictPort: true } },
}),
).rejects.toThrow();
+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,
@@ -1,15 +0,0 @@
import { test, expect } from 'vitest';
import spawn from 'nano-spawn';
// Tests to ensure the total size of the WXT module is as small as possible
// https://pkg-size.dev/wxt
test('Only one version of esbuild should be installed (each version is ~20mb of node_modules)', async () => {
const { stdout } = await spawn('bun', ['why', 'esbuild']);
// This text represents the wxt package being responsible for a version of esbuild?
// If this doesn't work, we'll need to find a better way to check this.
const count = stdout.match(/\n [└├]─ wxt@workspace/g)?.length || 0;
expect(count).toBe(1);
});
@@ -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(
@@ -461,7 +506,7 @@ describe('Output Directory Structure', () => {
});
describe('globalName option', () => {
it('generates an IIFE with a default name', async () => {
it('does not generate a IIFE return variable by default', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
@@ -473,6 +518,25 @@ describe('Output Directory Structure', () => {
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output.includes('var content')).toBe(false);
});
it('does generates the IIFE name based on the entrypoint name when true', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: true,
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
@@ -1,26 +0,0 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe('Remote Code', () => {
it('should download "url:*" modules and include them in the final bundle', async () => {
const url = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js';
const project = new TestProject();
project.addFile(
'entrypoints/popup.ts',
`import "url:${url}"
export default defineUnlistedScript(() => {})`,
);
await project.build();
const output = await project.serializeFile('.output/chrome-mv3/popup.js');
expect(output).toContain(
// Some text that will hopefully be in future versions of this script
'__lodash_placeholder__',
);
expect(output).not.toContain(url);
expect(
await project.pathExists(`.wxt/cache/${encodeURIComponent(url)}`),
).toBe(true);
});
});
+157
View File
@@ -0,0 +1,157 @@
import { expect, describe, vi, it, beforeEach } from 'vitest';
import { ExtensionRunner } from '../../src';
import { createSafariRunner } from '../../src/core/runners/safari';
import { createManualRunner } from '../../src/core/runners/manual';
import { createWebExtRunner } from '../../src/core/runners/web-ext';
import { createWslRunner } from '../../src/core/runners/wsl';
import { TestProject } from '../utils';
import { wxt } from '../../src/core/wxt';
// Globals for modifying mock behaviors
let isWsl = false;
let importWebExtRunnerError: Error | undefined = undefined;
// Mock runners to create constants for checking equality
type TestExtensionRunner = { name: string } & ExtensionRunner;
function createMockExtensionRunner(name: string): TestExtensionRunner {
return {
name,
closeBrowser: () => Promise.resolve(),
openBrowser: () => Promise.resolve(),
};
}
vi.mock('../../src/core/runners/safari', () => {
const runner = createMockExtensionRunner('safari');
return { createSafariRunner: () => runner };
});
const safariRunner = createSafariRunner();
vi.mock('../../src/core/runners/manual', () => {
const runner = createMockExtensionRunner('manual');
return { createManualRunner: () => runner };
});
const manualRunner = createManualRunner();
vi.mock('../../src/core/runners/web-ext', () => {
const runner = createMockExtensionRunner('web-ext');
return {
createWebExtRunner: () => {
if (!importWebExtRunnerError) return runner;
else throw importWebExtRunnerError;
},
};
});
const webExtRunner = createWebExtRunner();
vi.mock('../../src/core/runners/wsl', () => {
const runner = createMockExtensionRunner('wsl');
return { createWslRunner: () => runner };
});
const wslRunner = createWslRunner();
// Other mocks
vi.mock('is-wsl', () => ({
get default() {
return isWsl;
},
}));
/** Imitate a real module not found error - needs the correct `code` property. */
class ModuleNotFoundError extends Error {
code = 'ERR_MODULE_NOT_FOUND';
constructor(mod: string) {
super(`Cannot find package '${mod}' imported from ...`);
this.name = 'ModuleNotFoundError';
}
}
describe('Runners', () => {
beforeEach(() => {
isWsl = false;
importWebExtRunnerError = undefined;
});
describe('build', () => {
const command = 'build';
it('should use the manual runner as a placeholder since the runner is not used during builds', async () => {
await TestProject.simple().registerWxt(command);
expect(wxt.config.runner).toBe(manualRunner);
});
});
describe('dev', () => {
const command = 'serve';
describe('inside WSL', () => {
beforeEach(() => {
isWsl = true;
});
it('should use the WSL runner', async () => {
await TestProject.simple().registerWxt(command);
expect(wxt.config.runner).toBe(wslRunner);
});
});
describe('web-ext is installed', () => {
it('should use the web-ext runner', async () => {
await TestProject.simple().registerWxt(command);
expect(wxt.config.runner).toBe(webExtRunner);
});
describe('disabled', () => {
it('should use the manual runner', async () => {
await TestProject.simple().registerWxt(command, {
webExt: { disabled: true },
});
expect(wxt.config.runner).toBe(manualRunner);
});
});
});
describe('web-ext is not installed', () => {
beforeEach(() => {
importWebExtRunnerError = new ModuleNotFoundError('web-ext');
});
it('should use the manual runner', async () => {
await TestProject.simple().registerWxt(command);
expect(wxt.config.runner).toBe(manualRunner);
});
});
describe('some other error when importing the web-ext runner', () => {
beforeEach(() => {
importWebExtRunnerError = Error('test');
});
it('should throw the error', async () => {
await expect(TestProject.simple().registerWxt(command)).rejects.toThrow(
importWebExtRunnerError,
);
});
});
describe('targeting safari', () => {
it('should use the safari runner', async () => {
await TestProject.simple().registerWxt(command, {
browser: 'safari',
});
expect(wxt.config.runner).toBe(safariRunner);
});
});
});
});
+146 -41
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 () => {
@@ -323,35 +324,81 @@ describe('TypeScript Project', () => {
----------------------------------------
{
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"target": "ESNext",
"module": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": 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>');
@@ -367,31 +414,58 @@ describe('TypeScript Project', () => {
----------------------------------------
{
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"target": "ESNext",
"module": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"paths": {
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"@": [
"../src"
],
"@/*": [
"../src/*"
],
"~": [
"../src"
],
"~/*": [
"../src/*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
@@ -415,33 +489,64 @@ describe('TypeScript Project', () => {
----------------------------------------
{
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"target": "ESNext",
"module": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": 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"
]
}"
`);
});

Some files were not shown because too many files have changed in this diff Show More