Compare commits

...

414 Commits

Author SHA1 Message Date
Aaron d5fe4248e2 bump version for early release 2024-04-19 20:34:36 -05:00
Aaron 58140cf3e8 update test snapshot 2024-04-19 20:33:21 -05:00
Aaron 1a39f856af get it working 2024-04-19 20:31:32 -05:00
Aaron 82b769e939 feat: Support returning values from scripts executed with the scripting API 2024-04-19 16:25:17 -05:00
Aaron e47519fb76 docs: Update content script registration JSDoc
See https://github.com/wxt-dev/wxt/discussions/623
2024-04-19 15:59:14 -05:00
Ayden 171e32c272 docs: Add ElemSnap to homepage (#621) 2024-04-19 08:00:41 -05:00
wuzequanyouzi 54e5ed0bb4 chore: Update vue template config (#607)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-04-15 12:43:43 -05:00
Aaron a94d673f31 docs: Fix typo in assets page 2024-04-15 12:17:02 -05:00
yacine-bens b7f7222c26 docs: Add Facebook Video Controls to homepage (#615) 2024-04-15 12:13:06 -05:00
Can Rau aa5a9d4fa9 docs: Tix typo in entrypoints.md (#614) 2024-04-14 17:21:12 -05:00
日高 凌 77da2d6ade docs: Fix typo on example for wxt.config.ts. (#609) 2024-04-12 09:00:15 -05:00
Bas van Zanten 3119c04e10 feat: Add zip compression settings (#605) 2024-04-12 08:59:54 -05:00
couriourc c09994263e doc: Fix bad extension API examples (#608) 2024-04-12 01:01:07 -05:00
ThinkStu 2a4fa9ae5c docs: Add Fluent Read to homepage (#600)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-04-07 09:01:02 -05:00
Aaron c833d3b15b Update test snapshot after vite upgrade 2024-04-07 08:53:15 -05:00
Aaron d6e095bfce Fix type error 2024-04-07 08:47:52 -05:00
Aaron 825160a3cc chore(deps): Upgrade vite 2024-04-03 14:16:51 -05:00
Aaron e29ca33f82 Add missing cleanup from PR 2024-04-03 14:15:55 -05:00
GitHub Actions ca5a52c6f1 chore(release): v0.17.12 2024-04-03 19:04:03 +00:00
Aaron 982c69f8ae feat: Add hooks for extending vite config (#599) 2024-04-03 14:00:48 -05:00
Aaron 6fc7d9254a fix(content-script-ui): Properly assign and unassign mounted value (#598) 2024-04-03 12:38:46 -05:00
dependabot[bot] 2e0e104d48 chore(deps): bump c12 from 1.5.1 to 1.10.0 (#592)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 23:23:11 -05:00
dependabot[bot] 02430fd7b8 chore(deps-dev): bump sass from 1.69.5 to 1.72.0 (#591)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 23:23:01 -05:00
dependabot[bot] 2e6d35d193 chore(deps-dev): bump vue from 3.3.10 to 3.4.21 (#589)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 23:08:37 -05:00
dependabot[bot] 7cf412ac1f chore(deps): bump vitest from 1.2.2 to 1.4.0 (#590)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 22:15:44 -05:00
Aaron 9398cbb596 docs: Add discord server link (#593) 2024-04-01 19:13:12 -05:00
dependabot[bot] ca9b96142e chore(deps-dev): bump @aklinker1/check from 1.1.1 to 1.2.0 (#588)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 18:10:28 -05:00
Aaron db57c8e079 chore: Remove unnecssary 'Omit' types 2024-03-31 08:40:21 -05:00
GitHub Actions 7e37e11caf chore(release): v0.17.11 2024-03-31 13:22:25 +00:00
Aaron bb90029c54 fix: Resolve absolute paths from the public directory properly (#583) 2024-03-31 08:19:06 -05:00
Aaron d3b14afbce chore: Extract helper function 2024-03-30 14:49:10 -05:00
Aaron ba2197a609 chore: Add some array utils (#582) 2024-03-30 13:30:45 -05:00
Aaron 4c558d3eb6 chore: Simplify virtual module setup (#581) 2024-03-30 12:44:27 -05:00
Qiwei Yang dcd175444a docs: Add "GitHub Custom Notifier" to homepage (#580) 2024-03-30 08:37:26 -05:00
Aaron 16fc58404a docs: Fix zip.includeSources example 2024-03-30 08:37:00 -05:00
GitHub Actions 28c7e7d9ed chore(release): v0.17.10 2024-03-29 05:52:30 +00:00
Zizheng Tai 5a3b42a4b0 feat: Add dev.server.port config (#577)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-29 00:49:36 -05:00
Aaron c9c5d721ec chore: Refactor order of config resolution (#578) 2024-03-29 00:21:09 -05:00
Aaron 1556b9ad73 chore: Use @aklinker1/check to simplify checks setup (#550) 2024-03-25 15:15:40 -05:00
GitHub Actions ca15b1f321 chore(release): v0.17.9 2024-03-25 06:13:23 +00:00
Aaron 448436cb08 chore: Merge user config using defu (#568) 2024-03-25 01:09:52 -05:00
Aaron c9ddc3d0f2 fix: Don't override wxt.config.ts options when CLI flags are not passed (#567) 2024-03-25 00:24:56 -05:00
Guillaume 6b72f883b6 feat: Add {{mode}} Template Variable (#566)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-25 00:01:59 -05:00
GitHub Actions cd3446bd7f chore(release): v0.17.8 2024-03-24 16:26:00 +00:00
Aaron 829b371bba fix(dev): Don't crash dev mode when rebuild fails (#565) 2024-03-24 11:23:16 -05:00
Aaron 36b399dd49 feat(analysis): Open stats.html file automatically (#564) 2024-03-24 11:05:34 -05:00
btea 08f32aa685 chore: Add a test covering wxt init in a non-empty directory (#563)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-24 10:15:59 -05:00
Vlad Fedosov 05db264e02 docs: Fixed "Reload the Extension" section (#559)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-20 16:52:52 -05:00
btea de5a01834c fix(init): Don't overwrite existing files when initializing a new project (#556)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-20 09:22:53 -05:00
Lpmvb 1d260dbacb chore(vue-template): Upgrade to vue-tsc v2 (#549) 2024-03-15 08:08:40 -05:00
Aaron 53c63b4556 chore: Switch to seed plugin for testing (#547) 2024-03-15 00:43:20 -05:00
btea 816878bc15 fix: Append option description error (#546) 2024-03-13 23:07:32 -05:00
Guillaume e087a0d6ef docs: Add examples for GitHub Actions (#540)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-13 23:07:17 -05:00
Aaron 56b71491a6 chore: Increase hook timeout for Windows/NPM tests 2024-03-13 12:34:59 -05:00
Aaron 121b521157 docs: Mention adding unlisted scripts and pages to web_accessible_resources
This closes #536
2024-03-12 07:57:38 -05:00
Sunshio 16c2e3218b docs: Add documentation for Bun.sh (#543)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-03-12 07:46:46 -05:00
Aaron d9cba55d34 chore: Increase unit test timeout 2024-03-12 07:45:42 -05:00
Guillaume 832cd5d640 docs: Add Plex skipper to "Using WXT" section (#541) 2024-03-10 09:03:18 -05:00
btea bb95e08d71 docs: Update README demo video (#539) 2024-03-10 09:03:03 -05:00
Aaron 35269dad9e docs: Update homepage demo video
This closes #535
2024-03-09 10:04:23 -06:00
btea 64d1498460 fix: Remove deprecated extension from Vue template (#534) 2024-03-08 21:03:56 -06:00
Luca Dalli 0d540a6df9 docs: Replace pnpx with pnpm dlx (#527) 2024-03-06 16:04:52 -06:00
Aaron b47c150daa fix(init): Better error logging when templates fail to load
This fixes #528
2024-03-06 07:46:26 -06:00
GitHub Actions 38345481df chore(release): v0.17.7 2024-03-04 16:29:56 +00:00
Aaron da92449e0f Undo change to log 2024-03-04 10:26:30 -06:00
Aaron 23b3980645 fix(zip): List .wxt/local_modules overrides relative to the package.json they're added to (#526) 2024-03-04 10:23:18 -06:00
Aaron 81b4b629c9 chore: Increase test timeout for windows NPM tests (#525) 2024-03-04 07:48:39 -06:00
Aaron 4f7e9cb050 chore: Bump templates to v0.17 (#524) 2024-03-04 07:32:18 -06:00
GitHub Actions b226d9b947 chore(release): v0.17.6 2024-03-03 16:05:15 +00:00
Aaron aabba41e86 fix(zip): List all private packages correctly in a PNPM workspace (#520) 2024-03-03 09:56:42 -06:00
Aaron f19e6908b0 feat: Automatically generate browser_action based on action for MV2 (#519) 2024-03-03 09:52:43 -06:00
Aaron 5f651cc3fe feat: Automatically remove top-level MV2-only or MV3-only keys (#518)
When targeting MV2, all MV3 keys will be removed. When targeting MV3, all MV2 keys will be removed.
2024-03-03 09:22:24 -06:00
Aaron 8a6acbaaf2 feat: Add warnings when important directories are missing (#516) 2024-03-02 08:56:20 -06:00
dependabot[bot] 13ab8e4d01 chore(deps-dev): bump @types/react from 18.2.34 to 18.2.61 (#510)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 08:55:51 -06:00
dependabot[bot] bd039dcbab chore(deps): bump chokidar from 3.5.3 to 3.6.0 (#511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 08:55:00 -06:00
Aaron Klinker 9cd4e8342b docs: Mentions moving folders into srcDir
Apart of #515
2024-03-02 08:42:31 -06:00
dependabot[bot] c9633cefec chore(deps): bump linkedom from 0.16.1 to 0.16.8 (#513)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-01 17:04:05 -06:00
GitHub Actions 1dd1370ed7 chore(release): v0.17.5 2024-03-01 18:01:15 +00:00
Aaron 9ff90d0acf feat: Download and override private packages for Firefox code review (#507) 2024-03-01 10:17:45 -06:00
Aaron Klinker b754435589 docs: Fix CLI generation 2024-02-29 12:13:28 -06:00
Aaron Klinker 195d2cceaf docs: Add docs about configuring the manifest as a function 2024-02-29 11:39:06 -06:00
Aaron 420d78b638 feat: Expose package management utils under wxt.pm (#502) 2024-02-28 21:38:35 -06:00
btea b35e499355 docs: Fix typos (#503) 2024-02-28 21:36:43 -06:00
Aaron 41c3d6d015 chore: Use JSZip for wxt zip, enabling future features (#501) 2024-02-28 14:57:29 -06:00
GitHub Actions 64610bad41 chore(release): v0.17.4 2024-02-28 18:28:34 +00:00
Aaron 45b7d7c67f fix: Resolve the path to node_modules/wxt correctly (#498) 2024-02-28 12:25:04 -06:00
Aaron 0bb2746869 feat: Add ResolvedConfig.wxtModuleDir, resolving the directory once (#497) 2024-02-28 11:52:46 -06:00
Aaron Klinker f9b0aa45f8 chore: Fix tests after template change 2024-02-28 11:34:15 -06:00
btea 6f6ce33bbd feat: Add basic content script to templates (#495)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-02-28 11:30:53 -06:00
Aaron Klinker e1c6020997 docs: Add testing example for ContentScriptContext
Apart of #491
2024-02-28 11:17:20 -06:00
Aaron Klinker 012bd7e67f docs: Update context usage 2024-02-28 11:16:56 -06:00
Aaron Klinker 35778f70cb docs: Update comparison page 2024-02-28 10:39:47 -06:00
Aaron Klinker cb4f9aa752 docs: Add CRXJS to comparison page 2024-02-28 08:05:39 -06:00
Aaron Klinker f80fb42483 docs: Fix typos 2024-02-28 07:42:22 -06:00
Leo Shklovskii 02c79ca244 docs: Added DocVersionRedirector to "Using WXT" section (#492) 2024-02-27 23:47:10 -06:00
Qiwei Yang 50e8c86ce9 doc: Add markdown sticky ntoes (#488) 2024-02-25 13:37:10 -06:00
GitHub Actions 6fbfc15be7 chore(release): v0.17.3 2024-02-24 16:43:31 +00:00
Aaron c793d3cf6b fix(storage): Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes (#487) 2024-02-24 10:40:03 -06:00
Aaron Klinker b5f4d8cc1b feat(storage): Guarantee storage.getItems returns values in the same order as requested 2024-02-24 08:56:45 -06:00
Aaron Klinker 77e6d1f5a4 fix: Content scripts crash when using storage.defineItem 2024-02-24 08:23:12 -06:00
GitHub Actions 3f149ac4fa chore(release): v0.17.2 2024-02-23 21:49:55 +00:00
Aaron c0dbffaa82 fix: Don't use sub-dependency binaries directly (#482) 2024-02-23 15:45:18 -06:00
Aaron Klinker 707f03a3eb Update changelog 2024-02-23 12:55:55 -06:00
GitHub Actions 8372462041 chore(release): v0.17.1 2024-02-23 18:54:07 +00:00
Aaron Klinker 3fbbe2ccde fix: Content scripts not loading in dev mode 2024-02-23 12:51:11 -06:00
Leo Shklovskii e7880fcde0 docs: Lots of small typo fixes (#480) 2024-02-23 11:45:58 -06:00
Aaron Klinker ee5e76e4b1 Update changelog 2024-02-22 14:24:38 -06:00
GitHub Actions 1ae8d6bc46 chore(release): v0.17.0 2024-02-22 17:55:31 +00:00
Aaron de7dd09f60 fix: Improve dev mode for content scripts registered at runtime (#474) 2024-02-22 11:52:37 -06:00
Aaron 1649785826 fix(storage)!: Only run migrations when the extension is updated (#478) 2024-02-22 11:46:59 -06:00
Aaron Klinker 91fc41cb15 docs(storage): Update docs 2024-02-22 11:46:59 -06:00
Aaron 9c80ad832b feat(storage)!: Improved support for default values on storage items (#477) 2024-02-22 11:46:48 -06:00
Aaron Klinker 86b7162d99 Update changelog 2024-02-21 08:24:26 -06:00
Aaron Klinker b8f8acd33d Fix typo 2024-02-21 08:24:09 -06:00
GitHub Actions d8905d23a4 chore(release): v0.16.11 2024-02-21 14:17:40 +00:00
Aaron 5d2d124e5a fix: Output main JS file for HTML entrypoints to chunks directory (#473) 2024-02-21 08:13:43 -06:00
Aaron Klinker 4fda203def chore(e2e): Remove log 2024-02-21 08:13:21 -06:00
Aaron Klinker 905bfc7a38 ci: Upgrade pnpm/action-setup to v2 2024-02-20 14:35:53 -06:00
Aaron Klinker 7c6973f0e6 ci: Fix codecov warning in release workflow 2024-02-20 14:35:08 -06:00
GitHub Actions 969e39c541 chore(release): v0.16.10 2024-02-20 20:33:12 +00:00
Aaron b256939cc4 fix: Respect configFile option (#472) 2024-02-20 14:30:21 -06:00
Aaron c46137644b feat: Customize when content scripts are registered, in the manifest or at runtime (#471) 2024-02-20 14:18:53 -06:00
Aaron 1bd7ef56c4 fix: Don't assume react when importing JSX entrypoints during build (#470) 2024-02-20 13:53:00 -06:00
Aaron Klinker c258b99cec Update changelog 2024-02-20 08:26:02 -06:00
GitHub Actions d3abb32cb4 chore(release): v0.16.9 2024-02-20 14:17:52 +00:00
Aaron 223ede9ff9 feat: Support setting side panel options in HTML file (#468) 2024-02-20 08:14:34 -06:00
Aaron 9d31a4eba1 chore: Simplify entrypoint types (#464) 2024-02-19 16:17:54 -06:00
okou ea0e40c269 fix: Fix order of ShadowRootUI hooks calling (#459) 2024-02-19 07:45:57 -06:00
Aaron Klinker bc24ea45a4 docs: Add wrapper div to react's createShadowRootUi example
Related to #456.
2024-02-17 09:20:35 -06:00
GitHub Actions 86d0c9b397 chore(release): v0.16.8 2024-02-15 21:41:43 +00:00
Aaron c275ccf0da fix: Watch files outside project root during development (#454) 2024-02-15 14:23:32 -06:00
Aaron Klinker 447a48f306 docs: Add loading and error states for "Who's using WXT" section 2024-02-15 10:37:58 -06:00
GitHub Actions 70b5cbc8db chore(release): v0.16.7 2024-02-15 16:35:13 +00:00
Aaron 3459de0375 perf: Upgrade Vite to 5.1 (#452) 2024-02-15 10:31:20 -06:00
Aaron c6817479bd feat: Generate ESLint globals file for auto-imports (#450) 2024-02-15 10:14:57 -06:00
Aaron f02aae21f6 chore(e2e): Use wxt prepare instead of wxt build when possible to speed up E2E tests (#451) 2024-02-15 09:59:36 -06:00
Aaron Klinker 87a62a1791 docs: Remove anchor from content script ui examples 2024-02-14 14:43:34 -06:00
Aaron Klinker a0d1643021 docs: Add section about dev mode differences 2024-02-14 14:41:40 -06:00
William Gérald Blondel 856a946fa7 Fix typo on homepage hero section (#438) 2024-02-11 10:13:17 -06:00
GitHub Actions 9d97164c20 chore(release): v0.16.6 2024-02-10 10:29:52 +00:00
lionelhorn 6d526e7043 feat: Add option to customize the analysis artifacts output (#431)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-02-10 04:26:33 -06:00
Aaron Klinker 946072fc5a docs: Fix createShadowRootUi unmount calls 2024-02-10 04:04:16 -06:00
Aaron Klinker 6b8dfdf02d chore: Enable skipped test since it works now 2024-02-10 03:01:38 -06:00
okou ba85fdf470 fix: Use insertBefore on mounting content script UI 2024-02-10 03:01:38 -06:00
okou 295f8601e3 refactor: Use Element.prepend on mounting UI 2024-02-10 03:01:38 -06:00
Aaron a38ccd8624 doc: Fix custom block 2024-02-09 05:20:51 -06:00
GitHub Actions b2dcec2f8a chore(release): v0.16.5 2024-02-09 06:01:00 +00:00
Aaron Klinker e8355029d9 fix: Support node 20 when running wxt submit 2024-02-08 23:55:53 -06:00
Aaron Klinker 2b374b98d2 docs: Remove "coming soon" from automated publishing feature 2024-02-05 23:36:13 -06:00
GitHub Actions d14d76a0c8 chore(release): v0.16.4 2024-02-06 05:21:00 +00:00
Aaron Klinker d0f1c7065c chore: Fix fake path in test data generator 2024-02-05 23:17:14 -06:00
Aaron 3edb66d76d feat: Add option to customize the analysis output filename (#426) 2024-02-05 23:15:15 -06:00
Aaron bba8cdace1 fix: Exclude analysis files from the build summary (#425) 2024-02-05 23:09:30 -06:00
Aaron 22f82a67ee feat: Automatically convert MV3 web_accessible_resources to MV2 (#423) 2024-02-05 22:49:18 -06:00
Aaron b32c60ca73 fix: Don't use immer for transformManifest (#424) 2024-02-05 22:37:26 -06:00
Aaron Klinker c93620cba5 Update changelog 2024-02-04 17:48:26 -06:00
GitHub Actions dd05ed4d4c chore(release): v0.16.3 2024-02-04 23:41:38 +00:00
Aaron 21636defda feat: Hooks (#419)
Deprecated `transformManifest` since a hook now exists for it.
2024-02-04 17:37:31 -06:00
Aaron bb022646e3 chore: Register global wxt instance (#418) 2024-02-04 16:34:10 -06:00
Aaron Klinker 37ad2c7ceb fix(init): Use ungh to prevent rate limits when loading templates 2024-02-04 14:11:41 -06:00
dependabot[bot] b992631e00 chore(deps): bump rollup-plugin-visualizer from 5.9.3 to 5.12.0 (#412)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:14:47 -06:00
dependabot[bot] 35a5cc6717 chore(deps-dev): bump typescript from 5.3.2 to 5.3.3 (#409)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:14:15 -06:00
dependabot[bot] 310b56fe5d chore(deps): bump defu from 6.1.3 to 6.1.4 (#413)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:13:28 -06:00
dependabot[bot] 148234382e chore(deps): bump @types/webextension-polyfill from 0.10.5 to 0.10.7 (#410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:13:03 -06:00
dependabot[bot] c1a97b77b0 chore(deps): bump actions/checkout from 3 to 4 (#407)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:11:32 -06:00
Chen Hua 557ccb4854 docs: fix typo of opinionated (#416) 2024-02-04 03:11:03 -06:00
Chen Hua 83e7e8aab9 docs: Fix typo of intuitive (#415) 2024-02-04 03:10:20 -06:00
dependabot[bot] 7bf490c59d chore(deps-dev): bump happy-dom from 12.10.3 to 13.3.8 (#411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:08:24 -06:00
dependabot[bot] 9670babdbb chore(deps): bump codecov/codecov-action from 3 to 4 (#408)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 03:06:11 -06:00
Florian Metz 42dfc2d5cf chore: Add dependabot for github actions (#404) 2024-02-03 16:32:46 -06:00
GitHub Actions 1527f0e275 chore(release): v0.16.2 2024-02-03 21:02:28 +00:00
Aaron 66bd957ee0 fix: Don't crash background service worker when using import.meta.url (#402) 2024-02-03 14:57:34 -06:00
GitHub Actions 72e3c5baa2 chore(release): v0.16.1 2024-02-03 19:57:51 +00:00
Aaron Klinker 9318346ab3 fix: Don't require config to run wxt submit init 2024-02-03 13:52:15 -06:00
Aaron Klinker 1d6aceab0a Update changelog 2024-02-03 11:05:16 -06:00
Florian Metz c1b5a9d270 docs: Add premid extension to homepage (#399) 2024-02-02 22:46:46 -06:00
Aaron Klinker f0b2a12724 chore(templates): Upgrade to wxt ^0.16.0 2024-02-02 16:20:24 -06:00
Aaron Klinker c45677ed48 Update changelog 2024-02-02 16:19:33 -06:00
GitHub Actions 7a3f075e21 chore(release): v0.16.0 2024-02-02 16:55:11 +00:00
Aaron Klinker 1e12ce2a0d docs: Document how to opt into ESM 2024-02-02 10:48:00 -06:00
Aaron eca30296a7 feat!: ESM background support (#398)
BREAKING CHANGE: The build output has changed slightly. This isn't a huge deal, and no steps are required for users unless you're doing something weird with the output files after a build.

Before:

```
.output/
  <target>/
    chunks/
      popup-<hash>.js
    popup.html
```

After:

```
.output/
  <target>/
    popup.html
    popup.js
```

This applies for all HTML files, not just the popup.
2024-02-02 10:17:43 -06:00
dependabot[bot] 41fa320815 chore(deps-dev): bump @vitest/coverage-v8 from 1.0.1 to 1.2.2 (#397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:07:50 -06:00
dependabot[bot] 873c24fa50 chore(deps-dev): bump p-map from 7.0.0 to 7.0.1 (#396)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:06:51 -06:00
dependabot[bot] d92db5ba8f chore(deps-dev): bump lint-staged from 15.2.0 to 15.2.1 (#395)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:06:31 -06:00
dependabot[bot] 1a6fe33f97 chore(deps): bump fast-glob from 3.3.1 to 3.3.2 (#394)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 07:05:42 -06:00
GitHub Actions 2db4ea77a9 chore(release): v0.15.4 2024-01-31 17:42:07 +00:00
Aaron ba5349ccaf fix(submit): Load .env.submit automatically when running wxt submit and wxt submit init (#391) 2024-01-31 11:38:11 -06:00
GitHub Actions addb689de9 chore(release): v0.15.3 2024-01-31 14:59:26 +00:00
Aaron 2af8d28b82 fix(dev): Reload <name>/index.html entrypoints properly on save (#390) 2024-01-31 08:50:23 -06:00
Aaron Klinker ddbc66813e Update changelog 2024-01-30 13:04:32 -06:00
GitHub Actions 0c8fc013a8 chore(release): v0.15.2 2024-01-30 19:01:54 +00:00
Aaron 26e30aaae9 feat: Add submit command (#370) 2024-01-30 12:55:17 -06:00
Nenad Novaković 6557dfe430 fix(dev): Resolve script and link aliases (#387)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-01-30 12:05:31 -06:00
Aaron Klinker 9cee9eefb4 Update changelog 2024-01-29 14:12:27 -06:00
GitHub Actions f9cd7aaf05 chore(release): v0.15.1 2024-01-29 20:10:18 +00:00
0x7a7a 9049804413 docs: Fixed doc errors on the guide/extension-api page (#383)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-01-29 14:06:27 -06:00
Aaron d027a496f5 Upgrade wxt init templates to v0.15 (#385) 2024-01-29 14:04:58 -06:00
Aaron dd7abddb7c feat: Allow passing custom preferences to chrome, enabling dev mode on chrome://extensions and allowing content script sourcemaps automatically (#384) 2024-01-29 13:58:46 -06:00
Aaron Klinker 98d2792e02 chore: Fix vite version conflicts in demo extension 2024-01-29 13:53:57 -06:00
Aaron Klinker 39b76d3ec0 fix(security): Upgrade to vite@5.0.12 to resolve CVE-2024-23331 2024-01-29 11:25:31 -06:00
Aaron Klinker bc29bfd434 Update changelog 2024-01-29 11:22:41 -06:00
GitHub Actions 5e60f37ec5 chore(release): v0.15.0 2024-01-29 17:18:38 +00:00
Aaron 6f901b32eb fix: Enable inline sourcemaps by default during development (#382) 2024-01-29 11:15:57 -06:00
Aaron 927aca612e fix!: Move browser constants to import.meta.env (#380)
BREAKING CHANGE: Renamed undocumented constants:

- `__BROWSER__` &rarr; `import.meta.env.BROWSER`
- `__COMMAND__` &rarr; `import.meta.env.COMMAND`
- `__MANIFEST_VERSION__` &rarr; `import.meta.env.MANIFEST_VERSION`
- `__IS_CHROME__` &rarr; `import.meta.env.CHROME`
- `__IS_FIREFOX__` &rarr; `import.meta.env.FIREFOX`
- `__IS_SAFARI__` &rarr; `import.meta.env.SAFARI`
- `__IS_EDGE__` &rarr; `import.meta.env.EDGE`
- `__IS_OPERA__` &rarr; `import.meta.env.OPERA`
2024-01-29 11:14:18 -06:00
Nenad Novaković 2dcdae91eb feat(zip)!: add includeSources and rename ignoredSources to excludeSources (#378)
Co-authored-by: Aaron <aaronklinker1@gmail.com>

BREAKING CHANGE: Renamed `ignoredSources` to `excludeSources`
2024-01-29 11:13:39 -06:00
Aaron Klinker eb758bdeb8 chore: Update contributor docs
See https://github.com/wxt-dev/wxt/pull/378\#issuecomment-1915064721
2024-01-29 10:59:54 -06:00
Aaron 5acf51af9d fix: Generate missing sourcemap in wxt:unimport plugin (#381) 2024-01-29 10:30:42 -06:00
Aaron Klinker f9718a1b71 docs: Fix typo 2024-01-28 09:42:45 -06:00
GitHub Actions cf7f23d4c3 chore(release): v0.14.7 2024-01-27 17:31:33 +00:00
Aaron Klinker 62ecb6f41e fix: Upgrade web-ext-run
This closes #324
2024-01-27 11:25:49 -06:00
Aaron Klinker fc45c37f22 docs: Add missing await to createShadowRootUi examples
This closes #374
2024-01-27 11:02:15 -06:00
Aaron Klinker 68874e607e docs: Document defined constants for the build target 2024-01-27 10:56:14 -06:00
Aaron Klinker 35865adcb2 fix(storage): Throw better error message when importing outside a extension environment
This closes #371
2024-01-27 00:26:35 -06:00
Aaron Klinker 3b63a51475 fix: Improve error messages when importing and building entrypoints 2024-01-27 00:20:48 -06:00
東奈比 d32e59e38f docs: fix incorrect sample code (#372) 2024-01-26 10:26:10 -06:00
Aaron Klinker dab8efa140 docs: Add matches to content script examples 2024-01-24 07:58:47 -06:00
GitHub Actions c298d8552b chore(release): v0.14.6 2024-01-24 06:34:25 +00:00
Aaron 87267347f3 feat: Add basic validation for entrypoint options (#368) 2024-01-24 00:31:05 -06:00
Aaron 326e84b667 feat: Restart dev mode when saving config (#365) 2024-01-19 22:58:49 -06:00
Aaron Klinker 4b005b450e ci: Check for type errors in demo before building 2024-01-19 22:57:06 -06:00
Aaron 3bf40fc4ed fix: Sort build output files naturally (#364) 2024-01-19 11:35:09 -06:00
Aaron 9ace395e11 fix: Add subdependency bin directory so wxt build --analyze works with PNPM (#363) 2024-01-19 11:32:18 -06:00
GitHub Actions 64f42064ae chore(release): v0.14.5 2024-01-19 14:36:07 +00:00
Aaron a525d786f1 feat: Add dev.reloadCommand config (#362) 2024-01-19 08:33:03 -06:00
Aaron 948ee810da fix: Disable reload dev command when 4 commands are already registered (#361) 2024-01-19 07:47:04 -06:00
GitHub Actions 8687b2980e chore(release): v0.14.4 2024-01-18 20:54:29 +00:00
Aaron 6f0814bc97 fix: Allow requiring built-in node modules from ESM CLI (#356) 2024-01-18 14:51:35 -06:00
Aaron 148c0da51f chore: Add unit tests for passing flags via the CLI (#354) 2024-01-18 14:49:35 -06:00
GitHub Actions ba09f3e0f1 chore(release): v0.14.3 2024-01-17 05:20:19 +00:00
Yuns 54e24652da fix: Make getArrayFromFlags result can be undefined (#352) 2024-01-16 23:15:18 -06:00
GitHub Actions d6516142d2 chore(release): v0.14.2 2024-01-16 19:57:03 +00:00
Yuns 460667e3c0 feat: Add filterEntrypoints option to speed up development (#344)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-01-16 13:52:58 -06:00
Armin 6ed61d0766 docs: Fix typos and outdated ui function usage (#347) 2024-01-16 13:01:12 -06:00
Aaron 971daf2784 fix: Allow browser.runtime.getURL to include hashes and query params for HTML paths (#350) 2024-01-16 12:59:38 -06:00
Aaron 89d8ef9a50 fix: Improve error message and document use of imported variables outside an entrypoint's main function (#346) 2024-01-14 09:36:22 -06:00
Aaron Klinker a329e24818 chore: Fix typo in function name 2024-01-12 13:03:43 -06:00
Aaron 249cf63da8 perf: Only call findEntrypoint once per build (#342) 2024-01-12 13:02:44 -06:00
Aaron Klinker 70a496108a chore: Update templates to ^0.14.0 2024-01-12 08:53:32 -06:00
GitHub Actions 214889e052 chore(release): v0.14.1 2024-01-12 14:50:29 +00:00
Aaron Klinker b6ab7a9567 fix: Use Alt+R/Opt+R to reload extension during development
This closes #339
2024-01-12 08:47:21 -06:00
Aaron Klinker 1ceb9e2e80 Add missing breaking change to changelog 2024-01-11 17:00:43 -06:00
Aaron Klinker 15ea81514f Update changelog 2024-01-11 13:28:41 -06:00
GitHub Actions 21d4e6dede chore(release): v0.14.0 2024-01-11 17:17:23 +00:00
Aaron 5343430c0f feat!: Refactor content script UI functions and add helper for "integrated" UIs (#333)
BREAKING CHANGES: Several APIs/options have been renamed:

- `type: "inline" | "overlay" | "modal"` has been changed to `position: "inline" | "overlay" | "modal"`
- `createContentScriptUi({ ... })` &rarr; `createShadowRootUi({ ... })`
- `createContentScriptIframe({ ... })` &rarr; `createIframeUi({ ... })`
- `onRemove` is now called ***before*** the UI is removed from the DOM, previously it was called after the UI was removed
2024-01-10 20:48:17 -06:00
GitHub Actions 8c834835c4 chore(release): v0.13.5 2024-01-08 20:10:48 +00:00
Aaron 9a2cc18f64 chore: Update bug report template 2024-01-08 13:50:20 -06:00
Aaron Klinker 8683bd4b77 docs: Add section about customizing other browser options during development 2024-01-08 13:42:50 -06:00
Aaron dca52864bb fix: Strip path from web_accessible_resources[0].matches (#332) 2024-01-08 13:17:31 -06:00
GitHub Actions 0c5643f648 chore(release): v0.13.4 2024-01-07 16:51:31 +00:00
Aaron Klinker b7cdf1597c fix: Disable minification during development
This closes #328
2024-01-07 10:48:25 -06:00
Aaron Klinker 2770974261 chore: Use const instead of let 2024-01-07 10:46:49 -06:00
GitHub Actions ab6a34afb6 chore(release): v0.13.3 2024-01-05 05:14:03 +00:00
Aaron b0eb7da9b4 chore: Refactor manifest generation E2E tests to unit tests (#323) 2024-01-02 17:05:23 -06:00
Aaron 4f649fc872 feat(DX): Add ctrl+E/cmd+E shortcut to reload extension during development (#322) 2024-01-02 12:06:22 -06:00
dependabot[bot] 1af64aa02f chore(deps-dev): bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 (#316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:33:18 -06:00
dependabot[bot] c35acff259 chore(deps-dev): bump prettier from 3.1.0 to 3.1.1 (#318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:31:32 -06:00
dependabot[bot] 995d4f8080 chore(deps-dev): bump tsx from 4.6.2 to 4.7.0 (#320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:31:16 -06:00
GitHub Actions b7e2aa3d8f chore(release): v0.13.2 2024-01-01 16:06:12 +00:00
冯不游 e347f4529f feat: Add isolateEvents option to createContentScripUi (#313)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-01-01 10:01:33 -06:00
李志博 3d9683418d Upload a Chinese dubbing plugin for Youtube. (#314)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-01-01 09:45:44 -06:00
Aaron bdce363eed chore: Re-enable coverage (#312) 2023-12-29 17:28:35 -06:00
Aaron e7775f291a docs: Use example tags to automate relevant example lists (#311) 2023-12-29 17:25:46 -06:00
Aaron 59f6a28c65 chore: Upgrade template dependencies (#310) 2023-12-29 12:25:13 -06:00
Aaron f7534464bc chore: Update templates to ^0.13.0 (#309) 2023-12-29 12:14:05 -06:00
Aaron 01445fb1da docs: Add example for setting up custom panels/panes in devtools (#308) 2023-12-29 12:08:33 -06:00
Aaron Klinker 1360eb7c01 docs: Update content script entrypoint docs 2023-12-29 02:01:53 -06:00
Aaron Klinker c99a281169 docs: Update unlisted pages/scripts description 2023-12-29 01:54:39 -06:00
Aaron Klinker 76e63e24c0 docs: Remove duplicate entrypoints/ path 2023-12-29 01:51:26 -06:00
GitHub Actions f5ba1173da chore(release): v0.13.1 2023-12-29 07:39:32 +00:00
Aaron 12079e6ffc chore: Update scripts/build.ts show current build step in progress, not completed count (#306) 2023-12-29 01:21:18 -06:00
Aaron ff6740d429 fix: Ship vite/client types internally for proper resolution using PNPM (#304) 2023-12-29 01:21:05 -06:00
Aaron e6657cb533 fix(storage): Support multiple : characters in storage keys (#303) 2023-12-29 00:40:47 -06:00
Aaron Klinker 2ad099bf7e docs: General fixes and improvements 2023-12-27 12:32:59 -06:00
Aaron Klinker 6421ab34a4 docs: Reorder guide 2023-12-27 12:32:45 -06:00
Aaron Klinker 8919c4015a Update changelog 2023-12-27 11:07:57 -06:00
GitHub Actions 712f2ed4ac chore(release): v0.13.0 2023-12-27 17:05:45 +00:00
Aaron d1b9e5ded6 feat!: New wxt/storage APIs (#300)
BREAKING CHANGE: `wxt/storage` no longer relies on `unstorage`. Some `unstorage` APIs, like `prefixStorage`, have been removed, while others, like `snapshot`, are methods on the new `storage` object. Most of the standard usage remains the same.
2023-12-27 11:00:10 -06:00
GitHub Actions 86182044d1 chore(release): v0.12.5 2023-12-24 14:28:21 +00:00
Aaron e1f181356c fix: Correct import in dev-only, noop background (#298) 2023-12-24 08:24:53 -06:00
GitHub Actions 5f241cf407 chore(release): v0.12.4 2023-12-23 15:53:43 +00:00
Aaron b4a2cf4cb2 fix: Disable Vite CJS warnings (#296) 2023-12-23 09:35:43 -06:00
GitHub Actions aea137a3a1 chore(release): v0.12.3 2023-12-23 04:26:47 +00:00
Aaron ad0e8e4144 fix: Correctly mock webextension-polyfill for Vitest (#294) 2023-12-22 22:21:09 -06:00
GitHub Actions 7fd5752ef1 chore(release): v0.12.2 2023-12-19 17:48:24 +00:00
Aaron 1a8b7ff635 feat: Support PNPM without hoisting dependencies (#291) 2023-12-19 11:42:20 -06:00
GitHub Actions fe3ca0fcbd chore(release): v0.12.1 2023-12-16 17:24:16 +00:00
Aaron f49ee9f005 fix: Fix package.json lint errors (#290) 2023-12-16 11:18:16 -06:00
Aaron 961440c0ac fix: Upgrade @webext-core/match-patterns to 1.0.3 (#289) 2023-12-16 11:07:18 -06:00
Aaron 9b562b0ca8 chore: Upgrade templates to wxt@^0.12.0 (#285) 2023-12-13 16:21:52 -06:00
Aaron Klinker 7b2563b2dc Update changelog with breaking changes 2023-12-13 16:02:24 -06:00
GitHub Actions 63f507ecf5 chore(release): v0.12.0 2023-12-13 21:57:34 +00:00
Aaron 0a88955394 fix: Throw error when no entrypoints are found (#283) 2023-12-13 15:50:28 -06:00
Aaron Klinker 291d25b675 docs: Fix dead links 2023-12-13 15:50:09 -06:00
Aaron 6f902cc598 docs: Improve content script UI guide (#272) 2023-12-13 15:43:40 -06:00
Aaron fde488ac82 feat!: Add support for "main world" content scripts (#284)
BREAKING CHANGE: `defineContentScript` and `defineBackground` are now exported from `wxt/sandbox` instead of `wxt/client`. If you use auto-imports, no changes are required. If you have disabled auto-imports, you'll need to manually update your import statements.
2023-12-13 15:41:39 -06:00
Aaron 0873c24ad8 chore: Convert WXT CLI to an ESM binary (#279) 2023-12-12 14:44:04 -06:00
Aaron 7c02497148 fix: Only use type imports for Vite (#278) 2023-12-12 14:24:42 -06:00
GitHub Actions 25441a3b97 chore(release): v0.11.2 2023-12-12 19:24:47 +00:00
Aaron f1cf866fe1 fix: Improve duplicate entrypoint name detection and catch the error before loading their config (#276) 2023-12-12 13:22:09 -06:00
Aaron d92a126489 fix: Discover .js, .jsx, and .tsx unlisted scripts correctly (#274) 2023-12-12 12:24:38 -06:00
Aaron Klinker bcb93afe4e chore: Add missing navigation item 2023-12-09 23:37:29 -06:00
Aaron 21cf3642cb docs: Improve content script UI docs (#268) 2023-12-09 23:36:35 -06:00
Andrew Walsh 2ef28ec0c4 Update content-scripts.md (#266) 2023-12-09 23:33:59 -06:00
Aaron 59094c9af3 chore: Update sSolid template to vite 5 (#265) 2023-12-07 23:05:08 -06:00
GitHub Actions ba41691296 chore(release): v0.11.1 2023-12-07 21:41:45 +00:00
Aaron 8786aa91b3 feat: Add util for detecting URL changes in content scripts (#264) 2023-12-07 13:29:33 -06:00
Aaron 0623d10653 chore: Upgrade templates to wxt@^0.11.0 (#263) 2023-12-06 11:39:03 -06:00
Aaron Klinker 85174d4994 Fix changelog 2023-12-06 11:33:49 -06:00
GitHub Actions 4e2dd5d618 chore(release): v0.11.0 2023-12-06 17:24:11 +00:00
Aaron 1a39a0d153 feat!: Vite 5 support (#261) 2023-12-06 11:18:52 -06:00
Aaron f922648dd7 chore: Abstract vite from WXT's core logic (#242) 2023-12-05 16:55:22 -06:00
Ítalo Brasil e6142e3608 docs: Adds tl;dv to homepage (#260) 2023-12-05 12:30:57 -06:00
Aaron ed73451fd0 chore: Speed up CI using pnpm instead of npm (#259) 2023-12-04 14:14:44 -06:00
GitHub Actions 8a25a2f60f chore(release): v0.10.4 2023-12-04 15:54:42 +00:00
Aaron 397e9a8b45 feat: Add config to customize outDir (#258) 2023-12-04 09:50:24 -06:00
dependabot[bot] 7cef7680de chore(deps): bump esbuild from 0.19.5 to 0.19.8 (#253)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:23:31 -06:00
dependabot[bot] 0c57375758 chore(deps-dev): bump tsx from 3.14.0 to 4.6.1 (#252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:09:19 -06:00
dependabot[bot] 2c6af84165 chore(deps): bump rollup-plugin-visualizer from 5.9.2 to 5.9.3 (#256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:08:06 -06:00
dependabot[bot] 58e1371701 chore(deps-dev): bump @types/lodash.merge from 4.6.8 to 4.6.9 (#255)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:06:58 -06:00
dependabot[bot] 0d8e7463b8 chore(deps-dev): bump prettier from 3.0.3 to 3.1.0 (#254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 15:06:24 -06:00
Aaron 77eeacaf0f docs: Update sidepanel availability (#250) 2023-11-29 17:12:37 -06:00
冯不游 e80c134150 docs: Add Doozy to homepage (#249) 2023-11-29 07:21:46 -06:00
GitHub Actions 03c8ab4d6f chore(release): v0.10.3 2023-11-28 18:18:59 +00:00
Aaron df13b9705c fix(auto-imports): Don't add imports to node_module dependencies (#247) 2023-11-28 12:15:05 -06:00
Aaron 762ba0080d chore(deps): Upgrade to typescript 5.3 (#245) 2023-11-27 10:03:03 -06:00
Aaron Klinker 317b1b6dcc docs: Fix typo 2023-11-27 09:57:09 -06:00
Aaron 93175a6477 chore: Use defu for merging some config objects (#243) 2023-11-26 12:23:10 -06:00
Aaron 60d6707b11 chore: Use normalize-path instead of vite.normalizePath (#244) 2023-11-26 11:36:42 -06:00
Aaron Klinker 97cbda3dab ci: only print response headers from docs webhook 2023-11-25 14:57:04 -06:00
Aaron Klinker 1611c1dba6 ci: Publish docs on push to main 2023-11-25 14:51:30 -06:00
Aaron Klinker b59252284a Print verbose webhook 2023-11-25 14:48:05 -06:00
Aaron Klinker 742b99657a chore: Trigger docs upgrade via webhook 2023-11-25 14:45:41 -06:00
Aaron Klinker e2997a43e0 Fix docker push command 2023-11-24 14:18:27 -06:00
Aaron Klinker 21dead60fd Self-host docs 2023-11-24 14:16:18 -06:00
GitHub Actions 3d1bc0a12c chore(release): v0.10.2 2023-11-20 19:52:55 +00:00
Aaron Klinker 4b24bee1fb Update demo app logs 2023-11-20 13:47:31 -06:00
Aaron Klinker 82ed821eb5 fix: Apply mode option to build steps correctly 2023-11-20 13:47:18 -06:00
Aaron 60625280c2 chore: Upgrade templates to v0.10 (#239) 2023-11-16 16:50:30 -06:00
GitHub Actions 083792b5f0 chore(release): v0.10.1 2023-11-16 17:46:36 +00:00
Aaron Klinker 3da3e07bda fix: Remove WXT global to remove unused modules from production builds 2023-11-16 11:35:20 -06:00
Aaron Klinker 64d61eb3f1 Update changelog 2023-11-14 23:26:27 -06:00
GitHub Actions aa6009e476 chore(release): v0.10.0 2023-11-15 05:21:53 +00:00
Aaron d2cb8f9416 feat: Add wxt/storage API (#234) 2023-11-14 23:07:45 -06:00
Nenad Novaković 046a4809d3 feat!: Allow plural directory and only png's for manifest icons (#237)
BREAKING CHANGE: No longer discover icons with extensions other than `.png`. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file.
2023-11-14 23:07:09 -06:00
Aaron Klinker 207b750d4e docs: Update main README links 2023-11-12 10:37:56 -06:00
Aaron 077fa9939e feat: List bun as an experimental option in wxt init (#233) 2023-11-11 15:45:29 -06:00
Aaron 44d715a29e fix: Don't use bun to load entrypoint config (#232) 2023-11-11 14:46:13 -06:00
GitHub Actions 3ab9fe4ae3 chore(release): v0.9.2 2023-11-11 19:57:25 +00:00
Aaron 91b28c2cb9 feat: Experimental option to exclude webextension-polyfill (#231) 2023-11-11 13:53:47 -06:00
Aaron Klinker d1b523061f ci: Fix sync-release workflow 2023-11-11 12:49:40 -06:00
Aaron Klinker c69ea3967f Fix changelog 2023-11-11 12:47:32 -06:00
GitHub Actions 41527a2200 chore(release): v0.9.1 2023-11-11 18:27:09 +00:00
Aaron 281f28192d fix: Generate valid manifest for Firefox MV3 (#229) 2023-11-11 12:22:46 -06:00
Aaron 67ffa44f9c docs: Update feature comparison 2023-11-11 08:53:59 -06:00
Aaron Klinker 421c0e412d docs: Add StayFree to homepage (#221) 2023-11-08 10:51:42 -06:00
Aaron d21ee08eb6 docs: Add UltraWideo to homepage (#193) 2023-11-08 09:26:51 -06:00
Aaron 7fa150dfae docs: Improve the "Used By" section on homepage (#220) 2023-11-08 08:27:58 -06:00
Aaron 7ba52b18fd ci: Automatically sync GitHub releases with CHANGELOG.md on push (#218) 2023-11-07 13:08:46 -06:00
Aaron 66e5079b84 feat: Add alias config for customizing path aliases (#216) 2023-11-07 11:38:48 -06:00
Aaron Klinker f1e8084be8 chore: Update Vite dependency range to ^4.0.0 || ^5.0.0-0 2023-11-07 10:42:36 -06:00
Aaron Klinker 609ae2ae37 fix: Move webextension-polyfill from peer to regular dependencies
This closes https://github.com/wxt-dev/wxt/issues/215
2023-11-07 10:38:24 -06:00
Aaron Klinker c81dfff37c docs: Add examples 2023-11-05 11:35:59 -06:00
Aaron 9c27820add chore: Upgrade templates to wxt@^0.9.0 (#214) 2023-11-04 13:01:05 -05:00
Aaron 22f8e10918 chore: Remove whitespace from genearted .wxt files (#211) 2023-11-04 12:28:31 -05:00
Aaron Klinker 96d41f8b7c Update changelog 2023-11-04 11:17:32 -05:00
GitHub Actions c4ce44e001 chore(release): v0.9.0 2023-11-04 15:56:43 +00:00
yyyanghj 6641ffaeed fix!: Remove lib from .wxt/tsconfig.json (#209)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2023-11-04 10:50:52 -05:00
Aaron 450dc0975f Add CLI screenshot back to README 2023-11-04 09:12:06 -05:00
Aaron d343555f41 Update MP4 title 2023-11-04 01:34:25 -05:00
Aaron 7993908c6c docs: Add demo video (#208) 2023-11-04 01:32:19 -05:00
yyyanghj 94e1dbce24 chore: Fix Svelte and React template READMEs (#207) 2023-11-03 12:59:35 -05:00
Aaron d14215946a Cleanup README badge links 2023-11-03 11:46:14 -05:00
Aaron Klinker 345406f02a docs: Fix heading 2023-11-03 09:39:09 -05:00
GitHub Actions 6b689bab23 chore(release): v0.8.7 2023-11-03 14:36:36 +00:00
Aaron fcdf0dcbe9 feat: createContentScriptIframe utility (#206) 2023-11-03 09:31:16 -05:00
Aaron bd35acdae1 chore: Run E2E tests in parallel (#204) 2023-11-02 11:15:07 -05:00
Aaron 21c35ee593 ci: Separate validation into multiple jobs (#203) 2023-11-02 11:08:19 -05:00
Aaron Klinker 0f09cbee0e chore: Improve prettier git hook 2023-11-02 10:31:54 -05:00
Aaron Klinker 183bb02e29 chore(deps-dev): Update sass to 1.69.5 2023-11-02 10:26:14 -05:00
Aaron 0bf4ea0897 chore(deps): Upgrade vite to 4.5.0 (#202) 2023-11-02 10:24:50 -05:00
Aaron Klinker cd4d00e23e chore(deps-dev): Update @type/* packages for demo 2023-11-02 10:16:52 -05:00
Aaron Klinker 5de18e5371 chore(deps-dev): Upgrade vitepress to 1.0.0-rc.24 2023-11-02 10:04:33 -05:00
Aaron af823418ee chore(deps): Upgrade giget to ^1.1.3 (#199) 2023-11-02 09:11:17 -05:00
Aaron Klinker 0b8d101c33 chore(deps-dev): Upgrade vue to 3.3.7 2023-11-02 08:58:25 -05:00
Aaron b587849126 chore(deps-dev): Upgrade typedoc to ^0.25.3 (#201) 2023-11-02 08:57:16 -05:00
Aaron 2a35ce0a1e chore(deps-dev): Upgrade execa to ^8.0.1 (#200) 2023-11-02 08:57:06 -05:00
Aaron Klinker c390b70f76 chore(deps): Upgrade unimport to 3.4.0 2023-11-02 08:54:24 -05:00
Aaron Klinker c9fd739128 chore(deps): Upgrade linkedom to 0.16.1 2023-11-02 08:52:22 -05:00
Aaron Klinker 0806c06042 chore(deps): Upgrade jiti to 1.21.0 2023-11-02 08:51:14 -05:00
Aaron Klinker 5f74a544a4 chore(deps-dev): Upgrade lint-staged to ^15.0.2 2023-11-02 08:47:53 -05:00
Aaron Klinker 121778894c chore(deps): Upgrade immer to ^10.0.3 2023-11-02 08:46:25 -05:00
Aaron Klinker 102c72a03c chore(deps): Upgrade esbuild to ^0.19.5 2023-11-02 08:42:16 -05:00
Aaron Klinker a0507866db chore(deps): Upgrade c12 to ^1.5.1 2023-11-02 08:41:05 -05:00
Aaron Klinker f3874da780 chore: Upgrade types 2023-11-02 08:39:47 -05:00
dependabot[bot] cc5d24ec92 chore(deps): bump @types/webextension-polyfill from 0.10.0 to 0.10.5 (#196)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:05:50 -05:00
dependabot[bot] 08760015da chore(deps-dev): bump tsx from 3.12.8 to 3.14.0 (#198)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:02:53 -05:00
dependabot[bot] 5368371020 chore(deps): bump filesize from 10.0.12 to 10.1.0 (#197)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:01:50 -05:00
dependabot[bot] 08d62a437a chore(deps): bump is-wsl from 3.0.0 to 3.1.0 (#195)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 08:00:52 -05:00
dependabot[bot] 37e2348d79 chore(deps-dev): bump happy-dom from 12.4.0 to 12.10.3 (#194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 07:59:55 -05:00
GitHub Actions 6a30dc46fa chore(release): v0.8.6 2023-10-30 15:41:46 +00:00
Aaron Klinker b75c553e51 fix: Inline WXT modules inside WxtVitest plugin 2023-10-30 10:25:22 -05:00
GitHub Actions a56face03d chore(release): v0.8.5 2023-10-30 14:54:59 +00:00
Aaron Klinker 19756c61ce docs: Add test.server.deps.inline to Vitest guide 2023-10-30 09:48:52 -05:00
Aaron 5f54b4de17 chore: Reduce package size by 70%, 1.92 MB to 590 kB (#190) 2023-10-29 23:00:10 -05:00
Aaron Klinker ea8935c47c Hide code coverage badge while it's broken 2023-10-29 21:24:21 -05:00
Aaron e37f73880d feat: Refactor project structure to export initialize, prepare, and zip functions (#182) 2023-10-29 21:19:31 -05:00
Aaron Klinker 2e24b9e18a chore: Update template docs 2023-10-29 20:24:38 -05:00
Aaron Klinker f8a0fb37a2 fix: Enable Vue SFC auto-imports in vue template 2023-10-29 20:24:27 -05:00
Nenad Novaković dd26b99027 docs: Update auto-imports.md (#186) 2023-10-29 08:29:54 -05:00
Aaron Klinker d9e9b43f8d docs: Improve runner.binaries documentation 2023-10-25 03:14:53 -05:00
GitHub Actions d580083727 chore(release): v0.8.4 2023-10-20 15:26:38 +00:00
Aaron 5a70d9e57d fix: Allow actions without a popup (#181) 2023-10-20 10:20:40 -05:00
GitHub Actions 1b1af245bd chore(release): v0.8.3 2023-10-18 21:38:14 +00:00
Aaron 874a531a62 feat: Add testing utils under wxt/testing (#178) 2023-10-18 16:28:26 -05:00
296 changed files with 18147 additions and 7322 deletions
+3
View File
@@ -1,2 +1,5 @@
* text=auto eol=lf
pnpm-lock.yaml linguist-generated
package-lock.json linguist-generated
bun.lockb linguist-generated
yarn.lock linguist-generated
+14 -6
View File
@@ -12,14 +12,22 @@ A clear and concise description of what the bug is.
### To Reproduce
Upload a ZIP or share a link to a repo representing the minimal reproduction. **_If you don't upload a minimal reproduction, you bug report will be closed._**
Share minimal reproduction. Examples of a minimal reproduction:
Steps to reproduce the behavior:
- PR with a failing test case
- ZIP file containing a minimal WXT project (be sure to include your lockfile!!)
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
> **_⚠️ If you don't upload a minimal reproduction, your issue will be closed until a reproduction is added._**
>
> Why? https://antfu.me/posts/why-reproductions-are-required
Steps to reproduce the bug using the reproduction:
1. Install dependencies: `pnpm i`
2. Start dev mode: `pnpm dev`
3. Click this...
4. Do that...
5. Etc...
### Expected behavior
+17
View File
@@ -0,0 +1,17 @@
name: Basic Setup
description: Install PNPM, Node, and dependencies
runs:
using: composite
steps:
- name: Setup PNPM
uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
- name: Install Dependencies
shell: bash
run: pnpm install
+4
View File
@@ -9,3 +9,7 @@ updates:
directory: '/' # Location of package manifests
schedule:
interval: 'monthly'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'monthly'
+27
View File
@@ -0,0 +1,27 @@
name: Publish Docs
on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag:
description: Docker Image Tag
required: true
default: latest
jobs:
publish:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- uses: docker/login-action@v3
with:
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- run: pnpm docs:build
- run: docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
- run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
- run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}
+3 -15
View File
@@ -5,30 +5,18 @@ on:
jobs:
validate:
uses: './.github/workflows/validate.yml'
secrets: inherit
publish:
runs-on: ubuntu-22.04
needs:
- validate
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- uses: ./.github/actions/setup
- name: Bump and Tag
run: |
+22
View File
@@ -0,0 +1,22 @@
name: Sync Releases
on:
workflow_dispatch:
push:
branches:
- main
paths:
- CHANGELOG.md
jobs:
sync:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
- run: pnpm sync-releases all --token ${{ secrets.GITHUB_TOKEN }}
+50 -125
View File
@@ -8,139 +8,64 @@ on:
jobs:
checks:
name: Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Formatting
run: pnpm format:check
- name: Type Check
run: pnpm compile
- name: Build Demo
run: |
pnpm build:all:chrome-mv2
pnpm build:all:chrome-mv3
pnpm build:all:firefox-mv2
pnpm build:all:firefox-mv3
pnpm tsc --noEmit
pnpm wxt zip
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm check
validate-demo:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm tsc --noEmit
working-directory: demo
- name: Tests
- run: pnpm build:all
working-directory: demo
- run: pnpm wxt zip
working-directory: demo
- run: pnpm vitest run
working-directory: demo
tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- uses: oven-sh/setup-bun@v1
- name: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
windows-tests:
name: Windows Tests
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Tests
run: pnpm test:coverage --reporter=default --reporter=hanging-process
project-templates:
name: Project Templates
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm test run
template:
runs-on: ubuntu-22.04
strategy:
matrix:
template:
- react
- solid
- svelte
- vanilla
- vue
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build Local Tarball
run: pnpm pack
- name: Validate Vanilla
working-directory: templates/vanilla
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- name: Validate Vue
working-directory: templates/vue
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- name: Validate React
working-directory: templates/react
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- name: Validate Svelte
working-directory: templates/svelte
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run check
- name: Validate Solid
working-directory: templates/solid
run: |
npm i
npm i -D ../../wxt-*.tgz
npm ls vite
npm run build
npm run compile
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm pack
- run: npm i
working-directory: templates/${{ matrix.template }}
- run: npm i -D ../../wxt-*.tgz
working-directory: templates/${{ matrix.template }}
- run: pnpm compile
if: matrix.template != 'svelte'
working-directory: templates/${{ matrix.template }}
- run: pnpm check
if: matrix.template == 'svelte'
working-directory: templates/${{ matrix.template }}
- run: pnpm build
working-directory: templates/${{ matrix.template }}
-9
View File
@@ -5,12 +5,3 @@ dist
docs/.vitepress/cache
pnpm-lock.yaml
CHANGELOG.md
.gitattributes
.gitignore
.prettierignore
*.png
*.svg
*.txt
_gitignore
_redirects
*.svelte
+1295 -5
View File
File diff suppressed because it is too large Load Diff
+43 -1
View File
@@ -57,9 +57,17 @@ pnpm test
pnpm docs:dev
```
## Updating Docs
Documentation is written with VitePress, and is located in the `docs/` directory.
The API reference is generated from JSDoc comments in the source code. If there's a typo or change you want to make in there, you'll need to update the source code instead of a file in the `docs/` directory.
## Testing
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones.
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones, if they exist.
> If they don't exist, feel free to create them, but that's a lot for a one-time contributor. A maintainer might add them to your PR though.
To run tests for a specific file, add the filename at the end of the test command:
@@ -70,3 +78,37 @@ pnpm test manifest-contents
Unit and E2E tests are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR.
## Templates
Each directory inside `templates/` is it's own standalone project. Simply `cd` into the directory you're updating, install dependencies with `npm` (NOT `pnpm`), and run the relevant commands
```sh
cd templates/vue
npm i
npm run dev
npm run build
```
Note that templates are hardcoded to a specific version of `wxt` from NPM, they do not use the local version. PR checks will test your changes against the templates, but if you want to manually do it, update the package.json dependency:
```diff
"devDependencies": {
"typescript": "^5.3.2",
"vite-plugin-solid": "^2.7.0",
- "wxt": "^0.8.0"
+ "wxt": "../.."
}
```
Then run `npm i` again.
### Adding Templates
To add a template, copy the vanilla template and give it a new name.
```sh
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.
+15 -16
View File
@@ -4,17 +4,13 @@
</h1>
<p align="center">
<a href="https://www.npmjs.com/package/wxt" target="_blank">
<img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
</a>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048">
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048">
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048">
</a>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
</p>
<p align="center">
@@ -30,19 +26,25 @@
&bull;
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
&bull;
<a href="https://wxt.dev/entrypoints/background.html" target="_blank">Entrypoints</a>
&bull;
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
&bull;
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
&bull;
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
</p>
![Example CLI Output](./docs/assets/cli-output.png)
## Demo
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
## Quick Start
Bootstrap a new project:
```sh
pnpx wxt@latest init <project-name>
pnpm dlx wxt@latest init <project-name>
```
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
@@ -55,14 +57,11 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
- 📂 File based entrypoints
- 🚔 TypeScript
- 🦾 Auto-imports
- ⬇️ Download and bundle remote URL imports
- 🤖 Automated publishing
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
### Coming Soon
- 🤖 Automated publishing
- ⬇️ Download and bundle remote URL imports
## Contributors
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env node
/**
* A alias around `publish-extension` that is always installed on the path without having to install
* `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');
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env node
require('../dist/cli.cjs');
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/cli.js';
+8 -8
View File
@@ -1,30 +1,30 @@
{
"name": "WXT Demo",
"name": "demo",
"version": "1.0.0",
"description": "Demo extension for WXT",
"type": "module",
"scripts": {
"dev": "pnpm -w build && wxt",
"build": "pnpm -w build && wxt build",
"build:all": "pnpm -w build && run-s -s build:all:*",
"build:all": "pnpm -w build && run-s -s 'build:all:*'",
"build:all:chrome-mv3": "wxt build",
"build:all:chrome-mv2": "wxt build --mv2",
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
"build:all:firefox-mv2": "wxt build -b firefox",
"test": "pnpm -w build && vitest",
"zip": "pnpm -w build && wxt zip",
"compile": "pnpm -w build && tsc --noEmit",
"postinstall": "pnpm -w build && wxt prepare"
},
"dependencies": {
"webextension-polyfill": "^0.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"vitest": "^1.2.2"
},
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/webextension-polyfill": "^0.10.0",
"sass": "^1.64.0",
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"sass": "^1.69.5",
"wxt": "workspace:*"
}
}
@@ -0,0 +1,29 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import background from '../background';
browser.i18n.getMessage = () => 'fake-message';
const logMock = vi.fn();
console.log = logMock;
describe('Background Entrypoint', () => {
beforeEach(() => {
fakeBrowser.reset();
});
it("should log the extenion's runtime ID", () => {
const id = 'some-id';
fakeBrowser.runtime.id = id;
background.main();
expect(logMock).toBeCalledWith(id);
});
it('should set the start time in storage', async () => {
background.main();
await new Promise((res) => setTimeout(res));
expect(await storage.getItem('session:startTime')).toBeDefined();
});
});
+38 -20
View File
@@ -1,24 +1,42 @@
export default defineBackground(() => {
console.log(browser.runtime.id);
logId();
console.log({
browser: __BROWSER__,
chrome: __IS_CHROME__,
firefox: __IS_FIREFOX__,
manifestVersion: __MANIFEST_VERSION__,
});
import messages from '~/public/_locales/en/messages.json';
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon/128.png');
export default defineBackground({
// type: 'module',
// @ts-expect-error: should only accept known message names
browser.i18n.getMessage('test');
browser.i18n.getMessage('prompt_for_name');
browser.i18n.getMessage('hello', 'Aaron');
browser.i18n.getMessage('bye', ['Aaron']);
browser.i18n.getMessage('@@extension_id');
main() {
console.log(browser.runtime.id);
logId();
console.log({
url: import.meta.url,
browser: import.meta.env.BROWSER,
chrome: import.meta.env.CHROME,
firefox: import.meta.env.FIREFOX,
manifestVersion: import.meta.env.MANIFEST_VERSION,
messages,
});
console.log('WXT MODE:', import.meta.env.MODE);
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon/128.png');
browser.runtime.getURL('/example.html#hash');
browser.runtime.getURL('/example.html?query=param');
// @ts-expect-error: should only allow hashes/query params on HTML files
browser.runtime.getURL('/icon-128.png?query=param');
// @ts-expect-error: should only accept known message names
browser.i18n.getMessage('test');
browser.i18n.getMessage('prompt_for_name');
browser.i18n.getMessage('hello', 'Aaron');
browser.i18n.getMessage('bye', ['Aaron']);
browser.i18n.getMessage('@@extension_id');
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
storage.setItem('session:startTime', Date.now());
},
});
+5 -1
View File
@@ -6,7 +6,11 @@ export default defineContentScript({
console.log(browser.runtime.id);
logId();
console.log('WXT MODE:', import.meta.env.MODE);
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
const n = (Math.random() * 100).toFixed(1);
ctx.setInterval(() => {
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Iframe Example</title>
</head>
<body>
<p>Hello iframe page!</p>
<script type="module" src="./main.ts"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
console.log('iframe 2');
+13
View File
@@ -0,0 +1,13 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main(ctx) {
const ui = createIframeUi(ctx, {
page: '/iframe-src.html',
position: 'overlay',
anchor: 'form[action="/search"]',
});
ui.mount();
console.log('Mounted iframe');
},
});
@@ -0,0 +1,11 @@
export default defineContentScript({
// Site that uses HTML5 history
matches: ['*://*.crunchyroll.com/*'],
main(ctx) {
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl, oldUrl }) => {
console.log('Location changed:', newUrl.href, oldUrl.href);
});
console.log('Watching for location change...');
},
});
@@ -0,0 +1,8 @@
export default defineContentScript({
matches: ['*://*/*'],
world: 'MAIN',
main() {
console.log(`Hello from ${location.hostname}!`);
},
});
+5 -1
View File
@@ -4,4 +4,8 @@ console.log(browser.runtime.id);
logId();
console.log(2);
console.log('WXT MODE:', import.meta.env.MODE);
console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
DEV: import.meta.env.DEV,
PROD: import.meta.env.PROD,
});
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sidebar</title>
</head>
<body>
<p>Example</p>
</body>
</html>
+3 -3
View File
@@ -6,12 +6,12 @@ export default defineContentScript({
cssInjectionMode: 'ui',
async main(ctx) {
const ui = await createContentScriptUi(ctx, {
const ui = await createShadowRootUi(ctx, {
name: 'demo-ui',
type: 'inline',
position: 'inline',
append: 'before',
anchor: 'form[role=search]',
mount: (container) => {
onMount: (container) => {
const app = document.createElement('div');
app.textContent = 'Custom content script UI';
container.append(app);
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
import { WxtVitest } from 'wxt/testing';
export default defineConfig({
test: {
mockReset: true,
restoreMocks: true,
},
plugins: [WxtVitest()],
});
+13
View File
@@ -3,6 +3,19 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
manifest: {
permissions: ['storage'],
default_locale: 'en',
web_accessible_resources: [
{
resources: ['/iframe-src.html'],
matches: ['*://*.google.com/*'],
},
],
},
zip: {
downloadPackages: ['sass'],
},
analysis: {
open: true,
},
});
+2
View File
@@ -0,0 +1,2 @@
FROM lipanski/docker-static-website:latest
COPY dist .
@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
const props = defineProps<{
tag?: string;
}>();
const examples = ref();
onMounted(async () => {
const res = await fetch(
'https://raw.githubusercontent.com/wxt-dev/wxt-examples/main/examples.json',
);
examples.value = await res.json();
});
const filteredExamples = computed(() => {
if (props.tag == null) return examples.value;
return examples.value.filter((example) => {
return example.tags?.includes(props.tag);
});
});
</script>
<template>
<ul>
<li v-if="examples == null">Loading...</li>
<template v-else>
<li v-for="example of filteredExamples">
<a :href="example.url" target="_blank">{{ example.name }}</a>
</li>
</template>
</ul>
</template>
+70 -19
View File
@@ -5,17 +5,42 @@ import useListExtensionDetails, {
} from '../composables/useListExtensionDetails';
// Add extension IDs here. Order doesn't matter, will be sorted by weekly active users
// During the transition from chrome.google.com/webstore to
// chromewebstore.google.com, queue.wxt.dev might return null for your
// extension. If it does, use "<slug>/<id>" instead of just the ID. The slug
// can be retrieved from the URL of the item on chromewebstore.google.com
const chromeExtensionIds = [
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
'ceicccfeikoipigeghddpocceifjelph', // Plex Skipper
'aelkipgppclpfimeamgmlonimflbhlgf', // GitHub Custom Notifier
'djnlaiohfaaifbibleebjggkghlmcpcj', // Fluent Read
'nhclljcpfmmaiojbhhnkpjcfmacfcian', // Facebook Video Controls
'mblkhbaakhbhiimkbcnmeciblfhmafna', // ElemSnap - Quick capture of webpage elements and conversion to images
];
const { data } = useListExtensionDetails(chromeExtensionIds);
const sortedExtensions = computed(() =>
!data.value
? undefined
: [...data.value].sort((l, r) => r.weeklyActiveUsers - l.weeklyActiveUsers),
);
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
const sortedExtensions = computed(() => {
if (!data.value?.length) return [];
return [...data.value]
.filter((item) => item != null)
.map((item) => ({
...item,
// Sort based on the user count weighted by the rating
sortKey: ((item.rating ?? 5) / 5) * item.weeklyActiveUsers,
}))
.filter((item) => !!item)
.sort((l, r) => r.sortKey - l.sortKey);
});
function getStoreUrl(extension: ChromeExtension) {
const url = new URL(extension.storeUrl);
@@ -27,32 +52,50 @@ function getStoreUrl(extension: ChromeExtension) {
<template>
<section class="vp-doc">
<div class="container">
<h2>Who's Using WXT?</h2>
<h2 id="whos-using-wxt">Who's Using WXT?</h2>
<p>
Battle tested and ready for production. Explore chrome extensions made
with WXT.
</p>
<ul>
<li v-for="extension of sortedExtensions">
<p v-if="isLoading" style="text-align: center; opacity: 50%">
Loading...
</p>
<p
v-else-if="err || sortedExtensions.length === 0"
style="text-align: center; opacity: 50%"
>
Failed to load extension details.
</p>
<ul v-else>
<li
v-for="extension of sortedExtensions"
:key="extension.id"
class="relative"
>
<img
:src="extension.iconUrl"
:alt="`${extension.name} icon`"
referrerpolicy="no-referrer"
/>
<div>
<div class="relative">
<a
:href="getStoreUrl(extension)"
target="_blank"
:title="extension.name"
class="extension-name"
>{{ extension.name }}</a
>
<p class="description" :title="extension.shortDescription">
{{ extension.shortDescription }}
</p>
<p class="user-count">
{{ extension.weeklyActiveUsers.toLocaleString() }} users
</p>
</div>
<p class="user-count">
<span>{{ extension.weeklyActiveUsers.toLocaleString() }} users</span
><template v-if="extension.rating != null"
>,
<span>{{ extension.rating }} stars</span>
</template>
</p>
</li>
</ul>
<p class="centered pr">
@@ -121,7 +164,8 @@ li {
background-color: var(--vp-c-bg-soft);
border-radius: 12px;
flex: 1;
gap: 16px;
gap: 24px;
align-items: center;
}
.centered {
@@ -136,9 +180,10 @@ li .description {
}
li .user-count {
opacity: 70%;
text-align: right;
width: 100%;
font-size: small;
position: absolute;
bottom: 12px;
right: 16px;
}
li a {
@@ -157,8 +202,6 @@ li a:hover {
li div {
flex: 1;
display: flex;
flex-direction: column;
}
li .description {
@@ -167,10 +210,18 @@ li .description {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex-grow: 1;
margin-bottom: 16px;
}
li .extension-name {
font-size: large;
}
.pr {
opacity: 70%;
}
.relative {
position: relative;
}
</style>
@@ -7,6 +7,7 @@ export interface ChromeExtension {
weeklyActiveUsers: number;
shortDescription: string;
storeUrl: string;
rating: number | undefined;
}
const operationName = 'WxtDocsUsedBy';
@@ -18,12 +19,14 @@ const query = `query ${operationName}($ids:[String!]!) {
weeklyActiveUsers
shortDescription
storeUrl
rating
}
}`;
export default function (ids: string[]) {
const data = ref<ChromeExtension[]>();
const err = ref<unknown>();
const isLoading = ref(true);
fetch('https://queue.wxt.dev/api', {
method: 'POST',
@@ -34,6 +37,7 @@ export default function (ids: string[]) {
}),
})
.then(async (res) => {
isLoading.value = false;
const {
data: { chromeExtensions },
} = await res.json();
@@ -41,6 +45,7 @@ export default function (ids: string[]) {
err.value = undefined;
})
.catch((error) => {
isLoading.value = false;
console.error(error);
data.value = undefined;
err.value = error;
@@ -49,5 +54,6 @@ export default function (ids: string[]) {
return {
data,
err,
isLoading,
};
}
+15 -8
View File
@@ -19,7 +19,7 @@ const title = 'Next-gen Web Extension Framework';
const titleSuffix = ' WXT';
const description =
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilties for building, zipping, and publishing your extension, it's easy to get started.";
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
const ogTitle = `${title}${titleSuffix}`;
const ogUrl = 'https://wxt.dev';
const ogImage = 'https://wxt.dev/social-preview.png';
@@ -71,6 +71,7 @@ export default defineConfig({
nav: [
{ text: 'Guide', link: '/guide/installation.md' },
{ text: 'Entrypoints', link: '/entrypoints/background.md' },
{ text: 'Examples', link: '/examples.md' },
{ text: 'API', link: '/api/cli.md' },
],
@@ -83,16 +84,19 @@ export default defineConfig({
{ text: 'Installation', link: '/guide/installation.md' },
{ text: 'Configuration', link: '/guide/configuration.md' },
{ text: 'Entrypoints', link: '/guide/entrypoints.md' },
{ text: 'Assets', link: '/guide/assets.md' },
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
{ text: 'Publishing', link: '/guide/publishing.md' },
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
{ text: 'Manifest.json', link: '/guide/manifest.md' },
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
{ text: 'Storage', link: '/guide/storage.md' },
{ text: 'Assets', link: '/guide/assets.md' },
{ text: 'Content Script UI', link: '/guide/content-script-ui.md' },
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
{ text: 'ES Modules', link: '/guide/esm.md' },
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
{ text: 'Vite', link: '/guide/vite.md' },
{ text: 'Remote Code', link: '/guide/remote-code.md' },
{ text: 'Publishing', link: '/guide/publishing.md' },
{ text: 'Development', link: '/guide/development.md' },
{ text: 'Testing', link: '/guide/testing.md' },
{ text: 'Vite', link: '/guide/vite.md' },
],
},
{
@@ -120,7 +124,7 @@ export default defineConfig({
{ text: 'Options', link: '/entrypoints/options.md' },
{ text: 'Popup', link: '/entrypoints/popup.md' },
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
{ text: 'Side Panel', link: '/entrypoints/sidepanel.md' },
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
{
text: 'Unlisted Scripts',
@@ -142,6 +146,9 @@ export default defineConfig({
],
},
socialLinks: [{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' }],
socialLinks: [
{ icon: 'discord', link: 'https://discord.gg/ZFsZqGery9' },
{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' },
],
},
});
+8 -2
View File
@@ -32,14 +32,20 @@ export function generateCliDocs() {
'```',
];
const commands = await Promise.all(
const commands = await Promise.allSettled(
extractCommands(dev.info).map(async (command) => {
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
const { rest: docs } = splitInfo(res.stdout);
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
}),
);
lines.push(...commands);
lines.push(
...commands
.filter((res) => res.status === 'fulfilled')
.map(
(res) => (res as unknown as PromiseFulfilledResult<string>).value,
),
);
const text = await format(
PREFACE +
+2
View File
@@ -2,6 +2,7 @@ import DefaultTheme from 'vitepress/theme';
import Icon from '../components/Icon.vue';
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
import UsingWxtSection from '../components/UsingWxtSection.vue';
import ExampleList from '../components/ExampleList.vue';
import './custom.css';
export default {
@@ -10,5 +11,6 @@ export default {
ctx.app.component('Icon', Icon);
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
ctx.app.component('UsingWxtSection', UsingWxtSection);
ctx.app.component('ExampleList', ExampleList);
},
};
+1 -1
View File
@@ -16,7 +16,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
## Definition
:::warning
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added syncronously on background startup. If your main function returns a promise, WXT will log an error.
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added synchronously on background startup. If your main function returns a promise, WXT will log an error.
:::
```ts
+22 -161
View File
@@ -20,7 +20,7 @@ When creating content script entrypoints, they are automatically included in the
```ts
export default defineContentScript({
// Set manifest options
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
matches: string[],
excludeMatches: undefined | [],
includeGlobs: undefined | [],
excludeGlobs: undefined | [],
@@ -37,6 +37,9 @@ export default defineContentScript({
// Configure how CSS is injected onto the page
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
// Configure how/when content script will be registered
registration: undefined | "manifest" | "runtime",
main(ctx) {
// Executed when content script is loaded
},
@@ -49,9 +52,11 @@ When defining multiple content scripts, content script entrypoints that have the
## Context
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
When your extension updates or is uninstalled, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
```ts
export default defineContentScript({
@@ -62,26 +67,33 @@ export default defineContentScript({
// ...
});
// Stop fetch requests
fetch('...url', { signal: ctx.signal });
// Timeout utilities
// Timeout utilities that are automatically cleared when invalidated
ctx.setTimeout(() => {
// ...
}, 5e3);
ctx.setInterval(() => {
// ...
}, 60e3);
// Or add event listeners that get removed when invalidated
ctx.addEventListener(document, 'visibilitychange', (event) => {
// ...
});
// You can also stop fetch requests
fetch('...url', { signal: ctx.signal });
},
});
```
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
:::tip
:::warning
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
If you're using a framework like React, Vue, Svelte, etc., make sure you're unmounting your UI properly in the `onRemove` option of [`createShadowRootUi`](https://wxt.dev/guide/content-script-ui.html#shadow-root).
:::
## CSS
@@ -90,7 +102,7 @@ To include CSS with your content script, import the CSS file at the top of your
```
<srcDir>
<srcDir>/
└─ entrypoints/
└─ overlay.content/
├─ index.ts
@@ -138,155 +150,4 @@ export default defineContentScript({
});
```
## UI
WXT provides a utility function, `createContentScriptUi` to simplify mounting a UI from a content script. Internally, it uses the `ShadowRoot` API to isolate your CSS from the webpages.
`createContentScriptUi` requires a `ContentScriptContext` so that when the context is invalidated, the UI is automatically removed from the webpage.
:::details When to use `createContentScriptUi`
You should only use `createContentScriptUi` if you want your UI's styles isolated from the webpages. If you want to create a more "integrated" UI that uses the page's styles, you can just use the regular JS API's to append your UI to the page.
```ts
const ui = document.createElement('div');
const anchor = document.querySelector('#anchor-selector');
anchor.append(ui);
```
:::
### Usage
To use `createContentScriptUi`, follow these steps:
1. Import your CSS file at the top of your content script
2. Set `cssInjectionMode: "ui"` inside `defineContentScript`
3. Call `createContentScriptUi`
4. Call `ui.mount()` to add the UI to the webpage
Here's a basic example:
```ts
// entrypoints/ui.content/index.ts
import './style.css';
export default defineContentScript({
// ...
cssInjectionMode: 'ui',
async main(ctx) {
const ui = await createContentScriptUi(ctx, {
name: 'example-ui',
type: 'inline',
anchor: '#some-element',
append: 'after',
mount(container) {
// Mount UI inside `container`...
},
});
// Yoy must call `mount` to add the UI to the page.
ui.mount();
},
});
```
If you're using a frontend framework, you'll also need to include an `onRemoved` callback:
:::code-group
```ts [Vue]
import { createApp } from 'vue';
createContentScriptUi(ctx, {
// ...
mount(container) {
// Create a new app and mount it inside the container
const app = createApp(...);
app.mount(container);
return app;
},
onRemove(app) {
// When the UI is removed from the DOM, call unmount to stop the app
app.unmount();
},
});
```
```ts [React]
import ReactDOM from 'react-dom/client';
createContentScriptUi(ctx, {
// ...
mount(container) {
// Create a root using the container and render your app
const root = ReactDOM.createRoot(container);
root.render(...);
return root;
},
onRemove(root) {
// When the UI is removed from the DOM, call unmount to stop the app
root.unmount();
},
});
```
```ts [Svelte]
import App from './App.svelte';
createContentScriptUi(ctx, {
// ...
mount(container) {
// Mount your app component inside the container
return new App({
target: container,
});
},
onRemove(app) {
// When the UI is removed from the DOM, call $destroy to stop the app
app.$destroy();
},
});
```
```ts [Solid]
import { render } from 'solid-js/web';
createContentScriptUi(ctx, {
// ...
mount(container) {
// Render your app component into the container
return render(() => ..., container)
},
onRemove(unmount) {
// When the UI is removed from the DOM, call unmount to stop the app
unmount();
},
});
```
:::
### `anchor`
The anchor dictates where the UI will be mounted.
### `append`
Customize where the UI get's appended to the DOM, relative to the `anchor` element.
### `type`
There are 3 types of UI's you can mount.
- `inline`: Shows up inline based on the `anchor` and `append` options
- `overlay`: Shows up inline, but styled to be 0px by 0px, with overflow visible. This causes the UI to overlay on top of the webpage's content
- `modal`: A fullscreen overlay that covers the entire screen, regardless of where it's anchored.
> TODO: Add visualization of the different UI types.
### Overlay `alignment`
Because the overlay UI type results in a 0px by 0px container being added to the webpage, the `alignment` option allows you to configure which corner of your UI is aligned with the 0x0 element.
> TODO: Add visualization of the different alignments.
See [Content Script UI](/guide/content-script-ui) for more info on creating UIs and including CSS in content scripts.
+1 -1
View File
@@ -2,7 +2,7 @@
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recomended approach to include CSS with a content script.
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recommended approach to include CSS with a content script.
:::info
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/api/wxt/interfaces/InlineConfig#transformmanifest) to manually add your CSS file to the manifest.
+10 -3
View File
@@ -13,15 +13,12 @@
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
@@ -31,3 +28,13 @@ Plain old HTML file.
</body>
</html>
```
## Adding UI Elements
Chrome extensions allow you to add panels and side panes to the devtools window.
![DevTools window showing Elements panel and Styles sidebar pane.](https://developer.chrome.com/static/docs/extensions/how-to/devtools/extend-devtools/image/devtools-window-showing-e-9051f7f0347cd_1920.png)
See the WXT's examples for a full walkthrough of extending the devtools window:
<ExampleList tag="devtools" />
+4 -4
View File
@@ -10,10 +10,10 @@ Firefox does not support sandboxed pages.
<EntrypointPatterns
:patterns="[
['entrypoints/sandbox.html', 'sandbox.html'],
['entrypoints/sandbox/index.html', 'sandbox.html'],
['entrypoints/<name>.sandbox.html', '<name>.html` '],
['entrypoints/<name>.sandbox/index.html', '<name>.html` '],
['sandbox.html', 'sandbox.html'],
['sandbox/index.html', 'sandbox.html'],
['<name>.sandbox.html', '<name>.html` '],
['<name>.sandbox/index.html', '<name>.html` '],
]"
/>
+20 -8
View File
@@ -1,19 +1,21 @@
# Side Panel
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
:::tip Chromium Only
Firefox does not support sidepanel pages.
In Chrome, side panels use the "side_panel" API, while Firefox uses the "sidebar_action" API.
:::warning
Chrome added support for sidepanels in Manifest V3, they are not available in Manifest V2.
:::
## Filenames
<EntrypointPatterns
:patterns="[
['entrypoints/sidepanel.html', 'sidepanel.html'],
['entrypoints/sidepanel/index.html', 'sidepanel.html'],
['entrypoints/<name>.sidepanel.html', '<name>.html` '],
['entrypoints/<name>.sidepanel/index.html', '<name>.html` '],
['sidepanel.html', 'sidepanel.html'],
['sidepanel/index.html', 'sidepanel.html'],
['<name>.sidepanel.html', '<name>.html` '],
['<name>.sidepanel/index.html', '<name>.html` '],
]"
/>
@@ -25,7 +27,17 @@ Firefox does not support sidepanel pages.
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<title>Default Side Panel Title</title>
<meta
name="manifest.default_icon"
content="{
16: '/icon-16.png',
24: '/icon-24.png',
...
}"
/>
<meta name="manifest.open_at_install" content="true|false" />
<meta name="manifest.browser_style" content="true|false" />
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
+3 -1
View File
@@ -1,6 +1,8 @@
# Unlisted Pages
HTML pages that are built by Vite, but are not included in the manifest.
HTML pages that are bundled and shipped with the extension, but are not included in the manifest.
If you plan on using the page in an iframe, don't forget to add the page to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
### Examples
+2 -2
View File
@@ -1,8 +1,8 @@
# Unlisted Scripts
TypeScript files that are built, but are not included in the manifest.
TypeScript files that are bundled and shipped with the extension, but are not included in the manifest.
You are responsible for loading/running these scripts where needed.
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related stylesheets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
## Filenames
+7
View File
@@ -0,0 +1,7 @@
# Examples
Simple walkthroughs to accomplish common tasks or patterns with WXT.
<ExampleList />
> Full code available at [`wxt-dev/wxt-examples`](https://github.com/wxt-dev/wxt-examples)
+1 -1
View File
@@ -63,7 +63,7 @@ Files in the assets directory will be processed by Vite. They are imported in yo
<!-- ... -->
</head>
<body>
<img src="~/illustration.svg" />
<img src="~/assets/illustration.svg" />
<!-- ... -->
</body>
</html>
+9 -6
View File
@@ -11,12 +11,15 @@ To setup your test environment for auto-imports, see [Testing](/guide/testing).
Some WXT APIs can be used without importing them:
- [`browser`](/api/wxt/browser/variables/browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
- [`defineContentScript`](/api/wxt/client/functions/defineContentScript) from `wxt/client`
- [`defineBackground`](/api/wxt/client/functions/defineBackground) from `wxt/client`
- [`createContentScriptUi`](/api/wxt/client/functions/createContentScriptUi) from `wxt/client`
- [`defineContentScript`](/api/wxt/sandbox/functions/defineContentScript) from `wxt/sandbox`
- [`defineBackground`](/api/wxt/sandbox/functions/defineBackground) from `wxt/sandbox`
- [`defineUnlistedScript`](/api/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
- [`createIntegratedUi`](/api/wxt/client/functions/createIntegratedUi) from `wxt/client`
- [`createShadowRootUi`](/api/wxt/client/functions/createShadowRootUi) from `wxt/client`
- [`createIframeUi`](/api/wxt/client/functions/createIframeUi) from `wxt/client`
- [`fakeBrowser`](/api/wxt/testing/variables/fakeBrowser) from `wxt/testing`
And more. All `wxt/*` APIs can be used without imports.
And more!
## Project Auto-imports
@@ -62,8 +65,8 @@ import { defineConfig } from 'wxt';
export default defineConfig({
imports: {
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
preset: ['vue'],
// Add auto-imports for vue functions like createApp, ref, computed, watch, toRaw, etc...
presets: ['vue'],
},
});
```
+30 -29
View File
@@ -1,46 +1,47 @@
# Compare
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another web extension framework) and [CRXJS](https://crxjs.dev/vite-plugin) (the most popular bundler plugin).
## Overview
| Features | WXT | Plasmo |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
| Supports all browsers | ✅ | ✅ |
| MV2 Support | ✅ | ✅ |
| MV3 Support | ✅ | ✅ |
| Create Extension ZIPs | ✅ | ✅ |
| Create Firefox Sources ZIP | ✅ | ❌ |
| First-class TypeScript support | ✅ | ✅ |
| File based entrypoint discovery | | |
| Inline entrypoint config | ✅ | ✅ |
| Auto-imports | ✅ | ❌ |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` |
| Automated publishing | 🟡 Coming soon | ✅ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ |
| Features | WXT | Plasmo | CRXJS |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| Create Extension ZIPs | ✅ | ✅ | ❌ |
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
| First-class TypeScript support | ✅ | ✅ | ✅ |
| Entrypoint discovery | File based | File based | |
| Inline entrypoint config | ✅ | ✅ | Manifest based |
| Auto-imports | ✅ | ❌ | ❌ |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
| Automated publishing | | ✅ | ❌ |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
| <strong style="opacity: 50%">Dev Mode</strong> | | |
| `.env` Files | ✅ | ✅ |
| Opens browser and install extension | ✅ | ❌ |
| HMR for UIs | ✅ | 🟡 React only |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
| Storage | ❌ | |
| Messaging | | ✅ |
| Content Script UI | ❌ | ✅ |
| `.env` Files | ✅ | ✅ | ✅ |
| Opens browser and install extension | ✅ | ❌ | ❌ |
| HMR for UIs | ✅ | 🟡 React only | ✅ |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| Respects Content Script `run_at` | ✅ | | ❌ ESM-style loaders run asynchronously |
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
| Storage | | ✅ | ❌ |
| Messaging | ❌ | ✅ | ❌ |
| Content Script UI | ✅ | ✅ | ❌ |
## Dev Mode
## Why use WXT?
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
WXT's main goal is improving the development experience (DX) of creating web extensions, while not sacrificing support. There are two things WXT does differently:
1. Automatically opens a browser with the extension installed when starting development
2. Reload each part of the extension individually rather than reloading the entire extension
Opening a browser automatically makes it super easy to start and stop development without having to manually load the extension in your browser.
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interupts your development flow and is really annoying.
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interrupts your development flow and is really annoying.
WXT solves this problem by reloading HTML pages and content scripts individually (when possible) to keep your UIs open while you develop them. This is a MV3 feature, so if you're developing a MV2 extension, you'll get the same dev experience as Plasmo.
+11 -13
View File
@@ -1,6 +1,6 @@
# Configuration
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and set fields on your `manifest.json`.
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory structure of your project, and set fields on your `manifest.json`.
## Config File
@@ -51,12 +51,6 @@ export default defineConfig({
});
```
## Vite Config
[Vite](https://vitejs.dev/) is the bundler used to build each entrypoint of your extension. Vite can be configured via the `vite` option.
See [Vite's documentation](https://vitejs.dev/config/) for configuring the bundler.
## Frontend Frameworks
Adding a framework like Vue, React, or Svelte is easy!
@@ -70,9 +64,9 @@ import { defineConfig } from 'wxt';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
vite: {
vite: () => ({
plugins: [vue()],
},
}),
});
```
@@ -81,9 +75,9 @@ import { defineConfig } from 'wxt';
import react from '@vitejs/plugin-react';
export default defineConfig({
vite: {
vite: () => ({
plugins: [react()],
},
}),
});
```
@@ -92,10 +86,14 @@ import { defineConfig } from 'wxt';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
vite: {
vite: () => ({
plugins: [svelte()],
},
}),
});
```
:::
:::info
Internally, WXT runs `vite build` multiple times during a single build. It can't share the same instance of some plugins between each build, so a function is used to return completely new instances of each plugin for each build.
:::
+408
View File
@@ -0,0 +1,408 @@
# Content Script UI
There are three ways to mount a UI inside a content script:
[[toc]]
Each has their own set of advantages and disadvantages.
| Method | Isolated Styles | Isolated Events | HMR | Use page's context |
| ----------- | :-------------: | :-----------------: | :-: | :----------------: |
| Integrated | ❌ | ❌ | ❌ | ✅ |
| Shadow Root | ✅ | ✅ (off by default) | ❌ | ✅ |
| IFrame | ✅ | ✅ | ✅ | ❌ |
## Integrated
Integrated content script UIs are injected alongside the content of a page. This means that they are affected by CSS on that page.
:::code-group
```ts [Vanilla]
// entrypoints/example-ui.content.ts
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
onMount: (container) => {
// Append children to the container
const app = document.createElement('p');
app.textContent = '...';
container.append(app);
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```ts [Vue]
// entrypoints/example-ui.content/index.ts
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
onMount: (container) => {
// Create the app and mount it to the UI container
const app = createApp(App);
app.mount(container);
return app;
},
onRemove: (app) => {
// Unmount the app when the UI is removed
app.unmount();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```tsx [React]
// entrypoints/example-ui.content/index.tsx
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
onMount: (container) => {
// Create a root on the UI container and render a component
const root = ReactDOM.createRoot(container);
root.render(<App />);
return root;
},
onRemove: (root) => {
// Unmount the root when the UI is removed
root.unmount();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```ts [Svelte]
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
onMount: (container) => {
// Create the Svelte app inside the UI container
const app = new App({
target: container,
});
return app;
},
onRemove: (app) => {
// Destroy the app when the UI is removed
app.$destroy();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
```tsx [Solid]
// entrypoints/example-ui.content/index.ts
import { render } from 'solid-js/web';
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
const ui = createIntegratedUi(ctx, {
position: 'inline',
onMount: (container) => {
// Render your app to the UI container
const unmount = render(() => <div>...</div>, container);
},
onRemove: (unmount) => {
// Unmount the app when the UI is removed
unmount();
},
});
// Call mount to add the UI to the DOM
ui.mount();
},
});
```
:::
See the [API Reference](/api/wxt/client/functions/createIntegratedUi) for the complete list of options.
You can control how CSS is injected for an integrated content script UI with the [`cssInjectionMode`](/api/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) property. Usually, you'll want to leave it as `"manifest"`, the default, so the UI inherits its style from the website's CSS.
## Shadow Root
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
WXT's [`createShadowRootUi`](/api/wxt/client/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs with isolated CSS. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
To use `createShadowRootUi`, follow these steps:
1. Import your CSS file at the top of your content script
2. Set [`cssInjectionMode: "ui"`](/api/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) inside `defineContentScript`
3. Define your UI with `createShadowRootUi()`
4. Mount the UI so it is visible to users
:::code-group
```ts [Vanilla]
// 1. Import the style
import './style.css';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
onMount(container) {
// Define how your UI will be mounted inside the container
const app = document.createElement('p');
app.textContent = 'Hello world!';
container.append(app);
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```ts [Vue]
// 1. Import the style
import './style.css';
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
onMount: (container) => {
// Define how your UI will be mounted inside the container
const app = createApp(App);
app.mount(container);
return app;
},
onRemove: (app) => {
// Unmount the app when the UI is removed
app?.unmount();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```tsx [React]
// 1. Import the style
import './style.css';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
onMount: (container) => {
// Container is a body, and React warns when creating a root on the body, so create a wrapper div
const app = document.createElement('div');
container.append(app);
// Create a root on the UI container and render a component
const root = ReactDOM.createRoot(app);
root.render(<App />);
return root;
},
onRemove: (root) => {
// Unmount the root when the UI is removed
root?.unmount();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```ts [Svelte]
// 1. Import the style
import './style.css';
import App from './App.svelte';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
onMount: (container) => {
// Create the Svelte app inside the UI container
const app = new App({
target: container,
});
return app;
},
onRemove: (app) => {
// Destroy the app when the UI is removed
app?.$destroy();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
```tsx [Solid]
// 1. Import the style
import './style.css';
import { render } from 'solid-js/web';
export default defineContentScript({
matches: ['<all_urls>'],
// 2. Set cssInjectionMode
cssInjectionMode: 'ui',
async main(ctx) {
// 3. Define your UI
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
onMount: (container) => {
// Render your app to the UI container
const unmount = render(() => <div>...</div>, container);
},
onRemove: (unmount) => {
// Unmount the app when the UI is removed
unmount?.();
},
});
// 4. Mount the UI
ui.mount();
},
});
```
:::
See the [API Reference](/api/wxt/client/functions/createShadowRootUi) for the complete list of options.
:::info TailwindCSS
`createShadowRootUi` supports TailwindCSS out of the box! When importing the styles, just import the main CSS file containing the `@tailwind` directives, and everything will just work :+1:.
:::
## IFrame
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
1. Create an HTML page that will be loaded into your IFrame
```html
<!-- entrypoints/example-iframe.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your Content Script IFrame</title>
</head>
<body>
<!-- ... -->
</body>
</html>
```
1. Add the page to the manifest's `web_accessible_resources`
```ts
// wxt.config.ts
export default defineConfig({
manifest: {
web_accessible_resources: [
{
resources: ['example-iframe.html'],
matches: [...],
},
],
},
});
```
1. Create and mount the IFrame
```ts
export default defineContentScript({
matches: ['<all_urls>'],
async main(ctx) {
// Define the UI
const ui = await createIframeUi(ctx, {
page: '/example-iframe.html',
position: 'inline',
onMount: (wrapper, iframe) => {
// Add styles to the iframe like width
iframe.width = 123;
},
});
// Show UI to user
ui.mount();
},
});
```
See the [API Reference](/api/wxt/client/functions/createIframeUi) for the complete list of options.
+65 -2
View File
@@ -13,6 +13,24 @@ WXT's main goal is providing the best DX it possibly can. When running your exte
| `wxt.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
| `web-ext.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
## Dev Mode vs Production Builds
There are some notable differences between the development and production versions of an extension. During development:
1. **Content scripts are not listed in the `manifest.json`** when targeting MV3. Instead, the [`scripting`](https://developer.chrome.com/docs/extensions/reference/api/scripting) permission is used to register content scripts at runtime so they can be reloaded individually.
To get the list of content scripts during development, run the following in the background's console:
```ts
await chrome.scripting.getRegisteredContentScripts();
```
2. **The CSP is modified to allow loading scripts from the dev server**. Make sure you're using Chrome v110 or above for HMR to work.
3. If you don't include a background script/service worker, one will be created to perform various tasks in dev mode, mostly related to reloading different parts of the extension on change.
For production builds, none of the above modifications will be applied, and you're extension/manifest will only include what you have defined.
## Configure Browser Startup
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/wxt/interfaces/InlineConfig#runner) option, or in a separate gitignored file, `web-ext.config.ts`.
@@ -39,7 +57,9 @@ export default defineRunnerConfig({
:::
You may also setup default for your entire computer by creating a `web-ext.config.ts` file in your home directory. This is useful if you want to specify config for all project on your computer, like that you want to use Chrome Beta instead of Chrome.
### Browser Binaries
`web-ext`'s browser discovery is very limited. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
```ts
// ~/web-ext.config.ts
@@ -47,7 +67,50 @@ import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
binaries: {
chrome: '/path/to/chrome-beta',
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"
},
});
```
:::tip
When configuring browser binaries, it's helpful to put them in `~/web-ext.config.ts` instead of the project directory's `web-ext.config.ts` file. When placed in your home directory (`~/`), this config will be used by all WXT projects, so you only need to configure the binaries once.
:::
### Other options
You can customize other options as well, like startup URLs, profiles, or additional command line arguments:
```ts
// web-ext.config.ts
import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
startUrls: ['https://google.com', 'https://duckduckgo.com'],
chromiumProfile: '/path/to/profile/to/use',
chromiumArgs: ['--window-size=400x300'],
});
```
For a full list of options, see the [API Reference](/api/wxt/interfaces/ExtensionRunnerConfig).
## Reload the Extension
Normally, to manually reload an extension, you have to visit `chrome://extensions` and click the reload button for your extension.
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut `Alt+R`, that reloads the extension when pressed, without visiting `chrome://extensions`. This can also be customized or disabled:
```ts [wxt.config.ts]
import { defineConfig } from 'wxt';
export default defineConfig({
dev: {
reloadCommand: 'Alt+T', // false, to disable
},
});
```
:::info
This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`.
:::
+54 -1
View File
@@ -58,7 +58,7 @@ See [`/entrypoints` folder](/entrypoints/background) documentation for a full li
## Entrypoint Options
Some entrypoints, like content scripts, actions, or the background, can recieve additional options.
Some entrypoints, like content scripts, actions, or the background, can receive additional options.
In HTML files, options are listed as `meta` tags:
@@ -86,3 +86,56 @@ export default defineContentScript({
:::info
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
:::
### Side Effects
You cannot use imported variables outside the `main` function in JS entrypoints. This includes options, as shown below:
```ts
// entrypoints/content.ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
export default defineContentScript({
matches: GOOGLE_MATCHES,
main() {
// ...
},
});
```
```
$ wxt build
wxt build
WXT 0.14.1
Building chrome-mv3 for production with Vite 5.0.5
✖ Command failed after 360 ms
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
```
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `runAt`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
:::details Why?
When importing your entrypoint to get its definition, the file is imported in a **_node environment_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension usually has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
:::
:::warning
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
:::
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
```ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
export default defineContentScript({
matches: GOOGLE_MATCHES, // [!code --]
matches: ['*//*.google.com/*'], // [!code ++]
main() {
// ...
},
});
```
+38
View File
@@ -0,0 +1,38 @@
# ES Modules
Configure entrypoints to use ESM at runtime.
Currently, ESM entrypoints are opt-in, so you must configure each entrypoint with that in mind.
## HTML Pages <Badge type="warning" text="≥0.0.1" />
In general, you should always make HTML pages import ESM scripts, unless you need to support old browsers.
To make a script ESM, add `type="module"`:
<!-- prettier-ignore -->
```html
<script src="./main.ts"></script> <!-- [!code --] -->
<script src="./main.ts" type="module"></script> <!-- [!code ++] -->
```
## Background <Badge type="warning" text="≥0.16.0" />
In your background script, set `type: "module"`:
```ts
export default defineBackground({
type: 'module', // !code ++
main() {
// ...
},
});
```
:::warning
Only MV3 support ESM background scripts/service workers. When targeting MV2, the `type` option is ignored and the background is always bundled into a single file as IIFE.
:::
## Content Scripts
Coming soon. Follow [Content Script ESM Support #357](https://github.com/wxt-dev/wxt/issues/357) for updates.
+9 -11
View File
@@ -39,9 +39,9 @@ Then we can use `browser.storage` to save the install date to local storage.
```ts
// background.ts
export default defineBackground(() => {
browser.runtime.onInstall.addEventListener(({ reason }) => {
browser.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
browser.storage.local.setItem({ installDate: Date.now() });
browser.storage.local.set({ installDate: Date.now() });
}
});
});
@@ -57,21 +57,19 @@ Here's a basic request/response example:
// popup/main.ts
const res = await browser.runtime.sendMessage('ping');
console.log('res'); // "pong"
console.log(res); // "pong"
```
```ts
// background.ts
export default defineBackground(() => {
browser.runtime.onMessage.addEventListener(
(message, sender, sendResponse) => {
console.log(message); // "ping"
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log(message); // "ping"
// Wait 1 second and respond with "pong"
setTimeout(() => sendResponse('pong'), 1000);
return true;
},
);
// Wait 1 second and respond with "pong"
setTimeout(() => sendResponse('pong'), 1000);
return true;
});
});
```
+19 -4
View File
@@ -7,13 +7,17 @@ Bootstrap a new project, start from scratch, or [migrate an existing project](/g
:::code-group
```sh [pnpm]
pnpx wxt@latest init <project-name>
pnpm dlx wxt@latest init <project-name>
```
```sh [npm]
npx wxt@latest init <project-name>
```
```sh [bun]
bunx wxt@latest init <project-name>
```
:::
There are several starting templates available.
@@ -55,6 +59,12 @@ cd project-name
yarn init
```
```sh [bun]
mkdir project-name
cd project-name
bun init
```
:::
Then install `wxt`:
@@ -73,6 +83,10 @@ npm i --save-dev wxt
yarn add --dev wxt
```
```sh [bun]
bun add --dev wxt
```
:::
Add your first entrypoint:
@@ -102,7 +116,7 @@ Finally, add scripts to your `package.json`:
## Migrate an Existing Project
Before starting the migration, it is recommended to run `pnpx wxt@latest init` to see what a basic project looks like. Once you have an understanding of how WXT projects are structured, you're ready to convert the project, using the initialized project as a reference.
Before starting the migration, it is recommended to run `pnpm dlx wxt@latest init` to see what a basic project looks like. Once you have an understanding of how WXT projects are structured, you're ready to convert the project, using the initialized project as a reference.
Migrating a project to WXT comes down to a few steps:
@@ -133,7 +147,7 @@ The dev command will build the extension for development, open the browser, and
When running the dev command, WXT will make several changes to your `manifest.json` to improve your development experience:
- If missing, add a background script/service worker to enable fast reloads
- Add serveral `permissions` and `host_permissions` to enable HMR and fast reloads
- Add several `permissions` and `host_permissions` to enable HMR and fast reloads
- Modify the CSP to allow connections with the dev server
- Remove `content_scripts` and register them at runtime so they can be easily reloaded when you save a file
@@ -145,5 +159,6 @@ If you're an experienced web extension developer and think the dev manifest look
You're ready to build your web extension!
- Learn how to [add entrypoints](./entrypoints) like the popup, options page, or content scripts
- Configure your entrypoints to [use ESM](./esm) at runtime
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perform common tasks with WXT
+3 -3
View File
@@ -7,7 +7,7 @@ head:
# Introduction
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
WXT is a free and open source framework for building web extensions in an conventional, intuitive, and safe way **_for all browsers_**.
WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
@@ -15,12 +15,12 @@ WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX
## Conventions
WXT is an optionated framework. This helps keep projects consistent and easy to pick up.
WXT is an opinionated framework. This helps keep projects consistent and easy to pick up.
- **Generated manifest**: Based on your project's file structure
- **Entrypoint configuration**: Configure entrypoints from the same file they're declare in
- **Type-safety is a priority**: Out-of-the-box TypeScript support with improved browser API typing
- **Simple output file structure**: Ouptut file paths minimize the path at runtime
- **Simple output file structure**: Output file paths minimize the path at runtime
## Development
+126 -9
View File
@@ -1,12 +1,17 @@
---
outline: deep
---
# Manifest.json
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
The manifest.json is generated at build-time based on files in the `entrypoints/` directory and `wxt.config.ts`.
## Confiuration
## Configuration
While entrypoints are generated and added to the manifest at build-time, you can customize or add to your `manifest.json` in the config file.
```ts
// wxt.config.ts entrypoint of your extension
import { defineConfig } from 'wxt';
export default defineConfig({
@@ -19,18 +24,20 @@ export default defineConfig({
});
```
## `name`
### `name`
If not provided via the `manifest` config, the [manifest's `name`](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) defaults to your package.json's `name` property.
## `version` and `version_name`
### `version` and `version_name`
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) fields are based on your package.json's `version` property.
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
- `version_name` is the exact string listed in your package.json
- `version_name` is the exact string listed in your `package.json` or `wxt.config.ts` file
- `version` is the string cleaned up, with any invalid suffixes removed
### Example
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
#### Example
```json
// package.json
@@ -47,7 +54,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
}
```
## `icons`
### `icons`
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
@@ -80,7 +87,7 @@ export default defineConfig({
});
```
## `permissions`
### `permissions`
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
@@ -118,3 +125,113 @@ export default defineConfig({
},
});
```
See the official localization examples for more details:
<ExampleList tag="i18n" />
## Per-Manifest Version Config
WXT applies several transformations to your manifest to simplify managing both MV2 and MV3 keys in your `wxt.config.ts` file:
1. Top level MV2-only or MV3-only keys are stripped from the final manifest when targeting the other manifest version
2. Some keys, are automatically converted between versions when possible:
- Define `web_accessible_resources` in it's MV3 style and it will be converted to the MV2 style automatically
- `action` will automatically be converted to `browser_action` for MV3. To use `page_action` instead, add both `action` and `page_action` entries to your manifest
For example, a `wxt.config.ts` file that looks like this:
```ts
import { defineConfig } from 'wxt';
export default defineConfig({
mainfest: {
action: {
default_title: 'Some Title',
},
web_accessible_resources: [
{
matches: ['*://*.google.com/*'],
resources: ['icon/*.png'],
},
],
},
});
```
Will be output differently for each manifest version:
:::code-group
```json [MV2]
{
"manifest_version": 2,
// ...
"browser_action": {
"default_title": "Some Title"
},
"web_accessible_resources": ["icon/*.png"]
}
```
```json [MV3]
{
"manifest_version": 3,
// ...
"action": {
"default_title": "Some Title"
},
"web_accessible_resources": [
{
"matches": ["*://*.google.com/*"],
"resources": ["icon/*.png"]
}
]
}
```
:::
:::tip
If this isn't enough control for your use-case, remember you can use a function for the `manifest` key and generate it however you'd like, or you can use the `build:manifestGenerated` hook to apply additional transformations.
:::
## Per-Browser Configuration
The `manifest` field can be a function. If you are building and extension for multiple browsers, and need to modify the manifest per browser, using a function instead of an object is very useful.
```ts
export default defineConfig({
manifest: ({ browser, manifestVersion, mode, command }) => {
return {
// Your manifest
};
},
});
```
:::info
The first argument is of type `ConfigEnv`. See the [API reference](/api/wxt/interfaces/ConfigEnv) for info about each property.
:::
For example, say you use OAuth, and you need to provide a different `oauth.client_id` for each browser:
```ts
const clientIds = {
chrome: '<your-chrome-client-id>',
edge: '<your-edge-client-id>',
firefox: '<your-firefox-client-id>',
opera: '<your-opera-client-id>',
};
export default defineConfig({
manifest: ({ browser }) => ({
oauth: {
client_id: clientIds[browser],
scopes: [
// ...
],
},
}),
});
```
+2 -2
View File
@@ -12,7 +12,7 @@ Always start by generating a new vanilla project and merging it into your projec
```sh
cd path/to/your/project
pnpx wxt@latest init example-wxt --template vanilla
pnpm dlx wxt@latest init example-wxt --template vanilla
```
In general, you'll need to:
@@ -32,7 +32,7 @@ Every project is different, so there's no one-solution-fits-all to migrating you
## Popular Tools/Frameworks
Here's specific steps for other popuplar frameworks/build tools.
Here's specific steps for other popular frameworks/build tools.
### `vite-plugin-web-extension`
+29 -6
View File
@@ -1,6 +1,6 @@
# Multiple Browsers
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
You can build an extension for any combination of browser and manifest version. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
Separate build targets are written to their own output directories:
@@ -17,14 +17,12 @@ Separate build targets are written to their own output directories:
To build for a specific browser, pass the `-b --browser` flag from the CLI:
```
```sh
wxt --browser firefox
wxt build --browser firefox
```
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted manifest version used with that browser.
By default, it will build for `chrome`. When excluding the [manifest version flags](#target-manifest-version), it will default to the commonly accepted manifest version for that browser.
| Browser | Default Manifest Version |
| ---------------- | :----------------------: |
@@ -34,6 +32,10 @@ By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` f
| `edge` | 3 |
| Any other string | 3 |
:::tip
To configure which browser is opened when running dev mode via `wxt -b <browser>`, see the [Development docs](/guide/development#configure-browser-startup) docs.
:::
## Target Manifest Version
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
@@ -43,7 +45,7 @@ wxt --mv2
wxt build --mv2
```
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targeting MV2 for Chrome.
## Customizing Entrypoints
@@ -108,3 +110,24 @@ export default defineContentScript({
:::warning
Only `defineBackground` and `defineContentScript` support per-browser options right now.
:::
## Runtime
To determine the browser or manifest version at runtime, you can use any of the below variables:
- `import.meta.env.BROWSER`: A string, the target browser, usually equal to the `--browser` flag
- `import.meta.env.MANIFEST_VERSION`: A number, either `2` or `3`, depending on the manifest version targeted
- `import.meta.env.CHROME`: A boolean equivalent to `import.meta.env.BROWSER === "chrome"`
- `import.meta.env.FIREFOX`: A boolean equivalent to `import.meta.env.BROWSER === "firefox"`
- `import.meta.env.EDGE`: A boolean equivalent to `import.meta.env.BROWSER === "edge"`
- `import.meta.env.SAFARI`: A boolean equivalent to `import.meta.env.BROWSER === "safari"`
- `import.meta.env.OPERA`: A boolean equivalent to `import.meta.env.BROWSER === "opera"`
- `import.meta.env.COMMAND`: A string, `"serve"` when running `wxt` for development or `"build"` in all other cases.
:::info
These variables are constants defined at build time based on the build target. They do not actually detect which browser the code is running in.
For example, if you build for `--browser chrome` and publish it on Edge, `import.meta.env.BROWSER` will be `"chrome"`, not `"edge"`. You have to build a separate ZIP for `--browser edge` before `import.meta.env.BROWSER` will be `"edge"`.
If you need to know the actual browser your code is being ran on, you should use a [user agent parser](https://www.npmjs.com/package/ua-parser-js).
:::
+150 -90
View File
@@ -1,106 +1,127 @@
---
outline: deep
---
# Publishing
WXT offers several utilities that simplify the publishing process.
WXT will help you ZIP your extensions and submit them to the stores for review.
## First Time Publishing
If you're publishing an extension to a store for the first time, it's recommended that you manually navigate the process. Each store has unique steps and requirements that you need to familiarize yourself with.
If you're publishing an extension to a store for the first time, you must manually navigate the process. WXT doesn't help you create listings, each store has unique steps and requirements that you need to familiarize yourself with.
Each store requires that a ZIP file be uploaded. You can generate these using the `wxt zip` command:
For specific details about each store, see the stores sections below.
- [Chrome Web Store](#chrome-web-store)
- [Firefox Addon Store](#firefox-addon-store)
- [Edge Addons](#edge-addons)
## Automation
WXT provides two commands to help automate the release process:
- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
- `wxt submit init`: Help setup all the required secrets and options for the `wxt submit` command
To get started, run `wxt submit init` and follow the prompts. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
> In CI, make sure you add all the environment variables to the submit step.
To release an update, build all the ZIPs you plan on releasing:
```sh
wxt zip
wxt zip -b firefox
# etc
```
Generated ZIP files are stored in the `.output` directory.
Then run the `wxt submit` command, passing in all the ZIP files you want to release. In this case, we'll do a release for all 3 major stores: Chrome Web Store, Edge Addons, and Firefox Addons Store.
## Automation
If it's your first time running the command, you'll want to test your secrets by passing the `--dry-run` flag:
To automate releasing updates, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
```sh
wxt submit --dry-run \
--chrome-zip .output/<your-extension>-<version>-chrome.zip \
--firefox-zip .output/<your-extension>-<version>-firefox.zip --firefox-sources-zip .output/<your-extension>-<version>-sources.zip \
--edge-zip .output/<your-extension>-<version>-chrome.zip
```
:::info
🚧 WXT plans to eventually incorporate the `publish-browser-extension` package into its own `wxt submit` command.
If the dry run passes, remove the flag and do the actual release:
```sh
wxt submit \
--chrome-zip .output/<your-extension>-<version>-chrome.zip \
--firefox-zip .output/<your-extension>-<version>-firefox.zip --firefox-sources-zip .output/<your-extension>-<version>-sources.zip \
--edge-zip .output/<your-extension>-<version>-chrome.zip
```
:::tip
If you only need to release to a single store, only pass that store's ZIP flag.
:::
1. Install the necessary dependencies:
```sh
pnpm add -D publish-browser-extension env-cmd
```
2. Add scripts to your `package.json` file:
```json
{
"scripts": {
"submit": "env-cmd -f .env.submit -- publish-extension",
"submit:dry": "env-cmd -f .env.submit -- publish-extension --dry-run"
}
}
```
3. Create a `.env.submit` file and include the code below. If you're not publishing to certain stores, simply ignore their respective variables.
```txt
CHROME_EXTENSION_ID=""
CHROME_CLIENT_ID=""
CHROME_CLIENT_SECRET=""
CHROME_REFRESH_TOKEN=""
FIREFOX_EXTENSION_ID=""
FIREFOX_JWT_ISSUER=""
FIREFOX_JWT_SECRET=""
EDGE_PRODUCT_ID=""
EDGE_CLIENT_ID=""
EDGE_CLIENT_SECRET=""
EDGE_ACCESS_TOKEN_URL=""
```
> Each value will be filled in during the next step.
4. Run `npx publish-extension --help` for assistance with filling out all the values. Insert the obtained values within the double quotes.
5. ZIP all the targets you plan to publish, in this case Chrome and Firefox.
```sh
wxt zip
wxt zip -b firefox
```
6. Test your credentials by running the `submit:dry` command:
```sh
pnpm submit:dry \
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
```
7. Upload and submit your extension for review:
```sh
pnpm submit \
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
```
:::tip
See the [Firefox Addon Store](#firefox-addon-store) section for more details about the `--firefox-sources-zip` option.
:::
## GitHub Action
Here's an example of a GitHub Action to automate submiting new versions of your extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
Here's an example of a GitHub Action that submits new versions of an extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
```yml
# TODO
name: Release
on:
workflow_dispatch:
jobs:
submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 'latest'
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Zip extensions
run: |
pnpm zip
pnpm zip:firefox
- name: Submit to stores
run: |
pnpm wxt submit \
--chrome-zip .output/*-chrome.zip \
--firefox-zip .output/*-firefox.zip --firefox-sources-zip .output/*-sources.zip
env:
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
```
## Chrome Web Store
The action above lays the foundation for a basic workflow, including `zip` and `submit` steps. To further enhance your GitHub Action and delve into more complex scenarios, consider exploring the following examples from real projects. They introduce advanced features such as version management, changelog generation, and GitHub releases, tailored for different needs:
✅ Automated &bull; [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) &bull; [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
- [`aklinker1/github-better-line-counts`](https://github.com/aklinker1/github-better-line-counts/blob/main/.github/workflows/submit.yml) - Conventional commits, automated version bump and changelog generation, triggered manually, optional dry run for testing
- [`GuiEpi/plex-skipper`](https://github.com/GuiEpi/plex-skipper/blob/main/.github/workflows/deploy.yml) - Triggered automatically when `package.json` version is changed, creates and uploads artifacts to GitHub release.
> These examples are designed to provide clear insights and are a good starting point for customizing your own workflows. Feel free to explore and adapt them to your project needs.
## Stores
### Chrome Web Store
> ✅ Supported &bull; [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) &bull; [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
To create a ZIP for Chrome:
@@ -108,15 +129,15 @@ To create a ZIP for Chrome:
wxt zip
```
## Firefox Addon Store
### Firefox Addon Store
✅ Automated &bull; [Developer Dashboard](https://addons.mozilla.org/developers/) &bull; [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
> ✅ Supported &bull; [Developer Dashboard](https://addons.mozilla.org/developers/) &bull; [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
WXT and `publish-browser-extension` both fully support generating and automatically submitting a source code ZIP.
WXT fully supports generating and automatically submitting a source code ZIP.
When you run `wxt zip -b firefox`, your sources are zipped into the `.output` directory along with your built extension. WXT is configured to exclude certain files such as config files, hidden files, and tests. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
When you run `wxt zip -b firefox`, your sources are zipped into the `.output` directory alongside the extension. WXT will automatically exclude certain files such as config files, hidden files, and tests. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
To customize which files are zipped, add the `zip` option to your config file.
@@ -150,24 +171,63 @@ yarn
yarn zip:firefox
```
```sh [bun]
bun i
bun zip:firefox
```
:::
Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.
:::warning
If you use a `.env` files, they can effect the chunk hashes in the output directory. Either delete the .env file before running `wxt zip -b firefox`, or include it in your sources zip with the [`zip.includeSources`](/api/wxt/interfaces/InlineConfig#includesources) option. Be careful to not include any secrets in your `.env` files.
See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
:::
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
## Safari
#### Private Packages
🚧 Not automated at this time
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
```ts
// wxt.config.ts
export default defineConfig({
zip: {
downloadPackages: [
'@mycompany/some-package',
//...
],
},
});
```
Depending on your package manager, the `package.json` in the sources zip will be modified to use the downloaded dependencies via the `overrides` or `resolutions` field.
:::warning
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT cannot create at this time. For now, if you want to publish to Safari, follow this guide:
https://developer.apple.com/documentation/safariservices/safari_web_extensions/distributing_your_safari_web_extension
WXT uses the command `npm pack <package-name>` to download the package. That means regardless of your package manager, you need to properly setup a `.npmrc` file. NPM and PNPM both respect `.npmrc` files, but Yarn and Bun have their own ways of authorizing private registries, so you'll need to add an `.npmrc` file.
:::
## Edge Addons
### Safari
✅ Automated &bull; [Developer Dashboard](https://aka.ms/PartnerCenterLogin) &bull; [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
> 🚧 Not supported yet
WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT does not create yet. For now, if you want to publish to Safari, follow this guide:
- [Converting a web extension for Safari](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari) - "Convert your existing extension to a Safari web extension using Xcodes command-line tool."
When running the `safari-web-extension-converter` CLI tool, pass the `.output/safari-mv2` or `.output/safari-mv3` directory, not your source code directory.
```sh
pnpm wxt build -b safari
xcrun safari-web-extension-converter .output/safari-mv2
```
### Edge Addons
> ✅ Supported &bull; [Developer Dashboard](https://aka.ms/PartnerCenterLogin) &bull; [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
No need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP.
+266
View File
@@ -0,0 +1,266 @@
---
outline: deep
---
# Storage API
WXT provides a simplified API to replace the `browser.storage.*` APIs. Use the `storage` auto-import from `wxt/storage` or import it manually to get started:
```ts
import { storage } from 'wxt/storage';
```
[[toc]]
## Basic Usage
All storage keys must be prefixed by their storage area.
```ts
// ❌ This will throw an error
await storage.getItem('installDate');
// ✅ This is good
await storage.getItem('local:installDate');
```
You can use `local:`, `session:`, `sync:`, or `managed:`.
If you use TypeScript, you can add a type parameter to most methods to specify the expected type of the key's value:
```ts
await storage.getItem<number>('local:installDate');
await storage.watch<number>(
'local:installDate',
(newInstallDate, oldInstallDate) => {
// ...
},
);
await storage.getMeta<{ v: number }>('local:installDate');
```
For a full list of methods available, see the [API reference](/api/wxt/storage/interfaces/WxtStorage).
## Watchers
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
```ts
const unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {
console.log('Count changed:', { newCount, oldCount });
});
```
To remove the listener, call the returned `unwatch` function:
```ts
const unwatch = storage.watch(...);
// Some time later...
unwatch();
```
## Metadata
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
```ts
await Promise.all([
storage.setItem('local:preference', true),
storage.setMeta('local:preference', { lastModified: Date.now() }),
]);
```
When setting different properties of metadata from multiple calls, the properties are combined instead of overwritten:
```ts
await storage.setMeta('local:preference', { lastModified: Date.now() });
await storage.setMeta('local:preference', { v: 2 });
await storage.getMeta('local:preference'); // { v: 2, lastModified: 1703690746007 }
```
You can remove all metadata associated with a key, or just specific properties:
```ts
// Remove all properties
await storage.removeMeta('local:preference');
// Remove one property
await storage.removeMeta('local:preference', 'lastModified');
// Remove multiple properties
await storage.removeMeta('local:preference', ['lastModified', 'v']);
```
## Defining Storage Items
Writing the key and type parameter for the same key over and over again can be annoying. As an alternative, you can use `storage.defineItem` to create a "storage item".
Storage items contain the same APIs as the `storage` variable, but you can configure its type, default value, and more in a single place:
```ts
// utils/storage.ts
const showChangelogOnUpdate = storage.defineItem<boolean>(
'local:showChangelogOnUpdate',
{
defaultValue: true,
},
);
```
Now, instead of using the `storage` variable, you can use the helper functions on the storage item you created:
```ts
await showChangelogOnUpdate.getValue();
await showChangelogOnUpdate.setValue(false);
await showChangelogOnUpdate.removeValue();
const unwatch = showChangelogOnUpdate.watch(() => {
// ...
});
```
For a full list of properties and methods available, see the [API reference](/api/wxt/storage/interfaces/WxtStorageItem).
### Versioning
You can add versioning to storage items if you expect them to grow or change over time. When defining the first version of an item, start with version 1.
For example, consider a storage item that stores a list of websites that are ignored by an extension.
:::code-group
```ts [v1]
type IgnoredWebsiteV1 = string;
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>(
'local:ignoredWebsites',
{
defaultValue: [],
version: 1,
},
);
```
<!-- prettier-ignore -->
```ts [v2]
import { nanoid } from 'nanoid'; // [!code ++]
type IgnoredWebsiteV1 = string;
interface IgnoredWebsiteV2 { // [!code ++]
id: string; // [!code ++]
website: string; // [!code ++]
} // [!code ++]
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>( // [!code --]
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]
'local:ignoredWebsites',
{
defaultValue: [],
version: 1, // [!code --]
version: 2, // [!code ++]
migrations: { // [!code ++]
// Ran when migrating from v1 to v2 // [!code ++]
2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { // [!code ++]
return websites.map((website) => ({ id: nanoid(), website })); // [!code ++]
}, // [!code ++]
}, // [!code ++]
},
);
```
<!-- prettier-ignore -->
```ts [v3]
import { nanoid } from 'nanoid';
type IgnoredWebsiteV1 = string;
interface IgnoredWebsiteV2 {
id: string;
website: string;
}
interface IgnoredWebsiteV3 { // [!code ++]
id: string; // [!code ++]
website: string; // [!code ++]
enabled: boolean; // [!code ++]
} // [!code ++]
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code --]
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code ++]
'local:ignoredWebsites',
{
defaultValue: [],
version: 2, // [!code --]
version: 3, // [!code ++]
migrations: {
// Ran when migrating from v1 to v2
2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => {
return websites.map((website) => ({ id: nanoid(), website }));
},
// Ran when migrating from v2 to v3 // [!code ++]
3: (websites: IgnoredWebsiteV2[]): IgnoredWebsiteV3[] => { // [!code ++]
return websites.map((website) => ({ ...website, enabled: true })); // [!code ++]
}, // [!code ++]
},
},
);
```
:::
:::info
Internally, this uses a metadata property called `v` to track the value's current version.
:::
In this case, we thought that the ignored website list might change in the future, and were able to setup a versioned storage item from the start.
Realistically, you won't know a item needs versioned until you need to change it's schema. Thankfully, it's simple to add versioning to an unversioned storage item.
When a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work!
Lets look at the same ignored websites example from before, but start with an unversioned item this time:
:::code-group
```ts [Unversioned]
export const ignoredWebsites = storage.defineItem<string[]>(
'local:ignoredWebsites',
{
defaultValue: [],
},
);
```
<!-- prettier-ignore -->
```ts [v2]
import { nanoid } from 'nanoid'; // [!code ++]
// Retroactively add a type for the first version // [!code ++]
type IgnoredWebsiteV1 = string; // [!code ++]
interface IgnoredWebsiteV2 { // [!code ++]
id: string; // [!code ++]
website: string; // [!code ++]
} // [!code ++]
export const ignoredWebsites = storage.defineItem<string[]>( // [!code --]
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]
'local:ignoredWebsites',
{
defaultValue: [],
version: 2, // [!code ++]
migrations: { // [!code ++]
// Ran when migrating from v1 to v2 // [!code ++]
2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { // [!code ++]
return websites.map((website) => ({ id: nanoid(), website })); // [!code ++]
}, // [!code ++]
}, // [!code ++]
},
);
```
:::
### Running Migrations
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
+14 -81
View File
@@ -1,92 +1,25 @@
# Testing
WXT provides a couple of utils for unit testing your extension.
## Official Frameworks
[[toc]]
WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either [Playwright](https://playwright.dev/) or [Puppeteer](https://pptr.dev/) for E2E tests against Chromium browsers.
## Fake Browser
For details setting up each testing framework, see the official examples:
The `wxt/fake-browser` package includes an in-memory implementation of the `browser` variable you can use for testing. WXT simply re-exports the `fakeBrowser` variable from [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/).
<ExampleList tag="testing" />
Here's an example test using Vitest:
### Unofficial Frameworks
```ts
import { describe, it, expect, vi } from 'vitest';
import { browser } from 'wxt/browser';
import { fakeBrowser } from 'wxt';
Puppeteer and Playwright are the only E2E test runners that support Chrome Extensions. There are no other options at the time of writing.
// Function we're testing
function onHelloMessage(cb: () => void) {
browser.runtime.onMessage.addEventListener((message) => {
if (message.type === 'hello') return 'world';
});
}
There are other options for unit tests however, like [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), or [`node:test`](https://nodejs.org/api/test.html). **_WXT does not claim to support any of them_** because none of them support all of WXT's features, like TypeScript or auto-imports.
// Mock the real `browser` object with a fake one
vi.mock('wxt/browser', () => import('wxt/fake-browser'));
If you want to try to use a different framework for unit tests, you will need to configure the environment manually:
describe('onHelloMessage', () => {
it("should call the callback when the message type is 'hello'", () => {
const cb = vi.fn();
const expected = 'world';
- **Auto-imports**: Add `unimport` to your test environment or disable them by setting `imports: false` in your `wxt.config.ts` file
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.js`
- **[Remote Code Bundling](/guide/remote-code)**: If you use it, configure your environment to handle the `url:` module prefix
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `import.meta.env.BROWSER`) by adding them to the global scope before accessing them (`import.meta.env.BROWSER = "chrome"`)
- **Import paths**: If you use the `@/` or `~/` path aliases, add them to your test environment
onHelloMessage(cb);
const actual = await fakeBrowser.runtime.sendMessage({ type: 'hello' });
expect(cb).toBeCalledTimes(1);
expect(actual).toBe(expected);
});
it("should ignore the message when the message type is not 'hello'", () => {
const cb = vi.fn();
onHelloMessage(cb);
await fakeBrowser.runtime.sendMessage({ type: 'not-hello' }).catch();
expect(cb).not.toBeCalled();
});
});
```
See [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/) for setup, implemented APIs, and example tests.
## Handling Auto-imports
By default, WXT uses auto-imports. For tests, this can cause issues if your test environment is not setup to handle them correctly.
:::warning 🚧&ensp;Testing utils are not implemented yet!
Eventually, WXT will provide utilities for setting up these auto-imports. For now, you'll need to set them up manually.
:::
Not all testing frameworks can handle auto-imports. If your framework or setup is not listed below, it may be easiest to disable auto-imports.
To setup auto-imports manually, use [`unplugin-auto-import`](https://www.npmjs.com/package/unplugin-auto-import). It uses the same tool, `unimport`, as WXT and will result in compatiple auto-imports. `unplugin-auto-import` supports lots of different tools (vite, webpack, esbuild, rollup, etc). You can try and integrate it into your build process.
### Vitest (Recommended)
Vitest is easy, simply add `uplugin-auto-import` to your project.
```ts
// vitest.config.ts
import autoImports from 'unplugin-auto-import/vite';
export default defineConfig({
plugins: [
autoImports({
imports: [{ name: 'defineConfig', from: 'wxt' }],
presets: [{ package: 'wxt/client' }, { package: 'wxt/browser' }],
dirs: ['components', 'composables', 'hooks', 'utils'],
}),
],
});
```
### Jest
Don't use jest and auto-imports. You could try and configure jest to be transpiled by one of `unplugin-auto-import`'s supported built tools, but I don't know of a way to configure this. See [unplugin/unplugin-auto-import#33](https://github.com/unplugin/unplugin-auto-import/issues/33) if you want to try and set it up.
I would recommend disabling auto-imports or migrating to Vitest if you want to use auto-imports.
### Mocha
TODO: Is this possible? Maybe with `esbuild-mocha`? I would recommend moving to Vitest.
[Here's how Vitest is configured](https://github.com/wxt-dev/wxt/blob/main/src/testing/wxt-vitest-plugin.ts) for reference.
+14 -13
View File
@@ -6,7 +6,7 @@ title: Next-gen Web Extension Framework
hero:
name: WXT
text: Next-gen Web Extension Framework
tagline: An open source tool that makes Chrome Extension devlopment faster than ever before.
tagline: An open source tool that makes Chrome Extension development faster than ever before.
image:
src: /hero-logo.svg
alt: WXT
@@ -47,11 +47,9 @@ features:
details: Nuxt-like auto-imports to speed up development.
link: /guide/auto-imports
linkText: Read docs
- icon: ⬇️
title: Bundle Remote Code
details: Downloads and bundles remote code imported from URLs.
link: /guide/remote-code
linkText: Read docs
- icon: 🤖
title: Automated Publishing
details: Automatically zip, upload, submit, and publish extensions.
- icon: 🎨
title: Frontend Framework Agnostic
details: Works with any front-end framework with a Vite plugin.
@@ -64,20 +62,23 @@ features:
linkText: See templates
- icon: 📏
title: Bundle Analysis
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
- icon: 🤖
title: Automated Publishing
details: 'Coming soon. Automatically zip, upload, and release extensions.'
details: Tools for analyzing the final extension bundle and minimizing your extension's size.
- icon: ⬇️
title: Bundle Remote Code
details: Downloads and bundles remote code imported from URLs.
link: /guide/remote-code
linkText: Read docs
---
<section class="vp-doc">
<section class="vp-doc" style="padding: 0 48px">
<div class="container">
<h2>Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First</h2>
<p>
WXT's simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
</p>
<div style="margin: auto; width: 100%; max-width: 700px; text-align: center">
<img src="./assets/cli-output.png" alt="Screenshot of WXT's build output" />
<div style="margin: auto; width: 100%; max-width: 900px; text-align: center">
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94" controls></video>
<br />
<small>
And who doesn't appreciate a beautiful CLI?
</small>
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+4 -2
View File
@@ -2,8 +2,10 @@
"entryPoints": [
"../src",
"../src/client",
"../src/client/browser.ts",
"../src/client/sandbox"
"../src/browser.ts",
"../src/sandbox",
"../src/storage.ts",
"../src/testing"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
"out": "./api",
+120
View File
@@ -0,0 +1,120 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TestProject } from '../utils';
import { resetBundleIncrement } from '~/core/builders/vite/plugins';
import open from 'open';
vi.mock('open');
const openMock = vi.mocked(open);
vi.mock('ci-info', () => ({
isCI: false,
}));
describe('Analysis', () => {
beforeEach(() => {
resetBundleIncrement();
});
it('should output a stats.html with no part files by default', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
project.addFile('entrypoints/options.html');
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
await project.build({
analysis: {
enabled: true,
},
});
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
false,
);
});
it('should save part files when requested', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
project.addFile('entrypoints/options.html');
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
await project.build({
analysis: {
enabled: true,
keepArtifacts: true,
},
});
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);
});
it('should support customizing the stats output directory', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
project.addFile('entrypoints/options.html');
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
await project.build({
analysis: {
enabled: true,
outputFile: 'stats/bundle.html',
},
});
expect(await project.fileExists('stats/bundle.html')).toBe(true);
});
it('should place artifacts next to the custom output file', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
project.addFile('entrypoints/options.html');
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
await project.build({
analysis: {
enabled: true,
outputFile: 'stats/bundle.html',
keepArtifacts: true,
},
});
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);
});
it('should open the stats in the browser when requested', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
project.addFile('entrypoints/options.html');
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
await project.build({
analysis: {
enabled: true,
open: true,
},
});
expect(openMock).toBeCalledTimes(1);
expect(openMock).toBeCalledWith(project.resolvePath('stats.html'));
});
});
+105 -21
View File
@@ -7,7 +7,7 @@ describe('Auto Imports', () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
await project.prepare();
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
.toMatchInlineSnapshot(`
@@ -17,12 +17,18 @@ describe('Auto Imports', () => {
export {}
declare global {
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
const browser: typeof import('wxt/browser')['browser']
const createContentScriptUi: typeof import('wxt/client')['createContentScriptUi']
const defineBackground: typeof import('wxt/client')['defineBackground']
const createIframeUi: typeof import('wxt/client')['createIframeUi']
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
const defineBackground: typeof import('wxt/sandbox')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
const storage: typeof import('wxt/storage')['storage']
}
"
`);
@@ -32,20 +38,20 @@ describe('Auto Imports', () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
await project.prepare();
expect(await project.serializeFile('.wxt/wxt.d.ts'))
.toMatchInlineSnapshot(`
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/imports.d.ts\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
"
`);
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
"
`);
});
});
@@ -57,7 +63,7 @@ describe('Auto Imports', () => {
});
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
await project.prepare();
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
});
@@ -69,7 +75,7 @@ describe('Auto Imports', () => {
});
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
await project.prepare();
expect(
await project.serializeFile('.wxt/wxt.d.ts'),
@@ -78,13 +84,91 @@ describe('Auto Imports', () => {
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
"
`,
);
});
});
describe('eslintrc', () => {
it('should output the globals list for ESLint to consume', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.prepare({
imports: {
eslintrc: {
enabled: true,
},
},
});
expect(await project.serializeFile('.wxt/eslintrc-auto-import.json'))
.toMatchInlineSnapshot(`
".wxt/eslintrc-auto-import.json
----------------------------------------
{
"globals": {
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"fakeBrowser": true,
"storage": true
}
}
"
`);
});
it('should allow customizing the output', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.prepare({
imports: {
eslintrc: {
enabled: true,
filePath: project.resolvePath('example.json'),
globalsPropValue: 'readonly',
},
},
});
expect(await project.serializeFile('example.json'))
.toMatchInlineSnapshot(`
"example.json
----------------------------------------
{
"globals": {
"ContentScriptContext": "readonly",
"InvalidMatchPattern": "readonly",
"MatchPattern": "readonly",
"browser": "readonly",
"createIframeUi": "readonly",
"createIntegratedUi": "readonly",
"createShadowRootUi": "readonly",
"defineBackground": "readonly",
"defineConfig": "readonly",
"defineContentScript": "readonly",
"defineUnlistedScript": "readonly",
"fakeBrowser": "readonly",
"storage": "readonly"
}
}
"
`);
});
});
});
+112
View File
@@ -0,0 +1,112 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { WxtHooks } from '~/types';
const hooks: WxtHooks = {
ready: vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
'build:manifestGenerated': vi.fn(),
'entrypoints:resolved': vi.fn(),
'entrypoints:grouped': vi.fn(),
'vite:build:extendConfig': vi.fn(),
'vite:devServer:extendConfig': vi.fn(),
};
function expectHooksToBeCalled(
called: Record<keyof WxtHooks, boolean | number>,
) {
Object.keys(hooks).forEach((key) => {
const hookName = key as keyof WxtHooks;
const value = called[hookName];
const times = typeof value === 'number' ? value : value ? 1 : 0;
expect(
hooks[hookName],
`Expected "${hookName}" to be called ${times} time(s)`,
).toBeCalledTimes(times);
});
}
describe('Hooks', () => {
beforeEach(() => {
Object.values(hooks).forEach((fn) => fn.mockReset());
});
it('prepare should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.prepare({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': false,
'build:done': false,
'build:manifestGenerated': false,
'entrypoints:grouped': false,
'entrypoints:resolved': true,
'vite:build:extendConfig': false,
'vite:devServer:extendConfig': false,
});
});
it('build should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.build({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'vite:build:extendConfig': 1,
'vite:devServer:extendConfig': false,
});
});
it('zip should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
await project.zip({ hooks });
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'vite:build:extendConfig': 1,
'vite:devServer:extendConfig': false,
});
});
it('server.start should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');
const server = await project.startServer({
hooks,
runner: {
disabled: true,
},
});
await server.stop();
expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
'vite:build:extendConfig': 2,
'vite:devServer:extendConfig': 1,
});
});
});
+15
View File
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { execaCommand } from 'execa';
import glob from 'fast-glob';
import { mkdir, writeJson } from 'fs-extra';
describe('Init command', () => {
it('should download and create a template', async () => {
@@ -25,6 +26,7 @@ describe('Init command', () => {
"assets/vue.svg",
"components/HelloWorld.vue",
"entrypoints/background.ts",
"entrypoints/content.ts",
"entrypoints/popup/App.vue",
"entrypoints/popup/index.html",
"entrypoints/popup/main.ts",
@@ -41,4 +43,17 @@ describe('Init command', () => {
]
`);
});
it('should throw an error if the directory is not empty', async () => {
const project = new TestProject();
await mkdir(project.root);
await writeJson(project.resolvePath('package.json'), {});
await expect(() =>
execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
env: { ...process.env, CI: 'true' },
stdio: 'ignore',
}),
).rejects.toThrowError('Command failed with exit code 1:');
});
});
-569
View File
@@ -1,514 +1,7 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
// TODO: move to unit tests to speed this up - this doesn't have to be in E2E tests
describe('Manifest Content', () => {
describe('popup', () => {
const popupContent = (type?: 'browser_action' | 'page_action') => `
<html>
<head>
${type == null ? '' : `<meta name="manifest.type" content="${type}">`}
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }">
<title>Default Title</title>
</head>
</html>
`;
it('should include an action for mv3', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', popupContent());
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.action).toEqual({
default_icon: { '16': '/icon/16.png' },
default_title: 'Default Title',
default_popup: 'popup.html',
});
});
it.each([
[undefined, 'browser_action'],
['browser_action', 'browser_action'],
['page_action', 'page_action'],
] as const)(
'should include a browser_action for mv2',
async (type, expectedType) => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', popupContent(type));
await project.build({ manifestVersion: 2 });
const manifest = await project.getOutputManifest(
'.output/chrome-mv2/manifest.json',
);
expect(manifest[expectedType]).toEqual({
default_icon: { '16': '/icon/16.png' },
default_title: 'Default Title',
default_popup: 'popup.html',
});
},
);
});
describe('options', () => {
const optionsContent = `
<html>
<head>
<meta name="manifest.open_in_tab" content="false">
<meta name="manifest.chrome_style" content="true">
<meta name="manifest.browser_style" content="true">
</head>
</html>
`;
it('should include a options_ui and chrome_style for chrome', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', optionsContent);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.options_ui).toEqual({
open_in_tab: false,
chrome_style: true,
page: 'options.html',
});
});
it('should include a options_ui and browser_style for firefox', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', optionsContent);
await project.build({ browser: 'firefox' });
const manifest = await project.getOutputManifest(
'.output/firefox-mv2/manifest.json',
);
expect(manifest.options_ui).toEqual({
open_in_tab: false,
browser_style: true,
page: 'options.html',
});
});
});
describe('background', () => {
const backgroundContent = `
export default defineBackground({
persistent: true,
type: "module",
main: () => {},
})
`;
it('should include a background script for mv2', async () => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.background).toEqual({
type: 'module',
service_worker: 'background.js',
});
});
it('should include a options_ui and browser_style for firefox', async () => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ manifestVersion: 2 });
const manifest = await project.getOutputManifest(
'.output/chrome-mv2/manifest.json',
);
expect(manifest.background).toEqual({
persistent: true,
scripts: ['background.js'],
});
});
});
describe('icons', () => {
it('should auto-discover icons with the correct name', async () => {
const project = new TestProject();
project.addFile('public/icon-16.png');
project.addFile('public/icon/32.jpeg');
project.addFile('public/icon@48w.jpg');
project.addFile('public/icon-64x64.gif');
project.addFile('public/icon@96.bmp');
project.addFile('public/icon/128x128.ico');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual({
'16': 'icon-16.png',
'32': 'icon/32.jpeg',
'48': 'icon@48w.jpg',
'64': 'icon-64x64.gif',
'96': 'icon@96.bmp',
'128': 'icon/128x128.ico',
});
});
it('should return undefined when no icons are found', async () => {
const project = new TestProject();
project.addFile('public/logo.png');
project.addFile('public/icon.jpeg');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toBeUndefined();
});
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
const project = new TestProject();
project.addFile('public/icon-16.png');
project.addFile('public/icon-32.png');
project.addFile('public/logo-16.png');
project.addFile('public/logo-32.png');
project.addFile('public/logo-48.png');
const icons = {
'16': 'logo-16.png',
'32': 'logo-32.png',
'48': 'logo-48.png',
};
project.setConfigFileConfig({
manifest: {
icons,
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual(icons);
});
});
describe('content_scripts', () => {
it('should group content scripts and styles together based on their manifest properties', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/one.content/style.css',
`body { color: red }`,
);
project.addFile(
'entrypoints/two.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body { color: green }`,
);
project.addFile(
'entrypoints/three.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/three.content/style.css',
`body { color: blue }`,
);
project.addFile(
'entrypoints/four.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://duckduckgo.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/four.content/style.css',
`body { color: yellow }`,
);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['content-scripts/four.css'],
js: ['content-scripts/four.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['content-scripts/three.css', 'content-scripts/two.css'],
js: ['content-scripts/three.js', 'content-scripts/two.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/one.css'],
js: ['content-scripts/one.js'],
});
});
it('should add to any content scripts declared in wxt.config.ts', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body {
background-color: red;
}`,
);
project.setConfigFileConfig({
manifest: {
content_scripts: [
{
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
},
],
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
});
});
it('should add a CSS entry when cssInjectionMode is undefined', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
it('should add a CSS entry when cssInjectionMode is "manifest"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manifest",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
it('should not add an entry for CSS when cssInjectionMode is "manual"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manual",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV2', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
await project.build({
manifestVersion: 2,
});
expect(await project.serializeFile('.output/chrome-mv2/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv2/manifest.json
----------------------------------------
{\\"manifest_version\\":2,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[\\"content-scripts/content.css\\"]}"
`);
});
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV3', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
await project.build({
manifestVersion: 3,
});
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}]}"
`);
});
});
it('should combine web accessible resources', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
project.setConfigFileConfig({
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['https://one.com/*'] },
],
},
});
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"web_accessible_resources\\":[{\\"resources\\":[\\"one.png\\"],\\"matches\\":[\\"https://one.com/*\\"]},{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}],\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
it('should respect the transformManifest option', async () => {
const project = new TestProject();
project.addFile(
'wxt.config.ts',
`import { defineConfig } from 'wxt';
export default defineConfig({
transformManifest(manifest) {
manifest.author = "Custom Author"
}
})`,
);
await project.build();
const output = await project.serializeFile(
'.output/chrome-mv3/manifest.json',
);
expect(output).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"author\\":\\"Custom Author\\"}"
`);
});
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
@@ -537,66 +30,4 @@ describe('Manifest Content', () => {
expect(safariManifest.background.persistent).toBe(expected);
},
);
describe('versions', () => {
it.each([
['chrome', 3] as const,
['safari', 2] as const,
['edge', 3] as const,
])(
'should include version_name on %s when it needs simplified',
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0-alpha1',
});
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0');
expect(manifest.version_name).toBe('1.0.0-alpha1');
},
);
it.each([['firefox', 2] as const])(
"should not include a version_name on %s because the browser doesn't support it",
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0-alpha1',
});
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0');
expect(manifest.version_name).toBeUndefined();
},
);
it.each([
['chrome', 3] as const,
['firefox', 2] as const,
['safari', 3] as const,
['edge', 3] as const,
])(
'should not include the version_name if it is equal to version',
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0.1',
});
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0.1');
expect(manifest.version_name).toBeUndefined();
},
);
});
});
+191 -4
View File
@@ -7,13 +7,18 @@ describe('Output Directory Structure', () => {
project.addFile('entrypoints/.DS_Store');
project.addFile('entrypoints/.hidden1/index.html');
project.addFile('entrypoints/.hidden2.html');
project.addFile('entrypoints/unlisted.html');
await project.build();
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\"}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0"}
================================================================================
.output/chrome-mv3/unlisted.html
----------------------------------------
"
`);
});
@@ -69,7 +74,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"css":["content-scripts/one.css","content-scripts/two.css"],"js":["content-scripts/one.js","content-scripts/two.js"]}]}"
`);
});
@@ -96,7 +101,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"js":["content-scripts/overlay-one.js"]}]}"
`);
});
@@ -197,7 +202,7 @@ describe('Output Directory Structure', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"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\\"]}]}"
{"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(
@@ -208,4 +213,186 @@ describe('Output Directory Structure', () => {
);
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
});
it("should output to a custom directory when overriding 'outDir'", async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.setConfigFileConfig({
outDir: 'dist',
});
await project.build();
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
true,
);
});
it('should generate ESM background script when type=module', async () => {
const project = new TestProject();
project.addFile(
'utils/log.ts',
`export function logHello(name: string) {
console.log(\`Hello \${name}!\`);
}`,
);
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
type: "module",
main() {
logHello("background");
},
})`,
);
project.addFile(
'entrypoints/popup/index.html',
`<html>
<head>
<script type="module" src="./main.ts"></script>
</head>
</html>`,
);
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
experimental: {
// Simplify the build output for comparison
includeBrowserPolyfill: false,
},
vite: () => ({
build: {
// Make output for snapshot readible
minify: false,
},
}),
});
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
import { l as logHello } from "./chunks/log-BsZv2eRn.js";
function defineBackground(arg) {
if (typeof arg === "function")
return { main: arg };
return arg;
}
const definition = defineBackground({
type: "module",
main() {
logHello("background");
}
});
chrome;
function print(method, ...args) {
return;
}
var 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)
};
try {
const res = definition.main();
if (res 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 () => {
const project = new TestProject();
project.addFile(
'utils/log.ts',
`export function logHello(name: string) {
console.log(\`Hello \${name}!\`);
}`,
);
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
main() {
logHello("background");
},
})`,
);
project.addFile(
'entrypoints/popup/index.html',
`<html>
<head>
<script type="module" src="./main.ts"></script>
</head>
</html>`,
);
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
experimental: {
// Simplify the build output for comparison
includeBrowserPolyfill: false,
},
vite: () => ({
build: {
// Make output for snapshot readible
minify: false,
},
}),
});
expect(await project.serializeFile('.output/chrome-mv3/background.js'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
(function() {
"use strict";
function defineBackground(arg) {
if (typeof arg === "function")
return { main: arg };
return arg;
}
function logHello(name) {
console.log(\`Hello \${name}!\`);
}
_background;
const definition = defineBackground({
main() {
logHello("background");
}
});
_background;
chrome;
function print(method, ...args) {
return;
}
var 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)
};
try {
const res = definition.main();
if (res 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;
}
})();
_background;
"
`);
});
});
+1 -1
View File
@@ -37,7 +37,7 @@ describe('React', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":"<all_urls>","js":["content-scripts/demo.js"]}]}"
`);
});
});
+167 -109
View File
@@ -4,8 +4,9 @@ import { TestProject } from '../utils';
describe('TypeScript Project', () => {
it('should generate defined constants correctly', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
await project.build();
await project.prepare();
const output = await project.serializeFile('.wxt/types/globals.d.ts');
expect(output).toMatchInlineSnapshot(`
@@ -13,16 +14,19 @@ describe('TypeScript Project', () => {
----------------------------------------
// Generated by wxt
export {}
declare global {
const __MANIFEST_VERSION__: 2 | 3;
const __BROWSER__: string;
const __IS_CHROME__: boolean;
const __IS_FIREFOX__: boolean;
const __IS_SAFARI__: boolean;
const __IS_EDGE__: boolean;
const __IS_OPERA__: boolean;
const __COMMAND__: \\"build\\" | \\"serve\\";
const __ENTRYPOINT__: string;
interface ImportMetaEnv {
readonly MANIFEST_VERSION: 2 | 3;
readonly BROWSER: string;
readonly CHROME: boolean;
readonly FIREFOX: boolean;
readonly SAFARI: boolean;
readonly EDGE: boolean;
readonly OPERA: boolean;
readonly COMMAND: "build" | "serve";
readonly ENTRYPOINT: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
"
`);
@@ -34,22 +38,24 @@ describe('TypeScript Project', () => {
project.addFile('entrypoints/options.html');
project.addFile('entrypoints/sandbox.html');
await project.build();
await project.prepare();
const output = await project.serializeFile('.wxt/types/paths.d.ts');
expect(output).toMatchInlineSnapshot(`
".wxt/types/paths.d.ts
----------------------------------------
// Generated by wxt
import \\"wxt/browser\\";
import "wxt/browser";
declare module \\"wxt/browser\\" {
type PublicPath =
| \\"/options.html\\"
| \\"/popup.html\\"
| \\"/sandbox.html\\"
declare module "wxt/browser" {
export type PublicPath =
| "/options.html"
| "/popup.html"
| "/sandbox.html"
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
}
}
"
@@ -58,6 +64,7 @@ describe('TypeScript Project', () => {
it('should augment the types for browser.i18n.getMessage', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile(
'public/_locales/en/messages.json',
JSON.stringify({
@@ -96,16 +103,16 @@ describe('TypeScript Project', () => {
},
});
await project.build();
await project.prepare();
const output = await project.serializeFile('.wxt/types/i18n.d.ts');
expect(output).toMatchInlineSnapshot(`
".wxt/types/i18n.d.ts
----------------------------------------
// Generated by wxt
import \\"wxt/browser\\";
import "wxt/browser";
declare module \\"wxt/browser\\" {
declare module "wxt/browser" {
/**
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
*/
@@ -120,91 +127,91 @@ describe('TypeScript Project', () => {
/**
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
Note: You can't use this message in a manifest file.
*
* \\"<browser.runtime.id>\\"
*
* "<browser.runtime.id>"
*/
getMessage(
messageName: \\"@@extension_id\\",
messageName: "@@extension_id",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
*
*
* \\"<browser.i18n.getUiLocale()>\\"
* No message description.
*
* "<browser.i18n.getUiLocale()>"
*/
getMessage(
messageName: \\"@@ui_locale\\",
messageName: "@@ui_locale",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* The text direction for the current locale, either \\"ltr\\" for left-to-right languages such as English or \\"rtl\\" for right-to-left languages such as Japanese.
*
* \\"<ltr|rtl>\\"
* The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
*
* "<ltr|rtl>"
*/
getMessage(
messageName: \\"@@bidi_dir\\",
messageName: "@@bidi_dir",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
*
* \\"<rtl|ltr>\\"
* If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
*
* "<rtl|ltr>"
*/
getMessage(
messageName: \\"@@bidi_reversed_dir\\",
messageName: "@@bidi_reversed_dir",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
*
* \\"<left|right>\\"
* If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
*
* "<left|right>"
*/
getMessage(
messageName: \\"@@bidi_start_edge\\",
messageName: "@@bidi_start_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
*
* \\"<right|left>\\"
* If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
*
* "<right|left>"
*/
getMessage(
messageName: \\"@@bidi_end_edge\\",
messageName: "@@bidi_end_edge",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* Ask for the user's name
*
* \\"What's your name?\\"
*
* "What's your name?"
*/
getMessage(
messageName: \\"prompt_for_name\\",
messageName: "prompt_for_name",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* Greet the user
*
* \\"Hello, $USER$\\"
*
* "Hello, $USER$"
*/
getMessage(
messageName: \\"hello\\",
messageName: "hello",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
/**
* Say goodbye to the user
*
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
*
* "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
*/
getMessage(
messageName: \\"bye\\",
messageName: "bye",
substitutions?: string | string[],
options?: GetMessageOptions,
): string;
@@ -216,104 +223,155 @@ describe('TypeScript Project', () => {
it('should reference all the required types in a single declaration file', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
await project.build();
await project.prepare();
const output = await project.serializeFile('.wxt/wxt.d.ts');
expect(output).toMatchInlineSnapshot(`
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/imports.d.ts\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
"
`);
});
it('should generate a TSConfig file for the project', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
await project.build();
await project.prepare();
const output = await project.serializeFile('.wxt/tsconfig.json');
expect(output).toMatchInlineSnapshot(`
".wxt/tsconfig.json
----------------------------------------
{
\\"compilerOptions\\": {
\\"target\\": \\"ESNext\\",
\\"module\\": \\"ESNext\\",
\\"moduleResolution\\": \\"Bundler\\",
\\"noEmit\\": true,
\\"esModuleInterop\\": true,
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@\\": [\\"..\\"],
\\"@/*\\": [\\"../*\\"],
\\"~\\": [\\"..\\"],
\\"~/*\\": [\\"../*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"@": [".."],
"@/*": ["../*"],
"~": [".."],
"~/*": ["../*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
\\"include\\": [
\\"../**/*\\",
\\"./wxt.d.ts\\"
"include": [
"../**/*",
"./wxt.d.ts"
],
\\"exclude\\": [\\"../.output\\"]
"exclude": ["../.output"]
}"
`);
});
it('should generate correct path aliases for a custom srcDir', async () => {
const project = new TestProject();
project.addFile('src/entrypoints/unlisted.html');
project.setConfigFileConfig({
srcDir: 'src',
});
await project.build();
await project.prepare();
const output = await project.serializeFile('.wxt/tsconfig.json');
expect(output).toMatchInlineSnapshot(`
".wxt/tsconfig.json
----------------------------------------
{
\\"compilerOptions\\": {
\\"target\\": \\"ESNext\\",
\\"module\\": \\"ESNext\\",
\\"moduleResolution\\": \\"Bundler\\",
\\"noEmit\\": true,
\\"esModuleInterop\\": true,
\\"forceConsistentCasingInFileNames\\": true,
\\"resolveJsonModule\\": true,
\\"strict\\": true,
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@\\": [\\"../src\\"],
\\"@/*\\": [\\"../src/*\\"],
\\"~\\": [\\"../src\\"],
\\"~/*\\": [\\"../src/*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
\\"include\\": [
\\"../**/*\\",
\\"./wxt.d.ts\\"
"include": [
"../**/*",
"./wxt.d.ts"
],
\\"exclude\\": [\\"../.output\\"]
"exclude": ["../.output"]
}"
`);
});
it('should add additional path aliases listed in the alias config, preventing defaults from being overridden', async () => {
const project = new TestProject();
project.addFile('src/entrypoints/unlisted.html');
project.setConfigFileConfig({
srcDir: 'src',
alias: {
example: 'example',
'@': 'ignored-path',
},
});
await project.prepare();
const output = await project.serializeFile('.wxt/tsconfig.json');
expect(output).toMatchInlineSnapshot(`
".wxt/tsconfig.json
----------------------------------------
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"example": ["../example"],
"example/*": ["../example/*"],
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
}"
`);
});
+52 -5
View File
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { InlineConfig } from '~/types';
describe('User Config', () => {
// Root directory is tested with all tests.
@@ -28,7 +29,7 @@ describe('User Config', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
`);
});
@@ -54,12 +55,13 @@ describe('User Config', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
`);
});
it('should merge inline and user config based manifests', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile(
'wxt.config.ts',
`import { defineConfig } from 'wxt';
@@ -78,11 +80,56 @@ describe('User Config', () => {
}),
});
const output = await project.serializeOutput();
expect(output).toMatchInlineSnapshot(`
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","example_customization":["3","build","production","chrome"]}"
`);
});
it('should exclude the polyfill when the experimental setting is set to false', async () => {
const buildBackground = async (config?: InlineConfig) => {
const background = `export default defineBackground(() => console.log(browser.runtime.id));`;
const projectWithPolyfill = new TestProject();
projectWithPolyfill.addFile('entrypoints/background.ts', background);
await projectWithPolyfill.build(config);
return await projectWithPolyfill.serializeFile(
'.output/chrome-mv3/background.js',
);
};
const withPolyfill = await buildBackground();
const withoutPolyfill = await buildBackground({
experimental: {
includeBrowserPolyfill: false,
},
});
expect(withoutPolyfill).not.toBe(withPolyfill);
});
it('should respect changing config files', async () => {
const project = new TestProject();
project.addFile(
'src/entrypoints/background.ts',
`export default defineBackground(
() => console.log('Hello background'),
);`,
);
project.addFile(
'test.config.ts',
`import { defineConfig } from 'wxt';
export default defineConfig({
outDir: ".custom-output",
srcDir: "src",
});`,
);
await project.build({ configFile: 'test.config.ts' });
expect(
await project.fileExists('.custom-output/chrome-mv3/background.js'),
).toBe(true);
});
});
+99
View File
@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest';
import { TestProject } from '../utils';
import extract from 'extract-zip';
import { execaCommand } from 'execa';
import { readFile, writeFile } from 'fs-extra';
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('Zipping', () => {
it('should download packages and produce a valid build when zipping sources', async () => {
const project = new TestProject({
name: 'test',
version: '1.0.0',
dependencies: {
flatten: '1.0.3',
},
});
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
await project.zip({
browser: 'firefox',
zip: { downloadPackages: ['flatten'] },
});
expect(await project.fileExists('.output/')).toBe(true);
await extract(sourcesZip, { dir: unzipDir });
// Update package json wxt path
const packageJsonPath = project.resolvePath(unzipDir, 'package.json');
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
packageJson.dependencies.wxt = '../../../../..';
await writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
'utf-8',
);
// Build zipped extension
await expect(
execaCommand('pnpm i --ignore-workspace --frozen-lockfile false', {
cwd: unzipDir,
}),
).resolves.toMatchObject({ exitCode: 0 });
await expect(
execaCommand('pnpm wxt build -b firefox', { cwd: unzipDir }),
).resolves.toMatchObject({ exitCode: 0 });
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
expect(
await project.serializeFile(
project.resolvePath(unzipDir, 'package.json'),
),
).toMatchInlineSnapshot(`
".output/test-1.0.0-sources/package.json
----------------------------------------
{
"name": "test",
"description": "Example description",
"version": "1.0.0",
"dependencies": {
"wxt": "../../../../..",
"flatten": "1.0.3"
},
"resolutions": {
"flatten@1.0.3": "file://./.wxt/local_modules/flatten-1.0.3.tgz"
}
}"
`);
});
it('should correctly apply template variables for zip file names based on provided config', async () => {
const project = new TestProject({
name: 'test',
version: '1.0.0',
});
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {});',
);
const artifactZip = '.output/test-1.0.0-firefox-development.zip';
const sourcesZip = '.output/test-1.0.0-development-sources.zip';
await project.zip({
browser: 'firefox',
mode: 'development',
zip: {
artifactTemplate: '{{name}}-{{version}}-{{browser}}-{{mode}}.zip',
sourcesTemplate: '{{name}}-{{version}}-{{mode}}-sources.zip',
},
});
expect(await project.fileExists(artifactZip)).toBe(true);
expect(await project.fileExists(sourcesZip)).toBe(true);
});
});
+48 -14
View File
@@ -2,7 +2,14 @@ import { dirname, join, relative, resolve } from 'path';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { execaCommand } from 'execa';
import { InlineConfig, UserConfig, build } from '../src';
import {
InlineConfig,
UserConfig,
build,
createServer,
prepare,
zip,
} from '../src';
import { normalizePath } from '../src/core/utils/paths';
import merge from 'lodash.merge';
@@ -63,22 +70,49 @@ export class TestProject {
if (filename === 'wxt.config.ts') this.config = {};
}
/**
* Write the files to the test directory install dependencies, and build the project.
*/
async prepare(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await prepare({ ...config, root: this.root });
}
async build(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await build({ ...config, root: this.root });
}
async zip(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await zip({ ...config, root: this.root });
}
async startServer(config: InlineConfig = {}) {
await this.writeProjectToDisk();
const server = await createServer({ ...config, root: this.root });
await server.start();
return server;
}
/**
* Call `path.resolve` relative to the project's root directory.
*/
resolvePath(...path: string[]): string {
return resolve(this.root, ...path);
}
private async writeProjectToDisk() {
if (this.config == null) this.setConfigFileConfig();
for (const file of this.files) {
const [name, content] = file;
const filePath = resolve(this.root, name);
const filePath = this.resolvePath(name);
const fileDir = dirname(filePath);
await fs.ensureDir(fileDir);
await fs.writeFile(filePath, content ?? '', 'utf-8');
}
await execaCommand('npm i --ignore-scripts', { cwd: this.root });
await build({ ...config, root: this.root });
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
cwd: this.root,
});
}
/**
@@ -113,13 +147,13 @@ export class TestProject {
ignoreContentsOfFilenames?: string[],
): Promise<string> {
const outputFiles = await glob('**/*', {
cwd: resolve(this.root, dir),
cwd: this.resolvePath(dir),
ignore: ['**/node_modules', '**/.output'],
});
outputFiles.sort();
const fileContents = [];
for (const file of outputFiles) {
const path = resolve(this.root, dir, file);
const path = this.resolvePath(dir, file);
const isContentIgnored = !!ignoreContentsOfFilenames?.find(
(ignoredFile) => normalizePath(path).endsWith(ignoredFile),
);
@@ -129,25 +163,25 @@ export class TestProject {
}
/**
* @param path An abosolute path to a file or a path relative to the root.
* @param path An absolute path to a file or a path relative to the root.
* @param ignoreContents An optional boolean that, when true, causes this function to not print
* the file contents.
*/
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
const absolutePath = resolve(this.root, path);
const absolutePath = this.resolvePath(path);
return [
normalizePath(relative(this.root, absolutePath)),
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
].join(`\n${''.padEnd(40, '-')}\n`);
}
fileExists(path: string): Promise<boolean> {
return fs.exists(resolve(this.root, path));
fileExists(...path: string[]): Promise<boolean> {
return fs.exists(this.resolvePath(...path));
}
async getOutputManifest(
path: string = '.output/chrome-mv3/manifest.json',
): Promise<any> {
return await fs.readJson(resolve(this.root, path));
return await fs.readJson(this.resolvePath(path));
}
}
+102 -59
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.8.2",
"version": "0.17.13-alpha1",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -30,37 +30,68 @@
"bin",
"dist"
],
"bin": "./bin/wxt.cjs",
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.cjs",
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./client": {
"import": "./dist/client.js",
"types": "./dist/client.d.ts"
"types": "./dist/client.d.ts",
"import": "./dist/client.js"
},
"./sandbox": {
"import": "./dist/sandbox.js",
"types": "./dist/sandbox.d.ts"
"types": "./dist/sandbox.d.ts",
"import": "./dist/sandbox.js"
},
"./browser": {
"import": "./dist/browser.js",
"types": "./dist/browser.d.ts"
"types": "./dist/browser.d.ts",
"import": "./dist/browser.js"
},
"./testing": {
"import": {
"types": "./dist/testing.d.ts",
"default": "./dist/testing.js"
},
"require": {
"types": "./dist/testing.d.cts",
"default": "./dist/testing.cjs"
}
},
"./storage": {
"import": {
"types": "./dist/storage.d.ts",
"default": "./dist/storage.js"
},
"require": {
"types": "./dist/storage.d.cts",
"default": "./dist/storage.cjs"
}
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
}
},
"scripts": {
"wxt": "tsx src/cli/index.ts",
"build": "tsx scripts/build.ts",
"format": "prettier --write .",
"format:check": "prettier --write .",
"compile": "tsc --noEmit",
"check": "run-s -c check:*",
"check:default": "check",
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
"test": "vitest",
"test:coverage": "vitest run --coverage.enabled --coverage.exclude=e2e --coverage.exclude=src/testing",
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=src/**\" \"--coverage.exclude=src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\"",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
@@ -70,71 +101,83 @@
"sync-releases": "pnpx changelogen@latest gh release"
},
"dependencies": {
"@types/webextension-polyfill": "^0.10.1",
"@webext-core/fake-browser": "^1.2.2",
"@webext-core/isolated-element": "^1.0.4",
"@webext-core/match-patterns": "^1.0.2",
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@types/webextension-polyfill": "^0.10.5",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.1",
"@webext-core/match-patterns": "^1.0.3",
"async-mutex": "^0.4.0",
"c12": "^1.4.2",
"c12": "^1.5.1",
"cac": "^6.7.14",
"chokidar": "^3.5.3",
"ci-info": "^4.0.0",
"consola": "^3.2.3",
"esbuild": "^0.19.4",
"defu": "^6.1.3",
"dequal": "^2.0.3",
"esbuild": "^0.19.5",
"fast-glob": "^3.3.1",
"filesize": "^10.0.8",
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.2",
"immer": "^10.0.2",
"giget": "^1.1.3",
"hookable": "^5.5.3",
"is-wsl": "^3.0.0",
"jiti": "^1.19.1",
"jiti": "^1.21.0",
"json5": "^2.2.3",
"linkedom": "^0.15.1",
"jszip": "^3.10.1",
"linkedom": "^0.16.1",
"minimatch": "^9.0.3",
"natural-compare": "^1.4.0",
"normalize-path": "^3.0.0",
"nypm": "^0.3.6",
"open": "^10.1.0",
"ora": "^7.0.1",
"picocolors": "^1.0.0",
"prompts": "^2.4.2",
"rollup-plugin-visualizer": "^5.9.2",
"unimport": "^3.1.0",
"vite": "^4.4.11",
"web-ext-run": "^0.1.0",
"webextension-polyfill": "^0.10.0",
"zip-dir": "^2.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@types/fs-extra": "^11.0.1",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^20.8.6",
"@types/prompts": "^2.4.4",
"@vitest/coverage-v8": "^0.34.6",
"execa": "^7.2.0",
"jsdom": "^22.1.0",
"lint-staged": "^14.0.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.1",
"simple-git-hooks": "^2.9.0",
"tsup": "^7.1.0",
"tsx": "^3.12.7",
"typedoc": "^0.25.2",
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.2.2",
"vitepress": "1.0.0-rc.10",
"vitest": "^0.34.6",
"vitest-mock-extended": "^1.3.1",
"vue": "^3.3.4",
"publish-browser-extension": "^2.1.3",
"unimport": "^3.4.0",
"vite": "^5.2.8",
"web-ext-run": "^0.2.0",
"webextension-polyfill": "^0.10.0"
},
"peerDependencies": {
"webextension-polyfill": ">=0.10.0"
"devDependencies": {
"@aklinker1/check": "^1.1.1",
"@faker-js/faker": "^8.3.1",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/natural-compare": "^1.4.3",
"@types/node": "^20.10.3",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"@vitest/coverage-v8": "^1.0.1",
"execa": "^8.0.1",
"extract-zip": "^2.0.1",
"happy-dom": "^13.3.8",
"lint-staged": "^15.2.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"p-map": "^7.0.0",
"prettier": "^3.1.0",
"publint": "^0.2.6",
"simple-git-hooks": "^2.9.0",
"tsup": "^8.0.1",
"tsx": "^4.6.2",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.3.2",
"vitepress": "1.0.0-rc.34",
"vitest": "^1.2.2",
"vitest-mock-extended": "^1.3.1",
"vitest-plugin-random-seed": "^1.0.2",
"vue": "^3.3.10"
},
"packageManager": "pnpm@8.6.3",
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "prettier --write"
"*": "prettier --ignore-unknown --write"
},
"changelog": {
"excludeAuthors": [
+2047 -1180
View File
File diff suppressed because it is too large Load Diff
+85 -65
View File
@@ -1,94 +1,114 @@
import tsup from 'tsup';
import glob from 'fast-glob';
import { printFileList } from '../src/core/log/printFileList';
import { formatDuration } from '../src/core/utils/formatDuration';
import { printFileList } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
import ora from 'ora';
import fs from 'fs-extra';
import { consola } from 'consola';
import pMap from 'p-map';
import os from 'node:os';
import path from 'node:path';
import {
virtualModuleNames,
virtualEntrypointModuleNames,
} from '~/core/utils/virtual-modules';
const spinner = ora('Building WXT').start();
const spinnerText = 'Building WXT';
const spinner = ora(spinnerText).start();
const startTime = Date.now();
const outDir = 'dist';
const virtualEntrypoints = ['background', 'content-script', 'unlisted-script'];
await fs.rm(path.join(outDir, '*'), { recursive: true, force: true });
await fs.rm(outDir, { recursive: true, force: true });
const preset = {
dts: true,
silent: true,
sourcemap: false,
external: virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
} satisfies tsup.Options;
await Promise.all([
tsup.build({
function spinnerPMap(configs: tsup.Options[]) {
let progress = 1;
const updateSpinner = () => {
spinner.text = `${spinnerText} [${progress}/${configs.length}]`;
};
updateSpinner();
return pMap(
config,
async (config) => {
const res = await tsup.build(config);
progress++;
updateSpinner();
return res;
},
{
stopOnError: true,
concurrency: process.env.CI === 'true' ? os.cpus().length : Infinity,
},
);
}
const config: tsup.Options[] = [
// CJS/ESM
{
...preset,
entry: {
index: 'src/index.ts',
testing: 'src/testing/index.ts',
storage: 'src/storage.ts',
},
format: ['cjs', 'esm'],
sourcemap: true,
dts: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { cli: 'src/cli/index.ts' },
format: ['cjs'],
sourcemap: 'inline',
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { client: 'src/client/index.ts' },
clean: true,
},
// ESM-only
{
...preset,
entry: {
browser: 'src/browser.ts',
sandbox: 'src/sandbox/index.ts',
client: 'src/client/index.ts',
},
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { browser: 'src/client/browser.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: { sandbox: 'src/client/sandbox/index.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
}),
...virtualEntrypoints.map((entryName) =>
tsup.build({
entry: {
[`virtual-modules/${entryName}-entrypoint`]: `src/client/virtual-modules/${entryName}-entrypoint.ts`,
},
{
...preset,
entry: virtualModuleNames.reduce<Record<string, string>>(
(acc, moduleName) => {
acc[`virtual/${moduleName}`] = `src/virtual/${moduleName}.ts`;
return acc;
},
format: ['esm'],
sourcemap: true,
silent: true,
external: [`virtual:user-${entryName}`, 'vite'],
}),
),
tsup.build({
{},
),
format: ['esm'],
splitting: false,
dts: false,
external: [...preset.external, 'wxt'],
},
// CJS-only
{
...preset,
entry: {
'virtual-modules/reload-html': `src/client/virtual-modules/reload-html.ts`,
cli: 'src/cli/index.ts',
},
format: ['esm'],
sourcemap: true,
silent: true,
external: ['vite'],
}),
tsup.build({
entry: {
'virtual-modules/fake-browser': `src/client/virtual-modules/fake-browser.ts`,
banner: {
// Fixes dynamic require of nodejs modules. See https://github.com/wxt-dev/wxt/issues/355
// https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
},
format: ['esm', 'cjs'],
silent: true,
external: ['vite'],
}),
]).catch((err) => {
},
];
await spinnerPMap(config).catch((err) => {
spinner.fail();
console.error(err);
process.exit(1);
});
// Copy "public" files that need shipped inside WXT
await fs.copyFile('src/vite-builder-env.d.ts', 'dist/vite-builder-env.d.ts');
spinner.clear().stop();
const duration = Date.now() - startTime;
-16
View File
@@ -1,16 +0,0 @@
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;
}
-1
View File
@@ -1 +0,0 @@
declare const __TEST_SEED__: number;
+854
View File
@@ -0,0 +1,854 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
import { browser } from '~/browser';
import { WxtStorageItem, storage } from '~/storage';
/**
* This works because fakeBrowser is synchronous, and is will finish any number of chained
* calls within a single tick of the event loop, ie: a timeout of 0.
*/
async function waitForMigrations() {
return new Promise((res) => setTimeout(res));
}
describe('Storage Utils', () => {
beforeEach(() => {
fakeBrowser.reset();
storage.unwatch();
});
describe.each(['local', 'sync', 'managed', 'session'] as const)(
'storage - %s',
(storageArea) => {
describe('getItem', () => {
it('should return the value from the correct storage area', async () => {
const expected = 123;
await fakeBrowser.storage[storageArea].set({ count: expected });
const actual = await storage.getItem(`${storageArea}:count`);
expect(actual).toBe(expected);
});
it('should return the value if multiple : are use in the key', async () => {
const expected = 'value';
await fakeBrowser.storage[storageArea].set({ 'some:key': expected });
const actual = await storage.getItem(`${storageArea}:some:key`);
expect(actual).toBe(expected);
});
it("should return null if the value doesn't exist", async () => {
const actual = await storage.getItem(`${storageArea}:count`);
expect(actual).toBeNull();
});
it('should return the default value if passed in options', async () => {
const expected = 0;
const actual = await storage.getItem(`${storageArea}:count`, {
defaultValue: expected,
});
expect(actual).toBe(expected);
});
});
describe('getItems', () => {
it('should return an array of values', async () => {
const expected = [
{ key: `${storageArea}:count`, value: 234 },
{ key: `${storageArea}:installDate`, value: null },
{ key: `${storageArea}:otherValue`, value: 345 },
];
const params = [
expected[0].key,
expected[1].key,
{
key: expected[2].key,
options: { defaultValue: expected[2].value },
},
];
await fakeBrowser.storage[storageArea].set({
count: expected[0].value,
});
const actual = await storage.getItems(params);
expect(actual).toEqual(expected);
});
});
describe('getMeta', () => {
it('should return item metadata from key+$', async () => {
const expected = { v: 1 };
await fakeBrowser.storage[storageArea].set({ count$: expected });
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual(expected);
});
it('should return an empty object if missing', async () => {
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual({});
});
});
describe('setItem', () => {
it('should set the value in the correct storage area', async () => {
const key = `${storageArea}:count`;
const value = 321;
await storage.setItem(key, value);
});
it.each([undefined, null])(
'should remove the item from storage when setting the value to %s',
async (value) => {
await fakeBrowser.storage[storageArea].set({ count: 345 });
await storage.setItem(`${storageArea}:count`, value as null);
// For some reason storage sets the value to "null" instead of deleting it. So using
// fakeBrowser during the expect fails. Using storage works. I've confirmed that this
// doesn't happen in a real extension environment.
expect(await storage.getItem(`${storageArea}:count`)).toBeNull();
},
);
});
describe('setItems', () => {
it('should set multiple items in storage', async () => {
const expected = [
{ key: `${storageArea}:count`, value: 234 },
{ key: `${storageArea}:installDate`, value: null },
];
await fakeBrowser.storage[storageArea].set({
count: 123,
installDate: 321,
});
await storage.setItems(expected);
const actual = await storage.getItems(
expected.map((item) => item.key),
);
expect(actual).toHaveLength(2);
expected.forEach((item) => {
expect(actual).toContainEqual(item);
});
});
});
describe('setMeta', () => {
it('should set metadata at key+$', async () => {
const existing = { v: 1 };
await browser.storage[storageArea].set({ count$: existing });
const newValues = {
date: Date.now(),
};
const expected = { ...existing, ...newValues };
await storage.setMeta(`${storageArea}:count`, newValues);
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual(expected);
});
it.each([undefined, null])(
'should remove any properties set to %s',
async (version) => {
const existing = { v: 1 };
await browser.storage[storageArea].set({ count$: existing });
const expected = {};
await storage.setMeta(`${storageArea}:count`, { v: version });
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual(expected);
},
);
});
describe('removeItem', () => {
it('should remove the key from storage', async () => {
await fakeBrowser.storage[storageArea].set({ count: 456 });
await storage.removeItem(`${storageArea}:count`);
const actual = await storage.getItem(`${storageArea}:count`);
expect(actual).toBeNull();
});
it('should not remove the metadata by default', async () => {
const expected = { v: 1 };
await fakeBrowser.storage[storageArea].set({
count$: expected,
count: 3,
});
await storage.removeItem(`${storageArea}:count`);
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual(expected);
});
it('should remove the metadata when requested', async () => {
await fakeBrowser.storage[storageArea].set({
count$: { v: 1 },
count: 3,
});
await storage.removeItem(`${storageArea}:count`, {
removeMeta: true,
});
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual({});
});
});
describe('removeItems', () => {
it('should remove multiple items', async () => {
const key1 = `${storageArea}:one`;
const key2 = `${storageArea}:two`;
const key3 = `${storageArea}:three`;
await fakeBrowser.storage[storageArea].set({
['one']: '1',
['two']: null,
['two$']: { v: 1 },
['three']: '1',
['three$']: { v: 1 },
});
await storage.removeItems([
key1,
key2,
{ key: key3, options: { removeMeta: true } },
]);
expect(await storage.getItem(key1)).toBeNull();
expect(await storage.getItem(key2)).toBeNull();
expect(await storage.getMeta(key2)).toEqual({ v: 1 });
expect(await storage.getItem(key3)).toBeNull();
expect(await storage.getMeta(key3)).toEqual({});
});
});
describe('removeMeta', () => {
it('should remove all metadata', async () => {
await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });
await storage.removeMeta(`${storageArea}:count`);
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual({});
});
it('should only remove specific properties', async () => {
await fakeBrowser.storage[storageArea].set({
count$: { v: 4, d: Date.now() },
});
await storage.removeMeta(`${storageArea}:count`, ['d']);
const actual = await storage.getMeta(`${storageArea}:count`);
expect(actual).toEqual({ v: 4 });
});
});
describe('snapshot', () => {
it('should return a snapshot of the entire storage without area prefixes', async () => {
const expected = {
count: 1,
count$: { v: 2 },
example: 'test',
};
await fakeBrowser.storage[storageArea].set(expected);
const actual = await storage.snapshot(storageArea);
expect(actual).toEqual(expected);
});
it('should exclude specific properties and their metadata', async () => {
const input = {
count: 1,
count$: { v: 2 },
example: 'test',
};
const excludeKeys = ['count'];
const expected = {
example: 'test',
};
await fakeBrowser.storage[storageArea].set(input);
const actual = await storage.snapshot(storageArea, { excludeKeys });
expect(actual).toEqual(expected);
});
});
describe('restoreSnapshot', () => {
it('should restore a snapshot object by setting all values in storage', async () => {
const data = {
one: 'one',
two: 'two',
};
const existing = {
two: 'two-two',
three: 'three',
};
await fakeBrowser.storage[storageArea].set(existing);
await storage.restoreSnapshot(storageArea, data);
const actual = await storage.snapshot(storageArea);
expect(actual).toEqual({ ...existing, ...data });
});
it('should overwrite, not merge, any metadata keys in the snapshot', async () => {
const existing = {
count: 1,
count$: {
v: 2,
},
};
const data = {
count$: {
restoredAt: Date.now(),
},
};
const expected = {
...existing,
count$: data.count$,
};
await fakeBrowser.storage[storageArea].set(existing);
await storage.restoreSnapshot(storageArea, data);
const actual = await storage.snapshot(storageArea);
expect(actual).toEqual(expected);
});
});
describe('watch', () => {
it('should not trigger if the changed key is different from the requested key', async () => {
const cb = vi.fn();
storage.watch(`${storageArea}:key`, cb);
await storage.setItem(`${storageArea}:not-the-key`, '123');
expect(cb).not.toBeCalled();
});
it("should not trigger if the value doesn't change", async () => {
const cb = vi.fn();
const value = '123';
await storage.setItem(`${storageArea}:key`, value);
storage.watch(`${storageArea}:key`, cb);
await storage.setItem(`${storageArea}:key`, value);
expect(cb).not.toBeCalled();
});
it('should call the callback when the value changes', async () => {
const cb = vi.fn();
const newValue = '123';
const oldValue = null;
storage.watch(`${storageArea}:key`, cb);
await storage.setItem(`${storageArea}:key`, newValue);
expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith(newValue, oldValue);
});
it('should remove the listener when calling the returned function', async () => {
const cb = vi.fn();
const unwatch = storage.watch(`${storageArea}:key`, cb);
unwatch();
await storage.setItem(`${storageArea}:key`, '123');
expect(cb).not.toBeCalled();
});
});
describe('unwatch', () => {
it('should remove all watch listeners', async () => {
const cb = vi.fn();
storage.watch(`${storageArea}:key`, cb);
storage.unwatch();
await storage.setItem(`${storageArea}:key`, '123');
expect(cb).not.toBeCalled();
});
});
},
);
describe('defineItem', () => {
describe('versioning', () => {
it('should migrate values to the latest when a version upgrade is detected', async () => {
await fakeBrowser.storage.local.set({
count: 2,
count$: { v: 1 },
});
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
const item = storage.defineItem<number, { v: number }>(`local:count`, {
defaultValue: 0,
version: 3,
migrations: {
2: migrateToV2,
3: migrateToV3,
},
});
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
expect(actualValue).toEqual(12);
expect(actualMeta).toEqual({ v: 3 });
expect(migrateToV2).toBeCalledTimes(1);
expect(migrateToV2).toBeCalledWith(2);
expect(migrateToV3).toBeCalledTimes(1);
expect(migrateToV3).toBeCalledWith(4);
});
it("should not run migrations if the value doesn't exist yet", async () => {
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
const item = storage.defineItem<number, { v: number }>(`local:count`, {
defaultValue: 0,
version: 3,
migrations: {
2: migrateToV2,
3: migrateToV3,
},
});
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
expect(actualValue).toEqual(0);
expect(actualMeta).toEqual({});
expect(migrateToV2).not.toBeCalled();
expect(migrateToV3).not.toBeCalled();
});
it('should run the v2 migration when converting an unversioned item to a versioned one', async () => {
await fakeBrowser.storage.local.set({
count: 2,
});
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
const item = storage.defineItem<number, { v: number }>(`local:count`, {
defaultValue: 0,
version: 2,
migrations: {
2: migrateToV2,
},
});
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
expect(actualValue).toEqual(4);
expect(actualMeta).toEqual({ v: 2 });
expect(migrateToV2).toBeCalledTimes(1);
expect(migrateToV2).toBeCalledWith(2);
});
it('should not run old migrations if the version is unchanged', async () => {
await fakeBrowser.storage.local.set({
count: 2,
count$: { v: 3 },
});
const migrateToV2 = vi.fn((oldCount) => oldCount * 2);
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
storage.defineItem<number, { v: number }>(`local:count`, {
defaultValue: 0,
version: 3,
migrations: {
2: migrateToV2,
3: migrateToV3,
},
});
await waitForMigrations();
expect(migrateToV2).not.toBeCalled();
expect(migrateToV3).not.toBeCalled();
});
it('should skip missing migration functions', async () => {
await fakeBrowser.storage.local.set({
count: 2,
count$: { v: 0 },
});
const migrateToV1 = vi.fn((oldCount) => oldCount * 1);
const migrateToV3 = vi.fn((oldCount) => oldCount * 3);
const item = storage.defineItem<number, { v: number }>(`local:count`, {
defaultValue: 0,
version: 3,
migrations: {
1: migrateToV1,
3: migrateToV3,
},
});
await waitForMigrations();
const actualValue = await item.getValue();
const actualMeta = await item.getMeta();
expect(actualValue).toEqual(6);
expect(actualMeta).toEqual({ v: 3 });
expect(migrateToV1).toBeCalledTimes(1);
expect(migrateToV1).toBeCalledWith(2);
expect(migrateToV3).toBeCalledTimes(1);
expect(migrateToV3).toBeCalledWith(2);
});
it('should throw an error if the new version is less than the previous version', async () => {
const prevVersion = 2;
const nextVersion = 1;
await fakeBrowser.storage.local.set({
count: 0,
count$: { v: prevVersion },
});
const item = storage.defineItem(`local:count`, {
defaultValue: 0,
version: nextVersion,
});
await waitForMigrations();
await expect(item.migrate()).rejects.toThrow(
'Version downgrade detected (v2 -> v1) for "local:count"',
);
});
});
describe('getValue', () => {
it('should return the value from storage', async () => {
const expected = 2;
const item = storage.defineItem<number>(`local:count`);
await fakeBrowser.storage.local.set({ count: expected });
const actual = await item.getValue();
expect(actual).toBe(expected);
});
it('should return null if missing', async () => {
const item = storage.defineItem<number>(`local:count`);
const actual = await item.getValue();
expect(actual).toBeNull();
});
it('should return the provided default value if missing', async () => {
const expected = 0;
const item = storage.defineItem(`local:count`, {
defaultValue: expected,
});
const actual = await item.getValue();
expect(actual).toEqual(expected);
});
});
describe('getMeta', () => {
it('should return the value from storage at key+$', async () => {
const expected = { v: 2 };
const item = storage.defineItem<number, { v: number }>(`local:count`);
await fakeBrowser.storage.local.set({ count$: expected });
const actual = await item.getMeta();
expect(actual).toBe(expected);
});
it('should return an empty object if missing', async () => {
const expected = {};
const item = storage.defineItem<number, { v: number }>(`local:count`);
const actual = await item.getMeta();
expect(actual).toEqual(expected);
});
});
describe('setValue', () => {
it('should set the value in storage', async () => {
const expected = 1;
const item = storage.defineItem<number>(`local:count`);
await item.setValue(expected);
const actual = await item.getValue();
expect(actual).toBe(expected);
});
it.each([undefined, null])(
'should remove the value in storage when %s is passed in',
async (value) => {
const item = storage.defineItem<number>(`local:count`);
// @ts-expect-error: undefined is not assignable to null, but we're testing that case on purpose
await item.setValue(value);
const actual = await item.getValue();
expect(actual).toBeNull();
},
);
});
describe('setMeta', () => {
it('should set metadata at key+$', async () => {
const expected = { date: Date.now() };
const item = storage.defineItem<number, { date: number }>(
`local:count`,
);
await item.setMeta(expected);
const actual = await item.getMeta();
expect(actual).toEqual(expected);
});
it('should add to metadata if already present', async () => {
const existing = { v: 2 };
const newFields = { date: Date.now() };
const expected = { ...existing, ...newFields };
const item = storage.defineItem<number, { date: number; v: number }>(
`local:count`,
);
await fakeBrowser.storage.local.set({
count$: existing,
});
await item.setMeta(newFields);
const actual = await item.getMeta();
expect(actual).toEqual(expected);
});
});
describe('removeValue', () => {
it('should remove the key from storage', async () => {
const item = storage.defineItem(`local:count`);
await fakeBrowser.storage.local.set({ count: 456 });
await item.removeValue();
const actual = await item.getValue();
expect(actual).toBeNull();
});
it('should not remove the metadata by default', async () => {
const item = storage.defineItem(`local:count`);
const expected = { v: 1 };
await fakeBrowser.storage.local.set({
count$: expected,
count: 3,
});
await item.removeValue();
const actual = await item.getMeta();
expect(actual).toEqual(expected);
});
it('should remove the metadata when requested', async () => {
const item = storage.defineItem(`local:count`);
await fakeBrowser.storage.local.set({
count$: { v: 1 },
count: 3,
});
await item.removeValue({ removeMeta: true });
const actual = await item.getMeta();
expect(actual).toEqual({});
});
});
describe('removeMeta', () => {
it('should remove all metadata', async () => {
const item = storage.defineItem<number, { v: number }>(`local:count`);
await fakeBrowser.storage.local.set({ count$: { v: 4 } });
await item.removeMeta();
const actual = await item.getMeta();
expect(actual).toEqual({});
});
it('should only remove specific properties', async () => {
const item = storage.defineItem<number, { v: number; d: number }>(
`local:count`,
);
await fakeBrowser.storage.local.set({
count$: { v: 4, d: Date.now() },
});
await item.removeMeta(['d']);
const actual = await item.getMeta();
expect(actual).toEqual({ v: 4 });
});
});
describe('watch', () => {
it("should not trigger if the changed key is different from the item's key", async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();
item.watch(cb);
await storage.setItem(`local:not-the-key`, '123');
expect(cb).not.toBeCalled();
});
it("should not trigger if the value doesn't change", async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();
const value = '123';
await item.setValue(value);
item.watch(cb);
await item.setValue(value);
expect(cb).not.toBeCalled();
});
it('should call the callback when the value changes', async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();
const newValue = '123';
const oldValue = null;
item.watch(cb);
await item.setValue(newValue);
expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith(newValue, oldValue);
});
it('should use the default value for the newValue when the item is removed', async () => {
const defaultValue = 'default';
const item = storage.defineItem<string>(`local:key`, {
defaultValue,
});
const cb = vi.fn();
const oldValue = '123';
await item.setValue(oldValue);
item.watch(cb);
await item.removeValue();
expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith(defaultValue, oldValue);
});
it("should use the default value for the oldItem when the item didn't exist in storage yet", async () => {
const defaultValue = 'default';
const item = storage.defineItem<string>(`local:key`, {
defaultValue,
});
const cb = vi.fn();
const newValue = '123';
await item.removeValue();
item.watch(cb);
await item.setValue(newValue);
expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith(newValue, defaultValue);
});
it('should remove the listener when calling the returned function', async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();
const unwatch = item.watch(cb);
unwatch();
await item.setValue('123');
expect(cb).not.toBeCalled();
});
});
describe('unwatch', () => {
it('should remove all watch listeners', async () => {
const item = storage.defineItem(`local:key`);
const cb = vi.fn();
item.watch(cb);
storage.unwatch();
await item.setValue('123');
expect(cb).not.toBeCalled();
});
});
describe('defaultValue', () => {
it('should return the default value when provided', () => {
const defaultValue = 123;
const item = storage.defineItem(`local:test`, {
defaultValue,
});
expect(item.defaultValue).toBe(defaultValue);
});
it('should return null when not provided', () => {
const item = storage.defineItem<number>(`local:test`);
expect(item.defaultValue).toBeNull();
});
});
describe('types', () => {
it('should define a nullable value when options are not passed', () => {
const item = storage.defineItem<number>(`local:test`);
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
});
it('should define a non-null value when options are passed with a nullish default value', () => {
const item = storage.defineItem(`local:test`, {
defaultValue: 123,
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
});
it('should define a nullable value when options are passed with null default value', () => {
const item = storage.defineItem<number | null>(`local:test`, {
defaultValue: null,
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
});
});
});
});
+81
View File
@@ -0,0 +1,81 @@
/**
* @module wxt/browser
*/
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
export interface AugmentedBrowser extends Browser {
runtime: WxtRuntime;
i18n: WxtI18n;
}
export interface WxtRuntime extends Runtime.Static {
// Overriden per-project
}
export interface WxtI18n extends I18n.Static {
// Overriden per-project
}
export const browser: AugmentedBrowser = originalBrowser;
// re-export all the types from webextension-polyfill
// Because webextension-polyfill uses a weird namespace with "import export", there isn't a good way
// to get these types without re-listing them.
/** @ignore */
export type {
ActivityLog,
Alarms,
Bookmarks,
Action,
BrowserAction,
BrowserSettings,
BrowsingData,
CaptivePortal,
Clipboard,
Commands,
ContentScripts,
ContextualIdentities,
Cookies,
DeclarativeNetRequest,
Devtools,
Dns,
Downloads,
Events,
Experiments,
Extension,
ExtensionTypes,
Find,
GeckoProfiler,
History,
I18n,
Identity,
Idle,
Management,
Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox.
ContextMenus,
Menus,
NetworkStatus,
NormandyAddonStudy,
Notifications,
Omnibox,
PageAction,
Permissions,
Pkcs11,
Privacy,
Proxy,
Runtime,
Scripting,
Search,
Sessions,
SidebarAction,
Storage,
Tabs,
Theme,
TopSites,
Types,
Urlbar,
UserScripts,
WebNavigation,
WebRequest,
Windows,
} from 'webextension-polyfill';
+394
View File
@@ -0,0 +1,394 @@
import { describe, it, vi, beforeEach, expect } from 'vitest';
import { build } from '~/core/build';
import { createServer } from '~/core/create-server';
import { zip } from '~/core/zip';
import { prepare } from '~/core/prepare';
import { clean } from '~/core/clean';
import { initialize } from '~/core/initialize';
import { mock } from 'vitest-mock-extended';
import consola from 'consola';
vi.mock('~/core/build');
const buildMock = vi.mocked(build);
vi.mock('~/core/create-server');
const createServerMock = vi.mocked(createServer);
vi.mock('~/core/zip');
const zipMock = vi.mocked(zip);
vi.mock('~/core/prepare');
const prepareMock = vi.mocked(prepare);
vi.mock('~/core/clean');
const cleanMock = vi.mocked(clean);
vi.mock('~/core/initialize');
const initializeMock = vi.mocked(initialize);
consola.wrapConsole();
const ogArgv = process.argv;
function mockArgv(...args: string[]) {
process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];
}
async function importCli() {
await import('~/cli');
}
describe('CLI', () => {
beforeEach(() => {
vi.resetModules();
process.argv = ogArgv;
createServerMock.mockResolvedValue(mock());
});
describe('dev', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv();
await importCli();
expect(createServerMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('path/to/root');
await importCli();
expect(createServerMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('-c', './path/to/config.ts');
await importCli();
expect(createServerMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('-m', 'development');
await importCli();
expect(createServerMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('-b', 'firefox');
await importCli();
expect(createServerMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should pass correct filtered entrypoints', async () => {
mockArgv('-e', 'popup', '-e', 'options');
await importCli();
expect(createServerMock).toBeCalledWith({
filterEntrypoints: ['popup', 'options'],
});
});
it('should respect passing --mv2', async () => {
mockArgv('--mv2');
await importCli();
expect(createServerMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('--mv3');
await importCli();
expect(createServerMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should respect passing --port', async () => {
const expectedPort = 3100;
mockArgv('--port', String(expectedPort));
await importCli();
expect(createServerMock).toBeCalledWith({
dev: {
server: {
port: expectedPort,
},
},
});
});
it('should respect passing --debug', async () => {
mockArgv('--debug');
await importCli();
expect(createServerMock).toBeCalledWith({
debug: true,
});
});
});
describe('build', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('build');
await importCli();
expect(buildMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('build', 'path/to/root');
await importCli();
expect(buildMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('build', '-c', './path/to/config.ts');
await importCli();
expect(buildMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('build', '-m', 'development');
await importCli();
expect(buildMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('build', '-b', 'firefox');
await importCli();
expect(buildMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should pass correct filtered entrypoints', async () => {
mockArgv('build', '-e', 'popup', '-e', 'options');
await importCli();
expect(buildMock).toBeCalledWith({
filterEntrypoints: ['popup', 'options'],
});
});
it('should respect passing --mv2', async () => {
mockArgv('build', '--mv2');
await importCli();
expect(buildMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('build', '--mv3');
await importCli();
expect(buildMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should include analysis in the build', async () => {
mockArgv('build', '--analyze');
await importCli();
expect(buildMock).toBeCalledWith({
analysis: {
enabled: true,
},
});
});
it('should respect passing --debug', async () => {
mockArgv('build', '--debug');
await importCli();
expect(buildMock).toBeCalledWith({
debug: true,
});
});
});
describe('zip', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('zip');
await importCli();
expect(zipMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('zip', 'path/to/root');
await importCli();
expect(zipMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('zip', '-c', './path/to/config.ts');
await importCli();
expect(zipMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('zip', '-m', 'development');
await importCli();
expect(zipMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('zip', '-b', 'firefox');
await importCli();
expect(zipMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should respect passing --mv2', async () => {
mockArgv('zip', '--mv2');
await importCli();
expect(zipMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('zip', '--mv3');
await importCli();
expect(zipMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should respect passing --debug', async () => {
mockArgv('zip', '--debug');
await importCli();
expect(zipMock).toBeCalledWith({
debug: true,
});
});
});
describe('prepare', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('prepare');
await importCli();
expect(prepareMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('prepare', 'path/to/root');
await importCli();
expect(prepareMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('prepare', '-c', './path/to/config.ts');
await importCli();
expect(prepareMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing --debug', async () => {
mockArgv('prepare', '--debug');
await importCli();
expect(prepareMock).toBeCalledWith({
debug: true,
});
});
});
describe('clean', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('clean');
await importCli();
expect(cleanMock).toBeCalledWith(undefined);
});
it('should respect passing a custom root', async () => {
mockArgv('clean', 'path/to/root');
await importCli();
expect(cleanMock).toBeCalledWith('path/to/root');
});
});
describe('init', () => {
it('should not pass any options when no flags are passed', async () => {
mockArgv('init');
await importCli();
expect(initializeMock).toBeCalledWith({});
});
it('should respect the provided folder', async () => {
mockArgv('init', 'path/to/folder');
await importCli();
expect(initializeMock).toBeCalledWith({
directory: 'path/to/folder',
});
});
it('should respect passing --template', async () => {
mockArgv('init', '-t', 'vue');
await importCli();
expect(initializeMock).toBeCalledWith({
template: 'vue',
});
});
it('should respect passing --pm', async () => {
mockArgv('init', '--pm', 'pnpm');
await importCli();
expect(initializeMock).toBeCalledWith({
packageManager: 'pnpm',
});
});
});
});
+103
View File
@@ -0,0 +1,103 @@
import { CAC, Command } from 'cac';
import consola, { LogLevels } from 'consola';
import { filterTruthy, toArray } from '~/core/utils/arrays';
import { printHeader } from '~/core/utils/log';
import { formatDuration } from '~/core/utils/time';
import { ValidationError } from '~/core/utils/validation';
import { registerWxt } from '~/core/wxt';
/**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
*/
export function wrapAction(
cb: (
...args: any[]
) => void | { isOngoing?: boolean } | Promise<void | { isOngoing?: boolean }>,
options?: {
disableFinishedLog?: boolean;
},
) {
return async (...args: any[]) => {
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
// passed
const isDebug = !!args.find((arg) => arg?.debug);
if (isDebug) {
consola.level = LogLevels.debug;
}
const startTime = Date.now();
try {
printHeader();
const status = await cb(...args);
if (!status?.isOngoing && !options?.disableFinishedLog)
consola.success(
`Finished in ${formatDuration(Date.now() - startTime)}`,
);
} catch (err) {
consola.fail(
`Command failed after ${formatDuration(Date.now() - startTime)}`,
);
if (err instanceof ValidationError) {
// Don't log these errors, they've already been logged
} else {
consola.error(err);
}
process.exit(1);
}
};
}
/**
* Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out
* the
*/
export function getArrayFromFlags<T>(
flags: any,
name: string,
): T[] | undefined {
const array = toArray<T | undefined>(flags[name]);
const result = filterTruthy(array);
return result.length ? result : undefined;
}
const aliasCommandNames = new Set<string>();
/**
* @param base Command to add this one to
* @param name The command name to add
* @param alias The CLI tool being aliased
* @param bin The CLI tool binary name. Usually the same as the alias
* @param docsUrl URL to the docs for the aliased CLI tool
*/
export function createAliasedCommand(
base: CAC,
name: string,
alias: string,
bin: string,
docsUrl: string,
) {
const aliasedCommand = base
.command(name, `Alias for ${alias} (${docsUrl})`)
.allowUnknownOptions()
.action(async () => {
try {
await registerWxt('build');
const args = process.argv.slice(
process.argv.indexOf(aliasedCommand.name) + 1,
);
const { execa } = await import('execa');
await execa(bin, args, {
stdio: 'inherit',
});
} catch {
// Let the other aliased CLI log errors, just exit
process.exit(1);
}
});
aliasCommandNames.add(aliasedCommand.name);
}
export function isAliasedCommand(command: Command | undefined): boolean {
return !!command && aliasCommandNames.has(command.name);
}
+162
View File
@@ -0,0 +1,162 @@
import cac from 'cac';
import { build, clean, createServer, initialize, prepare, zip } from '~/core';
import {
createAliasedCommand,
getArrayFromFlags,
wrapAction,
} from './cli-utils';
const cli = cac('wxt');
cli.option('--debug', 'enable debug mode');
// DEV
cli
.command('[root]', 'start dev server')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('-p, --port <port>', 'specify a port for the dev server')
.option(
'-e, --filter-entrypoint <entrypoint>',
'only build specific entrypoints',
{
type: [],
},
)
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(
wrapAction(async (root, flags) => {
const server = await createServer({
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
dev:
flags.port == null
? undefined
: {
server: {
port: parseInt(flags.port),
},
},
});
await server.start();
return { isOngoing: true };
}),
);
// BUILD
cli
.command('build [root]', 'build for production')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option(
'-e, --filter-entrypoint <entrypoint>',
'only build specific entrypoints',
{
type: [],
},
)
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.option('--analyze', 'visualize extension bundle')
.option('--analyze-open', 'automatically open stats.html in browser')
.action(
wrapAction(async (root, flags) => {
await build({
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
analysis: flags.analyze
? {
enabled: true,
open: flags.analyzeOpen,
}
: undefined,
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
});
}),
);
// ZIP
cli
.command('zip [root]', 'build for production and zip output')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(
wrapAction(async (root, flags) => {
await zip({
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
});
}),
);
// PREPARE
cli
.command('prepare [root]', 'prepare typescript project')
.option('-c, --config <file>', 'use specified config file')
.action(
wrapAction(async (root, flags) => {
await prepare({
root,
configFile: flags.config,
debug: flags.debug,
});
}),
);
// CLEAN
cli
.command('clean [root]', 'clean generated files and caches')
.alias('cleanup')
.action(
wrapAction(async (root, flags) => {
await clean(root);
}),
);
// INIT
cli
.command('init [directory]', 'initialize a new project')
.option('-t, --template <template>', 'template to use')
.option('--pm <packageManager>', 'which package manager to use')
.action(
wrapAction(
async (directory, flags) => {
await initialize({
directory,
template: flags.template,
packageManager: flags.pm,
});
},
{ disableFinishedLog: true },
),
);
// SUBMIT
createAliasedCommand(
cli,
'submit',
'publish-extension',
'wxt-publish-extension',
'https://www.npmjs.com/publish-browser-extension',
);
export default cli;
-31
View File
@@ -1,31 +0,0 @@
import * as wxt from '../..';
import { defineCommand } from '../utils/defineCommand';
export const build = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
debug?: boolean;
analyze?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
analysis: {
enabled: flags.analyze,
},
};
await wxt.build(cliConfig);
});
-11
View File
@@ -1,11 +0,0 @@
import { defineCommand } from '../utils/defineCommand';
import * as wxt from '../..';
export const clean = defineCommand<
[
root: string | undefined,
flags: {
debug?: boolean;
},
]
>(wxt.clean);
-30
View File
@@ -1,30 +0,0 @@
import * as wxt from '../..';
import { defineCommand } from '../utils/defineCommand';
export const dev = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
debug?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
};
const server = await wxt.createServer(cliConfig);
await server.start();
return true;
});
-7
View File
@@ -1,7 +0,0 @@
export * from './build';
export * from './dev';
export * from './init';
export * from './prepare';
export * from './publish';
export * from './zip';
export * from './clean';
-181
View File
@@ -1,181 +0,0 @@
import { defineCommand } from '../utils/defineCommand';
import prompts from 'prompts';
import { consola } from 'consola';
import { downloadTemplate } from 'giget';
import fs from 'fs-extra';
import path from 'node:path';
import pc from 'picocolors';
import { Formatter } from 'picocolors/types';
export const init = defineCommand<
[
directory: string | undefined,
options: { template?: string; pm?: string; debug?: boolean },
]
>(
async (userDirectory, flags) => {
consola.info('Initalizing new project');
const templates = await listTemplates();
const defaultTemplate = templates.find(
(template) => template.name === flags.template?.toLowerCase().trim(),
);
const input = await prompts(
[
{
name: 'directory',
type: () => (userDirectory == null ? 'text' : undefined),
message: 'Project Directory',
initial: userDirectory,
},
{
name: 'template',
type: () => (defaultTemplate == null ? 'select' : undefined),
message: 'Choose a template',
choices: templates.map((template) => ({
title:
TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,
value: template,
})),
},
{
name: 'packageManager',
type: () => (flags.pm == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ title: 'npm', value: 'npm' },
{ title: 'pnpm', value: 'pnpm' },
{ title: 'yarn', value: 'yarn' },
],
},
],
{
onCancel: () => process.exit(1),
},
);
input.directory ??= userDirectory;
input.template ??= defaultTemplate;
input.packageManager ??= flags.pm;
await cloneProject(input);
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
console.log();
consola.log(
`✨ WXT project created with the ${
TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??
input.template.name
} template.`,
);
console.log();
consola.log('Next steps:');
let step = 0;
if (cdPath !== '') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
console.log();
},
{ disableFinishedLog: true },
);
interface Template {
/**
* Template's name.
*/
name: string;
/**
* Path to template directory in github repo.
*/
path: string;
}
async function listTemplates(): Promise<Template[]> {
try {
const res = await fetch(
'https://api.github.com/repos/wxt-dev/wxt/contents/templates',
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
},
);
if (res.status >= 300)
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
const data = (await res.json()) as Array<{
type: 'file' | 'dir';
name: string;
path: string;
}>;
return data
.filter((item: any) => item.type === 'dir')
.map((item) => ({ name: item.name, path: item.path }))
.sort((l, r) => {
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
const diff = lWeight - rWeight;
if (diff !== 0) return diff;
return l.name.localeCompare(r.name);
});
} catch (err) {
throw Error(`Cannot load templates: ${JSON.stringify(err, null, 2)}`);
}
}
async function cloneProject({
directory,
template,
packageManager,
}: {
directory: string;
template: Template;
packageManager: string;
}) {
const { default: ora } = await import('ora');
const spinner = ora('Downloading template').start();
try {
// 1. Clone repo
await downloadTemplate(`gh:wxt-dev/wxt/${template.path}`, {
dir: directory,
force: true,
});
// 2. Move _gitignore -> .gitignore
await fs
.move(
path.join(directory, '_gitignore'),
path.join(directory, '.gitignore'),
)
.catch((err) =>
consola.warn('Failed to move _gitignore to .gitignore:', err),
);
// 3. Add .npmrc for pnpm
if (packageManager === 'pnpm') {
await fs.writeFile(
path.join(directory, '.npmrc'),
'shamefully-hoist=true\n',
);
}
spinner.succeed();
} catch (err) {
spinner.fail();
throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);
}
}
const TEMPLATE_COLORS: Record<string, Formatter> = {
vanilla: pc.blue,
vue: pc.green,
react: pc.cyan,
svelte: pc.red,
solid: pc.blue,
};
const TEMPLATE_SORT_WEIGHT: Record<string, number> = {
vanilla: 0,
vue: 1,
react: 2,
};
-27
View File
@@ -1,27 +0,0 @@
import { getInternalConfig } from '../../core/utils/getInternalConfig';
import { findEntrypoints } from '../../core/build/findEntrypoints';
import { generateTypesDir } from '../../core/build/generateTypesDir';
import { defineCommand } from '../utils/defineCommand';
import * as wxt from '../..';
export const prepare = defineCommand<
[
root: string | undefined,
flags: {
config?: string;
debug?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
configFile: flags.config,
debug: flags.debug,
};
const config = await getInternalConfig(cliConfig, 'build');
config.logger.info('Generating types...');
const entrypoints = await findEntrypoints(config);
await generateTypesDir(entrypoints, config);
});
-11
View File
@@ -1,11 +0,0 @@
import { consola } from 'consola';
import { defineCommand } from '../utils/defineCommand';
export const publish = defineCommand(
async (
root: any,
{ config: configFile, debug }: { config?: string; debug?: string },
) => {
consola.warn('wxt publish: Not implemented');
},
);
-32
View File
@@ -1,32 +0,0 @@
import * as wxt from '../..';
import { buildInternal } from '../../core/build';
import { getInternalConfig } from '../../core/utils/getInternalConfig';
import { zipExtension } from '../../core/zip';
import { defineCommand } from '../utils/defineCommand';
export const zip = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
debug?: boolean;
},
]
>(async (root, flags) => {
const cliConfig: wxt.InlineConfig = {
root,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
};
const config = await getInternalConfig(cliConfig, 'build');
const output = await buildInternal(config);
await zipExtension(config, output);
});
+15 -60
View File
@@ -1,64 +1,19 @@
import cac from 'cac';
import { version } from '../../package.json';
import * as commands from './commands';
import cli from './commands';
import { version } from '~/version';
import { isAliasedCommand } from './cli-utils';
const cli = cac('wxt');
cli.help();
cli.version(version);
// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
process.env.VITE_CJS_IGNORE_WARNING = 'true';
cli.option('--debug', 'enable debug mode');
// Grab the command that we're trying to run
cli.parse(process.argv, { run: false });
// DEV
cli
.command('[root]', 'start dev server')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(commands.dev);
// If it's not an alias, add the help and version options, then parse again
if (!isAliasedCommand(cli.matchedCommand)) {
cli.help();
cli.version(version);
cli.parse(process.argv, { run: false });
}
// BUILD
cli
.command('build [root]', 'build for production')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.option('--analyze', 'visualize extension bundle')
.action(commands.build);
// ZIP
cli
.command('zip [root]', 'build for production and zip output')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(commands.zip);
// PREPARE
cli
.command('prepare [root]', 'prepare typescript project')
.option('-c, --config <file>', 'use specified config file')
.action(commands.prepare);
// CLEAN
cli
.command('clean [root]', 'clean generated files and caches')
.alias('cleanup')
.action(commands.clean);
// PUBLISH
cli.command('publish [root]', 'publish to stores').action(commands.publish);
// INIT
cli
.command('init [directory]', 'initialize a new project')
.option('-t, --template <template>', 'template to use')
.option('--pm <packageManager>', 'which package manager to use')
.action(commands.init);
cli.parse();
// Run the alias or command
await cli.runMatchedCommand();
@@ -1,336 +0,0 @@
/** @vitest-environment jsdom */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ContentScriptContext, createContentScriptUi } from '..';
import { createIsolatedElement } from '@webext-core/isolated-element';
import { mock } from 'vitest-mock-extended';
import { browser } from '../browser';
vi.mock('webextension-polyfill', () =>
vi.importActual('../virtual-modules/fake-browser'),
);
vi.mock('@webext-core/isolated-element', async () => {
const { vi } = await import('vitest');
return {
createIsolatedElement: vi.fn(),
};
});
const createIsolatedElementMock = vi.mocked(createIsolatedElement);
const testApp = (container: Element) => {
const app = document.createElement('div');
app.textContent = 'App';
container.append(app);
};
const createCtx = () => new ContentScriptContext('test');
const fetch = vi.fn();
describe('createContentScriptUi', () => {
beforeEach(() => {
document.body.innerHTML = `
<div id="parent">
<p id="one">one</p>
<p id="two">two</p>
<p id="three"></p>
</div>
`;
window.fetch = fetch;
fetch.mockResolvedValue({ text: () => Promise.resolve('') });
createIsolatedElementMock.mockImplementation(async (config) => {
const parentElement = document.createElement(config.name);
const isolatedElement = document.createElement('html');
parentElement.append(isolatedElement);
return {
isolatedElement,
parentElement: parentElement,
shadow: mock<ShadowRoot>({
querySelector: (selector: string) => {
if (selector === 'html') return isolatedElement;
},
}),
};
});
});
describe('css', () => {
it('should load the CSS for the current entrypoint when cssInjectionMode=ui', async () => {
fetch.mockResolvedValue({ text: () => Promise.resolve('body {}') });
const ctx = new ContentScriptContext('test', {
matches: [],
cssInjectionMode: 'ui',
});
await createContentScriptUi(ctx, {
name: 'test',
type: 'inline',
mount: testApp,
});
expect(fetch).toBeCalledTimes(1);
expect(fetch).toBeCalledWith(
`chrome-extension://${browser.runtime.id}/content-scripts/${__ENTRYPOINT__}.css`,
);
expect(createIsolatedElementMock).toBeCalledTimes(1);
expect(createIsolatedElementMock).toBeCalledWith(
expect.objectContaining({
css: {
textContent: 'body {}',
},
}),
);
});
it('should still load the UI when fetch fails to load CSS file when cssInjectionMode=ui', async () => {
const error = Error('Test fetch error');
fetch.mockRejectedValue(error);
const ctx = new ContentScriptContext('test', {
matches: [],
cssInjectionMode: 'ui',
});
await createContentScriptUi(ctx, {
name: 'test',
type: 'inline',
mount: testApp,
});
expect(fetch).toBeCalledTimes(1);
expect(fetch).toBeCalledWith(
`chrome-extension://${browser.runtime.id}/content-scripts/${__ENTRYPOINT__}.css`,
);
expect(createIsolatedElementMock).toBeCalledTimes(1);
expect(createIsolatedElementMock).toBeCalledWith(
expect.objectContaining({
css: {
textContent: '',
},
}),
);
});
it.each(['manifest', 'manual'] as const)(
'should not fetch CSS when cssInjectionMode=%s',
async (cssInjectionMode) => {
const ctx = new ContentScriptContext('test', {
matches: [],
cssInjectionMode,
});
await createContentScriptUi(ctx, {
name: 'test',
type: 'inline',
mount: testApp,
});
expect(fetch).not.toBeCalled();
},
);
});
describe('mount', () => {
describe('append option', () => {
it.each([undefined, 'last' as const])(
'should append the element as the last child to the anchor when append=%s',
async (append) => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#parent',
append,
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(4)')?.tagName,
).toEqual('TEST-APP');
},
);
it('should append the element as the first child to the anchor when append=first', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#parent',
append: 'first',
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(1)')?.tagName,
).toEqual('TEST-APP');
});
it('should append the element normally when append=first but there are no other children', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#three',
append: 'first',
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#three > :nth-child(1)')?.tagName,
).toEqual('TEST-APP');
});
it('should replace the anchor with the element when append=replace', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#two',
append: 'replace',
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(2)')?.tagName,
).toEqual('TEST-APP');
});
it('should append the element after the anchor when append=before', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#two',
append: 'before',
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(2)')?.tagName,
).toEqual('TEST-APP');
});
it('should append the element after the anchor when append=after', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#two',
append: 'after',
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(3)')?.tagName,
).toEqual('TEST-APP');
});
it('should apply a custom function', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: '#three',
append: (anchor, ui) => anchor.replaceWith(ui),
mount: testApp,
});
ui.mount();
expect(
document.querySelector('#parent > :nth-child(3)')?.tagName,
).toEqual('TEST-APP');
});
});
it('should default the anchor to the body when unset', async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
mount: testApp,
});
ui.mount();
expect(document.querySelector('body > :nth-child(2)')?.tagName).toEqual(
'TEST-APP',
);
});
it.each(['#four', () => document.querySelector('#four')])(
"should throw an error if the anchor doesn't exist",
async (anchor) => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test',
type: 'inline',
anchor,
mount: testApp,
});
expect(ui.mount).toThrow(
'Failed to mount content script ui: could not find anchor element',
);
},
);
});
describe('remove', () => {
it("should not fail if the ui hasn't been mounted", async () => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
mount: testApp,
});
ui.remove();
});
it('should automatically remove the UI when the context is invalidated', async () => {
const ctx = createCtx();
const ui = await createContentScriptUi(ctx, {
name: 'test-app',
type: 'inline',
mount: testApp,
});
ui.mount();
expect(document.querySelector('test-app')).toBeDefined();
ctx.abort();
expect(document.querySelector('test-app')).toBeNull();
});
});
describe('type', () => {
it.each(['inline', 'overlay', 'modal'] as const)(
'should render type=%s',
async (type) => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type,
mount: testApp,
});
ui.mount();
expect(document.querySelector('test-app')).toBeDefined();
},
);
});
describe('anchor', () => {
it.each([
() => '#two',
() => () => '#two',
() => document.querySelector('#two'),
() => () => document.querySelector('#two'),
])('should render anchor=%s', async (getAnchor) => {
const ui = await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
anchor: getAnchor(),
mount: testApp,
});
ui.mount();
expect(document.querySelector('test-app')).toBeDefined();
});
});
});
-19
View File
@@ -1,19 +0,0 @@
/**
* @module wxt/browser
*/
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
export interface AugmentedBrowser extends Browser {
runtime: WxtRuntime;
i18n: WxtI18n;
}
export interface WxtRuntime extends Runtime.Static {
// Overriden per-project
}
export interface WxtI18n extends I18n.Static {
// Overriden per-project
}
export const browser: AugmentedBrowser = originalBrowser;

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