Compare commits

...

179 Commits

Author SHA1 Message Date
github-actions[bot] 1171cf52f0 chore(release): @wxt-dev/analytics v0.5.4
📼 VHS / Create VHS (push) Waiting to run
2026-02-22 17:29:53 -06:00
Aaron c5978b1072 fix: Continue using useAppConfig to support older versions of WXT 2026-02-22 17:29:53 -06:00
Aaron e5ef9367ed ci: Fix VHS action (#2155) 2026-02-22 17:29:53 -06:00
github-actions[bot] 0ca908a6cc chore(release): @wxt-dev/analytics v0.5.3
📼 VHS / Create VHS (push) Waiting to run
2026-02-22 17:29:53 -06:00
github-actions[bot] e20c012eb3 chore(release): @wxt-dev/storage v1.2.8
📼 VHS / Create VHS (push) Waiting to run
2026-02-22 17:29:53 -06:00
github-actions[bot] e88df26d7c chore(release): @wxt-dev/auto-icons v1.1.1
📼 VHS / Create VHS (push) Waiting to run
2026-02-22 17:29:53 -06:00
github-actions[bot] 9491e6845e chore(release): @wxt-dev/i18n v0.2.5
📼 VHS / Create VHS (push) Waiting to run
2026-02-22 17:29:53 -06:00
Aaron e4433c4212 ci: Fix scripts/list_unreleased_commits.sh (#2153) 2026-02-22 17:29:53 -06:00
Jonathan Viney c6fdc5a5d5 feat: Add globalName entrypoint option. (#2017) 2026-02-22 17:29:53 -06:00
Aaron 68dd3fc027 docs: Update section in SECURITY.md around reporting vulnerabilities (#2147) 2026-02-22 17:29:53 -06:00
omerfardemir cd7aec6752 fix: auto icons override default icons (#1616)
Co-authored-by: Florian Metz <me@timeraa.dev>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Smit b80556498d fix: Improve background script detection logic for analytics package (#1808)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Aaron 38530cdb39 feat: New @wxt-dev/is-background package (#2152)
Co-authored-by: Smit <sm17p@users.noreply.github.com>
2026-02-22 17:29:53 -06:00
Tam Dang 078fd5990e fix(types): include CSS entrypoints in PublicPath generation (#2150)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Aaron b4be5feaa6 ci: Fix auto-labeling workflow for forks 2026-02-22 17:29:53 -06:00
Aaron d982f97bd8 feat(modules): Add support for augumenting entrypoint options (#2149) 2026-02-22 17:29:53 -06:00
Aaron 653608c98c docs: Add deep outline to modules page to show recipes 2026-02-22 17:29:53 -06:00
Aaron d63be2d6f3 ci: Create workflow for labeling and adding authors to PRs (#2148) 2026-02-22 17:29:53 -06:00
Nick Doan 4500ca577f fix(types): Add type safety to browser.runtime.executeScript files option (#2142)
Co-authored-by: nickbar01234 <nickbar01234gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-22 17:29:53 -06:00
Aaron cc935daec0 fix: Allow userId option to return undefined 2026-02-22 17:29:53 -06:00
Patryk Kuniczak 51ff46ec58 chore: Created new types, instead of any for analytics (#2119) 2026-02-22 17:29:53 -06:00
Patryk Kuniczak 206b7731cf chore: Remove @ts-expect-error from manifest.test.ts and fix typo (#2123) 2026-02-22 17:29:53 -06:00
Patryk Kuniczak d16f59d33d chore: Simplify imports in wxt/e2e (#2122) 2026-02-22 17:29:53 -06:00
Patryk Kuniczak a5ec046346 chore: Rename zipdir to zipDir (#2126) 2026-02-22 17:29:49 -06:00
Patryk Kuniczak 51dd803115 chore: Remove unnecessary @ts-ignore of builders/vite index.ts (#2127) 2026-02-21 10:47:39 -06:00
Patryk Kuniczak 6e39424a30 chore: Make type for __ENTRYPOINT__ of vitest.globalSetup.ts (#2128) 2026-02-21 10:46:36 -06:00
Anthony Ciccarello 6ca84782aa fix: enable mv3 dev mode with firefox 147 (#2135) 2026-02-21 16:45:04 +00:00
Henrik Wenz ee1ba6369e docs: Add wxt-local-analytics to community resources (#2141) 2026-02-21 16:41:13 +00:00
Aaron f35d70e6fe fix: Correctly update version metadata when setting a value for the first time (#2139)
Co-authored-by: willow <42willow@pm.me>
Co-authored-by: absdjfh <absdjfh@users.noreply.github.com>
2026-02-21 10:39:21 -06:00
sheng zhang 430a3acfeb fix: Add getAppConfig as an alias to useAppConfig (#2144)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-21 15:38:42 +00:00
aklinker1 395c5841f9 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-02-21 00:39:02 +00:00
Oleksii 173161ca4d feat: use Navigation API for location change detection with polling fallback (#2136) 2026-02-19 01:21:48 +00:00
Patryk Kuniczak a1a8e14368 feat: Create tests for all func of network.ts (#2132) 2026-02-18 19:14:36 -06:00
qixing-jk 2379806c61 docs: add All API Hub to showcase extensions (#2137) 2026-02-19 00:58:20 +00:00
Muzammil f604208a8b docs: Added "Glossy New Tab" to the homepage (#2133) 2026-02-15 03:48:59 +00:00
Patryk Kuniczak cf31fbec5d chore: add missing async to network.ts (#2107) 2026-02-13 12:18:37 +00:00
github-actions[bot] 279be8deb7 chore(release): @wxt-dev/storage v1.2.7
📼 VHS / Create VHS (push) Waiting to run
2026-02-13 00:01:40 +00:00
github-actions[bot] 7366d8aab7 chore(release): wxt v0.20.17
📼 VHS / Create VHS (push) Waiting to run
2026-02-12 22:35:05 +00:00
Aaron a7ab409d48 fix: Update left-over CJS require of publish-browser-extension to ESM (#2120) 2026-02-12 16:30:47 -06:00
Aaron 9dea0cf3ae chore: Cleanup type tests 2026-02-12 15:53:41 -06:00
Patryk Kuniczak 50d7c1ec80 chore: Change browser tests to fix deprecated warning (#2117)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-12 21:35:27 +00:00
github-actions[bot] f32cdafdf6 chore(release): @wxt-dev/analytics v0.5.2
📼 VHS / Create VHS (push) Waiting to run
2026-02-12 21:08:03 +00:00
Honwhy Wang 2e2272d07a fix: Allow custom API URL in Google Analytics 4 provider options (#1653)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-12 13:34:05 -06:00
github-actions[bot] 27ffff7e0a chore(release): wxt v0.20.16
📼 VHS / Create VHS (push) Waiting to run
2026-02-12 19:16:33 +00:00
Aaron 305cebe0d0 fix: Revert "rename name to names prop of rollup to fix deprecated warning (#2106)"
This reverts commit 9470d2b696.
2026-02-12 13:11:36 -06:00
Aaron 4602a068f8 Update contributor in changelog 2026-02-12 13:06:06 -06:00
Aaron 3a947d72c6 chore: Include doc updates in wxt changelog 2026-02-12 13:03:58 -06:00
Aaron 1940e3530f chore: Speed up E2E tests by skipping pnpm install when possible (#2113) 2026-02-11 12:24:36 -06:00
github-actions[bot] dd549dc686 chore(release): wxt v0.20.15
📼 VHS / Create VHS (push) Waiting to run
2026-02-11 07:13:31 +00:00
Nick Doan 3417bd3ed2 feat: Add log --level cli option (#1973)
Co-authored-by: nickbar01234 <nickbar01234gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-11 04:57:27 +00:00
Patryk Kuniczak 9470d2b696 fix: rename name to names prop of rollup to fix deprecated warning (#2106) 2026-02-10 12:23:19 -06:00
Aaron f523a7d31d docs: Add notes about chrome services and feature flags (#2110) 2026-02-10 12:22:18 -06:00
Patryk Kuniczak ea4c2b035b fix: Remove prepublish script of root package.json (#2109) 2026-02-10 10:11:47 +01:00
willow c5bfba837f fix(storage): Set version number on init (#1996) 2026-02-10 01:41:00 +00:00
aklinker1 c5ad957b25 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-02-10 00:47:54 +00:00
Patryk Kuniczak cc849e7cea docs: rename keys name of getMetas() to be proper (#2105) 2026-02-09 22:28:49 +01:00
Patryk Kuniczak 22428da54d chore: simplify imports of cli/__tests__/index.ts (#2104) 2026-02-09 20:59:46 +00:00
Patryk Kuniczak f6071f7e2d fix: Change deprecated function presetUno to presetWind3 (#2103) 2026-02-09 21:55:28 +01:00
Aaron 08e8560865 chore: Upgrade PNPM to 10.29.2 (#2102) 2026-02-09 12:46:47 -06:00
Aaron 75f01db543 refactor: Simplify import paths (#2093)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 02:09:51 -06:00
Aaron 5a028d2551 chore: Remove unused code (#2087)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:45:11 -06:00
Aaron 07a4ba8bc5 fix: Make ExtensionRunner#closeBrowser optional (#2092)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:44:35 -06:00
Aaron f84921f735 chore: Fix typos (#2086)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:39:46 -06:00
Aaron 872494e490 chore: Update JSDoc to use @internal (#2091)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:39:07 -06:00
Aaron d0848b0be5 chore: Cleanup @ts-* comments (#2094)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:38:42 -06:00
Aaron c848956777 chore: Cleanup comment line lengths (#2090)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:38:09 -06:00
Aaron 296997d10e refactor: Cleanup unnecssary checks (#2089)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:37:47 -06:00
Aaron ffdb22238c refactor: async/await cleanup (#2088)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:37:16 -06:00
Aaron a63c320403 refactor: Code cleanup for storage package (#2085)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:36:40 -06:00
Aaron aa86ed78a3 refactor: Code cleanup in analytics package (#2084)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:36:15 -06:00
wotan-allfather 19456ec855 feat(popup): Add Firefox default_area and theme_icons support (#2097)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 07:28:26 +00:00
Aaron 2c57840c44 revert: Use uiContainer to apply position when creating shadow root ui (#2036)
This reverts commit 26b7927f62.
2026-02-09 00:44:07 -06:00
Namu 26b7927f62 fix: Use uiContainer to apply position when creating shadow root ui (#2036)
Co-authored-by: Namu <535248+namukang@users.noreply.github.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 06:32:35 +00:00
Eli d4267af725 refactor: cleanup esm (#1950)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 04:27:51 +00:00
ariasuni 16a2aedcd0 docs: Small improvements for hooks (#2095)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 02:46:40 +00:00
Puzant 755f04f777 docs: Added KeyFloat (#2082)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 02:43:58 +00:00
Namu cf9539ddab fix: content script is incorrectly invalidated when injected multiple times (#2035)
Co-authored-by: Namu <535248+namukang@users.noreply.github.com>
2026-02-09 02:38:54 +00:00
Aaron 99d558f33c refactor: Standardize file existence checks to pathExists (#2083)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-08 15:21:45 -06:00
Patryk Kuniczak 1fe073004b fix: normalize path for createAnalytics of analytics/index.ts (#2013) 2026-02-08 08:05:09 -06:00
Aaron 6ae5fb5edb docs: Fix JSDoc 2026-02-08 08:03:46 -06:00
Patryk Kuniczak 3a3953edf6 feat: Export normalizePath from wxt module (#2080) 2026-02-08 08:18:49 +00:00
rxliuli c8e788eaf8 docs: Add blog post resource for building browser extensions (#2077) 2026-02-07 13:40:14 +00:00
Aaron 088a44496d docs: Add llms.txt to documentation site (#1725) 2026-02-06 16:38:37 -06:00
Aaron 60a57c5abc chore: Fix markdownlint issues 2026-02-06 16:34:08 -06:00
Aaron 482466bbbf docs: Update MAINTAINERS.md
Added an example of how to ask for a bug reproduction.
2026-02-06 15:10:11 -06:00
Aaron 65deb7317b chore: Upgrade github actions (#2073) 2026-02-06 14:14:19 -06:00
Patryk Kuniczak 00ae061822 chore: fix recognize lack of argument for scripts (#2015) 2026-02-06 14:04:14 -06:00
Aaron 9bd7fc1bd5 chore: Don't report unreleased chore/doc commits 2026-02-06 13:36:04 -06:00
Aaron 98ddb83f1d docs: Add PR guidelines for small and targeted changes 2026-02-06 10:55:00 -06:00
Aaron 6dd8b9d072 docs: Add MAINTAINERS.md file for notes specific to maintainers 2026-02-06 10:41:57 -06:00
github-actions[bot] 1e3482a878 chore(release): wxt v0.20.14
📼 VHS / Create VHS (push) Waiting to run
2026-02-06 15:56:04 +00:00
Aaron 734846843f ci: Update templates when bumping WXT version
This closes #2018
2026-02-06 09:51:10 -06:00
Aaron f1eac66494 chore: Fix markdown lint errors 2026-02-06 09:33:06 -06:00
Aaron b4c3280f58 docs: Add new sections to CONTRIBUTING.md
Added sections on WXT's vision, breaking changes policy, and becoming a maintainer.
2026-02-06 09:31:19 -06:00
Invictus Navarchus b2f2998767 chore: update wxt version to ^0.20.13 in all templates (#2019)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-05 19:36:50 -06:00
blurredart 720eb93c67 docs: add missing import in code sample for configuration (#2057)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-05 17:31:56 -06:00
dareka cdf8611e0b fix: MaxListenersExceededWarning (#2058) 2026-02-05 17:31:44 -06:00
Scan 905d797b65 docs: Add Margin extension to extensions using wxt (#2060)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-05 17:22:09 -06:00
tsubasa-ito 6ca8ccc332 docs: add Lofi BGM Player to example projects (#2056)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-05 17:21:18 -06:00
Nazım Gediz Aydındoğmuş 76496e8865 docs: Added "Teams Chat Exporter" to the homepage (#2050)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-05 17:20:15 -06:00
Muzammil f38fec8588 docs: Added "Capture It - Capture & Edit Screenshots" to the homepage (#2033)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-05 17:18:40 -06:00
Ilya Kubariev 4351ac9657 fix: Add .jsonc support for locale files to match docs (#2066) 2026-02-05 17:17:26 -06:00
Mark f3834ac0e2 fix(content-script-context): handle undefined browser.runtime (#2042) 2026-02-05 17:17:16 -06:00
ariasuni 48891a7ca5 fix: rename and make firefoxPrefs in web-ext.config.ts work (#2068) 2026-02-05 17:16:58 -06:00
Aaron 3b6a4aa635 chore: Refresh lockfile and upgrade subdependencies (#2071) 2026-02-05 17:10:47 -06:00
Aaron b20f0471c2 chore: Upgrade major deps (#2070) 2026-02-05 16:57:49 -06:00
Aaron 0f9e66e1e8 chore: Update dependencies (#2069) 2026-02-05 16:37:13 -06:00
Aaron 6fb4cee43f fix: Remove failing test 2026-02-05 16:13:03 -06:00
aklinker1 0e8afbbf56 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-02-04 00:37:48 +00:00
aklinker1 83decf7bf5 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-01-22 00:35:56 +00:00
aklinker1 5c77cf23b8 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-01-21 00:35:03 +00:00
aklinker1 606fd3164c fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-01-19 00:36:13 +00:00
aklinker1 71308d0d6f fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-01-11 00:37:39 +00:00
Patryk Kuniczak 8bbea7cda4 docs: Fix alignment of 2 last comments of regex patterns (#2012) 2025-12-23 09:42:43 -06:00
Aaron a87d3e4809 chore: Move script-only dev dependencies to top-level package.json (#2007) 2025-12-18 00:28:49 -06:00
Aaron 1f3db58254 chore: Use tsdown to build packages (#2006) 2025-12-18 00:02:19 -06:00
Aaron 43f6c06271 Update out-of-date lockfile 2025-12-17 21:30:07 -06:00
aklinker1 04ae542b13 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-12-17 00:32:12 +00:00
bohuslavsemenov f97c81df0d feat: add onBeforeMount callback to createIframeUi options (#1899)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 17:13:33 -06:00
github-actions[bot] b7b3fc53eb chore(release): wxt v0.20.13
📼 VHS / Create VHS (push) Waiting to run
2025-12-16 23:10:49 +00:00
rxliuli 4b44fb849d fix: Don't return promises from unlisted scripts that do not have an async main function (#1907)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:58:46 -06:00
Dan McGee 5d0a266e9c fix: Add another defineItem signature when init function is passed (#1909) 2025-12-16 16:56:24 -06:00
Sebastian Landwehr 24667861db feat: Support .wxtrc config file (#1833)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:33:37 -06:00
Johan Kiviniemi 25cc403886 refactor: Use script.text instead of innerHTML in injectScript (#1764)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:17:07 -06:00
Johan Kiviniemi 923312c482 feat: Make injectScript return the created script element (#1838)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:09:07 -06:00
Johan Kiviniemi adeef6eb73 fix: Make injectScript wait until script is actually loaded (#1763)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 15:54:41 -06:00
Johan Kiviniemi 6f84d45960 feat: Add modifyScript option to injectScript (#1762)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 15:47:40 -06:00
Johan Kiviniemi 938c25cab4 feat: Remove script element immediately in injectScript (#1761)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 15:42:56 -06:00
github-actions[bot] 0404637da8 chore(release): wxt v0.20.12
📼 VHS / Create VHS (push) Waiting to run
2025-12-16 20:57:58 +00:00
Aaron 98d95e4b7f chore(deps): Upgrade node-forge to address CVE-2025-12816, CVE-2025-66031 (#2003) 2025-12-16 14:50:50 -06:00
Aaron 2687a35781 chore(dev-deps): Upgrade happy-dom to address CVE-2025-61927 (#2002) 2025-12-16 14:41:09 -06:00
Aaron 886dae28fa chore: Upgrade dev and non-major prod dependencies (#2000) 2025-12-16 14:33:59 -06:00
Aaron 8c9bc8cdbc chore: Add support for vite-node v5 (#2001) 2025-12-16 11:46:06 -06:00
Jaguar Zhou 5414a0aa34 fix: wxt prepare fails with the error "__vite_ssr_exportName__ is not defined" when using Vite 7 (#1884)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 11:04:14 -06:00
K.Utsunomiya 223cf8613b docs: Add note about reviewing project structure in migration guide (#1990) 2025-12-16 08:36:02 -06:00
Alexander Harding 4f1776befc feat: Default to using use_dynamic_url: true for content script css files (#1993)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-15 19:04:07 -06:00
Aaron 38473b9f6c chore: Fix formatting 2025-12-15 18:42:23 -06:00
Manabu Nakazawa 41b428c75f docs: Add "Redirect Web" to homepage (#1995)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-15 18:32:41 -06:00
yiang231 d8d754658f docs: Add "defineConfig" config to startup examples (#1989) 2025-12-15 18:31:43 -06:00
Shawn e52ebf1d53 docs: Change manifest.title to manifest.name in build:manifestGenerated hook example (#1985) 2025-12-15 18:29:10 -06:00
Nguyen Canh Toan 535c7a5f92 docs: add Zen Virtual Piano & Crypto Pulse to example projects (#1977) 2025-12-15 18:27:21 -06:00
TomvdPeet 99b2bd784a Fix comments for includeSources and excludeSources (#1998)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-15 18:19:48 -06:00
Aaron 31ebf966fd chore: Fix other type error after chrome types update 2025-12-15 18:19:25 -06:00
Aaron 6036c6e8e0 chore: Fix type errors after chrome type upgrades 2025-12-15 18:14:29 -06:00
Aaron 597151b5dd ci: Fix browser auto-release workflow 2025-12-15 18:07:44 -06:00
aklinker1 29aeaed223 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-12-11 18:41:00 +00:00
Alex Khrisanfov edf87952c7 ci: Fix update-browser-package action (#1992)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-11 12:39:18 -06:00
Aaron 78f8434a06 chore: Use linkedom a test instead of happy-dom (#1957) 2025-10-27 18:11:56 -05:00
Aaron 7eaccad683 chore: Update lockfile 2025-10-27 17:49:24 -05:00
Aaron bfa9c1e1c8 chore: Move PNPM settings to pnpm-workspace.yaml folder 2025-10-27 17:39:59 -05:00
Aaron a2d6577579 docs: Fix syntax error 2025-10-27 17:20:59 -05:00
Ethan Lanting 0e8191488f docs: Added "BetterCampus (prev. BetterCanvas)" to the homepage (#1888)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-10-27 17:08:12 -05:00
Shahil Yadav a1510bb081 docs: Added "Leetcode Fonts" to the homepage (#1889)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-10-27 17:07:05 -05:00
Fantasy 1eaca30f97 docs: Added "sitedata" and "TranslateManga" to homepage (#1902)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-10-27 17:05:52 -05:00
yukidaruma bc79987ed5 docs: Added "Livestream Chat Reader" to the homepage (#1910)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-10-27 17:04:46 -05:00
Alexander Harding 36a501506e feat: Remove data-wxt-* attributes (#1913) 2025-10-27 17:03:01 -05:00
dependabot[bot] e1d47f5f63 chore(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#1920)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 16:55:26 -05:00
dependabot[bot] 7faa9b5a64 chore(deps): bump docker/login-action from 3.5.0 to 3.6.0 (#1921)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 16:55:00 -05:00
dependabot[bot] 295bee708c chore(deps): bump actions/github-script from 7.0.1 to 8.0.0 (#1922)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 16:54:32 -05:00
Burak Unver 9e917481c4 docs: Added "ChatGPT Token Counter" to the homepage (#1926)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-10-27 16:53:41 -05:00
anghunk c5a8b9fb3d docs: Add LinuxDo Scripts extension to the list (#1940) 2025-10-27 16:26:46 -05:00
Florian Kühne 3f4e32f8da fix: Optimize splitShadowRootCss (#1934)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-10-27 16:06:01 -05:00
Aaron f630098f1d chore: Upgrade @aklinker1/check to reduce dev dep size 2025-10-27 09:09:20 -05:00
Eli 608d6e2b54 ci: Don't run cron jobs on forks (#1951) 2025-10-26 20:53:35 -05:00
github-actions[bot] fad5ab6c44 chore(release): @wxt-dev/storage v1.2.6
📼 VHS / Create VHS (push) Waiting to run
2025-10-25 16:40:59 +00:00
Aaron d4cb7997fe ci: Publish packages using OIDC with correctly generated package.json
files
2025-10-25 11:34:59 -05:00
Carlos Ortiz 28364b8be1 ci: publish using pnpm instead of npm (#1943) 2025-10-24 21:11:43 -05:00
github-actions[bot] d28a5c7d5e chore(release): @wxt-dev/storage v1.2.5
📼 VHS / Create VHS (push) Waiting to run
2025-10-13 03:09:36 +00:00
Aaron df707aef4c ci: Use correct version of NPM that supports OIDC auth 2025-10-12 22:02:37 -05:00
Aaron 77af2e58ec ci: Log all NPM versions on runner 2025-10-12 21:59:34 -05:00
Aaron f738e56337 ci: Upgrade actions/setup-node to v5 2025-10-12 21:57:24 -05:00
Aaron f2fe2584e0 ci: try out ubuntu-24.04 runner 2025-10-12 21:53:28 -05:00
Aaron 02673ae017 ci: Debug installed NPM versions more 2025-10-12 21:51:37 -05:00
Aaron 8dd294a163 ci: Disable publishing while debugging NPM changes 2025-10-12 21:49:01 -05:00
Aaron 469e735d53 ci: List installed NPM version 2025-10-12 21:47:57 -05:00
github-actions[bot] b02001796e chore(release): @wxt-dev/storage v1.2.4
📼 VHS / Create VHS (push) Waiting to run
2025-10-13 02:46:42 +00:00
Aaron 0d4d2fdd7e ci: Print NPM version 2025-10-12 21:42:15 -05:00
github-actions[bot] b65e5446fa chore(release): @wxt-dev/storage v1.2.3
📼 VHS / Create VHS (push) Waiting to run
2025-10-13 02:00:40 +00:00
Aaron f75fe3ce5b ci: Separate NPM upgrade from publish step 2025-10-12 20:52:54 -05:00
182 changed files with 10046 additions and 7043 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ runs:
uses: pnpm/action-setup@f2b2b233b538f500472c7274c7012f57857d8ce0 # v4.1.0
- name: 🛠️ Setup NodeJS
uses: actions/setup-node@b7234cc9fe124f0f4932554b4e5284543083ae7b # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 20
cache: pnpm
+111
View File
@@ -0,0 +1,111 @@
name: ✨ Auto-label PR
on:
pull_request_target:
types: [opened, synchronized, reopened]
jobs:
update-pr:
name: Update PR
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Gather Info
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});
// Check if PR has assignees
const hasAssignees = pr.assignees && pr.assignees.length > 0;
core.setOutput('has_assignees', hasAssignees);
core.setOutput('author', pr.user.login);
// Get list of changed files
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});
// Find all packages that were modified
const packagesRegex = /^packages\/([^\/]+)\//;
const affectedPackages = new Set();
for (const file of files) {
const match = file.filename.match(packagesRegex);
if (match) {
affectedPackages.add(match[1]);
}
}
const labels = Array.from(affectedPackages).map(pkg => `pkg/${pkg}`);
core.setOutput('labels', JSON.stringify(labels));
console.log('Detected package labels:', labels);
// Get current labels on the PR that match pkg/* pattern
const currentPkgLabels = pr.labels
.map(label => label.name)
.filter(name => name.startsWith('pkg/'));
core.setOutput('current_pkg_labels', JSON.stringify(currentPkgLabels));
console.log('Current pkg labels:', currentPkgLabels);
- name: Sync Author
if: steps.check.outputs.has_assignees == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
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 }}']
});
console.log('Assigned PR author: ${{ steps.check.outputs.author }}');
- name: Sync Labels
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const newLabels = ${{ steps.check.outputs.labels }};
const currentLabels = ${{ steps.check.outputs.current_pkg_labels }};
// Find labels to add (in newLabels but not in currentLabels)
const labelsToAdd = newLabels.filter(label => !currentLabels.includes(label));
// Find labels to remove (in currentLabels but not in newLabels)
const labelsToRemove = currentLabels.filter(label => !newLabels.includes(label));
// Add new labels
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labelsToAdd
});
console.log('Added labels:', labelsToAdd);
}
// Remove obsolete labels
for (const label of labelsToRemove) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: label
});
console.log('Removed label:', label);
}
if (labelsToAdd.length === 0 && labelsToRemove.length === 0) {
console.log('No label changes needed');
}
@@ -7,9 +7,10 @@ on:
jobs:
notify:
runs-on: ubuntu-22.04
if: github.repository_owner == 'wxt-dev'
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Post Thank You Comment
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
comment: Thanks for helping make WXT better!
with:
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Only fetch the config file from the repository
sparse-checkout-cone-mode: false
+2 -2
View File
@@ -23,13 +23,13 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
- name: Login to Docker Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
+5 -3
View File
@@ -10,6 +10,7 @@ on:
- analytics
- auto-icons
- i18n
- is-background
- module-react
- module-solid
- module-svelte
@@ -31,7 +32,7 @@ jobs:
publish:
name: Publish
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: write # Push version changes
id-token: write # OIDC for NPM publishing
@@ -39,7 +40,7 @@ jobs:
- validate
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example
@@ -62,8 +63,9 @@ jobs:
- name: Publish to NPM
working-directory: packages/${{ inputs.package }}
run: |
pnpm pack
sudo npm i -g npm@latest
npm publish
/usr/local/bin/npm publish *.tgz
- name: Create GitHub release
run: pnpm tsx scripts/create-github-release.ts ${{ inputs.package }}
+2 -1
View File
@@ -10,6 +10,7 @@ on:
- analytics
- auto-icons
- i18n
- is-background
- module-react
- module-solid
- module-svelte
@@ -30,7 +31,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+8 -4
View File
@@ -11,12 +11,15 @@ jobs:
sync:
name: 'Sync with @types/chrome'
runs-on: ubuntu-latest
if: github.repository_owner == 'wxt-dev'
permissions:
contents: write # Push version changes
id-token: write # OIDC for NPM publishing
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Setup
uses: ./.github/actions/setup
@@ -33,15 +36,16 @@ jobs:
- name: Commit Changes
id: commit
uses: stefanzweifel/git-auto-commit-action@01d77ca6cb089da1360e540865f7d035c95aa199 # v6.0.1
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
commit_message: 'fix: Upgrade `@wxt-dev/browser` to latest `@types/chrome` version'
- name: Publish Package
- name: Publish to NPM
if: steps.commit.outputs.changes_detected == 'true'
working-directory: packages/browser
run: |
pnpm pack
sudo npm i -g npm@latest
npm publish
/usr/local/bin/npm publish *.tgz
+7 -7
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -68,13 +68,13 @@ jobs:
coverage: false
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
- name: Setup Bun
uses: oven-sh/setup-bun@22457c87c1b161cf7dde222c3e82b2b5f8d2bed2 # v2.0.2
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
- name: Run Tests
if: ${{ ! matrix.coverage }}
@@ -86,7 +86,7 @@ jobs:
- name: Upload Coverage
if: matrix.coverage
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -104,7 +104,7 @@ jobs:
- vue
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+12 -6
View File
@@ -11,19 +11,24 @@ permissions:
jobs:
vhs:
name: Create VHS
runs-on: ubuntu-22.04
runs-on: macos-latest
if: ${{ github.repository == 'wxt-dev/wxt' }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
with:
install: false
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
with:
go-version: '1.25.1'
# This prevents pnpm dlx from downloading WXT in the video
- name: Pre-install WXT
run: |
@@ -31,12 +36,13 @@ jobs:
pnpm dlx wxt@latest --version
- name: Record VHS
uses: charmbracelet/vhs-action@59641cdc7fadf3978db65eb8c6937ea2752f4ec3 # v2.1.0
with:
path: 'docs/tapes/init-demo.tape'
run: |
brew install ttyd ffmpeg
go install github.com/charmbracelet/vhs@517bcda0faf416728bcf6b7fe489eb0e2469d9b5 # v0.10.0
vhs docs/tapes/init-demo.tape
- name: Save recorded GIF
uses: stefanzweifel/git-auto-commit-action@01d77ca6cb089da1360e540865f7d035c95aa199 # v6.0.1
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+2 -1
View File
@@ -10,5 +10,6 @@
// Additional guidelines for Copilot
"github.copilot.chat.codeGeneration.instructions": [
{ "file": "CONTRIBUTING.md" }
]
],
"oxc.enable": true
}
+32 -7
View File
@@ -6,6 +6,19 @@ If you are changing the docs or fixing a bug, feel free to fork and open a PR.
If you want to add a new feature, please create an issue or discussion first so we can decide if the feature is inline with the vision for WXT.
## WXT's Vision
WXT is two things:
1. A build tool
2. A set of runtime utilities
The long term goal of WXT is provide an opinionated build tool that keeps WXT projects standard, while providing light-weight runtime utils that simplify a lot of the boilerplate/overhead when setting up a new extension.
I also want to provide a way for developers to use either one of those two things independently, and not require them to use both. This is why all of WXT's runtime utils are shipped as their own NPM packages, most of them not bundled inside the core `wxt` package. If you just want to use the packages, they're availalbe, and if you just want to use WXT's built tool, you don't have to import any of WXT's utilities, you can use your own.
> The few runtime utils shipped inside WXT are things that should be used by 90% of extensions. That said, they're also legacy utils left in from before I started creating separate NPM packages, and in the future, they may be removed from the core package.
## Conventional Commits
This project uses [Conventional Commit format](https://www.conventionalcommits.org/en/v1.0.0/) to automatically generate a changelog and better understand the changes in the project
@@ -20,6 +33,19 @@ Here are some examples of conventional commit messages:
The title of your pull request should follow the [conventional commit format](#conventional-commits). When a pull request is merged to the main branch, all changes are going to be squashed into a single commit. The message of this commit will be the title of the pull request. And for every release, the commit messages are used to generate the changelog.
## Breaking Changes Policy
A quick word on WXT's breaking changes policy. I am willing to make breaking changes, but they have to be for a good enough reason - they have to make WXT better as a whole, they can't be based on one opinion.
Breaking changes also require a major release. Major releases have happened once or twice a year, so after you merge your PR, you'll have to wait a little bit before it is released.
To make a breaking change:
1. Make sure you're PR is targeting the `major` branch
2. Add `!` after the conventional commit type (`fix!: ...`, `feat!: ...`, `chore!: ...`, etc) to indicate that it is a breaking change
3. At the top of the PR, provide documentation that will inform developers about the breaking change, why it was done, and how to migrate their extension so nothing breaks.
- This documentation will be put ["Upgrading WXT"](https://wxt.dev/guide/resources/upgrading.html) page in the docs, read through previous breaking change docs for an idea of what is required.
## Setup
WXT uses `pnpm`, so make sure you have it installed.
@@ -137,13 +163,6 @@ cp -r templates/vanilla templates/<new-template-name>
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
## Releasing Updates
Releases are done with GitHub actions:
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
## Upgrading Dependencies
WXT has custom rules around what dependencies can be upgraded. Use the `scripts/upgrade-deps.ts` script to upgrade dependencies and follow these rules.
@@ -188,3 +207,9 @@ Anyone is welcome to submit a blog post on <https://wxt.dev/blog>!
- **English only**: Blog posts should be written in English. Unfortunately, our maintainers don't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓
- **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that.
## Become a Maintainer
If you're interested in becoming a maintainer, send an email to Aaron at <aaronklinker1@gmail.com> with your github username saying you're interested. The process is very informal, I will add you quickly if you've contributed code or answered questions and helped out the community!
Maintainers don't have to just write code - they can manage issues, answer questsions, review PRs, organize and prioritize work - there's lots of ways for you to help out.
+90
View File
@@ -0,0 +1,90 @@
# Maintainers
A couple of things for you to consider before merging a PR or giving the go-ahead on a feature proposal.
## Be picky about new features and packages
We are responsible for maintaining them and fixing related bugs after the PR is merged. The community can always release their own WXT modules or packages, not everything needs to be built into WXT.
## Prefer standards over customization
Don't merge PRs that just add another way to do something, like [this one](https://github.com/wxt-dev/wxt/pull/2053#issuecomment-3857010196).
WXT is opinionated, if you have questions about what is WXT's opinion or we need to create a new one, create an issue and @ me to discuss.
## PRs should be small and targeted
A PR should make one change. They should not make any unrelated changes outside of accomplishing the one thing. This makes PRs easier to review and they get merged more quickly - a win-win for everyone.
## `@wxt-dev/*` packages are separate
We can't make changes to these packages assuming people are using them only with WXT. For example, I almost missed [this PR](https://github.com/wxt-dev/wxt/pull/2049#issuecomment-3861251599).
I want these packages to be usable on their own so if people don't like WXT's build tool, they can still use our other awesome packages.
## Minimize dependencies
I don't like how heavy lots of frameworks are. It's unavoidable to a certain extent, but if you can do something without another dependency, try to.
If you need a dependency, look for one with zero dependencies and that's respectable.
## Look at milestones
I've organized WXT's long term plans into [milestones](https://github.com/wxt-dev/wxt/milestones), check those out to get an idea of my priorities for some of the larger features.
## Require tests
If someone opens a PR to fix a bug but doesn't include tests, don't merge the PR. Tests are required to prevent regressions and maintain a codebase long term.
## Ask for reproductions for bugs
You don't need to triage bugs if someone doesn't give you enough information. You can always ask for a repo with a reproduction or wait for more details.
If there's not an easy way to reproduce a bug, you're wasting your time triaging it. Just be mindful of your own time!
Here's an example of how to ask for a reproduction: <https://github.com/wxt-dev/wxt/issues/2064#issuecomment-3862579110>. You could be more blunt than this.
## Add yourself as a code owner
If you want to be responsible for a specific package or directory, add yourself to the [`.github/CODEOWNERS`](https://github.com/wxt-dev/wxt/blob/main/.github/CODEOWNERS) file to get added as a reviewer to PRs automatically. You can also add yourself to the default list to be added as a reviewer on all PRs.
## Releasing Package Updates
Releases are done with GitHub actions:
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
## Creating New Packages
Example PR: <https://github.com/wxt-dev/wxt/pull/2152>
1. Create the package.
2. Update CI workflow inputs.
3. Add docs page and version for "Other Packages" dropdown.
4. Merge the PR.
5. Tag the commit (look at other tags for pattern):
```sh
git tag <dir-name>-v<version>
git push --tags
```
6. Publish the package to NPM:
```sh
cd packages/<dir-name>
pnpm publish --access public
```
7. Create a basic release on GitHub mentioning the new package is available.
A couple of things to note:
- pkg.pr.new will fail on the original PR. It's fine to ignore and merge your PR as long as it fails due to your new package not being published to NPM yet.
- The regular release workflow DOES NOT WORK for new packages. You have to have at least one `<dir-name>-v<version>` tag created before you can run that workflow for your new package.
- You don't need to create a CHANGELOG.md file for the package, it will be created automatically after future changes are released via the normal release workflow.
+2 -6
View File
@@ -4,12 +4,8 @@ While WXT is in prerelease, only the latest version will receive security update
<img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
<!--
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
If you discover a security vulnerability, please email me at <aaronklinker1@gmail.com>. I will respond within a few days to acknowledge receipt of your report.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
-->
If the vulnerability is accepted, I will open a public issue to track the fix. If the vulnerability is not accepted, no further action will be taken.
@@ -102,6 +102,23 @@ const chromeExtensionIds = [
'ehmoihnjgkdimihkhokkmfjdgomohjgm', // Filmbudd Pro - Simple, private and synced ratings and watch notes across all your devices
'alglchohmdikgdjhafiicilegegieafa', // MultiField CopyCat - Copy, Paste & Autofill Web Forms Instantly
'aamihahiiogceidpbnfgehacgiecephe', // ChatSight - Add Table of Contents to ChatGPT
'cndibmoanboadcifjkjbdpjgfedanolh', // BetterCampus (prev. BetterCanvas)
'hinfimgacobnellbncbcpdlpaapcofaa', // Leetcode Fonts - Change fonts in leetcode effortlessly
'kbkbfefhhabpkibojinapkkgciiacggg', // TranslateManga - Manga Translator & Manga Tracker
'emeakbgdecgmdjgegnejpppcnkcnoaen', // SiteData - Free Website Traffic Checker & Reverse AdSense Tool
'gpnckbhgpnjciklpoehkmligeaebigaa', // Livestream Chat Reader - Text-to-Speech for YouTube/Twitch chat
'fjlalaedpfcojcfpkgkglbjjbbkofgnl', // ChatGPT Token Counter - Count tokens in real time on chatgpt conversation
'fbgblmjbeebanackldpbmpacppflgmlj', // LinuxDo Scripts - 为 linux.do 用户提供了一些增强功能
'dfacnjidgbagicaekenjgclfnhdnjjdi', // Zen Virtual Piano - https://zen-piano.web.app/
'naeibcegmgpofimedkmfgjgphfhfhlab', // Crypto Pulse price tracker - https://get-crypto-pulse.web.app/
'ffglckbhfbfmdkefdmjbhpnffkcmlhdh', // Redirect Web - Automatically redirect pages or open them in another app
'eglpfhbhmelampoihamjomgkeobgdofl', // Capture It - Capture & Edit Screenshots
'jmghclbfbbapimhbgnpffbimphlpolnm', // Teams Chat Exporter
'jdcppdokgfbnhiacbeplahgnciahnhck', // Lofi BGM Player - Free lofi focus music for work & study
'cgpmbiiagnehkikhcbnhiagfomajncpa', // Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.
'mfjdonmgmgcijagclnkfhmjiblbfjaid', // KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows
'dhiekgdaipindoapjmcnpompdknjeijf', // Glossy New Tab - Say Goodbye to Boring Tabs with live wallpapers
'lapnciffpekdengooeolaienkeoilfeo', // All API Hub AI Relay & New API Manager - https://github.com/qixing-jk/all-api-hub
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+4
View File
@@ -16,6 +16,7 @@ import { version as unocssVersion } from '../../packages/unocss/package.json';
import { version as storageVersion } from '../../packages/storage/package.json';
import { version as analyticsVersion } from '../../packages/analytics/package.json';
import { version as runnerVersion } from '../../packages/runner/package.json';
import { version as isBackgroundVersion } from '../../packages/is-background/package.json';
import addKnowledge from 'vitepress-knowledge';
import {
groupIconMdPlugin,
@@ -25,6 +26,7 @@ import {
import { Feed } from 'feed';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import llmstxt from 'vitepress-plugin-llms';
const origin = 'https://wxt.dev';
@@ -43,6 +45,7 @@ const otherPackages = {
storage: storageVersion,
unocss: unocssVersion,
runner: runnerVersion,
'is-background': isBackgroundVersion,
};
const knowledge = addKnowledge<DefaultTheme.Config>({
@@ -71,6 +74,7 @@ export default defineConfig({
vite: {
clearScreen: false,
plugins: [
llmstxt(),
groupIconVitePlugin({
customIcon: {
'wxt.config.ts': localIconLoader(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 136 KiB

@@ -32,6 +32,8 @@ You can configure browser startup in 3 places:
To set or customize the browser opened during development:
```ts [web-ext.config.ts]
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
binaries: {
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
@@ -41,6 +43,18 @@ export default defineWebExtConfig({
});
```
```ts [wxt.config.ts]
export default defineConfig({
webExt: {
binaries: {
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
},
},
});
```
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
### Persist Data
@@ -49,11 +63,13 @@ By default, to keep from modifying your browser's existing profiles, `web-ext` c
Right now, Chromium based browsers are the only browsers that support overriding this behavior and persisting data when running the `dev` script multiple times.
To persist data, set the `--user-data-dir` flag:
To persist data, set the `--user-data-dir` flag in any of the config files mentioned above:
:::code-group
```ts [Mac/Linux]
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
});
@@ -61,6 +77,7 @@ export default defineWebExtConfig({
```ts [Windows]
import { resolve } from 'node:path';
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
// On Windows, the path must be absolute
@@ -82,7 +99,30 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
```ts [web-ext.config.ts]
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
disabled: true,
});
```
### Enabling Chrome Features
Some APIs are disabled in Chrome during development because of the default flags `web-ext` uses to launch Chrome, like the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api).
If your extension depends on new features or services, you can enable them via `chromiumArgs`:
```ts
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
chromiumArgs: [
// For example, this flag enables the Prompt API
'--disable-features=DisableLoadExtensionCommandLineSwitch',
],
});
```
There is no comprehensive list of what feature flags enable what APIs and services.
Alternatively, if you can't find a flag that enables a feature you're looking for, [disable the opening the browser during development](#disable-opening-browser) and use your regular chrome profile for development.
+3 -1
View File
@@ -11,7 +11,7 @@ export default defineConfig({
hooks: {
'build:manifestGenerated': (wxt, manifest) => {
if (wxt.config.mode === 'development') {
manifest.title += ' (DEV)';
manifest.name += ' (DEV)';
}
},
},
@@ -20,6 +20,8 @@ export default defineConfig({
Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.
You can find the [list of all available hooks](/api/reference/wxt/interfaces/WxtHooks) in the API reference.
Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules) instead.
## Execution Order
+3 -3
View File
@@ -23,12 +23,12 @@ export default defineAppConfig({
This file is committed to the repo, so don't put any secrets here. Instead, use [Environment Variables](/guide/essentials/config/environment-variables)
:::
To access runtime config, WXT provides the `useAppConfig` function:
To access runtime config, WXT provides the `getAppConfig` function:
```ts
import { useAppConfig } from '#imports';
import { getAppConfig } from '#imports';
console.log(useAppConfig()); // { theme: "dark" }
console.log(getAppConfig()); // { theme: "dark" }
```
## Environment Variables in App Config
+70
View File
@@ -591,6 +591,76 @@ For MV3, `injectScript` is synchronous and the injected script will be evaluated
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
:::
The `script` element can be modified just before it is added to the DOM by using the `modifyScript` option. This can be used to e.g. modify `script.async`/`script.defer`, add event listeners to the element, or pass data to the script via `script.dataset`. An example:
```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
await injectScript('/example-main-world.js', {
modifyScript(script) {
script.dataset['greeting'] = 'Hello there';
},
});
},
});
```
```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
console.log(document.currentScript?.dataset['greeting']);
});
```
`injectScript` returns the created script element. It can be used to e.g. send messages to the script in the form of custom events. The script can add an event listener for them via `document.currentScript`. An example of bidirectional communication:
```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
const { script } = await injectScript('/example-main-world.js', {
modifyScript(script) {
// Add a listener before the injected script is loaded.
script.addEventListener('from-injected-script', (event) => {
if (event instanceof CustomEvent) {
console.log(`${event.type}:`, event.detail);
}
});
},
});
// Send an event after the injected script is loaded.
script.dispatchEvent(
new CustomEvent('from-content-script', {
detail: 'General Kenobi',
}),
);
},
});
```
```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
const script = document.currentScript;
script?.addEventListener('from-content-script', (event) => {
if (event instanceof CustomEvent) {
console.log(`${event.type}:`, event.detail);
}
});
script?.dispatchEvent(
new CustomEvent('from-injected-script', {
detail: 'Hello there',
}),
);
});
```
## Mounting UI to dynamic element
In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
+13
View File
@@ -470,6 +470,19 @@ When you define a Newtab entrypoint, WXT will automatically update the manifest
/>
<meta name="manifest.type" content="page_action|browser_action" />
<meta name="manifest.browser_style" content="true|false" />
<!-- Firefox only: where to place the action button -->
<meta
name="manifest.default_area"
content="navbar|menupanel|tabstrip|personaltoolbar"
/>
<!-- Firefox only: icons for light/dark themes -->
<meta
name="manifest.theme_icons"
content="[
{ light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
{ light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 }
]"
/>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
+59 -3
View File
@@ -1,3 +1,7 @@
---
outline: deep
---
# WXT Modules
WXT provides a "module system" that let's you run code at different steps in the build process to modify it.
@@ -66,7 +70,7 @@ Each module's setup function is executed after the `wxt.config.ts` file is loade
Refer to the [API reference](/api/reference/wxt/interfaces/Wxt) for a complete list of properties and functions available.
Also to make sure and read about all the [hooks that are available](https://wxt.dev/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
Also make sure to read about [all the hooks that are available](/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
### Recipes
@@ -131,12 +135,64 @@ declare module 'wxt/utils/define-app-config' {
Runtime options are returned when calling
```ts
const config = useAppConfig();
const config = getAppConfig();
console.log(config.myModule);
```
This is very useful when [generating runtime code](#generate-runtime-module).
#### Add custom entrypoint options
Modules can add custom options to entrypoints by augmenting the entrypoint options types. This allows you to add custom configuration that can be accessed during the build process.
```ts
import { defineWxtModule } from 'wxt/modules';
import 'wxt';
declare module 'wxt' {
export interface BackgroundEntrypointOptions {
// Add custom options to the background entrypoint
myCustomOption?: string;
}
}
export default defineWxtModule({
setup(wxt) {
wxt.hook('entrypoints:resolved', (_, entrypoints) => {
const background = entrypoints.find((e) => e.type === 'background');
if (background) {
console.log('Custom option:', background.options.myCustomOption);
}
});
},
});
```
Now users can set the custom option in their entrypoint:
```ts [entrypoints/background.ts]
export default defineBackground({
myCustomOption: 'custom value',
main() {
// ...
},
});
```
This works for all other JS and HTML entrypoints, here's an example of how to pass a custom option from an HTML file.
```html [entrypoints/popup.html]
<html>
<head>
<meta name="wxt.myHtmlOption" content="custom value" />
<title>Popup</title>
</head>
<body>
<!-- ... -->
</body>
</html>
```
#### Generate output file
```ts
@@ -220,7 +276,7 @@ export default defineWxtModule({
const analyticsModuleCode = `
import { createAnalytics } from 'some-module';
export const analytics = createAnalytics(useAppConfig().analytics);
export const analytics = createAnalytics(getAppConfig().analytics);
export const { reportEvent, reportPageView } = analytics;
`;
+2
View File
@@ -7,8 +7,10 @@ This page is dedicated to all the awesome people how have made something for WXT
## Blog Posts
- [Building Modern Cross Browser Web Extensions](https://aabidk.dev/tags/wxt/) by Aabid ([@aabidk20](https://github.com/aabidk20))
- [Building Browser Extensions with WXT](https://rxliuli.com/blog/browser-extension-dev-01-introduction-to-basic-concepts/) by rxliuli ([@rxliuli](https://github.com/rxliuli))
## NPM Packages
- [`@webext-core/*`](https://webext-core.aklinker1.io/): Easy-to-use utilities for writing and testing web extensions that work on all browsers.
- [`Comctx`](https://github.com/molvqingtai/comctx): Cross-context RPC solution with type safety and flexible adapters.
- [`wxt-local-analytics`](https://github.com/HaNdTriX/wxt-local-analytics): Local analytics provider for [`@wxt-dev/analytics`](https://wxt.dev/analytics)
+34
View File
@@ -164,6 +164,10 @@ 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.
## Does WXT provide docs for LLMs?
Yes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).
## Is there an LLM trained on WXT's docs that I chat with?
Yes! There's a "Ask AI" button in the bottom right of the page, try it out! Or visit <https://knowledge.wxt.dev/> for a fullscreen experience.
@@ -186,3 +190,33 @@ 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`.
## 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".
You have two options:
1. Pass the `--disable-features=DisableLoadExtensionCommandLineSwitch` feature flag to enable the service in the browser WXT opens:
```ts
// wxt.config.ts
export default defineConfig({
webExt: {
chromiumArgs: [
'--disable-features=DisableLoadExtensionCommandLineSwitch',
],
},
});
```
2. Disable the runner and install your extension in your regular chrome profile manually:
```ts
// wxt.config.ts
export default defineConfig({
webExt: {
disabled: true,
},
});
```
+5
View File
@@ -15,6 +15,11 @@ cd path/to/your/project
pnpm dlx wxt@latest init example-wxt --template vanilla
```
:::tip
We recommend reviewing [project structure](/guide/essentials/project-structure.md) before you get started.
You can customize directory names in `wxt.config.ts` to match your project's needs.
:::
In general, you'll need to:
&ensp;<input type="checkbox" /> Install `wxt`<br />
+2 -2
View File
@@ -177,7 +177,7 @@ import { defineContentScript } from 'wxt/sandbox'; // [!code --]
import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]
import { storage } from '#imports'; // [!code ++]
import { defineContentScript } from '#imports'; // [!code ++]
import { ContentScriptContext, useAppConfig } from '#imports'; // [!code ++]
import { ContentScriptContext, getAppConfig } from '#imports'; // [!code ++]
```
You can combine the imports into a single import statement, but it's easier to just find/replace each statement.
@@ -191,7 +191,7 @@ import {
storage, // [!code ++]
defineContentScript, // [!code ++]
ContentScriptContext, // [!code ++]
useAppConfig, // [!code ++]
getAppConfig, // [!code ++]
} from '#imports'; // [!code ++]
```
+1
View File
@@ -0,0 +1 @@
<!--@include: ../packages/is-background/README.md-->
+21 -30
View File
@@ -4,49 +4,51 @@
"engines": {
"node": ">=18.20.3"
},
"packageManager": "pnpm@10.15.1",
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
"scripts": {
"check": "check && pnpm -r --sequential run check",
"test": "pnpm -r --sequential run test run",
"test:coverage": "pnpm -r --sequential run test:coverage",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
"docs:build": "pnpm -s docs:gen && vitepress build docs",
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
},
"devDependencies": {
"@aklinker1/buildc": "^1.1.5",
"@aklinker1/check": "^2.1.0",
"@commitlint/config-conventional": "^19.8.1",
"@commitlint/types": "^19.8.1",
"@aklinker1/buildc": "^1.1.6",
"@aklinker1/check": "^2.2.0",
"@commitlint/config-conventional": "^20.4.1",
"@commitlint/types": "^20.4.0",
"@types/fs-extra": "^11.0.4",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/coverage-v8": "^4.0.18",
"changelogen": "^0.6.2",
"consola": "^3.4.2",
"fast-glob": "^3.3.3",
"feed": "^5.1.0",
"fs-extra": "^11.3.1",
"lint-staged": "^16.1.6",
"feed": "^5.2.0",
"fs-extra": "^11.3.3",
"lint-staged": "^16.2.7",
"markdown-it-footnote": "^4.0.0",
"markdownlint-cli": "^0.45.0",
"nano-spawn": "^1.0.2",
"prettier": "^3.6.2",
"semver": "^7.7.2",
"markdownlint-cli": "^0.47.0",
"nano-spawn": "^2.0.0",
"p-map": "^7.0.4",
"prettier": "^3.8.1",
"semver": "^7.7.4",
"simple-git-hooks": "^2.13.1",
"tsx": "4.20.5",
"tsdown": "^0.20.3",
"tsx": "4.21.0",
"typedoc": "^0.25.4",
"typedoc-plugin-frontmatter": "^1.3.0",
"typedoc-plugin-frontmatter": "^1.3.1",
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitepress": "^1.6.4",
"vitepress-knowledge": "^0.4.1",
"vitepress-plugin-group-icons": "^1.6.3",
"vitepress-plugin-group-icons": "^1.7.1",
"vitepress-plugin-llms": "^1.11.0",
"vitest-mock-extended": "^3.1.0",
"vue": "^3.5.21",
"vue": "^3.5.27",
"wxt": "workspace:*"
},
"simple-git-hooks": {
@@ -54,16 +56,5 @@
},
"lint-staged": {
"*": "prettier --ignore-unknown --write"
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@algolia/client-search",
"search-insights"
]
},
"patchedDependencies": {
"markdown-it-footnote": "patches/markdown-it-footnote.patch"
}
}
}
+60
View File
@@ -1,5 +1,65 @@
# Changelog
## v0.5.4
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.3...analytics-v0.5.4)
### 🩹 Fixes
- Continue using `useAppConfig` to support older versions of WXT ([bfd94556](https://github.com/wxt-dev/wxt/commit/bfd94556))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.5.3
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.2...analytics-v0.5.3)
### 🩹 Fixes
- Add `getAppConfig` as an alias to `useAppConfig` ([#2144](https://github.com/wxt-dev/wxt/pull/2144))
- Allow `userId` option to return `undefined` ([636cf1f8](https://github.com/wxt-dev/wxt/commit/636cf1f8))
- Improve background script detection logic for analytics package ([#1808](https://github.com/wxt-dev/wxt/pull/1808))
### 🏡 Chore
- Created new types, instead of `any` for `analytics` ([#2119](https://github.com/wxt-dev/wxt/pull/2119))
### ❤️ Contributors
- Smit ([@sm17p](https://github.com/sm17p))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Sheng Zhang ([@Arktomson](https://github.com/Arktomson))
## v0.5.2
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.1...analytics-v0.5.2)
### 🩹 Fixes
- Normalize path for createAnalytics of analytics/index.ts ([#2013](https://github.com/wxt-dev/wxt/pull/2013))
- Allow custom API URL in Google Analytics 4 provider options ([#1653](https://github.com/wxt-dev/wxt/pull/1653))
### 💅 Refactors
- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))
### 🏡 Chore
- Fix other type error after `chrome` types update ([31ebf966](https://github.com/wxt-dev/wxt/commit/31ebf966))
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
### ❤️ Contributors
- Honwhy Wang <honwhy.wang@gmail.com>
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
## v0.5.1
[compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.5.0...analytics-v0.5.1)
-21
View File
@@ -1,21 +0,0 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
// Build module and plugins
export default defineBuildConfig({
rootDir: resolve(__dirname, 'modules/analytics'),
outDir: resolve(__dirname, 'dist'),
entries: [
{ input: 'index.ts', name: 'module' },
{ input: 'client.ts', name: 'index' },
'background-plugin.ts',
'types.ts',
'providers/google-analytics-4.ts',
'providers/umami.ts',
],
externals: ['#analytics'],
replace: {
'import.meta.env.NPM': 'true',
},
declaration: true,
});
+60 -44
View File
@@ -2,17 +2,49 @@ import { UAParser } from 'ua-parser-js';
import type {
Analytics,
AnalyticsConfig,
AnalyticsEventMetadata,
AnalyticsPageViewEvent,
AnalyticsProvider,
AnalyticsStorageItem,
AnalyticsTrackEvent,
BaseAnalyticsEvent,
AnalyticsEventMetadata,
AnalyticsProvider,
} from './types';
import { browser } from '@wxt-dev/browser';
import { isBackground } from '@wxt-dev/is-background';
type AnalyticsMessage = {
[K in keyof Analytics]: {
fn: K;
args: Parameters<Analytics[K]>;
};
}[keyof Analytics];
type AnalyticsMethod =
| ((...args: Parameters<Analytics[keyof Analytics]>) => void)
| undefined;
type MethodForwarder = <K extends keyof Analytics>(
fn: K,
) => (...args: Parameters<Analytics[K]>) => void;
const ANALYTICS_PORT = '@wxt-dev/analytics';
const INTERACTIVE_TAGS = new Set([
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
]);
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'checkbox',
'menuitem',
'tab',
'radio',
]);
export function createAnalytics(config?: AnalyticsConfig): Analytics {
if (!browser?.runtime?.id)
throw Error(
@@ -24,10 +56,7 @@ export function createAnalytics(config?: AnalyticsConfig): Analytics {
);
}
// TODO: This only works for standard WXT extensions, add a more generic
// background script detector that works with non-WXT projects.
if (location.pathname === '/background.js')
return createBackgroundAnalytics(config);
if (isBackground()) return createBackgroundAnalytics(config);
return createFrontendAnalytics();
}
@@ -62,8 +91,8 @@ function createBackgroundAnalytics(
const getBackgroundMeta = () => ({
timestamp: Date.now(),
// Don't track sessions for the background, it can be running
// indefinitely, and will inflate session duration stats.
// Don't track sessions for the background, it can be running indefinitely
// and will inflate session duration stats.
sessionId: undefined,
language: navigator.language,
referrer: undefined,
@@ -75,7 +104,7 @@ function createBackgroundAnalytics(
const getBaseEvent = async (
meta: AnalyticsEventMetadata,
): Promise<BaseAnalyticsEvent> => {
const platform = await platformInfo;
const { arch, os } = await platformInfo;
return {
meta,
user: {
@@ -84,8 +113,8 @@ function createBackgroundAnalytics(
version: config?.version ?? manifest.version_name ?? manifest.version,
wxtMode: import.meta.env.MODE,
wxtBrowser: import.meta.env.BROWSER,
arch: platform.arch,
os: platform.os,
arch,
os,
browser: userAgent.browser.name,
browserVersion: userAgent.browser.version,
...(await userProperties),
@@ -147,7 +176,7 @@ function createBackgroundAnalytics(
},
track: async (
eventName: string,
eventProperties?: Record<string, string>,
eventProperties?: Record<string, string | undefined>,
meta: AnalyticsEventMetadata = getBackgroundMeta(),
) => {
const baseEvent = await getBaseEvent(meta);
@@ -181,9 +210,8 @@ function createBackgroundAnalytics(
// Listen for messages from the rest of the extension
browser.runtime.onConnect.addListener((port) => {
if (port.name === ANALYTICS_PORT) {
port.onMessage.addListener(({ fn, args }) => {
// @ts-expect-error: Untyped fn key
void analytics[fn]?.(...args);
port.onMessage.addListener(({ fn, args }: AnalyticsMessage) => {
void (analytics[fn] as AnalyticsMethod)?.(...args);
});
}
});
@@ -201,17 +229,15 @@ function createFrontendAnalytics(): Analytics {
sessionId,
timestamp: Date.now(),
language: navigator.language,
referrer: globalThis.document?.referrer || undefined,
screen: globalThis.window
? `${globalThis.window.screen.width}x${globalThis.window.screen.height}`
: undefined,
referrer: document.referrer || undefined,
screen: `${window.screen.width}x${window.screen.height}`,
url: location.href,
title: document.title || undefined,
});
const methodForwarder =
(fn: string) =>
(...args: any[]) => {
const methodForwarder: MethodForwarder =
(fn) =>
(...args) => {
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
};
@@ -222,11 +248,11 @@ function createFrontendAnalytics(): Analytics {
setEnabled: methodForwarder('setEnabled'),
autoTrack: (root) => {
const onClick = (event: Event) => {
const element = event.target as any;
const element = event.target as HTMLElement | null;
if (
!element ||
(!INTERACTIVE_TAGS.has(element.tagName) &&
!INTERACTIVE_ROLES.has(element.getAttribute('role')))
!INTERACTIVE_ROLES.has(element.getAttribute('role') ?? ''))
)
return;
@@ -235,7 +261,7 @@ function createFrontendAnalytics(): Analytics {
id: element.id || undefined,
className: element.className || undefined,
textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length
href: element.href,
href: (element as HTMLAnchorElement).href,
});
};
root.addEventListener('click', onClick, { capture: true, passive: true });
@@ -247,33 +273,23 @@ function createFrontendAnalytics(): Analytics {
return analytics;
}
function defineStorageItem<T>(key: string): AnalyticsStorageItem<T | undefined>;
function defineStorageItem<T>(
key: string,
defaultValue?: NonNullable<T>,
): AnalyticsStorageItem<T> {
defaultValue: T,
): AnalyticsStorageItem<T>;
function defineStorageItem(
key: string,
defaultValue?: unknown,
): AnalyticsStorageItem<unknown> {
return {
getValue: async () =>
(await browser.storage.local.get(key))[key] ?? defaultValue,
(await browser.storage.local.get<Record<string, unknown>>(key))[key] ??
defaultValue,
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
};
}
const INTERACTIVE_TAGS = new Set([
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
]);
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'checkbox',
'menuitem',
'tab',
'radio',
]);
export function defineAnalyticsProvider<T = never>(
definition: (
/** The analytics object. */
+12 -13
View File
@@ -8,6 +8,7 @@ import {
} from 'wxt/modules';
import { relative, resolve } from 'node:path';
import type { AnalyticsConfig } from './types';
import { normalizePath } from 'wxt';
declare module 'wxt/utils/define-app-config' {
export interface WxtAppConfig {
@@ -22,10 +23,10 @@ export default defineWxtModule({
// Paths
const wxtAnalyticsFolder = resolve(wxt.config.wxtDir, 'analytics');
const wxtAnalyticsIndex = resolve(wxtAnalyticsFolder, 'index.ts');
const clientModuleId = import.meta.env.NPM
const clientModuleId = process.env.NPM
? '@wxt-dev/analytics'
: resolve(wxt.config.modulesDir, 'analytics/client');
const pluginModuleId = import.meta.env.NPM
const pluginModuleId = process.env.NPM
? '@wxt-dev/analytics/background-plugin'
: resolve(wxt.config.modulesDir, 'analytics/background-plugin');
@@ -38,17 +39,15 @@ export default defineWxtModule({
});
// Generate #analytics module
const wxtAnalyticsCode = [
`import { createAnalytics } from '${
import.meta.env.NPM
? clientModuleId
: relative(wxtAnalyticsFolder, clientModuleId)
}';`,
`import { useAppConfig } from '#imports';`,
``,
`export const analytics = createAnalytics(useAppConfig().analytics);`,
``,
].join('\n');
const wxtAnalyticsCode = `import { createAnalytics } from '${
process.env.NPM
? clientModuleId
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
}';
import { useAppConfig } from '#imports';
export const analytics = createAnalytics(useAppConfig().analytics);
`;
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
wxt.hook('prepare:types', async (_, entries) => {
entries.push({
@@ -4,6 +4,7 @@ import type { BaseAnalyticsEvent } from '../types';
const DEFAULT_ENGAGEMENT_TIME_IN_MSEC = 100;
export interface GoogleAnalytics4ProviderOptions {
apiUrl?: string;
apiSecret: string;
measurementId: string;
}
@@ -18,7 +19,7 @@ export const googleAnalytics4 =
): Promise<void> => {
const url = new URL(
config?.debug ? '/debug/mp/collect' : '/mp/collect',
'https://www.google-analytics.com',
options.apiUrl ?? 'https://www.google-analytics.com',
);
if (options.apiSecret)
url.searchParams.set('api_secret', options.apiSecret);
@@ -2,7 +2,10 @@ export interface Analytics {
/** Report a page change. */
page: (url: string) => void;
/** Report a custom event. */
track: (eventName: string, eventProperties?: Record<string, string>) => void;
track: (
eventName: string,
eventProperties?: Record<string, string | undefined>,
) => void;
/** Save information about the user. */
identify: (userId: string, userProperties?: Record<string, string>) => void;
/** Automatically setup and track user interactions, returning a function to remove any listeners that were setup. */
@@ -32,7 +35,7 @@ export interface AnalyticsConfig {
/**
* Configure how the user Id is persisted. Defaults to using `browser.storage.local`.
*/
userId?: AnalyticsStorageItem<string>;
userId?: AnalyticsStorageItem<string | undefined>;
/**
* Configure how user properties are persisted. Defaults to using `browser.storage.local`.
*/
@@ -94,6 +97,6 @@ export interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {
export interface AnalyticsTrackEvent extends BaseAnalyticsEvent {
event: {
name: string;
properties?: Record<string, string>;
properties?: Record<string, string | undefined>;
};
}
+26 -27
View File
@@ -1,14 +1,36 @@
{
"name": "@wxt-dev/analytics",
"version": "0.5.1",
"version": "0.5.4",
"description": "Add analytics to your web extension",
"type": "module",
"scripts": {
"dev": "buildc --deps-only -- wxt",
"dev:build": "buildc --deps-only -- wxt build",
"check": "pnpm build && check",
"build": "buildc -- tsdown",
"prepack": "pnpm -s build",
"prepare": "buildc --deps-only -- wxt prepare"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/is-background": "workspace:^",
"ua-parser-js": "^1.0.40"
},
"peerDependencies": {
"wxt": ">=0.20.0"
},
"devDependencies": {
"@types/ua-parser-js": "^0.7.39",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"wxt": "workspace:*"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/analytics"
},
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -35,31 +57,8 @@
}
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"files": [
"dist"
],
"scripts": {
"dev": "buildc --deps-only -- wxt",
"dev:build": "buildc --deps-only -- wxt build",
"check": "pnpm build && check",
"build": "buildc -- unbuild",
"prepack": "pnpm -s build",
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.20.0"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"@types/ua-parser-js": "^0.7.39",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"wxt": "workspace:*"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"ua-parser-js": "^1.0.40"
}
]
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
index: './modules/analytics/client.ts',
module: './modules/analytics/index.ts',
'background-plugin': './modules/analytics/background-plugin.ts',
types: './modules/analytics/types.ts',
'providers/google-analytics-4':
'./modules/analytics/providers/google-analytics-4.ts',
'providers/umami': './modules/analytics/providers/umami.ts',
},
external: ['#analytics'],
define: {
'process.env.NPM': 'true',
},
});
+29
View File
@@ -1,5 +1,34 @@
# Changelog
## v1.1.1
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.1.0...auto-icons-v1.1.1)
### 🩹 Fixes
- Auto icons override default icons ([#1616](https://github.com/wxt-dev/wxt/pull/1616))
### 💅 Refactors
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
### 🏡 Chore
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
- **deps:** Upgrade non-breaking production dependencies ([#1877](https://github.com/wxt-dev/wxt/pull/1877))
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
### ❤️ Contributors
- Omerfardemir <od080624@gmail.com>
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v1.1.0
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.2...auto-icons-v1.1.0)
+11 -11
View File
@@ -24,10 +24,10 @@
}
],
"license": "MIT",
"version": "1.1.0",
"version": "1.1.1",
"type": "module",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -38,23 +38,23 @@
"dist"
],
"scripts": {
"build": "buildc -- unbuild",
"check": "pnpm build && check"
"build": "buildc -- tsdown",
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest"
},
"peerDependencies": {
"wxt": ">=0.19.0"
},
"dependencies": {
"defu": "^6.1.4",
"fs-extra": "^11.3.1",
"sharp": "^0.34.3"
"fs-extra": "^11.3.3",
"sharp": "^0.34.5"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"oxlint": "^1.14.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"wxt": "workspace:*"
}
}
@@ -0,0 +1,629 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { resolve } from 'node:path';
import * as fsExtra from 'fs-extra';
import sharp from 'sharp';
import type { Wxt, UserManifest } from 'wxt';
// Import the actual module
import autoIconsModule from '../index';
import type { AutoIconsOptions } from '../index';
// Mock dependencies
vi.mock('fs-extra', () => ({
ensureDir: vi.fn(),
pathExists: vi.fn(),
}));
vi.mock('sharp', () => ({
default: vi.fn(),
}));
// Type definitions for better type safety
interface MockWxt {
config: {
srcDir: string;
outDir: string;
mode: 'development' | 'production';
};
logger: {
warn: Mock;
};
hooks: {
hook: Mock;
};
}
interface PublicAsset {
type: string;
fileName: string;
}
interface BuildOutput {
publicAssets: PublicAsset[];
}
describe('auto-icons module', () => {
const mockWxt: MockWxt = {
config: {
srcDir: '/mock/src',
outDir: '/mock/dist',
mode: 'development',
},
logger: {
warn: vi.fn(),
},
hooks: {
hook: vi.fn(),
},
};
const createMockSharpInstance = () => {
const instance = {
png: vi.fn(),
grayscale: vi.fn(),
resize: vi.fn(),
toFile: vi.fn().mockResolvedValue(undefined),
};
// Make methods chainable
instance.png.mockReturnValue(instance);
instance.grayscale.mockReturnValue(instance);
instance.resize.mockImplementation(() => {
// Create a new instance for each resize to simulate real sharp behavior
const resizedInstance = { ...instance };
resizedInstance.toFile = vi.fn().mockResolvedValue(undefined);
return resizedInstance;
});
return instance;
};
let mockSharpInstance: ReturnType<typeof createMockSharpInstance>;
beforeEach(() => {
vi.clearAllMocks();
mockSharpInstance = createMockSharpInstance();
vi.mocked(sharp).mockReturnValue(
mockSharpInstance as unknown as sharp.Sharp,
);
vi.mocked(fsExtra.pathExists).mockResolvedValue(true as any);
vi.mocked(fsExtra.ensureDir).mockResolvedValue(undefined as any);
});
describe('module setup', () => {
it('should have correct module metadata', () => {
expect(autoIconsModule.name).toBe('@wxt-dev/auto-icons');
expect(autoIconsModule.configKey).toBe('autoIcons');
expect(typeof autoIconsModule.setup).toBe('function');
});
});
describe('options handling', () => {
it('should use default options when not provided', async () => {
const options: AutoIconsOptions = {};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
// Verify that the module was set up (hooks were registered)
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:manifestGenerated',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:done',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'prepare:publicPaths',
expect.any(Function),
);
});
it('should merge custom options with defaults', async () => {
const options: AutoIconsOptions = {
sizes: [64, 32],
grayscaleOnDevelopment: false,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
// Verify that the module was set up with custom options
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:manifestGenerated',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'build:done',
expect.any(Function),
);
expect(mockWxt.hooks.hook).toHaveBeenCalledWith(
'prepare:publicPaths',
expect.any(Function),
);
});
});
describe('error handling', () => {
it('should warn when disabled', async () => {
const options: AutoIconsOptions = {
enabled: false,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
'`[auto-icons]` @wxt-dev/auto-icons disabled',
);
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
});
it('should warn when base icon not found', async () => {
vi.mocked(fsExtra.pathExists).mockResolvedValue(false as any);
const options: AutoIconsOptions = {
enabled: true,
baseIconPath: 'assets/missing-icon.png',
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'Skipping icon generation, no base icon found at',
),
);
expect(mockWxt.hooks.hook).not.toHaveBeenCalled();
});
});
describe('manifest generation hook', () => {
it('should update manifest with default icons when no custom sizes provided', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
expect(manifestHook).toBeDefined();
const manifest: UserManifest = {};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
// Should use default sizes: [128, 48, 32, 16]
expect(manifest.icons).toEqual({
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
});
it('should merge custom sizes with defaults', async () => {
const options: AutoIconsOptions = {
enabled: true,
sizes: [96, 64], // These will be merged with defaults
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
expect(manifestHook).toBeDefined();
const manifest: UserManifest = {};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
// defu merges arrays, so we get both custom and default sizes
expect(manifest.icons).toEqual({
96: 'icons/96.png',
64: 'icons/64.png',
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
});
it('should warn when overwriting existing icons in manifest', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
const manifest: UserManifest = {
icons: {
128: 'existing-icon.png',
},
};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
expect(mockWxt.logger.warn).toHaveBeenCalledWith(
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
);
});
});
describe('icon generation hook', () => {
it('should generate icons with default sizes', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
const output: BuildOutput = {
publicAssets: [],
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
expect(buildHook).toBeDefined();
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
expect(sharp).toHaveBeenCalledWith(
resolve('/mock/src', 'assets/icon.png'),
);
expect(mockSharpInstance.png).toHaveBeenCalled();
// Should resize to default sizes
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
expect(fsExtra.ensureDir).toHaveBeenCalledWith(
resolve('/mock/dist', 'icons'),
);
expect(output.publicAssets).toEqual([
{ type: 'asset', fileName: 'icons/128.png' },
{ type: 'asset', fileName: 'icons/48.png' },
{ type: 'asset', fileName: 'icons/32.png' },
{ type: 'asset', fileName: 'icons/16.png' },
]);
});
it('should generate icons with custom sizes merged with defaults', async () => {
const options: AutoIconsOptions = {
enabled: true,
sizes: [96, 64],
};
const output: BuildOutput = {
publicAssets: [],
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
expect(buildHook).toBeDefined();
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
// Should include both custom and default sizes
expect(mockSharpInstance.resize).toHaveBeenCalledWith(96);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(64);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(128);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(48);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(32);
expect(mockSharpInstance.resize).toHaveBeenCalledWith(16);
expect(output.publicAssets).toEqual([
{ type: 'asset', fileName: 'icons/96.png' },
{ type: 'asset', fileName: 'icons/64.png' },
{ type: 'asset', fileName: 'icons/128.png' },
{ type: 'asset', fileName: 'icons/48.png' },
{ type: 'asset', fileName: 'icons/32.png' },
{ type: 'asset', fileName: 'icons/16.png' },
]);
});
it('should apply grayscale in development mode', async () => {
const options: AutoIconsOptions = {
enabled: true,
grayscaleOnDevelopment: true,
sizes: [128],
};
const output: BuildOutput = { publicAssets: [] };
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
expect(mockSharpInstance.grayscale).toHaveBeenCalled();
});
it('should not apply grayscale in production mode', async () => {
const prodMockWxt = {
...mockWxt,
config: {
...mockWxt.config,
mode: 'production' as const,
},
};
const options: AutoIconsOptions = {
enabled: true,
grayscaleOnDevelopment: true,
sizes: [128],
};
const output: BuildOutput = { publicAssets: [] };
await autoIconsModule.setup!(prodMockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(prodMockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
if (buildHook) {
await buildHook(prodMockWxt as unknown as Wxt, output);
}
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
});
it('should not apply grayscale when disabled', async () => {
const options: AutoIconsOptions = {
enabled: true,
grayscaleOnDevelopment: false,
sizes: [128],
};
const output: BuildOutput = { publicAssets: [] };
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
});
});
describe('public paths hook', () => {
it('should add default icon paths to public paths', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const pathsHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
expect(pathsHook).toBeDefined();
const paths: string[] = [];
if (pathsHook) {
pathsHook(mockWxt as unknown as Wxt, paths);
}
expect(paths).toEqual([
'icons/128.png',
'icons/48.png',
'icons/32.png',
'icons/16.png',
]);
});
});
describe('edge cases and error handling', () => {
it('should handle empty sizes array', async () => {
const options: AutoIconsOptions = {
enabled: true,
sizes: [], // Empty array should still merge with defaults
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
const manifest: UserManifest = {};
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
// Should still have default sizes due to defu merge
expect(manifest.icons).toEqual({
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
});
it('should handle sharp processing errors gracefully', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
// Make toFile throw an error - need to properly chain resize -> png -> toFile
const errorInstance = {
toFile: vi.fn().mockRejectedValue(new Error('Sharp processing failed')),
grayscale: vi.fn(),
composite: vi.fn(),
};
errorInstance.grayscale.mockReturnValue(errorInstance);
errorInstance.composite.mockReturnValue(errorInstance);
mockSharpInstance.resize = vi.fn().mockImplementation(() => ({
png: vi.fn().mockReturnValue(errorInstance),
}));
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const output: BuildOutput = { publicAssets: [] };
// Should throw the sharp error
if (buildHook) {
await expect(
buildHook(mockWxt as unknown as Wxt, output),
).rejects.toThrow('Sharp processing failed');
}
});
it('should handle file system errors during directory creation', async () => {
const options: AutoIconsOptions = {
enabled: true,
};
// Make ensureDir throw an error
vi.mocked(fsExtra.ensureDir).mockRejectedValue(
new Error('Directory creation failed'),
);
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const output: BuildOutput = { publicAssets: [] };
// The module doesn't await ensureDir, so it won't throw
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
// But ensureDir should have been called
expect(fsExtra.ensureDir).toHaveBeenCalled();
}
});
it('should handle custom base icon path correctly', async () => {
const customPath = 'custom/icon.png';
const options: AutoIconsOptions = {
enabled: true,
baseIconPath: customPath,
};
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const output: BuildOutput = { publicAssets: [] };
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
// Should resolve the path relative to srcDir
expect(sharp).toHaveBeenCalledWith(resolve('/mock/src', customPath));
});
});
describe('integration test', () => {
it('should handle full workflow correctly', async () => {
const options: AutoIconsOptions = {
enabled: true,
baseIconPath: 'assets/custom-icon.png',
sizes: [96], // Will be merged with defaults
grayscaleOnDevelopment: false,
};
const manifest: UserManifest = {};
const output: BuildOutput = { publicAssets: [] };
const paths: string[] = [];
// Setup the module
await autoIconsModule.setup!(mockWxt as unknown as Wxt, options);
// Execute all hooks
const manifestHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:manifestGenerated')?.[1];
const buildHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'build:done')?.[1];
const pathsHook = vi
.mocked(mockWxt.hooks.hook)
.mock.calls.find((call) => call[0] === 'prepare:publicPaths')?.[1];
if (manifestHook) {
await manifestHook(mockWxt as unknown as Wxt, manifest);
}
if (buildHook) {
await buildHook(mockWxt as unknown as Wxt, output);
}
if (pathsHook) {
pathsHook(mockWxt as unknown as Wxt, paths);
}
// Verify results - defu merges arrays
expect(manifest.icons).toEqual({
96: 'icons/96.png',
128: 'icons/128.png',
48: 'icons/48.png',
32: 'icons/32.png',
16: 'icons/16.png',
});
expect(output.publicAssets).toEqual([
{ type: 'asset', fileName: 'icons/96.png' },
{ type: 'asset', fileName: 'icons/128.png' },
{ type: 'asset', fileName: 'icons/48.png' },
{ type: 'asset', fileName: 'icons/32.png' },
{ type: 'asset', fileName: 'icons/16.png' },
]);
expect(paths).toEqual([
'icons/96.png',
'icons/128.png',
'icons/48.png',
'icons/32.png',
'icons/16.png',
]);
expect(sharp).toHaveBeenCalledWith(
resolve('/mock/src', 'assets/custom-icon.png'),
);
expect(mockSharpInstance.grayscale).not.toHaveBeenCalled();
});
});
});
+3 -3
View File
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
import { resolve, relative } from 'node:path';
import defu from 'defu';
import sharp from 'sharp';
import { ensureDir, exists } from 'fs-extra';
import { ensureDir, pathExists } from 'fs-extra';
export default defineWxtModule<AutoIconsOptions>({
name: '@wxt-dev/auto-icons',
@@ -37,7 +37,7 @@ export default defineWxtModule<AutoIconsOptions>({
if (!parsedOptions.enabled)
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
if (!(await exists(resolvedPath))) {
if (!(await pathExists(resolvedPath))) {
return wxt.logger.warn(
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
);
@@ -45,7 +45,7 @@ export default defineWxtModule<AutoIconsOptions>({
wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {
if (manifest.icons)
return wxt.logger.warn(
wxt.logger.warn(
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
);
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
mockReset: true,
restoreMocks: true,
},
});
+6 -7
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.4",
"version": "0.1.37",
"type": "module",
"main": "src/index.mjs",
"types": "src/index.d.ts",
@@ -23,12 +23,11 @@
"src"
],
"devDependencies": {
"@types/chrome": "0.1.6",
"fs-extra": "^11.3.1",
"nano-spawn": "^1.0.2",
"tsx": "4.20.5",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"@types/chrome": "0.1.37",
"fs-extra": "^11.3.3",
"nano-spawn": "^2.0.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"dependencies": {
"@types/filesystem": "*",
+1 -3
View File
@@ -60,9 +60,7 @@ for (const { file, srcPath, destPath } of declarationFileMapping) {
// Done!
console.log(
'\x1b[32m✔\x1b[0m Done in ' + performance.now().toFixed(0) + ' ms',
);
console.log('\x1b[32m✔\x1b[0m Done in ' + performance.now().toFixed(0) + ' ms');
// Transformations
+7 -7
View File
@@ -5,19 +5,19 @@ import { browser, type Browser } from '../index';
describe('browser', () => {
describe('types', () => {
it('should provide types via the Browser import', () => {
expectTypeOf<Browser.runtime.MessageSender>().toMatchTypeOf<chrome.runtime.MessageSender>();
expectTypeOf<Browser.storage.AreaName>().toMatchTypeOf<chrome.storage.AreaName>();
expectTypeOf<Browser.i18n.LanguageDetectionResult>().toMatchTypeOf<chrome.i18n.LanguageDetectionResult>();
expectTypeOf<Browser.runtime.MessageSender>().toEqualTypeOf<chrome.runtime.MessageSender>();
expectTypeOf<Browser.storage.AreaName>().toEqualTypeOf<chrome.storage.AreaName>();
expectTypeOf<Browser.i18n.LanguageDetectionResult>().toEqualTypeOf<chrome.i18n.LanguageDetectionResult>();
});
it('should provide values via the browser import', () => {
expectTypeOf(browser.runtime.id).toMatchTypeOf<string>();
expectTypeOf(browser.runtime.id).toEqualTypeOf<string>();
expectTypeOf(
browser.storage.local,
).toMatchTypeOf<Browser.storage.StorageArea>();
).toEqualTypeOf<Browser.storage.LocalStorageArea>();
expectTypeOf(
browser.i18n.detectLanguage('Hello, world!'),
).resolves.toMatchTypeOf<chrome.i18n.LanguageDetectionResult>();
browser.i18n.detectLanguage,
).returns.resolves.toEqualTypeOf<chrome.i18n.LanguageDetectionResult>();
});
});
});
+2759 -2389
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -24,10 +24,9 @@
],
"devDependencies": {
"@types/chrome": "{{chromeTypesVersion}}",
"fs-extra": "^11.3.1",
"nano-spawn": "^1.0.2",
"tsx": "4.19.4",
"typescript": "^5.9.2",
"vitest": "^3.1.2"
"fs-extra": "^11.3.3",
"nano-spawn": "^2.0.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
}
+26
View File
@@ -1,5 +1,31 @@
# Changelog
## v0.2.5
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.4...i18n-v0.2.5)
### 🩹 Fixes
- Add `.jsonc` support for locale files to match docs ([#2066](https://github.com/wxt-dev/wxt/pull/2066))
### 🏡 Chore
- Manually fix markdownlint errors ([#1711](https://github.com/wxt-dev/wxt/pull/1711))
- **deps:** Upgrade oxlint from 0.16.8 to 1.14.0 ([a01928e0](https://github.com/wxt-dev/wxt/commit/a01928e0))
- **deps:** Upgrade typescript from 5.8.3 to 5.9.2 ([a6eef643](https://github.com/wxt-dev/wxt/commit/a6eef643))
- Create script for managing dependency upgrades ([#1875](https://github.com/wxt-dev/wxt/pull/1875))
- **deps:** Upgrade all dev dependencies ([#1876](https://github.com/wxt-dev/wxt/pull/1876))
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
- Upgrade major deps ([#2070](https://github.com/wxt-dev/wxt/pull/2070))
### ❤️ Contributors
- Ilya Kubariev ([@gymnasy55](https://github.com/gymnasy55))
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.2.4
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)
+9 -11
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/i18n",
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
"version": "0.2.4",
"version": "0.2.5",
"type": "module",
"repository": {
"type": "git",
@@ -21,13 +21,13 @@
"license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev",
"scripts": {
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^",
"chokidar": "^4.0.3",
"chokidar": "^5.0.0",
"confbox": "^0.1.8 || ^0.2.2",
"fast-glob": "^3.3.3"
},
@@ -40,19 +40,17 @@
}
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"@types/node": "^20.17.6",
"oxlint": "^1.14.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"vitest": "^3.2.4",
"vitest-plugin-random-seed": "^1.1.1",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2",
"wxt": "workspace:*"
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"import": {
+2 -2
View File
@@ -44,7 +44,7 @@ export default defineWxtModule<I18nOptions>({
options ?? {};
const getLocalizationFiles = async () => {
const files = await glob('*.{json,json5,yml,yaml,toml}', {
const files = await glob('*.{json,json5,jsonc,yml,yaml,toml}', {
cwd: localesDir,
absolute: true,
});
@@ -88,7 +88,7 @@ export default defineWxtModule<I18nOptions>({
)!;
if (defaultLocaleFile == null) {
throw Error(
`\`[i18n]\` Required localization file does not exist: \`<localesDir>/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``,
`\`[i18n]\` Required localization file does not exist: \`<localesDir>/${wxt.config.manifest.default_locale}.{json|json5|jsonc|yml|yaml|toml}\``,
);
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'tsdown';
const entry = ['src/index.ts', 'src/build.ts', 'src/module.ts'];
export default defineConfig([
{
entry,
},
{
entry,
format: 'cjs',
},
]);
+17
View File
@@ -0,0 +1,17 @@
# `@wxt-dev/is-background`
Exports a getter to determine if the current JS context is the background or not.
## Installation
```sh
pnpm add @wxt-dev/is-background
```
## Usage
```ts
import { isBackground } from '@wxt-dev/is-background';
isBackground(); // true | false
```
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@wxt-dev/is-background",
"type": "module",
"version": "1.0.0",
"description": "Check if the current context is the background or not.",
"license": "MIT",
"scripts": {
"build": "buildc -- tsdown",
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest",
"test:coverage": "pnpm test run --coverage",
"prepack": "pnpm build"
},
"dependencies": {
"@wxt-dev/browser": "workspace:^"
},
"devDependencies": {
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/is-background"
},
"keywords": [
"wxt",
"chrome",
"web",
"extension",
"is",
"background",
"script",
"page",
"service",
"worker"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"funding": "https://github.com/sponsors/wxt-dev",
"files": [
"dist"
],
"types": "./dist/index.d.mts",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
}
@@ -0,0 +1,145 @@
import { describe, it, vi, expect } from 'vitest';
import { getIsBackground } from '../getter';
let mockBrowser: any;
vi.mock('@wxt-dev/browser', () => ({
get browser() {
return mockBrowser;
},
}));
const backgroundWindow = Symbol('Background');
const otherWindow = Symbol('Other');
function setupEnv(options: {
window: symbol | undefined;
hasExtensionApis: boolean;
hasGetBackgroundPage: boolean;
hasServiceWorkerGlobalScope: boolean;
}): void {
vi.unstubAllGlobals();
mockBrowser = undefined;
if (options.window) {
vi.stubGlobal('window', options.window);
}
if (options.hasExtensionApis) {
mockBrowser = {
runtime: {
id: 'test',
},
...(options.hasGetBackgroundPage && {
extension: {
getBackgroundPage: () => backgroundWindow,
},
}),
};
}
if (options.hasServiceWorkerGlobalScope) {
class ServiceWorkerGlobalScope {}
vi.stubGlobal('ServiceWorkerGlobalScope', ServiceWorkerGlobalScope);
vi.stubGlobal('self', new ServiceWorkerGlobalScope());
}
}
describe('isBackground Getter', () => {
describe('Non-extension contexts', () => {
it('should return false', () => {
setupEnv({
hasServiceWorkerGlobalScope: false,
hasExtensionApis: false,
hasGetBackgroundPage: false,
window: otherWindow,
});
expect(getIsBackground()).toBe(false);
});
});
describe('Chromium & Safari', () => {
const hasExtensionApis = true;
describe('MV2', () => {
const hasServiceWorkerGlobalScope = false;
const hasGetBackgroundPage = true;
it('should return true inside the background page', () => {
setupEnv({
hasServiceWorkerGlobalScope,
hasExtensionApis,
hasGetBackgroundPage,
window: backgroundWindow,
});
expect(getIsBackground()).toBe(true);
});
it('should return false outside the background page', () => {
setupEnv({
hasServiceWorkerGlobalScope,
hasExtensionApis,
hasGetBackgroundPage,
window: otherWindow,
});
expect(getIsBackground()).toBe(false);
});
});
describe('MV3', () => {
const hasGetBackgroundPage = false;
it('should return true inside the service worker', () => {
setupEnv({
hasServiceWorkerGlobalScope: true,
hasExtensionApis,
hasGetBackgroundPage,
window: undefined,
});
expect(getIsBackground()).toBe(true);
});
it('should return false outside the service worker', () => {
setupEnv({
hasServiceWorkerGlobalScope: false,
hasExtensionApis,
hasGetBackgroundPage,
window: otherWindow,
});
expect(getIsBackground()).toBe(false);
});
});
});
describe('Firefox, MV2 & MV3', () => {
const hasServiceWorkerGlobalScope = false;
const hasExtensionApis = true;
const hasGetBackgroundPage = true;
it('should return true inside the background page', () => {
setupEnv({
hasServiceWorkerGlobalScope,
hasExtensionApis,
hasGetBackgroundPage,
window: backgroundWindow,
});
expect(getIsBackground()).toBe(true);
});
it('should return false outside the background page', () => {
setupEnv({
hasServiceWorkerGlobalScope,
hasExtensionApis,
hasGetBackgroundPage,
window: otherWindow,
});
expect(getIsBackground()).toBe(false);
});
});
});
+32
View File
@@ -0,0 +1,32 @@
import { browser } from '@wxt-dev/browser';
declare class ServiceWorkerGlobalScope {}
export function getIsBackground(): boolean {
// Are we in an extension context?
if (!browser?.runtime?.id) return false;
// Is this a true MV3 service worker?
//
// - ✅ Chromium MV3
// - ❌ Firefox MV3 - Uses a non-persistent HTML page instead of a service worker.
// - ✅ Safari MV3
if (
typeof ServiceWorkerGlobalScope !== 'undefined' &&
self instanceof ServiceWorkerGlobalScope
) {
return true;
}
// Is this the background page?
//
// - ✅ Chromium MV2
// - ✅ Firefox MV2
// - ✅ Firefox MV3 - Works with the non-persistent HTML page
// - ✅ Safari MV2
return (
typeof window !== 'undefined' &&
typeof browser.extension?.getBackgroundPage === 'function' &&
browser.extension.getBackgroundPage() === window
);
}
+27
View File
@@ -0,0 +1,27 @@
/**
* This module uses a lazy getter function so the logic isn't ran until it's needed.
*
* This has a few benefits:
* 1. Easier to mock in tests
* 2. Safe to import in NodeJS environments (but it should be safe to run just in-case)
* 3. Keeps startup fast by waiting to run the slow functions (`instanceof` or `browser.extension.getBackgroundPage`) until needed
*
* @module @wxt-dev/is-background
*/
import { getIsBackground } from './getter';
let cached: boolean | undefined;
/**
* Getter that returns if the current context is apart of an extension's
* background or not.
*
* > This function caches the result when called for the first time so it
* > doesn't have to recalculate.
*
* @returns true when in a background page or service worker.
*/
export function isBackground(): boolean {
if (cached == null) cached = getIsBackground();
return cached;
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"exclude": ["node_modules/**", "dist/**"]
}
-12
View File
@@ -1,12 +0,0 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
export default defineBuildConfig({
rootDir: resolve(__dirname, 'modules'),
outDir: resolve(__dirname, 'dist'),
entries: [{ input: 'react.ts', name: 'index' }],
rollup: {
emitCJS: true,
},
declaration: true,
});
+8 -10
View File
@@ -22,7 +22,7 @@
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"import": {
@@ -41,7 +41,7 @@
"scripts": {
"dev": "wxt",
"check": "pnpm build && check",
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
@@ -51,14 +51,12 @@
"@vitejs/plugin-react": "^4.4.1 || ^5.0.0"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"publint": "^0.3.12",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"publint": "^0.3.17",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"typescript": "^5.9.3",
"wxt": "workspace:*"
}
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsdown';
const entry = {
index: './modules/react.ts',
};
export default defineConfig([
{
entry,
},
{
entry,
format: 'cjs',
},
]);
-12
View File
@@ -1,12 +0,0 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
export default defineBuildConfig({
rootDir: resolve(__dirname, 'modules'),
outDir: resolve(__dirname, 'dist'),
entries: [{ input: 'solid.ts', name: 'index' }],
rollup: {
emitCJS: true,
},
declaration: true,
});
+6 -8
View File
@@ -22,7 +22,7 @@
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"import": {
@@ -41,21 +41,19 @@
"scripts": {
"dev": "wxt",
"check": "pnpm build && check",
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.19.16"
},
"dependencies": {
"vite-plugin-solid": "^2.11.8"
"vite-plugin-solid": "^2.11.10"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"publint": "^0.3.12",
"solid-js": "^1.9.9",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"publint": "^0.3.17",
"solid-js": "^1.9.11",
"typescript": "^5.9.3",
"wxt": "workspace:*"
}
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsdown';
const entry = {
index: './modules/solid.ts',
};
export default defineConfig([
{
entry,
},
{
entry,
format: 'cjs',
},
]);
+4 -6
View File
@@ -22,7 +22,7 @@
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"import": {
@@ -39,7 +39,7 @@
"dist"
],
"scripts": {
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"check": "pnpm build && check"
},
"peerDependencies": {
@@ -50,10 +50,8 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0 || ^6.0.0"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"wxt": "workspace:*"
}
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsdown';
const entry = {
index: './src/index.ts',
};
export default defineConfig([
{
entry,
},
{
entry,
format: 'cjs',
},
]);
+4 -6
View File
@@ -22,7 +22,7 @@
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"import": {
@@ -39,7 +39,7 @@
"dist"
],
"scripts": {
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"check": "pnpm build && check"
},
"peerDependencies": {
@@ -49,10 +49,8 @@
"@vitejs/plugin-vue": "^5.2.3 || ^6.0.0"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"wxt": "workspace:*"
}
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsdown';
const entry = {
index: './src/index.ts',
};
export default defineConfig([
{
entry,
},
{
entry,
format: 'cjs',
},
]);
+6 -10
View File
@@ -24,21 +24,17 @@
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest",
"dev": "tsx --trace-warnings dev.ts",
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"prepublishOnly": "pnpm build"
},
"dependencies": {},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"oxlint": "^1.14.0",
"publint": "^0.3.12",
"tsx": "4.20.5",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"vitest": "^3.2.4"
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"main": "dist/index.mjs",
"types": "dist/index.d.ts",
"types": "dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
+2 -2
View File
@@ -114,7 +114,7 @@ async function findBrowserBinary(target: string): Promise<string | undefined> {
for (const target of targets) {
const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];
for (const path of potentialPaths) {
if (await exists(path)) return path;
if (await pathExists(path)) return path;
}
}
}
@@ -207,7 +207,7 @@ function deduplicateArgs(
return args;
}
async function exists(path: string): Promise<boolean> {
async function pathExists(path: string): Promise<boolean> {
try {
await open(path, 'r');
return true;
+60
View File
@@ -1,5 +1,65 @@
# Changelog
## v1.2.8
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.7...storage-v1.2.8)
### 🩹 Fixes
- Correctly update version metadata when setting a value for the first time ([#2139](https://github.com/wxt-dev/wxt/pull/2139))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v1.2.7
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.6...storage-v1.2.7)
### 🩹 Fixes
- Add another `defineItem` signature when `init` function is passed ([#1909](https://github.com/wxt-dev/wxt/pull/1909))
- **storage:** Set version number on init ([#1996](https://github.com/wxt-dev/wxt/pull/1996))
### 💅 Refactors
- Code cleanup in analytics package ([#2084](https://github.com/wxt-dev/wxt/pull/2084))
### 📖 Documentation
- Rename keys name of getMetas() to be proper ([#2105](https://github.com/wxt-dev/wxt/pull/2105))
### 🏡 Chore
- Fix type errors after `chrome` type upgrades ([6036c6e8](https://github.com/wxt-dev/wxt/commit/6036c6e8))
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
### ❤️ Contributors
- Willow ([@42willow](https://github.com/42willow))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Dan McGee <dpmcgee@gmail.com>
## v1.2.6
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.5...storage-v1.2.6)
## v1.2.5
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.4...storage-v1.2.5)
## v1.2.4
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.3...storage-v1.2.4)
## v1.2.3
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.2...storage-v1.2.3)
## v1.2.2
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.2.1...storage-v1.2.2)
+8 -10
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/storage",
"description": "Web extension storage API provided by WXT, supports all browsers.",
"version": "1.2.2",
"version": "1.2.8",
"type": "module",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev",
"scripts": {
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check",
"test": "buildc --deps-only -- vitest"
},
@@ -35,16 +35,14 @@
"dequal": "^2.0.3"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"@webext-core/fake-browser": "^1.3.2",
"oxlint": "^1.14.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"vitest": "^3.2.4"
"@webext-core/fake-browser": "^1.3.4",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
+115 -2
View File
@@ -1,7 +1,7 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
import { MigrationError, type WxtStorageItem, storage } from '../index';
import { browser } from '@wxt-dev/browser';
import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
import { MigrationError, type WxtStorageItem, storage } from '../index';
/**
* This works because fakeBrowser is synchronous, and is will finish any number of chained
@@ -23,6 +23,9 @@ describe('Storage Utils', () => {
beforeEach(() => {
fakeBrowser.reset();
storage.unwatch();
// Setup a spy to check for excessive calls
fakeBrowser.storage.local.set = vi.spyOn(fakeBrowser.storage.local, 'set');
});
describe.each(['local', 'sync', 'managed', 'session'] as const)(
@@ -706,6 +709,47 @@ describe('Storage Utils', () => {
expect(migrateToV3).toBeCalledWith(4);
});
it('should set the version without running migrations for empty storage items', async () => {
const migrate = vi.fn((n: number) => n * 2);
const item = storage.defineItem<number>('local:key', {
init: () => 1,
version: 2,
migrations: {
2: migrate,
},
});
const value = await item.getValue();
const meta = await item.getMeta();
expect(value).toEqual(1);
expect(meta).toEqual({ v: 2 });
expect(migrate).toBeCalledTimes(0);
});
it('should set the version and run migrations if a storage value already exists and v2 is added', async () => {
const key = 'local:key';
// Mimic as if an initial value was set by a previous version. We expect
// this value to be migrated and doubled to V2.
await storage.setItem(key, 1);
const migrate = vi.fn((n: number) => n * 2);
const item = storage.defineItem<number>(key, {
init: () => 1,
version: 2,
migrations: {
2: migrate,
},
});
const value = await item.getValue();
const meta = await item.getMeta();
expect(value).toEqual(2);
expect(meta).toEqual({ v: 2 });
expect(migrate).toBeCalledTimes(1);
});
it('should call onMigrationComplete callback function if defined', async () => {
await fakeBrowser.storage.local.set({
count: 2,
@@ -908,6 +952,7 @@ describe('Storage Utils', () => {
{ migratedValue: expect.any(Number) },
);
});
it('should not print migration logs if debug option is undefined or false', async () => {
await fakeBrowser.storage.local.set({
count: 2,
@@ -939,6 +984,62 @@ describe('Storage Utils', () => {
await waitForMigrations();
expect(consoleSpy).toHaveBeenCalledTimes(0);
});
describe('calling setValue', () => {
const migrateToV2 = vi.fn((v1) => v1);
const defineTestItem = () =>
storage.defineItem<number>('local:count', {
version: 2,
migrations: {
2: migrateToV2,
},
});
it('should set the version metadata when setting the value for the first time', async () => {
const item = defineTestItem();
expect(await item.getValue()).toBeNull();
expect(await item.getMeta()).toEqual({});
expect(fakeBrowser.storage.local.set).toBeCalledTimes(0);
await item.setValue(1);
expect(await item.getValue()).toBe(1);
expect(await item.getMeta()).toEqual({ v: 2 });
// Called twice, once for setting the value, once for updating the metadata.
expect(fakeBrowser.storage.local.set).toBeCalledTimes(2);
expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 1 });
expect(fakeBrowser.storage.local.set).toBeCalledWith({
count$: { v: 2 },
});
await item.setValue(2);
expect(await item.getValue()).toBe(2);
expect(await item.getMeta()).toEqual({ v: 2 });
// Only called one more time, just for setting the value
expect(fakeBrowser.storage.local.set).toBeCalledTimes(3);
expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 2 });
// Migration function never called throughout the whole test
expect(migrateToV2).not.toBeCalled();
});
it('should not set the version metadata when a value is already in storage', async () => {
await fakeBrowser.storage.local.set({ count: 1, count$: { v: 2 } });
vi.mocked(fakeBrowser.storage.local.set).mockClear();
const item = defineTestItem();
await item.setValue(2);
expect(await item.getValue()).toEqual(2);
expect(await item.getMeta()).toEqual({ v: 2 });
expect(fakeBrowser.storage.local.set).toBeCalledTimes(1);
expect(fakeBrowser.storage.local.set).toBeCalledWith({ count: 2 });
expect(migrateToV2).not.toBeCalled();
});
});
});
describe('getValue', () => {
@@ -1315,6 +1416,18 @@ describe('Storage Utils', () => {
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
});
it('should define a non-null value when options are passed with a non-null init function', () => {
const item = storage.defineItem(`local:test`, {
init: () => 123,
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
const item2 = storage.defineItem(`local:test`, {
init: () => Promise.resolve(123),
});
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();
});
});
});
+166 -26
View File
@@ -4,9 +4,9 @@
* See [the guide](https://wxt.dev/storage.html) for more information.
* @module @wxt-dev/storage
*/
import { dequal } from 'dequal/lite';
import { Mutex } from 'async-mutex';
import { browser, type Browser } from '@wxt-dev/browser';
import { Mutex } from 'async-mutex';
import { dequal } from 'dequal/lite';
export const storage = createStorage();
@@ -17,6 +17,7 @@ function createStorage(): WxtStorage {
sync: createDriver('sync'),
managed: createDriver('managed'),
};
const getDriver = (area: StorageArea) => {
const driver = drivers[area];
if (driver == null) {
@@ -25,14 +26,17 @@ function createStorage(): WxtStorage {
}
return driver;
};
const resolveKey = (key: StorageItemKey) => {
const deliminatorIndex = key.indexOf(':');
const driverArea = key.substring(0, deliminatorIndex) as StorageArea;
const driverKey = key.substring(deliminatorIndex + 1);
if (driverKey == null)
if (driverKey == null) {
throw Error(
`Storage key should be in the form of "area:key", but received "${key}"`,
);
}
return {
driverArea,
@@ -40,17 +44,23 @@ function createStorage(): WxtStorage {
driver: getDriver(driverArea),
};
};
const getMetaKey = (key: string) => key + '$';
const mergeMeta = (oldMeta: any, newMeta: any): any => {
const newFields = { ...oldMeta };
Object.entries(newMeta).forEach(([key, value]) => {
if (value == null) delete newFields[key];
else newFields[key] = value;
});
return newFields;
};
const getValueOrFallback = (value: any, fallback: any) =>
value ?? fallback ?? null;
const getMetaValue = (properties: any) =>
typeof properties === 'object' && !Array.isArray(properties)
? properties
@@ -64,11 +74,13 @@ function createStorage(): WxtStorage {
const res = await driver.getItem<any>(driverKey);
return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue);
};
const getMeta = async (driver: WxtStorageDriver, driverKey: string) => {
const metaKey = getMetaKey(driverKey);
const res = await driver.getItem<any>(metaKey);
return getMetaValue(res);
};
const setItem = async (
driver: WxtStorageDriver,
driverKey: string,
@@ -76,6 +88,7 @@ function createStorage(): WxtStorage {
) => {
await driver.setItem(driverKey, value ?? null);
};
const setMeta = async (
driver: WxtStorageDriver,
driverKey: string,
@@ -85,23 +98,27 @@ function createStorage(): WxtStorage {
const existingFields = getMetaValue(await driver.getItem(metaKey));
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
};
const removeItem = async (
driver: WxtStorageDriver,
driverKey: string,
opts: RemoveItemOptions | undefined,
) => {
await driver.removeItem(driverKey);
if (opts?.removeMeta) {
const metaKey = getMetaKey(driverKey);
await driver.removeItem(metaKey);
}
};
const removeMeta = async (
driver: WxtStorageDriver,
driverKey: string,
properties: string | string[] | undefined,
) => {
const metaKey = getMetaKey(driverKey);
if (properties == null) {
await driver.removeItem(metaKey);
} else {
@@ -110,19 +127,19 @@ function createStorage(): WxtStorage {
await driver.setItem(metaKey, newFields);
}
};
const watch = (
driver: WxtStorageDriver,
driverKey: string,
cb: WatchCallback<any>,
) => {
return driver.watch(driverKey, cb);
};
) => driver.watch(driverKey, cb);
const storage: WxtStorage = {
return {
getItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
return await getItem(driver, driverKey, opts);
},
getItems: async (keys) => {
const areaToKeyMap = new Map<StorageArea, string[]>();
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
@@ -131,6 +148,7 @@ function createStorage(): WxtStorage {
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: GetItemOptions<any> | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
@@ -143,9 +161,11 @@ function createStorage(): WxtStorage {
keyStr = key.key;
opts = key.options;
}
orderedKeys.push(keyStr);
const { driverArea, driverKey } = resolveKey(keyStr);
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
keyToOptsMap.set(keyStr, opts);
});
@@ -154,6 +174,7 @@ function createStorage(): WxtStorage {
await Promise.all(
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
const driverResults = await drivers[driverArea].getItems(keys);
driverResults.forEach((driverResult) => {
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
const opts = keyToOptsMap.get(key);
@@ -161,6 +182,7 @@ function createStorage(): WxtStorage {
driverResult.value,
opts?.fallback ?? opts?.defaultValue,
);
resultsMap.set(key, value);
});
}),
@@ -171,14 +193,17 @@ function createStorage(): WxtStorage {
value: resultsMap.get(key),
}));
},
getMeta: async (key) => {
const { driver, driverKey } = resolveKey(key);
return await getMeta(driver, driverKey);
},
getMetas: async (args) => {
const keys = args.map((arg) => {
const key = typeof arg === 'string' ? arg : arg.key;
const { driverArea, driverKey } = resolveKey(key);
return {
key,
driverArea,
@@ -186,6 +211,7 @@ function createStorage(): WxtStorage {
driverMetaKey: getMetaKey(driverKey),
};
});
const areaToDriverMetaKeysMap = keys.reduce<
Partial<Record<StorageArea, (typeof keys)[number][]>>
>((map, key) => {
@@ -211,10 +237,12 @@ function createStorage(): WxtStorage {
meta: resultsMap[key.key],
}));
},
setItem: async (key, value) => {
const { driver, driverKey } = resolveKey(key);
await setItem(driver, driverKey, value);
},
setItems: async (items) => {
const areaToKeyValueMap: Partial<
Record<StorageArea, Array<{ key: string; value: any }>>
@@ -229,6 +257,7 @@ function createStorage(): WxtStorage {
value: item.value,
});
});
await Promise.all(
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
const driver = getDriver(driverArea as StorageArea);
@@ -236,10 +265,12 @@ function createStorage(): WxtStorage {
}),
);
},
setMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await setMeta(driver, driverKey, properties);
},
setMetas: async (items) => {
const areaToMetaUpdatesMap: Partial<
Record<StorageArea, { key: string; properties: any }[]>
@@ -278,16 +309,19 @@ function createStorage(): WxtStorage {
),
);
},
removeItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
await removeItem(driver, driverKey, opts);
},
removeItems: async (keys) => {
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: RemoveItemOptions | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
@@ -303,9 +337,11 @@ function createStorage(): WxtStorage {
keyStr = key.key;
opts = key.options;
}
const { driverArea, driverKey } = resolveKey(keyStr);
areaToKeysMap[driverArea] ??= [];
areaToKeysMap[driverArea].push(driverKey);
if (opts?.removeMeta) {
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
}
@@ -318,36 +354,45 @@ function createStorage(): WxtStorage {
}),
);
},
clear: async (base) => {
const driver = getDriver(base);
await driver.clear();
},
removeMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await removeMeta(driver, driverKey, properties);
},
snapshot: async (base, opts) => {
const driver = getDriver(base);
const data = await driver.snapshot();
opts?.excludeKeys?.forEach((key) => {
delete data[key];
delete data[getMetaKey(key)];
});
return data;
},
restoreSnapshot: async (base, data) => {
const driver = getDriver(base);
await driver.restoreSnapshot(data);
},
watch: (key, cb) => {
const { driver, driverKey } = resolveKey(key);
return watch(driver, driverKey, cb);
},
unwatch() {
Object.values(drivers).forEach((driver) => {
driver.unwatch();
});
},
defineItem: (key, opts?: WxtStorageItemOptions<any>) => {
const { driver, driverKey } = resolveKey(key);
@@ -357,17 +402,25 @@ function createStorage(): WxtStorage {
onMigrationComplete,
debug = false,
} = opts ?? {};
if (targetVersion < 1) {
throw Error(
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
);
}
const migrate = async () => {
let needsVersionSet = false;
const migrate: WxtStorageItem<any, any>['migrate'] = async () => {
const driverMetaKey = getMetaKey(driverKey);
const [{ value }, { value: meta }] = await driver.getItems([
driverKey,
driverMetaKey,
]);
// Used in setValue to also set the version when needed
needsVersionSet = value == null && meta?.v == null && !!targetVersion;
if (value == null) return;
const currentVersion = meta?.v ?? 1;
@@ -376,11 +429,12 @@ function createStorage(): WxtStorage {
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
);
}
if (currentVersion === targetVersion) {
return;
}
if (debug === true) {
if (debug) {
console.debug(
`[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
);
@@ -395,7 +449,7 @@ function createStorage(): WxtStorage {
migratedValue =
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
if (debug === true) {
if (debug) {
console.debug(
`[@wxt-dev/storage] Storage migration processed for version: v${migrateToVersion}`,
);
@@ -411,14 +465,16 @@ function createStorage(): WxtStorage {
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
]);
if (debug === true) {
if (debug) {
console.debug(
`[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`,
{ migratedValue },
);
}
onMigrationComplete?.(migratedValue, targetVersion);
};
const migrationsDone =
opts?.migrations == null
? Promise.resolve()
@@ -441,6 +497,9 @@ function createStorage(): WxtStorage {
const newValue = await opts.init();
await driver.setItem<any>(driverKey, newValue);
if (value == null && targetVersion > 1) {
await setMeta(driver, driverKey, { v: targetVersion });
}
return newValue;
});
@@ -449,62 +508,86 @@ function createStorage(): WxtStorage {
return {
key,
get defaultValue() {
return getFallback();
},
get fallback() {
return getFallback();
},
getValue: async () => {
await migrationsDone;
if (opts?.init) {
return await getOrInitValue();
} else {
return await getItem(driver, driverKey, opts);
}
},
getMeta: async () => {
await migrationsDone;
return await getMeta(driver, driverKey);
},
setValue: async (value) => {
await migrationsDone;
return await setItem(driver, driverKey, value);
if (needsVersionSet) {
needsVersionSet = false;
await Promise.all([
// Note: These calls cannot be done in a single `setItems` call;
// metadata needs to be merged together with existing data and
// setItems overwrites the whole value without merging.
setItem(driver, driverKey, value),
setMeta(driver, driverKey, { v: targetVersion }),
]);
} else {
await setItem(driver, driverKey, value);
}
},
setMeta: async (properties) => {
await migrationsDone;
return await setMeta(driver, driverKey, properties);
},
removeValue: async (opts) => {
await migrationsDone;
return await removeItem(driver, driverKey, opts);
},
removeMeta: async (properties) => {
await migrationsDone;
return await removeMeta(driver, driverKey, properties);
},
watch: (cb) =>
watch(driver, driverKey, (newValue, oldValue) =>
cb(newValue ?? getFallback(), oldValue ?? getFallback()),
),
migrate,
};
},
};
return storage;
}
function createDriver(storageArea: StorageArea): WxtStorageDriver {
const getStorageArea = () => {
if (browser.runtime == null) {
throw Error(
[
"'wxt/storage' must be loaded in a web extension environment",
'\n - If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371',
" - If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html\n",
].join('\n'),
);
throw Error(`'wxt/storage' must be loaded in a web extension environment
- If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371
- If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html
`);
}
if (browser.storage == null) {
throw Error(
"You must add the 'storage' permission to your manifest to use 'wxt/storage'",
@@ -514,18 +597,23 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
const area = browser.storage[storageArea];
if (area == null)
throw Error(`"browser.storage.${storageArea}" is undefined`);
return area;
};
const watchListeners = new Set<(changes: StorageAreaChanges) => void>();
return {
getItem: async (key) => {
const res = await getStorageArea().get(key);
const res = await getStorageArea().get<Record<string, any>>(key);
return res[key];
},
getItems: async (keys) => {
const result = await getStorageArea().get(keys);
return keys.map((key) => ({ key, value: result[key] ?? null }));
},
setItem: async (key, value) => {
if (value == null) {
await getStorageArea().remove(key);
@@ -533,6 +621,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
await getStorageArea().set({ [key]: value });
}
},
setItems: async (values) => {
const map = values.reduce<Record<string, unknown>>(
(map, { key, value }) => {
@@ -541,37 +630,51 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
},
{},
);
await getStorageArea().set(map);
},
removeItem: async (key) => {
await getStorageArea().remove(key);
},
removeItems: async (keys) => {
await getStorageArea().remove(keys);
},
clear: async () => {
await getStorageArea().clear();
},
snapshot: async () => {
return await getStorageArea().get();
},
restoreSnapshot: async (data) => {
await getStorageArea().set(data);
},
watch(key, cb) {
const listener = (changes: StorageAreaChanges) => {
const change = changes[key];
if (change == null) return;
if (dequal(change.newValue, change.oldValue)) return;
const change = changes[key] as {
newValue?: any;
oldValue?: any | null;
} | null;
if (change == null || dequal(change.newValue, change.oldValue)) return;
cb(change.newValue ?? null, change.oldValue ?? null);
};
getStorageArea().onChanged.addListener(listener);
watchListeners.add(listener);
return () => {
getStorageArea().onChanged.removeListener(listener);
watchListeners.delete(listener);
};
},
unwatch() {
watchListeners.forEach((listener) => {
getStorageArea().onChanged.removeListener(listener);
@@ -612,6 +715,7 @@ export interface WxtStorage {
| { key: StorageItemKey; options?: GetItemOptions<any> }
>,
): Promise<Array<{ key: StorageItemKey; value: any }>>;
/**
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value
* is not an object, it returns an empty object.
@@ -620,15 +724,17 @@ export interface WxtStorage {
* await storage.getMeta("local:installDate");
*/
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
/**
* Get the metadata of multiple storage items.
*
* @param items List of keys or items to get the metadata of.
* @param keys List of keys or items to get the metadata of.
* @returns An array containing storage keys and their metadata.
*/
getMetas(
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/**
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
* `removeItem`.
@@ -637,6 +743,7 @@ export interface WxtStorage {
* await storage.setItem<number>("local:installDate", Date.now());
*/
setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;
/**
* Set multiple values in storage. If a value is set to `null` or `undefined`, the key is removed.
*
@@ -652,6 +759,7 @@ export interface WxtStorage {
| { item: WxtStorageItem<any, any>; value: any }
>,
): Promise<void>;
/**
* Sets metadata properties. If some properties are already set, but are not included in the
* `properties` parameter, they will not be removed.
@@ -663,10 +771,11 @@ export interface WxtStorage {
key: StorageItemKey,
properties: T | null,
): Promise<void>;
/**
* Set the metadata of multiple storage items.
*
* @param items List of storage keys or items and metadata to set for each.
* @param metas List of storage keys or items and metadata to set for each.
*/
setMetas(
metas: Array<
@@ -674,6 +783,7 @@ export interface WxtStorage {
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
>,
): Promise<void>;
/**
* Removes an item from storage.
*
@@ -681,6 +791,7 @@ export interface WxtStorage {
* await storage.removeItem("local:installDate");
*/
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
/**
* Remove a list of keys from storage.
*/
@@ -712,6 +823,7 @@ export interface WxtStorage {
key: StorageItemKey,
properties?: string | string[],
): Promise<void>;
/**
* Return all the items in storage.
*/
@@ -719,15 +831,18 @@ export interface WxtStorage {
base: StorageArea,
opts?: SnapshotOptions,
): Promise<Record<string, unknown>>;
/**
* Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are
* not overridden. Only values existing in the snapshot are overridden.
*/
restoreSnapshot(base: StorageArea, data: any): Promise<void>;
/**
* Watch for changes to a specific key in storage.
*/
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;
/**
* Remove all watch listeners.
*/
@@ -749,6 +864,12 @@ export interface WxtStorage {
key: StorageItemKey,
options: WxtStorageItemOptions<TValue> & { defaultValue: TValue },
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue> & {
init: () => TValue | Promise<TValue>;
},
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
@@ -777,42 +898,52 @@ export interface WxtStorageItem<
* The storage key passed when creating the storage item.
*/
key: StorageItemKey;
/**
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue;
/**
* The value provided by the `fallback` option.
*/
fallback: TValue;
/**
* Get the latest value from storage.
*/
getValue(): Promise<TValue>;
/**
* Get metadata.
*/
getMeta(): Promise<NullablePartial<TMetadata>>;
/**
* Set the value in storage.
*/
setValue(value: TValue): Promise<void>;
/**
* Set metadata properties.
*/
setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
/**
* Remove the value from storage.
*/
removeValue(opts?: RemoveItemOptions): Promise<void>;
/**
* Remove all metadata or certain properties from metadata.
*/
removeMeta(properties?: string[]): Promise<void>;
/**
* Listen for changes to the value in storage.
*/
watch(cb: WatchCallback<TValue>): Unwatch;
/**
* If there are migrations defined on the storage item, migrate to the latest version.
*
@@ -830,6 +961,7 @@ export interface GetItemOptions<T> {
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getItem` would otherwise return `null`.
*/
@@ -858,10 +990,12 @@ export interface WxtStorageItemOptions<T> {
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getValue` would otherwise return `null`.
*/
fallback?: T;
/**
* If passed, a value in storage will be initialized immediately after
* defining the storage item. This function returns the value that will be
@@ -869,20 +1003,24 @@ export interface WxtStorageItemOptions<T> {
* already exist.
*/
init?: () => T | Promise<T>;
/**
* Provide a version number for the storage item to enable migrations. When changing the version
* in the future, migration functions will be ran on application startup.
*/
version?: number;
/**
* A map of version numbers to the functions used to migrate the data to that version.
*/
migrations?: Record<number, (oldValue: any) => any>;
/**
* Print debug logs, such as migration process.
* @default false
*/
debug?: boolean;
/**
* A callback function that runs on migration complete.
*/
@@ -900,10 +1038,12 @@ export type StorageAreaChanges = {
type NullablePartial<T> = {
[key in keyof T]+?: T[key] | undefined | null;
};
/**
* Callback called when a value in storage is changed.
*/
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
/**
* Call to remove a watch listener
*/
+5 -7
View File
@@ -21,7 +21,7 @@
"license": "MIT",
"type": "module",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -32,7 +32,7 @@
"dist"
],
"scripts": {
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"check": "buildc --deps-only -- check"
},
"peerDependencies": {
@@ -40,11 +40,9 @@
"wxt": ">=0.19.0"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"oxlint": "^1.14.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
"wxt": "workspace:*"
},
@@ -1,15 +0,0 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
export default defineBuildConfig({
rootDir: resolve(__dirname, 'modules/webextension-polyfill'),
outDir: resolve(__dirname, 'dist'),
entries: [
{ input: 'index.ts', name: 'index' },
{ input: 'browser.ts', name: 'browser' },
],
replace: {
'process.env.NPM': 'true',
},
declaration: true,
});
+5 -7
View File
@@ -20,7 +20,7 @@
"version": "1.0.0",
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
@@ -37,7 +37,7 @@
"scripts": {
"dev": "wxt",
"check": "pnpm build && check",
"build": "buildc -- unbuild",
"build": "buildc -- tsdown",
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
@@ -45,11 +45,9 @@
"wxt": ">=0.20.0"
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"@types/webextension-polyfill": "^0.12.3",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"@types/webextension-polyfill": "^0.12.4",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"webextension-polyfill": "^0.12.0",
"wxt": "workspace:*"
}
@@ -0,0 +1,11 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
index: 'modules/webextension-polyfill/index.ts',
browser: 'modules/webextension-polyfill/browser.ts',
},
define: {
'process.env.NPM': 'true',
},
});
+8 -8
View File
@@ -18,19 +18,19 @@
},
"dependencies": {
"@wxt-dev/i18n": "workspace:*",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@wxt-dev/auto-icons": "workspace:*",
"@wxt-dev/unocss": "workspace:*",
"sass": "^1.92.0",
"typescript": "^5.9.2",
"sass": "^1.97.3",
"typescript": "^5.9.3",
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
"vitest": "^3.2.4",
"vitest-plugin-random-seed": "^1.1.1",
"vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2",
"wxt": "workspace:*"
},
"buildc": {
@@ -12,7 +12,7 @@ export default defineBackground({
manifestVersion: import.meta.env.MANIFEST_VERSION,
});
console.log(useAppConfig());
console.log(getAppConfig());
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
@@ -43,3 +43,14 @@ export default defineBackground({
storage.setItem('session:startTime', Date.now());
},
});
function _otherTypeChecksNotEvaluated() {
browser.scripting.executeScript({
target: { tabId: 1 },
files: [
'/background.js',
// @ts-expect-error: Should error for non-existing paths
'/other.js',
],
});
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineConfig } from 'wxt';
import { presetUno } from 'unocss';
import { presetWind3 } from 'unocss';
export default defineConfig({
srcDir: 'src',
@@ -53,7 +53,7 @@ export default defineConfig({
],
},
},
presets: [presetUno()],
presets: [presetWind3()],
},
},
});
+166
View File
@@ -1,5 +1,171 @@
# Changelog
## v0.20.17
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.16...wxt-v0.20.17)
### 🩹 Fixes
- Update left-over CJS require of `publish-browser-extension` to ESM ([#2120](https://github.com/wxt-dev/wxt/pull/2120))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.16
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.15...wxt-v0.20.16)
### 🩹 Fixes
- Revert "rename `name` to `names` prop of `rollup` to fix deprecated warning " ([#2106](https://github.com/wxt-dev/wxt/pull/2106))
### 🏡 Chore
- Speed up E2E tests by skipping `pnpm install` when possible ([#2113](https://github.com/wxt-dev/wxt/pull/2113))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.15
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.14...wxt-v0.20.15)
### 🚀 Enhancements
- Export `normalizePath` from `wxt` module ([#2080](https://github.com/wxt-dev/wxt/pull/2080))
- **popup:** Add Firefox `default_area` and `theme_icons` support ([#2097](https://github.com/wxt-dev/wxt/pull/2097))
- Add log `--level` cli option ([#1973](https://github.com/wxt-dev/wxt/pull/1973))
### 🩹 Fixes
- Content script is incorrectly invalidated when injected multiple times ([#2035](https://github.com/wxt-dev/wxt/pull/2035))
- Use `uiContainer` to apply position when creating shadow root ui ([#2036](https://github.com/wxt-dev/wxt/pull/2036))
- Make `ExtensionRunner#closeBrowser` optional ([#2092](https://github.com/wxt-dev/wxt/pull/2092))
- Rename `name` to `names` prop of `rollup` to fix deprecated warning ([#2106](https://github.com/wxt-dev/wxt/pull/2106))
### 💅 Refactors
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
- Cleanup esm ([#1950](https://github.com/wxt-dev/wxt/pull/1950))
- `async`/`await` cleanup ([#2088](https://github.com/wxt-dev/wxt/pull/2088))
- Cleanup unnecssary checks ([#2089](https://github.com/wxt-dev/wxt/pull/2089))
- Simplify import paths ([#2093](https://github.com/wxt-dev/wxt/pull/2093))
### 📖 Documentation
- Fix JSDoc ([6ae5fb5e](https://github.com/wxt-dev/wxt/commit/6ae5fb5e))
### 🏡 Chore
- Cleanup comment line lengths ([#2090](https://github.com/wxt-dev/wxt/pull/2090))
- Cleanup `@ts-*` comments ([#2094](https://github.com/wxt-dev/wxt/pull/2094))
- Update JSDoc to use `@internal` ([#2091](https://github.com/wxt-dev/wxt/pull/2091))
- Fix typos ([#2086](https://github.com/wxt-dev/wxt/pull/2086))
- Remove unused code ([#2087](https://github.com/wxt-dev/wxt/pull/2087))
- Simplify imports of `cli/__tests__/index.ts` ([#2104](https://github.com/wxt-dev/wxt/pull/2104))
### ❤️ Contributors
- Nick Doan ([@nickbar01234](https://github.com/nickbar01234))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Wotan-allfather <wotan-ai@proton.me>
- Namu ([@namuorg](https://github.com/namuorg))
- Eli ([@lishaduck](https://github.com/lishaduck))
## v0.20.14
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.13...wxt-v0.20.14)
### 🚀 Enhancements
- Add `onBeforeMount` callback to `createIframeUi` options ([#1899](https://github.com/wxt-dev/wxt/pull/1899))
### 🩹 Fixes
- Remove failing test ([6fb4cee4](https://github.com/wxt-dev/wxt/commit/6fb4cee4))
- Rename and make firefoxPrefs in web-ext.config.ts work ([#2068](https://github.com/wxt-dev/wxt/pull/2068))
- **content-script-context:** Handle undefined browser.runtime ([#2042](https://github.com/wxt-dev/wxt/pull/2042))
- `MaxListenersExceededWarning` ([#2058](https://github.com/wxt-dev/wxt/pull/2058))
### 📖 Documentation
- Fix alignment of 2 last comments of regex patterns ([#2012](https://github.com/wxt-dev/wxt/pull/2012))
### 🏡 Chore
- Use `tsdown` to build packages ([#2006](https://github.com/wxt-dev/wxt/pull/2006))
- Move script-only dev dependencies to top-level `package.json` ([#2007](https://github.com/wxt-dev/wxt/pull/2007))
- Update dependencies ([#2069](https://github.com/wxt-dev/wxt/pull/2069))
- Upgrade major deps ([#2070](https://github.com/wxt-dev/wxt/pull/2070))
- Refresh lockfile and upgrade subdependencies ([#2071](https://github.com/wxt-dev/wxt/pull/2071))
### ❤️ Contributors
- Dareka ([@darekadareka](https://github.com/darekadareka))
- Mark ([@mjfaga](https://github.com/mjfaga))
- ariasuni ([@ariasuni](https://github.com/ariasuni))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Bohuslavsemenov ([@bohuslavsemenov](https://github.com/bohuslavsemenov))
## v0.20.13
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.12...wxt-v0.20.13)
### 🚀 Enhancements
- Remove script element immediately in injectScript ([#1761](https://github.com/wxt-dev/wxt/pull/1761))
- Add `modifyScript` option to `injectScript` ([#1762](https://github.com/wxt-dev/wxt/pull/1762))
- Make `injectScript` return the created script element ([#1838](https://github.com/wxt-dev/wxt/pull/1838))
- Support `.wxtrc` config file ([#1833](https://github.com/wxt-dev/wxt/pull/1833))
### 🩹 Fixes
- Make `injectScript` wait until script is actually loaded ([#1763](https://github.com/wxt-dev/wxt/pull/1763))
- Don't return promises from unlisted scripts that do not have an async `main` function ([#1907](https://github.com/wxt-dev/wxt/pull/1907))
### 💅 Refactors
- Use `script.text` instead of `innerHTML` in `injectScript` ([#1764](https://github.com/wxt-dev/wxt/pull/1764))
### ❤️ Contributors
- Rxliuli ([@rxliuli](https://github.com/rxliuli))
- Sebastian Landwehr <info@sebastianlandwehr.com>
- Johan Kiviniemi ([@ion1](https://github.com/ion1))
## v0.20.12
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.11...wxt-v0.20.12)
### 🚀 Enhancements
- Remove `data-wxt-*` attributes ([#1913](https://github.com/wxt-dev/wxt/pull/1913))
- Default to using `use_dynamic_url: true` for content script css files ([#1993](https://github.com/wxt-dev/wxt/pull/1993))
### 🩹 Fixes
- Wxt normal logs are drowned by `dotenv@17.0.0` ads ([#1883](https://github.com/wxt-dev/wxt/pull/1883))
- Optimize `splitShadowRootCss` ([#1934](https://github.com/wxt-dev/wxt/pull/1934))
- `wxt prepare` fails with the error "__vite_ssr_exportName__ is not defined" when using Vite 7 ([#1884](https://github.com/wxt-dev/wxt/pull/1884))
### 🏡 Chore
- Use `linkedom` a test instead of `happy-dom` ([#1957](https://github.com/wxt-dev/wxt/pull/1957))
- Add support for `vite-node` v5 ([#2001](https://github.com/wxt-dev/wxt/pull/2001))
- Upgrade dev and non-major prod dependencies ([#2000](https://github.com/wxt-dev/wxt/pull/2000))
- **dev-deps:** Upgrade `happy-dom` to address CVE-2025-61927 ([#2002](https://github.com/wxt-dev/wxt/pull/2002))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Jaguar Zhou ([@aiktb](https://github.com/aiktb))
- Alexander Harding <noreply@harding.dev>
- Florian Kühne ([@ZerGo0](https://github.com/ZerGo0))
## v0.20.11
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.10...wxt-v0.20.11)
@@ -4,4 +4,4 @@
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
* sub-dependency binaries to "node_modules/.bin")
*/
require('publish-browser-extension/cli');
import 'publish-browser-extension/cli';
-46
View File
@@ -1,46 +0,0 @@
import { defineBuildConfig } from 'unbuild';
import { version, exports } from './package.json';
import { readFile, writeFile } from 'fs/promises';
import {
virtualEntrypointModuleNames,
virtualModuleNames,
} from './src/core/utils/virtual-modules';
export default defineBuildConfig([
// Non-virtual modules can be transpiled with mkdist
{
entries: [
{
builder: 'mkdist',
input: 'src',
pattern: ['**/*', '!**/__tests__', '!**/*.md', '!virtual', '!@types'],
declaration: true,
},
],
hooks: {
async 'build:done'() {
// Replace any template variables in output files
await replaceVars('dist/version.mjs', { version });
},
},
},
// Virtual modules must be bundled individually
...virtualModuleNames.map((moduleName) => ({
entries: [`src/virtual/${moduleName}.ts`],
externals: [
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
'virtual:wxt-plugins',
'virtual:app-config',
...Object.keys(exports).map((path) => 'wxt' + path.slice(1)), // ./utils/storage => wxt/utils/storage
],
})),
]);
async function replaceVars(file: string, vars: Record<string, string>) {
let text = await readFile(file, 'utf8');
Object.entries(vars).forEach(([name, value]) => {
text = text.replaceAll(`{{${name}}}`, value);
});
await writeFile(file, text, 'utf8');
}
@@ -47,6 +47,7 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"getAppConfig": true,
"injectScript": true,
"storage": true,
"useAppConfig": true
@@ -101,6 +102,7 @@ const globals = {
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"getAppConfig": true,
"injectScript": true,
"storage": true,
"useAppConfig": true
@@ -164,6 +166,7 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"getAppConfig": true,
"injectScript": true,
"storage": true,
"useAppConfig": true
@@ -219,6 +222,7 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
"defineUnlistedScript": "readonly",
"defineWxtPlugin": "readonly",
"fakeBrowser": "readonly",
"getAppConfig": "readonly",
"injectScript": "readonly",
"storage": "readonly",
"useAppConfig": "readonly"
+9 -9
View File
@@ -30,8 +30,8 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
expect(await project.pathExists('stats.html')).toBe(true);
expect(await project.pathExists('.output/chrome-mv3/stats-0.json')).toBe(
false,
);
});
@@ -52,9 +52,9 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.fileExists('stats-0.json')).toBe(true);
expect(await project.fileExists('stats-1.json')).toBe(true);
expect(await project.pathExists('stats.html')).toBe(true);
expect(await project.pathExists('stats-0.json')).toBe(true);
expect(await project.pathExists('stats-1.json')).toBe(true);
});
it('should support customizing the stats output directory', async () => {
@@ -73,7 +73,7 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats/bundle.html')).toBe(true);
expect(await project.pathExists('stats/bundle.html')).toBe(true);
});
it('should place artifacts next to the custom output file', async () => {
@@ -93,9 +93,9 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats/bundle.html')).toBe(true);
expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
expect(await project.pathExists('stats/bundle.html')).toBe(true);
expect(await project.pathExists('stats/bundle-0.json')).toBe(true);
expect(await project.pathExists('stats/bundle-1.json')).toBe(true);
});
it('should open the stats in the browser when requested', async () => {
+22 -35
View File
@@ -17,22 +17,23 @@ describe('Auto Imports', () => {
// Generated by wxt
export {}
declare global {
const ContentScriptContext: typeof import('wxt/utils/content-script-context')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/utils/match-patterns')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/utils/match-patterns')['MatchPattern']
const browser: typeof import('wxt/browser')['browser']
const createIframeUi: typeof import('wxt/utils/content-script-ui/iframe')['createIframeUi']
const createIntegratedUi: typeof import('wxt/utils/content-script-ui/integrated')['createIntegratedUi']
const createShadowRootUi: typeof import('wxt/utils/content-script-ui/shadow-root')['createShadowRootUi']
const defineAppConfig: typeof import('wxt/utils/define-app-config')['defineAppConfig']
const defineBackground: typeof import('wxt/utils/define-background')['defineBackground']
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 injectScript: typeof import('wxt/utils/inject-script')['injectScript']
const storage: typeof import('wxt/utils/storage')['storage']
const useAppConfig: typeof import('wxt/utils/app-config')['useAppConfig']
const ContentScriptContext: typeof import('wxt/utils/content-script-context').ContentScriptContext
const InvalidMatchPattern: typeof import('wxt/utils/match-patterns').InvalidMatchPattern
const MatchPattern: typeof import('wxt/utils/match-patterns').MatchPattern
const browser: typeof import('wxt/browser').browser
const createIframeUi: typeof import('wxt/utils/content-script-ui/iframe').createIframeUi
const createIntegratedUi: typeof import('wxt/utils/content-script-ui/integrated').createIntegratedUi
const createShadowRootUi: typeof import('wxt/utils/content-script-ui/shadow-root').createShadowRootUi
const defineAppConfig: typeof import('wxt/utils/define-app-config').defineAppConfig
const defineBackground: typeof import('wxt/utils/define-background').defineBackground
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 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
const useAppConfig: typeof import('wxt/utils/app-config').useAppConfig
}
// for type re-export
declare global {
@@ -111,7 +112,7 @@ describe('Auto Imports', () => {
declare module '#imports' {
export { browser, Browser } from 'wxt/browser';
export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
@@ -142,7 +143,7 @@ describe('Auto Imports', () => {
await project.prepare();
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
expect(await project.pathExists('.wxt/types/imports.d.ts')).toBe(false);
});
it('should only include imports-module.d.ts in the the project', async () => {
@@ -196,7 +197,7 @@ describe('Auto Imports', () => {
declare module '#imports' {
export { browser, Browser } from 'wxt/browser';
export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { getAppConfig, useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
@@ -280,24 +281,10 @@ describe('Auto Imports', () => {
},
});
expect(await project.fileExists('.wxt/eslint-auto-imports.mjs')).toBe(
expect(await project.pathExists('.wxt/eslint-auto-imports.mjs')).toBe(
false,
);
expect(await project.fileExists('.wxt/eslintrc-auto-import.json')).toBe(
false,
);
});
it('should NOT output an ESlint config file by default', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.prepare();
expect(await project.fileExists('.wxt/eslint-auto-imports.mjs')).toBe(
false,
);
expect(await project.fileExists('.wxt/eslintrc-auto-import.json')).toBe(
expect(await project.pathExists('.wxt/eslintrc-auto-import.json')).toBe(
false,
);
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { WxtHooks } from '../../src/types';
import type { WxtHooks } from '../../src';
const hooks: WxtHooks = {
ready: vi.fn(),
+7 -7
View File
@@ -1,8 +1,8 @@
import { describe, it, expect, vi } from 'vitest';
import { TestProject } from '../utils';
import type { GenericEntrypoint, InlineConfig } from '../../src/types';
import type { InlineConfig, UnlistedScriptEntrypoint } from '../../src';
import { readFile } from 'fs-extra';
import { normalizePath } from '../../src/core/utils/paths';
import { normalizePath } from '../../src';
describe('Module Helpers', () => {
describe('options', () => {
@@ -49,7 +49,7 @@ describe('Module Helpers', () => {
'export default defineBackground(() => {})',
);
const entrypoint: GenericEntrypoint = {
const entrypoint: UnlistedScriptEntrypoint = {
type: 'unlisted-script',
inputPath: project.resolvePath('modules/test/injected.ts'),
name: 'injected',
@@ -77,7 +77,7 @@ describe('Module Helpers', () => {
await project.build(config);
expect(await project.fileExists('.output/chrome-mv3/injected.js')).toBe(
expect(await project.pathExists('.output/chrome-mv3/injected.js')).toBe(
true,
);
});
@@ -118,10 +118,10 @@ describe('Module Helpers', () => {
fileName: 'module.txt',
});
await expect(
project.fileExists('.output/chrome-mv3/module.txt'),
project.pathExists('.output/chrome-mv3/module.txt'),
).resolves.toBe(true);
await expect(
project.fileExists('.output/chrome-mv3/example/generated.txt'),
project.pathExists('.output/chrome-mv3/example/generated.txt'),
).resolves.toBe(true);
});
@@ -298,7 +298,7 @@ describe('Module Helpers', () => {
await expect(
project.serializeFile('.wxt/types/imports.d.ts'),
).resolves.toContain("const ref: typeof import('vue')['ref']");
).resolves.toContain("const ref: typeof import('vue').ref");
});
});
});
+183 -91
View File
@@ -126,7 +126,7 @@ describe('Output Directory Structure', () => {
await project.build({ browser: 'firefox' });
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(
false,
);
});
@@ -146,7 +146,7 @@ describe('Output Directory Structure', () => {
await project.build({ browser: 'chrome' });
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(
false,
);
});
@@ -174,7 +174,7 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.pathExists('stats.html')).toBe(true);
});
it('should support JavaScript entrypoints', async () => {
@@ -210,14 +210,14 @@ describe('Output Directory Structure', () => {
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
`);
expect(await project.fileExists('.output/chrome-mv3/background.js'));
expect(await project.pathExists('.output/chrome-mv3/background.js'));
expect(
await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
await project.pathExists('.output/chrome-mv3/content-scripts/content.js'),
);
expect(
await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
await project.pathExists('.output/chrome-mv3/content-scripts/named.js'),
);
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
expect(await project.pathExists('.output/chrome-mv3/unlisted.js'));
});
it('should support CSS entrypoints', async () => {
@@ -300,7 +300,7 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
expect(await project.pathExists('dist/chrome-mv3/manifest.json')).toBe(
true,
);
});
@@ -335,7 +335,7 @@ describe('Output Directory Structure', () => {
await project.build({
vite: () => ({
build: {
// Make output for snapshot readible
// Make output for the snapshot readable
minify: false,
},
}),
@@ -343,44 +343,40 @@ describe('Output Directory Structure', () => {
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
import { l as logHello, i as initPlugins } from "./chunks/_virtual_wxt-plugins-OjKtWpmY.js";
function defineBackground(arg) {
if (arg == null || typeof arg === "function") return { main: arg };
return arg;
}
const definition = defineBackground({
type: "module",
main() {
logHello("background");
".output/chrome-mv3/background.js
----------------------------------------
import { l as logHello, i as initPlugins } from "./chunks/_virtual_wxt-plugins-OjKtWpmY.js";
function defineBackground(arg) {
if (arg == null || typeof arg === "function") return { main: arg };
return arg;
}
});
globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome;
function print(method, ...args) {
return;
}
const logger = {
debug: (...args) => print(console.debug, ...args),
log: (...args) => print(console.log, ...args),
warn: (...args) => print(console.warn, ...args),
error: (...args) => print(console.error, ...args)
};
let result;
try {
initPlugins();
result = definition.main();
if (result instanceof Promise) {
console.warn(
"The background's main() function return a promise, but it must be synchronous"
);
const definition = defineBackground({
type: "module",
main() {
logHello("background");
}
});
globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome;
function print(method, ...args) {
return;
}
} catch (err) {
logger.error("The background crashed on startup!");
throw err;
}
"
`);
const logger = {
debug: (...args) => print(console.debug, ...args),
log: (...args) => print(console.log, ...args),
warn: (...args) => print(console.warn, ...args),
error: (...args) => print(console.error, ...args)
};
let result;
try {
initPlugins();
result = definition.main();
if (result instanceof Promise) console.warn("The background's main() function return a promise, but it must be synchronous");
} catch (err) {
logger.error("The background crashed on startup!");
throw err;
}
"
`);
});
it('should generate IIFE background script when type=undefined', async () => {
@@ -412,7 +408,7 @@ describe('Output Directory Structure', () => {
await project.build({
vite: () => ({
build: {
// Make output for snapshot readible
// Make output for the snapshot readable
minify: false,
},
}),
@@ -420,51 +416,147 @@ describe('Output Directory Structure', () => {
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
var background = (function() {
"use strict";
function defineBackground(arg) {
if (arg == null || typeof arg === "function") return { main: arg };
return arg;
".output/chrome-mv3/background.js
----------------------------------------
var background = (function() {
"use strict";
function defineBackground(arg) {
if (arg == null || typeof arg === "function") return { main: arg };
return arg;
}
function logHello(name) {
console.log(\`Hello \${name}!\`);
}
const definition = defineBackground({
main() {
logHello("background");
}
function logHello(name) {
console.log(\`Hello \${name}!\`);
}
const definition = defineBackground({
main() {
logHello("background");
}
});
function initPlugins() {
}
globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome;
function print(method, ...args) {
return;
}
const logger = {
debug: (...args) => print(console.debug, ...args),
log: (...args) => print(console.log, ...args),
warn: (...args) => print(console.warn, ...args),
error: (...args) => print(console.error, ...args)
};
let result;
try {
initPlugins();
result = definition.main();
if (result instanceof Promise) {
console.warn(
"The background's main() function return a promise, but it must be synchronous"
);
}
} catch (err) {
logger.error("The background crashed on startup!");
throw err;
}
const result$1 = result;
return result$1;
})();
"
`);
});
function initPlugins() {
}
globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome;
function print(method, ...args) {
return;
}
const logger = {
debug: (...args) => print(console.debug, ...args),
log: (...args) => print(console.log, ...args),
warn: (...args) => print(console.warn, ...args),
error: (...args) => print(console.error, ...args)
};
let result;
try {
initPlugins();
result = definition.main();
if (result instanceof Promise) console.warn("The background's main() function return a promise, but it must be synchronous");
} catch (err) {
logger.error("The background crashed on startup!");
throw err;
}
var background_entrypoint_default = result;
return background_entrypoint_default;
})();
"
`);
});
describe('globalName option', () => {
it('generates an IIFE with a default name', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(/^var content\s?=[\s\S]*^content;$/gm);
});
it('generates an IIFE with a specific name', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: "MyContentScript",
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(
/^var MyContentScript =[\s\S]*^MyContentScript;$/gm,
);
});
it('generates an IIFE with a specific name provided by a function', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: () => "MyContentScript",
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(
/^var MyContentScript =[\s\S]*^MyContentScript;$/gm,
);
});
it('generates an anonymous IIFE when not minified', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: false,
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: false } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(/^\(function\(\) {[\s\S]*^}\)\(\);$/gm);
});
it('generates an anonymous IIFE when minified', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content.js',
`export default defineContentScript({
globalName: false,
matches: ["*://*/*"],
main() {},
})`,
);
await project.build({ vite: () => ({ build: { minify: true } }) });
const output = await project.serializeFile(
'.output/chrome-mv3/content-scripts/content.js',
);
expect(output).toMatch(/^\(function\(\){[\s\S]*}\)\(\);$/gm);
});
});
});
+1 -1
View File
@@ -31,7 +31,7 @@ describe('React', () => {
await project.build();
expect(
await project.fileExists('.output/chrome-mv3/content-scripts/demo.js'),
await project.pathExists('.output/chrome-mv3/content-scripts/demo.js'),
).toBe(true);
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
+2 -2
View File
@@ -16,11 +16,11 @@ describe('Remote Code', () => {
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.com',
'__lodash_placeholder__',
);
expect(output).not.toContain(url);
expect(
await project.fileExists(`.wxt/cache/${encodeURIComponent(url)}`),
await project.pathExists(`.wxt/cache/${encodeURIComponent(url)}`),
).toBe(true);
});
});
@@ -63,6 +63,29 @@ describe('TypeScript Project', () => {
`);
});
it('should include CSS entrypoints in browser.runtime.getURL paths', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html', '<html></html>');
project.addFile(
'entrypoints/plain.css',
`body {
color: red;
}`,
);
project.addFile(
'entrypoints/overlay.content.css',
`body {
color: blue;
}`,
);
await project.prepare();
const output = await project.serializeFile('.wxt/types/paths.d.ts');
expect(output).toContain('| "/plain.css"');
expect(output).toContain('| "/content-scripts/overlay.css"');
});
it('should augment the types for browser.i18n.getMessage', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html', '<html></html>');
+6 -8
View File
@@ -108,7 +108,7 @@ describe('User Config', () => {
await project.build({ configFile: 'test.config.ts' });
expect(
await project.fileExists('.custom-output/chrome-mv3/background.js'),
await project.pathExists('.custom-output/chrome-mv3/background.js'),
).toBe(true);
});
@@ -129,27 +129,25 @@ describe('User Config', () => {
await project.build();
expect(
await project.fileExists('.output/test-chrome-mv3-production-build'),
await project.pathExists('.output/test-chrome-mv3-production-build'),
).toBe(true);
await project.build({ mode: 'development' });
expect(
await project.fileExists('.output/test-chrome-mv3-development-dev-build'),
await project.pathExists('.output/test-chrome-mv3-development-dev-build'),
).toBe(true);
});
it('should throw error when config file not exist', async () => {
it('should not throw error when config file not exist', async () => {
const project = new TestProject();
project.addFile(
'src/entrypoints/background.ts',
'entrypoints/background.ts',
`export default defineBackground(
() => console.log('Hello background'),
);`,
);
await expect(
project.build({ configFile: 'foo.config.ts' }),
).rejects.toThrowError(/not found/);
await project.build({ configFile: 'foo.config.ts' });
});
});
+18 -18
View File
@@ -26,7 +26,7 @@ describe('Zipping', () => {
browser: 'firefox',
zip: { downloadPackages: ['flatten'] },
});
expect(await project.fileExists('.output/')).toBe(true);
expect(await project.pathExists('.output/')).toBe(true);
await extract(sourcesZip, { dir: unzipDir });
// Update package json wxt path
@@ -51,7 +51,7 @@ describe('Zipping', () => {
}),
).resolves.not.toHaveProperty('exitCode');
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
await expect(project.pathExists(unzipDir, '.output')).resolves.toBe(true);
expect(
await project.serializeFile(
project.resolvePath(unzipDir, 'package.json'),
@@ -95,8 +95,8 @@ describe('Zipping', () => {
},
});
expect(await project.fileExists(artifactZip)).toBe(true);
expect(await project.fileExists(sourcesZip)).toBe(true);
expect(await project.pathExists(artifactZip)).toBe(true);
expect(await project.pathExists(sourcesZip)).toBe(true);
});
it('should not zip hidden files into sources by default', async () => {
@@ -117,8 +117,8 @@ describe('Zipping', () => {
browser: 'firefox',
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, '.env')).toBe(false);
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
expect(await project.pathExists(unzipDir, '.env')).toBe(false);
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
});
it('should not zip files inside hidden directories if only the directory is specified', async () => {
@@ -142,8 +142,8 @@ describe('Zipping', () => {
},
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file')).toBe(
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file')).toBe(
false,
);
});
@@ -171,12 +171,12 @@ describe('Zipping', () => {
},
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, '.env')).toBe(true);
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(true);
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
expect(await project.pathExists(unzipDir, '.env')).toBe(true);
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(true);
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
true,
);
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
true,
);
});
@@ -210,10 +210,10 @@ describe('Zipping', () => {
});
await extract(sourcesZip, { dir: unzipDir });
expect(
await project.fileExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
await project.pathExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
).toBe(false);
expect(
await project.fileExists(unzipDir, 'entrypoints/all.content.ts'),
await project.pathExists(unzipDir, 'entrypoints/all.content.ts'),
).toBe(true);
});
@@ -234,7 +234,7 @@ describe('Zipping', () => {
browser,
});
expect(await project.fileExists(sourcesZip)).toBe(true);
expect(await project.pathExists(sourcesZip)).toBe(true);
},
);
@@ -258,7 +258,7 @@ describe('Zipping', () => {
},
});
expect(await project.fileExists(sourcesZip)).toBe(true);
expect(await project.pathExists(sourcesZip)).toBe(true);
},
);
@@ -282,7 +282,7 @@ describe('Zipping', () => {
},
});
expect(await project.fileExists(sourcesZip)).toBe(false);
expect(await project.pathExists(sourcesZip)).toBe(false);
},
);
@@ -305,6 +305,6 @@ describe('Zipping', () => {
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, 'manifest.json')).toBe(true);
expect(await project.pathExists(unzipDir, 'manifest.json')).toBe(true);
});
});
+24 -19
View File
@@ -1,7 +1,8 @@
import { dirname, relative, resolve } from 'path';
import fs, { mkdir } from 'fs-extra';
import glob from 'fast-glob';
import fs, { mkdir } from 'fs-extra';
import merge from 'lodash.merge';
import spawn from 'nano-spawn';
import { dirname, relative, resolve } from 'path';
import {
InlineConfig,
UserConfig,
@@ -10,8 +11,7 @@ import {
prepare,
zip,
} from '../src';
import { normalizePath } from '../src/core/utils/paths';
import merge from 'lodash.merge';
import { normalizePath } from '../src/core/utils';
// Run "pnpm wxt" to use the "wxt" dev script, not the "wxt" binary from the
// wxt package. This uses the TS files instead of the compiled JS package
@@ -24,6 +24,7 @@ export class TestProject {
files: Array<[string, string]> = [];
config: UserConfig | undefined;
readonly root: string;
readonly hasCustomDependencies: boolean;
constructor(packageJson: any = {}) {
// We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts
@@ -31,6 +32,13 @@ export class TestProject {
// end to make each test's path unique.
const id = Math.random().toString(32).substring(3);
this.root = resolve(E2E_DIR, 'dist', id);
this.hasCustomDependencies =
Object.keys({
...packageJson.dependencies,
...packageJson.devDependencies,
}).length > 0;
this.files.push([
'package.json',
JSON.stringify(
@@ -119,9 +127,14 @@ export class TestProject {
await fs.writeFile(filePath, content ?? '', 'utf-8');
}
await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], {
cwd: this.root,
});
// Only install dependencies if the project has custom ones - otherwise the
// project will reuse the ones in `packages/wxt/node_modules`!
if (this.hasCustomDependencies) {
await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], {
cwd: this.root,
});
}
await mkdir(resolve(this.root, 'public'), { recursive: true }).catch(
() => {},
);
@@ -138,14 +151,6 @@ export class TestProject {
return this.serializeDir('.output', ignoreContentsOfFilenames);
}
/**
* Read all the files from the test project's `.wxt` directory and combine them into a string
* that can be used in a snapshot.
*/
serializeWxtDir(): Promise<string> {
return this.serializeDir(resolve(this.root, '.wxt/types'));
}
/**
* Deeply print the filename and contents of all files in a directory.
*
@@ -185,13 +190,13 @@ export class TestProject {
].join(`\n${''.padEnd(40, '-')}\n`);
}
fileExists(...path: string[]): Promise<boolean> {
return fs.exists(this.resolvePath(...path));
pathExists(...path: string[]): Promise<boolean> {
return fs.pathExists(this.resolvePath(...path));
}
async getOutputManifest(
getOutputManifest(
path: string = '.output/chrome-mv3/manifest.json',
): Promise<any> {
return await fs.readJson(this.resolvePath(path));
return fs.readJson(this.resolvePath(path));
}
}
+3
View File
@@ -0,0 +1,3 @@
declare namespace globalThis {
var __ENTRYPOINT__: string;
}
+61 -55
View File
@@ -1,12 +1,12 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.11",
"version": "0.20.17",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
"wxt": "tsx src/cli/index.ts",
"build": "buildc -- unbuild",
"build": "buildc -- tsdown --config-loader unrun",
"check": "pnpm build && pnpm run --reporter-hide-prefix /^check:.*/",
"check:default": "check",
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
@@ -18,69 +18,75 @@
"dependencies": {
"@1natsu/wait-element": "^4.1.2",
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@webext-core/fake-browser": "^1.3.2",
"@webext-core/isolated-element": "^1.1.2",
"@webext-core/fake-browser": "^1.3.4",
"@webext-core/isolated-element": "^1.1.3",
"@webext-core/match-patterns": "^1.0.3",
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0",
"c12": "^3.2.0",
"c12": "^3.3.3",
"cac": "^6.7.14",
"chokidar": "^4.0.3",
"ci-info": "^4.3.0",
"chokidar": "^5.0.0",
"ci-info": "^4.4.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
"dotenv": "^17.2.2",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
"esbuild": "^0.25.0",
"esbuild": "^0.27.1",
"fast-glob": "^3.3.3",
"filesize": "^11.0.2",
"fs-extra": "^11.3.1",
"filesize": "^11.0.13",
"fs-extra": "^11.3.3",
"get-port-please": "^3.2.0",
"giget": "^1.2.3 || ^2.0.0",
"hookable": "^5.5.3",
"giget": "^1.2.3 || ^2.0.0 || ^3.0.0",
"hookable": "^6.0.1",
"import-meta-resolve": "^4.2.0",
"is-wsl": "^3.1.0",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.18.12",
"magicast": "^0.3.5",
"minimatch": "^10.0.3",
"nano-spawn": "^1.0.2",
"magicast": "^0.5.2",
"minimatch": "^10.1.2",
"nano-spawn": "^2.0.0",
"normalize-path": "^3.0.0",
"nypm": "^0.6.1",
"nypm": "^0.6.5",
"ohash": "^2.0.11",
"open": "^10.2.0",
"ora": "^8.2.0",
"perfect-debounce": "^2.0.0",
"open": "^11.0.0",
"ora": "^9.3.0",
"perfect-debounce": "^2.1.0",
"picocolors": "^1.1.1",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2",
"publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.0",
"scule": "^1.3.0",
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0",
"vite": "^5.4.19 || ^6.3.4 || ^7.0.0",
"vite-node": "^2.1.4 || ^3.1.2",
"vite-node": "^3.2.4 || ^5.0.0",
"web-ext-run": "^0.2.4"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
},
"devDependencies": {
"@aklinker1/check": "^2.1.0",
"@faker-js/faker": "^10.0.0",
"@faker-js/faker": "^10.2.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^20.17.6",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"eslint": "^9.38.0",
"extract-zip": "^2.0.1",
"happy-dom": "^18.0.1",
"happy-dom": "^20.5.0",
"lodash.merge": "^4.6.2",
"oxlint": "^1.14.0",
"publint": "^0.3.12",
"typescript": "^5.9.2",
"unbuild": "^3.6.1",
"vitest": "^3.2.4",
"vitest-plugin-random-seed": "^1.1.1"
"oxlint": "^1.43.0",
"publint": "^0.3.17",
"typescript": "^5.9.3",
"vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2"
},
"peerDependenciesMeta": {},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git"
@@ -106,96 +112,96 @@
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
"wxt-publish-extension": "./bin/wxt-publish-extension.mjs"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./utils/app-config": {
"types": "./dist/utils/app-config.d.ts",
"types": "./dist/utils/app-config.d.mts",
"default": "./dist/utils/app-config.mjs"
},
"./utils/inject-script": {
"types": "./dist/utils/inject-script.d.ts",
"types": "./dist/utils/inject-script.d.mts",
"default": "./dist/utils/inject-script.mjs"
},
"./utils/content-script-context": {
"types": "./dist/utils/content-script-context.d.ts",
"types": "./dist/utils/content-script-context.d.mts",
"default": "./dist/utils/content-script-context.mjs"
},
"./utils/content-script-ui/types": {
"types": "./dist/utils/content-script-ui/types.d.ts",
"types": "./dist/utils/content-script-ui/types.d.mts",
"default": "./dist/utils/content-script-ui/types.mjs"
},
"./utils/content-script-ui/integrated": {
"types": "./dist/utils/content-script-ui/integrated.d.ts",
"types": "./dist/utils/content-script-ui/integrated.d.mts",
"default": "./dist/utils/content-script-ui/integrated.mjs"
},
"./utils/content-script-ui/shadow-root": {
"types": "./dist/utils/content-script-ui/shadow-root.d.ts",
"types": "./dist/utils/content-script-ui/shadow-root.d.mts",
"default": "./dist/utils/content-script-ui/shadow-root.mjs"
},
"./utils/content-script-ui/iframe": {
"types": "./dist/utils/content-script-ui/iframe.d.ts",
"types": "./dist/utils/content-script-ui/iframe.d.mts",
"default": "./dist/utils/content-script-ui/iframe.mjs"
},
"./utils/define-app-config": {
"types": "./dist/utils/define-app-config.d.ts",
"types": "./dist/utils/define-app-config.d.mts",
"default": "./dist/utils/define-app-config.mjs"
},
"./utils/define-background": {
"types": "./dist/utils/define-background.d.ts",
"types": "./dist/utils/define-background.d.mts",
"default": "./dist/utils/define-background.mjs"
},
"./utils/define-content-script": {
"types": "./dist/utils/define-content-script.d.ts",
"types": "./dist/utils/define-content-script.d.mts",
"default": "./dist/utils/define-content-script.mjs"
},
"./utils/define-unlisted-script": {
"types": "./dist/utils/define-unlisted-script.d.ts",
"types": "./dist/utils/define-unlisted-script.d.mts",
"default": "./dist/utils/define-unlisted-script.mjs"
},
"./utils/define-wxt-plugin": {
"types": "./dist/utils/define-wxt-plugin.d.ts",
"types": "./dist/utils/define-wxt-plugin.d.mts",
"default": "./dist/utils/define-wxt-plugin.mjs"
},
"./utils/match-patterns": {
"types": "./dist/utils/match-patterns.d.ts",
"types": "./dist/utils/match-patterns.d.mts",
"default": "./dist/utils/match-patterns.mjs"
},
"./utils/split-shadow-root-css": {
"types": "./dist/utils/split-shadow-root-css.d.ts",
"types": "./dist/utils/split-shadow-root-css.d.mts",
"default": "./dist/utils/split-shadow-root-css.mjs"
},
"./utils/storage": {
"types": "./dist/utils/storage.d.ts",
"types": "./dist/utils/storage.d.mts",
"default": "./dist/utils/storage.mjs"
},
"./browser": {
"types": "./dist/browser.d.ts",
"types": "./dist/browser.d.mts",
"default": "./dist/browser.mjs"
},
"./testing/fake-browser": {
"types": "./dist/testing/fake-browser.d.ts",
"types": "./dist/testing/fake-browser.d.mts",
"default": "./dist/testing/fake-browser.mjs"
},
"./testing/vitest-plugin": {
"types": "./dist/testing/wxt-vitest-plugin.d.ts",
"types": "./dist/testing/wxt-vitest-plugin.d.mts",
"default": "./dist/testing/wxt-vitest-plugin.mjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"types": "./dist/testing/index.d.mts",
"default": "./dist/testing/index.mjs"
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
},
"./modules": {
"types": "./dist/modules.d.ts",
"types": "./dist/modules.d.mts",
"default": "./dist/modules.mjs"
}
}
-17
View File
@@ -1,22 +1,5 @@
// Custom TS definitions for non-TS packages
declare module 'zip-dir' {
// Represents the options object for zipdir function
interface ZipDirOptions {
saveTo?: string;
filter?: (path: string, stat: import('fs').Stats) => boolean;
each?: (path: string) => void;
}
function zipdir(
dirPath: string,
options?: ZipDirOptions,
callback?: (error: Error | null, buffer: Buffer) => void,
): Promise<Buffer>;
export = zipdir;
}
declare module 'web-ext-run' {
export interface WebExtRunInstance {
reloadAllExtensions(): Promise<void>;

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